Andrew Wallo il y a 4 mois
Parent
révision
4242133852

+ 57
- 57
composer.lock Voir le fichier

@@ -497,16 +497,16 @@
497 497
         },
498 498
         {
499 499
             "name": "aws/aws-sdk-php",
500
-            "version": "3.343.22",
500
+            "version": "3.344.0",
501 501
             "source": {
502 502
                 "type": "git",
503 503
                 "url": "https://github.com/aws/aws-sdk-php.git",
504
-                "reference": "174cc187df3bde52c21e9c00a4e99610a08732a3"
504
+                "reference": "787a8ec6301657d9cbdb389db4fa92243c68666a"
505 505
             },
506 506
             "dist": {
507 507
                 "type": "zip",
508
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/174cc187df3bde52c21e9c00a4e99610a08732a3",
509
-                "reference": "174cc187df3bde52c21e9c00a4e99610a08732a3",
508
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/787a8ec6301657d9cbdb389db4fa92243c68666a",
509
+                "reference": "787a8ec6301657d9cbdb389db4fa92243c68666a",
510 510
                 "shasum": ""
511 511
             },
512 512
             "require": {
@@ -588,9 +588,9 @@
588 588
             "support": {
589 589
                 "forum": "https://github.com/aws/aws-sdk-php/discussions",
590 590
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
591
-                "source": "https://github.com/aws/aws-sdk-php/tree/3.343.22"
591
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.344.0"
592 592
             },
593
-            "time": "2025-05-30T18:11:02+00:00"
593
+            "time": "2025-06-04T18:36:41+00:00"
594 594
         },
595 595
         {
596 596
             "name": "aws/aws-sdk-php-laravel",
@@ -1799,16 +1799,16 @@
1799 1799
         },
1800 1800
         {
1801 1801
             "name": "filament/actions",
1802
-            "version": "v3.3.17",
1802
+            "version": "v3.3.20",
1803 1803
             "source": {
1804 1804
                 "type": "git",
1805 1805
                 "url": "https://github.com/filamentphp/actions.git",
1806
-                "reference": "66b509aa72fa882ce91218eb743684a9350bc3fb"
1806
+                "reference": "151f776552ee10d70591c2649708bc4b0a7cba91"
1807 1807
             },
1808 1808
             "dist": {
1809 1809
                 "type": "zip",
1810
-                "url": "https://api.github.com/repos/filamentphp/actions/zipball/66b509aa72fa882ce91218eb743684a9350bc3fb",
1811
-                "reference": "66b509aa72fa882ce91218eb743684a9350bc3fb",
1810
+                "url": "https://api.github.com/repos/filamentphp/actions/zipball/151f776552ee10d70591c2649708bc4b0a7cba91",
1811
+                "reference": "151f776552ee10d70591c2649708bc4b0a7cba91",
1812 1812
                 "shasum": ""
1813 1813
             },
1814 1814
             "require": {
@@ -1848,11 +1848,11 @@
1848 1848
                 "issues": "https://github.com/filamentphp/filament/issues",
1849 1849
                 "source": "https://github.com/filamentphp/filament"
1850 1850
             },
1851
-            "time": "2025-05-19T07:25:24+00:00"
1851
+            "time": "2025-06-03T06:15:27+00:00"
1852 1852
         },
1853 1853
         {
1854 1854
             "name": "filament/filament",
1855
-            "version": "v3.3.17",
1855
+            "version": "v3.3.20",
1856 1856
             "source": {
1857 1857
                 "type": "git",
1858 1858
                 "url": "https://github.com/filamentphp/panels.git",
@@ -1917,16 +1917,16 @@
1917 1917
         },
1918 1918
         {
1919 1919
             "name": "filament/forms",
1920
-            "version": "v3.3.17",
1920
+            "version": "v3.3.20",
1921 1921
             "source": {
1922 1922
                 "type": "git",
1923 1923
                 "url": "https://github.com/filamentphp/forms.git",
1924
-                "reference": "5cef64051fcb23cbbc6152baaac5da1c44ec9a63"
1924
+                "reference": "d73cdda057a4f5bd409eab9573101e73edb404cc"
1925 1925
             },
1926 1926
             "dist": {
1927 1927
                 "type": "zip",
1928
-                "url": "https://api.github.com/repos/filamentphp/forms/zipball/5cef64051fcb23cbbc6152baaac5da1c44ec9a63",
1929
-                "reference": "5cef64051fcb23cbbc6152baaac5da1c44ec9a63",
1928
+                "url": "https://api.github.com/repos/filamentphp/forms/zipball/d73cdda057a4f5bd409eab9573101e73edb404cc",
1929
+                "reference": "d73cdda057a4f5bd409eab9573101e73edb404cc",
1930 1930
                 "shasum": ""
1931 1931
             },
1932 1932
             "require": {
@@ -1969,20 +1969,20 @@
1969 1969
                 "issues": "https://github.com/filamentphp/filament/issues",
1970 1970
                 "source": "https://github.com/filamentphp/filament"
1971 1971
             },
1972
-            "time": "2025-05-27T18:46:33+00:00"
1972
+            "time": "2025-06-03T13:40:37+00:00"
1973 1973
         },
1974 1974
         {
1975 1975
             "name": "filament/infolists",
1976
-            "version": "v3.3.17",
1976
+            "version": "v3.3.20",
1977 1977
             "source": {
1978 1978
                 "type": "git",
1979 1979
                 "url": "https://github.com/filamentphp/infolists.git",
1980
-                "reference": "cc71f1c15f132660986384d302a33a2b20618a96"
1980
+                "reference": "b54ff0fa89f654eca1c14edfd41a7e16ccb8165d"
1981 1981
             },
1982 1982
             "dist": {
1983 1983
                 "type": "zip",
1984
-                "url": "https://api.github.com/repos/filamentphp/infolists/zipball/cc71f1c15f132660986384d302a33a2b20618a96",
1985
-                "reference": "cc71f1c15f132660986384d302a33a2b20618a96",
1984
+                "url": "https://api.github.com/repos/filamentphp/infolists/zipball/b54ff0fa89f654eca1c14edfd41a7e16ccb8165d",
1985
+                "reference": "b54ff0fa89f654eca1c14edfd41a7e16ccb8165d",
1986 1986
                 "shasum": ""
1987 1987
             },
1988 1988
             "require": {
@@ -2020,11 +2020,11 @@
2020 2020
                 "issues": "https://github.com/filamentphp/filament/issues",
2021 2021
                 "source": "https://github.com/filamentphp/filament"
2022 2022
             },
2023
-            "time": "2025-04-23T06:39:44+00:00"
2023
+            "time": "2025-06-02T09:43:23+00:00"
2024 2024
         },
2025 2025
         {
2026 2026
             "name": "filament/notifications",
2027
-            "version": "v3.3.17",
2027
+            "version": "v3.3.20",
2028 2028
             "source": {
2029 2029
                 "type": "git",
2030 2030
                 "url": "https://github.com/filamentphp/notifications.git",
@@ -2076,16 +2076,16 @@
2076 2076
         },
2077 2077
         {
2078 2078
             "name": "filament/support",
2079
-            "version": "v3.3.17",
2079
+            "version": "v3.3.20",
2080 2080
             "source": {
2081 2081
                 "type": "git",
2082 2082
                 "url": "https://github.com/filamentphp/support.git",
2083
-                "reference": "537663fa2c5057aa236a189255b623b5124d32d8"
2083
+                "reference": "4f9793ad3339301ca53ea6f2c984734f7ac38ce7"
2084 2084
             },
2085 2085
             "dist": {
2086 2086
                 "type": "zip",
2087
-                "url": "https://api.github.com/repos/filamentphp/support/zipball/537663fa2c5057aa236a189255b623b5124d32d8",
2088
-                "reference": "537663fa2c5057aa236a189255b623b5124d32d8",
2087
+                "url": "https://api.github.com/repos/filamentphp/support/zipball/4f9793ad3339301ca53ea6f2c984734f7ac38ce7",
2088
+                "reference": "4f9793ad3339301ca53ea6f2c984734f7ac38ce7",
2089 2089
                 "shasum": ""
2090 2090
             },
2091 2091
             "require": {
@@ -2131,20 +2131,20 @@
2131 2131
                 "issues": "https://github.com/filamentphp/filament/issues",
2132 2132
                 "source": "https://github.com/filamentphp/filament"
2133 2133
             },
2134
-            "time": "2025-05-21T08:45:20+00:00"
2134
+            "time": "2025-06-03T06:16:13+00:00"
2135 2135
         },
2136 2136
         {
2137 2137
             "name": "filament/tables",
2138
-            "version": "v3.3.17",
2138
+            "version": "v3.3.20",
2139 2139
             "source": {
2140 2140
                 "type": "git",
2141 2141
                 "url": "https://github.com/filamentphp/tables.git",
2142
-                "reference": "64806e3c13caeabb23a8668a7aaf1efc8395df96"
2142
+                "reference": "1a107a8411549297b97d1142b1f7a5fa7a65e32b"
2143 2143
             },
2144 2144
             "dist": {
2145 2145
                 "type": "zip",
2146
-                "url": "https://api.github.com/repos/filamentphp/tables/zipball/64806e3c13caeabb23a8668a7aaf1efc8395df96",
2147
-                "reference": "64806e3c13caeabb23a8668a7aaf1efc8395df96",
2146
+                "url": "https://api.github.com/repos/filamentphp/tables/zipball/1a107a8411549297b97d1142b1f7a5fa7a65e32b",
2147
+                "reference": "1a107a8411549297b97d1142b1f7a5fa7a65e32b",
2148 2148
                 "shasum": ""
2149 2149
             },
2150 2150
             "require": {
@@ -2183,11 +2183,11 @@
2183 2183
                 "issues": "https://github.com/filamentphp/filament/issues",
2184 2184
                 "source": "https://github.com/filamentphp/filament"
2185 2185
             },
2186
-            "time": "2025-05-19T07:26:42+00:00"
2186
+            "time": "2025-06-02T09:43:47+00:00"
2187 2187
         },
2188 2188
         {
2189 2189
             "name": "filament/widgets",
2190
-            "version": "v3.3.17",
2190
+            "version": "v3.3.20",
2191 2191
             "source": {
2192 2192
                 "type": "git",
2193 2193
                 "url": "https://github.com/filamentphp/widgets.git",
@@ -3117,16 +3117,16 @@
3117 3117
         },
3118 3118
         {
3119 3119
             "name": "laravel/framework",
3120
-            "version": "v11.45.0",
3120
+            "version": "v11.45.1",
3121 3121
             "source": {
3122 3122
                 "type": "git",
3123 3123
                 "url": "https://github.com/laravel/framework.git",
3124
-                "reference": "d0730deb427632004d24801be7ca1ed2c10fbc4e"
3124
+                "reference": "b09ba32795b8e71df10856a2694706663984a239"
3125 3125
             },
3126 3126
             "dist": {
3127 3127
                 "type": "zip",
3128
-                "url": "https://api.github.com/repos/laravel/framework/zipball/d0730deb427632004d24801be7ca1ed2c10fbc4e",
3129
-                "reference": "d0730deb427632004d24801be7ca1ed2c10fbc4e",
3128
+                "url": "https://api.github.com/repos/laravel/framework/zipball/b09ba32795b8e71df10856a2694706663984a239",
3129
+                "reference": "b09ba32795b8e71df10856a2694706663984a239",
3130 3130
                 "shasum": ""
3131 3131
             },
3132 3132
             "require": {
@@ -3328,7 +3328,7 @@
3328 3328
                 "issues": "https://github.com/laravel/framework/issues",
3329 3329
                 "source": "https://github.com/laravel/framework"
3330 3330
             },
3331
-            "time": "2025-05-20T15:15:58+00:00"
3331
+            "time": "2025-06-03T14:01:40+00:00"
3332 3332
         },
3333 3333
         {
3334 3334
             "name": "laravel/prompts",
@@ -4976,16 +4976,16 @@
4976 4976
         },
4977 4977
         {
4978 4978
             "name": "nette/utils",
4979
-            "version": "v4.0.6",
4979
+            "version": "v4.0.7",
4980 4980
             "source": {
4981 4981
                 "type": "git",
4982 4982
                 "url": "https://github.com/nette/utils.git",
4983
-                "reference": "ce708655043c7050eb050df361c5e313cf708309"
4983
+                "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2"
4984 4984
             },
4985 4985
             "dist": {
4986 4986
                 "type": "zip",
4987
-                "url": "https://api.github.com/repos/nette/utils/zipball/ce708655043c7050eb050df361c5e313cf708309",
4988
-                "reference": "ce708655043c7050eb050df361c5e313cf708309",
4987
+                "url": "https://api.github.com/repos/nette/utils/zipball/e67c4061eb40b9c113b218214e42cb5a0dda28f2",
4988
+                "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2",
4989 4989
                 "shasum": ""
4990 4990
             },
4991 4991
             "require": {
@@ -5056,9 +5056,9 @@
5056 5056
             ],
5057 5057
             "support": {
5058 5058
                 "issues": "https://github.com/nette/utils/issues",
5059
-                "source": "https://github.com/nette/utils/tree/v4.0.6"
5059
+                "source": "https://github.com/nette/utils/tree/v4.0.7"
5060 5060
             },
5061
-            "time": "2025-03-30T21:06:30+00:00"
5061
+            "time": "2025-06-03T04:55:08+00:00"
5062 5062
         },
5063 5063
         {
5064 5064
             "name": "nikic/php-parser",
@@ -9606,16 +9606,16 @@
9606 9606
         },
9607 9607
         {
9608 9608
             "name": "filp/whoops",
9609
-            "version": "2.18.0",
9609
+            "version": "2.18.1",
9610 9610
             "source": {
9611 9611
                 "type": "git",
9612 9612
                 "url": "https://github.com/filp/whoops.git",
9613
-                "reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e"
9613
+                "reference": "8fcc6a862f2e7b94eb4221fd0819ddba3d30ab26"
9614 9614
             },
9615 9615
             "dist": {
9616 9616
                 "type": "zip",
9617
-                "url": "https://api.github.com/repos/filp/whoops/zipball/a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e",
9618
-                "reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e",
9617
+                "url": "https://api.github.com/repos/filp/whoops/zipball/8fcc6a862f2e7b94eb4221fd0819ddba3d30ab26",
9618
+                "reference": "8fcc6a862f2e7b94eb4221fd0819ddba3d30ab26",
9619 9619
                 "shasum": ""
9620 9620
             },
9621 9621
             "require": {
@@ -9665,7 +9665,7 @@
9665 9665
             ],
9666 9666
             "support": {
9667 9667
                 "issues": "https://github.com/filp/whoops/issues",
9668
-                "source": "https://github.com/filp/whoops/tree/2.18.0"
9668
+                "source": "https://github.com/filp/whoops/tree/2.18.1"
9669 9669
             },
9670 9670
             "funding": [
9671 9671
                 {
@@ -9673,7 +9673,7 @@
9673 9673
                     "type": "github"
9674 9674
                 }
9675 9675
             ],
9676
-            "time": "2025-03-15T12:00:00+00:00"
9676
+            "time": "2025-06-03T18:56:14+00:00"
9677 9677
         },
9678 9678
         {
9679 9679
             "name": "hamcrest/hamcrest-php",
@@ -10792,16 +10792,16 @@
10792 10792
         },
10793 10793
         {
10794 10794
             "name": "php-di/php-di",
10795
-            "version": "7.0.10",
10795
+            "version": "7.0.11",
10796 10796
             "source": {
10797 10797
                 "type": "git",
10798 10798
                 "url": "https://github.com/PHP-DI/PHP-DI.git",
10799
-                "reference": "0d1ed64126577e9a095b3204dcaee58cf76432c2"
10799
+                "reference": "32f111a6d214564520a57831d397263e8946c1d2"
10800 10800
             },
10801 10801
             "dist": {
10802 10802
                 "type": "zip",
10803
-                "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/0d1ed64126577e9a095b3204dcaee58cf76432c2",
10804
-                "reference": "0d1ed64126577e9a095b3204dcaee58cf76432c2",
10803
+                "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/32f111a6d214564520a57831d397263e8946c1d2",
10804
+                "reference": "32f111a6d214564520a57831d397263e8946c1d2",
10805 10805
                 "shasum": ""
10806 10806
             },
10807 10807
             "require": {
@@ -10849,7 +10849,7 @@
10849 10849
             ],
10850 10850
             "support": {
10851 10851
                 "issues": "https://github.com/PHP-DI/PHP-DI/issues",
10852
-                "source": "https://github.com/PHP-DI/PHP-DI/tree/7.0.10"
10852
+                "source": "https://github.com/PHP-DI/PHP-DI/tree/7.0.11"
10853 10853
             },
10854 10854
             "funding": [
10855 10855
                 {
@@ -10861,7 +10861,7 @@
10861 10861
                     "type": "tidelift"
10862 10862
                 }
10863 10863
             ],
10864
-            "time": "2025-04-22T08:53:15+00:00"
10864
+            "time": "2025-06-03T07:45:57+00:00"
10865 10865
         },
10866 10866
         {
10867 10867
             "name": "phpdocumentor/reflection-common",

+ 79
- 82
database/factories/Accounting/BillFactory.php Voir le fichier

@@ -33,23 +33,20 @@ class BillFactory extends Factory
33 33
      */
34 34
     public function definition(): array
35 35
     {
36
-        $isFutureBill = $this->faker->boolean();
37
-
38
-        if ($isFutureBill) {
39
-            $billDate = $this->faker->dateTimeBetween('-10 days', '+10 days');
40
-        } else {
41
-            $billDate = $this->faker->dateTimeBetween('-1 year', '-30 days');
42
-        }
43
-
44
-        $dueDays = $this->faker->numberBetween(14, 60);
36
+        $billDate = $this->faker->dateTimeBetween('-1 year', '-1 day');
45 37
 
46 38
         return [
47 39
             'company_id' => 1,
48
-            'vendor_id' => fn (array $attributes) => Vendor::where('company_id', $attributes['company_id'])->inRandomOrder()->value('id'),
40
+            'vendor_id' => function (array $attributes) {
41
+                return Vendor::where('company_id', $attributes['company_id'])->inRandomOrder()->value('id')
42
+                    ?? Vendor::factory()->state([
43
+                        'company_id' => $attributes['company_id'],
44
+                    ]);
45
+            },
49 46
             'bill_number' => $this->faker->unique()->numerify('BILL-####'),
50 47
             'order_number' => $this->faker->unique()->numerify('PO-####'),
51 48
             'date' => $billDate,
52
-            'due_date' => Carbon::parse($billDate)->addDays($dueDays),
49
+            'due_date' => $this->faker->dateTimeInInterval($billDate, '+6 months'),
53 50
             'status' => BillStatus::Open,
54 51
             'discount_method' => $this->faker->randomElement(DocumentDiscountMethod::class),
55 52
             'discount_computation' => AdjustmentComputation::Percentage,
@@ -78,6 +75,9 @@ class BillFactory extends Factory
78 75
     public function withLineItems(int $count = 3): static
79 76
     {
80 77
         return $this->afterCreating(function (Bill $bill) use ($count) {
78
+            // Clear existing line items first
79
+            $bill->lineItems()->delete();
80
+
81 81
             DocumentLineItem::factory()
82 82
                 ->count($count)
83 83
                 ->forBill($bill)
@@ -90,36 +90,23 @@ class BillFactory extends Factory
90 90
     public function initialized(): static
91 91
     {
92 92
         return $this->afterCreating(function (Bill $bill) {
93
-            $this->ensureLineItems($bill);
94
-
95
-            if ($bill->wasInitialized()) {
96
-                return;
97
-            }
98
-
99
-            $postedAt = Carbon::parse($bill->date)
100
-                ->addHours($this->faker->numberBetween(1, 24));
101
-
102
-            $bill->createInitialTransaction($postedAt);
93
+            $this->performInitialization($bill);
103 94
         });
104 95
     }
105 96
 
106 97
     public function partial(int $maxPayments = 4): static
107 98
     {
108 99
         return $this->afterCreating(function (Bill $bill) use ($maxPayments) {
109
-            $this->ensureInitialized($bill);
110
-
111
-            $this->withPayments(max: $maxPayments, billStatus: BillStatus::Partial)
112
-                ->callAfterCreating(collect([$bill]));
100
+            $this->performInitialization($bill);
101
+            $this->performPayments($bill, $maxPayments, BillStatus::Partial);
113 102
         });
114 103
     }
115 104
 
116 105
     public function paid(int $maxPayments = 4): static
117 106
     {
118 107
         return $this->afterCreating(function (Bill $bill) use ($maxPayments) {
119
-            $this->ensureInitialized($bill);
120
-
121
-            $this->withPayments(max: $maxPayments)
122
-                ->callAfterCreating(collect([$bill]));
108
+            $this->performInitialization($bill);
109
+            $this->performPayments($bill, $maxPayments, BillStatus::Paid);
123 110
         });
124 111
     }
125 112
 
@@ -130,75 +117,99 @@ class BillFactory extends Factory
130 117
                 'due_date' => now()->subDays($this->faker->numberBetween(1, 30)),
131 118
             ])
132 119
             ->afterCreating(function (Bill $bill) {
133
-                $this->ensureInitialized($bill);
120
+                $this->performInitialization($bill);
134 121
             });
135 122
     }
136 123
 
137
-    public function withPayments(?int $min = null, ?int $max = null, BillStatus $billStatus = BillStatus::Paid): static
124
+    protected function performInitialization(Bill $bill): void
138 125
     {
139
-        $min ??= 1;
126
+        if ($bill->wasInitialized()) {
127
+            return;
128
+        }
140 129
 
141
-        return $this->afterCreating(function (Bill $bill) use ($billStatus, $max, $min) {
142
-            $this->ensureInitialized($bill);
130
+        $postedAt = Carbon::parse($bill->date)
131
+            ->addHours($this->faker->numberBetween(1, 24));
143 132
 
144
-            $bill->refresh();
133
+        if ($postedAt->isAfter(now())) {
134
+            $postedAt = Carbon::parse($this->faker->dateTimeBetween($bill->date, now()));
135
+        }
145 136
 
146
-            $amountDue = $bill->getRawOriginal('amount_due');
137
+        $bill->createInitialTransaction($postedAt);
138
+    }
147 139
 
148
-            $totalAmountDue = match ($billStatus) {
149
-                BillStatus::Partial => (int) floor($amountDue * 0.5),
150
-                default => $amountDue,
151
-            };
140
+    protected function performPayments(Bill $bill, int $maxPayments, BillStatus $billStatus): void
141
+    {
142
+        $bill->refresh();
152 143
 
153
-            if ($totalAmountDue <= 0 || empty($totalAmountDue)) {
154
-                return;
155
-            }
144
+        $amountDue = $bill->getRawOriginal('amount_due');
156 145
 
157
-            $paymentCount = $max && $min ? $this->faker->numberBetween($min, $max) : $min;
158
-            $paymentAmount = (int) floor($totalAmountDue / $paymentCount);
159
-            $remainingAmount = $totalAmountDue;
146
+        $totalAmountDue = match ($billStatus) {
147
+            BillStatus::Partial => (int) floor($amountDue * 0.5),
148
+            default => $amountDue,
149
+        };
160 150
 
161
-            $paymentDate = Carbon::parse($bill->initialTransaction->posted_at);
162
-            $paymentDates = [];
151
+        if ($totalAmountDue <= 0 || empty($totalAmountDue)) {
152
+            return;
153
+        }
163 154
 
164
-            for ($i = 0; $i < $paymentCount; $i++) {
165
-                $amount = $i === $paymentCount - 1 ? $remainingAmount : $paymentAmount;
155
+        $paymentCount = $this->faker->numberBetween(1, $maxPayments);
156
+        $paymentAmount = (int) floor($totalAmountDue / $paymentCount);
157
+        $remainingAmount = $totalAmountDue;
166 158
 
167
-                if ($amount <= 0) {
168
-                    break;
169
-                }
159
+        $initialPaymentDate = Carbon::parse($bill->initialTransaction->posted_at);
160
+        $maxPaymentDate = now();
161
+
162
+        $paymentDates = [];
170 163
 
171
-                $postedAt = $paymentDate->copy()->addDays($this->faker->numberBetween(1, 30));
172
-                $paymentDates[] = $postedAt;
164
+        for ($i = 0; $i < $paymentCount; $i++) {
165
+            $amount = $i === $paymentCount - 1 ? $remainingAmount : $paymentAmount;
173 166
 
174
-                $data = [
175
-                    'posted_at' => $postedAt,
176
-                    'amount' => $amount,
177
-                    'payment_method' => $this->faker->randomElement(PaymentMethod::class),
178
-                    'bank_account_id' => BankAccount::where('company_id', $bill->company_id)->inRandomOrder()->value('id'),
179
-                    'notes' => $this->faker->sentence,
180
-                ];
167
+            if ($amount <= 0) {
168
+                break;
169
+            }
181 170
 
182
-                $bill->recordPayment($data);
183
-                $remainingAmount -= $amount;
171
+            if ($i === 0) {
172
+                $postedAt = $initialPaymentDate->copy()->addDays($this->faker->numberBetween(1, 15));
173
+            } else {
174
+                $postedAt = $paymentDates[$i - 1]->copy()->addDays($this->faker->numberBetween(1, 10));
184 175
             }
185 176
 
186
-            if ($billStatus !== BillStatus::Paid) {
187
-                return;
177
+            if ($postedAt->isAfter($maxPaymentDate)) {
178
+                $postedAt = Carbon::parse($this->faker->dateTimeBetween($initialPaymentDate, $maxPaymentDate));
188 179
             }
189 180
 
181
+            $paymentDates[] = $postedAt;
182
+
183
+            $data = [
184
+                'posted_at' => $postedAt,
185
+                'amount' => $amount,
186
+                'payment_method' => $this->faker->randomElement(PaymentMethod::class),
187
+                'bank_account_id' => BankAccount::where('company_id', $bill->company_id)->inRandomOrder()->value('id'),
188
+                'notes' => $this->faker->sentence,
189
+            ];
190
+
191
+            $bill->recordPayment($data);
192
+            $remainingAmount -= $amount;
193
+        }
194
+
195
+        if ($billStatus === BillStatus::Paid && ! empty($paymentDates)) {
190 196
             $latestPaymentDate = max($paymentDates);
191 197
             $bill->updateQuietly([
192 198
                 'status' => $billStatus,
193 199
                 'paid_at' => $latestPaymentDate,
194 200
             ]);
195
-        });
201
+        }
196 202
     }
197 203
 
198 204
     public function configure(): static
199 205
     {
200 206
         return $this->afterCreating(function (Bill $bill) {
201
-            $this->ensureInitialized($bill);
207
+            DocumentLineItem::factory()
208
+                ->count(3)
209
+                ->forBill($bill)
210
+                ->create();
211
+
212
+            $this->recalculateTotals($bill);
202 213
 
203 214
             $number = DocumentDefault::getBaseNumber() + $bill->id;
204 215
 
@@ -215,20 +226,6 @@ class BillFactory extends Factory
215 226
         });
216 227
     }
217 228
 
218
-    protected function ensureLineItems(Bill $bill): void
219
-    {
220
-        if (! $bill->hasLineItems()) {
221
-            $this->withLineItems()->callAfterCreating(collect([$bill]));
222
-        }
223
-    }
224
-
225
-    protected function ensureInitialized(Bill $bill): void
226
-    {
227
-        if (! $bill->wasInitialized()) {
228
-            $this->initialized()->callAfterCreating(collect([$bill]));
229
-        }
230
-    }
231
-
232 229
     protected function recalculateTotals(Bill $bill): void
233 230
     {
234 231
         $bill->refresh();

+ 81
- 46
database/factories/Accounting/EstimateFactory.php Voir le fichier

@@ -31,17 +31,22 @@ class EstimateFactory extends Factory
31 31
      */
32 32
     public function definition(): array
33 33
     {
34
-        $estimateDate = $this->faker->dateTimeBetween('-1 year');
34
+        $estimateDate = $this->faker->dateTimeBetween('-2 months', '-1 day');
35 35
 
36 36
         return [
37 37
             'company_id' => 1,
38
-            'client_id' => fn (array $attributes) => Client::where('company_id', $attributes['company_id'])->inRandomOrder()->value('id'),
38
+            'client_id' => function (array $attributes) {
39
+                return Client::where('company_id', $attributes['company_id'])->inRandomOrder()->value('id')
40
+                    ?? Client::factory()->state([
41
+                        'company_id' => $attributes['company_id'],
42
+                    ]);
43
+            },
39 44
             'header' => 'Estimate',
40 45
             'subheader' => 'Estimate',
41 46
             'estimate_number' => $this->faker->unique()->numerify('EST-####'),
42 47
             'reference_number' => $this->faker->unique()->numerify('REF-####'),
43 48
             'date' => $estimateDate,
44
-            'expiration_date' => Carbon::parse($estimateDate)->addDays($this->faker->numberBetween(14, 30)),
49
+            'expiration_date' => $this->faker->dateTimeInInterval($estimateDate, '+3 months'),
45 50
             'status' => EstimateStatus::Draft,
46 51
             'discount_method' => $this->faker->randomElement(DocumentDiscountMethod::class),
47 52
             'discount_computation' => AdjustmentComputation::Percentage,
@@ -71,6 +76,9 @@ class EstimateFactory extends Factory
71 76
     public function withLineItems(int $count = 3): static
72 77
     {
73 78
         return $this->afterCreating(function (Estimate $estimate) use ($count) {
79
+            // Clear existing line items first
80
+            $estimate->lineItems()->delete();
81
+
74 82
             DocumentLineItem::factory()
75 83
                 ->count($count)
76 84
                 ->forEstimate($estimate)
@@ -83,27 +91,22 @@ class EstimateFactory extends Factory
83 91
     public function approved(): static
84 92
     {
85 93
         return $this->afterCreating(function (Estimate $estimate) {
86
-            $this->ensureLineItems($estimate);
87
-
88
-            if (! $estimate->canBeApproved()) {
89
-                return;
90
-            }
91
-
92
-            $approvedAt = Carbon::parse($estimate->date)
93
-                ->addHours($this->faker->numberBetween(1, 24));
94
-
95
-            $estimate->approveDraft($approvedAt);
94
+            $this->performApproval($estimate);
96 95
         });
97 96
     }
98 97
 
99 98
     public function accepted(): static
100 99
     {
101 100
         return $this->afterCreating(function (Estimate $estimate) {
102
-            $this->ensureSent($estimate);
101
+            $this->performSent($estimate);
103 102
 
104 103
             $acceptedAt = Carbon::parse($estimate->last_sent_at)
105 104
                 ->addDays($this->faker->numberBetween(1, 7));
106 105
 
106
+            if ($acceptedAt->isAfter(now())) {
107
+                $acceptedAt = Carbon::parse($this->faker->dateTimeBetween($estimate->last_sent_at, now()));
108
+            }
109
+
107 110
             $estimate->markAsAccepted($acceptedAt);
108 111
         });
109 112
     }
@@ -112,12 +115,25 @@ class EstimateFactory extends Factory
112 115
     {
113 116
         return $this->afterCreating(function (Estimate $estimate) {
114 117
             if (! $estimate->wasAccepted()) {
115
-                $this->accepted()->callAfterCreating(collect([$estimate]));
118
+                $this->performSent($estimate);
119
+
120
+                $acceptedAt = Carbon::parse($estimate->last_sent_at)
121
+                    ->addDays($this->faker->numberBetween(1, 7));
122
+
123
+                if ($acceptedAt->isAfter(now())) {
124
+                    $acceptedAt = Carbon::parse($this->faker->dateTimeBetween($estimate->last_sent_at, now()));
125
+                }
126
+
127
+                $estimate->markAsAccepted($acceptedAt);
116 128
             }
117 129
 
118 130
             $convertedAt = Carbon::parse($estimate->accepted_at)
119 131
                 ->addDays($this->faker->numberBetween(1, 7));
120 132
 
133
+            if ($convertedAt->isAfter(now())) {
134
+                $convertedAt = Carbon::parse($this->faker->dateTimeBetween($estimate->accepted_at, now()));
135
+            }
136
+
121 137
             $estimate->convertToInvoice($convertedAt);
122 138
         });
123 139
     }
@@ -125,11 +141,15 @@ class EstimateFactory extends Factory
125 141
     public function declined(): static
126 142
     {
127 143
         return $this->afterCreating(function (Estimate $estimate) {
128
-            $this->ensureSent($estimate);
144
+            $this->performSent($estimate);
129 145
 
130 146
             $declinedAt = Carbon::parse($estimate->last_sent_at)
131 147
                 ->addDays($this->faker->numberBetween(1, 7));
132 148
 
149
+            if ($declinedAt->isAfter(now())) {
150
+                $declinedAt = Carbon::parse($this->faker->dateTimeBetween($estimate->last_sent_at, now()));
151
+            }
152
+
133 153
             $estimate->markAsDeclined($declinedAt);
134 154
         });
135 155
     }
@@ -137,23 +157,22 @@ class EstimateFactory extends Factory
137 157
     public function sent(): static
138 158
     {
139 159
         return $this->afterCreating(function (Estimate $estimate) {
140
-            $this->ensureApproved($estimate);
141
-
142
-            $sentAt = Carbon::parse($estimate->approved_at)
143
-                ->addHours($this->faker->numberBetween(1, 24));
144
-
145
-            $estimate->markAsSent($sentAt);
160
+            $this->performSent($estimate);
146 161
         });
147 162
     }
148 163
 
149 164
     public function viewed(): static
150 165
     {
151 166
         return $this->afterCreating(function (Estimate $estimate) {
152
-            $this->ensureSent($estimate);
167
+            $this->performSent($estimate);
153 168
 
154 169
             $viewedAt = Carbon::parse($estimate->last_sent_at)
155 170
                 ->addHours($this->faker->numberBetween(1, 24));
156 171
 
172
+            if ($viewedAt->isAfter(now())) {
173
+                $viewedAt = Carbon::parse($this->faker->dateTimeBetween($estimate->last_sent_at, now()));
174
+            }
175
+
157 176
             $estimate->markAsViewed($viewedAt);
158 177
         });
159 178
     }
@@ -165,14 +184,51 @@ class EstimateFactory extends Factory
165 184
                 'expiration_date' => now()->subDays($this->faker->numberBetween(1, 30)),
166 185
             ])
167 186
             ->afterCreating(function (Estimate $estimate) {
168
-                $this->ensureApproved($estimate);
187
+                $this->performApproval($estimate);
169 188
             });
170 189
     }
171 190
 
191
+    protected function performApproval(Estimate $estimate): void
192
+    {
193
+        if (! $estimate->canBeApproved()) {
194
+            throw new \InvalidArgumentException('Estimate cannot be approved. Current status: ' . $estimate->status->value);
195
+        }
196
+
197
+        $approvedAt = Carbon::parse($estimate->date)
198
+            ->addHours($this->faker->numberBetween(1, 24));
199
+
200
+        if ($approvedAt->isAfter(now())) {
201
+            $approvedAt = Carbon::parse($this->faker->dateTimeBetween($estimate->date, now()));
202
+        }
203
+
204
+        $estimate->approveDraft($approvedAt);
205
+    }
206
+
207
+    protected function performSent(Estimate $estimate): void
208
+    {
209
+        if (! $estimate->wasApproved()) {
210
+            $this->performApproval($estimate);
211
+        }
212
+
213
+        $sentAt = Carbon::parse($estimate->approved_at)
214
+            ->addHours($this->faker->numberBetween(1, 24));
215
+
216
+        if ($sentAt->isAfter(now())) {
217
+            $sentAt = Carbon::parse($this->faker->dateTimeBetween($estimate->approved_at, now()));
218
+        }
219
+
220
+        $estimate->markAsSent($sentAt);
221
+    }
222
+
172 223
     public function configure(): static
173 224
     {
174 225
         return $this->afterCreating(function (Estimate $estimate) {
175
-            $this->ensureLineItems($estimate);
226
+            DocumentLineItem::factory()
227
+                ->count(3)
228
+                ->forEstimate($estimate)
229
+                ->create();
230
+
231
+            $this->recalculateTotals($estimate);
176 232
 
177 233
             $number = DocumentDefault::getBaseNumber() + $estimate->id;
178 234
 
@@ -189,27 +245,6 @@ class EstimateFactory extends Factory
189 245
         });
190 246
     }
191 247
 
192
-    protected function ensureLineItems(Estimate $estimate): void
193
-    {
194
-        if (! $estimate->hasLineItems()) {
195
-            $this->withLineItems()->callAfterCreating(collect([$estimate]));
196
-        }
197
-    }
198
-
199
-    protected function ensureApproved(Estimate $estimate): void
200
-    {
201
-        if (! $estimate->wasApproved()) {
202
-            $this->approved()->callAfterCreating(collect([$estimate]));
203
-        }
204
-    }
205
-
206
-    protected function ensureSent(Estimate $estimate): void
207
-    {
208
-        if (! $estimate->hasBeenSent()) {
209
-            $this->sent()->callAfterCreating(collect([$estimate]));
210
-        }
211
-    }
212
-
213 248
     protected function recalculateTotals(Estimate $estimate): void
214 249
     {
215 250
         $estimate->refresh();

+ 31
- 7
database/factories/Accounting/InvoiceFactory.php Voir le fichier

@@ -34,7 +34,7 @@ class InvoiceFactory extends Factory
34 34
      */
35 35
     public function definition(): array
36 36
     {
37
-        $invoiceDate = $this->faker->dateTimeBetween('-2 months');
37
+        $invoiceDate = $this->faker->dateTimeBetween('-2 months', '-1 day');
38 38
 
39 39
         return [
40 40
             'company_id' => 1,
@@ -79,6 +79,8 @@ class InvoiceFactory extends Factory
79 79
     public function withLineItems(int $count = 3): static
80 80
     {
81 81
         return $this->afterCreating(function (Invoice $invoice) use ($count) {
82
+            $invoice->lineItems()->delete();
83
+
82 84
             DocumentLineItem::factory()
83 85
                 ->count($count)
84 86
                 ->forInvoice($invoice)
@@ -139,10 +141,6 @@ class InvoiceFactory extends Factory
139 141
 
140 142
     protected function performApproval(Invoice $invoice): void
141 143
     {
142
-        if (! $invoice->hasLineItems()) {
143
-            throw new \InvalidArgumentException('Cannot approve invoice without line items. Use withLineItems() first.');
144
-        }
145
-
146 144
         if (! $invoice->canBeApproved()) {
147 145
             throw new \InvalidArgumentException('Invoice cannot be approved. Current status: ' . $invoice->status->value);
148 146
         }
@@ -150,6 +148,10 @@ class InvoiceFactory extends Factory
150 148
         $approvedAt = Carbon::parse($invoice->date)
151 149
             ->addHours($this->faker->numberBetween(1, 24));
152 150
 
151
+        if ($approvedAt->isAfter(now())) {
152
+            $approvedAt = Carbon::parse($this->faker->dateTimeBetween($invoice->date, now()));
153
+        }
154
+
153 155
         $invoice->approveDraft($approvedAt);
154 156
     }
155 157
 
@@ -162,6 +164,10 @@ class InvoiceFactory extends Factory
162 164
         $sentAt = Carbon::parse($invoice->approved_at)
163 165
             ->addHours($this->faker->numberBetween(1, 24));
164 166
 
167
+        if ($sentAt->isAfter(now())) {
168
+            $sentAt = Carbon::parse($this->faker->dateTimeBetween($invoice->approved_at, now()));
169
+        }
170
+
165 171
         $invoice->markAsSent($sentAt);
166 172
     }
167 173
 
@@ -188,7 +194,9 @@ class InvoiceFactory extends Factory
188 194
         $paymentAmount = (int) floor($totalAmountDue / $paymentCount);
189 195
         $remainingAmount = $totalAmountDue;
190 196
 
191
-        $paymentDate = Carbon::parse($invoice->approved_at);
197
+        $initialPaymentDate = Carbon::parse($invoice->approved_at);
198
+        $maxPaymentDate = now();
199
+
192 200
         $paymentDates = [];
193 201
 
194 202
         for ($i = 0; $i < $paymentCount; $i++) {
@@ -198,7 +206,16 @@ class InvoiceFactory extends Factory
198 206
                 break;
199 207
             }
200 208
 
201
-            $postedAt = $paymentDate->copy()->addDays($this->faker->numberBetween(1, 30));
209
+            if ($i === 0) {
210
+                $postedAt = $initialPaymentDate->copy()->addDays($this->faker->numberBetween(1, 15));
211
+            } else {
212
+                $postedAt = $paymentDates[$i - 1]->copy()->addDays($this->faker->numberBetween(1, 10));
213
+            }
214
+
215
+            if ($postedAt->isAfter($maxPaymentDate)) {
216
+                $postedAt = Carbon::parse($this->faker->dateTimeBetween($initialPaymentDate, $maxPaymentDate));
217
+            }
218
+
202 219
             $paymentDates[] = $postedAt;
203 220
 
204 221
             $data = [
@@ -225,6 +242,13 @@ class InvoiceFactory extends Factory
225 242
     public function configure(): static
226 243
     {
227 244
         return $this->afterCreating(function (Invoice $invoice) {
245
+            DocumentLineItem::factory()
246
+                ->count(3)
247
+                ->forInvoice($invoice)
248
+                ->create();
249
+
250
+            $this->recalculateTotals($invoice);
251
+
228 252
             $number = DocumentDefault::getBaseNumber() + $invoice->id;
229 253
 
230 254
             $invoice->updateQuietly([

+ 158
- 57
database/factories/Accounting/RecurringInvoiceFactory.php Voir le fichier

@@ -39,7 +39,12 @@ class RecurringInvoiceFactory extends Factory
39 39
     {
40 40
         return [
41 41
             'company_id' => 1,
42
-            'client_id' => fn (array $attributes) => Client::where('company_id', $attributes['company_id'])->inRandomOrder()->value('id'),
42
+            'client_id' => function (array $attributes) {
43
+                return Client::where('company_id', $attributes['company_id'])->inRandomOrder()->value('id')
44
+                    ?? Client::factory()->state([
45
+                        'company_id' => $attributes['company_id'],
46
+                    ]);
47
+            },
43 48
             'header' => 'Invoice',
44 49
             'subheader' => 'Invoice',
45 50
             'order_number' => $this->faker->unique()->numerify('ORD-####'),
@@ -73,6 +78,9 @@ class RecurringInvoiceFactory extends Factory
73 78
     public function withLineItems(int $count = 3): static
74 79
     {
75 80
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($count) {
81
+            // Clear existing line items first
82
+            $recurringInvoice->lineItems()->delete();
83
+
76 84
             DocumentLineItem::factory()
77 85
                 ->count($count)
78 86
                 ->forInvoice($recurringInvoice)
@@ -82,40 +90,31 @@ class RecurringInvoiceFactory extends Factory
82 90
         });
83 91
     }
84 92
 
93
+    public function configure(): static
94
+    {
95
+        return $this->afterCreating(function (RecurringInvoice $recurringInvoice) {
96
+            DocumentLineItem::factory()
97
+                ->count(3)
98
+                ->forInvoice($recurringInvoice)
99
+                ->create();
100
+
101
+            $this->recalculateTotals($recurringInvoice);
102
+        });
103
+    }
104
+
85 105
     public function withSchedule(
86 106
         ?Frequency $frequency = null,
87 107
         ?Carbon $startDate = null,
88 108
         ?EndType $endType = null
89 109
     ): static {
90
-        return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($frequency, $endType, $startDate) {
91
-            $this->ensureLineItems($recurringInvoice);
92
-
93
-            $frequency ??= $this->faker->randomElement(Frequency::class);
94
-            $endType ??= EndType::Never;
95
-
96
-            // Adjust the start date range based on frequency
97
-            $startDate = match ($frequency) {
98
-                Frequency::Daily => Carbon::parse($this->faker->dateTimeBetween('-30 days')), // At most 30 days back
99
-                default => $startDate ?? Carbon::parse($this->faker->dateTimeBetween('-1 year')),
100
-            };
101
-
102
-            $state = match ($frequency) {
103
-                Frequency::Daily => $this->withDailySchedule($startDate, $endType),
104
-                Frequency::Weekly => $this->withWeeklySchedule($startDate, $endType),
105
-                Frequency::Monthly => $this->withMonthlySchedule($startDate, $endType),
106
-                Frequency::Yearly => $this->withYearlySchedule($startDate, $endType),
107
-                Frequency::Custom => $this->withCustomSchedule($startDate, $endType),
108
-            };
109
-
110
-            $state->callAfterCreating(collect([$recurringInvoice]));
110
+        return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($frequency, $startDate, $endType) {
111
+            $this->performScheduleSetup($recurringInvoice, $frequency, $startDate, $endType);
111 112
         });
112 113
     }
113 114
 
114 115
     public function withDailySchedule(Carbon $startDate, EndType $endType): static
115 116
     {
116 117
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
117
-            $this->ensureLineItems($recurringInvoice);
118
-
119 118
             $recurringInvoice->updateQuietly([
120 119
                 'frequency' => Frequency::Daily,
121 120
                 'start_date' => $startDate,
@@ -127,8 +126,6 @@ class RecurringInvoiceFactory extends Factory
127 126
     public function withWeeklySchedule(Carbon $startDate, EndType $endType): static
128 127
     {
129 128
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
130
-            $this->ensureLineItems($recurringInvoice);
131
-
132 129
             $recurringInvoice->updateQuietly([
133 130
                 'frequency' => Frequency::Weekly,
134 131
                 'day_of_week' => DayOfWeek::from($startDate->dayOfWeek),
@@ -141,8 +138,6 @@ class RecurringInvoiceFactory extends Factory
141 138
     public function withMonthlySchedule(Carbon $startDate, EndType $endType): static
142 139
     {
143 140
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
144
-            $this->ensureLineItems($recurringInvoice);
145
-
146 141
             $recurringInvoice->updateQuietly([
147 142
                 'frequency' => Frequency::Monthly,
148 143
                 'day_of_month' => DayOfMonth::from($startDate->day),
@@ -155,8 +150,6 @@ class RecurringInvoiceFactory extends Factory
155 150
     public function withYearlySchedule(Carbon $startDate, EndType $endType): static
156 151
     {
157 152
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
158
-            $this->ensureLineItems($recurringInvoice);
159
-
160 153
             $recurringInvoice->updateQuietly([
161 154
                 'frequency' => Frequency::Yearly,
162 155
                 'month' => Month::from($startDate->month),
@@ -174,8 +167,6 @@ class RecurringInvoiceFactory extends Factory
174 167
         ?int $intervalValue = null
175 168
     ): static {
176 169
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($intervalType, $intervalValue, $startDate, $endType) {
177
-            $this->ensureLineItems($recurringInvoice);
178
-
179 170
             $intervalType ??= $this->faker->randomElement(IntervalType::class);
180 171
             $intervalValue ??= match ($intervalType) {
181 172
                 IntervalType::Day => $this->faker->numberBetween(1, 7),
@@ -215,7 +206,7 @@ class RecurringInvoiceFactory extends Factory
215 206
                     break;
216 207
             }
217 208
 
218
-            return $recurringInvoice->updateQuietly($state);
209
+            $recurringInvoice->updateQuietly($state);
219 210
         });
220 211
     }
221 212
 
@@ -248,35 +239,32 @@ class RecurringInvoiceFactory extends Factory
248 239
     public function approved(): static
249 240
     {
250 241
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) {
251
-            $this->ensureLineItems($recurringInvoice);
242
+            $this->performApproval($recurringInvoice);
243
+        });
244
+    }
252 245
 
246
+    public function active(): static
247
+    {
248
+        return $this->afterCreating(function (RecurringInvoice $recurringInvoice) {
253 249
             if (! $recurringInvoice->hasSchedule()) {
254
-                $this->withSchedule()->callAfterCreating(collect([$recurringInvoice]));
250
+                $this->performScheduleSetup($recurringInvoice);
255 251
                 $recurringInvoice->refresh();
256 252
             }
257 253
 
258
-            $approvedAt = $recurringInvoice->start_date
259
-                ? $recurringInvoice->start_date->copy()->subDays($this->faker->numberBetween(1, 7))
260
-                : now()->subDays($this->faker->numberBetween(1, 30));
261
-
262
-            $recurringInvoice->approveDraft($approvedAt);
254
+            $this->performApproval($recurringInvoice);
263 255
         });
264 256
     }
265 257
 
266
-    public function active(): static
267
-    {
268
-        return $this->withLineItems()
269
-            ->withSchedule()
270
-            ->approved();
271
-    }
272
-
273 258
     public function ended(): static
274 259
     {
275 260
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) {
276
-            $this->ensureLineItems($recurringInvoice);
277
-
278 261
             if (! $recurringInvoice->canBeEnded()) {
279
-                $this->active()->callAfterCreating(collect([$recurringInvoice]));
262
+                if (! $recurringInvoice->hasSchedule()) {
263
+                    $this->performScheduleSetup($recurringInvoice);
264
+                    $recurringInvoice->refresh();
265
+                }
266
+
267
+                $this->performApproval($recurringInvoice);
280 268
             }
281 269
 
282 270
             $endedAt = $recurringInvoice->last_date
@@ -290,18 +278,131 @@ class RecurringInvoiceFactory extends Factory
290 278
         });
291 279
     }
292 280
 
293
-    public function configure(): static
281
+    protected function performScheduleSetup(
282
+        RecurringInvoice $recurringInvoice,
283
+        ?Frequency $frequency = null,
284
+        ?Carbon $startDate = null,
285
+        ?EndType $endType = null
286
+    ): void {
287
+        $frequency ??= $this->faker->randomElement(Frequency::class);
288
+        $endType ??= EndType::Never;
289
+
290
+        // Adjust the start date range based on frequency
291
+        $startDate = match ($frequency) {
292
+            Frequency::Daily => Carbon::parse($this->faker->dateTimeBetween('-30 days')), // At most 30 days back
293
+            default => $startDate ?? Carbon::parse($this->faker->dateTimeBetween('-1 year')),
294
+        };
295
+
296
+        match ($frequency) {
297
+            Frequency::Daily => $this->performDailySchedule($recurringInvoice, $startDate, $endType),
298
+            Frequency::Weekly => $this->performWeeklySchedule($recurringInvoice, $startDate, $endType),
299
+            Frequency::Monthly => $this->performMonthlySchedule($recurringInvoice, $startDate, $endType),
300
+            Frequency::Yearly => $this->performYearlySchedule($recurringInvoice, $startDate, $endType),
301
+            Frequency::Custom => $this->performCustomSchedule($recurringInvoice, $startDate, $endType),
302
+        };
303
+    }
304
+
305
+    protected function performDailySchedule(RecurringInvoice $recurringInvoice, Carbon $startDate, EndType $endType): void
294 306
     {
295
-        return $this->afterCreating(function (RecurringInvoice $recurringInvoice) {
296
-            $this->ensureLineItems($recurringInvoice);
297
-        });
307
+        $recurringInvoice->updateQuietly([
308
+            'frequency' => Frequency::Daily,
309
+            'start_date' => $startDate,
310
+            'end_type' => $endType,
311
+        ]);
298 312
     }
299 313
 
300
-    protected function ensureLineItems(RecurringInvoice $recurringInvoice): void
314
+    protected function performWeeklySchedule(RecurringInvoice $recurringInvoice, Carbon $startDate, EndType $endType): void
301 315
     {
302
-        if (! $recurringInvoice->hasLineItems()) {
303
-            $this->withLineItems()->callAfterCreating(collect([$recurringInvoice]));
316
+        $recurringInvoice->updateQuietly([
317
+            'frequency' => Frequency::Weekly,
318
+            'day_of_week' => DayOfWeek::from($startDate->dayOfWeek),
319
+            'start_date' => $startDate,
320
+            'end_type' => $endType,
321
+        ]);
322
+    }
323
+
324
+    protected function performMonthlySchedule(RecurringInvoice $recurringInvoice, Carbon $startDate, EndType $endType): void
325
+    {
326
+        $recurringInvoice->updateQuietly([
327
+            'frequency' => Frequency::Monthly,
328
+            'day_of_month' => DayOfMonth::from($startDate->day),
329
+            'start_date' => $startDate,
330
+            'end_type' => $endType,
331
+        ]);
332
+    }
333
+
334
+    protected function performYearlySchedule(RecurringInvoice $recurringInvoice, Carbon $startDate, EndType $endType): void
335
+    {
336
+        $recurringInvoice->updateQuietly([
337
+            'frequency' => Frequency::Yearly,
338
+            'month' => Month::from($startDate->month),
339
+            'day_of_month' => DayOfMonth::from($startDate->day),
340
+            'start_date' => $startDate,
341
+            'end_type' => $endType,
342
+        ]);
343
+    }
344
+
345
+    protected function performCustomSchedule(
346
+        RecurringInvoice $recurringInvoice,
347
+        Carbon $startDate,
348
+        EndType $endType,
349
+        ?IntervalType $intervalType = null,
350
+        ?int $intervalValue = null
351
+    ): void {
352
+        $intervalType ??= $this->faker->randomElement(IntervalType::class);
353
+        $intervalValue ??= match ($intervalType) {
354
+            IntervalType::Day => $this->faker->numberBetween(1, 7),
355
+            IntervalType::Week => $this->faker->numberBetween(1, 4),
356
+            IntervalType::Month => $this->faker->numberBetween(1, 3),
357
+            IntervalType::Year => 1,
358
+        };
359
+
360
+        $state = [
361
+            'frequency' => Frequency::Custom,
362
+            'interval_type' => $intervalType,
363
+            'interval_value' => $intervalValue,
364
+            'start_date' => $startDate,
365
+            'end_type' => $endType,
366
+        ];
367
+
368
+        // Add interval-specific attributes
369
+        switch ($intervalType) {
370
+            case IntervalType::Day:
371
+                // No additional attributes needed
372
+                break;
373
+
374
+            case IntervalType::Week:
375
+                $state['day_of_week'] = DayOfWeek::from($startDate->dayOfWeek);
376
+
377
+                break;
378
+
379
+            case IntervalType::Month:
380
+                $state['day_of_month'] = DayOfMonth::from($startDate->day);
381
+
382
+                break;
383
+
384
+            case IntervalType::Year:
385
+                $state['month'] = Month::from($startDate->month);
386
+                $state['day_of_month'] = DayOfMonth::from($startDate->day);
387
+
388
+                break;
304 389
         }
390
+
391
+        $recurringInvoice->updateQuietly($state);
392
+    }
393
+
394
+    protected function performApproval(RecurringInvoice $recurringInvoice): void
395
+    {
396
+        if (! $recurringInvoice->hasSchedule()) {
397
+            $this->performScheduleSetup($recurringInvoice);
398
+            $recurringInvoice->refresh();
399
+        }
400
+
401
+        $approvedAt = $recurringInvoice->start_date
402
+            ? $recurringInvoice->start_date->copy()->subDays($this->faker->numberBetween(1, 7))
403
+            : now()->subDays($this->faker->numberBetween(1, 30));
404
+
405
+        $recurringInvoice->approveDraft($approvedAt);
305 406
     }
306 407
 
307 408
     protected function recalculateTotals(RecurringInvoice $recurringInvoice): void

+ 1
- 1
database/factories/Common/OfferingFactory.php Voir le fichier

@@ -33,7 +33,7 @@ class OfferingFactory extends Factory
33 33
             'name' => $this->faker->words(3, true),
34 34
             'description' => $this->faker->sentence,
35 35
             'type' => $this->faker->randomElement(OfferingType::cases()),
36
-            'price' => $this->faker->numberBetween(5, 1000),
36
+            'price' => $this->faker->numberBetween(500, 50000), // $5.00 to $500.00
37 37
             'sellable' => false,
38 38
             'purchasable' => false,
39 39
             'income_account_id' => null,

+ 14
- 0
tests/Feature/Accounting/RecurringInvoiceTest.php Voir le fichier

@@ -10,6 +10,20 @@ beforeEach(function () {
10 10
     $this->withOfferings();
11 11
 });
12 12
 
13
+it('creates a basic recurring invoice with line items and calculates totals correctly', function () {
14
+    $recurringInvoice = RecurringInvoice::factory()
15
+        ->withLineItems()
16
+        ->create();
17
+
18
+    $recurringInvoice->refresh();
19
+
20
+    expect($recurringInvoice)
21
+        ->hasLineItems()->toBeTrue()
22
+        ->lineItems->count()->toBe(3)
23
+        ->subtotal->toBeGreaterThan(0)
24
+        ->total->toBeGreaterThan(0);
25
+});
26
+
13 27
 test('recurring invoice properly handles months with fewer days for monthly frequency', function () {
14 28
     // Start from January 31st
15 29
     Carbon::setTestNow('2024-01-31');

Chargement…
Annuler
Enregistrer