# SDK JavaScript

`@hatang/sdk` gói gọn toàn bộ tích hợp Thanh toán: tạo đơn, xác nhận thanh toán, xác minh IPN và webhook. Mọi hàm API trả về `{ data, error }` và không throw khi API báo lỗi. Các hàm xác minh chỉ dùng Web Crypto nên chạy được cả trên Bun, Deno, Vercel và Cloudflare Workers.

Trang này nói về phần thanh toán. Bản thân client (tùy chọn, lỗi, các sản phẩm khác) được mô tả ở [SDK Node.js](https://hatang.dev/vi/docs/guides/sdks/node.md).

## Cài đặt và cấu hình

```bash
npm install @hatang/sdk
```

```ts title="lib/hatang.ts"
import { Hatang } from "@hatang/sdk";

export const hatang = new Hatang(process.env.HATANG_API_KEY, {
  baseUrl: "https://api.hatang.dev",
  // Bỏ ở production, hoặc đặt HATANG_ENVIRONMENT=development thay cho dòng này.
  environment: "development",
});
```

| Tùy chọn | Biến môi trường | Ý nghĩa |
|---|---|---|
| tham số đầu tiên | `HATANG_API_KEY` | API key (`ht_…`) có `payments:read` và `payments:write` |
| `baseUrl` | `HATANG_BASE_URL` | `https://api.hatang.dev` |
| `environment` | `HATANG_ENVIRONMENT` | `development` hoặc `production`, gửi dưới dạng header `Hatang-Environment` |

## 1. Tạo payment cho đơn hàng

`createOrder` dùng mã đơn làm idempotency key: gọi hai lần cho cùng một đơn (bấm đúp, thử lại request) trả về cùng một payment. Mã đơn cũng được lưu vào `metadata.order_id`.

```ts
const { data: payment, error } = await hatang.payments.createOrder({
  order_id: "SO-1001",
  amount: 150_000,
  description: "Đơn hàng #1001",
  return_url: "https://shop.example.vn/orders/SO-1001",
  ipn_url: "https://shop.example.vn/api/hatang/ipn",
  // Tùy chọn: phát hành hóa đơn điện tử khi đã thanh toán.
  invoice: {
    buyer: { name: "Nguyễn Văn Khách", tax_code: "0109876543", email: "ketoan@khach.vn" },
    items: [{ name: "Gói dịch vụ", quantity: 1, unit_price: 136_364, vat_rate: 10 }],
  },
});
if (error) throw new Error(`${error.name}: ${error.message}`);

// Chuyển người mua tới trang checkout…
redirect(payment.checkout_url);
// …hoặc tự hiển thị QR: <img src={payment.qr.image_url} />, ngân hàng ở payment.bank, mã ở payment.code.
```

Thêm `code: "SO1001"` để dùng mã đơn của bạn làm nội dung chuyển khoản thay cho mã `HT…` tự sinh. Xem [Mã thanh toán](https://hatang.dev/vi/docs/guides/payments/create-payment.md#payment-code).

Gọi lại `createOrder` cho cùng `order_id` nhưng với trường **khác** sẽ trả về `invalid_idempotent_request`. Khi đơn thay đổi, hãy dùng mã đơn mới.

## 2. Nhận IPN

Truyền body gốc và headers. `constructIpnEvent` kiểm tra chữ ký HMAC và trả về lời gọi đã parse, hoặc throw `HatangSignatureError`.

```ts title="app/api/hatang/ipn/route.ts (Next.js)"
import { constructIpnEvent, HatangSignatureError } from "@hatang/sdk";

export async function POST(req: Request) {
  const raw = await req.text(); // trước khi parse JSON
  try {
    const ipn = await constructIpnEvent(raw, req.headers, process.env.HATANG_SECRET_KEY!);
    if (await alreadyHandled(ipn.id)) return new Response(null, { status: 200 });
    const order = ipn.data.metadata.order_id as string;
    if (ipn.data.status === "paid" || ipn.data.status === "overpaid") await markOrderPaid(order, ipn.data.paid_amount);
    if (ipn.data.invoice?.status === "issued") await saveInvoiceNumber(order, ipn.data.invoice.number);
    return new Response(null, { status: 200 });
  } catch (err) {
    if (err instanceof HatangSignatureError) return new Response(null, { status: 401 });
    throw err; // 5xx khiến Hatang thử lại sau
  }
}
```

```ts title="Express"
import express from "express";
import { constructIpnEvent } from "@hatang/sdk";

app.post("/api/hatang/ipn", express.raw({ type: "application/json" }), async (req, res) => {
  const ipn = await constructIpnEvent(req.body, req.headers, process.env.HATANG_SECRET_KEY!).catch(() => null);
  if (!ipn) return res.sendStatus(401);
  await queue.add("hatang-ipn", ipn); // xử lý nền, trả lời nhanh
  res.sendStatus(200);
});
```

> **Caution**
>
> `express.json()` sẽ parse rồi serialize lại body, khiến chữ ký không còn khớp. Hãy dùng `express.raw()` cho route IPN.

## 3. Xác nhận trước khi giao hàng {#confirm-before-you-fulfil}

Khi người mua quay lại `return_url`, hỏi Hạ Tầng thay vì tin trình duyệt. `verify` chỉ trả `verified: true` khi payment đã `paid` (hoặc `overpaid`) và khớp với thông tin bạn truyền:

```ts
const { data: check, error } = await hatang.payments.verify(paymentId, {
  order_id: "SO-1001",
  amount: 150_000,
  environment: "production", // payment development không bao giờ mở khóa đơn thật
});
if (error) return showError(error.message);
if (check.verified) await fulfil("SO-1001");
else console.log(check.reasons); // vd ["status is partially_paid"], ["belongs to another order"]
```

| Giá trị trong `reasons` | Ý nghĩa |
|---|---|
| `status is <status>` | Chưa `paid` hoặc `overpaid` |
| `paid <n>, expected <amount>` | Đã trả, nhưng ít hơn số tiền bạn cần |
| `belongs to another order` | `metadata.order_id` khác `order_id` bạn truyền |
| `is a <environment> payment` | Payment thuộc môi trường kia |

## 4. Webhook (tùy chọn)

Webhook nhận mọi sự kiện của doanh nghiệp, không chỉ một payment:

```ts
import { constructWebhookEvent, type Payment } from "@hatang/sdk";

const event = await constructWebhookEvent<Payment>(raw, req.headers, process.env.HATANG_WEBHOOK_SECRET!); // whsec_…
if (event.type === "payment.succeeded") await markOrderPaid(event.data.metadata.order_id as string, event.data.paid_amount);
```

`constructWebhookEvent` throw `HatangSignatureError` khi chữ ký sai hoặc timestamp lệch quá 5 phút. Truyền `{ toleranceSec }` làm tham số thứ tư để đổi khoảng này.

## 5. Test ở development

Với `environment: "development"` và đã liên kết ngân hàng giả lập:

```ts
await hatang.payments.simulateTransfer({ payment_id: payment.id });                  // trả đủ
await hatang.payments.simulateTransfer({ payment_id: payment.id, amount: 50_000 }); // trả thiếu
```

## Xử lý lỗi

```ts
const { data, error } = await hatang.payments.create({ amount: 150_000, code: "SO1001" });
if (error) {
  switch (error.name) {
    case "code_taken":         // 409: mã đã được dùng trên tài khoản ngân hàng này
    case "no_bank_connection": // 422: cần liên kết tài khoản ngân hàng trước
    case "validation_error":   // 422: error.message nêu trường bị sai
      return showError(error.message);
    case "rate_limit_exceeded": // 429: giảm tốc độ rồi thử lại
      return retryLater();
    default:
      throw new Error(`${error.statusCode} ${error.name}: ${error.message}`);
  }
}
```

Lỗi mạng trả về `error.name === "application_error"` với `statusCode: 0`. Danh sách đầy đủ ở [Lỗi](https://hatang.dev/vi/docs/reference/errors.md).

## Danh sách hàm

| Hàm | Chức năng |
|---|---|
| `payments.createOrder({ order_id, amount, … })` | Tạo payment cho đơn; an toàn khi gọi lại |
| `payments.create(payload, { idempotencyKey })` | Như trên, với key và metadata của bạn |
| `payments.verify(id, { order_id, amount, environment })` | `{ verified, paid, reasons, payment }` |
| `payments.get(id)` / `list(params)` | Đọc payment |
| `payments.cancel(id)` / `markPaid(id, { amount, note })` | Hủy payment đang chờ, hoặc ghi nhận tiền nhận ngoài ngân hàng |
| `payments.ipnDeliveries(id)` | Các lần gửi IPN của một payment |
| `payments.qr({ amount, content, connection_id })` | VietQR cho luồng chỉ nhận giao dịch |
| `payments.simulateTransfer(…)` | Chỉ ở development |
| `payments.transactions.list(params)` / `get(id)` | Giao dịch ngân hàng |
| `payments.bankConnections.list` / `get` / `create` / `confirm` / `update` / `unlink` / `confirmUnlink` | Tài khoản ngân hàng |
| `payments.statements.import({ connection_id, content })` / `get(id)` | Nhập sao kê |
| `einvoices.create` / `get` / `list` / `update` / `remove` / `publish` / `retry` / `replace` / `resolve` / `sync` / `history` | Hóa đơn điện tử |
| `einvoices.providers()` / `einvoices.connections.*` | Tài khoản nhà cung cấp hóa đơn |
| `constructIpnEvent(raw, headers, secretKey)` / `verifyIpn(raw, signature, secretKey)` | Chữ ký IPN (HMAC-SHA256) |
| `constructWebhookEvent(raw, headers, secret)` / `verifyWebhook(raw, headers, secret)` | Chữ ký webhook (Svix), cửa sổ 5 phút |

## Bước tiếp theo

- [Xác minh IPN](https://hatang.dev/vi/docs/guides/payments/verify-ipn.md) không cần SDK.
- [Tạo thanh toán](https://hatang.dev/vi/docs/guides/payments/create-payment.md): mọi trường và trạng thái.
- [SDK Node.js](https://hatang.dev/vi/docs/guides/sdks/node.md): client cho mọi sản phẩm Hạ Tầng.

Source: https://hatang.dev/vi/docs/guides/payments/sdk-javascript
