You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

InvoiceFactory.php 5.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. <?php
  2. namespace Database\Factories\Accounting;
  3. use App\Enums\Accounting\InvoiceStatus;
  4. use App\Enums\Accounting\PaymentMethod;
  5. use App\Models\Accounting\DocumentLineItem;
  6. use App\Models\Accounting\Invoice;
  7. use App\Models\Banking\BankAccount;
  8. use App\Models\Common\Client;
  9. use App\Utilities\Currency\CurrencyConverter;
  10. use Illuminate\Database\Eloquent\Factories\Factory;
  11. use Illuminate\Support\Carbon;
  12. /**
  13. * @extends Factory<Invoice>
  14. */
  15. class InvoiceFactory extends Factory
  16. {
  17. /**
  18. * The name of the factory's corresponding model.
  19. */
  20. protected $model = Invoice::class;
  21. /**
  22. * Define the model's default state.
  23. *
  24. * @return array<string, mixed>
  25. */
  26. public function definition(): array
  27. {
  28. $invoiceDate = $this->faker->dateTimeBetween('-1 year');
  29. return [
  30. 'company_id' => 1,
  31. 'client_id' => Client::inRandomOrder()->value('id'),
  32. 'header' => 'Invoice',
  33. 'subheader' => 'Invoice',
  34. 'invoice_number' => $this->faker->unique()->numerify('INV-#####'),
  35. 'order_number' => $this->faker->unique()->numerify('ORD-#####'),
  36. 'date' => $invoiceDate,
  37. 'due_date' => Carbon::parse($invoiceDate)->addDays($this->faker->numberBetween(14, 60)),
  38. 'status' => InvoiceStatus::Draft,
  39. 'currency_code' => 'USD',
  40. 'terms' => $this->faker->sentence,
  41. 'footer' => $this->faker->sentence,
  42. 'created_by' => 1,
  43. 'updated_by' => 1,
  44. ];
  45. }
  46. public function withLineItems(int $count = 3): self
  47. {
  48. return $this->has(DocumentLineItem::factory()->count($count), 'lineItems');
  49. }
  50. public function approved(): static
  51. {
  52. return $this->afterCreating(function (Invoice $invoice) {
  53. if (! $invoice->isDraft()) {
  54. return;
  55. }
  56. $this->recalculateTotals($invoice);
  57. $approvedAt = Carbon::parse($invoice->date)->addHours($this->faker->numberBetween(1, 24));
  58. $invoice->approveDraft($approvedAt);
  59. });
  60. }
  61. public function withPayments(?int $min = 1, ?int $max = null, InvoiceStatus $invoiceStatus = InvoiceStatus::Paid): static
  62. {
  63. return $this->afterCreating(function (Invoice $invoice) use ($invoiceStatus, $max, $min) {
  64. if ($invoice->isDraft()) {
  65. $this->recalculateTotals($invoice);
  66. $approvedAt = Carbon::parse($invoice->date)->addHours($this->faker->numberBetween(1, 24));
  67. $invoice->approveDraft($approvedAt);
  68. }
  69. $invoice->refresh();
  70. $totalAmountDue = $invoice->getRawOriginal('amount_due');
  71. if ($invoiceStatus === InvoiceStatus::Overpaid) {
  72. $totalAmountDue += random_int(1000, 10000);
  73. } elseif ($invoiceStatus === InvoiceStatus::Partial) {
  74. $totalAmountDue = (int) floor($totalAmountDue * 0.5);
  75. }
  76. if ($totalAmountDue <= 0 || empty($totalAmountDue)) {
  77. return;
  78. }
  79. $paymentCount = $max && $min ? $this->faker->numberBetween($min, $max) : $min;
  80. $paymentAmount = (int) floor($totalAmountDue / $paymentCount);
  81. $remainingAmount = $totalAmountDue;
  82. $paymentDate = Carbon::parse($invoice->approved_at);
  83. $paymentDates = [];
  84. for ($i = 0; $i < $paymentCount; $i++) {
  85. $amount = $i === $paymentCount - 1 ? $remainingAmount : $paymentAmount;
  86. if ($amount <= 0) {
  87. break;
  88. }
  89. $postedAt = $paymentDate->copy()->addDays($this->faker->numberBetween(1, 30));
  90. $paymentDates[] = $postedAt;
  91. $data = [
  92. 'posted_at' => $postedAt,
  93. 'amount' => CurrencyConverter::convertCentsToFormatSimple($amount, $invoice->currency_code),
  94. 'payment_method' => $this->faker->randomElement(PaymentMethod::class),
  95. 'bank_account_id' => BankAccount::inRandomOrder()->value('id'),
  96. 'notes' => $this->faker->sentence,
  97. ];
  98. $invoice->recordPayment($data);
  99. $remainingAmount -= $amount;
  100. }
  101. // If it's a paid invoice, use the latest payment date as paid_at
  102. if ($invoiceStatus === InvoiceStatus::Paid) {
  103. $latestPaymentDate = max($paymentDates);
  104. $invoice->updateQuietly([
  105. 'status' => InvoiceStatus::Paid,
  106. 'paid_at' => $latestPaymentDate,
  107. ]);
  108. }
  109. });
  110. }
  111. public function configure(): static
  112. {
  113. return $this->afterCreating(function (Invoice $invoice) {
  114. // Use the invoice's ID to generate invoice and order numbers
  115. $paddedId = str_pad((string) $invoice->id, 5, '0', STR_PAD_LEFT);
  116. $invoice->updateQuietly([
  117. 'invoice_number' => "INV-{$paddedId}",
  118. 'order_number' => "ORD-{$paddedId}",
  119. ]);
  120. $this->recalculateTotals($invoice);
  121. if ($invoice->approved_at && $invoice->is_currently_overdue) {
  122. $invoice->updateQuietly([
  123. 'status' => InvoiceStatus::Overdue,
  124. ]);
  125. }
  126. });
  127. }
  128. protected function recalculateTotals(Invoice $invoice): void
  129. {
  130. if ($invoice->lineItems()->exists()) {
  131. $invoice->refresh();
  132. $subtotal = $invoice->lineItems()->sum('subtotal') / 100;
  133. $taxTotal = $invoice->lineItems()->sum('tax_total') / 100;
  134. $discountTotal = $invoice->lineItems()->sum('discount_total') / 100;
  135. $grandTotal = $subtotal + $taxTotal - $discountTotal;
  136. $invoice->update([
  137. 'subtotal' => $subtotal,
  138. 'tax_total' => $taxTotal,
  139. 'discount_total' => $discountTotal,
  140. 'total' => $grandTotal,
  141. ]);
  142. }
  143. }
  144. }