| |
| |
|
|
| import { supabase } from './supabase' |
|
|
| |
|
|
| export type ParticipantType = 'citizen' | 'enterprise' | 'platform' | 'reserve' |
| export type PayoutStatus = 'pending' | 'sent' | 'confirmed' | 'failed' |
| export type CycleStatus = 'pending' | 'completed' | 'failed' | 'compensating' |
|
|
| export interface SettlementParticipantRow { |
| rowId: string |
| cycleId: string |
| tradeId: string |
| cycleKey: string |
| cycleStatus: CycleStatus |
| grossProceeds: bigint |
| platformFee: bigint |
| beneficiaryPool: bigint |
| citizenPool: bigint |
| reservePool: bigint |
| roundingResidual: bigint |
| ruleSetVersion: string |
| participantId: string |
| participantType: ParticipantType |
| participantName: string |
| basisLabel: string | null |
| co2Grams: bigint | null |
| contributionWeight: number | null |
| grossAmount: bigint |
| withholdingTax: bigint |
| netAmount: bigint |
| ledgerTxnId: string | null |
| idempotencyKey: string |
| payoutStatus: PayoutStatus |
| distributedAt: string |
| } |
|
|
| export interface SettlementSummary { |
| cycleId: string |
| tradeId: string |
| cycleKey: string |
| cycleStatus: CycleStatus |
| grossProceeds: bigint |
| platformFee: bigint |
| beneficiaryPool: bigint |
| citizenPool: bigint |
| reservePool: bigint |
| roundingResidual: bigint |
| ruleSetVersion: string |
| participantCounts: { citizen: number; enterprise: number; platform: number; reserve: number } |
| totalDistributed: bigint |
| invariantOk: boolean |
| } |
|
|
| export interface DistributeResult { |
| cycleId: string |
| cycleKey: string |
| status: string |
| grossProceeds: number |
| platformFee: number |
| beneficiaryPool: number |
| citizenPool: number |
| reservePool: number |
| roundingResidual: number |
| totalDistributed: number |
| ruleSetVersion: string |
| } |
|
|
| |
|
|
| |
| function mapRow(r: any): SettlementParticipantRow { |
| return { |
| rowId: r.row_id, |
| cycleId: r.cycle_id, |
| tradeId: r.trade_id, |
| cycleKey: r.cycle_key, |
| cycleStatus: r.cycle_status as CycleStatus, |
| grossProceeds: BigInt(r.gross_proceeds ?? 0), |
| platformFee: BigInt(r.platform_fee ?? 0), |
| beneficiaryPool: BigInt(r.beneficiary_pool ?? 0), |
| citizenPool: BigInt(r.citizen_pool ?? 0), |
| reservePool: BigInt(r.reserve_pool ?? 0), |
| roundingResidual: BigInt(r.rounding_residual ?? 0), |
| ruleSetVersion: r.rule_set_version ?? 'v1.0', |
| participantId: r.participant_id, |
| participantType: r.participant_type as ParticipantType, |
| participantName: r.participant_name, |
| basisLabel: r.basis_label ?? null, |
| co2Grams: r.co2_grams != null ? BigInt(r.co2_grams) : null, |
| contributionWeight: r.contribution_weight != null ? Number(r.contribution_weight) : null, |
| grossAmount: BigInt(r.gross_amount ?? 0), |
| withholdingTax: BigInt(r.withholding_tax ?? 0), |
| netAmount: BigInt(r.net_amount ?? 0), |
| ledgerTxnId: r.ledger_txn_id ?? null, |
| idempotencyKey: r.idempotency_key, |
| payoutStatus: r.payout_status as PayoutStatus, |
| distributedAt: r.distributed_at, |
| } |
| } |
|
|
| |
|
|
| |
| |
| |
| |
| export async function distributeProceeds(tradeId: string): Promise<DistributeResult> { |
| const { data, error } = await supabase.rpc('distribute_sale_proceeds', { |
| p_trade_id: tradeId, |
| }) |
| if (error) throw error |
| return data as DistributeResult |
| } |
|
|
| |
| |
| |
| export async function getReconciliationRows(tradeId: string): Promise<SettlementParticipantRow[]> { |
| const { data, error } = await supabase |
| .from('settlement_reconciliation_v1') |
| .select('*') |
| .eq('trade_id', tradeId) |
| .order('participant_type') |
| .order('gross_amount', { ascending: false }) |
| if (error) throw error |
| return (data ?? []).map(mapRow) |
| } |
|
|
| |
| |
| |
| |
| export async function getCitizenSettlements( |
| userId: string, |
| limit = 20, |
| ): Promise<SettlementParticipantRow[]> { |
| const { data, error } = await supabase |
| .from('settlement_reconciliation_v1') |
| .select('*') |
| .eq('participant_id', userId) |
| .eq('participant_type', 'citizen') |
| .order('distributed_at', { ascending: false }) |
| .limit(limit) |
| if (error) throw error |
| return (data ?? []).map(mapRow) |
| } |
|
|
| |
| |
| |
| |
| export function buildSummary(rows: SettlementParticipantRow[]): SettlementSummary | null { |
| if (rows.length === 0) return null |
|
|
| const first = rows[0] |
| const counts = { citizen: 0, enterprise: 0, platform: 0, reserve: 0 } |
| let totalDistributed = 0n |
|
|
| for (const r of rows) { |
| counts[r.participantType]++ |
| totalDistributed += r.netAmount |
| } |
|
|
| const invariantOk = |
| totalDistributed + first.roundingResidual === first.grossProceeds |
|
|
| return { |
| cycleId: first.cycleId, |
| tradeId: first.tradeId, |
| cycleKey: first.cycleKey, |
| cycleStatus: first.cycleStatus, |
| grossProceeds: first.grossProceeds, |
| platformFee: first.platformFee, |
| beneficiaryPool: first.beneficiaryPool, |
| citizenPool: first.citizenPool, |
| reservePool: first.reservePool, |
| roundingResidual: first.roundingResidual, |
| ruleSetVersion: first.ruleSetVersion, |
| participantCounts: counts, |
| totalDistributed, |
| invariantOk, |
| } |
| } |
|
|
| |
|
|
| const KRW_LOCALE: Intl.NumberFormatOptions = { |
| style: 'currency', |
| currency: 'KRW', |
| maximumFractionDigits: 0, |
| } |
|
|
| export function formatKrw(amount: bigint | number): string { |
| return new Intl.NumberFormat('ko-KR', KRW_LOCALE).format(Number(amount)) |
| } |
|
|
| export function participantTypeLabel(t: ParticipantType): string { |
| const MAP: Record<ParticipantType, string> = { |
| citizen: 'μλ―Ό', |
| enterprise: 'μν κΈ°μ
', |
| platform: 'νλ«νΌ', |
| reserve: 'μ€λΉκΈ', |
| } |
| return MAP[t] |
| } |
|
|
| export function payoutStatusBadge(s: PayoutStatus): { label: string; color: string } { |
| const MAP: Record<PayoutStatus, { label: string; color: string }> = { |
| pending: { label: 'λκΈ°', color: 'text-amber-600 bg-amber-50' }, |
| sent: { label: 'λ°μ‘', color: 'text-blue-600 bg-blue-50' }, |
| confirmed: { label: 'νμ ', color: 'text-green-600 bg-green-50' }, |
| failed: { label: 'μ€ν¨', color: 'text-red-600 bg-red-50' }, |
| } |
| return MAP[s] |
| } |
|
|