clavitor/clavitor.ai/admin/schema.sql

574 lines
26 KiB
SQL

-- Clavitor Corporate DB Schema
-- 95% Paddle format + 5% our extensions
-- ============================================
-- PADDLE PRODUCT CATALOG
-- ============================================
CREATE TABLE products (
id TEXT PRIMARY KEY, -- pro_xxx (Paddle ID)
name TEXT NOT NULL, -- "Personal", "Family", "Pro"
description TEXT,
tax_category TEXT, -- Paddle tax category
image_url TEXT,
custom_data TEXT, -- JSON for our use
status TEXT NOT NULL DEFAULT 'active', -- active, archived
created_at INTEGER NOT NULL,
updated_at INTEGER
);
CREATE TABLE prices (
id TEXT PRIMARY KEY, -- pri_xxx (Paddle ID)
product_id TEXT NOT NULL REFERENCES products(id),
description TEXT, -- Internal notes
name TEXT, -- Shown to customers: "Yearly USD"
billing_cycle TEXT, -- JSON: {"interval": "year", "frequency": 1}
trial_period TEXT, -- JSON or null: {"interval": "day", "frequency": 7}
tax_mode TEXT DEFAULT 'account_setting',
unit_price TEXT NOT NULL, -- JSON: {"amount": "1200", "currency_code": "USD"}
unit_price_overrides TEXT, -- JSON array for country-specific pricing
quantity TEXT, -- JSON: {"minimum": 1, "maximum": null}
status TEXT NOT NULL DEFAULT 'active',
custom_data TEXT, -- JSON: our plan identifier (Personal/Pro/etc)
created_at INTEGER NOT NULL,
updated_at INTEGER
);
CREATE INDEX idx_prices_product ON prices(product_id);
CREATE TABLE discounts (
id TEXT PRIMARY KEY, -- dsc_xxx (Paddle ID)
status TEXT NOT NULL DEFAULT 'active',
description TEXT NOT NULL, -- "Johan MSP - 30% margin"
enabled_for_checkout BOOLEAN DEFAULT 0, -- We apply manually, not customer-facing
code TEXT, -- Null for MSP margins
type TEXT NOT NULL, -- percentage, flat, flat_per_seat
mode TEXT DEFAULT 'standard',
amount TEXT NOT NULL, -- "30" for 30%
currency_code TEXT, -- Only for flat types
recur BOOLEAN DEFAULT 1, -- True for subscriptions
maximum_recurring_intervals INTEGER,
usage_limit INTEGER,
restrict_to TEXT, -- JSON array of product/price IDs
expires_at INTEGER,
times_used INTEGER DEFAULT 0,
discount_group_id TEXT, -- dsg_xxx for grouping
custom_data TEXT, -- JSON: {"msp_id": "xxx", "tier": "standard"}
created_at INTEGER NOT NULL,
updated_at INTEGER
);
-- ============================================
-- PADDLE CUSTOMERS + OUR HIERARCHY
-- ============================================
CREATE TABLE customers (
id TEXT PRIMARY KEY, -- ctm_xxx (Paddle ID)
-- PADDLE FIELDS (95%)
email TEXT NOT NULL,
name TEXT,
locale TEXT DEFAULT 'en',
status TEXT NOT NULL DEFAULT 'active', -- active, archived
marketing_consent BOOLEAN DEFAULT 0,
import_meta TEXT, -- JSON if imported
-- OUR EXTENSIONS (5%)
parent_id TEXT REFERENCES customers(id), -- Hierarchy: MSP → Customer → Dept
level INTEGER NOT NULL DEFAULT 1, -- 1=MSP, 2=EndCustomer, 3=Dept
internal_notes TEXT,
custom_data TEXT, -- JSON: {"source": "web", "segment": "startup"}
created_at INTEGER NOT NULL,
updated_at INTEGER
);
CREATE INDEX idx_customers_parent ON customers(parent_id);
CREATE INDEX idx_customers_level ON customers(level);
CREATE TABLE addresses (
id TEXT PRIMARY KEY, -- add_xxx (Paddle ID)
customer_id TEXT NOT NULL REFERENCES customers(id),
description TEXT,
city TEXT,
region TEXT,
country_code TEXT NOT NULL, -- ISO 3166-1 alpha-2
zip_code TEXT,
status TEXT NOT NULL DEFAULT 'active',
created_at INTEGER NOT NULL,
updated_at INTEGER
);
CREATE INDEX idx_addresses_customer ON addresses(customer_id);
CREATE TABLE businesses (
id TEXT PRIMARY KEY, -- biz_xxx (Paddle ID)
customer_id TEXT NOT NULL REFERENCES customers(id),
name TEXT NOT NULL, -- Company name
company_number TEXT,
tax_identifier TEXT, -- VAT number
contacts TEXT, -- JSON: [{"name": "", "email": ""}]
status TEXT NOT NULL DEFAULT 'active',
custom_data TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER
);
CREATE INDEX idx_businesses_customer ON businesses(customer_id);
-- ============================================
-- PADDLE BILLING + OUR VAULT LINK
-- ============================================
CREATE TABLE subscriptions (
id TEXT PRIMARY KEY, -- sub_xxx (Paddle ID)
-- PADDLE REFERENCES
customer_id TEXT NOT NULL REFERENCES customers(id),
address_id TEXT NOT NULL REFERENCES addresses(id),
business_id TEXT REFERENCES businesses(id), -- Optional for B2B
-- PADDLE BILLING
currency_code TEXT NOT NULL,
status TEXT NOT NULL, -- active, past_due, paused, canceled
collection_mode TEXT NOT NULL DEFAULT 'automatic', -- automatic, manual
billing_details TEXT, -- JSON: payment_terms, etc.
discount TEXT, -- JSON: {id, starts_at, ends_at, type}
-- PADDLE TIMESTAMPS
started_at INTEGER,
first_billed_at INTEGER,
next_billed_at INTEGER,
paused_at INTEGER,
canceled_at INTEGER,
-- OUR EXTENSION (5%): Link to vault
vault_l0 TEXT, -- Which vault this subscription pays for
-- PADDLE METADATA
billing_cycle TEXT, -- JSON: {interval, frequency}
current_billing_period TEXT, -- JSON: {starts_at, ends_at}
scheduled_change TEXT, -- JSON: {action, effective_at, resume_at}
items TEXT NOT NULL, -- JSON array of subscription items
management_urls TEXT, -- JSON: {update_payment_method, cancel}
custom_data TEXT,
import_meta TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER,
consent_requirements TEXT -- JSON array
);
CREATE INDEX idx_subscriptions_customer ON subscriptions(customer_id);
CREATE INDEX idx_subscriptions_vault ON subscriptions(vault_l0);
CREATE INDEX idx_subscriptions_status ON subscriptions(status);
CREATE TABLE subscription_items (
id TEXT PRIMARY KEY, -- Paddle item ID
subscription_id TEXT NOT NULL REFERENCES subscriptions(id),
price_id TEXT NOT NULL, -- pri_xxx
product_id TEXT NOT NULL, -- pro_xxx
quantity INTEGER NOT NULL DEFAULT 1,
status TEXT NOT NULL DEFAULT 'active',
recurring BOOLEAN DEFAULT 1,
created_at INTEGER NOT NULL,
updated_at INTEGER,
previously_billed_at INTEGER,
next_billed_at INTEGER,
trial_dates TEXT, -- JSON: {starts_at, ends_at}
price_snapshot TEXT, -- JSON: price entity at time of add
product_snapshot TEXT -- JSON: product entity at time of add
);
CREATE INDEX idx_items_subscription ON subscription_items(subscription_id);
CREATE TABLE transactions (
id TEXT PRIMARY KEY, -- txn_xxx (Paddle ID)
subscription_id TEXT REFERENCES subscriptions(id),
customer_id TEXT NOT NULL REFERENCES customers(id),
address_id TEXT NOT NULL REFERENCES addresses(id),
business_id TEXT REFERENCES businesses(id),
invoice_id TEXT, -- Deprecated, but Paddle includes it
invoice_number TEXT,
discount_id TEXT REFERENCES discounts(id),
-- TRANSACTION STATUS
status TEXT NOT NULL, -- ready, billed, completed, past_due, canceled
origin TEXT, -- checkout, api, subscription_renewal, etc.
-- FINANCIALS (from Paddle 'details' object)
currency_code TEXT NOT NULL,
details TEXT NOT NULL, -- JSON: totals, tax_rates_used, line_items
payments TEXT, -- JSON array of payment attempts
-- ADJUSTED TOTALS (post-adjustments)
adjusted_totals TEXT,
payout_totals TEXT,
adjusted_payout_totals TEXT,
-- INVOICE
billing_details TEXT, -- JSON: enable_checkout, payment_terms, etc.
additional_information TEXT, -- Notes on invoice
purchase_order_number TEXT,
-- CHECKOUT (if applicable)
checkout_url TEXT,
-- TIMESTAMPS
billed_at INTEGER,
completed_at INTEGER,
revised_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER,
-- OUR EXTENSION: Track if we processed this
processed_at INTEGER,
processing_error TEXT
);
CREATE INDEX idx_transactions_subscription ON transactions(subscription_id);
CREATE INDEX idx_transactions_customer ON transactions(customer_id);
CREATE INDEX idx_transactions_status ON transactions(status);
CREATE INDEX idx_transactions_billed ON transactions(billed_at);
CREATE TABLE adjustments (
id TEXT PRIMARY KEY, -- adj_xxx (Paddle ID)
transaction_id TEXT NOT NULL REFERENCES transactions(id),
subscription_id TEXT REFERENCES subscriptions(id),
customer_id TEXT NOT NULL REFERENCES customers(id),
action TEXT NOT NULL, -- chargeback, credit, refund
type TEXT, -- partial, full, tax, proration
amount TEXT NOT NULL, -- "1200" cents
currency_code TEXT NOT NULL,
totals TEXT NOT NULL, -- JSON: adjusted totals
payout_totals TEXT,
reason TEXT,
credit_applied_to TEXT, -- For credit adjustments
created_at INTEGER NOT NULL
);
-- ============================================
-- OUR VAULT-SPECIFIC DATA (Beyond Paddle)
-- ============================================
CREATE TABLE vaults (
l0 TEXT PRIMARY KEY, -- First 4 bytes of L3, hex
customer_id TEXT NOT NULL REFERENCES customers(id),
subscription_id TEXT REFERENCES subscriptions(id),
status TEXT NOT NULL DEFAULT 'active', -- active, suspended, archived
created_at INTEGER NOT NULL,
last_seen_at INTEGER,
-- For audit trail
created_by_human INTEGER -- Who created this vault (if tracked)
);
CREATE INDEX idx_vaults_customer ON vaults(customer_id);
-- WL3 Registry (for POP sync) - OUR DATA
CREATE TABLE credentials (
id INTEGER PRIMARY KEY, -- Our 8-byte int64
customer_id TEXT NOT NULL REFERENCES customers(id),
p0 TEXT NOT NULL, -- First 4 bytes of PRF (lookup key)
l0 TEXT NOT NULL REFERENCES vaults(l0),
credential_id BLOB NOT NULL, -- WebAuthn credential ID
wl3_path TEXT NOT NULL, -- Path in storage: a1/a1b2c3d4/20260405_abc.cla
enrolled_by INTEGER, -- Who authorized this device
created_at INTEGER NOT NULL,
last_used_at INTEGER
);
CREATE INDEX idx_credentials_customer ON credentials(customer_id);
CREATE INDEX idx_credentials_l0 ON credentials(l0);
CREATE INDEX idx_credentials_p0 ON credentials(p0);
CREATE INDEX idx_credentials_created ON credentials(created_at);
-- Our RBAC (not in Paddle - our internal access control)
CREATE TABLE roles (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE, -- SuperAdmin, MSPAdmin, TenantAdmin, VaultUser
permissions TEXT NOT NULL, -- JSON
description TEXT,
created_at INTEGER NOT NULL
);
-- Pre-populate
INSERT INTO roles (id, name, permissions, description, created_at) VALUES
(1, 'SuperAdmin', '{"all": true}', 'Full system access', strftime('%s', 'now')),
(2, 'MSPAdmin', '{"customer_admin": true, "billing_view": true}', 'MSP-level admin', strftime('%s', 'now')),
(3, 'TenantAdmin', '{"vault_create": true, "agent_create": true, "device_enroll": true}', 'Customer admin', strftime('%s', 'now')),
(4, 'VaultUser', '{"vault_read": true, "vault_write": true}', 'Regular user', strftime('%s', 'now')),
(5, 'BillingAdmin', '{"billing_view": true, "invoices": true}', 'Can view/pay invoices', strftime('%s', 'now')),
(6, 'ReadOnly', '{"vault_read": true}', 'View only', strftime('%s', 'now'));
-- Our internal users (humans in our system)
CREATE TABLE humans (
id INTEGER PRIMARY KEY, -- Our 8-byte int64
customer_id TEXT REFERENCES customers(id), -- Can be null for our internal staff
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
created_at INTEGER NOT NULL,
updated_at INTEGER
);
-- RBAC: Who has what role for which customer/tenant
CREATE TABLE role_assignments (
id INTEGER PRIMARY KEY,
human_id INTEGER NOT NULL REFERENCES humans(id),
customer_id TEXT NOT NULL REFERENCES customers(id), -- The tenant they have access to
role_id INTEGER NOT NULL REFERENCES roles(id),
granted_by INTEGER REFERENCES humans(id),
created_at INTEGER NOT NULL,
UNIQUE(human_id, customer_id, role_id)
);
CREATE INDEX idx_role_assignments_human ON role_assignments(human_id);
CREATE INDEX idx_role_assignments_customer ON role_assignments(customer_id);
-- ============================================
-- POP SYNC STATE (Our distributed system)
-- ============================================
CREATE TABLE pop_sync_state (
pop_id TEXT PRIMARY KEY, -- e.g., "zurich-01", "singapore-02"
last_sync_at INTEGER NOT NULL,
last_sync_cursor INTEGER, -- Last credential.id synced
status TEXT NOT NULL DEFAULT 'active', -- active, paused, error
error_message TEXT,
updated_at INTEGER NOT NULL
);
-- ============================================
-- AUDIT LOG (Our record keeping)
-- ============================================
CREATE TABLE events (
id INTEGER PRIMARY KEY,
occurred_at INTEGER NOT NULL,
-- What happened
event_type TEXT NOT NULL, -- customer_created, subscription_started, payment_received, etc.
entity_type TEXT, -- customer, subscription, transaction, vault, etc.
entity_id TEXT, -- The Paddle or internal ID
-- Who did it / Who it affected
customer_id TEXT REFERENCES customers(id),
human_id INTEGER REFERENCES humans(id),
vault_l0 TEXT,
-- Details
amount_cents INTEGER,
currency_code TEXT,
details TEXT, -- JSON: full event data
-- Source
source TEXT DEFAULT 'api', -- api, webhook, system
ip_address TEXT,
user_agent TEXT
);
CREATE INDEX idx_events_time ON events(occurred_at);
CREATE INDEX idx_events_customer ON events(customer_id);
CREATE INDEX idx_events_type ON events(event_type);
-- ============================================
-- SYNC TRACKING FIELDS (for dual-write layer)
-- ============================================
-- Track when we last synced with Paddle and what event we processed
-- This helps detect: "We haven't heard from Paddle in X minutes, retry"
ALTER TABLE customers ADD COLUMN last_paddle_sync_at INTEGER; -- Timestamp of last successful sync
ALTER TABLE customers ADD COLUMN paddle_event_id TEXT; -- Last webhook event ID processed
ALTER TABLE customers ADD COLUMN pending_since INTEGER; -- When operation was queued for retry logic
ALTER TABLE subscriptions ADD COLUMN last_paddle_sync_at INTEGER;
ALTER TABLE subscriptions ADD COLUMN paddle_event_id TEXT;
ALTER TABLE subscriptions ADD COLUMN pending_since INTEGER;
ALTER TABLE discounts ADD COLUMN last_paddle_sync_at INTEGER;
ALTER TABLE discounts ADD COLUMN paddle_event_id TEXT;
ALTER TABLE discounts ADD COLUMN pending_since INTEGER;
-- Index for finding stale pending syncs (retry after 5 minutes)
CREATE INDEX idx_subscriptions_pending_sync
ON subscriptions(sync_status, pending_since)
WHERE sync_status = 'pending';
CREATE INDEX idx_customers_pending_sync
ON customers(sync_status, pending_since)
WHERE sync_status = 'pending';
-- Index for finding records that haven't synced in a while
CREATE INDEX idx_subscriptions_stale_sync
ON subscriptions(last_paddle_sync_at)
WHERE last_paddle_sync_at IS NULL OR last_paddle_sync_at < strftime('%s', 'now', '-1 hour');
-- ============================================
-- WL3 STORAGE (for POP distribution)
-- ============================================
-- TODO: Replace with WORM filesystem (256 shards)
-- Path format: {shard}/{p0}/{timestamp}_{random}.cla
CREATE TABLE wl3_storage (
path TEXT PRIMARY KEY,
content BLOB NOT NULL, -- The encrypted wrapped L3
p0 TEXT NOT NULL, -- For quick lookup
customer_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
sync_count INTEGER DEFAULT 0, -- How many POPs have pulled this
last_sync_at INTEGER
);
CREATE INDEX idx_wl3_p0 ON wl3_storage(p0);
CREATE INDEX idx_wl3_customer ON wl3_storage(customer_id);
CREATE INDEX idx_wl3_created ON wl3_storage(created_at);
-- TODO: SUPER DUPER SECURE POP AUTHENTICATION
-- Requirements:
-- 1. mTLS - POP presents certificate signed by our CA
-- 2. Signed requests - each request signed with POP's private key
-- 3. Replay protection - nonce + timestamp validation
-- 4. IP allowlist - only known POP IPs
-- 5. Rate limiting per POP ID
-- 6. Request signing with HMAC
-- 7. Audit logging of ALL POP access
-- 8. Short-lived tokens (5 min) with refresh
-- ============================================
-- CURRENCY CONVERSION & PRICING
-- ============================================
--
-- DESIGN PRINCIPLE: "Pretty Commercial Prices"
-- -------------------------------------------------
-- We NEVER show exact converted prices (e.g., $29.99 → €27.42).
--
-- Why? Because:
-- - €27.42 looks calculated, foreign, and suspicious
-- - €29 looks local, intentional, and trustworthy
-- - Psychological pricing works differently per market
--
-- CONVERSION STRATEGY:
-- 1. Convert USD amount using exchange rate
-- 2. Round UP to "pretty" numbers based on magnitude:
-- - Under €25: Round to nearest €0.99 (€23.21 → €24.00)
-- - €25-€100: Round to nearest €5 (€47.32 → €49.99)
-- - €100-€500: Round to nearest €10 (€123.45 → €129)
-- - €500+: Round to nearest €50 or €99 ending
-- 3. Never drop below margin floor (stored separately)
-- 4. Use currency-local patterns (USD loves .99, JPY loves round 100s)
--
-- This table tracks metadata needed for proper formatting.
CREATE TABLE currencies (
code TEXT PRIMARY KEY, -- ISO 4217: "USD", "EUR", "JPY"
name TEXT NOT NULL, -- "US Dollar", "Euro"
decimals INTEGER NOT NULL DEFAULT 2, -- Minor units: USD=2, JPY=0, BHD=3
exchange_rate REAL, -- vs USD (1 USD = X of this currency). NULL = not fetched yet
rate_fetched_at INTEGER, -- When rate was last updated
symbol TEXT, -- "$", "€", "¥"
symbol_position TEXT DEFAULT 'prefix', -- 'prefix' ($10) or 'suffix' (10€)
pretty_pattern TEXT DEFAULT 'x.99', -- 'x.99', 'x00', 'x99', 'x50'
is_active INTEGER DEFAULT 1, -- 1 = supported, 0 = disabled
created_at INTEGER NOT NULL,
updated_at INTEGER
);
-- Exchange rate sources (in priority order):
-- 1. Frankfurter API (free, ECB rates): https://api.frankfurter.app/latest?from=USD
-- 2. Open Exchange Rates (requires API key): https://openexchangerates.org/
-- 3. Paddle's pricing-preview (for customer-facing prices with tax)
--
-- NOTE: Exchange rates are fetched dynamically. Seed data below has NULL rates.
-- Run: go run scripts/update_rates.go to populate exchange_rate column.
-- Comprehensive ISO 4217 currency list (top 50 by trading volume)
-- Rates should be fetched from Frankfurter API (free, no API key required)
INSERT INTO currencies (code, name, decimals, exchange_rate, symbol, symbol_position, pretty_pattern, is_active, created_at) VALUES
-- Major reserve currencies (always active)
('USD', 'US Dollar', 2, NULL, '$', 'prefix', 'x.99', 1, strftime('%s', 'now')),
('EUR', 'Euro', 2, NULL, '', 'prefix', 'x.99', 1, strftime('%s', 'now')),
('GBP', 'British Pound', 2, NULL, '£', 'prefix', 'x.99', 1, strftime('%s', 'now')),
('JPY', 'Japanese Yen', 0, NULL, '¥', 'prefix', 'x00', 1, strftime('%s', 'now')),
('CNY', 'Chinese Yuan', 2, NULL, '¥', 'prefix', 'x.00', 1, strftime('%s', 'now')),
-- Other major trading currencies
('AUD', 'Australian Dollar', 2, NULL, 'A$', 'prefix', 'x.99', 1, strftime('%s', 'now')),
('CAD', 'Canadian Dollar', 2, NULL, 'C$', 'prefix', 'x.99', 1, strftime('%s', 'now')),
('CHF', 'Swiss Franc', 2, NULL, 'Fr', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('SEK', 'Swedish Krona', 2, NULL, 'kr', 'suffix', 'x.00', 1, strftime('%s', 'now')),
('NZD', 'New Zealand Dollar', 2, NULL, 'NZ$', 'prefix', 'x.99', 1, strftime('%s', 'now')),
('SGD', 'Singapore Dollar', 2, NULL, 'S$', 'prefix', 'x.99', 1, strftime('%s', 'now')),
('HKD', 'Hong Kong Dollar', 2, NULL, 'HK$', 'prefix', 'x.99', 1, strftime('%s', 'now')),
('NOK', 'Norwegian Krone', 2, NULL, 'kr', 'suffix', 'x.00', 1, strftime('%s', 'now')),
('MXN', 'Mexican Peso', 2, NULL, '$', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('INR', 'Indian Rupee', 2, NULL, '', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('BRL', 'Brazilian Real', 2, NULL, 'R$', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('ZAR', 'South African Rand', 2, NULL, 'R', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('KRW', 'South Korean Won', 0, NULL, '', 'prefix', 'x000', 1, strftime('%s', 'now')),
('TWD', 'New Taiwan Dollar', 2, NULL, 'NT$', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('DKK', 'Danish Krone', 2, NULL, 'kr', 'suffix', 'x.00', 1, strftime('%s', 'now')),
('PLN', 'Polish Zloty', 2, NULL, '', 'suffix', 'x.00', 1, strftime('%s', 'now')),
('THB', 'Thai Baht', 2, NULL, '฿', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('IDR', 'Indonesian Rupiah', 2, NULL, 'Rp', 'prefix', 'x000', 1, strftime('%s', 'now')),
('HUF', 'Hungarian Forint', 2, NULL, 'Ft', 'suffix', 'x00', 1, strftime('%s', 'now')),
('CZK', 'Czech Koruna', 2, NULL, '', 'suffix', 'x.00', 1, strftime('%s', 'now')),
('ILS', 'Israeli Shekel', 2, NULL, '', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('CLP', 'Chilean Peso', 0, NULL, '$', 'prefix', 'x00', 1, strftime('%s', 'now')),
('PHP', 'Philippine Peso', 2, NULL, '', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('AED', 'UAE Dirham', 2, NULL, 'Dh', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('COP', 'Colombian Peso', 2, NULL, '$', 'prefix', 'x00', 1, strftime('%s', 'now')),
('SAR', 'Saudi Riyal', 2, NULL, '', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('MYR', 'Malaysian Ringgit', 2, NULL, 'RM', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('RON', 'Romanian Leu', 2, NULL, 'lei', 'suffix', 'x.00', 1, strftime('%s', 'now')),
-- Middle Eastern (3-decimal currencies)
('BHD', 'Bahraini Dinar', 3, NULL, 'BD', 'prefix', 'x.000', 0, strftime('%s', 'now')),
('KWD', 'Kuwaiti Dinar', 3, NULL, 'KD', 'prefix', 'x.000', 0, strftime('%s', 'now')),
('OMR', 'Omani Rial', 3, NULL, 'OR', 'prefix', 'x.000', 0, strftime('%s', 'now')),
('JOD', 'Jordanian Dinar', 3, NULL, 'JD', 'prefix', 'x.000', 0, strftime('%s', 'now')),
-- Other European
('TRY', 'Turkish Lira', 2, NULL, '', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('RUB', 'Russian Ruble', 2, NULL, '', 'suffix', 'x.00', 0, strftime('%s', 'now')), -- Sanctions
('UAH', 'Ukrainian Hryvnia', 2, NULL, '', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('BGN', 'Bulgarian Lev', 2, NULL, 'лв', 'suffix', 'x.00', 1, strftime('%s', 'now')),
('HRK', 'Croatian Kuna', 2, NULL, 'kn', 'suffix', 'x.00', 1, strftime('%s', 'now')),
('ISK', 'Icelandic Krona', 0, NULL, 'kr', 'suffix', 'x00', 1, strftime('%s', 'now')),
-- African
('EGP', 'Egyptian Pound', 2, NULL, '£', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('NGN', 'Nigerian Naira', 2, NULL, '', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('KES', 'Kenyan Shilling', 2, NULL, 'KSh', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('GHS', 'Ghanaian Cedi', 2, NULL, '', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('MAD', 'Moroccan Dirham', 2, NULL, 'DH', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('TND', 'Tunisian Dinar', 3, NULL, 'DT', 'prefix', 'x.000', 0, strftime('%s', 'now')),
-- Asian
('PKR', 'Pakistani Rupee', 2, NULL, '', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('BDT', 'Bangladeshi Taka', 2, NULL, '', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('VND', 'Vietnamese Dong', 0, NULL, '', 'suffix', 'x000', 1, strftime('%s', 'now')),
('MMK', 'Myanmar Kyat', 2, NULL, 'K', 'suffix', 'x00', 0, strftime('%s', 'now')),
('KHR', 'Cambodian Riel', 2, NULL, '', 'prefix', 'x00', 0, strftime('%s', 'now')),
('LAK', 'Lao Kip', 2, NULL, '', 'prefix', 'x000', 0, strftime('%s', 'now')),
('MNT', 'Mongolian Tugrik', 2, NULL, '', 'suffix', 'x00', 0, strftime('%s', 'now')),
-- Latin American
('ARS', 'Argentine Peso', 2, NULL, '$', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('PEN', 'Peruvian Sol', 2, NULL, 'S/', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('UYU', 'Uruguayan Peso', 2, NULL, '$', 'prefix', 'x.00', 1, strftime('%s', 'now')),
('PYG', 'Paraguayan Guarani', 0, NULL, '', 'prefix', 'x000', 0, strftime('%s', 'now')),
('BOB', 'Bolivian Boliviano', 2, NULL, 'Bs', 'prefix', 'x.00', 0, strftime('%s', 'now')),
-- Caribbean
('XCD', 'Eastern Caribbean Dollar', 2, NULL, '$', 'prefix', 'x.99', 0, strftime('%s', 'now')); -- Eastern Caribbean