================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Pricing\Application\Permissions\PricingPermissions.php
================================================================
<?php
namespace App\Modules\Pricing\Application\Permissions;
final class PricingPermissions {
 public const VIEW='pricing.view';
 public const MANAGE='pricing.manage';
 private function __construct(){}
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Pricing\Application\Resolution\EffectivePriceResolver.php
================================================================
<?php
namespace App\Modules\Pricing\Application\Resolution;
use App\Modules\Pricing\Domain\Models\PriceEntry;
use App\Modules\Pricing\Domain\Models\PriceList;
use App\Modules\Pricing\Domain\Models\PriceListScope;

final class EffectivePriceResolver {
 public function resolve(int $tenantId,int $variantId,?int $variantUnitId=null,?int $companyId=null,?int $branchId=null,?string $group=null,?string $customer=null): ?array {
   $now=now();
   $lists=PriceList::query()->where('tenant_id',$tenantId)->where('status','active')
     ->where(fn($q)=>$q->whereNull('effective_from')->orWhere('effective_from','<=',$now))
     ->where(fn($q)=>$q->whereNull('effective_to')->orWhere('effective_to','>',$now))->get();
   $candidates=[];
   foreach($lists as $list){
     $scopes=PriceListScope::query()->where('tenant_id',$tenantId)->where('price_list_id',$list->id)->get();
     $best=null;
     foreach($scopes as $s){
       $score=match($s->scope_type){
         'customer' => $customer!==null && $s->customer_public_id===$customer ? 500:null,
         'customer_group' => $group!==null && $s->customer_group_key===$group ? 400:null,
         'branch' => $branchId!==null && $s->branch_id===$branchId ? 300:null,
         'company' => $companyId!==null && $s->company_id===$companyId ? 200:null,
         'tenant' => 100,
         default => null,
       };
       if($score!==null) $best=max($best??$score,$score);
     }
     if($best===null && !$list->is_default) continue;
     $candidates[]=['list'=>$list,'score'=>$best??0];
   }
   usort($candidates,fn($a,$b)=>$a['score']===$b['score'] ? $a['list']->priority<=>$b['list']->priority : $b['score']<=>$a['score']);
   foreach($candidates as $c){
     $entry=PriceEntry::query()->where('tenant_id',$tenantId)->where('price_list_id',$c['list']->id)->where('variant_id',$variantId)->where('status','active')
       ->where(fn($q)=>$q->whereNull('effective_from')->orWhere('effective_from','<=',$now))
       ->where(fn($q)=>$q->whereNull('effective_to')->orWhere('effective_to','>',$now))
       ->when($variantUnitId===null,fn($q)=>$q->whereNull('variant_unit_id'),fn($q)=>$q->where('variant_unit_id',$variantUnitId))->first();
     if($entry) return ['price_list_id'=>$c['list']->public_id,'price_list_code'=>$c['list']->code,'currency_code'=>$c['list']->currency_code,'price_amount'=>$entry->price_amount,'min_price_amount'=>$entry->min_price_amount,'specificity'=>$c['score']];
   }
   return null;
 }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Pricing\Domain\Models\MarginGuard.php
================================================================
<?php
namespace App\Modules\Pricing\Domain\Models;
use App\Modules\Core\Domain\Concerns\HasPublicUlid;
use Illuminate\Database\Eloquent\Model;
final class MarginGuard extends Model {
 use HasPublicUlid; protected $table='pricing.margin_guards';
 protected $fillable=['tenant_id','scope_type','company_id','branch_id','minimum_margin_percent','minimum_markup_percent','violation_action','approval_action_code','status'];
 protected function casts(): array { return ['minimum_margin_percent'=>'decimal:4','minimum_markup_percent'=>'decimal:4']; }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Pricing\Domain\Models\PriceEntry.php
================================================================
<?php
namespace App\Modules\Pricing\Domain\Models;
use App\Modules\Core\Domain\Concerns\HasPublicUlid;
use Illuminate\Database\Eloquent\Model;
final class PriceEntry extends Model {
 use HasPublicUlid; protected $table='pricing.price_entries';
 protected $fillable=['tenant_id','price_list_id','variant_id','variant_unit_id','price_amount','min_price_amount','effective_from','effective_to','status','metadata'];
 protected function casts(): array { return ['price_amount'=>'decimal:6','min_price_amount'=>'decimal:6','effective_from'=>'immutable_datetime','effective_to'=>'immutable_datetime','metadata'=>'array']; }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Pricing\Domain\Models\PriceList.php
================================================================
<?php
namespace App\Modules\Pricing\Domain\Models;
use App\Modules\Core\Domain\Concerns\HasPublicUlid;
use Illuminate\Database\Eloquent\Model;
final class PriceList extends Model {
 use HasPublicUlid;
 protected $table='pricing.price_lists';
 protected $fillable=['tenant_id','name','code','currency_code','priority','is_default','effective_from','effective_to','status','metadata'];
 protected function casts(): array { return ['priority'=>'integer','is_default'=>'boolean','effective_from'=>'immutable_datetime','effective_to'=>'immutable_datetime','metadata'=>'array']; }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Pricing\Domain\Models\PriceListScope.php
================================================================
<?php
namespace App\Modules\Pricing\Domain\Models;
use App\Modules\Core\Domain\Concerns\HasPublicUlid;
use Illuminate\Database\Eloquent\Model;
final class PriceListScope extends Model {
 use HasPublicUlid; protected $table='pricing.price_list_scopes';
 protected $fillable=['tenant_id','price_list_id','scope_type','company_id','branch_id','customer_group_key','customer_public_id'];
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Pricing\Http\Controllers\EffectivePriceController.php
================================================================
<?php
namespace App\Modules\Pricing\Http\Controllers;
use App\Http\Controllers\Controller; use App\Modules\Catalog\Domain\Models\Variant; use App\Modules\Catalog\Domain\Models\VariantUnit; use App\Modules\Core\Application\Context\OrganizationScopeContext; use App\Modules\Core\Application\Context\TenantContext; use App\Modules\Pricing\Application\Resolution\EffectivePriceResolver; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException;
final class EffectivePriceController extends Controller {
 public function show(string $id,Request $r,TenantContext $t,OrganizationScopeContext $o,EffectivePriceResolver $x): JsonResponse {
  $v=Variant::query()->where('tenant_id',$t->tenantId())->where('public_id',$id)->firstOrFail();$vu=null;
  if($r->query('variant_unit_id')){$u=VariantUnit::query()->where('tenant_id',$t->tenantId())->where('variant_id',$v->id)->where('public_id',$r->query('variant_unit_id'))->first();if(!$u)throw ValidationException::withMessages(['variant_unit_id'=>['Variant unit was not found.']]);$vu=$u->id;}
  $p=$x->resolve($t->tenantId(),$v->id,$vu,$o->companyId(),$o->branchId(),$r->query('customer_group_key'),$r->query('customer_public_id'));
  return $p?response()->json(['data'=>$p]):response()->json(['message'=>'No effective price was found.','code'=>'PRICE_NOT_FOUND'],404);
 }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Pricing\Http\Controllers\MarginGuardController.php
================================================================
<?php

namespace App\Modules\Pricing\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Modules\Audit\Application\AuditRecorder;
use App\Modules\Core\Application\Context\OrganizationScopeContext;
use App\Modules\Core\Application\Context\TenantContext;
use App\Modules\Pricing\Domain\Models\MarginGuard;
use App\Modules\Pricing\Http\Requests\StoreMarginGuardRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Validation\ValidationException;

final class MarginGuardController extends Controller
{
    public function store(
        StoreMarginGuardRequest $request,
        TenantContext $tenantContext,
        OrganizationScopeContext $scopeContext,
        AuditRecorder $auditRecorder,
    ): JsonResponse {
        $validated = $request->validated();

        if (
            ! isset($validated['minimum_margin_percent'])
            && ! isset($validated['minimum_markup_percent'])
        ) {
            throw ValidationException::withMessages([
                'minimum_margin_percent' => ['Provide margin or markup.'],
            ]);
        }

        if (
            $validated['violation_action'] === 'approval'
            && empty($validated['approval_action_code'])
        ) {
            throw ValidationException::withMessages([
                'approval_action_code' => ['Approval action code is required.'],
            ]);
        }

        $companyId = null;
        $branchId = null;

        if ($validated['scope_type'] === 'company') {
            if ($scopeContext->companyId() === null) {
                throw ValidationException::withMessages([
                    'scope_type' => ['X-Company-ID is required.'],
                ]);
            }

            $companyId = $scopeContext->companyId();
        } elseif ($validated['scope_type'] === 'branch') {
            if ($scopeContext->companyId() === null || $scopeContext->branchId() === null) {
                throw ValidationException::withMessages([
                    'scope_type' => ['X-Branch-ID is required.'],
                ]);
            }

            $companyId = $scopeContext->companyId();
            $branchId = $scopeContext->branchId();
        }

        $duplicate = MarginGuard::query()
            ->where('tenant_id', $tenantContext->tenantId())
            ->where('scope_type', $validated['scope_type'])
            ->where('status', 'active')
            ->when($validated['scope_type'] === 'company', fn ($q) => $q->where('company_id', $companyId))
            ->when($validated['scope_type'] === 'branch', fn ($q) => $q->where('branch_id', $branchId))
            ->exists();

        if ($duplicate) {
            throw ValidationException::withMessages([
                'scope_type' => ['An active margin guard already exists for this scope.'],
            ]);
        }

        $guard = MarginGuard::query()->create([
            'tenant_id' => $tenantContext->tenantId(),
            'scope_type' => $validated['scope_type'],
            'company_id' => $companyId,
            'branch_id' => $branchId,
            'minimum_margin_percent' => $validated['minimum_margin_percent'] ?? null,
            'minimum_markup_percent' => $validated['minimum_markup_percent'] ?? null,
            'violation_action' => $validated['violation_action'],
            'approval_action_code' => $validated['approval_action_code'] ?? null,
            'status' => 'active',
        ]);

        $auditRecorder->record(
            'pricing.margin_guard.created',
            $tenantContext->tenantId(),
            $request->user()->id,
            'pricing.margin_guard',
            $guard->public_id,
            after: [
                'scope_type' => $guard->scope_type,
                'violation_action' => $guard->violation_action,
            ],
            request: $request,
        );

        return response()->json([
            'data' => [
                'id' => $guard->public_id,
                'scope_type' => $guard->scope_type,
                'violation_action' => $guard->violation_action,
            ],
        ], 201);
    }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Pricing\Http\Controllers\PriceEntryController.php
================================================================
<?php

namespace App\Modules\Pricing\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Modules\Audit\Application\AuditRecorder;
use App\Modules\Catalog\Domain\Models\Variant;
use App\Modules\Catalog\Domain\Models\VariantUnit;
use App\Modules\Core\Application\Context\TenantContext;
use App\Modules\Pricing\Domain\Models\PriceEntry;
use App\Modules\Pricing\Domain\Models\PriceList;
use App\Modules\Pricing\Http\Requests\StorePriceEntryRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Validation\ValidationException;

final class PriceEntryController extends Controller
{
    public function store(
        string $priceListPublicId,
        StorePriceEntryRequest $request,
        TenantContext $tenantContext,
        AuditRecorder $auditRecorder,
    ): JsonResponse {
        $tenantId = $tenantContext->tenantId();

        $priceList = PriceList::query()
            ->where('tenant_id', $tenantId)
            ->where('public_id', $priceListPublicId)
            ->firstOrFail();

        $validated = $request->validated();

        $variant = Variant::query()
            ->where('tenant_id', $tenantId)
            ->where('public_id', $validated['variant_id'])
            ->first();

        if ($variant === null) {
            throw ValidationException::withMessages([
                'variant_id' => ['Variant was not found.'],
            ]);
        }

        $variantUnitId = null;

        if (! empty($validated['variant_unit_id'])) {
            $variantUnit = VariantUnit::query()
                ->where('tenant_id', $tenantId)
                ->where('variant_id', $variant->id)
                ->where('public_id', $validated['variant_unit_id'])
                ->first();

            if ($variantUnit === null) {
                throw ValidationException::withMessages([
                    'variant_unit_id' => ['Variant unit was not found.'],
                ]);
            }

            $variantUnitId = $variantUnit->id;
        }

        $duplicate = PriceEntry::query()
            ->where('tenant_id', $tenantId)
            ->where('price_list_id', $priceList->id)
            ->where('variant_id', $variant->id)
            ->where('status', 'active')
            ->when(
                $variantUnitId === null,
                fn ($q) => $q->whereNull('variant_unit_id'),
                fn ($q) => $q->where('variant_unit_id', $variantUnitId)
            )
            ->exists();

        if ($duplicate) {
            throw ValidationException::withMessages([
                'variant_id' => ['An active price already exists for this list, variant, and unit.'],
            ]);
        }

        $entry = PriceEntry::query()->create([
            'tenant_id' => $tenantId,
            'price_list_id' => $priceList->id,
            'variant_id' => $variant->id,
            'variant_unit_id' => $variantUnitId,
            'price_amount' => $validated['price_amount'],
            'min_price_amount' => $validated['min_price_amount'] ?? null,
            'effective_from' => $validated['effective_from'] ?? null,
            'effective_to' => $validated['effective_to'] ?? null,
            'status' => 'active',
            'metadata' => $validated['metadata'] ?? null,
        ]);

        $auditRecorder->record(
            'pricing.price_entry.created',
            $tenantId,
            $request->user()->id,
            'pricing.price_entry',
            $entry->public_id,
            after: [
                'price_list_id' => $priceList->public_id,
                'variant_id' => $variant->public_id,
                'price_amount' => $entry->price_amount,
                'min_price_amount' => $entry->min_price_amount,
            ],
            request: $request,
        );

        return response()->json([
            'data' => [
                'id' => $entry->public_id,
                'price_amount' => $entry->price_amount,
                'min_price_amount' => $entry->min_price_amount,
            ],
        ], 201);
    }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Pricing\Http\Controllers\PriceListController.php
================================================================
<?php

namespace App\Modules\Pricing\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Modules\Audit\Application\AuditRecorder;
use App\Modules\Core\Application\Context\TenantContext;
use App\Modules\Pricing\Domain\Models\PriceList;
use App\Modules\Pricing\Http\Requests\StorePriceListRequest;
use Illuminate\Database\QueryException;
use Illuminate\Http\JsonResponse;
use Illuminate\Validation\ValidationException;

final class PriceListController extends Controller
{
    public function index(TenantContext $tenantContext): JsonResponse
    {
        return response()->json([
            'data' => PriceList::query()
                ->where('tenant_id', $tenantContext->tenantId())
                ->orderBy('priority')
                ->get()
                ->map(fn ($list) => [
                    'id' => $list->public_id,
                    'name' => $list->name,
                    'code' => $list->code,
                    'currency_code' => $list->currency_code,
                    'priority' => $list->priority,
                    'is_default' => (bool) $list->is_default,
                    'status' => $list->status,
                ])
                ->values(),
        ]);
    }

