feat(connector): [ilixium] add Ilixium connector with all six core flows - #2125
Open
shuklatushar226 wants to merge 10 commits into
Open
feat(connector): [ilixium] add Ilixium connector with all six core flows#2125shuklatushar226 wants to merge 10 commits into
shuklatushar226 wants to merge 10 commits into
Conversation
This was referenced Aug 13, 2026
Add the Ilixium Direct API connector with card Authorize support for both no-3DS and 3DS, including the POST /direct/threedcomplete 3DS-completion leg. Auth is X-MERCHANT-DIGEST only: Base64(SHA512(body)) then Base64(SHA512(that || digest password)). Credentials are read at runtime from the request context; nothing is hardcoded. All other flows are registered as not_implemented / not_supported. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Connectors that finalise 3-D Secure through the Authorize flow (Ilixium, NMI, Airwallex, Nexixpay, ...) are re-invoked after the ACS posts back to `TermUrl`. At that point the caller no longer holds the raw instrument, so Hyperswitch sends `payment_method_data: None` and omits the proto field. The Authorize handler required `payment_method` unconditionally and rejected the call with `InvalidArgument: Missing required field: payment_method` before any connector code ran, making 3DS completion impossible for every such connector. `payment_method` is now required only when this is not a redirect-return leg. The leg is identified by `redirection_response`, the same signal the connectors themselves key off (`request.redirect_response.is_some()`). A plain authorize with no `payment_method` is still rejected exactly as before. `PaymentsAuthorizeData::payment_method_data` is not optional, so the absent instrument is modelled explicitly as `PaymentMethodData::NoInstrumentAfterRedirect` rather than substituted with a default that a connector could mistake for a payable instrument. Every existing match on `PaymentMethodData` now handles the new variant through its existing unsupported-payment-method rejection, so no connector's behaviour changes; adding the variant is compiler-enforced, so a connector that later wants to accept the leg has to opt in deliberately. Verified end to end against a mock Ilixium server: HS /payments -> UCS -> `POST /direct/auth`, ACS challenge, complete-authorize -> `POST /direct/threedcomplete`, payment `succeeded`. The no-3DS authorize and the missing-`payment_method` rejection were re-checked and are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completes the six core payment flows for Ilixium. Card, one-time only. - Capture POST /direct/capture - Void POST /direct/reversal (a separate endpoint, not capture-with-zero) - Refund POST /direct/refund (variant A, referencing a previous transaction) - PSync POST /history/operations, filtered client-side - RSync POST /history/operations, filtered to type == REFUND build_headers is now generic over the flow-common-data type so the refund flows can reuse it, and connector_base_url_refunds is added alongside the payments helper. Notes for reviewers: Capture, Void and Refund are all keyed off the ORIGINAL authorisation's transaction.merchantRef; Ilixium publishes no gateway-id lookup. The existing derive_merchant_ref helper is deterministic, so follow-up operations recompute the same reference with no persisted mapping. Partial reversals are rejected by Ilixium with code 142, so Void must send the full original amount. PaymentVoidData.amount is optional and every amount field on the Void PaymentFlowData is None, so a missing amount fails locally with MissingRequiredField rather than sending a zero or partial reversal. Ilixium publishes no per-payment or per-refund status endpoint, so PSync and RSync both go through the bulk /history/operations reconciliation report and filter client-side. That report caps the window at 24h, uses a different status enum (EXCEPTION, VALIDATION_ERRORS) and a different transactionType enum (CNP_ECOMMERCE), and carries no paymentHistory or cardResponse. A payment outside the window cannot be synced; the window can be anchored by the caller via connector_feature_data.ilixium_history_period_start. Refund responses carry no refund-specific identifier: merchantRef and gatewayRef both echo the original payment. connector_refund_id therefore prefers status.operationRef and falls back to the refund's own UCS reference. While operationRef is not live, multiple partial refunds of the same amount on one payment are not individually distinguishable. Verified against a mock Ilixium server driven end to end from Hyperswitch: 23 captures, 0 digest mismatches, 0 schema failures, and merchantRef proven identical between Authorize and its Capture/Void/Refund (with a negative control confirming a wrong reference is rejected with code 104). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Rebasing onto main surfaced two upstream API changes that the branch predates, plus three spell-check hits. No Ilixium behaviour changes. - `ErrorResponse` gained `typed_connector_response`, `raw_connector_response`, `raw_connector_request` and `typed_connector_request`. All ten Ilixium initializers now pass `None` for the four, matching the repo-majority pattern (385 sites use `None`) and preserving existing behaviour exactly. - `ConnectorRequestData` no longer exposes `get_inner_value`; it now wraps the wire body (`content`) alongside a masked typed copy kept for observability (`typed_request`). `build_headers` takes the digest over `.content`, i.e. the unmasked bytes that actually go on the wire — digesting the masked copy would sign redacted JSON and the gateway would reject every request. - typos: `unparseable` -> `unparsable`, `mis-attribution` -> `misattribution`. The third hit was the `ThreedSecureComplete` variant. Renaming it naively would have been a silent wire-contract change: the enum derives `rename_all = "SCREAMING_SNAKE_CASE"`, so any respelling re-derives a different string and Ilixium's real value would fall through to `#[serde(other)] Unknown` with no error anywhere. The variant is renamed *and* its wire value pinned with an explicit `#[serde(rename = "THREED_SECURE_COMPLETE")]`, with `THREED` allowlisted in `.typos.toml` the same way `Payed` already is for Santander. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shuklatushar226
force-pushed
the
feat/grace-Illexium
branch
from
August 15, 2026 19:27
8fed6a8 to
4ffde48
Compare
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
`Compilation Check` runs `check_connector_specs`, which asserts that every connector
registered in connector-integration has a matching `connector_specs/<name>/` directory.
Ilixium was added without one, so Phase 1 reported 104 integration connectors against
103 spec directories and the job failed. The branch's compile errors had been masking
this — the job never got far enough to run the check.
The six suites listed are exactly the flows the connector implements (Authorize,
Capture, Void, PSync, Refund, RSync); `PaymentService/Reverse` is not among them.
Phase 2 ("flow coverage") independently validates this: it now checks 101 connectors
instead of 100 and reports all flows accounted, which it would not do if the suite
list disagreed with the macro's declared flows.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`execute_connector_processing_step` passed `&router_data.clone()` to `build_request_v2` and `build_kafka_record`. `router_data` is already owned and still live afterwards, so the clone existed only to produce a reference the borrow checker would have given for free. `RouterDataV2` is on the order of 15-20 KB by value here — `PaymentFlowData` alone is 13 KB, most of which is the `Connectors` config — so each of these materialised a large temporary on the hot path of every request, once per transport. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Connectors` carries one `ConnectorParams` (96 bytes) per supported connector — a fixed inline
footprint even when every URL is empty. At 108 connectors it is 10,344 bytes and grows by 96
bytes with every connector added.
It is stored *by value* in ten structs: nine flow-data types (`PaymentFlowData`,
`RefundFlowData`, `DisputeFlowData`, `VerifyWebhookSourceFlowData`,
`RefreshPaymentMethodFlowData`, `MerchantAuthenticationFlowData`, `FrmFlowData`,
`SurchargeFlowData`, `PayoutFlowData`) plus the top-level `Config`. All nine derive `Clone` and
are embedded by value in `RouterDataV2`, which is cloned several times per request, so the size
is paid repeatedly in both stack and memcpy — and it scales with the number of connectors in
the repo rather than with anything about the payment.
That limit is now being reached: on a branch adding one connector the Stripe flow tests abort
with `fatal runtime error: stack overflow`, while the same tests pass on main. Stripe is the
only connector with credentials in CI and therefore the only test that exercises the full
request/response path. The connector added there contributes 96 bytes; it is the straw, not the
cause, and the next connector added would do the same.
`Connectors` becomes a newtype over `Arc<ConnectorsInner>`:
size_of::<Connectors>() 10344 -> 8
size_of::<PaymentFlowData>() 13056 -> 2816
size_of::<RefundFlowData>() 10752 -> 512
size_of::<PayoutFlowData>() 10608 -> 368
The `Arc` goes *inside* the type rather than at each use site, which is what keeps the change
small: `Deref<Target = ConnectorsInner>` means all 113 `ConnectorCommon::base_url`
implementations and every other read site compile untouched (each reads exactly one field, e.g.
`connectors.stripe.base_url`), and `Connectors` stays a by-value type so the `ForeignTryFrom`
tuple impls, the `apply_url_overrides` call sites and the `patch_*` methods returning `Self`
keep compiling while moving 8 bytes instead of 10 KB. Only the 18 literal `Connectors { .. }`
constructions, all in connector test modules, needed updating.
Notes:
- `Serialize`/`Deserialize` are implemented by hand rather than via `#[serde(transparent)]`,
because serde only supports `Arc` under its `rc` feature, which this workspace does not
enable. They round-trip through `ConnectorsInner`, so the on-disk config shape is unchanged;
verified by booting grpc-server against the existing `config/development.toml`.
- `config_patch_derive::Patch` names a field's patch type `<FieldType>Patch`, so
`ConnectorsPatch` is kept as an alias of `ConnectorsInnerPatch` with a delegating impl.
- The five `patch_*_connector_urls` helpers mutate through `Arc::make_mut`, so the deep copy
now happens only when a patch actually applies rather than on every call.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the Ilixium connector with all six core payment flows. Card, one-time payments only (3DS and no-3DS).
POST /direct/auth(+POST /direct/threedcompleteon the 3DS return leg)POST /direct/capturePOST /direct/reversalPOST /direct/refund(variant A, referencing a previous transaction)POST /history/operations, filtered client-sidePOST /history/operations, filtered totype == REFUNDNot in scope: wallets, APMs, bank transfers, mandates/MIT/recurring, tokenization, standalone refunds, disputes.
Commit
664e8abf3is not connector code.PaymentService::authorizeunconditionally requiredpayment_method, but on a 3DS return leg Hyperswitch has no raw card data and sendspayment_method_data: None, so UCS rejected the call withInvalidArgumentbefore any connector code ran. This blocked every UCS connector that completes 3DS through the Authorize flow — which is exactly Ilixium's documented design.The fix makes
payment_methodrequired only whenredirection_responseis absent, and adds aPaymentMethodData::NoInstrumentAfterRedirectvariant. It touches 34 connectortransformers.rsfiles, but only to add that variant to each one's existing unsupported-payment-method reject arm — compiler-enforced, no behaviour change whenpayment_methodis present. A plain authorize missingpayment_methodis still rejected exactly as before.There was a second gate on the same field in
PaymentsAuthorizeData::foreign_try_from; relaxing only the handler would still have failed.Design notes for reviewers
Everything is keyed off
transaction.merchantRef. Ilixium publishes no gateway-id lookup, so Capture, Void and Refund all quote the original authorisation's merchantRef. The existingderive_merchant_refhelper is deterministic (verbatim when it fits[\w]{4,20}, elsehex(SHA-512(ref))[..20]), so follow-up operations recompute the same reference with no persisted mapping.Void must send the full original amount. Partial reversals are rejected with code 142.
PaymentVoidData::amountis optional and every amount field on the VoidPaymentFlowDataisNone, so a missing amount fails locally withMissingRequiredFieldrather than sending a zero or partial reversal — which would earn a 142 and burn an operation slot (Ilixium serialises operations per transaction, code 117).PSync/RSync have no real endpoint. Ilixium publishes no per-payment or per-refund status query.
/ipframe/apm/statusis ruled out: different host (pripframev2.ilixium.com), tagged "Alternative Payment Methods", and requires aresponseKeythat/direct/authnever returns. So both sync flows use the bulk/history/operationsreconciliation report and filter client-side. That report caps the window at 24 hours, uses a different status enum (addsEXCEPTION,VALIDATION_ERRORS) and a differenttransactionTypeenum (CNP_ECOMMERCE), and carries nopaymentHistory/cardResponse. A payment outside the window cannot be synced; the caller can anchor the window viaconnector_feature_data.ilixium_history_period_start. No-match leaves the status unchanged and returns an actionable error rather than inventing a terminal failure.build_headersis now generic over the flow-common-data type so the refund flows can reuse it, withconnector_base_url_refundsalongside the payments helper.Known limitations
merchantRefandgatewayRefboth echo the original payment.connector_refund_idprefersstatus.operationRef, which the vendor documents as "available soon", and falls back to the refund's own UCS reference. WhileoperationRefis not live, multiple partial refunds of the same amount on one payment cannot be told apart; RSync matches on amount then latestentryDateand warns when more than one candidate exists.customer.dateOfBirthis mandatory at Ilixium with no UCS counterpart. Sourced from merchant metadata keyilixium_date_of_birth, omitted when absent — not fabricated.ConnectorSpecificConfig::Ilixium.base_url.requires_capture→processing). The connector correctly echoes the incoming status, butPaymentServiceGetRequestcarries no current attempt status, so UCS defaults toPending. Platform limitation affecting all connectors, not fixed here.Testing
No Ilixium credentials exist, so this is verified against a mock Ilixium server driven end to end from Hyperswitch through UCS — not against the live sandbox.
x-merchant-digestwas independently recomputed from the raw received bytes on every capture, confirmingBase64(SHA512(Base64(SHA512(body)) ‖ password))./paymentsand/refunds: no-3DS authorize; 3DS authorize through both legs including the ACS redirect; manual-capture → Capture; manual-capture → Void; authorize → Capture → Refund → RSync; PSync.ILIXIUM_NOT_IN_HISTORY_WINDOWrather than a terminal failure.cargo buildfull workspace,fmtandclippyall clean.Requires the Hyperswitch-side PR juspay/hyperswitch#13708, which registers Ilixium for UCS routing and fixes
connector_order_idbeing hardcoded toNoneon the refund path (without it, Ilixium refunds are impossible from Hyperswitch).