================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Loyalty\Application\Ledger\LoyaltyLedgerService.php
================================================================
<?php

namespace App\Modules\Loyalty\Application\Ledger;

use App\Modules\CRM\Domain\Models\Customer;
use App\Modules\Loyalty\Domain\Models\LoyaltyAccount;
use App\Modules\Loyalty\Domain\Models\LoyaltyEntry;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;

final class LoyaltyLedgerService
{
    public function post(
        Customer $customer,
        string $entryType,
        int $pointsDelta,
        string $sourceType,
        string $sourcePublicId,
        ?string $sourceLineKey,
        ?int $actorUserId,
        ?\DateTimeInterface $expiresAt=null,
        ?array $metadata=null,
    ): LoyaltyEntry {
        if($pointsDelta===0) throw new ConflictHttpException('Loyalty points delta cannot be zero.');

        return DB::transaction(function () use (
            $customer,$entryType,$pointsDelta,$sourceType,$sourcePublicId,$sourceLineKey,$actorUserId,$expiresAt,$metadata
        ) {
            $account=LoyaltyAccount::query()
                ->where('tenant_id',$customer->tenant_id)
                ->where('customer_id',$customer->id)
                ->lockForUpdate()
                ->first();

            if(!$account) {
                $account=LoyaltyAccount::query()->create([
                    'tenant_id'=>$customer->tenant_id,
                    'customer_id'=>$customer->id,
                    'points_balance'=>0,
                    'lifetime_earned'=>0,
                    'lifetime_redeemed'=>0,
                ]);
                $account=LoyaltyAccount::query()->whereKey($account->id)->lockForUpdate()->firstOrFail();
            }

            $existing=LoyaltyEntry::query()
                ->where('tenant_id',$customer->tenant_id)
                ->where('account_id',$account->id)
                ->where('source_type',$sourceType)
                ->where('source_public_id',$sourcePublicId)
                ->where('source_line_key',$sourceLineKey)
                ->first();

            if($existing) return $existing;

            $newBalance=$account->points_balance+$pointsDelta;
            if($newBalance<0) throw new ConflictHttpException('Insufficient loyalty points.');

            $entry=LoyaltyEntry::query()->create([
                'tenant_id'=>$customer->tenant_id,
                'account_id'=>$account->id,
                'entry_type'=>$entryType,
                'points_delta'=>$pointsDelta,
                'source_type'=>$sourceType,
                'source_public_id'=>$sourcePublicId,
                'source_line_key'=>$sourceLineKey,
                'occurred_at'=>now(),
                'expires_at'=>$expiresAt,
                'created_by_user_id'=>$actorUserId,
                'metadata'=>$metadata,
                'created_at'=>now(),
            ]);

            $account->points_balance=$newBalance;
            if($pointsDelta>0 && $entryType==='earn') $account->lifetime_earned += $pointsDelta;
            if($pointsDelta<0 && $entryType==='redeem') $account->lifetime_redeemed += abs($pointsDelta);
            $account->last_activity_at=now();
            $account->save();

            return $entry;
        });
    }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Loyalty\Domain\Models\LoyaltyAccount.php
================================================================
<?php
namespace App\Modules\Loyalty\Domain\Models;
use App\Modules\Core\Domain\Concerns\HasPublicUlid;
use Illuminate\Database\Eloquent\Model;
final class LoyaltyAccount extends Model {
 use HasPublicUlid;
 protected $table='loyalty.accounts';
 protected $fillable=['tenant_id','customer_id','points_balance','lifetime_earned','lifetime_redeemed','last_activity_at'];
 protected function casts(): array { return ['points_balance'=>'integer','lifetime_earned'=>'integer','lifetime_redeemed'=>'integer','last_activity_at'=>'immutable_datetime']; }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Loyalty\Domain\Models\LoyaltyEntry.php
================================================================
<?php
namespace App\Modules\Loyalty\Domain\Models;
use App\Modules\Core\Domain\Concerns\HasPublicUlid;
use Illuminate\Database\Eloquent\Model;
final class LoyaltyEntry extends Model {
 use HasPublicUlid;
 public $timestamps=false;
 protected $table='loyalty.entries';
 protected $fillable=['tenant_id','account_id','entry_type','points_delta','source_type','source_public_id','source_line_key','occurred_at','expires_at','created_by_user_id','metadata','created_at'];
 protected function casts(): array { return ['points_delta'=>'integer','occurred_at'=>'immutable_datetime','expires_at'=>'immutable_datetime','metadata'=>'array','created_at'=>'immutable_datetime']; }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Wallet\Application\Ledger\WalletLedgerService.php
================================================================
<?php

namespace App\Modules\Wallet\Application\Ledger;

use App\Modules\CRM\Domain\Models\Customer;
use App\Modules\Wallet\Domain\Models\WalletAccount;
use App\Modules\Wallet\Domain\Models\WalletEntry;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;

final class WalletLedgerService
{
    public function post(
        Customer $customer,
        string $currencyCode,
        string $entryType,
        string $amountDelta,
        string $sourceType,
        string $sourcePublicId,
        string $idempotencyKey,
        ?int $actorUserId,
        ?\DateTimeInterface $expiresAt=null,
        ?array $metadata=null,
    ): WalletEntry {
        return DB::transaction(function () use (
            $customer,$currencyCode,$entryType,$amountDelta,$sourceType,$sourcePublicId,$idempotencyKey,$actorUserId,$expiresAt,$metadata
        ) {
            $existing=WalletEntry::query()
                ->where('tenant_id',$customer->tenant_id)
                ->where('idempotency_key',$idempotencyKey)
                ->first();

            if($existing) return $existing;

            $account=WalletAccount::query()
                ->where('tenant_id',$customer->tenant_id)
                ->where('customer_id',$customer->id)
                ->where('currency_code',$currencyCode)
                ->lockForUpdate()
                ->first();

            if(!$account) {
                $account=WalletAccount::query()->create([
                    'tenant_id'=>$customer->tenant_id,
                    'customer_id'=>$customer->id,
                    'currency_code'=>$currencyCode,
                    'balance_amount'=>'0.000000',
                ]);
                $account=WalletAccount::query()->whereKey($account->id)->lockForUpdate()->firstOrFail();
            }

            $newBalance=bcadd((string)$account->balance_amount,$amountDelta,6);
            if(bccomp($newBalance,'0.000000',6)<0) throw new ConflictHttpException('Insufficient wallet balance.');

            $entry=WalletEntry::query()->create([
                'tenant_id'=>$customer->tenant_id,
                'account_id'=>$account->id,
                'entry_type'=>$entryType,
                'amount_delta'=>$amountDelta,
                'source_type'=>$sourceType,
                'source_public_id'=>$sourcePublicId,
                'idempotency_key'=>$idempotencyKey,
                'occurred_at'=>now(),
                'expires_at'=>$expiresAt,
                'created_by_user_id'=>$actorUserId,
                'metadata'=>$metadata,
                'created_at'=>now(),
            ]);

            $account->balance_amount=$newBalance;
            $account->last_activity_at=now();
            $account->save();

            return $entry;
        });
    }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Wallet\Domain\Models\WalletAccount.php
================================================================
<?php
namespace App\Modules\Wallet\Domain\Models;
use App\Modules\Core\Domain\Concerns\HasPublicUlid;
use Illuminate\Database\Eloquent\Model;
final class WalletAccount extends Model {
 use HasPublicUlid;
 protected $table='wallet.accounts';
 protected $fillable=['tenant_id','customer_id','currency_code','balance_amount','last_activity_at'];
 protected function casts(): array { return ['balance_amount'=>'decimal:6','last_activity_at'=>'immutable_datetime']; }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Wallet\Domain\Models\WalletEntry.php
================================================================
<?php
namespace App\Modules\Wallet\Domain\Models;
use App\Modules\Core\Domain\Concerns\HasPublicUlid;
use Illuminate\Database\Eloquent\Model;
final class WalletEntry extends Model {
 use HasPublicUlid;
 public $timestamps=false;
 protected $table='wallet.entries';
 protected $fillable=['tenant_id','account_id','entry_type','amount_delta','source_type','source_public_id','idempotency_key','occurred_at','expires_at','created_by_user_id','metadata','created_at'];
 protected function casts(): array { return ['amount_delta'=>'decimal:6','occurred_at'=>'immutable_datetime','expires_at'=>'immutable_datetime','metadata'=>'array','created_at'=>'immutable_datetime']; }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Credit\Application\Ledger\CreditLedgerService.php
================================================================
<?php

namespace App\Modules\Credit\Application\Ledger;

use App\Modules\Credit\Domain\Models\CreditAccount;
use App\Modules\Credit\Domain\Models\CreditEntry;
use App\Modules\CRM\Domain\Models\Customer;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;

final class CreditLedgerService
{
    public function account(Customer $customer,string $currencyCode): CreditAccount
    {
        return CreditAccount::query()->firstOrCreate(
            [
                'tenant_id'=>$customer->tenant_id,
                'customer_id'=>$customer->id,
                'currency_code'=>$currencyCode,
            ],
            [
                'credit_limit_amount'=>'0.000000',
                'outstanding_amount'=>'0.000000',
                'status'=>'active',
            ]
        );
    }

