Cross-Portal Workflows

Deep technical reference for every end-to-end flow: API calls, DB operations, on-chain transactions, and service-to-service sequences.

The complete journey from business registration to secondary market trading. This is the most complex workflow spanning Issuer, Admin, and Investor portals, the platform-api, blockchain-svc, and the Polygon chain.

STATUS FLOW

draft → primary → secondary ↔ paused → matured | delisted

Token Deploy Sequence

  Issuer          platform-api:8081       blockchain-svc:8082       Polygon (ERC-3643)
    │                    │                        │                       │
    │─POST /tokens──────▶│                        │                       │
    │                    │ INSERT tokens           │                       │
    │                    │ (status=draft)          │                       │
    │◀───── 201 ─────────│                        │                       │
    │                    │                        │                       │
  Admin                  │                        │                       │
    │─POST /tokens/:id   │                        │                       │
    │  /deploy──────────▶│                        │                       │
    │                    │─POST /deploy───────────▶│                       │
    │                    │                        │─── Deploy IR ─────────▶│ TX1
    │                    │                        │─── Deploy Compliance ─▶│ TX2
    │                    │                        │─── Deploy Token ──────▶│ TX3
    │                    │                        │─── addAgent(IR) ──────▶│ TX4
    │                    │                        │─── addAgent(Token) ───▶│ TX5
    │                    │                        │◀── DeployResult ───────│
    │                    │◀─── addresses + hashes─│                       │
    │                    │ UPDATE tokens           │                       │
    │                    │ (contract_address,      │                       │
    │                    │  identity_registry_addr,│                       │
    │                    │  status=primary)        │                       │
    │◀─── 200 ───────────│                        │                       │
1

Issuer registers business

POST /api/v1/businesses → INSERT INTO businesses (status='pending'). Requires KYC level 2. Includes company name, registration number, country, industry, alef_allocation_pct.

2

Admin approves business

PATCH /api/v1/admin/businesses/:id → UPDATE businesses SET status='active'. Only after approval can the issuer create tokens.

3

Issuer creates token (draft)

POST /api/v1/tokens → INSERT INTO tokens (status='draft'). Validates primary_offering_pct + team_pct + reserve_pct = 100. Sets price_per_token, total_supply, min_investment, raise_currency.

4

Admin deploys to blockchain (5 on-chain txns)

POST /api/v1/admin/tokens/:id/deploy → platform-api calls blockchain-svc POST /api/v1/deploy. Deploys IdentityRegistry, Compliance(IR), SecurityToken(name, symbol, decimals, IR, Compliance), then addAgent×2. Gas limit: 3M per deploy, 300K per call. All gas recorded in gas_transactions.

5

Admin starts primary offering

POST /api/v1/admin/tokens/:id/start-offering → UPDATE tokens SET status='primary', primary_start, primary_end. Token enters subscription mode.

6

Investors subscribe

POST /api/v1/tokens/:id/subscribe → SELECT users FOR UPDATE (check kyc_status='approved'), deduct fiat_balance, INSERT token_allocations (allocation_type='primary', status='pending_payment'), INSERT wallet_transactions (type='token_buy', status='pending').

7

Admin confirms allocations

POST /api/v1/admin/tokens/:id/confirm-payment → UPDATE token_allocations SET status='confirmed', increment tokens_sold, total_raised. UPSERT wallets (balance += qty). Update wallet_transaction to 'completed'. Async: call ExecuteOnChainAllocation.

8

On-chain allocation (async)

ExecuteOnChainAllocation: registerIdentity(investor, countryCode) on IdentityRegistry (idempotent), then mint(investor, amount*10^decimals) on SecurityToken. On success: on_chain_status='minted', on_chain_tx_hash set. On failure: on_chain_status='failed', on_chain_error set.

9

Admin enables secondary trading

POST /api/v1/admin/tokens/:id/enable-secondary → UPDATE tokens SET status='secondary'. Triggers ProcessGoLiveAllocation (treasury gets alef_allocation_pct tokens) and ProcessSuccessFee. Full orderbook trading begins.

Key API Endpoints

