================================================================
FILE: .\app\Modules\Sales\Domain\Models\Sale.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 Sale extends Model
{
    use HasPublicUlid;

    protected $table = 'sales.sales';

    protected $fillable = [
        'tenant_id','company_id','branch_id','register_id','cart_id',
        'created_by_user_id','sale_number','business_date','occurred_at',
        'currency_code','customer_public_id','customer_group_key',
        'subtotal_amount','discount_amount','total_amount',
        'status','payment_status','pricing_snapshot','promotion_snapshot','metadata',
    ];

    protected function casts(): array
    {
        return [
            'business_date'=>'date:Y-m-d',
            'occurred_at'=>'immutable_datetime',
            'subtotal_amount'=>'decimal:6',
            'discount_amount'=>'decimal:6',
            'total_amount'=>'decimal:6',
            'pricing_snapshot'=>'array',
            'promotion_snapshot'=>'array',
            'metadata'=>'array',
        ];
    }

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

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

namespace App\Modules\Sales\Domain\Models;

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

final class SaleLine extends Model
{
    use HasPublicUlid;

    public $timestamps = false;

    protected $table = 'sales.sale_lines';

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

    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',
            'created_at'=>'immutable_datetime',
        ];
    }

    public function save(array $options = []): bool
    {
        if ($this->exists) {
            throw new LogicException('Sale lines are immutable.');
        }

        return parent::save($options);
    }

    public function delete(): ?bool
    {
        throw new LogicException('Sale lines are immutable.');
    }
}

================================================================
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\Payments\Domain\Models\Payment.php
================================================================
<?php

namespace App\Modules\Payments\Domain\Models;

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

final class Payment extends Model
{
    use HasPublicUlid;

    protected $table = 'payments.payments';

    protected $fillable = [
        'tenant_id','sale_id','method_type','amount','currency_code','status',
        'idempotency_key','provider_reference','created_by_user_id',
        'resolved_at','metadata',
    ];

    protected function casts(): array
    {
        return [
            'amount'=>'decimal:6',
            'resolved_at'=>'immutable_datetime',
            'metadata'=>'array',
        ];
    }

    public function attempts(): HasMany
    {
        return $this->hasMany(PaymentAttempt::class, 'payment_id');
    }
}

================================================================
FILE: .\app\Modules\Payments\Application\Settlement\CollectPaymentAction.php
================================================================
<?php

namespace App\Modules\Payments\Application\Settlement;

use App\Modules\Audit\Application\AuditRecorder;
use App\Modules\Cash\Application\Shifts\AutoPostCapturedCashPaymentToShiftAction;
use App\Modules\Payments\Domain\Models\Payment;
use App\Modules\Payments\Domain\Models\PaymentAttempt;
use App\Modules\Sales\Domain\Models\Sale;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;

