#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
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
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,failedanddisputedare 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 |