# Integration Checklist

Read first: [QR Code Loyalty Guide](04-qr-code-loyalty-guide.md) · [Phone Number Guide](05-phone-number-guide.md) · [Kiosk and POS Configuration](06-kiosk-and-pos-configuration.md) · [Message & Error Codes](08-message-and-error-codes.md)

This is the normative go-live gate for an **In-Store Loyalty API** integration. Every item is objectively verifiable — you either did it or you did not — and LoyaltyPlant runs an equivalent checklist during certification. Work through it against the latest protocol version, **1.7**.

Items marked **(conditional)** apply only if you implement that capability; skip them otherwise and note the skip in your test report.

## 1. Contract conformance

- [ ] All requests target versioned paths for the version you certify against — `/one-time-salt/1.7`, `/qr-code/1.7`, `/order-closed/1.7`, `/refund/1.7`.
- [ ] All requests are HTTP `POST` with `Content-Type: application/json`.
- [ ] Request and response payloads validate against the schemas in [cloudvalidator-api.yaml](/in-store/api/) — field types, required fields, and enum values match (the spec lint passes).
- [ ] `establishmentId` is configurable per outlet on the POS side, not hard-coded.

## 2. Authentication

- [ ] A fresh `one-time-salt` is fetched immediately before **each** authenticated call (`qr-code`, `order-closed`, `refund`) — no salt pooling or caching.
- [ ] `AuthorizationToken` is computed as `SHA256(apiPrivateKey + salt)` in lowercase hex; the key comes first and the request body is not part of the hash.
- [ ] `SaltId` and `AuthorizationToken` headers are sent on every authenticated call; `one-time-salt` itself is sent with no auth headers.
- [ ] A salt is never reused; a reused or unknown `SaltId` returns HTTP 403 `{"message":"Token expired."}` and is handled as an auth error, not a loyalty outcome.
- [ ] HTTP 403 responses are handled distinctly from business outcomes (recompute token with a fresh salt; surface a clear operator error).
- [ ] A request with a wrong/unknown `apiPrivateKey` is verified to fail with HTTP 403 (and surfaces a clear "LP communication error").

## 3. QR accrual flow

Points are only credited after `order-closed`; the scan itself just pre-validates. The customer scans their QR code at the self-service kiosk, or the cashier scans the customer's QR code at the cash register / POS terminal. Verify the accrual path end to end, including the cases where the customer changes the order after scanning.

- [ ] A QR code (or 5-character short code) read from the LoyaltyPlant app — scanned by the customer at the kiosk, or by the cashier at the cash register / POS terminal — is sent in the `qrCode` field of `qr-code`.
- [ ] `orderId` and the full `posInfo` (`establishmentId`, `terminalId`, `terminalCurrentTimestamp`, `terminalTimeZone`) are sent with every `qr-code` request.
- [ ] The bonus card can be read before, during, and after items are added to the order, with correct results in each case.
- [ ] Scanning the same card twice into one order credits points only once.
- [ ] A repeated identical QR string is rejected (`504`/`505`), and a non-LoyaltyPlant QR is rejected (`503`); the returned message is displayed in both cases.
- [ ] If all items are removed after a scan, the order closes as a valid zero/empty transaction without error.
- [ ] Loss of connectivity during scan or closure is handled gracefully (operator sees a clear "no connection" state; the order is preserved for retry when connectivity returns), including the case where the POS app is closed and reopened mid-order.
- [ ] An anonymous app user (empty profile name/email) scans and accrues points correctly.

## 4. QR redemption flow

A reward is a `menuItems` line returned with a 100% discount; redemption only settles correctly if that exact item and discount are echoed back at closure (the "reward echo" rule). Redemption is triggered the same way as accrual — the customer scans their QR code at the self-service kiosk, or the cashier scans the customer's QR code at the cash register / POS terminal. Verify both the redeem and the change-of-mind paths.

- [ ] Returned `menuItems` (reward items) and their `modifiers` are added to the order.
- [ ] Item-level and order-level `discounts` (`type` = `percent` / `amount` / `fixedPrice`, with `discountId` and `value`) are applied.
- [ ] **Reward echo:** each reward item and its accompanying 100%-discount (the `discountId` LoyaltyPlant returned) is echoed back, unchanged, in the `order-closed` order contents.
- [ ] If a returned `payment` is present, it is registered on the order and its `transactionId` is stored for the closure.
- [ ] Removing a reward before closure is handled: the reward is omitted from `order-closed` and LoyaltyPlant returns the reward (verified the points come back).
- [ ] Two rewards in one order, and rewards for two different customers in one order, settle correctly.

## 5. Phone-number flow (conditional)

This is the phone number-based flow. The customer enters their number on the self-service kiosk, or the cashier enters it at the POS / cash register.

