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.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  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. /**
  12. * @extends Factory<Invoice>
  13. */
  14. class InvoiceFactory extends Factory
  15. {
  16. /**
  17. * The name of the factory's corresponding model.
  18. */
  19. protected $model = Invoice::class;
  20. /**
  21. * Define the model's default state.
  22. *
  23. * @return array<string, mixed>
  24. */
  25. public function definition(): array
  26. {
  27. return [
  28. 'company_id' => 1,
  29. 'client_id' => Client::inRandomOrder()->value('id'),
  30. 'header' => 'Invoice',
  31. 'subheader' => 'Invoice',
  32. 'invoice_number' => $this->faker->unique()->numerify('INV-#####'),
  33. 'order_number' => $this->faker->unique()->numerify('ORD-#####'),
  34. 'date' => $this->faker->dateTimeBetween('-1 year'),
  35. 'due_date' => $this->faker->dateTimeBetween('now', '+2 months'),
  36. 'status' => InvoiceStatus::Draft,
  37. 'currency_code' => 'USD',
  38. 'terms' => $this->faker->sentence,
  39. 'footer' => $this->faker->sentence,
  40. 'created_by' => 1,
  41. 'updated_by' => 1,
  42. ];
  43. }
  44. public function withLineItems(int $count = 3): self
  45. {
  46. return $this->has(DocumentLineItem::factory()->count($count), 'lineItems');
  47. }
  48. public function approved(): static
  49. {
  50. return $this->afterCreating(function (Invoice $invoice) {
  51. if (! $invoice->isDraft()) {
  52. return;
  53. }
  54. $invoice->approveDraft();
  55. });
  56. }
  57. public function withPayments(?int $min = 1, ?int $max = null, InvoiceStatus $invoiceStatus = InvoiceStatus::Paid): static
  58. {
  59. return $this->afterCreating(function (Invoice $invoice) use ($invoiceStatus, $max, $min) {
  60. if ($invoice->isDraft()) {
  61. $invoice->approveDraft();
  62. }
  63. $this->recalculateTotals($invoice);
  64. $invoice->refresh();
  65. $totalAmountDue = $invoice->getRawOriginal('amount_due');
  66. if ($invoiceStatus === InvoiceStatus::Overpaid) {
  67. $totalAmountDue += random_int(1000, 10000);
  68. } elseif ($invoiceStatus === InvoiceStatus::Partial) {
  69. $totalAmountDue = (int) floor($totalAmountDue * 0.5);
  70. }
  71. if ($totalAmountDue <= 0 || empty($totalAmountDue)) {
  72. return;
  73. }
  74. $paymentCount = $max && $min ? $this->faker->numberBetween($min, $max) : $min;
  75. $paymentAmount = (int) floor($totalAmountDue / $paymentCount);
  76. $remainingAmount = $totalAmountDue;
  77. for ($i = 0; $i < $paymentCount; $i++) {
  78. $amount = $i === $paymentCount - 1 ? $remainingAmount : $paymentAmount;
  79. if ($amount <= 0) {
  80. break;
  81. }
  82. $data = [
  83. 'posted_at' => $invoice->date->addDay(),
  84. 'amount' => CurrencyConverter::convertCentsToFormatSimple($amount, $invoice->currency_code),
  85. 'payment_method' => $this->faker->randomElement(PaymentMethod::class),
  86. 'bank_account_id' => BankAccount::inRandomOrder()->value('id'),
  87. 'notes' => $this->faker->sentence,
  88. ];
  89. $invoice->recordPayment($data);
  90. $remainingAmount -= $amount;
  91. }
  92. });
  93. }
  94. public function configure(): static
  95. {
  96. return $this->afterCreating(function (Invoice $invoice) {
  97. // Use the invoice's ID to generate invoice and order numbers
  98. $paddedId = str_pad((string) $invoice->id, 5, '0', STR_PAD_LEFT);
  99. $invoice->updateQuietly([
  100. 'invoice_number' => "INV-{$paddedId}",
  101. 'order_number' => "ORD-{$paddedId}",
  102. ]);
  103. $this->recalculateTotals($invoice);
  104. if ($invoice->due_date->isPast() && ! in_array($invoice->status, [InvoiceStatus::Draft, InvoiceStatus::Paid, InvoiceStatus::Void, InvoiceStatus::Overpaid])) {
  105. $invoice->updateQuietly([
  106. 'status' => InvoiceStatus::Overdue,
  107. ]);
  108. }
  109. });
  110. }
  111. protected function recalculateTotals(Invoice $invoice): void
  112. {
  113. if ($invoice->lineItems()->exists()) {
  114. $subtotal = $invoice->lineItems()->sum('subtotal') / 100;
  115. $taxTotal = $invoice->lineItems()->sum('tax_total') / 100;
  116. $discountTotal = $invoice->lineItems()->sum('discount_total') / 100;
  117. $grandTotal = $subtotal + $taxTotal - $discountTotal;
  118. $invoice->updateQuietly([
  119. 'subtotal' => $subtotal,
  120. 'tax_total' => $taxTotal,
  121. 'discount_total' => $discountTotal,
  122. 'total' => $grandTotal,
  123. ]);
  124. }
  125. }
  126. }