Sfoglia il codice sorgente

refactor: remove contract value fields from ClassPackage and ClassPackageUnit

- Removed contract_value, contract_material_value, contract_register_value, and contrat_discount_value from ClassPackage and ClassPackageUnit models, requests, resources, and services.
- Updated validation rules to reflect the removal of these fields.
- Adjusted the ClassPackageService and ClassPackageUnitService to handle pricing through Pavao and Irrecusável proposals only.
- Deleted ClassPackageUnitProduct model and its associated database table.
- Created a migration to drop the value fields from the class_packages and class_package_units tables.
- Updated documentation to reflect changes in the proposal structure and pricing logic.
alvesantos 7 ore fa
parent
commit
dd707f5e7a

+ 2 - 10
app/Http/Requests/ClassPackageRequest.php

@@ -17,9 +17,6 @@ public function rules(): array
         return [
             'name'                    => "$required|string|max:255",
             'quantity_classes'        => "$required|integer|min:1",
-            'contract_value'          => 'nullable|numeric|min:0',
-            'contract_register_value' => 'nullable|numeric|min:0',
-            'contrat_discount_value'  => 'nullable|numeric|min:0',
             'class_duration_minutes'  => "$required|integer|min:1|max:1440",
 
             // 'weekday'           => 'nullable|integer|min:0|max:6|required_with:start_time',
@@ -27,11 +24,6 @@ public function rules(): array
             // 'second_weekday'    => 'nullable|integer|min:0|max:6|different:weekday|required_with:second_start_time',
             // 'second_start_time' => 'nullable|date_format:H:i|required_with:second_weekday',
 
-            'materials'              => 'nullable|array',
-            'materials.*.product_id' => 'required_with:materials|integer|exists:products,id',
-            'materials.*.quantity'   => 'required_with:materials|integer|min:1',
-            'materials.*.price'      => 'required_with:materials|numeric|min:0',
-
             'group_ids'   => 'nullable|array',
             'group_ids.*' => 'integer|exists:groups,id',
 
@@ -39,8 +31,8 @@ public function rules(): array
             'unit_visibilities.*.unit_id' => 'required_with:unit_visibilities|integer|exists:units,id',
             'unit_visibilities.*.visible' => 'required_with:unit_visibilities|boolean',
 
-            ...$this->proposalPricingRules('pavao', $required),
-            ...$this->proposalPricingRules('irrecusavel', $required, withCondition: true),
+            ...$this->proposalPricingRules('pavao'),
+            ...$this->proposalPricingRules('irrecusavel', withCondition: true),
         ];
     }
 

+ 2 - 10
app/Http/Requests/ClassPackageUnitRequest.php

@@ -17,9 +17,6 @@ public function rules(): array
         return [
             'name'                    => "$required|string|max:255",
             'quantity_classes'        => "$required|integer|min:1",
-            'contract_value'          => 'nullable|numeric|min:0',
-            'contract_register_value' => 'nullable|numeric|min:0',
-            'contrat_discount_value'  => 'nullable|numeric|min:0',
             'visible'                 => 'sometimes|boolean',
             'class_duration_minutes'  => "$required|integer|min:1|max:1440",
 
@@ -28,13 +25,8 @@ public function rules(): array
             // 'second_weekday'    => 'nullable|integer|min:0|max:6|different:weekday|required_with:second_start_time',
             // 'second_start_time' => 'nullable|date_format:H:i|required_with:second_weekday',
 
-            'materials'              => 'nullable|array',
-            'materials.*.product_id' => 'required_with:materials|integer|exists:products,id',
-            'materials.*.quantity'   => 'required_with:materials|integer|min:1',
-            'materials.*.price'      => 'required_with:materials|numeric|min:0',
-
-            ...$this->proposalPricingRules('pavao', $required),
-            ...$this->proposalPricingRules('irrecusavel', $required, withCondition: true),
+            ...$this->proposalPricingRules('pavao'),
+            ...$this->proposalPricingRules('irrecusavel', withCondition: true),
         ];
     }
 

+ 18 - 13
app/Http/Requests/Concerns/ValidatesProposalPricing.php

