# Seller on sale: banned / unapproved sellers vanish for customers (2026-11-02)

Branch `fix/seller-off-products-2026-11-02`. Term in [`CONTEXT.md`](../../../CONTEXT.md) ("Seller on sale"); code map in [`04-auth-users-sellers.md`](../04-auth-users-sellers.md) §4.10d.

## The rule

A seller is **on sale** only while their store is approved (`shops.verification_status = 1`) **and** their account is not banned (`users.banned = 0`). Only an on-sale seller's products reach customers, on the website and in the app.

Staff take a seller off with **Ban** or the **Approved** switch / reject. The products themselves are never changed: they stay published, stay in the seller's panel, and cart rows are hidden, not deleted. Turning the seller back on brings everything back exactly as it was.

## One rule, in one place

- `verified_sellers_id()` (cached seller list) leaves out banned sellers too. Before, it only looked at approval.
- `where_seller_on_sale($query)` and `Product::sellerOnSale()` apply the rule to a query; `product_seller_on_sale($product)` to one loaded product. They read the cached list, so they add no database queries. `filter_products()` (every product list) uses the same helper.
- **A new customer path that shows or sells a product must use one of them.**

## Where an off seller's products are hidden

- **Website**
  - product page (404)
  - quick view (the "product unavailable" box)
  - variant price (`in_stock = 0`)
  - compare page
  - flash deals (homepage block and `/flash-deal/{slug}`)
  - "products by category" rows on the homepage
  - add to cart
  - product feed `/export/product` (Facebook / Google)
- **Cart and checkout**: `CartProductOnSaleScope` hides those rows (never deletes them), so they cannot be checked out; they come back with their quantity when the seller is on sale again.
- **App**
  - product details `products/{slug}/{user_id}` answers like an unknown product (`{"data": [], "success": true}`); it now also hides unpublished / unapproved products, as the website does
  - variant price `products/variant/price`
  - add to cart `carts/add`
  - the shop's product lists (`shops/products/*`) and `products/seller/{id}`
  - `GET products` (index / show)
  - flash deals
  - the coupon product list
- **Back-in-stock alerts** keep waiting while the seller is off (`StockAlerts::onSale()`).

## The seller's profile page

`/shop/{slug}` and `/shop/{slug}/…` behave as before (decided 2026-11-02): an unapproved store's page opens, a banned store's page is a 404.

## When a seller is banned, unbanned, approved or un-approved

The `Shop` (verification_status) and `User` (a seller's `banned`) model hooks call `seller_on_sale_changed()`, whatever screen made the change:

- every cached product list, app answer and the product feed is retired (`forget_product_caches()`);
- when the seller comes back, customers waiting for one of their products to be back in stock are queued.

## Admin

Switching **Approved** on for a banned seller is refused with «ألغِ حظر هذا التاجر قبل الموافقة عليه» (Unban this seller before approving them), and the switch flips back. Unban first.

## Not changed

Orders already placed, the seller's balance and panel, admin screens, preorder (already checked the seller), Ban is still a GET link.

## Tests

- `tests/Feature/SellerOnSaleTest.php` (12 cases, SQLite) and 2 cases in `StockAlertsTest`.
- Opt-in `SellerOnSaleRuntimeTest` (MySQL copy, rolled back): bans and un-approves a real seller through the admin routes, checks every website and app path above, checks that approving a banned seller is refused, and that unbanning + approving brings everything back.

  ```bash
  KAPITANO_RUNTIME_DB_TESTS=1 vendor/bin/phpunit --filter SellerOnSaleRuntimeTest
  ```

- 16 existing test fixtures gained the `added_by` / `banned` / settings columns the cart and lists now read.
- Full suite at the time: 3,851 tests; the only 4 failures (push-flow time-zone tests) fail the same way on `dev` without this change.

## Existing bugs found, not fixed here

- App `shops/products/all/{id}` answers 500 for any shop that has products: `ProductCollection` uses `route('products.related')`, which does not exist (`SearchProductCollection` too).
- App routes `subCategory`, `subSubCategory` and `newFromSeller` point to methods missing from `Api/V2/ProductController`.

## Deploy

No migration. Run `php artisan cache:clear` once, so the cached seller list is rebuilt without banned sellers.
