SDK Collection Link V2 — Node.js/TypeScript và PHP

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

# SDK Collection Link V2 — Node.js/TypeScript và PHP

SDK 0.1.0 đóng gói tạo thanh toán V2 và kiểm tra IPN. Dùng trên **backend của merchant**, không dùng ở trình duyệt/mobile vì cần Secret Key. Bản này được phân phối bằng file tải, **chưa publish lên npm hoặc Packagist**.

- <a href="/downloads/pay2s-collection-link-sdk-0.1.0.tgz" download>Tải SDK Node.js/TypeScript (.tgz)</a>
- [Tải SDK PHP (.zip)](/downloads/pay2s-collection-link-php-0.1.0.zip)

## 1. Cài đặt

### Node.js >=20

Tải file `.tgz` về dự án, sau đó:

```bash
npm install ./pay2s-collection-link-sdk-0.1.0.tgz
```

```js
const { Pay2S, Pay2SError } = require('@pay2s/collection-link-sdk');
```

TypeScript/ESM:

```ts
import { Pay2S, type PaymentInput } from '@pay2s/collection-link-sdk';
```

### PHP >=8.1

Cần extension `curl`, `json`, `mbstring`. Giải nén ZIP vào `packages/pay2s-collection-link/` trong dự án (file `composer.json` của SDK nằm ngay trong thư mục này). Thêm vào `composer.json` **của dự án**, giữ lại cấu hình có sẵn:

```json
{
  "repositories": [
    {"type":"path","url":"packages/pay2s-collection-link","options":{"symlink":false}}
  ]
}
```

Sau đó chạy:

```bash
composer require pay2s/collection-link-sdk:0.1.0
```

```php
require __DIR__ . '/vendor/autoload.php';
use Pay2S\CollectionLink\Pay2S;
```

Nếu chưa dùng Composer, có thể require trực tiếp `src/Pay2SError.php` và `src/Pay2S.php` trong thư mục SDK.

## 2. Cấu hình

Đặt ở môi trường backend, không commit giá trị thật:

```dotenv
PAY2S_PARTNER_CODE=
PAY2S_ACCESS_KEY=
PAY2S_SECRET_KEY=
```

```js
const pay2s = new Pay2S({
  partnerCode: process.env.PAY2S_PARTNER_CODE,
  accessKey: process.env.PAY2S_ACCESS_KEY,
  secretKey: process.env.PAY2S_SECRET_KEY,
  timeoutMs: 30000
});
```

Endpoint được cố định là `https://payment.pay2s.vn/v1/gateway/api/create`. SDK không tự tạo credential, kết nối ngân hàng hoặc cấu hình HĐĐT trên Pay2S.

## 3. Tạo thanh toán

Lưu đơn và `requestId` ở database trước khi gọi. Lấy tiền và sản phẩm từ dữ liệu server, không tin tổng tiền do trình duyệt gửi lên.

```js
const result = await pay2s.createPayment({
  orderId: 'ORDER_20260918_001',
  requestId: 'REQUEST_20260918_001',
  orderInfo: 'TT ORDER_20260918_001',
  amount: 110000,
  bankAccounts: [{ account_number: 'YOUR_ACCOUNT_NUMBER', bank_id: 'ACB' }],
  redirectUrl: 'https://shop.example/payment/return',
  ipnUrl: 'https://shop.example/api/pay2s/ipn',
  metadata: {
    invoiceType: 'vat',
    customerInfo: { buyerContactName: 'Bán cho người tiêu dùng' },
    items: [{ itemName: 'Sản phẩm mẫu', quantity: 1, unitPrice: 100000, taxRate: 10 }],
    invoiceOptions: { requested: true, buyerNotTakingInvoice: true, source: 'api' }
  }
});

// Lưu result.orderInfo (mã P2S... thực tế), result.payUrl vào đơn ở database.
// Sau khi lưu xong mới trả result.payUrl cho trình duyệt để khách thanh toán.
```

`metadata` là object JSON, **chưa Base64**. SDK tạo `extraData`, cố định `signatureVersion: "2"`, ký HMAC đúng thứ tự và gửi request. Dùng `itemName`, không dùng `productName`. Toàn bộ metadata người mua/hàng hóa xem [Collection Link V2](/api/collection-link-v2).