    public function setLimit(Customer $customer,string $currencyCode,string $limit): CreditAccount
    {
        return DB::transaction(function () use ($customer,$currencyCode,$limit) {
            $account=$this->account($customer,$currencyCode);
            $account=CreditAccount::query()->whereKey($account->id)->lockForUpdate()->firstOrFail();

            if(bccomp($limit,(string)$account->outstanding_amount,6)<0) {
                throw new ConflictHttpException('Credit limit cannot be below current outstanding balance.');
            }

            $account->credit_limit_amount=$limit;
            $account->save();

            return $account->fresh();
        });
    }

    public function post(
        Customer $customer,
        string $currencyCode,
        string $entryType,
        string $amountDelta,
        string $sourceType,
        string $sourcePublicId,
        string $idempotencyKey,
        ?int $actorUserId,
        ?array $metadata=null,
    ): CreditEntry {
        return DB::transaction(function () use (
            $customer,$currencyCode,$entryType,$amountDelta,$sourceType,$sourcePublicId,$idempotencyKey,$actorUserId,$metadata
        ) {
            $existing=CreditEntry::query()
                ->where('tenant_id',$customer->tenant_id)
                ->where('idempotency_key',$idempotencyKey)
                ->first();

            if($existing) return $existing;

            $account=$this->account($customer,$currencyCode);
            $account=CreditAccount::query()->whereKey($account->id)->lockForUpdate()->firstOrFail();

            if($account->status!=='active') throw new ConflictHttpException('Customer credit account is not active.');

            $newOutstanding=bcadd((string)$account->outstanding_amount,$amountDelta,6);

            if(bccomp($newOutstanding,'0.000000',6)<0) {
                throw new ConflictHttpException('Credit settlement exceeds outstanding balance.');
            }

            if(bccomp($newOutstanding,(string)$account->credit_limit_amount,6)>0) {
                throw new ConflictHttpException('Customer credit limit exceeded.');
            }

            $entry=CreditEntry::query()->create([
                'tenant_id'=>$customer->tenant_id,
                'account_id'=>$account->id,
                'entry_type'=>$entryType,
                'amount_delta'=>$amountDelta,
                'source_type'=>$sourceType,
                'source_public_id'=>$sourcePublicId,
                'idempotency_key'=>$idempotencyKey,
                'occurred_at'=>now(),
                'created_by_user_id'=>$actorUserId,
                'metadata'=>$metadata,
                'created_at'=>now(),
            ]);

            $account->outstanding_amount=$newOutstanding;
            $account->last_activity_at=now();
            $account->save();

            return $entry;
        });
    }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Credit\Domain\Models\CreditAccount.php
================================================================
<?php
namespace App\Modules\Credit\Domain\Models;
use App\Modules\Core\Domain\Concerns\HasPublicUlid;
use Illuminate\Database\Eloquent\Model;
final class CreditAccount extends Model {
 use HasPublicUlid;
 protected $table='credit.accounts';
 protected $fillable=['tenant_id','customer_id','currency_code','credit_limit_amount','outstanding_amount','status','last_activity_at'];
 protected function casts(): array { return ['credit_limit_amount'=>'decimal:6','outstanding_amount'=>'decimal:6','last_activity_at'=>'immutable_datetime']; }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\app\Modules\Credit\Domain\Models\CreditEntry.php
================================================================
<?php
namespace App\Modules\Credit\Domain\Models;
use App\Modules\Core\Domain\Concerns\HasPublicUlid;
use Illuminate\Database\Eloquent\Model;
final class CreditEntry extends Model {
 use HasPublicUlid;
 public $timestamps=false;
 protected $table='credit.entries';
 protected $fillable=['tenant_id','account_id','entry_type','amount_delta','source_type','source_public_id','idempotency_key','occurred_at','created_by_user_id','metadata','created_at'];
 protected function casts(): array { return ['amount_delta'=>'decimal:6','occurred_at'=>'immutable_datetime','metadata'=>'array','created_at'=>'immutable_datetime']; }
}

================================================================
FILE: .\app\Modules\Sales\Application\Returns\PostSaleReturnAction.php
================================================================
<?php

namespace App\Modules\Sales\Application\Returns;

use App\Modules\Approvals\Domain\Models\ApprovalPolicy;
use App\Modules\Approvals\Domain\Models\ApprovalRequest;
use App\Modules\Audit\Application\AuditRecorder;
use App\Modules\Inventory\Application\Ledger\InventoryLedgerService;
use App\Modules\Payments\Application\Refunds\RefundCapturedPaymentAction;
use App\Modules\Payments\Application\Refunds\SaleRefundStatusUpdater;
use App\Modules\Payments\Application\Settlement\SalePaymentStatusUpdater;
use App\Modules\Payments\Domain\Models\Payment;
use App\Modules\Sales\Domain\Models\Sale;
use App\Modules\Sales\Domain\Models\SaleLine;
use App\Modules\Sales\Domain\Models\SaleReturn;
use App\Modules\Sales\Domain\Models\SaleReturnLine;
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 PostSaleReturnAction
{
    public function __construct(
        private InventoryLedgerService $inventory,
        private RefundCapturedPaymentAction $refundPayment,
        private SaleRefundStatusUpdater $returnRefundStatus,
        private SalePaymentStatusUpdater $salePaymentStatus,
        private AuditRecorder $auditRecorder,
    ) {}

    public function execute(
        SaleReturn $return,
        int $actorUserId,
        Request $request,
        ?string $approvalRequestPublicId=null,
    ): SaleReturn {
        return DB::transaction(function () use (
            $return,$actorUserId,$request,$approvalRequestPublicId
        ) {
            $locked=SaleReturn::query()
                ->with(['lines','sale'])
                ->where('tenant_id',$return->tenant_id)
                ->whereKey($return->id)
                ->lockForUpdate()
                ->firstOrFail();

            if($locked->status==='posted') {
                return $locked;
            }

            if($locked->status!=='draft') {
                throw new ConflictHttpException('Only draft returns can be posted.');
            }

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

            $this->assertApproval($locked,$approvalRequestPublicId);

            $lines=$locked->lines->sortBy('sale_line_id')->values();

            foreach($lines as $returnLine) {
                $saleLine=SaleLine::query()
                    ->where('tenant_id',$locked->tenant_id)
                    ->whereKey($returnLine->sale_line_id)
                    ->lockForUpdate()
                    ->firstOrFail();

                $alreadyReturned='0.000000';

                SaleReturnLine::query()
                    ->from('sales.return_lines as rl')
                    ->join('sales.returns as r','r.id','=','rl.sale_return_id')
                    ->where('rl.tenant_id',$locked->tenant_id)
                    ->where('rl.sale_line_id',$saleLine->id)
                    ->where('rl.sale_return_id','<>',$locked->id)
                    ->where('r.status','posted')
                    ->pluck('rl.quantity')
                    ->each(function($quantity) use (&$alreadyReturned) {
                        $alreadyReturned=bcadd($alreadyReturned,(string)$quantity,6);
                    });

                $remaining=bcsub((string)$saleLine->quantity,$alreadyReturned,6);

                if(bccomp((string)$returnLine->quantity,$remaining,6)>0) {
                    throw ValidationException::withMessages([
                        'lines'=>['Cumulative returned quantity exceeds original sold quantity.'],
                    ]);
                }
            }

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

            foreach($lines as $returnLine) {
                if($returnLine->disposition!=='restock') {
                    continue;
                }

                if($returnLine->warehouse_id===null) {
                    throw new ConflictHttpException('Restock return line is missing warehouse.');
                }

                $this->inventory->post(
                    tenantId:$locked->tenant_id,
                    warehouseId:$returnLine->warehouse_id,
                    variantId:$returnLine->variant_id,
                    variantUnitId:$returnLine->variant_unit_id,
                    movementType:'sale_reversal',
                    quantityDelta:(string)$returnLine->quantity,
                    sourceType:'sales_return',
                    sourcePublicId:$locked->public_id,
                    sourceLinePublicId:$returnLine->public_id,
                    occurredAt:$at,
                    businessDate:$sale->business_date->format('Y-m-d'),
                    actorUserId:$actorUserId,
                    metadata:[
                        'sale_id'=>$sale->public_id,
                        'sale_line_id'=>$returnLine->sale_line_id,
                        'disposition'=>'restock',
                    ],
                );
            }

            $instructions=$locked->metadata['refund_instructions'] ?? [];

            foreach($instructions as $instruction) {
                $payment=Payment::query()
                    ->where('tenant_id',$locked->tenant_id)
                    ->where('sale_id',$sale->id)
                    ->where('public_id',$instruction['payment_id'])
                    ->firstOrFail();

                $this->refundPayment->execute(
                    $payment,
                    $locked,
                    (string)$instruction['amount'],
                    'return-'.$locked->public_id.'-'.$instruction['sequence'],
                    $actorUserId,
                    $request,
                    $instruction['metadata']??null,
                );
            }

            $locked->status='posted';
            $locked->approval_request_id=$approvalRequestPublicId
                ? ApprovalRequest::query()
                    ->where('tenant_id',$locked->tenant_id)
                    ->where('public_id',$approvalRequestPublicId)
                    ->value('id')
                : null;
            $locked->posted_by_user_id=$actorUserId;
            $locked->posted_at=$at;
            $locked->save();

            $locked=$this->returnRefundStatus->refresh($locked);
            $this->salePaymentStatus->refresh($sale);

            $this->auditRecorder->record(
                'sales.return.posted',
                $locked->tenant_id,
                $actorUserId,
                'sales.return',
                $locked->public_id,
                after:[
                    'sale_id'=>$sale->public_id,
                    'return_number'=>$locked->return_number,
                    'total_amount'=>$locked->total_amount,
                    'refund_status'=>$locked->refund_status,
                ],
                request:$request,
            );

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

    private function assertApproval(
        SaleReturn $return,
        ?string $approvalRequestPublicId,
    ): void {
        $policy=ApprovalPolicy::query()
            ->where('tenant_id',$return->tenant_id)
            ->where('action_code','sales.return.post')
            ->where('status','active')
            ->orderByDesc('id')
            ->get()
            ->first(function($candidate) use ($return) {
                $conditions=$candidate->conditions ?? [];
                $threshold=(string)($conditions['max_return_amount_without_approval'] ?? '0.000000');

                return bccomp((string)$return->total_amount,$threshold,6)>0;
            });

        if(!$policy) return;

        if(!$approvalRequestPublicId) {
            throw ValidationException::withMessages([
                'approval_request_id'=>['Approved request is required for this return amount.'],
            ]);
        }

        $approval=ApprovalRequest::query()
            ->where('tenant_id',$return->tenant_id)
            ->where('public_id',$approvalRequestPublicId)
            ->where('policy_id',$policy->id)
            ->where('action_code','sales.return.post')
            ->where('subject_type','sales.return')
            ->where('subject_public_id',$return->public_id)
            ->where('status','approved')
            ->first();

        if(!$approval) {
            throw ValidationException::withMessages([
                'approval_request_id'=>[
                    'Approval request is missing, not approved, or does not match this return.',
                ],
            ]);
        }

        $approvedAmount=(string)($approval->payload['return_total_amount'] ?? $return->total_amount);

        if(bccomp($approvedAmount,(string)$return->total_amount,6)!==0) {
            throw ValidationException::withMessages([
                'approval_request_id'=>['Approved return amount does not match current return total.'],
            ]);
        }
    }
}

================================================================
FILE: .\app\Modules\Sales\Application\Returns\CreateSaleReturnAction.php
================================================================
<?php

namespace App\Modules\Sales\Application\Returns;

use App\Modules\Audit\Application\AuditRecorder;
use App\Modules\Core\Domain\Models\Warehouse;
use App\Modules\Payments\Domain\Models\Payment;
use App\Modules\Sales\Domain\Models\Sale;
use App\Modules\Sales\Domain\Models\SaleLine;
use App\Modules\Sales\Domain\Models\SaleReturn;
use App\Modules\Sales\Domain\Models\SaleReturnLine;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;

final readonly class CreateSaleReturnAction
{
    public function __construct(private AuditRecorder $auditRecorder) {}

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

        if($existing) return $existing->load('lines');

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

            if(in_array($lockedSale->status,['voided'],true)) {
                throw new ConflictHttpException('Voided sale cannot be returned.');
            }

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

            if($existing) return $existing->load('lines');

            $requestedByLine=[];

            foreach($data['lines'] as $item) {
                $line=SaleLine::query()
                    ->where('tenant_id',$lockedSale->tenant_id)
                    ->where('sale_id',$lockedSale->id)
                    ->where('public_id',$item['sale_line_id'])
                    ->first();

                if(!$line) {
                    throw ValidationException::withMessages([
                        'lines'=>['Return line does not belong to this sale.'],
                    ]);
                }

                if(isset($requestedByLine[$line->id])) {
                    throw ValidationException::withMessages([
                        'lines'=>['Each sale line may appear only once in a return draft.'],
                    ]);
                }

                $warehouseId=null;

                if($item['disposition']==='restock') {
                    if(empty($item['warehouse_id'])) {
                        throw ValidationException::withMessages([
                            'lines'=>['Restock disposition requires warehouse_id.'],
                        ]);
                    }

                    $warehouse=Warehouse::query()
                        ->where('tenant_id',$lockedSale->tenant_id)
                        ->where('public_id',$item['warehouse_id'])
                        ->first();

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

                    $warehouseId=$warehouse->id;
                }

                $requestedByLine[$line->id]=[
                    'line'=>$line,
                    'quantity'=>(string)$item['quantity'],
                    'disposition'=>$item['disposition'],
                    'warehouse_id'=>$warehouseId,
                    'metadata'=>$item['metadata']??null,
                ];
            }

            $total='0.000000';

            foreach($requestedByLine as &$item) {
                /** @var SaleLine $line */
                $line=$item['line'];

                if(bccomp($item['quantity'],(string)$line->quantity,6)>0) {
                    throw ValidationException::withMessages([
                        'lines'=>['Return quantity cannot exceed original sold quantity.'],
                    ]);
                }

                $lineAmount=bcdiv(
                    bcmul((string)$line->net_amount,$item['quantity'],12),
                    (string)$line->quantity,
                    6
                );

                $item['return_amount']=$lineAmount;
                $total=bcadd($total,$lineAmount,6);
            }
            unset($item);

            $refundTotal='0.000000';
            $refundInstructions=[];

            foreach($data['refunds'] ?? [] as $index=>$refund) {
                $payment=Payment::query()
                    ->where('tenant_id',$lockedSale->tenant_id)
                    ->where('sale_id',$lockedSale->id)
                    ->where('public_id',$refund['payment_id'])
                    ->where('status','captured')
                    ->first();

                if(!$payment) {
                    throw ValidationException::withMessages([
                        'refunds'=>['Refund payment must be a captured payment belonging to this sale.'],
                    ]);
                }

                $amount=(string)$refund['amount'];
                $refundTotal=bcadd($refundTotal,$amount,6);

                $refundInstructions[]=[
                    'payment_id'=>$payment->public_id,
                    'amount'=>$amount,
                    'metadata'=>$refund['metadata']??null,
                    'sequence'=>$index+1,
                ];
            }

            if(bccomp($refundTotal,$total,6)>0) {
                throw ValidationException::withMessages([
                    'refunds'=>['Refund allocations cannot exceed the return total.'],
                ]);
            }

            $return=SaleReturn::query()->create([
                'tenant_id'=>$lockedSale->tenant_id,
                'sale_id'=>$lockedSale->id,
                'return_number'=>'RET-'.strtoupper(substr((string)str()->ulid(),-12)),
                'status'=>'draft',
                'refund_status'=>empty($refundInstructions) ? 'none' : 'pending',
                'subtotal_amount'=>$total,
                'total_amount'=>$total,
                'reason'=>$data['reason'],
                'idempotency_key'=>$data['idempotency_key'],
                'created_by_user_id'=>$actorUserId,
                'metadata'=>array_merge(
                    $data['metadata']??[],
                    ['refund_instructions'=>$refundInstructions]
                ),
            ]);

            foreach($requestedByLine as $item) {
                $line=$item['line'];

                SaleReturnLine::query()->create([
                    'tenant_id'=>$lockedSale->tenant_id,
                    'sale_return_id'=>$return->id,
                    'sale_line_id'=>$line->id,
                    'variant_id'=>$line->variant_id,
                    'variant_unit_id'=>$line->variant_unit_id,
                    'warehouse_id'=>$item['warehouse_id'],
                    'quantity'=>$item['quantity'],
                    'return_amount'=>$item['return_amount'],
                    'disposition'=>$item['disposition'],
                    'metadata'=>$item['metadata'],
                    'created_at'=>now(),
                ]);
            }

            $this->auditRecorder->record(
                'sales.return.created',
                $lockedSale->tenant_id,
                $actorUserId,
                'sales.return',
                $return->public_id,
                after:[
                    'sale_id'=>$lockedSale->public_id,
                    'return_number'=>$return->return_number,
                    'total_amount'=>$return->total_amount,
                    'status'=>$return->status,
                ],
                request:$request,
            );

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

================================================================
FILE: .\app\Modules\Payments\Application\Refunds\RefundCapturedPaymentAction.php
================================================================
<?php

namespace App\Modules\Payments\Application\Refunds;

use App\Modules\Cash\Application\Shifts\PostCashRefundToShiftAction;
use App\Modules\Core\Domain\Models\Register;
use App\Modules\Payments\Domain\Models\Payment;
use App\Modules\Payments\Domain\Models\PaymentRefund;
use App\Modules\Sales\Domain\Models\Sale;
use App\Modules\Sales\Domain\Models\SaleReturn;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;

final readonly class RefundCapturedPaymentAction
{
    public function __construct(
        private PostCashRefundToShiftAction $cashRefund,
    ) {}

    public function execute(
        Payment $payment,
        SaleReturn $saleReturn,
        string $amount,
        string $idempotencyKey,
        int $actorUserId,
        Request $request,
        ?array $metadata=null,
    ): PaymentRefund {
        $existing=PaymentRefund::query()
            ->where('tenant_id',$payment->tenant_id)
            ->where('idempotency_key',$idempotencyKey)
            ->first();

        if($existing) return $existing;

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

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

            if($lockedPayment->sale_id!==$saleReturn->sale_id) {
                throw ValidationException::withMessages([
                    'payment_id'=>['Refund payment does not belong to the returned sale.'],
                ]);
            }

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

            if($existing) return $existing;

            $allocated='0.000000';

            PaymentRefund::query()
                ->where('tenant_id',$lockedPayment->tenant_id)
                ->where('payment_id',$lockedPayment->id)
                ->whereIn('status',['pending','completed'])
                ->pluck('amount')
                ->each(function($value) use (&$allocated) {
                    $allocated=bcadd($allocated,(string)$value,6);
                });

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

            $refund=PaymentRefund::query()->create([
                'tenant_id'=>$lockedPayment->tenant_id,
                'payment_id'=>$lockedPayment->id,
                'sale_return_id'=>$saleReturn->id,
                'method_type'=>$lockedPayment->method_type,
                'amount'=>$amount,
                'currency_code'=>$lockedPayment->currency_code,
                'status'=>'pending',
                'idempotency_key'=>$idempotencyKey,
                'created_by_user_id'=>$actorUserId,
                'metadata'=>$metadata,
                'created_at'=>now(),
            ]);

            if($lockedPayment->method_type==='cash') {
                $sale=Sale::query()
                    ->where('tenant_id',$lockedPayment->tenant_id)
                    ->whereKey($lockedPayment->sale_id)
                    ->firstOrFail();

                if($sale->register_id===null) {
                    throw new ConflictHttpException('Cash refund requires a register-linked sale.');
                }

                $register=Register::query()
                    ->where('tenant_id',$sale->tenant_id)
                    ->whereKey($sale->register_id)
                    ->firstOrFail();

                $cash=$this->cashRefund->execute(
                    $lockedPayment,
                    [
                        'register_id'=>$register->public_id,
                        'amount'=>$amount,
                        'idempotency_key'=>'return-cash-'.$refund->public_id,
                        'reason'=>'Sales return '.$saleReturn->return_number,
                        'metadata'=>[
                            'sale_return_id'=>$saleReturn->public_id,
                            'payment_refund_id'=>$refund->public_id,
                        ],
                    ],
                    $actorUserId,
                    $request,
                );

                $refund->status='completed';
                $refund->resolved_by_user_id=$actorUserId;
                $refund->resolved_at=now();
                $refund->metadata=array_merge(
                    $refund->metadata ?? [],
                    ['cash_sale_refund_id'=>$cash->public_id]
                );
                $refund->save();
            }

            return $refund->fresh();
        });
    }
}

================================================================
FILE: .\app\Modules\Payments\Application\Refunds\ResolvePaymentRefundAction.php
================================================================
<?php

namespace App\Modules\Payments\Application\Refunds;

use App\Modules\Audit\Application\AuditRecorder;
use App\Modules\Payments\Domain\Models\PaymentRefund;
use App\Modules\Payments\Application\Settlement\SalePaymentStatusUpdater;
use App\Modules\Sales\Domain\Models\Sale;
use App\Modules\Sales\Domain\Models\SaleReturn;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;

final readonly class ResolvePaymentRefundAction
{
    public function __construct(
        private SaleRefundStatusUpdater $returnStatus,
        private SalePaymentStatusUpdater $salePaymentStatus,
        private AuditRecorder $auditRecorder,
    ) {}

    public function execute(
        PaymentRefund $refund,
        array $data,
        int $actorUserId,
        Request $request,
    ): PaymentRefund {
        return DB::transaction(function () use ($refund,$data,$actorUserId,$request) {
            $locked=PaymentRefund::query()
                ->where('tenant_id',$refund->tenant_id)
                ->whereKey($refund->id)
                ->lockForUpdate()
                ->firstOrFail();

            if($locked->method_type==='cash') {
                throw new ConflictHttpException('Cash refunds are resolved synchronously through the register shift.');
            }

            if($locked->status!=='pending') {
                return $locked;
            }

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

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

            $locked->save();

            if($locked->sale_return_id!==null) {
                $return=SaleReturn::query()->whereKey($locked->sale_return_id)->lockForUpdate()->firstOrFail();
                $this->returnStatus->refresh($return);

                $sale=Sale::query()->whereKey($return->sale_id)->lockForUpdate()->firstOrFail();
                $this->salePaymentStatus->refresh($sale);
            }

            $this->auditRecorder->record(
                'payments.refund.resolved',
                $locked->tenant_id,
                $actorUserId,
                'payments.refund',
                $locked->public_id,
                before:['status'=>$before],
                after:[
                    'status'=>$locked->status,
                    'provider_reference'=>$locked->provider_reference,
                ],
                request:$request,
            );

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

================================================================
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\Payments\Domain\Models\PaymentRefund;
use App\Modules\Sales\Domain\Models\Sale;

final class SalePaymentStatusUpdater
{
    public function refresh(Sale $sale): Sale
    {
        $captured=$this->capturedAmount($sale);
        $refunded=$this->completedRefundAmount($sale);

        $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($refunded,'0.000000',6)>0 && bccomp($refunded,$captured,6)>=0) {
            $paymentStatus='refunded';
            $saleStatus=bccomp($captured,(string)$sale->total_amount,6)>=0
                ? 'completed'
                : 'pending_payment';
        } elseif(bccomp($refunded,'0.000000',6)>0) {
            $paymentStatus='partially_refunded';
            $saleStatus=bccomp($captured,(string)$sale->total_amount,6)>=0
                ? 'completed'
                : '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;
    }

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

        PaymentRefund::query()
            ->from('payments.refunds as r')
            ->join('payments.payments as p','p.id','=','r.payment_id')
            ->where('r.tenant_id',$sale->tenant_id)
            ->where('p.sale_id',$sale->id)
            ->where('r.status','completed')
            ->pluck('r.amount')
            ->each(function($amount) use (&$total) {
                $total=bcadd($total,(string)$amount,6);
            });

        return $total;
    }
}

================================================================
FILE: .\app\Modules\CRM\Application\Value\SettleSaleWithWalletAction.php
================================================================
<?php

namespace App\Modules\CRM\Application\Value;

use App\Modules\Audit\Application\AuditRecorder;
use App\Modules\CRM\Domain\Models\Customer;
use App\Modules\Payments\Application\Settlement\SalePaymentStatusUpdater;
use App\Modules\Payments\Domain\Models\Payment;
use App\Modules\Payments\Domain\Models\PaymentAttempt;
use App\Modules\Sales\Domain\Models\Sale;
use App\Modules\Wallet\Application\Ledger\WalletLedgerService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;

final readonly class SettleSaleWithWalletAction
{
    public function __construct(
        private WalletLedgerService $wallet,
        private SalePaymentStatusUpdater $statusUpdater,
        private AuditRecorder $audit,
    ) {}

    public function execute(Sale $sale,string $amount,string $idempotencyKey,int $actorUserId,Request $request,?array $metadata=null): Payment
    {
        return DB::transaction(function () use ($sale,$amount,$idempotencyKey,$actorUserId,$request,$metadata) {
            $locked=Sale::query()->whereKey($sale->id)->lockForUpdate()->firstOrFail();

            if($locked->customer_id===null) throw new ConflictHttpException('Wallet settlement requires a linked customer.');

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

            if($existing) return $existing;

            $captured=$this->statusUpdater->capturedAmount($locked);
            $remaining=bcsub((string)$locked->total_amount,$captured,6);

            if(bccomp($amount,$remaining,6)>0) throw new ConflictHttpException('Wallet amount exceeds remaining sale balance.');

            $customer=Customer::query()
                ->where('tenant_id',$locked->tenant_id)
                ->whereKey($locked->customer_id)
                ->firstOrFail();

            $walletEntry=$this->wallet->post(
                $customer,
                $locked->currency_code,
                'debit',
                bcmul($amount,'-1',6),
                'sale',
                $locked->public_id,
                'sale-wallet-'.$idempotencyKey,
                $actorUserId,
                null,
                array_merge($metadata??[],['sale_id'=>$locked->public_id]),
            );

            $payment=Payment::query()->create([
                'tenant_id'=>$locked->tenant_id,
                'sale_id'=>$locked->id,
                'method_type'=>'wallet',
                'amount'=>$amount,
                'currency_code'=>$locked->currency_code,
                'status'=>'captured',
                'idempotency_key'=>$idempotencyKey,
                'created_by_user_id'=>$actorUserId,
                'resolved_at'=>now(),
                'metadata'=>array_merge($metadata??[],['wallet_entry_id'=>$walletEntry->public_id]),
            ]);

            PaymentAttempt::query()->create([
                'tenant_id'=>$locked->tenant_id,
                'payment_id'=>$payment->id,
                'operation_key'=>$idempotencyKey,
                'attempt_type'=>'collect',
                'status'=>'succeeded',
                'request_snapshot'=>['method_type'=>'wallet','amount'=>$amount],
                'response_snapshot'=>['outcome'=>'succeeded'],
                'started_at'=>now(),
                'completed_at'=>now(),
            ]);

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

            $this->audit->record(
                'wallet.sale.collected',$locked->tenant_id,$actorUserId,
                'payments.payment',$payment->public_id,
                after:['sale_id'=>$locked->public_id,'amount'=>$amount,'sale_payment_status'=>$saleAfter->payment_status],
                request:$request,
            );

            return $payment;
        });
    }
}

================================================================
FILE: .\app\Modules\CRM\Application\Value\ChargeSaleToCreditAction.php
================================================================
<?php

namespace App\Modules\CRM\Application\Value;

use App\Modules\Audit\Application\AuditRecorder;
use App\Modules\Credit\Application\Ledger\CreditLedgerService;
use App\Modules\CRM\Domain\Models\Customer;
use App\Modules\Payments\Application\Settlement\SalePaymentStatusUpdater;
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 ChargeSaleToCreditAction
{
    public function __construct(
        private CreditLedgerService $credit,
        private SalePaymentStatusUpdater $statusUpdater,
        private AuditRecorder $audit,
    ) {}

    public function execute(Sale $sale,string $amount,string $idempotencyKey,int $actorUserId,Request $request,?array $metadata=null): Sale
    {
        return DB::transaction(function () use ($sale,$amount,$idempotencyKey,$actorUserId,$request,$metadata) {
            $locked=Sale::query()->whereKey($sale->id)->lockForUpdate()->firstOrFail();

            if($locked->customer_id===null) throw new ConflictHttpException('Credit sale requires a linked customer.');

            $captured=$this->statusUpdater->capturedAmount($locked);
            $currentCredit=$this->creditChargeAmount($locked);
            $remaining=bcsub(bcsub((string)$locked->total_amount,$captured,6),$currentCredit,6);

            if(bccomp($amount,$remaining,6)>0) throw new ConflictHttpException('Credit charge exceeds remaining sale balance.');

            $customer=Customer::query()
                ->where('tenant_id',$locked->tenant_id)
                ->whereKey($locked->customer_id)
                ->firstOrFail();

            $entry=$this->credit->post(
                $customer,
                $locked->currency_code,
                'charge',
                $amount,
                'sale',
                $locked->public_id,
                $idempotencyKey,
                $actorUserId,
                array_merge($metadata??[],['sale_id'=>$locked->public_id]),
            );

            $captured=$this->statusUpdater->capturedAmount($locked);
            $creditTotal=$this->creditChargeAmount($locked);
            $covered=bcadd($captured,$creditTotal,6);

            $locked->payment_status=bccomp($covered,(string)$locked->total_amount,6)>=0
                ? 'on_account'
                : 'partially_on_account';
            $locked->status=bccomp($covered,(string)$locked->total_amount,6)>=0
                ? 'completed'
                : 'pending_payment';
            $locked->save();

            $this->audit->record(
                'credit.sale.charged',$locked->tenant_id,$actorUserId,
                'credit.entry',$entry->public_id,
                after:['sale_id'=>$locked->public_id,'amount'=>$amount,'sale_payment_status'=>$locked->payment_status],
                request:$request,
            );

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

    private function creditChargeAmount(Sale $sale): string
    {
        $total='0.000000';

        \App\Modules\Credit\Domain\Models\CreditEntry::query()
            ->from('credit.entries as e')
            ->join('credit.accounts as a','a.id','=','e.account_id')
            ->where('e.tenant_id',$sale->tenant_id)
            ->where('e.source_type','sale')
            ->where('e.source_public_id',$sale->public_id)
            ->where('e.entry_type','charge')
            ->pluck('e.amount_delta')
            ->each(function($amount) use (&$total) {
                $total=bcadd($total,(string)$amount,6);
            });

        return $total;
    }
}

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\database\migrations\2026_09_01_113000_create_payments_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 payments');

        // Extend the existing sale payment-state contract for uncertain terminal outcomes.
        DB::statement('ALTER TABLE sales.sales DROP CONSTRAINT IF EXISTS sales_sales_payment_status_check');
        DB::statement("
            ALTER TABLE sales.sales
            ADD CONSTRAINT sales_sales_payment_status_check
            CHECK (payment_status IN ('unpaid','partial','paid','unknown','refunded','partially_refunded'))
        ");

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

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

            $table->string('method_type', 30);
            $table->decimal('amount', 18, 6);
            $table->string('currency_code', 3);

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

            $table->string('idempotency_key', 120);
            $table->string('provider_reference', 180)->nullable();

            $table->unsignedBigInteger('created_by_user_id');
            $table->timestampTz('resolved_at')->nullable();

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

            $table->timestampsTz();

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

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

        DB::statement("
            ALTER TABLE payments.payments
            ADD CONSTRAINT payments_payments_method_check
            CHECK (method_type IN ('cash','card','external'))
        ");

        DB::statement("
            ALTER TABLE payments.payments
            ADD CONSTRAINT payments_payments_status_check
            CHECK (status IN ('pending','captured','failed','unknown','reversed'))
        ");

        DB::statement("
            ALTER TABLE payments.payments
            ADD CONSTRAINT payments_payments_amount_check
            CHECK (amount > 0)
        ");

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

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

            $table->string('operation_key', 120);
            $table->string('attempt_type', 30);
            $table->string('status', 30);

            $table->string('provider_reference', 180)->nullable();

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

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

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

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

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

        DB::statement("
            ALTER TABLE payments.attempts
            ADD CONSTRAINT payments_attempts_type_check
            CHECK (attempt_type IN ('collect','resolve'))
        ");

        DB::statement("
            ALTER TABLE payments.attempts
            ADD CONSTRAINT payments_attempts_status_check
            CHECK (status IN ('started','succeeded','failed','unknown'))
        ");
    }

    public function down(): void
    {
        Schema::dropIfExists('payments.attempts');
        Schema::dropIfExists('payments.payments');

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

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\database\migrations\2026_09_04_033000_create_cash_refund_shift_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_cash_refunds', 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->string('reason', 255);
            $table->string('idempotency_key', 120);

            $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','idempotency_key']);
            $table->index(['tenant_id','payment_id','posted_at']);
            $table->index(['tenant_id','shift_id','posted_at']);
        });
    }

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

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\database\migrations\2026_09_04_043000_harden_cash_payment_shift_integrity.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
    {
        Schema::create('cash.payment_refund_balances', function (Blueprint $table) {
            $table->bigIncrements('id');

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

            $table->decimal('refunded_amount', 18, 6)->default(0);
            $table->timestampTz('last_refund_at')->nullable();

            $table->timestampsTz();

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

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

        DB::statement("
            INSERT INTO cash.payment_refund_balances
                (tenant_id, payment_id, refunded_amount, last_refund_at, created_at, updated_at)
            SELECT
                tenant_id,
                payment_id,
                COALESCE(SUM(amount), 0),
                MAX(posted_at),
                NOW(),
                NOW()
            FROM cash.sale_cash_refunds
            GROUP BY tenant_id, payment_id
            ON CONFLICT (tenant_id, payment_id) DO NOTHING
        ");

        DB::statement("
            ALTER TABLE cash.payment_refund_balances
            ADD CONSTRAINT cash_payment_refund_balances_non_negative_check
            CHECK (refunded_amount >= 0)
        ");
    }

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

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\database\migrations\2026_09_04_063000_create_sales_return_refund_bundle.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
    {
        Schema::create('sales.returns', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->ulid('public_id')->unique();

            $table->unsignedBigInteger('tenant_id');
            $table->unsignedBigInteger('sale_id');
            $table->string('return_number', 90);
            $table->string('status', 30)->default('draft');
            $table->string('refund_status', 30)->default('none');

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

            $table->string('reason', 255);
            $table->string('idempotency_key', 120);
            $table->unsignedBigInteger('approval_request_id')->nullable();

            $table->unsignedBigInteger('created_by_user_id');
            $table->unsignedBigInteger('posted_by_user_id')->nullable();
            $table->timestampTz('posted_at')->nullable();

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

            $table->foreign('tenant_id')->references('id')->on('core.tenants')->restrictOnDelete();
            $table->foreign('sale_id')->references('id')->on('sales.sales')->restrictOnDelete();
            $table->foreign('approval_request_id')->references('id')->on('approvals.requests')->restrictOnDelete();
            $table->foreign('created_by_user_id')->references('id')->on('users')->restrictOnDelete();
            $table->foreign('posted_by_user_id')->references('id')->on('users')->restrictOnDelete();

            $table->unique(['tenant_id','return_number']);
            $table->unique(['tenant_id','idempotency_key']);
            $table->index(['tenant_id','sale_id','status']);
            $table->index(['tenant_id','refund_status','created_at']);
        });

        DB::statement("
            ALTER TABLE sales.returns
            ADD CONSTRAINT sales_returns_status_check
            CHECK (status IN ('draft','posted','cancelled'))
        ");

        DB::statement("
            ALTER TABLE sales.returns
            ADD CONSTRAINT sales_returns_refund_status_check
            CHECK (refund_status IN ('none','pending','partial','completed','failed'))
        ");

        DB::statement("
            ALTER TABLE sales.returns
            ADD CONSTRAINT sales_returns_amounts_check
            CHECK (
                subtotal_amount >= 0
                AND total_amount >= 0
                AND total_amount = subtotal_amount
            )
        ");

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

            $table->unsignedBigInteger('tenant_id');
            $table->unsignedBigInteger('sale_return_id');
            $table->unsignedBigInteger('sale_line_id');

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

            $table->decimal('quantity', 18, 6);
            $table->decimal('return_amount', 18, 6);
            $table->string('disposition', 30)->default('no_stock');

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

            $table->foreign('tenant_id')->references('id')->on('core.tenants')->restrictOnDelete();
            $table->foreign('sale_return_id')->references('id')->on('sales.returns')->restrictOnDelete();
            $table->foreign('sale_line_id')->references('id')->on('sales.sale_lines')->restrictOnDelete();
            $table->foreign('variant_id')->references('id')->on('catalog.variants')->restrictOnDelete();
            $table->foreign('variant_unit_id')->references('id')->on('catalog.variant_units')->restrictOnDelete();
            $table->foreign('warehouse_id')->references('id')->on('core.warehouses')->restrictOnDelete();

            $table->index(['tenant_id','sale_return_id']);
            $table->index(['tenant_id','sale_line_id']);
            $table->index(['tenant_id','warehouse_id']);
        });

        DB::statement("
            ALTER TABLE sales.return_lines
            ADD CONSTRAINT sales_return_lines_quantity_amount_check
            CHECK (quantity > 0 AND return_amount >= 0)
        ");

        DB::statement("
            ALTER TABLE sales.return_lines
            ADD CONSTRAINT sales_return_lines_disposition_check
            CHECK (disposition IN ('restock','damaged','waste','no_stock'))
        ");

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

            $table->unsignedBigInteger('tenant_id');
            $table->unsignedBigInteger('payment_id');
            $table->unsignedBigInteger('sale_return_id')->nullable();

            $table->string('method_type', 30);
            $table->decimal('amount', 18, 6);
            $table->string('currency_code', 3);
            $table->string('status', 30)->default('pending');

            $table->string('idempotency_key', 140);
            $table->string('provider_reference')->nullable();

            $table->unsignedBigInteger('created_by_user_id');
            $table->unsignedBigInteger('resolved_by_user_id')->nullable();
            $table->timestampTz('resolved_at')->nullable();

            $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('sale_return_id')->references('id')->on('sales.returns')->restrictOnDelete();
            $table->foreign('created_by_user_id')->references('id')->on('users')->restrictOnDelete();
            $table->foreign('resolved_by_user_id')->references('id')->on('users')->restrictOnDelete();

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

        DB::statement("
            ALTER TABLE payments.refunds
            ADD CONSTRAINT payments_refunds_method_check
            CHECK (method_type IN ('cash','card','external'))
        ");

        DB::statement("
            ALTER TABLE payments.refunds
            ADD CONSTRAINT payments_refunds_status_check
            CHECK (status IN ('pending','completed','failed'))
        ");

        DB::statement("
            ALTER TABLE payments.refunds
            ADD CONSTRAINT payments_refunds_amount_check
            CHECK (amount > 0)
        ");

        DB::statement(<<<'SQL'
CREATE OR REPLACE FUNCTION sales.prevent_posted_return_line_mutation()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE return_status text;
BEGIN
    SELECT status INTO return_status
    FROM sales.returns
    WHERE id = OLD.sale_return_id;

    IF return_status = 'posted' THEN
        RAISE EXCEPTION 'posted sales.return_lines are immutable';
    END IF;

    RETURN NEW;
END;
$$
SQL);

        DB::statement(<<<'SQL'
CREATE TRIGGER sales_return_lines_guard_update
BEFORE UPDATE ON sales.return_lines
FOR EACH ROW EXECUTE FUNCTION sales.prevent_posted_return_line_mutation()
SQL);

        DB::statement(<<<'SQL'
CREATE TRIGGER sales_return_lines_guard_delete
BEFORE DELETE ON sales.return_lines
FOR EACH ROW EXECUTE FUNCTION sales.prevent_posted_return_line_mutation()
SQL);
    }

    public function down(): void
    {
        DB::statement('DROP TRIGGER IF EXISTS sales_return_lines_guard_delete ON sales.return_lines');
        DB::statement('DROP TRIGGER IF EXISTS sales_return_lines_guard_update ON sales.return_lines');
        DB::statement('DROP FUNCTION IF EXISTS sales.prevent_posted_return_line_mutation()');

        Schema::dropIfExists('payments.refunds');
        Schema::dropIfExists('sales.return_lines');
        Schema::dropIfExists('sales.returns');
    }
};

================================================================
FILE: F:\POS 2026\retail-platform\apps\api\database\migrations\2026_09_04_093000_create_customer_value_credit_foundation.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 loyalty');
        DB::statement('CREATE SCHEMA IF NOT EXISTS wallet');
        DB::statement('CREATE SCHEMA IF NOT EXISTS credit');

        Schema::create('loyalty.accounts', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->ulid('public_id')->unique();
            $table->unsignedBigInteger('tenant_id');
            $table->unsignedBigInteger('customer_id');
            $table->bigInteger('points_balance')->default(0);
            $table->bigInteger('lifetime_earned')->default(0);
            $table->bigInteger('lifetime_redeemed')->default(0);
            $table->timestampTz('last_activity_at')->nullable();
            $table->timestampsTz();

            $table->foreign('tenant_id')->references('id')->on('core.tenants')->restrictOnDelete();
            $table->foreign('customer_id')->references('id')->on('crm.customers')->restrictOnDelete();
            $table->unique(['tenant_id','customer_id']);
        });

        Schema::create('loyalty.entries', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->ulid('public_id')->unique();
            $table->unsignedBigInteger('tenant_id');
            $table->unsignedBigInteger('account_id');
            $table->string('entry_type',40);
            $table->bigInteger('points_delta');
            $table->string('source_type',80);
            $table->string('source_public_id',64);
            $table->string('source_line_key',120)->nullable();
            $table->timestampTz('occurred_at');
            $table->timestampTz('expires_at')->nullable();
            $table->unsignedBigInteger('created_by_user_id')->nullable();
            $table->jsonb('metadata')->nullable();
            $table->timestampTz('created_at')->useCurrent();

            $table->foreign('tenant_id')->references('id')->on('core.tenants')->restrictOnDelete();
            $table->foreign('account_id')->references('id')->on('loyalty.accounts')->restrictOnDelete();
            $table->foreign('created_by_user_id')->references('id')->on('users')->restrictOnDelete();

            $table->unique(
                ['tenant_id','account_id','source_type','source_public_id','source_line_key'],
                'loyalty_entries_source_uq'
            );
            $table->index(['tenant_id','account_id','occurred_at']);
            $table->index(['tenant_id','expires_at']);
        });

        DB::statement("
            ALTER TABLE loyalty.entries
            ADD CONSTRAINT loyalty_entries_type_check
            CHECK (entry_type IN ('earn','redeem','expire','reversal','adjustment'))
        ");
        DB::statement("
            ALTER TABLE loyalty.entries
            ADD CONSTRAINT loyalty_entries_nonzero_check
            CHECK (points_delta <> 0)
        ");

        Schema::create('wallet.accounts', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->ulid('public_id')->unique();
            $table->unsignedBigInteger('tenant_id');
            $table->unsignedBigInteger('customer_id');
            $table->string('currency_code',3)->default('EGP');
            $table->decimal('balance_amount',18,6)->default(0);
            $table->timestampTz('last_activity_at')->nullable();
            $table->timestampsTz();

            $table->foreign('tenant_id')->references('id')->on('core.tenants')->restrictOnDelete();
            $table->foreign('customer_id')->references('id')->on('crm.customers')->restrictOnDelete();
            $table->unique(['tenant_id','customer_id','currency_code']);
        });

        Schema::create('wallet.entries', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->ulid('public_id')->unique();
            $table->unsignedBigInteger('tenant_id');
            $table->unsignedBigInteger('account_id');
            $table->string('entry_type',40);
            $table->decimal('amount_delta',18,6);
            $table->string('source_type',80);
            $table->string('source_public_id',64);
            $table->string('idempotency_key',140);
            $table->timestampTz('occurred_at');
            $table->timestampTz('expires_at')->nullable();
            $table->unsignedBigInteger('created_by_user_id')->nullable();
            $table->jsonb('metadata')->nullable();
            $table->timestampTz('created_at')->useCurrent();

            $table->foreign('tenant_id')->references('id')->on('core.tenants')->restrictOnDelete();
            $table->foreign('account_id')->references('id')->on('wallet.accounts')->restrictOnDelete();
            $table->foreign('created_by_user_id')->references('id')->on('users')->restrictOnDelete();

            $table->unique(['tenant_id','idempotency_key']);
            $table->index(['tenant_id','account_id','occurred_at']);
            $table->index(['tenant_id','expires_at']);
        });

        DB::statement("
            ALTER TABLE wallet.entries
            ADD CONSTRAINT wallet_entries_type_check
            CHECK (entry_type IN ('credit','debit','cashback','refund','expire','reversal','adjustment'))
        ");
        DB::statement("
            ALTER TABLE wallet.entries
            ADD CONSTRAINT wallet_entries_nonzero_check
            CHECK (amount_delta <> 0)
        ");

        Schema::create('credit.accounts', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->ulid('public_id')->unique();
            $table->unsignedBigInteger('tenant_id');
            $table->unsignedBigInteger('customer_id');
            $table->string('currency_code',3)->default('EGP');
            $table->decimal('credit_limit_amount',18,6)->default(0);
            $table->decimal('outstanding_amount',18,6)->default(0);
            $table->string('status',30)->default('active');
            $table->timestampTz('last_activity_at')->nullable();
            $table->timestampsTz();

            $table->foreign('tenant_id')->references('id')->on('core.tenants')->restrictOnDelete();
            $table->foreign('customer_id')->references('id')->on('crm.customers')->restrictOnDelete();
            $table->unique(['tenant_id','customer_id','currency_code']);
        });

        DB::statement("
            ALTER TABLE credit.accounts
            ADD CONSTRAINT credit_accounts_status_check
            CHECK (status IN ('active','suspended','closed'))
        ");
        DB::statement("
            ALTER TABLE credit.accounts
            ADD CONSTRAINT credit_accounts_amount_check
            CHECK (
                credit_limit_amount >= 0
                AND outstanding_amount >= 0
                AND outstanding_amount <= credit_limit_amount
            )
        ");

        Schema::create('credit.entries', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->ulid('public_id')->unique();
            $table->unsignedBigInteger('tenant_id');
            $table->unsignedBigInteger('account_id');
            $table->string('entry_type',40);
            $table->decimal('amount_delta',18,6);
            $table->string('source_type',80);
            $table->string('source_public_id',64);
            $table->string('idempotency_key',140);
            $table->timestampTz('occurred_at');
            $table->unsignedBigInteger('created_by_user_id')->nullable();
            $table->jsonb('metadata')->nullable();
            $table->timestampTz('created_at')->useCurrent();

            $table->foreign('tenant_id')->references('id')->on('core.tenants')->restrictOnDelete();
            $table->foreign('account_id')->references('id')->on('credit.accounts')->restrictOnDelete();
            $table->foreign('created_by_user_id')->references('id')->on('users')->restrictOnDelete();

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

        DB::statement("
            ALTER TABLE credit.entries
            ADD CONSTRAINT credit_entries_type_check
            CHECK (entry_type IN ('charge','payment','return_credit','reversal','adjustment'))
        ");
        DB::statement("
            ALTER TABLE credit.entries
            ADD CONSTRAINT credit_entries_nonzero_check
            CHECK (amount_delta <> 0)
        ");

        /*
         * Credit is AR, not a fake payment. Extend the sale state so a sale can be
         * operationally completed while remaining on-account.
         */
        DB::statement('ALTER TABLE sales.sales DROP CONSTRAINT IF EXISTS sales_sales_payment_status_check');
        DB::statement("
            ALTER TABLE sales.sales
            ADD CONSTRAINT sales_sales_payment_status_check
            CHECK (
                payment_status IN (
                    'unpaid','partial','paid','unknown',
                    'refunded','partially_refunded','on_account','partially_on_account'
                )
            )
        ");
    }

    public function down(): void
    {
        DB::statement('ALTER TABLE sales.sales DROP CONSTRAINT IF EXISTS sales_sales_payment_status_check');
        DB::statement("
            ALTER TABLE sales.sales
            ADD CONSTRAINT sales_sales_payment_status_check
            CHECK (payment_status IN ('unpaid','partial','paid','unknown','refunded','partially_refunded'))
        ");

        Schema::dropIfExists('credit.entries');
        Schema::dropIfExists('credit.accounts');
        Schema::dropIfExists('wallet.entries');
        Schema::dropIfExists('wallet.accounts');
        Schema::dropIfExists('loyalty.entries');
        Schema::dropIfExists('loyalty.accounts');
    }
};

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

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

return new class extends Migration
{
    public function up(): void
    {
        DB::statement('ALTER TABLE payments.payments DROP CONSTRAINT IF EXISTS payments_payments_method_check');
        DB::statement("
            ALTER TABLE payments.payments
            ADD CONSTRAINT payments_payments_method_check
            CHECK (method_type IN ('cash','card','external','wallet'))
        ");
    }

    public function down(): void
    {
        DB::statement('ALTER TABLE payments.payments DROP CONSTRAINT IF EXISTS payments_payments_method_check');
        DB::statement("
            ALTER TABLE payments.payments
            ADD CONSTRAINT payments_payments_method_check
            CHECK (method_type IN ('cash','card','external'))
        ");
    }
};

