# Amaleena Rebuild — Architecture Spec (v0.1)

Rebuild of **amaleena.my** (currently WordPress + WooCommerce + Elementor) on Laravel 13 / PHP 8.3+.
Owner: Amaleena Jewel Sdn Bhd (1147485-U). Language: Malay (BM). Market: Malaysia only.

---

## 1. Stack

| Layer | Choice | Notes |
|---|---|---|
| Framework | Laravel 13, PHP 8.3+ | Strict types, PSR-12 |
| Storefront UI | Livewire + Alpine.js + Tailwind + Vite | |
| Admin | Filament (verify Laravel 13 support) | Fallback: hand-built Livewire for critical screens |
| DB | MySQL 8 | InnoDB, FK constraints on |
| Auth | Fortify (Livewire views) | Customer + admin guards |
| Queue | `database` driver | Shared hosting — no Redis assumed |
| Cache/Session | `database` or `file` | No Redis on cPanel |
| Payments | `PaymentGateway` interface | Impl TBD (ToyyibPay / Billplz assumed) |
| Gold price | `GoldPriceProvider` interface | Manual admin entry default |

### Assumptions (each behind an interface, cheap to swap)
1. Filament ships a Laravel 13-compatible version. Verify before committing.
2. Payment gateway = ToyyibPay or Billplz (FPX-first). Confirm actual provider at build time.
3. Gold price entered manually by admin; optional scheduled API feed added later.

---

## 2. Domain model

Money stored as **integer cents** (`unsigned bigint`) to avoid float errors. Weight as `decimal(6,2)` grams.

### 2.1 Pricing

**gold_prices** (append-only, effective-dated)
- id, purity (enum: 916, 835, 750, 375), sell_price_per_gram (cents), buy_price_per_gram (cents, for trade-in), effective_from (datetime), created_by (fk users), timestamps
- Index: (purity, effective_from). Current = latest row per purity where effective_from <= now().
- **Never updated/deleted** — new price = new row.

### 2.2 Catalog

**categories** (adjacency list)
- id, parent_id (nullable, fk self), name, slug (unique), position, is_active, timestamps
- Tree: Emas 916 → {Cincin 916 → Belah Rotan, Bangle 916, Rantai Tangan 916 → {Evergreen, Fesyen}}, Koleksi → {Manja Series → {Cincin Fesyen, Loket}, Amaleena 375}

**products**
- id, name, slug (unique), description, purity (enum), type (enum: simple|variable),
  workmanship_type (enum: per_gram|flat), workmanship_amount (cents),
  price_override (cents, nullable — bypasses computed price), is_active, published_at, timestamps
- `$fillable` explicit; `price_override` NOT mass-assignable from storefront.

**product_variations**
- id, product_id (fk), weight_grams (decimal 6,2), sku (unique, nullable), price_override (cents, nullable), stock (int, nullable = made-to-order), image_id (nullable), position, timestamps
- Variation attribute = weight ("Spesifikasi" in current site, e.g. 3.22g / 3.59g).

**category_product** (pivot) — product_id, category_id. Products belong to multiple categories.

**media / product_images** — id, product_id, disk, path, alt, position, is_primary. Validate mime+ext+size on upload; store outside webroot or on `public` disk with random names.

### 2.3 Cart & Orders

**carts** — id, customer_id (nullable), session_token (nullable), timestamps
**cart_items** — id, cart_id, product_variation_id, quantity. Price computed live at render; never trusted from client.

**orders**
- id, number (unique, e.g. AML-2026-000123), customer_id (fk),
  status (enum: pending_payment|paid|processing|shipped|delivered|cancelled|refunded),
  subtotal, shipping_total, discount_total, grand_total (all cents),
  shipping_name/phone/line1/line2/city/postcode/state, // snapshot
  courier (nullable), tracking_number (nullable),
  affiliate_id (nullable, fk), store_credit_id (nullable, fk),
  payment_reference (nullable), paid_at (nullable), timestamps
- Index: number, customer_id, status.

**order_items** (immutable price snapshot)
- id, order_id, product_id, product_variation_id (nullable if deleted later),
  name, weight_grams, purity, gold_price_per_gram (cents), workmanship (cents),
  unit_price (cents), quantity, line_total (cents), gold_price_id (fk, audit)
- Frozen at placement. Never recomputed.

### 2.4 Store credit / e-Voucher

**store_credits** — id, customer_id, code (unique), type (enum: refund_voucher|tradein_credit|cancellation_refund), initial_amount, balance (cents), expires_at (nullable), status (enum: active|used|expired|void), timestamps
**store_credit_transactions** (ledger) — id, store_credit_id, order_id (nullable), amount (signed cents), type (enum: issue|redeem|reverse), created_by, timestamps
- Redemption: DB transaction + `lockForUpdate()` on the credit row, decrement balance, write ledger row. Prevents double-spend / race.

### 2.5 Trade-in (buy used gold)

**trade_in_requests** — id, customer_id, status (enum: submitted|under_review|approved|rejected|completed), total_estimated, total_final (cents, nullable), resulting_store_credit_id (nullable), admin_notes, reviewed_by (nullable), timestamps
**trade_in_items** — id, trade_in_request_id, purity (enum), weight_grams, estimated_value, final_value (cents, nullable)
- Estimate = weight × buy_price_per_gram(purity). Online = quote/request only; final value set after in-store verification. Approval → issues store_credit. Every status change audit-logged.