    public function store(
        StorePriceListRequest $request,
        TenantContext $tenantContext,
        AuditRecorder $auditRecorder,
    ): JsonResponse {
        $validated = $request->validated();

        $tenantId = $tenantContext->tenantId();
        $currencyCode = strtoupper($validated['currency_code'] ?? 'EGP');
        $isDefault = (bool) ($validated['is_default'] ?? false);

        if (
            PriceList::query()
                ->where('tenant_id', $tenantId)
                ->where('code', $validated['code'])
                ->exists()
        ) {
            throw ValidationException::withMessages([
                'code' => ['Price list code already exists.'],
            ]);
        }

        /*
         * PostgreSQL marks the whole current transaction as failed after a
         * unique-constraint violation. PHPUnit RefreshDatabase keeps the test
         * inside one transaction, so catching the exception alone is not enough:
         * the next query would fail with SQLSTATE[25P02].
         *
         * Validate the predictable conflict before INSERT, while keeping the
         * database unique index as the final race-condition guard.
         */
        if (
            $isDefault
            && PriceList::query()
                ->where('tenant_id', $tenantId)
                ->where('currency_code', $currencyCode)
                ->where('is_default', true)
                ->where('status', 'active')
                ->exists()
        ) {
            throw ValidationException::withMessages([
                'is_default' => [
                    'Only one active default price list is allowed per tenant and currency.',
                ],
            ]);
        }

        try {
            $priceList = PriceList::query()->create([
                'tenant_id' => $tenantId,
                'name' => $validated['name'],
                'code' => $validated['code'],
                'currency_code' => $currencyCode,
                'priority' => $validated['priority'] ?? 100,
                'is_default' => $isDefault,
                'effective_from' => $validated['effective_from'] ?? null,
                'effective_to' => $validated['effective_to'] ?? null,
                'status' => 'active',
                'metadata' => $validated['metadata'] ?? null,
            ]);
        } catch (QueryException $exception) {
            /*
             * Keep the DB constraint as a concurrency backstop.
             * In normal HTTP execution this safely returns a validation error.
             */
            if ($exception->getCode() === '23505') {
                throw ValidationException::withMessages([
                    'is_default' => [
                        'A conflicting active default price list or duplicate code already exists.',
                    ],
                ]);
            }

            throw $exception;
        }

        $auditRecorder->record(
            'pricing.price_list.created',
            $tenantId,
            $request->user()->id,
            'pricing.price_list',
            $priceList->public_id,
            after: [
                'code' => $priceList->code,
                'currency_code' => $priceList->currency_code,
                'priority' => $priceList->priority,
                'is_default' => $priceList->is_default,
            ],
            request: $request,
        );

        return response()->json([
            'data' => [
                'id' => $priceList->public_id,
                'name' => $priceList->name,
                'code' => $priceList->code,
                'currency_code' => $priceList->currency_code,
                'priority' => $priceList->priority,
                'is_default' => (bool) $priceList->is_default,
                'status' => $priceList->status,
            ],
        ], 201);
    }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Pricing\Http\Controllers\PriceListScopeController.php
================================================================
<?php

namespace App\Modules\Pricing\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Modules\Audit\Application\AuditRecorder;
use App\Modules\Core\Application\Context\OrganizationScopeContext;
use App\Modules\Core\Application\Context\TenantContext;
use App\Modules\Pricing\Domain\Models\PriceList;
use App\Modules\Pricing\Domain\Models\PriceListScope;
use App\Modules\Pricing\Http\Requests\AttachPriceListScopeRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Validation\ValidationException;

final class PriceListScopeController extends Controller
{
    public function store(
        string $priceListPublicId,
        AttachPriceListScopeRequest $request,
        TenantContext $tenantContext,
        OrganizationScopeContext $scopeContext,
        AuditRecorder $auditRecorder,
    ): JsonResponse {
        $tenantId = $tenantContext->tenantId();

        $priceList = PriceList::query()
            ->where('tenant_id', $tenantId)
            ->where('public_id', $priceListPublicId)
            ->firstOrFail();

        $validated = $request->validated();
        $scopeType = $validated['scope_type'];

        $companyId = null;
        $branchId = null;
        $customerGroupKey = null;
        $customerPublicId = null;

        if ($scopeType === 'company') {
            if ($scopeContext->companyId() === null) {
                throw ValidationException::withMessages([
                    'scope_type' => ['X-Company-ID is required.'],
                ]);
            }

            $companyId = $scopeContext->companyId();
        } elseif ($scopeType === 'branch') {
            if ($scopeContext->companyId() === null || $scopeContext->branchId() === null) {
                throw ValidationException::withMessages([
                    'scope_type' => ['X-Branch-ID is required.'],
                ]);
            }

            $companyId = $scopeContext->companyId();
            $branchId = $scopeContext->branchId();
        } elseif ($scopeType === 'customer_group') {
            if (empty($validated['customer_group_key'])) {
                throw ValidationException::withMessages([
                    'customer_group_key' => ['Customer group key is required.'],
                ]);
            }

            $customerGroupKey = $validated['customer_group_key'];
        } elseif ($scopeType === 'customer') {
            if (empty($validated['customer_public_id'])) {
                throw ValidationException::withMessages([
                    'customer_public_id' => ['Customer public ID is required.'],
                ]);
            }

            $customerPublicId = $validated['customer_public_id'];
        }

        $duplicate = PriceListScope::query()
            ->where('tenant_id', $tenantId)
            ->where('price_list_id', $priceList->id)
            ->where('scope_type', $scopeType)
            ->when($scopeType === 'company', fn ($q) => $q->where('company_id', $companyId))
            ->when($scopeType === 'branch', fn ($q) => $q->where('branch_id', $branchId))
            ->when($scopeType === 'customer_group', fn ($q) => $q->where('customer_group_key', $customerGroupKey))
            ->when($scopeType === 'customer', fn ($q) => $q->where('customer_public_id', $customerPublicId))
            ->exists();

        if ($duplicate) {
            throw ValidationException::withMessages([
                'scope_type' => ['This scope is already attached to the price list.'],
            ]);
        }

        $scope = PriceListScope::query()->create([
            'tenant_id' => $tenantId,
            'price_list_id' => $priceList->id,
            'scope_type' => $scopeType,
            'company_id' => $companyId,
            'branch_id' => $branchId,
            'customer_group_key' => $customerGroupKey,
            'customer_public_id' => $customerPublicId,
        ]);

        $auditRecorder->record(
            'pricing.price_list.scope_attached',
            $tenantId,
            $request->user()->id,
            'pricing.price_list_scope',
            $scope->public_id,
            after: [
                'price_list_id' => $priceList->public_id,
                'scope_type' => $scopeType,
            ],
            request: $request,
        );

        return response()->json([
            'data' => [
                'id' => $scope->public_id,
                'scope_type' => $scopeType,
            ],
        ], 201);
    }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Pricing\Http\Requests\AttachPriceListScopeRequest.php
================================================================
<?php
namespace App\Modules\Pricing\Http\Requests;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule;
final class AttachPriceListScopeRequest extends FormRequest {
 public function authorize(): bool{return true;}
 public function rules(): array{return ['scope_type'=>['required',Rule::in(['tenant','company','branch','customer_group','customer'])],'customer_group_key'=>['nullable','string','max:100'],'customer_public_id'=>['nullable','string','max:64']];}
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Pricing\Http\Requests\StoreMarginGuardRequest.php
================================================================
<?php
namespace App\Modules\Pricing\Http\Requests;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule;
final class StoreMarginGuardRequest extends FormRequest {
 public function authorize(): bool{return true;}
 public function rules(): array{return ['scope_type'=>['required',Rule::in(['tenant','company','branch'])],'minimum_margin_percent'=>['nullable','numeric'],'minimum_markup_percent'=>['nullable','numeric'],'violation_action'=>['required',Rule::in(['block','approval'])],'approval_action_code'=>['nullable','string','max:180']];}
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Pricing\Http\Requests\StorePriceEntryRequest.php
================================================================
<?php
namespace App\Modules\Pricing\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
final class StorePriceEntryRequest extends FormRequest {
 public function authorize(): bool{return true;}
 public function rules(): array{return ['variant_id'=>['required','string','size:26'],'variant_unit_id'=>['nullable','string','size:26'],'price_amount'=>['required','numeric','min:0'],'min_price_amount'=>['nullable','numeric','min:0','lte:price_amount'],'effective_from'=>['nullable','date'],'effective_to'=>['nullable','date','after:effective_from'],'metadata'=>['nullable','array']];}
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Pricing\Http\Requests\StorePriceListRequest.php
================================================================
<?php
namespace App\Modules\Pricing\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
final class StorePriceListRequest extends FormRequest {
 public function authorize(): bool{return true;}
 public function rules(): array{return ['name'=>['required','string','max:180'],'code'=>['required','string','max:100'],'currency_code'=>['nullable','string','size:3'],'priority'=>['nullable','integer','min:0'],'is_default'=>['nullable','boolean'],'effective_from'=>['nullable','date'],'effective_to'=>['nullable','date','after:effective_from'],'metadata'=>['nullable','array']];}
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Promotions\Application\Evaluation\PromotionEligibilityEvaluator.php
================================================================
<?php
namespace App\Modules\Promotions\Application\Evaluation;

use App\Modules\Promotions\Domain\Models\PromotionCampaign;
use App\Modules\Promotions\Domain\Models\PromotionUsageCounter;

final class PromotionEligibilityEvaluator
{
    public function evaluate(
        PromotionCampaign $campaign,
        ?int $companyId,
        ?int $branchId,
        ?string $customerGroupKey,
        ?string $customerPublicId,
        ?int $productFamilyId,
        ?int $variantId,
        ?int $categoryId,
        string $quantity,
        string $basketAmount,
    ): array {
        $now = now();

        if ($campaign->status !== 'active') {
            return ['eligible'=>false,'reason'=>'INACTIVE'];
        }

        if ($campaign->effective_from && $campaign->effective_from->isFuture()) {
            return ['eligible'=>false,'reason'=>'NOT_STARTED'];
        }

        if ($campaign->effective_to && !$campaign->effective_to->isFuture()) {
            return ['eligible'=>false,'reason'=>'EXPIRED'];
        }

        $campaign->loadMissing(['scopes','targets','condition','reward']);

        if (!$this->scopeMatches($campaign, $companyId, $branchId, $customerGroupKey, $customerPublicId)) {
            return ['eligible'=>false,'reason'=>'SCOPE_MISMATCH'];
        }

        if (!$this->targetMatches($campaign, $productFamilyId, $variantId, $categoryId)) {
            return ['eligible'=>false,'reason'=>'TARGET_MISMATCH'];
        }

        if ($campaign->condition) {
            if ($campaign->condition->minimum_quantity !== null
                && bccomp($quantity, (string)$campaign->condition->minimum_quantity, 6) < 0) {
                return ['eligible'=>false,'reason'=>'MINIMUM_QUANTITY'];
            }

            if ($campaign->condition->minimum_basket_amount !== null
                && bccomp($basketAmount, (string)$campaign->condition->minimum_basket_amount, 6) < 0) {
                return ['eligible'=>false,'reason'=>'MINIMUM_BASKET'];
            }
        }

        if ($campaign->max_total_uses !== null) {
            $total = PromotionUsageCounter::query()
                ->where('tenant_id',$campaign->tenant_id)
                ->where('campaign_id',$campaign->id)
                ->whereNull('customer_public_id')
                ->value('usage_count') ?? 0;

            if ($total >= $campaign->max_total_uses) {
                return ['eligible'=>false,'reason'=>'TOTAL_USAGE_LIMIT'];
            }
        }

        if ($campaign->max_uses_per_customer !== null && $customerPublicId !== null) {
            $customer = PromotionUsageCounter::query()
                ->where('tenant_id',$campaign->tenant_id)
                ->where('campaign_id',$campaign->id)
                ->where('customer_public_id',$customerPublicId)
                ->value('usage_count') ?? 0;

            if ($customer >= $campaign->max_uses_per_customer) {
                return ['eligible'=>false,'reason'=>'CUSTOMER_USAGE_LIMIT'];
            }
        }

        return [
            'eligible'=>true,
            'reason'=>null,
            'reward_type'=>$campaign->reward?->reward_type,
            'exclusive'=>(bool)$campaign->is_exclusive,
            'priority'=>$campaign->priority,
        ];
    }