final readonly class CollectPaymentAction
{
    public function __construct(
        private SalePaymentStatusUpdater $statusUpdater,
        private AuditRecorder $auditRecorder,
        private AutoPostCapturedCashPaymentToShiftAction $autoPostCashShift,
    ) {}

    public function execute(
        Sale $sale,
        array $data,
        int $actorUserId,
        Request $request,
    ): Payment {
        $existing=Payment::query()
            ->where('tenant_id',$sale->tenant_id)
            ->where('idempotency_key',$data['idempotency_key'])
            ->first();

        if ($existing) {
            if ($existing->sale_id !== $sale->id) {
                throw new ConflictHttpException('Idempotency key already belongs to another sale.');
            }

            /*
             * Recovery behavior:
             * an old captured cash payment may predate automatic posting.
             * A safe retry can complete the missing shift posting exactly once.
             */
            if ($existing->status==='captured' && $existing->method_type==='cash') {
                $saleFresh=Sale::query()->findOrFail($existing->sale_id);
                $this->autoPostCashShift->execute($existing,$saleFresh,$actorUserId);
            }

            return $existing->load('attempts');
        }

        return DB::transaction(function () use ($sale,$data,$actorUserId,$request) {
            $lockedSale=Sale::query()
                ->whereKey($sale->id)
                ->lockForUpdate()
                ->firstOrFail();

            $existing=Payment::query()
                ->where('tenant_id',$lockedSale->tenant_id)
                ->where('idempotency_key',$data['idempotency_key'])
                ->lockForUpdate()
                ->first();

            if ($existing) {
                if ($existing->sale_id !== $lockedSale->id) {
                    throw new ConflictHttpException('Idempotency key already belongs to another sale.');
                }

                if ($existing->status==='captured' && $existing->method_type==='cash') {
                    $this->autoPostCashShift->execute($existing,$lockedSale,$actorUserId);
                }

                return $existing->load('attempts');
            }

            if (in_array($lockedSale->status,['voided','reversed'],true)) {
                throw new ConflictHttpException('Sale cannot accept payments.');
            }

            if ($lockedSale->payment_status === 'paid') {
                throw new ConflictHttpException('Sale is already fully paid.');
            }

            $captured=$this->statusUpdater->capturedAmount($lockedSale);
            $remaining=bcsub((string)$lockedSale->total_amount,$captured,6);
            $amount=(string)$data['amount'];

            if (bccomp($amount,$remaining,6) > 0) {
                throw ValidationException::withMessages([
                    'amount'=>['Payment amount cannot exceed the remaining sale balance.'],
                ]);
            }

            $method=$data['method_type'];
            $outcome=$method === 'cash'
                ? 'succeeded'
                : ($data['outcome'] ?? null);

            if ($method !== 'cash' && $outcome === null) {
                throw ValidationException::withMessages([
                    'outcome'=>['Card and external payments require succeeded, failed, or unknown outcome.'],
                ]);
            }

            $status=match($outcome) {
                'succeeded'=>'captured',
                'failed'=>'failed',
                'unknown'=>'unknown',
                default=>'pending',
            };

            $payment=Payment::query()->create([
                'tenant_id'=>$lockedSale->tenant_id,
                'sale_id'=>$lockedSale->id,
                'method_type'=>$method,
                'amount'=>$amount,
                'currency_code'=>$lockedSale->currency_code,
                'status'=>$status,
                'idempotency_key'=>$data['idempotency_key'],
                'provider_reference'=>$data['provider_reference']??null,
                'created_by_user_id'=>$actorUserId,
                'resolved_at'=>in_array($status,['captured','failed'],true) ? now() : null,
                'metadata'=>$data['metadata']??null,
            ]);

            PaymentAttempt::query()->create([
                'tenant_id'=>$lockedSale->tenant_id,
                'payment_id'=>$payment->id,
                'operation_key'=>$data['idempotency_key'],
                'attempt_type'=>'collect',
                'status'=>match($status) {
                    'captured'=>'succeeded',
                    'failed'=>'failed',
                    'unknown'=>'unknown',
                    default=>'started',
                },
                'provider_reference'=>$data['provider_reference']??null,
                'request_snapshot'=>[
                    'method_type'=>$method,
                    'amount'=>$amount,
                ],
                'response_snapshot'=>[
                    'outcome'=>$outcome,
                ],
                'started_at'=>now(),
                'completed_at'=>$status==='pending' ? null : now(),
            ]);

            $saleAfter=$this->statusUpdater->refresh($lockedSale);

            /*
             * Same database transaction:
             * if a registered cash sale has no open shift, this throws and the
             * payment capture rolls back. No captured cash can escape the shift ledger.
             */
            if ($payment->status==='captured' && $payment->method_type==='cash') {
                $this->autoPostCashShift->execute($payment,$saleAfter,$actorUserId);
            }

            $this->auditRecorder->record(
                'payments.payment.collected',
                $lockedSale->tenant_id,
                $actorUserId,
                'payments.payment',
                $payment->public_id,
                after:[
                    'sale_id'=>$lockedSale->public_id,
                    'method_type'=>$payment->method_type,
                    'amount'=>$payment->amount,
                    'status'=>$payment->status,
                    'sale_payment_status'=>$saleAfter->payment_status,
                ],
                request:$request,
            );

            return $payment->fresh('attempts');
        });
    }
}

================================================================
FILE: .\app\Modules\Payments\Application\Settlement\ResolvePaymentAction.php
================================================================
<?php

