# WhatsApp Order Agent — Magento-Side Answers

Response to *WhatsApp Order Agent — Bot Side Logic* (`magento_order.py`, 2026-08-04).
Answers §9's open questions, in order, plus one new endpoint added as a result.

**Audience:** Bot developer (human or Claude) working on `magento_order.py`
**Magento code:** `app/code/RazerPay/PaymentAgent`, `app/code/RazerPay/Payment`
**Last updated:** 2026-08-04

---

## 1. `channel_code` survival on `PUT /V1/carts/{cartId}/selected-payment-method` — **confirmed working, not guest-only**

This was never a guest-vs-customer distinction. Traced the exact call chain in Magento core:

- `PaymentMethodManagementInterface::set()` (`vendor/magento/module-quote/Model/PaymentMethodManagement.php:52-85`) — the service behind your `PUT selected-payment-method` call — does `$quote->getPayment()->importData($paymentData)` at line 78.
- `Quote\Payment::importData()` (`vendor/magento/module-quote/Model/Quote/Payment.php:170-203`) calls `$method->assignData($data)` at line 197.
- `AbstractMethod::assignData()` (`vendor/magento/module-payment/Model/Method/AbstractMethod.php:747-767`) dispatches event `payment_method_assign_data_{methodCode}` with the full data object (including `additional_data`).
- This is the **exact same code path** the guest `payment-information` route uses internally (`QuoteManagement::placeOrderRun()` also calls `$quote->getPayment()->importData($data)` — `vendor/magento/module-quote/Model/QuoteManagement.php:439`).
- `RazerPay_PaymentAgent/etc/events.xml` binds `payment_method_assign_data_razerpay_payment_agent` to `RazerPay\Payment\Observer\DataAssignObserver`, which reads `additional_data['channel_code']` generically (not method-specific) and calls `$paymentModel->setAdditionalInformation('channel_code', ...)`.

So: **`channel_code` reaches the order's payment `additional_information` identically regardless of which REST route sets it.** No fallback needed, question 2 is moot.

One thing to double check on your end once you run a real Fiuu transaction: `additional_information` on quote payment is copied to order payment automatically by Magento's quote→order conversion (`Magento\Quote\Model\Quote\Payment\ToOrderPayment`) — this is generic core behavior, not something this module touches, so it should carry through, but it's worth confirming on the first real `place` since it's the one link in the chain not directly re-traced here.

---

## 2. Fallback `payment-information` route for admin tokens — doesn't exist, not needed

Checked `vendor/magento/module-checkout/etc/webapi.xml`: there is no `/V1/carts/:cartId/payment-information` route. Only `/V1/guest-carts/:cartId/payment-information` (anonymous) and `/V1/carts/mine/payment-information` (customer-token `self` scope) exist. Your two-step `selected-payment-method` + `order` approach (§3, steps 7 & 9) is the only core-supported way to do this with an admin/integration token against an arbitrary customer's cart — not a workaround, it's correct.

---

## 3. Channel codes endpoint — added

```
GET /V1/razerpay-payment-agent/channels
Authorization: Bearer <integration token>   (same RazerPay_PaymentAgent::payment_link resource, no new grant needed)
```

Response:
```json
[
  {"code": "fpx_mb2u", "title": "FPX Maybank2u", "category": "banking"},
  {"code": "RPP_DuitNowQR", "title": "DuitNow QR", "category": "qr"},
  {"code": "TNG-EWALLET", "title": "Touch 'n Go eWallet", "category": "ewallet"}
]
```
`code` is the exact string to send as `additional_data.channel_code` — it's literally the value Magento forwards to Fiuu's `channel` parameter unmodified, so use it as-is (case included).

**This also surfaces a real bug risk in your current allowlist**: the store's configured channels are a mix of cases — most FPX bank codes are **lowercase** (`fpx_mb2u`, `fpx_cimbclicks`, ...), while e-wallets/cards are mixed case (`BOOST`, `RPP_DuitNowQR`, `TNG-EWALLET`, `GrabPay`, `AlipayPlus`, `ShopeePay`, `WeChatPay`, `creditAN`, `FPX_B2B*`). If your hand-maintained config used `FPX_MB2U` (as in the original integration doc's example — that example was wrong, sorry) it will NOT match what's configured, and Fiuu will likely reject or silently misroute the channel. **Recommend switching your allowlist to call this endpoint at startup/periodically instead of a hardcoded list**, exactly as you requested.

