# WeChat Pay Native Integration: Context File (5 Oct 2026)

Branch: `wechat-my-fix` (repo: `pr-10-folder`)
Purpose: one place that explains what is DONE, what we FOUND, and what we will DO NEXT.
Last updated: 5 Oct 2026 (after the first real call to WeChat Pay Global).

---

## 0. Where we are right now (read this first)

- Step 1 (switch to the Global API) is coded and compiles. It is NOT committed yet.
- Step 2 (`query_order`) is coded and compiles, with unit tests. NOT committed, NOT tested yet:
  the user will test it and ask for the commit commands later (see section 11).
- The client has been asked for the APIv3 key and the AppID (email sent). We are waiting.
- We made the first REAL call to WeChat Pay Global. Our host, path, request format,
  private key and signing were accepted (the request got past authentication).
- WeChat then rejected it with: `商户号mch_id与appid不匹配` ("merchant ID and appid do not match").
- Likely cause (an INFERENCE, not confirmed): `2000002804173264` is probably not the API AppID.
  The onboarding screenshot labels it "Application ID" next to "Application Status: Passed", so it may
  be the application reference number. Other possibility: it IS the right AppID but is not bound
  to merchant ID 909018166 in the merchant platform.
- What the docs say (Preparation page, .../4012356375): the AppID is the Official Account AppID
  (Development > Basic Configuration), and must be bound to the merchant ID under "Dev Configuration"
  in the merchant platform. The docs do NOT say the AppID starts with `wx`; that was general
  knowledge, not from the doc.
- BLOCKER: we need to know which AppID is bound to merchant ID `909018166`, from someone with
  merchant platform access.
- Agreement: the onboarding screenshot (older) showed "Unsigned". It is now probably signed, because
  the API certificate and key (which the client generated in the platform) are now available and
  WeChat accepted our signature. Not proven. See section 9.

---

## 1. The goal

We are the **merchant side**. We do onboarding and want to accept **WeChat Pay Native Payment**
(the customer scans a QR shown by us).

### The flow we are building (same pattern as Alipay)

```
Device ──> CloudLayer ──> QR Middle Layer (this repo) ──> WeChat Pay
                                  │                          │
                                  │   1. request a QR (code_url)
                                  │<─────────────────────────┘
        QR shown on device <──────┘

Customer scans QR in WeChat and pays

WeChat ──(signed + encrypted notification)──> /api/wechat/notify_payment (this repo)
          this repo: verify -> decrypt -> update DB -> notify CloudLayer
CloudLayer ──(MQTT)──> Device shows "payment success"
```

Steps in words:
1. Device asks CloudLayer for a payment; CloudLayer calls the QR Middle Layer.
2. Middle Layer creates a transaction in the DB and asks WeChat for a QR (`code_url`).
3. The `code_url` goes back through CloudLayer to the device, which displays the QR.
4. Customer pays. WeChat calls our `notify_url`.
5. We verify the signature, decrypt, update the DB, and tell CloudLayer, which tells the device.

---

## 2. Documentation we are following (WeChat Pay GLOBAL docs)

| Topic | Page |
|---|---|
| Native Payment intro | https://pay.weixin.qq.com/doc/global/v3/en/4012356374 |
| Preparation (AppID, mchid, keys) | .../4012356375 |
| Development guidelines | .../4012356376 |
| API list | .../4012356377 |
| Order placement (create QR) | .../4012356478 |
| Payment notification | .../4012356489 |
| Refund notification | .../4012356486 |
| Signature verification | .../4012357149 |
| Quick Pay (NOT what we use: merchant scans the customer) | .../4012356358 and .../4012356443 |

Important: this is the **Global (cross-border)** API. The host is `https://apihk.mch.weixin.qq.com`
and the paths start with `/v3/global/`. It is NOT the mainland-China domestic API.

Global endpoints (from the docs):
- Create QR: `POST /v3/global/transactions/native`
- Query order: `GET /v3/global/transactions/outTradeNo/{out_trade_no}` (or `/id/{id}`)
- Close order: `POST /v3/global/transactions/out-trade-no/{out_trade_no}/close`
- Refund: `POST /v3/global/refunds`; query `/v3/global/refunds/out-refund-no/{no}`
- Certificates: `GET /v3/global/certificates`

---

## 3. Branches: what is where

- `wechat-my-fix` (current) and `wechat-simulator` were identical except for ONE commit on
  the simulator branch (`6b2ce82`).
- That commit had two kinds of content:
  - **Useful for production** (brought into `wechat-my-fix`, commit `0826aa6`).
  - **Dev-only simulator** (fake WeChat server, fake keys, `/sim/wechat` routes). NOT brought
    over. It still lives on `wechat-simulator`, useful later for testing query/polling/webhook
    without real money. If used, its paths would need the `/v3/global` prefix.

---

## 4. Merchant account facts (from the onboarding screen)

| Field | Value |
|---|---|
| Merchant ID (mchid) | `909018166` |
| Merchant name | MERCURY PAYMENTS SERVICES L.L.C |
| Settlement currency | AED (so `AED` in the code is correct) |
| "Application ID" on the screen | `2000002804173264`  <- label from the onboarding page; probably NOT the API AppID (unconfirmed) |
| Application status | Passed |
| Agreement signing | Showed **Unsigned** on an OLDER screenshot; likely signed by now (unconfirmed) |