namespace App\Modules\Payments\Application\Settlement;

use App\Modules\Audit\Application\AuditRecorder;
use App\Modules\Cash\Application\Shifts\AutoPostCapturedCashPaymentToShiftAction;
use App\Modules\Payments\Domain\Models\Payment;
use App\Modules\Payments\Domain\Models\PaymentAttempt;
use App\Modules\Sales\Domain\Models\Sale;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;

final readonly class ResolvePaymentAction
{
    public function __construct(
        private SalePaymentStatusUpdater $statusUpdater,
        private AuditRecorder $auditRecorder,
        private AutoPostCapturedCashPaymentToShiftAction $autoPostCashShift,
    ) {}

    public function execute(
        Payment $payment,
        array $data,
        int $actorUserId,
        Request $request,
    ): Payment {
        $existingAttempt=PaymentAttempt::query()
            ->where('tenant_id',$payment->tenant_id)
            ->where('operation_key',$data['resolution_key'])
            ->first();

        if ($existingAttempt) {
            if ($existingAttempt->payment_id !== $payment->id) {
                throw new ConflictHttpException('Resolution key already belongs to another payment.');
            }

            $fresh=$payment->fresh();

            if ($fresh->status==='captured' && $fresh->method_type==='cash') {
                $sale=Sale::query()->findOrFail($fresh->sale_id);
                $this->autoPostCashShift->execute($fresh,$sale,$actorUserId);
            }

            return $fresh->load('attempts');
        }

        return DB::transaction(function () use ($payment,$data,$actorUserId,$request) {
            $locked=Payment::query()->whereKey($payment->id)->lockForUpdate()->firstOrFail();

            $existingAttempt=PaymentAttempt::query()
                ->where('tenant_id',$locked->tenant_id)
                ->where('operation_key',$data['resolution_key'])
                ->lockForUpdate()
                ->first();

            if ($existingAttempt) {
                if ($existingAttempt->payment_id !== $locked->id) {
                    throw new ConflictHttpException('Resolution key already belongs to another payment.');
                }

                $sale=Sale::query()->whereKey($locked->sale_id)->lockForUpdate()->firstOrFail();

                if ($locked->status==='captured' && $locked->method_type==='cash') {
                    $this->autoPostCashShift->execute($locked,$sale,$actorUserId);
                }

                return $locked->fresh('attempts');
            }

            if ($locked->status !== 'unknown') {
                throw new ConflictHttpException('Only UNKNOWN payments can be resolved.');
            }

            $before=$locked->status;
            $locked->status=$data['outcome']==='succeeded' ? 'captured' : 'failed';
            $locked->provider_reference=$data['provider_reference'] ?? $locked->provider_reference;
            $locked->resolved_at=now();

            if (!empty($data['metadata'])) {
                $locked->metadata=array_merge($locked->metadata ?? [],$data['metadata']);
            }

            $locked->save();

            PaymentAttempt::query()->create([
                'tenant_id'=>$locked->tenant_id,
                'payment_id'=>$locked->id,
                'operation_key'=>$data['resolution_key'],
                'attempt_type'=>'resolve',
                'status'=>$data['outcome']==='succeeded' ? 'succeeded' : 'failed',
                'provider_reference'=>$data['provider_reference']??$locked->provider_reference,
                'request_snapshot'=>[
                    'outcome'=>$data['outcome'],
                ],
                'response_snapshot'=>[
                    'payment_status'=>$locked->status,
                ],
                'started_at'=>now(),
                'completed_at'=>now(),
            ]);

            $sale=Sale::query()->whereKey($locked->sale_id)->lockForUpdate()->firstOrFail();
            $saleAfter=$this->statusUpdater->refresh($sale);

            if ($locked->status==='captured' && $locked->method_type==='cash') {
                $this->autoPostCashShift->execute($locked,$saleAfter,$actorUserId);
            }

            $this->auditRecorder->record(
                'payments.payment.resolved',
                $locked->tenant_id,
                $actorUserId,
                'payments.payment',
                $locked->public_id,
                before:['status'=>$before],
                after:[
                    'status'=>$locked->status,
                    'sale_id'=>$sale->public_id,
                    'sale_payment_status'=>$saleAfter->payment_status,
                ],
                request:$request,
            );

            return $locked->fresh('attempts');
        });
    }
}