POST/api/v1/tokens(platform-api)Create token (draft)
POST/api/v1/admin/tokens/:id/deploy(platform-api)Deploy to blockchain
POST/api/v1/admin/tokens/:id/start-offering(platform-api)Open primary
POST/api/v1/tokens/:id/subscribe(platform-api)Investor subscribes
POST/api/v1/admin/tokens/:id/confirm-payment(platform-api)Confirm allocation
POST/api/v1/admin/tokens/:id/enable-secondary(platform-api)Enable CLOB trading
POST/api/v1/deploy(blockchain-svc)Deploy ERC-3643 contracts

Database Tables

tokenstable
idUUID v7Primary key
business_idUUIDFK to businesses
symbolVARCHAR(10)e.g. DAGRI, TPROP
statusVARCHAR(20)draft/primary/secondary/paused/matured/delisted
contract_addressVARCHAR(42)SecurityToken address (set on deploy)
identity_registry_addrVARCHAR(42)IdentityRegistry address
total_supplyBIGINTTotal token supply
tokens_soldBIGINTSold in primary (incremented on confirm)
total_raisedNUMERICUSD raised in primary
price_per_tokenNUMERICPrimary offering price
trading_modeVARCHAR(30)primary_subscription/full_orderbook/fixed_price_otc
maturity_dateTIMESTAMPTZAuto-mature when reached
token_allocationstable
idUUID v7Primary key
token_idUUIDFK to tokens
user_idUUIDFK to investor
quantityBIGINTTokens allocated
allocation_typeVARCHAR(20)primary/secondary/airdrop
statusVARCHAR(20)pending_payment/confirmed/rejected
on_chain_statusVARCHAR(20)pending/minting/minted/failed
on_chain_tx_hashVARCHAR(66)Mint transaction hash
round_idUUIDFK to offering_rounds (optional)

On-Chain Operations

TX
IdentityRegistry.constructor()
Gas: ~3MPaid by: platform

Deploys identity registry for this token

TX
Compliance.constructor(irAddr)
Gas: ~3MPaid by: platform

Deploys compliance module linked to IR

TX
SecurityToken.constructor(name, symbol, decimals, IR, compliance)
Gas: ~3MPaid by: platform

Deploys the ERC-3643 security token

TX
IdentityRegistry.addAgent(deployer)
Gas: ~300KPaid by: platform

Grants deployer agent role on IR

TX
SecurityToken.addAgent(deployer)
Gas: ~300KPaid by: platform

Grants deployer agent role on token

TX
IdentityRegistry.registerIdentity(investor, countryCode)
Event: IdentityRegisteredGas: ~65KPaid by: issuer

Registers investor identity (idempotent)

TX
SecurityToken.mint(investor, amount)
Event: TokensMintedGas: ~65KPaid by: issuer

Mints tokens to investor wallet

Design decision: DB-first, blockchain async. The DB commit happens first (allocation confirmed, balances updated). On-chain minting is asynchronous. If blockchain fails, on_chain_status='failed' and the admin can retry from the dashboard. This prevents blockchain latency from blocking the user experience.
Token deployment is irreversible. The 5 contract deployments cost ~12M gas total. Once deployed, contract_address and identity_registry_addr are immutable on the token record.

Manual KYC review flow (MVP). Production will integrate Sumsub for automated ID verification.

STATUS FLOW

none → pending → in_review → approved | rejected → (resubmit) → pending

KYC Approval + On-Chain Identity

  Investor        platform-api:8081          Admin            blockchain-svc:8082    Polygon
    │                   │                      │                     │                 │
    │─POST /kyc────────▶│                      │                     │                 │
    │                   │ INSERT kyc_applications                     │                 │
    │                   │ UPDATE users.kyc_status='pending'           │                 │
    │◀── 201 ───────────│                      │                     │                 │
    │                   │                      │                     │                 │
    │                   │◀─POST /kyc/:id/review│                     │                 │
    │                   │  {decision: approve}  │                     │                 │
    │                   │ UPDATE kyc_applications.status='approved'   │                 │
    │                   │ UPDATE users (kyc_status, kyc_level,        │                 │
    │                   │   accreditation_status, fatca_status)       │                 │
    │                   │──── 200 ─────────────▶│                     │                 │
    │                   │                      │                     │                 │
    │                   │     (later, on token allocation confirm)    │                 │
    │                   │─POST /register-identity──────────────────▶│                 │
    │                   │                      │                     │─registerIdentity▶│
    │                   │                      │                     │◀──tx_hash────────│
1

User submits KYC application