The API AppID is still unconfirmed. Per the Preparation doc it is the Official Account AppID,
bound to the merchant ID in the merchant platform under "Dev Configuration".
A search of the repo, all git history, the DB columns and all of /var/www found no other record
of `2000002804173264` and no real AppID.

Do not use `wx2421b1c4370ec43b` or `wxdace645e0bc2c424` (found in git history): those are WeChat's
documentation sample values, not ours.

---

## 5. DONE so far

### Already in the code before today (commit `f5dc6ef`)
- Native order creation with a signed request (`wechat.ex`).
- Request signing, AEAD decryption, platform-certificate fetch + 12h cache,
  signature verification, 5-minute replay window (`wechat_pay_client.ex`).
- Payment webhook `/api/wechat/notify_payment`: verifies, decrypts, updates the transaction
  (`wechat_webhook_controller.ex`).

### Committed today (commit `0826aa6` on `wechat-my-fix`)
- `WeChat.close_order/1`: closes an unpaid order. Used automatically when a new transaction
  replaces an old one (in `qr_middle_layer_controller.ex`).
- `qr_middle_layer_controller`: the WeChat response now carries `qrCodeId` = the `code_url`,
  `paymentId`, and the store/brand/merchant details. `paymentId` is saved as
  `payment_reference_id` so cancel/close can find the order.
- Webhook forwards the result to the CloudLayer (`notify_cloud_layer`), best-effort.
- Configurable base URL: `:wechat_api_base_url`.
- `WeChatPayClient.merchant_serial_no/0`: uses `:wechat_merchant_serial_no`, or derives it from
  the cert file at `:wechat_merchant_cert_path`. (This function was previously only in the
  working tree, not committed; it is now committed.)
- Extra event-log fields on the payment request/notification.
- `config/runtime.exs`: env-var overrides (`WECHAT_APPID`, `WECHAT_MCHID`,
  `WECHAT_MERCHANT_SERIAL_NO`, `WECHAT_PRIVATE_KEY_PATH`, `WECHAT_MERCHANT_CERT_PATH`,
  `WECHAT_NOTIFY_URL`, `WECHAT_APIV3_KEY`).
- `config/config.exs`: `"WeChat Pay"` provider-name mapping to the WeChat module.
- `.gitignore`: `.env`.

### Step 1 code changes made AFTER the context file was first written (NOT committed yet)
- `wechat.ex`: default host is now `https://apihk.mch.weixin.qq.com`; create path is
  `/v3/global/transactions/native`; request now sends `trade_type: "NATIVE"` and
  `merchant_category_code`; close path is `/v3/global/transactions/out-trade-no/{no}/close`.
- `merchant_category_code` comes from `provider_params[:merchant_mcc]` (the merchant's
  `groups.mcc_code`). If it is missing, `generate/1` returns the error
  "missing merchant_mcc (merchant_category_code) for merchant".
- `wechat_pay_client.ex`: certificates path is now `/v3/global/certificates`; default host is apihk.
- `mix compile`: no errors (only pre-existing warnings; the `generate/2` behaviour warning
  already existed).

### Step 2 code (uncommitted): `query_order`
- `lib/da_product_app/qr_provider/wechat.ex`:
  - `query_order(out_trade_no, log_opts \\ [])`: signed `GET` to
    `/v3/global/transactions/out-trade-no/{no}?mchid=<mchid>` (the query string is part of the signed path).
    Returns `{:ok, %{trade_state:, status:, order:}}`, or `{:error, %{status_code:, body:}}` for WeChat
    errors (e.g. 404 `ORDER_NOT_EXIST`), or `{:error, reason}`.
  - `query_order_path/2` and `trade_status/1`: `SUCCESS` -> `:success`; `NOTPAY`/`USERPAYING` -> `:pending`;
    `CLOSED`/`REVOKED`/`PAYERROR` -> `:failed`; `REFUND` -> `:refunded`; anything else -> `:unknown`.
  - Logs "Status Enquiry to Provider" / "Status Response from Provider" events (failures swallowed).
- `test/da_product_app/qr_provider/wechat_test.exs`: 7 unit tests (mapping + path). Run with
  `mix test test/da_product_app/qr_provider/wechat_test.exs` (the `test` alias runs ecto.create/migrate first).
- Live check script (needs no AppID): `.../scratchpad/wechat_query_check.exs`. With no argument it
  queries a made-up order; expected `404 ORDER_NOT_EXIST` (proves path + signing + mchid). If it returns the
  "encryption key not set" SIGN_ERROR, the APIv3 key blocks every signed call.
- `mix compile`: no errors.
- Observation for step 3: the webhook's DB update already only touches rows in `pending`/`QR_GENERATED`
  (so it is naturally idempotent), but `notify_cloud_layer` fires on every duplicate notification.