================================================================
FILE: .\app\Modules\Payments\Application\Settlement\SalePaymentStatusUpdater.php
================================================================
<?php

namespace App\Modules\Payments\Application\Settlement;

use App\Modules\Payments\Domain\Models\Payment;
use App\Modules\Sales\Domain\Models\Sale;

final class SalePaymentStatusUpdater
{
    public function refresh(Sale $sale): Sale
    {
        $captured='0.000000';

        Payment::query()
            ->where('tenant_id',$sale->tenant_id)
            ->where('sale_id',$sale->id)
            ->where('status','captured')
            ->orderBy('id')
            ->pluck('amount')
            ->each(function ($amount) use (&$captured) {
                $captured=bcadd($captured,(string)$amount,6);
            });

        $hasUnknown=Payment::query()
            ->where('tenant_id',$sale->tenant_id)
            ->where('sale_id',$sale->id)
            ->where('status','unknown')
            ->exists();

        if ($hasUnknown) {
            $paymentStatus='unknown';
            $saleStatus='pending_payment';
        } elseif (bccomp($captured,(string)$sale->total_amount,6) >= 0) {
            $paymentStatus='paid';
            $saleStatus='completed';
        } elseif (bccomp($captured,'0.000000',6) > 0) {
            $paymentStatus='partial';
            $saleStatus='pending_payment';
        } else {
            $paymentStatus='unpaid';
            $saleStatus='pending_payment';
        }

        $sale->payment_status=$paymentStatus;
        $sale->status=$saleStatus;
        $sale->save();

        return $sale->fresh();
    }

    public function capturedAmount(Sale $sale): string
    {
        $total='0.000000';

        Payment::query()
            ->where('tenant_id',$sale->tenant_id)
            ->where('sale_id',$sale->id)
            ->where('status','captured')
            ->pluck('amount')
            ->each(function ($amount) use (&$total) {
                $total=bcadd($total,(string)$amount,6);
            });

        return $total;
    }
}

================================================================
FILE: .\app\Modules\Cash\Application\Shifts\PostCashRefundToShiftAction.php
================================================================
<?php

namespace App\Modules\Cash\Application\Shifts;