POST /api/v1/kyc → Duplicate check (no pending/in_review apps). INSERT INTO kyc_applications (verification_provider='manual'). UPDATE users SET kyc_status='pending'. Valid doc types: passport, emirates_id, national_id, drivers_license.

2

Application enters admin queue

Appears in Admin/Staff → KYC Management. Sorted by submission date. Notification sent to KYC reviewers.

3

Reviewer examines application

GET /api/v1/admin/kyc/:id → Full applicant details with PII. Status auto-updates to in_review on first access.

4

Approve or reject

POST /api/v1/admin/kyc/:id/review → SELECT FOR UPDATE on kyc_applications. On approve: kyc_level set (default 2), accreditation auto-derived (level>=3→institutional, >=2→professional, else retail), FATCA auto-set (US nationality→blocked, else clear). On reject: rejection_reason stored, user.kyc_status='rejected'.

5

Audit log created

INSERT INTO audit_logs with reviewer ID, timestamp, action type, and decision details.

Key API Endpoints

POST/api/v1/kyc(platform-api)Submit KYC application
GET/api/v1/admin/kyc(platform-api)List all applications
GET/api/v1/admin/kyc/:id(platform-api)Get application detail
POST/api/v1/admin/kyc/:id/review(platform-api)Approve or reject
kyc_applicationstable
idUUID v7Primary key
user_idUUIDFK to users
statusVARCHAR(20)pending/in_review/approved/rejected
full_nameVARCHAR(200)Legal name
nationalityVARCHAR(3)ISO country code
document_typeVARCHAR(20)passport/emirates_id/national_id/drivers_license
document_numberVARCHAR(50)Encrypted document ID
reviewed_byUUIDFK to admin/staff user
rejection_reasonTEXTRequired on rejection
verification_providerVARCHAR(20)'manual' for MVP
Approve KYC — DB Operations (in single transaction)sql
UPDATE kyc_applications SET status='approved', reviewed_by=$1,
    reviewed_at=NOW(), review_notes=$2 WHERE id=$3;

UPDATE users SET kyc_status='approved', kyc_level=$1,
    accreditation_status=$2, fatca_status=$3 WHERE id=$4;
-- accreditation: level>=3→'institutional', >=2→'professional', else 'retail'
-- fatca: nationality='US'→'blocked', else 'clear'
On-chain identity registration does NOT happen during KYC approval. It happens later in ExecuteOnChainAllocation when the investor's first token allocation is confirmed. This is because the on-chain IdentityRegistry is per-token, not global.

How an order moves from the investor's screen through the matching engine, anti-manipulation checks, balance locking, trade creation, and WebSocket broadcast.

ORDER STATUS FLOW

open → partially_filled → filled | cancelled
pending_stop → open (when stop price triggered) → filled | cancelled

Order Placement → Trade → Settlement

  Investor         trading-server:8080      Matching Engine     DB (PostgreSQL)      WebSocket Hub
    │                     │                      │                    │                    │
    │─POST /orders───────▶│                      │                    │                    │
    │                     │ Circuit breaker check │                    │                    │
    │                     │ Fat-finger check      │                    │                    │
    │                     │ NAV deviation check   │                    │                    │
    │                     │ BEGIN TX              │                    │                    │
    │                     │─── Lock token row ────────────────────────▶│                    │
    │                     │─── Check user eligibility ───────────────▶│                    │
    │                     │─── Lock balance (fiat or tokens) ────────▶│                    │
    │                     │─── INSERT orders ─────────────────────────▶│                    │
    │                     │                      │                    │                    │
    │                     │─── ProcessOrder() ──▶│                    │                    │
    │                     │◀── result.Trades ────│                    │                    │
    │                     │                      │                    │                    │
    │                     │─── INSERT trades ─────────────────────────▶│                    │
    │                     │─── UPDATE wallets (buyer +qty) ──────────▶│                    │
    │                     │─── UPDATE wallets (seller -qty) ─────────▶│                    │
    │                     │─── UPDATE users.fiat_balance (seller) ───▶│                    │
    │                     │─── UPDATE users.fiat_balance (treasury) ─▶│                    │
    │                     │ COMMIT TX             │                    │                    │
    │                     │                      │                    │                    │
    │                     │─── BroadcastTrade ────────────────────────────────────────────▶│
    │                     │─── BroadcastOrderBook ───────────────────────────────────────▶│
    │◀── 200 + trades ────│                      │                    │                    │
1

Pre-flight validation

