File size: 8,810 Bytes
c7af659
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
// Programme D3 — simulated financial layer (ledger, transfers, merchants, ATM, intents).
// Typed client for the /finance/v1 surface. Reuses the D2 session cookie + in-memory CSRF token
// and the D2 passkey step-up ceremony. ALL money is simulated, SAR only, integer minor units.

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 } : {}
}

/** True when the backend requires a fresh passkey step-up (HTTP 422 step_up_required). */
export function isStepUpRequired(e: unknown): boolean {
  return e instanceof ApiRequestError && e.status === 422 && /step_up_required/.test(e.message)
}

/** Runs `fn`; on 422 step_up_required, runs the D2 step-up ceremony then retries once. */
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
  }
}

// ---- Types (safe projections only) ----
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
}

// ---- Formatting ----
/** Format integer minor units (halalas) as a SAR major-unit string, e.g. 2550 -> "25.50". */
export function formatMinor(minor: number, currency = 'SAR'): string {
  const v = (minor / 100).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
  return `${v} ${currency}`
}

// ---- Status ----
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`)
}

// ---- Accounts / history ----
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
}

// ---- Transfers ----
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(),
    }),
  )
}

// ---- Merchants ----
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(),
    }),
  )
}

// ---- ATM ----
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
}

// ---- Intent assistant (voice/text) ----
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() }),
  )
}