649 lines
23 KiB
TypeScript
649 lines
23 KiB
TypeScript
const BASE = "/api";
|
||
|
||
const TOKEN_KEY = "ar_token";
|
||
export const getToken = () => localStorage.getItem(TOKEN_KEY);
|
||
export const setToken = (t: string) => localStorage.setItem(TOKEN_KEY, t);
|
||
export const clearToken = () => localStorage.removeItem(TOKEN_KEY);
|
||
|
||
/** Set by the auth provider: called on a 401 so the app can drop to the login screen. */
|
||
export let onUnauthorized: (() => void) | null = null;
|
||
export const setOnUnauthorized = (fn: (() => void) | null) => { onUnauthorized = fn; };
|
||
|
||
async function req<T>(path: string, opts: RequestInit = {}): Promise<T> {
|
||
const headers: Record<string, string> = {};
|
||
if (opts.body && !(opts.body instanceof FormData)) headers["Content-Type"] = "application/json";
|
||
const token = getToken();
|
||
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||
const res = await fetch(`${BASE}${path}`, { headers, ...opts });
|
||
if (!res.ok) {
|
||
let detail = res.statusText;
|
||
try {
|
||
detail = (await res.json()).detail ?? detail;
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
if (res.status === 401 && !path.startsWith("/auth/")) {
|
||
clearToken();
|
||
onUnauthorized?.();
|
||
}
|
||
throw new Error(detail);
|
||
}
|
||
const ct = res.headers.get("content-type") ?? "";
|
||
return ct.includes("json") ? res.json() : (undefined as unknown as T);
|
||
}
|
||
|
||
// ---- Types -------------------------------------------------------------
|
||
export interface SessionT {
|
||
id: number;
|
||
name: string;
|
||
reporting_month: string | null;
|
||
month_end_date: string | null;
|
||
reporting_currency: string;
|
||
clearing_lag_days: number;
|
||
rounding_tolerance: number;
|
||
allowance_for_returns: number;
|
||
manual_adjustment: number;
|
||
opening_mode?: string;
|
||
opening_source_session_id?: number | null;
|
||
status: string;
|
||
progress_stage: string;
|
||
progress_pct: number;
|
||
progress_rows_done: number;
|
||
progress_rows_total: number;
|
||
eta_seconds: number;
|
||
error: string;
|
||
created_at: string;
|
||
updated_at: string;
|
||
/** A month-end control failed with error severity — no receivable figure is published. */
|
||
blocked?: boolean;
|
||
blocked_reason?: string;
|
||
/** auto = bank-receipt date wins, clearing-lag fallback · manual = bank dates only. */
|
||
payout_mode?: string;
|
||
/** Bank receipts / payout mode changed after the last run — re-process to apply. */
|
||
needs_reprocess?: boolean;
|
||
/** Journal approved = published to the Accounts Summary (list endpoint only). */
|
||
journal_approved?: boolean;
|
||
/** Another closing exists for the same reporting month (list endpoint only). */
|
||
duplicate_month?: boolean;
|
||
}
|
||
|
||
export interface UploadResultT {
|
||
files: FileT[];
|
||
skipped: { filename: string; reason: string }[];
|
||
}
|
||
|
||
export interface AuthUserT {
|
||
username: string;
|
||
display_name: string;
|
||
}
|
||
|
||
export interface PayoutT {
|
||
marketplace: string;
|
||
account_type: string;
|
||
settlement_id: string;
|
||
amazon_date: string | null;
|
||
amount: number;
|
||
rows: number;
|
||
bank_date: string | null;
|
||
bank_amount: number | null;
|
||
note: string;
|
||
entered_by: string;
|
||
received_now: boolean | null;
|
||
received_next_run: boolean;
|
||
}
|
||
|
||
export interface PayoutsT {
|
||
payout_mode: string;
|
||
clearing_lag_days: number;
|
||
month_end: string | null;
|
||
needs_reprocess: boolean;
|
||
payouts: PayoutT[];
|
||
}
|
||
|
||
/** One month-end control (core/controls.py). Distinct from ControlRowT, which is a row of
|
||
* the Finance reconciliation control sheet. */
|
||
export interface MonthEndControlT {
|
||
key: string;
|
||
label: string;
|
||
status: "pass" | "fail" | "not_applicable";
|
||
severity: "error" | "warning" | "info";
|
||
detail: string;
|
||
evidence: string[];
|
||
checked_at: string | null;
|
||
}
|
||
|
||
export interface ControlsT {
|
||
available: boolean;
|
||
controls: MonthEndControlT[];
|
||
blocked: boolean;
|
||
blocked_reason: string;
|
||
passed: number;
|
||
failed: number;
|
||
total: number;
|
||
confirmed?: number;
|
||
}
|
||
|
||
export interface FileT {
|
||
id: number;
|
||
filename: string;
|
||
size_bytes: number;
|
||
sha256: string;
|
||
data_sheet: string | null;
|
||
imported_rows: number;
|
||
min_date: string | null;
|
||
max_date: string | null;
|
||
currency: string | null;
|
||
marketplace: string | null;
|
||
status: string;
|
||
message: string;
|
||
worksheets: string[];
|
||
}
|
||
|
||
/**
|
||
* Endpoints that publish a receivable figure return this instead when a month-end control
|
||
* has failed: `available:false, blocked:true` and NO numbers. Every consumer must check
|
||
* `blocked` before reading a figure — the fields below are absent in that case.
|
||
*/
|
||
export interface BlockableT {
|
||
blocked?: boolean;
|
||
blocked_reason?: string;
|
||
available?: boolean;
|
||
}
|
||
|
||
export interface SummaryT extends BlockableT {
|
||
closing_receivable_usd: number | null;
|
||
reconciliation_status: string | null;
|
||
reserve_total: number;
|
||
transfers_total: number;
|
||
receivable_orders: number;
|
||
paid_orders: number;
|
||
num_settlements: number;
|
||
num_receivable_settlements: number;
|
||
num_paid_settlements: number;
|
||
num_transactions: number;
|
||
num_receivable_transactions: number;
|
||
exceptions_by_severity: Record<string, number>;
|
||
receivable_by_marketplace: {
|
||
marketplace: string;
|
||
receivable_local: number;
|
||
receivable_usd: number;
|
||
currency: string;
|
||
}[];
|
||
}
|
||
|
||
export interface SettlementT {
|
||
marketplace: string;
|
||
account_type: string;
|
||
settlement_id: string;
|
||
order_total: number;
|
||
transfer_total: number;
|
||
transfer_amount: number | null;
|
||
transfer_date: string | null;
|
||
transfer_received: boolean | null;
|
||
row_count: number;
|
||
first_date: string | null;
|
||
last_date: string | null;
|
||
status: string;
|
||
}
|
||
|
||
export interface TxnT {
|
||
id: number;
|
||
source_file: string;
|
||
source_row: number;
|
||
marketplace: string;
|
||
settlement_id: string;
|
||
order_id: string | null;
|
||
sku: string | null;
|
||
txn_type: string;
|
||
account_type: string;
|
||
posted_date: string | null;
|
||
total: number;
|
||
currency: string;
|
||
settlement_status: string | null;
|
||
receivable_flag: boolean;
|
||
}
|
||
|
||
export interface ReconT {
|
||
uploaded_total: number;
|
||
receivable_orders: number;
|
||
paid_orders: number;
|
||
transfers_total: number;
|
||
reserve_total: number;
|
||
manual_adjustments: number;
|
||
final_receivable_usd: number;
|
||
identity_difference: number;
|
||
status: string;
|
||
notes: string[];
|
||
}
|
||
|
||
export interface ExceptionT {
|
||
category: string;
|
||
severity: string;
|
||
detail: string;
|
||
source: string;
|
||
}
|
||
|
||
export interface ReceivableRowT {
|
||
marketplace: string;
|
||
account_type: string;
|
||
additional_sales: number;
|
||
reserve: number;
|
||
receivable_local: number;
|
||
fx_rate: number;
|
||
receivable_usd: number;
|
||
currency: string;
|
||
}
|
||
|
||
export interface JournalLineT {
|
||
key: string;
|
||
gl_account: string;
|
||
values: number[];
|
||
total: number;
|
||
}
|
||
export interface JournalT {
|
||
available: boolean;
|
||
entry_no?: string;
|
||
marketplace?: string;
|
||
marketplaces?: string[];
|
||
periods?: { key: string; label: string; min_date: string | null; max_date: string | null }[];
|
||
lines?: JournalLineT[];
|
||
receivable?: JournalLineT;
|
||
/** Balancing figure of the accrual entry (Transfer excluded): Dr A/R by net revenue. */
|
||
receivable_accrual?: JournalLineT;
|
||
reviewed_by?: string;
|
||
reviewed_at?: string | null;
|
||
approved_by?: string;
|
||
approved_at?: string | null;
|
||
}
|
||
|
||
export interface AccountsSummaryT {
|
||
available: boolean;
|
||
line_keys: string[];
|
||
receivable_key: string;
|
||
months: {
|
||
month: string; session_id: number; session_name: string;
|
||
reviewed_by: string; approved_by: string; approved_at: string | null; entry_no: string;
|
||
}[];
|
||
marketplaces: string[];
|
||
cells: {
|
||
month: string; session_id: number; marketplace: string; currency: string; fx_rate: number;
|
||
values: Record<string, number>; receivable: number;
|
||
}[];
|
||
/** Months with results that are NOT published (unapproved / blocked), with the reason —
|
||
* so a month never silently vanishes from this view. */
|
||
pending?: { month: string; session_id: number; session_name: string; reason: string }[];
|
||
}
|
||
|
||
export interface ComponentT {
|
||
key: string;
|
||
label: string;
|
||
group: "revenue" | "fee" | "payout" | "memo";
|
||
values: number[];
|
||
total: number;
|
||
}
|
||
export interface FinanceSummaryT extends BlockableT {
|
||
available: boolean;
|
||
marketplace?: string;
|
||
marketplaces?: string[];
|
||
currency?: string;
|
||
reporting_month?: string;
|
||
month_end?: string;
|
||
period_labels?: string[];
|
||
opening_balance?: number;
|
||
components?: ComponentT[];
|
||
gross_revenue?: number;
|
||
net_revenue?: number;
|
||
disbursements?: number;
|
||
in_transit_payouts?: number;
|
||
closing_receivable?: number;
|
||
settlement_closing?: number | null;
|
||
finance_closing?: number | null;
|
||
difference?: number | null;
|
||
status?: string;
|
||
verified_by?: string;
|
||
verified_at?: string | null;
|
||
ledger?: LedgerRowT[];
|
||
}
|
||
|
||
export interface LedgerPeriodT {
|
||
key: string; label: string; revenue: number; payouts_received: number;
|
||
payouts_in_transit: number; rows: number; balance: number;
|
||
/** USD equivalents, converted at each transaction date's FX rate. */
|
||
revenue_usd: number; payouts_received_usd: number;
|
||
payouts_in_transit_usd: number; balance_usd: number;
|
||
}
|
||
export interface LedgerDetailT {
|
||
available: boolean;
|
||
marketplace?: string; marketplaces?: string[]; currency?: string;
|
||
granularity?: string; date_from?: string | null; date_to?: string | null;
|
||
opening?: number; periods?: LedgerPeriodT[]; closing?: number;
|
||
session_closing?: number; filtered?: boolean; in_transit_total?: number;
|
||
/** The marketplace month rate; the opening balance converts at this rate. */
|
||
month_rate?: number;
|
||
opening_usd?: number;
|
||
/** Roll-forward valued at transaction-date rates — differs from closing × month rate
|
||
* whenever daily overrides exist. */
|
||
closing_usd?: number;
|
||
in_transit_total_usd?: number;
|
||
}
|
||
|
||
export interface FxDailyRowT {
|
||
date: string; local: number; rate: number; usd: number;
|
||
source: string; rows: number; revenue: number; payouts: number;
|
||
}
|
||
export interface FxDailyT {
|
||
available: boolean;
|
||
marketplace?: string; marketplaces?: string[]; currency?: string;
|
||
month_rate?: number; rows?: FxDailyRowT[];
|
||
total_local?: number; total_usd?: number;
|
||
}
|
||
|
||
export interface ReconLineT {
|
||
key: string; label: string; amount: number;
|
||
effect: "increase" | "decrease" | "mixed" | "neutral"; note: string;
|
||
}
|
||
export interface ReconGroupT {
|
||
key: string; title: string; lines: ReconLineT[];
|
||
subtotal: number; subtotal_label: string;
|
||
}
|
||
export interface ReconDetailT {
|
||
available: boolean;
|
||
marketplace?: string; marketplaces?: string[]; currency?: string;
|
||
sign_legend?: Record<string, string>;
|
||
groups?: ReconGroupT[];
|
||
net_revenue?: number; opening?: number; closing?: number;
|
||
settlement_closing?: number | null; variance?: number | null; variance_status?: string;
|
||
}
|
||
|
||
export interface AllMarketRowT {
|
||
marketplace: string; currency: string; fx_rate: number;
|
||
opening: number; net_revenue: number; gross_revenue: number;
|
||
payouts_received: number; in_transit: number;
|
||
closing_local: number; closing_usd: number;
|
||
settlement_closing: number | null; settlement_closing_usd: number | null;
|
||
variance: number | null;
|
||
}
|
||
export interface AllMarketsT {
|
||
available: boolean;
|
||
markets?: AllMarketRowT[];
|
||
total?: {
|
||
currency: string; opening: number; gross_revenue: number; net_revenue: number;
|
||
payouts_received: number; in_transit: number; closing_usd: number;
|
||
settlement_closing_usd: number; variance: number;
|
||
};
|
||
}
|
||
|
||
export interface MappingRulesT {
|
||
fields: string[];
|
||
rules: { id: number; normalized_header: string; field: string }[];
|
||
}
|
||
|
||
export interface ControlRowT {
|
||
key: string;
|
||
label: string;
|
||
critical: boolean;
|
||
dashboard: number;
|
||
finance: number | null;
|
||
difference: number | null;
|
||
status: string;
|
||
}
|
||
export interface ControlT {
|
||
available: boolean;
|
||
tolerance?: number;
|
||
rows?: ControlRowT[];
|
||
verified_by?: string;
|
||
verified_at?: string | null;
|
||
comment?: string;
|
||
can_complete?: boolean;
|
||
completed?: boolean;
|
||
}
|
||
|
||
export interface OpeningCandidateT {
|
||
session_id: number;
|
||
name: string;
|
||
reporting_month: string | null;
|
||
month_end: string | null;
|
||
markets: { marketplace: string; amount: number; currency: string }[];
|
||
}
|
||
export interface OpeningCandidatesT {
|
||
current_mode: string;
|
||
current_source: number | null;
|
||
candidates: OpeningCandidateT[];
|
||
}
|
||
|
||
export interface OpeningBalanceT {
|
||
marketplace: string;
|
||
amount: number;
|
||
reason: string;
|
||
source: string;
|
||
}
|
||
|
||
export interface DefinitionT {
|
||
formula: string;
|
||
source: string;
|
||
note?: string;
|
||
}
|
||
|
||
export interface OpeningWorksheetRowT {
|
||
marketplace: string;
|
||
currency: string;
|
||
fx_rate: number;
|
||
opening: number;
|
||
source: string;
|
||
reason: string;
|
||
net_revenue: number;
|
||
payouts_received: number;
|
||
movement: number;
|
||
roll_forward_closing: number;
|
||
settlement_closing: number | null;
|
||
variance: number | null;
|
||
implied_opening: number | null;
|
||
reconciled: boolean;
|
||
}
|
||
|
||
export interface OpeningWorksheetT {
|
||
available: boolean;
|
||
reporting_month: string;
|
||
mode: string;
|
||
source_session_id: number | null;
|
||
rows: OpeningWorksheetRowT[];
|
||
all_zero: boolean;
|
||
unreconciled: number;
|
||
total_abs_variance_usd: number;
|
||
candidates: OpeningCandidateT[];
|
||
}
|
||
export interface LedgerRowT {
|
||
period: string;
|
||
description: string;
|
||
debit: number | null;
|
||
credit: number | null;
|
||
balance: number;
|
||
}
|
||
export interface ArMovementT {
|
||
available: boolean;
|
||
marketplace?: string;
|
||
marketplaces?: string[];
|
||
currency?: string;
|
||
opening?: number;
|
||
opening_source?: string;
|
||
opening_reason?: string;
|
||
gross_revenue?: number;
|
||
net_revenue?: number;
|
||
net_revenue_per_period?: number[];
|
||
total_receivable?: number;
|
||
received_payouts?: number;
|
||
all_payouts?: number;
|
||
in_transit_payouts?: number;
|
||
closing?: number;
|
||
settlement_closing?: number | null;
|
||
difference_vs_settlement?: number | null;
|
||
period_labels?: string[];
|
||
revenue_lines?: JournalLineT[];
|
||
ledger?: LedgerRowT[];
|
||
}
|
||
|
||
const q = (o: Record<string, string | undefined>) =>
|
||
Object.entries(o).filter(([, v]) => v).map(([k, v]) => `${k}=${encodeURIComponent(v!)}`).join("&");
|
||
|
||
// ---- Endpoints ---------------------------------------------------------
|
||
export const api = {
|
||
health: () => req<{ status: string; version: string }>("/health"),
|
||
|
||
authStatus: () => req<{ auth_required: boolean; email_enabled: boolean }>("/auth/status"),
|
||
login: (username: string, password: string) =>
|
||
req<{ token: string; user: AuthUserT }>("/auth/login", {
|
||
method: "POST", body: JSON.stringify({ username, password }),
|
||
}),
|
||
me: () => req<{ authenticated: boolean; username?: string; display_name?: string }>("/auth/me"),
|
||
changePassword: (current_password: string, new_password: string) =>
|
||
req<{ changed: boolean }>("/auth/change-password", {
|
||
method: "POST", body: JSON.stringify({ current_password, new_password }),
|
||
}),
|
||
requestPasswordCode: (username = "") =>
|
||
req<{ sent: boolean; detail: string }>("/auth/request-code", {
|
||
method: "POST", body: JSON.stringify({ username }),
|
||
}),
|
||
resetPassword: (code: string, new_password: string, username = "") =>
|
||
req<{ changed: boolean }>("/auth/reset-password", {
|
||
method: "POST", body: JSON.stringify({ username, code, new_password }),
|
||
}),
|
||
|
||
listSessions: () => req<SessionT[]>("/sessions"),
|
||
createSession: (body: Partial<SessionT> & { allow_duplicate?: boolean }) =>
|
||
req<SessionT>("/sessions", { method: "POST", body: JSON.stringify(body) }),
|
||
getSession: (id: number) => req<SessionT>(`/sessions/${id}`),
|
||
updateSession: (id: number, body: Partial<SessionT>) =>
|
||
req<SessionT>(`/sessions/${id}`, { method: "PATCH", body: JSON.stringify(body) }),
|
||
deleteSession: (id: number) => req<void>(`/sessions/${id}`, { method: "DELETE" }),
|
||
reopenSession: (id: number) => req<SessionT>(`/sessions/${id}/reopen`, { method: "POST" }),
|
||
|
||
listFiles: (id: number) => req<FileT[]>(`/sessions/${id}/files`),
|
||
uploadFiles: (id: number, files: File[]) => {
|
||
const fd = new FormData();
|
||
files.forEach((f) => fd.append("files", f));
|
||
return req<UploadResultT>(`/sessions/${id}/files`, { method: "POST", body: fd });
|
||
},
|
||
deleteFile: (id: number, fileId: number) =>
|
||
req<void>(`/sessions/${id}/files/${fileId}`, { method: "DELETE" }),
|
||
|
||
process: (id: number) => req<{ started: boolean }>(`/sessions/${id}/process`, { method: "POST" }),
|
||
status: (id: number) =>
|
||
req<{ status: string; stage: string; pct: number; error: string; session: SessionT }>(
|
||
`/sessions/${id}/status`
|
||
),
|
||
|
||
summary: (id: number) => req<SummaryT>(`/sessions/${id}/summary`),
|
||
receivable: (id: number) => req<ReceivableRowT[]>(`/sessions/${id}/receivable`),
|
||
settlements: (id: number) => req<SettlementT[]>(`/sessions/${id}/settlements`),
|
||
transactions: (id: number, params: Record<string, string | number | boolean | undefined>) => {
|
||
const q = new URLSearchParams();
|
||
Object.entries(params).forEach(([k, v]) => v !== undefined && v !== "" && q.set(k, String(v)));
|
||
return req<{ total: number; limit: number; offset: number; rows: TxnT[] }>(
|
||
`/sessions/${id}/transactions?${q}`
|
||
);
|
||
},
|
||
exceptions: (id: number) => req<ExceptionT[]>(`/sessions/${id}/exceptions`),
|
||
mappingRules: () => req<MappingRulesT>("/mapping-rules"),
|
||
saveMappingRules: (items: { header: string; field: string }[]) =>
|
||
req<MappingRulesT>("/mapping-rules", { method: "PUT", body: JSON.stringify(items) }),
|
||
deleteMappingRule: (ruleId: number) =>
|
||
req<MappingRulesT>(`/mapping-rules/${ruleId}`, { method: "DELETE" }),
|
||
reconciliation: (id: number) => req<ReconT>(`/sessions/${id}/reconciliation`),
|
||
aging: (id: number) =>
|
||
req<BlockableT & { bands: string[]; rows: Record<string, number | string>[] }>(
|
||
`/sessions/${id}/aging`),
|
||
journal: (id: number, marketplace?: string) =>
|
||
req<JournalT>(`/sessions/${id}/journal${marketplace ? `?marketplace=${encodeURIComponent(marketplace)}` : ""}`),
|
||
reviewJournal: (id: number, name: string) =>
|
||
req<JournalT>(`/sessions/${id}/journal/review`, { method: "POST", body: JSON.stringify({ name }) }),
|
||
approveJournal: (id: number, name: string) =>
|
||
req<JournalT>(`/sessions/${id}/journal/approve`, { method: "POST", body: JSON.stringify({ name }) }),
|
||
resetJournalSignoff: (id: number) =>
|
||
req<JournalT>(`/sessions/${id}/journal/reset-signoff`, { method: "POST" }),
|
||
accountsSummary: () => req<AccountsSummaryT>("/accounts-summary"),
|
||
setJournalEntryNo: (id: number, entry_no: string) =>
|
||
req(`/sessions/${id}/journal/entry-no`, { method: "PUT", body: JSON.stringify({ entry_no }) }),
|
||
openings: (id: number) => req<OpeningBalanceT[]>(`/sessions/${id}/opening-balances`),
|
||
openingWorksheet: (id: number) =>
|
||
req<OpeningWorksheetT>(`/sessions/${id}/opening-balances/worksheet`),
|
||
putOpenings: (id: number, items: OpeningBalanceT[]) =>
|
||
req<OpeningBalanceT[]>(`/sessions/${id}/opening-balances`, { method: "PUT", body: JSON.stringify(items) }),
|
||
openingCandidates: (id: number) =>
|
||
req<OpeningCandidatesT>(`/sessions/${id}/opening-candidates`),
|
||
carryForwardOpenings: (id: number, from_session_id?: number) =>
|
||
req<{ applied: number; from: string; balances: OpeningBalanceT[] }>(
|
||
`/sessions/${id}/opening-balances/carry-forward`,
|
||
{ method: "POST", body: JSON.stringify({ from_session_id: from_session_id ?? null }) }),
|
||
resetOpenings: (id: number) =>
|
||
req<{ balances: OpeningBalanceT[] }>(`/sessions/${id}/opening-balances/reset`, { method: "POST" }),
|
||
arMovement: (id: number, marketplace?: string) =>
|
||
req<ArMovementT>(`/sessions/${id}/ar-movement${marketplace ? `?marketplace=${encodeURIComponent(marketplace)}` : ""}`),
|
||
ledgerDetail: (id: number, marketplace?: string, granularity = "day", from?: string, to?: string) =>
|
||
req<LedgerDetailT>(`/sessions/${id}/ledger-detail?${q({ marketplace, granularity, date_from: from, date_to: to })}`),
|
||
fxDaily: (id: number, marketplace?: string, from?: string, to?: string) =>
|
||
req<FxDailyT>(`/sessions/${id}/fx-daily?${q({ marketplace, date_from: from, date_to: to })}`),
|
||
putFxDaily: (id: number, items: { marketplace: string; rate_date: string; rate: number; source?: string }[]) =>
|
||
req<FxDailyT>(`/sessions/${id}/fx-daily`, { method: "PUT", body: JSON.stringify(items) }),
|
||
reconDetail: (id: number, marketplace?: string) =>
|
||
req<ReconDetailT>(`/sessions/${id}/reconciliation-detail?${q({ marketplace })}`),
|
||
allMarkets: (id: number) => req<AllMarketsT>(`/sessions/${id}/all-markets`),
|
||
control: (id: number) => req<ControlT>(`/sessions/${id}/reconciliation-control`),
|
||
putControl: (id: number, body: Record<string, number | string>) =>
|
||
req<ControlT>(`/sessions/${id}/reconciliation-control`, { method: "PUT", body: JSON.stringify(body) }),
|
||
verifyControl: (id: number, verified_by: string, comment: string) =>
|
||
req<ControlT>(`/sessions/${id}/reconciliation-control/verify`, {
|
||
method: "POST", body: JSON.stringify({ verified_by, comment }),
|
||
}),
|
||
completeSession: (id: number) => req<{ status: string }>(`/sessions/${id}/complete`, { method: "POST" }),
|
||
|
||
/** Formula + source for every dashboard figure — content of the (i) info buttons. */
|
||
definitions: () => req<Record<string, DefinitionT>>("/definitions"),
|
||
|
||
payouts: (id: number, marketplace?: string) =>
|
||
req<PayoutsT>(`/sessions/${id}/payouts${marketplace ? `?marketplace=${encodeURIComponent(marketplace)}` : ""}`),
|
||
putPayoutReceipts: (id: number, items: {
|
||
marketplace: string; account_type: string; settlement_id: string;
|
||
bank_date: string | null; bank_amount?: number | null; note?: string; entered_by?: string;
|
||
}[]) => req<{ saved: number; removed: number; needs_reprocess: boolean }>(
|
||
`/sessions/${id}/payouts/receipts`, { method: "PUT", body: JSON.stringify(items) }),
|
||
putPayoutMode: (id: number, mode: "auto" | "manual") =>
|
||
req<{ payout_mode: string; needs_reprocess: boolean }>(
|
||
`/sessions/${id}/payouts/mode`, { method: "PUT", body: JSON.stringify({ mode }) }),
|
||
|
||
controls: (id: number) => req<ControlsT>(`/sessions/${id}/controls`),
|
||
runControls: (id: number) => req<ControlsT>(`/sessions/${id}/controls/run`, { method: "POST" }),
|
||
confirmAllFx: (id: number, confirmed_by: string) =>
|
||
req<ControlsT>(`/sessions/${id}/fx/confirm-all`, {
|
||
method: "POST", body: JSON.stringify({ confirmed_by }),
|
||
}),
|
||
|
||
getReserves: (id: number) =>
|
||
req<{ marketplace: string; account_type: string; amount: number }[]>(`/sessions/${id}/reserves`),
|
||
putReserves: (id: number, items: { marketplace: string; account_type: string; amount: number }[]) =>
|
||
req(`/sessions/${id}/reserves`, { method: "PUT", body: JSON.stringify(items) }),
|
||
getFx: (id: number) =>
|
||
req<{ marketplace: string; currency: string; rate: number;
|
||
source: string; rate_date: string | null }[]>(`/sessions/${id}/fx`),
|
||
putFx: (id: number, items: { marketplace: string; currency: string; rate: number }[]) =>
|
||
req(`/sessions/${id}/fx`, { method: "PUT", body: JSON.stringify(items) }),
|
||
fetchFx: (id: number) =>
|
||
req<{ updated: { marketplace: string; currency: string; rate: number }[];
|
||
missing: string[]; source: string; rate_date: string }>(
|
||
`/sessions/${id}/fx/fetch`, { method: "POST" }),
|
||
fetchFxDaily: (id: number, body: { marketplace?: string; date_from?: string; date_to?: string } = {}) =>
|
||
req<{ saved: number; date_from: string; date_to: string; provider: string;
|
||
marketplaces: string[] }>(
|
||
`/sessions/${id}/fx/fetch-daily`, { method: "POST", body: JSON.stringify(body) }),
|
||
|
||
startExport: (id: number, kind: "full" | "summary" = "full") =>
|
||
req<{ started: boolean; kind: string }>(`/sessions/${id}/export?kind=${kind}`, { method: "POST" }),
|
||
financeSummary: (id: number, marketplace?: string) =>
|
||
req<FinanceSummaryT>(`/sessions/${id}/finance-summary${marketplace ? `?marketplace=${encodeURIComponent(marketplace)}` : ""}`),
|
||
listExports: (id: number) =>
|
||
req<{ id: number; kind?: string; size_bytes: number; generated_at: string; available: boolean }[]>(
|
||
`/sessions/${id}/exports`
|
||
),
|
||
downloadUrl: (id: number, kind: "full" | "summary" = "full") =>
|
||
`${BASE}/sessions/${id}/export/download?kind=${kind}`,
|
||
};
|