{
	"info": {
		"_postman_id": "a10fa8c0-1d2e-4b7a-9f31-alopay000001",
		"name": "AloPay - Merchant API",
		"description": "Bộ request thử nghiệm cổng AloPay (BWoPay Merchant API).\n\n## Trước khi chạy\n\nVào tab **Variables** của collection, điền 4 giá trị lấy ở CMS đối tác AloPay (mục \"API & Tích hợp\"):\n\n- `base_url` — tên miền AloPay, KHÔNG kèm `/api/partner` (collection tự nối)\n- `api_key` — X-Api-Key\n- `api_secret` — API Secret\n- `callback_pin` — Callback PIN\n\n## Vì sao body là `{{_raw_body}}` chứ không viết JSON thẳng\n\nChữ ký ký trên **đúng chuỗi byte** được gửi đi. Nếu viết JSON thẳng vào ô body, Postman có thể đổi khoảng trắng hoặc thứ tự khoá sau khi script đã ký xong — lệch 1 byte là cổng trả 401. Nên script tự dựng chuỗi, ký chuỗi đó, rồi đưa nguyên chuỗi vào body qua biến.\n\nĐừng sửa ô body. Muốn đổi dữ liệu gửi đi thì sửa trong tab **Pre-request Script** của từng request.\n\n## Hai lệnh GET trong cùng một giây\n\nChữ ký của GET không phụ thuộc đường dẫn (mục 11 tài liệu), nên hai lệnh GET khác nhau trong cùng một giây có chữ ký giống hệt nhau và lệnh sau bị coi là phát lại → 401. Các request GET ở đây tự nhích timestamp lên để tránh, bạn không phải canh giờ.\n\n## CẢNH BÁO TIỀN THẬT\n\n- **Tạo đơn RÚT trừ số dư merchant ngay lập tức**, trước khi tiền rời ngân hàng. Thử với số tiền nhỏ.\n- Thư mục **6. Giả lập callback** bắn thẳng vào hệ thống của mình và sẽ **cộng tiền cho đối tác / chốt đơn thật**. Chỉ chạy trên môi trường test, hoặc với đơn test mà bạn chấp nhận hậu quả.",
		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
	},
	"variable": [
		{ "key": "base_url", "value": "https://alopay.biz", "type": "string" },
		{ "key": "api_key", "value": "", "type": "string" },
		{ "key": "api_secret", "value": "", "type": "string" },
		{ "key": "callback_pin", "value": "", "type": "string" },
		{ "key": "hub_callback_url", "value": "https://247pay.vip/api/Callback/AloPay", "type": "string" },
		{ "key": "amount_in", "value": "1000000", "type": "string" },
		{ "key": "amount_out", "value": "50000", "type": "string" },
		{ "key": "bank_code", "value": "MB", "type": "string" },
		{ "key": "account_no", "value": "0000000000", "type": "string" },
		{ "key": "account_name", "value": "NGUYEN VAN A", "type": "string" },
		{ "key": "ref_in", "value": "", "type": "string" },
		{ "key": "ref_out", "value": "", "type": "string" },
		{ "key": "last_code", "value": "", "type": "string" },
		{ "key": "_raw_body", "value": "", "type": "string" },
		{ "key": "_sig", "value": "", "type": "string" },
		{ "key": "_ts", "value": "", "type": "string" },
		{ "key": "_last_get_ts", "value": "0", "type": "string" }
	],
	"item": [
		{
			"name": "1. Tạo đơn NẠP - POST /deposit",
			"event": [
				{
					"listen": "prerequest",
					"script": {
						"type": "text/javascript",
						"exec": [
							"const CryptoJS = require('crypto-js');",
							"",
							"// Mỗi lần gửi sinh ref mới để không đụng khoá chống trùng (mục 9 tài liệu).",
							"// Muốn thử gửi lại ĐÚNG ref cũ (kiểm tra idempotency / lỗi 409) thì comment",
							"// dòng dưới và đặt thẳng: const ref = 'TEST-IN-123';",
							"const ref = 'TEST-IN-' + Date.now();",
							"",
							"const amount = Number(pm.collectionVariables.get('amount_in')) || 1000000;",
							"",
							"// Ký trên đúng chuỗi sẽ gửi. Body của request là {{_raw_body}} nên Postman",
							"// không có cơ hội serialize lại và làm sai chữ ký.",
							"const raw = JSON.stringify({ ref: ref, amount: amount });",
							"",
							"const ts  = String(Math.floor(Date.now() / 1000));",
							"const sig = CryptoJS",
							"    .HmacSHA256(ts + '.' + raw, pm.collectionVariables.get('api_secret'))",
							"    .toString(CryptoJS.enc.Hex);",
							"",
							"pm.collectionVariables.set('ref_in', ref);",
							"pm.collectionVariables.set('_raw_body', raw);",
							"pm.collectionVariables.set('_ts', ts);",
							"pm.collectionVariables.set('_sig', sig);"
						]
					}
				},
				{
					"listen": "test",
					"script": {
						"type": "text/javascript",
						"exec": [
							"const LOI = {",
							"    400: 'Số dư merchant không đủ (chỉ gặp ở đơn rút)',",
							"    401: 'Sai X-Api-Key, sai chữ ký, timestamp lệch quá 300s, chữ ký đã dùng lại, hoặc merchant đang bị khoá',",
							"    403: 'Partner bị khoá / chức năng nạp-rút đang tắt / IP chưa được whitelist',",
							"    409: 'ref này đã dùng cho một đơn khác nội dung',",
							"    422: 'Body sai định dạng - detail là MẢNG object, không phải chuỗi',",
							"    503: 'AloPay đang bảo trì'",
							"};",
							"",
							"if (pm.response.code === 200) {",
							"    const d = pm.response.json();",
							"    pm.collectionVariables.set('last_code', d.code);",
							"",
							"    console.log('Mã đơn AloPay :', d.code);",
							"    console.log('Chuyển tới    :', d.pay_bank_code, '-', d.pay_account_no, '-', d.pay_account_name);",
							"    console.log('Nội dung CK   :', d.pay_content);",
							"    console.log('Hết hạn (UTC) :', d.expire_at, '-> giờ VN cộng thêm 7 tiếng');",
							"",
							"    pm.test('Tạo đơn nạp thành công', () => pm.response.to.have.status(200));",
							"    pm.test('Có đủ thông tin cho khách chuyển khoản', () => {",
							"        pm.expect(d).to.have.property('pay_account_no').and.not.empty;",
							"        pm.expect(d).to.have.property('pay_content').and.not.empty;",
							"    });",
							"    pm.test('Trả lại đúng ref đã gửi', () => {",
							"        pm.expect(d.ref).to.eql(pm.collectionVariables.get('ref_in'));",
							"    });",
							"} else {",
							"    console.log('LỖI ' + pm.response.code + ': ' + (LOI[pm.response.code] || 'chưa rõ'));",
							"    console.log('Cổng trả về  :', pm.response.text());",
							"    pm.test('Tạo đơn nạp thành công', () => pm.response.to.have.status(200));",
							"}"
						]
					}
				}
			],
			"request": {
				"method": "POST",
				"header": [
					{ "key": "Content-Type", "value": "application/json" },
					{ "key": "Accept", "value": "application/json" },
					{ "key": "X-Api-Key", "value": "{{api_key}}" },
					{ "key": "X-Signature", "value": "{{_sig}}" },
					{ "key": "X-Timestamp", "value": "{{_ts}}" }
				],
				"body": {
					"mode": "raw",
					"raw": "{{_raw_body}}",
					"options": { "raw": { "language": "json" } }
				},
				"url": {
					"raw": "{{base_url}}/api/partner/deposit",
					"host": [ "{{base_url}}" ],
					"path": [ "api", "partner", "deposit" ]
				},
				"description": "Body gửi đi: `{\"ref\":\"...\",\"amount\":1000000}`\n\nRàng buộc: `ref` 1-64 ký tự, `amount` nguyên dương, không có phần thập phân.\n\nMã đơn trả về (`code`) được lưu vào biến `last_code` để request tra cứu dùng lại."
			}
		},
		{
			"name": "2. Tạo đơn RÚT - POST /withdraw",
			"event": [
				{
					"listen": "prerequest",
					"script": {
						"type": "text/javascript",
						"exec": [
							"const CryptoJS = require('crypto-js');",
							"",
							"const ref = 'TEST-OUT-' + Date.now();",
							"",
							"// Tên chủ tài khoản: bỏ dấu tiếng Việt cho giống hệ thống thật đang gửi.",
							"const khongDau = (s) => (s || '')",
							"    .normalize('NFD').replace(/[\\u0300-\\u036f]/g, '')",
							"    .replace(/đ/g, 'd').replace(/Đ/g, 'D')",
							"    .toUpperCase();",
							"",
							"// account_name KHÔNG bắt buộc nhưng nên gửi: AloPay không tự xác minh khớp tên.",
							"const payload = {",
							"    ref:          ref,",
							"    amount:       Number(pm.collectionVariables.get('amount_out')) || 50000,",
							"    bank_code:    pm.collectionVariables.get('bank_code'),",
							"    account_no:   pm.collectionVariables.get('account_no'),",
							"    account_name: khongDau(pm.collectionVariables.get('account_name'))",
							"};",
							"",
							"const raw = JSON.stringify(payload);",
							"const ts  = String(Math.floor(Date.now() / 1000));",
							"const sig = CryptoJS",
							"    .HmacSHA256(ts + '.' + raw, pm.collectionVariables.get('api_secret'))",
							"    .toString(CryptoJS.enc.Hex);",
							"",
							"pm.collectionVariables.set('ref_out', ref);",
							"pm.collectionVariables.set('_raw_body', raw);",
							"pm.collectionVariables.set('_ts', ts);",
							"pm.collectionVariables.set('_sig', sig);"
						]
					}
				},
				{
					"listen": "test",
					"script": {
						"type": "text/javascript",
						"exec": [
							"if (pm.response.code === 200) {",
							"    const d = pm.response.json();",
							"    pm.collectionVariables.set('last_code', d.code);",
							"",
							"    console.log('Mã đơn AloPay:', d.code, '| phí:', d.fee, '| trạng thái:', d.status);",
							"    console.log('Số dư merchant đã bị trừ', (d.amount + d.fee), 'ngay lúc này.');",
							"",
							"    pm.test('Tạo đơn rút thành công', () => pm.response.to.have.status(200));",
							"} else if (pm.response.code === 400) {",
							"    console.log('Số dư merchant không đủ:', pm.response.text());",
							"    console.log('Chạy request 4 để xem số dư hiện tại.');",
							"    pm.test('Tạo đơn rút thành công', () => pm.response.to.have.status(200));",
							"} else if (pm.response.code === 403) {",
							"    console.log('Bị chặn - mã NH sai / STK nằm trong danh sách chặn / số tiền ngoài hạn mức / IP chưa whitelist');",
							"    console.log(pm.response.text());",
							"    pm.test('Tạo đơn rút thành công', () => pm.response.to.have.status(200));",
							"} else if (pm.response.code === 422) {",
							"    // 422 trả detail là MẢNG object, khác hẳn các mã lỗi còn lại.",
							"    const detail = pm.response.json().detail || [];",
							"    detail.forEach(e => console.log('Sai tham số:', (e.loc || []).join('.'), '-', e.msg));",
							"    pm.test('Tạo đơn rút thành công', () => pm.response.to.have.status(200));",
							"} else {",
							"    console.log('LỖI ' + pm.response.code + ':', pm.response.text());",
							"    pm.test('Tạo đơn rút thành công', () => pm.response.to.have.status(200));",
							"}"
						]
					}
				}
			],
			"request": {
				"method": "POST",
				"header": [
					{ "key": "Content-Type", "value": "application/json" },
					{ "key": "Accept", "value": "application/json" },
					{ "key": "X-Api-Key", "value": "{{api_key}}" },
					{ "key": "X-Signature", "value": "{{_sig}}" },
					{ "key": "X-Timestamp", "value": "{{_ts}}" }
				],
				"body": {
					"mode": "raw",
					"raw": "{{_raw_body}}",
					"options": { "raw": { "language": "json" } }
				},
				"url": {
					"raw": "{{base_url}}/api/partner/withdraw",
					"host": [ "{{base_url}}" ],
					"path": [ "api", "partner", "withdraw" ]
				},
				"description": "TIỀN THẬT: số dư merchant bị trừ `amount + fee` ngay khi API trả 200, trước khi tiền rời ngân hàng. Nếu đơn bị huỷ sau đó thì tiền được hoàn lại.\n\nSửa số tiền / ngân hàng / số tài khoản ở tab **Variables**: `amount_out`, `bank_code`, `account_no`, `account_name`.\n\n`bank_code` dùng mã Napas: MB, VCB, ACB, TCB..."
			}
		},
		{
			"name": "3. Tra cứu đơn - GET /order/{code}",
			"event": [
				{
					"listen": "prerequest",
					"script": {
						"type": "text/javascript",
						"exec": [
							"const CryptoJS = require('crypto-js');",
							"",
							"// Lệnh GET không có body -> raw_body rỗng, chuỗi ký là '{ts}.' + ''",
							"let ts = Math.floor(Date.now() / 1000);",
							"",
							"// Mục 11 tài liệu: chữ ký của GET KHÔNG phụ thuộc đường dẫn. Hai lệnh GET",
							"// khác nhau trong cùng một giây sẽ có chữ ký giống hệt nhau -> lệnh sau bị",
							"// coi là phát lại và trả 401. Nhích timestamp lên 1 giây để chữ ký khác đi,",
							"// vẫn nằm thoải mái trong cửa sổ cho phép (±300s).",
							"const prev = Number(pm.collectionVariables.get('_last_get_ts')) || 0;",
							"if (ts <= prev) {",
							"    ts = prev + 1;",
							"}",
							"pm.collectionVariables.set('_last_get_ts', String(ts));",
							"",
							"const sig = CryptoJS",
							"    .HmacSHA256(String(ts) + '.', pm.collectionVariables.get('api_secret'))",
							"    .toString(CryptoJS.enc.Hex);",
							"",
							"pm.collectionVariables.set('_ts', String(ts));",
							"pm.collectionVariables.set('_sig', sig);"
						]
					}
				},
				{
					"listen": "test",
					"script": {
						"type": "text/javascript",
						"exec": [
							"if (pm.response.code === 200) {",
							"    const d = pm.response.json();",
							"    console.log('Loại đơn   :', d.type);",
							"    console.log('Trạng thái :', d.status);",
							"    console.log('Callback   :', d.callback, '(none = chưa gửi, success = mình đã trả 200, failed = đang chờ gửi lại)');",
							"    if (d.error) {",
							"        console.log('Lỗi        :', d.error, '| tiền thực nhận:', d.real_amount);",
							"    }",
							"    if (d.type === 'withdraw' && d.status === 'failed') {",
							"        console.log('LƯU Ý: với đơn rút, failed KHÔNG phải kết quả cuối - đơn còn có thể sang success hoặc cancelled.');",
							"    }",
							"    pm.test('Tra cứu được đơn', () => pm.response.to.have.status(200));",
							"} else if (pm.response.code === 404) {",
							"    console.log('Không có đơn này, hoặc đơn thuộc merchant khác. Nhớ dùng mã code của AloPay, KHÔNG phải ref của mình.');",
							"    pm.test('Tra cứu được đơn', () => pm.response.to.have.status(200));",
							"} else {",
							"    console.log('LỖI ' + pm.response.code + ':', pm.response.text());",
							"    pm.test('Tra cứu được đơn', () => pm.response.to.have.status(200));",
							"}"
						]
					}
				}
			],
			"request": {
				"method": "GET",
				"header": [
					{ "key": "Accept", "value": "application/json" },
					{ "key": "X-Api-Key", "value": "{{api_key}}" },
					{ "key": "X-Signature", "value": "{{_sig}}" },
					{ "key": "X-Timestamp", "value": "{{_ts}}" }
				],
				"url": {
					"raw": "{{base_url}}/api/partner/order/{{last_code}}",
					"host": [ "{{base_url}}" ],
					"path": [ "api", "partner", "order", "{{last_code}}" ]
				},
				"description": "`{{last_code}}` tự điền bằng mã đơn của request 1 hoặc 2 vừa chạy. Muốn tra đơn khác thì sửa biến `last_code` ở tab Variables.\n\nPhải dùng mã `code` của AloPay (DH.../RT...), không dùng `ref` của mình."
			}
		},
		{
			"name": "4. Tra số dư - GET /balance",
			"event": [
				{
					"listen": "prerequest",
					"script": {
						"type": "text/javascript",
						"exec": [
							"const CryptoJS = require('crypto-js');",
							"",
							"// Không có body -> chuỗi ký là '{ts}.' + '' (giống GET /order).",
							"let ts = Math.floor(Date.now() / 1000);",
							"",
							"// Mục 11: chữ ký GET không phụ thuộc đường dẫn, nên hai lệnh GET trong",
							"// cùng một giây sẽ trùng chữ ký và lệnh sau bị coi là phát lại (401).",
							"// Nhích timestamp lên cho khác đi, vẫn nằm trong cửa sổ ±300s.",
							"const prev = Number(pm.collectionVariables.get('_last_get_ts')) || 0;",
							"if (ts <= prev) {",
							"    ts = prev + 1;",
							"}",
							"pm.collectionVariables.set('_last_get_ts', String(ts));",
							"",
							"const sig = CryptoJS",
							"    .HmacSHA256(String(ts) + '.', pm.collectionVariables.get('api_secret'))",
							"    .toString(CryptoJS.enc.Hex);",
							"",
							"pm.collectionVariables.set('_ts', String(ts));",
							"pm.collectionVariables.set('_sig', sig);"
						]
					}
				},
				{
					"listen": "test",
					"script": {
						"type": "text/javascript",
						"exec": [
							"if (pm.response.code === 200) {",
							"    const d = pm.response.json();",
							"    console.log('Merchant :', d.merchant);",
							"    console.log('Số dư    :', Number(d.balance).toLocaleString('vi-VN'), 'VND');",
							"",
							"    pm.test('Tra được số dư', () => pm.response.to.have.status(200));",
							"    pm.test('Có trường balance và là số', () => {",
							"        pm.expect(d).to.have.property('balance');",
							"        pm.expect(Number(d.balance)).to.be.a('number').and.not.NaN;",
							"    });",
							"} else if (pm.response.code === 404) {",
							"    console.log('Cổng chưa bật endpoint /balance - báo lại AloPay.');",
							"    pm.test('Tra được số dư', () => pm.response.to.have.status(200));",
							"} else {",
							"    console.log('LỖI ' + pm.response.code + ':', pm.response.text());",
							"    console.log('401 = sai khoá / sai chữ ký / timestamp lệch / chữ ký đã dùng lại / merchant bị khoá');",
							"    pm.test('Tra được số dư', () => pm.response.to.have.status(200));",
							"}"
						]
					}
				}
			],
			"request": {
				"method": "GET",
				"header": [
					{ "key": "Accept", "value": "application/json" },
					{ "key": "X-Api-Key", "value": "{{api_key}}" },
					{ "key": "X-Signature", "value": "{{_sig}}" },
					{ "key": "X-Timestamp", "value": "{{_ts}}" }
				],
				"url": {
					"raw": "{{base_url}}/api/partner/balance",
					"host": [ "{{base_url}}" ],
					"path": [ "api", "partner", "balance" ]
				},
				"description": "Trả về `merchant` và `balance` — số dư VND hiện tại của merchant.\n\nĐây chính là số dư bị trừ khi tạo đơn rút: rút quá số dư thì `/withdraw` trả 400.\n\nKhông có body. Bấm Send hai lần liên tiếp cũng không sao — script tự nhích timestamp để chữ ký khác nhau."
			}
		},
		{
			"name": "5. Thử sai - để biết cổng báo lỗi thế nào",
			"item": [
				{
					"name": "5.1 Sai chữ ký (chờ 401)",
					"event": [
						{
							"listen": "prerequest",
							"script": {
								"type": "text/javascript",
								"exec": [
									"const raw = JSON.stringify({ ref: 'TEST-SAI-CHUKY-' + Date.now(), amount: 1000000 });",
									"pm.collectionVariables.set('_raw_body', raw);",
									"pm.collectionVariables.set('_ts', String(Math.floor(Date.now() / 1000)));",
									"pm.collectionVariables.set('_sig', 'a'.repeat(64));"
								]
							}
						},
						{
							"listen": "test",
							"script": {
								"type": "text/javascript",
								"exec": [
									"console.log('Cổng trả:', pm.response.code, pm.response.text());",
									"pm.test('Chữ ký sai phải bị từ chối 401', () => pm.response.to.have.status(401));",
									"pm.test('KHÔNG được tạo đơn', () => pm.expect(pm.response.code).to.not.eql(200));"
								]
							}
						}
					],
					"request": {
						"method": "POST",
						"header": [
							{ "key": "Content-Type", "value": "application/json" },
							{ "key": "Accept", "value": "application/json" },
							{ "key": "X-Api-Key", "value": "{{api_key}}" },
							{ "key": "X-Signature", "value": "{{_sig}}" },
							{ "key": "X-Timestamp", "value": "{{_ts}}" }
						],
						"body": {
							"mode": "raw",
							"raw": "{{_raw_body}}",
							"options": { "raw": { "language": "json" } }
						},
						"url": {
							"raw": "{{base_url}}/api/partner/deposit",
							"host": [ "{{base_url}}" ],
							"path": [ "api", "partner", "deposit" ]
						},
						"description": "Gửi chữ ký rác. Dùng để xác nhận cổng đang thực sự kiểm tra chữ ký, và để đối chiếu nội dung lỗi."
					}
				},
				{
					"name": "5.2 Timestamp quá hạn (chờ 401)",
					"event": [
						{
							"listen": "prerequest",
							"script": {
								"type": "text/javascript",
								"exec": [
									"const CryptoJS = require('crypto-js');",
									"",
									"const raw = JSON.stringify({ ref: 'TEST-QUAHAN-' + Date.now(), amount: 1000000 });",
									"// Lùi 10 phút - vượt cửa sổ chống phát lại mặc định ±300s.",
									"const ts  = String(Math.floor(Date.now() / 1000) - 600);",
									"const sig = CryptoJS",
									"    .HmacSHA256(ts + '.' + raw, pm.collectionVariables.get('api_secret'))",
									"    .toString(CryptoJS.enc.Hex);",
									"",
									"pm.collectionVariables.set('_raw_body', raw);",
									"pm.collectionVariables.set('_ts', ts);",
									"pm.collectionVariables.set('_sig', sig);"
								]
							}
						},
						{
							"listen": "test",
							"script": {
								"type": "text/javascript",
								"exec": [
									"console.log('Cổng trả:', pm.response.code, pm.response.text());",
									"pm.test('Timestamp quá hạn phải bị từ chối 401', () => pm.response.to.have.status(401));"
								]
							}
						}
					],
					"request": {
						"method": "POST",
						"header": [
							{ "key": "Content-Type", "value": "application/json" },
							{ "key": "Accept", "value": "application/json" },
							{ "key": "X-Api-Key", "value": "{{api_key}}" },
							{ "key": "X-Signature", "value": "{{_sig}}" },
							{ "key": "X-Timestamp", "value": "{{_ts}}" }
						],
						"body": {
							"mode": "raw",
							"raw": "{{_raw_body}}",
							"options": { "raw": { "language": "json" } }
						},
						"url": {
							"raw": "{{base_url}}/api/partner/deposit",
							"host": [ "{{base_url}}" ],
							"path": [ "api", "partner", "deposit" ]
						},
						"description": "Chữ ký tính đúng nhưng timestamp lùi 10 phút. Nếu request này vẫn ra 200 thì cổng KHÔNG kiểm tra cửa sổ thời gian - báo lại AloPay."
					}
				},
				{
					"name": "5.3 Dùng lại ref với số tiền khác (chờ 409)",
					"event": [
						{
							"listen": "prerequest",
							"script": {
								"type": "text/javascript",
								"exec": [
									"const CryptoJS = require('crypto-js');",
									"",
									"// Dùng lại ref của đơn nạp vừa tạo nhưng đổi số tiền -> phải bị 409.",
									"const ref = pm.collectionVariables.get('ref_in');",
									"if (!ref) {",
									"    console.log('Chạy request 1 (tạo đơn nạp) trước đã.');",
									"}",
									"",
									"const amount = (Number(pm.collectionVariables.get('amount_in')) || 1000000) + 12345;",
									"const raw = JSON.stringify({ ref: ref, amount: amount });",
									"const ts  = String(Math.floor(Date.now() / 1000));",
									"const sig = CryptoJS",
									"    .HmacSHA256(ts + '.' + raw, pm.collectionVariables.get('api_secret'))",
									"    .toString(CryptoJS.enc.Hex);",
									"",
									"pm.collectionVariables.set('_raw_body', raw);",
									"pm.collectionVariables.set('_ts', ts);",
									"pm.collectionVariables.set('_sig', sig);"
								]
							}
						},
						{
							"listen": "test",
							"script": {
								"type": "text/javascript",
								"exec": [
									"console.log('Cổng trả:', pm.response.code, pm.response.text());",
									"pm.test('ref trùng nội dung khác phải bị 409', () => pm.response.to.have.status(409));"
								]
							}
						}
					],
					"request": {
						"method": "POST",
						"header": [
							{ "key": "Content-Type", "value": "application/json" },
							{ "key": "Accept", "value": "application/json" },
							{ "key": "X-Api-Key", "value": "{{api_key}}" },
							{ "key": "X-Signature", "value": "{{_sig}}" },
							{ "key": "X-Timestamp", "value": "{{_ts}}" }
						],
						"body": {
							"mode": "raw",
							"raw": "{{_raw_body}}",
							"options": { "raw": { "language": "json" } }
						},
						"url": {
							"raw": "{{base_url}}/api/partner/deposit",
							"host": [ "{{base_url}}" ],
							"path": [ "api", "partner", "deposit" ]
						},
						"description": "Chạy request 1 trước. Request này gửi lại đúng `ref` đó nhưng số tiền lệch 12.345đ -> cổng phải trả 409.\n\nGửi lại đúng ref VÀ đúng nội dung thì ngược lại: trả 200 với chính đơn cũ, không tạo đơn mới."
					}
				}
			],
			"description": "Mấy request cố tình sai, để biết cổng phản ứng thế nào và đối chiếu với cách hệ thống mình đang đọc lỗi. Không tạo ra đơn nào."
		},
		{
			"name": "6. Giả lập callback AloPay gửi về hệ thống mình",
			"item": [
				{
					"name": "6.1 Nạp THÀNH CÔNG (hệ thống sẽ cộng tiền)",
					"event": [
						{
							"listen": "prerequest",
							"script": {
								"type": "text/javascript",
								"exec": [
									"const CryptoJS = require('crypto-js');",
									"",
									"const amount = Number(pm.collectionVariables.get('amount_in')) || 1000000;",
									"",
									"const raw = JSON.stringify({",
									"    type:     'deposit',",
									"    code:     pm.collectionVariables.get('last_code') || 'DH_TEST_0001',",
									"    ref:      pm.collectionVariables.get('ref_in'),",
									"    merchant: 'PARTNERCODE',",
									"    amount:   amount,",
									"    net:      Math.round(amount * 0.99),",
									"    status:   'success',",
									"    pin:      pm.collectionVariables.get('callback_pin')",
									"});",
									"",
									"// Callback ký KHÁC request: chỉ HMAC(raw_body), KHÔNG ghép timestamp.",
									"const sig = CryptoJS",
									"    .HmacSHA256(raw, pm.collectionVariables.get('api_secret'))",
									"    .toString(CryptoJS.enc.Hex);",
									"",
									"pm.collectionVariables.set('_raw_body', raw);",
									"pm.collectionVariables.set('_sig', sig);"
								]
							}
						},
						{
							"listen": "test",
							"script": {
								"type": "text/javascript",
								"exec": [
									"console.log('Hệ thống mình trả:', pm.response.code, pm.response.text());",
									"pm.test('Hệ thống nhận callback (200)', () => pm.response.to.have.status(200));",
									"console.log('Gửi lại lần nữa phải vẫn ra 200 nhưng KHÔNG cộng tiền thêm lần hai - bấm Send lần nữa rồi kiểm tra số dư đối tác.');"
								]
							}
						}
					],
					"request": {
						"method": "POST",
						"header": [
							{ "key": "Content-Type", "value": "application/json" },
							{ "key": "X-Signature", "value": "{{_sig}}" }
						],
						"body": {
							"mode": "raw",
							"raw": "{{_raw_body}}",
							"options": { "raw": { "language": "json" } }
						},
						"url": {
							"raw": "{{hub_callback_url}}",
							"host": [ "{{hub_callback_url}}" ]
						},
						"description": "CỘNG TIỀN THẬT cho đối tác của đơn `ref_in`. Chỉ chạy khi bạn hiểu rõ đơn đó là đơn test.\n\nBấm Send hai lần để kiểm tra chống trùng: lần hai vẫn phải ra 200 nhưng số dư đối tác không đổi."
					}
				},
				{
					"name": "6.2 Nạp LỖI SAI_SO_TIEN (không được cộng tiền)",
					"event": [
						{
							"listen": "prerequest",
							"script": {
								"type": "text/javascript",
								"exec": [
									"const CryptoJS = require('crypto-js');",
									"",
									"const amount = Number(pm.collectionVariables.get('amount_in')) || 1000000;",
									"",
									"const raw = JSON.stringify({",
									"    type:        'deposit',",
									"    code:        pm.collectionVariables.get('last_code') || 'DH_TEST_0001',",
									"    ref:         pm.collectionVariables.get('ref_in'),",
									"    merchant:    'PARTNERCODE',",
									"    amount:      amount,",
									"    net:         Math.round(amount * 0.99),",
									"    status:      'failed',",
									"    error:       'SAI_SO_TIEN',",
									"    real_amount: amount - 50000,",
									"    pin:         pm.collectionVariables.get('callback_pin')",
									"});",
									"",
									"const sig = CryptoJS",
									"    .HmacSHA256(raw, pm.collectionVariables.get('api_secret'))",
									"    .toString(CryptoJS.enc.Hex);",
									"",
									"pm.collectionVariables.set('_raw_body', raw);",
									"pm.collectionVariables.set('_sig', sig);"
								]
							}
						},
						{
							"listen": "test",
							"script": {
								"type": "text/javascript",
								"exec": [
									"console.log('Hệ thống mình trả:', pm.response.code, pm.response.text());",
									"pm.test('Hệ thống nhận callback (200)', () => pm.response.to.have.status(200));",
									"console.log('KIỂM TRA: đơn phải chuyển sang lỗi, ghi số tiền thực nhận, và TUYỆT ĐỐI không cộng tiền cho đối tác - vì AloPay cũng chưa cộng cho mình.');"
								]
							}
						}
					],
					"request": {
						"method": "POST",
						"header": [
							{ "key": "Content-Type", "value": "application/json" },
							{ "key": "X-Signature", "value": "{{_sig}}" }
						],
						"body": {
							"mode": "raw",
							"raw": "{{_raw_body}}",
							"options": { "raw": { "language": "json" } }
						},
						"url": {
							"raw": "{{hub_callback_url}}",
							"host": [ "{{hub_callback_url}}" ]
						},
						"description": "Tiền đã về ngân hàng nhưng sai số -> AloPay KHÔNG cộng cho mình, nên mình cũng không được cộng cho đối tác. Đổi `error` thành SAI_BANK / QUA_HAN / SAI_CODE để thử các trường hợp còn lại."
					}
				},
				{
					"name": "6.3 Rút THÀNH CÔNG",
					"event": [
						{
							"listen": "prerequest",
							"script": {
								"type": "text/javascript",
								"exec": [
									"const CryptoJS = require('crypto-js');",
									"",
									"const raw = JSON.stringify({",
									"    type:     'withdraw',",
									"    code:     pm.collectionVariables.get('last_code') || 'RT_TEST_0001',",
									"    ref:      pm.collectionVariables.get('ref_out'),",
									"    merchant: 'PARTNERCODE',",
									"    amount:   Number(pm.collectionVariables.get('amount_out')) || 50000,",
									"    status:   'success',",
									"    pin:      pm.collectionVariables.get('callback_pin')",
									"});",
									"",
									"const sig = CryptoJS",
									"    .HmacSHA256(raw, pm.collectionVariables.get('api_secret'))",
									"    .toString(CryptoJS.enc.Hex);",
									"",
									"pm.collectionVariables.set('_raw_body', raw);",
									"pm.collectionVariables.set('_sig', sig);"
								]
							}
						},
						{
							"listen": "test",
							"script": {
								"type": "text/javascript",
								"exec": [
									"console.log('Hệ thống mình trả:', pm.response.code, pm.response.text());",
									"pm.test('Hệ thống nhận callback (200)', () => pm.response.to.have.status(200));"
								]
							}
						}
					],
					"request": {
						"method": "POST",
						"header": [
							{ "key": "Content-Type", "value": "application/json" },
							{ "key": "X-Signature", "value": "{{_sig}}" }
						],
						"body": {
							"mode": "raw",
							"raw": "{{_raw_body}}",
							"options": { "raw": { "language": "json" } }
						},
						"url": {
							"raw": "{{hub_callback_url}}",
							"host": [ "{{hub_callback_url}}" ]
						},
						"description": "Chốt đơn rút `ref_out` thành công và callback về cho đối tác."
					}
				},
				{
					"name": "6.4 Rút BỊ HUỶ (hoàn tiền / treo đơn lớn)",
					"event": [
						{
							"listen": "prerequest",
							"script": {
								"type": "text/javascript",
								"exec": [
									"const CryptoJS = require('crypto-js');",
									"",
									"const raw = JSON.stringify({",
									"    type:     'withdraw',",
									"    code:     pm.collectionVariables.get('last_code') || 'RT_TEST_0001',",
									"    ref:      pm.collectionVariables.get('ref_out'),",
									"    merchant: 'PARTNERCODE',",
									"    amount:   Number(pm.collectionVariables.get('amount_out')) || 50000,",
									"    status:   'cancelled',",
									"    pin:      pm.collectionVariables.get('callback_pin')",
									"});",
									"",
									"const sig = CryptoJS",
									"    .HmacSHA256(raw, pm.collectionVariables.get('api_secret'))",
									"    .toString(CryptoJS.enc.Hex);",
									"",
									"pm.collectionVariables.set('_raw_body', raw);",
									"pm.collectionVariables.set('_sig', sig);"
								]
							}
						},
						{
							"listen": "test",
							"script": {
								"type": "text/javascript",
								"exec": [
									"console.log('Hệ thống mình trả:', pm.response.code, pm.response.text());",
									"pm.test('Hệ thống nhận callback (200)', () => pm.response.to.have.status(200));",
									"console.log('Đơn dưới 100 triệu: huỷ + hoàn tiền đối tác.');",
									"console.log('Đơn từ 100 triệu trở lên (chỉ hub): TREO lại, xoá cổng, không hoàn, không callback cho đối tác - để nhân viên đẩy cổng khác.');"
								]
							}
						}
					],
					"request": {
						"method": "POST",
						"header": [
							{ "key": "Content-Type", "value": "application/json" },
							{ "key": "X-Signature", "value": "{{_sig}}" }
						],
						"body": {
							"mode": "raw",
							"raw": "{{_raw_body}}",
							"options": { "raw": { "language": "json" } }
						},
						"url": {
							"raw": "{{hub_callback_url}}",
							"host": [ "{{hub_callback_url}}" ]
						},
						"description": "Muốn thử luật treo đơn lớn: đặt `amount_out` từ 100000000 trở lên, chạy lại request 2 rồi chạy request này."
					}
				},
				{
					"name": "6.5 Callback SAI PIN (hệ thống phải từ chối)",
					"event": [
						{
							"listen": "prerequest",
							"script": {
								"type": "text/javascript",
								"exec": [
									"const CryptoJS = require('crypto-js');",
									"",
									"// Chữ ký ĐÚNG nhưng PIN sai. Nếu hệ thống mình trả 200 là có lỗ hổng:",
									"// ai lấy được secret mà không có PIN vẫn chốt được đơn.",
									"const raw = JSON.stringify({",
									"    type:     'deposit',",
									"    code:     pm.collectionVariables.get('last_code') || 'DH_TEST_0001',",
									"    ref:      pm.collectionVariables.get('ref_in'),",
									"    merchant: 'PARTNERCODE',",
									"    amount:   Number(pm.collectionVariables.get('amount_in')) || 1000000,",
									"    net:      Number(pm.collectionVariables.get('amount_in')) || 1000000,",
									"    status:   'success',",
									"    pin:      '000000'",
									"});",
									"",
									"const sig = CryptoJS",
									"    .HmacSHA256(raw, pm.collectionVariables.get('api_secret'))",
									"    .toString(CryptoJS.enc.Hex);",
									"",
									"pm.collectionVariables.set('_raw_body', raw);",
									"pm.collectionVariables.set('_sig', sig);"
								]
							}
						},
						{
							"listen": "test",
							"script": {
								"type": "text/javascript",
								"exec": [
									"console.log('Hệ thống mình trả:', pm.response.code, pm.response.text());",
									"pm.test('Sai PIN phải bị từ chối, KHÔNG được trả 200', () => {",
									"    pm.expect(pm.response.code).to.not.eql(200);",
									"});",
									"pm.test('Trả về 400', () => pm.response.to.have.status(400));"
								]
							}
						}
					],
					"request": {
						"method": "POST",
						"header": [
							{ "key": "Content-Type", "value": "application/json" },
							{ "key": "X-Signature", "value": "{{_sig}}" }
						],
						"body": {
							"mode": "raw",
							"raw": "{{_raw_body}}",
							"options": { "raw": { "language": "json" } }
						},
						"url": {
							"raw": "{{hub_callback_url}}",
							"host": [ "{{hub_callback_url}}" ]
						},
						"description": "Kiểm tra phòng thủ của hệ thống mình. Phải ra 400 và không đụng gì tới đơn."
					}
				}
			],
			"description": "CẨN THẬN - mấy request này bắn thẳng vào hệ thống của mình và làm thay đổi đơn / số dư thật.\n\nĐổi đích bằng biến `hub_callback_url`. Với gatehubpay thì đường dẫn không có tiền tố `/api`."
		}
	]
}