### Step 3 code (uncommitted): idempotent settle, shared by webhook and (later) polling
- NEW `lib/da_product_app/qr_provider/wechat_payment.ex` (`DaProductApp.QRProviders.WeChatPayment`):
  - `settle(m_ref_num, trade_state, order)`: one conditional UPDATE (`WHERE m_ref_num = ? AND status IN
    ('pending','QR_GENERATED')`); notifies the cloud layer only if a row changed. Returns `{:ok, :settled}`,
    `{:ok, :already_settled}` (duplicate/cancelled/already final: nothing sent) or `{:ok, :ignored}`
    (NOTPAY, USERPAYING, REFUND, unknown).
  - `target_status/1`, `cloud_payload/4` (public, unit-tested). Currency now comes from WeChat's order
    (fallback `AED`) instead of being hardcoded.
  - The cloud URL still defaults to `http://demo.ctrmv.com:4004/api/payment/notify-success` (config hygiene item
    still open; `WECHAT_CLOUD_NOTIFY_URL` is not read by `runtime.exs` yet).
- `wechat_webhook_controller.ex`: now calls `WeChatPayment.settle/3`; its own `update_transaction_status` and
  `notify_cloud_layer` were removed. The webhook still always answers 200 SUCCESS after a valid notification.
- Tests: `test/da_product_app/qr_provider/wechat_payment_test.exs` (pure functions). The DB/atomicity behaviour
  is covered by a manual check only.
- `mix compile`: no errors, no new warnings in these files. NOT committed, NOT tested by the user yet.

### Step 3 correction made while writing step 4 (uncommitted)
- `settle/3` now notifies the cloud layer ONLY on `success`. Reason: the committed
  `payment_notification_controller.ex` (`/api/payment/notify-success`) has no status check and turns every call
  into a "money received" MQTT push to the device, so a failure notification would have shown as a payment.
  Polling makes failures reachable, so failures now only update the DB (same as Alipay today).
- The user's UNCOMMITTED local edit to that controller already ignores `status == "failed"`; we do not rely on it.

### Step 4 code (uncommitted): polling for WeChat
- NEW `lib/da_product_app/qr_provider/wechat_poller.ex` (`WeChatPoller`): `start/2` runs a background task;
  `run/3` is the loop (injectable functions so it is testable without DB/network).
  - Each attempt: stop if the transaction is missing, cancelled, or no longer open; else `WeChat.query_order/2`
    then `WeChatPayment.settle/3` (the same function the webhook uses, so no double settle/notify).
  - Keeps polling on NOTPAY/USERPAYING and on transient errors (e.g. 404 ORDER_NOT_EXIST, network); stops on
    401/403 (credentials problem) instead of hammering WeChat; survives a crash inside one check.
  - Window: 5s initial delay, 10s interval, 30 attempts (about 5 minutes). Configurable via
    `config :da_product_app, :wechat_poll, initial_delay_ms: .., interval_ms: .., max_attempts: ..`.
  - At the end of the window an unpaid order is left alone (still pending; the QR is still payable and the webhook
    still settles it). Alipay behaves the same way after its window. No auto-close/fail on timeout (decision pending).
- `wechat_payment.ex`: added `open_statuses/0` (used by the poller).
- `qr_middle_layer_controller.ex`: ONE 10-line block inserted just before the Alipay polling block: starts the
  poller when `provider_name == "WeChat Pay" && payment_id && match?({:ok, _}, result)`. Nothing else in that file
  changed vs HEAD. (Bug caught and fixed while writing it: chained `and` with a string would have raised
  BadBooleanError; uses `&&`.)
- Tests: `test/da_product_app/qr_provider/wechat_poller_test.exs` (10 tests, fakes for status/query/settle/sleep).
  All 22 WeChat unit tests (steps 2-4) pass, run without starting the app:
  `MIX_ENV=dev mix run --no-start -e 'ExUnit.start(autorun: false); for f <- [...], do: Code.require_file(f); ExUnit.run()'`.
- `mix compile`: no errors; no new warnings (the `iteration`/`tx` unused-variable warnings are in existing code).
- NOT committed, NOT tested live (needs a real order, so the APIv3 key and AppID).

### Credentials set-up done
- The private key and certificate were copied from the laptop to the server with `scp` and
  placed in `/etc/wechatpay/` (`apiclient_key.pem`, `apiclient_cert.pem`, `chmod 600`).
- Env vars used for the test: `WECHAT_APPID`, `WECHAT_MCHID`, `WECHAT_PRIVATE_KEY_PATH`,
  `WECHAT_MERCHANT_CERT_PATH` (the serial is derived from the cert), `WECHAT_NOTIFY_URL`.
- `.env` already holds `WECHAT_APPID` and `WECHAT_MCHID` (and has the serial/key path lines
  commented out). The app does not auto-load `.env`; load it with `set -a; source .env; set +a`.
  Exports made AFTER sourcing win over `.env`.

### The QR check script (does not touch the DB)
- File: `/tmp/claude-1030/-var-www-internaltesting-tejaswini-prverification-pr-10-folder/69af5392-7935-42d8-bc35-9bc8dc489c69/scratchpad/wechat_qr_check.exs`
- It sends one signed create-order request to WeChat and prints the raw response.
  Currency is optional in the script (omitted -> WeChat uses the settlement currency).