use App\Modules\Audit\Application\AuditRecorder;
use App\Modules\Cash\Application\Ledger\CashLedgerService;
use App\Modules\Cash\Domain\Models\CashPaymentRefundBalance;
use App\Modules\Cash\Domain\Models\CashSaleRefund;
use App\Modules\Cash\Domain\Models\RegisterShift;
use App\Modules\Cash\Domain\Models\RegisterShiftEntry;
use App\Modules\Core\Domain\Models\Register;
use App\Modules\Payments\Domain\Models\Payment;
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 PostCashRefundToShiftAction
{
    public function __construct(
        private CashLedgerService $cashLedger,
        private AuditRecorder $auditRecorder,
    ) {}

    public function execute(
        Payment $payment,
        array $data,
        int $actorUserId,
        Request $request,
    ): CashSaleRefund {
        $existing=CashSaleRefund::query()
            ->where('tenant_id',$payment->tenant_id)
            ->where('idempotency_key',$data['idempotency_key'])
            ->first();

        if($existing) return $existing;

        return DB::transaction(function () use ($payment,$data,$actorUserId,$request) {
            $lockedPayment=Payment::query()
                ->where('tenant_id',$payment->tenant_id)
                ->whereKey($payment->id)
                ->lockForUpdate()
                ->firstOrFail();

            if($lockedPayment->method_type!=='cash') {
                throw ValidationException::withMessages([
                    'payment'=>['Only cash payments can be refunded from a cash shift.'],
                ]);
            }

            if($lockedPayment->status!=='captured') {
                throw new ConflictHttpException('Only captured cash payments can be refunded.');
            }

            $existing=CashSaleRefund::query()
                ->where('tenant_id',$lockedPayment->tenant_id)
                ->where('idempotency_key',$data['idempotency_key'])
                ->lockForUpdate()
                ->first();

            if($existing) return $existing;

            $register=Register::query()
                ->where('tenant_id',$lockedPayment->tenant_id)
                ->where('public_id',$data['register_id'])
                ->where('status','active')
                ->first();

            if(!$register) {
                throw ValidationException::withMessages([
                    'register_id'=>['Active register was not found in this tenant.'],
                ]);
            }

            $shift=RegisterShift::query()
                ->where('tenant_id',$lockedPayment->tenant_id)
                ->where('register_id',$register->id)
                ->where('status','open')
                ->lockForUpdate()
                ->first();

            if(!$shift) {
                throw new ConflictHttpException('Register has no open shift.');
            }

            $refundBalance=CashPaymentRefundBalance::query()
                ->where('tenant_id',$lockedPayment->tenant_id)
                ->where('payment_id',$lockedPayment->id)
                ->lockForUpdate()
                ->first();

            if(!$refundBalance) {
                $refundBalance=CashPaymentRefundBalance::query()->create([
                    'tenant_id'=>$lockedPayment->tenant_id,
                    'payment_id'=>$lockedPayment->id,
                    'refunded_amount'=>'0.000000',
                ]);
            }

            $amount=(string)$data['amount'];
            $afterRefunded=bcadd((string)$refundBalance->refunded_amount,$amount,6);

            if(bccomp($afterRefunded,(string)$lockedPayment->amount,6)>0) {
                throw ValidationException::withMessages([
                    'amount'=>['Refund exceeds remaining refundable cash payment amount.'],
                ]);
            }

            if(bccomp($amount,(string)$shift->expected_cash_amount,6)>0) {
                throw ValidationException::withMessages([
                    'amount'=>['Refund exceeds shift expected cash.'],
                ]);
            }

            $at=CarbonImmutable::now('UTC');

            $refund=CashSaleRefund::query()->create([
                'tenant_id'=>$lockedPayment->tenant_id,
                'payment_id'=>$lockedPayment->id,
                'shift_id'=>$shift->id,
                'amount'=>$amount,
                'reason'=>$data['reason'],
                'idempotency_key'=>$data['idempotency_key'],
                'posted_at'=>$at,
                'posted_by_user_id'=>$actorUserId,
                'metadata'=>$data['metadata']??null,
                'created_at'=>now(),
            ]);

            $movement=$this->cashLedger->post(
                $lockedPayment->tenant_id,
                $shift->cash_account_id,
                'shift_cash_out',
                bcmul($amount,'-1',6),
                'sale_cash_refund',
                $refund->public_id,
                $at,
                $shift->business_date->format('Y-m-d'),
                $actorUserId,
                [
                    'payment_id'=>$lockedPayment->public_id,
                    'refund_id'=>$refund->public_id,
                    'register_id'=>$register->public_id,
                    'reason'=>$data['reason'],
                ],
            );

            RegisterShiftEntry::query()->create([
                'tenant_id'=>$lockedPayment->tenant_id,
                'shift_id'=>$shift->id,
                'entry_type'=>'refund_cash',
                'amount_delta'=>bcmul($amount,'-1',6),
                'source_type'=>'sale_cash_refund',
                'source_public_id'=>$refund->public_id,
                'occurred_at'=>$at,
                'created_by_user_id'=>$actorUserId,
                'metadata'=>[
                    'payment_id'=>$lockedPayment->public_id,
                    'refund_id'=>$refund->public_id,
                    'cash_movement_id'=>$movement->public_id,
                    'reason'=>$data['reason'],
                ],
                'created_at'=>now(),
            ]);

            $shift->expected_cash_amount=bcsub((string)$shift->expected_cash_amount,$amount,6);
            $shift->save();

            $refundBalance->refunded_amount=$afterRefunded;
            $refundBalance->last_refund_at=$at;
            $refundBalance->save();

            $this->auditRecorder->record(
                'cash.shift.refund.posted',
                $lockedPayment->tenant_id,
                $actorUserId,
                'cash.sale_refund',
                $refund->public_id,
                after:[
                    'payment_id'=>$lockedPayment->public_id,
                    'register_id'=>$register->public_id,
                    'amount'=>$amount,
                    'refunded_total'=>$afterRefunded,
                    'reason'=>$data['reason'],
                ],
                request:$request,
            );

            return $refund;
        });
    }
}

