139 lines
3.6 KiB
TypeScript
139 lines
3.6 KiB
TypeScript
import {
|
|
ApiError,
|
|
apiFetch,
|
|
apiPath,
|
|
type ApiSettings
|
|
} from "@govoplan/core-webui";
|
|
|
|
export type PaymentStatus = "requested" | "paid";
|
|
|
|
export type EvidenceReference = {
|
|
kind: string;
|
|
owner_module: string;
|
|
evidence_id: string;
|
|
tenant_id: string;
|
|
version?: string | null;
|
|
checksum?: string | null;
|
|
};
|
|
|
|
export type PaymentReconciliation = {
|
|
reconciliation_id: string;
|
|
mode: "manual";
|
|
amount_minor: number;
|
|
currency: string;
|
|
transaction_reference: string;
|
|
evidence_ref: EvidenceReference;
|
|
received_at: string;
|
|
recorded_at: string;
|
|
recorded_by_ref: string;
|
|
};
|
|
|
|
export type PaymentEvent = {
|
|
event_id: string;
|
|
event_type: string;
|
|
status: PaymentStatus;
|
|
occurred_at: string;
|
|
actor_ref: string;
|
|
payload: Record<string, unknown>;
|
|
};
|
|
|
|
export type PaymentRequest = {
|
|
payment_id: string;
|
|
tenant_id: string;
|
|
payment_reference: string;
|
|
source: {
|
|
module: string;
|
|
resource_type: string;
|
|
resource_id: string;
|
|
};
|
|
amount_minor: number;
|
|
currency: string;
|
|
subject: string;
|
|
status: PaymentStatus;
|
|
requested_at: string;
|
|
requested_by_ref: string;
|
|
due_at?: string | null;
|
|
settled_at?: string | null;
|
|
context_refs: Record<string, string>;
|
|
metadata: Record<string, unknown>;
|
|
reconciliation?: PaymentReconciliation | null;
|
|
events: PaymentEvent[];
|
|
replayed: boolean;
|
|
};
|
|
|
|
export type PaymentRequestCreate = {
|
|
source_module: string;
|
|
source_resource_type: string;
|
|
source_resource_id: string;
|
|
amount_minor: number;
|
|
currency: string;
|
|
subject: string;
|
|
idempotency_key: string;
|
|
due_at?: string | null;
|
|
context_refs: Record<string, string>;
|
|
metadata: Record<string, unknown>;
|
|
};
|
|
|
|
export type ManualPaymentReconciliationCreate = {
|
|
amount_minor: number;
|
|
currency: string;
|
|
transaction_reference: string;
|
|
evidence_ref: EvidenceReference;
|
|
idempotency_key: string;
|
|
received_at: string;
|
|
metadata: Record<string, unknown>;
|
|
};
|
|
|
|
export async function listPaymentRequests(
|
|
settings: ApiSettings,
|
|
filters: { status?: PaymentStatus; sourceResourceId?: string; limit?: number } = {},
|
|
signal?: AbortSignal
|
|
): Promise<PaymentRequest[]> {
|
|
const response = await apiFetch<{ payments: PaymentRequest[] }>(
|
|
settings,
|
|
apiPath("/api/v1/payments/requests", {
|
|
status: filters.status,
|
|
source_resource_id: filters.sourceResourceId,
|
|
limit: filters.limit ?? 200
|
|
}),
|
|
{ signal }
|
|
);
|
|
return response.payments;
|
|
}
|
|
|
|
export function createPaymentRequest(
|
|
settings: ApiSettings,
|
|
payload: PaymentRequestCreate
|
|
): Promise<PaymentRequest> {
|
|
return apiFetch<PaymentRequest>(settings, "/api/v1/payments/requests", {
|
|
method: "POST",
|
|
body: JSON.stringify(payload)
|
|
});
|
|
}
|
|
|
|
export function reconcileManualPayment(
|
|
settings: ApiSettings,
|
|
paymentId: string,
|
|
payload: ManualPaymentReconciliationCreate
|
|
): Promise<PaymentRequest> {
|
|
return apiFetch<PaymentRequest>(
|
|
settings,
|
|
`/api/v1/payments/requests/${encodeURIComponent(paymentId)}/manual-reconciliations`,
|
|
{ method: "POST", body: JSON.stringify(payload) }
|
|
);
|
|
}
|
|
|
|
export function paymentApiErrorMessage(reason: unknown): string {
|
|
if (reason instanceof ApiError) {
|
|
try {
|
|
const payload = JSON.parse(reason.body) as { detail?: unknown };
|
|
if (typeof payload.detail === "string") return payload.detail;
|
|
} catch {
|
|
// The response body may be plain text.
|
|
}
|
|
if (reason.status === 409) return "The payment changed or this replay key is already bound to different evidence. Reload and review the current state.";
|
|
if (reason.status === 403) return "Your current role does not permit this payment action.";
|
|
}
|
|
return reason instanceof Error ? reason.message : String(reason);
|
|
}
|