README: replace migration section with pre-launch recreate-the-schema policy

The project is pre-launch: there is no production database and all dev starts
from a fresh volume recreated from init-scripts/init-script.sql. The obsolete
'Database migrations' section (ALTER statements and apply-before-deploy notes
for existing deployments) is replaced with the no-ALTER policy — schema
changes are edited directly into the CREATE statements, no migration-managed
delta exists, and the diff on the next recreate IS the migration.
This commit is contained in:
2026-08-22 00:34:49 +01:00
parent ea771ffaf5
commit 7df983052b
+6 -63
View File
@@ -105,71 +105,14 @@ The heavier static analyzers (`golangci-lint`, `staticcheck`, `gosec`) are **not
CI caches Go modules (`~/go/pkg/mod`) and npm dependencies (`~/.npm`, `node_modules`) via `actions/cache` — keyed on `go.sum` and `package-lock.json` respectively. Cache is served by Gitea's built-in cache server at `git.popertots.com`. First run downloads everything (~3m35s), subsequent runs restore from cache in seconds. CI caches Go modules (`~/go/pkg/mod`) and npm dependencies (`~/.npm`, `node_modules`) via `actions/cache` — keyed on `go.sum` and `package-lock.json` respectively. Cache is served by Gitea's built-in cache server at `git.popertots.com`. First run downloads everything (~3m35s), subsequent runs restore from cache in seconds.
### Database migrations ### Database schema policy (pre-launch — no ALTERs)
The schema lives in `init-scripts/init-script.sql` and is applied automatically on a **fresh** volume via `docker-entrypoint-initdb.d`. Existing deployments must apply the payment-system delta manually (the schema is not migration-managed): The schema is single-source in `init-scripts/init-script.sql`, applied automatically on a **fresh** volume via `docker-entrypoint-initdb.d`. This project is **pre-launch**: there is no production database, and all dev work starts from a fresh DB recreation. Therefore:
```sql - **No `ALTER TABLE` / `ALTER TYPE` / `ADD VALUE` statements anywhere** — not in `init-script.sql`, not in tests, not in code.
-- Refund system columns (refunds table) - Any schema change is edited **directly into the `CREATE` statements** in `init-script.sql`.
ALTER TABLE refunds ADD COLUMN IF NOT EXISTS refund_attempts INT NOT NULL DEFAULT 0; - There is **no migration-managed delta** and no "apply before deploying" step. If a local dev DB needs updating, drop and recreate it (`docker compose down -v && docker compose up --build -d`), or apply the change by hand locally — never commit ALTERs.
ALTER TABLE refunds ADD COLUMN IF NOT EXISTS origin VARCHAR(16) NOT NULL DEFAULT 'manual'; - Do not document changes as migration snippets; the schema diff on the next recreate is the migration.
ALTER TABLE refunds ADD COLUMN IF NOT EXISTS idempotency_key VARCHAR(64) UNIQUE;
-- refund_failed notification reason (admin_notification_reason enum)
-- NOTE: ALTER TYPE ... ADD VALUE cannot run inside a transaction block; run on a connection with autocommit.
ALTER TYPE admin_notification_reason ADD VALUE IF NOT EXISTS 'refund_failed';
-- Refunds may now reference non-booking payments (gift-card purchase refunds)
ALTER TABLE refunds ALTER COLUMN booking_id DROP NOT NULL;
-- Terminal checkout in-flight guard + payment_type passthrough (terminal_checkouts table)
-- NOTE: CREATE TABLE is a fresh addition, not a column change. Apply before deploying
-- the terminal-payment changes or CreateTerminalPayment/GetCheckoutStatus fail at runtime.
CREATE TABLE IF NOT EXISTS terminal_checkouts (
checkout_id VARCHAR(64) PRIMARY KEY,
booking_id CHAR(12) NOT NULL REFERENCES bookings(id) ON DELETE CASCADE,
payment_type payment_type NOT NULL DEFAULT 'full',
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
amount NUMERIC(10,2) NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_terminal_checkouts_booking ON terminal_checkouts(booking_id, status);
CREATE INDEX IF NOT EXISTS idx_terminal_checkouts_status ON terminal_checkouts(status);
-- Square webhook dedup (square_webhook_events table)
-- NOTE: CREATE TABLE is a fresh addition, not a column change. Apply before deploying
-- the webhook handler changes or the dedup INSERT fails at runtime.
-- Required for the Square webhook dedup; without it webhooks fail closed 503.
CREATE TABLE IF NOT EXISTS square_webhook_events (event_id TEXT PRIMARY KEY, received_at TIMESTAMPTZ NOT NULL DEFAULT NOW());
```
`backend/handlers/payments/refunds.go` casts `'refund_failed'::admin_notification_reason` (the sweep job in `internal/jobs/cleanup.go` only registers the handler), so an un-migrated DB fails at runtime — apply these before deploying the payment changes.
#### Saved-card per-user uniqueness + Square customer provisioning (P14)
The `user_saved_cards.square_card_id` UNIQUE constraint is now scoped **per user** (`UNIQUE (user_id, square_card_id)`), so the same physical card saved by two users produces two independent rows instead of user B mutating user A's saved-card row. Existing deployments must swap the constraint (the auto-generated constraint name is `user_saved_cards_square_card_id_key`):
```sql
ALTER TABLE user_saved_cards DROP CONSTRAINT user_saved_cards_square_card_id_key;
ALTER TABLE user_saved_cards ADD CONSTRAINT user_saved_cards_user_id_square_card_id_key UNIQUE (user_id, square_card_id);
```
`square_customer_id TEXT` (nullable) was also added to `user_saved_cards` — populated the first time a user saves a card (Square customer provisioning, P14) and reused thereafter:
```sql
ALTER TABLE user_saved_cards ADD COLUMN IF NOT EXISTS square_customer_id TEXT;
```
#### Saved-card Square references scrubbing (GDPR account anonymization)
Account anonymization (`anonymize_user`, `delete_guest_user`, `AnonymizeStaleGuestAccounts`) now NULLs `square_card_id` / `square_customer_id` on `user_saved_cards` so external Square references are removed on erasure (Feature Catalog §9.2). This requires `square_card_id` to be nullable — existing deployments must apply:
```sql
ALTER TABLE user_saved_cards ALTER COLUMN square_card_id DROP NOT NULL;
```
Fresh installs get the nullable column from `init-scripts/init-script.sql`; the schema is not migration-managed, so this one-liner is required for existing DBs before deploying the anonymization changes.
## Full Documentation ## Full Documentation