================================================================
FILE: .\app\Modules\Cash\Domain\Models\CashSaleRefund.php
================================================================
<?php

namespace App\Modules\Cash\Domain\Models;

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

final class CashSaleRefund extends Model
{
    use HasPublicUlid;

    public $timestamps = false;

    protected $table = 'cash.sale_cash_refunds';

    protected $fillable = [
        'tenant_id','payment_id','shift_id',
        'amount','reason','idempotency_key',
        'posted_at','posted_by_user_id','metadata','created_at',
    ];

    protected function casts(): array
    {
        return [
            'amount'=>'decimal:6',
            'posted_at'=>'immutable_datetime',
            'metadata'=>'array',
            'created_at'=>'immutable_datetime',
        ];
    }
}

================================================================
FILE: .\app\Modules\Cash\Domain\Models\CashPaymentRefundBalance.php
================================================================
<?php

namespace App\Modules\Cash\Domain\Models;

use Illuminate\Database\Eloquent\Model;

final class CashPaymentRefundBalance extends Model
{
    protected $table = 'cash.payment_refund_balances';

    protected $fillable = [
        'tenant_id','payment_id','refunded_amount','last_refund_at',
    ];

    protected function casts(): array
    {
        return [
            'refunded_amount'=>'decimal:6',
            'last_refund_at'=>'immutable_datetime',
        ];
    }
}

================================================================
FILE: .\app\Modules\Inventory\Application\Ledger\InventoryLedgerService.php
================================================================
<?php

namespace App\Modules\Inventory\Application\Ledger;

use App\Modules\Inventory\Application\Guards\WarehouseFreezeGuard;
use App\Modules\Inventory\Application\Valuation\WeightedAverageValuationService;
use App\Modules\Inventory\Domain\Models\InventoryBalance;
use App\Modules\Inventory\Domain\Models\InventoryLot;
use App\Modules\Inventory\Domain\Models\InventoryLotBalance;
use App\Modules\Inventory\Domain\Models\InventoryMovement;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

