@gpmpay/sdk
Version:
Official Node.js SDK for GPM Pay — VietQR codes, transaction webhooks, and payment reconciliation.
317 lines (258 loc) • 18.3 kB
Markdown
# Changelog
Định dạng theo [Keep a Changelog](https://keepachangelog.com/vi/1.1.0/),
phiên bản theo [Semantic Versioning](https://semver.org/lang/vi/).
## [Unreleased]
## [0.4.0]
Sửa các phát hiện từ một đợt đối soát end-to-end do người tích hợp bên ngoài
chạy trên bản `0.3.0` tải từ npm.
> **Về bản `0.3.0` trên npm.** Nó được publish thủ công từ một cây thư mục cũ,
> không đi qua `.github/workflows/publish-sdk.yml`, nên tarball lệch hẳn với
> repo. Cụ thể: `dist/` của nó mang `VERSION = "0.4.0"` trong khi `package.json`
> ghi `0.3.0` — `gpmpay --version` nói dối; nó **thiếu `client.banks`**; và
> CHANGELOG trong tarball giải thích sai rằng `GET /banks` "luôn trả 403 với API
> token". Thực tế backend đã mở route đó bằng `@ApiScopes('bank-accounts:read')`
> và `client.banks.list()` chưa từng bị gỡ khỏi repo. Bản `0.4.0` này là bản
> phát hành đầu tiên khớp đúng với source.
### Breaking
- **`simulator.createTransaction()` đổi kiểu trả về thành
`SimulatedTransactionResult`** — `{ transaction, historyIds }`. Trước đây nó
khai `Promise<Transaction>` nhưng backend chưa bao giờ trả `Transaction` trần,
nên `result.id` / `result.amount` / `result.source` luôn `undefined` lúc chạy.
TypeScript không cảnh báo được vì kiểu khai báo nói dối.
**Chuyển đổi:** đọc `result.transaction` thay cho `result`. Code đang phải vá
quanh bằng `res.transaction` giờ chạy đúng **và** type-check sạch.
`historyIds` là thứ mới dùng được: rỗng nghĩa là không endpoint nào bật
`fireOnSimulated`, tức handler của bạn sẽ không bao giờ được gọi.
- **Gỡ `GpmPayConflictError`.** Không route nào SDK phơi ra có thể sinh 409 —
tất cả đều là đọc, hoặc ghi không có ràng buộc duy nhất. Giữ lại chỉ khiến
người đọc tài liệu viết một nhánh `catch` không bao giờ chạy.
**Chuyển đổi:** một 409 gặp qua `client.request()` nay về dưới dạng
`GpmPayAPIError` với `.status === 409`.
### Fixed
- **`gpmpay --no-color` chạy được.** Cờ này được quảng cáo ở 5 chỗ (`--help`,
cả hai README, cả hai `docs/*/05-api-reference.md`) nhưng CLI từ chối nó ở mọi
vị trí: option được đăng ký dưới tên `color`, còn `node:util.parseArgs` không
hỗ trợ tiền tố `--no-`, nên nhánh `values.color === false` là dead code. Nay
đăng ký đúng tên `no-color`. Kèm theo, khối `--help` đổi từ hằng ở cấp module
thành hàm — trước đó nó gọi `style.bold()` ngay lúc import, tức đã nhuộm màu
xong trước khi cờ kịp có tác dụng.
### Backend đi kèm bản này
Không phải thay đổi của SDK, nhưng người dùng SDK thấy trực tiếp:
- **Id sai định dạng ở path param trả 400 thay vì 500.** Toàn backend trước đó
không dùng `ParseUUIDPipe` ở đâu cả (0/45 `@Param('id')`), và không đăng ký
exception filter nào, nên `PrismaClientKnownRequestError` lọt thẳng ra 500.
Người tích hợp bắt `GpmPayServerError` sẽ retry và gọi on-call cho một lỗi
vĩnh viễn. Query filter `bankAccountId` / `settingId` / `transactionId` cũng
đã đổi sang `@IsUUID()`.
- **`webhookSettings.create()` từ chối URL rác.** DTO phía backend mang **hai**
`@ValidateIf` chồng lên nhau; class-validator AND mọi điều kiện của cùng một
property, nên `url` chỉ được validate khi `driver === GOOGLE_SHEETS`. Với
driver HTTP (mặc định) thì `url: "khong-phai-url"`, `url: 123`, thậm chí thiếu
hẳn `url` đều tạo được endpoint `isActive: true` — rồi mỗi giao dịch tốn 6 lần
thử giao vào một địa chỉ không tồn tại. `http://localhost:3030` vẫn hợp lệ để
không cản đường dev.
## [0.3.0]
Đồng bộ với đợt refactor **gỡ merchant order** ở backend
(`docs/refactor/remove-merchant-order.md`). Toàn bộ luồng "GPM Pay khớp lệnh
thay bạn" đã bị xoá khỏi hệ thống, nên nó cũng biến mất khỏi SDK.
Sau bản này chỉ còn **một** mô hình: bạn tự sinh mã, tự dựng QR, tự đối soát
`payload.content` khi webhook về.
### Breaking
- **Gỡ `client.orders` và toàn bộ `waitForPayment`.** 4 endpoint `/orders*` đã
bị xoá khỏi backend — mọi lời gọi nay trả 404. Kèm theo đó: `classifyOrder`,
`WaitForPaymentOptions`, `WaitForPaymentResult`, `simulator.payOrder()`, và
các type `MerchantOrder`, `MerchantOrderDetail`, `CreateOrderParams`,
`MerchantOrderStatus`.
**Chuyển đổi:** tự sinh mã đơn, dựng QR bằng `buildPaymentInstructions()`,
rồi dò mã đó trong `payload.content` ở handler webhook. Xem
[`docs/vi/02-payments.md`](./docs/vi/02-payments.md).
- **Gỡ 2 scope `orders:read` / `orders:write`** khỏi `ApiTokenScope` và
`ALL_API_SCOPES`. Backend chỉ còn `transactions:read`, `bank-accounts:read`,
`webhooks:manage`. Token cũ chỉ có `orders:write` nay mất quyền gọi simulator
— route đó đã đổi sang `transactions:read`.
- **`WebhookPayload` không còn `code` và `order`.** Backend đã bỏ hai field này
khỏi payload; giữ lại trong type chỉ là tiếp tục nói dối. Payload nay đúng 11
field. **Chuyển đổi:** mã đối soát của bạn nằm trong `content`. Lưu ý
`referenceCode` là mã giao dịch **của ngân hàng**, không phải mã đơn.
- **`Transaction` không còn `matchedOrderId` và `matchedOrder`.** Cột
`matched_order_id` đã bị drop khỏi DB.
- **`buildPaymentInstructions()` đổi tham số.** Trước nhận một `MerchantOrder`
và đọc `order.qrPayload` do server sinh; nay không còn order nào để đọc, nên
nó **tự dựng** QR từ input thuần:
```ts
// 0.2.x
buildPaymentInstructions(order);
// 0.3.0
buildPaymentInstructions({
bankAccount, // từ client.bankAccounts.retrieve(id)
amount: 250_000,
transferContent: 'DH1042', // mã của bạn
});
```
Kết quả bỏ `expiresAt` (hạn thanh toán nay do bạn tự quản), `qrImageUrl` đổi
từ `string | null` thành `string`. Ném `GpmPayConfigError` nếu bank account
thiếu quan hệ `bank` (mã BIN nằm trong đó).
- **Gỡ `REFERENCE_CODE_REGEX`, `parseReferenceCode()`,
`containsReferenceCode()`.** Ba helper này chỉ hiểu định dạng `GPMPAY` + 10
ký tự do server mint — thứ không còn tồn tại. **Chuyển đổi:** dùng regex của
chính bạn cho định dạng mã bạn tự chọn.
- **Thu gọn `client.apiTokens` còn `remove()`.** Không liên quan tới order:
`ApiTokenGuard` của backend là **fail-closed**, route nào không khai báo
`@ApiScopes()` thì mọi API token đều bị chặn. Các route `/api-tokens`
list/create/regenerate/status **luôn trả 403** với API token, nên chúng chưa
từng chạy được. Quản lý ở dashboard. `DELETE /api-tokens/:id` được giữ vì mang
`@AllowAnyApiToken()`.
- **`GpmPayConflictError` bỏ field `externalOrderId`** và phần parse message
409 kèm theo.
- **`PingResult` đổi hình dạng.** Bỏ `token` và `bankAccountCount`, thêm
`scopes: { granted, denied }` — xem mục Changed.
### Fixed
- **`GET /banks` được backend mở lại cho API token** (`@ApiScopes('bank-accounts:read')`).
Trước đó guard fail-closed chặn nhầm một danh mục tham chiếu NAPAS công khai.
`client.banks.list()` vì vậy **vẫn còn** — chỉ cần khi bạn muốn ngân hàng mình
không có tài khoản ở đó; với tài khoản của chính bạn thì `bin` đã đi kèm quan
hệ `bank` trong `bankAccounts.*`.
- **`POST /webhook-settings/telegram/test-connection`** cũng bị thiếu scope
tương tự (5/6 route cùng controller đã có `webhooks:manage`). Đã thêm, nên
luồng cấu hình driver `TELEGRAM` qua API token thử được `chatId` trước.
### Changed
- **`ping()` nay probe từng scope thật.** Trước nó đọc `GET /api-tokens` để lấy
scope của chính token đang dùng; route đó không khai báo scope nên guard
fail-closed **luôn** chặn → `token` vĩnh viễn `null` và `gpmpay ping` mất khả
năng hiện scope. Nay nó bắn song song mỗi scope một lệnh đọc rẻ nhất và suy ra
kết quả. Một scope chỉ vào `denied` khi thực sự nhận 403; lỗi 401 hoặc 5xx
được ném ra ngoài chứ không bị báo nhầm thành thiếu scope.
- **`GpmPayPermissionError.reason` thêm giá trị `'endpoint'`.** Thông điệp
fail-closed `"This endpoint is not available to API tokens"` trước đây rơi vào
nhánh `'ownership'` với câu "tài nguyên thuộc tài khoản khác" — sai, và đẩy
người dùng đi tìm nhầm bug. Union nay là `'scope' | 'endpoint' | 'ownership'`.
### Removed
- Lệnh CLI `gpmpay orders *` và `gpmpay simulate pay <order-id>`; cờ
`--order` của `webhook send` và `--wait` của `simulate`.
- Ví dụ `examples/checkout-poll/` (thuần polling order) và
`examples/express-webhook/` (đã trùng hoàn toàn với `self-reconcile/`).
### Docs
- Viết lại `README.md`, `README.en.md`, `AGENTS.md`, 5 guide `docs/vi/*` và 5
guide `docs/en/*` theo một mô hình duy nhất — bỏ hẳn khái niệm "luồng A /
luồng B".
- `AGENTS.md` thêm cảnh báo tường minh cho AI agent còn nhớ API cũ, và bộ quy
tắc chọn mã đối soát (ngắn, `A-Z0-9`, đặt đầu nội dung, dò bằng regex).
## [0.2.0]
### Breaking
- **`WebhookHistory` không còn `responseStatusCode`, `errorMessage`,
`deliveredAt`.** Cả ba field **chưa bao giờ tồn tại trên dây** — cột thật
trong Prisma là `responseStatus`, và không có `errorMessage`/`deliveredAt`
nào cả. Code đọc `history.responseStatusCode` compile sạch rồi in ra rỗng
lúc chạy, nên giữ lại chỉ là tiếp tục nói dối. Nay khớp đúng wire:
`responseStatus`, `durationMs`, `requestBody`, cùng quan hệ `setting` và
`transaction` mà `list()` thật sự trả về.
- **`WebhookSetting` không còn `hasAuthorizationSecret`, `hasWpSecret`,
`hasTelegramBotToken`.** Backend không bao giờ sinh ra chúng. Thay bằng
`authorizationSecret?: string | null` — là **ciphertext**, không giải mã
được, chỉ dùng như cờ "đã cấu hình secret hay chưa" (`!== null`).
### Added
- **`gpmpay accounts list` / `accounts get <id>`.** Trước đây CLI bắt buộc
`orders create --account <uuid>` nhưng không có cách nào lấy uuid đó — tự mâu
thuẫn. Cột id in nguyên vẹn, không tô màu, không cắt, để copy-paste được.
- **`gpmpay webhook send --url <url>`** — ký một payload mẫu rồi POST thẳng vào
handler của bạn. Là lệnh duy nhất **không cần API token** và không gọi API GPM
Pay, nên dùng được từ trước khi có tài khoản. `--bad-signature` và
`--skew <s>` sinh delivery mà handler đúng **phải** từ chối; `--content`,
`--amount`, `--order`, `--file` để dựng payload theo ý mình.
- **`gpmpay simulate tx` / `simulate pay <order-id>`** — để chính backend bắn
webhook thật. Từ chối chạy trên production trừ khi có `--allow-production`.
- **`gpmpay webhook settings | history | retry <id>`** — xem endpoint đã đăng ký
kèm chế độ xác thực của từng cái, lịch sử giao với mã HTTP và số lần thử, và
đẩy lại một lần giao thất bại.
- Alias: `accounts`/`bank-accounts`, `simulate`/`sim`, `webhook`/`webhooks`.
### Fixed
- **`--json` rò rỉ secret.** API không projection response (dùng Prisma
`include`, không `select`), nên `BankAccount.ingestSecret`,
`verificationCode`, `WebhookSetting.authorizationSecret`, `telegramBotToken`,
`wpSecret` đều đi trên dây dù SDK không khai báo chúng — và `JSON.stringify`
serialize object **lúc chạy**, nên type không bảo vệ được gì. Lỗi này đang
tồn tại: `gpmpay orders get --json` in ra `bankAccount.ingestSecret` từ
trước. Nay mọi đầu ra `--json` đi qua một lớp che chung.
- **`--limit abc` bị bỏ qua im lặng.** `Number('abc')` là `NaN` và query
builder loại giá trị non-finite, nên gõ nhầm nghĩa là "không giới hạn". Nay
báo lỗi exit 2. Tương tự với `--type`, `--status`, `--driver`: mọi cờ nhận
giá trị enum đều được kiểm tra trước khi gửi, thay vì trả về 0 dòng một cách
vô hại.
### Changed
- **Tài liệu mục "Xác thực chữ ký" nay nói đủ ba chế độ.** Backend hỗ trợ
`HMAC`, `API_KEY`, `NONE` và **header khác nhau hoàn toàn** giữa chúng; docs
cũ chỉ nói về HMAC, nên ai chọn `API_KEY` sẽ đi tìm `X-GPMPay-Signature`
không bao giờ tồn tại. Cũng nói rõ: không có header `X-GPMPay-Timestamp`,
driver `HTTP` không gửi `X-GPMPay-Event`, và enum là `HMAC` chứ không phải
`HMAC_SHA256`.
- **`baseUrl` không còn xuất hiện trong hướng dẫn.** Mặc định đã là
`https://api.gpmpay.com`; mục "`baseUrl` nhận mọi cách viết" và mọi URL
localhost đã gỡ khỏi README và các guide, chỉ còn một dòng trong API
reference. `GPMPAY_API_URL` không còn được tài liệu hoá. Hành vi không đổi.
- **Code mẫu đa nền tảng gom vào khối `<details>`** ở
`docs/{vi,en}/03-webhooks.md` (Express mở sẵn, Next.js App/Pages, Fastify,
Hono/Workers/Deno, framework bất kỳ). README chỉ giữ Express inline.
- **README gọn hơn ~20%** (553 → 441 dòng) — bỏ phần trùng lặp với `docs/`.
## [0.1.3]
### Fixed
- **Từ trang npm vẫn không bấm được vào tài liệu.** `0.1.2` gỡ link tương đối
nhưng thay bằng code span (`docs/vi/01-getting-started.md`) — không phải link,
nên ai chưa cài package thì không đọc được gì. Nay bảng tài liệu trong cả hai
README dùng URL tuyệt đối tới <https://unpkg.com/browse/@gpmpay/sdk/>, nơi mọi
file đã publish đều xem được. Bản markdown thô cho AI agent nằm ở
<https://cdn.jsdelivr.net/npm/@gpmpay/sdk/>.
- **Link đổi ngôn ngữ trỏ về chính nó.** Cả `README.md` và `README.en.md` đều
dùng `npmjs.com/package/@gpmpay/sdk?activeTab=readme`, nên bấm "English" chỉ
tải lại đúng README tiếng Việt. Nay `README.md` → `README.en.md` trên unpkg,
và `README.en.md` → trang npm (chính là bản tiếng Việt được render).
- **Link tạo API token trỏ vào trang 404.** `app.gpmpay.com/integrations/api-tokens`
không tồn tại; route đúng là `app.gpmpay.com/api-tokens`. URL này xuất hiện
trong cả README, `AGENTS.md`, guide getting-started, **và trong thông báo lỗi
runtime** (`GpmPayConfigError`, `gpmpay ping`, `gpmpay --help`) — nên user gặp
lỗi token là bị đẩy thẳng vào trang 404. Đã sửa toàn bộ.
### Added
- `AGENTS.md` mở đầu bằng canonical raw URL, để AI agent fetch được file này mà
không cần cài package.
- `bugs.email` để trang npm có kênh liên hệ.
### Notes
- Vẫn **chưa** khai báo `repository`: repo nguồn đang private, khai báo vào chỉ
làm npm hiện một link 404 cho mọi người. Khi nào có repo public cho SDK thì
thêm `repository` + bật `--provenance` trong workflow phát hành.
## [0.1.2]
### Fixed
- **Link tài liệu trong README bị hỏng trên npmjs.com.** README dùng link tương
đối (`./docs/vi/...`), mà npm không map được về đâu nên ghép thẳng vào URL
trang package → `npmjs.com/package/@gpmpay/README.en.md` → 404. Nay README
chỉ dùng đường dẫn trong package và URL tuyệt đối tới trang docs công khai.
- `homepage` trỏ sai (`gpmpay.com/docs#sdk`); đúng là
`app.gpmpay.com/docs#nodejs-sdk`.
### Added
- `docs/` và `examples/` nay được ship kèm package, nên đọc được ngay trong
`node_modules/@gpmpay/sdk/` mà không cần truy cập repo.
## [0.1.1]
Bản phát hành đầu tiên thực sự dùng được.
Số hiệu `0.1.0` đã bị publish rồi gỡ xuống, mà npm không cho tái sử dụng version
đã gỡ — nên bản đầu tiên mang số `0.1.1`. Nội dung không khác `0.1.0`.
## [0.1.0] — đã gỡ, không dùng lại được
Bản đầu tiên.
### Added
- `GpmPay` client — **bắt buộc** API token, constructor ném `GpmPayConfigError`
ngay nếu thiếu hoặc sai định dạng, không đợi tới 401 ở request đầu.
- `GpmPay.fromEnv()` đọc `GPMPAY_API_TOKEN` / `GPMPAY_API_URL`.
- Resource: `orders`, `transactions`, `bankAccounts`, `banks`, `webhookSettings`,
`webhookHistories`, `apiTokens`, `simulator`.
- `orders.waitForPayment()` — poll có backoff, jitter, huỷ được bằng `AbortSignal`.
- `simulator.payOrder()` — giả lập đúng giao dịch làm đơn chuyển sang `PAID`.
- `webhookSettings.createHmacEndpoint()` — tạo endpoint HMAC và trả secret một lần.
- Xác thực webhook: `verifyWebhookSignature`, `assertWebhookSignature`,
`constructWebhookEvent`, `signWebhookPayload`, `verifyApiKeyHeader`.
- Adapter webhook cho Express (`gpmpayWebhook`) và Next.js
(`createNextWebhookHandler`, `verifyNextRequest`, `readRawBody`).
- Helper VietQR: `buildVietQrPayload`, `buildVietQrImageUrl`,
`buildPaymentInstructions`, `parseReferenceCode`, `containsReferenceCode`.
- CLI `gpmpay`: `ping`, `orders create|get|wait|list`, `transactions list`,
`webhook listen|verify`.
- Cây lỗi có kiểu, ánh xạ từ đúng các response 400/401/403/404/409/429/5xx của API.
### Notes
- Zero runtime dependency. Yêu cầu Node >= 18.17.
- Chỉ dùng phía server — API token là secret, không được đưa ra trình duyệt.