|
@@ -15,6 +15,7 @@ use App\Models\Setting\DocumentDefault;
|
15
|
15
|
use App\Utilities\RateCalculator;
|
16
|
16
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
17
|
17
|
use Illuminate\Support\Carbon;
|
|
18
|
+use Random\RandomException;
|
18
|
19
|
|
19
|
20
|
/**
|
20
|
21
|
* @extends Factory<Invoice>
|
|
@@ -33,17 +34,22 @@ class InvoiceFactory extends Factory
|
33
|
34
|
*/
|
34
|
35
|
public function definition(): array
|
35
|
36
|
{
|
36
|
|
- $invoiceDate = $this->faker->dateTimeBetween('-1 year');
|
|
37
|
+ $invoiceDate = $this->faker->dateTimeBetween('-2 months');
|
37
|
38
|
|
38
|
39
|
return [
|
39
|
40
|
'company_id' => 1,
|
40
|
|
- 'client_id' => fn (array $attributes) => Client::where('company_id', $attributes['company_id'])->inRandomOrder()->value('id'),
|
|
41
|
+ 'client_id' => function (array $attributes) {
|
|
42
|
+ return Client::where('company_id', $attributes['company_id'])->inRandomOrder()->value('id')
|
|
43
|
+ ?? Client::factory()->state([
|
|
44
|
+ 'company_id' => $attributes['company_id'],
|
|
45
|
+ ]);
|
|
46
|
+ },
|
41
|
47
|
'header' => 'Invoice',
|
42
|
48
|
'subheader' => 'Invoice',
|
43
|
49
|
'invoice_number' => $this->faker->unique()->numerify('INV-####'),
|
44
|
50
|
'order_number' => $this->faker->unique()->numerify('ORD-####'),
|
45
|
51
|
'date' => $invoiceDate,
|
46
|
|
- 'due_date' => Carbon::parse($invoiceDate)->addDays($this->faker->numberBetween(14, 60)),
|
|
52
|
+ 'due_date' => $this->faker->dateTimeInInterval($invoiceDate, '+3 months'),
|
47
|
53
|
'status' => InvoiceStatus::Draft,
|
48
|
54
|
'discount_method' => $this->faker->randomElement(DocumentDiscountMethod::class),
|
49
|
55
|
'discount_computation' => AdjustmentComputation::Percentage,
|
|
@@ -85,58 +91,38 @@ class InvoiceFactory extends Factory
|
85
|
91
|
public function approved(): static
|
86
|
92
|
{
|
87
|
93
|
return $this->afterCreating(function (Invoice $invoice) {
|
88
|
|
- $this->ensureLineItems($invoice);
|
89
|
|
-
|
90
|
|
- if (! $invoice->canBeApproved()) {
|
91
|
|
- return;
|
92
|
|
- }
|
93
|
|
-
|
94
|
|
- $approvedAt = Carbon::parse($invoice->date)
|
95
|
|
- ->addHours($this->faker->numberBetween(1, 24));
|
96
|
|
-
|
97
|
|
- $invoice->approveDraft($approvedAt);
|
|
94
|
+ $this->performApproval($invoice);
|
98
|
95
|
});
|
99
|
96
|
}
|
100
|
97
|
|
101
|
98
|
public function sent(): static
|
102
|
99
|
{
|
103
|
100
|
return $this->afterCreating(function (Invoice $invoice) {
|
104
|
|
- $this->ensureApproved($invoice);
|
105
|
|
-
|
106
|
|
- $sentAt = Carbon::parse($invoice->approved_at)
|
107
|
|
- ->addHours($this->faker->numberBetween(1, 24));
|
108
|
|
-
|
109
|
|
- $invoice->markAsSent($sentAt);
|
|
101
|
+ $this->performSent($invoice);
|
110
|
102
|
});
|
111
|
103
|
}
|
112
|
104
|
|
113
|
105
|
public function partial(int $maxPayments = 4): static
|
114
|
106
|
{
|
115
|
107
|
return $this->afterCreating(function (Invoice $invoice) use ($maxPayments) {
|
116
|
|
- $this->ensureSent($invoice);
|
117
|
|
-
|
118
|
|
- $this->withPayments(max: $maxPayments, invoiceStatus: InvoiceStatus::Partial)
|
119
|
|
- ->callAfterCreating(collect([$invoice]));
|
|
108
|
+ $this->performSent($invoice);
|
|
109
|
+ $this->performPayments($invoice, $maxPayments, InvoiceStatus::Partial);
|
120
|
110
|
});
|
121
|
111
|
}
|
122
|
112
|
|
123
|
113
|
public function paid(int $maxPayments = 4): static
|
124
|
114
|
{
|
125
|
115
|
return $this->afterCreating(function (Invoice $invoice) use ($maxPayments) {
|
126
|
|
- $this->ensureSent($invoice);
|
127
|
|
-
|
128
|
|
- $this->withPayments(max: $maxPayments)
|
129
|
|
- ->callAfterCreating(collect([$invoice]));
|
|
116
|
+ $this->performSent($invoice);
|
|
117
|
+ $this->performPayments($invoice, $maxPayments, InvoiceStatus::Paid);
|
130
|
118
|
});
|
131
|
119
|
}
|
132
|
120
|
|
133
|
121
|
public function overpaid(int $maxPayments = 4): static
|
134
|
122
|
{
|
135
|
123
|
return $this->afterCreating(function (Invoice $invoice) use ($maxPayments) {
|
136
|
|
- $this->ensureSent($invoice);
|
137
|
|
-
|
138
|
|
- $this->withPayments(max: $maxPayments, invoiceStatus: InvoiceStatus::Overpaid)
|
139
|
|
- ->callAfterCreating(collect([$invoice]));
|
|
124
|
+ $this->performSent($invoice);
|
|
125
|
+ $this->performPayments($invoice, $maxPayments, InvoiceStatus::Overpaid);
|
140
|
126
|
});
|
141
|
127
|
}
|
142
|
128
|
|
|
@@ -147,77 +133,98 @@ class InvoiceFactory extends Factory
|
147
|
133
|
'due_date' => now()->subDays($this->faker->numberBetween(1, 30)),
|
148
|
134
|
])
|
149
|
135
|
->afterCreating(function (Invoice $invoice) {
|
150
|
|
- $this->ensureApproved($invoice);
|
|
136
|
+ $this->performApproval($invoice);
|
151
|
137
|
});
|
152
|
138
|
}
|
153
|
139
|
|
154
|
|
- public function withPayments(?int $min = null, ?int $max = null, InvoiceStatus $invoiceStatus = InvoiceStatus::Paid): static
|
|
140
|
+ protected function performApproval(Invoice $invoice): void
|
155
|
141
|
{
|
156
|
|
- $min ??= 1;
|
|
142
|
+ if (! $invoice->hasLineItems()) {
|
|
143
|
+ throw new \InvalidArgumentException('Cannot approve invoice without line items. Use withLineItems() first.');
|
|
144
|
+ }
|
157
|
145
|
|
158
|
|
- return $this->afterCreating(function (Invoice $invoice) use ($invoiceStatus, $max, $min) {
|
159
|
|
- $this->ensureSent($invoice);
|
|
146
|
+ if (! $invoice->canBeApproved()) {
|
|
147
|
+ throw new \InvalidArgumentException('Invoice cannot be approved. Current status: ' . $invoice->status->value);
|
|
148
|
+ }
|
160
|
149
|
|
161
|
|
- $invoice->refresh();
|
|
150
|
+ $approvedAt = Carbon::parse($invoice->date)
|
|
151
|
+ ->addHours($this->faker->numberBetween(1, 24));
|
162
|
152
|
|
163
|
|
- $amountDue = $invoice->getRawOriginal('amount_due');
|
|
153
|
+ $invoice->approveDraft($approvedAt);
|
|
154
|
+ }
|
164
|
155
|
|
165
|
|
- $totalAmountDue = match ($invoiceStatus) {
|
166
|
|
- InvoiceStatus::Overpaid => $amountDue + random_int(1000, 10000),
|
167
|
|
- InvoiceStatus::Partial => (int) floor($amountDue * 0.5),
|
168
|
|
- default => $amountDue,
|
169
|
|
- };
|
|
156
|
+ protected function performSent(Invoice $invoice): void
|
|
157
|
+ {
|
|
158
|
+ if (! $invoice->wasApproved()) {
|
|
159
|
+ $this->performApproval($invoice);
|
|
160
|
+ }
|
170
|
161
|
|
171
|
|
- if ($totalAmountDue <= 0 || empty($totalAmountDue)) {
|
172
|
|
- return;
|
173
|
|
- }
|
|
162
|
+ $sentAt = Carbon::parse($invoice->approved_at)
|
|
163
|
+ ->addHours($this->faker->numberBetween(1, 24));
|
174
|
164
|
|
175
|
|
- $paymentCount = $max && $min ? $this->faker->numberBetween($min, $max) : $min;
|
176
|
|
- $paymentAmount = (int) floor($totalAmountDue / $paymentCount);
|
177
|
|
- $remainingAmount = $totalAmountDue;
|
|
165
|
+ $invoice->markAsSent($sentAt);
|
|
166
|
+ }
|
178
|
167
|
|
179
|
|
- $paymentDate = Carbon::parse($invoice->approved_at);
|
180
|
|
- $paymentDates = [];
|
|
168
|
+ /**
|
|
169
|
+ * @throws RandomException
|
|
170
|
+ */
|
|
171
|
+ protected function performPayments(Invoice $invoice, int $maxPayments, InvoiceStatus $invoiceStatus): void
|
|
172
|
+ {
|
|
173
|
+ $invoice->refresh();
|
181
|
174
|
|
182
|
|
- for ($i = 0; $i < $paymentCount; $i++) {
|
183
|
|
- $amount = $i === $paymentCount - 1 ? $remainingAmount : $paymentAmount;
|
|
175
|
+ $amountDue = $invoice->getRawOriginal('amount_due');
|
184
|
176
|
|
185
|
|
- if ($amount <= 0) {
|
186
|
|
- break;
|
187
|
|
- }
|
|
177
|
+ $totalAmountDue = match ($invoiceStatus) {
|
|
178
|
+ InvoiceStatus::Overpaid => $amountDue + random_int(1000, 10000),
|
|
179
|
+ InvoiceStatus::Partial => (int) floor($amountDue * 0.5),
|
|
180
|
+ default => $amountDue,
|
|
181
|
+ };
|
188
|
182
|
|
189
|
|
- $postedAt = $paymentDate->copy()->addDays($this->faker->numberBetween(1, 30));
|
190
|
|
- $paymentDates[] = $postedAt;
|
|
183
|
+ if ($totalAmountDue <= 0 || empty($totalAmountDue)) {
|
|
184
|
+ return;
|
|
185
|
+ }
|
191
|
186
|
|
192
|
|
- $data = [
|
193
|
|
- 'posted_at' => $postedAt,
|
194
|
|
- 'amount' => $amount,
|
195
|
|
- 'payment_method' => $this->faker->randomElement(PaymentMethod::class),
|
196
|
|
- 'bank_account_id' => BankAccount::where('company_id', $invoice->company_id)->inRandomOrder()->value('id'),
|
197
|
|
- 'notes' => $this->faker->sentence,
|
198
|
|
- ];
|
|
187
|
+ $paymentCount = $this->faker->numberBetween(1, $maxPayments);
|
|
188
|
+ $paymentAmount = (int) floor($totalAmountDue / $paymentCount);
|
|
189
|
+ $remainingAmount = $totalAmountDue;
|
199
|
190
|
|
200
|
|
- $invoice->recordPayment($data);
|
201
|
|
- $remainingAmount -= $amount;
|
202
|
|
- }
|
|
191
|
+ $paymentDate = Carbon::parse($invoice->approved_at);
|
|
192
|
+ $paymentDates = [];
|
|
193
|
+
|
|
194
|
+ for ($i = 0; $i < $paymentCount; $i++) {
|
|
195
|
+ $amount = $i === $paymentCount - 1 ? $remainingAmount : $paymentAmount;
|
203
|
196
|
|
204
|
|
- if ($invoiceStatus !== InvoiceStatus::Paid) {
|
205
|
|
- return;
|
|
197
|
+ if ($amount <= 0) {
|
|
198
|
+ break;
|
206
|
199
|
}
|
207
|
200
|
|
|
201
|
+ $postedAt = $paymentDate->copy()->addDays($this->faker->numberBetween(1, 30));
|
|
202
|
+ $paymentDates[] = $postedAt;
|
|
203
|
+
|
|
204
|
+ $data = [
|
|
205
|
+ 'posted_at' => $postedAt,
|
|
206
|
+ 'amount' => $amount,
|
|
207
|
+ 'payment_method' => $this->faker->randomElement(PaymentMethod::class),
|
|
208
|
+ 'bank_account_id' => BankAccount::where('company_id', $invoice->company_id)->inRandomOrder()->value('id'),
|
|
209
|
+ 'notes' => $this->faker->sentence,
|
|
210
|
+ ];
|
|
211
|
+
|
|
212
|
+ $invoice->recordPayment($data);
|
|
213
|
+ $remainingAmount -= $amount;
|
|
214
|
+ }
|
|
215
|
+
|
|
216
|
+ if ($invoiceStatus === InvoiceStatus::Paid && ! empty($paymentDates)) {
|
208
|
217
|
$latestPaymentDate = max($paymentDates);
|
209
|
218
|
$invoice->updateQuietly([
|
210
|
219
|
'status' => $invoiceStatus,
|
211
|
220
|
'paid_at' => $latestPaymentDate,
|
212
|
221
|
]);
|
213
|
|
- });
|
|
222
|
+ }
|
214
|
223
|
}
|
215
|
224
|
|
216
|
225
|
public function configure(): static
|
217
|
226
|
{
|
218
|
227
|
return $this->afterCreating(function (Invoice $invoice) {
|
219
|
|
- $this->ensureLineItems($invoice);
|
220
|
|
-
|
221
|
228
|
$number = DocumentDefault::getBaseNumber() + $invoice->id;
|
222
|
229
|
|
223
|
230
|
$invoice->updateQuietly([
|
|
@@ -233,27 +240,6 @@ class InvoiceFactory extends Factory
|
233
|
240
|
});
|
234
|
241
|
}
|
235
|
242
|
|
236
|
|
- protected function ensureLineItems(Invoice $invoice): void
|
237
|
|
- {
|
238
|
|
- if (! $invoice->hasLineItems()) {
|
239
|
|
- $this->withLineItems()->callAfterCreating(collect([$invoice]));
|
240
|
|
- }
|
241
|
|
- }
|
242
|
|
-
|
243
|
|
- protected function ensureApproved(Invoice $invoice): void
|
244
|
|
- {
|
245
|
|
- if (! $invoice->wasApproved()) {
|
246
|
|
- $this->approved()->callAfterCreating(collect([$invoice]));
|
247
|
|
- }
|
248
|
|
- }
|
249
|
|
-
|
250
|
|
- protected function ensureSent(Invoice $invoice): void
|
251
|
|
- {
|
252
|
|
- if (! $invoice->hasBeenSent()) {
|
253
|
|
- $this->sent()->callAfterCreating(collect([$invoice]));
|
254
|
|
- }
|
255
|
|
- }
|
256
|
|
-
|
257
|
243
|
protected function recalculateTotals(Invoice $invoice): void
|
258
|
244
|
{
|
259
|
245
|
$invoice->refresh();
|