final readonly class InventoryLedgerService
{
    public function __construct(
        private WarehouseFreezeGuard $freezeGuard,
        private WeightedAverageValuationService $valuation,
    ) {}

    public function post(
        int $tenantId,
        int $warehouseId,
        int $variantId,
        ?int $variantUnitId,
        string $movementType,
        string $quantityDelta,
        string $sourceType,
        string $sourcePublicId,
        ?string $sourceLinePublicId,
        \DateTimeInterface $occurredAt,
        string $businessDate,
        ?int $actorUserId,
        ?array $metadata=null,
        bool $bypassFreeze=false,
        ?int $lotId=null,
        ?string $unitCostAmount=null,
    ): InventoryMovement {
        return DB::transaction(function () use (
            $tenantId,$warehouseId,$variantId,$variantUnitId,$movementType,$quantityDelta,
            $sourceType,$sourcePublicId,$sourceLinePublicId,$occurredAt,$businessDate,
            $actorUserId,$metadata,$bypassFreeze,$lotId,$unitCostAmount
        ) {
            if (!$bypassFreeze) {
                $this->freezeGuard->assertNotFrozen($tenantId,$warehouseId);
            }

            $lot=null;

            if ($lotId!==null) {
                $lot=InventoryLot::query()
                    ->where('tenant_id',$tenantId)
                    ->where('variant_id',$variantId)
                    ->whereKey($lotId)
                    ->first();

                if (!$lot) {
                    throw ValidationException::withMessages([
                        'lot_id'=>['Lot was not found for this tenant and variant.'],
                    ]);
                }

                if ($lot->variant_unit_id!==null && $variantUnitId!==$lot->variant_unit_id) {
                    throw ValidationException::withMessages([
                        'variant_unit_id'=>['Movement unit does not match lot unit.'],
                    ]);
                }
            }

            $lockKey=crc32("inventory:$tenantId:$warehouseId:$variantId:".($variantUnitId ?? 0));
            DB::select('SELECT pg_advisory_xact_lock(?)',[$lockKey]);

            if ($lotId!==null) {
                $lotLock=crc32("inventory-lot:$tenantId:$warehouseId:$lotId");
                DB::select('SELECT pg_advisory_xact_lock(?)',[$lotLock]);
            }

            $existing=InventoryMovement::query()
                ->where('tenant_id',$tenantId)
                ->where('source_type',$sourceType)
                ->where('source_public_id',$sourcePublicId)
                ->when(
                    $sourceLinePublicId===null,
                    fn($q)=>$q->whereNull('source_line_public_id'),
                    fn($q)=>$q->where('source_line_public_id',$sourceLinePublicId)
                )
                ->first();

            if ($existing) return $existing;

            $balance=InventoryBalance::query()
                ->where('tenant_id',$tenantId)
                ->where('warehouse_id',$warehouseId)
                ->where('variant_id',$variantId)
                ->when(
                    $variantUnitId===null,
                    fn($q)=>$q->whereNull('variant_unit_id'),
                    fn($q)=>$q->where('variant_unit_id',$variantUnitId)
                )
                ->lockForUpdate()
                ->first();

            if ($balance===null) {
                $balance=InventoryBalance::query()->create([
                    'tenant_id'=>$tenantId,
                    'warehouse_id'=>$warehouseId,
                    'variant_id'=>$variantId,
                    'variant_unit_id'=>$variantUnitId,
                    'quantity_on_hand'=>'0.000000',
                ]);
            }

            $after=bcadd((string)$balance->quantity_on_hand,$quantityDelta,6);

            if (bccomp($after,'0.000000',6)<0) {
                throw ValidationException::withMessages([
                    'quantity'=>['Insufficient stock. Negative inventory is disabled.'],
                ]);
            }

            $lotBalance=null;
            $lotAfter=null;

            if ($lotId!==null) {
                $lotBalance=InventoryLotBalance::query()
                    ->where('tenant_id',$tenantId)
                    ->where('warehouse_id',$warehouseId)
                    ->where('lot_id',$lotId)
                    ->lockForUpdate()
                    ->first();

                if ($lotBalance===null) {
                    $lotBalance=InventoryLotBalance::query()->create([
                        'tenant_id'=>$tenantId,
                        'warehouse_id'=>$warehouseId,
                        'lot_id'=>$lotId,
                        'quantity_on_hand'=>'0.000000',
                    ]);
                }

                $lotAfter=bcadd((string)$lotBalance->quantity_on_hand,$quantityDelta,6);

                if (bccomp($lotAfter,'0.000000',6)<0) {
                    throw ValidationException::withMessages([
                        'quantity'=>['Insufficient stock in selected lot.'],
                    ]);
                }
            }

            $valuation=$this->valuation->apply(
                tenantId:$tenantId,
                warehouseId:$warehouseId,
                variantId:$variantId,
                variantUnitId:$variantUnitId,
                quantityDelta:$quantityDelta,
                incomingUnitCost:$unitCostAmount,
                occurredAt:$occurredAt,
            );

            $movement=InventoryMovement::query()->create([
                'tenant_id'=>$tenantId,
                'warehouse_id'=>$warehouseId,
                'variant_id'=>$variantId,
                'variant_unit_id'=>$variantUnitId,
                'lot_id'=>$lotId,
                'movement_type'=>$movementType,
                'quantity_delta'=>$quantityDelta,
                'unit_cost_amount'=>$valuation['unit_cost_amount'],
                'value_delta'=>$valuation['value_delta'],
                'source_type'=>$sourceType,
                'source_public_id'=>$sourcePublicId,
                'source_line_public_id'=>$sourceLinePublicId,
                'occurred_at'=>$occurredAt,
                'business_date'=>$businessDate,
                'created_by_user_id'=>$actorUserId,
                'metadata'=>$metadata,
                'created_at'=>now(),
            ]);

            $balance->quantity_on_hand=$after;
            $balance->last_movement_at=$occurredAt;
            $balance->save();

            if ($lotBalance!==null) {
                $lotBalance->quantity_on_hand=$lotAfter;
                $lotBalance->last_movement_at=$occurredAt;
                $lotBalance->save();
            }

            return $movement;
        });
    }
}

