Andrew Wallo 4 月之前
父節點
當前提交
4242133852

+ 57
- 57
composer.lock 查看文件

497
         },
497
         },
498
         {
498
         {
499
             "name": "aws/aws-sdk-php",
499
             "name": "aws/aws-sdk-php",
500
-            "version": "3.343.22",
500
+            "version": "3.344.0",
501
             "source": {
501
             "source": {
502
                 "type": "git",
502
                 "type": "git",
503
                 "url": "https://github.com/aws/aws-sdk-php.git",
503
                 "url": "https://github.com/aws/aws-sdk-php.git",
504
-                "reference": "174cc187df3bde52c21e9c00a4e99610a08732a3"
504
+                "reference": "787a8ec6301657d9cbdb389db4fa92243c68666a"
505
             },
505
             },
506
             "dist": {
506
             "dist": {
507
                 "type": "zip",
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
                 "shasum": ""
510
                 "shasum": ""
511
             },
511
             },
512
             "require": {
512
             "require": {
588
             "support": {
588
             "support": {
589
                 "forum": "https://github.com/aws/aws-sdk-php/discussions",
589
                 "forum": "https://github.com/aws/aws-sdk-php/discussions",
590
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
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
             "name": "aws/aws-sdk-php-laravel",
596
             "name": "aws/aws-sdk-php-laravel",
1799
         },
1799
         },
1800
         {
1800
         {
1801
             "name": "filament/actions",
1801
             "name": "filament/actions",
1802
-            "version": "v3.3.17",
1802
+            "version": "v3.3.20",
1803
             "source": {
1803
             "source": {
1804
                 "type": "git",
1804
                 "type": "git",
1805
                 "url": "https://github.com/filamentphp/actions.git",
1805
                 "url": "https://github.com/filamentphp/actions.git",
1806
-                "reference": "66b509aa72fa882ce91218eb743684a9350bc3fb"
1806
+                "reference": "151f776552ee10d70591c2649708bc4b0a7cba91"
1807
             },
1807
             },
1808
             "dist": {
1808
             "dist": {
1809
                 "type": "zip",
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
                 "shasum": ""
1812
                 "shasum": ""
1813
             },
1813
             },
1814
             "require": {
1814
             "require": {
1848
                 "issues": "https://github.com/filamentphp/filament/issues",
1848
                 "issues": "https://github.com/filamentphp/filament/issues",
1849
                 "source": "https://github.com/filamentphp/filament"
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
             "name": "filament/filament",
1854
             "name": "filament/filament",
1855
-            "version": "v3.3.17",
1855
+            "version": "v3.3.20",
1856
             "source": {
1856
             "source": {
1857
                 "type": "git",
1857
                 "type": "git",
1858
                 "url": "https://github.com/filamentphp/panels.git",
1858
                 "url": "https://github.com/filamentphp/panels.git",
1917
         },
1917
         },
1918
         {
1918
         {
1919
             "name": "filament/forms",
1919
             "name": "filament/forms",
1920
-            "version": "v3.3.17",
1920
+            "version": "v3.3.20",
1921
             "source": {
1921
             "source": {
1922
                 "type": "git",
1922
                 "type": "git",
1923
                 "url": "https://github.com/filamentphp/forms.git",
1923
                 "url": "https://github.com/filamentphp/forms.git",
1924
-                "reference": "5cef64051fcb23cbbc6152baaac5da1c44ec9a63"
1924
+                "reference": "d73cdda057a4f5bd409eab9573101e73edb404cc"
1925
             },
1925
             },
1926
             "dist": {
1926
             "dist": {
1927
                 "type": "zip",
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
                 "shasum": ""
1930
                 "shasum": ""
1931
             },
1931
             },
1932
             "require": {
1932
             "require": {
1969
                 "issues": "https://github.com/filamentphp/filament/issues",
1969
                 "issues": "https://github.com/filamentphp/filament/issues",
1970
                 "source": "https://github.com/filamentphp/filament"
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
             "name": "filament/infolists",
1975
             "name": "filament/infolists",
1976
-            "version": "v3.3.17",
1976
+            "version": "v3.3.20",
1977
             "source": {
1977
             "source": {
1978
                 "type": "git",
1978
                 "type": "git",
1979
                 "url": "https://github.com/filamentphp/infolists.git",
1979
                 "url": "https://github.com/filamentphp/infolists.git",
1980
-                "reference": "cc71f1c15f132660986384d302a33a2b20618a96"
1980
+                "reference": "b54ff0fa89f654eca1c14edfd41a7e16ccb8165d"
1981
             },
1981
             },
1982
             "dist": {
1982
             "dist": {
1983
                 "type": "zip",
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
                 "shasum": ""
1986
                 "shasum": ""
1987
             },
1987
             },
1988
             "require": {
1988
             "require": {
2020
                 "issues": "https://github.com/filamentphp/filament/issues",
2020
                 "issues": "https://github.com/filamentphp/filament/issues",
2021
                 "source": "https://github.com/filamentphp/filament"
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
             "name": "filament/notifications",
2026
             "name": "filament/notifications",
2027
-            "version": "v3.3.17",
2027
+            "version": "v3.3.20",
2028
             "source": {
2028
             "source": {
2029
                 "type": "git",
2029
                 "type": "git",
2030
                 "url": "https://github.com/filamentphp/notifications.git",
2030
                 "url": "https://github.com/filamentphp/notifications.git",
2076
         },
2076
         },
2077
         {
2077
         {
2078
             "name": "filament/support",
2078
             "name": "filament/support",
2079
-            "version": "v3.3.17",
2079
+            "version": "v3.3.20",
2080
             "source": {
2080
             "source": {
2081
                 "type": "git",
2081
                 "type": "git",
2082
                 "url": "https://github.com/filamentphp/support.git",
2082
                 "url": "https://github.com/filamentphp/support.git",
2083
-                "reference": "537663fa2c5057aa236a189255b623b5124d32d8"
2083
+                "reference": "4f9793ad3339301ca53ea6f2c984734f7ac38ce7"
2084
             },
2084
             },
2085
             "dist": {
2085
             "dist": {
2086
                 "type": "zip",
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
                 "shasum": ""
2089
                 "shasum": ""
2090
             },
2090
             },
2091
             "require": {
2091
             "require": {
2131
                 "issues": "https://github.com/filamentphp/filament/issues",
2131
                 "issues": "https://github.com/filamentphp/filament/issues",
2132
                 "source": "https://github.com/filamentphp/filament"
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
             "name": "filament/tables",
2137
             "name": "filament/tables",
2138
-            "version": "v3.3.17",
2138
+            "version": "v3.3.20",
2139
             "source": {
2139
             "source": {
2140
                 "type": "git",
2140
                 "type": "git",
2141
                 "url": "https://github.com/filamentphp/tables.git",
2141
                 "url": "https://github.com/filamentphp/tables.git",
2142
-                "reference": "64806e3c13caeabb23a8668a7aaf1efc8395df96"
2142
+                "reference": "1a107a8411549297b97d1142b1f7a5fa7a65e32b"
2143
             },
2143
             },
2144
             "dist": {
2144
             "dist": {
2145
                 "type": "zip",
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
                 "shasum": ""
2148
                 "shasum": ""
2149
             },
2149
             },
2150
             "require": {
2150
             "require": {
2183
                 "issues": "https://github.com/filamentphp/filament/issues",
2183
                 "issues": "https://github.com/filamentphp/filament/issues",
2184
                 "source": "https://github.com/filamentphp/filament"
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
             "name": "filament/widgets",
2189
             "name": "filament/widgets",
2190
-            "version": "v3.3.17",
2190
+            "version": "v3.3.20",
2191
             "source": {
2191
             "source": {
2192
                 "type": "git",
2192
                 "type": "git",
2193
                 "url": "https://github.com/filamentphp/widgets.git",
2193
                 "url": "https://github.com/filamentphp/widgets.git",
3117
         },
3117
         },
3118
         {
3118
         {
3119
             "name": "laravel/framework",
3119
             "name": "laravel/framework",
3120
-            "version": "v11.45.0",
3120
+            "version": "v11.45.1",
3121
             "source": {
3121
             "source": {
3122
                 "type": "git",
3122
                 "type": "git",
3123
                 "url": "https://github.com/laravel/framework.git",
3123
                 "url": "https://github.com/laravel/framework.git",
3124
-                "reference": "d0730deb427632004d24801be7ca1ed2c10fbc4e"
3124
+                "reference": "b09ba32795b8e71df10856a2694706663984a239"
3125
             },
3125
             },
3126
             "dist": {
3126
             "dist": {
3127
                 "type": "zip",
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
                 "shasum": ""
3130
                 "shasum": ""
3131
             },
3131
             },
3132
             "require": {
3132
             "require": {
3328
                 "issues": "https://github.com/laravel/framework/issues",
3328
                 "issues": "https://github.com/laravel/framework/issues",
3329
                 "source": "https://github.com/laravel/framework"
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
             "name": "laravel/prompts",
3334
             "name": "laravel/prompts",
4976
         },
4976
         },
4977
         {
4977
         {
4978
             "name": "nette/utils",
4978
             "name": "nette/utils",
4979
-            "version": "v4.0.6",
4979
+            "version": "v4.0.7",
4980
             "source": {
4980
             "source": {
4981
                 "type": "git",
4981
                 "type": "git",
4982
                 "url": "https://github.com/nette/utils.git",
4982
                 "url": "https://github.com/nette/utils.git",
4983
-                "reference": "ce708655043c7050eb050df361c5e313cf708309"
4983
+                "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2"
4984
             },
4984
             },
4985
             "dist": {
4985
             "dist": {
4986
                 "type": "zip",
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
                 "shasum": ""
4989
                 "shasum": ""
4990
             },
4990
             },
4991
             "require": {
4991
             "require": {
5056
             ],
5056
             ],
5057
             "support": {
5057
             "support": {
5058
                 "issues": "https://github.com/nette/utils/issues",
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
             "name": "nikic/php-parser",
5064
             "name": "nikic/php-parser",
9606
         },
9606
         },
9607
         {
9607
         {
9608
             "name": "filp/whoops",
9608
             "name": "filp/whoops",
9609
-            "version": "2.18.0",
9609
+            "version": "2.18.1",
9610
             "source": {
9610
             "source": {
9611
                 "type": "git",
9611
                 "type": "git",
9612
                 "url": "https://github.com/filp/whoops.git",
9612
                 "url": "https://github.com/filp/whoops.git",
9613
-                "reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e"
9613
+                "reference": "8fcc6a862f2e7b94eb4221fd0819ddba3d30ab26"
9614
             },
9614
             },
9615
             "dist": {
9615
             "dist": {
9616
                 "type": "zip",
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
                 "shasum": ""
9619
                 "shasum": ""
9620
             },
9620
             },
9621
             "require": {
9621
             "require": {
9665
             ],
9665
             ],
9666
             "support": {
9666
             "support": {
9667
                 "issues": "https://github.com/filp/whoops/issues",
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
             "funding": [
9670
             "funding": [
9671
                 {
9671
                 {
9673
                     "type": "github"
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
             "name": "hamcrest/hamcrest-php",
9679
             "name": "hamcrest/hamcrest-php",
10792
         },
10792
         },
10793
         {
10793
         {
10794
             "name": "php-di/php-di",
10794
             "name": "php-di/php-di",
10795
-            "version": "7.0.10",
10795
+            "version": "7.0.11",
10796
             "source": {
10796
             "source": {
10797
                 "type": "git",
10797
                 "type": "git",
10798
                 "url": "https://github.com/PHP-DI/PHP-DI.git",
10798
                 "url": "https://github.com/PHP-DI/PHP-DI.git",
10799
-                "reference": "0d1ed64126577e9a095b3204dcaee58cf76432c2"
10799
+                "reference": "32f111a6d214564520a57831d397263e8946c1d2"
10800
             },
10800
             },
10801
             "dist": {
10801
             "dist": {
10802
                 "type": "zip",
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
                 "shasum": ""
10805
                 "shasum": ""
10806
             },
10806
             },
10807
             "require": {
10807
             "require": {
10849
             ],
10849
             ],
10850
             "support": {
10850
             "support": {
10851
                 "issues": "https://github.com/PHP-DI/PHP-DI/issues",
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
             "funding": [
10854
             "funding": [
10855
                 {
10855
                 {
10861
                     "type": "tidelift"
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
             "name": "phpdocumentor/reflection-common",
10867
             "name": "phpdocumentor/reflection-common",

+ 79
- 82
database/factories/Accounting/BillFactory.php 查看文件

33
      */
33
      */
34
     public function definition(): array
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
         return [
38
         return [
47
             'company_id' => 1,
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
             'bill_number' => $this->faker->unique()->numerify('BILL-####'),
46
             'bill_number' => $this->faker->unique()->numerify('BILL-####'),
50
             'order_number' => $this->faker->unique()->numerify('PO-####'),
47
             'order_number' => $this->faker->unique()->numerify('PO-####'),
51
             'date' => $billDate,
48
             'date' => $billDate,
52
-            'due_date' => Carbon::parse($billDate)->addDays($dueDays),
49
+            'due_date' => $this->faker->dateTimeInInterval($billDate, '+6 months'),
53
             'status' => BillStatus::Open,
50
             'status' => BillStatus::Open,
54
             'discount_method' => $this->faker->randomElement(DocumentDiscountMethod::class),
51
             'discount_method' => $this->faker->randomElement(DocumentDiscountMethod::class),
55
             'discount_computation' => AdjustmentComputation::Percentage,
52
             'discount_computation' => AdjustmentComputation::Percentage,
78
     public function withLineItems(int $count = 3): static
75
     public function withLineItems(int $count = 3): static
79
     {
76
     {
80
         return $this->afterCreating(function (Bill $bill) use ($count) {
77
         return $this->afterCreating(function (Bill $bill) use ($count) {
78
+            // Clear existing line items first
79
+            $bill->lineItems()->delete();
80
+
81
             DocumentLineItem::factory()
81
             DocumentLineItem::factory()
82
                 ->count($count)
82
                 ->count($count)
83
                 ->forBill($bill)
83
                 ->forBill($bill)
90
     public function initialized(): static
90
     public function initialized(): static
91
     {
91
     {
92
         return $this->afterCreating(function (Bill $bill) {
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
     public function partial(int $maxPayments = 4): static
97
     public function partial(int $maxPayments = 4): static
107
     {
98
     {
108
         return $this->afterCreating(function (Bill $bill) use ($maxPayments) {
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
     public function paid(int $maxPayments = 4): static
105
     public function paid(int $maxPayments = 4): static
117
     {
106
     {
118
         return $this->afterCreating(function (Bill $bill) use ($maxPayments) {
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
                 'due_date' => now()->subDays($this->faker->numberBetween(1, 30)),
117
                 'due_date' => now()->subDays($this->faker->numberBetween(1, 30)),
131
             ])
118
             ])
132
             ->afterCreating(function (Bill $bill) {
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
             $latestPaymentDate = max($paymentDates);
196
             $latestPaymentDate = max($paymentDates);
191
             $bill->updateQuietly([
197
             $bill->updateQuietly([
192
                 'status' => $billStatus,
198
                 'status' => $billStatus,
193
                 'paid_at' => $latestPaymentDate,
199
                 'paid_at' => $latestPaymentDate,
194
             ]);
200
             ]);
195
-        });
201
+        }
196
     }
202
     }
197
 
203
 
198
     public function configure(): static
204
     public function configure(): static
199
     {
205
     {
200
         return $this->afterCreating(function (Bill $bill) {
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
             $number = DocumentDefault::getBaseNumber() + $bill->id;
214
             $number = DocumentDefault::getBaseNumber() + $bill->id;
204
 
215
 
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
     protected function recalculateTotals(Bill $bill): void
229
     protected function recalculateTotals(Bill $bill): void
233
     {
230
     {
234
         $bill->refresh();
231
         $bill->refresh();

+ 81
- 46
database/factories/Accounting/EstimateFactory.php 查看文件

31
      */
31
      */
32
     public function definition(): array
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
         return [
36
         return [
37
             'company_id' => 1,
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
             'header' => 'Estimate',
44
             'header' => 'Estimate',
40
             'subheader' => 'Estimate',
45
             'subheader' => 'Estimate',
41
             'estimate_number' => $this->faker->unique()->numerify('EST-####'),
46
             'estimate_number' => $this->faker->unique()->numerify('EST-####'),
42
             'reference_number' => $this->faker->unique()->numerify('REF-####'),
47
             'reference_number' => $this->faker->unique()->numerify('REF-####'),
43
             'date' => $estimateDate,
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
             'status' => EstimateStatus::Draft,
50
             'status' => EstimateStatus::Draft,
46
             'discount_method' => $this->faker->randomElement(DocumentDiscountMethod::class),
51
             'discount_method' => $this->faker->randomElement(DocumentDiscountMethod::class),
47
             'discount_computation' => AdjustmentComputation::Percentage,
52
             'discount_computation' => AdjustmentComputation::Percentage,
71
     public function withLineItems(int $count = 3): static
76
     public function withLineItems(int $count = 3): static
72
     {
77
     {
73
         return $this->afterCreating(function (Estimate $estimate) use ($count) {
78
         return $this->afterCreating(function (Estimate $estimate) use ($count) {
79
+            // Clear existing line items first
80
+            $estimate->lineItems()->delete();
81
+
74
             DocumentLineItem::factory()
82
             DocumentLineItem::factory()
75
                 ->count($count)
83
                 ->count($count)
76
                 ->forEstimate($estimate)
84
                 ->forEstimate($estimate)
83
     public function approved(): static
91
     public function approved(): static
84
     {
92
     {
85
         return $this->afterCreating(function (Estimate $estimate) {
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
     public function accepted(): static
98
     public function accepted(): static
100
     {
99
     {
101
         return $this->afterCreating(function (Estimate $estimate) {
100
         return $this->afterCreating(function (Estimate $estimate) {
102
-            $this->ensureSent($estimate);
101
+            $this->performSent($estimate);
103
 
102
 
104
             $acceptedAt = Carbon::parse($estimate->last_sent_at)
103
             $acceptedAt = Carbon::parse($estimate->last_sent_at)
105
                 ->addDays($this->faker->numberBetween(1, 7));
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
             $estimate->markAsAccepted($acceptedAt);
110
             $estimate->markAsAccepted($acceptedAt);
108
         });
111
         });
109
     }
112
     }
112
     {
115
     {
113
         return $this->afterCreating(function (Estimate $estimate) {
116
         return $this->afterCreating(function (Estimate $estimate) {
114
             if (! $estimate->wasAccepted()) {
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
             $convertedAt = Carbon::parse($estimate->accepted_at)
130
             $convertedAt = Carbon::parse($estimate->accepted_at)
119
                 ->addDays($this->faker->numberBetween(1, 7));
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
             $estimate->convertToInvoice($convertedAt);
137
             $estimate->convertToInvoice($convertedAt);
122
         });
138
         });
123
     }
139
     }
125
     public function declined(): static
141
     public function declined(): static
126
     {
142
     {
127
         return $this->afterCreating(function (Estimate $estimate) {
143
         return $this->afterCreating(function (Estimate $estimate) {
128
-            $this->ensureSent($estimate);
144
+            $this->performSent($estimate);
129
 
145
 
130
             $declinedAt = Carbon::parse($estimate->last_sent_at)
146
             $declinedAt = Carbon::parse($estimate->last_sent_at)
131
                 ->addDays($this->faker->numberBetween(1, 7));
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
             $estimate->markAsDeclined($declinedAt);
153
             $estimate->markAsDeclined($declinedAt);
134
         });
154
         });
135
     }
155
     }
137
     public function sent(): static
157
     public function sent(): static
138
     {
158
     {
139
         return $this->afterCreating(function (Estimate $estimate) {
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
     public function viewed(): static
164
     public function viewed(): static
150
     {
165
     {
151
         return $this->afterCreating(function (Estimate $estimate) {
166
         return $this->afterCreating(function (Estimate $estimate) {
152
-            $this->ensureSent($estimate);
167
+            $this->performSent($estimate);
153
 
168
 
154
             $viewedAt = Carbon::parse($estimate->last_sent_at)
169
             $viewedAt = Carbon::parse($estimate->last_sent_at)
155
                 ->addHours($this->faker->numberBetween(1, 24));
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
             $estimate->markAsViewed($viewedAt);
176
             $estimate->markAsViewed($viewedAt);
158
         });
177
         });
159
     }
178
     }
165
                 'expiration_date' => now()->subDays($this->faker->numberBetween(1, 30)),
184
                 'expiration_date' => now()->subDays($this->faker->numberBetween(1, 30)),
166
             ])
185
             ])
167
             ->afterCreating(function (Estimate $estimate) {
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
     public function configure(): static
223
     public function configure(): static
173
     {
224
     {
174
         return $this->afterCreating(function (Estimate $estimate) {
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
             $number = DocumentDefault::getBaseNumber() + $estimate->id;
233
             $number = DocumentDefault::getBaseNumber() + $estimate->id;
178
 
234
 
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
     protected function recalculateTotals(Estimate $estimate): void
248
     protected function recalculateTotals(Estimate $estimate): void
214
     {
249
     {
215
         $estimate->refresh();
250
         $estimate->refresh();

+ 31
- 7
database/factories/Accounting/InvoiceFactory.php 查看文件

34
      */
34
      */
35
     public function definition(): array
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
         return [
39
         return [
40
             'company_id' => 1,
40
             'company_id' => 1,
79
     public function withLineItems(int $count = 3): static
79
     public function withLineItems(int $count = 3): static
80
     {
80
     {
81
         return $this->afterCreating(function (Invoice $invoice) use ($count) {
81
         return $this->afterCreating(function (Invoice $invoice) use ($count) {
82
+            $invoice->lineItems()->delete();
83
+
82
             DocumentLineItem::factory()
84
             DocumentLineItem::factory()
83
                 ->count($count)
85
                 ->count($count)
84
                 ->forInvoice($invoice)
86
                 ->forInvoice($invoice)
139
 
141
 
140
     protected function performApproval(Invoice $invoice): void
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
         if (! $invoice->canBeApproved()) {
144
         if (! $invoice->canBeApproved()) {
147
             throw new \InvalidArgumentException('Invoice cannot be approved. Current status: ' . $invoice->status->value);
145
             throw new \InvalidArgumentException('Invoice cannot be approved. Current status: ' . $invoice->status->value);
148
         }
146
         }
150
         $approvedAt = Carbon::parse($invoice->date)
148
         $approvedAt = Carbon::parse($invoice->date)
151
             ->addHours($this->faker->numberBetween(1, 24));
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
         $invoice->approveDraft($approvedAt);
155
         $invoice->approveDraft($approvedAt);
154
     }
156
     }
155
 
157
 
162
         $sentAt = Carbon::parse($invoice->approved_at)
164
         $sentAt = Carbon::parse($invoice->approved_at)
163
             ->addHours($this->faker->numberBetween(1, 24));
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
         $invoice->markAsSent($sentAt);
171
         $invoice->markAsSent($sentAt);
166
     }
172
     }
167
 
173
 
188
         $paymentAmount = (int) floor($totalAmountDue / $paymentCount);
194
         $paymentAmount = (int) floor($totalAmountDue / $paymentCount);
189
         $remainingAmount = $totalAmountDue;
195
         $remainingAmount = $totalAmountDue;
190
 
196
 
191
-        $paymentDate = Carbon::parse($invoice->approved_at);
197
+        $initialPaymentDate = Carbon::parse($invoice->approved_at);
198
+        $maxPaymentDate = now();
199
+
192
         $paymentDates = [];
200
         $paymentDates = [];
193
 
201
 
194
         for ($i = 0; $i < $paymentCount; $i++) {
202
         for ($i = 0; $i < $paymentCount; $i++) {
198
                 break;
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
             $paymentDates[] = $postedAt;
219
             $paymentDates[] = $postedAt;
203
 
220
 
204
             $data = [
221
             $data = [
225
     public function configure(): static
242
     public function configure(): static
226
     {
243
     {
227
         return $this->afterCreating(function (Invoice $invoice) {
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
             $number = DocumentDefault::getBaseNumber() + $invoice->id;
252
             $number = DocumentDefault::getBaseNumber() + $invoice->id;
229
 
253
 
230
             $invoice->updateQuietly([
254
             $invoice->updateQuietly([

+ 158
- 57
database/factories/Accounting/RecurringInvoiceFactory.php 查看文件

39
     {
39
     {
40
         return [
40
         return [
41
             'company_id' => 1,
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
             'header' => 'Invoice',
48
             'header' => 'Invoice',
44
             'subheader' => 'Invoice',
49
             'subheader' => 'Invoice',
45
             'order_number' => $this->faker->unique()->numerify('ORD-####'),
50
             'order_number' => $this->faker->unique()->numerify('ORD-####'),
73
     public function withLineItems(int $count = 3): static
78
     public function withLineItems(int $count = 3): static
74
     {
79
     {
75
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($count) {
80
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($count) {
81
+            // Clear existing line items first
82
+            $recurringInvoice->lineItems()->delete();
83
+
76
             DocumentLineItem::factory()
84
             DocumentLineItem::factory()
77
                 ->count($count)
85
                 ->count($count)
78
                 ->forInvoice($recurringInvoice)
86
                 ->forInvoice($recurringInvoice)
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
     public function withSchedule(
105
     public function withSchedule(
86
         ?Frequency $frequency = null,
106
         ?Frequency $frequency = null,
87
         ?Carbon $startDate = null,
107
         ?Carbon $startDate = null,
88
         ?EndType $endType = null
108
         ?EndType $endType = null
89
     ): static {
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
     public function withDailySchedule(Carbon $startDate, EndType $endType): static
115
     public function withDailySchedule(Carbon $startDate, EndType $endType): static
115
     {
116
     {
116
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
117
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
117
-            $this->ensureLineItems($recurringInvoice);
118
-
119
             $recurringInvoice->updateQuietly([
118
             $recurringInvoice->updateQuietly([
120
                 'frequency' => Frequency::Daily,
119
                 'frequency' => Frequency::Daily,
121
                 'start_date' => $startDate,
120
                 'start_date' => $startDate,
127
     public function withWeeklySchedule(Carbon $startDate, EndType $endType): static
126
     public function withWeeklySchedule(Carbon $startDate, EndType $endType): static
128
     {
127
     {
129
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
128
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
130
-            $this->ensureLineItems($recurringInvoice);
131
-
132
             $recurringInvoice->updateQuietly([
129
             $recurringInvoice->updateQuietly([
133
                 'frequency' => Frequency::Weekly,
130
                 'frequency' => Frequency::Weekly,
134
                 'day_of_week' => DayOfWeek::from($startDate->dayOfWeek),
131
                 'day_of_week' => DayOfWeek::from($startDate->dayOfWeek),
141
     public function withMonthlySchedule(Carbon $startDate, EndType $endType): static
138
     public function withMonthlySchedule(Carbon $startDate, EndType $endType): static
142
     {
139
     {
143
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
140
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
144
-            $this->ensureLineItems($recurringInvoice);
145
-
146
             $recurringInvoice->updateQuietly([
141
             $recurringInvoice->updateQuietly([
147
                 'frequency' => Frequency::Monthly,
142
                 'frequency' => Frequency::Monthly,
148
                 'day_of_month' => DayOfMonth::from($startDate->day),
143
                 'day_of_month' => DayOfMonth::from($startDate->day),
155
     public function withYearlySchedule(Carbon $startDate, EndType $endType): static
150
     public function withYearlySchedule(Carbon $startDate, EndType $endType): static
156
     {
151
     {
157
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
152
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
158
-            $this->ensureLineItems($recurringInvoice);
159
-
160
             $recurringInvoice->updateQuietly([
153
             $recurringInvoice->updateQuietly([
161
                 'frequency' => Frequency::Yearly,
154
                 'frequency' => Frequency::Yearly,
162
                 'month' => Month::from($startDate->month),
155
                 'month' => Month::from($startDate->month),
174
         ?int $intervalValue = null
167
         ?int $intervalValue = null
175
     ): static {
168
     ): static {
176
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($intervalType, $intervalValue, $startDate, $endType) {
169
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($intervalType, $intervalValue, $startDate, $endType) {
177
-            $this->ensureLineItems($recurringInvoice);
178
-
179
             $intervalType ??= $this->faker->randomElement(IntervalType::class);
170
             $intervalType ??= $this->faker->randomElement(IntervalType::class);
180
             $intervalValue ??= match ($intervalType) {
171
             $intervalValue ??= match ($intervalType) {
181
                 IntervalType::Day => $this->faker->numberBetween(1, 7),
172
                 IntervalType::Day => $this->faker->numberBetween(1, 7),
215
                     break;
206
                     break;
216
             }
207
             }
217
 
208
 
218
-            return $recurringInvoice->updateQuietly($state);
209
+            $recurringInvoice->updateQuietly($state);
219
         });
210
         });
220
     }
211
     }
221
 
212
 
248
     public function approved(): static
239
     public function approved(): static
249
     {
240
     {
250
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) {
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
             if (! $recurringInvoice->hasSchedule()) {
249
             if (! $recurringInvoice->hasSchedule()) {
254
-                $this->withSchedule()->callAfterCreating(collect([$recurringInvoice]));
250
+                $this->performScheduleSetup($recurringInvoice);
255
                 $recurringInvoice->refresh();
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
     public function ended(): static
258
     public function ended(): static
274
     {
259
     {
275
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) {
260
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) {
276
-            $this->ensureLineItems($recurringInvoice);
277
-
278
             if (! $recurringInvoice->canBeEnded()) {
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
             $endedAt = $recurringInvoice->last_date
270
             $endedAt = $recurringInvoice->last_date
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
     protected function recalculateTotals(RecurringInvoice $recurringInvoice): void
408
     protected function recalculateTotals(RecurringInvoice $recurringInvoice): void

+ 1
- 1
database/factories/Common/OfferingFactory.php 查看文件

33
             'name' => $this->faker->words(3, true),
33
             'name' => $this->faker->words(3, true),
34
             'description' => $this->faker->sentence,
34
             'description' => $this->faker->sentence,
35
             'type' => $this->faker->randomElement(OfferingType::cases()),
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
             'sellable' => false,
37
             'sellable' => false,
38
             'purchasable' => false,
38
             'purchasable' => false,
39
             'income_account_id' => null,
39
             'income_account_id' => null,

+ 14
- 0
tests/Feature/Accounting/RecurringInvoiceTest.php 查看文件

10
     $this->withOfferings();
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
 test('recurring invoice properly handles months with fewer days for monthly frequency', function () {
27
 test('recurring invoice properly handles months with fewer days for monthly frequency', function () {
14
     // Start from January 31st
28
     // Start from January 31st
15
     Carbon::setTestNow('2024-01-31');
29
     Carbon::setTestNow('2024-01-31');

Loading…
取消
儲存