Invitations
Invitation codes, signed links and the activation funnel from share to first order.
Invitations let a delegate bring a prospect onto the reseller app and get credited for it (cahier §6.6, §12 — IVT). Prisma file: invitations.prisma.
One prospect has at most one live invitation; it carries an 8-character code (displayed AB12-CD34, link https://app.hoco.dz/i/AB12CD34) and a signed token for the install referrer. Each step of the funnel — shared, app installed, account created, phone verified, catalog viewed, first order — is an InvitationEvent; the invitation keeps the first time of each step as columns so the funnel report (admin.invitations.list) is a plain query. The funnel is monotonic: a later step back-fills missing earlier ones with inferred = true (lifecycles).
Tables
Invitation
A unique, secure invitation of one prospect by one delegate to install the reseller app (cahier §6.6, IVT-01…04), with the timestamp of every activation step. Table invitations · written by invitations (delegate.invitations.*, invitations.resolve/attribute, sign-up and order consumers).
| Field | Prisma type | Null | Default | Constraints / notes |
|---|---|---|---|---|
id | String @db.Uuid | — | uuid(7) | PK. |
code | String @db.Char(8) | — | — | Unique. 8 characters from an alphabet without ambiguous letters (no 0 O 1 I L), stored without the dash, displayed AB12-CD34; link https://app.hoco.dz/i/AB12CD34. |
tokenHash | String @db.Char(64) | — | — | Unique. SHA-256 (hex) of the signed token carried by the install referrer and app link. The token is HMAC(INVITE_SECRET, id), so the server can rebuild the link at any time and never stores the token itself. |
resellerId | String @db.Uuid | — | — | FK → Reseller · Restrict. |
delegateId | String @db.Uuid | — | — | FK → DelegateProfile · Restrict. The inviting delegate credited with the conversion (cahier §12). |
currentStep | InvitationStep | yes | — | Furthest step reached (the funnel status); null until first shared. |
lastChannel | ShareChannel | yes | — | — |
sentAt | DateTime | yes | — | First share (SENT). |
appInstalledAt | DateTime | yes | — | — |
accountCreatedAt | DateTime | yes | — | — |
phoneVerifiedAt | DateTime | yes | — | — |
catalogViewedAt | DateTime | yes | — | — |
firstOrderAt | DateTime | yes | — | — |
firstOrderId | String @db.Uuid | yes | — | Unique. FK → Order · Restrict. |
attributedUserId | String @db.Uuid | yes | — | FK → User · Restrict. Reseller account that signed up or claimed through this invitation. |
expiresAt | DateTime | — | — | Creation + invitations.validityDays (30). |
revokedAt | DateTime | yes | — | Set when superseded by a new invitation or revoked by the back-office. |
createdAt | DateTime | — | now() | — |
updatedAt | DateTime | — | @updatedAt | — |
- Relations:
reseller→ Reseller (resellerId,onDelete: Restrict) ·delegate→ DelegateProfile (delegateId,onDelete: Restrict) ·firstOrder→ Order (firstOrderId,onDelete: Restrict) ·attributedUser→ User (attributedUserId,onDelete: Restrict). - Referenced by: InvitationEvent.
invitationId. - Keys & indexes:
codeunique ·tokenHashunique ·firstOrderIdunique ·@@index([delegateId, createdAt])·@@index([resellerId, createdAt])·@@index([currentStep]). - Enums: InvitationStep, ShareChannel.
- History: Each step reached → InvitationEvent; the step columns are a cache of the first event of each step for funnel queries (
admin.invitations.list). - Derived, not stored: expired (invitation expirée) —
revokedAt IS NULL AND expiresAt < now(); QR payload and share texts — built fromcodeand the link at read time. invitations.resolveexposes only the store name, the delegate's first name and validity — never phone numbers (endpoints.json).
Constraints and indexes Prisma cannot express (raw SQL in the migration):
-- at most one live invitation per reseller (create returns it while valid)
CREATE UNIQUE INDEX invitations_one_live ON invitations (reseller_id) WHERE revoked_at IS NULL;Example row:
{
"id": "01a19ba5-d960-7d8f-ac9f-8d80d4f8f101",
"code": "K7QM4XPA",
"tokenHash": "5b1c0e4f9a7d2c3b8e6f0a1d4c7b9e2f5a8d1c4b7e0f3a6d9c2b5e8f1a4d7c0b",
"resellerId": "01a19ba2-2fe0-7300-a148-73d1a4ef38c0",
"delegateId": "01a1a024-7d34-7424-adb6-099ecaff5095",
"currentStep": "FIRST_ORDER",
"lastChannel": "WHATSAPP",
"sentAt": "2026-11-02T09:46:10.000Z",
"appInstalledAt": "2026-11-03T08:02:00.000Z",
"accountCreatedAt": "2026-11-03T08:12:00.000Z",
"phoneVerifiedAt": "2026-11-03T08:12:00.000Z",
"catalogViewedAt": "2026-11-03T08:13:30.000Z",
"firstOrderAt": "2026-11-05T15:20:00.000Z",
"firstOrderId": "01a1ac4b-a100-74df-8237-614783cfeacd",
"attributedUserId": "01a1a077-1080-7aed-87e0-11ae957ae11e",
"expiresAt": "2026-12-02T09:45:00.000Z",
"revokedAt": null,
"createdAt": "2026-11-02T09:45:00.000Z",
"updatedAt": "2026-11-05T15:20:01.000Z"
}InvitationEvent
Append-only activation funnel: each share and each step reached (IVT-03). Table invitation_events · written by invitations (only writer).
| Field | Prisma type | Null | Default | Constraints / notes |
|---|---|---|---|---|
id | String @db.Uuid | — | uuid(7) | PK. |
invitationId | String @db.Uuid | — | — | FK → Invitation · Cascade. |
step | InvitationStep | — | — | — |
channel | ShareChannel | yes | — | For SENT rows. |
source | String @db.VarChar(24) | yes | — | Attribution source: INSTALL_REFERRER, APP_LINK, CODE_ENTERED, DELEGATE, SYSTEM. |
inferred | Boolean | — | false | Back-filled because a later step implies it (monotonic funnel, lifecycles page). |
deviceId | String @db.VarChar(64) | yes | — | X-Device-Id of the first app open. |
userId | String @db.Uuid | yes | — | FK → User · Restrict. Account involved from ACCOUNT_CREATED on. |
metadata | Json | yes | — | Raw attribution data (referrer string, app version). |
occurredAt | DateTime | — | — | — |
createdAt | DateTime | — | now() | Server receipt time. |
- Relations:
invitation→ Invitation (invitationId,onDelete: Cascade) ·user→ User (userId,onDelete: Restrict). - Keys & indexes:
@@index([invitationId, occurredAt])·@@index([step, occurredAt]). - Enums: InvitationStep, ShareChannel.
Constraints and indexes Prisma cannot express (raw SQL in the migration):
-- each funnel step once per invitation; SENT repeats for every share,
-- APP_INSTALLED is kept once per device (attribution precedence of sign-up)
CREATE UNIQUE INDEX invitation_events_step_once ON invitation_events (invitation_id, step) WHERE step NOT IN ('SENT', 'APP_INSTALLED');
CREATE UNIQUE INDEX invitation_events_install_device ON invitation_events (invitation_id, device_id) WHERE step = 'APP_INSTALLED';Example row:
{
"id": "01a1a06d-eca8-70e2-af2b-207703bf5eb2",
"invitationId": "01a19ba5-d960-7d8f-ac9f-8d80d4f8f101",
"step": "APP_INSTALLED",
"channel": null,
"source": "INSTALL_REFERRER",
"inferred": false,
"deviceId": "a3f1c9e2-5b7d-4e10-9c3a-77d2e1f0b8c4",
"userId": null,
"metadata": {
"referrer": "utm_source=hoco_invite&inv=…",
"appVersion": "1.0.0"
},
"occurredAt": "2026-11-03T08:02:00.000Z",
"createdAt": "2026-11-03T08:02:01.000Z"
}Field operations
Objectives, tours and their history, visits with GPS check-in, visit types and checklists, photos, reports and location pings.
Support and notifications
Tickets and after-sales claims with their items and resolution, regional WhatsApp support, announcements, the notification inbox, delivery log, templates and rules.