Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

RecurringInvoiceFactory.php 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. <?php
  2. namespace Database\Factories\Accounting;
  3. use App\Enums\Accounting\AdjustmentComputation;
  4. use App\Enums\Accounting\DayOfMonth;
  5. use App\Enums\Accounting\DayOfWeek;
  6. use App\Enums\Accounting\DocumentDiscountMethod;
  7. use App\Enums\Accounting\EndType;
  8. use App\Enums\Accounting\Frequency;
  9. use App\Enums\Accounting\IntervalType;
  10. use App\Enums\Accounting\Month;
  11. use App\Enums\Accounting\RecurringInvoiceStatus;
  12. use App\Enums\Setting\PaymentTerms;
  13. use App\Models\Accounting\DocumentLineItem;
  14. use App\Models\Accounting\RecurringInvoice;
  15. use App\Models\Common\Client;
  16. use App\Models\Company;
  17. use App\Utilities\RateCalculator;
  18. use Illuminate\Database\Eloquent\Factories\Factory;
  19. use Illuminate\Support\Carbon;
  20. /**
  21. * @extends Factory<RecurringInvoice>
  22. */
  23. class RecurringInvoiceFactory extends Factory
  24. {
  25. /**
  26. * The name of the factory's corresponding model.
  27. */
  28. protected $model = RecurringInvoice::class;
  29. /**
  30. * Define the model's default state.
  31. *
  32. * @return array<string, mixed>
  33. */
  34. public function definition(): array
  35. {
  36. return [
  37. 'company_id' => 1,
  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. },
  44. 'header' => 'Invoice',
  45. 'subheader' => 'Invoice',
  46. 'order_number' => $this->faker->unique()->numerify('ORD-####'),
  47. 'payment_terms' => PaymentTerms::Net30,
  48. 'status' => RecurringInvoiceStatus::Draft,
  49. 'discount_method' => $this->faker->randomElement(DocumentDiscountMethod::class),
  50. 'discount_computation' => AdjustmentComputation::Percentage,
  51. 'discount_rate' => function (array $attributes) {
  52. $discountMethod = DocumentDiscountMethod::parse($attributes['discount_method']);
  53. if ($discountMethod?->isPerDocument()) {
  54. return $this->faker->numberBetween(50000, 200000); // 5% - 20%
  55. }
  56. return 0;
  57. },
  58. 'currency_code' => function (array $attributes) {
  59. $client = Client::find($attributes['client_id']);
  60. return $client->currency_code ??
  61. Company::find($attributes['company_id'])->default->currency_code ??
  62. 'USD';
  63. },
  64. 'terms' => $this->faker->sentence,
  65. 'footer' => $this->faker->sentence,
  66. 'created_by' => 1,
  67. 'updated_by' => 1,
  68. ];
  69. }
  70. public function withLineItems(int $count = 3): static
  71. {
  72. return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($count) {
  73. // Clear existing line items first
  74. $recurringInvoice->lineItems()->delete();
  75. DocumentLineItem::factory()
  76. ->count($count)
  77. ->forInvoice($recurringInvoice)
  78. ->create();
  79. $this->recalculateTotals($recurringInvoice);
  80. });
  81. }
  82. public function configure(): static
  83. {
  84. return $this->afterCreating(function (RecurringInvoice $recurringInvoice) {
  85. DocumentLineItem::factory()
  86. ->count(3)
  87. ->forInvoice($recurringInvoice)
  88. ->create();
  89. $this->recalculateTotals($recurringInvoice);
  90. });
  91. }
  92. public function withSchedule(
  93. ?Frequency $frequency = null,
  94. ?Carbon $startDate = null,
  95. ?EndType $endType = null
  96. ): static {
  97. return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($frequency, $startDate, $endType) {
  98. $this->performScheduleSetup($recurringInvoice, $frequency, $startDate, $endType);
  99. });
  100. }
  101. public function withDailySchedule(Carbon $startDate, EndType $endType): static
  102. {
  103. return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
  104. $recurringInvoice->updateQuietly([
  105. 'frequency' => Frequency::Daily,
  106. 'start_date' => $startDate,
  107. 'end_type' => $endType,
  108. ]);
  109. });
  110. }
  111. public function withWeeklySchedule(Carbon $startDate, EndType $endType): static
  112. {
  113. return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
  114. $recurringInvoice->updateQuietly([
  115. 'frequency' => Frequency::Weekly,
  116. 'day_of_week' => DayOfWeek::from($startDate->dayOfWeek),
  117. 'start_date' => $startDate,
  118. 'end_type' => $endType,
  119. ]);
  120. });
  121. }
  122. public function withMonthlySchedule(Carbon $startDate, EndType $endType): static
  123. {
  124. return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
  125. $recurringInvoice->updateQuietly([
  126. 'frequency' => Frequency::Monthly,
  127. 'day_of_month' => DayOfMonth::from($startDate->day),
  128. 'start_date' => $startDate,
  129. 'end_type' => $endType,
  130. ]);
  131. });
  132. }
  133. public function withYearlySchedule(Carbon $startDate, EndType $endType): static
  134. {
  135. return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
  136. $recurringInvoice->updateQuietly([
  137. 'frequency' => Frequency::Yearly,
  138. 'month' => Month::from($startDate->month),
  139. 'day_of_month' => DayOfMonth::from($startDate->day),
  140. 'start_date' => $startDate,
  141. 'end_type' => $endType,
  142. ]);
  143. });
  144. }
  145. public function withCustomSchedule(
  146. Carbon $startDate,
  147. EndType $endType,
  148. ?IntervalType $intervalType = null,
  149. ?int $intervalValue = null
  150. ): static {
  151. return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($intervalType, $intervalValue, $startDate, $endType) {
  152. $intervalType ??= $this->faker->randomElement(IntervalType::class);
  153. $intervalValue ??= match ($intervalType) {
  154. IntervalType::Day => $this->faker->numberBetween(1, 7),
  155. IntervalType::Week => $this->faker->numberBetween(1, 4),
  156. IntervalType::Month => $this->faker->numberBetween(1, 3),
  157. IntervalType::Year => 1,
  158. };
  159. $state = [
  160. 'frequency' => Frequency::Custom,
  161. 'interval_type' => $intervalType,
  162. 'interval_value' => $intervalValue,
  163. 'start_date' => $startDate,
  164. 'end_type' => $endType,
  165. ];
  166. // Add interval-specific attributes
  167. switch ($intervalType) {
  168. case IntervalType::Day:
  169. // No additional attributes needed
  170. break;
  171. case IntervalType::Week:
  172. $state['day_of_week'] = DayOfWeek::from($startDate->dayOfWeek);
  173. break;
  174. case IntervalType::Month:
  175. $state['day_of_month'] = DayOfMonth::from($startDate->day);
  176. break;
  177. case IntervalType::Year:
  178. $state['month'] = Month::from($startDate->month);
  179. $state['day_of_month'] = DayOfMonth::from($startDate->day);
  180. break;
  181. }
  182. $recurringInvoice->updateQuietly($state);
  183. });
  184. }
  185. public function endAfter(int $occurrences = 12): static
  186. {
  187. return $this->state([
  188. 'end_type' => EndType::After,
  189. 'max_occurrences' => $occurrences,
  190. ]);
  191. }
  192. public function endOn(?Carbon $endDate = null): static
  193. {
  194. $endDate ??= now()->addMonths($this->faker->numberBetween(1, 12));
  195. return $this->state([
  196. 'end_type' => EndType::On,
  197. 'end_date' => $endDate,
  198. ]);
  199. }
  200. public function autoSend(string $sendTime = '09:00'): static
  201. {
  202. return $this->state([
  203. 'auto_send' => true,
  204. 'send_time' => $sendTime,
  205. ]);
  206. }
  207. public function approved(): static
  208. {
  209. return $this->afterCreating(function (RecurringInvoice $recurringInvoice) {
  210. $this->performApproval($recurringInvoice);
  211. });
  212. }
  213. public function active(): static
  214. {
  215. return $this->afterCreating(function (RecurringInvoice $recurringInvoice) {
  216. if (! $recurringInvoice->hasSchedule()) {
  217. $this->performScheduleSetup($recurringInvoice);
  218. $recurringInvoice->refresh();
  219. }
  220. $this->performApproval($recurringInvoice);
  221. });
  222. }
  223. public function ended(): static
  224. {
  225. return $this->afterCreating(function (RecurringInvoice $recurringInvoice) {
  226. if (! $recurringInvoice->canBeEnded()) {
  227. if (! $recurringInvoice->hasSchedule()) {
  228. $this->performScheduleSetup($recurringInvoice);
  229. $recurringInvoice->refresh();
  230. }
  231. $this->performApproval($recurringInvoice);
  232. }
  233. $endedAt = $recurringInvoice->last_date
  234. ? $recurringInvoice->last_date->copy()->addDays($this->faker->numberBetween(1, 7))
  235. : now()->subDays($this->faker->numberBetween(1, 30));
  236. $recurringInvoice->updateQuietly([
  237. 'ended_at' => $endedAt,
  238. 'status' => RecurringInvoiceStatus::Ended,
  239. ]);
  240. });
  241. }
  242. protected function performScheduleSetup(
  243. RecurringInvoice $recurringInvoice,
  244. ?Frequency $frequency = null,
  245. ?Carbon $startDate = null,
  246. ?EndType $endType = null
  247. ): void {
  248. $frequency ??= $this->faker->randomElement(Frequency::class);
  249. $endType ??= EndType::Never;
  250. // Adjust the start date range based on frequency
  251. $startDate = match ($frequency) {
  252. Frequency::Daily => Carbon::parse($this->faker->dateTimeBetween('-30 days')), // At most 30 days back
  253. default => $startDate ?? Carbon::parse($this->faker->dateTimeBetween('-1 year')),
  254. };
  255. match ($frequency) {
  256. Frequency::Daily => $this->performDailySchedule($recurringInvoice, $startDate, $endType),
  257. Frequency::Weekly => $this->performWeeklySchedule($recurringInvoice, $startDate, $endType),
  258. Frequency::Monthly => $this->performMonthlySchedule($recurringInvoice, $startDate, $endType),
  259. Frequency::Yearly => $this->performYearlySchedule($recurringInvoice, $startDate, $endType),
  260. Frequency::Custom => $this->performCustomSchedule($recurringInvoice, $startDate, $endType),
  261. };
  262. }
  263. protected function performDailySchedule(RecurringInvoice $recurringInvoice, Carbon $startDate, EndType $endType): void
  264. {
  265. $recurringInvoice->updateQuietly([
  266. 'frequency' => Frequency::Daily,
  267. 'start_date' => $startDate,
  268. 'end_type' => $endType,
  269. ]);
  270. }
  271. protected function performWeeklySchedule(RecurringInvoice $recurringInvoice, Carbon $startDate, EndType $endType): void
  272. {
  273. $recurringInvoice->updateQuietly([
  274. 'frequency' => Frequency::Weekly,
  275. 'day_of_week' => DayOfWeek::from($startDate->dayOfWeek),
  276. 'start_date' => $startDate,
  277. 'end_type' => $endType,
  278. ]);
  279. }
  280. protected function performMonthlySchedule(RecurringInvoice $recurringInvoice, Carbon $startDate, EndType $endType): void
  281. {
  282. $recurringInvoice->updateQuietly([
  283. 'frequency' => Frequency::Monthly,
  284. 'day_of_month' => DayOfMonth::from($startDate->day),
  285. 'start_date' => $startDate,
  286. 'end_type' => $endType,
  287. ]);
  288. }
  289. protected function performYearlySchedule(RecurringInvoice $recurringInvoice, Carbon $startDate, EndType $endType): void
  290. {
  291. $recurringInvoice->updateQuietly([
  292. 'frequency' => Frequency::Yearly,
  293. 'month' => Month::from($startDate->month),
  294. 'day_of_month' => DayOfMonth::from($startDate->day),
  295. 'start_date' => $startDate,
  296. 'end_type' => $endType,
  297. ]);
  298. }
  299. protected function performCustomSchedule(
  300. RecurringInvoice $recurringInvoice,
  301. Carbon $startDate,
  302. EndType $endType,
  303. ?IntervalType $intervalType = null,
  304. ?int $intervalValue = null
  305. ): void {
  306. $intervalType ??= $this->faker->randomElement(IntervalType::class);
  307. $intervalValue ??= match ($intervalType) {
  308. IntervalType::Day => $this->faker->numberBetween(1, 7),
  309. IntervalType::Week => $this->faker->numberBetween(1, 4),
  310. IntervalType::Month => $this->faker->numberBetween(1, 3),
  311. IntervalType::Year => 1,
  312. };
  313. $state = [
  314. 'frequency' => Frequency::Custom,
  315. 'interval_type' => $intervalType,
  316. 'interval_value' => $intervalValue,
  317. 'start_date' => $startDate,
  318. 'end_type' => $endType,
  319. ];
  320. // Add interval-specific attributes
  321. switch ($intervalType) {
  322. case IntervalType::Day:
  323. // No additional attributes needed
  324. break;
  325. case IntervalType::Week:
  326. $state['day_of_week'] = DayOfWeek::from($startDate->dayOfWeek);
  327. break;
  328. case IntervalType::Month:
  329. $state['day_of_month'] = DayOfMonth::from($startDate->day);
  330. break;
  331. case IntervalType::Year:
  332. $state['month'] = Month::from($startDate->month);
  333. $state['day_of_month'] = DayOfMonth::from($startDate->day);
  334. break;
  335. }
  336. $recurringInvoice->updateQuietly($state);
  337. }
  338. protected function performApproval(RecurringInvoice $recurringInvoice): void
  339. {
  340. if (! $recurringInvoice->hasSchedule()) {
  341. $this->performScheduleSetup($recurringInvoice);
  342. $recurringInvoice->refresh();
  343. }
  344. $approvedAt = $recurringInvoice->start_date
  345. ? $recurringInvoice->start_date->copy()->subDays($this->faker->numberBetween(1, 7))
  346. : now()->subDays($this->faker->numberBetween(1, 30));
  347. $recurringInvoice->approveDraft($approvedAt);
  348. }
  349. protected function recalculateTotals(RecurringInvoice $recurringInvoice): void
  350. {
  351. $recurringInvoice->refresh();
  352. if (! $recurringInvoice->hasLineItems()) {
  353. return;
  354. }
  355. $subtotalCents = $recurringInvoice->lineItems()->sum('subtotal');
  356. $taxTotalCents = $recurringInvoice->lineItems()->sum('tax_total');
  357. $discountTotalCents = 0;
  358. if ($recurringInvoice->discount_method?->isPerLineItem()) {
  359. $discountTotalCents = $recurringInvoice->lineItems()->sum('discount_total');
  360. } elseif ($recurringInvoice->discount_method?->isPerDocument() && $recurringInvoice->discount_rate) {
  361. if ($recurringInvoice->discount_computation?->isPercentage()) {
  362. $scaledRate = RateCalculator::parseLocalizedRate($recurringInvoice->discount_rate);
  363. $discountTotalCents = RateCalculator::calculatePercentage($subtotalCents, $scaledRate);
  364. } else {
  365. $discountTotalCents = $recurringInvoice->getRawOriginal('discount_rate');
  366. }
  367. }
  368. $grandTotalCents = $subtotalCents + $taxTotalCents - $discountTotalCents;
  369. $recurringInvoice->update([
  370. 'subtotal' => $subtotalCents,
  371. 'tax_total' => $taxTotalCents,
  372. 'discount_total' => $discountTotalCents,
  373. 'total' => $grandTotalCents,
  374. ]);
  375. }
  376. }