    private function scopeMatches(
        PromotionCampaign $campaign,
        ?int $companyId,
        ?int $branchId,
        ?string $group,
        ?string $customer
    ): bool {
        if ($campaign->scopes->isEmpty()) return true;

        foreach ($campaign->scopes as $scope) {
            if ($scope->scope_type === 'tenant') return true;
            if ($scope->scope_type === 'company' && $companyId !== null && $scope->company_id === $companyId) return true;
            if ($scope->scope_type === 'branch' && $branchId !== null && $scope->branch_id === $branchId) return true;
            if ($scope->scope_type === 'customer_group' && $group !== null && $scope->customer_group_key === $group) return true;
            if ($scope->scope_type === 'customer' && $customer !== null && $scope->customer_public_id === $customer) return true;
        }

        return false;
    }

    private function targetMatches(
        PromotionCampaign $campaign,
        ?int $familyId,
        ?int $variantId,
        ?int $categoryId
    ): bool {
        if ($campaign->targets->isEmpty()) return true;

        foreach ($campaign->targets as $target) {
            if ($target->target_type === 'all') return true;
            if ($target->target_type === 'product_family' && $familyId !== null && $target->product_family_id === $familyId) return true;
            if ($target->target_type === 'variant' && $variantId !== null && $target->variant_id === $variantId) return true;
            if ($target->target_type === 'category' && $categoryId !== null && $target->category_id === $categoryId) return true;
        }

        return false;
    }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Promotions\Application\Permissions\PromotionPermissions.php
================================================================
<?php
namespace App\Modules\Promotions\Application\Permissions;
final class PromotionPermissions {
 public const VIEW='promotions.view';
 public const MANAGE='promotions.manage';
 private function __construct(){}
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Promotions\Application\Services\PromotionCompiler.php
================================================================
<?php
namespace App\Modules\Promotions\Application\Services;

use App\Modules\Promotions\Domain\Models\PromotionCampaign;

final class PromotionCompiler
{
    public function compile(PromotionCampaign $campaign): array
    {
        $campaign->loadMissing(['scopes','targets','condition','reward']);

        return [
            'version' => 1,
            'campaign_id' => $campaign->public_id,
            'priority' => $campaign->priority,
            'exclusive' => (bool) $campaign->is_exclusive,
            'scopes' => $campaign->scopes->map(fn ($scope) => [
                'type' => $scope->scope_type,
                'company_id' => $scope->company_id,
                'branch_id' => $scope->branch_id,
                'customer_group_key' => $scope->customer_group_key,
                'customer_public_id' => $scope->customer_public_id,
            ])->values()->all(),
            'targets' => $campaign->targets->map(fn ($target) => [
                'type' => $target->target_type,
                'product_family_id' => $target->product_family_id,
                'variant_id' => $target->variant_id,
                'category_id' => $target->category_id,
            ])->values()->all(),
            'condition' => $campaign->condition ? [
                'minimum_quantity' => $campaign->condition->minimum_quantity,
                'minimum_basket_amount' => $campaign->condition->minimum_basket_amount,
            ] : null,
            'reward' => $campaign->reward ? [
                'type' => $campaign->reward->reward_type,
                'percentage_value' => $campaign->reward->percentage_value,
                'fixed_amount' => $campaign->reward->fixed_amount,
                'buy_quantity' => $campaign->reward->buy_quantity,
                'get_quantity' => $campaign->reward->get_quantity,
                'reward_variant_id' => $campaign->reward->reward_variant_id,
            ] : null,
            'usage' => [
                'max_total_uses' => $campaign->max_total_uses,
                'max_uses_per_customer' => $campaign->max_uses_per_customer,
            ],
        ];
    }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Promotions\Domain\Models\PromotionCampaign.php
================================================================
<?php
namespace App\Modules\Promotions\Domain\Models;

use App\Modules\Core\Domain\Concerns\HasPublicUlid;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;

final class PromotionCampaign extends Model
{
    use HasPublicUlid;

    protected $table = 'promotions.campaigns';

    protected $fillable = [
        'tenant_id','name','code','priority','is_exclusive',
        'effective_from','effective_to','max_total_uses','max_uses_per_customer',
        'status','compiled_rule','metadata',
    ];

    protected function casts(): array
    {
        return [
            'priority'=>'integer',
            'is_exclusive'=>'boolean',
            'effective_from'=>'immutable_datetime',
            'effective_to'=>'immutable_datetime',
            'max_total_uses'=>'integer',
            'max_uses_per_customer'=>'integer',
            'compiled_rule'=>'array',
            'metadata'=>'array',
        ];
    }

    public function scopes(): HasMany { return $this->hasMany(PromotionScope::class, 'campaign_id'); }
    public function targets(): HasMany { return $this->hasMany(PromotionTarget::class, 'campaign_id'); }
    public function condition(): HasOne { return $this->hasOne(PromotionCondition::class, 'campaign_id'); }
    public function reward(): HasOne { return $this->hasOne(PromotionReward::class, 'campaign_id'); }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Promotions\Domain\Models\PromotionCondition.php
================================================================
<?php
namespace App\Modules\Promotions\Domain\Models;
use App\Modules\Core\Domain\Concerns\HasPublicUlid;
use Illuminate\Database\Eloquent\Model;
final class PromotionCondition extends Model {
 use HasPublicUlid;
 protected $table='promotions.conditions';
 protected $fillable=['tenant_id','campaign_id','minimum_quantity','minimum_basket_amount'];
 protected function casts(): array { return ['minimum_quantity'=>'decimal:6','minimum_basket_amount'=>'decimal:6']; }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Promotions\Domain\Models\PromotionReward.php
================================================================
<?php
namespace App\Modules\Promotions\Domain\Models;
use App\Modules\Core\Domain\Concerns\HasPublicUlid;
use Illuminate\Database\Eloquent\Model;
final class PromotionReward extends Model {
 use HasPublicUlid;
 protected $table='promotions.rewards';
 protected $fillable=['tenant_id','campaign_id','reward_type','percentage_value','fixed_amount','buy_quantity','get_quantity','reward_variant_id'];
 protected function casts(): array {
  return [
   'percentage_value'=>'decimal:4','fixed_amount'=>'decimal:6',
   'buy_quantity'=>'decimal:6','get_quantity'=>'decimal:6'
  ];
 }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Promotions\Domain\Models\PromotionScope.php
================================================================
<?php
namespace App\Modules\Promotions\Domain\Models;
use App\Modules\Core\Domain\Concerns\HasPublicUlid;
use Illuminate\Database\Eloquent\Model;
final class PromotionScope extends Model {
 use HasPublicUlid;
 protected $table='promotions.scopes';
 protected $fillable=['tenant_id','campaign_id','scope_type','company_id','branch_id','customer_group_key','customer_public_id'];
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Promotions\Domain\Models\PromotionTarget.php
================================================================
<?php
namespace App\Modules\Promotions\Domain\Models;
use App\Modules\Core\Domain\Concerns\HasPublicUlid;
use Illuminate\Database\Eloquent\Model;
final class PromotionTarget extends Model {
 use HasPublicUlid;
 protected $table='promotions.targets';
 protected $fillable=['tenant_id','campaign_id','target_type','product_family_id','variant_id','category_id'];
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Promotions\Domain\Models\PromotionUsageCounter.php
================================================================
<?php
namespace App\Modules\Promotions\Domain\Models;
use Illuminate\Database\Eloquent\Model;
final class PromotionUsageCounter extends Model {
 protected $table='promotions.usage_counters';
 protected $fillable=['tenant_id','campaign_id','customer_public_id','usage_count','last_used_at'];
 protected function casts(): array { return ['usage_count'=>'integer','last_used_at'=>'immutable_datetime']; }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Promotions\Http\Controllers\PromotionCampaignController.php
================================================================
<?php
namespace App\Modules\Promotions\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Modules\Audit\Application\AuditRecorder;
use App\Modules\Catalog\Domain\Models\Variant;
use App\Modules\Core\Application\Context\TenantContext;
use App\Modules\Promotions\Application\Services\PromotionCompiler;
use App\Modules\Promotions\Domain\Models\PromotionCampaign;
use App\Modules\Promotions\Domain\Models\PromotionCondition;
use App\Modules\Promotions\Domain\Models\PromotionReward;
use App\Modules\Promotions\Http\Requests\StorePromotionCampaignRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

final class PromotionCampaignController extends Controller
{
    public function index(TenantContext $tenantContext): JsonResponse
    {
        return response()->json([
            'data'=>PromotionCampaign::query()
                ->where('tenant_id',$tenantContext->tenantId())
                ->orderBy('priority')
                ->orderBy('name')
                ->get()
                ->map(fn ($campaign)=>[
                    'id'=>$campaign->public_id,
                    'name'=>$campaign->name,
                    'code'=>$campaign->code,
                    'priority'=>$campaign->priority,
                    'is_exclusive'=>(bool)$campaign->is_exclusive,
                    'status'=>$campaign->status,
                ])->values(),
        ]);
    }