Check side (buy/sell), quantity > 0, order_type (limit/market/stop_loss/stop_limit). Circuit breaker: if IsTripped() → reject. Fat-finger: if price deviates too far from last trade → FatFingerBlock (reject) or FatFingerConfirm (require confirm_fat_finger=true). NAV deviation: if price > 20% from latest NAV → require confirm_nav_deviation=true.

2

User eligibility (in DB transaction)

Lock token row (SELECT FOR UPDATE). Check trading_mode = 'full_orderbook'. Verify user: wallet_address set, kyc_status='approved', fatca_status!='blocked', is_activated=true. Accreditation investment limits: retail=$500, professional=$5000, institutional=$50000.

3

Balance locking

Buy order: UPDATE users SET fiat_balance = fiat_balance - (price * qty). Sell order: UPDATE wallets SET locked_balance = locked_balance + qty. Prevents double-spending during matching.

4

Order insertion

INSERT INTO orders (..., status='open'). Stop orders get status='pending_stop', added to in-memory StopBook, and return early (no matching yet).

5

Matching engine processes order

eng.ProcessOrder(order) → price-time priority matching. Engine runs in-memory at ~760K orders/sec. Returns result.Trades (matched trade pairs).

6

Trade creation & instant settlement

For each match: INSERT INTO trades (settlement_status='pending'). Fee = total * effective_fee_bps / 10000. Buyer wallet: UPSERT balance += qty. Seller wallet: balance -= qty, locked_balance -= qty. Seller fiat: += (total - fee). Treasury: += fee.

7

Status updates & history

Taker order updated to 'filled' or 'partially_filled'. INSERT INTO order_status_history. INSERT INTO trade_settlement_history (initial 'pending').

8

WebSocket broadcast (async)

BroadcastTrade, BroadcastOrderBook, BroadcastOrderStatus pushed to all connected clients. If lastTradePrice changed, checkAndTriggerStops() fires for any pending stop orders.

Key API Endpoints

POST/api/v1/orders(trading-server)Place order
GET/api/v1/orders(trading-server)List user orders
DELETE/api/v1/orders/:id(trading-server)Cancel order
GET/api/v1/orderbook/:tokenId(trading-server)Get orderbook
GET/api/v1/trades(trading-server)List user trades
orderstable
idUUID v7Primary key
user_idUUIDFK to users
token_idUUIDFK to tokens
sideVARCHAR(4)buy / sell
order_typeVARCHAR(15)limit/market/stop_loss/stop_limit
priceNUMERICLimit price (NULL for market)
stop_priceNUMERICTrigger price for stop orders
quantityBIGINTTotal order quantity
filled_quantityBIGINTQuantity matched so far
remaining_quantityBIGINTquantity - filled_quantity
locked_fiatNUMERICFiat locked for buy orders
statusVARCHAR(20)open/partially_filled/filled/cancelled/pending_stop
tradestable
idUUID v7Primary key
token_idUUIDFK to tokens
buy_order_idUUIDFK to buy order
sell_order_idUUIDFK to sell order
buyer_idUUIDFK to buyer
seller_idUUIDFK to seller
priceNUMERICExecution price
quantityBIGINTMatched quantity
totalNUMERICprice * quantity
platform_feeNUMERICFee in USD (total * bps / 10000)
effective_fee_bpsINTEGERFee rate applied (default 50 bps)
settlement_statusVARCHAR(20)pending/processing/settled/failed
settlement_typeVARCHAR(20)on_chain / off_chain
tx_hashVARCHAR(66)On-chain TX hash (if settled on-chain)

Trading Modes

primary_subscription

Fixed-price subscriptions. No CLOB. Orders rejected if trading_mode != full_orderbook.

full_orderbook

Full CLOB with market, limit, stop_loss, stop_limit orders. Price-time priority.

fixed_price_otc

OTC at admin-set price. Routed to placeOTCOrder() → INSERT otc_orders, requires admin approval.

Split-brain prevention: If the DB commit fails after matching, the engine calls eng.CancelOrder() to roll back the in-memory order book state. This ensures the in-memory engine and DB always stay consistent.
Anti-manipulation suite: circuit breaker (halts on 10%+ price swing in 5 min), fat-finger protection (blocks orders >3x last price), wash trading detection, spoofing detection, layering detection. All configurable per-token.

Background settlement processor in trading-server that picks up pending trades and executes on-chain transfers via blockchain-svc. Runs every 5 seconds.

