# WPS2 — Checkout prerequisites + coupon tenancy fix

**Repo:** BE only — `/home/moonui4/moon-erp-be` (branch `hazemdev4`)
**Depends on:** WPS ✅ `08804f477` · **Blocks:** WP5 (checkout)
**Review:** code-reviewer · **[FIN]:** yes — this touches shipping cost, payment methods and coupon validation · **Migration:** no

Two separate pieces of work, owner-approved 2026-07-18. Keep them as **two commits**.

---

## Part A — Fix the cross-company coupon leak (security)

`Modules/WebStore/app/Services/CheckoutService.php` looks up coupons at **lines 65 and 109**:

```php
$coupon = StoreCoupon::where('code', $couponCode)->first();
```

**No `company_id` filter** — in a file that filters `company_id` correctly on statuses at lines 116/123/131. So a coupon code belonging to **another company** is honoured by `checkout/calculate` and by order placement.

The inconsistency makes it worse: `CartController::applyCoupon` **does** filter correctly, so the same code is rejected in one place and accepted in another. WP5's checkout passes `coupon_code` straight to `calculate`, which is what makes this reachable.

**Required:**
- Scope both lookups to the customer's company (derive it the way the surrounding code does — `$customer->company_id`; do not invent a new source of truth).
- Check the **whole file** for other unscoped queries while you are in it, and any sibling lookup in the same service layer. **Fix in the shared layer** if the same unscoped pattern repeats.
- **Write a Pest regression test** that fails before your fix and passes after: a coupon belonging to company B must not apply to a company-A customer's checkout. Say plainly in your report that you confirmed it fails on the old code.
- Do **not** attempt to fix the other known tenancy gaps (public catalog endpoints not filtering `company_id`, out-of-stock not blocked on add-to-cart). They are separate tickets. Mention them if you touch adjacent code, but stay in scope.

⚠️ Remember: the backend has **zero Eloquent global scopes**. `TenantAware` stamps `company_id` on create only. Every query carries its own filter or it leaks — there is no safety net.

---

## Part B — Seed the checkout prerequisites

`moonui4_dev_be` cannot place a single order today. Verified counts, all **zero**: `store_payment_methods`, `governorates`, `cities`, `store_order_statuses` (company 1), `store_payment_statuses` (company 1).

**Use what already exists before writing anything new:**
- `Modules/WebStore/database/seeders/DefaultStoreStatusesSeeder.php` — covers order + payment statuses. Run it; do not rewrite it.
- `Modules/Core/database/seeders/CountryGovernorateSeeder.php` — covers governorates/cities. Inspect it first: confirm what it actually seeds, whether it is idempotent, and whether it stamps `company_id`. **If it is not safe to re-run, say so and stop rather than running it.**

**What needs new work: payment methods.** There is no seeder anywhere. Seed **exactly one active method — Cash on Delivery (`type = 'cod'`)** — bilingual name, `company_id = 1`. Owner decision D4: the storefront ships COD-only; card/online options are deliberately hidden as «قريبًا» rather than offered. **Do not seed online/card methods** — an active method the backend cannot actually process is a false promise to a customer.

**Shipping cost:** the storefront reads `city.shipping_cost` (flat, per city; `tax_amount` is hard-coded 0 server-side). If the governorate seeder leaves every city at `0`, shipping will silently always be free and WP5 would be verified against a fiction. Ensure at least a few cities carry a **non-zero, realistic** `shipping_cost` so the calculation is genuinely exercised — and report which cities and what values.

### Constraints (same as WPS — these are absolute)

1. ⛔ **NEVER** `migrate:fresh`, `migrate:refresh`, `db:wipe`, or `RefreshDatabase`. Dev DBs are **not binlogged**; a wrong wipe is unrecoverable. If you think you need one, STOP and report.
2. ⛔ Touch only `moonui4_dev_be`. Never `/home/moonui`, `/home/moonui2`, `/home/moonui3`.
3. **No migration.** Use the schema as it is.
4. **Idempotent.** Running everything 3× must leave identical counts. Use `updateOrCreate`/`firstOrNew` on a stable natural key. Note the WPS lesson: these models extend `BaseModel` with **`SoftDeletes`**, so a plain `updateOrCreate` cannot see a trashed row while the unique constraint still counts it — use `withTrashed()->firstOrNew()` and un-trash.
5. **Never destructive.** Create and update your own rows only. Never delete, truncate, or modify rows you did not create.
6. Not wired into any automatic path (`DatabaseSeeder`, `local-deploy.sh`, the scheduler). Human-invoked only.

## Acceptance criteria — paste real output, do not assert

- [ ] **Part A:** the regression test **fails on the pre-fix code** and passes after — show both.
- [ ] `GET /api/store/payment-methods` (live) returns exactly one COD method. Paste it.
- [ ] Governorates and cities are populated; paste counts and 3 sample cities with their `shipping_cost`.
- [ ] Order + payment statuses exist for company 1; paste counts.
- [ ] Everything re-run 3× → identical counts (paste before/after).
- [ ] Rows with `company_id != 1` created by you: **0**.
- [ ] `vendor/bin/pint --dirty --format agent` clean.
- [ ] `php artisan test Modules/WebStore --compact` — baseline is **376 passed / 1158 assertions**. Your new test **adds** to that; zero pre-existing failures may appear.

## Commits

Two conventional commits on `hazemdev4`:
1. The coupon fix + its test — this is a real security fix, so add **one bilingual bullet** under `## [Unreleased]` in `docs/moonstack/CHANGELOG.md` and verify the file stays well-formed.
2. The seeder work — dev fixture, `[skip-changelog]`, **no** changelog bullet.

**Do not push, deploy, or merge.**

## Report back

Paste actual output for every check. Flag anything in this brief that turns out to be wrong — every work package so far has found real errors in its own brief. If the governorate seeder does something unexpected, **stop and report rather than improvising**.
