import type { StorageAdapter, Page, PageOptions, PayLogger, Authorizer, FeePolicy, PayHooks, RateProvider, EffectiveAtPolicy, FinalityPolicy } from "../interfaces.js"; import type { ID, RoundingMode } from "../types.js"; import type { Operation } from "../schemas.js"; /** Input for crediting a wallet via an external payment. */ export interface DepositInput { /** The wallet to credit. */ walletId: ID; /** Amount in the wallet's minor currency unit (e.g. kobo, cents). Must be a positive integer. */ amountMinor: number; /** ISO 4217 currency code. Must match the wallet's currency. */ currency: string; /** Optional idempotency key — resubmitting the same key returns the original operation. */ idempotencyKey?: string; /** Provider-side transaction reference (e.g. Paystack reference). */ externalId?: string; /** Payment provider name (must match a registered adapter name, e.g. `"paystack"`). */ provider?: string; /** * How the operation reaches finality. Defaults to the wallet's `defaultFinalityMode`. * * @remarks * **Soft-finality wallets and balance visibility:** When a wallet is created with * `defaultFinalityMode: "soft"`, calling `deposit()` without this field creates the * operation in `"pending"` state. `pay.wallets.getBalance()` returns `available = 0` * until `pay.operations.settle(depositOp.id)` is explicitly called. * * Pass `finalityMode: "hard"` to make the balance immediately available — this * overrides the wallet's default on a per-call basis: * ```ts * await pay.operations.deposit({ * walletId, * amountMinor: 2000, * currency: "PCRD", * finalityMode: "hard", // available immediately — no settle() call needed * }); * ``` * * @see {@link OperationsDomain.settle} */ finalityMode?: "soft" | "hard"; /** Arbitrary key-value metadata stored on the operation. */ metadata?: Record; /** * TTL in milliseconds for the pending operation. When set, `pendingExpiresAt` * is stored on the operation. After expiry, `expireOverdue()` or lazy triggers * in `operations.get()` and `webhooks.handleWithEvent()` will settle at * `metadata._pay_totalPaid` (if > 0) or fail the operation. * Used by multi-payment adapters (e.g. 100Pay). Omit for non-expiring deposits. */ pendingTtlMs?: number; } /** Input for debiting a wallet (e.g. a withdrawal or outbound payment). */ export interface WithdrawInput { /** The wallet to debit. */ walletId: ID; /** Amount in the wallet's minor currency unit. Must be a positive integer. */ amountMinor: number; /** ISO 4217 currency code. Must match the wallet's currency. */ currency: string; /** Optional idempotency key. */ idempotencyKey?: string; /** How the operation reaches finality. Defaults to the wallet's `defaultFinalityMode`. */ finalityMode?: "soft" | "hard"; /** Arbitrary key-value metadata stored on the operation. */ metadata?: Record; /** * Internal flag — set by pay.payouts.send() to prevent double-authorization. * The payouts domain authorizes with action "payout.send" before calling * withdraw(), so the inner authorizer check must be skipped. * Never set this from application code. * @internal */ _skipAuthorizer?: boolean; } /** Input for moving funds between two wallets within the same tenant. */ export interface TransferInput { /** Source wallet to debit. */ fromWalletId: ID; /** Destination wallet to credit. */ toWalletId: ID; /** Amount in the transfer currency's minor unit. Must be a positive integer. */ amountMinor: number; /** ISO 4217 currency code. Both wallets must use this currency. */ currency: string; /** Optional idempotency key. */ idempotencyKey?: string; /** How the operation reaches finality. Defaults to the source wallet's `defaultFinalityMode`. */ finalityMode?: "soft" | "hard"; /** Arbitrary key-value metadata stored on the operation. */ metadata?: Record; } /** Input for a currency-conversion operation between two same-owner wallets. */ export interface ConvertInput { /** Source wallet to debit (must hold `fromCurrency`). */ fromWalletId: ID; /** Destination wallet to credit (must hold `toCurrency`). */ toWalletId: ID; /** Amount to convert, expressed in `fromCurrency` minor units. */ amountMinor: number; /** ISO 4217 source currency code. */ fromCurrency: string; /** ISO 4217 target currency code. */ toCurrency: string; /** Pre-fetched FX snapshot id; if omitted, adapter will fetch live rate */ fxSnapshotId?: ID; /** Optional idempotency key. */ idempotencyKey?: string; /** How the operation reaches finality. Defaults to the source wallet's `defaultFinalityMode`. */ finalityMode?: "soft" | "hard"; /** Arbitrary key-value metadata stored on the operation. */ metadata?: Record; } /** Admin-initiated balance correction (§10). */ export interface AdjustInput { /** The wallet to adjust. */ walletId: ID; /** Direction of the correction from the perspective of the target wallet. */ entryType: "credit" | "debit"; /** Amount of the adjustment in minor units. */ amountMinor: number; /** ISO 4217 currency code. Must match the wallet's currency. */ currency: string; /** Operation being corrected — required when intent is fee_correction. */ parentOperationId?: ID; /** Optional idempotency key. */ idempotencyKey?: string; /** Arbitrary key-value metadata stored on the operation. */ metadata?: Record; } /** * Input for a first-party wallet payment — charge a user's own wallet for a service. * * @alpha Designed but not yet implemented. */ export interface PayInput { /** The wallet to debit (buyer/payer). */ walletId: ID; /** * The wallet to credit (merchant/service recipient). * Defaults to the system wallet for the currency when omitted. */ toWalletId?: ID; /** Amount to charge in minor units. */ amountMinor: number; /** ISO 4217 currency code. Must match the wallet's currency. */ currency: string; /** Optional line-item breakdown for the charge. */ lineItems?: Array<{ /** Human-readable description of the line item. */ description: string; /** Line item amount in minor units. */ amountMinor: number; /** Quantity multiplier (defaults to 1). */ quantity?: number; }>; /** Optional idempotency key. */ idempotencyKey?: string; /** How the operation reaches finality. Defaults to the wallet's `defaultFinalityMode`. */ finalityMode?: "soft" | "hard"; /** Arbitrary key-value metadata stored on the operation. */ metadata?: Record; } /** * Options for cancelling a pending operation. * * @alpha Designed but not yet implemented. */ export interface CancelInput { /** Human-readable reason for the cancellation (stored in operation metadata). */ reason?: string; /** Optional idempotency key. */ idempotencyKey?: string; } /** * Resolution action for a stuck manual-resolution operation (e.g. a * `transferWithConversion` that partially failed and could not self-reverse). * * @param action - `retry_reversal` to re-attempt the reversal, or `mark_resolved` to manually close without a reversal. * @param reconciliationOperationId - Id of the operation that corrects the balance (required for `mark_resolved` when the bridge wallet has a non-zero balance). * @param notes - Free-text notes stored on the operation for audit purposes. * @param idempotencyKey - Optional idempotency key. * @param resolvedBy - Identifier of the actor (operator id, service account id, or system identifier) that initiated the resolution (§46.9.7 step 4). Stored for audit trail. * * @alpha Designed but not yet implemented. */ export interface ResolveManualResolutionInput { /** * `retry_reversal` — attempt to reverse the stuck leg again. * `mark_resolved` — mark the case as manually resolved without a reversal * (use when a human has corrected the balance offline). */ action: "retry_reversal" | "mark_resolved"; /** Id of the reconciliation operation that corrects the balance (required for `mark_resolved`). */ reconciliationOperationId?: ID; /** Free-text notes stored with the operation for audit purposes. */ notes?: string; /** Optional idempotency key. */ idempotencyKey?: string; /** * Identifier of the actor (operator id, service account id, or system identifier) * that initiated the resolution (§46.9.7 step 4). Stored in metadata for audit purposes. */ resolvedBy?: string; } /** * Options for settling an operation, supporting provider fee handling. * * feeHandling modes: * - "gross" (default): Settle the full amount. Provider fees are recorded on * the operation for audit purposes but do not affect the wallet balance. * - "separate": Settle the full amount, then immediately create and complete * a child `fee` operation that debits the wallet by the provider fee. The * net wallet balance equals amountMinor - providerFees. */ export interface SettleOptions { /** * Provider-reported processing fee in minor units (e.g. 75 kobo). * Required when feeHandling is "separate". */ providerFees?: number; /** ISO 4217 currency code for the provider fee. Defaults to the wallet's currency. */ providerFeesCurrency?: string; /** How to handle the provider fee in the ledger. Defaults to "gross". */ feeHandling?: "gross" | "separate"; /** * Override the amount recorded in the ledger entries. * * When set and different from `op.amount` (the quoted amount), the pending * entries are failed and new `completed` entries are created at this amount. * `op.amount` is preserved as the quoted amount for audit purposes. * * Use cases: * - Partial settlement: expired underpaid operation settled at `_pay_totalPaid` * (less than quoted). * - Overpayment: 100Pay settles each payment individually, so `total_paid` may * exceed `billing.amount`. Pass `amountOverride: totalPaidMinor` to credit * the actual amount received. */ amountOverride?: number; /** * Arbitrary key-value pairs to deep-merge into the operation's metadata upon * settlement. Use for caller-supplied context that the provider adapter does * not persist — e.g. payer identity extracted from a webhook payload. * * Keys are merged with `storage.mergeOperationMetadata` after the operation * status is set to `completed`. Existing metadata keys not present here are * preserved unchanged. */ extraMetadata?: Record; } /** Double-entry ledger operations — deposits, withdrawals, transfers, conversions, and lifecycle transitions. */ export interface OperationsDomain { /** * Credit a wallet with funds received from an external payment provider. * * Creates a `deposit` operation in `pending` status. Call {@link settle} once * the provider confirms the payment, or use {@link WebhooksDomain.handle} for * automatic webhook-driven settlement. * * @param input - Deposit parameters. * @returns The newly created `deposit` operation (status: `pending`). * @throws {WalletNotFoundError} if the wallet does not exist. * @throws {WalletArchivedError} if the wallet has been archived. * @throws {WalletFrozenError} if the wallet is frozen for debits or all operations. * @throws {CurrencyMismatchError} if `currency` does not match the wallet's currency. * @throws {InvalidAmountError} if `amountMinor` is not a positive integer. * @throws {IdempotencyConflictError} if the key was already used with different parameters. * * @example * ```ts * const op = await pay.operations.deposit({ * walletId: wallet.id, * amountMinor: 10000, // ₦100.00 * currency: "NGN", * externalId: "ref_abc123", * provider: "paystack", * idempotencyKey: "dep_order_001", * }); * // op.status === "pending" until settled via webhook * ``` */ deposit(input: DepositInput): Promise; /** * Debit a wallet to record an outgoing payment (e.g. bank withdrawal). * * @param input - Withdrawal parameters. * @returns The newly created `withdrawal` operation. * @throws {WalletNotFoundError} if the wallet does not exist. * @throws {WalletArchivedError} if the wallet has been archived. * @throws {WalletFrozenError} if the wallet is frozen. * @throws {CurrencyMismatchError} if `currency` does not match the wallet's currency. * @throws {InvalidAmountError} if `amountMinor` is not a positive integer. * @throws {InsufficientFundsError} if available balance < `amountMinor` and `allowNegativeBalance` is `false`. * @throws {IdempotencyConflictError} if the key was already used with different parameters. * * @example * ```ts * const op = await pay.operations.withdraw({ * walletId: wallet.id, * amountMinor: 5000, * currency: "NGN", * idempotencyKey: "wdr_001", * }); * ``` */ withdraw(input: WithdrawInput): Promise; /** * Move funds between two wallets of the same currency within the tenant. * * Creates paired debit/credit ledger entries atomically. * * @param input - Transfer parameters. * @returns The newly created `transfer` operation. * @throws {WalletNotFoundError} if either wallet does not exist. * @throws {WalletArchivedError} if either wallet is archived. * @throws {CurrencyMismatchError} if the wallets have different currencies or `currency` mismatches. * @throws {InsufficientFundsError} if the source wallet has insufficient available balance. * @throws {IdempotencyConflictError} if the key was already used with different parameters. * * @example * ```ts * const op = await pay.operations.transfer({ * fromWalletId: senderWallet.id, * toWalletId: receiverWallet.id, * amountMinor: 20000, * currency: "NGN", * }); * ``` */ transfer(input: TransferInput): Promise; /** * Convert an amount from one currency to another across two wallets. * * Fetches a live FX rate (or uses the supplied snapshot) and creates paired * debit/credit entries at the locked rate. * * @param input - Conversion parameters. * @returns The newly created `conversion` operation. * @throws {WalletNotFoundError} if either wallet does not exist. * @throws {FxRateUnavailableError} if no rate provider is configured or the snapshot is expired. * @throws {CurrencyMismatchError} if wallet currencies do not match `fromCurrency`/`toCurrency`. * @throws {InsufficientFundsError} if the source wallet has insufficient available balance. * @throws {IdempotencyConflictError} if the key was already used with different parameters. * * @example * ```ts * const op = await pay.operations.convert({ * fromWalletId: ngnWallet.id, * toWalletId: usdWallet.id, * amountMinor: 100000, // ₦1000 * fromCurrency: "NGN", * toCurrency: "USD", * }); * ``` */ convert(input: ConvertInput): Promise; /** * Fetch a single operation by its id. * * @param operationId - The ULID identifier of the operation. * @returns The operation record. * @throws {OperationNotFoundError} if no operation with that id exists in this tenant. */ get(operationId: ID): Promise; /** * List all operations for a specific wallet, ordered by `effectiveAt` descending. * * @param walletId - The wallet to query. * @param page - Optional pagination cursor and limit. * @returns A paginated list of operations. * @throws {WalletNotFoundError} if the wallet does not exist. */ listByWallet(walletId: ID, page?: PageOptions): Promise>; /** * Cross-wallet tenant-scoped operation listing. * * Requires the configured authorizer to approve `operations.list` when one * is present. Supports optional filtering by wallet, type, status, provider, * date range, and idempotency key. * * @param page - Optional pagination cursor and limit. * @param filter - Optional filter criteria. * @returns A paginated list of operations matching the filter. * @throws {AuthorizationError} if the authorizer denies `operations.list`. */ list(page?: PageOptions, filter?: { walletId?: string; /** * Return operations where the given wallet appears on **either** side * (i.e. `walletId === anyWalletId` OR `counterpartyWalletId === anyWalletId`). * Use this to build a complete transaction history for a wallet, including * incoming transfers and conversion credits where the wallet is the destination. * Mutually exclusive with `walletId` — if both are set, `anyWalletId` takes precedence. */ anyWalletId?: string; /** * Return operations where **any** of the given wallets appear on either side. * Equivalent to `anyWalletId` but for multiple wallets simultaneously. * Use when building an aggregated transaction history across all of a user's wallets. * Takes precedence over both `walletId` and `anyWalletId`. */ anyWalletIds?: string[]; types?: string[]; statuses?: string[]; provider?: string; fromEffectiveAt?: Date; toEffectiveAt?: Date; idempotencyKey?: string; /** * Return only operations whose `pendingExpiresAt` is before this date. * Used internally by `expireOverdue()`. */ pendingExpiredBefore?: Date; }): Promise>; /** * Settle a pending operation — mark its ledger entries and the operation * itself as `completed`. * * Optionally handles provider fees via `feeHandling` in `opts`. * * @param operationId - The operation to settle. * @param opts - Optional fee handling configuration. * @returns The settled operation (status: `completed`). * @throws {OperationNotFoundError} if the operation does not exist. * @throws {OperationStatusError} if the operation is not in `pending` or `processing` status. */ settle(operationId: ID, opts?: SettleOptions): Promise; /** * Fail a pending or processing operation — mark it as `failed` and release * any reserved balance. * * @param operationId - The operation to fail. * @returns The failed operation. * @throws {OperationNotFoundError} if the operation does not exist. * @throws {OperationStatusError} if the operation is already in a terminal state. */ fail(operationId: ID): Promise; /** * Reverse a completed operation by creating compensating ledger entries. * * Subject to the configured {@link FinalityPolicy} reversal window. * Hard-finality operations (e.g. withdrawals) cannot be reversed. * * @param operationId - The completed operation to reverse. * @returns The new `reversal` operation. * @throws {OperationNotFoundError} if the operation does not exist. * @throws {ReversalNotAllowedError} if the operation has hard finality, wrong status, or the reversal window has expired. */ reverse(operationId: ID): Promise; /** * Admin-initiated balance correction — credit or debit a wallet without an * external payment event (§10). * * Use for reconciliation, corrections, and manual adjustments. * * @param input - Adjustment parameters including direction (`entryType`) and amount. * @returns The created `adjustment` operation (immediately `completed`). * @throws {WalletNotFoundError} if the wallet does not exist. * @throws {CurrencyMismatchError} if `currency` does not match the wallet's currency. * @throws {InvalidAmountError} if `amountMinor` is not a positive integer. * @throws {AuthorizationError} if the configured authorizer denies `operation.adjust`. * * @example * ```ts * // Credit a wallet by ₦50 to correct a previous error * const adj = await pay.operations.adjust({ * walletId: wallet.id, * entryType: "credit", * amountMinor: 5000, * currency: "NGN", * }); * ``` */ adjust(input: AdjustInput): Promise; /** * First-party wallet payment — charge a user's own wallet for a product or service. * * @alpha Designed but not yet implemented. Calling this method will always * throw {@link OperationNotSupportedError} until a future release. * * @param input - Payment parameters including line items. * @throws {OperationNotSupportedError} always — not yet implemented. */ pay(input: PayInput): Promise; /** * Cancel a pending operation before it is settled. * * @alpha Designed but not yet implemented. Calling this method will always * throw {@link OperationNotSupportedError} until a future release. * * @param operationId - The pending operation to cancel. * @param opts - Optional cancellation reason and idempotency key. * @throws {OperationNotSupportedError} always — not yet implemented. */ cancel(operationId: ID, opts?: CancelInput): Promise; /** * Resolve a stuck manual-resolution case from a partial `transferWithConversion` failure. * * @alpha Designed but not yet implemented. Calling this method will always * throw {@link OperationNotSupportedError} until a future release. * * @param operationId - The operation in `manual_resolution` status. * @param input - The resolution action and optional reconciliation operation. * @throws {OperationNotSupportedError} always — not yet implemented. */ resolveManualResolution(operationId: ID, input: ResolveManualResolutionInput): Promise; /** * Sweep for expired pending operations and settle or fail them. * * Finds all `pending` operations whose `pendingExpiresAt` is before `opts.before` * (or before `now` when `opts.before` is omitted): * - If `metadata._pay_totalPaid > 0` → settle at that amount via `amountOverride`. * - Otherwise → fail the operation (no funds received). * * Call this from a scheduled job or CRON in serverless environments. * The same expiry logic also fires lazily inside `operations.get()` and * `webhooks.handleWithEvent()`. * * @param opts.before - Expire operations whose `pendingExpiresAt` is before this date. Defaults to `new Date()`. * @returns Counts of operations settled (partial) and failed (no funds received). */ expireOverdue(opts?: { before?: Date; }): Promise<{ settled: number; failed: number; }>; } export interface OperationsDomainOptions { tenantId: ID; storage: StorageAdapter; rateProvider?: RateProvider; logger?: PayLogger; authorizer?: Authorizer; feePolicy?: FeePolicy; hooks?: PayHooks; roundingMode?: RoundingMode; fxMetadata?: { /** Enable automatic FX metadata attachment on operations. Defaults to true. */ enabled?: boolean; /** Base currency used for auto quote metadata. Defaults to USD. */ baseCurrency?: string; /** Throw when FX metadata quote fails. Defaults to false. */ strict?: boolean; }; /** * Optional policy controlling how far in the past or future an entry's * effectiveAt may be set. Violations throw InvalidEffectiveAtError. * Default: 7-day backdate window, no future dates allowed. */ effectiveAtPolicy?: EffectiveAtPolicy; /** * Optional global finality and reversal-window policy (§36). * Controls whether reversal is allowed and for how long after completion. * When omitted, default windows apply: deposit/transfer/payment = 24h, * conversion = 5m; withdrawal/fee/reversal/adjustment = hard. */ finalityPolicy?: FinalityPolicy; } export declare function createOperationsDomain(opts: OperationsDomainOptions): OperationsDomain; //# sourceMappingURL=operations.d.ts.map