Mô phỏng thanh toán đơn demo

Nội dung Markdown đầy đủ của trang tài liệu.

# Mô phỏng thanh toán đơn demo

Tự động kiểm thử **Create đơn demo → mô phỏng thanh toán → nhận IPN**. Dùng ngay **Secret Key của tài khoản demo** làm Bearer token; không cần token mới hoặc ký HMAC request.

## Request

```http
POST https://payment.pay2s.vn/v1/gateway/api/simulate-payment
Authorization: Bearer YOUR_DEMO_SECRET_KEY
Content-Type: application/json
```

```json
{
  "orderId": "DEVd33eb13817916599723201625c7",
  "resendIpn": false
}
```

- `orderId`: mã đã gửi lúc create, không phải mã PAY2SJSC; bắt buộc là chuỗi.
- `resendIpn`: boolean tùy chọn, mặc định `false`. Đặt `true` để chủ động gửi lại IPN của đơn demo đã thanh toán.
- Không cần gửi `accessKey`, `partnerCode`, `requestId`, `requestType` hay `signature`. Không gửi Secret Key trong URL/body, không Base64 hay HMAC khóa.
- Số tiền lấy từ đơn đã lưu; không truyền `amount` để thay đổi số tiền.

## Điều kiện

Create vẫn dùng [API hiện tại](/api/collection-link). Response create phải có `environment: "demo"`: tài khoản là demo và tất cả ngân hàng được chọn đều là demo. Server kiểm tra tài khoản, ngân hàng nhận và cờ demo đã lưu của đơn; không tin cờ môi trường do client tự gửi.

Đơn phải thuộc phạm vi Secret Key, có `ipnUrl` và đang `pending` hoặc đã thanh toán demo trước đó. Đơn production, đơn đã hủy và `orderId` trùng nhiều đơn bị từ chối. Secret Key cửa hàng demo chỉ truy cập đơn cửa hàng đó; Secret Key tài khoản truy cập đơn không gắn cửa hàng, cùng phạm vi create.

Không chuyển tiền thật. API giữ nguyên dữ liệu/yêu cầu hóa đơn từ create, không tự thêm yêu cầu xuất hóa đơn. Luồng hóa đơn demo phụ thuộc cấu hình sandbox.

## Postman

Import [collection và Environment mẫu](/others/postman). Điền `secretKey`, `orderId` rồi chọn:

- **Simulate Payment**: mô phỏng thanh toán, không ép gửi lại IPN nếu đã thanh toán.
- **Resend Demo IPN**: cùng endpoint, với `resendIpn: true` để test lại callback/trigger.

Cả hai đọc Secret Key từ Environment, đặt header Authorization và tạo body. Không có script ký HMAC cho hai request này. Create, Cancel và API HĐĐT vẫn giữ cách xác thực riêng.

## Response

```json
{
  "status": true,
  "message": "Đã ghi nhận giao dịch demo và khớp đơn thành công.",
  "data": {
    "partnerCode": "PAY2S7EPF0SB1ZP27W71",
    "orderId": "DEVd33eb13817916599723201625c7",
    "invoiceNumber": "PAY2SJSCE21278F7A19FE328",
    "environment": "demo",
    "orderStatus": "completed",
    "transactionNumber": "DEMO-20261011021933-1720299E09",
    "alreadyPaid": false,
    "ipnRequeued": false,
    "ipnStatus": "queued"
  }
}
```

`alreadyPaid: true` nghĩa là đơn đã thanh toán trước đó; không có giao dịch mới. `ipnRequeued: true` nghĩa là request này đã đặt lại hàng chờ IPN. `ipnStatus` có thể là `queued`, `sending`, `sent`, `retrying`, `failed` tại thời điểm trả kết quả.

## Gửi và gửi lại IPN

Worker gửi POST tới `ipnUrl` của đơn. **IPN vẫn có chữ ký `m2signature` như thanh toán thật**; chỉ request giả lập được đơn giản hóa. Kiểm tra theo [chữ ký IPN](/others/signature#xác-thực-ipn), phản hồi HTTP 200 và `{"success":true}` khi xử lý xong.

IPN gửi bất đồng bộ; response mô phỏng thành công không đảm bảo callback đã tới. Bộ test cần đợi IPN với thời gian chờ phù hợp. Đối chiếu bằng `orderId`; IPN hiện dùng `requestId` là ID nội bộ, `requestTrace` là requestId từ create.

- Gọi lại thông thường: không tạo giao dịch mới, không ép gửi lại IPN.
- `resendIpn: true`: nếu đơn đã thanh toán và IPN đã gửi/đang retry/hết lượt retry, đưa lại vào hàng chờ.
- IPN đang chờ hoặc đang gửi: giữ lượt hiện có, không tạo lượt song song.
- Đơn pending: thanh toán mô phỏng và gửi IPN lần đầu.

Mỗi lần gọi `resendIpn: true` sau khi lượt trước hoàn tất có thể tạo callback mới. Không dùng cờ này cho retry thanh toán thông thường. Callback giữ mã đơn/giao dịch nhưng thời gian và chữ ký có thể khác; phía nhận phải tránh cộng tiền/kích hoạt dịch vụ trùng.

## Mã lỗi

| HTTP | Ý nghĩa |
| --- | --- |
| 400 | JSON, orderId hoặc kiểu resendIpn không hợp lệ |
| 401 | Thiếu/sai Secret Key |
| 403 | Tài khoản hoặc đơn production |
| 404 | Không có đơn trong phạm vi khóa |
| 409 | Đơn đã hủy, ngân hàng demo không hợp lệ, thiếu ipnUrl hoặc mã đơn bị trùng |
| 500 | Lỗi xử lý; có thể thử lại cùng orderId |

Không tìm thấy đơn thì báo lỗi; không tạo giao dịch độc lập thay thế.


## Request và response đối chiếu ngày 11/10/2026

Các mẫu dưới đây giữ cấu trúc trường và kiểu dữ liệu. Khóa, chữ ký, URL phiên đã được thay bằng placeholder; danh sách chỉ giữ tối đa hai phần tử và dữ liệu ảnh/PDF/XML dài được rút gọn. Không sao chép placeholder để gọi API thật.

### Mô phỏng tiền vào đơn demo

<div class="doc-api-actions" data-doc-api-actions><a class="try-button" data-doc-playground href="/playground/?api=simulate-payment">Thử API trong Playground ↗</a><a class="try-button experience-button" data-doc-experience href="/demos/?flow=payment&api=simulate-payment">Trải nghiệm thanh toán & IPN ↗</a></div>

**Đã gọi API demo/UAT.** 

`POST /v1/gateway/api/simulate-payment`

Body:

```json
{
  "orderId": "DEVd33eb13817916599723201625c7",
  "resendIpn": false
}
```

HTTP 200. Response:

```json
{
  "status": true,
  "message": "Đã ghi nhận giao dịch demo và khớp đơn thành công.",
  "data": {
    "partnerCode": "PAY2S7EPF0SB1ZP27W71",
    "orderId": "DEVd33eb13817916599723201625c7",
    "invoiceNumber": "PAY2SJSCE21278F7A19FE328",
    "environment": "demo",
    "orderStatus": "completed",
    "transactionNumber": "DEMO-20261011021933-1720299E09",
    "alreadyPaid": false,
    "ipnRequeued": false,
    "ipnStatus": "queued"
  }
}
```