152 lines
5.8 KiB
Markdown
152 lines
5.8 KiB
Markdown
|
|
# Phase 8 Backend Contract — Customer Identity, VK ID, MAX/Telegram Messaging
|
|||
|
|
|
|||
|
|
Companion to [PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md](../PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md) Phase 8 (Sprints 8.1–8.5). Covers plan §2.9, §3.4, and all of §14 (the v3.1-only addition).
|
|||
|
|
|
|||
|
|
**Status: ready to build. Sprint order fixed by Sprint 0.1 decision: VK ID first, then everything else** ("do all after vk"). Sequence below follows that: identity core → VK ID → email/phone OTP → MAX/Telegram → Notification Orchestrator.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. Entities
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
interface Customer {
|
|||
|
|
id: string;
|
|||
|
|
marketplaceId: string; // or global identity strategy, tenant-configurable
|
|||
|
|
name?: string;
|
|||
|
|
email?: string;
|
|||
|
|
phone?: string;
|
|||
|
|
status: 'active' | 'suspended';
|
|||
|
|
createdAt: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface ExternalIdentity {
|
|||
|
|
customerId: string;
|
|||
|
|
provider: 'vk_id' | 'telegram' | 'max';
|
|||
|
|
providerUserId: string;
|
|||
|
|
verifiedAt: string;
|
|||
|
|
metadata: Record<string, unknown>;
|
|||
|
|
lastUsedAt: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface ContactMethod {
|
|||
|
|
customerId: string;
|
|||
|
|
type: 'email' | 'phone';
|
|||
|
|
value: string;
|
|||
|
|
verifiedAt?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface ContactChannel {
|
|||
|
|
customerId: string;
|
|||
|
|
provider: 'telegram' | 'vk' | 'max';
|
|||
|
|
chatId: string;
|
|||
|
|
verified: boolean;
|
|||
|
|
notificationsEnabled: boolean;
|
|||
|
|
deliveryEnabled: boolean;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface MessagingConsent {
|
|||
|
|
customerId: string;
|
|||
|
|
channel: string;
|
|||
|
|
purpose: 'marketing' | 'order_service_messages';
|
|||
|
|
grantedAt?: string;
|
|||
|
|
revokedAt?: string;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Telegram is demoted from sole identity to one `ExternalIdentity` provider among several — it must remain fully functional, just no longer the only path.
|
|||
|
|
|
|||
|
|
## 2. Sprint 8.2 — VK ID (build first)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
GET /api/identity/v1/vk/authorize -> redirects into VK's OAuth 2.1/PKCE flow
|
|||
|
|
POST /api/identity/v1/vk/callback { code, codeVerifier } -> completes OAuth **backend-side**,
|
|||
|
|
links ExternalIdentity, returns session
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Invariants:
|
|||
|
|
- OAuth completion happens entirely backend-side; the VK client secret never reaches the frontend.
|
|||
|
|
- A repeat login for the same `providerUserId` must resolve to the same `Customer`, never create a duplicate.
|
|||
|
|
- If `providerUserId` is already linked to a *different* `Customer` than the one currently authenticated (or none), this is an identity conflict — route to controlled resolution, never silently overwrite the existing binding (plan §14.3).
|
|||
|
|
|
|||
|
|
## 3. Sprint 8.3 — Email/phone OTP (after VK ID)
|
|||
|
|
|
|||
|
|
Implements the already-approved [email/phone login spec](../superpowers/specs/2026-08-15-email-phone-login-design.md). Per v3.1 §14, position this as **recovery/fallback** when a messenger channel is unavailable — not the primary login path. No new contract beyond that spec; this section exists only to fix its place in the build order relative to VK ID.
|
|||
|
|
|
|||
|
|
## 4. Sprint 8.4 — MAX + Telegram bot channels
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
interface BotConversationBinding {
|
|||
|
|
customerId: string;
|
|||
|
|
marketplaceId: string;
|
|||
|
|
provider: 'telegram' | 'max';
|
|||
|
|
chatId: string;
|
|||
|
|
state: string; // see §5 state machine
|
|||
|
|
orderId?: string;
|
|||
|
|
lastMessageAt: string;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
MAX linking flow (bot-assisted, one-time code):
|
|||
|
|
```
|
|||
|
|
POST /api/identity/v1/max/link-code -> { code, expiresAt } (TTL, single-use, bound to marketplace + browser session)
|
|||
|
|
```
|
|||
|
|
User opens the MAX bot, sends the code; a confirmed bot update on the backend calls:
|
|||
|
|
```
|
|||
|
|
POST /api/providers/v1/max/bot-webhook -- idempotent; a repeated update must not create a duplicate binding
|
|||
|
|
```
|
|||
|
|
which links the pending `Customer` session to the MAX `chatId`.
|
|||
|
|
|
|||
|
|
All three providers' incoming bot updates (VK, MAX, Telegram) normalize into one shape:
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
interface MessagingEvent {
|
|||
|
|
provider: 'telegram' | 'vk' | 'max';
|
|||
|
|
chatId: string;
|
|||
|
|
orderId?: string;
|
|||
|
|
text?: string;
|
|||
|
|
receivedAt: string;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Provider bot tokens/secrets never reach the frontend, ever — only the backend calls each provider's Bot API.
|
|||
|
|
|
|||
|
|
## 5. Sprint 8.5 — Notification Orchestrator + Delivery Conversation State Machine
|
|||
|
|
|
|||
|
|
On `order.paid` (Phase 2 event bus), the orchestrator picks the customer's chosen channel (captured at checkout, see [Phase 6](PHASE-6-CART-CHECKOUT-CONTRACT.md) and `OrderContactSnapshot` in [Phase 2](PHASE-2-ORDERS-NOTIFICATIONS-CONTRACT.md)) and drives:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
not_started -> awaiting_customer -> details_received -> manager_assigned/auto_confirmed -> shipment_planned -> completed
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
interface DeliveryDetailsSnapshot {
|
|||
|
|
orderId: string;
|
|||
|
|
city?: string;
|
|||
|
|
address?: string;
|
|||
|
|
recipientName?: string;
|
|||
|
|
phone?: string;
|
|||
|
|
timeWindow?: string;
|
|||
|
|
comment?: string;
|
|||
|
|
receivedAt: string;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Hard rules:
|
|||
|
|
- **The bot never changes financial statuses.** It can only write `DeliveryDetailsSnapshot` fields via a dedicated Delivery Service — no bot code path touches `Order.paymentStatus`/`orderStatus`.
|
|||
|
|
- The backoffice `Notification` (Phase 2 §6) fires unconditionally on `order.paid`, independent of whether the customer's messenger channel is reachable.
|
|||
|
|
- If the chosen channel is unavailable, log a `DeliveryAttempt` error (Phase 2 §6) and fall back per tenant-configured policy (e.g. email/SMS) — never block the order itself.
|
|||
|
|
- Follow-up messages are rate-limited per tenant policy; after the configured attempt limit, hand off to a human manager instead of continuing to message.
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
POST /api/providers/v1/{provider}/bot-webhook -- generic entrypoint for all three providers
|
|||
|
|
GET /api/admin/v2/orders/{orderId}/conversation -- message history + current state, for manager handoff
|
|||
|
|
POST /api/admin/v2/orders/{orderId}/conversation/handoff
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 6. What the frontend will start doing once this ships
|
|||
|
|
|
|||
|
|
- VK ID login button + OAuth redirect flow on storefront (primary social login).
|
|||
|
|
- MAX/Telegram linking UI (one-time code flow).
|
|||
|
|
- Checkout channel-choice step ("where should we send confirmation?") — VK / MAX / Telegram / email/SMS fallback.
|
|||
|
|
- Manager-facing conversation view (message history, current delivery state, accept handoff).
|