CREATE SCHEMA IF NOT EXISTS wallet; CREATE TABLE wallet.segment ( id smallint NOT NULL, code text NOT NULL, PRIMARY KEY (id) INCLUDE (code), UNIQUE (code) INCLUDE (id) ); INSERT INTO wallet.segment (id, code) VALUES (1, 'main'), (2, 'affiliate'); CREATE TABLE wallet.currency ( id smallint NOT NULL, code text NOT NULL, PRIMARY KEY (id) INCLUDE (code), UNIQUE (code) INCLUDE (id) ); INSERT INTO wallet.currency (id, code) VALUES (1, 'BTC'), (2, 'USDT'); CREATE TABLE wallet.category ( id smallint GENERATED BY DEFAULT AS IDENTITY NOT NULL, origin text NOT NULL, type text NOT NULL, description text, PRIMARY KEY (id) INCLUDE (origin, type), UNIQUE (origin, type) ); CREATE TABLE wallet.tx ( id uuid DEFAULT gen_random_uuid_v7() PRIMARY KEY, account_id uuid REFERENCES public.account (id) NOT NULL, currency_id smallint REFERENCES wallet.currency (code) NOT NULL, category_id smallint REFERENCES wallet.category (id) NOT NULL, segment_id smallint REFERENCES wallet.segment (id) NOT NULL, amount numeric(36, 18) CHECK ( amount <> 0 ) NOT NULL, created_at timestamptz DEFAULT NOW() NOT NULL ); CREATE TABLE wallet.account ( created_at timestamptz NOT NULL DEFAULT NOW(), account_id uuid NOT NULL REFERENCES public.account (id), segment_id smallint NOT NULL REFERENCES wallet.segment (id), currency_id smallint NOT NULL REFERENCES wallet.currency (code), balance numeric(36, 18) NOT NULL, CONSTRAINT wallet_pk PRIMARY KEY (account_id, segment_id, currency_id) ); CREATE VIEW wallet.account$ AS SELECT a.id AS account_id, c.code AS currency, s.code AS segment, COALESCE(wa.balance, (0)::numeric) AS balance FROM ((public.account a CROSS JOIN wallet.currency c CROSS JOIN wallet.segment s) INNER JOIN wallet.account wa ON (((a.id = wa.account_id) AND (c.id = wa.currency_id)))); CREATE OR REPLACE FUNCTION wallet.provide_tx( p_account_id uuid, p_currency text, p_amount numeric, p_origin text, p_type text, p_allow_overdraft boolean DEFAULT FALSE, p_segment text DEFAULT 'main', OUT id uuid, OUT balance numeric, OUT amount numeric, OUT currency text, OUT segment text, OUT currency_id smallint, OUT segment_id smallint, OUT category_id smallint ) LANGUAGE plpgsql AS $$ BEGIN SELECT c.id, c.code INTO currency_id, currency FROM wallet.currency c WHERE c.code = p_currency; IF NOT FOUND THEN RAISE EXCEPTION 'Currency "%" does not exist', p_currency USING DETAIL = 'WALLET_PROVIDE_TX_UNKNOWN_CURRENCY'; END IF; SELECT s.id INTO segment_id FROM wallet.segment s WHERE s.code = p_segment; IF NOT FOUND THEN RAISE EXCEPTION 'Segment "%" does not exist', p_segment USING DETAIL = 'WALLET_PROVIDE_TX_UNKNOWN_SEGMENT'; END IF; SELECT c.id INTO category_id FROM wallet.category c WHERE origin = p_origin AND type = p_type; IF NOT FOUND THEN INSERT INTO wallet.category (origin, type) VALUES (p_origin, p_type) RETURNING category.id INTO category_id; END IF; INSERT INTO wallet.account(account_id, currency_id, segment_id, balance) VALUES (p_account_id, currency_id, segment_id, p_amount) ON CONFLICT (account_id, segment_id, currency_id) DO UPDATE SET balance = account.balance + p_amount RETURNING account.balance INTO balance; IF balance < 0 AND p_amount < 0 AND NOT p_allow_overdraft THEN RAISE EXCEPTION 'Insufficient funds: balance is % after transaction of % %', balance, p_amount, p_currency USING DETAIL = 'WALLET_PROVIDE_TX_INSUFFICIENT_BALANCE'; END IF; INSERT INTO wallet.tx (account_id, segment_id, currency_id, category_id, amount) VALUES (p_account_id, segment_id, currency_id, category_id, p_amount) RETURNING tx.id, tx.amount INTO id, amount; END; $$;