BTePOS: record StarBT loyalty as a LOY payment-method transaction - #127
Open
rmcotton-sap wants to merge 9 commits into
Open
BTePOS: record StarBT loyalty as a LOY payment-method transaction#127rmcotton-sap wants to merge 9 commits into
rmcotton-sap wants to merge 9 commits into
Conversation
When an order is paid partly with BT StarBT loyalty points, the points portion
settles outside the card rails and is invisible to OPF. The collection now
records it as a second OPF transaction during authorization verification, so the
loyalty amount appears alongside the card payment.
Uses POST /merchant/transactions in its ExternalAuthorizationTransactionRequest
form: accountGroupId plus paymentMethodCode, keyed to a LOY alternative payment
method. This replaces the earlier AccountPaymentTransactionRequest shape, which
recorded the loyalty leg against a separate ACCOUNT_PAYMENT integration. The
external form keeps both legs on one integration and lets the transaction carry
a real payment method rather than a generic account reference.
Verified against the live API: the payload returns 201 and the transaction is
stored with paymentMethod and paymentMethodCode both reading LOY.
Three values that were hardcoded in the source configuration are now variables,
since none of them travel to another tenant:
- the OPF host, previously a literal tenant URL, now {{rootUrl}}/{{service}}
- the currency, previously fixed to RON, now follows the order currency
(restoring the intent of commit 7260ae8)
- the target, previously a fixed account id, now loyaltyAccountGroupId plus
loyaltyPaymentMethodCode
Steps 2 and 3 are gated by the hasLoyalties condition, so an order with no
loyalty component skips both extra calls and behaves exactly as before.
The README gains a step to create the LOY APM in the workbench before importing,
and documents that the write-back needs an opf-txn-mgt client with the resource
parameter appended to the token URL, because OPF's OAuth2 configuration silently
drops that field.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Without the association OPF echoes the raw code back as the payment method; with it the transaction resolves the APM display name (Loyalty Points), which is what the storefront and back office show. Verified live on both sides of the change. Note the batch endpoint returns 207 with per-item statuses, so the outer response code alone does not confirm success. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…POS order id Adds a fourth verify step calling /merchant/transactions-tags-batch, so the loyalty transaction carries CART_REF and ORDER_REF and is linked to the same cart and order as the card payment. Without it the transaction is orphaned: it does not appear in a tagName/tagValue query and reconciliation that walks tags misses it. Tags cannot be set on the create call -- both a tags array and matching customFields are accepted and silently ignored, verified against the live API. Also derives orderPaymentId as LOY- prefixed onto the card leg's payment order id rather than reading a custom field, and documents the distinction between the two identifiers. orderPaymentId is OPF's own id; pspReference must remain the BTePOS order id because deposit.do maps its orderId from pspReference. Keying it to the OPF-side value makes capture fail with errorCode 6, No such order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…derPaymentId
Two defects found by inspecting the live mapping.
orderPaymentId rendered empty. LOY-${input.orderId} is a composite expression, so
OPF only evaluates it with isScript true; it was false, so FreeMarker never ran it.
Same fix applied to the tag call, which builds the same composite.
accountGroupId came from a variable rather than the transaction it belongs to. A
chained GET now runs before the create and recovers it:
GET /merchant/transactions?orderPaymentId=${input.orderId}&expand=accountGroup
-> value[0].accountGroup.id -> custom field, persist false
POST /merchant/transactions
$.accountGroupId <- ${input.customFields.loyAccountGroupId}
The plain transaction response omits accountGroupId, which is why this looked
impossible earlier; expand=accountGroup supplies it and can be set in the mapping
URL. Verified against a live order returning accountGroup.id 3883.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d amount Both legs now use the order's own payment id rather than a LOY- prefixed variant. OPF holds two authorizations against it, each with its own transaction id and pspReference, and each is captured or refunded by passing its authorizationId. Also maps paymentAmountInfo.approvedAmount to authorizationAmountInExponent on the card verify response. When part of the basket is paid with loyalty points BTePOS pre-authorizes only the remainder on the card, but OPF was keeping the full order total, so the loyalty amount was counted twice and capture failed with errorCode 8, deposited amount is greater then registered amount. The ORDER_REF tag is dropped. Commerce writes it when the order is placed, which is after authorization verification runs, so it is not available at write-back time and was being written empty. CART_REF is set and is what links the legs. Verified end to end on a live split payment of 537.99 EUR: AUTHORIZATION card 522.99 + loyalty 15.00 = 537.99 SETTLEMENT card 522.99 + loyalty 15.00 = 537.99 REFUND card 522.99 + loyalty 15.00 = 537.99 Captured loyalty then card, refunded card then loyalty, each by authorizationId. Every BTePOS call returned errorCode 0 and each leg routed to its own order id. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The write-back reads the account group from the card authorization it is recording alongside, via the chained GET on /merchant/transactions with expand=accountGroup, so configuring it separately was redundant and risked drifting from the value actually in use. Removed from the environment template and the README. The collection now needs no tenant-specific account group configuration for the loyalty leg. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The numbered list omitted the chained GET that reads the account group, and the gating note said steps 2 and 3 when four calls are gated by hasLoyalties. Also removes a paragraph duplicated during earlier edits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…alues
The loyalty write-back URLs and the payment method code were written as
{{rootUrl}}/{{service}} and {{loyaltyPaymentMethodCode}}. Those are Postman
variables, substituted at import time, so the literal tenant URL and code are
baked into the stored OPF mapping. They now use ${vars.opfHost} and
${vars.loyaltyPaymentMethodCode}, which OPF resolves at runtime, matching the
configuration the end-to-end tests were run against and keeping an exported
config portable between tenants.
This also fixes a real gap: the Variable folder created only apiDomain, so
neither opfHost nor loyaltyPaymentMethodCode existed on the account. Both are now
created there, and opfHost is added to the environment template.
Postman variables remain correct for the collection's own request URLs, which is
Postman calling OPF. The distinction is between where the collection posts and
what OPF stores.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both legs share the order's payment ID, so the loyalty transaction already carries the same CART_REF and ORDER_REF tags as the card payment. The explicit call to transactions-tags-batch was left over from the earlier design where the loyalty leg had its own prefixed payment ID and was therefore orphaned from the cart. The chain is now four calls, three of them gated by hasLoyalties. 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.
What
Records the BT StarBT loyalty portion of a split payment as its own OPF transaction, so the points leg appears alongside the card payment instead of being invisible to OPF.
The change
Uses
POST /merchant/transactionsin itsExternalAuthorizationTransactionRequestform —accountGroupIdpluspaymentMethodCode, keyed to a LOY alternative payment method.This replaces an earlier
AccountPaymentTransactionRequestshape that recorded the loyalty leg against a separateACCOUNT_PAYMENTintegration. The external form keeps both legs on one integration and lets the transaction carry a real payment method rather than a generic account reference.Verified against the live API — the payload returns
201and the transaction is stored withpaymentMethodandpaymentMethodCodeboth readingLOY:{"transactionType":"AUTHORIZATION","amount":5.0,"currency":"RON", "status":"ACCEPTED","paymentMethod":"LOY","paymentMethodCode":"LOY"}How it works
The write-back runs during authorization verification:
getOrderStatusExtended.dofor the card payment (always runs)getOrderStatusExtended.dofor the loyalty order — only when the order has onePOST {{rootUrl}}/{{service}}/merchant/transactionswithpaymentMethodCode: LOYSteps 2 and 3 are gated by the
hasLoyaltiescondition, so an order with no loyalty component skips both extra calls and behaves exactly as before.Three values de-hardcoded
None of these would travel to another tenant:
{{rootUrl}}/{{service}}RON7260ae8{{loyaltyAccountGroupId}}+{{loyaltyPaymentMethodCode}}README
Adds a step to create the LOY APM in the workbench before importing (code, name, type, capture pattern, refund/recurring support), and documents the OAuth2 requirement: the write-back calls the OPF API, so it needs an
opf-txn-mgtclient, and since OPF's OAuth2 configuration silently drops aresourcefield, that parameter has to be appended to the token URL instead.Import order is called out too — Authentications and Mapping condition expressions must run before Authorization, as they create the auth and condition whose generated IDs the authorization mapping references.
Notes for reviewers
POST /merchant/transactionsendpoint returns403for anopf-int-mgtprovisioning token — this is expected, it requires the transaction-management scope.🤖 Generated with Claude Code