    public function store(
        StorePromotionCampaignRequest $request,
        TenantContext $tenantContext,
        PromotionCompiler $compiler,
        AuditRecorder $auditRecorder,
    ): JsonResponse {
        $v=$request->validated();
        $tenantId=$tenantContext->tenantId();

        if (PromotionCampaign::query()->where('tenant_id',$tenantId)->where('code',$v['code'])->exists()) {
            throw ValidationException::withMessages(['code'=>['Promotion code already exists.']]);
        }

        $reward=$v['reward'];

        if ($reward['reward_type']==='percentage_discount' && !isset($reward['percentage_value'])) {
            throw ValidationException::withMessages(['reward.percentage_value'=>['Percentage value is required.']]);
        }

        if ($reward['reward_type']==='fixed_discount' && !isset($reward['fixed_amount'])) {
            throw ValidationException::withMessages(['reward.fixed_amount'=>['Fixed amount is required.']]);
        }

        $rewardVariantId=null;
        if ($reward['reward_type']==='buy_x_get_y') {
            if (!isset($reward['buy_quantity'],$reward['get_quantity'],$reward['reward_variant_id'])) {
                throw ValidationException::withMessages(['reward.reward_variant_id'=>['Buy X Get Y requires quantities and reward variant.']]);
            }

            $variant=Variant::query()
                ->where('tenant_id',$tenantId)
                ->where('public_id',$reward['reward_variant_id'])
                ->first();

            if ($variant===null) {
                throw ValidationException::withMessages(['reward.reward_variant_id'=>['Reward variant was not found.']]);
            }

            $rewardVariantId=$variant->id;
        }

        $campaign=DB::transaction(function() use ($v,$reward,$rewardVariantId,$tenantId,$request,$compiler,$auditRecorder) {
            $campaign=PromotionCampaign::query()->create([
                'tenant_id'=>$tenantId,
                'name'=>$v['name'],
                'code'=>$v['code'],
                'priority'=>$v['priority']??100,
                'is_exclusive'=>$v['is_exclusive']??false,
                'effective_from'=>$v['effective_from']??null,
                'effective_to'=>$v['effective_to']??null,
                'max_total_uses'=>$v['max_total_uses']??null,
                'max_uses_per_customer'=>$v['max_uses_per_customer']??null,
                'status'=>'active',
                'metadata'=>$v['metadata']??null,
            ]);

            if (!empty($v['condition'])) {
                PromotionCondition::query()->create([
                    'tenant_id'=>$tenantId,
                    'campaign_id'=>$campaign->id,
                    'minimum_quantity'=>$v['condition']['minimum_quantity']??null,
                    'minimum_basket_amount'=>$v['condition']['minimum_basket_amount']??null,
                ]);
            }

            PromotionReward::query()->create([
                'tenant_id'=>$tenantId,
                'campaign_id'=>$campaign->id,
                'reward_type'=>$reward['reward_type'],
                'percentage_value'=>$reward['percentage_value']??null,
                'fixed_amount'=>$reward['fixed_amount']??null,
                'buy_quantity'=>$reward['buy_quantity']??null,
                'get_quantity'=>$reward['get_quantity']??null,
                'reward_variant_id'=>$rewardVariantId,
            ]);

            $campaign->load(['scopes','targets','condition','reward']);
            $campaign->compiled_rule=$compiler->compile($campaign);
            $campaign->save();

            $auditRecorder->record(
                'promotions.campaign.created',
                $tenantId,
                $request->user()->id,
                'promotions.campaign',
                $campaign->public_id,
                after:[
                    'code'=>$campaign->code,
                    'priority'=>$campaign->priority,
                    'is_exclusive'=>$campaign->is_exclusive,
                    'reward_type'=>$campaign->reward->reward_type,
                ],
                request:$request,
            );

            return $campaign;
        });

        return response()->json(['data'=>[
            'id'=>$campaign->public_id,
            'name'=>$campaign->name,
            'code'=>$campaign->code,
            'priority'=>$campaign->priority,
            'is_exclusive'=>(bool)$campaign->is_exclusive,
            'compiled_rule'=>$campaign->compiled_rule,
        ]],201);
    }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Promotions\Http\Controllers\PromotionEvaluateController.php
================================================================
<?php
namespace App\Modules\Promotions\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Modules\Catalog\Domain\Models\Variant;
use App\Modules\Core\Application\Context\OrganizationScopeContext;
use App\Modules\Core\Application\Context\TenantContext;
use App\Modules\Promotions\Application\Evaluation\PromotionEligibilityEvaluator;
use App\Modules\Promotions\Domain\Models\PromotionCampaign;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;

final class PromotionEvaluateController extends Controller
{
    public function show(
        string $campaignPublicId,
        Request $request,
        TenantContext $tenantContext,
        OrganizationScopeContext $scopeContext,
        PromotionEligibilityEvaluator $evaluator,
    ): JsonResponse {
        $campaign=PromotionCampaign::query()
            ->where('tenant_id',$tenantContext->tenantId())
            ->where('public_id',$campaignPublicId)
            ->firstOrFail();

        $variantPublicId=$request->query('variant_id');
        $variant=null;

        if ($variantPublicId) {
            $variant=Variant::query()
                ->with('family')
                ->where('tenant_id',$tenantContext->tenantId())
                ->where('public_id',$variantPublicId)
                ->first();

            if (!$variant) throw ValidationException::withMessages(['variant_id'=>['Variant was not found.']]);
        }

        $result=$evaluator->evaluate(
            campaign:$campaign,
            companyId:$scopeContext->companyId(),
            branchId:$scopeContext->branchId(),
            customerGroupKey:$request->query('customer_group_key'),
            customerPublicId:$request->query('customer_public_id'),
            productFamilyId:$variant?->product_family_id,
            variantId:$variant?->id,
            categoryId:$variant?->family?->category_id,
            quantity:(string)($request->query('quantity','1')),
            basketAmount:(string)($request->query('basket_amount','0')),
        );

        return response()->json(['data'=>$result]);
    }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Promotions\Http\Controllers\PromotionScopeController.php
================================================================
<?php
namespace App\Modules\Promotions\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Modules\Audit\Application\AuditRecorder;
use App\Modules\Core\Application\Context\OrganizationScopeContext;
use App\Modules\Core\Application\Context\TenantContext;
use App\Modules\Promotions\Application\Services\PromotionCompiler;
use App\Modules\Promotions\Domain\Models\PromotionCampaign;
use App\Modules\Promotions\Domain\Models\PromotionScope;
use App\Modules\Promotions\Http\Requests\AttachPromotionScopeRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Validation\ValidationException;

final class PromotionScopeController extends Controller
{
    public function store(
        string $campaignPublicId,
        AttachPromotionScopeRequest $request,
        TenantContext $tenantContext,
        OrganizationScopeContext $scopeContext,
        PromotionCompiler $compiler,
        AuditRecorder $auditRecorder,
    ): JsonResponse {
        $tenantId=$tenantContext->tenantId();

        $campaign=PromotionCampaign::query()
            ->where('tenant_id',$tenantId)
            ->where('public_id',$campaignPublicId)
            ->firstOrFail();

        $v=$request->validated();
        $type=$v['scope_type'];

        $companyId=null; $branchId=null; $group=null; $customer=null;

        if ($type==='company') {
            if ($scopeContext->companyId()===null) throw ValidationException::withMessages(['scope_type'=>['X-Company-ID is required.']]);
            $companyId=$scopeContext->companyId();
        } elseif ($type==='branch') {
            if ($scopeContext->companyId()===null || $scopeContext->branchId()===null) throw ValidationException::withMessages(['scope_type'=>['X-Branch-ID is required.']]);
            $companyId=$scopeContext->companyId(); $branchId=$scopeContext->branchId();
        } elseif ($type==='customer_group') {
            if (empty($v['customer_group_key'])) throw ValidationException::withMessages(['customer_group_key'=>['Customer group key is required.']]);
            $group=$v['customer_group_key'];
        } elseif ($type==='customer') {
            if (empty($v['customer_public_id'])) throw ValidationException::withMessages(['customer_public_id'=>['Customer public ID is required.']]);
            $customer=$v['customer_public_id'];
        }

        $duplicate=PromotionScope::query()
            ->where('tenant_id',$tenantId)
            ->where('campaign_id',$campaign->id)
            ->where('scope_type',$type)
            ->when($type==='company',fn($q)=>$q->where('company_id',$companyId))
            ->when($type==='branch',fn($q)=>$q->where('branch_id',$branchId))
            ->when($type==='customer_group',fn($q)=>$q->where('customer_group_key',$group))
            ->when($type==='customer',fn($q)=>$q->where('customer_public_id',$customer))
            ->exists();

        if ($duplicate) throw ValidationException::withMessages(['scope_type'=>['This scope is already attached.']]);

        $scope=PromotionScope::query()->create([
            'tenant_id'=>$tenantId,'campaign_id'=>$campaign->id,'scope_type'=>$type,
            'company_id'=>$companyId,'branch_id'=>$branchId,
            'customer_group_key'=>$group,'customer_public_id'=>$customer,
        ]);

        $campaign->load(['scopes','targets','condition','reward']);
        $campaign->compiled_rule=$compiler->compile($campaign);
        $campaign->save();

        $auditRecorder->record(
            'promotions.scope.attached',$tenantId,$request->user()->id,
            'promotions.scope',$scope->public_id,
            after:['campaign_id'=>$campaign->public_id,'scope_type'=>$type],
            request:$request
        );

        return response()->json(['data'=>['id'=>$scope->public_id,'scope_type'=>$type]],201);
    }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Promotions\Http\Controllers\PromotionTargetController.php
================================================================
<?php
namespace App\Modules\Promotions\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Modules\Audit\Application\AuditRecorder;
use App\Modules\Catalog\Domain\Models\Category;
use App\Modules\Catalog\Domain\Models\ProductFamily;
use App\Modules\Catalog\Domain\Models\Variant;
use App\Modules\Core\Application\Context\TenantContext;
use App\Modules\Promotions\Application\Services\PromotionCompiler;
use App\Modules\Promotions\Domain\Models\PromotionCampaign;
use App\Modules\Promotions\Domain\Models\PromotionTarget;
use App\Modules\Promotions\Http\Requests\AttachPromotionTargetRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Validation\ValidationException;

final class PromotionTargetController extends Controller
{
    public function store(
        string $campaignPublicId,
        AttachPromotionTargetRequest $request,
        TenantContext $tenantContext,
        PromotionCompiler $compiler,
        AuditRecorder $auditRecorder,
    ): JsonResponse {
        $tenantId=$tenantContext->tenantId();

        $campaign=PromotionCampaign::query()
            ->where('tenant_id',$tenantId)
            ->where('public_id',$campaignPublicId)
            ->firstOrFail();

        $v=$request->validated();
        $type=$v['target_type'];

        $familyId=null; $variantId=null; $categoryId=null;

        if ($type==='product_family') {
            if (empty($v['product_family_id'])) throw ValidationException::withMessages(['product_family_id'=>['Product family is required.']]);
            $m=ProductFamily::query()->where('tenant_id',$tenantId)->where('public_id',$v['product_family_id'])->first();
            if (!$m) throw ValidationException::withMessages(['product_family_id'=>['Product family was not found.']]);
            $familyId=$m->id;
        } elseif ($type==='variant') {
            if (empty($v['variant_id'])) throw ValidationException::withMessages(['variant_id'=>['Variant is required.']]);
            $m=Variant::query()->where('tenant_id',$tenantId)->where('public_id',$v['variant_id'])->first();
            if (!$m) throw ValidationException::withMessages(['variant_id'=>['Variant was not found.']]);
            $variantId=$m->id;
        } elseif ($type==='category') {
            if (empty($v['category_id'])) throw ValidationException::withMessages(['category_id'=>['Category is required.']]);
            $m=Category::query()->where('tenant_id',$tenantId)->where('public_id',$v['category_id'])->first();
            if (!$m) throw ValidationException::withMessages(['category_id'=>['Category was not found.']]);
            $categoryId=$m->id;
        }

        $target=PromotionTarget::query()->create([
            'tenant_id'=>$tenantId,'campaign_id'=>$campaign->id,'target_type'=>$type,
            'product_family_id'=>$familyId,'variant_id'=>$variantId,'category_id'=>$categoryId,
        ]);

        $campaign->load(['scopes','targets','condition','reward']);
        $campaign->compiled_rule=$compiler->compile($campaign);
        $campaign->save();

        $auditRecorder->record(
            'promotions.target.attached',$tenantId,$request->user()->id,
            'promotions.target',$target->public_id,
            after:['campaign_id'=>$campaign->public_id,'target_type'=>$type],
            request:$request
        );

        return response()->json(['data'=>['id'=>$target->public_id,'target_type'=>$type]],201);
    }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Promotions\Http\Requests\AttachPromotionScopeRequest.php
================================================================
<?php
namespace App\Modules\Promotions\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

final class AttachPromotionScopeRequest extends FormRequest
{
    public function authorize(): bool { return true; }
    public function rules(): array {
        return [
            'scope_type'=>['required',Rule::in(['tenant','company','branch','customer_group','customer'])],
            'customer_group_key'=>['nullable','string','max:100'],
            'customer_public_id'=>['nullable','string','max:64'],
        ];
    }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Promotions\Http\Requests\AttachPromotionTargetRequest.php
================================================================
<?php
namespace App\Modules\Promotions\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

final class AttachPromotionTargetRequest extends FormRequest
{
    public function authorize(): bool { return true; }
    public function rules(): array {
        return [
            'target_type'=>['required',Rule::in(['all','product_family','variant','category'])],
            'product_family_id'=>['nullable','string','size:26'],
            'variant_id'=>['nullable','string','size:26'],
            'category_id'=>['nullable','string','size:26'],
        ];
    }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Promotions\Http\Requests\StorePromotionCampaignRequest.php
================================================================
<?php
namespace App\Modules\Promotions\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

final class StorePromotionCampaignRequest extends FormRequest
{
    public function authorize(): bool { return true; }

