Files
nick-doc/White-Label Shops PRD - 2026-06-10.md
Siavash Sameni 035dbfeb6a docs: add White-Label Shops PRD (gap analysis + phase plan)
14 feature gaps mapped against existing implementation on
feature/white-label-shops: upgrade/billing, theming, seller dashboard,
storefront, user base, custom email, invoicing, bot admin commands,
multi-admin, monitoring, DB isolation, backup, external payment, CRM.

Phase 0 (schema + services + admin UI) marked complete.
Phases 1–4 planned with estimates and dependencies.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 10:49:14 +04:00

455 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: White-Label Shops — Product Requirements Document
tags: [prd, white-label, multi-shop, tenant, feature]
created: 2026-06-10
status: draft
branch: feature/white-label-shops
---
# White-Label Shops — PRD
## 1. Context & Vision
Amanat is an escrow marketplace. Today every seller already has a self-contained shop:
they can list items, track orders, manage inventory, and handle delivery/dispute flows.
This PRD defines the **White-Label Shops** tier: an upgrade path that lets a seller
spin up one or more fully branded, independently-operated shops — each with their own
domain, their own Telegram bot, their own user base, their own theming, and their own
billing identity — all running on Amanat infrastructure.
The goal is that a white-label shop feels like a completely separate product to the
end buyer. The platform's presence is invisible unless the shop owner explicitly
acknowledges it.
---
## 2. What Is Already Built (Baseline)
The following is in production or merged on `feature/white-label-shops`. The PRD
**does not** re-specify these; it specifies the gaps.
| Component | Status |
|-----------|--------|
| DB schema: `tenants`, `tenant_domains`, `tenant_bots`, `tenant_user_roles`, `tenant_payment_policies`, `tenant_integrations` | ✅ Migrated |
| Tenant CRUD backend (`tenantService.ts`) | ✅ Built |
| Tenant role-based auth middleware (`tenantAuthService.ts`) | ✅ Built |
| Custom domain provisioning + Caddy route injection (`domainProvisioningService.ts`) | ✅ Built |
| Bot registration + AES-256-GCM token encryption + claim flow (`tenantBotService.ts`) | ✅ Built |
| Drizzle repos for all 6 tenant tables | ✅ Built |
| Platform admin UI: tenant list + detail pages | ✅ Built |
| `escrow-multi` deployment stack (separate DB, separate containers) | ✅ Running on multi.amn.gg |
| Storefront bootstrap endpoint (`GET /api/storefront/bootstrap`) | ✅ Built |
| `TenantContext` on frontend (resolves shop branding from Host header) | ✅ Built |
---
## 3. Gap Map (What This PRD Specifies)
| # | Feature Area | Gap |
|---|-------------|-----|
| G1 | Upgrade & Billing | No seller-facing upgrade flow from normal → multi-shop |
| G2 | Shop Theming | `brand` JSONB stored but no theming pipeline applied to UI |
| G3 | Seller Shop Dashboard | No tenant-owner-facing admin UI (only platform-admin UI exists) |
| G4 | Storefront (buyer-facing) | Catalog / checkout / order routes are 501 stubs |
| G5 | Tenant User Base | No per-tenant user management (register, login, Telegram link, password) |
| G6 | Email with Custom Domain | No per-tenant SMTP / transactional email |
| G7 | Invoicing | No per-tenant branded invoice generation |
| G8 | Telegram Bot Admin Commands | Bot claim works; admin commands not designed |
| G9 | Multi-Admin Management | Role model exists; no seller-facing UI to grant/revoke shop admins |
| G10 | Platform Monitoring | No per-shop observability, alerting, or ops dashboard |
| G11 | DB Isolation | `isolation_mode` column exists; per-tenant schema isolation not enforced |
| G12 | Backup & Recovery | No automated backup job or restore procedure per tenant |
| G13 | External Payment Provider | `external_provider` rail defined; integration not implemented |
| G14 | CRM Light | No customer/buyer management view for shop owners |
---
## 4. Detailed Requirements
---
### G1 — Upgrade & Billing
**Story:** A normal seller sees an "Upgrade to Multi-Shop" banner in their dashboard.
They click, pick a plan, pay (via Amanat escrow or Stripe), and their account is
immediately provisioned with a `white_label` tenant in `pending` status. They land on
a setup wizard.
**Requirements:**
| ID | Requirement |
|----|------------|
| G1-1 | A `seller_plans` table (or config) defines plan tiers: `starter` (1 shop), `growth` (3 shops), `business` (unlimited). Each tier has a monthly price and a `max_tenants` allowance. |
| G1-2 | The upgrade purchase flow must go through Amanat's existing escrow payment rail (not a separate billing system). On confirmation, the backend grants the seller a `tenant_allowance` record. |
| G1-3 | On successful purchase, the system auto-creates one `tenant` row for the buyer (status=`pending`) and sends a welcome notification (Telegram + email). |
| G1-4 | The seller dashboard must show remaining shop allowance and a "Create New Shop" button guarded by allowance. |
| G1-5 | Downgrade: if a seller's subscription lapses, shops go to `suspended` (not deleted). Data retained for 90 days; shops reactivate if subscription resumes. |
| G1-6 | Platform admin can manually override a seller's plan tier (for pilots, enterprise deals). |
**Out of scope:** Recurring billing automation. First version uses manual renewal via
Amanat purchase; Stripe subscription integration is a separate initiative.
---
### G2 — Shop Theming
**Story:** In the shop setup wizard, the owner uploads a logo, picks a primary/accent
colour, sets a display name and tagline. These are immediately reflected on the
storefront and in Telegram Mini App headers.
**Requirements:**
| ID | Requirement |
|----|------------|
| G2-1 | The `brand` JSONB column in `tenants` must be validated against a schema: `{ logo_url, favicon_url, primary_color (hex), accent_color (hex), display_name, tagline, font_family? }`. |
| G2-2 | `GET /api/storefront/bootstrap` already returns `brand`. The frontend must apply primary/accent colours as CSS variables (`--brand-primary`, `--brand-accent`) on the root element when `TenantContext` is not the Amanat default. |
| G2-3 | Logo and favicon must be stored in a tenant-scoped path in object storage (prefix: `tenants/{tenantId}/`). The file ownership check from H2 (audit) must apply here. |
| G2-4 | A "Brand Preview" live preview panel in the seller dashboard must show how the storefront looks with current brand settings before saving. |
| G2-5 | Email templates and invoice PDFs must inject `logo_url`, `primary_color`, and `display_name` from `brand` when sent on behalf of the shop. |
| G2-6 | Telegram Mini App `headerColor` and `backgroundColor` must be set via `WebApp.setHeaderColor` / `WebApp.setBackgroundColor` using `primary_color` from bootstrap when running inside a tenant bot. |
---
### G3 — Seller Shop Dashboard
**Story:** After setting up a shop, the owner navigates to a separate "Shop Admin"
section (distinct from the normal seller dashboard). This section feels like owning
a real admin panel for their own platform.
The seller dashboard already exists for normal orders. The shop dashboard is an
additional, separate surface.
**Requirements:**
| ID | Requirement |
|----|------------|
| G3-1 | Route: `/dashboard/shop/[tenantSlug]` (or subdomain-aware: `/dashboard/shop` when browsing on the tenant's custom domain). The shop admin shell must load tenant brand from `TenantContext`. |
| G3-2 | Navigation sections in shop admin: Overview, Catalog, Orders, Customers, Bots, Domains, Admins, Settings, Billing. |
| G3-3 | Overview: total orders (7d/30d), GMV, new customers, active offers, revenue chart. Data must be scoped strictly to this tenant. |
| G3-4 | Catalog: CRUD for the shop's own product catalog (separate from the global Amanat marketplace catalog). Each item: title, description, price, category, images, stock, active toggle. |
| G3-5 | Orders: list of orders placed through this shop (buyer, item, amount, status, payment rail, escrow state). Seller can trigger delivery confirmation, raise dispute, release escrow from here. |
| G3-6 | Customers: list of users who have placed at least one order in this shop. Fields: display name, joined date, order count, total spent. No cross-tenant data visible. |
| G3-7 | Bots: mirrors the existing platform-admin bot panel but scoped to this tenant (register bot, get claim link, set Mini App URL, revoke). |
| G3-8 | Domains: mirrors the existing platform-admin domain panel. Add CNAME, trigger verification, check TLS, remove. |
| G3-9 | Admins: list of users with roles in this tenant. Owner can grant `manager`, `finance`, `support`, `developer` roles to any Amanat user by searching their username/email. Owner cannot revoke their own owner role. |
| G3-10 | Settings: brand editor (G2), locale, timezone, currency, shop URL slug (rename, slug-rename triggers Caddy route update). |
| G3-11 | Billing: current plan, renewal date, shop count vs. allowance, upgrade / downgrade button. |
| G3-12 | All shop-admin API calls must go through `tenantAuthService.requireTenantRole()` middleware — not the platform-admin guard. |
---
### G4 — Storefront (Buyer-Facing)
**Story:** A buyer visits `myshop.amn.gg` (or `shop.example.com`). They see the shop's
branded catalog, can search/filter items, add to cart, and check out using escrow.
They don't see "Amanat" unless the shop has disclosed it.
The storefront routes currently return HTTP 501. This gap is the largest one.
**Requirements:**
| ID | Requirement |
|----|------------|
| G4-1 | `GET /api/storefront/catalog?page&limit&search&category` — Return paginated items for the resolved tenant. Items sourced from the shop's own catalog (G3-4). |
| G4-2 | `GET /api/storefront/items/:itemId` — Item detail: title, description, images, price, seller info (display name, rating), stock status. |
| G4-3 | `POST /api/storefront/checkout` — Initiate an order for a tenant item. Must create a `payment` record scoped to this tenant's `payment_policy`. Enforce `allowed_rails` from `tenant_payment_policies`. |
| G4-4 | `GET /api/storefront/orders/:orderId` — Buyer can look up their own order by ID. Must verify buyer identity matches. |
| G4-5 | Frontend storefront pages (path-routed when on Amanat base domain, full-page when on custom domain): `/` (catalog), `/items/[id]` (item detail), `/checkout`, `/orders/[id]` (order status). |
| G4-6 | Cart is client-side (localStorage or in-memory). No server-side cart required for v1. |
| G4-7 | The storefront must be responsive / mobile-first and function as a Telegram Mini App for the tenant's registered bots. |
| G4-8 | SEO: `<title>` and `<meta og:*>` must use `brand.display_name` and `brand.tagline`. |
| G4-9 | Buyers who are not logged into Amanat must be able to browse the catalog. Login is required only at checkout. Login on custom domain must still authenticate against the Amanat identity provider (federated SSO), or against the tenant's own user base if G5 is enabled. |
---
### G5 — Tenant User Base
**Story:** A shop owner wants their buyers to experience a self-contained login flow.
Buyers register/log in at `myshop.amn.gg/login`, see the shop's branding, and never
encounter "Amanat" in the auth flow. Behind the scenes, Amanat can use federated
identity or maintain a per-tenant user namespace.
**Requirements:**
| ID | Requirement |
|----|------------|
| G5-1 | Tenant auth mode (configurable in `features` JSONB): `federated` (default — buyers are Amanat users, no separate accounts) or `isolated` (per-tenant user namespace). V1 only ships `federated`; `isolated` is designed but not built. |
| G5-2 | In `federated` mode: login at a custom domain proxies through the Amanat JWT flow. The JWT issued must carry a `tenantId` claim so backend middleware knows the tenant context. Session cookies must be `Domain`-scoped to the custom domain, not `.amn.gg`. |
| G5-3 | Login options visible to buyers at a tenant storefront are controlled by the shop's `features.login_methods` config: `email_password`, `google_oauth`, `telegram_widget`. Default: all three. The shop owner can disable any method (e.g., Telegram-only). |
| G5-4 | Password reset emails sent to buyers from a tenant shop must use the tenant's SMTP config (G6) and must brand the email with the shop name, not "Amanat". |
| G5-5 | A buyer's Telegram account can be linked at the tenant shop login screen using the Telegram Login Widget or the tenant's own bot (if registered). |
| G5-6 | The Customers panel (G3-6) is the shop admin's view into the tenant user base. It must show login method, linked Telegram, last active, and order count. |
| G5-7 | A shop admin can manually disable a buyer's access to their shop (without affecting the buyer's Amanat account on other shops). |
| G5-8 | `isolated` user namespace (future): separate `tenant_users` table, bcrypt passwords, no Amanat JWT dependency. Migrations must be designed before any `isolated` tenant goes live. |
---
### G6 — Email with Custom Domain
**Story:** When a buyer on `shop.example.com` gets an order confirmation, the "From"
address is `orders@example.com` — not `noreply@amn.gg`. The shop owner provides their
own SMTP credentials (or Mailgun/SendGrid API key) and we deliver on their behalf.
**Requirements:**
| ID | Requirement |
|----|------------|
| G6-1 | A `tenant_integrations` row with `kind='notification'`, `provider='smtp'` stores: `host`, `port`, `user`, `from_address` in `config` JSONB, and password AES-256-GCM encrypted in `encrypted_config`. |
| G6-2 | Backend email service must resolve the active `notification` integration for the tenant before sending. Fallback to Amanat SMTP if none configured. |
| G6-3 | Supported providers for v1: `smtp` (generic), `sendgrid` (API key). `mailgun` is phase 2. |
| G6-4 | DKIM/SPF setup guidance: after the shop owner adds their SMTP details, the UI must display the DKIM selector and SPF record they need to add to their DNS, with a "Verify SPF/DKIM" button that probes the DNS records. |
| G6-5 | Email templates for order confirmation, payment receipt, delivery notice, and dispute opened must exist in both English and Persian (Farsi) and use the tenant's `brand` (logo, primary colour, display name). |
| G6-6 | `POST /api/tenants/:id/integrations` — Create/update integration. Credentials must never be logged or returned in plain text. |
| G6-7 | Test email: `POST /api/tenants/:id/integrations/:integrationId/test` — Send a test email to the tenant owner's registered email address. |
---
### G7 — Invoicing
**Story:** When a buyer completes checkout and an order is confirmed, they can download
a PDF invoice. The invoice header shows the shop's logo and name, not Amanat's.
The shop admin can also download invoices from the Orders panel.
**Requirements:**
| ID | Requirement |
|----|------------|
| G7-1 | Invoice PDF generation: triggered on order status transition to `confirmed` or `delivered`. Libraries: `pdfkit` or `puppeteer` (existing dependency evaluation needed). |
| G7-2 | Invoice fields: invoice number (`{tenantSlug}-{YYYY}-{sequence}`), date, buyer name, seller name (`brand.display_name`), logo, line items (description, qty, unit price, total), subtotal, platform fee if disclosed, total, payment rail used. |
| G7-3 | Invoice storage: `tenants/{tenantId}/invoices/{invoiceId}.pdf` in object storage, not in DB. |
| G7-4 | `GET /api/storefront/orders/:orderId/invoice` — Buyer downloads their invoice. Auth required; buyer must own the order. |
| G7-5 | `GET /api/tenants/:id/orders/:orderId/invoice` — Shop admin download. |
| G7-6 | The invoice must note the payment method (escrow, direct, external) but must NOT disclose blockchain transaction hashes unless the shop explicitly enables it in settings. |
| G7-7 | If the shop has not configured a logo, the Amanat wordmark is used as fallback (not absent). |
---
### G8 — Telegram Bot Admin Commands
**Story:** The shop owner has claimed their bot (via `/start <claimToken>`). Now they
want to use the bot to get order alerts, approve/reject high-value orders, and answer
buyer queries. The bot must distinguish between the shop owner/admin and regular buyers.
**Requirements:**
| ID | Requirement |
|----|------------|
| G8-1 | After claim, the bot must respond to `/start` for a non-admin buyer with a welcome message and a "Browse Shop" Mini App button. |
| G8-2 | Admin commands (only for users in `tenant_user_roles` with role ≥ `manager`): `/orders` — last 10 open orders; `/order <id>` — order detail + action buttons (confirm delivery, raise dispute); `/stats` — 7-day GMV and order count; `/pause` — pause new orders (sets `shop_settings.accepting_orders = false`); `/resume` — unpause. |
| G8-3 | Order notification to admin: when a new order is placed on the shop, send a Telegram message to all users with `manager` or `owner` role (who have a linked Telegram account) via the tenant's bot (not via the platform @amnescrow_Bot). |
| G8-4 | Dispute alert: when a buyer opens a dispute, immediately notify the admin(s) via the tenant bot with the dispute ID and a "View Dispute" deep link. |
| G8-5 | The tenant bot webhook handler (`POST /api/telegram/tenant-webhook/:botId`) already exists. Extend it to route commands to the above handlers after verifying `admin_telegram_user_id` or cross-checking `tenant_user_roles`. |
| G8-6 | Rate limit: 20 bot command requests / minute per user to prevent abuse. |
| G8-7 | Bot errors (expired token, revoked bot) must set the bot status to `suspended` and notify the shop owner via Amanat's own Telegram notification (not the tenant bot). |
---
### G9 — Multi-Admin Management
**Story:** The shop owner wants to add their business partner as a manager. They go to
the Admins section in the shop dashboard, search for the partner's Amanat username,
pick a role, and send an invite. The partner gets a Telegram/email notification.
The backend role model is already built. The gap is the seller-facing UI and the
invite flow.
**Requirements:**
| ID | Requirement |
|----|------------|
| G9-1 | `POST /api/tenants/:id/roles` (existing) — Seller-accessible when requester has `owner` role. Add validation: a tenant may have at most 1 `owner`, up to 5 `manager`, up to 10 combined `finance`/`support`/`developer` roles. |
| G9-2 | Invite flow: instead of granting the role immediately, create a `tenant_invitations` record (id, tenant_id, email_or_username, role, token, expires_at). Send invite notification. Invitee must accept before role is active. |
| G9-3 | `GET /api/tenants/:id/invitations` — List pending invitations (owner/manager only). |
| G9-4 | `POST /api/tenant-invitations/:token/accept` — Invitee accepts. Validates token not expired, creates `tenant_user_roles` row, deletes invitation. |
| G9-5 | `DELETE /api/tenants/:id/roles` (existing) — Seller can revoke any role except their own `owner`. |
| G9-6 | Role change notification: when a role is granted or revoked, notify the affected user via Telegram and email. |
| G9-7 | Frontend: Admins panel in shop dashboard shows current roles + pending invitations. "Invite Admin" button opens a dialog: search field (username/email), role picker, send invite. |
---
### G10 — Platform Monitoring
**Story:** The platform ops team (Amanat) must see at a glance that all white-label
shops are healthy: uptime, error rates, queue depths, domain/TLS status, bot health.
Shop owners get alerts when something affects their shop.
**Requirements:**
| ID | Requirement |
|----|------------|
| G10-1 | A `tenant_health` view (or materialized cache) per tenant: domain TLS status, bot webhook last received (staleness), last order timestamp, error count in last 24h. |
| G10-2 | Platform admin dashboard (existing tenant detail page) must show this health summary per tenant with colour-coded status (green/yellow/red). |
| G10-3 | Alert conditions and notification targets: |
| | • TLS cert expired or near expiry (< 7 days) → notify shop owner |
| | • Bot webhook not received for > 4 hours → notify shop owner |
| | • Domain DNS verification failed → notify shop owner |
| | • Error rate > 5% of requests in 1h window → notify platform ops |
| | • No orders in 72h for an active shop → no alert (normal) |
| G10-4 | Alerts are delivered via Amanat's platform bot (@amnescrow_Bot) to the shop owner's linked Telegram, and by email if they have one configured. Not via the tenant bot (which may be broken). |
| G10-5 | Metrics endpoint: `GET /api/internal/tenants/health-summary` (platform-admin only) — returns JSON array of tenant health records for all active tenants. Used by a future Grafana panel. |
| G10-6 | Log isolation: backend logs must include a `tenantId` field in every log line emitted during a tenant-scoped request. This enables per-tenant log filtering in Loki/Grafana. |
| G10-7 | The domain provisioning poller (already runs every 60s) must emit a structured log on each poll result so it can be scraped. |
---
### G11 — Database Isolation
**Story:** By default all tenants share the `escrow_multi` Postgres database, with
row-level ownership enforced by `tenant_id` FKs. For enterprise shops that require
stronger isolation (regulatory, contractual), the platform must support a separate
Postgres schema or separate Postgres database.
**Requirements:**
| ID | Requirement |
|----|------------|
| G11-1 | The `isolation_mode` column already exists on `tenants` with values `shared` \| `schema` \| `database`. V1 only provisions `shared` mode tenants. |
| G11-2 | All queries that read tenant-scoped data (catalog, orders, customers) must include an explicit `WHERE tenant_id = $1` clause. No implicit context. The code review checklist must include this check. |
| G11-3 | `schema` mode (Phase 2): on tenant creation with `isolation_mode='schema'`, run `CREATE SCHEMA tenant_{slug}` and apply migrations scoped to that schema. Connection pool must select the schema via `SET search_path`. |
| G11-4 | `database` mode (Phase 3): separate Postgres database + separate connection pool. Only viable for enterprise tier. Migration tooling must support it. |
| G11-5 | Regardless of isolation mode, the `tenant_user_roles` table in the platform schema always governs access. There is no separate auth DB per tenant in any mode. |
| G11-6 | The existing DB privilege isolation migration (`0018_db_privilege_isolation.sql`) that creates `escrow_vital_user` and `escrow_nonvital_user` Postgres roles must be extended so that vital tables (orders, payments, balances) are inaccessible to the `nonvital` role even across schemas. |
---
### G12 — Backup & Recovery
**Story:** The platform runs nightly backups of each tenant's data and notifies shop
owners that their data is safe. In a recovery scenario, a shop's data can be restored
to a point-in-time without affecting other tenants.
**Requirements:**
| ID | Requirement |
|----|------------|
| G12-1 | A backup job (cron, daily 02:00 UTC) runs `pg_dump --schema=public --table='tenant_*' --where="tenant_id='...'"` for each active tenant and uploads a `.dump` file to object storage at `backups/{tenantId}/{date}.dump`. |
| G12-2 | Object storage retention: 30 daily backups, then weekly for 6 months, then monthly for 3 years. Lifecycle policy must be set on the bucket. |
| G12-3 | Backup success/failure is recorded in a `backup_logs` table: tenant_id, job_run_at, status, size_bytes, storage_path, error. |
| G12-4 | Platform alert: if any tenant's backup job fails 2 consecutive times, notify platform ops via @amnescrow_Bot and email. |
| G12-5 | Shop owner notification: weekly digest email ("Your shop data was backed up successfully on [dates]. Backup size: X MB."). No alert on success for daily — only on failure. |
| G12-6 | Restore procedure: platform admin can trigger a restore for a specific tenant from the admin panel. Restore runs in a transaction; original data is renamed (not deleted) before restore so rollback is possible. This is an operator action, not self-service for V1. |
| G12-7 | The backup job must not hold locks on the tenant tables for > 5 seconds. Use `--snapshot` / `REPEATABLE READ` isolation. |
---
### G13 — External Payment Provider
**Story:** A large merchant doesn't want to use Amanat's escrow rails for all
transactions. They want to offer credit-card checkout powered by Stripe (or their
existing payment gateway) alongside — or instead of — Amanat escrow.
The `tenant_payment_policies.allowed_rails` enum already includes `external_provider`.
The gap is the integration layer.
**Requirements:**
| ID | Requirement |
|----|------------|
| G13-1 | A `tenant_integrations` row with `kind='payment'`, `provider='stripe'` (or `paypal`, `zarinpal` for IR market) stores the provider API credentials (AES-256-GCM encrypted). |
| G13-2 | `POST /api/storefront/checkout` must check `tenant_payment_policies.allowed_rails`. If `external_provider` is in allowed_rails and buyer selects it, the backend creates a checkout session on the external provider and returns a `redirect_url`. |
| G13-3 | Webhook handler: `POST /api/tenants/:id/payment-webhook/:provider` — receives payment confirmation from the external provider, verifies signature, transitions order status. |
| G13-4 | V1 providers: `stripe` only. `paypal` and `zarinpal` are Phase 2. |
| G13-5 | If the tenant's external provider integration is not configured, `external_provider` must not appear as a buyer-facing option even if it's in `allowed_rails`. |
| G13-6 | Amanat's escrow guarantee does not apply to `external_provider` transactions. The buyer UI must show a clear disclosure: "Payment processed by [Provider Name]. Amanat escrow does not apply to this transaction." |
| G13-7 | Revenue split: Amanat charges a platform fee (configurable per tenant) even on external provider transactions. The fee is invoiced monthly to the shop owner, not deducted at checkout. |
---
### G14 — CRM Light
**Story:** A shop owner wants to see who their repeat buyers are, filter by order
count or total spent, and send a bulk announcement to buyers who opted in to
notifications.
**Requirements:**
| ID | Requirement |
|----|------------|
| G14-1 | The Customers panel (G3-6) is the entry point. It must support sorting and filtering: by join date, total orders, total spent, last active, login method, Telegram linked (yes/no). |
| G14-2 | Buyer profile detail page (within shop admin, not exposed to other tenants): name, email (if federated, masked as `j***@gmail.com`), Telegram username (if linked), order history within this shop, total lifetime spend in this shop. |
| G14-3 | Bulk notification: "Announce to Customers" — compose a message (plain text, max 1000 chars), target audience (all opted-in, filtered list), delivery channel (Telegram only for V1; email in Phase 2). |
| G14-4 | Opt-in/opt-out: buyers can opt out of marketing notifications from the shop at any time (unsubscribe link in email, `/unsubscribe` command in tenant bot). Opt-out is per-tenant, not global. |
| G14-5 | Announcement rate limit: max 2 bulk announcements per shop per 24 hours to prevent spam. Platform can override. |
| G14-6 | Announcement log: every announcement is recorded (content, targets count, sent at, delivery count, opt-out count). Retained for 12 months. |
| G14-7 | No PII export API for V1. Shop owners can view customer data in the UI but cannot bulk-export email lists. |
---
## 5. User Stories Summary
| Story | Actor | When | Then |
|-------|-------|------|------|
| Upgrade to multi-shop | Seller | Clicks "Upgrade" in dashboard | Lands on plan picker, pays, shop provisioned |
| Create a shop | Shop owner | After upgrade | Setup wizard: name → brand → domain → bot |
| Apply theming | Shop owner | In Settings / Brand | Storefront and Mini App reflect new colours / logo |
| Manage catalog | Shop owner | In Shop Admin → Catalog | Add, edit, archive items; set price, stock, images |
| Invite shop manager | Shop owner | In Shop Admin → Admins | Send invite by username; manager gets notification |
| Buyer browses shop | Buyer | Opens custom domain | Sees branded storefront, searches catalog |
| Buyer checks out | Buyer | Adds item to cart | Selects payment rail; escrow or external provider |
| Order notification | Shop admin | Order placed | Telegram message via tenant bot with order summary |
| Domain setup | Shop owner | In Shop Admin → Domains | Adds CNAME, verifies DNS, TLS cert auto-issued |
| Bot claim | Shop owner | After bot registration | Sends `/start <token>` to their bot; becomes bot admin |
| Send announcement | Shop owner | In CRM → Announce | Sends Telegram message to opted-in buyers |
| Download invoice | Buyer | On order confirmation | Downloads branded PDF invoice |
| Monitor shop health | Platform ops | Admin dashboard | Sees per-shop TLS, bot, and error status |
| Receive backup alert | Shop owner | Weekly digest | Email confirming backup size and dates |
| Restore shop data | Platform ops | After incident | Admin UI triggers restore; owner notified |
---
## 6. Technical Dependencies & Constraints
| Constraint | Detail |
|-----------|--------|
| Multi-stack isolation | Work on `feature/white-label-shops` targets `escrow-multi` only. Never touch `escrow-dev`. |
| Separate bot tokens | `escrow-multi` TELEGRAM_BOT_TOKEN must differ from `escrow-dev`. Each tenant bot is a third token. |
| Caddy Admin API | Domain provisioning calls `infra-caddy:2019`. The Caddy container must have Admin API enabled and the backend must be on `shared-web` network. |
| `TENANT_SECRET_KEY` | Must be 32 bytes of cryptographically random data, set before any bot is registered. Key rotation requires re-encrypting all `encrypted_token` fields. |
| PDF generation | No PDF library in backend yet. `pdfkit` is lightweight; `puppeteer` gives richer templates but adds ~200MB to the Docker image. Recommend `pdfkit` for V1. |
| Per-tenant SMTP | Backend email service must be refactored to accept a transport config per tenant rather than reading global env vars. |
| Object storage | No S3/Minio integration exists yet. Required for logo uploads (G2), invoice storage (G7), and backups (G12). `minio` sidecar in `escrow-multi` stack is the simplest path. |
| DB backup job | Needs a separate `escrow-multi-backup` container or cron job on the host. Cannot run inside the backend container. |
| Tenant catalog | Requires a `tenant_catalog_items` table (not yet in schema). Must be added in a new migration before G4 can be built. |
| Invitation table | `tenant_invitations` table not yet in schema. Add before G9. |
| `backup_logs` table | Not yet in schema. Add before G12. |
---
## 7. Phase Plan
| Phase | Features | Estimate | Depends On |
|-------|---------|----------|------------|
| **Phase 0 (done)** | Schema, services, admin UI, domain provisioning, bot lifecycle | — | — |
| **Phase 1** | G1 (upgrade/billing), G2 (theming), G3 (seller shop dashboard), G9 (multi-admin) | 34 weeks | Phase 0 |
| **Phase 2** | G4 (storefront), G5 (user base / federated login), G8 (bot admin commands), G14 (CRM light) | 46 weeks | Phase 1 |
| **Phase 3** | G6 (custom email), G7 (invoicing), G10 (monitoring), G12 (backup) | 34 weeks | Phase 2 |
| **Phase 4** | G11 (schema isolation), G13 (external payment provider) | 46 weeks | Phase 3 |
---
## 8. Out of Scope (This PRD)
- Recurring billing / Stripe subscription management
- `isolated` user namespace (per-tenant user DB)
- `database`-mode isolation (separate Postgres per tenant)
- Multi-language catalog items (seller-managed translation)
- Advanced analytics / BI dashboards for shop owners
- Shop-to-shop referral / affiliate programs
- White-label mobile apps (iOS/Android native)
- Automated A/B testing on storefronts
---
## 9. Open Questions
| # | Question | Owner | Due |
|---|---------|-------|-----|
| OQ1 | PDF library choice: `pdfkit` vs `puppeteer`? | Engineering | Phase 3 kickoff |
| OQ2 | Object storage provider: Minio on-premise vs. external S3? | Ops | Phase 1 kickoff |
| OQ3 | First-version upgrade price points for plan tiers? | Product | Phase 1 kickoff |
| OQ4 | Should `federated` login show "Powered by Amanat" or hide it entirely? | Legal / Product | Phase 2 kickoff |
| OQ5 | Stripe vs. Zarinpal priority for external payment provider? | Business | Phase 4 kickoff |
| OQ6 | Max allowed shops per `business` tier — unlimited or a number (e.g., 20)? | Product | Phase 1 kickoff |
| OQ7 | Should backup notification be opt-out or opt-in for shop owners? | Product | Phase 3 kickoff |