# API Contract: 创建 OMG AIO 支付订单

## POST `/pay/omg/create`

### Authentication

- Header: `token: <user-login-token>`
- Spring annotations: `@Anonymous` + project `@Auth`
- Controller method must declare `@RequestHeader String token`.

### Request

```http
POST /pay/omg/create
Content-Type: application/json
token: <login-token>

{
  "orderId": "991786433092835",
  "paymentMethod": "CREDIT"
}
```

Rules:

- DTO has exactly two fields: `orderId` and `paymentMethod`.
- Controller uses explicit `@RequestBody(required = false)`.
- `orderId` is trimmed, non-empty and at most 64 characters.
- `paymentMethod` must be exactly `CREDIT` or `APPLE_PAY`.
- Client cannot provide amount, MerchantID, gateway URL, ReturnURL, text, raw OMG payment parameters or signature fields.

### Success

Outer response keeps the project `AjaxResult` format. `data` is:

```json
{
  "status": "CREATED",
  "gatewayUrl": "https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5",
  "formFields": {
    "MerchantID": "1000031",
    "MerchantTradeNo": "OMGR8K3P7W2M9C4X6A1B",
    "MerchantTradeDate": "2026/08/13 15:30:23",
    "PaymentType": "aio",
    "TotalAmount": "100",
    "TradeDesc": "Foodie order 991786433092835",
    "ItemName": "Order 991786433092835",
    "ReturnURL": "https://foodieapi.waimai-paotui.com/pay/omg/notify",
    "OrderResultURL": "https://foodieapi.waimai-paotui.com/pay/omg/result",
    "ChoosePayment": "Credit",
    "UnionPay": "2",
    "EncryptType": "1",
    "InvoiceMark": "N",
    "NeedExtraPaidInfo": "Y",
    "CheckMacValue": "<64 uppercase hexadecimal characters>"
  }
}
```

The example above is the `CREDIT` variant. For `APPLE_PAY`, `ChoosePayment` is `ApplePay`, and `UnionPay` and `IgnorePayment` are absent. `ChoosePayment=ALL` is never generated.

The client must submit every `formFields` entry in the current page as an `application/x-www-form-urlencoded` POST to `gatewayUrl`. It must not use iframe, a new window or a GET link.

### Business failure

```json
{
  "code": 500,
  "msg": "<localized message>",
  "data": {
    "status": "PAYMENT_ATTEMPT_EXISTS"
  }
}
```

Stable statuses:

| Status | Meaning |
|---|---|
| `AUTH_REQUIRED` | No usable user identity reached the new Controller |
| `ORDER_REQUIRED` | Request or orderId is missing/invalid |
| `ORDER_NOT_AVAILABLE` | Order does not exist or is not owned by this user |
| `MULTI_STORE_ORDER_NOT_SUPPORTED` | Matched row is a multi-store child order |
| `ORDER_STATE_NOT_PAYABLE` | State is not 0, 1 or 2 |
| `ORDER_ALREADY_PAID` | `pay_status` is not 0 |
| `ORDER_AMOUNT_INVALID` | Amount is null or not positive |
| `PAYMENT_TYPE_INVALID` | `pay_type` is not `"2"` |
| `PAYMENT_METHOD_INVALID` | `paymentMethod` is missing or is not `CREDIT/APPLE_PAY` |
| `STORE_CREDENTIAL_UNAVAILABLE` | No enabled credential exists for the order store |
| `PAYMENT_ATTEMPT_EXISTS` | The order already has an active `CREATED` attempt |
| `PAYMENT_CONFIGURATION_INVALID` | Stage/ReturnURL safety validation failed |
| `PAYMENT_CREATION_FAILED` | Controlled generation/insert attempts could not complete |

No error response includes token, HashKey, HashIV, CheckMacValue, form fields, SQL or stack trace.

## POST `/pay/omg/notify`

### Request

```http
POST /pay/omg/notify
Content-Type: application/x-www-form-urlencoded

MerchantID=1000031&MerchantTradeNo=OMGR8K3P7W2M9C4X6A1B&RtnCode=1&RtnMsg=Succeeded&TradeNo=26081300000000000001&TradeAmt=100&PaymentDate=2026%2F08%2F13+16%3A20%3A00&PaymentType=Credit_CreditCard&PaymentTypeChargeFee=3&TradeDate=2026%2F08%2F13+16%3A18%3A00&SimulatePaid=1&CustomField1=&CustomField2=&CustomField3=&CustomField4=&card4no=4242&CheckMacValue=<CHECK_MAC_VALUE>
```

Rules:

- No login token is required.
- Controller receives one explicit `OmgNotifyRequest` DTO; it does not accept `Map` or `HttpServletRequest`.
- The request boundary preserves the exact raw body plus every decoded parameter, including unknown fields and empty values.
- Duplicate parameter names, malformed percent encoding, unsupported content type or an oversized body are invalid.
- Signature input contains every actual parameter except `CheckMacValue`; no field whitelist is used.
- Credential lookup begins from the locked `MerchantTradeNo` attempt and uses its credential snapshot.
- `MerchantID` and `TradeAmt` must equal the creation snapshot.

