Glossary & Data Model
Terms
Service window — The pre-existing 24-hour window that opens each time a contact sends a message; the clinic may reply with a free-form message (not a template) during it. Tracked implicitly via OnlineConversation.lastMessage.
Free window — New in this PR. A window during which a reply costs nothing, tracked explicitly via OnlineConversation.freeUntil. Opened for 24 hours the moment an ad-referred contact writes in, and extended to 72 hours if the clinic answers within that first day.
Ad referral / click-to-WhatsApp ad — A contact who reached the clinic's WhatsApp by clicking a Meta ad configured to open a WhatsApp chat. Detected by the presence of referral.ctwa_clid on the inbound webhook payload. Tracked via OnlineConversation.adReferralAt.
Message category — What WhatsApp bills a given outgoing message as: SERVICE (free-form reply), UTILITY (transactional template, e.g. an appointment reminder), MARKETING (promotional template), or AUTHENTICATION (OTP/verification template). Stored per-message on ConversationMessage.category.
Free / Paid — Whether a specific outgoing message happened to be sent inside an active free window (ConversationMessage.free = true) or not (false). null means the message predates this feature and was never classified.
Unknown — The UI's label for messages with free: null (cost report) or category: null (category breakdown) — always because the message was sent before this feature shipped, never a live/current state.
Daily paid message limit — A per-company, per-sender-kind (bot or user) cap on how many paid replies may be sent in a rolling ~24-hour window, configured via WAConfig.maxBotPaidMessages / maxUserPaidMessages.
VIEW_WA / VIEW_CREATED_WA — The two existing permissions that gate seeing WhatsApp conversations at all; also gate the new cost report and analytics (no new, narrower permission was introduced for cost data).
Data model additions
OnlineConversation
| Field | Type | Meaning |
|---|---|---|
adReferralAt | DateTime? | When an ad-click-originated message arrived. Cleared to null once the free window it can open actually opens. |
freeUntil | DateTime? | Timestamp until which replies to this contact are free. |
ConversationMessage
| Field | Type | Meaning |
|---|---|---|
free | Boolean? | Whether this specific outgoing message was free when sent. null = predates this feature. |
category | WhatsAppMessageCategory? | What this message is billed as. null = predates this feature. |
WAConfig
| Field | Type | Meaning |
|---|---|---|
maxBotPaidMessages | Int? | Daily cap on paid replies sent by the automated bot. null = no cap. |
maxUserPaidMessages | Int? | Daily cap on paid replies sent by clinic staff. null = no cap. |
New enum: WhatsAppMessageCategory
SERVICE | UTILITY | MARKETING | AUTHENTICATION
Defined in packages/prisma/schema.prisma and mirrored in packages/server/src/enums.graphql. Distinct from the pre-existing TemplateCategory enum (UTILITY | MARKETING), which classifies a template, not an individual sent message — WhatsAppMessageCategory is the superset used for the per-message cost record, adding SERVICE (any free-form reply) and AUTHENTICATION.
New GraphQL queries
conversationMessagesInfo(rangeDate: [DateTime!]!, filters: Filter, conversation: ID): [TypeSum!]!
conversationMessagesReport(orderBy: String!, skip: Int!, take: Int!, rangeDate: [DateTime!]!, filters: Filter): [ConversationMessage]!
TypeSum (an existing shared aggregation type) gained a free: Boolean field alongside its pre-existing type: String.
New GraphQL mutation arguments
updateWAConfig(..., maxBotPaidMessages: Int, maxUserPaidMessages: Int).
Redis keys
whatsapp:phoneId:paid:{bot|user}:{companyId} — a per-company, per-sender-kind counter of paid messages sent in the current rolling window, with an 86400-second (24h) TTL that restarts from the first paid message of each window. See Daily Spending Limits.