ProposalPricingService.php 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. <?php
  2. namespace App\Services;
  3. use Illuminate\Database\Eloquent\Model;
  4. use Illuminate\Support\Str;
  5. /**
  6. * Generic CRUD/clone helpers for the Pavão/Irrecusável pricing blocks, reused
  7. * across the 4 tables (base/unit level x Pavão/Irrecusável) since they all
  8. * share the same column shape (Matrícula, Aulas, Materiais, Valor Total).
  9. */
  10. class ProposalPricingService
  11. {
  12. private const VALUE_FIELDS = [
  13. 'registration_value',
  14. 'registration_included_in_course',
  15. 'registration_installments_allowed',
  16. 'registration_max_installments',
  17. 'classes_value',
  18. 'classes_included_in_course',
  19. 'classes_installments_allowed',
  20. 'classes_max_installments',
  21. 'classes_discount_percentage',
  22. 'materials_included_in_course',
  23. 'materials_installments_allowed',
  24. 'materials_max_installments',
  25. 'total_value',
  26. 'total_max_installments',
  27. ];
  28. /**
  29. * Create or update the pricing block owned by $ownerId (a class_package or
  30. * class_package_unit row), plus its material lines.
  31. */
  32. public function upsert(
  33. string $modelClass,
  34. string $productModelClass,
  35. string $ownerColumn,
  36. int $ownerId,
  37. array $data,
  38. bool $withCondition = false,
  39. ): Model {
  40. $materials = $data['materials'] ?? [];
  41. $attributes = array_intersect_key($data, array_flip($this->fields($withCondition)));
  42. $attributes['materials_total_value'] = $this->calcMaterialValue($materials);
  43. /** @var Model $proposal */
  44. $proposal = $modelClass::updateOrCreate([$ownerColumn => $ownerId], $attributes);
  45. $this->syncMaterials($proposal, $productModelClass, $materials);
  46. return $proposal->fresh('products.product');
  47. }
  48. /**
  49. * Clone a base-level Pavão/Irrecusável proposal (with materials) into a
  50. * unit-level copy. Only called at replication time (new unit, or new
  51. * proposal replicated to existing units) — unit copies are independent
  52. * afterwards and are never overwritten by later base updates.
  53. */
  54. public function cloneToUnit(
  55. ?Model $baseProposal,
  56. string $unitModelClass,
  57. string $unitProductModelClass,
  58. string $unitOwnerColumn,
  59. int $unitOwnerId,
  60. string $baseForeignKeyColumn,
  61. bool $withCondition = false,
  62. ): void {
  63. if (!$baseProposal) return;
  64. $attributes = $baseProposal->only($this->fields($withCondition));
  65. $attributes['materials_total_value'] = $baseProposal->materials_total_value;
  66. $attributes[$unitOwnerColumn] = $unitOwnerId;
  67. $attributes[$baseForeignKeyColumn] = $baseProposal->id;
  68. /** @var Model $unitProposal */
  69. $unitProposal = $unitModelClass::create($attributes);
  70. $baseProposal->products->each(function ($product) use ($unitProposal, $unitProductModelClass) {
  71. $unitProductModelClass::create([
  72. $this->foreignKeyFor($unitProposal) => $unitProposal->id,
  73. 'product_id' => $product->product_id,
  74. 'quantity' => $product->quantity,
  75. 'price' => $product->price,
  76. ]);
  77. });
  78. }
  79. /**
  80. * Remove the pricing block for $ownerId, if one exists — used when the
  81. * user turns a Pavão/Irrecusável section off. Only ever touches the
  82. * owner's own row (base or unit); never cascades to already-cloned copies.
  83. */
  84. public function deleteFor(string $modelClass, string $ownerColumn, int $ownerId): void
  85. {
  86. $modelClass::where($ownerColumn, $ownerId)->delete();
  87. }
  88. private function fields(bool $withCondition): array
  89. {
  90. return $withCondition ? [...self::VALUE_FIELDS, 'condition'] : self::VALUE_FIELDS;
  91. }
  92. private function syncMaterials(Model $proposal, string $productModelClass, array $materials): void
  93. {
  94. $proposal->products()->delete();
  95. foreach ($materials as $material) {
  96. $productModelClass::create([
  97. $this->foreignKeyFor($proposal) => $proposal->id,
  98. 'product_id' => $material['product_id'],
  99. 'quantity' => $material['quantity'],
  100. 'price' => $material['price'],
  101. ]);
  102. }
  103. }
  104. private function foreignKeyFor(Model $model): string
  105. {
  106. return Str::snake(class_basename($model)) . '_id';
  107. }
  108. private function calcMaterialValue(array $materials): float
  109. {
  110. return array_reduce($materials, fn($carry, $m) => $carry + ($m['quantity'] * $m['price']), 0.0);
  111. }
  112. }