Skip to content
Payment Emulator LabDocumentationOpen console

Architecture

Business domain

The emulator sits between an application being tested and the payment providerthat application expects. It reproduces selected provider contracts whilekeeping economic behaviour in

5 min read

Índice / Index · Español

#Product model

The emulator sits between an application being tested and the payment provider that application expects. It reproduces selected provider contracts while keeping economic behaviour in a common model, so the same test environment can compare providers without flattening their public APIs.

#Actors

Actor What it does Credential
Application under test Calls Mercado Pago, Stripe or PayPal routes Application access token
Administrator Manages people, gateways and all tenants User session
Merchant Owns applications and views their payments and money User session
Customer Owns a wallet and may pay from its balance User session, optional at checkout
Anonymous buyer Completes a hosted checkout with an external method Payment link capability
Root operator Bootstraps the first admin and drives CI/seed ADMIN_TOKEN; not a person
Worker Delivers webhooks and advances scheduled state Database and transport access, no user identity

#Core concepts

#Application

An ApplicationEntity is the tenant and represents one integration sandbox. It selects a provider and default scenario, owns provider-shaped credentials and may have a merchant owner, webhook destination, banking profile and expiry. The application credential is stored only as a digest and returned in clear text only when created or rotated.

#Provider plugin

A plugin translates provider vocabulary into the canonical model. Mercado Pago, Stripe and PayPal have routed payment implementations. The fidelity reports identify their selected surfaces and remaining differences; implemented does not mean the provider's entire product catalogue is supported.

#Payment intent and provider resource

PaymentIntentEntity is the provider-neutral economic fact. ProviderResourceEntity is the same payment as the provider exposes it. The separation lets Stripe return a PaymentIntent and Mercado Pago return an Order without adding either vocabulary to the canonical table.

Diagram source
mermaid
erDiagram
    USER ||--o{ APPLICATION : owns
    USER ||--o{ USER_SESSION : signs_in_with
    USER ||--o{ BANK_ACCOUNT : owns
    BANK ||--o{ BANK_ACCOUNT : issues_numbers_for
    APPLICATION ||--o{ PAYMENT_INTENT : contains
    PAYMENT_INTENT ||--|{ PROVIDER_RESOURCE : projects_as
    APPLICATION ||--o{ LEDGER_ACCOUNT : provisions
    APPLICATION ||--o{ LEDGER_ENTRY : records
    LEDGER_ENTRY ||--|{ LEDGER_POSTING : contains
    LEDGER_ACCOUNT ||--o{ LEDGER_POSTING : receives
    APPLICATION ||--o{ WEBHOOK_EVENT : owes
    WEBHOOK_EVENT ||--o{ WEBHOOK_ATTEMPT : records
    APPLICATION ||--o{ TRACE : captures
    TRACE ||--o{ TRACE_EVENT : contains

#Scenario

A scenario is deterministic test data. Its when clause matches a provider, canonical operation, amount range and/or exact metadata. Its then clause can choose a canonical outcome, pin provider wire status, inject latency/failure, schedule webhooks or arrange a timed transition. Built-ins live as JSON under scenarios/; custom versions live in PostgreSQL.

#Ledger

An account belongs to one application and currency. An immutable entry contains two or more postings whose debits equal credits. A balance is calculated from postings according to the account's normal side; it is never stored as a mutable total. See Fake bank and accounting.

#Bank and external bank account

A BankEntity is an institution: a code somebody writes, a unique prefix its account numbers begin with, and how long those numbers are. The prefix is identity and is never edited — see Fake bank and accounting.

A BankAccountEntity belongs to a user, sits at one of those banks, and stores only the last four characters of the supplied account number. It is a counterparty referenced by funding or withdrawal entries, not another account inside the gateway's chart. Closing it keeps historical ledger references intact.

#Webhook outbox

A WebhookEventEntity means a notification is owed. It is committed in the same transaction as the payment state that caused it. WebhookAttemptEntity records the worker's delivery attempts. Queue jobs are replaceable transport, whichever store holds them; the event row is durable truth.

#Payment lifecycle

Diagram source
mermaid
stateDiagram-v2
    [*] --> created
    created --> approved
    created --> requires_action
    created --> authorized
    created --> captured
    created --> canceled
    created --> failed
    requires_action --> approved
    approved --> authorized
    approved --> captured
    approved --> canceled
    approved --> failed
    requires_action --> authorized
    requires_action --> captured
    requires_action --> canceled
    requires_action --> failed
    authorized --> captured
    authorized --> canceled
    authorized --> failed
    captured --> settled
    captured --> refunded
    captured --> disputed
    settled --> refunded
    settled --> disputed
    refunded --> disputed
  • created: resource exists but no payment attempt has happened.
  • requires_action: the payment waits for the buyer or an external event.
  • approved: buyer consent recorded, with no debit or hold.
  • authorized: funds are committed but not captured; a ledger hold may exist.
  • captured: money entered the gateway and the merchant's net is in reserve.
  • settled: reserve was released into merchant payable.
  • canceled, failed and disputed are terminal states. A refunded payment may still become disputed in the current state machine.

Provider mappings are total but not one-to-one. Stripe maps both captured and settled to succeeded; Mercado Pago maps them to processed/accredited. Canonical state answers economic questions that provider wire state may not.

#Funding source

Every payment records whether its money came from:

  • balance: the gateway debits the signed-in customer's wallet.
  • external: card, bank transfer or ticket brings money into system cash.

This value is persisted because a later refund must return money through the same side even if the provider's payment-method catalogue changes.

#Domain invariants

  • An application credential resolves exactly one tenant and provider.
  • Provider words do not enter the canonical intent.
  • A provider plugin cannot write state, accounting or queue jobs.
  • Every ledger entry balances in one currency and every posting is positive.
  • The same idempotency key/body replays; a different body conflicts.
  • A manual-capture attempt cannot become captured until the integration captures.
  • A webhook event is written in the payment transaction before delivery is tried.
  • A customer session can spend only that customer's wallet.
  • A merchant sees only applications it owns; invisible IDs return 404.

#Glossary

Term Meaning in this repository
Canonical Provider-neutral state or operation used by the core
Wire Exact provider-facing request/response vocabulary
Control plane Emulator administration APIs: users, applications, gateways, ledger views
Provider surface Routes shaped like Mercado Pago, Stripe or PayPal and authenticated by application token
Plugin Plain provider object that declares/ translates a contract but never persists
Kit Provider-specific checkout component and theme
Minor units Integer smallest currency units; 1000 USD means 10.00 USD
Posting One debit or credit line in a ledger entry
Hold Funds committed by authorization but not yet captured
Reserve Captured merchant money not yet released
Payable Released merchant money available to withdraw
Outbox Durable rows representing notifications still owed
BFF Backend for frontend; each Nuxt Nitro server mediates its own browser
Fidelity How closely an emulated provider surface matches documented/SDK behaviour
Payment Emulator Lab · RonuSoftwareMIT