SETTLEMENT STATUS FLOW

pending → processing → settled | failed → (retry) → processing

Settlement Processor Loop

  SettlementProcessor (5s loop)        DB                     blockchain-svc:8082    Polygon
    │                                   │                          │                   │
    │─SELECT trades WHERE               │                          │                   │
    │  settlement_status='pending'      │                          │                   │
    │  AND settlement_attempts < 5      │                          │                   │
    │  FOR UPDATE SKIP LOCKED ─────────▶│                          │                   │
    │◀─ trade rows (LIMIT 10) ──────────│                          │                   │
    │                                   │                          │                   │
    │ [if contract_address empty                                   │                   │
    │  OR wallet_address missing]       │                          │                   │
    │─UPDATE settlement_status=         │                          │                   │
    │  'settled', type='off_chain' ────▶│  (skip on-chain)         │                   │
    │                                   │                          │                   │
    │ [if all addresses present]        │                          │                   │
    │─UPDATE settlement_status=         │                          │                   │
    │  'processing' ───────────────────▶│                          │                   │
    │─POST /api/v1/transfer ───────────────────────────────────────▶│                   │
    │                                   │                          │─forcedTransfer()──▶│
    │                                   │                          │◀──tx_hash─────────│
    │◀─ {tx_hash} ─────────────────────────────────────────────────│                   │
    │─UPDATE settlement_status=         │                          │                   │
    │  'settled', tx_hash=$1,           │                          │                   │
    │  settled_at=NOW() ───────────────▶│                          │                   │
    │                                   │                          │                   │
    │ [on error: attempts < 5]          │                          │                   │
    │─UPDATE settlement_attempts++,     │                          │                   │
    │  status='pending' ──────────────▶│                          │                   │
    │ [on error: attempts >= 5]         │                          │                   │
    │─UPDATE status='failed' ──────────▶│                          │                   │
1

Polling loop (every 5 seconds)

SettlementProcessor with ticker at 5s interval. HTTP client timeout: 3 minutes. Processes up to 10 trades per tick.

2

Claim pending trades

SELECT ... FROM trades WHERE settlement_status='pending' AND settlement_attempts < 5 ORDER BY created_at ASC LIMIT 10 FOR UPDATE OF t SKIP LOCKED. Joins tokens (contract_address), users (wallet_address for buyer + seller).

3

Off-chain fallback

If contract_address is empty OR buyer/seller wallet_address is missing → immediately settle as off_chain. No blockchain call. UPDATE settlement_status='settled', settlement_type='off_chain'.

4

On-chain transfer

POST to blockchain-svc /api/v1/transfer with {token_addr, from_addr (seller), to_addr (buyer), amount}. blockchain-svc calls SecurityToken.forcedTransfer(from, to, amount). Returns tx_hash.

5

Success → settled

UPDATE trades SET settlement_status='settled', settlement_type='on_chain', tx_hash=$1, settled_at=NOW(). INSERT INTO trade_settlement_history.

6

Failure → retry or fail

Increment settlement_attempts. If attempts < 5: status stays 'pending', will retry next tick. If attempts >= 5: status='failed'. Admin can manually retry via POST /admin/trades/:id/retry-settlement.

Key API Endpoints

GET/api/v1/admin/trades(trading-server)List all trades (admin)
POST/api/v1/admin/trades/:id/retry-settlement(trading-server)Retry failed settlement
POST/api/v1/transfer(blockchain-svc)Execute on-chain transfer
TX
SecurityToken.forcedTransfer(seller, buyer, qty)
Event: TransferGas: ~85KPaid by: platform

Agent-initiated transfer bypassing approve/transferFrom. Compliance checks enforced by contract.

Why forcedTransfer not approve+transferFrom? The platform is a registered agent on the SecurityToken contract. forcedTransfer allows the platform to move tokens without requiring seller approval for each trade, which is essential for an exchange. The contract still enforces compliance (canTransfer check).
Every status transition is recorded in trade_settlement_history for audit. Failed settlements appear in the admin Settlement Dashboard with error details.

End-to-end dividend flow from issuer declaration through per-holder payment distribution. Supports fiat (USD), token, and stablecoin (USDC) dividend types.

STATUS FLOW

draft → pending_approval → approved → distributing → distributed | cancelled

