Two root causes fixed:
1. VITE_API_URL was empty in .env.production, so Better-Auth's client
had no baseURL and could not correctly route the OAuth callback.
2. OAuth callbackURL was window.location.origin (root path), causing
Better-Auth to redirect to / instead of /admin after login — since
unauthenticated users at / are redirected to /login, this created a
loop that appeared as 'session not persisting.'
With VITE_API_URL=https://uat.groombook.dev and callbackURL=/admin,
the callback lands on /admin which renders the admin layout and
correctly establishes the session cookie.
Co-authored-by: Chris Farhood <chris@farhood.org>
Co-authored-by: Paperclip <noreply@paperclip.ing>
- Wrap conversation mocks in { items, nextCursor } response shape
(loadConversations reads json.items, bare array caused undefined.length crash)
- Guard scrollIntoView with ?. (jsdom doesn't implement it)
- Use getAllByText for text appearing in both preview and thread
Co-Authored-By: Paperclip <noreply@paperclip.ing>
1. **Remove duplicate staffReadAt** in `packages/db/src/schema.ts`
(TS1117 duplicate identifier — merge conflict artifact)
2. **Add count to db index exports** in `packages/db/src/index.ts`
(`count` from drizzle-orm was used in conversations.ts but not exported)
3. **Use dev version of conversations.ts** (no type errors, sql\`count(*)\`)
— PR branch version had incompatible type errors (staff.businessId,
count, optedOutAt fields not in schema)
4. **Remove duplicate conversationsRouter import** in `apps/api/src/index.ts`
All 289 tests pass, 0 lint errors.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Replace incorrect `apps/groombook/` path prefix with `apps/` in both
promote-to-uat.yml and promote-prod.yml. The infra repo structure uses
`apps/` directly without a `groombook/` level.
GRO-1248
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Extract Conversation interface fields to match API response:
replace lastMessageBody with lastMessage object, externalNumber with
clientPhone, remove staffReadAt
- loadConversations(): extract json.items array instead of raw array
- loadMessages(): extract json.items and reverse() for chronological order
- Update test mocks to use { items, nextCursor } response shape
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Adds staff conversations API (GET /api/conversations, GET /api/conversations/:id/messages, POST /api/conversations/:id/messages) with auth scoping and cross-tenant protection
- Adds staffReadAt column to conversations table for unread tracking
- Adds staff Messages page with two-column inbox layout (thread list + conversation view + composer)
- Adds Messages entry to staff sidebar navigation
- Includes tests for the MessagesPage component
Part of GRO-106 (SMS/MMS integration) Phase 1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add and() + lt() imports from @groombook/db
- Apply businessId to conversation WHERE clause for cross-tenant isolation
(GET /portal/conversation: clientId AND businessId both scoped)
- Fix cursor pagination: apply lt(messages.createdAt, cursorMsg.createdAt)
to the cursor WHERE clause so pages actually paginate
- Add UAT_PLAYBOOK.md §4.9.1 Communication tab test cases:
TC-APP-4.9.6 message history with conversation
TC-APP-4.9.7 empty state (no conversation yet)
TC-APP-4.9.8 composer disabled with tooltip
TC-APP-4.9.9 cross-tenant isolation
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Added GET /portal/conversation and GET /portal/conversation/messages endpoints
- Created Communication.api.ts with typed fetchers and React hooks
- Rewired Communication.tsx to use real API, removed mock data
- Added composer-disabled bar with "Reply from your phone" tooltip
- Added conversation route tests to portal.test.ts
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add impersonationAuditLogs table mock and db.insert() method to the
@groombook/db mock in portal.test.ts to resolve "No 'impersonationAuditLogs'
export is defined" errors. The portalAudit middleware calls db.insert()
on every request, which was missing from the mock.
Passes all 26 portal tests.
The telnyx webhook handler at /api/webhooks/telnyx/messaging was
returning 401 for all requests including those with valid signatures.
This was caused by the authMiddleware being applied to all /api/*
routes via api.use("*", authMiddleware) after the webhook route was
registered at the app level.
authMiddleware already skips /api/auth/ paths; adding the same skip
for /api/webhooks/* fixes the issue — webhook endpoints use their own
signature validation and do not require Better-Auth session auth.
Root cause: authMiddleware was applied to webhook routes that were
registered at the app level before the api sub-app middleware, but
the skip condition only covered /api/auth/, not /api/webhooks/.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
0031_steady_veda has no corresponding SQL file — caused Drizzle migration
runner to exit 1 in E2E. Renumber 0032_staff_read_at to idx 31.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add `staffReadAt` column to conversations table schema
- Add migration 0032_staff_read_at.sql for the new column
- Create /api/conversations router with GET / (list), GET /:id/messages (paginated), POST /:id/messages (send)
- Mark conversations as read (staffReadAt = NOW()) when staff fetches messages
- Return 409 when client has opted out of SMS
- 404 on cross-tenant access
- Add conversations.test.ts covering all 5 acceptance criteria
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add custom rate limit rules for /sign-in/social, /sign-in/email, and /sign-up/email
- Override default Better Auth limits (3 req/10s) with more permissive limits
- Apply rules to both placeholder and real auth configs
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* 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
* 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
* 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
* 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
* 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
* 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: groombook-engineer[bot] <269742240+groombook-engineer[bot]@users.noreply.github.com>
Co-authored-by: Chris Farhood <chris@farhood.org>
Co-authored-by: Paperclip <noreply@paperclip.ing>
* 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>
* docs(GRO-106): 10DLC pilot registration runbook
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(GRO-106): address QA review feedback
- Change business_vertical from FINANCE_INSURANCE_BANKING to PROFESSIONAL_SERVICES
- Fix broken internal issue links (GRO-106, GRO-981) to plain text
- Add owner stamp alongside last-updated date
- Fix phone placeholder in SQL and API example to use +1XXXXXXXXXX
- Add trailing newline to both runbook files
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
Co-authored-by: Chris Farhood <chris@farhood.org>
Co-authored-by: Paperclip <noreply@paperclip.ing>