-- Membership: platform-managed levels, an append-only growth ledger, and the -- customer's current level on `users`. Messaging: per-customer system messages -- emitted by order/shipment/refund events with unread state and soft deletion. CREATE TABLE member_levels ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name JSONB NOT NULL, icon TEXT NOT NULL, growth_threshold BIGINT NOT NULL CHECK (growth_threshold >= 0), benefits JSONB NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT member_levels_icon CHECK (btrim(icon) <> '') ); -- Thresholds are unique; level order follows the threshold. CREATE UNIQUE INDEX member_levels_threshold_idx ON member_levels (growth_threshold); ALTER TABLE users ADD COLUMN level UUID REFERENCES member_levels (id); CREATE INDEX users_level_idx ON users (level); CREATE TABLE growth_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE, -- Growth value in whole base-currency units; 0 only for sub-unit orders. delta BIGINT NOT NULL CHECK (delta >= 0), growth_total BIGINT NOT NULL CHECK (growth_total >= 0), reason TEXT NOT NULL, reference_type TEXT, reference_id UUID, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- One accrual per user and referenced order, so a retried completion is a no-op. CREATE UNIQUE INDEX growth_logs_reference_idx ON growth_logs (user_id, reference_type, reference_id) WHERE reference_id IS NOT NULL; CREATE INDEX growth_logs_user_idx ON growth_logs (user_id, created_at DESC, id DESC); CREATE TYPE message_kind AS ENUM ('order_paid', 'order_shipped', 'refund_completed'); CREATE TYPE message_status AS ENUM ('unread', 'read'); CREATE TABLE messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE, kind message_kind NOT NULL, title JSONB NOT NULL, body JSONB NOT NULL, reference_type TEXT, reference_id UUID, status message_status NOT NULL DEFAULT 'unread', read_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT messages_read_consistent CHECK ( (status = 'unread' AND read_at IS NULL) OR (status = 'read' AND read_at IS NOT NULL) ) ); -- One message per customer, kind, and reference: re-running an event handler -- conflicts instead of duplicating. Soft-deleted rows still occupy the slot so -- a re-run cannot resurrect a deleted message as a new one. CREATE UNIQUE INDEX messages_event_idx ON messages (user_id, kind, reference_type, reference_id) WHERE reference_id IS NOT NULL; CREATE INDEX messages_user_idx ON messages (user_id, created_at DESC, id DESC) WHERE deleted_at IS NULL; CREATE INDEX messages_unread_idx ON messages (user_id) WHERE deleted_at IS NULL AND status = 'unread';