Dividend Distribution

  Issuer           platform-api:8081                    DB
    │                     │                              │
    │─POST /dividends────▶│                              │
    │                     │─ SELECT holders from wallets─▶│
    │                     │─ INSERT dividend_declarations─▶│
    │                     │  (status=draft, holder_count) │
    │◀── 201 ─────────────│                              │
    │                     │                              │
    │─POST /dividends/:id │                              │
    │  /submit───────────▶│                              │
    │                     │─ UPDATE status='pending_approval'
    │                     │                              │
  Admin                   │                              │
    │─POST /dividends/:id │                              │
    │  /approve──────────▶│─ UPDATE status='approved' ──▶│
    │                     │                              │
    │─POST /dividends/:id │                              │
    │  /distribute───────▶│─ SELECT FOR UPDATE ──────────▶│
    │                     │─ UPDATE status='distributing'─▶│
    │                     │─ SELECT holders (wallets) ───▶│
    │                     │                              │
    │                     │  For each holder:            │
    │                     │─ UPDATE users.fiat_balance ──▶│ (fiat type)
    │                     │─ INSERT wallet_transactions ─▶│
    │                     │─ INSERT dividend_payments ───▶│
    │                     │                              │
    │                     │─ Deduct from issuer balance ─▶│
    │                     │─ Credit treasury (fee) ──────▶│
    │                     │─ UPDATE status='distributed' ▶│
    │◀── 200 ─────────────│                              │
1

Issuer declares dividend

POST /api/v1/dividends → Snapshots holders (SELECT user_id, balance FROM wallets WHERE token_id=$1 AND balance > 0). Calculates total_gross_usd = per_token_usd * total_held. Fee = total_gross * fee_pct / 100. INSERT INTO dividend_declarations (status='draft').

2

Submit for approval

POST /api/v1/dividends/:id/submit → UPDATE status='pending_approval'. Appears in admin queue.

3

Admin approves

POST /api/v1/admin/dividends/:id/approve → UPDATE status='approved'. Admin verifies amounts, dates, and issuer balance.

4

Admin distributes (fiat type)

POST /api/v1/admin/dividends/:id/distribute → SELECT FOR UPDATE (must be 'approved'). For each holder: gross = per_token_usd * balance, fee = gross * fee_pct / 100, net = gross - fee. UPDATE users SET fiat_balance += net. INSERT wallet_transactions (type='dividend', status='completed'). INSERT dividend_payments (status='paid').

5

Issuer debit & treasury credit

UPDATE users SET fiat_balance -= total_gross_usd WHERE id = issuer_user_id. UPDATE users SET fiat_balance += total_fee_usd WHERE id = TreasuryUserID. Deadlock prevention: treasury locked BEFORE issuer in tx.

6

Mark distributed

UPDATE dividend_declarations SET status='distributed', distributed_at=NOW(), holder_count=$1.

Dividend Types

Fiat (USD)

Credits users.fiat_balance. Wallet tx type: 'dividend'. Most common.

Stablecoin (USDC)

Credits user_balances table (currency='USDC'). UPSERT on conflict.

Token

Mints additional tokens. UPSERT wallets (balance += qty). Deducts from issuer token wallet.

Key API Endpoints

POST/api/v1/dividends(platform-api)Declare dividend
POST/api/v1/dividends/:id/submit(platform-api)Submit for approval
POST/api/v1/admin/dividends/:id/approve(platform-api)Approve declaration
POST/api/v1/admin/dividends/:id/distribute(platform-api)Execute distribution
dividend_declarationstable
idUUID v7Primary key
token_idUUIDFK to tokens
dividend_typeVARCHAR(20)fiat/token/stablecoin
per_token_usdNUMERICUSD per token (fiat/stablecoin)
per_token_qtyBIGINTTokens per token held (token type)
total_gross_usdNUMERICTotal before fees
fee_pctNUMERICPlatform fee percentage
total_fee_usdNUMERICTotal fee amount
holder_countINTEGERSnapshot of holders at declaration
statusVARCHAR(20)draft/pending_approval/approved/distributing/distributed/cancelled
dividend_paymentstable
idUUID v7Primary key
dividend_idUUIDFK to dividend_declarations
user_idUUIDFK to holder
token_balanceBIGINTHolder balance at snapshot
gross_amountNUMERICper_token * balance
fee_amountNUMERICHolder-level fee
net_amountNUMERICAmount credited
statusVARCHAR(20)paid / failed