Skip to content

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:

  1. The transaction is stored with status="held_for_review".
  2. The order moves to the HeldForReview state.
  3. The order's fraud_status is set to "held_for_review" and FDS filter details are stored in fraud_details.
  4. 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:

  1. Review the fraud details and FDS filter information.
  2. Click Release Fraud Hold to approve the transaction.
  3. The system calls updateHeldTransactionRequest on Authorize.net.
  4. 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_id are 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:

  1. Navigate to Orders → Create Order.
  2. Select an account, channel, and optionally a location.
  3. Add line items by variant ID and quantity.
  4. Set shipping and billing addresses.
  5. Choose a saved card or payment method, and select the capture mode.
  6. 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:

  1. Add, remove, or update line items.
  2. The system recalculates totals.
  3. The old authorization is automatically voided.
  4. 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:

  1. Click Add Method to create a new payment gateway configuration.
  2. Choose a gateway (Authorize.net or Test).
  3. Set environment (Sandbox or Production).
  4. Set capture mode (Authorize Only or Authorize + Capture).
  5. Enter API credentials.
  6. Choose accepted card brands (Visa, Mastercard, etc.).
  7. Set supported currencies.
  8. Enable or disable CVV and billing address requirements.
  9. 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:

  1. The storefront loads Accept.js dynamically.
  2. Card data is entered in the browser and never touches the server.
  3. Accept.js returns an opaque data token (descriptor + value).
  4. The token is sent to the server with the checkout mutation.
  5. 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-curation
  • buyer_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 reads
  • SECRETS_MASTER_KEY_ROTATING_FROM — optional comma-separated list of older keys tried during reads (via MultiFernet)

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:

  1. Submit — staff submits via submitRefundForApproval(orderId, input). The request enters the Refund Approvals Inbox with status=pending.
  2. Approve / Reject — an approver decides via decideRefundApproval(refundRequestId, approved, notes?). Self-approval is blocked at the API.
  3. Execute — on approval, RefundExecutionWorkflow (in vectis/modules/refund_approval/workflows.py) runs with per-tender progress. Card legs refund first, then store credit / ACH if applicable.
  4. 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.