GitHub Markdown

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

Functional pre-release

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.

Re-run `setup:di:compile` after any DI change

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.

Set credentials through the admin UI, not directly in `core_config_data`

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 statement descriptor must not contain a space, underscore or slash

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.
Only the invoice-scoped "Refund" button 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_review bug: a completed, gateway-captured 3DS challenge used to leave the order permanently in payment_review instead of advancing to processing, 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's Creditmemo/RefundOperation::execute() adds the memo to base_total_refunded on the same in-memory order before calling refund(), 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/.