Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

RecurringInvoiceFactory.php 13KB

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