Commit Graph

15 Commits

Author SHA1 Message Date
Chris Farhood 7d3adeae98 fix(GRO-1368): remove unused getDb import from consent.ts
getDb was imported but never used — db is passed as a parameter to
handleConsentKeyword. This was the primary TypeScript/lint error
flagged by QA.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-21 19:49:33 +00:00
Chris Farhood 1fbe670751 feat(GRO-106): STOP/HELP compliance + consent log
- Add detectKeyword() and handleConsentKeyword() in consent.ts
- Wire keyword detection into handleMessageReceived() in inbound.ts
- Add 24-unit test suite for consent.ts covering all keywords,
  case insensitivity, whitespace tolerance, idempotency, and
  help keyword state preservation

Fixes from QA review:
- Use getDb() instead of non-existent db export; import Db type
- Destructure clientId from findOrCreateConversation result
- Rename staffId → sentByStaffId in sendMessage call
- Remove messagingHelpReply query (column not yet in schema)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-21 19:49:33 +00:00
Chris Farhood 90af76f222 feat(appointments): cascading delay prevention for appointment overruns
When a PATCH /appointments/:id extends endTime beyond the original, detect
and automatically shift downstream same-groomer appointments by the overrun
delta plus buffer. Only affects scheduled/confirmed appointments; appointments
that would shift outside business hours are flagged for manual review.

Clients receive email notification of rescheduled times.

