| Version 3 (modified by , 2 weeks ago) ( diff ) |
|---|
Напреден развој на базата
Валидација на трговски барања (Trade Request Validation)
Опис на барањата за податочни ограничувања
Системот мора да обезбеди дека:
- Корисникот не смее да поднесе BUY барање ако неговото портфолио нема доволно готовина (balance) за покривање на вкупната вредност на барањето (price_per_unit * quantity)
- Корисникот не смее да поднесе SELL барање за акција ако неговото портфолио не ги поседува бараните акции во доволна количина
- Статусот на trade_request смее да биде само: PENDING, EXECUTED, CANCELLED
- Типот на trade_request смее да биде само: BUY, SELL
Имплементација
Прилагодени домени (Custom Domains)
CREATE DOMAIN trade_status_domain AS VARCHAR(255)
CHECK (VALUE IN ('PENDING', 'COMPLETED', 'DECLINED'));
CREATE DOMAIN trade_type_domain AS VARCHAR(255)
CHECK (VALUE IN ('BUY', 'SELL'));
ALTER TABLE trade_request ALTER COLUMN status TYPE trade_status_domain;
ALTER TABLE trade_request ALTER COLUMN type TYPE trade_type_domain;
Тригери
BEFORE INSERT тригер на trade_request кој ги валидира BUY и SELL барањата пред да бидат зачувани.
CREATE OR REPLACE FUNCTION validate_trade_request()
RETURNS TRIGGER AS $$
DECLARE
v_balance NUMERIC(18,2);
v_total_cost NUMERIC(18,2);
v_owned_quantity INTEGER;
v_stock_id BIGINT;
BEGIN
-- total cost of the request
v_total_cost := NEW.price_per_unit * NEW.quantity;
IF NEW.type = 'BUY' THEN
-- Check - portfolio has enough balance
SELECT p.balance
INTO v_balance
FROM portfolios p
WHERE p.id = NEW.portfolio_id;
IF v_balance IS NULL THEN
RAISE EXCEPTION 'Portfolio with id=% not found', NEW.portfolio_id;
END IF;
IF v_balance < v_total_cost THEN
RAISE EXCEPTION
'Insufficient balance: portfolio has %, but trade requires %',
v_balance, v_total_cost;
END IF;
ELSIF NEW.type = 'SELL' THEN
-- stock_id from symbol
SELECT s.id INTO v_stock_id
FROM stock s
WHERE s.symbol = NEW.stock_symbol;
IF v_stock_id IS NULL THEN
RAISE EXCEPTION 'Stock with symbol=% not found', NEW.stock_symbol;
END IF;
-- Sum all stock in the portfolio
SELECT COALESCE(SUM(ph.quantity), 0)
INTO v_owned_quantity
FROM portfolio_holdings ph
WHERE ph.portfolio_id = NEW.portfolio_id
AND ph.stock_id = v_stock_id;
IF v_owned_quantity < NEW.quantity THEN
RAISE EXCEPTION
'Insufficient holdings: portfolio owns % shares of %, but trade requires %',
v_owned_quantity, NEW.stock_symbol, NEW.quantity;
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_validate_trade_request
BEFORE INSERT ON trade_request
FOR EACH ROW
EXECUTE FUNCTION validate_trade_request();
Погледи (Views)
Поглед за преглед на сите pending барања со информации за корисникот и портфолиото.
CREATE OR REPLACE VIEW pending_trade_requests AS
SELECT
tr.id AS trade_id,
tr.type AS trade_type,
tr.stock_symbol,
tr.quantity,
tr.price_per_unit,
(tr.price_per_unit * tr.quantity)::NUMERIC AS total_value,
tr.status,
tr.timestamp,
p.id AS portfolio_id,
p.balance AS portfolio_balance,
u.id AS user_id,
u.username,
u.email
FROM trade_request tr
JOIN portfolios p ON tr.portfolio_id = p.id
JOIN users u ON p.user_id = u.id
WHERE tr.status = 'PENDING'
ORDER BY tr.timestamp DESC;
Автоматско ажурирање на портфолио при извршување на трансакција
Опис на барањата за податочни ограничувања
Системот мора да обезбеди дека:
- Кога се INSERT-ира нова трансакција од тип BUY, балансот на портфолиото на корисникот автоматски се намалува за (price * quantity), а во portfolio_holdings се додава или се ажурира avg_price на постоечкото холдинг за таа акција
- Кога се INSERT-ира нова трансакција од тип SELL, балансот на портфолиото автоматски се зголемува за (price * quantity), а количината во portfolio_holdings се намалува; ако quantity достигне 0, записот се брише
- avg_price при BUY се пресметува со weighted average формула: (стара_вредност + нова_вредност) / (стар_qty + нов_qty)
Имплементација
Тригери
AFTER INSERT тригер на transactions кој автоматски го ажурира балансот на портфолиото и portfolio_holdings.
CREATE OR REPLACE FUNCTION sync_portfolio_on_transaction()
RETURNS TRIGGER AS $$
DECLARE
v_portfolio_id BIGINT;
v_existing_qty INTEGER;
v_existing_avg NUMERIC(38,2);
v_new_avg NUMERIC(38,2);
v_total_value NUMERIC(38,2);
BEGIN
v_total_value := NEW.price * NEW.quantity;
-- portfolio from user
SELECT p.id INTO v_portfolio_id
FROM portfolios p
WHERE p.user_id = NEW.user_id;
IF v_portfolio_id IS NULL THEN
RAISE EXCEPTION 'No portfolio found for user_id=%', NEW.user_id;
END IF;
IF NEW.type = 'BUY' THEN
-- balance
UPDATE portfolios
SET balance = balance - v_total_value
WHERE id = v_portfolio_id;
-- chekc if holding already exists
SELECT ph.quantity, ph.avg_price
INTO v_existing_qty, v_existing_avg
FROM portfolio_holdings ph
WHERE ph.portfolio_id = v_portfolio_id
AND ph.stock_id = NEW.stock_id
LIMIT 1;
IF v_existing_qty IS NOT NULL THEN
-- weighted average price
v_new_avg := ((v_existing_avg * v_existing_qty) + (NEW.price * NEW.quantity))
/ (v_existing_qty + NEW.quantity);
UPDATE portfolio_holdings
SET quantity = quantity + NEW.quantity,
avg_price = v_new_avg
WHERE portfolio_id = v_portfolio_id
AND stock_id = NEW.stock_id;
ELSE
-- insert new holding lot
INSERT INTO portfolio_holdings (id, quantity, avg_price, stock_id, portfolio_id)
VALUES (
nextval('portfolio_holdings_id_seq'),
NEW.quantity,
NEW.price,
NEW.stock_id,
v_portfolio_id
);
END IF;
ELSIF NEW.type = 'SELL' THEN
-- add proceeds to balance
UPDATE portfolios
SET balance = balance + v_total_value
WHERE id = v_portfolio_id;
-- reduce holding quantity
UPDATE portfolio_holdings
SET quantity = quantity - NEW.quantity
WHERE portfolio_id = v_portfolio_id
AND stock_id = NEW.stock_id;
-- remove holding if quantity reaches zero
DELETE FROM portfolio_holdings
WHERE portfolio_id = v_portfolio_id
AND stock_id = NEW.stock_id
AND quantity <= 0;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_sync_portfolio_on_transaction
AFTER INSERT ON transactions
FOR EACH ROW
EXECUTE FUNCTION sync_portfolio_on_transaction();
Погледи (Views)
Поглед за преглед на вкупната вредност на портфолиото за секој корисник, со тековните пазарни цени.
CREATE OR REPLACE VIEW portfolio_summary AS
SELECT
u.id AS user_id,
u.username,
p.id AS portfolio_id,
p.balance AS cash_balance,
COALESCE(SUM(ph.quantity * s.current_price), 0) AS holdings_market_value,
COALESCE(SUM(ph.quantity * ph.avg_price), 0) AS holdings_cost_basis,
COALESCE(SUM(ph.quantity * s.current_price)
- SUM(ph.quantity * ph.avg_price), 0) AS unrealized_pnl,
p.balance + COALESCE(SUM(ph.quantity * s.current_price), 0) AS total_portfolio_value,
COUNT(DISTINCT ph.stock_id) AS num_stocks_held
FROM users u
JOIN portfolios p ON u.id = p.user_id
LEFT JOIN portfolio_holdings ph ON p.id = ph.portfolio_id
LEFT JOIN stock s ON ph.stock_id = s.id
WHERE u.role = 'USER'
GROUP BY u.id, u.username, p.id, p.balance
ORDER BY total_portfolio_value DESC;
Валидација на Watchlist Alert прагови
Опис на барањата за податочни ограничувања
Системот мора да обезбеди дека:
- Ако се зададени и двата прага (price_above и price_below), price_above мора да биде строго поголем од price_below; спротивното е логички невозможно
- Барем еден од двата прагови мора да биде зададен (не смеат и двата да бидат NULL истовремено)
- Праговите мора да бидат позитивни броеви (> 0)
Имплементација
Тригери
BEFORE INSERT OR UPDATE тригер на watchlist кој ги валидира alert праговите.
CREATE OR REPLACE FUNCTION validate_watchlist_thresholds()
RETURNS TRIGGER AS $$
BEGIN
-- 1one threshold must be set
IF NEW.price_above IS NULL AND NEW.price_below IS NULL THEN
RAISE EXCEPTION
'Watchlist entry must have at least one threshold set (price_above or price_below)';
END IF;
-- thresholds must be positive
IF NEW.price_above IS NOT NULL AND NEW.price_above <= 0 THEN
RAISE EXCEPTION 'price_above must be a positive value, got %', NEW.price_above;
END IF;
IF NEW.price_below IS NOT NULL AND NEW.price_below <= 0 THEN
RAISE EXCEPTION 'price_below must be a positive value, got %', NEW.price_below;
END IF;
-- price_above --- greater than price_below when both are set
IF NEW.price_above IS NOT NULL AND NEW.price_below IS NOT NULL THEN
IF NEW.price_above <= NEW.price_below THEN
RAISE EXCEPTION
'price_above (%) must be strictly greater than price_below (%)',
NEW.price_above, NEW.price_below;
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_validate_watchlist_thresholds
BEFORE INSERT OR UPDATE ON watchlist
FOR EACH ROW
EXECUTE FUNCTION validate_watchlist_thresholds();
Функции / Stored Procedures
Функција која ги враќа сите watchlist записи за кои тековната цена на акцијата го надминала или паднала под зададениот праг (активни alerts).
CREATE OR REPLACE FUNCTION get_triggered_watchlist_alerts()
RETURNS TABLE (
watchlist_id BIGINT,
user_id BIGINT,
username TEXT,
stock_id BIGINT,
symbol TEXT,
current_price DOUBLE PRECISION,
price_above DOUBLE PRECISION,
price_below DOUBLE PRECISION,
alert_type TEXT
) AS $$
BEGIN
RETURN QUERY
SELECT
w.id AS watchlist_id,
u.id AS user_id,
u.username::TEXT,
s.id AS stock_id,
s.symbol::TEXT,
s.current_price,
w.price_above,
w.price_below,
CASE
WHEN w.price_above IS NOT NULL
AND s.current_price >= w.price_above THEN 'PRICE_ABOVE_TRIGGERED'
WHEN w.price_below IS NOT NULL
AND s.current_price <= w.price_below THEN 'PRICE_BELOW_TRIGGERED'
END::TEXT AS alert_type
FROM watchlist w
JOIN stock s ON w.stock_id = s.id
JOIN users u ON w.user_id = u.id
WHERE
(w.price_above IS NOT NULL AND s.current_price >= w.price_above)
OR
(w.price_below IS NOT NULL AND s.current_price <= w.price_below)
ORDER BY u.id, s.symbol;
END;
$$ LANGUAGE plpgsql;
Погледи (Views)
Поглед за преглед на сите watchlist записи со тековните цени и статус на alert.
CREATE OR REPLACE VIEW watchlist_with_alert_status AS
SELECT
w.id AS watchlist_id,
u.id AS user_id,
u.username,
s.id AS stock_id,
s.symbol,
s.name AS stock_name,
s.current_price,
w.price_above,
w.price_below,
CASE
WHEN w.price_above IS NOT NULL
AND s.current_price >= w.price_above THEN 'PRICE_ABOVE_TRIGGERED'
WHEN w.price_below IS NOT NULL
AND s.current_price <= w.price_below THEN 'PRICE_BELOW_TRIGGERED'
ELSE 'WATCHING'
END AS alert_status
FROM watchlist w
JOIN stock s ON w.stock_id = s.id
JOIN users u ON w.user_id = u.id
ORDER BY alert_status DESC, u.username, s.symbol;
Управување со OAuth токени (OAuth Token Expiry Management)
Опис на барањата за податочни ограничувања
Системот мора да обезбеди дека:
- Не смее да се INSERT-ира нов OAuth токен за корисник кој веќе има активен (неистечен) токен за ист провајдер
- При обид за користење на OAuth токен, системот мора да провери дали е истечен (expires_at < NOW()) и да фрли грешка ако е
- Истечените токени треба автоматски да се чистат преку background job за да не се трупаат во базата
Имплементација
Тригери
BEFORE INSERT тригер на oauth_pending_links кој спречува дупликат активни токени за ист корисник и провајдер.
CREATE OR REPLACE FUNCTION validate_oauth_token_before_insert()
RETURNS TRIGGER AS $$
DECLARE
v_existing_count INTEGER;
BEGIN
-- check for existing non-expired token for same user and provider
SELECT COUNT(*) INTO v_existing_count
FROM oauth_pending_links
WHERE user_id = NEW.user_id
AND provider = NEW.provider
AND expires_at > NOW();
IF v_existing_count > 0 THEN
RAISE EXCEPTION
'User % already has an active OAuth token for provider %. Wait for it to expire or revoke it first.',
NEW.user_id, NEW.provider;
END IF;
-- token is not created already expired
IF NEW.expires_at <= NEW.created_at THEN
RAISE EXCEPTION
'expires_at (%) must be after created_at (%)',
NEW.expires_at, NEW.created_at;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_validate_oauth_token
BEFORE INSERT ON oauth_pending_links
FOR EACH ROW
EXECUTE FUNCTION validate_oauth_token_before_insert();
Функции / Stored Procedures
Функција за верификација на OAuth токен — проверува дали токенот постои и не е истечен, и го враќа корисничкото id и провајдерот.
CREATE OR REPLACE FUNCTION verify_oauth_token(p_token VARCHAR(255))
RETURNS TABLE (
user_id BIGINT,
provider TEXT,
email TEXT,
is_valid BOOLEAN,
reason TEXT
) AS $$
DECLARE
v_record oauth_pending_links%ROWTYPE;
BEGIN
SELECT * INTO v_record
FROM oauth_pending_links
WHERE token = p_token;
IF v_record.token IS NULL THEN
RETURN QUERY SELECT
NULL::BIGINT, NULL::TEXT, NULL::TEXT,
FALSE, 'Token does not exist';
RETURN;
END IF;
IF v_record.expires_at < NOW() THEN
RETURN QUERY SELECT
v_record.user_id,
v_record.provider::TEXT,
v_record.email::TEXT,
FALSE, 'Token has expired';
RETURN;
END IF;
RETURN QUERY SELECT
v_record.user_id,
v_record.provider::TEXT,
v_record.email::TEXT,
TRUE, 'Token is valid';
END;
$$ LANGUAGE plpgsql;
Процедура за рачно чистење на истечени OAuth токени (може да се повика и преку background job).
CREATE OR REPLACE PROCEDURE cleanup_expired_oauth_tokens()
LANGUAGE plpgsql AS $$
DECLARE
v_deleted_count INTEGER;
BEGIN
DELETE FROM oauth_pending_links
WHERE expires_at < NOW();
GET DIAGNOSTICS v_deleted_count = ROW_COUNT;
RAISE NOTICE 'Cleaned up % expired OAuth token(s) at %', v_deleted_count, NOW();
END;
$$;
Погледи (Views)
Поглед за преглед на сите OAuth токени со нивниот статус (активен / истечен).
CREATE OR REPLACE VIEW oauth_tokens_status AS
SELECT
o.token,
o.user_id,
u.username,
u.email AS user_email,
o.provider,
o.email AS oauth_email,
o.created_at,
o.expires_at,
CASE
WHEN o.expires_at > NOW() THEN 'ACTIVE'
ELSE 'EXPIRED'
END AS token_status,
EXTRACT(EPOCH FROM (o.expires_at - NOW()))::INTEGER AS seconds_until_expiry
FROM oauth_pending_links o
JOIN users u ON o.user_id = u.id
ORDER BY o.expires_at DESC;