    public function rules(): array
    {
        return [
            'name'=>['required','string','max:180'],
            'code'=>['required','string','max:100'],
            'priority'=>['nullable','integer','min:0'],
            'is_exclusive'=>['nullable','boolean'],
            'effective_from'=>['nullable','date'],
            'effective_to'=>['nullable','date','after:effective_from'],
            'max_total_uses'=>['nullable','integer','min:1'],
            'max_uses_per_customer'=>['nullable','integer','min:1'],
            'metadata'=>['nullable','array'],

            'condition'=>['nullable','array'],
            'condition.minimum_quantity'=>['nullable','numeric','gt:0'],
            'condition.minimum_basket_amount'=>['nullable','numeric','min:0'],

            'reward'=>['required','array'],
            'reward.reward_type'=>['required',Rule::in(['percentage_discount','fixed_discount','buy_x_get_y'])],
            'reward.percentage_value'=>['nullable','numeric','gt:0','lte:100'],
            'reward.fixed_amount'=>['nullable','numeric','gt:0'],
            'reward.buy_quantity'=>['nullable','numeric','gt:0'],
            'reward.get_quantity'=>['nullable','numeric','gt:0'],
            'reward.reward_variant_id'=>['nullable','string','size:26'],
        ];
    }
}

================================================================
FILE: .\app\Modules\Sales\Application\Cart\CartRecalculator.php
================================================================
<?php

namespace App\Modules\Sales\Application\Cart;

use App\Modules\Catalog\Domain\Models\Variant;
use App\Modules\Pricing\Application\Resolution\EffectivePriceResolver;
use App\Modules\Sales\Application\Pricing\CartLinePromotionResolver;
use App\Modules\Sales\Domain\Models\Cart;
use Illuminate\Validation\ValidationException;

final readonly class CartRecalculator
{
    public function __construct(
        private EffectivePriceResolver $priceResolver,
        private CartLinePromotionResolver $promotionResolver,
    ) {}

    public function recalculate(Cart $cart): Cart
    {
        $cart->load(['lines']);

        $subtotal='0.000000';
        $discountTotal='0.000000';

        foreach ($cart->lines as $line) {
            $variant=Variant::query()
                ->with('family')
                ->where('tenant_id',$cart->tenant_id)
                ->whereKey($line->variant_id)
                ->firstOrFail();

            /*
             * Use positional arguments here deliberately.
             *
             * EffectivePriceResolver was introduced earlier with parameter names
             * that changed during the Pricing repair cycle. Named arguments made
             * Sales depend on those internal parameter names and caused:
             *
             *   Unknown named parameter $customerGroupKey
             *
             * The resolver contract is the argument ORDER / semantic contract,
             * not its private PHP parameter labels.
             */
            $price=$this->priceResolver->resolve(
                $cart->tenant_id,
                $line->variant_id,
                $line->variant_unit_id,
                $cart->company_id,
                $cart->branch_id,
                $cart->customer_group_key,
                $cart->customer_public_id,
            );

            if ($price === null) {
                throw ValidationException::withMessages([
                    'price'=>['No effective price was found for one of the cart lines.'],
                ]);
            }

            $unitPrice=(string)$price['price_amount'];
            $gross=bcmul($unitPrice,(string)$line->quantity,6);

            $promotions=$this->promotionResolver->resolve(
                tenantId:$cart->tenant_id,
                variant:$variant,
                companyId:$cart->company_id,
                branchId:$cart->branch_id,
                customerGroupKey:$cart->customer_group_key,
                customerPublicId:$cart->customer_public_id,
                quantity:(string)$line->quantity,
                basketAmount:bcadd($subtotal,$gross,6),
                grossAmount:$gross,
            );

            $lineDiscount=$promotions['discount_amount'];
            $net=bcsub($gross,$lineDiscount,6);

            $line->fill([
                'unit_price_amount'=>$unitPrice,
                'gross_amount'=>$gross,
                'discount_amount'=>$lineDiscount,
                'net_amount'=>$net,
                'price_snapshot'=>$price,
                'promotion_snapshot'=>$promotions['promotions'],
            ]);
            $line->save();

            $subtotal=bcadd($subtotal,$gross,6);
            $discountTotal=bcadd($discountTotal,$lineDiscount,6);
        }

        $cart->fill([
            'subtotal_amount'=>$subtotal,
            'discount_amount'=>$discountTotal,
            'total_amount'=>bcsub($subtotal,$discountTotal,6),
        ]);
        $cart->save();

        return $cart->fresh('lines');
    }
}

================================================================
FILE: .\app\Modules\Sales\Application\Checkout\CheckoutCartAction.php
================================================================
<?php

namespace App\Modules\Sales\Application\Checkout;

use App\Modules\Audit\Application\AuditRecorder;
use App\Modules\Catalog\Domain\Models\Variant;
use App\Modules\Sales\Application\Numbering\SaleNumberGenerator;
use App\Modules\Sales\Domain\Models\Cart;
use App\Modules\Sales\Domain\Models\CheckoutCommand;
use App\Modules\Sales\Domain\Models\Sale;
use App\Modules\Sales\Domain\Models\SaleLine;
use Carbon\CarbonImmutable;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;

final readonly class CheckoutCartAction
{
    public function __construct(
        private SaleNumberGenerator $numberGenerator,
        private AuditRecorder $auditRecorder,
    ) {}

    public function execute(
        Cart $cart,
        int $expectedVersion,
        string $idempotencyKey,
        int $actorUserId,
        Request $request,
    ): Sale {
        $existing=CheckoutCommand::query()
            ->where('tenant_id',$cart->tenant_id)
            ->where('idempotency_key',$idempotencyKey)
            ->first();

        if ($existing?->sale_id) {
            return Sale::query()->with('lines')->findOrFail($existing->sale_id);
        }

        return DB::transaction(function () use ($cart,$expectedVersion,$idempotencyKey,$actorUserId,$request) {
            $locked=Cart::query()
                ->with('lines')
                ->whereKey($cart->id)
                ->lockForUpdate()
                ->firstOrFail();

            $existing=CheckoutCommand::query()
                ->where('tenant_id',$locked->tenant_id)
                ->where('idempotency_key',$idempotencyKey)
                ->lockForUpdate()
                ->first();

            if ($existing?->sale_id) {
                return Sale::query()->with('lines')->findOrFail($existing->sale_id);
            }

            if ($locked->status === 'converted') {
                $sale=Sale::query()
                    ->with('lines')
                    ->where('tenant_id',$locked->tenant_id)
                    ->where('cart_id',$locked->id)
                    ->first();

                if ($sale) return $sale;

                throw new ConflictHttpException('Cart was already converted.');
            }

            if ($locked->status !== 'open') {
                throw new ConflictHttpException('Cart is not open for checkout.');
            }

            if ($locked->version !== $expectedVersion) {
                throw new ConflictHttpException('Cart version conflict.');
            }

            if ($locked->lines->isEmpty()) {
                throw ValidationException::withMessages([
                    'cart'=>['Cannot checkout an empty cart.'],
                ]);
            }

            if (bccomp((string)$locked->total_amount,'0.000000',6) < 0) {
                throw ValidationException::withMessages([
                    'cart'=>['Cart total is invalid.'],
                ]);
            }

            $command=$existing ?? CheckoutCommand::query()->create([
                'tenant_id'=>$locked->tenant_id,
                'cart_id'=>$locked->id,
                'idempotency_key'=>$idempotencyKey,
                'status'=>'started',
                'created_at'=>now(),
            ]);

            $occurredAt=CarbonImmutable::now('UTC');
            $number=$this->numberGenerator->next(
                $locked->tenant_id,
                $locked->branch_id,
                $occurredAt,
            );

            $sale=Sale::query()->create([
                'tenant_id'=>$locked->tenant_id,
                'company_id'=>$locked->company_id,
                'branch_id'=>$locked->branch_id,
                'register_id'=>$locked->register_id,
                'cart_id'=>$locked->id,
                'created_by_user_id'=>$actorUserId,
                'sale_number'=>$number['sale_number'],
                'business_date'=>$number['business_date'],
                'occurred_at'=>$occurredAt,
                'currency_code'=>$locked->currency_code,
                'customer_public_id'=>$locked->customer_public_id,
                'customer_group_key'=>$locked->customer_group_key,
                'subtotal_amount'=>$locked->subtotal_amount,
                'discount_amount'=>$locked->discount_amount,
                'total_amount'=>$locked->total_amount,
                'status'=>'pending_payment',
                'payment_status'=>'unpaid',
                'pricing_snapshot'=>[
                    'source'=>'server_cart',
                    'cart_version'=>$locked->version,
                ],
                'promotion_snapshot'=>[
                    'line_promotions'=>$locked->lines
                        ->filter(fn($line)=>!empty($line->promotion_snapshot))
                        ->map(fn($line)=>[
                            'cart_line_id'=>$line->public_id,
                            'promotions'=>$line->promotion_snapshot,
                        ])->values()->all(),
                ],
            ]);

            foreach ($locked->lines as $line) {
                $variant=Variant::query()
                    ->where('tenant_id',$locked->tenant_id)
                    ->whereKey($line->variant_id)
                    ->firstOrFail();

                SaleLine::query()->create([
                    'tenant_id'=>$locked->tenant_id,
                    'sale_id'=>$sale->id,
                    'variant_id'=>$line->variant_id,
                    'variant_unit_id'=>$line->variant_unit_id,
                    'sku_snapshot'=>$variant->sku,
                    'name_snapshot'=>$variant->name,
                    'quantity'=>$line->quantity,
                    'unit_price_amount'=>$line->unit_price_amount,
                    'gross_amount'=>$line->gross_amount,
                    'discount_amount'=>$line->discount_amount,
                    'net_amount'=>$line->net_amount,
                    'price_snapshot'=>$line->price_snapshot,
                    'promotion_snapshot'=>$line->promotion_snapshot,
                    'metadata'=>$line->metadata,
                    'created_at'=>$occurredAt,
                ]);
            }

            $locked->status='converted';
            $locked->version++;
            $locked->save();

            $command->sale_id=$sale->id;
            $command->status='completed';
            $command->response_snapshot=[
                'sale_id'=>$sale->public_id,
                'sale_number'=>$sale->sale_number,
                'status'=>$sale->status,
                'payment_status'=>$sale->payment_status,
                'total_amount'=>$sale->total_amount,
            ];
            $command->completed_at=now();
            $command->save();

            $this->auditRecorder->record(
                'sales.checkout.completed',
                $locked->tenant_id,
                $actorUserId,
                'sales.sale',
                $sale->public_id,
                after:[
                    'cart_id'=>$locked->public_id,
                    'sale_number'=>$sale->sale_number,
                    'total_amount'=>$sale->total_amount,
                    'status'=>$sale->status,
                    'payment_status'=>$sale->payment_status,
                ],
                request:$request,
            );

            return $sale->fresh('lines');
        });
    }
}

================================================================
FILE: .\app\Modules\Sales\Application\Numbering\SaleNumberGenerator.php
================================================================
<?php

namespace App\Modules\Sales\Application\Numbering;

use App\Modules\Sales\Domain\Models\SaleNumberSequence;
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\DB;

final class SaleNumberGenerator
{
    public function next(int $tenantId, ?int $branchId, CarbonImmutable $occurredAt): array
    {
        $businessDate=$occurredAt->toDateString();

        $value=DB::transaction(function () use ($tenantId,$branchId,$businessDate) {
            /*
             * Serialize first-use and increment for this tenant/branch/day.
             * Advisory locks avoid the classic "both sessions see no sequence row"
             * race before lockForUpdate can lock an existing row.
             */
            $branchKey=$branchId ?? 0;
            $lockKey=crc32("sales-number:$tenantId:$branchKey:$businessDate");

            DB::select('SELECT pg_advisory_xact_lock(?)', [$lockKey]);

            $sequence=SaleNumberSequence::query()
                ->where('tenant_id',$tenantId)
                ->where('business_date',$businessDate)
                ->when($branchId===null,fn($q)=>$q->whereNull('branch_id'),fn($q)=>$q->where('branch_id',$branchId))
                ->lockForUpdate()
                ->first();

            if ($sequence===null) {
                $sequence=SaleNumberSequence::query()->create([
                    'tenant_id'=>$tenantId,
                    'branch_id'=>$branchId,
                    'business_date'=>$businessDate,
                    'last_value'=>0,
                ]);
            }

            $sequence->last_value++;
            $sequence->save();

            return $sequence->last_value;
        });

        $branchPart=$branchId===null ? 'C' : 'B'.$branchId;

        return [
            'business_date'=>$businessDate,
            'sale_number'=>sprintf(
                '%s-%s-%06d',
                $occurredAt->format('Ymd'),
                $branchPart,
                $value
            ),
        ];
    }
}

================================================================
FILE: .\app\Modules\Sales\Domain\Models\CheckoutCommand.php
================================================================
<?php

namespace App\Modules\Sales\Domain\Models;

use App\Modules\Core\Domain\Concerns\HasPublicUlid;
use Illuminate\Database\Eloquent\Model;

final class CheckoutCommand extends Model
{
    use HasPublicUlid;

