================================================================ FILE: .\app\Modules\Sales\Domain\Models\Sale.php ================================================================ '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 ================================================================ '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 ================================================================ '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 ================================================================ 'decimal:6', 'resolved_at'=>'immutable_datetime', 'metadata'=>'array', 'created_at'=>'immutable_datetime', ]; } } ================================================================ FILE: .\app\Modules\Pricing\Domain\Models\PriceListScope.php ================================================================ 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 ================================================================ 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 ================================================================ 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 ================================================================ 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 ================================================================ 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'); } };