2d88f18f75
* feat(GRO-106): messaging schema + migrations - Add conversations, messages, message_attachments, message_consent_events tables - Add messagingChannelEnum, messageDirectionEnum, messageStatusEnum, messageConsentKindEnum - Extend business_settings with messagingPhoneNumber and telnyxMessagingProfileId columns - Add required indexes and unique constraints with cascade-on-delete FKs - Add migration 0030_messaging.sql Co-Authored-By: Paperclip <noreply@paperclip.ing> * fix(GRO-981): restore journal entries and add DESC to indexes - _journal.json: restore idx 28 (0028_sms_reminders), add idx 29 (0029_db_indexes_constraints), renumber 0030_messaging to idx 30 (was missing 0028 and 0029 entries — they were silently skipped) - schema.ts: add .desc() to conversations.lastMessageAt and messages.createdAt indexes per spec - 0030_messaging.sql: add DESC to both generated index statements Co-Authored-By: Paperclip <noreply@paperclip.ing> * feat(GRO-106): inbound Telnyx webhook + persistence - Add POST /api/webhooks/telnyx/messaging route with HMAC signature verification - Add services/messaging/inbound.ts: findOrCreateConversation, upsertMessage (idempotent on providerMessageId), delivery receipt handling - Register telnyxWebhooksRouter in index.ts (before auth middleware) - Add unit tests for signature validation, find-or-create, idempotent insert, delivery receipt Co-Authored-By: Paperclip <noreply@paperclip.ing> * fix(GRO-982): address all QA blocking failures - #7: Extract validateTelnyxSignature in sms.ts as standalone exported fn, reuse in TelnyxProvider.validateWebhookSignature and telnyx.ts route - #1: Replace uuid v4 import with crypto.randomUUID() (built-in, no dep) - #2: Remove updatedAt from messages update in handleMessageFinalized (no such column exists) - #3: Fix test import path ../../ → ../../../ for telnyx route import - #4: validateTelnyxSignature accepts string | undefined | null to match Hono c.req.header() return type - #5&6: Add null guards for .returning() results in findOrCreateConversation and upsertMessage - #8: Remove dead buildFindOrCreateConversationParams function - #9: Remove unused imports (messageDirectionEnum, messageStatusEnum, resolveBusinessIdByMessagingNumber in test) - #10: Wrap upsertMessage insert in try/catch; unique violation returns {isNew: false} instead of crashing - #11: Add EOF newlines to all modified files Co-Authored-By: Paperclip <noreply@paperclip.ing> * chore: add uuid dependency for messaging services * fix(GRO-982): address 5 test failures in inbound webhook - Fix signature route tests: use /messaging not full mount path - Fix handleMessageReceived mock order: business lookup first - Fix stale mock state: add full mockReset in handleMessageFinalized beforeEach - Fix delivery logic: set delivered for all message.finalized events - Deduplicate test that was accidentally added twice Co-Authored-By: Paperclip <noreply@paperclip.ing> * fix(GRO-982): look up or create client by phone before inserting conversation Fixes FK constraint violation where clientId was set to businessSettings.id or a random UUID. Now looks up clients.phone = clientPhone first; if no match, creates a placeholder client with phone as name and a placeholder email. * fix(GRO-982): address QA round 4 blocking failures - Fix URL in signature tests: use /messaging not full path - Reorder mocks: businessSettings first, then conversations, clients, messages - Add mockDb.mockReset in handleMessageFinalized beforeEach - Remove direction guard: set delivered for any message.finalized * fix(GRO-982): add missing message insert mock in handleMessageReceived test * fix(GRO-982): simplify test mocks to match actual code flow --------- Co-authored-by: Chris Farhood <chris@farhood.org> Co-authored-by: Paperclip <noreply@paperclip.ing>
60 lines
1.6 KiB
TypeScript
60 lines
1.6 KiB
TypeScript
import { Hono } from "hono";
|
|
import { validateTelnyxSignature } from "../../services/sms.js";
|
|
import {
|
|
handleMessageReceived,
|
|
handleMessageFinalized,
|
|
TelnyxMessageReceivedPayload,
|
|
} from "../../services/messaging/inbound.js";
|
|
|
|
export const telnyxWebhooksRouter = new Hono();
|
|
|
|
telnyxWebhooksRouter.post("/messaging", async (c) => {
|
|
const signature = c.req.header("telnyx-signature");
|
|
|
|
let rawBody: string;
|
|
try {
|
|
rawBody = await c.req.text();
|
|
} catch {
|
|
return c.json({ error: "Could not read body" }, 400);
|
|
}
|
|
|
|
if (!validateTelnyxSignature(rawBody, signature)) {
|
|
return c.json({ error: "Invalid signature" }, 401);
|
|
}
|
|
|
|
let payload: TelnyxMessageReceivedPayload;
|
|
try {
|
|
payload = JSON.parse(rawBody) as TelnyxMessageReceivedPayload;
|
|
} catch {
|
|
return c.json({ error: "Invalid JSON" }, 400);
|
|
}
|
|
|
|
const eventType = payload.data?.event_type;
|
|
if (!eventType) {
|
|
return c.json({ error: "Missing event_type" }, 400);
|
|
}
|
|
|
|
if (eventType === "message.received") {
|
|
try {
|
|
await handleMessageReceived(payload);
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
if (msg.startsWith("No business owns")) {
|
|
return c.json({ error: "Unknown messaging number" }, 404);
|
|
}
|
|
return c.json({ error: msg }, 500);
|
|
}
|
|
return c.json({ received: true });
|
|
}
|
|
|
|
if (eventType === "message.finalized") {
|
|
const result = await handleMessageFinalized(payload);
|
|
if (result) {
|
|
return c.json({ received: true, messageId: result.messageId, status: result.newStatus });
|
|
}
|
|
return c.json({ received: true, messageId: null });
|
|
}
|
|
|
|
return c.json({ received: true });
|
|
});
|