docs: update test counts from ~1,642 to 1,716 (4 skipped, 0 failures)

Reflect current test run results across README, Overview, Technical Manual, and Testing Architecture docs.
This commit is contained in:
2026-08-22 00:34:49 +01:00
parent a991642157
commit 9eecb375cf
5 changed files with 289 additions and 7 deletions
@@ -0,0 +1,282 @@
# Staged Default Hours Change with Conflict Resolution
## Overview
When an admin edits default working hours, instead of applying changes immediately:
1. Run conflict resolution (same pattern as holiday hours/time blockers)
2. Stage the change with a future effective date (admin picks, default +2 weeks)
3. Current hours continue to apply until the switch-over date
4. At midnight on the effective date, the change auto-applies and triggers a notification
5. Contact page shows "These opening hours will change from [date]"
6. Available hours use current hours before the date, new hours after
---
## What Changes
### New DB Table: `default_hours_scheduled_changes`
```sql
CREATE TABLE default_hours_scheduled_changes (
id SERIAL PRIMARY KEY,
effective_date DATE NOT NULL, -- London midnight date to switch over
created_by CHAR(12) NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
applied_at TIMESTAMPTZ, -- NULL until cron applies it
cancelled_at TIMESTAMPTZ, -- NULL unless admin cancels
hours JSONB NOT NULL -- [{weekday, startTime, endTime, isOpen}]
);
```
Only ONE pending change is allowed at a time. If a pending change exists and the admin tries to create another, they must cancel the existing one first.
### New `admin_notification_reason` enum value
```sql
ALTER TYPE admin_notification_reason ADD VALUE 'default_hours_changed';
```
Used by the cron job when it applies the change — creates a single notification for admin review.
---
## Files to Create/Modify
### Backend
| # | File | Action |
|---|---|---|
| 1 | `init-scripts/init-script.sql` | Add new table and enum value |
| 2 | `backend/handlers/scheduling/default-hours.go` | Add `ScheduleDefaultHoursChange`, `GetScheduledDefaultHoursChange`, `CancelScheduledDefaultHoursChange` handlers |
| 3 | `backend/handlers/scheduling/default-hours.go` | Modify `GetDefaultHours` — return both current and pending future hours |
| 4 | `backend/handlers/scheduling/default-hours.go` | Modify `computeAvailableHours` / `GetWorkingHours` — inject future hours for dates >= effective_date |
| 5 | `backend/handlers/scheduling/scheduled-cleanup.go` | Add `ApplyScheduledDefaultHours` cron handler |
| 6 | `backend/internal/jobs/cleanup.go` | Register `apply-default-hours` cron job (daily at 00:05) |
| 7 | `backend/main.go` | Register new routes |
### Frontend
| # | File | Action |
|---|---|---|
| 8 | `frontend/src/lib/components/admin/WeeklySchedule.svelte` | Add conflict resolution UI + date picker + staged save flow |
| 9 | `frontend/src/lib/components/layout/BusinessHours.svelte` | Show upcoming hours change with date |
| 10 | `frontend/src/routes/admin/+page.svelte` | Wire modal props if needed |
---
## Detail: Backend Design
### Handler: `ScheduleDefaultHoursChange` (POST)
`PUT /api/admin/default-hours` → replaced with a staging flow:
```
POST /api/admin/default-hours/schedule
Content-Type: application/json
{
"hours": [{"weekday": 0, "startTime": "10:00", "endTime": "18:00", "isOpen": true}, ...],
"effective_date": "2026-08-17" // optional, defaults to +14 days from today London
}
```
**Flow:**
1. Parse + validate input
2. Check for existing pending change — return 409 if one exists
3. Run conflict detection between current bookings and the proposed hours
4. If conflicts exist → return 409 with `{error: "conflicts exist", bookings: [...]}` (same format as holiday hours)
5. If no conflicts → store the pending change, return 201 with `{effective_date: "2026-08-17"}`
### Handler: `GetScheduledDefaultHoursChange` (GET)
```
GET /api/admin/default-hours/scheduled
```
Returns the pending change or 404:
```json
{
"effective_date": "2026-08-17",
"hours": [...],
"created_at": "...",
"created_by": "..."
}
```
### Handler: `CancelScheduledDefaultHoursChange` (DELETE)
```
DELETE /api/admin/default-hours/scheduled
```
Sets `cancelled_at` on the pending change. Returns 200.
### Handler: `ApplyScheduledDefaultHours` (cron)
Runs at "5 0 * * *" (00:05 daily — 5 minutes after midnight to avoid midnight race conditions).
**Flow:**
1. Query `default_hours_scheduled_changes` where `effective_date <= CURRENT_DATE` (London time) AND `applied_at IS NULL` AND `cancelled_at IS NULL`
2. For each due change:
a. BEGIN transaction
b. DELETE all existing `working_hours` rows
c. INSERT new rows from the change's hours JSON
d. INSERT an `admin_notifications` with reason `default_hours_changed` (details: "Default hours changed from [old summary] to [new summary]")
e. SET `applied_at = NOW()`
f. COMMIT
### Modified: `GetDefaultHours`
Current: returns `SELECT weekday, start_time, end_time, is_open FROM working_hours ORDER BY weekday`
New: if a pending change exists with `effective_date > today`, include an extra field:
```json
[
{"weekday": 0, "startTime": "00:00", "endTime": "00:00", "isOpen": false, ...},
...
]
```
Response changes to include optional `scheduled_change` field:
```json
{
"current": [...],
"scheduled_change": {
"effective_date": "2026-08-17",
"hours": [...]
}
}
```
This is backwards-compatible for existing consumers — they read the array from `current`.
### Modified: `computeAvailableHours` / `GetWorkingHours`
The hours resolution currently goes: proposed > exceptional > default > closed.
For the staged hours feature, I need to inject the **scheduled future default hours** for dates >= effective_date:
Priority order: proposed > exceptional > scheduled_future_default > default > closed
In `computeAvailableHours` (the per-day loop), after looking up the `defaultMap` entry:
```go
// Before falling through to default/closed, check if there's a staged future change
if scheduledChangeHours != nil && d.In(londonLocation).Format("2006-01-02") >= scheduledEffectiveDate {
if fh, ok := scheduledChangeHours[weekday]; ok {
baseStart = fh.StartTime
baseEnd = fh.EndTime
isOpen = fh.IsOpen
day.Source = "scheduled_change"
}
} else if def, ok := defaultMap[weekday]; ok {
// existing default logic...
}
```
This means:
- Today → current working hours apply
- Between today and effective_date → current working hours apply (no change)
- On/after effective_date → new scheduled hours apply
- Exceptional hours always override (higher priority)
For `GetWorkingHours`, same logic applies — it needs to return the correct hours for each date in the range.
---
## Detail: Frontend Design
### WeeklySchedule.svelte — "Edit Schedule" Modal
The modal gets a new top section (same pattern as HolidayHours conflict resolution):
**Step 1: Admin opens modal, edits hours**
Same time picker interface as today. No change to the editing UX.
**Step 2: Conflict resolution (new)**
- A date picker for "Apply from" (defaults to +14 days from today in London)
- A `checkConflictingBookings()` function that:
- Takes the proposed hours + effective date range (from effective_date to effective_date + 90 days or so)
- Calls `POST /api/admin/default-hours/schedule` with a dry-run flag or a dedicated conflict endpoint
- Shows amber warning with conflicting bookings + "View Booking" / "View Client" buttons
- Guard: submit button disabled while conflicts exist
**Step 3: Submit**
- Button text: `Schedule Change for [date]`
- On success toast: `Default hours will change at 23:59 on 17/08/2026`
- The modal closes, admin sees a "Pending change" indicator on the WeeklySchedule card
**Step 4: Pending change indicator**
- After a change is scheduled, the WeeklySchedule card shows:
- An amber banner: "Default hours are scheduled to change on 17/08/2026"
- A "Cancel" button that calls `DELETE /api/admin/default-hours/scheduled`
- The `fetchDefaultHours` response now includes `scheduled_change` — display it
### BusinessHours.svelte — Contact Page
Add a new section below the current "Upcoming Holiday Hours" section:
```svelte
{#if scheduledChange}
<hr class="my-2 border-gray-200" />
<p class="mb-2 text-center text-xs font-medium text-amber-600">
Opening hours will change from {formattedDate}
</p>
{#each scheduledChange.hours as h}
<div class="flex items-center justify-between text-sm">
<span class="font-medium text-gray-700">{dayNames[h.weekday]}</span>
<span class="text-gray-500">
{#if h.isOpen}
{formatTime(h.startTime)} {formatTime(h.endTime)}
{:else}
Closed
{/if}
</span>
</div>
{/each}
{/if}
```
To load this, the `fetchData` function needs an additional API call:
```
GET /api/admin/default-hours/scheduled (public, or a new public endpoint)
```
Since this is shown on the public contact page, the endpoint should be under the public read-only group (OptionalAuth) — similar to how `GET /scheduling/working-hours` is public.
### admin/+page.svelte
If `WeeklySchedule` needs `openUserModal` / `openBookingModal` for conflict resolution, wire the same props. Currently `WeeklySchedule` does not accept these props, but the conflict resolution flow needs "View Client" and "View Booking" buttons.
---
## Conflict Resolution vs Holiday Hours — Reusing the Pattern
The conflict detection for default hours changes reuses the EXACT same pattern as the holiday hours conflict handler, but with a date range instead of week starts:
```
POST /api/admin/default-hours/conflicting-bookings
{
"proposed_hours": [{"weekday": 0, "startTime": "10:00", "endTime": "18:00", "isOpen": true}, ...],
"start_date": "2026-08-17",
"end_date": "2026-11-17" // default: +90 days from effective date
}
```
This reuses the same `parseTimeToMinutes` comparison logic and the `ActiveBookingStatuses` filter. The response is the same `OverlappingBookingsResponse` format.
**Note:** The availability-for-reschedule logic is simpler than holiday hours because:
- Default hours changes apply PERMANENTLY (not per-week like holiday hours)
- The "what hours apply for rescheduling" is just: current hours until effective_date, future hours after
- No per-week mapping needed
---
## Risks
| Risk | Mitigation |
|---|---|
| Admin sets effective_date in the past | Validate: must be >= tomorrow (London date + 1) |
| Cron miss at midnight doesn't apply change | Cron runs 00:05 to avoid midnight race. Query uses `<= CURRENT_DATE` so it catches any missed days |
| Two admins try to schedule simultaneously | Unique constraint on `(applied_at IS NULL AND cancelled_at IS NULL)` — use a partial unique index |
| Notification is an admin-only todo placeholder | Add `default_hours_changed` to the enum, create one notification. Future work: user notification |
| Scheduling a change far in the future (6+ months) | Conflicts only checked against existing bookings in the window. Long-range changes may need re-checking when new bookings are made — acceptable for v1 |
+1 -1
View File
@@ -76,7 +76,7 @@ Default logins (password: `password`):
```bash ```bash
cd backend && go build -o bin/backend ./main.go cd backend && go build -o bin/backend ./main.go
cd frontend && npm ci && npm run build cd frontend && npm ci && npm run build
cd backend && go test -tags "test,dev" -count=1 -parallel 8 ./... # ~1,642 tests passed (~13s) cd backend && go test -tags "test,dev" -count=1 -parallel 8 ./... # 1,716 tests passed (4 skipped, ~13s)
cd backend && go test -tags "test,dev" -count=1 -race -timeout 480s ./... # race detector (all packages, ~4min) cd backend && go test -tags "test,dev" -count=1 -race -timeout 480s ./... # race detector (all packages, ~4min)
cd backend && go test -tags "test,dev" -count=10 -parallel 8 ./... # thorough verification (~2-3min) cd backend && go test -tags "test,dev" -count=10 -parallel 8 ./... # thorough verification (~2-3min)
``` ```
+1 -1
View File
@@ -218,7 +218,7 @@ npm run dev # Dev server with HMR
```bash ```bash
cd backend cd backend
go test -tags "test,dev" ./... # ~1,642 tests passed go test -tags "test,dev" ./... # 1,716 tests passed (4 skipped)
go test -tags "test,dev" -v -run TestName ./... # Single test go test -tags "test,dev" -v -run TestName ./... # Single test
``` ```
+1 -1
View File
@@ -1267,7 +1267,7 @@ Files with this pattern: `bookings.go` (4 handlers), `custom_services.go`, `user
### Test Coverage ### Test Coverage
**~1,642 tests run** across all packages (0 failures). Coverage improved from 50.4% to 65.0% via 56 new test files covering booking handlers, user handlers, payments (giftcards, till, refunds), DAV, auth, middleware, validators, zxcvbn, and scheduling. Key additions: coverage improvement tests (bookings_coverage_test.go, user_coverage_test.go, payments coverage expansion — all meaningful error-path tests, not padding), split-lunch detection tests, savepoint/transaction-context tests for time-sensitive operations, VAT lifecycle and parallel-deadlock regression tests, and cleanup of 10 dead test functions flagged by staticcheck U1000. **1,716 tests run** across all packages (4 skipped, 0 failures). Coverage improved from 50.4% to 65.0% via 56 new test files covering booking handlers, user handlers, payments (giftcards, till, refunds), DAV, auth, middleware, validators, zxcvbn, and scheduling. Key additions: coverage improvement tests (bookings_coverage_test.go, user_coverage_test.go, payments coverage expansion — all meaningful error-path tests, not padding), split-lunch detection tests, savepoint/transaction-context tests for time-sensitive operations, VAT lifecycle and parallel-deadlock regression tests, and cleanup of 10 dead test functions flagged by staticcheck U1000.
| Package | Coverage Area | | Package | Coverage Area |
|---------|--------------| |---------|--------------|
@@ -1,6 +1,6 @@
# Testing Architecture & DB Management # Testing Architecture & DB Management
**Last Updated:** July 2026 (v5 — coverage 50.4%→65.0%, ~1,642 tests) **Last Updated:** July 2026 (v5 — coverage 50.4%→65.0%, 1,716 tests)
--- ---
@@ -501,7 +501,7 @@ This appears in `TestAccount_DeleteGuest` and `TestLoyalty_Get`. The `dav.Servic
|--------|-------| |--------|-------|
| Quick check (`-count=1`) | **~13s** | | Quick check (`-count=1`) | **~13s** |
| Packages | 19 tested, 0 failures | | Packages | 19 tested, 0 failures |
| Tests | ~1,642 passed, 0 failing | | Tests | 1,716 passed, 4 skipped, 0 failing |
New test additions in this batch: New test additions in this batch:
| Test | Coverage | | Test | Coverage |
@@ -520,7 +520,7 @@ New test additions in this batch:
| `TestCancelReservation_DoesNotTouchAnonReservations` | Inverse-isolation test — user cancel ignores `RESERVATION:anon:%` (defensive — the WHERE clause only matches `RESERVATION:user:%`) | | `TestCancelReservation_DoesNotTouchAnonReservations` | Inverse-isolation test — user cancel ignores `RESERVATION:anon:%` (defensive — the WHERE clause only matches `RESERVATION:user:%`) |
| `TestCancelReservation_DoesNotTouchAdminReservations` | Inverse-isolation test — user cancel ignores `RESERVATION:admin:%`. Pairs with the admin-side test that verifies admin cancel ignores `RESERVATION:user:%`. Proves the two endpoints are properly partitioned. | | `TestCancelReservation_DoesNotTouchAdminReservations` | Inverse-isolation test — user cancel ignores `RESERVATION:admin:%`. Pairs with the admin-side test that verifies admin cancel ignores `RESERVATION:user:%`. Proves the two endpoints are properly partitioned. |
**Total tests:** ~1,642 passed across all packages. 0 failures. Growth driven by: coverage improvement pass (new test files for bookings, user, payments, giftcards, till, refunds, DAV, auth, middleware, validators, zxcvbn — 56 new files, coverage 50.4%→65.0%), VAT lifecycle and parallel-deadlock regression tests, savepoint/transaction-context pattern for time-sensitive tests, split-lunch detection tests, and removal of 10 dead test functions flagged by staticcheck U1000. **Total tests:** 1,716 passed across all packages (4 skipped). 0 failures. Growth driven by: coverage improvement pass (new test files for bookings, user, payments, giftcards, till, refunds, DAV, auth, middleware, validators, zxcvbn — 56 new files, coverage 50.4%→65.0%), VAT lifecycle and parallel-deadlock regression tests, savepoint/transaction-context pattern for time-sensitive tests, split-lunch detection tests, and removal of 10 dead test functions flagged by staticcheck U1000.
### What Drives Test Time ### What Drives Test Time
@@ -637,7 +637,7 @@ This shouldn't appear anymore — the auth package's TestMain was updated to use
### Q: What's the total test count? ### Q: What's the total test count?
~1,642 tests run across all packages. 0 failures. 1,716 tests run across all packages (4 skipped). 0 failures.
**Notable new tests:** Centralised job scheduler tests (3 — RegisterAll count, schedules, handler signatures), scheduled-cleanup handler tests (21 — NotifyUnpaidOneWeek/Month, TransitionDiscountCampaigns, CleanupExpiredVerificationCodes/RefreshTokens), GDPR export cache cleanup (4), stale login entry cleanup (4), rate limiter cleanup tests (6), rate limiter production behavior tests (6). Duplicate completion guard (idempotent second `"completed"` call), daily stamp cap (two completions same day → 1 stamp), invalid status transitions (no-show→completed rejected with 400), sequential edit (two edits in sequence), timezone independence (UTC in, UTC out — no shift), past-booking no-show guard (past confirmed booking cancelled → `client_cancelled`, not `no_show`). New closing_time tests (3), content-type middleware tests (2), clock package tests, expanded admin reserve overlap tests, expanded gift card buy flow tests with VAT, and full admin reservation cancel coverage (12 tests covering walkin + callin + isolation + no-op + idempotency + response format parity). **Notable new tests:** Centralised job scheduler tests (3 — RegisterAll count, schedules, handler signatures), scheduled-cleanup handler tests (21 — NotifyUnpaidOneWeek/Month, TransitionDiscountCampaigns, CleanupExpiredVerificationCodes/RefreshTokens), GDPR export cache cleanup (4), stale login entry cleanup (4), rate limiter cleanup tests (6), rate limiter production behavior tests (6). Duplicate completion guard (idempotent second `"completed"` call), daily stamp cap (two completions same day → 1 stamp), invalid status transitions (no-show→completed rejected with 400), sequential edit (two edits in sequence), timezone independence (UTC in, UTC out — no shift), past-booking no-show guard (past confirmed booking cancelled → `client_cancelled`, not `no_show`). New closing_time tests (3), content-type middleware tests (2), clock package tests, expanded admin reserve overlap tests, expanded gift card buy flow tests with VAT, and full admin reservation cancel coverage (12 tests covering walkin + callin + isolation + no-op + idempotency + response format parity).