@@ -7,22 +7,26 @@
 trait ValidatesProposalPricing
 {
     /**
-     * Validation rules for a Pavão/Irrecusável pricing block (Matrícula, Aulas,
-     * Materiais, Valor Total do Curso), nested under $prefix ("pavao"/"irrecusavel").
+     * Validation rules for an optional Pavão/Irrecusável pricing block
+     * (Matrícula, Aulas, Materiais, Valor Total do Curso), nested under
+     * $prefix ("pavao"/"irrecusavel"). The block itself is optional — a
+     * proposal can have Pavão, Irrecusável, both, or neither yet. Sending it
+     * as `null` means "this proposal doesn't have this modality" (removes an
+     * existing one); sending an object requires every sub-field.
      */
-    protected function proposalPricingRules(string $prefix, string $required, bool $withCondition = false): array
+    protected function proposalPricingRules(string $prefix, bool $withCondition = false): array
     {
         $rules = [
-            $prefix => "$required|array",
+            $prefix => 'sometimes|nullable|array',
 
             "$prefix.registration_value"                => 'nullable|numeric|min:0',
-            "$prefix.registration_included_in_course"   => "$required|boolean",
-            "$prefix.registration_installments_allowed" => "$required|boolean",
+            "$prefix.registration_included_in_course"   => "required_with:$prefix|boolean",
+            "$prefix.registration_installments_allowed" => "required_with:$prefix|boolean",
             "$prefix.registration_max_installments"     => 'nullable|integer|min:1',
 
             "$prefix.classes_value"                => 'nullable|numeric|min:0',
-            "$prefix.classes_included_in_course"   => "$required|boolean",
-            "$prefix.classes_installments_allowed" => "$required|boolean",
+            "$prefix.classes_included_in_course"   => "required_with:$prefix|boolean",
+            "$prefix.classes_installments_allowed" => "required_with:$prefix|boolean",
             "$prefix.classes_max_installments"     => 'nullable|integer|min:1',
             "$prefix.classes_discount_percentage"  => 'nullable|numeric|min:0|max:100',
 
@@ -30,8 +34,8 @@ protected function proposalPricingRules(string $prefix, string $required, bool $
             "$prefix.materials.*.product_id" => "required_with:$prefix.materials|integer|exists:products,id",
             "$prefix.materials.*.quantity"   => "required_with:$prefix.materials|integer|min:1",
             "$prefix.materials.*.price"      => "required_with:$prefix.materials|numeric|min:0",
-            "$prefix.materials_included_in_course"   => "$required|boolean",
-            "$prefix.materials_installments_allowed" => "$required|boolean",
+            "$prefix.materials_included_in_course"   => "required_with:$prefix|boolean",
+            "$prefix.materials_installments_allowed" => "required_with:$prefix|boolean",
             "$prefix.materials_max_installments"     => 'nullable|integer|min:1',
 
             "$prefix.total_value"            => 'nullable|numeric|min:0',
@@ -46,14 +50,15 @@ protected function proposalPricingRules(string $prefix, string $required, bool $
     }
 
     /**
-     * For each pricing block present in the request, enforce that exactly one of
-     * "included in course" / "allows installments" is selected per section.
+     * For each pricing block actually sent (present and not null), enforce
+     * that exactly one of "included in course" / "allows installments" is
+     * selected per section.
      */
     protected function validateProposalPricingSections(Validator $validator, array $prefixes): void
     {
         $validator->after(function (Validator $validator) use ($prefixes) {
             foreach ($prefixes as $prefix) {
-                if (!$this->has($prefix)) continue;
+                if (!$this->has($prefix) || $this->input($prefix) === null) continue;
 
                 foreach (['registration', 'classes', 'materials'] as $section) {
                     $includedKey     = "$prefix.{$section}_included_in_course";

+ 0 - 13
app/Http/Resources/ClassPackageResource.php

@@ -19,10 +19,6 @@ public function toArray(Request $request): array
             'id'                      => $this->id,
             'name'                    => $this->name,
             'quantity_classes'        => $this->quantity_classes,
-            'contract_value'          => $this->contract_value,
-            'contract_material_value' => $this->contract_material_value,
-            'contract_register_value' => $this->contract_register_value,
-            'contrat_discount_value'  => $this->contrat_discount_value,
             'class_duration_minutes'  => $this->class_duration_minutes,
             'weekday'                 => $this->weekday,
             'start_time'              => $this->start_time,
@@ -31,15 +27,6 @@ public function toArray(Request $request): array
             'created_at'              => Carbon::parse($this->created_at)->format('Y-m-d H:i:s'),
             'updated_at'              => Carbon::parse($this->updated_at)->format('Y-m-d H:i:s'),
 
-            'materials' => $this->whenLoaded('products', fn() =>
-                $this->products->map(fn($product) => [
-                    'product_id' => $product->id,
-                    'name'       => $product->name,
-                    'quantity'   => $product->pivot->quantity,
-                    'price'      => (float) $product->pivot->price,
-                ])
-            ),
-
             'unit_visibilities' => $this->whenLoaded('unitPackages', fn() =>
                 $this->unitPackages->map(fn($up) => [
                     'unit_id' => $up->unit_id,

+ 0 - 13
app/Http/Resources/ClassPackageUnitResource.php

@@ -19,10 +19,6 @@ public function toArray(Request $request): array
             'class_package_id'        => $this->class_package_id,
             'name'                    => $this->name,
             'quantity_classes'        => $this->quantity_classes,
-            'contract_value'          => $this->contract_value,
-            'contract_material_value' => $this->contract_material_value,
-            'contract_register_value' => $this->contract_register_value,
-            'contrat_discount_value'  => $this->contrat_discount_value,
             'visible'                 => $this->visible,
             'class_duration_minutes'  => $this->class_duration_minutes,
             'weekday'                 => $this->weekday,
@@ -32,15 +28,6 @@ public function toArray(Request $request): array
             'created_at'              => Carbon::parse($this->created_at)->format('Y-m-d H:i:s'),
             'updated_at'              => Carbon::parse($this->updated_at)->format('Y-m-d H:i:s'),
 
-            'materials' => $this->whenLoaded('products', fn() =>
-                $this->products->map(fn($item) => [
-                    'product_id' => $item->product_id,
-                    'name'       => $item->product?->name,
-                    'quantity'   => $item->quantity,
-                    'price'      => (float) $item->price,
-                ])
-            ),
-
             'pavao'       => $this->whenLoaded('pavaoProposalUnit', fn() => $this->proposalPricingArray($this->pavaoProposalUnit)),
             'irrecusavel' => $this->whenLoaded('irrecusableProposalUnit', fn() => $this->proposalPricingArray($this->irrecusableProposalUnit, withCondition: true)),
         ];

+ 0 - 2
app/Http/Resources/Select/PackageSelectResource.php

@@ -13,8 +13,6 @@ public function toArray(Request $request): array
             'id' => $this->id,
             'name' => $this->name,
             'quantity_classes' => $this->quantity_classes,
-            'contract_value' => $this->contract_value,
-            'contract_register_value' => $this->contract_register_value,
             'class_duration_minutes' => $this->class_duration_minutes,
             'weekday' => $this->weekday,
             'start_time' => $this->start_time,

+ 2 - 21
app/Models/ClassPackage.php

@@ -14,10 +14,6 @@
  * @property int $id
  * @property string $name
  * @property int $quantity_classes
- * @property float|null $contract_value
- * @property float $contract_material_value
- * @property float|null $contract_register_value
- * @property float|null $contrat_discount_value
  * @property \Illuminate\Support\Carbon|null $created_at
  * @property \Illuminate\Support\Carbon|null $updated_at
  * @property \Illuminate\Support\Carbon|null $deleted_at
@@ -28,19 +24,15 @@
  * @property string|null $second_start_time
  * @property-read \Illuminate\Database\Eloquent\Collection<int, Group> $groups
  * @property-read int|null $groups_count
- * @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Product> $products
- * @property-read int|null $products_count
  * @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ClassPackageUnit> $unitPackages
  * @property-read int|null $unit_packages_count
+ * @property-read \App\Models\PavaoProposal|null $pavaoProposal
+ * @property-read \App\Models\IrrecusableProposal|null $irrecusableProposal
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackage newModelQuery()
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackage newQuery()
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackage onlyTrashed()
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackage query()
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackage whereClassDurationMinutes($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackage whereContractMaterialValue($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackage whereContractRegisterValue($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackage whereContractValue($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackage whereContratDiscountValue($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackage whereCreatedAt($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackage whereDeletedAt($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackage whereId($value)
@@ -65,10 +57,6 @@ class ClassPackage extends Model
 
     protected $casts = [
         'quantity_classes'        => 'integer',
-        'contract_value'          => 'float',
-        'contract_material_value' => 'float',
-        'contract_register_value' => 'float',
-        'contrat_discount_value'  => 'float',
         'class_duration_minutes'  => 'integer',
         'weekday'                 => 'integer',
         'second_weekday'          => 'integer',
@@ -81,13 +69,6 @@ public function unitPackages(): HasMany
         return $this->hasMany(ClassPackageUnit::class);
     }
 
-    public function products(): BelongsToMany
-    {
-        return $this->belongsToMany(Product::class, 'class_package_products')
-            ->withPivot(['quantity', 'price'])
-            ->withTimestamps();
-    }
-
     public function groups(): BelongsToMany
     {
         return $this->belongsToMany(Group::class, 'class_package_groups');

+ 2 - 20
app/Models/ClassPackageUnit.php

@@ -5,7 +5,6 @@
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
-use Illuminate\Database\Eloquent\Relations\HasMany;
 use Illuminate\Database\Eloquent\Relations\HasOne;
 use Illuminate\Database\Eloquent\SoftDeletes;
 
@@ -15,10 +14,6 @@
  * @property int|null $class_package_id
  * @property string $name
  * @property int $quantity_classes
- * @property float|null $contract_value
- * @property float $contract_material_value
- * @property float|null $contract_register_value
- * @property float|null $contrat_discount_value
  * @property bool $visible
  * @property \Illuminate\Support\Carbon|null $created_at
  * @property \Illuminate\Support\Carbon|null $updated_at
@@ -29,19 +24,15 @@
  * @property int|null $second_weekday
  * @property string|null $second_start_time
  * @property-read \App\Models\ClassPackage|null $basePackage
- * @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ClassPackageUnitProduct> $products
- * @property-read int|null $products_count
  * @property-read \App\Models\Unit $unit
+ * @property-read \App\Models\PavaoProposalUnit|null $pavaoProposalUnit
+ * @property-read \App\Models\IrrecusableProposalUnit|null $irrecusableProposalUnit
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackageUnit newModelQuery()
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackageUnit newQuery()
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackageUnit onlyTrashed()
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackageUnit query()
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackageUnit whereClassDurationMinutes($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackageUnit whereClassPackageId($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackageUnit whereContractMaterialValue($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackageUnit whereContractRegisterValue($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackageUnit whereContractValue($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackageUnit whereContratDiscountValue($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackageUnit whereCreatedAt($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackageUnit whereDeletedAt($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackageUnit whereId($value)
@@ -68,10 +59,6 @@ class ClassPackageUnit extends Model
 
     protected $casts = [
         'quantity_classes'        => 'integer',
-        'contract_value'          => 'float',
-        'contract_material_value' => 'float',
-        'contract_register_value' => 'float',
-        'contrat_discount_value'  => 'float',
         'visible'                 => 'boolean',
         'class_duration_minutes'  => 'integer',
         'weekday'                 => 'integer',
@@ -90,11 +77,6 @@ public function basePackage(): BelongsTo
         return $this->belongsTo(ClassPackage::class, 'class_package_id');
     }
 
-    public function products(): HasMany
-    {
-        return $this->hasMany(ClassPackageUnitProduct::class);
-    }
-
     public function pavaoProposalUnit(): HasOne
     {
         return $this->hasOne(PavaoProposalUnit::class);

+ 0 - 55
app/Models/ClassPackageUnitProduct.php

@@ -1,55 +0,0 @@
-<?php
-
-namespace App\Models;
-
-use Illuminate\Database\Eloquent\Factories\HasFactory;
-use Illuminate\Database\Eloquent\Model;
-use Illuminate\Database\Eloquent\Relations\BelongsTo;
-
-/**
- * @property int $id
- * @property int $class_package_unit_id
- * @property int $product_id
- * @property int $quantity
- * @property float $price
- * @property \Illuminate\Support\Carbon|null $created_at
- * @property \Illuminate\Support\Carbon|null $updated_at
- * @property-read \App\Models\ClassPackageUnit $packageUnit
- * @property-read \App\Models\Product $product
- * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackageUnitProduct newModelQuery()
- * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackageUnitProduct newQuery()
- * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackageUnitProduct query()
- * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackageUnitProduct whereClassPackageUnitId($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackageUnitProduct whereCreatedAt($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackageUnitProduct whereId($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackageUnitProduct wherePrice($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackageUnitProduct whereProductId($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackageUnitProduct whereQuantity($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|ClassPackageUnitProduct whereUpdatedAt($value)
- * @mixin \Eloquent
- */
-class ClassPackageUnitProduct extends Model
-{
-    use HasFactory;
-
-    protected $table = 'class_package_unit_products';
-
-    protected $guarded = ['id'];
-
-    protected $casts = [
-        'quantity'   => 'integer',
-        'price'      => 'float',
-        'created_at' => 'datetime',
-        'updated_at' => 'datetime',
-    ];
-
-    public function packageUnit(): BelongsTo
-    {
-        return $this->belongsTo(ClassPackageUnit::class, 'class_package_unit_id');
-    }
-
-    public function product(): BelongsTo
-    {
-        return $this->belongsTo(Product::class);
-    }
-}

+ 38 - 59
app/Services/ClassPackageService.php

@@ -14,6 +14,13 @@
 
 class ClassPackageService
 {
+    private const EAGER_LOAD = [
+        'unitPackages',
+        'groups',
+        'pavaoProposal.products.product',
+        'irrecusableProposal.products.product',
+    ];
+
     public function __construct(
         protected ClassPackageUnitService $unitService,
         protected ProposalPricingService $pricingService,
@@ -21,34 +28,34 @@ public function __construct(
 
     public function getAll(): Collection
     {
-        return ClassPackage::with('products')->orderBy('created_at', 'desc')->get();
+        return ClassPackage::with(['pavaoProposal', 'irrecusableProposal'])->orderBy('created_at', 'desc')->get();
     }
 
     public function findById(int $id): ?ClassPackage
     {
-        return ClassPackage::with(['products', 'unitPackages', 'groups', 'pavaoProposal.products.product', 'irrecusableProposal.products.product'])->find($id);
+        return ClassPackage::with(self::EAGER_LOAD)->find($id);
     }
 
     public function create(array $data): ClassPackage
     {
-        $materials        = $data['materials'] ?? [];
         $unitVisibilities = $data['unit_visibilities'] ?? null;
         $groupIds         = $data['group_ids'] ?? [];
-        $pavao            = $data['pavao'] ?? [];
-        $irrecusavel      = $data['irrecusavel'] ?? [];
-        unset($data['materials'], $data['unit_visibilities'], $data['group_ids'], $data['pavao'], $data['irrecusavel']);
-
-        $data['contract_material_value'] = $this->calcMaterialValue($materials);
+        $pavao            = array_key_exists('pavao', $data) ? $data['pavao'] : null;
+        $irrecusavel      = array_key_exists('irrecusavel', $data) ? $data['irrecusavel'] : null;
+        unset($data['unit_visibilities'], $data['group_ids'], $data['pavao'], $data['irrecusavel']);
 
         $package = ClassPackage::create($data);
 
-        $this->syncMaterials($package, $materials);
+        if ($pavao !== null) {
+            $this->pricingService->upsert(PavaoProposal::class, PavaoProposalProduct::class, 'class_package_id', $package->id, $pavao);
+        }
 
-        $this->pricingService->upsert(PavaoProposal::class, PavaoProposalProduct::class, 'class_package_id', $package->id, $pavao);
-        $this->pricingService->upsert(IrrecusableProposal::class, IrrecusableProposalProduct::class, 'class_package_id', $package->id, $irrecusavel, withCondition: true);
+        if ($irrecusavel !== null) {
+            $this->pricingService->upsert(IrrecusableProposal::class, IrrecusableProposalProduct::class, 'class_package_id', $package->id, $irrecusavel, withCondition: true);
+        }
 
         // Replicate to all units (all start visible = true)
-        $this->replicateToAllUnits($package->load(['products', 'pavaoProposal.products', 'irrecusableProposal.products']));
+        $this->replicateToAllUnits($package->load(['pavaoProposal.products', 'irrecusableProposal.products']));
 
         // Sync selected groups
         if (!empty($groupIds)) {
@@ -58,7 +65,7 @@ public function create(array $data): ClassPackage
         // Apply visibility overrides
         $this->applyVisibilityOverrides($package, $unitVisibilities, $groupIds);
 
-        return $package->load(['products', 'unitPackages', 'groups', 'pavaoProposal.products.product', 'irrecusableProposal.products.product']);
+        return $package->load(self::EAGER_LOAD);
     }
 
     public function update(int $id, array $data): ?ClassPackage
@@ -66,16 +73,13 @@ public function update(int $id, array $data): ?ClassPackage
         $package = $this->findById($id);
         if (!$package) return null;
 
-        $materials        = $data['materials'] ?? null;
         $unitVisibilities = $data['unit_visibilities'] ?? null;
         $groupIds         = array_key_exists('group_ids', $data) ? ($data['group_ids'] ?? []) : null;
-        $pavao            = $data['pavao'] ?? null;
-        $irrecusavel      = $data['irrecusavel'] ?? null;
-        unset($data['materials'], $data['unit_visibilities'], $data['group_ids'], $data['pavao'], $data['irrecusavel']);
-
-        if ($materials !== null) {
-            $data['contract_material_value'] = $this->calcMaterialValue($materials);
-        }
+        $pavaoProvided       = array_key_exists('pavao', $data);
+        $irrecusavelProvided = array_key_exists('irrecusavel', $data);
+        $pavao       = $data['pavao'] ?? null;
+        $irrecusavel = $data['irrecusavel'] ?? null;
+        unset($data['unit_visibilities'], $data['group_ids'], $data['pavao'], $data['irrecusavel']);
 
         $package->update($data);
 
@@ -90,19 +94,21 @@ public function update(int $id, array $data): ?ClassPackage
             ClassPackageUnit::where('class_package_id', $package->id)->update($scheduleFields);
         }
 
-        if ($materials !== null) {
-            $this->syncMaterials($package, $materials);
-            // Propagate material changes to unit packages
-            $this->syncUnitPackageProducts($package->load('products'));
-        }
-
         // Pavão/Irrecusável updates never propagate to already-cloned unit copies.
-        if ($pavao !== null) {
-            $this->pricingService->upsert(PavaoProposal::class, PavaoProposalProduct::class, 'class_package_id', $package->id, $pavao);
+        if ($pavaoProvided) {
+            if ($pavao !== null) {
+                $this->pricingService->upsert(PavaoProposal::class, PavaoProposalProduct::class, 'class_package_id', $package->id, $pavao);
+            } else {
+                $this->pricingService->deleteFor(PavaoProposal::class, 'class_package_id', $package->id);
+            }
         }
 
-        if ($irrecusavel !== null) {
-            $this->pricingService->upsert(IrrecusableProposal::class, IrrecusableProposalProduct::class, 'class_package_id', $package->id, $irrecusavel, withCondition: true);
+        if ($irrecusavelProvided) {
+            if ($irrecusavel !== null) {
+                $this->pricingService->upsert(IrrecusableProposal::class, IrrecusableProposalProduct::class, 'class_package_id', $package->id, $irrecusavel, withCondition: true);
+            } else {
+                $this->pricingService->deleteFor(IrrecusableProposal::class, 'class_package_id', $package->id);
+            }
         }
 
         if ($groupIds !== null) {
@@ -116,7 +122,7 @@ public function update(int $id, array $data): ?ClassPackage
             $this->applyVisibilityOverrides($package, $unitVisibilities, $currentGroupIds);
         }
 
-        return $package->fresh(['products', 'unitPackages', 'groups', 'pavaoProposal.products.product', 'irrecusableProposal.products.product']);
+        return $package->fresh(self::EAGER_LOAD);
     }
 
     public function delete(int $id): bool
@@ -179,31 +185,4 @@ private function applyVisibilityOverrides(ClassPackage $package, ?array $unitVis
             }
         }
     }
-
-    /**
-     * When materials are updated, propagate product changes to all ClassPackageUnit records.
-     */
-    private function syncUnitPackageProducts(ClassPackage $package): void
-    {
-        ClassPackageUnit::where('class_package_id', $package->id)->each(function (ClassPackageUnit $up) use ($package) {
-            $this->unitService->syncProductsFromBasePackage($up, $package);
-        });
-    }
-
-    private function syncMaterials(ClassPackage $package, array $materials): void
-    {
-        $syncData = [];
-        foreach ($materials as $m) {
-            $syncData[$m['product_id']] = [
-                'quantity' => $m['quantity'],
-                'price'    => $m['price'],
-            ];
-        }
-        $package->products()->sync($syncData);
-    }
-
-    private function calcMaterialValue(array $materials): float
-    {
-        return array_reduce($materials, fn($carry, $m) => $carry + ($m['quantity'] * $m['price']), 0.0);
-    }
 }

+ 26 - 75
app/Services/ClassPackageUnitService.php

@@ -2,8 +2,8 @@
 
 namespace App\Services;
 
+use App\Models\ClassPackage;
 use App\Models\ClassPackageUnit;
-use App\Models\ClassPackageUnitProduct;
 use App\Models\IrrecusableProposalUnit;
 use App\Models\IrrecusableProposalUnitProduct;
 use App\Models\PavaoProposalUnit;
@@ -17,7 +17,6 @@ public function __construct(
     ) {}
 
     private const EAGER_LOAD = [
-        'products.product',
         'pavaoProposalUnit.products.product',
         'irrecusableProposalUnit.products.product',
     ];
@@ -48,19 +47,19 @@ public function findByIdForUnit(int $id, int $unitId): ClassPackageUnit
 
     public function create(array $data): ClassPackageUnit
     {
-        $materials   = $data['materials'] ?? [];
-        $pavao       = $data['pavao'] ?? [];
-        $irrecusavel = $data['irrecusavel'] ?? [];
-        unset($data['materials'], $data['pavao'], $data['irrecusavel']);
-
-        $data['contract_material_value'] = $this->calcMaterialValue($materials);
+        $pavao       = array_key_exists('pavao', $data) ? $data['pavao'] : null;
+        $irrecusavel = array_key_exists('irrecusavel', $data) ? $data['irrecusavel'] : null;
+        unset($data['pavao'], $data['irrecusavel']);
 
         $packageUnit = ClassPackageUnit::create($data);
 
-        $this->syncProducts($packageUnit, $materials);
+        if ($pavao !== null) {
+            $this->pricingService->upsert(PavaoProposalUnit::class, PavaoProposalUnitProduct::class, 'class_package_unit_id', $packageUnit->id, $pavao);
+        }
 
-        $this->pricingService->upsert(PavaoProposalUnit::class, PavaoProposalUnitProduct::class, 'class_package_unit_id', $packageUnit->id, $pavao);
-        $this->pricingService->upsert(IrrecusableProposalUnit::class, IrrecusableProposalUnitProduct::class, 'class_package_unit_id', $packageUnit->id, $irrecusavel, withCondition: true);
+        if ($irrecusavel !== null) {
+            $this->pricingService->upsert(IrrecusableProposalUnit::class, IrrecusableProposalUnitProduct::class, 'class_package_unit_id', $packageUnit->id, $irrecusavel, withCondition: true);
+        }
 
         return $packageUnit->load(self::EAGER_LOAD);
     }
@@ -69,28 +68,29 @@ public function update(int $id, int $unitId, array $data): ClassPackageUnit
     {
         $packageUnit = $this->findByIdForUnit($id, $unitId);
 
-        $materials   = $data['materials'] ?? null;
+        $pavaoProvided       = array_key_exists('pavao', $data);
+        $irrecusavelProvided = array_key_exists('irrecusavel', $data);
         $pavao       = $data['pavao'] ?? null;
         $irrecusavel = $data['irrecusavel'] ?? null;
-        unset($data['materials'], $data['pavao'], $data['irrecusavel']);
-
-        if ($materials !== null) {
-            $data['contract_material_value'] = $this->calcMaterialValue($materials);
-        }
+        unset($data['pavao'], $data['irrecusavel']);
 
         $packageUnit->update($data);
 
-        if ($materials !== null) {
-            $this->syncProducts($packageUnit, $materials);
-        }
-
         // Edits here only affect this unit's own copy — never propagate up to the base package.
-        if ($pavao !== null) {
-            $this->pricingService->upsert(PavaoProposalUnit::class, PavaoProposalUnitProduct::class, 'class_package_unit_id', $packageUnit->id, $pavao);
+        if ($pavaoProvided) {
+            if ($pavao !== null) {
+                $this->pricingService->upsert(PavaoProposalUnit::class, PavaoProposalUnitProduct::class, 'class_package_unit_id', $packageUnit->id, $pavao);
+            } else {
+                $this->pricingService->deleteFor(PavaoProposalUnit::class, 'class_package_unit_id', $packageUnit->id);
+            }
         }
 
-        if ($irrecusavel !== null) {
-            $this->pricingService->upsert(IrrecusableProposalUnit::class, IrrecusableProposalUnitProduct::class, 'class_package_unit_id', $packageUnit->id, $irrecusavel, withCondition: true);
+        if ($irrecusavelProvided) {
+            if ($irrecusavel !== null) {
+                $this->pricingService->upsert(IrrecusableProposalUnit::class, IrrecusableProposalUnitProduct::class, 'class_package_unit_id', $packageUnit->id, $irrecusavel, withCondition: true);
+            } else {
+                $this->pricingService->deleteFor(IrrecusableProposalUnit::class, 'class_package_unit_id', $packageUnit->id);
+            }
         }
 
         return $packageUnit->fresh(self::EAGER_LOAD);
@@ -112,34 +112,13 @@ public function delete(int $id, int $unitId): bool
         return $packageUnit->delete();
     }
 
-    /**
-     * Sync products on an existing ClassPackageUnit from the base package.
-     */
-    public function syncProductsFromBasePackage(ClassPackageUnit $packageUnit, \App\Models\ClassPackage $basePackage): void
-    {
-        $packageUnit->products()->delete();
-
-        $basePackage->products->each(function ($product) use ($packageUnit) {
-            ClassPackageUnitProduct::create([
-                'class_package_unit_id' => $packageUnit->id,
-                'product_id'            => $product->id,
-                'quantity'              => $product->pivot->quantity,
-                'price'                 => $product->pivot->price,
-            ]);
-        });
-    }
-
-    public function replicateFromBasePackage(int $unitId, \App\Models\ClassPackage $basePackage): ClassPackageUnit
+    public function replicateFromBasePackage(int $unitId, ClassPackage $basePackage): ClassPackageUnit
     {
         $packageUnit = ClassPackageUnit::create([
             'unit_id'                 => $unitId,
             'class_package_id'        => $basePackage->id,
             'name'                    => $basePackage->name,
             'quantity_classes'        => $basePackage->quantity_classes,
-            'contract_value'          => $basePackage->contract_value,
-            'contract_material_value' => $basePackage->contract_material_value,
-            'contract_register_value' => $basePackage->contract_register_value,
-            'contrat_discount_value'  => $basePackage->contrat_discount_value,
             'class_duration_minutes'  => $basePackage->class_duration_minutes,
             'weekday'                 => $basePackage->weekday,
             'start_time'              => $basePackage->start_time,
@@ -148,15 +127,6 @@ public function replicateFromBasePackage(int $unitId, \App\Models\ClassPackage $
             'visible'                 => true,
         ]);
 
-        $basePackage->products->each(function ($product) use ($packageUnit) {
-            ClassPackageUnitProduct::create([
-                'class_package_unit_id' => $packageUnit->id,
-                'product_id'            => $product->id,
-                'quantity'              => $product->pivot->quantity,
-                'price'                 => $product->pivot->price,
-            ]);
-        });
-
         $this->pricingService->cloneToUnit(
             $basePackage->pavaoProposal,
             PavaoProposalUnit::class,
@@ -178,23 +148,4 @@ public function replicateFromBasePackage(int $unitId, \App\Models\ClassPackage $
 
         return $packageUnit;
     }
-
-    private function syncProducts(ClassPackageUnit $packageUnit, array $materials): void
-    {
-        $packageUnit->products()->delete();
-
-        foreach ($materials as $m) {
-            ClassPackageUnitProduct::create([
-                'class_package_unit_id' => $packageUnit->id,
-                'product_id'            => $m['product_id'],
-                'quantity'              => $m['quantity'],
-                'price'                 => $m['price'],
-            ]);
-        }
-    }
-
-    private function calcMaterialValue(array $materials): float
-    {
-        return array_reduce($materials, fn($carry, $m) => $carry + ($m['quantity'] * $m['price']), 0.0);
-    }
 }

+ 10 - 0
app/Services/ProposalPricingService.php

@@ -90,6 +90,16 @@ public function cloneToUnit(
         });
     }
 
+    /**
+     * Remove the pricing block for $ownerId, if one exists — used when the
+     * user turns a Pavão/Irrecusável section off. Only ever touches the
+     * owner's own row (base or unit); never cascades to already-cloned copies.
+     */
+    public function deleteFor(string $modelClass, string $ownerColumn, int $ownerId): void
+    {
+        $modelClass::where($ownerColumn, $ownerId)->delete();
+    }
+
     private function fields(bool $withCondition): array
     {
         return $withCondition ? [...self::VALUE_FIELDS, 'condition'] : self::VALUE_FIELDS;

+ 1 - 1
app/Services/UnitService.php

@@ -115,7 +115,7 @@ public function delete(int $id): bool
 
     private function replicatePackagesToUnit(int $unitId): void
     {
-        ClassPackage::with(['products', 'pavaoProposal.products', 'irrecusableProposal.products'])->get()->each(function (ClassPackage $package) use ($unitId) {
+        ClassPackage::with(['pavaoProposal.products', 'irrecusableProposal.products'])->get()->each(function (ClassPackage $package) use ($unitId) {
             $this->packageUnitService->replicateFromBasePackage($unitId, $package);
         });
     }

+ 72 - 0
database/migrations/2026_08_18_000001_drop_value_fields_from_class_packages.php

@@ -0,0 +1,72 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+/**
+ * Valor e material saem de Dados Básicos (class_packages/class_package_units) —
+ * a partir de agora só existem dentro de Pavão/Irrecusável. Ver docs/proposta_comercial_v2_ajustes.md.
+ */
+return new class extends Migration
+{
+    private const VALUE_COLUMNS = [
+        'contract_value',
+        'contract_material_value',
+        'contract_register_value',
+        'contrat_discount_value',
+    ];
+
+    public function up(): void
+    {
+        Schema::dropIfExists('class_package_unit_products');
+        Schema::dropIfExists('class_package_products');
+
+        Schema::table('class_packages', function (Blueprint $table) {
+            $table->dropColumn(self::VALUE_COLUMNS);
+        });
+
+        Schema::table('class_package_units', function (Blueprint $table) {
+            $table->dropColumn(self::VALUE_COLUMNS);
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('class_packages', function (Blueprint $table) {
+            $table->decimal('contract_value', 10, 2)->nullable();
+            $table->decimal('contract_material_value', 10, 2)->default(0);
+            $table->decimal('contract_register_value', 10, 2)->nullable();
+            $table->decimal('contrat_discount_value', 10, 2)->nullable();
+        });
+
+        Schema::table('class_package_units', function (Blueprint $table) {
+            $table->decimal('contract_value', 10, 2)->nullable();
+            $table->decimal('contract_material_value', 10, 2)->default(0);
+            $table->decimal('contract_register_value', 10, 2)->nullable();
+            $table->decimal('contrat_discount_value', 10, 2)->nullable();
+        });
+
+        Schema::create('class_package_products', function (Blueprint $table) {
+            $table->id();
+            $table->foreignId('class_package_id')->constrained('class_packages')->cascadeOnDelete();
+            $table->foreignId('product_id')->constrained('products')->cascadeOnDelete();
+            $table->integer('quantity')->default(1);
+            $table->decimal('price', 10, 2);
+            $table->timestamps();
+
+            $table->unique(['class_package_id', 'product_id']);
+        });
+
+        Schema::create('class_package_unit_products', function (Blueprint $table) {
+            $table->id();
+            $table->foreignId('class_package_unit_id')->constrained('class_package_units')->cascadeOnDelete();
+            $table->foreignId('product_id')->constrained('products')->cascadeOnDelete();
+            $table->integer('quantity')->default(1);
+            $table->decimal('price', 10, 2);
+            $table->timestamps();
+
+            $table->unique(['class_package_unit_id', 'product_id']);
+        });
+    }
+};

+ 44 - 0
docs/proposta_comercial_v2_ajustes.md

@@ -0,0 +1,44 @@
+# Proposta Comercial — Ajustes v2
+
+- Data Criação do Documento: 18 de Agosto de 2026
+- Autor: Gabriel Alves (Desenvolvedor)
+- Complementa: [proposta_comercial_v1.md](./proposta_comercial_v1.md)
+
+---
+
+## Motivo
+
+Na v1, a tab "Dados" (Dados Básicos da proposta) ainda carregava campos de valor (Matrícula, Total do Contrato, Desconto, Materiais), herdados do antigo módulo de Pacotes. Ficou definido que **valor não deve existir em Dados** — valor só existe dentro de Pavão e Irrecusável, que são as duas modalidades de precificação de uma proposta.
+
+## O que muda em Dados Básicos
+
+Dados Básicos passa a ter **apenas**:
+
+- Nome
+- Quantidade de Aulas
+- Duração da Aula
+
+Saem de Dados: Valor da Matrícula, Valor Total do Contrato, Desconto (%), Materiais. Esses campos e a tabela de materiais da proposta base (`class_package_products`/`class_package_unit_products`) foram removidos — valor e material só existem dentro de Pavão/Irrecusável (`pavao_proposals`/`irrecusable_proposals` e as tabelas `_unit` correspondentes, que já cobrem isso desde a v1).
+
+## Pavão e Irrecusável passam a ser opcionais
+
+Antes, toda proposta era obrigada a ter Pavão **e** Irrecusável. Agora uma proposta pode ter:
+
+- só Pavão,
+- só Irrecusável,
+- os dois,
+- ou nenhum ainda (proposta em rascunho, sem precificação definida).
+
+Cada tab tem um toggle "esta proposta possui Pavão/Irrecusável" — só quando ligado é que os campos daquela modalidade existem e são obrigatórios/validados.
+
+## Regra de negócio para o futuro módulo de Contrato com Aluno
+
+O módulo de Contrato ainda está "Aguardando Definições" (ver v1) e **não foi implementado nesta rodada** — o texto abaixo é o requisito de negócio já levantado, para guiar a implementação quando o Contrato for especificado:
+
+- Ao vincular uma proposta comercial a um contrato, o usuário escolhe **qual modalidade usar** — Pavão ou Irrecusável — dentre as que a proposta tiver disponíveis.
+- Os valores do contrato (Matrícula, Aulas, Materiais, Total) vêm **da modalidade escolhida**, nunca das duas somadas.
+- Para cada valor (Matrícula, Aulas, Materiais), a regra de parcelamento já cadastrada na proposta se aplica da mesma forma:
+  - **Incluso no Valor do Curso** → soma no Valor Total do Curso; gera conta a receber **só** do Valor Total (parcelado conforme os campos de parcelamento do Total), sem cobrança separada para aquele item.
+  - **Permite Parcelar** → **não** soma no Valor Total do Curso; gera conta a receber **separada**, parcelada com base na Quantidade de Parcelas Máximas daquele item — igual à lógica que os contratos já usam hoje para gerar contas a receber.
+- Essa lógica (incluso vs. parcela separada → conta a receber) vale igualmente para Matrícula, Aulas e Materiais.
+- É esperado que, ao detalhar o módulo de Contrato, sejam necessários campos adicionais nas tabelas de Pavão/Irrecusável (ex.: dia de vencimento, regra de geração de conta a receber) — a definir junto com a especificação do Contrato.

+ 25 - 2
tests/Unit/Http/Requests/Concerns/ValidatesProposalPricingTest.php

@@ -110,6 +110,29 @@ public function test_it_validates_pavao_and_irrecusavel_blocks_independently():
         $this->assertTrue($validator->errors()->has('irrecusavel.registration_included_in_course'));
     }
 
+    public function test_it_accepts_an_explicitly_null_block_as_not_having_that_modality(): void
+    {
+        $validator = $this->validatorFor([
+            'pavao' => null,
+        ]);
+
+        $this->assertTrue($validator->passes());
+    }
+
+    public function test_it_requires_the_section_fields_once_the_block_is_sent(): void
+    {
+        $validator = $this->validatorFor([
+            'pavao' => [
+                'registration_value' => 100,
+            ],
+        ]);
+
+        $this->assertFalse($validator->passes());
+        $this->assertTrue($validator->errors()->has('pavao.registration_included_in_course'));
+        $this->assertTrue($validator->errors()->has('pavao.classes_included_in_course'));
+        $this->assertTrue($validator->errors()->has('pavao.materials_included_in_course'));
+    }
+
     private function validatorFor(array $data): Validator
     {
         $request   = TestableProposalPricingRequest::create('/', 'POST', $data);
@@ -128,8 +151,8 @@ class TestableProposalPricingRequest extends FormRequest
     public function rules(): array
     {
         return [
-            ...$this->proposalPricingRules('pavao', 'sometimes'),
-            ...$this->proposalPricingRules('irrecusavel', 'sometimes', withCondition: true),
+            ...$this->proposalPricingRules('pavao'),
+            ...$this->proposalPricingRules('irrecusavel', withCondition: true),
         ];
     }