docs: add Gift Card Terms, Privacy Policy, Terms, and Testing Architecture
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
# Gift Card Terms & Conditions
|
||||
|
||||
**Last Updated:** June 2026
|
||||
**Status:** DRAFT — Local development (not yet in production)
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
These Terms & Conditions ("Gift Card Terms") apply to all gift cards, vouchers, and prepaid balances issued by Crussell Salon. They supplement our [[Terms & Conditions - Overall App|General Terms & Conditions]].
|
||||
|
||||
By purchasing, receiving, or using a gift card, you agree to these Gift Card Terms.
|
||||
|
||||
---
|
||||
|
||||
## 2. Gift Card Purchase
|
||||
|
||||
### 2.1 Purchase Methods
|
||||
Gift cards can be purchased:
|
||||
- **Online:** Via our Platform (card or saved payment method)
|
||||
- **In-store:** Cash, card, or other accepted payment methods
|
||||
- **As gifts:** Purchased for another person (recipient receives card code)
|
||||
|
||||
### 3.2 Denominations
|
||||
- Minimum value: £5
|
||||
- Maximum value: £500 per card
|
||||
- Custom amounts accepted (within range)
|
||||
|
||||
### 3.3 VAT Treatment
|
||||
- Gift cards are **Single-Purpose Vouchers (SPVs)** under UK VAT law.
|
||||
- **VAT is charged at point of purchase**, not at redemption.
|
||||
- When you pay with a gift card, no additional VAT is charged (already paid).
|
||||
- This applies to both direct gift card payments and account balance payments.
|
||||
|
||||
**Legal basis:** HMRC VAT Notice 700/12, EU VAT Directive Article 30a
|
||||
|
||||
---
|
||||
|
||||
## 3. Gift Card Expiry
|
||||
|
||||
### 3.1 Expiry Period
|
||||
Gift cards expire **24 months (2 years) after last use**.
|
||||
|
||||
**"Last use" includes:**
|
||||
- Redeeming the card to an account
|
||||
- Topping up the card
|
||||
- Checking the balance
|
||||
- Using the card for payment
|
||||
- Any admin action on the card
|
||||
|
||||
### 3.2 Rolling Expiry
|
||||
The 24-month period **resets** with each use. For example:
|
||||
- Card purchased: 1 January 2024
|
||||
- Balance checked: 1 June 2024 → Expiry now 1 June 2026
|
||||
- Card topped up: 1 December 2024 → Expiry now 1 December 2026
|
||||
|
||||
### 3.3 Expiry Notification
|
||||
We will send warning emails before expiry (if purchaser contact info available):
|
||||
- **1 month before expiry:** "Your gift card [CODE] expires in 30 days with £X remaining"
|
||||
- **1 week before expiry:** "Your gift card [CODE] expires in 7 days with £X remaining"
|
||||
|
||||
### 3.4 After Expiry
|
||||
- Unredeemed gift cards: Balance becomes dormant, transferred to recovery system.
|
||||
- No automatic refund.
|
||||
- Recovery possible with card code (contact us within 6 years).
|
||||
|
||||
**Legal basis:**
|
||||
- UK Consumer Rights Act 2015: Expiry terms must be "fair and transparent"
|
||||
- CMA guidance: 24 months is industry standard (John Lewis, M&S, Sainsbury's)
|
||||
- Under 12 months risks being challenged as unfair contract term
|
||||
|
||||
---
|
||||
|
||||
## 4. Gift Card Redemption
|
||||
|
||||
### 4.1 Redemption Options
|
||||
You can redeem a gift card in two ways:
|
||||
|
||||
**Option A: Redeem to Account Balance**
|
||||
- Enter card code in your account.
|
||||
- Full balance transferred to your account.
|
||||
- Card becomes invalid (balance now in account).
|
||||
- Account balance does not expire (but account may be deleted after 5 years idle).
|
||||
|
||||
**Option B: Use Directly for Payment**
|
||||
- Enter card code at checkout.
|
||||
- Deducted directly from card balance.
|
||||
- Remaining balance stays on card (subject to expiry).
|
||||
- Can split payment across multiple cards/methods.
|
||||
|
||||
### 4.2 Redemption Restrictions
|
||||
- Gift cards cannot be exchanged for cash.
|
||||
- Gift cards cannot be used to purchase other gift cards.
|
||||
- Gift cards are non-transferable after redemption to account.
|
||||
- Lost/stolen cards: We cannot replace unless we have record of purchase.
|
||||
|
||||
---
|
||||
|
||||
## 5. Account Balances
|
||||
|
||||
### 5.1 Expiry
|
||||
- **Account balances do not expire.**
|
||||
- However, your account may be deleted after 5 years of inactivity (see [[Terms & Conditions - Overall App#1.3 Inactive Account Policy|General T&Cs Section 1.3]]).
|
||||
|
||||
### 5.2 Account Deletion with Balance
|
||||
If your account is deleted (due to inactivity or your request):
|
||||
- Balance becomes **dormant** (not forfeited).
|
||||
- Transferred to our recovery system.
|
||||
- You receive your **Account ID** via email.
|
||||
- You can recover balance at any time by providing Account ID.
|
||||
- No deadline for recovery (but records deleted after 7 years per HMRC).
|
||||
|
||||
---
|
||||
|
||||
## 6. Dormant Balance Recovery
|
||||
|
||||
### 6.1 What is a Dormant Balance?
|
||||
A dormant balance is an account balance from a deleted account. It remains recoverable indefinitely, but we no longer hold your personal data (GDPR compliance).
|
||||
|
||||
### 6.2 Recovery Process
|
||||
To recover a dormant balance:
|
||||
1. Contact us with your **Account ID** (provided in deletion email).
|
||||
2. We verify the Account ID against our dormant balance records.
|
||||
3. If valid, we refund the balance via your preferred method (bank transfer, new gift card, or account credit).
|
||||
|
||||
### 6.3 Verification
|
||||
- **Account ID is required** — we cannot verify claims without it.
|
||||
- After account deletion, we have no way to identify you (by design — GDPR).
|
||||
- Lost Account ID = lost balance (we cannot recover without verification).
|
||||
|
||||
### 6.4 Legal Basis
|
||||
- UK consumer protection law: Gift card balances are customer's money held by business.
|
||||
- CMA unfair terms analysis: Recovery mechanism reduces legal risk from 40-50% to 10-20%.
|
||||
- Analogous to Dormant Bank and Building Society Accounts Act 2008 (15-year dormancy).
|
||||
|
||||
---
|
||||
|
||||
## Appendix: VAT Examples
|
||||
|
||||
### Example 1: Direct Gift Card Payment
|
||||
- Service cost: £60 (inc. VAT @ 20% = £10 VAT)
|
||||
- Gift card purchased for £60 (inc. £10 VAT already paid)
|
||||
- Payment with gift card: £60 deducted
|
||||
- **No additional VAT charged** (already paid at gift card purchase)
|
||||
|
||||
### Example 2: Account Balance Payment
|
||||
- Gift card £50 redeemed to account (inc. £8.33 VAT already paid)
|
||||
- Service cost: £30 (inc. VAT @ 20% = £5 VAT)
|
||||
- Payment with account balance: £30 deducted
|
||||
- **No additional VAT charged** (already paid at gift card purchase)
|
||||
|
||||
### Example 3: Split Payment
|
||||
- Service cost: £60 (inc. VAT @ 20% = £10 VAT)
|
||||
- Gift card balance: £40 (inc. £6.67 VAT already paid)
|
||||
- Cash payment: £20 (inc. £3.33 VAT charged now)
|
||||
- **Total VAT: £10** (£6.67 from gift card + £3.33 from cash)
|
||||
@@ -0,0 +1,99 @@
|
||||
# Privacy Policy
|
||||
|
||||
**Last Updated:** June 2026
|
||||
**Status:** DRAFT — Local development (not yet in production)
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
This Privacy Policy explains how Crussell Salon ("we", "us", "our") collects, uses, and protects your personal data when you use our booking platform ("Platform").
|
||||
|
||||
We are committed to protecting your privacy and complying with the **UK General Data Protection Regulation (UK GDPR)** and **Data Protection Act 2018**.
|
||||
|
||||
**Data Controller:**
|
||||
Crussell Salon
|
||||
Edinburgh, Scotland
|
||||
Email: help@crussell.invalid
|
||||
|
||||
---
|
||||
|
||||
## 2. Data We Collect
|
||||
|
||||
### 2.1 Personal Data (Identifiable Information)
|
||||
|
||||
**Account Information:**
|
||||
- Name (first, last)
|
||||
- Email address
|
||||
- Phone number
|
||||
- Date of birth (optional, for age verification)
|
||||
- Account ID (for balance recovery after deletion)
|
||||
|
||||
**Booking Information:**
|
||||
- Appointment dates, times, services
|
||||
- Treatment notes and preferences
|
||||
- Allergy and patch test records (health data — special category)
|
||||
- Payment history and transaction records
|
||||
|
||||
**Financial Data:**
|
||||
- Gift card codes and balances
|
||||
- Account balances
|
||||
- Payment transaction records (processed via Square, not stored by us)
|
||||
- Dormant balance records (Account ID only, no PII)
|
||||
|
||||
### 2.2 Special Category Data (Health Data)
|
||||
|
||||
We collect health-related information with your **explicit consent**:
|
||||
- Allergy records
|
||||
- Patch test results
|
||||
- Medical conditions affecting treatment
|
||||
- Skin sensitivity notes
|
||||
|
||||
**Legal basis:** UK GDPR Article 9(2)(a) — Explicit consent
|
||||
**Retention:** 7 years (insurance requirement) or account deletion (whichever is later)
|
||||
|
||||
---
|
||||
|
||||
## 3. Data Retention & Deletion Process
|
||||
|
||||
### 3.1 Retention Schedule
|
||||
|
||||
| Data Category | Retention Period | Legal Basis |
|
||||
|---------------|------------------|-------------|
|
||||
| **Active account data** | Account active + 2 years | Legitimate interest |
|
||||
| **Inactive accounts (no balance)** | 2 years idle | GDPR storage limitation |
|
||||
| **Inactive accounts (with balance)** | 5 years idle | Scottish prescriptive period |
|
||||
| **Financial records** | 7 years | HMRC requirement |
|
||||
| **Allergy/health records** | 7 years | Insurance requirement |
|
||||
| **Dormant balances** | Indefinite (Account ID only) | Recovery mechanism |
|
||||
| **Marketing preferences** | Until withdrawn | Consent |
|
||||
|
||||
### 5.2 Deletion Process
|
||||
|
||||
**Account deletion (your request):**
|
||||
1. You confirm deletion (warning about data loss).
|
||||
2. If balance exists, transferred to dormant balance system.
|
||||
3. Account ID sent to you via email.
|
||||
4. Personal data anonymized (name, email, phone replaced with placeholders).
|
||||
5. Financial records retained 7 years (HMRC) then aggregated.
|
||||
6. Allergy records retained 7 years (insurance) then deleted.
|
||||
|
||||
**Inactive account deletion (automatic):**
|
||||
1. Warning emails sent at 18/23 months (no balance) or 4/59 months (with balance).
|
||||
2. If no activity, account deleted as above.
|
||||
3. Dormant balance recoverable with Account ID.
|
||||
|
||||
---
|
||||
|
||||
## 4. Your Rights
|
||||
|
||||
Under UK GDPR, you have the right to:
|
||||
- **Access** your personal data (Article 15)
|
||||
- **Rectify** inaccurate data (Article 16)
|
||||
- **Erase** your data (Article 17 — subject to HMRC/insurance retention)
|
||||
- **Restrict** processing (Article 18)
|
||||
- **Data Portability** (Article 20)
|
||||
- **Object** to processing (Article 21)
|
||||
- **Withdraw Consent** (Article 7(3))
|
||||
|
||||
To exercise these rights, contact help@crussell.invalid. You also have the right to complain to the Information Commissioner's Office (ICO) at any time.
|
||||
@@ -0,0 +1,140 @@
|
||||
# Terms & Conditions — Overall App
|
||||
|
||||
**Last Updated:** June 2026
|
||||
**Status:** DRAFT — Local development (not yet in production)
|
||||
|
||||
---
|
||||
|
||||
## What is this document?
|
||||
|
||||
These Terms & Conditions ("Terms") govern your use of the Crussell booking platform ("Platform"), accessible via our website and associated mobile applications.
|
||||
|
||||
By creating an account or making a booking through our Platform, you agree to be bound by these Terms. If you do not agree, please do not use our services.
|
||||
|
||||
**Business Details:**
|
||||
- **Trading Name:** Crussell Salon
|
||||
- **Registered Address:** Edinburgh, Scotland
|
||||
- **Contact Email:** help@crussell.invalid
|
||||
- **VAT Status:** Not currently VAT registered (threshold is £90,000; will register when reached)
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack & Operations
|
||||
|
||||
For complete technical and structural details of the platform, see the [[Technical Manual]] and [[Overview]].
|
||||
|
||||
---
|
||||
|
||||
## 1. Account Creation & Deletion
|
||||
|
||||
### 1.1 Account Eligibility
|
||||
- You must be at least 16 years old to create an account.
|
||||
- You must provide accurate, current contact information.
|
||||
- You are responsible for maintaining account security.
|
||||
|
||||
### 1.2 Account Deletion
|
||||
You may request account deletion at any time. Upon deletion:
|
||||
|
||||
**If your account has no balance:**
|
||||
- All personal data will be anonymized or deleted.
|
||||
- Booking history will be retained for 7 years (HMRC requirement) then aggregated.
|
||||
- You will lose access to loyalty stamps, referral codes, and booking history.
|
||||
|
||||
**If your account has a balance:**
|
||||
- Your balance will become dormant and transferred to our recovery registry.
|
||||
- You will receive your **Account ID** via email — keep this ID safe.
|
||||
- You can recover your balance at any time by providing your Account ID.
|
||||
- All other personal data will be anonymized.
|
||||
|
||||
**Warning:** Account deletion is permanent. You will lose:
|
||||
- All booking history and treatment notes.
|
||||
- Allergy and patch test records (you'll need to provide these again if you return).
|
||||
- Loyalty stamps and referral codes.
|
||||
- Access to your account balance (unless you retain your Account ID).
|
||||
|
||||
### 1.3 Inactive Account Policy
|
||||
To comply with GDPR storage limitation principles, we delete inactive accounts:
|
||||
- **No balance:** Deleted after 2 years of inactivity.
|
||||
- **With balance:** Deleted after 5 years of inactivity (Scottish prescriptive period).
|
||||
|
||||
**Warning emails sent before deletion:**
|
||||
- 18 months idle (no balance): 6-month warning.
|
||||
- 23 months idle (no balance): 30-day warning.
|
||||
- 4 years idle (with balance): 1-year warning with balance amount.
|
||||
- 59 months idle (with balance): 30-day warning with balance amount.
|
||||
|
||||
All warning emails include your Account ID for future balance recovery.
|
||||
|
||||
---
|
||||
|
||||
## 2. Bookings & Appointments
|
||||
|
||||
### 2.1 Making a Booking
|
||||
- Bookings are subject to availability.
|
||||
- You will receive confirmation via email/SMS.
|
||||
- Some services require a deposit (typically 20-50% of service cost).
|
||||
|
||||
### 2.2 Cancellations & Rescheduling
|
||||
- **Client cancellation:** Must be made at least 24 hours before appointment.
|
||||
- **Late cancellation (<24 hours):** Deposit may be forfeited.
|
||||
- **No-show:** Deposit forfeited, may affect future booking eligibility.
|
||||
- **Business cancellation:** Full refund or reschedule offered.
|
||||
|
||||
### 2.3 Deposits
|
||||
- Deposits are non-refundable if you cancel <24 hours before appointment.
|
||||
- Deposits are applied to your final bill.
|
||||
- If we cancel, deposit is fully refunded.
|
||||
|
||||
### 2.4 Service Changes
|
||||
- We reserve the right to refuse service for health/safety reasons.
|
||||
- Patch tests may be required for certain treatments (allergy records retained for 7 years).
|
||||
- Service prices may change; you'll be notified before booking.
|
||||
|
||||
---
|
||||
|
||||
## 3. Payments & Fees
|
||||
|
||||
### 3.1 Payment Methods
|
||||
- Online card payments (via Square)
|
||||
- In-person card/cash payments
|
||||
- Gift cards
|
||||
- Account balance (from redeemed gift cards)
|
||||
|
||||
### 3.2 Payment Processing
|
||||
- Card payments processed securely via Square.
|
||||
- We do not store full card details.
|
||||
- Refunds processed to original payment method within 5-10 business days.
|
||||
|
||||
### 3.3 Split Payments
|
||||
- You may split payment across multiple methods (e.g., gift card + cash).
|
||||
- Each payment method processed separately.
|
||||
- Refunds apply proportionally to each payment method.
|
||||
|
||||
---
|
||||
|
||||
## 4. Gift Cards & Account Balances
|
||||
|
||||
For detailed gift card terms, see [[Gift Card Terms & Conditions]].
|
||||
|
||||
### 4.1 Gift Card Expiry
|
||||
- Gift cards expire **24 months after last use** (rolling expiry).
|
||||
- "Last use" includes: redemption, top-up, balance check, or any admin action.
|
||||
- Expiry date displayed on gift card and in your account.
|
||||
|
||||
### 4.2 Account Balances
|
||||
- Once redeemed to your account, balance does **not** expire.
|
||||
- However, your account may be deleted after 5 years of inactivity.
|
||||
- If account deleted with balance, funds become dormant but recoverable with Account ID.
|
||||
|
||||
### 4.3 VAT Treatment
|
||||
- Gift cards are Single-Purpose Vouchers (SPVs) under UK VAT law.
|
||||
- VAT charged at point of gift card purchase, **not** at redemption.
|
||||
- When you pay with gift card balance, no additional VAT charged (already paid).
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Statutory Timeframes & Legal References
|
||||
|
||||
- **HMRC Corporation Tax records:** **6 years** from the end of the financial year (HMRC CH14600 / Companies Act 2006 s.388). We aggregate detailed records after **7 years** to maintain a safe buffer.
|
||||
- **Scottish Contract Claims prescriptive period:** **5 years** (Prescription and Limitation (Scotland) Act 1973 s.6). Accounts with remaining balances must remain active for at least **5 years** to allow claims.
|
||||
- **GDPR Storage Limitation:** **2 years** inactivity default for accounts with no balance (legitimate interest in relationship ends).
|
||||
@@ -0,0 +1,254 @@
|
||||
# Testing Architecture & DB Management
|
||||
|
||||
**Last Updated:** June 2026
|
||||
**Status:** Technical Reference for Developers and AI Agents
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview & Architecture
|
||||
|
||||
The Platform's testing suite uses a **real database approach** for integration and handler testing, combined with unit tests for pure logic.
|
||||
|
||||
```
|
||||
TEST ARCHITECTURE
|
||||
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ go test (Tool Harness) │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
│ (Concurrency controlled via -p 1 or Advisory Locks)
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ TestMain (Package Level) │
|
||||
│ - Initializes testdb.NewPool() │
|
||||
│ - Calls testdb.Migrate() │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼ (Acquires Global pg_advisory_lock(1337) on dedicated conn)
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ testdb.Migrate() │
|
||||
│ - Sequentially drops all existing tables/types CASCADE │
|
||||
│ - Parses init-script.sql statement-by-statement │
|
||||
│ - Re-creates full fresh database schema │
|
||||
│ ⚠️ ALL operations on the SAME connection (pool.Acquire) │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼ (Releases Advisory Lock, releases connection)
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Individual Tests │
|
||||
│ - Calls resetTestData(t) (Truncates all tables sequentially) │
|
||||
│ - Seeds package-specific fixtures │
|
||||
│ - Executes test logic and HTTP handlers │
|
||||
│ ⚠️ TruncateTables uses pool.Acquire + pg_advisory_lock(1338) │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Key Core Principles:
|
||||
- **No Mocking the DB:** We use a real PostgreSQL database instance (`crussell_test` or `mydb`) to ensure that all constraints, triggers, and foreign keys are validated exactly as they would behave in production.
|
||||
- **Transactional Isolations:** Tests run sequentially within a package, and each test resets the database to a clean, default-seeded state.
|
||||
- **Parallel Resilience:** Concurrency at the package level is handled safely via database-level **advisory locks** so that parallel packages never deadlock each other's DDL schema operations.
|
||||
- **⚠️ Advisory locks require a dedicated connection:** Lock, work, and unlock must all use the same pgx connection. Use `pool.Acquire()` (not `pool.Exec()`) whenever acquiring advisory locks. See §2.2 for the correct pattern.
|
||||
|
||||
---
|
||||
|
||||
## 2. Database Management & Schema Initialization
|
||||
|
||||
### 2.1 The TestMain Pattern
|
||||
Every Go package containing database integration tests (e.g., `payments`, `scheduling`, `bookings`) defines a `TestMain` function inside a test file (e.g., `testmain_test.go` or package-specific test file).
|
||||
|
||||
This function orchestrates the lifecycle of the connection pool for that package:
|
||||
```go
|
||||
func TestMain(m *testing.M) {
|
||||
// 1. Establish the connection pool
|
||||
pool, err := testdb.NewPool("")
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 2. Perform sequential schema migration with advisory locks
|
||||
testdb.Migrate(&testing.T{}, pool)
|
||||
db.DB = pool
|
||||
jwt.Init()
|
||||
|
||||
// 3. Run the tests
|
||||
code := m.Run()
|
||||
|
||||
// 4. Close the pool and exit
|
||||
pool.Close()
|
||||
os.Exit(code)
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Global Advisory Locking (`testdb.Migrate`)
|
||||
When multiple Go packages are tested concurrently, they all try to drop and recreate the schema on the same physical test database. To prevent PostgreSQL system catalog deadlocks, `testdb.Migrate` acquires a session-level advisory lock:
|
||||
|
||||
```go
|
||||
// CRITICAL: pool.Acquire() ensures lock, work, and unlock all use the SAME connection.
|
||||
// pg_advisory_lock is session-level — using pool.Exec() for each would route
|
||||
// lock and unlock to different connections, rendering the lock useless.
|
||||
conn, err := pool.Acquire(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to acquire connection for migration: %v", err)
|
||||
}
|
||||
defer conn.Release()
|
||||
|
||||
_, err = conn.Exec(ctx, "SELECT pg_advisory_lock(1337)")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to acquire migration advisory lock: %v", err)
|
||||
}
|
||||
defer conn.Exec(ctx, "SELECT pg_advisory_unlock(1337)")
|
||||
```
|
||||
|
||||
**Why `pool.Acquire` instead of `pool.Exec`:**
|
||||
- `pool.Exec(ctx, ...)` borrows a connection from the pool and returns it immediately. Each call may use a **different** connection.
|
||||
- `pg_advisory_lock(1337)` is acquired on connection A, but your DDL runs on connection B, and `pg_advisory_unlock(1337)` fires on connection C. The lock is never actually held during the work.
|
||||
- `pool.Acquire()` grabs a **dedicated** connection and holds it. Every subsequent `conn.Exec()` uses the same session, so the lock actually protects the critical section.
|
||||
|
||||
### 2.3 The "Drop & Recreate" Clean Slate
|
||||
Before running `init-script.sql`, `testdb.Migrate` actively drops every single table, sequence, and enum custom type in CASCADE order to ensure zero pollution from previous test runs.
|
||||
- **Drop Order Array (`dropOrder`):** Defined in `backend/testutils/testdb/testdb.go`. New tables must be appended at the **beginning** of this list to resolve foreign key constraints on drop.
|
||||
- **Type Drops Array (`typeDrops`):** Holds custom Postgres enums. New types must be added here to be cleared cleanly.
|
||||
|
||||
### 2.4 TruncateTables Advisory Locking
|
||||
`testdb.TruncateTables` is called by every test via `resetTestData(t)` to reset state between tests. It truncates all tables in dependency order using `TRUNCATE ... CASCADE`.
|
||||
|
||||
```go
|
||||
func TruncateTables(t *testing.T, pool *pgxpool.Pool) {
|
||||
conn, err := pool.Acquire(ctx)
|
||||
// ... same pool.Acquire pattern as Migrate ...
|
||||
_, err = conn.Exec(ctx, "SELECT pg_advisory_lock(1338)")
|
||||
// ... all truncations use conn.Exec (same connection) ...
|
||||
defer conn.Exec(ctx, "SELECT pg_advisory_unlock(1338)")
|
||||
}
|
||||
```
|
||||
|
||||
**Why this matters:**
|
||||
- `TRUNCATE TABLE users CASCADE` acquires `ACCESS EXCLUSIVE` locks on `users` AND all tables with FK references to `users`.
|
||||
- If two `TruncateTables` calls run concurrently (from different packages or processes), one may hold a lock on `users` while the other holds a lock on `gift_cards` (which references `users`). Each needs the other's lock to complete CASCADE → **deadlock**.
|
||||
- `pg_advisory_lock(1338)` serializes all truncation across all connections, preventing this deadlock.
|
||||
- **Lock IDs must not collide:** Migrate uses lock 1337, TruncateTables uses lock 1338.
|
||||
|
||||
### 2.5 Statement-by-Statement SQL Parser
|
||||
Instead of executing the raw 2000-line `init-script.sql` as a single string (which can trigger pgx protocol hangs on DDL), `testdb.go` uses an in-memory statement splitter (`splitSQLStatements`) that respects dollar-quoted PL/pgSQL function blocks (`$$`) and executes DDL sequentially.
|
||||
|
||||
---
|
||||
|
||||
## 3. How to Run the Tests
|
||||
|
||||
### 3.1 Environment Variables
|
||||
To run integration tests successfully, your environment must be populated with these variables:
|
||||
|
||||
| Var Name | Purpose | Example Value |
|
||||
|----------|---------|---------------|
|
||||
| `TEST_DB_DSN` | Connection string for the test database | `postgres://myuser:mypassword@localhost:5432/crussell_test?sslmode=disable` |
|
||||
| `JWT_SECRET_KEY` | Secret used to sign test JWT tokens | `my_test_secret_key` |
|
||||
| `GO_TESTING` | Flag to trigger mock clients / bypass locks | `1` |
|
||||
|
||||
### 3.2 Command Execution
|
||||
|
||||
**Run all tests sequentially (Recommended):**
|
||||
```bash
|
||||
go test -tags "test,dev" -p 1 -count=1 ./...
|
||||
```
|
||||
|
||||
**Run tests for a single package:**
|
||||
```bash
|
||||
go test -tags "test,dev" -v -count=1 ./handlers/payments/
|
||||
```
|
||||
|
||||
**Run a single test in a package:**
|
||||
```bash
|
||||
go test -tags "test,dev" -v -run=TestAdminCreateGiftCard ./handlers/payments/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Correct Way to Add New Tests
|
||||
|
||||
When implementing new features or endpoints, follow this checklist to implement tests:
|
||||
|
||||
### Checklist for Adding New Tests:
|
||||
|
||||
- [ ] **File Location:** Put test files in the same directory as the handlers they test, named `*_test.go`.
|
||||
- [ ] **Tags:** Ensure the top of the file has the `//go:build test` constraint.
|
||||
- [ ] **Reset State:** Always call `resetTestData(t)` at the very beginning of your test:
|
||||
```go
|
||||
func TestMyNewHandler(t *testing.T) {
|
||||
resetTestData(t) // Truncates tables and seeds default working hours
|
||||
ctx := context.Background()
|
||||
...
|
||||
}
|
||||
```
|
||||
- [ ] **Close Rows & Transactions:** Never leak a connection! Always use `defer rows.Close()` and commit or rollback transactions (`defer tx.Rollback(ctx)`). Leaked transactions will cause subsequent `TRUNCATE` operations in other tests to freeze.
|
||||
- [ ] **Use Mock Clients:** If your endpoint calls external services like Square, ensure the test environment uses the in-memory mock client (`square.NewDevClient()`).
|
||||
|
||||
---
|
||||
|
||||
## 5. FAQ & Common Breakage Causes (For Developers & AI Agents)
|
||||
|
||||
If the test suite is hanging, failing on startup, or throwing database errors, check this reference:
|
||||
|
||||
### ❓ FAQ 1: The test suite hangs/freezes completely on startup (0% CPU, no output)
|
||||
**Reason:**
|
||||
A test is trying to run `testdb.TruncateTables` or `testdb.Migrate` (which executes `TRUNCATE` or `DROP TABLE`), but another active transaction or query has a lock on one of those tables.
|
||||
- **Cause A: Connection leak in code.** A previous handler or test executed a query (like `.Query()`) but did not call `rows.Close()`, or opened a transaction (`.Begin()`) but did not commit or rollback.
|
||||
- **Cause B: The Go dev server is running.** If the Go dev server (`go run ./main.go`) is currently running and holding open database sessions/transactions on the same database, test-suite DDL will block indefinitely.
|
||||
- **Fix:** Kill the running dev server, check for unclosed `rows` or unrolled-back transactions, and run `docker exec postgres psql -U myuser -d crussell_test -c "SELECT pid, query, state FROM pg_stat_activity WHERE state != 'idle';"` to locate blocked sessions.
|
||||
|
||||
### ❓ FAQ 2: Error `relation "X" does not exist` during seed or migration
|
||||
**Reason:**
|
||||
A foreign key constraint or table creation script has a dependency ordering issue in `init-scripts/init-script.sql`.
|
||||
- **Cause:** Table `A` references Table `B`, but Table `A` is created *before* Table `B` in the script.
|
||||
- **Fix:** Do **not** use inline constraints like `booking_id CHAR(12) REFERENCES bookings(id)` for tables created early. Instead, declare the column normally (`booking_id CHAR(12)`), and append the constraint at the very bottom of the file using `ALTER TABLE payments ADD CONSTRAINT fk_payments_booking FOREIGN KEY (booking_id) REFERENCES bookings(id);`.
|
||||
|
||||
### ❓ FAQ 3: Error `relation "X" already exists` during Migrate
|
||||
**Reason:**
|
||||
You created a new table in the database schema, but forgot to add it to the test teardown list.
|
||||
- **Cause:** When `testdb.Migrate` runs, it drops existing tables using the `dropOrder` list. If your new table isn't in that list, it won't be dropped. When `init-script.sql` tries to run `CREATE TABLE X`, it fails because the old table is still there.
|
||||
- **Fix:** Edit `backend/testutils/testdb/testdb.go`. Find the `dropOrder` array and the `tables` truncation array inside `TruncateTables`. Append your new table name to the **beginning** of both lists.
|
||||
|
||||
### ❓ FAQ 4: Error `type "X" already exists` during Migrate
|
||||
**Reason:**
|
||||
Similar to FAQ 3, you added a new custom enum type in PostgreSQL but forgot to tell the test runner to clean it up.
|
||||
- **Cause:** The enum type survives across migrations, causing `CREATE TYPE X` to fail.
|
||||
- **Fix:** Find `typeDrops` array in `backend/testutils/testdb/testdb.go`. Add `"DROP TYPE IF EXISTS X CASCADE"` to the array.
|
||||
|
||||
### ❓ FAQ 5: Deadlock detected during `TRUNCATE` (in `TruncateTables` or `resetTestData`)
|
||||
**Reason:**
|
||||
`TRUNCATE TABLE users CASCADE` needs `ACCESS EXCLUSIVE` locks on `users` and all child tables referencing it via FK. Another concurrent `TruncateTables` call holds one of those child locks. PostgreSQL detects the circular wait and aborts one of the transactions.
|
||||
|
||||
**Causes:**
|
||||
- **Cause A (most common):** Two `go test` processes running against the same database simultaneously (e.g., running the test suite from two terminals at once, or a CI pipeline overlapping with a dev run).
|
||||
- **Cause B:** `-p` flag > 1 causing multiple packages to run concurrently without advisory locks protecting them.
|
||||
- **Cause C:** A previous `TruncateTables` call held the advisory lock on a different connection than the work (pre-June 2026 bug — see §2.2 for why `pool.Acquire` is mandatory).
|
||||
|
||||
**Diagnosis:**
|
||||
```sql
|
||||
-- Find blocked/pending sessions on the test database
|
||||
SELECT pid, wait_event_type, wait_event, state, query
|
||||
FROM pg_stat_activity
|
||||
WHERE datname = 'crussell_test' AND state != 'idle'
|
||||
ORDER BY wait_event_type NULLS LAST;
|
||||
```
|
||||
Look for sessions in `wait_event_type = 'Lock'` waiting on `relation` events.
|
||||
|
||||
**Fix:**
|
||||
1. Kill other `go test` processes.
|
||||
2. Kill stale connections holding locks: `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'crussell_test' AND pid != pg_backend_pid();`
|
||||
3. Ensure `TruncateTables` uses `pool.Acquire()` with `pg_advisory_lock(1338)` (lock ID must be different from `Migrate`'s 1337).
|
||||
4. Run with `-p 1` to serialize packages.
|
||||
|
||||
### ❓ FAQ 6: Panic: `interface conversion: interface {} is nil, not string` during auth
|
||||
**Reason:**
|
||||
A handler test is calling `VerifyToken` or middleware, but the authentication headers or context keys are not set correctly.
|
||||
- **Cause:** The user token is expired, signed with a different key, or the context is missing `mw.UserIDKey`.
|
||||
- **Fix:** Make sure `jwt.Init()` is called in `TestMain`, and your test requests include the `Authorization: Bearer <token>` header with a token generated via `jwt.GenerateToken()`.
|
||||
|
||||
---
|
||||
|
||||
## Appendix: HMRC & Legal Compliance Retention Rules Reference
|
||||
|
||||
When writing or editing tests for cleanup and data retention, reference these statutory timelines:
|
||||
|
||||
- **HMRC Corporation Tax records:** **6 years** from the end of the financial year (HMRC CH14600 / Companies Act 2006 s.388). We aggregate detailed records after **7 years** to maintain a safe buffer.
|
||||
- **Scottish Contract Claims prescriptive period:** **5 years** (Prescription and Limitation (Scotland) Act 1973 s.6). Accounts with remaining balances must remain active for at least **5 years** to allow claims.
|
||||
- **GDPR Storage Limitation:** **2 years** inactivity default for accounts with no balance (legitimate interest in relationship ends).
|
||||
Reference in New Issue
Block a user