Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

BillFactory.php 9.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. <?php
  2. namespace Database\Factories\Accounting;
  3. use App\Enums\Accounting\AdjustmentComputation;
  4. use App\Enums\Accounting\BillStatus;
  5. use App\Enums\Accounting\DocumentDiscountMethod;
  6. use App\Enums\Accounting\PaymentMethod;
  7. use App\Models\Accounting\Bill;
  8. use App\Models\Accounting\DocumentLineItem;
  9. use App\Models\Banking\BankAccount;
  10. use App\Models\Common\Vendor;
  11. use App\Models\Company;
  12. use App\Models\Setting\DocumentDefault;
  13. use App\Utilities\Currency\CurrencyConverter;
  14. use App\Utilities\RateCalculator;
  15. use Illuminate\Database\Eloquent\Factories\Factory;
  16. use Illuminate\Support\Carbon;
  17. /**
  18. * @extends Factory<Bill>
  19. */
  20. class BillFactory extends Factory
  21. {
  22. /**
  23. * The name of the factory's corresponding model.
  24. */
  25. protected $model = Bill::class;
  26. /**
  27. * Define the model's default state.
  28. *
  29. * @return array<string, mixed>
  30. */
  31. public function definition(): array
  32. {
  33. $isFutureBill = $this->faker->boolean();
  34. if ($isFutureBill) {
  35. $billDate = $this->faker->dateTimeBetween('-10 days', '+10 days');
  36. } else {
  37. $billDate = $this->faker->dateTimeBetween('-1 year', '-30 days');
  38. }
  39. $dueDays = $this->faker->numberBetween(14, 60);
  40. return [
  41. 'company_id' => 1,
  42. 'vendor_id' => fn (array $attributes) => Vendor::where('company_id', $attributes['company_id'])->inRandomOrder()->value('id'),
  43. 'bill_number' => $this->faker->unique()->numerify('BILL-####'),
  44. 'order_number' => $this->faker->unique()->numerify('PO-####'),
  45. 'date' => $billDate,
  46. 'due_date' => Carbon::parse($billDate)->addDays($dueDays),
  47. 'status' => BillStatus::Open,
  48. 'discount_method' => $this->faker->randomElement(DocumentDiscountMethod::class),
  49. 'discount_computation' => AdjustmentComputation::Percentage,
  50. 'discount_rate' => function (array $attributes) {
  51. $discountMethod = DocumentDiscountMethod::parse($attributes['discount_method']);
  52. if ($discountMethod?->isPerDocument()) {
  53. return $this->faker->numberBetween(50000, 200000); // 5% - 20%
  54. }
  55. return 0;
  56. },
  57. 'currency_code' => function (array $attributes) {
  58. $vendor = Vendor::find($attributes['vendor_id']);
  59. return $vendor->currency_code ??
  60. Company::find($attributes['company_id'])->default->currency_code ??
  61. 'USD';
  62. },
  63. 'notes' => $this->faker->sentence,
  64. 'created_by' => 1,
  65. 'updated_by' => 1,
  66. ];
  67. }
  68. public function withLineItems(int $count = 3): static
  69. {
  70. return $this->afterCreating(function (Bill $bill) use ($count) {
  71. DocumentLineItem::factory()
  72. ->count($count)
  73. ->forBill($bill)
  74. ->create();
  75. $this->recalculateTotals($bill);
  76. });
  77. }
  78. public function initialized(): static
  79. {
  80. return $this->afterCreating(function (Bill $bill) {
  81. $this->ensureLineItems($bill);
  82. if ($bill->wasInitialized()) {
  83. return;
  84. }
  85. $postedAt = Carbon::parse($bill->date)
  86. ->addHours($this->faker->numberBetween(1, 24));
  87. $bill->createInitialTransaction($postedAt);
  88. });
  89. }
  90. public function partial(int $maxPayments = 4): static
  91. {
  92. return $this->afterCreating(function (Bill $bill) use ($maxPayments) {
  93. $this->ensureInitialized($bill);
  94. $this->withPayments(max: $maxPayments, billStatus: BillStatus::Partial)
  95. ->callAfterCreating(collect([$bill]));
  96. });
  97. }
  98. public function paid(int $maxPayments = 4): static
  99. {
  100. return $this->afterCreating(function (Bill $bill) use ($maxPayments) {
  101. $this->ensureInitialized($bill);
  102. $this->withPayments(max: $maxPayments)
  103. ->callAfterCreating(collect([$bill]));
  104. });
  105. }
  106. public function overdue(): static
  107. {
  108. return $this
  109. ->state([
  110. 'due_date' => now()->subDays($this->faker->numberBetween(1, 30)),
  111. ])
  112. ->afterCreating(function (Bill $bill) {
  113. $this->ensureInitialized($bill);
  114. });
  115. }
  116. public function withPayments(?int $min = null, ?int $max = null, BillStatus $billStatus = BillStatus::Paid): static
  117. {
  118. $min ??= 1;
  119. return $this->afterCreating(function (Bill $bill) use ($billStatus, $max, $min) {
  120. $this->ensureInitialized($bill);
  121. $bill->refresh();
  122. $amountDue = $bill->getRawOriginal('amount_due');
  123. $totalAmountDue = match ($billStatus) {
  124. BillStatus::Partial => (int) floor($amountDue * 0.5),
  125. default => $amountDue,
  126. };
  127. if ($totalAmountDue <= 0 || empty($totalAmountDue)) {
  128. return;
  129. }
  130. $paymentCount = $max && $min ? $this->faker->numberBetween($min, $max) : $min;
  131. $paymentAmount = (int) floor($totalAmountDue / $paymentCount);
  132. $remainingAmount = $totalAmountDue;
  133. $paymentDate = Carbon::parse($bill->initialTransaction->posted_at);
  134. $paymentDates = [];
  135. for ($i = 0; $i < $paymentCount; $i++) {
  136. $amount = $i === $paymentCount - 1 ? $remainingAmount : $paymentAmount;
  137. if ($amount <= 0) {
  138. break;
  139. }
  140. $postedAt = $paymentDate->copy()->addDays($this->faker->numberBetween(1, 30));
  141. $paymentDates[] = $postedAt;
  142. $data = [
  143. 'posted_at' => $postedAt,
  144. 'amount' => CurrencyConverter::convertCentsToFormatSimple($amount, $bill->currency_code),
  145. 'payment_method' => $this->faker->randomElement(PaymentMethod::class),
  146. 'bank_account_id' => BankAccount::where('company_id', $bill->company_id)->inRandomOrder()->value('id'),
  147. 'notes' => $this->faker->sentence,
  148. ];
  149. $bill->recordPayment($data);
  150. $remainingAmount -= $amount;
  151. }
  152. if ($billStatus !== BillStatus::Paid) {
  153. return;
  154. }
  155. $latestPaymentDate = max($paymentDates);
  156. $bill->updateQuietly([
  157. 'status' => $billStatus,
  158. 'paid_at' => $latestPaymentDate,
  159. ]);
  160. });
  161. }
  162. public function configure(): static
  163. {
  164. return $this->afterCreating(function (Bill $bill) {
  165. $this->ensureInitialized($bill);
  166. $number = DocumentDefault::getBaseNumber() + $bill->id;
  167. $bill->updateQuietly([
  168. 'bill_number' => "BILL-{$number}",
  169. 'order_number' => "PO-{$number}",
  170. ]);
  171. if ($bill->wasInitialized() && $bill->is_currently_overdue) {
  172. $bill->updateQuietly([
  173. 'status' => BillStatus::Overdue,
  174. ]);
  175. }
  176. });
  177. }
  178. protected function ensureLineItems(Bill $bill): void
  179. {
  180. if (! $bill->hasLineItems()) {
  181. $this->withLineItems()->callAfterCreating(collect([$bill]));
  182. }
  183. }
  184. protected function ensureInitialized(Bill $bill): void
  185. {
  186. if (! $bill->wasInitialized()) {
  187. $this->initialized()->callAfterCreating(collect([$bill]));
  188. }
  189. }
  190. protected function recalculateTotals(Bill $bill): void
  191. {
  192. $bill->refresh();
  193. if (! $bill->hasLineItems()) {
  194. return;
  195. }
  196. $subtotalCents = $bill->lineItems()->sum('subtotal');
  197. $taxTotalCents = $bill->lineItems()->sum('tax_total');
  198. $discountTotalCents = 0;
  199. if ($bill->discount_method?->isPerLineItem()) {
  200. $discountTotalCents = $bill->lineItems()->sum('discount_total');
  201. } elseif ($bill->discount_method?->isPerDocument() && $bill->discount_rate) {
  202. if ($bill->discount_computation?->isPercentage()) {
  203. $scaledRate = RateCalculator::parseLocalizedRate($bill->discount_rate);
  204. $discountTotalCents = RateCalculator::calculatePercentage($subtotalCents, $scaledRate);
  205. } else {
  206. $discountTotalCents = CurrencyConverter::convertToCents($bill->discount_rate, $bill->currency_code);
  207. }
  208. }
  209. $grandTotalCents = $subtotalCents + $taxTotalCents - $discountTotalCents;
  210. $currencyCode = $bill->currency_code;
  211. $bill->update([
  212. 'subtotal' => CurrencyConverter::convertCentsToFormatSimple($subtotalCents, $currencyCode),
  213. 'tax_total' => CurrencyConverter::convertCentsToFormatSimple($taxTotalCents, $currencyCode),
  214. 'discount_total' => CurrencyConverter::convertCentsToFormatSimple($discountTotalCents, $currencyCode),
  215. 'total' => CurrencyConverter::convertCentsToFormatSimple($grandTotalCents, $currencyCode),
  216. ]);
  217. }
  218. }