- [ ] After the payment screen, the customer can enter a phone number on the self-service kiosk — or the cashier can enter it at the POS / cash register; `qr-code` is sent with `phoneNumber` (and optional `clientName`, `subscribedToPushNotifications`) **and no `qrCode`**.
- [ ] The phone-entry screen — on the kiosk and on the POS / cash register alike — is configurable: phone mask (non-mandatory, leading `+` non-deletable), marketing texts/banners per language, and legal-consent section.
- [ ] `509` (wrong phone format) and `510` (flow disabled) are handled — re-prompt or fall back to QR-only — whether the number was entered by the customer at the kiosk or by the cashier at the POS / cash register.

## 6. order-closed correctness

This is the request that credits points and settles rewards, so its contents and exactly-once delivery are the highest-risk items in the integration.

- [ ] `order-closed` carries the complete final order — all paid items, all reward items (with their discount), all order-level discounts, all `payments`, and `waiter`.
- [ ] `orderTotal` is reported **without** subtracting the points payment (the points payment is listed in `payments`); only reward-item value is netted out.
- [ ] For a LoyaltyPlant payment, `payments[i].transactionId` equals the `transactionId` from the `qr-code` response, and every points-payment transaction ID is unique.
- [ ] `order-closed` is sent **exactly once** per order; a duplicate is detected and not resent. A repeated `order-closed` is **not** a `506`/`ORDER_ALREADY_CLOSED` (that is the `processQrCode` scan-time code) — it returns HTTP 200 with `accepted: false` and `errorCode: 0` ("document not found or not in open state"), which on a retry confirms the first closure landed; branch on `accepted`.
- [ ] The closing call is guaranteed in every code path; an order closed without it auto-annuls (validated-but-unused rewards return automatically after about a day).
- [ ] The `order-closed` response is processed by branching on `accepted` (the authoritative success signal — **not** `errorCode`, which is `0` on both accepted and rejected closures): read `pointsReward`/`pointsSpent` on success, and `errorText` for the rejection reason when `accepted: false`.

## 7. Refunds

- [ ] On a full order cancellation/refund, `refund` is called with `establishmentId` and `orderId`; LoyaltyPlant returns used rewards and writes off accrued points.
- [ ] Partial refunds are not attempted — only the entire order can be cancelled.
- [ ] The `refund` response is processed by branching on `accepted` (the authoritative success signal — **not** `errorCode`, which is `0` on both accepted and rejected refunds); when `accepted: false`, the reason is in `errorText`.

## 8. Messages & errors

- [ ] Every `messages[]` entry from `qr-code` is displayed to the cashier / on the kiosk screen — none are swallowed.
- [ ] HTTP 200 is never treated as unconditional success: a rejection is detected by `accepted: false` on `qr-code`, `order-closed`, and `refund` alike — **not** by `errorCode`, which stays `0` on both accepted and rejected `order-closed`/`refund` (per the golden rule in [Message & Error Codes](08-message-and-error-codes.md)).
- [ ] Per-code handling follows the [Message & Error Codes](08-message-and-error-codes.md) recommendations (retry vs. show vs. escalate).
- [ ] A wrong/non-existent `establishmentId` is handled (operation fails cleanly with a surfaced error, no crash).

## 9. Configuration (conditional — POS/kiosk config fetch)

- [ ] If the integration fetches outlet configuration from LoyaltyPlant, it reads `apiUrl`, `apiVersion`, `establishmentId`, `password`, `discountId`, and `flow` from the config response and applies them per outlet.
- [ ] The sales-outlet identifier and the 100%-discount ID used for rewards are configured to match the values registered with LoyaltyPlant.
- [ ] The outlet is verified to appear correctly in the LP CRM after a successful scan and closure.

## 10. Onboarding & certification

LoyaltyPlant verifies the integration before it goes live; this section mirrors that verification process so there are no surprises at the gate.

- [ ] LoyaltyPlant is notified about **a week ahead** of the planned completion date, so the verification slot and test plan can be scheduled; if the date slips, a new one is agreed.
- [ ] The certification test cases are run on your side and a **test report** is produced against the LoyaltyPlant test plan.
- [ ] Access to your test environment is provided to LoyaltyPlant, which verifies the integration against its test plan; flagged cases are fixed and re-verified until all pass.
- [ ] Once every case passes, LoyaltyPlant considers the integration complete and ready for deployment.
- [ ] The **first deployment** to a POS terminal happens with LoyaltyPlant representatives present, or is recorded on video and shared with LoyaltyPlant.

> **Note:** Your LoyaltyPlant integration manager schedules the certification run and shares the test plan, test app, test LP CRM access, and certification test cases at kickoff.

---

← Back to [Overview](01-overview-and-concepts.md)
