Fixes
CI / ci (push) Canceled after 0s

Guest merge

Guest history now joins the account when the email is proven, in three places: OTP sign-in, Google sign-in, and staff adding a client. Unverified emails are never matched.

What moves: appointments, notes, push subscriptions and notifications go onto the account's profile, and the guest profile is deleted.
One email, several guests: if the email has several guest profiles (one per phone number), all are merged.
No profile yet: the oldest guest profile becomes the account's profile.
Account wins: it keeps its own name and phone, and only gaps get filled.
Stamps: summed, with the ledger entries moved across. If the combined total reaches the threshold, the reward is earned and the customer is notified.
Safety: it runs in one transaction and is audit-logged. A failure never blocks sign-in; the next sign-in retries.

Reminders
Staff: one reminder, 2h before; the 24h one is gone. Push and in-app are on by default and email is opt-in. Defaults are now per channel, and the settings page shows email off, push and in-app on.
Customer: one reminder at 24h. If booked too late for that, they get one at 2h instead, and none if the appointment is under 2h away.
In-app reminders: held back and released by the worker at the reminder time, so they no longer appear the moment you book. Cancelling or rescheduling clears them, and "mark all read" no longer marks unseen ones.
Guest links: every guest email now carries a working link. The booking-time token is unchanged, and later emails use one derived from the appointment id and AUTH_SECRET. Revoking a guest appointment's access kills both.
Reward expiry: two emails per reward at most, at 7 days and 1 day before.
Stale guard: the worker drops a reminder, without counting a failure, if the appointment has started or isn't confirmed.

OTP rate-limit tests (2): the failures came from my scratch environment, not the app. I had E2E_TEST_MODE=true in the scratch .env, which turns off the OTP rate limits, so the tests had nothing to assert on. I'd also used MariaDB, which throws error 1020 on concurrent upserts, while your real stack runs mysql:8.0. On MySQL 8 without the flag they pass every time. I also pinned that flag off in the integration config, so a .env with it set can't break them again. I tested that by turning it on in .env, and the OTP tests still passed.
Authorization and nav-role suites (2): this was a real bug. Both files import @/lib/auth/permissions, which builds the database connection when it's imported, before the test fixtures load .env. The suites crashed before running a single test, so the owner-permission checks (barbers can't do owner actions, nothing leaks across businesses) hadn't been running at all. I now load dotenv once in the integration config, so no test depends on import order. They run and pass (13 tests).
This commit is contained in:
Stefan Lazic committed 2026-10-07 16:39:12 +01:00
1 parent 7338c9d9e2
commit 1442d33d55
25 files changed
+1246 -302

No files matched your search

+12
View File
@@ -5,6 +5,18 @@ export default defineConfig({
test: {
environment: "node",
globals: false,
// Loads .env before any test file's imports run. Several modules under
// test (e.g. @/lib/auth/permissions -> @/lib/db/prisma) build the
// database connection at import time, so a test that imports one of
// them before fixtures.ts (which also loads dotenv) used to die with
// "Database connection is not configured" — a pure import-order
// accident, not a real failure.
setupFiles: ["dotenv/config"],
// E2E_TEST_MODE switches off the OTP rate limits (see otp.service.ts)
// for Playwright. Integration tests assert those limits exist, so pin it
// off here even if the surrounding environment/.env has it on — dotenv
// never overrides a variable that's already set.
env: { E2E_TEST_MODE: "false" },
include: ["**/*.integration.test.ts"],
// "**/node_modules/**" (not "node_modules/**") — see vitest.config.mts's
// own comment for why this matters once a nested node_modules