docs: sync vault with codebase state (2026-06-12)

- Update backend, frontend, scanner, deployment, amanat-assist service docs
- Update System Overview, Scanner Architecture, Telegram Mini App flow
- Update 10 - Services/README.md
- Add Tenant data model, Tenant API reference, Tenant Storefront Flow
- Add Multi-Shop Branch Project Scan (2026-06-10)
- Add tenant.md service doc
- Append activity log entry
- Reflects archived/search/stats route fix and new E2E test suite

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-06-12 11:42:18 +04:00
parent 18073afb52
commit e52ffce48a
18 changed files with 2619 additions and 1102 deletions

View File

@@ -6,8 +6,8 @@ related_apis: ["POST /api/auth/telegram", "[[Auth API]]"]
task: "5.4"
---
> **Last updated:** 2026-06-08
> **Status:** IN PROGRESS — Task 5.4 (dependencies: 5.1 auth infra, 5.2 Telegram sign-in endpoint)
> **Last updated:** 2026-06-12
> **Status:** LARGELY COMPLETE — Task 5.4 core implementation done; open items are `startapp` deep-link auto-routing, backend Socket.IO room scoping, archived-chat surfacing, review-prompt integration, and cross-platform QA.
> **Frontend branch:** `integrate-main-into-development` · v2.8.94+
> **Entry point:** `src/sections/telegram/` · route `/telegram`
@@ -68,15 +68,26 @@ The shell is a **single-page, no-router** design: all navigation (tabs, overlays
## 2. Launch Points
| Entry | Mechanism | `startapp` context |
|---|---|---|
| Bot profile | User opens bot → taps "Open App" | none |
| Menu button | Pinned button in any chat with the bot | none |
| Inline button | Bot sends a card with an embedded button | `req_<requestId>` |
| Direct deep link | `https://t.me/AmanehBot/app?startapp=req_<id>` | `req_<requestId>` |
| Web fallback | Browser at `/telegram` | none (unsupported state) |
| Entry | Mechanism | `startapp` context | Result |
|---|---|---|---|
| Bot profile | User opens bot → taps "Open App" | none | Shell loads at Home tab |
| Menu button | Pinned button in any chat with the bot | none | Shell loads at Home tab |
| Inline button | Bot sends a message card with an embedded button | `req_<requestId>` | Shell loads; request deep-link (see below) |
| Direct deep link | `https://t.me/AmanehBot/app?startapp=req_<id>` | `req_<requestId>` | Shell loads; request deep-link (see below) |
| Web fallback | Browser at `/telegram` (no Telegram SDK) | none | `TelegramUnsupportedState` — "Open in Telegram" prompt + web dashboard link |
`startapp` / `tgWebAppStartParam` is read from either the WebApp SDK (`window.Telegram.WebApp`) or from URL query/hash params (for older Telegram clients that append them directly).
### 2.1 startapp Context Parsing
`startapp` / `tgWebAppStartParam` is read from two sources in priority order:
1. `window.Telegram.WebApp.initDataUnsafe.start_param` — primary source when the SDK is injected.
2. URL query/hash params (`tgWebAppStartParam`) — fallback for older Telegram clients that append params directly to the URL.
Both are normalised into `context.startParam` by `getTelegramContext()` in `src/utils/telegram-webapp.ts`.
### 2.2 startapp Deep-Link Routing (Partial)
When `context.startParam` matches `req_<requestId>`, the intent is to auto-open `TelegramRequestDetailView` for that request on first render. **This routing is not yet wired**`startParam` is parsed and available in context but the shell does not yet act on it. This is open item #1 in section 16.
---
@@ -172,9 +183,18 @@ When `initData` is absent (accessed via a path that skips Telegram context), onl
### 6.3 Backend Endpoint
`POST /api/auth/telegram` — expects `{ initData: string }`. Backend verifies the HMAC using the Telegram bot token, extracts `user` from the payload, upserts a `User` record (`telegramId`, `telegramVerified: true`), and issues a JWT + refresh token. Returns `{ token, refreshToken, isNewUser }`.
`POST /api/auth/telegram` — expects `{ initData: string }`.
Registered at `authRoutes.ts` line 24: `router.post("/telegram", ctrl.telegramAuth.bind(ctrl))` — public route, no auth middleware.
**Verification steps (backend):**
1. Parse the `initData` query string into key-value pairs.
2. Extract `hash` from the pairs; remove it from the set.
3. Build the data-check string: sort remaining pairs alphabetically, join as `key=value\n`.
4. Compute `HMAC-SHA256(data_check_string, HMAC-SHA256("WebAppData", TELEGRAM_BOT_TOKEN))`.
5. Compare computed hash with the extracted `hash` — reject with 401 on mismatch.
6. Parse `user` JSON from `initDataUnsafe`; upsert `User` record with `telegramId`, `telegramVerified: true`.
7. Issue JWT + refresh token. Return `{ token, refreshToken, isNewUser }`.
Registered at `authRoutes.ts` line 24: `router.post("/telegram", ctrl.telegramAuth.bind(ctrl))` — public route, no auth middleware required (HMAC is the authentication proof).
### 6.4 Session Linking (Telegram ↔ Amaneh Account)
@@ -415,7 +435,16 @@ The shell has **five bottom tabs** rendered by `TelegramTabBar`:
- Fetches user points via `use-telegram-points.ts`.
- Shows points balance and transaction history.
### 9.16 Notifications Overlay
### 9.16 Dispute Surface
The Mini App does not yet have a dedicated dispute-filing view. Dispute access is handled via two escape hatches:
- **Request detail "View full details" link** (`openTelegramExternalLink`) — opens the web dashboard request detail page where dispute filing is available.
- **Support chat** — buyer or seller can reach a support agent from the Account tab or the Home tab quick-action cards; the support agent can escalate to a formal dispute.
A native in-shell dispute flow (matching the web dashboard `DisputeView`) is planned but not yet implemented. This is a known gap for the Task 5.4 feature surface.
### 9.17 Notifications Overlay
- `TelegramNotificationsView` is rendered as `overlayScreen = 'notifications'`.
- Fetches via `useTelegramNotifications``getNotifications(userId, 1, 50)``GET /api/notifications?userId=...&page=1&limit=50`.
@@ -453,6 +482,8 @@ The shell has **five bottom tabs** rendered by `TelegramTabBar`:
Cart operations (add/remove/quantity) are **pure localStorage** — no API calls until checkout.
Dispute endpoints (`POST /api/disputes`, `GET /api/disputes/:id`) are not yet called from the Mini App shell — dispute access is delegated to the web dashboard via `openTelegramExternalLink`.
---
## 11. Bilingual Support (EN / FA)
@@ -730,15 +761,19 @@ src/
| amanat-assist integration | Done | "Open Assist" CTA in Home + New Request; window.location hand-off with access_token |
| Deep link `startapp` routing | Partial | `startParam` parsed; auto-navigation to specific request not yet wired |
| Backend room-scoped Socket.IO | Partial | Global socket broadcast fixed client-side (v2.8.4); server-side room scoping is a follow-up |
| Dispute filing (in-shell) | Not started | Escape hatch via web dashboard link + support chat; native view planned |
| Review prompt integration | Partial | `TelegramReviewPrompt` component exists; trigger point (post-payment/delivery) not yet wired |
| Archived chats | Partial | `TelegramArchivedChatsView` exists; not yet surfaced in navigation |
| Client matrix QA (iOS/Android/Desktop) | Pending | Needs cross-platform testing pass |
### Open Items
1. **`startapp` deep link routing:** if `context.startParam` matches `req_<id>`, auto-open `TelegramRequestDetailView` on first render.
2. **Backend room-scoped Socket.IO:** server-side scoping for real-time chat updates (follow-up from client-side fix in v2.8.4).
3. **Client matrix QA:** iOS Telegram, Android Telegram, Telegram Desktop, and web clients all need a full feature pass.
4. **Review prompt:** `TelegramReviewPrompt` component exists but integration point (post-payment / post-delivery) is TBD.
5. **Archived chats:** `TelegramArchivedChatsView` exists but is not yet surfaced in the navigation.
1. **`startapp` deep link routing:** if `context.startParam` matches `req_<id>`, auto-open `TelegramRequestDetailView` on first render. `startParam` is already parsed and available in context; the shell needs a one-time effect on mount to act on it.
2. **Backend room-scoped Socket.IO:** server-side scoping for real-time chat and notification events (follow-up from client-side provider gate in v2.8.4 that fixed global cart-wipe).
3. **In-shell dispute filing:** add `TelegramDisputeView` matching the web dashboard dispute surface; currently only accessible via `openTelegramExternalLink` escape hatch.
4. **Review prompt:** wire `TelegramReviewPrompt` to trigger after payment confirmation or delivery acknowledgement.
5. **Archived chats:** surface `TelegramArchivedChatsView` from `TelegramChatView` (e.g. an "Archived" row at the bottom of the conversation list).
6. **Client matrix QA:** iOS Telegram, Android Telegram, Telegram Desktop, and web clients all need a full feature pass with particular attention to safe-area insets and BackButton behaviour on each platform.
---

View File

@@ -0,0 +1,215 @@
---
title: Tenant Storefront Flow
tags: [flow, tenant, white-label, storefront, multi-tenant]
---
# Tenant Storefront Flow
> **Last updated:** 2026-06-10 — current `feature/white-label-shops` scan.
> Related: [[Tenant]], [[Tenant API]], [[PRD - Seller-Owned White-Label Shops and Bots]]
Describes how a merchant tenant is created, approved, and how buyers land on a tenant storefront.
---
## 1. Tenant onboarding (operator-assisted, Phase 1)
```mermaid
sequenceDiagram
actor Seller
actor Operator
participant API as Backend /api/tenants
participant DB as PostgreSQL
Seller->>API: POST /api/tenants { slug, displayName, brand }
API->>DB: INSERT tenants (status=pending)
API->>DB: INSERT tenant_user_roles (role=owner)
API->>DB: INSERT tenant_payment_policies (default amn_escrow)
API-->>Seller: 201 { tenant, status: "pending" }
Note over Seller,Operator: Operator reviews in admin panel
Operator->>API: POST /api/tenants/:id/activate
API->>DB: UPDATE tenants SET status='active'
API-->>Operator: 200 { tenant, status: "active" }
```
Tenants start as `pending` and are not publicly accessible until a platform admin activates them. This prevents self-provisioning of white-label storefronts.
---
## 2. Domain registration and provisioning
Tenants are accessible at `<slug>.amn.gg` automatically once active. Custom domains are now implemented through DNS verification plus dynamic Caddy Admin API routes in the multi-stack.
```mermaid
sequenceDiagram
actor Seller
participant API as Backend
participant DNS as Seller DNS
participant Caddy as infra-caddy
participant DB as PostgreSQL
Seller->>API: POST /api/tenants/:id/domains { hostname: "shop.example.com" }
API->>DB: INSERT tenant_domains status=pending tlsStatus=pending
API-->>Seller: 201 { domain, status: "pending", verificationToken }
Seller->>DNS: Add CNAME shop.example.com -> multi.amn.gg
Seller->>API: POST /api/tenants/:id/domains/:domainId/verify
API->>DNS: resolve A/CNAME
DNS-->>API: hostname points to configured ingress
API->>Caddy: add route for hostname
API->>DB: UPDATE status=active, tlsStatus=pending
API-->>Seller: 200 { dnsVerified: true }
Seller->>API: POST /api/tenants/:id/domains/:domainId/tls-check
API->>Caddy: HTTPS probe
API->>DB: UPDATE tlsStatus=issued | pending | failed
```
The background poller also runs `verifyAndProvision()` for pending domains and re-checks active domains whose TLS status is still pending. On backend startup, `syncActiveDomains()` replays active domain routes into Caddy because API-injected routes are not the source of truth.
---
## 3. Buyer landing — storefront bootstrap
The frontend fetches `/api/storefront/bootstrap` on every page load. The tenant is resolved entirely server-side from the `Host` header — the browser supplies no tenant hint.
```mermaid
sequenceDiagram
actor Buyer
participant FE as Frontend (TenantProvider)
participant API as GET /api/storefront/bootstrap
participant MW as tenantResolutionMiddleware
participant DB as PostgreSQL
Buyer->>FE: Opens shop.example.com (or seller.amn.gg)
FE->>API: GET /api/storefront/bootstrap
Note right of API: Host: shop.example.com
API->>MW: tenantResolutionMiddleware
MW->>DB: SELECT * FROM tenant_domains WHERE hostname='shop.example.com' AND status='active'
DB-->>MW: domain row
MW->>DB: SELECT * FROM tenants WHERE id=domain.tenantId AND status='active'
DB-->>MW: tenant row
MW-->>API: req.tenant = tenant
API->>DB: SELECT * FROM tenant_payment_policies WHERE tenant_id=...
DB-->>API: policy row
API-->>FE: 200 { tenantId, slug, brand, features, paymentRails, localeDefaults }
FE->>FE: TenantProvider stores bootstrap
FE->>FE: useTenantTheme() derives CSS vars from brand.primaryColor
FE-->>Buyer: Branded storefront renders
```
**Fallback:** If `GET /api/storefront/bootstrap` returns 404 (no tenant for this host), `TenantProvider` uses `AMANAT_DEFAULTS` with `isAmanatDefault: true`. The frontend renders unchanged Amanat branding.
---
## 4. Tenant resolution paths
Three resolution paths are supported simultaneously:
| Host pattern | Example | Resolution method |
| --- | --- | --- |
| `<slug>.amn.gg` | `myshop.amn.gg` | Slug extracted from subdomain label → `findBySlug` |
| Custom CNAME | `shop.example.com` | `findByHostname``findById` |
| Preview (platform only) | `amn.gg/t/:slug/bootstrap` | Slug from URL param, host must be `amn.gg` / `localhost` |
```mermaid
flowchart TD
A[HTTP Request] --> B{Is host platform base?\namn.gg / localhost}
B -- yes + slug param --> C[resolveTenantBySlug\npreviewOnly=true]
B -- yes, no slug --> D[req.tenant = undefined\nAmanat default]
B -- no --> E{Ends with .amn.gg?}
E -- yes, single label --> F[resolveTenantByHost\nfindBySlug]
E -- no --> G[resolveTenantByHost\nfindByHostname]
C --> H{Found?}
F --> H
G --> H
H -- yes --> I[req.tenant = TenantRecord]
H -- no --> D
I --> J[Route handler]
D --> J
```
---
## 5. Telegram bot registration and claim
```mermaid
sequenceDiagram
actor Developer
participant API as POST /api/tenants/:id/telegram/bot
participant BotSvc as tenantBotService
participant TG as Telegram Bot API
participant DB as PostgreSQL
Developer->>API: { botToken, username?, miniAppUrl? }
Note right of Developer: botToken is write-only
API->>BotSvc: registerBot(tenantId, { botToken, username?, miniAppUrl? })
BotSvc->>TG: getMe when username omitted
BotSvc->>BotSvc: AES-256-GCM encrypt(botToken, TENANT_SECRET_KEY)
BotSvc->>BotSvc: generate webhookSecret + claimToken
BotSvc->>DB: INSERT tenant_bots (status=pending, encryptedToken, webhookSecret, claimToken)
BotSvc->>TG: setWebhook /api/telegram/tenant-webhook/:botId
API->>BotSvc: configureBotMenu(bot.id, shopUrl)
BotSvc->>TG: setChatMenuButton -> shopUrl/telegram/
BotSvc-->>API: public bot record with claimUrl
API-->>Developer: 201 { id, telegramBotId, username, status: "pending", claimUrl }
Developer->>TG: Open claimUrl and send /start <claimToken>
TG->>API: POST /api/telegram/tenant-webhook/:botId with secret header
API->>BotSvc: claimAdmin(botId, claimToken, telegramUserId)
BotSvc->>DB: UPDATE status=active, adminTelegramUserId
BotSvc->>TG: send confirmation message
```
---
## 6. Payment policy
Payment rails available to a tenant's buyers are controlled by `tenant_payment_policies`.
```mermaid
flowchart LR
PP[tenant_payment_policies] -->|allowedRails| R{Buyer checkout}
R -->|amn_escrow| E[Amanat escrow — full protection]
R -->|amn_direct| D[Amanat scanner — no escrow hold\nstrict buyer disclosure required]
R -->|external_provider| X[External processor — Amanat records evidence only]
R -->|manual_invoice| M[Operator / merchant confirms payment]
```
`buyerDisclosureMode = 'strict'` (default) mandates a prominent "not escrow protected" notice when `amn_direct` or external rails are used. The frontend reads `features.escrowCheckout` / `features.directCheckout` from the bootstrap payload to decide which checkout paths to expose.
---
## 7. Frontend context tree
```
<TenantProvider> ← fetches bootstrap, provides useTenant()
<ThemeProvider> ← existing MUI theme
<App>
useTenant() ← brand, features, paymentRails
useTenantTheme() ← primaryColor, cssVars (--tenant-primary)
```
`TenantProvider` wraps the application shell. All downstream components read tenant context via `useTenant()`. No tenant-specific props need to be threaded through the component tree.
---
## Phase roadmap
| Phase | What ships | Status |
| --- | --- | --- |
| 0 | Drizzle schema (6 tables), enums, repositories, tenant auth roles | ✅ `feature/white-label-shops` |
| 1 | Hosted subdomain (`seller.amn.gg`), tenant bootstrap endpoint, `TenantProvider`, admin tenant UI | ✅ `feature/white-label-shops` |
| 2 | Custom domain + DNS verification + Caddy route + TLS status checks | ✅ `feature/white-label-shops` |
| 3 | Tenant Telegram bot token storage, webhook registration, menu button, admin claim link | Partial — implemented for claim activation; multi-bot notification routing still planned |
| 4 | `amn_direct` payment rail + buyer disclosure | ⬜ Planned |
| 5 | Catalog / delivery / external payment adapters, billing events, stronger isolation | ⬜ Planned |
Related: [[Tenant]], [[Tenant API]], [[PRD - Seller-Owned White-Label Shops and Bots]], [[Escrow Flow]], [[Telegram Mini App]].