You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

RecurringInvoiceFactory.php 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  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' => fn (array $attributes) => Client::where('company_id', $attributes['company_id'])->inRandomOrder()->value('id'),
  39. 'header' => 'Invoice',
  40. 'subheader' => 'Invoice',
  41. 'order_number' => $this->faker->unique()->numerify('ORD-####'),
  42. 'payment_terms' => PaymentTerms::Net30,
  43. 'status' => RecurringInvoiceStatus::Draft,
  44. 'discount_method' => $this->faker->randomElement(DocumentDiscountMethod::class),
  45. 'discount_computation' => AdjustmentComputation::Percentage,
  46. 'discount_rate' => function (array $attributes) {
  47. $discountMethod = DocumentDiscountMethod::parse($attributes['discount_method']);
  48. if ($discountMethod?->isPerDocument()) {
  49. return $this->faker->numberBetween(50000, 200000); // 5% - 20%
  50. }
  51. return 0;
  52. },
  53. 'currency_code' => function (array $attributes) {
  54. $client = Client::find($attributes['client_id']);
  55. return $client->currency_code ??
  56. Company::find($attributes['company_id'])->default->currency_code ??
  57. 'USD';
  58. },
  59. 'terms' => $this->faker->sentence,
  60. 'footer' => $this->faker->sentence,
  61. 'created_by' => 1,
  62. 'updated_by' => 1,
  63. ];
  64. }
  65. public function withLineItems(int $count = 3): static
  66. {
  67. return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($count) {
  68. DocumentLineItem::factory()
  69. ->count($count)
  70. ->forInvoice($recurringInvoice)
  71. ->create();
  72. $this->recalculateTotals($recurringInvoice);
  73. });
  74. }
  75. public function withSchedule(
  76. ?Frequency $frequency = null,
  77. ?Carbon $startDate = null,
  78. ?EndType $endType = null
  79. ): static {
  80. return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($frequency, $endType, $startDate) {
  81. $this->ensureLineItems($recurringInvoice);
  82. $frequency ??= $this->faker->randomElement(Frequency::class);
  83. $endType ??= EndType::Never;
  84. // Adjust the start date range based on frequency
  85. $startDate = match ($frequency) {
  86. Frequency::Daily => Carbon::parse($this->faker->dateTimeBetween('-30 days')), // At most 30 days back
  87. default => $startDate ?? Carbon::parse($this->faker->dateTimeBetween('-1 year')),
  88. };
  89. $state = match ($frequency) {
  90. Frequency::Daily => $this->withDailySchedule($startDate, $endType),
  91. Frequency::Weekly => $this->withWeeklySchedule($startDate, $endType),
  92. Frequency::Monthly => $this->withMonthlySchedule($startDate, $endType),
  93. Frequency::Yearly => $this->withYearlySchedule($startDate, $endType),
  94. Frequency::Custom => $this->withCustomSchedule($startDate, $endType),
  95. };
  96. $state->callAfterCreating(collect([$recurringInvoice]));
  97. });
  98. }
  99. public function withDailySchedule(Carbon $startDate, EndType $endType): static
  100. {
  101. return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
  102. $this->ensureLineItems($recurringInvoice);
  103. $recurringInvoice->updateQuietly([
  104. 'frequency' => Frequency::Daily,
  105. 'start_date' => $startDate,
  106. 'end_type' => $endType,
  107. ]);
  108. });
  109. }
  110. public function withWeeklySchedule(Carbon $startDate, EndType $endType): static
  111. {
  112. return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
  113. $this->ensureLineItems($recurringInvoice);
  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. $this->ensureLineItems($recurringInvoice);
  126. $recurringInvoice->updateQuietly([
  127. 'frequency' => Frequency::Monthly,
  128. 'day_of_month' => DayOfMonth::from($startDate->day),
  129. 'start_date' => $startDate,
  130. 'end_type' => $endType,
  131. ]);
  132. });
  133. }
  134. public function withYearlySchedule(Carbon $startDate, EndType $endType): static
  135. {
  136. return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
  137. $this->ensureLineItems($recurringInvoice);
  138. $recurringInvoice->updateQuietly([
  139. 'frequency' => Frequency::Yearly,
  140. 'month' => Month::from($startDate->month),
  141. 'day_of_month' => DayOfMonth::from($startDate->day),
  142. 'start_date' => $startDate,
  143. 'end_type' => $endType,
  144. ]);
  145. });
  146. }
  147. public function withCustomSchedule(
  148. Carbon $startDate,
  149. EndType $endType,
  150. ?IntervalType $intervalType = null,
  151. ?int $intervalValue = null
  152. ): static {
  153. return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($intervalType, $intervalValue, $startDate, $endType) {
  154. $this->ensureLineItems($recurringInvoice);
  155. $intervalType ??= $this->faker->randomElement(IntervalType::class);
  156. $intervalValue ??= match ($intervalType) {
  157. IntervalType::Day => $this->faker->numberBetween(1, 7),
  158. IntervalType::Week => $this->faker->numberBetween(1, 4),
  159. IntervalType::Month => $this->faker->numberBetween(1, 3),
  160. IntervalType::Year => 1,
  161. };
  162. $state = [
  163. 'frequency' => Frequency::Custom,
  164. 'interval_type' => $intervalType,
  165. 'interval_value' => $intervalValue,
  166. 'start_date' => $startDate,
  167. 'end_type' => $endType,
  168. ];
  169. // Add interval-specific attributes
  170. switch ($intervalType) {
  171. case IntervalType::Day:
  172. // No additional attributes needed
  173. break;
  174. case IntervalType::Week:
  175. $state['day_of_week'] = DayOfWeek::from($startDate->dayOfWeek);
  176. break;
  177. case IntervalType::Month:
  178. $state['day_of_month'] = DayOfMonth::from($startDate->day);
  179. break;
  180. case IntervalType::Year:
  181. $state['month'] = Month::from($startDate->month);
  182. $state['day_of_month'] = DayOfMonth::from($startDate->day);
  183. break;
  184. }
  185. return $recurringInvoice->updateQuietly($state);
  186. });
  187. }
  188. public function endAfter(int $occurrences = 12): static
  189. {
  190. return $this->state([
  191. 'end_type' => EndType::After,
  192. 'max_occurrences' => $occurrences,
  193. ]);
  194. }
  195. public function endOn(?Carbon $endDate = null): static
  196. {
  197. $endDate ??= now()->addMonths($this->faker->numberBetween(1, 12));
  198. return $this->state([
  199. 'end_type' => EndType::On,
  200. 'end_date' => $endDate,
  201. ]);
  202. }
  203. public function autoSend(string $sendTime = '09:00'): static
  204. {
  205. return $this->state([
  206. 'auto_send' => true,
  207. 'send_time' => $sendTime,
  208. ]);
  209. }
  210. public function approved(): static
  211. {
  212. return $this->afterCreating(function (RecurringInvoice $recurringInvoice) {
  213. $this->ensureLineItems($recurringInvoice);
  214. if (! $recurringInvoice->hasSchedule()) {
  215. $this->withSchedule()->callAfterCreating(collect([$recurringInvoice]));
  216. $recurringInvoice->refresh();
  217. }
  218. $approvedAt = $recurringInvoice->start_date
  219. ? $recurringInvoice->start_date->copy()->subDays($this->faker->numberBetween(1, 7))
  220. : now()->subDays($this->faker->numberBetween(1, 30));
  221. $recurringInvoice->approveDraft($approvedAt);
  222. });
  223. }
  224. public function active(): static
  225. {
  226. return $this->withLineItems()
  227. ->withSchedule()
  228. ->approved();
  229. }
  230. public function ended(): static
  231. {
  232. return $this->afterCreating(function (RecurringInvoice $recurringInvoice) {
  233. $this->ensureLineItems($recurringInvoice);
  234. if (! $recurringInvoice->canBeEnded()) {
  235. $this->active()->callAfterCreating(collect([$recurringInvoice]));
  236. }
  237. $endedAt = $recurringInvoice->last_date
  238. ? $recurringInvoice->last_date->copy()->addDays($this->faker->numberBetween(1, 7))
  239. : now()->subDays($this->faker->numberBetween(1, 30));
  240. $recurringInvoice->updateQuietly([
  241. 'ended_at' => $endedAt,
  242. 'status' => RecurringInvoiceStatus::Ended,
  243. ]);
  244. });
  245. }
  246. public function configure(): static
  247. {
  248. return $this->afterCreating(function (RecurringInvoice $recurringInvoice) {
  249. $this->ensureLineItems($recurringInvoice);
  250. });
  251. }
  252. protected function ensureLineItems(RecurringInvoice $recurringInvoice): void
  253. {
  254. if (! $recurringInvoice->hasLineItems()) {
  255. $this->withLineItems()->callAfterCreating(collect([$recurringInvoice]));
  256. }
  257. }
  258. protected function recalculateTotals(RecurringInvoice $recurringInvoice): void
  259. {
  260. $recurringInvoice->refresh();
  261. if (! $recurringInvoice->hasLineItems()) {
  262. return;
  263. }
  264. $subtotalCents = $recurringInvoice->lineItems()->sum('subtotal');
  265. $taxTotalCents = $recurringInvoice->lineItems()->sum('tax_total');
  266. $discountTotalCents = 0;
  267. if ($recurringInvoice->discount_method?->isPerLineItem()) {
  268. $discountTotalCents = $recurringInvoice->lineItems()->sum('discount_total');
  269. } elseif ($recurringInvoice->discount_method?->isPerDocument() && $recurringInvoice->discount_rate) {
  270. if ($recurringInvoice->discount_computation?->isPercentage()) {
  271. $scaledRate = RateCalculator::parseLocalizedRate($recurringInvoice->discount_rate);
  272. $discountTotalCents = RateCalculator::calculatePercentage($subtotalCents, $scaledRate);
  273. } else {
  274. $discountTotalCents = $recurringInvoice->getRawOriginal('discount_rate');
  275. }
  276. }
  277. $grandTotalCents = $subtotalCents + $taxTotalCents - $discountTotalCents;
  278. $recurringInvoice->update([
  279. 'subtotal' => $subtotalCents,
  280. 'tax_total' => $taxTotalCents,
  281. 'discount_total' => $discountTotalCents,
  282. 'total' => $grandTotalCents,
  283. ]);
  284. }
  285. }