| |
| |
| |
|
|
| import { apiRequest, ApiRequestError } from './client' |
| import { getD2Csrf, stepUp } from './identity' |
|
|
| const V1 = '/finance/v1' |
|
|
| function csrfHeaders(): Record<string, string> { |
| const c = getD2Csrf() |
| return c ? { 'X-CSRF-Token': c } : {} |
| } |
|
|
| |
| export function isStepUpRequired(e: unknown): boolean { |
| return e instanceof ApiRequestError && e.status === 422 && /step_up_required/.test(e.message) |
| } |
|
|
| |
| export async function withD3StepUp<T>(fn: () => Promise<T>): Promise<T> { |
| try { |
| return await fn() |
| } catch (e) { |
| if (isStepUpRequired(e)) { |
| await stepUp() |
| return fn() |
| } |
| throw e |
| } |
| } |
|
|
| |
| export interface FinanceAccount { |
| id: string |
| public_ref: string |
| masked_ref: string |
| product_type: string |
| name_en: string |
| name_ar: string |
| currency: string |
| status: string |
| is_default: boolean |
| posted_minor: number |
| hold_minor: number |
| available_minor: number |
| simulation_only: boolean |
| } |
|
|
| export interface FinanceTxn { |
| id: string |
| type: string |
| state: string |
| amount_minor: number |
| currency: string |
| source_financial_account_id: string | null |
| dest_financial_account_id: string | null |
| merchant_id: string | null |
| note: string |
| posted_journal_id: string | null |
| refunded_minor: number |
| created_at: number |
| completed_at: number | null |
| simulation_only: boolean |
| } |
|
|
| export interface Receipt { |
| receipt_ref: string |
| transaction_ref: string |
| type: string |
| status: string |
| amount_minor: number |
| currency: string |
| source_masked_ref: string |
| recipient_label: string |
| note: string |
| journal_ref: string |
| simulation_only: boolean |
| } |
|
|
| export interface Merchant { |
| id: string |
| public_ref: string |
| name_en: string |
| name_ar: string |
| category: string |
| status: string |
| simulation_only: boolean |
| } |
|
|
| export interface RecipientMatch { |
| recipient_user_id: string |
| display_alias: string |
| public_handle: string |
| dest_account_id: string |
| dest_masked_ref: string |
| } |
|
|
| export interface AtmDeposit { |
| id: string |
| kiosk_ref: string |
| amount_minor: number |
| currency: string |
| state: string |
| expires_at: number |
| destination_account_id: string |
| destination_masked_ref?: string |
| created_at: number |
| simulation_only: boolean |
| } |
|
|
| export interface IntentView { |
| id: string |
| action: string |
| recipient_type: string |
| recipient_ref: string |
| resolved_ref: string |
| source_account_id: string | null |
| amount_minor: number | null |
| currency: string |
| note: string |
| origin: string |
| state: string |
| expires_at: number |
| version: number |
| unresolved: string[] |
| review: Record<string, unknown> |
| amount_error: string | null |
| notice: string |
| } |
|
|
| export interface TxnResult { |
| transaction: FinanceTxn |
| receipt: Receipt |
| } |
|
|
| |
| |
| export function formatMinor(minor: number, currency = 'SAR'): string { |
| const v = (minor / 100).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) |
| return `${v} ${currency}` |
| } |
|
|
| |
| export interface FinanceStatus { |
| enabled: boolean |
| ready?: boolean |
| default_currency?: string |
| journal_mode?: string |
| max_transfer_minor?: number |
| max_daily_outflow_minor?: number |
| simulation_only?: boolean |
| } |
|
|
| export async function financeStatus(): Promise<FinanceStatus> { |
| return apiRequest<FinanceStatus>(`${V1}/status`) |
| } |
|
|
| |
| export async function listAccounts(): Promise<FinanceAccount[]> { |
| const r = await apiRequest<{ accounts: FinanceAccount[] }>(`${V1}/accounts`) |
| return r.accounts |
| } |
|
|
| export async function accountTransactions(accountId: string): Promise<FinanceTxn[]> { |
| const r = await apiRequest<{ transactions: FinanceTxn[] }>(`${V1}/accounts/${accountId}/transactions`) |
| return r.transactions |
| } |
|
|
| export async function listTransactions(): Promise<FinanceTxn[]> { |
| const r = await apiRequest<{ transactions: FinanceTxn[] }>(`${V1}/transactions`) |
| return r.transactions |
| } |
|
|
| export async function getReceipt(txnId: string): Promise<Receipt> { |
| const r = await apiRequest<{ receipt: Receipt }>(`${V1}/transactions/${txnId}/receipt`) |
| return r.receipt |
| } |
|
|
| |
| export async function ownTransfer( |
| sourceId: string, destId: string, amount: string, note: string, idempotencyKey: string, |
| ): Promise<TxnResult> { |
| return withD3StepUp(() => |
| apiRequest<TxnResult>(`${V1}/transfers/own/submit`, { |
| body: { source_account_id: sourceId, dest_account_id: destId, amount, note, idempotency_key: idempotencyKey }, |
| headers: csrfHeaders(), |
| }), |
| ) |
| } |
|
|
| export async function resolveRecipient(handle: string): Promise<RecipientMatch> { |
| const r = await apiRequest<{ recipient: RecipientMatch }>(`${V1}/recipients/resolve`, { |
| body: { handle }, headers: csrfHeaders(), |
| }) |
| return r.recipient |
| } |
|
|
| export async function p2pTransfer( |
| sourceId: string, handle: string, amount: string, note: string, idempotencyKey: string, |
| ): Promise<TxnResult> { |
| return withD3StepUp(() => |
| apiRequest<TxnResult>(`${V1}/transfers/p2p/submit`, { |
| body: { source_account_id: sourceId, handle, amount, note, idempotency_key: idempotencyKey }, |
| headers: csrfHeaders(), |
| }), |
| ) |
| } |
|
|
| |
| export async function listMerchants(): Promise<Merchant[]> { |
| const r = await apiRequest<{ merchants: Merchant[] }>(`${V1}/merchants`) |
| return r.merchants |
| } |
|
|
| export async function payMerchant( |
| merchantId: string, sourceId: string, amount: string, note: string, idempotencyKey: string, |
| ): Promise<TxnResult> { |
| return withD3StepUp(() => |
| apiRequest<TxnResult>(`${V1}/merchants/${merchantId}/payments`, { |
| body: { source_account_id: sourceId, amount, note, idempotency_key: idempotencyKey }, |
| headers: csrfHeaders(), |
| }), |
| ) |
| } |
|
|
| export async function refundMerchant( |
| transactionId: string, merchantId: string, amount: string, idempotencyKey: string, |
| ): Promise<TxnResult> { |
| return withD3StepUp(() => |
| apiRequest<TxnResult>(`${V1}/merchant/transactions/${transactionId}/refunds`, { |
| body: { merchant_id: merchantId, amount, idempotency_key: idempotencyKey }, |
| headers: csrfHeaders(), |
| }), |
| ) |
| } |
|
|
| |
| export async function atmInitiate( |
| destinationAccountId: string, amount: string, kioskRef: string, |
| ): Promise<AtmDeposit> { |
| const r = await apiRequest<{ deposit: AtmDeposit }>(`${V1}/atm/deposits`, { |
| body: { destination_account_id: destinationAccountId, amount, kiosk_ref: kioskRef }, |
| headers: csrfHeaders(), |
| }) |
| return r.deposit |
| } |
|
|
| export async function atmCustomerConfirm(depositId: string): Promise<AtmDeposit> { |
| const r = await apiRequest<{ deposit: AtmDeposit }>(`${V1}/atm/deposits/${depositId}/customer-confirm`, { |
| body: {}, headers: csrfHeaders(), |
| }) |
| return r.deposit |
| } |
|
|
| export async function atmOperatorQueue(): Promise<AtmDeposit[]> { |
| const r = await apiRequest<{ deposits: AtmDeposit[] }>(`${V1}/operator/atm/deposits`) |
| return r.deposits |
| } |
|
|
| export async function atmOperatorConfirm(depositId: string): Promise<TxnResult> { |
| return withD3StepUp(() => |
| apiRequest<TxnResult>(`${V1}/operator/atm/deposits/${depositId}/confirm`, { |
| body: {}, headers: csrfHeaders(), |
| }), |
| ) |
| } |
|
|
| export async function atmOperatorReject(depositId: string): Promise<AtmDeposit> { |
| const r = await apiRequest<{ deposit: AtmDeposit }>(`${V1}/operator/atm/deposits/${depositId}/reject`, { |
| body: {}, headers: csrfHeaders(), |
| }) |
| return r.deposit |
| } |
|
|
| |
| export async function intentParse(text: string, origin = 'text'): Promise<IntentView> { |
| const r = await apiRequest<{ intent: IntentView }>(`${V1}/intents`, { |
| body: { text, origin }, headers: csrfHeaders(), |
| }) |
| return r.intent |
| } |
|
|
| export async function intentClarify( |
| id: string, fields: { amount?: string; handle?: string; source_account_id?: string; note?: string }, |
| ): Promise<IntentView> { |
| const r = await apiRequest<{ intent: IntentView }>(`${V1}/intent-assistant/${id}/clarify`, { |
| body: fields, headers: csrfHeaders(), |
| }) |
| return r.intent |
| } |
|
|
| export async function intentConfirm( |
| id: string, opts: { source_account_id?: string; dest_account_id?: string } = {}, |
| ): Promise<TxnResult> { |
| return withD3StepUp(() => |
| apiRequest<TxnResult>(`${V1}/intents/${id}/confirm`, { body: opts, headers: csrfHeaders() }), |
| ) |
| } |
|
|