- Run: `MIX_ENV=dev mix run --no-start <script path>` with the env vars set.
- The scratchpad lives in /tmp; copy the script somewhere permanent if you want to keep it.

---

## 6. RESULT OF THE FIRST REAL CALL

Request (no secrets):
```
POST https://apihk.mch.weixin.qq.com/v3/global/transactions/native
{"amount":{"currency":"AED","total":1},"appid":"2000002804173264","description":"QR check",
 "mchid":"909018166","merchant_category_code":"5411",
 "notify_url":"https://demo.ctrmv.com/api/wechat/notify_payment",
 "out_trade_no":"chk...","trade_type":"NATIVE"}
```
Response: `HTTP 400  {"code":"INVALID_REQUEST","message":"商户号mch_id与appid不匹配"}`

What this proves:
- The Global host and path are correct and reachable.
- Authentication/signing was accepted (a bad key or serial would give a SIGN_ERROR / 401),
  so the `WECHATPAY2-SHA256-RSA2048` scheme appears to be accepted by the Global API.
- The error is only the appid/mchid pairing. Why the pair is rejected is not known yet
  (see section 0 and the question for the client in section 9).
- MCC `5411` and `AED` have NOT been validated yet (WeChat stopped at the appid check first).

### Second real call: certificates check (no AppID involved)
Script: `.../scratchpad/wechat_cert_check.exs` (read-only `GET /v3/global/certificates`).
```
GET https://apihk.mch.weixin.qq.com/v3/global/certificates
mchid=909018166 merchant_serial_no=490F1F01A416AC387CE41C7B2C5C698E03E8A7C4  (derived from apiclient_cert.pem)
HTTP 401  {"code":"SIGN_ERROR","message":"商户未设置加密的密钥，请登录商户平台操作！..."}
```
Translation: "The merchant has not set the encryption key. Please log in to the merchant platform."
Meaning: the **APIv3 key has not been set** on the merchant account (Account Settings > API Security).
- WeChat recognises mchid 909018166 (the message is specific to its settings).
- It does NOT prove the signature was verified (the key check may come first), so the earlier
  conclusion "signature accepted => key/cert pair proven" is withdrawn.
- No new information about the AppID.
- The APIv3 key is needed anyway for webhook decryption and platform certificates.
- Not yet tested: whether create-order needs the APIv3 key too (the 400 appid error came first).

---

## 7. PROBLEMS FOUND (code vs Global docs)

| Item | Global docs say | Status in code |
|---|---|---|
| Host | `apihk.mch.weixin.qq.com` | FIXED (uncommitted) |
| Create order path | `POST /v3/global/transactions/native` | FIXED (uncommitted) |
| Required create fields | `trade_type: "NATIVE"`, `merchant_category_code` | FIXED (uncommitted) |
| Close order path | `/v3/global/transactions/out-trade-no/{no}/close` | FIXED (uncommitted) |
| Certificates path | `/v3/global/certificates` | FIXED (uncommitted) |
| Query order | Query Order page (.../4012356555): `GET /v3/global/transactions/out-trade-no/{out_trade_no}?mchid=...` (the Development Guidelines summary said `outTradeNo`; the Query Order page and close-order use `out-trade-no`, which we used) | CODED (uncommitted); confirm the path in the live test |
| Refund | `POST /v3/global/refunds` (+ query by `out-refund-no`) | NOT implemented |
| Notifications | ONLY `SUCCESS` is ever sent; failed/closed payments send nothing | webhook failure branches never fire |
| Idempotency | notifications can repeat; handle duplicates | repeated SUCCESS re-notifies CloudLayer |

Other gaps:
- No polling (Alipay has it). Since failures never arrive by webhook, query + polling is REQUIRED.
- No explicit WeChat cancel endpoint (`/cancelPayment` only matches `"alipay"`).
- No tests for WeChat.
- Hardcoded fallbacks: `notify_url` (`http://40.120.104.51:4012/...`) in `wechat.ex` and the cloud URL
  (`http://demo.ctrmv.com:4004/...`) in the webhook. `WECHAT_CLOUD_NOTIFY_URL` is NOT yet in the
  `runtime.exs` list, so the cloud URL cannot be set from the environment yet.
- `notify_url` must be public HTTPS with no query parameters. The placeholder
  `https://demo.ctrmv.com/api/wechat/notify_payment` used in the test is unverified.
- The QR (`code_url`) is valid 2 hours.

---

## 8. PLAN (in order)