    public $timestamps = false;

    protected $table = 'sales.checkout_commands';

    protected $fillable = [
        'tenant_id','cart_id','sale_id','idempotency_key','status',
        'response_snapshot','failure_code','created_at','completed_at',
    ];

    protected function casts(): array
    {
        return [
            'response_snapshot'=>'array',
            'created_at'=>'immutable_datetime',
            'completed_at'=>'immutable_datetime',
        ];
    }
}

================================================================
FILE: .\app\Modules\Sales\Domain\Models\Cart.php
================================================================
<?php

namespace App\Modules\Sales\Domain\Models;

use App\Modules\Core\Domain\Concerns\HasPublicUlid;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

final class Cart extends Model
{
    use HasPublicUlid;

    protected $table = 'sales.carts';

    protected $fillable = [
        'tenant_id','company_id','branch_id','register_id','created_by_user_id',
        'customer_public_id','customer_group_key','currency_code','status',
        'subtotal_amount','discount_amount','total_amount','version','expires_at','metadata',
    ];

    protected function casts(): array
    {
        return [
            'subtotal_amount'=>'decimal:6',
            'discount_amount'=>'decimal:6',
            'total_amount'=>'decimal:6',
            'version'=>'integer',
            'expires_at'=>'immutable_datetime',
            'metadata'=>'array',
        ];
    }

    public function lines(): HasMany
    {
        return $this->hasMany(CartLine::class, 'cart_id');
    }
}

================================================================
FILE: .\app\Modules\Sales\Domain\Models\CartLine.php
================================================================
<?php

namespace App\Modules\Sales\Domain\Models;

use App\Modules\Core\Domain\Concerns\HasPublicUlid;
use Illuminate\Database\Eloquent\Model;

final class CartLine extends Model
{
    use HasPublicUlid;

    protected $table = 'sales.cart_lines';

    protected $fillable = [
        'tenant_id','cart_id','variant_id','variant_unit_id','quantity',
        'unit_price_amount','gross_amount','discount_amount','net_amount',
        'price_snapshot','promotion_snapshot','metadata',
    ];

    protected function casts(): array
    {
        return [
            'quantity'=>'decimal:6',
            'unit_price_amount'=>'decimal:6',
            'gross_amount'=>'decimal:6',
            'discount_amount'=>'decimal:6',
            'net_amount'=>'decimal:6',
            'price_snapshot'=>'array',
            'promotion_snapshot'=>'array',
            'metadata'=>'array',
        ];
    }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\database\migrations\2026_09_01_073000_create_pricing_foundation_tables.php
================================================================
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void {
        DB::statement('CREATE SCHEMA IF NOT EXISTS pricing');

        Schema::create('pricing.price_lists', function (Blueprint $t) {
            $t->bigIncrements('id'); $t->ulid('public_id')->unique(); $t->unsignedBigInteger('tenant_id');
            $t->string('name',180); $t->string('code',100); $t->string('currency_code',3)->default('EGP');
            $t->unsignedInteger('priority')->default(100); $t->boolean('is_default')->default(false);
            $t->timestampTz('effective_from')->nullable(); $t->timestampTz('effective_to')->nullable();
            $t->string('status',30)->default('active'); $t->jsonb('metadata')->nullable(); $t->timestampsTz();
            $t->foreign('tenant_id')->references('id')->on('core.tenants')->restrictOnDelete();
            $t->unique(['tenant_id','code']); $t->index(['tenant_id','status','priority']);
        });
        DB::statement("ALTER TABLE pricing.price_lists ADD CONSTRAINT pricing_price_lists_range_check CHECK (effective_to IS NULL OR effective_from IS NULL OR effective_to > effective_from)");
        DB::statement("CREATE UNIQUE INDEX pricing_price_lists_one_default_per_currency ON pricing.price_lists (tenant_id,currency_code) WHERE is_default=true AND status='active'");

        Schema::create('pricing.price_list_scopes', function (Blueprint $t) {
            $t->bigIncrements('id'); $t->ulid('public_id')->unique(); $t->unsignedBigInteger('tenant_id'); $t->unsignedBigInteger('price_list_id');
            $t->string('scope_type',30); $t->unsignedBigInteger('company_id')->nullable(); $t->unsignedBigInteger('branch_id')->nullable();
            $t->string('customer_group_key',100)->nullable(); $t->string('customer_public_id',64)->nullable(); $t->timestampsTz();
            $t->foreign('tenant_id')->references('id')->on('core.tenants')->restrictOnDelete();
            $t->foreign('price_list_id')->references('id')->on('pricing.price_lists')->restrictOnDelete();
            $t->foreign('company_id')->references('id')->on('core.companies')->restrictOnDelete();
            $t->foreign('branch_id')->references('id')->on('core.branches')->restrictOnDelete();
            $t->index(['tenant_id','scope_type']);
        });
        DB::statement("ALTER TABLE pricing.price_list_scopes ADD CONSTRAINT pricing_scope_type_check CHECK (scope_type IN ('tenant','company','branch','customer_group','customer'))");
        DB::statement("ALTER TABLE pricing.price_list_scopes ADD CONSTRAINT pricing_scope_shape_check CHECK (
          (scope_type='tenant' AND company_id IS NULL AND branch_id IS NULL AND customer_group_key IS NULL AND customer_public_id IS NULL) OR
          (scope_type='company' AND company_id IS NOT NULL AND branch_id IS NULL AND customer_group_key IS NULL AND customer_public_id IS NULL) OR
          (scope_type='branch' AND company_id IS NOT NULL AND branch_id IS NOT NULL AND customer_group_key IS NULL AND customer_public_id IS NULL) OR
          (scope_type='customer_group' AND company_id IS NULL AND branch_id IS NULL AND customer_group_key IS NOT NULL AND customer_public_id IS NULL) OR
          (scope_type='customer' AND company_id IS NULL AND branch_id IS NULL AND customer_group_key IS NULL AND customer_public_id IS NOT NULL)
        )");
        DB::statement("CREATE UNIQUE INDEX pricing_scope_tenant_uq ON pricing.price_list_scopes(tenant_id,price_list_id) WHERE scope_type='tenant'");
        DB::statement("CREATE UNIQUE INDEX pricing_scope_company_uq ON pricing.price_list_scopes(tenant_id,price_list_id,company_id) WHERE scope_type='company'");
        DB::statement("CREATE UNIQUE INDEX pricing_scope_branch_uq ON pricing.price_list_scopes(tenant_id,price_list_id,branch_id) WHERE scope_type='branch'");
        DB::statement("CREATE UNIQUE INDEX pricing_scope_group_uq ON pricing.price_list_scopes(tenant_id,price_list_id,customer_group_key) WHERE scope_type='customer_group'");
        DB::statement("CREATE UNIQUE INDEX pricing_scope_customer_uq ON pricing.price_list_scopes(tenant_id,price_list_id,customer_public_id) WHERE scope_type='customer'");

        Schema::create('pricing.price_entries', function (Blueprint $t) {
            $t->bigIncrements('id'); $t->ulid('public_id')->unique(); $t->unsignedBigInteger('tenant_id'); $t->unsignedBigInteger('price_list_id');
            $t->unsignedBigInteger('variant_id'); $t->unsignedBigInteger('variant_unit_id')->nullable();
            $t->decimal('price_amount',18,6); $t->decimal('min_price_amount',18,6)->nullable();
            $t->timestampTz('effective_from')->nullable(); $t->timestampTz('effective_to')->nullable();
            $t->string('status',30)->default('active'); $t->jsonb('metadata')->nullable(); $t->timestampsTz();
            $t->foreign('tenant_id')->references('id')->on('core.tenants')->restrictOnDelete();
            $t->foreign('price_list_id')->references('id')->on('pricing.price_lists')->restrictOnDelete();
            $t->foreign('variant_id')->references('id')->on('catalog.variants')->restrictOnDelete();
            $t->foreign('variant_unit_id')->references('id')->on('catalog.variant_units')->restrictOnDelete();
            $t->index(['tenant_id','variant_id','status']);
        });
        DB::statement("ALTER TABLE pricing.price_entries ADD CONSTRAINT pricing_entry_amount_check CHECK (price_amount>=0 AND (min_price_amount IS NULL OR (min_price_amount>=0 AND min_price_amount<=price_amount)))");
        DB::statement("ALTER TABLE pricing.price_entries ADD CONSTRAINT pricing_entry_range_check CHECK (effective_to IS NULL OR effective_from IS NULL OR effective_to > effective_from)");
        DB::statement("CREATE UNIQUE INDEX pricing_entry_variant_uq ON pricing.price_entries(tenant_id,price_list_id,variant_id) WHERE variant_unit_id IS NULL AND status='active'");
        DB::statement("CREATE UNIQUE INDEX pricing_entry_unit_uq ON pricing.price_entries(tenant_id,price_list_id,variant_id,variant_unit_id) WHERE variant_unit_id IS NOT NULL AND status='active'");

        Schema::create('pricing.margin_guards', function (Blueprint $t) {
            $t->bigIncrements('id'); $t->ulid('public_id')->unique(); $t->unsignedBigInteger('tenant_id');
            $t->string('scope_type',30); $t->unsignedBigInteger('company_id')->nullable(); $t->unsignedBigInteger('branch_id')->nullable();
            $t->decimal('minimum_margin_percent',9,4)->nullable(); $t->decimal('minimum_markup_percent',9,4)->nullable();
            $t->string('violation_action',30)->default('block'); $t->string('approval_action_code',180)->nullable();
            $t->string('status',30)->default('active'); $t->timestampsTz();
            $t->foreign('tenant_id')->references('id')->on('core.tenants')->restrictOnDelete();
            $t->foreign('company_id')->references('id')->on('core.companies')->restrictOnDelete();
            $t->foreign('branch_id')->references('id')->on('core.branches')->restrictOnDelete();
        });
        DB::statement("ALTER TABLE pricing.margin_guards ADD CONSTRAINT pricing_margin_scope_check CHECK (scope_type IN ('tenant','company','branch'))");
        DB::statement("ALTER TABLE pricing.margin_guards ADD CONSTRAINT pricing_margin_action_check CHECK (violation_action IN ('block','approval'))");
        DB::statement("ALTER TABLE pricing.margin_guards ADD CONSTRAINT pricing_margin_values_check CHECK (minimum_margin_percent IS NOT NULL OR minimum_markup_percent IS NOT NULL)");
        DB::statement("ALTER TABLE pricing.margin_guards ADD CONSTRAINT pricing_margin_shape_check CHECK (
          (scope_type='tenant' AND company_id IS NULL AND branch_id IS NULL) OR
          (scope_type='company' AND company_id IS NOT NULL AND branch_id IS NULL) OR
          (scope_type='branch' AND company_id IS NOT NULL AND branch_id IS NOT NULL)
        )");
        DB::statement("CREATE UNIQUE INDEX pricing_margin_tenant_uq ON pricing.margin_guards(tenant_id) WHERE scope_type='tenant' AND status='active'");
        DB::statement("CREATE UNIQUE INDEX pricing_margin_company_uq ON pricing.margin_guards(tenant_id,company_id) WHERE scope_type='company' AND status='active'");
        DB::statement("CREATE UNIQUE INDEX pricing_margin_branch_uq ON pricing.margin_guards(tenant_id,branch_id) WHERE scope_type='branch' AND status='active'");
    }