### Acknowledged

```http
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8

1|OK
```

This response is used for a verified success, verified final failure, or an idempotently repeated fact. `RtnCode=1` marks the attempt and order paid even when `SimulatePaid=1`. `RtnCode!=1` marks a non-paid attempt failed and leaves the order unpaid.

### Retryable rejection

```http
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8

0|ERROR
```

This response is used when the request cannot be trusted or atomically processed: unknown trade number, invalid/missing fields, duplicate fields, credential snapshot unavailable, signature mismatch, MerchantID/amount mismatch, conflicting gateway `TradeNo`, or database transaction failure. No order/payment state is changed.

### IPN logging

Every HTTP request is inserted before business handling into existing `ipn_log` with `type=omg`, request IP, receive time and the full raw form body. This insert uses an independent transaction. Failure to write `ipn_log` is logged but does not block payment processing.

## POST `/pay/omg/query`

Header `token` is required. The JSON body contains only the business order ID:

```json
{"orderId":"991786433092835"}
```

The server verifies ownership and chooses the only current payment attempt. An unpaid order queries its unique `CREATED` attempt; an already-paid order queries its first `PAID` attempt. The client cannot provide `MerchantTradeNo`, amount, credentials or gateway URL.

- POST `MerchantID`, `MerchantTradeNo`, current Unix `TimeStamp` and `CheckMacValue` to the OMG stage `QueryTradeInfo/V5` endpoint.
- Use the attempt credential snapshot, parse and sign every actual response field (including additional and empty fields), excluding only `CheckMacValue`.
- Verify `MerchantID`, `MerchantTradeNo` and `TradeAmt` against the attempt snapshot.
- `TradeStatus=1` reuses the callback's locked idempotent settlement transaction and compensates a lost callback by marking the attempt/order paid.
- `TradeStatus=0` changes no local state; `10200095` synchronizes an unpaid attempt to failed; paid facts are irreversible.
- User-triggered query is not an incoming IPN and is not inserted into `ipn_log`.

The response whitelist includes normalized `status` (`PAID`, `UNPAID`, `FAILED`, `UNKNOWN`), raw `tradeStatus`, trade numbers, amount, gateway dates, payment type and fees. Invalid/tampered responses return `PAYMENT_QUERY_FAILED`; no eligible attempt returns `PAYMENT_QUERY_NOT_AVAILABLE`.

## POST `/pay/omg/retry`

Header `token` is required. The explicit JSON DTO contains only:

```json
{"orderId":"991786433092835","paymentMethod":"APPLE_PAY"}
```

The endpoint is used after the App has destroyed the original payment WebView and the user explicitly starts payment again. The server never returns or replays the old form. If no local attempt is currently queryable (a terminal `FAILED`/`SUPERSEDED` attempt has released its active pointer), skip the gateway query and create a fresh attempt directly; an already-paid order is still rejected by the create-side validation with `ORDER_ALREADY_PAID`. Otherwise it first executes the same authenticated, signed gateway query as `/query`:

- `PAID`: return `ORDER_ALREADY_PAID` and do not create a payment.
- `FAILED`: create a fresh attempt and form.
- `UNPAID` with blank `paymentType`: lock the order, require the active attempt's `MerchantTradeNo` to equal the verified query result, atomically mark that exact attempt `SUPERSEDED`, and create a fresh attempt/form in the same transaction.
- `UNPAID` with a nonblank `paymentType`, `UNKNOWN`, query failure, or a concurrent active-attempt change: do not replace the attempt and return a stable error.

At most one retry request can replace a given active attempt. A late trusted paid callback for a `SUPERSEDED` attempt remains eligible for the irreversible paid transition and closes any newer active attempt.

Success has the same response contract as `/create`: `{status:"CREATED", gatewayUrl, formFields}` with a new `MerchantTradeNo` and fields for the newly selected `paymentMethod`. Business errors include the existing query/create errors plus `PAYMENT_RETRY_NOT_AVAILABLE`; unexpected failures return `PAYMENT_RETRY_FAILED`. The client cannot send `MerchantTradeNo`, OMG `paymentType`, raw OMG payment parameters, amount, credentials, gateway URL, or old form fields.

## POST `/pay/omg/refund`

Current scope is a test-environment safety boundary. Header `token` is required and the JSON body contains only:

```json
{"orderId":"991786433092835"}
```

The client cannot provide refund amount, `MerchantTradeNo`, `TradeNo`, `Action`, credentials or gateway URL. After authentication and basic order-ID validation, the endpoint always returns:

```json
{
  "code":500,
  "msg":"OMG 测试环境不支持退款，请在正式环境启用后操作",
  "data":{"status":"PAYMENT_REFUND_UNAVAILABLE_IN_TEST_ENVIRONMENT"}
}
```

The endpoint does not call the production-only OMG `CreditDetail/DoAction` API, does not read or update order/payment/refund state, does not insert `ipn_log`, and does not create a refund record. Formal refund orchestration remains out of scope until the production environment is explicitly enabled.
