-- Customer commercial profile foundation. -- Rollback boundary: later slices can ignore these additive tables and continue -- reading legacy subscriptions. Existing subscription rows are not modified. ALTER TABLE access_requests DROP CONSTRAINT IF EXISTS access_requests_status_check; ALTER TABLE access_requests ADD CONSTRAINT access_requests_status_check CHECK (status IN ('pending', 'pending_classification', 'in_review', 'approved', 'rejected')); CREATE TABLE IF NOT EXISTS customer_commercial_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), access_request_id UUID REFERENCES access_requests(id) ON DELETE SET NULL, project_id UUID REFERENCES edge_projects(id) ON DELETE CASCADE, status VARCHAR(30) NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'in_review', 'approved', 'active', 'suspended', 'archived')), plan VARCHAR(30) NOT NULL DEFAULT 'free' CHECK (plan IN ('free', 'operative', 'enterprise')), payment_method VARCHAR(40) CHECK (payment_method IS NULL OR payment_method IN ('none', 'bank_transfer', 'manual_invoice', 'enterprise_contract')), payment_terms_days INT CHECK (payment_terms_days IS NULL OR payment_terms_days >= 0), credit_limit_cop BIGINT CHECK (credit_limit_cop IS NULL OR credit_limit_cop >= 0), tg_limit NUMERIC(12, 2) CHECK (tg_limit IS NULL OR tg_limit >= 0), pbase_cop BIGINT NOT NULL DEFAULT 110000 CHECK (pbase_cop > 0), discount_k NUMERIC(6, 4) NOT NULL DEFAULT 0.1000 CHECK (discount_k >= 0), currency CHAR(3) NOT NULL DEFAULT 'COP', effective_from DATE NOT NULL DEFAULT CURRENT_DATE, effective_to DATE CHECK (effective_to IS NULL OR effective_to >= effective_from), approved_by UUID REFERENCES users(id) ON DELETE SET NULL, approved_at TIMESTAMPTZ, enterprise_annual_cop BIGINT CHECK (enterprise_annual_cop IS NULL OR enterprise_annual_cop >= 0), enterprise_monthly_cop BIGINT CHECK (enterprise_monthly_cop IS NULL OR enterprise_monthly_cop >= 0), enterprise_override_reason TEXT, notes TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CHECK (access_request_id IS NOT NULL OR project_id IS NOT NULL) ); CREATE UNIQUE INDEX IF NOT EXISTS idx_customer_commercial_profiles_access_request ON customer_commercial_profiles(access_request_id) WHERE access_request_id IS NOT NULL; CREATE UNIQUE INDEX IF NOT EXISTS idx_customer_commercial_profiles_project ON customer_commercial_profiles(project_id) WHERE project_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_customer_commercial_profiles_status ON customer_commercial_profiles(status, effective_from DESC); CREATE INDEX IF NOT EXISTS idx_customer_commercial_profiles_plan ON customer_commercial_profiles(plan); CREATE TABLE IF NOT EXISTS customer_commercial_profile_versions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), commercial_profile_id UUID NOT NULL REFERENCES customer_commercial_profiles(id) ON DELETE CASCADE, actor_user_id UUID REFERENCES users(id) ON DELETE SET NULL, changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), change_reason TEXT NOT NULL, changed_fields TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], before_json JSONB NOT NULL DEFAULT '{}'::JSONB, after_json JSONB NOT NULL DEFAULT '{}'::JSONB ); CREATE INDEX IF NOT EXISTS idx_customer_commercial_profile_versions_profile ON customer_commercial_profile_versions(commercial_profile_id, changed_at DESC); CREATE INDEX IF NOT EXISTS idx_customer_commercial_profile_versions_actor ON customer_commercial_profile_versions(actor_user_id, changed_at DESC) WHERE actor_user_id IS NOT NULL; INSERT INTO customer_commercial_profiles ( project_id, status, plan, payment_method, payment_terms_days, tg_limit, pbase_cop, discount_k, effective_from, notes ) SELECT s.project_id, CASE WHEN s.status = 'suspended' THEN 'suspended' ELSE 'active' END, CASE WHEN s.tier = 'enterprise' THEN 'enterprise' ELSE 'operative' END, 'manual_invoice', 30, s.tag_limit::NUMERIC, COALESCE(NULLIF(s.price_per_tag, 90000), 110000), 0.1000, s.current_period_start::DATE, 'Backfilled from legacy subscription during commercial profile rollout.' FROM subscriptions s WHERE s.status IN ('active', 'trial', 'suspended') ON CONFLICT DO NOTHING;