| # | Step | Status |
|---|---|---|
| 0 | Bring useful parts of simulator commit into `wechat-my-fix` | DONE (`0826aa6`) |
| 1 | Switch to Global endpoints, add `trade_type` and `merchant_category_code` | CODE DONE, uncommitted. Live check blocked on the real AppID |
| 1b | Get the real `wx...` AppID bound to mchid 909018166; sign the agreement; re-run the QR check until a `code_url` comes back | TODO (needs merchant platform login) |
| 2 | `query_order/2` with event logging; map `SUCCESS`, `NOTPAY`, `USERPAYING`, `CLOSED`, `REVOKED`, `PAYERROR`, `REFUND` | CODE DONE, uncommitted, awaiting the user's test |
| 3 | Idempotent "settle payment once" logic shared by webhook and polling (DB update only from pending; CloudLayer notify only when the update changed a row) and refactor the webhook to use it | CODE DONE, uncommitted, awaiting the user's test (reordered: polling depends on this) |
| 4 | Polling task after QR creation (like Alipay), using `query_order` and the shared settle logic; stop on success, terminal state, or cancel | CODE DONE, uncommitted, unit-tested (10 tests pass); live test needs the credentials. Polls ~5 min then leaves the order to the webhook |
| 5 | Explicit WeChat cancel/close endpoint | TODO |
| 6 | Refund: `/v3/global/refunds`, refund notification endpoint, refund query | TODO |
| 7 | Tests (signing, decrypt, status mapping, webhook) | TODO |
| 8 | Go-live test: env vars, public HTTPS `notify_url`, one small real order end to end | TODO |

Steps 2-7 do not need the real AppID to code. They can be tested with the simulator branch.
Commit each step separately so each can be reviewed.

---

## 8b. PENDING LIST (code audit done while waiting for the APIv3 key and AppID)

Found by reading the code, not from memory.

### Blocked until the client sends the APIv3 key + AppID
- First successful create-QR (`code_url`), and validation of MCC `5411`, currency `AED`, `notify_url`.
- Platform-certificate download and webhook signature verification + decryption (both need the APIv3 key).
- Live confirmation of the query-order path (`out-trade-no`) and `ORDER_NOT_EXIST` behaviour.
- End-to-end real payment test.

### Can be coded now (nothing blocks it)
1. **Idempotent settle** (step 3). Webhook DB update is already safe (only from pending), but `notify_cloud_layer`
   fires on every duplicate notification; polling would add a second source of the same event.
2. **Polling for WeChat** (step 4). `qr_middle_layer_controller.ex` ~line 283 only polls for `alipay` and `aani`;
   WeChat has none. Since WeChat only notifies on SUCCESS, failed/expired orders are otherwise never detected.
   Also decide what happens when the QR expires still `NOTPAY` (call `close_order`, mark failed).
3. **Cancel** (step 5). `/cancelPayment` only has clauses for `"provider" => "alipay"` (lines ~876/892). The
   auto-cancel on a new transaction already closes WeChat orders. Verify the stored `provider_name` really is
   `"WeChat Pay"` (the auto-cancel and the polling condition both rely on that exact string).
4. **Refund** (step 6). Nothing exists: no `WeChat.refund`, `/refundPayment` has only `"alipay"` clauses (~1013/1028),
   no refund-notification route (only `post "/wechat/notify_payment"` exists), no refund query.
5. **Webhook clean-up.** The `CLOSED`/`PAYERROR`/`REVOKED` branches never fire (WeChat only notifies SUCCESS);
   `notify_cloud_layer` hardcodes currency `"AED"` instead of using `decrypted["amount"]["currency"]`.
6. **Config hygiene.** `WECHAT_CLOUD_NOTIFY_URL` is not in the `runtime.exs` list, so the cloud URL falls back to
   the hardcoded `http://demo.ctrmv.com:4004/api/payment/notify-success`. `wechat.ex` falls back to a hardcoded
   `notify_url` (`http://40.120.104.51:4012/...`, not HTTPS); better to return an error if unset.
7. **QR lifetime.** No `time_expire` is sent; WeChat's default is a 2-hour QR. Decide whether to send a shorter
   expiry matching the device timeout.
8. **Tests.** Only the `query_order` unit tests exist. Missing: settle/idempotency, webhook (can reuse the simulator's
   signed-callback generator), refund.
9. **Simulator branch** (if used for testing) still uses `/v3/pay/...` paths; it would need the `/v3/global` prefix.

### Already fine (checked)
- Device-facing `/qr/status` is DB-based (reads the transaction status), so it works for WeChat once the DB is updated.
- `PaymentNotificationController` looks the transaction up by `payment_reference_id`; WeChat saves `paymentId = m_ref_num`
  as `payment_reference_id` and the cloud notify sends `payment_id = m_ref_num`, so they line up. Amount is sent as a
  minor-units string, which that controller expects.
- The raw-body plug that signature verification needs is wired in `endpoint.ex`.
- Amount is converted to the smallest unit before the provider call.

### Watch out
- `payment_notification_controller.ex` has UNCOMMITTED local changes (not ours). It is in the WeChat notify path;
  review them before relying on the end-to-end flow.
- Decisions still open: merchants with NULL `mcc_code` (406 of 710), and `config/dev.exs` DB name.

### Not needed for the first release (optional later)
Settlement query (`/v3/global/settle/settlements`), bill/reconciliation download (`/v3/global/statements`).

---

## 9. DECISIONS AND OPEN QUESTIONS

Answered:
- Common mode vs institutional mode: the repo and `.env` use a single appid + mchid and nothing
  mentions `sub_mchid`/`sp_mchid`, so it looks like common mode (direct merchant). Not 100%
  confirmed; confirm in the merchant platform.
