On this page
What to testFor Quality
What to test
Conversation windows
- A conversation whose patient wrote < 24h ago and has no
freeUntil:
service window open, Paid shown, no free tag.
- A conversation whose patient wrote > 24h ago and has no
freeUntil:
conversation shows expired / not repliable.
- A conversation with
freeUntilin the future but the patient's last
message was > 24h ago: still repliable (the free window alone keeps it open), tag shows Free.
- An ad-referred conversation (
adReferralAtset, < 24h old): tag shows
Free (ad), with the "reply to open 72 free hours" note.
- Reply to that ad-referred conversation within the 24h window: confirm
freeUntil extends to +72h from the reply, adReferralAt clears to null, and the tag switches from Free (ad) to plain Free.
- Let the 24h ad-reply window lapse without replying: confirm no free
window opens, freeUntil stays whatever it was (likely already expired), and the conversation reads as Paid from then on.
- Edge case worth targeted testing: a conversation already sitting in
an active 72-hour free window (from a prior ad reply) receives another inbound message that also carries ctwa_clid. Per packages/whatsapp-official/src/messages/messages.service.ts:211-225, the new ad fields are written unconditionally whenever fromAd is true — verify whether this shortens an already-longer freeUntil back down to +24h, and decide whether that's the intended behavior.
- Countdown display past 24 hours: with a 72-hour free window active,
confirm the header timer reads e.g. 70:15:02 instead of wrapping to 22:15:02 (regression target for the secondsToHms change in WhatsAppChatHeader.js).
Message tagging
- Send a manual reply from the app inside a paid window → confirm the
created ConversationMessage has free: false, category: SERVICE.
- Send a manual reply inside a free window →
free: true, `category:
SERVICE`.
- Trigger a cron/reminder template send → confirm
categorymatches the
template's own category, and is undefined/null for templates that predate this feature (no category set on the MessageTemplate itself).
- Confirm inbound (patient-sent) messages never get
free/category
populated, under any window state.
- A message sent before this migration ran: confirm it reports as
"Unknown" in both the table and the analytics boxes, on both mobile and web.
Cost reporting
- Company-wide Messages Cost report only includes outgoing messages —
seed a conversation with only inbound messages and confirm it produces zero rows.
- Per-conversation analytics (
conversationMessagesInfowith a
conversation argument) vs. company-wide (without) — confirm the company-wide one is further scoped to the requesting user's company.
- Clicking the Free/Paid analytics box on web toggles the table filter;
clicking it again clears the filter (toggleFilter in ConversationMessagesInfo.js) — verify the "Unknown" box is genuinely not clickable (notFilterable: true), since there's no free: null filter wired server-side.
- Category column filter in the web table includes an explicit
"null"
value option for uncategorized messages — verify it actually matches rows with category: null and not just an accidental string "null".
Daily spending limits
- Set
maxUserPaidMessagesto a small number (e.g. 2), send that many
paid replies, then attempt one more → expect the mutation to throw Daily Paid Messages Limit Reached[Client Error] and no message sent to Meta.
- Set a limit of exactly
0→ confirm all paid replies are blocked
immediately (tests the userLimit === 0 special case in both handleSendWhatsappMessage.js and messages.service.ts), while free replies still go through.
- Leave both limits blank → confirm no cap is ever enforced regardless of
volume.
- Free replies (inside a free window) should never decrement or check
either counter — send a burst of free replies and confirm the Redis paid-count key is untouched.
- Concurrency edge case: the check-then-increment sequence
(get → compare → later incr/setex) is not atomic. Fire several paid replies concurrently right at the limit boundary and check whether the count can overshoot the configured limit under load — this is a plausible race, not yet guarded with a Lua script or INCR-first pattern.
- Confirm the ~24h rolling reset: send a paid message, wait until its
Redis key's TTL should have expired, and confirm a fresh allowance is available rather than waiting for a calendar-day boundary.
- Bot limit reached: confirm the bot simply does not send (no error
surfaced anywhere) and that no ConversationMessage row is created for the suppressed attempt.
Cross-cutting
- Duplicated logic across 4 implementations. The windowing math exists
separately in packages/server, packages/whatsapp-official, and both frontend whatsappUtils.js files. Regression-test all four independently after any change to the windowing rules — a fix applied to only one will not propagate.
- i18n: all 9 locale files got new keys (
freeMessages,paidMessages,
freeAdMessages, adReplyWindow, category names, unknownMessage, freeWindow, etc.). Spot-check the RTL locales (Arabic, Kurdish Sorani ckb, Kurdish Kurmanji ku) for tag/badge overflow given the new, sometimes-longer strings (e.g. "Free (ad)" / "مجاني (إعلان)").
- Permissions: confirm a user with only
VIEW_CREATED_WA(not the
broader VIEW_WA) can still see the new cost report and analytics — the permission rule ORs the two, same as the existing conversation-view permission.