Skip to content

BTePOS: record StarBT loyalty as a LOY payment-method transaction - #127

Open
rmcotton-sap wants to merge 9 commits into
SAP-samples:mainfrom
rmcotton-sap:feature/btepos-loyalty-external-txn
Open

BTePOS: record StarBT loyalty as a LOY payment-method transaction#127
rmcotton-sap wants to merge 9 commits into
SAP-samples:mainfrom
rmcotton-sap:feature/btepos-loyalty-external-txn

Conversation

@rmcotton-sap

Copy link
Copy Markdown
Contributor

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/transactions in its ExternalAuthorizationTransactionRequest form — accountGroupId plus paymentMethodCode, keyed to a LOY alternative payment method.

This replaces an earlier AccountPaymentTransactionRequest shape that 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:

{"transactionType":"AUTHORIZATION","amount":5.0,"currency":"RON",
 "status":"ACCEPTED","paymentMethod":"LOY","paymentMethodCode":"LOY"}

How it works

The write-back runs during authorization verification:

  1. getOrderStatusExtended.do for the card payment (always runs)
  2. getOrderStatusExtended.do for the loyalty order — only when the order has one
  3. POST {{rootUrl}}/{{service}}/merchant/transactions with paymentMethodCode: LOY

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.

Three values de-hardcoded

None of these would travel to another tenant:

Was Now
literal tenant URL {{rootUrl}}/{{service}}
currency fixed to RON follows the order currency — restores the intent of commit 7260ae8
fixed account id {{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-mgt client, and since OPF's OAuth2 configuration silently drops a resource field, 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

  • No secrets or tenant-specific values committed; sample ids replaced with placeholders.
  • The POST /merchant/transactions endpoint returns 403 for an opf-int-mgt provisioning token — this is expected, it requires the transaction-management scope.

🤖 Generated with Claude Code

rmcotton-sap and others added 9 commits September 7, 2026 12:53
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>
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