SDK yêu cầu `orderId`, `requestId`, `orderInfo` do website cung cấp; không tự sinh để tránh đổi mã khi gọi lại. `amount` là số hoặc chuỗi thập phân dương, tối đa 10 tỷ VND, tối đa 2 chữ số thập phân. Số tài khoản là chuỗi. `quantity`, `unitPrice`, `taxRate` là số JSON. Metadata tối đa 50 dòng, Base64 tối đa 32 KiB.

SDK kiểm tra định dạng cơ bản nhưng **không tự tính tổng hoặc suy đoán loại mẫu/thuế**. Merchant phải gửi tổng thanh toán khớp dữ liệu đơn và hàng hóa, đặc biệt khi có giảm thuế trực tiếp.

### PHP tương đương

```php
$pay2s = new Pay2S([
    'partnerCode' => getenv('PAY2S_PARTNER_CODE'),
    'accessKey' => getenv('PAY2S_ACCESS_KEY'),
    'secretKey' => getenv('PAY2S_SECRET_KEY'),
]);
$result = $pay2s->createPayment([
    'orderId' => 'ORDER_20260918_001',
    'requestId' => 'REQUEST_20260918_001',
    'orderInfo' => 'TT ORDER_20260918_001',
    'amount' => 110000,
    'bankAccounts' => [['account_number' => 'YOUR_ACCOUNT_NUMBER', 'bank_id' => 'ACB']],
    'redirectUrl' => 'https://shop.example/payment/return',
    'ipnUrl' => 'https://shop.example/api/pay2s/ipn',
    'metadata' => [
        'invoiceType' => 'vat',
        'customerInfo' => ['buyerContactName' => 'Bán cho người tiêu dùng'],
        'items' => [['itemName' => 'Sản phẩm mẫu', 'quantity' => 1, 'unitPrice' => 100000, 'taxRate' => 10]],
        'invoiceOptions' => ['requested' => true, 'buyerNotTakingInvoice' => true, 'source' => 'api'],
    ],
]);
// Lưu $result['orderInfo'], $result['payUrl'] vào đơn trước khi trả URL cho khách.
```

Muốn tự dùng HTTP client sẵn có: gọi `buildPayment(input)` để lấy payload đã ký, rồi POST JSON **nguyên payload** tới endpoint. Không sửa trường đã ký và không ký lại theo công thức V1.

## 4. Nhận IPN và xử lý đơn

`verifyIpn(data)` trả boolean, chỉ kiểm tra chữ ký `m2signature` và danh tính bộ khóa. `assertPaidIpn(data, expected)` kiểm tra thêm `resultCode = 0`, mã đơn, số tiền; kiểm tra `orderInfo` nếu truyền giá trị đã lưu từ response.

```js
if (!pay2s.verifyIpn(req.body)) {
  return res.status(401).json({ success: false });
}

// Tìm đơn trong database theo req.body.orderId, giới hạn đúng tài khoản merchant.
// expected phải lấy từ database, KHÔNG copy số tiền từ chính req.body.
pay2s.assertPaidIpn(req.body, {
  orderId: storedOrder.orderId,
  amount: storedOrder.amount,
  orderInfo: storedOrder.pay2sOrderInfo
});
```

PHP:

```php
$ipn = json_decode(file_get_contents('php://input'), true, 512, JSON_THROW_ON_ERROR);
if (!is_array($ipn) || !$pay2s->verifyIpn($ipn)) {
    http_response_code(401);
    header('Content-Type: application/json');
    echo json_encode(['success' => false]);
    exit;
}
// $storedOrder lấy từ database và khóa trong transaction ở bước xử lý bên dưới.
$pay2s->assertPaidIpn($ipn, [
    'orderId' => $storedOrder['order_id'],
    'amount' => $storedOrder['amount'],
    'orderInfo' => $storedOrder['pay2s_order_info'],
]);
```

Các đoạn trên minh họa gọi SDK; `req/res`, `storedOrder` và transaction thuộc ứng dụng của bạn. Khi triển khai endpoint IPN, phải có đầy đủ luồng:

1. Parse JSON có giới hạn dung lượng; dữ liệu sai trả 400, chữ ký sai trả 401.
2. Xác thực chữ ký trước khi xử lý dữ liệu thanh toán.
3. Bắt đầu transaction, đọc và **khóa đơn** theo `orderId` đúng tài khoản merchant.
4. Đối chiếu số tiền và `orderInfo` đã lưu bằng `assertPaidIpn`.
5. Nếu đơn đã paid: không cộng tiền/giao hàng lần hai. Nếu chưa: cập nhật trạng thái paid cùng mã giao dịch và ghi tác vụ giao hàng vào outbox trong cùng transaction.
6. Commit rồi trả **HTTP 200, `{"success":true}`** cho cả lần đầu và IPN hợp lệ gửi lại. Nếu database lỗi, rollback và trả lỗi để Pay2S có thể gửi lại.

Nếu IPN có chữ ký hợp lệ nhưng `resultCode` khác 0, `assertPaidIpn` ném lỗi `NOT_PAID`; xử lý như thông báo không thành công, tuyệt đối không cộng tiền. Xử lý exception trong handler của bạn, không để lỗi SDK làm treo request.

SDK **không tự lưu trạng thái hoặc chống replay bằng database**. Hai IPN giống nhau đều có chữ ký hợp lệ. Việc khóa đơn và cập nhật có điều kiện là bắt buộc để tránh cộng tiền hai lần.

`requestId` trong IPN có thể là ID nội bộ Pay2S, không bắt buộc bằng requestId lúc tạo đơn. `requestTrace` có thể xuất hiện nhưng không nằm trong chuỗi ký, không dùng nó để xác nhận thanh toán. Luôn nhận diện đơn bằng `orderId` đã ký và đối chiếu tiền từ database.

Không dùng thông tin redirect trình duyệt để đánh dấu paid. Chữ ký redirect khác chữ ký IPN và sẽ không qua `verifyIpn`.

## 5. Lỗi và gọi lại

Node.js dùng `error.code`; PHP dùng `$error->errorCode`. Cả hai có `httpStatus` và `resultCode` khi có phản hồi API.

| Mã SDK | Xử lý |
|---|---|
| `INVALID_INPUT` | Sửa dữ liệu trước khi gọi; request chưa được gửi |
| `TRANSPORT_ERROR` | Mạng/timeout; kết quả có thể chưa rõ, giữ orderId và đối soát |
| `INVALID_RESPONSE` | Phản hồi không đúng JSON/cấu trúc; không kết luận đơn chưa tạo |
| `API_ERROR` | Kiểm tra HTTP/resultCode; có thể là từ chối, rate limit hoặc lỗi hệ thống |
| `INVALID_IPN` | Không xử lý thanh toán |
| `NOT_PAID` | Không xác nhận thành công, không giao hàng/cộng tiền |
| `ORDER_MISMATCH` | Đối soát mã đơn/số tiền/nội dung; không đánh dấu paid |

Không tự retry trong SDK. Nếu cần thử lại sau lỗi mạng, kiểm tra đơn/IPN trước và giữ mã đơn gốc; không tạo orderId mới chỉ vì timeout. Không đánh dấu thanh toán thất bại vĩnh viễn chỉ vì request tạo link bị timeout.

## 6. Kiểm thử và hướng dẫn cho AI tích hợp

Gói SDK có test fixture dùng khóa giả và transport mô phỏng. Node: `node --test test.cjs`; PHP: `php tests.php` trong thư mục gói. Test không gọi API thật.

Khi giao tài liệu cho AI coding assistant, yêu cầu:

> Dùng SDK Collection Link V2 của Pay2S để tích hợp vào backend hiện tại. Đọc tài liệu SDK, Collection Link V2 và IPN. Tái sử dụng model đơn hàng, quản lý bí mật, transaction và queue sẵn có. Tạo endpoint thanh toán cùng endpoint IPN có kiểm tra chữ ký, đối chiếu dữ liệu từ database, khóa đơn và chống xử lý trùng. Không xác nhận thanh toán từ redirect. Không tự đổi orderId khi timeout. Viết test cho chữ ký sai, IPN gửi lại/đồng thời, sai tiền và database lỗi. Liệt kê cấu hình cần cung cấp, migration và file đã sửa.

Đây là SDK **thanh toán kèm metadata HĐĐT**. Chưa bao gồm API HĐĐT độc lập hoặc tra cứu MST. HĐĐT chỉ vào hàng chờ/tự động sau khi thanh toán theo cấu hình Pay2S; không tự gọi thêm API hóa đơn độc lập cho cùng đơn để tránh xuất trùng.