HOCO B2B
SRSDatabase design

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).

FieldPrisma typeNullDefaultConstraints / notes
idString @db.Uuid—uuid(7)PK.
codeString @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.
tokenHashString @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.
resellerIdString @db.Uuid——FK → Reseller · Restrict.
delegateIdString @db.Uuid——FK → DelegateProfile · Restrict. The inviting delegate credited with the conversion (cahier §12).
currentStepInvitationStepyes—Furthest step reached (the funnel status); null until first shared.
lastChannelShareChannelyes——
sentAtDateTimeyes—First share (SENT).
appInstalledAtDateTimeyes——
accountCreatedAtDateTimeyes——
phoneVerifiedAtDateTimeyes——
catalogViewedAtDateTimeyes——
firstOrderAtDateTimeyes——
firstOrderIdString @db.Uuidyes—Unique. FK → Order · Restrict.
attributedUserIdString @db.Uuidyes—FK → User · Restrict. Reseller account that signed up or claimed through this invitation.
expiresAtDateTime——Creation + invitations.validityDays (30).
revokedAtDateTimeyes—Set when superseded by a new invitation or revoked by the back-office.
createdAtDateTime—now()—
updatedAtDateTime—@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: code unique · tokenHash unique · firstOrderId unique · @@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 from code and the link at read time.
  • invitations.resolve exposes 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).

FieldPrisma typeNullDefaultConstraints / notes
idString @db.Uuid—uuid(7)PK.
invitationIdString @db.Uuid——FK → Invitation · Cascade.
stepInvitationStep———
channelShareChannelyes—For SENT rows.
sourceString @db.VarChar(24)yes—Attribution source: INSTALL_REFERRER, APP_LINK, CODE_ENTERED, DELEGATE, SYSTEM.
inferredBoolean—falseBack-filled because a later step implies it (monotonic funnel, lifecycles page).
deviceIdString @db.VarChar(64)yes—X-Device-Id of the first app open.
userIdString @db.Uuidyes—FK → User · Restrict. Account involved from ACCOUNT_CREATED on.
metadataJsonyes—Raw attribution data (referrer string, app version).
occurredAtDateTime———
createdAtDateTime—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"
}

On this page