# Nhận giao dịch không cần tạo đơn

Dùng luồng này khi hệ thống của bạn đã có mã đơn và chỉ cần biết khi tiền cho đơn đó về, ví dụ mã QR tĩnh ở quầy thu ngân. Bạn không tạo payment cho từng đơn. Hạ Tầng báo mọi giao dịch vào tài khoản dưới dạng `transaction.received`, kèm mã đơn của bạn nếu tìm thấy trong nội dung.

## Trước khi bắt đầu

- Một [kết nối ngân hàng](https://hatang.dev/vi/docs/guides/payments/bank-connections.md) đang hoạt động.
- Một [webhook endpoint](https://hatang.dev/vi/docs/guides/payments/webhooks.md) đăng ký sự kiện `transaction.received`. Luồng này không có IPN, vì không có payment nào để gắn `ipn_url`.

## 1. Khai báo định dạng mã đơn

Đặt `payment_code_pattern` trên tài khoản ngân hàng: tiền tố và khoảng độ dài. Hạ Tầng sẽ tìm mã này trong nội dung mọi giao dịch.

**cURL**

```bash
curl -X PATCH https://api.hatang.dev/payments/bank-connections/bcn_123 \
  -H "Authorization: Bearer $HATANG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "config": { "payment_code_pattern": { "prefix": "DH", "min_length": 4, "max_length": 8, "charset": "digits" } } }'
```

**Node.js**

```ts
await hatang.payments.bankConnections.update("bcn_123", {
  config: { payment_code_pattern: { prefix: "DH", min_length: 4, max_length: 8, charset: "digits" } },
});
```

**Python**

```python
requests.patch("https://api.hatang.dev/payments/bank-connections/bcn_123", headers=HEADERS, json={
    "config": {"payment_code_pattern": {"prefix": "DH", "min_length": 4, "max_length": 8, "charset": "digits"}},
}, timeout=10).raise_for_status()
```

**PHP**

```php
$client->patch('/payments/bank-connections/bcn_123', ['json' => [
    'config' => ['payment_code_pattern' => ['prefix' => 'DH', 'min_length' => 4, 'max_length' => 8, 'charset' => 'digits']],
]]);
```

### Cách tìm mã

1. Nội dung được chuẩn hóa: bỏ dấu, `đ` thành `D`, viết hoa toàn bộ, và bỏ mọi ký tự không phải chữ hoặc số. `ck dh 2025-0919 cam on` thành `CKDH20250919CAMON`.
2. Hạ Tầng tìm tiền tố theo sau là `min_length` đến `max_length` ký tự thuộc `charset`, và lấy kết quả đầu tiên, dài nhất có thể.

Với cấu hình trên, `DH20250919` được tìm thấy trong `ck dh 2025-0919 cam on`.

> **Caution**
>
> Khoảng trắng bị bỏ trước khi tìm, nên với `charset: "alnum"` các chữ phía sau sẽ dính vào mã: `DH1001 cam on` thành `DH1001CAMON` và kết quả là `DH1001CAMO`. Nên dùng `digits`, hoặc cho mọi mã dài đúng `max_length`.

## 2. Hiển thị VietQR chứa mã

`GET /payments/qr` tạo VietQR cho số tiền và nội dung bất kỳ. Không lưu gì.

**cURL**

```bash
# JSON kèm chuỗi EMVCo
curl "https://api.hatang.dev/payments/qr?amount=99000&content=DH20250919" \
  -H "Authorization: Bearer $HATANG_API_KEY"

# Ảnh SVG
curl "https://api.hatang.dev/payments/qr?amount=99000&content=DH20250919&format=svg" \
  -H "Authorization: Bearer $HATANG_API_KEY" -o qr.svg
```

**Node.js**

```ts
const { data: qr } = await hatang.payments.qr({ amount: 99_000, content: "DH20250919" });
// Render qr.payload bằng thư viện QR bất kỳ.
```

**Python**

```python
qr = requests.get("https://api.hatang.dev/payments/qr", headers=HEADERS,
                  params={"amount": 99000, "content": "DH20250919"}, timeout=10).json()
```

**PHP**

```php
$qr = json_decode($client->get('/payments/qr', ['query' => ['amount' => 99000, 'content' => 'DH20250919']])->getBody(), true);
```

```json
{
  "object": "vietqr",
  "payload": "00020101021238530010A0000007270123000697042201099104526580208QRIBFTTA53037045405990005802VN62140810DH202509196304FCC9",
  "connection_id": "bcn_01a0b90428fd7c499145a12c133abe41",
  "bank": { "bin": "970422", "account_number": "910452658", "account_name": "NGUYEN VAN A" },
  "amount": 99000,
  "content": "DH20250919"
}
```

| Tham số | Ý nghĩa |
|---|---|
| `amount` | VND. Bỏ trống để người mua tự nhập số tiền (QR dán ở quầy) |
| `content` | Nội dung điền sẵn, tối đa 50 ký tự |
| `connection_id` | Tài khoản nào; chỉ cần khi có nhiều tài khoản |
| `format` | `json` (mặc định) hoặc `svg` |
| `logo` | Với `format=svg`: `0` để bỏ logo Hạ Tầng khỏi ảnh |

QR chỉ điền sẵn nội dung. Người chuyển khoản thủ công phải tự gõ mã, vì vậy hãy in mã cạnh QR.

## 3. Nhận `transaction.received`

```json
{
  "type": "transaction.received",
  "created_at": "2026-09-19T09:34:12.946Z",
  "data": {
    "object": "transaction",
    "id": "txn_01a0b90429dc79c7a5689c7a0c485a01",
    "environment": "production",
    "connection_id": "bcn_01a0b90428fd7c499145a12c133abe41",
    "direction": "in",
    "amount": 99000,
    "currency": "VND",
    "description": "DH20250919 chuyen tien",
    "code": "DH20250919",
    "merchant_code": "DH20250919",
    "reference": "FT1789810452930810",
    "counterparty": { "account": "0987654321", "name": "NGUYEN VAN KHACH", "bank": "970436" },
    "booked_at": "2026-09-19T09:34:12.930Z",
    "source": "push",
    "payment_id": null,
    "match_status": "unmatched",
    "invoice_exempt": null,
    "created_at": "2026-09-19T09:34:12.946Z"
  }
}
```

| Trường | Ý nghĩa |
|---|---|
| `merchant_code` | Mã đơn của bạn tìm được trong nội dung; `null` nếu không tìm thấy |
| `code` | Mã lưu trên giao dịch: mã thanh toán Hạ Tầng khi giao dịch trả cho một payment, ngược lại là mã đơn của bạn |
| `reference` | Mã tham chiếu của ngân hàng |
| `source` | Hạ Tầng biết giao dịch bằng cách nào: `push` (ngân hàng báo), `reconcile` (đọc từ lịch sử tài khoản), `import` (sao kê) hoặc `simulator` (development) |
| `payment_id`, `match_status` | Có giá trị khi giao dịch trả cho một payment của bạn (`matched`); ngược lại là `unmatched` |
| `counterparty.bank` | Mã BIN ngân hàng người chuyển, nếu ngân hàng cung cấp |

Handler của bạn nên:

1. Xác minh chữ ký webhook (xem [Webhook](https://hatang.dev/vi/docs/guides/payments/webhooks.md)).
2. Tìm đơn theo `merchant_code`, và so `amount` với số tiền cần thu.
3. Lưu `data.id`. Cùng một giao dịch không bao giờ được báo hai lần, nhưng endpoint có thể nhận lại cùng một lần gửi sau khi timeout.
4. Kiểm tra `environment` và `direction`.

### Tình huống đặc biệt

| Tình huống | Bạn nhận được |
|---|---|
| Người mua quên ghi mã | `merchant_code: null`. Đối chiếu theo số tiền và `counterparty.name`, hoặc hỏi người mua |
| Người mua chuyển sai số tiền | Số tiền đúng như đã chuyển; bạn tự quyết định |
| Người mua chuyển hai lần | Hai giao dịch với `id` và `reference` khác nhau |
| Ngân hàng đẩy cùng giao dịch hai lần | Một giao dịch (cùng mã tham chiếu và số tiền trên tài khoản) |
| Giao dịch chứa cả mã thanh toán | Giao dịch cập nhật payment đó (`payment.succeeded`…) và `transaction.received` có `payment_id`. Một giao dịch không bao giờ bị tính hai lần |

## Lọc giao dịch được báo

Theo từng tài khoản ngân hàng, trong `transaction_events` của config hoặc trên dashboard:

| Cấu hình | Mặc định | Tác dụng |
|---|---|---|
| `direction` | `in` | `all` báo cả tiền ra |
| `require_code` | `false` | `true` chỉ báo giao dịch tìm thấy mã hoặc khớp với payment |
| `min_amount` | `0` | Giao dịch dưới số tiền này không được báo |

## Danh sách và tra cứu giao dịch

Mọi giao dịch đều được lưu, dù có được báo hay không.

**cURL**

```bash
curl "https://api.hatang.dev/payments/transactions?direction=in&match_status=unmatched&limit=50" \
  -H "Authorization: Bearer $HATANG_API_KEY"
```

**Node.js**

```ts
const { data: page } = await hatang.payments.transactions.list({ direction: "in", match_status: "unmatched", limit: 50 });
if (page.has_more) await hatang.payments.transactions.list({ after: page.next_cursor! });
```

**Python**

```python
page = requests.get("https://api.hatang.dev/payments/transactions", headers=HEADERS,
                    params={"direction": "in", "match_status": "unmatched", "limit": 50}, timeout=10).json()
```

**PHP**

```php
$page = json_decode($client->get('/payments/transactions', ['query' => [
    'direction' => 'in', 'match_status' => 'unmatched', 'limit' => 50,
]])->getBody(), true);
```

| Tham số | Ý nghĩa |
|---|---|
| `limit` | 1 đến 100, mặc định 20 |
| `after` | `next_cursor` của trang trước |
| `connection_id` | Một tài khoản ngân hàng |
| `direction` | `in` hoặc `out` |
| `match_status` | `matched`, `unmatched` hoặc `ignored` |
| `payment_id` | Các giao dịch đã trả cho payment này |

Phản hồi có dạng `{ "object": "list", "data": [...], "has_more": true, "next_cursor": "txn_..." }`, mới nhất trước. `GET /payments/transactions/{id}` trả về một giao dịch.

## Bước tiếp theo

- [Phát hành hóa đơn điện tử](https://hatang.dev/vi/docs/guides/payments/einvoices.md#invoices-without-a-payment) cho một giao dịch bằng `transaction_id`.
- [Nhập sao kê](https://hatang.dev/vi/docs/guides/payments/bank-connections.md#statement-import) để khôi phục giao dịch ngân hàng bỏ sót.
- [Tạo thanh toán](https://hatang.dev/vi/docs/guides/payments/create-payment.md) khi bạn cần trang checkout và hạn thanh toán.

Source: https://hatang.dev/vi/docs/guides/payments/transactions
