Files
nilesh.pandClaude Opus 4.8 4d3eb18a6c Initial commit: TravelHub multi-tenant flight booking platform
Angular + Go + PostgreSQL sample with strict per-tenant isolation.

- Backend: Go API (basic auth, tenant-scoped repository layer, pgx pool)
- Frontend: Angular 22 SPA (search, checkout, bookings, dashboard)
- Database: schema, seed data and demo bookings migrations
- Tests: 311 across Go (integration), Angular (unit) and Playwright (e2e),
  plus a generated evidence report with PDF export
- Security: whitebox VAPT report under security/

Secrets (.env, tokens, DB credentials) are git-ignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-13 11:38:01 +05:30

108 lines
5.4 KiB
SQL

-- TravelHub: multi-tenant flight booking schema
-- Strategy: shared database, shared schema, tenant_id discriminator on every
-- tenant-owned table. Reference data (airports, airlines) is global.
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- ---------------------------------------------------------------- tenants --
CREATE TABLE IF NOT EXISTS tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
slug TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'suspended')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ------------------------------------------------------------------ users --
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
email TEXT NOT NULL,
password_hash TEXT NOT NULL,
full_name TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'agent'
CHECK (role IN ('admin', 'agent')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- The same email may exist in different tenants; unique only within one.
CONSTRAINT users_tenant_email_key UNIQUE (tenant_id, email)
);
CREATE INDEX IF NOT EXISTS idx_users_tenant ON users(tenant_id);
-- ------------------------------------------------- reference data (global) --
CREATE TABLE IF NOT EXISTS airports (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
iata_code CHAR(3) NOT NULL UNIQUE,
name TEXT NOT NULL,
city TEXT NOT NULL,
country TEXT NOT NULL,
timezone TEXT NOT NULL DEFAULT 'UTC'
);
CREATE TABLE IF NOT EXISTS airlines (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
iata_code CHAR(2) NOT NULL UNIQUE,
name TEXT NOT NULL,
logo_url TEXT
);
-- ---------------------------------------------------------------- flights --
-- Inventory is per-tenant: each agency loads its own negotiated fares.
CREATE TABLE IF NOT EXISTS flights (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
airline_id UUID NOT NULL REFERENCES airlines(id),
flight_number TEXT NOT NULL,
origin_airport_id UUID NOT NULL REFERENCES airports(id),
destination_airport_id UUID NOT NULL REFERENCES airports(id),
departure_time TIMESTAMPTZ NOT NULL,
arrival_time TIMESTAMPTZ NOT NULL,
aircraft TEXT NOT NULL DEFAULT '',
cabin_class TEXT NOT NULL DEFAULT 'economy'
CHECK (cabin_class IN ('economy', 'premium_economy', 'business', 'first')),
base_price NUMERIC(10,2) NOT NULL CHECK (base_price >= 0),
currency CHAR(3) NOT NULL DEFAULT 'USD',
seats_total INTEGER NOT NULL CHECK (seats_total > 0),
seats_available INTEGER NOT NULL CHECK (seats_available >= 0),
status TEXT NOT NULL DEFAULT 'scheduled'
CHECK (status IN ('scheduled', 'delayed', 'cancelled')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT flights_seats_check CHECK (seats_available <= seats_total),
CONSTRAINT flights_times_check CHECK (arrival_time > departure_time)
);
CREATE INDEX IF NOT EXISTS idx_flights_search
ON flights(tenant_id, origin_airport_id, destination_airport_id, departure_time);
-- --------------------------------------------------------------- bookings --
CREATE TABLE IF NOT EXISTS bookings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id),
flight_id UUID NOT NULL REFERENCES flights(id),
booking_reference TEXT NOT NULL,
passenger_count INTEGER NOT NULL CHECK (passenger_count > 0),
total_amount NUMERIC(10,2) NOT NULL CHECK (total_amount >= 0),
currency CHAR(3) NOT NULL DEFAULT 'USD',
status TEXT NOT NULL DEFAULT 'confirmed'
CHECK (status IN ('confirmed', 'cancelled')),
contact_email TEXT NOT NULL,
contact_phone TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- PNR is unique per tenant, not globally.
CONSTRAINT bookings_tenant_ref_key UNIQUE (tenant_id, booking_reference)
);
CREATE INDEX IF NOT EXISTS idx_bookings_tenant_created ON bookings(tenant_id, created_at DESC);
-- ------------------------------------------------------------- passengers --
CREATE TABLE IF NOT EXISTS passengers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
booking_id UUID NOT NULL REFERENCES bookings(id) ON DELETE CASCADE,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
date_of_birth DATE,
passport_number TEXT NOT NULL DEFAULT '',
seat_number TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_passengers_booking ON passengers(booking_id);