GRO-1175: GRO-1162-G

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-16 16:08:05 +00:00
groombook-engineer[bot] 2d88f18f75 feat(GRO-106): inbound Telnyx webhook + persistence (#378)
* 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>
2026-05-11 00:43:40 +00:00
groombook-engineer[bot] 49dd698d22 feat(GRO-984): outbound SMS persistence
Outbound-only re-scoped slice. CI green. Reviewed by Lint Roller and CTO.
2026-05-04 17:55:47 +00:00
Test User 560d33edf8 fix(gro-609): fix two bugs found by CTO review
1. Refund stats now sum actual refund amounts from refunds table
   instead of incorrectly summing tip_cents from invoices table.

2. Stripe payment_intents.retrieve now expands payment_method
   so card.last4 is correctly available instead of null.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-19 01:55:32 +00:00
Test User 50e9e70935 feat(gro-609): add Stripe details to invoice modal and fix stats date filter
- Add GET /api/invoices/:id/stripe-details endpoint to fetch card last4 and
  payment status from Stripe
- Add getPaymentIntentDetails() to payment service
- Fix stats summary query to filter by startOfMonth
- Add cardLast4, paymentStatus, stripeRefundId transient fields to Invoice type
- Display Stripe details (card last4, payment status, refund status) in modal
- Add stripeRefundId and paymentFailureReason to Invoice schema (was missing in dev types)

Ref: GRO-609
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-19 01:02:49 +00:00
Flea Flicker 0abb79010d fix(GRO-639): replace sql ANY() with inArray for Drizzle compatibility
Use Drizzle's inArray() instead of raw sql template with = ANY()
to avoid PostgreSQL array binding issues in the reminder scheduler
bulk sent-check query.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-17 02:08:03 +00:00
Flea Flicker 66a6659ccd feat(GRO-600): extend reminder scheduler to send SMS alongside email
- Add SMS opt-in fields to clients schema (smsOptIn, smsConsentDate, smsOptOutDate, smsConsentText)
- Add channel column to reminderLogs with per-channel idempotency
- Create SMS service with Telnyx SDK integration and E.164 validation
- Update reminders service to conditionally send SMS to opted-in clients
- Add TCPA opt-out text to SMS reminders
- Graceful degradation: catch SMS errors without blocking email
- Fix: use clients.phone instead of non-existent clients.phoneE164
- Update clients route to expose SMS fields in API
- Add telnyx dependency to API package
- Create database migration 0028_sms_reminders

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-15 09:23:24 +00:00
groombook-cto[bot] c438f5772c feat(GRO-607): Stripe Elements payment UI replacing mock flow
* GRO-605: Stripe SDK integration + payment service

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* GRO-606: Add payment API endpoints (pay invoice, payment methods, refunds)

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* feat(GRO-597): Stripe payment backend — schema, service, API, webhooks

Consolidates GRO-605, GRO-606, GRO-608 into a single clean PR:
- GRO-605: Stripe SDK integration + payment service
- GRO-606: Payment API endpoints (pay invoice, payment methods, refunds)
- GRO-608: Stripe webhook handler

Migration consolidation:
- Single 0026_stripe_payment.sql migration adds stripeCustomerId to clients
  and stripe_payment_intent_id, stripe_refund_id, payment_failure_reason to invoices
- Removed duplicate 0027_stripe_identifiers.sql

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* GRO-607: Install Stripe frontend packages

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* GRO-607: Add /portal/config endpoint + rename date field

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* GRO-607: Replace mock payment flow with real Stripe Elements

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(GRO-607): Stripe Elements payment UI - lint/type fixes

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(GRO-607): remove unused eslint-disable directive in CustomerPortal

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(GRO-607): CTO review fixes — payment security and correctness

- Fix multi-invoice total calculation: use inArray() instead of eq()
  on single ID, sum all invoices not just first
- Add ownership check to payment method deletion: verify the payment
  method belongs to the authenticated Stripe customer before detaching
- Remove duplicate /config endpoint in portal.ts
- Fix webhook Stripe client: use getStripeClient() from payment service
  instead of constructing with WEBHOOK_SECRET
- Remove unnecessary body validator on /invoices/:id/pay route
- Export getStripeClient() for use by stripe-webhooks.ts
- Add inArray import to payment.ts

Co-Authored-By: Paperclip <noreply@paperclip.ing>

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-14 08:27:03 +00:00
Paperclip bc1f11a901 feat(GRO-565): Better Auth Phase 3 - password change, OIDC discovery, session cleanup, email verification
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-12 02:47:17 +00:00
groombook-engineer[bot] e3220af9ce fix(gro-38): prod/demo auth and API-based seed (#117)
Closes GRO-38. Adds POST /api/admin/seed (manager-only, gated by SEED_KNOWN_USERS_ONLY) and separates dev vs prod seeding paths. Reviewed and approved by CTO and QA.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-26 20:51:08 +00:00
Scrubs McBarkley f3923ddf54 fix(email): remove unused baseUrl variable in buildReminderEmail
Fixes ESLint @typescript-eslint/no-unused-vars error in CI.
baseUrl was declared but never used; confirmUrl/cancelUrl correctly
use apiUrl instead.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-24 17:25:09 +00:00
Scrubs McBarkley d1ab91adfa feat: appointment confirmation and cancellation (GH #98, GRO-153)
Add customer confirmation/cancellation flow for appointments:

- DB migration (0013): add confirmation_status, confirmed_at, cancelled_at,
  confirmation_token to appointments table with index on token column
- schema.ts + factories.ts + types: expose new columns and ConfirmationStatus type
- GET /api/book/confirm/:token — tokenized confirm via email link (redirects)
- GET /api/book/cancel/:token — tokenized cancel via email link, single-use token
- POST /api/appointments/:id/confirm — portal/staff confirm endpoint
- POST /api/appointments/:id/cancel — portal/staff cancel endpoint
- Reminder emails now include Confirm/Cancel CTA buttons with tokenized links
- Reminder service generates confirmation token if missing before sending
- Staff calendar shows confirmation status indicator on appointment cards
  and in the detail modal (confirmed ✓ / customer cancelled ✗)
- /booking/confirmed, /booking/cancelled, /booking/error redirect pages
- 23 new unit tests covering all new endpoints and edge cases

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-24 16:02:58 +00:00
groombook-paperclip[bot] addcefe70b feat: automated appointment reminders via email (GRO-23) (#29)
Implements Phase 1 of groombook/groombook#4 — automated email reminders
for upcoming appointments, with booking confirmations sent immediately
on creation.

- **DB**: new `reminder_logs` table tracks sent reminders per appointment
  (unique on appointmentId+type prevents duplicates); `clients` gains
  `email_opt_out` boolean (migration 0004_reminder_logs)
- **Email service**: `apps/api/src/services/email.ts` — nodemailer SMTP
  transport (disabled when SMTP_HOST is unset, so self-hosted installs
  without email config are unaffected); confirmation and reminder email
  templates included
- **Reminder scheduler**: `apps/api/src/services/reminders.ts` — node-cron
  job runs every minute, checks for appointments in the upcoming reminder
  windows (default: 24 h and 2 h), sends emails for opted-in clients,
  and records sends in reminder_logs (idempotent via ON CONFLICT DO NOTHING)
- **Confirmation email**: sent fire-and-forget after successful appointment
  creation (both single and recurring); never blocks the API response
- **Config**: SMTP_HOST, SMTP_PORT, SMTP_SECURE, SMTP_USER, SMTP_PASS,
  SMTP_FROM, REMINDER_HOURS_EARLY, REMINDER_HOURS_LATE env vars documented
  in .env.example; all optional — feature is silently disabled without them
- **Types**: Client.emailOptOut field added to shared types package

Co-authored-by: Groom Book CTO <cto@groombook.app>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-03-17 20:46:49 +00:00