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

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

================================================================
FILE: .\app\Modules\Sales\Domain\Models\SaleReturn.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\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

final class SaleReturn extends Model
{
    use HasPublicUlid;

    protected $table = 'sales.returns';

    protected $fillable = [
        'tenant_id','sale_id','return_number','status','refund_status',
        'subtotal_amount','total_amount','reason','idempotency_key',
        'approval_request_id','created_by_user_id','posted_by_user_id',
        'posted_at','metadata',
    ];

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

    public function sale(): BelongsTo
    {
        return $this->belongsTo(Sale::class,'sale_id');
    }

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

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

namespace App\Modules\Payments\Domain\Models;

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

final class PaymentRefund extends Model
{
    use HasPublicUlid;

    public $timestamps=false;

    protected $table='payments.refunds';

    protected $fillable=[
        'tenant_id','payment_id','sale_return_id','method_type',
        'amount','currency_code','status','idempotency_key',
        'provider_reference','created_by_user_id','resolved_by_user_id',
        'resolved_at','metadata','created_at',
    ];

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            $table->unsignedBigInteger('tenant_id');

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

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

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

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

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

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

            $table->timestampsTz();

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

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

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

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

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

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

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

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

            $table->timestampsTz();

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

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

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

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

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

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

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

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

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

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

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

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

            $table->timestampsTz();

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

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

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

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

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

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

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

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

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

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

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

            $table->timestampsTz();

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

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

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

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

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

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

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

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

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

            $table->timestampsTz();

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

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

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

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

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

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

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

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

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

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

            $table->timestampsTz();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            $table->timestampsTz();

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

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

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

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

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

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

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

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

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

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

            $table->timestampsTz();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            $table->unsignedBigInteger('created_by_user_id');

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

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

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

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

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

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

            $table->timestampsTz();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

