feat(payments): configurable checkout payments — wallet + Stripe crypto, original-method refunds (add-configurable-payments)
- payment domain: payments/memberships/attempts/receipts/refunds/commands/events, legacy-demo provenance (0024), checkout header-hash idempotency (0025) - wallet collection all-or-none across shop orders; group-wide cancellation; checkout cutover, status-only payOrder removed - Stripe crypto-only Checkout adapter, raw-body HMAC webhooks, durable command worker with leases - original-method refunds with receipt/allocation budgets; refund_pending/refund_failed aftersale states - admin channel CRUD/revisions/audit/verify + operations list/detail/reconcile; secret-ref allowlist (VMALL_PAYMENT_*), env-fixed demo/sandbox/live - mall/mobile checkout pay surfaces, admin console pages; shared contract updated - pre-launch leftovers recorded in docs/TBD-payments.md cargo test -p vmall-api green twice; four frontend builds green; browser smoke: mall wallet checkout paid, admin channels/operations verified
This commit is contained in:
+112
-18
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
AccountSummary,
|
||||
AdminPaymentDetail,
|
||||
Aftersale,
|
||||
AftersaleApplyBody,
|
||||
AftersaleArbitration,
|
||||
@@ -15,12 +16,15 @@ import type {
|
||||
BrandInput,
|
||||
Cart,
|
||||
Category,
|
||||
CheckoutResult,
|
||||
ContentInputByKind,
|
||||
ContentKind,
|
||||
Coupon,
|
||||
CouponTemplate,
|
||||
CouponTemplateInput,
|
||||
CreatePaymentAttemptBody,
|
||||
Currency,
|
||||
EligibleMethod,
|
||||
Favorite,
|
||||
FavoriteKind,
|
||||
FlashSaleItem,
|
||||
@@ -54,6 +58,20 @@ import type {
|
||||
Order,
|
||||
OrderStatus,
|
||||
Paged,
|
||||
Payment,
|
||||
PaymentAdapterDescriptor,
|
||||
PaymentAttemptResult,
|
||||
PaymentChannel,
|
||||
PaymentChannelAuditEntry,
|
||||
PaymentChannelCreateBody,
|
||||
PaymentChannelDetail,
|
||||
PaymentChannelQuery,
|
||||
PaymentChannelRevision,
|
||||
PaymentChannelUpdateBody,
|
||||
PaymentChannelVerification,
|
||||
PaymentOperation,
|
||||
PaymentOperationQuery,
|
||||
PaymentReconcileCommand,
|
||||
Product,
|
||||
ProductStatus,
|
||||
PublicFlashSaleSession,
|
||||
@@ -187,6 +205,7 @@ async function request<T>(
|
||||
path: string,
|
||||
body?: unknown,
|
||||
query?: Query,
|
||||
idempotencyKey?: string,
|
||||
): Promise<T> {
|
||||
const url = new URL(opts.baseUrl + path);
|
||||
if (query) {
|
||||
@@ -197,6 +216,7 @@ async function request<T>(
|
||||
const headers: Record<string, string> = { "content-type": "application/json" };
|
||||
const token = opts.getToken?.();
|
||||
if (token) headers.authorization = `Bearer ${token}`;
|
||||
if (idempotencyKey !== undefined) headers["idempotency-key"] = idempotencyKey;
|
||||
const res = await fetch(url.toString(), {
|
||||
method,
|
||||
headers,
|
||||
@@ -234,18 +254,38 @@ export interface ApiClient {
|
||||
addCartItem(skuId: string, qty: number): Promise<Cart>;
|
||||
updateCartItem(skuId: string, qty: number): Promise<Cart>;
|
||||
removeCartItem(skuId: string): Promise<Cart>;
|
||||
/**
|
||||
* Creates the checkout's shop orders and their shared payment in one
|
||||
* transaction. `idempotencyKey` is required: replaying the same key/payload
|
||||
* returns the original result, a changed payload is a 409, and a missing key
|
||||
* is a 400.
|
||||
*/
|
||||
checkout(
|
||||
shippingAddress: Address,
|
||||
currency: string,
|
||||
/** At most one owned coupon per generated shop order. */
|
||||
couponByShop?: Record<string, string>,
|
||||
/** Optional group-buying intent for one activity SKU at quantity 1. */
|
||||
groupBuy?: GroupBuyIntent,
|
||||
): Promise<Order[]>;
|
||||
couponByShop: Record<string, string> | undefined,
|
||||
groupBuy: GroupBuyIntent | undefined,
|
||||
idempotencyKey: string,
|
||||
): Promise<CheckoutResult>;
|
||||
listMyOrders(page?: number): Promise<Paged<Order>>;
|
||||
getOrder(id: string): Promise<Order>;
|
||||
cancelOrder(id: string): Promise<Order>;
|
||||
payOrder(id: string): Promise<Order>;
|
||||
/** Resolves the existing payment of owned pending orders, or creates a singleton. */
|
||||
resolvePayment(orderIds: string[]): Promise<Payment>;
|
||||
getPayment(id: string): Promise<Payment>;
|
||||
/** Eligible enabled channels for this payment; never secrets or provider config. */
|
||||
listPaymentMethods(id: string): Promise<EligibleMethod[]>;
|
||||
/**
|
||||
* Starts one collection attempt; `idempotencyKey` is required and the same
|
||||
* key/payload replays the same operation while a changed payload is a 409.
|
||||
*/
|
||||
createPaymentAttempt(
|
||||
id: string,
|
||||
body: CreatePaymentAttemptBody,
|
||||
idempotencyKey: string,
|
||||
): Promise<PaymentAttemptResult>;
|
||||
/** Cancels the whole unpaid payment group; 409 with authoritative state otherwise. */
|
||||
cancelPayment(id: string): Promise<Payment>;
|
||||
confirmDelivered(shipmentId: string): Promise<Shipment>;
|
||||
listMyShipments(): Promise<Shipment[]>;
|
||||
requestInvoice(
|
||||
@@ -372,7 +412,8 @@ export interface ApiClient {
|
||||
approveAftersale(id: string): Promise<Aftersale>;
|
||||
rejectAftersale(id: string): Promise<Aftersale>;
|
||||
confirmAftersaleReceipt(id: string): Promise<Aftersale>;
|
||||
refundAftersale(id: string): Promise<Aftersale>;
|
||||
/** Submits (or retries after definitive failure) the original-method refund. */
|
||||
refundAftersale(id: string): Promise<AftersaleDetail>;
|
||||
addAftersaleMessage(id: string, body: AftersaleMessageBody): Promise<AftersaleMessage>;
|
||||
listFreightTemplates(): Promise<FreightTemplate[]>;
|
||||
createFreightTemplate(body: FreightTemplateInput): Promise<FreightTemplate>;
|
||||
@@ -405,7 +446,8 @@ export interface ApiClient {
|
||||
): Promise<HomeContent>;
|
||||
listAftersales(status?: AftersaleStatus): Promise<Aftersale[]>;
|
||||
getAftersale(id: string): Promise<AftersaleDetail>;
|
||||
arbitrateAftersale(id: string, outcome: AftersaleArbitration): Promise<Aftersale>;
|
||||
/** Resolves an escalated application via the shared refund path. */
|
||||
arbitrateAftersale(id: string, outcome: AftersaleArbitration): Promise<AftersaleDetail>;
|
||||
listReviews(page?: number): Promise<Paged<Review>>;
|
||||
hideReview(id: string): Promise<Review>;
|
||||
deleteReview(id: string): Promise<void>;
|
||||
@@ -439,12 +481,40 @@ export interface ApiClient {
|
||||
updateMemberLevel(id: string, body: MemberLevelInput): Promise<MemberLevel>;
|
||||
/** Rejected with a 409 when any customer currently holds the level. */
|
||||
deleteMemberLevel(id: string): Promise<void>;
|
||||
/** Adapter catalog with capabilities and external activation prerequisites. */
|
||||
listPaymentAdapters(): Promise<PaymentAdapterDescriptor[]>;
|
||||
listPaymentChannels(q?: PaymentChannelQuery): Promise<Paged<PaymentChannel>>;
|
||||
getPaymentChannel(id: string): Promise<PaymentChannelDetail>;
|
||||
/** Returns the detail superset, including secret references and readiness. */
|
||||
createPaymentChannel(body: PaymentChannelCreateBody): Promise<PaymentChannelDetail>;
|
||||
/** 409 when `expected_revision` no longer matches the channel revision. */
|
||||
updatePaymentChannel(
|
||||
id: string,
|
||||
body: PaymentChannelUpdateBody,
|
||||
): Promise<PaymentChannelDetail>;
|
||||
/** Archival when the channel is referenced; never destructive removal. */
|
||||
deletePaymentChannel(id: string): Promise<void>;
|
||||
listPaymentChannelRevisions(id: string): Promise<PaymentChannelRevision[]>;
|
||||
listPaymentChannelAudit(id: string): Promise<PaymentChannelAuditEntry[]>;
|
||||
/** Read-only provider connection/account check; implies no approval. */
|
||||
verifyPaymentChannel(id: string): Promise<PaymentChannelVerification>;
|
||||
/** Paginated payment/refund/exception operations. */
|
||||
listPaymentOperations(q?: PaymentOperationQuery): Promise<Paged<PaymentOperation>>;
|
||||
/** Recovery detail: authoritative payment plus redacted channel audit history. */
|
||||
getPayment(id: string): Promise<AdminPaymentDetail>;
|
||||
/** Durable evidence-fetch command; a repeat never initiates collection. */
|
||||
reconcilePayment(id: string): Promise<PaymentReconcileCommand>;
|
||||
};
|
||||
}
|
||||
|
||||
export function createApi(opts: ApiClientOptions): ApiClient {
|
||||
const r = <T>(m: string, p: string, b?: unknown, q?: Query): Promise<T> =>
|
||||
request<T>(opts, m, p, b, q);
|
||||
const r = <T>(
|
||||
m: string,
|
||||
p: string,
|
||||
b?: unknown,
|
||||
q?: Query,
|
||||
idempotencyKey?: string,
|
||||
): Promise<T> => request<T>(opts, m, p, b, q, idempotencyKey);
|
||||
return {
|
||||
register: (email, password, displayName) =>
|
||||
r("POST", "/auth/register", { email, password, display_name: displayName }),
|
||||
@@ -462,17 +532,28 @@ export function createApi(opts: ApiClientOptions): ApiClient {
|
||||
addCartItem: (skuId, qty) => r("POST", "/cart/items", { sku_id: skuId, qty }),
|
||||
updateCartItem: (skuId, qty) => r("PUT", `/cart/items/${skuId}`, { qty }),
|
||||
removeCartItem: (skuId) => r("DELETE", `/cart/items/${skuId}`),
|
||||
checkout: (shippingAddress, currency, couponByShop = {}, groupBuy) =>
|
||||
r("POST", "/orders/checkout", {
|
||||
shipping_address: shippingAddress,
|
||||
currency,
|
||||
coupon_by_shop: couponByShop,
|
||||
group_buy: groupBuy ?? null,
|
||||
}),
|
||||
checkout: (shippingAddress, currency, couponByShop, groupBuy, idempotencyKey) =>
|
||||
r(
|
||||
"POST",
|
||||
"/orders/checkout",
|
||||
{
|
||||
shipping_address: shippingAddress,
|
||||
currency,
|
||||
coupon_by_shop: couponByShop ?? {},
|
||||
group_buy: groupBuy ?? null,
|
||||
},
|
||||
undefined,
|
||||
idempotencyKey,
|
||||
),
|
||||
listMyOrders: (page = 1) => r("GET", "/orders", undefined, { page }),
|
||||
getOrder: (id) => r("GET", `/orders/${id}`),
|
||||
cancelOrder: (id) => r("POST", `/orders/${id}/cancel`),
|
||||
payOrder: (id) => r("POST", `/orders/${id}/pay`),
|
||||
resolvePayment: (orderIds) => r("POST", "/payments", { order_ids: orderIds }),
|
||||
getPayment: (id) => r("GET", `/payments/${id}`),
|
||||
listPaymentMethods: (id) => r("GET", `/payments/${id}/methods`),
|
||||
createPaymentAttempt: (id, body, idempotencyKey) =>
|
||||
r("POST", `/payments/${id}/attempts`, body, undefined, idempotencyKey),
|
||||
cancelPayment: (id) => r("POST", `/payments/${id}/cancel`),
|
||||
confirmDelivered: (shipmentId) => r("POST", `/shipments/${shipmentId}/confirm-delivered`),
|
||||
listMyShipments: () => r("GET", "/shipments"),
|
||||
quoteShipping: (address, currency) =>
|
||||
@@ -654,6 +735,19 @@ export function createApi(opts: ApiClientOptions): ApiClient {
|
||||
createMemberLevel: (body) => r("POST", "/admin/member-levels", body),
|
||||
updateMemberLevel: (id, body) => r("PUT", `/admin/member-levels/${id}`, body),
|
||||
deleteMemberLevel: (id) => r("DELETE", `/admin/member-levels/${id}`),
|
||||
listPaymentAdapters: () => r("GET", "/admin/payments/channels/adapters"),
|
||||
listPaymentChannels: (q = {}) =>
|
||||
r("GET", "/admin/payments/channels", undefined, { ...q }),
|
||||
getPaymentChannel: (id) => r("GET", `/admin/payments/channels/${id}`),
|
||||
createPaymentChannel: (body) => r("POST", "/admin/payments/channels", body),
|
||||
updatePaymentChannel: (id, body) => r("PUT", `/admin/payments/channels/${id}`, body),
|
||||
deletePaymentChannel: (id) => r("DELETE", `/admin/payments/channels/${id}`),
|
||||
listPaymentChannelRevisions: (id) => r("GET", `/admin/payments/channels/${id}/revisions`),
|
||||
listPaymentChannelAudit: (id) => r("GET", `/admin/payments/channels/${id}/audit`),
|
||||
verifyPaymentChannel: (id) => r("POST", `/admin/payments/channels/${id}/verify`),
|
||||
listPaymentOperations: (q = {}) => r("GET", "/admin/payments", undefined, { ...q }),
|
||||
getPayment: (id) => r("GET", `/admin/payments/${id}`),
|
||||
reconcilePayment: (id) => r("POST", `/admin/payments/${id}/reconcile`),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -637,7 +637,11 @@ export type AftersaleStatus =
|
||||
| "buyer_shipping"
|
||||
| "merchant_confirmed"
|
||||
| "refunded"
|
||||
| "cancelled";
|
||||
| "cancelled"
|
||||
/** Refund submitted and awaiting verified provider success. */
|
||||
| "refund_pending"
|
||||
/** Refund definitively failed; one guarded retry is permitted. */
|
||||
| "refund_failed";
|
||||
|
||||
export interface Aftersale {
|
||||
id: string;
|
||||
@@ -685,6 +689,8 @@ export interface AftersaleDetail extends Aftersale {
|
||||
item: AftersaleItemSummary;
|
||||
messages: AftersaleMessage[];
|
||||
remaining_refundable_minor: number;
|
||||
/** The current/last refund operation, or null before any refund submission. */
|
||||
refund_operation: RefundOperation | null;
|
||||
}
|
||||
|
||||
export interface AftersaleApplyBody {
|
||||
@@ -707,6 +713,385 @@ export interface AftersaleReturnTrackingBody {
|
||||
|
||||
export type AftersaleArbitration = "refund" | "reject";
|
||||
|
||||
// ---- payments ----
|
||||
|
||||
/** Deployment payment environment; immutable per deployment, never toggled. */
|
||||
export type PaymentEnvironment = "demo" | "sandbox" | "live";
|
||||
|
||||
/** The two shipped collection adapters; no other adapter exists. */
|
||||
export type PaymentAdapter = "wallet" | "stripe_crypto";
|
||||
|
||||
/** Local application of a collected payment onto its member orders. */
|
||||
export type PaymentStatus = "unapplied" | "applied" | "closed";
|
||||
|
||||
export type PaymentAttemptStatus =
|
||||
| "created"
|
||||
| "pending"
|
||||
| "unknown"
|
||||
| "settled"
|
||||
| "failed"
|
||||
| "expired";
|
||||
|
||||
export type PaymentRefundStatus = "pending" | "unknown" | "succeeded" | "failed";
|
||||
|
||||
export type RefundPurpose = "aftersale" | "compensation";
|
||||
|
||||
/** Which storefront an attempt returns to; mapped server-side to an allowlisted origin. */
|
||||
export type ReturnSurface = "mall" | "mobile";
|
||||
|
||||
/**
|
||||
* Safe next action for the caller's surface. `url` is present only for
|
||||
* `redirect` and is always built from deployment-configured origins.
|
||||
*/
|
||||
export interface NextAction {
|
||||
type: "none" | "redirect" | "wait";
|
||||
url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkout result. Replaying the same owner-scoped `Idempotency-Key` with an
|
||||
* unchanged payload returns this original result; a changed payload is a 409.
|
||||
*/
|
||||
export interface CheckoutResult {
|
||||
orders: Order[];
|
||||
payment_id: string;
|
||||
}
|
||||
|
||||
/** Compact payment echo used by attempt responses. */
|
||||
export interface PaymentSummary {
|
||||
id: string;
|
||||
status: PaymentStatus;
|
||||
currency: string;
|
||||
total_minor: number;
|
||||
}
|
||||
|
||||
/** One checkout order's immutable membership in a payment. */
|
||||
export interface PaymentOrder extends Order {
|
||||
/** The order's snapshotted total at checkout; sums to the payment total. */
|
||||
amount_minor: number;
|
||||
}
|
||||
|
||||
/** One collection attempt against a channel. */
|
||||
export interface PaymentAttempt {
|
||||
id: string;
|
||||
channel_id: string;
|
||||
adapter: PaymentAdapter;
|
||||
status: PaymentAttemptStatus;
|
||||
next_action: NextAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response to `createPaymentAttempt`: the attempt identity/status, the payment
|
||||
* summary and the safe next action (wallet completion or pending/redirect).
|
||||
*/
|
||||
export interface PaymentAttemptResult {
|
||||
id: string;
|
||||
status: PaymentAttemptStatus;
|
||||
payment: PaymentSummary;
|
||||
next_action: NextAction;
|
||||
}
|
||||
|
||||
/** Request body for `createPaymentAttempt`; `return_surface` is persisted on the attempt. */
|
||||
export interface CreatePaymentAttemptBody {
|
||||
channel_id: string;
|
||||
return_surface: ReturnSurface;
|
||||
}
|
||||
|
||||
/** One eligible enabled channel and its safe descriptor; never secrets. */
|
||||
export interface EligibleMethod {
|
||||
/** Channel id accepted by `createPaymentAttempt`. */
|
||||
id: string;
|
||||
adapter: PaymentAdapter;
|
||||
label: LocalizedText;
|
||||
currencies: string[];
|
||||
min_amount_minor?: number;
|
||||
max_amount_minor?: number;
|
||||
}
|
||||
|
||||
/** One original-method refund operation; a retry gets a new operation identity. */
|
||||
export interface RefundOperation {
|
||||
id: string;
|
||||
purpose: RefundPurpose;
|
||||
status: PaymentRefundStatus;
|
||||
amount_minor: number;
|
||||
currency: string;
|
||||
aftersale_id?: string;
|
||||
/** Adapter that collected the funds; the refund returns by this original method. */
|
||||
method: PaymentAdapter;
|
||||
/** Provider operation reference when one exists. */
|
||||
reference: string | null;
|
||||
/** Bounded failure detail; null unless the operation failed or is unknown. */
|
||||
error: string | null;
|
||||
/** True only on definitive failure when one explicit guarded retry is permitted. */
|
||||
retry_allowed: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export type PaymentExceptionKind =
|
||||
| "unapplied_receipt"
|
||||
| "extra_collection"
|
||||
| "amount_mismatch"
|
||||
| "unknown_operation";
|
||||
|
||||
export type PaymentExceptionStatus = "open" | "resolved";
|
||||
|
||||
/** A collection/application anomaly awaiting operator resolution. */
|
||||
export interface PaymentException {
|
||||
id: string;
|
||||
kind: PaymentExceptionKind;
|
||||
detail?: LocalizedText | string;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider-exposed crypto asset details. `base_units` is an integer base-unit
|
||||
* quantity as a decimal string: asset quantities never use TS number arithmetic
|
||||
* or `formatMoney`, and absent chain details stay absent.
|
||||
*/
|
||||
export interface CryptoAssetMetadata {
|
||||
network: string | null;
|
||||
/** Contract address, or null for the network's native asset. */
|
||||
contract_address: string | null;
|
||||
base_units: string;
|
||||
symbol: string | null;
|
||||
quote_reference: string | null;
|
||||
quote_expires_at: string | null;
|
||||
transaction_reference: string | null;
|
||||
}
|
||||
|
||||
/** Immutable evidence of one actual collection. */
|
||||
export interface PaymentReceipt {
|
||||
id: string;
|
||||
payment_id: string;
|
||||
attempt_id: string | null;
|
||||
adapter: PaymentAdapter;
|
||||
environment: PaymentEnvironment;
|
||||
amount_minor: number;
|
||||
currency: string;
|
||||
/** Provider transaction identity within provider account/environment. */
|
||||
reference: string | null;
|
||||
crypto: CryptoAssetMetadata | null;
|
||||
collected_at: string;
|
||||
}
|
||||
|
||||
/** One server-owned payment unit for a checkout's shop orders. */
|
||||
export interface Payment extends PaymentSummary {
|
||||
environment: PaymentEnvironment;
|
||||
orders: PaymentOrder[];
|
||||
attempts: PaymentAttempt[];
|
||||
receipts: PaymentReceipt[];
|
||||
refunds: RefundOperation[];
|
||||
exceptions: PaymentException[];
|
||||
next_action: NextAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin recovery view of a payment: the authoritative detail plus the redacted
|
||||
* configuration audit of every channel its attempts referenced (admin route
|
||||
* only; buyers never receive channel configuration history).
|
||||
*/
|
||||
export interface AdminPaymentDetail extends Payment {
|
||||
audit: PaymentChannelAuditEntry[];
|
||||
}
|
||||
|
||||
// ---- payment channels (admin) ----
|
||||
|
||||
/** Adapter-level constraints the admin channel editor enforces. */
|
||||
export interface PaymentAdapterCapabilities {
|
||||
currencies: string[];
|
||||
min_amount_minor: number;
|
||||
max_amount_minor: number | null;
|
||||
/** Provider-managed choices surfaced informationally; never saved as restrictions. */
|
||||
provider_managed: string[];
|
||||
}
|
||||
|
||||
/** One shipped adapter in this deployment; probing it never implies approval. */
|
||||
export interface PaymentAdapterDescriptor {
|
||||
kind: PaymentAdapter;
|
||||
label: LocalizedText;
|
||||
environment: PaymentEnvironment;
|
||||
capabilities: PaymentAdapterCapabilities;
|
||||
/** Secret reference slot names a channel revision may configure. */
|
||||
secret_slots: string[];
|
||||
/** External activation prerequisites; a successful probe never substitutes for them. */
|
||||
activation_prerequisites: string[];
|
||||
}
|
||||
|
||||
/** One configured collection channel; credentials live only in its revisions. */
|
||||
export interface PaymentChannel {
|
||||
id: string;
|
||||
adapter: PaymentAdapter;
|
||||
label: LocalizedText;
|
||||
display_order: number;
|
||||
enabled: boolean;
|
||||
archived: boolean;
|
||||
supported_currencies: string[];
|
||||
min_amount_minor: number;
|
||||
max_amount_minor: number | null;
|
||||
provider_account_id: string | null;
|
||||
environment: PaymentEnvironment;
|
||||
/** Current immutable revision; updates must precondition on it. */
|
||||
revision: number;
|
||||
}
|
||||
|
||||
/** A channel secret slot and whether its reference is configured; never a value. */
|
||||
export interface PaymentSecretSlot {
|
||||
slot: string;
|
||||
configured: boolean;
|
||||
reference: string | null;
|
||||
}
|
||||
|
||||
/** Channel detail: the row plus its secret references and readiness context. */
|
||||
export interface PaymentChannelDetail extends PaymentChannel {
|
||||
secret_key_ref: string | null;
|
||||
webhook_secret_ref: string | null;
|
||||
secret_slots: PaymentSecretSlot[];
|
||||
unmet_prerequisites: string[];
|
||||
/** Adapter capabilities echoed for the editor; identical to the catalog entry. */
|
||||
capabilities: PaymentAdapterCapabilities;
|
||||
}
|
||||
|
||||
export interface PaymentChannelCreateBody {
|
||||
adapter: PaymentAdapter;
|
||||
label: LocalizedText;
|
||||
display_order: number;
|
||||
enabled: boolean;
|
||||
supported_currencies: string[];
|
||||
min_amount_minor: number;
|
||||
max_amount_minor: number | null;
|
||||
provider_account_id: string | null;
|
||||
secret_key_ref: string | null;
|
||||
webhook_secret_ref: string | null;
|
||||
}
|
||||
|
||||
export interface PaymentChannelUpdateBody extends PaymentChannelCreateBody {
|
||||
/** Optimistic precondition: the channel's current `revision`. A mismatch is 409. */
|
||||
expected_revision: number;
|
||||
}
|
||||
|
||||
/** Immutable channel configuration snapshot; secret references, never values. */
|
||||
export interface PaymentChannelRevision {
|
||||
id: string;
|
||||
channel_id: string;
|
||||
revision: number;
|
||||
adapter: PaymentAdapter;
|
||||
/** Adapter contract version recorded at revision creation. */
|
||||
adapter_contract_version: string;
|
||||
label: LocalizedText;
|
||||
display_order: number;
|
||||
enabled: boolean;
|
||||
supported_currencies: string[];
|
||||
min_amount_minor: number;
|
||||
max_amount_minor: number | null;
|
||||
provider_account_id: string | null;
|
||||
secret_key_ref: string | null;
|
||||
webhook_secret_ref: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export type PaymentChannelAuditAction =
|
||||
| "created"
|
||||
| "updated"
|
||||
| "enabled"
|
||||
| "disabled"
|
||||
| "verified"
|
||||
| "archived";
|
||||
|
||||
/** One redacted audit change; secret values are never recorded. */
|
||||
export interface PaymentChannelAuditChange {
|
||||
from: unknown;
|
||||
to: unknown;
|
||||
}
|
||||
|
||||
/** Append-only configuration audit entry. */
|
||||
export interface PaymentChannelAuditEntry {
|
||||
id: string;
|
||||
channel_id: string;
|
||||
action: PaymentChannelAuditAction;
|
||||
revision: number;
|
||||
actor: string;
|
||||
changes: Record<string, PaymentChannelAuditChange>;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/** Read-only provider connection/account check; success implies no approval. */
|
||||
export interface PaymentChannelVerification {
|
||||
ok: boolean;
|
||||
checked_at: string;
|
||||
provider_account: string | null;
|
||||
unmet_prerequisites: string[];
|
||||
detail: string | null;
|
||||
}
|
||||
|
||||
export interface PaymentChannelQuery {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
}
|
||||
|
||||
// ---- payment operations (admin) ----
|
||||
|
||||
export type PaymentOperationKind = "payment" | "refund" | "exception";
|
||||
|
||||
/** One admin operations-list row; discriminated by `kind`. */
|
||||
export type PaymentOperation =
|
||||
| PaymentOperationPayment
|
||||
| PaymentOperationRefund
|
||||
| PaymentOperationException;
|
||||
|
||||
interface PaymentOperationBase {
|
||||
id: string;
|
||||
kind: PaymentOperationKind;
|
||||
payment_id: string;
|
||||
environment: PaymentEnvironment;
|
||||
amount_minor: number;
|
||||
currency: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface PaymentOperationPayment extends PaymentOperationBase {
|
||||
kind: "payment";
|
||||
status: PaymentStatus;
|
||||
attempt_status: PaymentAttemptStatus | null;
|
||||
}
|
||||
|
||||
export interface PaymentOperationRefund extends PaymentOperationBase {
|
||||
kind: "refund";
|
||||
status: PaymentRefundStatus;
|
||||
purpose: RefundPurpose;
|
||||
aftersale_id: string | null;
|
||||
retry_allowed: boolean;
|
||||
}
|
||||
|
||||
export interface PaymentOperationException extends PaymentOperationBase {
|
||||
kind: "exception";
|
||||
status: PaymentExceptionStatus;
|
||||
exception_kind: PaymentExceptionKind;
|
||||
}
|
||||
|
||||
export interface PaymentOperationQuery {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
kind?: PaymentOperationKind;
|
||||
status?: PaymentStatus | PaymentAttemptStatus | PaymentRefundStatus | PaymentExceptionStatus;
|
||||
environment?: PaymentEnvironment;
|
||||
payment_id?: string;
|
||||
user_id?: string;
|
||||
}
|
||||
|
||||
export type PaymentCommandStatus = "queued" | "running" | "succeeded" | "failed";
|
||||
|
||||
/** Durable evidence-fetch command; a repeat request never initiates collection. */
|
||||
export interface PaymentReconcileCommand {
|
||||
id: string;
|
||||
payment_id: string;
|
||||
status: PaymentCommandStatus;
|
||||
error: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// ---- freight / shipping ----
|
||||
|
||||
export type FreightPricingMethod = "by_piece" | "by_weight";
|
||||
@@ -832,6 +1217,10 @@ export interface WalletSummary {
|
||||
available_minor: number;
|
||||
frozen_minor: number;
|
||||
currency: string;
|
||||
/** Deployment payment environment; immutable, never toggled per request. */
|
||||
environment: PaymentEnvironment;
|
||||
/** Demo recharge is refused server-side outside `demo`; hide the flow when false. */
|
||||
demo_recharge_enabled: boolean;
|
||||
}
|
||||
|
||||
/** Which balance bucket a ledger entry moved. */
|
||||
|
||||
Reference in New Issue
Block a user