- Merchant category code: not a fixed value. It comes from `groups.mcc_code`, which the middle
  layer already loads (`merchant_mcc`) and passes to Alipay/Aani.
- Currency: AED (confirmed by the onboarding screen).

Still open:
1. **Which AppID is bound to mchid 909018166?** Ask the client (they have merchant platform access,
   since they generated the API certificate/key). Suggested message:
   > For WeChat Pay Global Native Payment, which value should go in the `appid` field of the
   > "Order Placement" API for merchant ID 909018166? Is it the "Application ID" 2000002804173264
   > from the onboarding page, or a different AppID? Is it bound to the merchant ID (Dev
   > Configuration), and does anything (e.g. the agreement) have to be done first? The API
   > returned: 商户号mch_id与appid不匹配 (merchant ID and appid do not match).
   Docs to point them to: Preparation `.../4012356375` (AppID binding) and Order Placement
   `.../4012356478` (`appid` = "Merchant's WeChat Official Account APPID").
1b. **APIv3 key is not set on the merchant account** (WeChat said so on the certificates call).
   Ask the client to set it (Account Settings > API Security, 32 chars letters/digits) and share it
   securely (not in chat/email); it goes in `WECHAT_APIV3_KEY` on the server. Then re-run the
   certificates check: `HTTP 200` would confirm mchid + key + cert.
2. **Agreement signing status.** An older screenshot showed Unsigned; probably signed now because
   the key and certificate were generated (that needs platform login) and our signature was
   accepted. Not proven; the docs we read do not mention the agreement. If the same error persists
   after the AppID is confirmed, ask WeChat support whether the agreement affects API access.
3. **Merchants with no MCC.** In `shukria_transactions`, 406 of 710 groups have `mcc_code` NULL
   (most common non-null value is `5411`, 93 groups; also `5814`, `5812`, `4900`). Right now a
   NULL MCC makes WeChat QR creation return an error. Decide: block until onboarding fills it in,
   or fall back to a default such as `5411`. Also check that WeChat accepts the MCC list we use.
4. **Which database does the app use?** The data we want is in `shukria_transactions`. On
   `wechat-my-fix`, `config/dev.exs` still points to `lic_project_devteam` (port 4004);
   the simulator branch's dev.exs used `shukria_transactions` (port 4012). Change dev.exs
   before running the full flow.
5. **Public HTTPS `notify_url`** that WeChat can actually reach, and the CloudLayer URL
   (`WECHAT_CLOUD_NOTIFY_URL`).

---

## 10. NEXT ACTIONS (in order)

1. Ask the client (who has merchant platform access) the question in section 9, item 1.
2. They check "Dev Configuration" for the AppID bound to 909018166, and confirm the agreement is signed.
   They also SET the APIv3 key (Account Settings > API Security) and share it securely: WeChat
   reported it is not set (section 6, second call).
3. (If we get platform access ourselves, do 2 directly.)
4. Update `WECHAT_APPID` in `.env` (and re-export it), then re-run the QR check script.
   Success looks like `HTTP 200` with a `code_url` such as `weixin://wxpay/bizpayurl?...`.
5. If the next error is about MCC or currency, fix those (try `unset WECHAT_TEST_CURRENCY`).
6. Commit step 1.
7. Meanwhile, implement steps 2-5 (query, polling, idempotent webhook, cancel).

---

## 10b. Credentials explained (what each is, what we have)

| Item | What it is | Used for | Status |
|---|---|---|---|
| Merchant private key (`apiclient_key.pem`) | RSA private key made with the certificate | Signing every request to WeChat | Have it (server, `/etc/wechatpay/`) |
| Merchant certificate + serial (`apiclient_cert.pem`, serial `490F1F01A416AC387CE41C7B2C5C698E03E8A7C4`) | Public half + its ID | Tells WeChat which certificate verifies our signature | Have it |
| APIv3 key | 32-char string (letters/digits) set by a person in the merchant platform; NOT a file | Decrypting WeChat responses/notifications (payment webhook, platform certificates) | MISSING: WeChat says it is not set |
| AppID | ID of the Official Account/app bound to the merchant ID | `appid` field when creating an order | UNCONFIRMED: WeChat says appid and mch_id do not match |

`apiclient_cert.p12` is just another packaging of the private key + certificate (password-protected); it is
NOT the APIv3 key and is not needed (the app reads the PEM files). Do not copy it to the server.

