Skip to content

feat(paya): Level 3 line-item data for Paya commercial-card transactions - #1

Open
dnplkndll wants to merge 1 commit into
mainfrom
feat/paya-payment-app
Open

feat(paya): Level 3 line-item data for Paya commercial-card transactions#1
dnplkndll wants to merge 1 commit into
mainfrom
feat/paya-payment-app

Conversation

@dnplkndll

Copy link
Copy Markdown

What changed

Scaffolds apps/paya, a Saleor Payment App for the Paya / Sage Payments Bankcard API. Level 3 is implemented first because it is the reason to build the app at all.

Level 3 line-item data

Paya attaches a default Level 3 record to commercial-card transactions, which qualifies partially and no further. Full line-item detail must be POSTed to /v2/transactionlevel3s, and that is what earns the interchange reduction of roughly 0.5–1.5% on a commercial card. On a ~$2,250 average order that is $11–34 per order. The WooCommerce gateway the legacy DuroPC store used never made that call, so the old store qualified on Paya's defaults only.

Visa retired standalone Level 2 interchange on 18 April 2026 under its Commercial Enhanced Data Program, so on Visa it is now full Level 3 or no discount at all. Mastercard still recognises both.

Three behaviours are encoded because they are easy to get wrong and expensive to discover late:

  • Commodity code is always emitted. Visa requires it per item, and a missing one fails the line rather than degrading it. Unclassified products get the UNSPSC "unable to classify" placeholder.
  • replaceLevel3Data, not update. Each POST overwrites the previous Level 3 data rather than merging, so the payload is always complete.
  • Tax rates are derived from the amounts, not supplied alongside them, because an independently supplied rate can disagree after rounding and the processor rejects the record.

Enablement check

isLevel3EnabledForLocation() runs the check Paya documents. This matters before trusting any of the above: a correct integration on a location the processor has not enabled qualifies for nothing, and eligibility varies by backend processor (TSYS, FirstData, Vantiv).

Provenance

No code is derived from the legacy WooCommerce gateway plugin. It is GPL v3, this repository is BSD-3, and the licences are incompatible. The plugin was read only to establish that the merchant account uses the v2 REST API at api.sagepayments.com rather than the older eftbankcard.dll endpoint. Writing fresh from the public API docs is also what keeps an upstream contribution to saleor/apps possible.

Tests

7 unit tests passing over the Level 3 mapping.

Not in this PR

The six payment webhooks have directories but no handlers: PAYMENT_GATEWAY_INITIALIZE_SESSION, TRANSACTION_INITIALIZE_SESSION, TRANSACTION_PROCESS_SESSION, TRANSACTION_CHARGE_REQUESTED, TRANSACTION_REFUND_REQUESTED, TRANSACTION_CANCELATION_REQUESTED. There is also no manifest or register route yet, so the app cannot be installed into Saleor from this branch.

Note that the Saleor payment-apps documentation page lists only three of those six events; the first-party Stripe app implements all six, which is the contract this app will follow.

Blocked on two external answers: whether the merchant location is enabled for Level 3, and credentials for the certification environment so authorise/capture can be exercised against the sandbox rather than written blind.

https://claude.ai/code/session_016SKxntFG3G68SS5jQDnPpf

Scaffolds a Saleor Payment App for the Paya / Sage Payments Bankcard API. The
reason for building it rather than reusing an existing gateway app is Level 3.

Paya attaches a default Level 3 record to commercial-card transactions, which
qualifies partially and no further. Full line-item detail has to be POSTed to
/v2/transactionlevel3s, and that is what earns the interchange reduction of
roughly 0.5-1.5% on a commercial card. On a ~$2,250 average order that is
$11-34 per order. The WooCommerce gateway the legacy DuroPC store used never
made that call, so the old store qualified on Paya's defaults only.

Visa retired standalone Level 2 interchange on 18 April 2026 under its
Commercial Enhanced Data Program, so on Visa it is now full Level 3 or no
discount. The mapping therefore always emits the Visa-required order-level tax
rate and per-item commodity code rather than treating them as optional; a
missing commodity code fails the line rather than degrading it, so unclassified
products get the UNSPSC 'unable to classify' placeholder.

Two behaviours are encoded because they are easy to get wrong and expensive to
discover late. Each POST to /v2/transactionlevel3s overwrites the previous
Level 3 data rather than merging, so the client method is named replace and
always sends the complete set. And tax rates are derived from the amounts
rather than passed alongside them, because a rate supplied independently can
disagree with the amounts after rounding and the processor rejects the record.

Includes isLevel3EnabledForLocation(), which performs the check Paya documents
for whether a merchant location is actually enabled. This matters before
trusting any of the above: a correct integration on a location the processor
has not enabled qualifies for nothing, and eligibility varies by backend
processor.

No code is derived from the legacy WooCommerce plugin. It is GPL v3, this
repository is BSD-3, and the licences are incompatible; it was read only to
establish that the account uses the v2 REST API rather than the older
eftbankcard.dll endpoint. Writing fresh from the public API docs is also what
keeps an upstream contribution possible.

The six payment webhooks are not yet wired to authorise/capture calls.

Claude-Session: https://claude.ai/code/session_016SKxntFG3G68SS5jQDnPpf
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant