# LINE Pay Online / Offline Implementation Quickstart

## 1. Prerequisites

- JDK 21: `C:\Users\qmj\.jdks\graalvm-jdk-21.0.7`
- MySQL schema changes from `updatesql/sql.md` applied manually by the developer
- Redis/Redisson available
- LINE Pay Sandbox Channel ID/Secret
- 对应 LINE Pay 商户已开通 Offline API 权限；固定收款码但没有 API credentials 的商户不能联调
- 官方台湾 Sandbox My Code 生成器；不得使用真实 LINE App 生成的 My Code
- Public HTTPS API base URL reachable by LINE redirect
- Platform frontend: `E:\QtwCode\foodie\foodie-admin-vue`

Do not execute schema migration from Codex. Do not use the deprecated `TestTask`, `ZaloPayController`, `PayController`, or `OrderAppealController` flows.

## 2. Runtime configuration

`ruoyi-admin/src/main/resources/application.yml` contains non-secret defaults:

```yaml
line-pay:
  environment: sandbox
  base-url: https://sandbox-api-pay.line.me
  confirm-url: https://api.example.com/pay/line/confirm
  cancel-url: https://api.example.com/pay/line/cancel
  reconcile:
    fixed-delay-ms: 60000
    initial-delay-ms: 60000
    batch-size: 20
    row-lease-seconds: 120
    round-budget-seconds: 45
    auth-deadline-minutes: 30
    unknown-deadline-hours: 24
```

Channel credentials are saved per store through the platform API; they are not placed in application.yml.

## 3. Local verification

Use JDK 21 only in the current PowerShell process:

```powershell
$env:JAVA_HOME='C:\Users\qmj\.jdks\graalvm-jdk-21.0.7'
$env:PATH="$env:JAVA_HOME\bin;$env:PATH"
mvn -pl ruoyi-system -am -Dtest='*Line*Test' -Dsurefire.failIfNoSpecifiedTests=false test
mvn -pl ruoyi-admin -am -Dtest='*LinePay*Test,OrderLifecycleServiceTest,OmgPayControllerTest' -Dsurefire.failIfNoSpecifiedTests=false test
mvn -pl ruoyi-admin -am -DskipTests package
```

现有 Online 平台前端回归（仅当实际修改 `foodie-admin-vue` 时执行）：

```powershell
Set-Location E:\QtwCode\foodie\foodie-admin-vue
npm run lint
npm run build:prod
```

Repository checks:

```powershell
Set-Location E:\QtwCode\foodie\foodie_server
git diff --check
git status --short
```

Offline 增量的定向验证命令在全部后端功能、测试源码和 SQL 调整完成后统一执行：

```powershell
$env:JAVA_HOME='C:\Users\qmj\.jdks\graalvm-jdk-21.0.7'
$env:PATH="$env:JAVA_HOME\bin;$env:PATH"
mvn -pl ruoyi-system -am -Dtest='*Line*Test' -Dsurefire.failIfNoSpecifiedTests=false test
mvn -pl ruoyi-admin -am -Dtest='*LinePayOffline*Test,PosOrderShOprateControllerTest,*LinePay*Test,OrderLifecycleServiceTest,OmgPaymentControllerTest' -Dsurefire.failIfNoSpecifiedTests=false test
mvn -pl ruoyi-admin -am -DskipTests package
```

## 4. Sandbox acceptance flow

1. Platform opens Store Payment → LINE Pay.
2. Enter store Channel ID/Secret; verify the detail shows a new current version and enabled state.
3. Create one single-store order with `payType=3`.
4. Call `/pay/line/create`; assert payment row is `WAITING_AUTH` and response uses `paymentUrl.web`.
5. Repeat create before completion; assert the same `paymentId/lineOrderId` is returned with `reusedAttempt=true`.
6. Complete Sandbox web authorization and allow confirm redirect.
7. Confirm page returns quickly and App query eventually reports `PAID`.
8. Query DB read-only and verify payment remains `PAID`, order `pay_status=1`, and gateway logs contain REQUEST/CHECK/CONFIRM/RETRIEVE events.
9. Cancel a paid order; verify exactly one refund row is created, eventually `REFUNDED`, while payment remains `PAID` and order becomes `pay_status=2`.
10. Create a second attempt only after the first is explicitly cancelled/expired/failed; verify both rows remain and only the second has `active_dd_id`.

