Compare commits

...

23 Commits

Author SHA1 Message Date
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 9b49b6388d Merge pull request 'fix(e2e): respect PLAYWRIGHT_BASE_URL env var and add host.docker.internal resolution' (#430) from flea/gro-1496-e2e-err-connection-refused into dev
CI / Test (push) Successful in 22s
CI / Lint & Typecheck (push) Successful in 23s
CI / Build (push) Successful in 24s
CI / E2E Tests (push) Failing after 3m45s
CI / Build & Push Docker Images (push) Has been skipped
CI / Update Infra Image Tags (push) Has been skipped
CI / Web E2E (Dev) (push) Has been cancelled
CI / Deploy PR to groombook-dev (push) Has been cancelled
fix(e2e): respect PLAYWRIGHT_BASE_URL env var and add host.docker.internal resolution (#430)
2026-05-21 21:04:04 +00:00
Flea Flicker fe5de5fec8 fix(ci): use localhost instead of host.docker.internal for Playwright
CI / Test (push) Successful in 25s
CI / Lint & Typecheck (push) Successful in 23s
CI / Build (push) Successful in 23s
CI / E2E Tests (push) Failing after 5m31s
CI / Build & Push Docker Images (push) Has been skipped
CI / Update Infra Image Tags (push) Has been skipped
CI / Web E2E (Dev) (push) Has been cancelled
CI / Deploy PR to groombook-dev (push) Has been cancelled
host.docker.internal is a Docker Desktop feature unavailable on Gitea Actions
ubuntu-latest runners. Linux runners can reach the Docker Compose service
via localhost when using docker compose expose/published ports.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-21 20:58:02 +00:00
Flea Flicker 82f1e3856f fix(e2e): respect PLAYWRIGHT_BASE_URL env var and add host.docker.internal resolution
CI / Test (pull_request) Successful in 28s
CI / Lint & Typecheck (pull_request) Successful in 31s
CI / E2E Tests (pull_request) Successful in 1m32s
CI / Build (pull_request) Successful in 2m32s
CI / Build & Push Docker Images (pull_request) Successful in 35s
CI / Update Infra Image Tags (pull_request) Has been skipped
CI / Web E2E (Dev) (pull_request) Has been cancelled
CI / Deploy PR to groombook-dev (pull_request) Has been cancelled
The Playwright config hardcoded localhost:8080 as baseURL, ignoring
the PLAYWRIGHT_BASE_URL env var set in CI. Docker Compose was also
missing extra_hosts to resolve host.docker.internal on Gitea Actions
runners (which use DIND).

Fixes GRO-1496.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-21 20:53:30 +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
Flea Flicker 526251b63a fix: resolve lint errors and xlarge mismatch for dev→uat promotion
CI / Test (push) Successful in 26s
CI / Lint & Typecheck (push) Successful in 27s
CI / E2E Tests (push) Failing after 3m27s
CI / Update Infra Image Tags (push) Has been skipped
CI / Build (push) Successful in 24s
CI / Build & Push Docker Images (push) Has been skipped
CI / Web E2E (Dev) (push) Has been cancelled
CI / Deploy PR to groombook-dev (push) Has been cancelled
- Remove unused gte/lt/ne imports from cascade.ts
- Prefix unused params originalEndTime, originalStartTime, newStartTime
  with underscore in cascade.ts and appointments.ts callers
- Remove unused petCoatType query param from book.ts availability route
- Align xlarge value: Book.tsx now uses "xlarge" (no hyphen) everywhere
  to match the Zod booking schema

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-21 20:28:43 +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
7 changed files with 15 additions and 14 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ jobs:
- name: Run E2E tests - name: Run E2E tests
run: pnpm --filter @groombook/e2e test run: pnpm --filter @groombook/e2e test
env: env:
PLAYWRIGHT_BASE_URL: http://host.docker.internal:8080 PLAYWRIGHT_BASE_URL: http://localhost:8080
- name: Stop Docker Compose stack - name: Stop Docker Compose stack
if: always() if: always()
+7 -7
View File
@@ -1,4 +1,4 @@
import { eq, and, gt, gte, lt, ne, or, asc } from "@groombook/db"; import { eq, and, gt, or, asc } from "@groombook/db";
import { appointments, clients, pets, services, staff, type Db } from "@groombook/db"; import { appointments, clients, pets, services, staff, type Db } from "@groombook/db";
import { resolveBufferMinutes } from "./buffer.js"; import { resolveBufferMinutes } from "./buffer.js";
import { sendEmail, buildRescheduleNotificationEmail } from "../services/email.js"; import { sendEmail, buildRescheduleNotificationEmail } from "../services/email.js";
@@ -53,12 +53,12 @@ export async function detectAndCascadeOverrun({
db, db,
overrunningAppointmentId, overrunningAppointmentId,
newEndTime, newEndTime,
originalEndTime, _originalEndTime,
}: { }: {
db: Db; db: Db;
overrunningAppointmentId: string; overrunningAppointmentId: string;
newEndTime: Date; newEndTime: Date;
originalEndTime: Date; _originalEndTime: Date;
}): Promise<CascadeResult> { }): Promise<CascadeResult> {
const result: CascadeResult = { shifted: [], flaggedForReview: [] }; const result: CascadeResult = { shifted: [], flaggedForReview: [] };
@@ -178,16 +178,16 @@ export async function detectAndCascadeOverrun({
export function isOverrun({ export function isOverrun({
originalEndTime, originalEndTime,
newEndTime, newEndTime,
originalStartTime, _originalStartTime,
newStartTime, _newStartTime,
status, status,
currentTime, currentTime,
bufferMinutes, bufferMinutes,
}: { }: {
originalEndTime: Date; originalEndTime: Date;
newEndTime: Date; newEndTime: Date;
originalStartTime: Date; _originalStartTime: Date;
newStartTime?: Date; _newStartTime?: Date;
status: string; status: string;
currentTime: Date; currentTime: Date;
bufferMinutes: number; bufferMinutes: number;
+2 -2
View File
@@ -700,7 +700,7 @@ appointmentsRouter.patch(
isOverrun({ isOverrun({
originalEndTime, originalEndTime,
newEndTime: new Date(updateFields.endTime), newEndTime: new Date(updateFields.endTime),
originalStartTime: row.startTime, _originalStartTime: row.startTime,
status: row.status, status: row.status,
currentTime: new Date(), currentTime: new Date(),
bufferMinutes: row.bufferMinutes ?? 0, bufferMinutes: row.bufferMinutes ?? 0,
@@ -710,7 +710,7 @@ appointmentsRouter.patch(
db, db,
overrunningAppointmentId: id, overrunningAppointmentId: id,
newEndTime: new Date(updateFields.endTime), newEndTime: new Date(updateFields.endTime),
originalEndTime, _originalEndTime: originalEndTime,
}); });
return c.json({ ...row, cascade: cascadeResult }); return c.json({ ...row, cascade: cascadeResult });
} }
-1
View File
@@ -44,7 +44,6 @@ bookRouter.get("/availability", async (c) => {
const serviceId = c.req.query("serviceId"); const serviceId = c.req.query("serviceId");
const dateStr = c.req.query("date"); const dateStr = c.req.query("date");
const petSizeCategory = c.req.query("petSizeCategory") ?? undefined; const petSizeCategory = c.req.query("petSizeCategory") ?? undefined;
const petCoatType = c.req.query("petCoatType") ?? undefined;
if (!serviceId || !dateStr) { if (!serviceId || !dateStr) {
return c.json({ error: "serviceId and date are required" }, 400); return c.json({ error: "serviceId and date are required" }, 400);
+1 -1
View File
@@ -19,7 +19,7 @@ export default defineConfig({
reporter: process.env.CI ? "github" : "list", reporter: process.env.CI ? "github" : "list",
use: { use: {
baseURL: "http://localhost:8080", baseURL: process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:8080",
trace: "on-first-retry", trace: "on-first-retry",
screenshot: "only-on-failure", screenshot: "only-on-failure",
serviceWorkers: "block", serviceWorkers: "block",
+2 -2
View File
@@ -515,7 +515,7 @@ export function BookPage() {
<option value="small">Small (under 15 lbs)</option> <option value="small">Small (under 15 lbs)</option>
<option value="medium">Medium (1540 lbs)</option> <option value="medium">Medium (1540 lbs)</option>
<option value="large">Large (4080 lbs)</option> <option value="large">Large (4080 lbs)</option>
<option value="x-large">X-Large (over 80 lbs)</option> <option value="xlarge">X-Large (over 80 lbs)</option>
</select> </select>
</div> </div>
<div> <div>
@@ -568,7 +568,7 @@ export function BookPage() {
<div> <div>
<div style={{ color: "#9ca3af", fontSize: 12, fontWeight: 600, textTransform: "uppercase" }}>Service</div> <div style={{ color: "#9ca3af", fontSize: 12, fontWeight: 600, textTransform: "uppercase" }}>Service</div>
<div style={{ fontWeight: 600 }}>{selectedService.name}</div> <div style={{ fontWeight: 600 }}>{selectedService.name}</div>
<div style={{ color: "#6b7280" }}>{fmtPrice(selectedService.basePriceCents)} · {fmtDuration(selectedService.durationMinutes + ((form.petSizeCategory === "large" || form.petSizeCategory === "x-large") ? (selectedService.defaultBufferMinutes ?? 0) : 0))}</div> <div style={{ color: "#6b7280" }}>{fmtPrice(selectedService.basePriceCents)} · {fmtDuration(selectedService.durationMinutes + ((form.petSizeCategory === "large" || form.petSizeCategory === "xlarge") ? (selectedService.defaultBufferMinutes ?? 0) : 0))}</div>
</div> </div>
<div> <div>
<div style={{ color: "#9ca3af", fontSize: 12, fontWeight: 600, textTransform: "uppercase" }}>Date & Time</div> <div style={{ color: "#9ca3af", fontSize: 12, fontWeight: 600, textTransform: "uppercase" }}>Date & Time</div>
+2
View File
@@ -50,6 +50,8 @@ services:
dockerfile: apps/web/Dockerfile dockerfile: apps/web/Dockerfile
ports: ports:
- "8080:80" - "8080:80"
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on: depends_on:
- api - api