Files
govoplan-payments/webui/src/api/payments.ts
T
zemion a2dcd8f2dd
Module Package Release / publish-packages (push) Successful in 14s
Add guided Payments operator workspace
2026-08-19 13:17:13 +02:00

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);
}