    public function down(): void {
        Schema::dropIfExists('pricing.margin_guards');
        Schema::dropIfExists('pricing.price_entries');
        Schema::dropIfExists('pricing.price_list_scopes');
        Schema::dropIfExists('pricing.price_lists');
    }
};

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\database\migrations\2026_09_01_082900_cleanup_partial_promotions_v16.php
================================================================
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        DB::statement('CREATE SCHEMA IF NOT EXISTS promotions');

        /*
         * v16 may have failed before Laravel recorded the migration.
         * Remove only objects owned by that still-pending migration.
         */
        Schema::dropIfExists('promotions.usage_counters');
        Schema::dropIfExists('promotions.rewards');
        Schema::dropIfExists('promotions.conditions');
        Schema::dropIfExists('promotions.targets');
        Schema::dropIfExists('promotions.scopes');
        Schema::dropIfExists('promotions.campaigns');
    }

    public function down(): void
    {
        // No-op recovery migration.
    }
};

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\database\migrations\2026_09_01_083000_create_promotions_foundation_tables.php
================================================================
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        DB::statement('CREATE SCHEMA IF NOT EXISTS promotions');

        Schema::create('promotions.campaigns', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->ulid('public_id')->unique();

            $table->unsignedBigInteger('tenant_id');

            $table->string('name', 180);
            $table->string('code', 100);

            $table->unsignedInteger('priority')->default(100);
            $table->boolean('is_exclusive')->default(false);

            $table->timestampTz('effective_from')->nullable();
            $table->timestampTz('effective_to')->nullable();

            $table->unsignedBigInteger('max_total_uses')->nullable();
            $table->unsignedBigInteger('max_uses_per_customer')->nullable();

            $table->string('status', 30)->default('active');

            $table->jsonb('compiled_rule')->nullable();
            $table->jsonb('metadata')->nullable();

            $table->timestampsTz();

            $table->foreign('tenant_id')
                ->references('id')->on('core.tenants')
                ->restrictOnDelete();

            $table->unique(['tenant_id', 'code']);
            $table->index(['tenant_id', 'status', 'priority']);
        });

        DB::statement("
            ALTER TABLE promotions.campaigns
            ADD CONSTRAINT promotions_campaigns_effective_range_check
            CHECK (effective_to IS NULL OR effective_from IS NULL OR effective_to > effective_from)
        ");

        Schema::create('promotions.scopes', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->ulid('public_id')->unique();

            $table->unsignedBigInteger('tenant_id');
            $table->unsignedBigInteger('campaign_id');

            $table->string('scope_type', 30);

            $table->unsignedBigInteger('company_id')->nullable();
            $table->unsignedBigInteger('branch_id')->nullable();

            $table->string('customer_group_key', 100)->nullable();
            $table->string('customer_public_id', 64)->nullable();

            $table->timestampsTz();

            $table->foreign('tenant_id')
                ->references('id')->on('core.tenants')
                ->restrictOnDelete();

            $table->foreign('campaign_id')
                ->references('id')->on('promotions.campaigns')
                ->restrictOnDelete();

            $table->foreign('company_id')
                ->references('id')->on('core.companies')
                ->restrictOnDelete();

            $table->foreign('branch_id')
                ->references('id')->on('core.branches')
                ->restrictOnDelete();

            $table->index(['tenant_id', 'scope_type']);
        });