### Evidence from the WeChat documents (verbatim quotes)
Source: Preparation page https://pay.weixin.qq.com/doc/global/v3/en/4012356375
- "WeChat Pay API Key v3 is used to encrypt and decrypt sensitive information in interface transmission."
- "Log in to WeChat Merchant Platform, select Account Settings > API Security >Set APIv3 Secret, and click Set APIv3 secret."
- "Enter API Key v3, which consists 32 characters, including numbers and upper and lower case letters."
- "After an official account has been applied for successfully, the institution can log in to the official account platform to obtain the corresponding APPID."
- "After both APPID and mch_id have been applied for, the binding relationship between them needs to be established."
- Agreement signing / activation: "not mentioned" in that page.
Source: WeChat FAQ linked from the error (http://kf.qq.com/faq/180830E36vyQ180830AZFZvu.html): it is the APIv3 key
FAQ; it says the key used by merchants to decrypt messages is the APIv3 key and gives the path Merchant Platform ->
Account Center -> Account Settings -> API Security -> APIv3 Key -> Set. This ties the "merchant has not set the
encryption key" error to the APIv3 key.

What is inference (NOT in the docs): that `2000002804173264` is wrong (we only know the pair is rejected);
the name of the "Dev Configuration" tab (from a first summary pass, not re-verified verbatim); anything about the
agreement; whether creating an order also needs the APIv3 key. The Preparation page is written for
"institutions", so it may not apply identically to a direct merchant.

### Product/flow check (Native vs Quick Pay, Global vs domestic)
- Our flow (device shows a QR; customer scans with WeChat; WeChat notifies us; CloudLayer tells the device) IS
  Native payment. Quick Pay is the opposite (device scans the customer's code).
- "Global" is about the merchant account, not the flow. Evidence: all provided docs are `/doc/global/v3/`;
  `apihk` host and `/v3/global/...` paths respond; WeChat looked up merchant 909018166 on that host; currency AED.
  The Native intro page says: "No special use conditions are set for this product. After onboarding as a WeChat
  Pay institution or direct merchant, you can obtain the permission to use this product by default."
  NOT proven for this account until WeChat returns a `code_url`. Optional: ask the client to confirm merchant
  909018166 is enabled for Global Native.

### Email to the client (sent by the user)
Asked the client (who has merchant platform access) to: (1) set the APIv3 key and share it securely;
(2) say which AppID is bound to 909018166, or bind one; (3) optionally confirm the agreement is signed. The
email quoted both test requests/responses (Test A create order -> 400 appid mismatch; Test B certificates
-> 401 key not set) and the doc passages above. No secrets were included.

---

## 10c. COMMIT PLAN (nothing is committed yet; this is how we will split it later)

Agreed workflow: implement ALL steps first, the user tests everything, THEN the assistant helps commit each step
separately (the assistant never commits on its own initiative; it gives commands, or runs them only when asked).

Per-step snapshots of the shared files are kept OUTSIDE the repo at `~/wechat_step_snapshots/` (copy of the
scratchpad). Use them to build exact per-step commits when files hold several steps:
- `step1/wechat.ex` and `step1/wechat_pay_client.ex`: Step 1 only (Global host/paths, trade_type, MCC).
- `step2/wechat.ex`: Step 1 + Step 2 (adds `query_order`, `query_order_path`, `trade_status`, `log_query_event`).
- `step3/wechat_payment.ex`, `step3/wechat_webhook_controller.ex`: Step 3 versions (no `open_statuses/0`).
- `step4/wechat_payment.ex` (adds `open_statuses/0`), `step4/wechat_poller.ex`,
  `step4/qr_middle_layer_controller.ex` (the file differs from HEAD only by the 10-line poller hook).
Snapshots for later steps will be added as each is written.

| Commit | Files | Notes |
|---|---|---|
| 1. Global API | `wechat.ex` (step1 version), `wechat_pay_client.ex` | `wechat.ex` also holds step 2 now: stage the step1 version first |
| 2. query_order | `wechat.ex` (adds the query block), `test/.../wechat_test.exs` | |
| 3. idempotent settle | `wechat_payment.ex` (use `step3/` version), `wechat_webhook_controller.ex`, `test/.../wechat_payment_test.exs` | `wechat_payment.ex` is also touched by step 4 |
| 4. polling | `wechat_payment.ex` (current), `wechat_poller.ex`, `qr_middle_layer_controller.ex`, `test/.../wechat_poller_test.exs` | shares the middle-layer file with cancel/refund |
| 5. cancel | `qr_middle_layer_controller.ex` (+ `wechat.ex` if needed) | |
| 6. refund | `wechat.ex`, `qr_middle_layer_controller.ex`, router, new notification controller | candidate for a separate branch |

Technique for a file with mixed steps: put the earlier-step version of the file in the index, e.g.
`git hash-object -w <snapshot>` then `git update-index --cacheinfo 100644,<hash>,<path>`, commit, then
`git add <path>` for the next step. (Or `git add -p`, which needs an interactive terminal.)

Unrelated local changes that must NOT be included in any WeChat commit: `lib/da_product_app/brand.ex` (deleted),
`transactions/transactions.ex`, `payment_notification_controller.ex`, `qr_more_fun_controller.ex`,
`controllers/sample.txt`, and the `deps/` and `_build/` noise.

---

## 11. Housekeeping

- COMMIT POLICY: the assistant does not run `git commit`/`git add`. After each step is written, the user tests it
  and asks for commit commands later; one separate commit per step. Pending (uncommitted) work: step 1 (Global
  host/paths in `wechat.ex` + `wechat_pay_client.ex`) and step 2 (`query_order` in `wechat.ex` + the test file).
  Note both touch `wechat.ex`; to commit step 1 separately use `git add -p lib/da_product_app/qr_provider/wechat.ex`
  and pick only the step 1 hunks.

- Keep real keys OUTSIDE the repo (`/etc/wechatpay/`). `priv/cert/` is not gitignored.
- The key must be on the Linux server. A Windows path (e.g. `C:\...`) does not work on the server
  and backslashes get mangled when `.env` is sourced. Use a forward-slash Linux path.
- Copy files from the laptop with `scp "<real windows path>" greeshma@demo.ctrmv.com:/home/greeshma/<file>`
  (replace the placeholders; do not run the example paths literally).
- These local changes are NOT part of the WeChat work and were left uncommitted on purpose:
  `lib/da_product_app/brand.ex` (deleted), `transactions/transactions.ex`,
  `payment_notification_controller.ex`, `qr_more_fun_controller.ex`,
  `controllers/sample.txt`.
- Never paste private keys, the APIv3 key or passwords into chat or commits.

## 12. Env vars reference

```
WECHAT_APPID                     (the real wx... AppID, NOT 2000002804173264)
WECHAT_MCHID                     909018166
WECHAT_MERCHANT_SERIAL_NO        (or WECHAT_MERCHANT_CERT_PATH=/etc/wechatpay/apiclient_cert.pem)
WECHAT_PRIVATE_KEY_PATH          /etc/wechatpay/apiclient_key.pem
WECHAT_APIV3_KEY                 (needed for webhook + platform certificates)
WECHAT_NOTIFY_URL                (public https, no query string)
WECHAT_CLOUD_NOTIFY_URL          (needs adding to the config/runtime.exs list)
WECHAT_API_BASE_URL              (leave unset; default is now https://apihk.mch.weixin.qq.com)

QR check script only:
WECHAT_TEST_MCC=5411
WECHAT_TEST_CURRENCY=AED         (optional)
WECHAT_TEST_AMOUNT=1
```

## 13. Progress log (chronological)

1. Compared `wechat-my-fix` vs `wechat-simulator`: one commit apart.
2. Read the WeChat Global docs (Quick Pay pages, then Native pages): found we were using the wrong
   (domestic) API; Native + Global is the right product.
3. Cherry-picked the useful parts of the simulator commit, committed as `0826aa6`
   (and discovered `wechat_pay_client.ex` needed to be committed with it).
4. Wrote this context file.
5. Checked the repo: single appid/mchid (looks like common mode); MCC comes from `groups.mcc_code`;
   data DB is `shukria_transactions`.
6. Coded step 1 (Global host/paths, `trade_type`, MCC); compiled with no errors; not committed.
7. Built the QR check script; moved the private key and cert to the server.
8. First real call: signing OK, failed on appid/mchid mismatch.
9. Onboarding screenshot labelled `2000002804173264` as "Application ID"; merchant is
   MERCURY PAYMENTS SERVICES L.L.C, currency AED, status Passed, agreement showed Unsigned (older screenshot).
10. This file updated with all of the above.
11. Corrected an overstatement: it is NOT established that `2000002804173264` is not the AppID, and the
    docs do not say AppIDs start with `wx` (that was general knowledge). The claim that the unsigned
    agreement blocks the pairing was a guess; now judged unlikely (key/cert were generated, signature
    accepted). Main suspect: the AppID is wrong or not bound. A scan of all of /var/www found no other
    record of the number.
12. Standing practice agreed: keep updating this file as we make progress.
13. Ran the certificates check (no AppID). Result: 401 SIGN_ERROR, "merchant has not set the encryption
    key" => the APIv3 key is not set on the account. Merchant ID is recognised. Withdrew the earlier
    claim that the signature was proven accepted. Two things now needed from the client: the APIv3 key
    (set + shared securely) and the AppID bound to 909018166.
14. Re-read the docs: the Query Order page (.../4012356555) gives `out-trade-no` + `?mchid=`. Implemented
    `query_order` + `trade_status` + unit tests (step 2); uncommitted, awaiting the user's test.
15. User rejected a tool call that tried to commit step 1; agreed commit policy (section 11).
16. Re-fetched the Preparation page for verbatim quotes and read the linked WeChat FAQ; wrote section 10b
    (credentials explained, doc evidence, what is inference).
17. Drafted the client email with evidence; the user has asked the client for the credentials.
18. Confirmed the flow is Native payment; Global is well supported but not proven for the account.
19. Step order changed: 3 = shared idempotent settle logic, 4 = polling, 5 = cancel.
20. Code audit for pending work written as section 8b.
21. Branching advice: one branch, one commit per step; optional separate branch for refund; the user clarified
    they do not have to wait for testing before coding the next steps (only before committing).
22. Coded step 3 (`WeChatPayment.settle/3` + webhook refactor + unit tests); uncommitted.
23. Workflow clarified by the user: implement everything, test everything, then help commit each step
    separately. Saved per-step snapshots to `~/wechat_step_snapshots/` and wrote the commit plan (section 10c).
24. Found that the committed `/api/payment/notify-success` pushes "payment received" to the device regardless of
    status; changed `settle/3` to notify the cloud layer only on success.
25. Coded step 4 (polling: `WeChatPoller` + 10-line hook in the middle layer + tests); fixed a `BadBooleanError`
    I introduced before it ran; all 22 WeChat unit tests pass; snapshots updated (`step4/`). Uncommitted.
