Compare commits

..

20 Commits

Author SHA1 Message Date
The Dogfather e7032e46bf Merge pull request 'promote: dev → uat (GRO-1489 lint fixes)' (#433) from dev into uat
promote: dev → uat (GRO-1489 lint fixes) (#433)
2026-05-23 19:27:34 +00:00
The Dogfather 09edd1b8ec Merge pull request 'Promote dev → uat: fix(e2e) PLAYWRIGHT_BASE_URL + host.docker.internal (GRO-1496)' (#431) from dev into uat
Promote dev → uat: fix(e2e) PLAYWRIGHT_BASE_URL + host.docker.internal (GRO-1496) (#431)
2026-05-21 21:04:20 +00:00
The Dogfather 7bb7d8c32b Merge pull request 'promote: dev → uat (GRO-1369 types sync)' (#428) from dev into uat
Merge dev → uat: GRO-1369 types sync, cascade logic, SMS consent
2026-05-21 20:53:19 +00:00
The Dogfather 6f160dde51 Merge pull request 'promote: dev → uat (GRO-1248 path prefix fix)' (#425) from dev into uat
promote: dev → uat (GRO-1248 path prefix fix) (#425)
2026-05-20 13:01:26 +00:00
the-dogfather-cto[bot] 733af4f5f2 Merge pull request #420 from groombook/dev
chore: promote dev → uat (GRO-1289 CI path fix)
2026-05-14 21:03:16 +00:00
the-dogfather-cto[bot] 13abbdcec8 chore: promote dev to uat — GRO-1287 ci.yml path fix (#419)
chore: promote dev to uat (GRO-1287 ci.yml path fix)
2026-05-14 20:49:13 +00:00
the-dogfather-cto[bot] e4e783dec6 chore: promote dev to uat — VITE_API_URL fix (GRO-1280, GRO-1206)
chore: promote dev to uat — VITE_API_URL fix (GRO-1280)
2026-05-14 20:32:09 +00:00
the-dogfather-cto[bot] 1a88528eae promote: dev → uat (GRO-1236 OAuth callback fix)
promote: dev → uat (GRO-1236 OAuth callback fix)
2026-05-14 19:36:13 +00:00
Chris Farhood 0f6a5ebe35 Promote dev→uat: OAuth callback session fix (GRO-1236) 2026-05-14 19:25:42 +00:00
the-dogfather-cto[bot] 121735edca promote: dev → uat (GRO-1207 portal Communication tab real backend)
promote: dev → uat (GRO-1207 portal Communication tab real backend)
2026-05-14 16:59:09 +00:00
the-dogfather-cto[bot] 5b2f45e5f3 chore: promote dev → uat (GRO-1212 portal test mock fix)
chore: promote dev → uat (GRO-1212 portal test mock fix)
2026-05-14 12:08:38 +00:00
the-dogfather-cto[bot] ff0bd2903e promote: dev → uat (GRO-1208 conversations API + GRO-1211 telnyx webhook fix)
promote: dev → uat (GRO-1208 conversations API + GRO-1211 telnyx webhook fix)
2026-05-14 08:45:38 +00:00
the-dogfather-cto[bot] 831b90dbe2 Merge dev→uat: auth rate limiting (GRO-1024)
chore: promote dev → uat (fix auth rate limits)
2026-05-11 03:40:46 +00:00
the-dogfather-cto[bot] 4b4ffa0ca4 Promote dev → uat: TELNYX_WEBHOOK_SECRET .env.example
Promote dev → uat: TELNYX_WEBHOOK_SECRET .env.example
2026-05-11 02:27:54 +00:00
the-dogfather-cto[bot] f0f271e046 feat(GRO-106): inbound Telnyx webhook + persistence (#378) (#388)
* 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>
2026-05-11 00:51:17 +00:00
the-dogfather-cto[bot] 673b85b64b Merge pull request #386 from groombook/dev
chore: promote dev → uat (GRO-1036 security fixes)
2026-05-04 22:53:58 +00:00
the-dogfather-cto[bot] a368d567d8 promote dev → uat: portal mobile overflow fix (GRO-730) (#384)
promote dev → uat: portal mobile overflow fix (GRO-730)
2026-05-04 21:25:36 +00:00
the-dogfather-cto[bot] 5332147ac1 Merge dev → uat: 10DLC pilot registration runbook (GRO-106)
promote dev → uat: 10DLC pilot registration runbook (GRO-106)
2026-05-04 20:55:50 +00:00
the-dogfather-cto[bot] b38de28d2e Merge pull request #380 from groombook/dev
promote: dev → uat (GRO-693 E2E mock fixes)
2026-05-04 15:16:52 +00:00
the-dogfather-cto[bot] 66a80cf9e7 Merge pull request #376 from groombook/dev
promote: GRO-106 messaging schema → UAT
2026-05-04 02:25:31 +00:00
2 changed files with 6 additions and 87 deletions
+6 -11
View File
@@ -77,17 +77,12 @@ The scripted Playwright suites in `apps/e2e/` and `apps/web/e2e/` are retained f
| # | Scenario | Steps | Expected | | # | Scenario | Steps | Expected |
|---|----------|-------|----------| |---|----------|-------|----------|
| TC-APP-4.1.1 | OIDC login (Authentik) | 1. Navigate to UAT environment<br>2. Click "Login with Authentik"<br>3. Enter test credentials<br>4. Authorize the application | User is redirected to app dashboard, session is established | | TC-APP-4.1.1 | OIDC login | 1. Navigate to UAT environment<br>2. Click "Login with Authentik"<br>3. Enter test credentials<br>4. Authorize the application | User is redirected to app dashboard, session is established |
| TC-APP-4.1.2 | Email + password login (UAT Super) | 1. Navigate to UAT environment sign-in page<br>2. Select email+password flow<br>3. Enter `uat-super@groombook.dev` and UAT super password<br>4. Submit | User is logged in and redirected to dashboard with manager access | | TC-APP-4.1.2 | Session persistence | 1. Log in as any user<br>2. Close browser tab<br>3. Reopen browser and navigate to UAT | User remains logged in, no re-authentication required |
| TC-APP-4.1.3 | Email + password login (UAT Groomer) | 1. Navigate to UAT environment sign-in page<br>2. Select email+password flow<br>3. Enter `uat-groomer@groombook.dev` and UAT groomer password<br>4. Submit | User is logged in and redirected to dashboard with staff/groomer access | | TC-APP-4.1.3 | Logout | 1. Log in as any user<br>2. Click logout button<br>3. Attempt to access protected route | User is logged out and redirected to login page |
| TC-APP-4.1.4 | Email + password login (UAT Customer) | 1. Navigate to UAT environment sign-in page<br>2. Select email+password flow<br>3. Enter `uat-customer@groombook.dev` and UAT customer password<br>4. Submit | User is logged in with client portal access | | TC-APP-4.1.4 | RBAC - Manager access | 1. Log in as Manager<br>2. Navigate to Settings, Staff Management, Reports | All administrative features are accessible |
| TC-APP-4.1.5 | Email + password login (UAT Tester) | 1. Navigate to UAT environment sign-in page<br>2. Select email+password flow<br>3. Enter `uat-tester@groombook.dev` and UAT tester password<br>4. Submit | User is logged in with staff/tester access | | TC-APP-4.1.5 | RBAC - Staff access | 1. Log in as Staff<br>2. Attempt to access Settings, Staff Management | Access denied or limited view, staff can only see assigned appointments |
| TC-APP-4.1.6 | Session persistence | 1. Log in as any user<br>2. Close browser tab<br>3. Reopen browser and navigate to UAT | User remains logged in, no re-authentication required | | TC-APP-4.1.6 | RBAC - Client access | 1. Log in as Client<br>2. Navigate to portal<br>3. Attempt to access admin areas | Client can only view their own appointments, pets, and profile |
| TC-APP-4.1.7 | Logout | 1. Log in as any user<br>2. Click logout button<br>3. Attempt to access protected route | User is logged out and redirected to login page |
| TC-APP-4.1.8 | RBAC - Manager access | 1. Log in as Manager (OIDC or email+password)<br>2. Navigate to Settings, Staff Management, Reports | All administrative features are accessible |
| TC-APP-4.1.9 | RBAC - Staff access | 1. Log in as Staff (OIDC or email+password)<br>2. Attempt to access Settings, Staff Management | Access denied or limited view, staff can only see assigned appointments |
| TC-APP-4.1.10 | RBAC - Client access | 1. Log in as Client (email+password)<br>2. Navigate to portal<br>3. Attempt to access admin areas | Client can only view their own appointments, pets, and profile |
| TC-APP-4.1.11 | Login after hourly reset | 1. Wait for or trigger `reset-demo-data` CronJob to run<br>2. Attempt email+password login as any UAT persona | Login succeeds — Better Auth credential accounts survive the reset cycle |
### 4.2 Setup Wizard / OOBE ### 4.2 Setup Wizard / OOBE
-76
View File
@@ -20,7 +20,6 @@ import postgres from "postgres";
import { drizzle } from "drizzle-orm/postgres-js"; import { drizzle } from "drizzle-orm/postgres-js";
import { eq, sql } from "drizzle-orm"; import { eq, sql } from "drizzle-orm";
import * as schema from "./schema.js"; import * as schema from "./schema.js";
import { randomBytes, scrypt } from "node:crypto";
// ── Seed profile configuration ───────────────────────────────────────────── // ── Seed profile configuration ─────────────────────────────────────────────
@@ -510,81 +509,6 @@ async function seedKnownUsers() {
} }
console.log(`✓ Seeded ${demoSvcs.length} services`); console.log(`✓ Seeded ${demoSvcs.length} services`);
// ── Better Auth credential accounts for UAT personas ─────────────────────
// Creates user + account rows so UAT personas can email+password login.
// Uses the same scrypt config as better-auth (keylen=64, N=16384, r=8, p=1).
const uatCredAccounts: Array<{ email: string; passwordEnvKey: string; staffId: string }> = [
{ email: "uat-super@groombook.dev", passwordEnvKey: "SEED_UAT_SUPER_PASSWORD", staffId: "00000000-0000-0000-0000-000000000003" },
{ email: "uat-groomer@groombook.dev", passwordEnvKey: "SEED_UAT_GROOMER_PASSWORD", staffId: "00000000-0000-0000-0000-000000000004" },
{ email: "uat-customer@groombook.dev", passwordEnvKey: "SEED_UAT_CUSTOMER_PASSWORD", staffId: "" },
{ email: "uat-tester@groombook.dev", passwordEnvKey: "SEED_UAT_TESTER_PASSWORD", staffId: "" },
];
for (const acct of uatCredAccounts) {
const password = process.env[acct.passwordEnvKey];
if (!password) {
console.log(`⊘ No ${acct.passwordEnvKey} set — skipping Better Auth account for ${acct.email}`);
continue;
}
// Check if user already exists
const [existingUser] = await db
.select()
.from(schema.user)
.where(eq(schema.user.email, acct.email))
.limit(1);
let userId: string;
if (existingUser) {
userId = existingUser.id;
console.log(`✓ Better Auth user '${acct.email}' already exists — skipping`);
} else {
// Hash with same scrypt params as better-auth: keylen=64, N=16384, r=8, p=1
// Use Promise-based scrypt API (callback pattern, wrapped in Promise)
const salt = randomBytes(16);
const key = await new Promise<Buffer>((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
scrypt(password.normalize("NFKC"), salt, 64, { N: 16384, r: 8, p: 1 } as any, (err: Error | null, derivedKey: Buffer) => {
if (err) reject(err);
else resolve(derivedKey);
});
});
const passwordHash = `${salt.toString("hex")}:${key.toString("hex")}`;
const [newUser] = await db.insert(schema.user).values({
id: uuid(),
name: acct.email.split("@")[0]!,
email: acct.email,
emailVerified: true,
}).returning();
userId = newUser!.id;
await db.insert(schema.account).values({
id: uuid(),
accountId: userId,
providerId: "credential",
userId,
password: passwordHash,
});
console.log(`✓ Created Better Auth credential account for '${acct.email}'`);
}
// Link staff record to Better Auth user if staff exists and has no userId yet
if (acct.staffId) {
const [existingStaff] = await db
.select()
.from(schema.staff)
.where(eq(schema.staff.id, acct.staffId))
.limit(1);
if (existingStaff && !existingStaff.userId) {
await db.update(schema.staff)
.set({ userId })
.where(eq(schema.staff.id, acct.staffId));
console.log(` ↳ Linked staff '${acct.email}' to Better Auth user`);
}
}
}
// ── Client: Demo Client ── // ── Client: Demo Client ──
const [existingClient] = await db const [existingClient] = await db
.select() .select()