Payments¶
Vectis provides a full payment lifecycle — from authorization through capture, void, and refund — with pluggable gateway strategies and built-in support for Authorize.net CIM.
Payment Flow Overview¶
flowchart TD
CO[Checkout / Admin Create Order] --> PS[PaymentService]
PS -->|capture_mode=authorize| AUTH[strategy.authorize]
PS -->|capture_mode=capture| CHARGE[strategy.charge]
AUTH --> AUTHORIZED[status: authorized]
CHARGE --> CAPTURED[status: captured]
AUTH -->|responseCode=4| HELD[status: held_for_review]
AUTHORIZED -->|admin action| CAP[strategy.capture → captured]
AUTHORIZED -->|order cancelled| VOID[strategy.void → voided]
CAPTURED -->|order refunded| REF[strategy.refund → refunded]
HELD -->|admin release| AUTHORIZED
HELD -->|order cancelled| VOID Capture Modes¶
Every payment method has a capture mode configured in the admin:
| Mode | Behavior | Best For |
|---|---|---|
| Authorize Only | Holds funds on the card but does not settle. Admin must capture later. | B2B, orders that may be modified before fulfillment |
| Authorize + Capture | Charges the card immediately in a single API call. | B2C, digital goods, immediate fulfillment |
The capture mode is stored in PaymentMethod.config.capture_mode and defaults to "authorize" for credit card gateways.
Choosing a capture mode
Use Authorize Only when orders may be edited after placement (e.g., B2B customers adding items) or when you want manual review before settling funds. Use Authorize + Capture for straightforward retail flows where the order ships immediately.
Transaction Types¶
Every payment operation creates a PaymentTransaction record with both a status and a type:
| Type | Description |
|---|---|
authorization | Funds held but not settled |
capture | Previously authorized funds settled |
charge | Auth + capture in one call |
void | Authorization cancelled before settlement |
refund | Settled funds returned to buyer |
| Status | Meaning |
|---|---|
authorized | Funds held, awaiting capture |
captured | Funds settled |
voided | Authorization cancelled |
refunded | Funds returned |
held_for_review | Flagged by gateway fraud filters |
failed | Transaction rejected |
Automatic Void & Refund¶
When an order transitions to Cancelled or Refunded, the system automatically processes the appropriate payment reversal:
- Authorized transactions are voided (no settlement occurred, so no refund needed).
- Captured transactions are refunded (full amount returned to the buyer).
- Held for review transactions are voided.
This happens as a side effect of the order state machine — no manual action is required. Failed reversals are logged but do not block the order transition.
Fraud Filter Handling¶
When Authorize.net's FDS (Fraud Detection Suite) flags a transaction:
- The transaction is stored with
status="held_for_review". - The order moves to the HeldForReview state.
- The order's
fraud_statusis set to"held_for_review"and FDS filter details are stored infraud_details. - The admin order detail page shows an orange Fraud Hold banner with the filter names.
Releasing a Fraud Hold¶
From the admin order detail page:
- Review the fraud details and FDS filter information.
- Click Release Fraud Hold to approve the transaction.
- The system calls
updateHeldTransactionRequeston Authorize.net. - The transaction status changes to
"authorized"and the order moves to AwaitingFulfillment.
Alternatively, you can cancel the order, which voids the held transaction.
Saved Payment Methods (CIM)¶
Customers can save credit cards for future use. Cards are tokenized client-side using Accept.js and stored as gateway profile references — Vectis never stores raw card data.
Storage Model¶
Each saved card stores:
- Gateway profile IDs — CIM customer and payment profile IDs
- Display fields — brand, last four digits, expiry (safe to display)
- Account and location scope — which B2B account and optionally which location the card belongs to
Location Scoping (B2B)¶
For B2B customers with multiple locations:
- Cards can be scoped to a specific location using
location_id. - Location employees see only their location's cards.
- Account owners and admins see all cards across all locations.
- Cards without a
location_idare account-wide and visible to everyone in the account.
Managing Cards¶
Storefront — customers manage cards at /account/payment-methods:
- Add new cards (tokenized via Accept.js)
- Delete cards
- Set a default card
Admin — staff manage cards in the customer detail page (Saved Cards tab):
- View all cards across locations
- Charge a saved card directly
- Delete cards
Admin Order Creation¶
Staff can create orders directly from the admin panel without going through the storefront checkout:
- Navigate to Orders → Create Order.
- Select an account, channel, and optionally a location.
- Add line items by variant ID and quantity.
- Set shipping and billing addresses.
- Choose a saved card or payment method, and select the capture mode.
- Click Create Order.
The system resolves prices from the catalog, creates the order, and processes payment through the same PaymentService pipeline as checkout.
Order Modification & Reauthorization¶
For orders with payment_status="authorized" (not yet captured), staff and B2B buyers can modify the order:
- Add, remove, or update line items.
- The system recalculates totals.
- The old authorization is automatically voided.
- A new authorization is placed for the updated grand total.
This is exposed via the modifyOrder mutation and is only available when the order is in an editable state.
Warning
Only authorized orders can be modified. Once payment is captured, the order must be refunded and re-created to change line items.
Configuring Payment Gateways¶
From Payments in the admin panel:
- Click Add Method to create a new payment gateway configuration.
- Choose a gateway (Authorize.net or Test).
- Set environment (Sandbox or Production).
- Set capture mode (Authorize Only or Authorize + Capture).
- Enter API credentials.
- Choose accepted card brands (Visa, Mastercard, etc.).
- Set supported currencies.
- Enable or disable CVV and billing address requirements.
- Test Connection to verify credentials.
Each payment method has a unique code used during checkout to route to the correct gateway.
Accept.js Integration¶
Vectis uses Authorize.net's Accept.js for PCI-compliant client-side card tokenization:
- The storefront loads Accept.js dynamically.
- Card data is entered in the browser and never touches the server.
- Accept.js returns an opaque data token (descriptor + value).
- The token is sent to the server with the checkout mutation.
- The server uses the token to authorize or charge via the Authorize.net API.
Split Tender / Multiple Tenders¶
Carts can carry more than one payment tender (a credit card + store credit, two cards, ACH + card, etc.). Buyers and approvers compose tenders via addCartTender(cartId, paymentMethodId, sequence, amount?, isRemainder) and removeCartTender(id); the cart aggregate validates that the sum of tenders equals the grand total before checkout proceeds.
PaymentTender.source (database column, Decided C12) carries provenance:
cascade— tender added by an approver during cart-approval re-curationbuyer_prepended— tender added by the buyer
The split-payment composer was moved to the checkout surface (commit b42d046) so cart-page edits don't accidentally collapse the buyer's tender plan.
ACH-vs-Card Expiry (Decided C10)¶
ACH tenders settle asynchronously and can take days to clear; card authorizations expire in 7–30 days depending on the gateway. The daily Temporal schedule vectis-void-expiring-card-auths runs VoidExpiringCardAuthsWorkflow to void any authorized card tender whose paired ACH leg hasn't settled before the card-auth expiration window. The order returns to the buyer with a vectis.workflow.fault.v1 event so the workflow-faults inbox flags the situation.
Gateway Capability Introspection (Decided #233)¶
The paymentMethodCapabilities(channelId: ID!, includeOrphaned: Boolean = false, includeDisabled: Boolean = false) query returns the capability matrix for every configured gateway: authorize, capture, void, refund, saved_cards, three_d_secure, ach, partial_refund, multi_currency. Each capability has a status (SUPPORTED, PARTIAL, UNSUPPORTED, UNAVAILABLE when the extension is missing) and an optional human-readable note.
The admin "Payment Methods" page uses this to:
- Disable the "Save card" toggle when the gateway doesn't support tokenization
- Surface a warning when 3DS is forced on a gateway that returns
PARTIAL - Filter the gateway picker in the admin Create Order page to only gateways that support the order's required capabilities
includeOrphaned returns gateway codes referenced by old DB rows but missing from any installed extension (useful for cleaning up post-uninstall). includeDisabled returns gateways toggled off on the channel.
Webhook Secret Rotation (Decided #155, OQ #28)¶
Webhook signing secrets and other sensitive payloads are stored Fernet-encrypted via the EncryptedString column type defined in backend/vectis/core/crypto.py. Two env vars drive rotation:
SECRETS_MASTER_KEY— current master key, used for new writes and readsSECRETS_MASTER_KEY_ROTATING_FROM— optional comma-separated list of older keys tried during reads (viaMultiFernet)
Encrypted values written with the previous key keep decoding until that key is removed from the rotating list, so you can rotate without downtime. Individual gateway extensions implement their own webhook-signature verification on top of this storage layer.
Per-Gateway Settlement Dispatcher (Decided #198)¶
Settlement webhooks (Authorize.net, NMI) are routed through a per-gateway dispatcher in the payment module. Each handler reads the resolved secret, verifies the signature, idempotently records the settlement (idempotency key = gateway_code + transaction_id), and updates the PaymentTransaction lifecycle. Replays return 200 OK without doubling state.
Store Credit & Overdraft (Decided #173)¶
Store credit applies before the gateway during checkout (existing behavior). When the channel setting allow_store_credit_overdraft is enabled, a redemption that would exceed the available balance creates an overdraft draft visible at /store-credit/overdraft-drafts in the admin. The draft holds the order in AwaitingPayment until an admin reviews and either:
- Approves — issues additional store credit and applies it, then settles the gateway leg for the remainder
- Denies — voids the partial debit and refunds the over-redeemed amount
Stale drafts are reaped by the GcExpiredOverdraftDraftsWorkflow Temporal schedule.
Refund Approval Workflow (OQ #26 / #27)¶
Refunds above the channel's auto-approval threshold (or any refund where the policy requires a second approver) enter a Temporal-backed approval flow:
- Submit — staff submits via
submitRefundForApproval(orderId, input). The request enters the Refund Approvals Inbox withstatus=pending. - Approve / Reject — an approver decides via
decideRefundApproval(refundRequestId, approved, notes?). Self-approval is blocked at the API. - Execute — on approval,
RefundExecutionWorkflow(invectis/modules/refund_approval/workflows.py) runs with per-tender progress. Card legs refund first, then store credit / ACH if applicable. - Retry — failed executions (timeouts, empty-transaction states) can be retried with
retryRefundExecution(refundRequestId). The workflow is idempotent against partial success.
The admin inbox shows per-tender state so the approver can see "Card $80 refunded, ACH $20 pending" rather than a single status field. Faults emit on vectis.workflow.fault.v1 and surface in the Workflow Faults inbox.