        DB::statement("
            ALTER TABLE promotions.scopes
            ADD CONSTRAINT promotions_scopes_type_check
            CHECK (scope_type IN ('tenant','company','branch','customer_group','customer'))
        ");

        DB::statement("
            ALTER TABLE promotions.scopes
            ADD CONSTRAINT promotions_scopes_shape_check
            CHECK (
                (scope_type='tenant'
                    AND company_id IS NULL AND branch_id IS NULL
                    AND customer_group_key IS NULL AND customer_public_id IS NULL)
                OR
                (scope_type='company'
                    AND company_id IS NOT NULL AND branch_id IS NULL
                    AND customer_group_key IS NULL AND customer_public_id IS NULL)
                OR
                (scope_type='branch'
                    AND company_id IS NOT NULL AND branch_id IS NOT NULL
                    AND customer_group_key IS NULL AND customer_public_id IS NULL)
                OR
                (scope_type='customer_group'
                    AND company_id IS NULL AND branch_id IS NULL
                    AND customer_group_key IS NOT NULL AND customer_public_id IS NULL)
                OR
                (scope_type='customer'
                    AND company_id IS NULL AND branch_id IS NULL
                    AND customer_group_key IS NULL AND customer_public_id IS NOT NULL)
            )
        ");

        DB::statement("CREATE UNIQUE INDEX promotions_scope_tenant_uq
            ON promotions.scopes(tenant_id,campaign_id) WHERE scope_type='tenant'");
        DB::statement("CREATE UNIQUE INDEX promotions_scope_company_uq
            ON promotions.scopes(tenant_id,campaign_id,company_id) WHERE scope_type='company'");
        DB::statement("CREATE UNIQUE INDEX promotions_scope_branch_uq
            ON promotions.scopes(tenant_id,campaign_id,branch_id) WHERE scope_type='branch'");
        DB::statement("CREATE UNIQUE INDEX promotions_scope_group_uq
            ON promotions.scopes(tenant_id,campaign_id,customer_group_key) WHERE scope_type='customer_group'");
        DB::statement("CREATE UNIQUE INDEX promotions_scope_customer_uq
            ON promotions.scopes(tenant_id,campaign_id,customer_public_id) WHERE scope_type='customer'");

        Schema::create('promotions.targets', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->ulid('public_id')->unique();

            $table->unsignedBigInteger('tenant_id');
            $table->unsignedBigInteger('campaign_id');

            $table->string('target_type', 30);

            $table->unsignedBigInteger('product_family_id')->nullable();
            $table->unsignedBigInteger('variant_id')->nullable();
            $table->unsignedBigInteger('category_id')->nullable();

            $table->timestampsTz();

            $table->foreign('tenant_id')
                ->references('id')->on('core.tenants')
                ->restrictOnDelete();

            $table->foreign('campaign_id')
                ->references('id')->on('promotions.campaigns')
                ->restrictOnDelete();

            $table->foreign('product_family_id')
                ->references('id')->on('catalog.product_families')
                ->restrictOnDelete();

            $table->foreign('variant_id')
                ->references('id')->on('catalog.variants')
                ->restrictOnDelete();

            $table->foreign('category_id')
                ->references('id')->on('catalog.categories')
                ->restrictOnDelete();

            $table->index(['tenant_id', 'campaign_id', 'target_type']);
        });

        DB::statement("
            ALTER TABLE promotions.targets
            ADD CONSTRAINT promotions_targets_type_check
            CHECK (target_type IN ('all','product_family','variant','category'))
        ");

        DB::statement("
            ALTER TABLE promotions.targets
            ADD CONSTRAINT promotions_targets_shape_check
            CHECK (
                (target_type='all'
                    AND product_family_id IS NULL AND variant_id IS NULL AND category_id IS NULL)
                OR
                (target_type='product_family'
                    AND product_family_id IS NOT NULL AND variant_id IS NULL AND category_id IS NULL)
                OR
                (target_type='variant'
                    AND product_family_id IS NULL AND variant_id IS NOT NULL AND category_id IS NULL)
                OR
                (target_type='category'
                    AND product_family_id IS NULL AND variant_id IS NULL AND category_id IS NOT NULL)
            )
        ");

        Schema::create('promotions.conditions', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->ulid('public_id')->unique();

            $table->unsignedBigInteger('tenant_id');
            $table->unsignedBigInteger('campaign_id');

            $table->decimal('minimum_quantity', 18, 6)->nullable();
            $table->decimal('minimum_basket_amount', 18, 6)->nullable();

            $table->timestampsTz();

            $table->foreign('tenant_id')
                ->references('id')->on('core.tenants')
                ->restrictOnDelete();

            $table->foreign('campaign_id')
                ->references('id')->on('promotions.campaigns')
                ->restrictOnDelete();

            $table->unique(['campaign_id']);
        });

        DB::statement("
            ALTER TABLE promotions.conditions
            ADD CONSTRAINT promotions_conditions_values_check
            CHECK (
                (minimum_quantity IS NULL OR minimum_quantity > 0)
                AND
                (minimum_basket_amount IS NULL OR minimum_basket_amount >= 0)
            )
        ");

        Schema::create('promotions.rewards', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->ulid('public_id')->unique();

            $table->unsignedBigInteger('tenant_id');
            $table->unsignedBigInteger('campaign_id');

            $table->string('reward_type', 30);

            $table->decimal('percentage_value', 9, 4)->nullable();
            $table->decimal('fixed_amount', 18, 6)->nullable();

            $table->decimal('buy_quantity', 18, 6)->nullable();
            $table->decimal('get_quantity', 18, 6)->nullable();
            $table->unsignedBigInteger('reward_variant_id')->nullable();

            $table->timestampsTz();

            $table->foreign('tenant_id')
                ->references('id')->on('core.tenants')
                ->restrictOnDelete();

            $table->foreign('campaign_id')
                ->references('id')->on('promotions.campaigns')
                ->restrictOnDelete();

            $table->foreign('reward_variant_id')
                ->references('id')->on('catalog.variants')
                ->restrictOnDelete();

            $table->unique(['campaign_id']);
        });

        DB::statement("
            ALTER TABLE promotions.rewards
            ADD CONSTRAINT promotions_rewards_type_check
            CHECK (reward_type IN ('percentage_discount','fixed_discount','buy_x_get_y'))
        ");

        DB::statement("
            ALTER TABLE promotions.rewards
            ADD CONSTRAINT promotions_rewards_shape_check
            CHECK (
                (reward_type='percentage_discount'
                    AND percentage_value IS NOT NULL
                    AND percentage_value > 0
                    AND percentage_value <= 100
                    AND fixed_amount IS NULL
                    AND buy_quantity IS NULL
                    AND get_quantity IS NULL
                    AND reward_variant_id IS NULL)
                OR
                (reward_type='fixed_discount'
                    AND fixed_amount IS NOT NULL
                    AND fixed_amount > 0
                    AND percentage_value IS NULL
                    AND buy_quantity IS NULL
                    AND get_quantity IS NULL
                    AND reward_variant_id IS NULL)
                OR
                (reward_type='buy_x_get_y'
                    AND buy_quantity IS NOT NULL
                    AND buy_quantity > 0
                    AND get_quantity IS NOT NULL
                    AND get_quantity > 0
                    AND reward_variant_id IS NOT NULL
                    AND percentage_value IS NULL
                    AND fixed_amount IS NULL)
            )
        ");

        Schema::create('promotions.usage_counters', function (Blueprint $table) {
            $table->bigIncrements('id');

            $table->unsignedBigInteger('tenant_id');
            $table->unsignedBigInteger('campaign_id');

            $table->string('customer_public_id', 64)->nullable();

            $table->unsignedBigInteger('usage_count')->default(0);
            $table->timestampTz('last_used_at')->nullable();

            $table->timestampsTz();

            $table->foreign('tenant_id')
                ->references('id')->on('core.tenants')
                ->restrictOnDelete();

            $table->foreign('campaign_id')
                ->references('id')->on('promotions.campaigns')
                ->restrictOnDelete();

            $table->index(['tenant_id', 'campaign_id']);
            $table->index(['tenant_id', 'customer_public_id']);
        });

        DB::statement("CREATE UNIQUE INDEX promotions_usage_total_uq
            ON promotions.usage_counters(tenant_id,campaign_id)
            WHERE customer_public_id IS NULL");

        DB::statement("CREATE UNIQUE INDEX promotions_usage_customer_uq
            ON promotions.usage_counters(tenant_id,campaign_id,customer_public_id)
            WHERE customer_public_id IS NOT NULL");
    }

    public function down(): void
    {
        Schema::dropIfExists('promotions.usage_counters');
        Schema::dropIfExists('promotions.rewards');
        Schema::dropIfExists('promotions.conditions');
        Schema::dropIfExists('promotions.targets');
        Schema::dropIfExists('promotions.scopes');
        Schema::dropIfExists('promotions.campaigns');
    }
};

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\database\migrations\2026_09_01_093000_create_sales_cart_foundation_tables.php
================================================================
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        DB::statement('CREATE SCHEMA IF NOT EXISTS sales');

        Schema::create('sales.carts', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->ulid('public_id')->unique();

            $table->unsignedBigInteger('tenant_id');
            $table->unsignedBigInteger('company_id')->nullable();
            $table->unsignedBigInteger('branch_id')->nullable();
            $table->unsignedBigInteger('register_id')->nullable();
            $table->unsignedBigInteger('created_by_user_id');

            $table->string('customer_public_id', 64)->nullable();
            $table->string('customer_group_key', 100)->nullable();

            $table->string('currency_code', 3)->default('EGP');
            $table->string('status', 30)->default('open');

            $table->decimal('subtotal_amount', 18, 6)->default(0);
            $table->decimal('discount_amount', 18, 6)->default(0);
            $table->decimal('total_amount', 18, 6)->default(0);

            $table->unsignedBigInteger('version')->default(1);

            $table->timestampTz('expires_at')->nullable();
            $table->jsonb('metadata')->nullable();

            $table->timestampsTz();

            $table->foreign('tenant_id')->references('id')->on('core.tenants')->restrictOnDelete();
            $table->foreign('company_id')->references('id')->on('core.companies')->restrictOnDelete();
            $table->foreign('branch_id')->references('id')->on('core.branches')->restrictOnDelete();
            $table->foreign('register_id')->references('id')->on('core.registers')->restrictOnDelete();
            $table->foreign('created_by_user_id')->references('id')->on('users')->restrictOnDelete();

            $table->index(['tenant_id', 'status', 'updated_at']);
            $table->index(['tenant_id', 'branch_id', 'status']);
            $table->index(['tenant_id', 'created_by_user_id', 'status']);
        });

        DB::statement("
            ALTER TABLE sales.carts
            ADD CONSTRAINT sales_carts_status_check
            CHECK (status IN ('open','converted','abandoned','cancelled'))
        ");

        DB::statement("
            ALTER TABLE sales.carts
            ADD CONSTRAINT sales_carts_amounts_check
            CHECK (
                subtotal_amount >= 0
                AND discount_amount >= 0
                AND total_amount >= 0
                AND discount_amount <= subtotal_amount
                AND total_amount = subtotal_amount - discount_amount
            )
        ");

        Schema::create('sales.cart_lines', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->ulid('public_id')->unique();

            $table->unsignedBigInteger('tenant_id');
            $table->unsignedBigInteger('cart_id');

            $table->unsignedBigInteger('variant_id');
            $table->unsignedBigInteger('variant_unit_id')->nullable();

            $table->decimal('quantity', 18, 6);
            $table->decimal('unit_price_amount', 18, 6);

            $table->decimal('gross_amount', 18, 6);
            $table->decimal('discount_amount', 18, 6)->default(0);
            $table->decimal('net_amount', 18, 6);

            $table->jsonb('price_snapshot');
            $table->jsonb('promotion_snapshot')->nullable();
            $table->jsonb('metadata')->nullable();

            $table->timestampsTz();

            $table->foreign('tenant_id')->references('id')->on('core.tenants')->restrictOnDelete();
            $table->foreign('cart_id')->references('id')->on('sales.carts')->restrictOnDelete();
            $table->foreign('variant_id')->references('id')->on('catalog.variants')->restrictOnDelete();
            $table->foreign('variant_unit_id')->references('id')->on('catalog.variant_units')->restrictOnDelete();

            $table->index(['tenant_id', 'cart_id']);
            $table->index(['tenant_id', 'variant_id']);
        });

        DB::statement("
            ALTER TABLE sales.cart_lines
            ADD CONSTRAINT sales_cart_lines_amounts_check
            CHECK (
                quantity > 0
                AND unit_price_amount >= 0
                AND gross_amount >= 0
                AND discount_amount >= 0
                AND net_amount >= 0
                AND discount_amount <= gross_amount
                AND net_amount = gross_amount - discount_amount
            )
        ");

        Schema::create('sales.cart_operations', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->ulid('public_id')->unique();

            $table->unsignedBigInteger('tenant_id');
            $table->unsignedBigInteger('cart_id');

            $table->string('client_operation_id', 100);
            $table->string('operation_type', 40);

            $table->jsonb('response_snapshot')->nullable();

            $table->timestampTz('created_at')->useCurrent();

            $table->foreign('tenant_id')->references('id')->on('core.tenants')->restrictOnDelete();
            $table->foreign('cart_id')->references('id')->on('sales.carts')->restrictOnDelete();

            $table->unique(['tenant_id', 'client_operation_id']);
            $table->index(['tenant_id', 'cart_id', 'created_at']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('sales.cart_operations');
        Schema::dropIfExists('sales.cart_lines');
        Schema::dropIfExists('sales.carts');
    }
};

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\database\migrations\2026_09_01_103000_create_sales_checkout_foundation_tables.php
================================================================
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        DB::statement('CREATE SCHEMA IF NOT EXISTS sales');

        Schema::create('sales.number_sequences', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedBigInteger('tenant_id');
            $table->unsignedBigInteger('branch_id')->nullable();
            $table->date('business_date');
            $table->unsignedBigInteger('last_value')->default(0);
            $table->timestampsTz();

            $table->foreign('tenant_id')->references('id')->on('core.tenants')->restrictOnDelete();
            $table->foreign('branch_id')->references('id')->on('core.branches')->restrictOnDelete();

            $table->unique(['tenant_id','branch_id','business_date']);
        });

        Schema::create('sales.sales', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->ulid('public_id')->unique();

            $table->unsignedBigInteger('tenant_id');
            $table->unsignedBigInteger('company_id')->nullable();
            $table->unsignedBigInteger('branch_id')->nullable();
            $table->unsignedBigInteger('register_id')->nullable();
            $table->unsignedBigInteger('cart_id');

            $table->unsignedBigInteger('created_by_user_id');

            $table->string('sale_number', 80);
            $table->date('business_date');
            $table->timestampTz('occurred_at');

            $table->string('currency_code', 3)->default('EGP');

            $table->string('customer_public_id', 64)->nullable();
            $table->string('customer_group_key', 100)->nullable();

            $table->decimal('subtotal_amount', 18, 6);
            $table->decimal('discount_amount', 18, 6);
            $table->decimal('total_amount', 18, 6);

            $table->string('status', 30)->default('pending_payment');
            $table->string('payment_status', 30)->default('unpaid');

            $table->jsonb('pricing_snapshot')->nullable();
            $table->jsonb('promotion_snapshot')->nullable();
            $table->jsonb('metadata')->nullable();

            $table->timestampsTz();

            $table->foreign('tenant_id')->references('id')->on('core.tenants')->restrictOnDelete();
            $table->foreign('company_id')->references('id')->on('core.companies')->restrictOnDelete();
            $table->foreign('branch_id')->references('id')->on('core.branches')->restrictOnDelete();
            $table->foreign('register_id')->references('id')->on('core.registers')->restrictOnDelete();
            $table->foreign('cart_id')->references('id')->on('sales.carts')->restrictOnDelete();
            $table->foreign('created_by_user_id')->references('id')->on('users')->restrictOnDelete();

            $table->unique(['tenant_id','sale_number']);
            $table->unique(['tenant_id','cart_id']);
            $table->index(['tenant_id','business_date','status']);
            $table->index(['tenant_id','branch_id','business_date']);
            $table->index(['tenant_id','payment_status','created_at']);
        });

        DB::statement("
            ALTER TABLE sales.sales
            ADD CONSTRAINT sales_sales_amounts_check
            CHECK (
                subtotal_amount >= 0
                AND discount_amount >= 0
                AND total_amount >= 0
                AND discount_amount <= subtotal_amount
                AND total_amount = subtotal_amount - discount_amount
            )
        ");

        DB::statement("
            ALTER TABLE sales.sales
            ADD CONSTRAINT sales_sales_status_check
            CHECK (status IN ('pending_payment','completed','voided','reversed'))
        ");

        DB::statement("
            ALTER TABLE sales.sales
            ADD CONSTRAINT sales_sales_payment_status_check
            CHECK (payment_status IN ('unpaid','partial','paid','refunded','partially_refunded'))
        ");

        Schema::create('sales.sale_lines', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->ulid('public_id')->unique();

            $table->unsignedBigInteger('tenant_id');
            $table->unsignedBigInteger('sale_id');

            $table->unsignedBigInteger('variant_id');
            $table->unsignedBigInteger('variant_unit_id')->nullable();

            $table->string('sku_snapshot', 160)->nullable();
            $table->string('name_snapshot', 220);

            $table->decimal('quantity', 18, 6);
            $table->decimal('unit_price_amount', 18, 6);
            $table->decimal('gross_amount', 18, 6);
            $table->decimal('discount_amount', 18, 6);
            $table->decimal('net_amount', 18, 6);

            $table->jsonb('price_snapshot');
            $table->jsonb('promotion_snapshot')->nullable();
            $table->jsonb('metadata')->nullable();

            $table->timestampTz('created_at')->useCurrent();

            $table->foreign('tenant_id')->references('id')->on('core.tenants')->restrictOnDelete();
            $table->foreign('sale_id')->references('id')->on('sales.sales')->restrictOnDelete();
            $table->foreign('variant_id')->references('id')->on('catalog.variants')->restrictOnDelete();
            $table->foreign('variant_unit_id')->references('id')->on('catalog.variant_units')->restrictOnDelete();

            $table->index(['tenant_id','sale_id']);
            $table->index(['tenant_id','variant_id']);
        });

        DB::statement("
            ALTER TABLE sales.sale_lines
            ADD CONSTRAINT sales_sale_lines_amounts_check
            CHECK (
                quantity > 0
                AND unit_price_amount >= 0
                AND gross_amount >= 0
                AND discount_amount >= 0
                AND net_amount >= 0
                AND discount_amount <= gross_amount
                AND net_amount = gross_amount - discount_amount
            )
        ");

        Schema::create('sales.checkout_commands', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->ulid('public_id')->unique();

            $table->unsignedBigInteger('tenant_id');
            $table->unsignedBigInteger('cart_id');
            $table->unsignedBigInteger('sale_id')->nullable();

            $table->string('idempotency_key', 120);
            $table->string('status', 30)->default('started');

            $table->jsonb('response_snapshot')->nullable();
            $table->text('failure_code')->nullable();

            $table->timestampTz('created_at')->useCurrent();
            $table->timestampTz('completed_at')->nullable();

            $table->foreign('tenant_id')->references('id')->on('core.tenants')->restrictOnDelete();
            $table->foreign('cart_id')->references('id')->on('sales.carts')->restrictOnDelete();
            $table->foreign('sale_id')->references('id')->on('sales.sales')->restrictOnDelete();

            $table->unique(['tenant_id','idempotency_key']);
            $table->index(['tenant_id','cart_id','status']);
        });

        DB::statement("
            ALTER TABLE sales.checkout_commands
            ADD CONSTRAINT sales_checkout_commands_status_check
            CHECK (status IN ('started','completed','failed','unknown'))
        ");

        DB::statement(<<<'SQL'
CREATE OR REPLACE FUNCTION sales.prevent_sale_line_mutation()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
    RAISE EXCEPTION 'sales.sale_lines are immutable';
END;
$$
SQL);

        DB::statement(<<<'SQL'
CREATE TRIGGER sales_sale_lines_immutable_update
BEFORE UPDATE ON sales.sale_lines
FOR EACH ROW EXECUTE FUNCTION sales.prevent_sale_line_mutation()
SQL);

        DB::statement(<<<'SQL'
CREATE TRIGGER sales_sale_lines_immutable_delete
BEFORE DELETE ON sales.sale_lines
FOR EACH ROW EXECUTE FUNCTION sales.prevent_sale_line_mutation()
SQL);
    }

    public function down(): void
    {
        DB::statement('DROP TRIGGER IF EXISTS sales_sale_lines_immutable_delete ON sales.sale_lines');
        DB::statement('DROP TRIGGER IF EXISTS sales_sale_lines_immutable_update ON sales.sale_lines');
        DB::statement('DROP FUNCTION IF EXISTS sales.prevent_sale_line_mutation()');

        Schema::dropIfExists('sales.checkout_commands');
        Schema::dropIfExists('sales.sale_lines');
        Schema::dropIfExists('sales.sales');
        Schema::dropIfExists('sales.number_sequences');
    }
};

