Magento 2
The Inovio Payment Gateway module for Magento 2 and Mage-OS, built on Magento's payment-provider gateway with Vault and 3-D Secure.
Tokenized direct-post card checkout for Magento 2 and Mage-OS, on the Inovio/Argus gateway. The card number never reaches the Magento server.
Overview and status
Sale, capture, refund, void, vault and 3-D Secure are implemented and have been exercised end to end against the gateway. An 8-spec Playwright suite drives the real storefront and the real admin. Not yet hardened for production: there is no automated unit-test suite for the module beyond the correctness-critical gateway logic.
The module is a formal Magento payment-provider gateway, with a command pool
(authorize, sale, capture, refund, void) and Magento Vault for stored
cards. All settings are scoped to default, website and store view, so a
multi-site install can use different credentials per site.
Requirements
| Magento | 2.4.4+, or Mage-OS |
| PHP | 8.1+ |
| PHP extensions | bcmath (required), curl, json |
| From Inovio | API username, API password, Site ID, Site Key, Gateway Product ID |
bcmath is required, not optional. The SDK's Money type does every decimal
calculation through it so amounts never touch a binary float. Installation fails
without it. Magento 2.4 requires bcmath in its own right, so a compliant
Magento install already has it.
Install
The module is installed with Composer. The SDK it depends on
(inovio/gateway-sdk) is not on Packagist, so the module's own
composer.json declares the VCS repository for it. You need GitHub access to
that private repository.
From the Magento root:
composer require inovio/module-payment-gateway
bin/magento module:enable Inovio_PaymentGateway
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento cache:flush
In production mode, also deploy static content:
bin/magento setup:static-content:deploy
Then configure under Stores, Configuration, Sales, Payment Methods, Inovio Payment Gateway.
Changing a constructor signature invalidates Magento's compiled DI. Removing
generated/code/<Vendor> without regenerating makes the payment method
silently vanish from checkout: no exception, just an absent radio button.
Worse, in developer mode Magento tries to regenerate lazily into
generated/code/, which is not writable by www-data, so every request dies
with a main.CRITICAL "Can't create directory" and every checkout fails
instantly.
Configuration
All settings live at Stores, Configuration, Sales, Payment Methods, Inovio Payment Gateway.
Gateway credentials
| Setting | Required | Meaning |
|---|---|---|
| Enabled | Yes | Turns the payment method on. |
| Title | No | The payment method name shoppers see at checkout. |
| API Username | Yes | Gateway REQ_USERNAME. |
| API Password | Yes | Gateway REQ_PASSWORD. Stored encrypted by Magento. |
| Site ID | Yes | Gateway SITE_ID. |
| Merchant Account ID | No | MERCH_ACCT_ID. Leave empty to let the gateway distribute by currency and country. |
| Site Key | Yes | The per-site HMAC secret. Not the API password. Stored encrypted. |
| Gateway Product ID | Yes | LI_PROD_ID. Not a Magento SKU. |
| Environment | Yes | Production, or Custom URL (QA / testing). |
| Custom Gateway URL | Only when Environment is Custom | The pmt_service.cfm URL for a non-production gateway. The token and 3-D Secure endpoints are derived from it, exactly as the SDK derives them, so the three cannot drift apart. |
Site Key is a separate per-site HMAC secret issued by Inovio support. It is not the API password, and it is not something you can generate. It is used only to sign the browser's tokenization request. Without it the browser cannot tokenize and checkout fails with error 121.
Gateway Product ID (LI_PROD_ID) is a product registered on the gateway,
not a product or SKU from your Magento catalogue. The whole order bills as one
line item under it.
req_password and site_key are obscure/encrypted fields. Writing a raw value
into the database bypasses Magento's encryption, and the module's decrypt()
then returns binary garbage, producing a signing key that is silently wrong.
The symptom is checkout stalling at the payment step with "Card could not be
processed", and the token service returning error_code 121, Get CCtoken GUID
signature match fail. An encrypted value in the database starts with 0:3:;
a 64-character hex string is a plaintext value that will not work.
Payment behaviour settings
| Setting | Required | Meaning |
|---|---|---|
| Payment Action | Yes | Authorize and Capture charges at checkout. Authorize Only reserves the funds and leaves the order awaiting an invoice you raise from the order screen. |
| Enable 3D Secure | No | Runs 3DS 2 device-data collection and, when the issuer demands it, a challenge at checkout. Requires a 3DS-configured merchant account. |
| Enable Stored Cards (Vault) | No | Magento Vault: lets logged-in shoppers save cards for reuse. Stores the gateway's own card references only, never a card number. Off by default. |
| Allowed Card Types | No | Which card brands the checkout form accepts. |
| Statement Descriptor | No | PMT_DESCRIPTOR, what appears on the cardholder's statement. See the warning below. |
| Descriptor Phone | No | PMT_DESCRIPTOR_PHONE, support number shown alongside the descriptor. |
Availability
| Setting | Meaning |
|---|---|
| Payment from Applicable Countries | All countries, or a specific list. |
| Payment from Specific Countries | The list, when the setting above is restricted. |
| Sort Order | Position of the method in the checkout payment list. |
The gateway rejects the entire transaction with Invalid Data if
PMT_DESCRIPTOR contains a space, an underscore or a forward slash. Every
sale fails, not just the descriptor.
| Descriptor | Result |
|---|---|
ACME STORE |
Rejected |
ACME_STORE |
Rejected |
ACME/STORE |
Rejected |
ACME-STORE |
Accepted |
ACMESTORE |
Accepted |
ACME.STORE |
Accepted |
The full allowed set is A-Z, a-z, 0-9, and . - * + & @.
This was mapped empirically by sending each candidate character in an
otherwise-identical approved request. A multi-word descriptor is the natural
thing for a merchant to type, and it kills every sale, so if you are unsure,
leave the field empty.
Payment behaviour
| Capability | Support |
|---|---|
| Sale | Authorize and capture in one step at checkout. |
| Authorize only | Reserve funds, invoice later from the order screen. |
| Capture | Full and partial, through Magento's own invoicing. |
| Void | Reverse an uncaptured authorization. |
| Refund | Full and partial, through Magento's own credit memos. |
| Saved cards | Magento Vault, the platform's native vault, using the gateway's CUST_ID and PMT_ID references. |
| 3-D Secure | Device-data collection and challenge; frictionless and challenge flows. |
| Timeout recovery | Reconciles via status() before failing an order. |
| Other | Multi-currency read-back, dynamic descriptors, AVS/CVV policy, idempotency on order increment ID. |
Out of scope in v1: wallets (Apple Pay and Google Pay), LatAm rails, and raw-card admin MOTO orders.
Refunds follow the shared settlement-aware contract described in
Refunds are settlement-aware. A credit
memo that covers everything still refundable on the order is a full refund
and calls reverseCapture($ref, creditOnFail: true) in one gateway call; the
gateway reverses a still-unsettled capture, or auto-credits an already-settled
one. A credit memo for less is a partial refund and calls the amount-scoped
refund(), which is always CCCREDIT, and which the gateway refuses with
service code 536 on an order that has not settled. The module surfaces that as
a loud, actionable error.
Amounts are handled in the base currency throughout. Magento's
SubjectReader::readAmount() supplies base amounts, and pairing those with the
order's display currency undercharges a multi-currency store. The enrollment
leg's exact base amount is persisted and reused by the 3DS completion leg.
Timeout reconciliation is filtered by the expected action per command, so a timed-out refund cannot be confirmed by the original sale leg.
How checkout works on this platform
The direct-post flow
Card fields render in the checkout page. A module-shipped script fetches an HMAC
signature from a module controller, which holds the per-site key via
Tokenize::signRequest, POSTs the PAN from the browser directly to Inovio's
token service, and submits the order with only the single-use TOKEN_GUID.
Nothing in the payment path on your server ever sees a card number. The token
service's CORS support for this flow is verified against production.
No Inovio-hosted infrastructure is required: there is no hosted payment page and no iframe you have to redirect to.
Saved cards store the gateway's own references (CUST_ID, PMT_ID) plus the
card brand and last four digits, which are display-only fields. No PAN is ever
stored. This was verified, not assumed: a full database
dump taken after a live test run contains zero occurrences of the test card
number.
3-D Secure
The SDK provides the server legs (ThreeDSecureClient); the module supplies a
hidden device-data-collection iframe and a visible ACS challenge iframe. Both
frictionless and challenge flows are supported. The challenge return comes back
through a module controller that calls completeSale() or completeAuthorize().
For the two-token mechanics, see Two-token 3D Secure. Collapsing the two tokens into one appears to work for every frictionless card, which never reaches the completion leg at all, and fails only for cards that are actually challenged.
Leaving Payment Review after a challenge. The 3DS enrollment leg parks the
order in payment_review. Magento's RegisterCaptureNotificationCommand only
promotes an order to STATE_PROCESSING from new, pending_payment or empty,
never from payment_review. So Gateway/ThreeDSCompletionProcessor.php
explicitly calls $payment->accept() after registering the notification,
mirroring the decline path's $payment->deny(false). Without it the payment is
captured but the order sits in payment_review forever.
Admin operations
| Operation | Where | What happens |
|---|---|---|
| Invoice (capture) | The order's Invoice action | Captures against the authorization. Full and partial both supported through Magento's own invoicing. |
| Void | The order screen | Reverses an uncaptured authorization. |
| Refund | Open the Invoice, then its Credit Memo button, then Refund | Calls RefundCommand and reaches the gateway. |
Magento exposes two differently-behaved credit-memo entry points, confirmed by
hand. The order-view toolbar's own Credit Memo button pops a confirm dialog
reading "This will create an offline refund", and that path never calls the
gateway. Opening the order's Invoice and clicking its Credit Memo
button reaches the real New Memo form, which offers Refund Offline and
Refund. Only Refund (online) reaches RefundCommand.
Saved cards
Saved cards use Magento Vault, the platform's native vault. Tokens carry the
gateway's CUST_ID and PMT_ID references plus brand and last four digits.
Vaulting is off by default; turn on Enable Stored Cards (Vault).
The gateway returns the same PMT_ID for the same PAN, and
vault_payment_token's unique key is
(payment_method_code, customer_id, gateway_token), which ignores is_active
and is_visible. VaultTokenBuilder therefore looks up the existing row via
PaymentTokenManagementInterface::getByGatewayToken() and reuses or reactivates
it, rather than always inserting. Without that, a customer who saves a card,
deletes it (a soft delete), then saves the same card again hits a duplicate-key
exception and Magento aborts the whole order with a generic error.
The reactivation needs an explicit repository save, which is worth knowing
before anyone simplifies it. Magento core's AfterPaymentSaveObserver only
links a token that already has an entity_id; it short-circuits before calling
PaymentTokenRepositoryInterface::save(), because its own path only ever sets
entity_id on a token it inserted in the same request. A row fetched from a
prior request therefore never persists its reactivation. New tokens still go
through the normal observer flow, and guest checkout (customer_id = 0) is
unaffected.
Known gaps
- No automated unit-test suite for the module beyond the correctness-critical gateway logic. The SDK's conformance suite covers gateway behaviour; module tests cover the Magento side (request builders, order lifecycle, vault, 3DS controllers).
- The method does not hide itself on incomplete credentials. Unlike the other three plugins, Magento shows the payment method and then fails if one of the five required fields is empty.
- Wallets, LatAm rails and raw-card admin MOTO orders are out of scope in v1.
Verified end to end
An 8-spec Playwright suite lives in e2e/. Every step in every spec goes through
the real storefront or admin UI. Nothing writes to the database, calls the
module's PHP directly, or POSTs to the module's own controllers to advance a
test; a handful of specs use a read-only query strictly to verify an outcome
after the fact.
npm test # all specs
npx playwright test tests/03-3ds.spec.js # one spec
| Spec | What it proves |
|---|---|
01-sale.spec.js |
Card checkout approves and confirms. |
02-vault.spec.js |
Save a card and reuse it on a later order; the saved card is listed in the customer account. |
03-3ds.spec.js |
A real Cardinal ACS challenge is presented and completed, and the order reaches processing. |
04-frictionless-3ds.spec.js |
With 3DS active and a frictionless PAN, no challenge appears and the order confirms. |
05-backoffice.spec.js |
The order detail screen shows the Inovio gateway references. |
06-invalid-card-input.spec.js |
Luhn-failing PAN, short PAN, past expiry and short CVV are all rejected client-side, with no order created. |
07-refund.spec.js |
A full online credit memo against the invoice produces a real CCREVERSECAP leg against the original AuthCap, confirmed by a read-only query against the gateway's own pmt.transaction table. |
08-partial-refund.spec.js |
Two-phase: pre-settlement, the partial online refund fails visibly in the admin with no money movement; then, with settlement simulated gateway-side, the same partial refund succeeds as a CCCREDIT for the smaller amount. |
Specs 03, 07 and 08 are regression guards for real, live bugs this suite found and that have since been fixed:
- 03 guards the
payment_reviewbug: a completed, gateway-captured 3DS challenge used to leave the order permanently inpayment_reviewinstead of advancing toprocessing, so the shopper was charged and the merchant's order never became actionable. - 08 guards a money-losing full-versus-partial classification bug in
RefundCommand. Core'sCreditmemo/RefundOperation::execute()adds the memo tobase_total_refundedon the same in-memory order before callingrefund(), so subtracting it raw double-counted and every first partial refund classified as full. A $15 refund on a $20 order reversed all $20 while Magento recorded $15: a silent $5 loss with no error anywhere in the admin. The unit test hid it, because its fixture fed a pre-refund figure that production never produces.
Settlement in spec 08 is simulated gateway-side. Settlement is an acquirer batch process with no UI anywhere, so this fakes the acquirer, not the shop; every Magento interaction in the spec stays real UI.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| No payment method appears at checkout | The method is disabled, or restricted by Payment from Specific Countries. | Set Enabled to Yes and check the country restriction. This module does not hide itself on incomplete credentials, so also fill in all five required fields. |
| Still no payment method after saving config | Magento's config cache. | bin/magento cache:flush. |
| Error 121 at checkout, or the card form never tokenizes | Site Key missing or wrong, or written directly into core_config_data. |
Enter the per-site HMAC Site Key through the admin UI. It is not the API password. |
Invalid Data returned on every transaction |
The statement descriptor contains a space, underscore or forward slash. | Remove them, or clear the field. |
Refund fails with SERVICE 536 |
The order has not settled, so a partial credit is the wrong verb. | Retry the partial refund after settlement. |
| A test transaction declines with Insufficient Funds | The Inovio test gateway decides from the order total, not the card. | Change the order total. See Testing. |
| Class-not-found or a stale layout after install | DI compilation or static content not regenerated. | Re-run setup:di:compile and, in production mode, setup:static-content:deploy. |
Source
Repository: Inoviopay/inovio-gateway-magento2 (private, available from Inovio).
Composer package inovio/module-payment-gateway, module Inovio_PaymentGateway,
installed to vendor/inovio/module-payment-gateway/.