chore(db): migrate tables to CHAR(12) short ID primary keys and add new tables

Add generate_*_id() functions for consistent short ID generation across all tables.

- Convert user_social_logins, verification_codes, custom_services,
  time_blockers, forgiven_no_shows, user_referrals from BIGSERIAL to CHAR(12)
- Add name_history, referral_discounts tables
- Add dedicated generate_*_id() functions for each table
- Add FK from name_history.booking_id to bookings(id)
- Add referral_discounts index on user_id

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-06-20 16:56:37 +01:00
co-authored by Sisyphus
parent 7f9c246818
commit 482958584a
+154 -23
View File
@@ -84,6 +84,82 @@ CREATE OR REPLACE FUNCTION generate_user_patch_test_id() RETURNS CHAR(12) AS $$
SELECT generate_short_id('user_patch_tests');
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_user_referrals_id() RETURNS CHAR(12) AS $$
SELECT generate_short_id('user_referrals');
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_referral_discounts_id() RETURNS CHAR(12) AS $$
SELECT generate_short_id('referral_discounts');
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_user_social_logins_id() RETURNS CHAR(12) AS $$
SELECT generate_short_id('user_social_logins');
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_name_history_id() RETURNS CHAR(12) AS $$
SELECT generate_short_id('name_history');
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_custom_services_id() RETURNS CHAR(12) AS $$
SELECT generate_short_id('custom_services');
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_verification_codes_id() RETURNS CHAR(12) AS $$
SELECT generate_short_id('verification_codes');
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_time_blockers_id() RETURNS CHAR(12) AS $$
SELECT generate_short_id('time_blockers');
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_forgiven_no_shows_id() RETURNS CHAR(12) AS $$
SELECT generate_short_id('forgiven_no_shows');
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_loyalty_redemptions_id() RETURNS CHAR(12) AS $$
SELECT generate_short_id('loyalty_redemptions');
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_discount_campaigns_id() RETURNS CHAR(12) AS $$
SELECT generate_short_id('discount_campaigns');
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_booking_discounts_id() RETURNS CHAR(12) AS $$
SELECT generate_short_id('booking_discounts');
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_tags_id() RETURNS CHAR(12) AS $$
SELECT generate_short_id('tags');
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_admin_notifications_id() RETURNS CHAR(12) AS $$
SELECT generate_short_id('admin_notifications');
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_user_notification_preferences_id() RETURNS CHAR(12) AS $$
SELECT generate_short_id('user_notification_preferences');
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_images_id() RETURNS CHAR(12) AS $$
SELECT generate_short_id('images');
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_gift_cards_id() RETURNS CHAR(12) AS $$
SELECT generate_short_id('gift_cards');
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_gift_card_transactions_id() RETURNS CHAR(12) AS $$
SELECT generate_short_id('gift_card_transactions');
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_gift_card_expired_balances_id() RETURNS CHAR(12) AS $$
SELECT generate_short_id('gift_card_expired_balances');
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_admin_audit_log_id() RETURNS CHAR(12) AS $$
SELECT generate_short_id('admin_audit_log');
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_verification_code() RETURNS CHAR(12) AS $$ SELECT substr(encode(gen_random_bytes(6), 'hex'), 1, 12); $$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION generate_referral_code()
@@ -153,7 +229,7 @@ CREATE TABLE users (
);
CREATE TABLE user_social_logins (
id BIGSERIAL PRIMARY KEY,
id CHAR(12) PRIMARY KEY DEFAULT generate_user_social_logins_id(),
user_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider account_type NOT NULL CHECK (provider IN ('google', 'microsoft', 'facebook')),
immutable_id TEXT NOT NULL, -- the stable provider user ID
@@ -176,13 +252,32 @@ CREATE INDEX idx_users_phone_trgm ON users USING GIN (phone gin_trgm_ops);
-- Guest accounts can share emails; registered accounts cannot
CREATE UNIQUE INDEX idx_users_email_registered ON users (email) WHERE account_role != 'guest';
-- =======================================
-- NAME HISTORY TABLE
-- =======================================
-- WHY: Tracks user-initiated first/last name changes for display in booking modals
-- and GDPR export. History is used to show "(formerly [old first] [old last])"
-- on completed booking receipts and admin views.
CREATE TABLE name_history (
id CHAR(12) PRIMARY KEY DEFAULT generate_name_history_id(),
user_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
previous_first_name VARCHAR(50) NOT NULL,
previous_last_name VARCHAR(50) NOT NULL,
booking_id CHAR(12),
changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_name_history_user_id ON name_history(user_id);
CREATE INDEX idx_name_history_changed_at ON name_history(changed_at);
-- =======================================
-- VERIFICATION CODES TABLE
-- =======================================
CREATE TYPE verification_purpose AS ENUM ('email_verify', 'password_reset');
CREATE TABLE verification_codes (
id BIGSERIAL PRIMARY KEY,
id CHAR(12) PRIMARY KEY DEFAULT generate_verification_codes_id(),
code CHAR(12) NOT NULL UNIQUE DEFAULT generate_verification_code(),
user_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
purpose verification_purpose NOT NULL,
@@ -248,7 +343,7 @@ CREATE INDEX idx_services_name ON services(name);
-- =======================================
CREATE TABLE custom_services (
id CHAR(12) PRIMARY KEY DEFAULT generate_short_id('custom_services'),
id CHAR(12) PRIMARY KEY DEFAULT generate_custom_services_id(),
name VARCHAR(100) NOT NULL,
description TEXT,
price NUMERIC(10,2) NOT NULL,
@@ -288,6 +383,9 @@ CREATE INDEX idx_bookings_starttime ON bookings(start_time);
CREATE INDEX idx_bookings_status ON bookings(status);
CREATE INDEX idx_bookings_userid_starttime ON bookings(user_id, start_time);
ALTER TABLE name_history ADD CONSTRAINT fk_name_history_booking_id
FOREIGN KEY (booking_id) REFERENCES bookings(id) ON DELETE SET NULL;
-- =======================================
-- BOOKING SERVICES JUNCTION TABLE
-- =======================================
@@ -335,7 +433,7 @@ CREATE TABLE booking_edit_requests (
CREATE INDEX idx_booking_edit_requests_booking ON booking_edit_requests(booking_id);
CREATE TABLE user_referrals (
id BIGSERIAL PRIMARY KEY,
id CHAR(12) PRIMARY KEY DEFAULT generate_user_referrals_id(),
referrer_id CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
referred_id CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
referred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
@@ -343,6 +441,20 @@ CREATE TABLE user_referrals (
UNIQUE (referrer_id, referred_id)
);
-- =======================================
-- REFERRAL DISCOUNTS TABLE
-- =======================================
CREATE TABLE referral_discounts (
id CHAR(12) PRIMARY KEY DEFAULT generate_referral_discounts_id(),
user_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
referral_id CHAR(12) NOT NULL REFERENCES user_referrals(id) ON DELETE CASCADE,
discount_percent NUMERIC(5,2) NOT NULL DEFAULT 10.00,
used BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
used_at TIMESTAMPTZ
);
CREATE INDEX idx_referral_discounts_user_id ON referral_discounts(user_id);
-- =======================================
-- DEFAULT WORKING HOURS TABLE
-- =======================================
@@ -402,7 +514,7 @@ CREATE UNIQUE INDEX idx_group_application_week ON exceptional_group_applications
-- Used for doctor appointments, extended lunch, training, etc.
CREATE TABLE time_blockers (
id CHAR(12) PRIMARY KEY DEFAULT generate_short_id('time_blockers'),
id CHAR(12) PRIMARY KEY DEFAULT generate_time_blockers_id(),
start_time TIMESTAMPTZ NOT NULL,
duration_minutes INT NOT NULL CHECK (duration_minutes > 0),
description TEXT,
@@ -420,7 +532,7 @@ CREATE INDEX idx_time_blockers_cron ON time_blockers(cron_expression) WHERE cron
-- =======================================
-- Tracks which no-shows have been forgiven (by full deposit payment)
CREATE TABLE forgiven_no_shows (
id CHAR(12) PRIMARY KEY DEFAULT generate_short_id('forgiven_no_shows'),
id CHAR(12) PRIMARY KEY DEFAULT generate_forgiven_no_shows_id(),
booking_id CHAR(12) NOT NULL UNIQUE REFERENCES bookings(id) ON DELETE CASCADE,
forgiven_by CHAR(12),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
@@ -476,7 +588,7 @@ CREATE INDEX idx_payments_created_at_status ON payments(created_at, status);
-- LOYALTY REDEMPTIONS TABLE
-- =======================================
CREATE TABLE loyalty_redemptions (
id CHAR(12) PRIMARY KEY DEFAULT generate_short_id('loyalty_redemptions'),
id CHAR(12) PRIMARY KEY DEFAULT generate_loyalty_redemptions_id(),
user_id CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
stamps_redeemed INT NOT NULL DEFAULT 10,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
@@ -493,7 +605,7 @@ CREATE INDEX idx_loyalty_redemptions_status ON loyalty_redemptions(status);
-- DISCOUNT CAMPAIGNS TABLE
-- =======================================
CREATE TABLE discount_campaigns (
id CHAR(12) PRIMARY KEY DEFAULT generate_short_id('discount_campaigns'),
id CHAR(12) PRIMARY KEY DEFAULT generate_discount_campaigns_id(),
name VARCHAR(100) NOT NULL,
description TEXT,
campaign_type campaign_type NOT NULL DEFAULT 'time_based',
@@ -523,7 +635,7 @@ CREATE INDEX idx_discount_campaigns_type ON discount_campaigns(campaign_type);
-- BOOKING DISCOUNTS TABLE
-- =======================================
CREATE TABLE booking_discounts (
id CHAR(12) PRIMARY KEY DEFAULT generate_short_id('booking_discounts'),
id CHAR(12) PRIMARY KEY DEFAULT generate_booking_discounts_id(),
booking_id CHAR(12) NOT NULL REFERENCES bookings(id) ON DELETE CASCADE,
user_id CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
discount_source VARCHAR(30) NOT NULL,
@@ -607,7 +719,7 @@ INSERT INTO business_settings (
CREATE TYPE admin_notification_reason AS ENUM ('pending_booking', 'cancelled_booking', 'rescheduled_booking', '1_week_no_pay', '1_month_no_pay', 'affiliate_claim', 'late_cancellation', 'deposit_paid', 'edit_request', 'edit_requested', 'new_booking', 'deposit_not_paid_by_deadline');
CREATE TABLE admin_notifications (
id SERIAL PRIMARY KEY,
id CHAR(12) PRIMARY KEY DEFAULT generate_admin_notifications_id(),
reason admin_notification_reason NOT NULL,
booking_id CHAR(12) REFERENCES bookings(id),
user_id CHAR(12) REFERENCES users(id),
@@ -617,7 +729,7 @@ CREATE TABLE admin_notifications (
-- User notification preferences for future user notification system
CREATE TABLE user_notification_preferences (
id SERIAL PRIMARY KEY,
id CHAR(12) PRIMARY KEY DEFAULT generate_user_notification_preferences_id(),
user_id CHAR(12) REFERENCES users(id) ON DELETE CASCADE,
email_enabled BOOLEAN DEFAULT false,
sms_enabled BOOLEAN DEFAULT false,
@@ -631,7 +743,7 @@ CREATE INDEX idx_bookings_start_time_status ON bookings(start_time, status);
CREATE INDEX idx_users_created_at ON users(created_at);
create table images (
id uuid primary key default gen_random_uuid(),
id CHAR(12) PRIMARY KEY DEFAULT generate_images_id(),
url text not null,
thumbnail_url text not null,
tag_names text[] not null default '{}',
@@ -647,7 +759,7 @@ create table images (
-- only for autocomplete text
create table tags (
id serial primary key,
id CHAR(12) PRIMARY KEY DEFAULT generate_tags_id(),
name text not null unique
);
@@ -917,6 +1029,16 @@ BEGIN
)
)
),
'referral_discounts', (
SELECT COALESCE(json_agg(json_build_object(
'id', rd.id,
'discount_percent', rd.discount_percent,
'used', rd.used,
'created_at', rd.created_at,
'used_at', rd.used_at
) ORDER BY rd.created_at DESC), '[]'::json)
FROM referral_discounts rd WHERE rd.user_id = target_user_id
),
'notification_preferences', (
SELECT COALESCE(json_agg(
json_build_object(
@@ -1074,22 +1196,31 @@ BEGIN
WHERE gct.user_id = target_user_id
),
'login_audit', (
(SELECT json_agg(json_build_object(
SELECT COALESCE(json_agg(json_build_object(
'attempt_type', la.attempt_type,
'ip_address', la.ip_address::text,
'success', la.success,
'created_at', la.created_at
) ORDER BY la.created_at DESC)
FROM login_audit la WHERE la.user_id = target_user_id)
) ORDER BY la.created_at DESC), '[]'::json)
FROM login_audit la WHERE la.user_id = target_user_id
),
'refresh_tokens', (
(SELECT json_agg(json_build_object(
SELECT COALESCE(json_agg(json_build_object(
'role', rt.role,
'revoked', rt.revoked,
'created_at', rt.created_at,
'expires_at', rt.expires_at
) ORDER BY rt.created_at DESC)
FROM refresh_tokens rt WHERE rt.user_id = target_user_id)
) ORDER BY rt.created_at DESC), '[]'::json)
FROM refresh_tokens rt WHERE rt.user_id = target_user_id
),
'name_history', (
SELECT COALESCE(json_agg(json_build_object(
'previous_first_name', nh.previous_first_name,
'previous_last_name', nh.previous_last_name,
'booking_id', nh.booking_id,
'changed_at', nh.changed_at
) ORDER BY nh.changed_at DESC), '[]'::json)
FROM name_history nh WHERE nh.user_id = target_user_id
),
'export_metadata', json_build_object(
'exported_at', NOW(),
@@ -1802,7 +1933,7 @@ CREATE INDEX idx_affiliate_payouts_affiliate ON affiliate_payouts(affiliate_id);
-- =======================================
CREATE TABLE gift_cards (
id CHAR(12) PRIMARY KEY DEFAULT generate_short_id('gift_cards'),
id CHAR(12) PRIMARY KEY DEFAULT generate_gift_cards_id(),
total_funds_added NUMERIC(12,2) NOT NULL DEFAULT 0,
amount_remaining NUMERIC(12,2) NOT NULL DEFAULT 0,
created_by CHAR(12) REFERENCES users(id),
@@ -1845,7 +1976,7 @@ CREATE TABLE user_giftcard_balances (
-- =======================================
CREATE TABLE gift_card_transactions (
id CHAR(12) PRIMARY KEY DEFAULT generate_short_id('gift_card_transactions'),
id CHAR(12) PRIMARY KEY DEFAULT generate_gift_card_transactions_id(),
gift_card_id CHAR(12) NOT NULL REFERENCES gift_cards(id),
transaction_type VARCHAR(20) NOT NULL,
amount NUMERIC(10,2) NOT NULL,
@@ -1879,7 +2010,7 @@ CREATE INDEX idx_gift_card_transactions_type ON gift_card_transactions(transacti
-- =======================================
CREATE TABLE gift_card_expired_balances (
id CHAR(12) PRIMARY KEY DEFAULT generate_short_id('gift_card_expired_balances'),
id CHAR(12) PRIMARY KEY DEFAULT generate_gift_card_expired_balances_id(),
account_id CHAR(12) NOT NULL,
original_balance NUMERIC(10,2) NOT NULL,
expired_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
@@ -1901,7 +2032,7 @@ CREATE INDEX idx_gift_card_transactions_created_at ON gift_card_transactions(cre
-- =======================================
CREATE TABLE admin_audit_log (
id CHAR(12) PRIMARY KEY DEFAULT generate_short_id('admin_audit_log'),
id CHAR(12) PRIMARY KEY DEFAULT generate_admin_audit_log_id(),
admin_id CHAR(12) NOT NULL REFERENCES users(id),
action_type VARCHAR(30) NOT NULL,
target_user_id CHAR(12) REFERENCES users(id),