選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

BillFactory.php 5.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. <?php
  2. namespace Database\Factories\Accounting;
  3. use App\Enums\Accounting\BillStatus;
  4. use App\Enums\Accounting\PaymentMethod;
  5. use App\Models\Accounting\Bill;
  6. use App\Models\Accounting\DocumentLineItem;
  7. use App\Models\Banking\BankAccount;
  8. use App\Models\Common\Vendor;
  9. use App\Utilities\Currency\CurrencyConverter;
  10. use Illuminate\Database\Eloquent\Factories\Factory;
  11. use Illuminate\Support\Carbon;
  12. /**
  13. * @extends Factory<Bill>
  14. */
  15. class BillFactory extends Factory
  16. {
  17. /**
  18. * The name of the factory's corresponding model.
  19. */
  20. protected $model = Bill::class;
  21. /**
  22. * Define the model's default state.
  23. *
  24. * @return array<string, mixed>
  25. */
  26. public function definition(): array
  27. {
  28. // 50% chance of being a future bill
  29. $isFutureBill = $this->faker->boolean();
  30. if ($isFutureBill) {
  31. // For future bills, date is recent and due date is in near future
  32. $billDate = $this->faker->dateTimeBetween('-10 days', '+10 days');
  33. } else {
  34. // For past bills, both date and due date are in the past
  35. $billDate = $this->faker->dateTimeBetween('-1 year', '-30 days');
  36. }
  37. $dueDays = $this->faker->numberBetween(14, 60);
  38. return [
  39. 'company_id' => 1,
  40. 'vendor_id' => Vendor::inRandomOrder()->value('id'),
  41. 'bill_number' => $this->faker->unique()->numerify('BILL-#####'),
  42. 'order_number' => $this->faker->unique()->numerify('PO-#####'),
  43. 'date' => $billDate,
  44. 'due_date' => Carbon::parse($billDate)->addDays($dueDays),
  45. 'status' => BillStatus::Unpaid,
  46. 'currency_code' => 'USD',
  47. 'notes' => $this->faker->sentence,
  48. 'created_by' => 1,
  49. 'updated_by' => 1,
  50. ];
  51. }
  52. public function withLineItems(int $count = 3): self
  53. {
  54. return $this->has(DocumentLineItem::factory()->forBill()->count($count), 'lineItems');
  55. }
  56. public function initialized(): static
  57. {
  58. return $this->afterCreating(function (Bill $bill) {
  59. if ($bill->hasInitialTransaction()) {
  60. return;
  61. }
  62. $this->recalculateTotals($bill);
  63. $postedAt = Carbon::parse($bill->date)->addHours($this->faker->numberBetween(1, 24));
  64. $bill->createInitialTransaction($postedAt);
  65. });
  66. }
  67. public function withPayments(?int $min = 1, ?int $max = null, BillStatus $billStatus = BillStatus::Paid): static
  68. {
  69. return $this->afterCreating(function (Bill $bill) use ($billStatus, $max, $min) {
  70. if (! $bill->hasInitialTransaction()) {
  71. $this->recalculateTotals($bill);
  72. $postedAt = Carbon::parse($bill->date)->addHours($this->faker->numberBetween(1, 24));
  73. $bill->createInitialTransaction($postedAt);
  74. }
  75. $bill->refresh();
  76. $totalAmountDue = $bill->getRawOriginal('amount_due');
  77. if ($billStatus === BillStatus::Partial) {
  78. $totalAmountDue = (int) floor($totalAmountDue * 0.5);
  79. }
  80. if ($totalAmountDue <= 0 || empty($totalAmountDue)) {
  81. return;
  82. }
  83. $paymentCount = $max && $min ? $this->faker->numberBetween($min, $max) : $min;
  84. $paymentAmount = (int) floor($totalAmountDue / $paymentCount);
  85. $remainingAmount = $totalAmountDue;
  86. $paymentDate = Carbon::parse($bill->initialTransaction->posted_at);
  87. $paymentDates = [];
  88. for ($i = 0; $i < $paymentCount; $i++) {
  89. $amount = $i === $paymentCount - 1 ? $remainingAmount : $paymentAmount;
  90. if ($amount <= 0) {
  91. break;
  92. }
  93. $postedAt = $paymentDate->copy()->addDays($this->faker->numberBetween(1, 30));
  94. $paymentDates[] = $postedAt;
  95. $data = [
  96. 'posted_at' => $postedAt,
  97. 'amount' => CurrencyConverter::convertCentsToFormatSimple($amount, $bill->currency_code),
  98. 'payment_method' => $this->faker->randomElement(PaymentMethod::class),
  99. 'bank_account_id' => BankAccount::inRandomOrder()->value('id'),
  100. 'notes' => $this->faker->sentence,
  101. ];
  102. $bill->recordPayment($data);
  103. $remainingAmount -= $amount;
  104. }
  105. if ($billStatus === BillStatus::Paid) {
  106. $latestPaymentDate = max($paymentDates);
  107. $bill->updateQuietly([
  108. 'status' => $billStatus,
  109. 'paid_at' => $latestPaymentDate,
  110. ]);
  111. }
  112. });
  113. }
  114. public function configure(): static
  115. {
  116. return $this->afterCreating(function (Bill $bill) {
  117. $paddedId = str_pad((string) $bill->id, 5, '0', STR_PAD_LEFT);
  118. $bill->updateQuietly([
  119. 'bill_number' => "BILL-{$paddedId}",
  120. 'order_number' => "PO-{$paddedId}",
  121. ]);
  122. $this->recalculateTotals($bill);
  123. // Check for overdue status
  124. if ($bill->due_date < today() && $bill->status !== BillStatus::Paid) {
  125. $bill->updateQuietly([
  126. 'status' => BillStatus::Overdue,
  127. ]);
  128. }
  129. });
  130. }
  131. protected function recalculateTotals(Bill $bill): void
  132. {
  133. if ($bill->lineItems()->exists()) {
  134. $bill->refresh();
  135. $subtotal = $bill->lineItems()->sum('subtotal') / 100;
  136. $taxTotal = $bill->lineItems()->sum('tax_total') / 100;
  137. $discountTotal = $bill->lineItems()->sum('discount_total') / 100;
  138. $grandTotal = $subtotal + $taxTotal - $discountTotal;
  139. $bill->update([
  140. 'subtotal' => $subtotal,
  141. 'tax_total' => $taxTotal,
  142. 'discount_total' => $discountTotal,
  143. 'total' => $grandTotal,
  144. ]);
  145. }
  146. }
  147. }