07-qr-payment

Example of the component nguyenduchoai/innoedge v0.1.4
<a id="top"></a>
# 07 — QR Payment: Thanh toán quét mã QR ngân hàng (Instant Banking Payments)

> 🇻🇳 **Tài liệu Tiếng Việt** (toàn bộ nội dung bên dưới) | [🇬🇧 English Documentation](#english)

Khách quét QR bằng app ngân hàng/ví, máy nhận tín hiệu tiền về trong ~1–3 giây.

## Phần cứng
Devkit + nút BOOT. Máy thật thì thêm màn hình để hiện QR.

## Luồng
```
Khách chọn gói
   ↓  innoedge_request_qr(20000)
Cloud tạo intent + gọi cổng (Pay2S/9Pay/MoMo/bank)
   ↓  on_qr(payload, ...)
Máy hiện QR — khách quét, chuyển tiền
   ↓  webhook ngân hàng → cloud
   ↓  on_paid(intent_id, amount)
Máy giao hàng / mở relay
```

## Bốn luật

**1. Render `payload` NGUYÊN VĂN.** Không parse, không dựng lại. Cùng một field
có thể là EMV VietQR, URL trang thanh toán ví, hoặc URL ảnh — tuỳ cấu hình cổng
của từng đối tác. Firmware chỉ vẽ.

**2. Chỉ `on_paid` mới được phép giao hàng.** Không phải "khách bảo đã chuyển",
không phải "QR đã hiện đủ lâu". Chỉ webhook xác nhận.

**3. `on_qr_error` phải hiện được cho khách.** Khi cổng ví lỗi và không có kênh
dự phòng, cloud **cố tình không phát QR** — phát QR mà không kênh nào xác nhận
là khách mất tiền thật. Hiện đúng `message` + nút thử lại. Cloud đã tự báo đối
tác qua alert `payment_gateway_down`, firmware không phải làm gì thêm.

**4. QR động cần online.** Khác tiền mặt (vào hàng đợi, gửi sau).
`innoedge_request_qr()` offline sẽ trả `ESP_ERR_INVALID_STATE` — hiện "vui lòng
dùng tiền mặt".

## Kết quả mong đợi
```
I (15200) qr: QR 20000đ · mã GT012D00088 · hết hạn sau 300s · intent=88
I (15210) qr: payload: 00020101021238570010A00000072701270006970422...
I (28400) qr: ĐÃ THANH TOÁN 20000đ (intent=88) — bắt đầu phục vụ
```

## Chống trả hai lần
SDK tự gửi `paid_ack` khi nhận `on_paid` để cloud thôi gửi lại. Nếu máy reboot
đúng lúc đó, cloud gửi lại — nên nghiệp vụ giao hàng nên tự chống trùng theo
`intent_id`. Với nhả tiền/credit, hãy dùng lệnh `dispense` (example 06) thay vì
làm trong `on_paid`: lệnh đó đã có chống-trùng bền qua reboot.

## Troubleshooting
| Triệu chứng | Nguyên nhân |
|---|---|
| `ESP_ERR_INVALID_STATE` | Máy chưa gán đối tác hoặc đang offline |
| Có `on_qr` nhưng không bao giờ `on_paid` | Webhook cổng chưa trỏ về cloud; kiểm tra cấu hình Pay2S của đối tác |
| App ngân hàng báo "mã không hợp lệ" | Firmware đang tự dựng lại payload thay vì render nguyên văn |
| `on_qr_error` liên tục | Cổng thanh toán của đối tác đang lỗi — xem alert `payment_gateway_down` trên app |

---

<a id="english"></a>
## 🇬🇧 English Documentation

> [🇻🇳 Quay lại Tiếng Việt](#top)

Customers scan a dynamic QR code using banking or e-wallet apps; the hardware receives a certified payment notification within ~1–3 seconds.

## Hardware Requirements
Devkit board + BOOT button. In production, connect an LCD/OLED display or e-paper screen to render the QR code.

## Architecture Flow
```
Customer selects package
   ↓  innoedge_request_qr(20000)
Cloud creates intent + calls payment gateway (VietQR, PayOS, SePAY, Tingee)
   ↓  on_qr(payload, ...)
Device displays QR code on screen — Customer scans and transfers funds
   ↓  Instant bank webhook -> Cloud
   ↓  on_paid(intent_id, amount)
Device dispenses goods / activates relay
```

## Four Essential Rules

1. **Render `payload` VERBATIM.** Never parse, alter, or reconstruct the QR string. Depending on gateway configuration, the payload may be an EMV VietQR string, a direct wallet checkout URL, or a hosted image URL. The firmware must simply render whatever string it receives.
2. **Only `on_paid` permits dispensing.** Not "customer says they transferred", not "the QR code was displayed long enough". Only a cryptographically verified webhook unlocks hardware.
3. **Handle `on_qr_error` gracefully.** If the payment gateway is down and no backup provider is configured, the cloud **intentionally refuses to issue a QR code**. Showing a QR code that cannot be verified would result in real customer financial loss. Display the error message with a "Retry" button.
4. **Dynamic QR requires internet connectivity.** Unlike cash pulses (which buffer safely in the offline NVS queue), `innoedge_request_qr()` called while offline returns `ESP_ERR_INVALID_STATE` so the screen can instruct customers to pay with cash.

## Expected Output
```
I (15200) qr: QR 20000 VND · Ref GT012D00088 · Expires in 300s · intent=88
I (15210) qr: payload: 00020101021238570010A00000072701270006970422...
I (28400) qr: PAYMENT CONFIRMED: 20000 VND (intent=88) — Starting service
```

## Troubleshooting
| Symptom | Cause & Solution |
|---|---|
| `ESP_ERR_INVALID_STATE` | Device is unassigned or currently offline. |
| QR generated but `on_paid` never triggers | Gateway webhook URL is not pointing to your cloud server; verify gateway webhook configuration. |
| Banking app reports "Invalid QR code" | Firmware modified or truncated the payload instead of rendering it verbatim. |
| Frequent `on_qr_error` | Gateway API credentials expired or gateway service degraded. |

To create a project from this example, run:

idf.py create-project-from-example "nguyenduchoai/innoedge=0.1.4:07-qr-payment"

or download archive (~7.45 KB)