================================================================
FILE: .\app\Modules\Inventory\Domain\Models\InventoryMovement.php
================================================================
<?php

namespace App\Modules\Inventory\Domain\Models;

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

final class InventoryMovement extends Model
{
    use HasPublicUlid;

    public $timestamps = false;

    protected $table = 'inventory.movements';

    protected $fillable = [
        'tenant_id','warehouse_id','variant_id','variant_unit_id','lot_id',
        'movement_type','quantity_delta','unit_cost_amount','value_delta',
        'source_type','source_public_id','source_line_public_id',
        'occurred_at','business_date','created_by_user_id','metadata','created_at',
    ];

    protected function casts(): array
    {
        return [
            'quantity_delta'=>'decimal:6',
            'unit_cost_amount'=>'decimal:6',
            'value_delta'=>'decimal:6',
            'occurred_at'=>'immutable_datetime',
            'business_date'=>'date:Y-m-d',
            'metadata'=>'array',
            'created_at'=>'immutable_datetime',
        ];
    }

    public function save(array $options = []): bool
    {
        if ($this->exists) {
            throw new LogicException('Inventory movements are immutable.');
        }

        return parent::save($options);
    }

    public function delete(): ?bool
    {
        throw new LogicException('Inventory movements are immutable.');
    }
}

================================================================
FILE: .\app\Modules\Inventory\Domain\Models\InventoryBalance.php
================================================================
<?php

namespace App\Modules\Inventory\Domain\Models;

use Illuminate\Database\Eloquent\Model;

final class InventoryBalance extends Model
{
    protected $table = 'inventory.balances';

    protected $fillable = [
        'tenant_id','warehouse_id','variant_id','variant_unit_id',
        'quantity_on_hand','last_movement_at',
    ];

    protected function casts(): array
    {
        return [
            'quantity_on_hand'=>'decimal:6',
            'last_movement_at'=>'immutable_datetime',
        ];
    }
}

================================================================
FILE: .\app\Modules\Sales\Application\Permissions\SalesPermissions.php
================================================================
<?php

namespace App\Modules\Sales\Application\Permissions;

final class SalesPermissions
{
    public const CART_VIEW = 'sales.cart.view';
    public const CART_MANAGE = 'sales.cart.manage';
    public const CHECKOUT = 'sales.checkout';
    public const SALE_VIEW = 'sales.sale.view';

    private function __construct() {}
}

================================================================
FILE: .\app\Modules\Payments\Application\Permissions\PaymentPermissions.php
================================================================
<?php

namespace App\Modules\Payments\Application\Permissions;

final class PaymentPermissions
{
    public const VIEW = 'payments.view';
    public const COLLECT = 'payments.collect';
    public const RESOLVE = 'payments.resolve';

    private function __construct() {}
}

================================================================
SALES MIGRATIONS
================================================================
================================================================
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');
    }
};

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

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('cash.sale_payment_postings', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->ulid('public_id')->unique();

            $table->unsignedBigInteger('tenant_id');
            $table->unsignedBigInteger('payment_id');
            $table->unsignedBigInteger('shift_id');

            $table->decimal('amount', 18, 6);
            $table->timestampTz('posted_at');
            $table->unsignedBigInteger('posted_by_user_id');

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

            $table->foreign('tenant_id')->references('id')->on('core.tenants')->restrictOnDelete();
            $table->foreign('payment_id')->references('id')->on('payments.payments')->restrictOnDelete();
            $table->foreign('shift_id')->references('id')->on('cash.shifts')->restrictOnDelete();
            $table->foreign('posted_by_user_id')->references('id')->on('users')->restrictOnDelete();

            $table->unique(['tenant_id','payment_id']);
            $table->index(['tenant_id','shift_id','posted_at']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('cash.sale_payment_postings');
    }
};