---

## 4. Pending payment expiry window

Checked `core_config_data` directly: `payment/razerpay_payment/pending_payment_expiry_hours` is **not set** (no override), so it falls back to the code default in `RazerPay\Payment\Gateway\Config\Config::getPendingPaymentExpiryHours()`: **24 hours**. This value is shared between `razerpay_payment` and `razerpay_payment_agent` (by design, per the shared-config decision) — there's no separate expiry for agent-created orders. If reps need a different window for WhatsApp-originated orders specifically, that's a new feature (separate config field), not currently built.

---

## 5. Behavioral differences: logged-in customer vs guest, anywhere in the payment/link module

**None found.** Checked every class in the payment-link path:
- `HostedParamsService::buildForOrder()` reads only order/billing-address fields (name, email, phone, amount, currency) — doesn't touch customer/guest status.
- `Callback.php` / `Notify.php` / `PaymentDomain` — order lookup is by increment id only, no customer-awareness anywhere.
- `AssignOrderProtectCode` observer, `DeferPendingPaymentOrderEmail` plugin, `CancelExpiredPendingPaymentOrders` cron — all gate on payment **method code**, never on `customer_id`/guest flag.

The hosted Fiuu page prefill (name/email/phone) comes from the order's **billing address**, which your bot sets explicitly in `shipping-information` (§3 step 5) — so it reflects whatever address data you send, not the customer's saved profile beyond what you copied into that call.

---

## 6. Fiuu callback idempotency — mostly yes, with one known pre-existing gap worth flagging now

For a **second** callback/notify arriving after the first has already fully completed and committed: yes, idempotent. `PaymentDomain::handleSuccessPaymentResponse()` checks `$salesOrderPayment->getLastTransId()` first and throws `SalesOrderPaymentTransactionExistedException` if already set (caught and logged, no duplicate invoice). Refund success similarly checks `isRefundAlreadyProcessed()`.

**Fixed (2026-08-04).** `PaymentDomain::handleSuccessPaymentResponse()` previously used a check-then-set cache flag (`razerpay_payment_processing_{incrementId}`) that had a race window: two near-simultaneous webhooks (Notify + Callback for the same order) could both pass the check before either wrote the flag, creating two invoices. Replaced with `Magento\Framework\Lock\LockManagerInterface` (DB-backed `GET_LOCK`/`RELEASE_LOCK`, confirmed active — `app/etc/env.php` has `lock.provider = db`) wrapping the whole success-handling block, plus a fresh order reload from the repository after acquiring the lock (so the second request sees the first request's committed state, not a stale in-memory copy). This is atomic across processes now, not just within one PHP worker. No API/behavior change visible to the bot — still throws the same `SalesOrderPaymentHandledException`/`SalesOrderPaymentTransactionExistedException` your `Callback.php`/`Notify.php` handlers already catch.

---

## 7. Sandbox / test channel — **none currently configured, this store is live production**

Checked `core_config_data` directly:
```
payment/razerpay_payment/account_type = production
payment/razerpay_payment/merchant_id = liveprawn
```
There is no sandbox account configured anywhere in this install. **Testing the Fiuu path end-to-end right now would move real money** through the live `liveprawn` merchant account. To test safely you'd need either:
- A separate Fiuu **sandbox** merchant account (different `merchant_id`/`verify_key`/`secret_key` from Fiuu, requested from their portal/rep) temporarily swapped into the shared `Razer Merchant Services` config with `account_type = sandbox`, or
- Test with the smallest real amount and immediately refund, accepting it's real money.

This is the same shared config both `razerpay_payment` and `razerpay_payment_agent` read from — flipping to sandbox for testing affects the storefront checkout too, so coordinate the timing if the site has live storefront traffic.

---

## Summary of what changed on the Magento side as a result of this doc

- Added `GET /V1/razerpay-payment-agent/channels` (§3) — same ACL resource as the payment-link endpoint, no new integration grant needed.
- No changes needed for the customer-cart-vs-guest-cart divergence — already fully compatible, confirmed by code trace rather than assumption.
- Flagged one pre-existing, unfixed race-condition risk (§6) for a decision — not blocking, but relevant now that there's no bot-side polling to catch a missed/duplicate webhook.
- Flagged that there is no sandbox environment available for testing (§7) — needs a decision before a real end-to-end Fiuu test run.
