Andrew Wallo пре 9 месеци
родитељ
комит
9b15af8f50

+ 10
- 2
app/Models/Accounting/RecurringInvoice.php Прегледај датотеку

@@ -187,7 +187,15 @@ class RecurringInvoice extends Document
187 187
 
188 188
     public function hasSchedule(): bool
189 189
     {
190
-        return $this->start_date !== null;
190
+        if (! $this->start_date) {
191
+            return false;
192
+        }
193
+
194
+        if (! $this->wasApproved() && $this->start_date->lt(today())) {
195
+            return false;
196
+        }
197
+
198
+        return true;
191 199
     }
192 200
 
193 201
     public function getScheduleDescription(): string
@@ -264,7 +272,7 @@ class RecurringInvoice extends Document
264 272
     {
265 273
         $lastDate ??= $this->last_date;
266 274
 
267
-        if (! $lastDate && $this->start_date) {
275
+        if (! $lastDate && $this->start_date && $this->wasApproved()) {
268 276
             return $this->start_date;
269 277
         }
270 278
 

+ 4
- 1
app/Observers/RecurringInvoiceObserver.php Прегледај датотеку

@@ -11,7 +11,10 @@ class RecurringInvoiceObserver
11 11
 {
12 12
     public function saving(RecurringInvoice $recurringInvoice): void
13 13
     {
14
-        if (($recurringInvoice->isDirty('start_date') && ! $recurringInvoice->last_date) || $this->otherScheduleDetailsChanged($recurringInvoice)) {
14
+        if (
15
+            $recurringInvoice->wasApproved() &&
16
+            (($recurringInvoice->isDirty('start_date') && ! $recurringInvoice->last_date) || $this->otherScheduleDetailsChanged($recurringInvoice))
17
+        ) {
15 18
             $recurringInvoice->next_date = $recurringInvoice->calculateNextDate();
16 19
         }
17 20
 

+ 2
- 1
database/factories/Accounting/DocumentLineItemFactory.php Прегледај датотеку

@@ -6,6 +6,7 @@ use App\Models\Accounting\Bill;
6 6
 use App\Models\Accounting\DocumentLineItem;
7 7
 use App\Models\Accounting\Estimate;
8 8
 use App\Models\Accounting\Invoice;
9
+use App\Models\Accounting\RecurringInvoice;
9 10
 use App\Models\Common\Offering;
10 11
 use Illuminate\Database\Eloquent\Factories\Factory;
11 12
 
@@ -37,7 +38,7 @@ class DocumentLineItemFactory extends Factory
37 38
         ];
38 39
     }
39 40
 
40
-    public function forInvoice(Invoice $invoice): static
41
+    public function forInvoice(Invoice | RecurringInvoice $invoice): static
41 42
     {
42 43
         return $this
43 44
             ->for($invoice, 'documentable')

+ 202
- 31
database/factories/Accounting/RecurringInvoiceFactory.php Прегледај датотеку

@@ -10,6 +10,7 @@ use App\Enums\Accounting\IntervalType;
10 10
 use App\Enums\Accounting\Month;
11 11
 use App\Enums\Accounting\RecurringInvoiceStatus;
12 12
 use App\Enums\Setting\PaymentTerms;
13
+use App\Models\Accounting\DocumentLineItem;
13 14
 use App\Models\Accounting\RecurringInvoice;
14 15
 use App\Models\Common\Client;
15 16
 use Illuminate\Database\Eloquent\Factories\Factory;
@@ -35,21 +36,12 @@ class RecurringInvoiceFactory extends Factory
35 36
         return [
36 37
             'company_id' => 1,
37 38
             'client_id' => Client::inRandomOrder()->value('id'),
39
+            'header' => 'Invoice',
40
+            'subheader' => 'Invoice',
41
+            'order_number' => $this->faker->unique()->numerify('ORD-#####'),
42
+            'payment_terms' => PaymentTerms::Net30,
38 43
             'status' => RecurringInvoiceStatus::Draft,
39
-
40
-            // Schedule configuration
41
-            'frequency' => Frequency::Monthly,
42
-            'day_of_month' => DayOfMonth::First,
43
-
44
-            // Date configuration
45
-            'start_date' => now()->addMonth()->startOfMonth(),
46
-            'end_type' => EndType::Never,
47
-
48
-            // Invoice configuration
49
-            'payment_terms' => PaymentTerms::DueUponReceipt,
50 44
             'currency_code' => 'USD',
51
-
52
-            // Timestamps and user tracking
53 45
             'terms' => $this->faker->sentence,
54 46
             'footer' => $this->faker->sentence,
55 47
             'created_by' => 1,
@@ -57,61 +49,240 @@ class RecurringInvoiceFactory extends Factory
57 49
         ];
58 50
     }
59 51
 
60
-    public function weekly(DayOfWeek $dayOfWeek = DayOfWeek::Monday): self
52
+    public function withLineItems(int $count = 3): static
53
+    {
54
+        return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($count) {
55
+            DocumentLineItem::factory()
56
+                ->count($count)
57
+                ->forInvoice($recurringInvoice)
58
+                ->create();
59
+
60
+            $this->recalculateTotals($recurringInvoice);
61
+        });
62
+    }
63
+
64
+    public function withSchedule(
65
+        ?Frequency $frequency = null,
66
+        ?Carbon $startDate = null,
67
+        ?EndType $endType = null
68
+    ): static {
69
+        $frequency ??= $this->faker->randomElement(Frequency::class);
70
+        $endType ??= EndType::Never;
71
+
72
+        // Adjust the start date range based on frequency
73
+        $startDate = match ($frequency) {
74
+            Frequency::Daily => Carbon::parse($this->faker->dateTimeBetween('-30 days')), // At most 30 days back
75
+            default => $startDate ?? Carbon::parse($this->faker->dateTimeBetween('-1 year')),
76
+        };
77
+
78
+        return match ($frequency) {
79
+            Frequency::Daily => $this->withDailySchedule($startDate, $endType),
80
+            Frequency::Weekly => $this->withWeeklySchedule($startDate, $endType),
81
+            Frequency::Monthly => $this->withMonthlySchedule($startDate, $endType),
82
+            Frequency::Yearly => $this->withYearlySchedule($startDate, $endType),
83
+            Frequency::Custom => $this->withCustomSchedule($startDate, $endType),
84
+        };
85
+    }
86
+
87
+    protected function withDailySchedule(Carbon $startDate, EndType $endType): static
88
+    {
89
+        return $this->state([
90
+            'frequency' => Frequency::Daily,
91
+            'start_date' => $startDate,
92
+            'end_type' => $endType,
93
+        ]);
94
+    }
95
+
96
+    protected function withWeeklySchedule(Carbon $startDate, EndType $endType): static
61 97
     {
62 98
         return $this->state([
63 99
             'frequency' => Frequency::Weekly,
64
-            'day_of_week' => $dayOfWeek,
100
+            'day_of_week' => DayOfWeek::from($startDate->dayOfWeek),
101
+            'start_date' => $startDate,
102
+            'end_type' => $endType,
65 103
         ]);
66 104
     }
67 105
 
68
-    public function monthly(DayOfMonth $dayOfMonth = DayOfMonth::First): self
106
+    protected function withMonthlySchedule(Carbon $startDate, EndType $endType): static
69 107
     {
70 108
         return $this->state([
71 109
             'frequency' => Frequency::Monthly,
72
-            'day_of_month' => $dayOfMonth,
110
+            'day_of_month' => DayOfMonth::from($startDate->day),
111
+            'start_date' => $startDate,
112
+            'end_type' => $endType,
73 113
         ]);
74 114
     }
75 115
 
76
-    public function yearly(Month $month = Month::January, DayOfMonth $dayOfMonth = DayOfMonth::First): self
116
+    protected function withYearlySchedule(Carbon $startDate, EndType $endType): static
77 117
     {
78 118
         return $this->state([
79 119
             'frequency' => Frequency::Yearly,
80
-            'month' => $month,
81
-            'day_of_month' => $dayOfMonth,
120
+            'month' => Month::from($startDate->month),
121
+            'day_of_month' => DayOfMonth::from($startDate->day),
122
+            'start_date' => $startDate,
123
+            'end_type' => $endType,
82 124
         ]);
83 125
     }
84 126
 
85
-    public function custom(IntervalType $intervalType, int $intervalValue = 1): self
86
-    {
87
-        return $this->state([
127
+    protected function withCustomSchedule(
128
+        Carbon $startDate,
129
+        EndType $endType,
130
+        ?IntervalType $intervalType = null,
131
+        ?int $intervalValue = null
132
+    ): static {
133
+        $intervalType ??= $this->faker->randomElement(IntervalType::class);
134
+        $intervalValue ??= match ($intervalType) {
135
+            IntervalType::Day => $this->faker->numberBetween(1, 7),
136
+            IntervalType::Week => $this->faker->numberBetween(1, 4),
137
+            IntervalType::Month => $this->faker->numberBetween(1, 3),
138
+            IntervalType::Year => 1,
139
+        };
140
+
141
+        $state = [
88 142
             'frequency' => Frequency::Custom,
89 143
             'interval_type' => $intervalType,
90 144
             'interval_value' => $intervalValue,
145
+            'start_date' => $startDate,
146
+            'end_type' => $endType,
147
+        ];
148
+
149
+        // Add interval-specific attributes
150
+        switch ($intervalType) {
151
+            case IntervalType::Day:
152
+                // No additional attributes needed
153
+                break;
154
+
155
+            case IntervalType::Week:
156
+                $state['day_of_week'] = DayOfWeek::from($startDate->dayOfWeek);
157
+
158
+                break;
159
+
160
+            case IntervalType::Month:
161
+                $state['day_of_month'] = DayOfMonth::from($startDate->day);
162
+
163
+                break;
164
+
165
+            case IntervalType::Year:
166
+                $state['month'] = Month::from($startDate->month);
167
+                $state['day_of_month'] = DayOfMonth::from($startDate->day);
168
+
169
+                break;
170
+        }
171
+
172
+        return $this->state($state);
173
+    }
174
+
175
+    public function endAfter(int $occurrences = 12): static
176
+    {
177
+        return $this->state([
178
+            'end_type' => EndType::After,
179
+            'max_occurrences' => $occurrences,
91 180
         ]);
92 181
     }
93 182
 
94
-    public function withEndDate(Carbon $endDate): self
183
+    public function endOn(?Carbon $endDate = null): static
95 184
     {
185
+        $endDate ??= now()->addMonths($this->faker->numberBetween(1, 12));
186
+
96 187
         return $this->state([
97 188
             'end_type' => EndType::On,
98 189
             'end_date' => $endDate,
99 190
         ]);
100 191
     }
101 192
 
102
-    public function withMaxOccurrences(int $maxOccurrences): self
193
+    public function autoSend(string $sendTime = '09:00'): static
103 194
     {
104 195
         return $this->state([
105
-            'end_type' => EndType::After,
106
-            'max_occurrences' => $maxOccurrences,
196
+            'auto_send' => true,
197
+            'send_time' => $sendTime,
107 198
         ]);
108 199
     }
109 200
 
110
-    public function autoSend(string $time = '09:00'): self
201
+    public function approved(): static
111 202
     {
112
-        return $this->state([
113
-            'auto_send' => true,
114
-            'send_time' => $time,
203
+        return $this->afterCreating(function (RecurringInvoice $recurringInvoice) {
204
+            $this->ensureLineItems($recurringInvoice);
205
+
206
+            if (! $recurringInvoice->hasSchedule()) {
207
+                $this->withSchedule()->callAfterCreating(collect([$recurringInvoice]));
208
+                $recurringInvoice->refresh();
209
+            }
210
+
211
+            if (! $recurringInvoice->canBeApproved()) {
212
+                return;
213
+            }
214
+
215
+            $approvedAt = $recurringInvoice->start_date
216
+                ? $recurringInvoice->start_date->copy()->subDays($this->faker->numberBetween(1, 7))
217
+                : now()->subDays($this->faker->numberBetween(1, 30));
218
+
219
+            $recurringInvoice->approveDraft($approvedAt);
220
+        });
221
+    }
222
+
223
+    public function active(): static
224
+    {
225
+        return $this->withLineItems()
226
+            ->withSchedule()
227
+            ->approved();
228
+    }
229
+
230
+    public function ended(): static
231
+    {
232
+        return $this->afterCreating(function (RecurringInvoice $recurringInvoice) {
233
+            if (! $recurringInvoice->canBeEnded()) {
234
+                $this->active()->callAfterCreating(collect([$recurringInvoice]));
235
+            }
236
+
237
+            $endedAt = now()->subDays($this->faker->numberBetween(1, 30));
238
+
239
+            $recurringInvoice->update([
240
+                'ended_at' => $endedAt,
241
+                'status' => RecurringInvoiceStatus::Ended,
242
+            ]);
243
+        });
244
+    }
245
+
246
+    public function configure(): static
247
+    {
248
+        return $this->afterCreating(function (RecurringInvoice $recurringInvoice) {
249
+            $this->ensureLineItems($recurringInvoice);
250
+
251
+            $nextDate = $recurringInvoice->calculateNextDate();
252
+
253
+            if ($nextDate) {
254
+                $recurringInvoice->updateQuietly([
255
+                    'next_date' => $nextDate,
256
+                ]);
257
+            }
258
+        });
259
+    }
260
+
261
+    protected function ensureLineItems(RecurringInvoice $recurringInvoice): void
262
+    {
263
+        if (! $recurringInvoice->hasLineItems()) {
264
+            $this->withLineItems()->callAfterCreating(collect([$recurringInvoice]));
265
+        }
266
+    }
267
+
268
+    protected function recalculateTotals(RecurringInvoice $recurringInvoice): void
269
+    {
270
+        $recurringInvoice->refresh();
271
+
272
+        if (! $recurringInvoice->hasLineItems()) {
273
+            return;
274
+        }
275
+
276
+        $subtotal = $recurringInvoice->lineItems()->sum('subtotal') / 100;
277
+        $taxTotal = $recurringInvoice->lineItems()->sum('tax_total') / 100;
278
+        $discountTotal = $recurringInvoice->lineItems()->sum('discount_total') / 100;
279
+        $grandTotal = $subtotal + $taxTotal - $discountTotal;
280
+
281
+        $recurringInvoice->update([
282
+            'subtotal' => $subtotal,
283
+            'tax_total' => $taxTotal,
284
+            'discount_total' => $discountTotal,
285
+            'total' => $grandTotal,
115 286
         ]);
116 287
     }
117 288
 }

+ 81
- 0
database/factories/CompanyFactory.php Прегледај датотеку

@@ -2,9 +2,11 @@
2 2
 
3 3
 namespace Database\Factories;
4 4
 
5
+use App\Enums\Accounting\Frequency;
5 6
 use App\Models\Accounting\Bill;
6 7
 use App\Models\Accounting\Estimate;
7 8
 use App\Models\Accounting\Invoice;
9
+use App\Models\Accounting\RecurringInvoice;
8 10
 use App\Models\Accounting\Transaction;
9 11
 use App\Models\Common\Client;
10 12
 use App\Models\Common\Offering;
@@ -164,6 +166,85 @@ class CompanyFactory extends Factory
164 166
         });
165 167
     }
166 168
 
169
+    public function withRecurringInvoices(int $count = 10): self
170
+    {
171
+        return $this->afterCreating(function (Company $company) use ($count) {
172
+            $draftCount = (int) floor($count * 0.2);     // 20% drafts without schedule
173
+            $scheduledCount = (int) floor($count * 0.2);  // 20% drafts with schedule
174
+            $activeCount = (int) floor($count * 0.4);     // 40% active and generating
175
+            $endedCount = (int) floor($count * 0.1);      // 10% manually ended
176
+            $completedCount = $count - ($draftCount + $scheduledCount + $activeCount + $endedCount); // 10% completed by end conditions
177
+
178
+            // Draft recurring invoices (no schedule)
179
+            RecurringInvoice::factory()
180
+                ->count($draftCount)
181
+                ->withLineItems()
182
+                ->create([
183
+                    'company_id' => $company->id,
184
+                    'created_by' => $company->user_id,
185
+                    'updated_by' => $company->user_id,
186
+                ]);
187
+
188
+            // Draft recurring invoices with schedule
189
+            RecurringInvoice::factory()
190
+                ->count($scheduledCount)
191
+                ->withLineItems()
192
+                ->withSchedule()
193
+                ->create([
194
+                    'company_id' => $company->id,
195
+                    'created_by' => $company->user_id,
196
+                    'updated_by' => $company->user_id,
197
+                ]);
198
+
199
+            // Active recurring invoices with various schedules and historical invoices
200
+            $frequencies = [
201
+                Frequency::Daily,
202
+                Frequency::Weekly,
203
+                Frequency::Monthly,
204
+                Frequency::Yearly,
205
+                Frequency::Custom,
206
+            ];
207
+
208
+            foreach (array_chunk(range(1, $activeCount), (int) ceil($activeCount / count($frequencies))) as $chunk) {
209
+                RecurringInvoice::factory()
210
+                    ->count(count($chunk))
211
+                    ->withLineItems()
212
+                    ->withSchedule(fake()->randomElement($frequencies)) // Randomize frequency
213
+                    ->active()
214
+                    ->create([
215
+                        'company_id' => $company->id,
216
+                        'created_by' => $company->user_id,
217
+                        'updated_by' => $company->user_id,
218
+                    ]);
219
+            }
220
+
221
+            // Manually ended recurring invoices
222
+            RecurringInvoice::factory()
223
+                ->count($endedCount)
224
+                ->withLineItems()
225
+                ->withSchedule()
226
+                ->ended()
227
+                ->create([
228
+                    'company_id' => $company->id,
229
+                    'created_by' => $company->user_id,
230
+                    'updated_by' => $company->user_id,
231
+                ]);
232
+
233
+            // Completed recurring invoices (reached end conditions)
234
+            RecurringInvoice::factory()
235
+                ->count($completedCount)
236
+                ->withLineItems()
237
+                ->withSchedule()
238
+                ->endAfter($this->faker->numberBetween(5, 12))
239
+                ->active()
240
+                ->create([
241
+                    'company_id' => $company->id,
242
+                    'created_by' => $company->user_id,
243
+                    'updated_by' => $company->user_id,
244
+                ]);
245
+        });
246
+    }
247
+
167 248
     public function withEstimates(int $count = 10): self
168 249
     {
169 250
         return $this->afterCreating(function (Company $company) use ($count) {

+ 1
- 0
database/seeders/DatabaseSeeder.php Прегледај датотеку

@@ -25,6 +25,7 @@ class DatabaseSeeder extends Seeder
25 25
                     ->withClients()
26 26
                     ->withVendors()
27 27
                     ->withInvoices(50)
28
+                    ->withRecurringInvoices()
28 29
                     ->withEstimates(50)
29 30
                     ->withBills(50);
30 31
             })

Loading…
Откажи
Сачувај