## 5. Offline Sandbox acceptance flow

1. 使用商家 token 调 `/system/orderShOprate/createOrder` 创建 `paymentMethod="3"` 的单门店订单。
2. 只读检查子订单 `order_source=MERCHANT`；用户端订单和历史订单保持 `USER`。
3. 从官方台湾 Sandbox My Code 生成器取得 18 位 `oneTimeKey`，调用 `/system/orderShOprate/linePay/offline/pay`。
4. 检查支付行 `payment_mode=OFFLINE`，且数据库、应用日志、异常和 `payment_gateway_log.payload` 均不存在原始 `oneTimeKey`。
5. Pay 返回 `1145/1169` 或 Check 返回 `AUTH_READY` 时，状态接口返回 `AUTH_REQUIRED`；重复提交新的 My Code 只返回旧尝试，不再次调用 Pay。
6. Pay 超时、响应不明确或返回未识别结果码时，状态接口返回 `PROCESSING`，任务只按 `lineOrderId` 调 Offline Check；Check 未识别结果码同样不得释放活跃键。
7. Check 返回 `COMPLETE` 且订单号、交易号、金额合计、非空 `paymentProvider` 匹配时，状态最终为 `PAID`，订单结算和通知只执行一次。
8. 使用错误商家、本人 `shId` 搭配他店 `mdId`、用户端/历史订单、多门店订单、非 `payType=3` 订单或无效 My Code 请求，确认在调用 LINE 前拒绝。
9. 模拟金额不一致，确认 `captured_amount` 保存 LINE 实际扣款额、不触发正常履约、只创建一条以实际扣款额为金额的全额退款意图，并且原订单保持禁止再次支付。
10. 对已支付 Offline 订单执行取消/退款，确认 URI 使用 `lineOrderId`；对 Online 订单回归确认仍使用 `transactionId`。

## 6. Required concurrency and security tests

- Two simultaneous create calls for the same LINE order return one attempt.
- LINE create and OMG create for the same order cannot both claim the payment channel.
- confirm redirect, scheduled task and admin reconcile cannot Confirm twice.
- user cancellation before late Capture still records PAID and creates one refund intent.
- store cancellation and late Capture produce the same result and enforce store ownership.
- credential rotation during a waiting/paid transaction does not change its `credential_id`; old transaction remains queryable/refundable.
- UNKNOWN and MANUAL_REVIEW never release `active_dd_id` or directly repeat side effects.
- Online 与 Offline 同时发起时只允许一个模式创建活跃尝试。
- 两个 Offline Pay 请求并发时只向 LINE 发送一次，后到请求的 `oneTimeKey` 不记录、不发送。
- `AUTH_REQUIRED`、`PROCESSING`、`AMOUNT_MISMATCH` 和缺失 `paymentProvider` 的 `MANUAL_REVIEW` 均阻止重新扫码。
- 普通商家和夜市商家按 `shId=userId` 鉴权，其他摊位商家按 `mdId=InfoUser.storeId` 鉴权。
- Controller、审计服务和异常分支的测试均断言原始 `oneTimeKey` 不可见。

## 7. Production acceptance items

Sandbox cannot prove every App handoff behavior. Before production enablement, verify on real iOS and Android devices:

- LINE payment browser returns to the intermediate page and the fixed Scheme handoff works.
- `com.twanmsdyh.app://payment/result` is registered and handled.
- auto-open works where allowed; the manual button works in LINE WebView and external browsers.
- App always calls `/query` and never trusts the deep-link as success.
- current production credentials complete an end-to-end TWD payment and full refund.
- 业务方确认该 Channel 已开通 Offline API，并用真实客户 My Code 验证 `AUTH_READY/COMPLETE/CANCEL/FAIL` 收敛。
- 验证 `paymentProvider` 的 `TSP/EPI` 原值；若生产返回其他非空值仍保存并允许完成，缺失或空值进入人工核对。