### 2.6 Affiliate

**affiliates** — id, user_id (fk), code (unique), commission_rate (decimal 5,2 %), status (enum: pending|active|suspended), payout_method, payout_details, timestamps
**affiliate_referrals** — id, affiliate_id, order_id, commission_amount (cents), status (enum: pending|approved|paid), approved_at (nullable), paid_at (nullable), timestamps
- Attribution: `?ref=CODE` sets signed cookie (30-day). Captured onto order at placement. Self-referral (affiliate == customer) blocked. Commission computed on order = paid/completed.

### 2.7 Reviews

**reviews** — id, product_id, customer_id, order_id (nullable = verified purchase), rating (tinyint 1–5), body, status (enum: pending|approved|rejected), timestamps
- Moderated (default pending). Escape output; strip HTML. One review per customer per product (unique constraint).

### 2.8 Status enums
All statuses as PHP backed enums (string), not free-text columns. E.g. `OrderStatus`, `TradeInStatus`, `StoreCreditStatus`, `ReferralStatus`, `Purity`, `WorkmanshipType`.

---

## 3. Key flows

### 3.1 Checkout price-lock
1. Cart renders live computed prices (`PricingService`).
2. On "Place order": recompute server-side, show confirmation of final price + weight (policy requirement).
3. On confirm: within a DB transaction, create order + snapshot each `order_item` (weight, purity, gold_price_per_gram, workmanship, unit_price, gold_price_id). Order totals now immutable.
4. Apply store credit (locked/decremented) and affiliate attribution.
5. Hand off to payment gateway.

### 3.2 Payment callback
- Gateway → callback endpoint. **Verify signature.** Look up order by reference. **Idempotent** status transition (ignore duplicate callbacks). Log raw payload. Move order pending_payment → paid, fire `OrderPaid` event (queued: email, affiliate commission, stock).

### 3.3 Trade-in
Submit request → est. value from buy_price → admin review (in-store weigh/verify) → approve sets final value + issues store_credit → customer redeems at checkout. Audit log each transition.

### 3.4 Voucher/credit redemption
At checkout, validate code server-side (owner match, active, not expired, balance > 0). On order: transaction + `lockForUpdate`, decrement, ledger row, attach to order.

### 3.5 Gold price update
Admin sets new sell/buy per gram per purity → new `gold_prices` row → entire catalog reprices instantly (computed, not stored). No product edits needed.

---

## 4. Shipping
- Free domestic shipping for grand_total ≥ RM300 (before shipping).
- Else zone rate: Peninsular vs East Malaysia (config-driven flat rates).
- Delivery estimate copy: Peninsular ≤5 days, East ≤7; processing 2–3 business days.
- Malaysia only — restrict state list to MY.

---

## 5. Shared hosting / queues
- Cron: `* * * * * php artisan schedule:run`
- Scheduler runs `queue:work --stop-when-empty` each minute (DB queue). Handles order emails, affiliate commission calc, notifications.
- No long-running worker. Keep jobs short/idempotent.
- Compiled Vite assets uploaded (no node build on server).
- `.env` outside webroot; `APP_DEBUG=false`; force HTTPS; secure/session cookies; error pages hide stack traces.

---

## 6. Security checklist (per module)
- **Pricing:** never accept client price/weight. Recompute server-side. Snapshot at order. `price_override` admin-only.
- **Store credit/voucher:** atomic decrement under row lock; server-side validation; ledger for audit; codes are random, non-sequential.
- **Affiliate dashboard:** policy-scoped to own data (IDOR guard on every query). Block self-referral. Verify ownership of payout data.
- **Trade-in/refund/approvals:** authorize via policy; audit-log create/update/approve/reject.
- **Payments:** signature-verified, idempotent callbacks; never trust callback amount without matching stored order total.
- **Reviews:** verified-purchase check; moderation; escape output.
- **General:** Form Request validation, policies/gates, CSRF, guarded mass-assignment, eager-load to kill N+1, pagination on all lists, rate-limit auth/OTP/password-reset/checkout, validate uploads (mime+ext+size+dimensions), no secrets in repo, hash passwords (bcrypt/argon).

---

## 7. Suggested build order
1. Foundation: auth, enums, `gold_prices`, `PricingService`, categories/products/variations + admin.
2. Catalog storefront: shop, category, product detail (live pricing), search, pagination.
3. Cart → checkout with price-lock + order snapshot.
4. Payment gateway integration + callbacks.
5. Store credit/e-voucher (needed by refunds + trade-in).
6. Trade-in module.
7. Affiliate module.
8. Reviews.
9. CMS pages (About, T&C, Refund, Galeri, campaigns), home page, shipping rules, emails.

---

## 8. Open questions (before/at relevant module)
- Actual payment gateway?
- Manual gold price entry only, or auto API feed?
- Upah: per-gram or flat per product (model supports both — which is the norm)?
- Made-to-order (no stock tracking) or real stock counts?
- Guest checkout allowed, or account required?
