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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  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('-2 months', '+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. $this->recalculateTotals($invoice);
  55. $invoice->approveDraft();
  56. });
  57. }
  58. public function withPayments(?int $min = 1, ?int $max = null, InvoiceStatus $invoiceStatus = InvoiceStatus::Paid): static
  59. {
  60. return $this->afterCreating(function (Invoice $invoice) use ($invoiceStatus, $max, $min) {
  61. if ($invoice->isDraft()) {
  62. $this->recalculateTotals($invoice);
  63. $invoice->approveDraft();
  64. }
  65. $invoice->refresh();
  66. $totalAmountDue = $invoice->getRawOriginal('amount_due');
  67. if ($invoiceStatus === InvoiceStatus::Overpaid) {
  68. $totalAmountDue += random_int(1000, 10000);
  69. } elseif ($invoiceStatus === InvoiceStatus::Partial) {
  70. $totalAmountDue = (int) floor($totalAmountDue * 0.5);
  71. }
  72. if ($totalAmountDue <= 0 || empty($totalAmountDue)) {
  73. return;
  74. }
  75. $paymentCount = $max && $min ? $this->faker->numberBetween($min, $max) : $min;
  76. $paymentAmount = (int) floor($totalAmountDue / $paymentCount);
  77. $remainingAmount = $totalAmountDue;
  78. for ($i = 0; $i < $paymentCount; $i++) {
  79. $amount = $i === $paymentCount - 1 ? $remainingAmount : $paymentAmount;
  80. if ($amount <= 0) {
  81. break;
  82. }
  83. $data = [
  84. 'posted_at' => $invoice->date->addDay(),
  85. 'amount' => CurrencyConverter::convertCentsToFormatSimple($amount, $invoice->currency_code),
  86. 'payment_method' => $this->faker->randomElement(PaymentMethod::class),
  87. 'bank_account_id' => BankAccount::inRandomOrder()->value('id'),
  88. 'notes' => $this->faker->sentence,
  89. ];
  90. $invoice->recordPayment($data);
  91. $remainingAmount -= $amount;
  92. }
  93. });
  94. }
  95. public function configure(): static
  96. {
  97. return $this->afterCreating(function (Invoice $invoice) {
  98. // Use the invoice's ID to generate invoice and order numbers
  99. $paddedId = str_pad((string) $invoice->id, 5, '0', STR_PAD_LEFT);
  100. $invoice->updateQuietly([
  101. 'invoice_number' => "INV-{$paddedId}",
  102. 'order_number' => "ORD-{$paddedId}",
  103. ]);
  104. $this->recalculateTotals($invoice);
  105. if ($invoice->is_currently_overdue) {
  106. $invoice->updateQuietly([
  107. 'status' => InvoiceStatus::Overdue,
  108. ]);
  109. }
  110. });
  111. }
  112. protected function recalculateTotals(Invoice $invoice): void
  113. {
  114. if ($invoice->lineItems()->exists()) {
  115. $invoice->refresh();
  116. $subtotal = $invoice->lineItems()->sum('subtotal') / 100;
  117. $taxTotal = $invoice->lineItems()->sum('tax_total') / 100;
  118. $discountTotal = $invoice->lineItems()->sum('discount_total') / 100;
  119. $grandTotal = $subtotal + $taxTotal - $discountTotal;
  120. $invoice->update([
  121. 'subtotal' => $subtotal,
  122. 'tax_total' => $taxTotal,
  123. 'discount_total' => $discountTotal,
  124. 'total' => $grandTotal,
  125. ]);
  126. }
  127. }
  128. }