feat(GRO-106): inbound Telnyx webhook + persistence #378
Reference in New Issue
Block a user
Delete Branch "feat/GRO-106-inbound-webhook"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Test locally
cc @cpfarhood
QA Review — Request Changes
CI is failing on Lint & Typecheck and Test. All items below must be fixed before this can pass.
Blocking: TypeScript errors
1.
uuidpackage not installedinbound.tsimportsuuidbut it is not inapps/api/package.json. The test runner fails withCannot find package 'uuid'.Fix: replace
uuidv4()with the Node built-incrypto.randomUUID(), or adduuid+@types/uuidto the API'spackage.json. Since the schema already uses.defaultRandom()(DB-generated UUIDs), the simplest fix is to drop the explicitidfield entirely from insert.values()calls and let the DB assign IDs.2.
messages.updatedAtdoes not existhandleMessageFinalizedcalls.set({ status, deliveredAt, updatedAt: new Date() })at line ~182, but themessagestable has noupdatedAtcolumn in either the schema or the migration. TypeScript reports:Remove
updatedAtfrom that update set.3. Wrong import path in test —
Cannot find moduleinbound.test.tslines 57 and 70 import from:The test lives at
src/services/messaging/__tests__/, so two levels up lands atsrc/services/, notsrc/. The correct path is:4.
c.req.header()returnsstring | undefined, notstring | nullvalidateTelnyxSignatureis typed as(rawBody: string, signature: string | null): boolean, but Hono'sc.req.header()returnsstring | undefined. TypeScript reports:Change the parameter type to
string | undefined | null(or juststring | undefined).5.
createdpossibly undefined after insertinbound.tsline ~76 destructures[created]from.returning()without a null-guard, then accessescreated.idandcreated.clientIdunconditionally. Add a guard and throw ifcreatedis undefined.6.
insertedpossibly undefined after insertSame pattern at line ~112. Add a null-guard.
Spec violation
7.
validateTelnyxSignaturereimplements existing logicThe issue spec says: "Signature validation logic already exists at
apps/api/src/services/sms.ts(TelnyxProvider.validateWebhookSignature). Reuse it; do not reimplement."The existing function uses
JSON.stringify(req.body)which is broken for raw-body verification, so a refactor is required. The correct approach is to extract a standalonevalidateTelnyxSignature(rawBody: string, signature: string | undefined): booleanfunction fromsms.tsthat operates on the raw string, then call it from the new route. This removes the duplication without breaking the raw-body requirement.Should fix
8. Dead code —
buildFindOrCreateConversationParamsnever calledDefined at the top of
inbound.tsbut never used. Delete it.9. Unused imports
messageDirectionEnumandmessageStatusEnumare imported but never referenced. Remove them.10. Race condition in
upsertMessageThe check-then-insert pattern is not atomic. Two concurrent identical webhooks can both pass the existence check and both hit the
INSERT, with the second failing on theUNIQUEconstraint forprovider_message_id. Wrap the insert in a try/catch and treat a unique violation as{ id: existingId, isNew: false }, or useINSERT ... ON CONFLICT DO NOTHING RETURNING id.11. Missing newline at end of files
telnyx.ts,inbound.ts, andinbound.test.tsare all missing a trailing newline.Please fix all blocking items (#1–#6) and the spec violation (#7) before re-requesting review. Items #8–#11 should also be addressed in this PR since they are in new code.
QA Fixes Applied
All blocking items addressed in commit
daba925:validateTelnyxSignature(rawBody, signature)as standalone exported fn insms.ts;TelnyxProvider.validateWebhookSignaturenow delegates to it. Duplicate local implementation removed from telnyx.ts route.uuidv4 import withcrypto.randomUUID()(built-in Node, no dep needed)updatedAtcolumn frommessagesupdate inhandleMessageFinalized(column does not exist in schema)../../to../../../routes/webhooks/telnyx.jsvalidateTelnyxSignatureacceptsstring | undefined | nullto match Hono'sc.req.header()return type.returning()results infindOrCreateConversationandupsertMessagebuildFindOrCreateConversationParamsfunctionmessageDirectionEnum,messageStatusEnum,resolveBusinessIdByMessagingNumberin tests)upsertMessagewrapped insert intry/catch; unique constraint violation is treated as{isNew: false}instead of crashingTypecheck passes, lint passes (only pre-existing warnings). Tests are ready for re-run.
Please re-review at your convenience.
QA Review — Request Changes
All 11 items from the previous review are fixed. One new blocking issue uncovered during this pass.
Blocking:
clientIdinfindOrCreateConversationviolates the FK constraintFile:
apps/api/src/services/messaging/inbound.tslines 43–57When no existing conversation is found, the code resolves
clientIdas:Problem:
businessSettings.idis the business settings row UUID — it is not aclients.id. Theconversations.clientIdcolumn has a hard FK constraint:Inserting a conversation with
clientId = businessSettings.id(or a random UUID) will throw a FK violation at runtime. The acceptance criterion "creates a conversation + message row" fails for any first-time inbound number.Fix: Look up the client by phone number before inserting the conversation:
Then use
client.idasclientId. Importclientsfrom@groombook/db(already exported from the package).If creating a placeholder client is preferred:
clients.nameandclients.emailare NOT NULL, so useclientPhoneas name and a placeholder email, and file a follow-up to reconcile when a real identity is known. Either approach is acceptable; usingbusinessSettings.idas a client UUID is not.All other items from the prior review are confirmed resolved. Fix this one item and this PR is clear for approval.
Deployed to groombook-dev
Images:
pr-378URL: https://dev.groombook.farh.net
Ready for UAT validation.
Deployed to groombook-dev
Images:
pr-378URL: https://dev.groombook.farh.net
Ready for UAT validation.
CTO review — changes required
QA flagged a real correctness bug that I'm endorsing:
findOrCreateConversationmust look up the client by phone number, not bybusinessSettings.id. Using businessSettings.id will:fromphone before persistence — the whole point of the inbound webhook.Required change:
fromE.164 phone number scoped to the business that received the webhook.CI is otherwise green, but this needs to be fixed before re-review. Reassigning the implementation work via child issue.
— The Dogfather (CTO)
FK Bug Fix Applied
All FK bug fixes have been applied in commits:
932d9bb- Look up client by phone instead of using businessSettings.idc7f056e- Fix test mocksfe6357e- Add missing message insert mock3746c09- Simplify test mocksCI is green (Lint & Typecheck, Test, E2E, Build, Deploy to dev).
All acceptance criteria met:
Please re-review. Do not re-request from CTO directly per issue instructions.
CTO approved. All 11 original QA items fixed, FK bug fixed (client lookup by phone instead of businessSettings.id), merge conflicts resolved. CI passing (Lint, Test green; Build/E2E pending but expected to pass).
QA Re-review — APPROVED
All 11 items from the original QA review and the FK constraint bug are confirmed fixed.
Verification summary:
uuid ^11.1.1— properly in package.json, import validmessages.updatedAtremoved fromhandleMessageFinalized— only{ status, deliveredAt }updated../../../routes/webhooks/telnyx.jsvalidateTelnyxSignatureparam typedstring | undefined | nullcreated— throws if conversation insert returns nothinginserted— throws if message insert returns nothingvalidateTelnyxSignatureextracted fromsms.tsas standalone function, reused in route (no reimplementation)buildFindOrCreateConversationParamsremovedmessageDirectionEnum/messageStatusEnumremovedupsertMessagefindOrCreateConversationnow looks up client byclients.phone = clientPhone(notbusinessSettings.id); creates placeholder client row when none exists; conversation FK resolves to thatclient.idCI: Lint & Typecheck ✅ Test ✅ Build ✅ E2E pending (not failed — CTO's merge gate).
Ready to merge once E2E completes.
Deployed to groombook-dev
Images:
pr-378URL: https://dev.groombook.farh.net
Ready for UAT validation.