# Môi trường và sandbox

Thanh toán là sản phẩm duy nhất của Hạ Tầng có môi trường. Mỗi doanh nghiệp có môi trường `development` và `production`, và dữ liệu thanh toán của hai bên **tách biệt hoàn toàn**: tài khoản ngân hàng, payment, giao dịch, nhà cung cấp hóa đơn, hóa đơn và khoá tích hợp. API key và webhook endpoint dùng chung. Trang này giải thích cách chọn môi trường và cách test trọn luồng mà không cần tiền thật.

## Khác nhau ở đâu

| | `development` | `production` |
|---|---|---|
| Ngân hàng | Ngân hàng giả lập (`simulator`): mã xác nhận `000000`, chuyển khoản giả lập bằng API | Tài khoản ngân hàng thật của bạn |
| Hóa đơn điện tử | Sandbox của nhà cung cấp (Mắt Bão demo); không gửi cơ quan thuế | Hóa đơn thật theo mã số thuế của bạn |
| Giả lập chuyển khoản | `POST /payments/simulate/transfer`, cùng nút trên trang checkout và trang payment trên dashboard | Không có (`403 environment_mismatch`) |
| Khoá tích hợp | `ak_test_…` / `sk_test_…` | `ak_…` / `sk_…` |
| Chọn bằng | `Hatang-Environment: development` | Không gửi header (mặc định), hoặc `Hatang-Environment: production` |

## Chọn môi trường cho từng request {#choose-the-environment}

Gửi header `Hatang-Environment` trong mỗi request:

| Header | Kết quả |
|---|---|
| `Hatang-Environment: development` | Request đọc và ghi dữ liệu development |
| `Hatang-Environment: production` hoặc không gửi | Production |
| Giá trị khác | `422 validation_error`: "Hatang-Environment must be development or production" |

Mọi phản hồi lặp lại môi trường đã dùng trong header `Hatang-Environment`, và mọi object thanh toán (payment, giao dịch, kết nối ngân hàng, hóa đơn) có trường `environment`.

**cURL**

```bash
curl https://api.hatang.dev/payments \
  -H "Authorization: Bearer $HATANG_API_KEY" \
  -H "Hatang-Environment: development"
```

**Node.js**

```ts
import { Hatang } from "@hatang/sdk";

// Tùy chọn này được gửi dưới dạng header Hatang-Environment trong mọi request.
// Không có tùy chọn, SDK đọc biến môi trường HATANG_ENVIRONMENT.
const hatang = new Hatang(process.env.HATANG_API_KEY, {
  baseUrl: "https://api.hatang.dev",
  environment: "development",
});
const { data } = await hatang.payments.list();
```

**Python**

```python
import os
from hatang import Hatang

# environment= gửi header Hatang-Environment trong mọi lần gọi
dev = Hatang(os.environ["HATANG_API_KEY"], environment="development")
payments = dev.payments.list()  # chỉ thanh toán của môi trường development
```

**PHP**

```php
<?php
$client = new GuzzleHttp\Client(['base_uri' => 'https://api.hatang.dev']);
$res = $client->get('/payments', ['headers' => [
    'Authorization' => 'Bearer ' . getenv('HATANG_API_KEY'),
    'Hatang-Environment' => 'development',
]]);
echo $res->getHeaderLine('Hatang-Environment'); // development
```

> **Tip**
>
> Đọc môi trường từ cấu hình (`HATANG_ENVIRONMENT=development` trên server staging, không đặt gì ở production) thay vì viết cứng trong code, để cùng một code chạy được ở cả hai nơi.

> **Note: Key development cũ**
>
> Key bắt đầu bằng `ht_test_` gắn với development: không gửi header thì dùng development, còn yêu cầu production sẽ trả về `403 environment_mismatch`.

Id của môi trường này không tồn tại ở môi trường kia: đọc một payment development khi đang chọn production sẽ trả về `404 not_found`.

## Trên dashboard

Thanh trên cùng của mọi trang Thanh toán hiển thị môi trường hiện tại và cho phép chuyển. Production có màu đỏ, và chuyển sang production sẽ hỏi xác nhận. Các sản phẩm khác không có nút chuyển này.

## Ngân hàng giả lập

Liên kết bằng `{ "provider": "simulator" }` và xác nhận bằng mã `000000` (xem [Bắt đầu nhanh](https://hatang.dev/vi/docs/guides/payments/quickstart.md)). Mặc định, mã QR ghi ngân hàng MB và số tài khoản là số tự sinh (bắt đầu bằng `SIM`), nên không ngân hàng thật nào nhận ra.

Để có VietQR thật cho ngân hàng của bạn, truyền `bank_code` và số tài khoản:

```json
{
  "provider": "simulator",
  "bank_code": "BIDV",
  "account_number": "96247001234",
  "account_name": "NGUYEN VAN A"
}
```

`bank_code` nhận các mã ngắn quen thuộc: `MB` (mặc định), `BIDV`, `ACB`, `VCB`, `ICB`, `TCB`, `VPB`, `TPB`, `STB` và các ngân hàng VietQR khác. Với số tài khoản thật, app ngân hàng của người mua sẽ chuyển tiền thật cho bạn. Nhưng development không tích hợp ngân hàng nào, nên Hạ Tầng không thấy tiền về: payment chỉ chuyển trạng thái khi bạn giả lập chuyển khoản hoặc đánh dấu đã thanh toán.

## Giả lập chuyển khoản {#simulate-a-transfer}

**cURL**

```bash
# Trả đủ một payment
curl https://api.hatang.dev/payments/simulate/transfer \
  -H "Authorization: Bearer $HATANG_API_KEY" \
  -H "Hatang-Environment: development" \
  -H "Content-Type: application/json" \
  -d '{ "payment_id": "pay_123" }'

# Trả thiếu
curl https://api.hatang.dev/payments/simulate/transfer \
  -H "Authorization: Bearer $HATANG_API_KEY" \
  -H "Hatang-Environment: development" \
  -H "Content-Type: application/json" \
  -d '{ "payment_id": "pay_123", "amount": 50000 }'
```

**Node.js**

```ts
await hatang.payments.simulateTransfer({ payment_id: "pay_123" });                  // trả đủ
await hatang.payments.simulateTransfer({ payment_id: "pay_123", amount: 50_000 }); // trả thiếu
// Giao dịch không gắn đơn, cho luồng chỉ nhận giao dịch:
await hatang.payments.simulateTransfer({ connection_id: "bcn_123", amount: 99_000, content: "DH20250919 chuyen tien" });
```

**Python**

```python
HEADERS = {"Authorization": f"Bearer {os.environ['HATANG_API_KEY']}", "Hatang-Environment": "development"}
requests.post("https://api.hatang.dev/payments/simulate/transfer", headers=HEADERS,
              json={"payment_id": "pay_123", "amount": 50000}, timeout=10).raise_for_status()
```

**PHP**

```php
$client->post('/payments/simulate/transfer', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('HATANG_API_KEY'),
        'Hatang-Environment' => 'development',
    ],
    'json' => ['payment_id' => 'pay_123', 'amount' => 50000],
]);
```

| Trường | Ý nghĩa |
|---|---|
| `payment_id` | Payment cần trả. Tự điền tài khoản ngân hàng, số tiền còn thiếu và mã |
| `connection_id` | Tài khoản ngân hàng, cho giao dịch không gắn payment. Cần `payment_id` hoặc `connection_id` |
| `amount` | Số tiền khác, để test trả thiếu và trả thừa. Mặc định là số tiền còn thiếu |
| `content` | Nội dung chuyển khoản như ngân hàng báo. Mặc định `CHUYEN TIEN <code>` |
| `direction` | `in` (mặc định) hoặc `out` |
| `reference` | Mã tham chiếu ngân hàng. Dùng lại một mã để giả lập ngân hàng đẩy cùng giao dịch hai lần |
| `counterparty_name` | Tên người chuyển. Mặc định `NGUYEN VAN KHACH` |

Lời gọi trả về `202`:

```json
{
  "object": "simulated_transfer",
  "reference": "SIM1758274452930A1B2C3",
  "amount": 50000,
  "direction": "in",
  "description": "CHUYEN TIEN HTBF7RDGVQ",
  "connection_id": "bcn_01a0b90428fd7c499145a12c133abe41",
  "duplicate": false
}
```

<figure className="checkout-single">
  <img src="/img/thanh-toan/checkout-sandbox.png" alt="Trang checkout ở môi trường development có nút giả lập" width="420" />
  <figcaption>Ở development, trang checkout có thêm nút giả lập chuyển khoản</figcaption>
</figure>

Giao dịch giả lập đi qua **đúng quy trình** như giao dịch từ ngân hàng thật: khớp mã, sự kiện, IPN và webhook. Handler của bạn nhận đúng những gì sẽ nhận ở production.

### Lỗi

| Lỗi | Khi nào |
|---|---|
| `403 environment_mismatch` | Request ở production |
| `422 validation_error` | Không có `payment_id` lẫn `connection_id`, hoặc tài khoản không phải tài khoản ngân hàng giả lập |
| `409 invalid_state` | Tài khoản ngân hàng không ở trạng thái `active` |
| `422 missing_required_field` | Không có `amount` và payment không còn thiếu tiền |

## Kịch bản test

| Kịch bản | Cách làm |
|---|---|
| Trả đủ | Giả lập chỉ với `payment_id` |
| Trả thiếu rồi trả nốt | Giả lập `amount` nhỏ hơn tổng, rồi giả lập lần nữa không có `amount` |
| Trả thừa | Giả lập `amount` lớn hơn tổng |
| Hết hạn | Tạo với `expires_in: 60`, đợi vài phút, kiểm tra `payment.expired` |
| Tiền về sau khi hết hạn | Giả lập trên payment đã hết hạn: payment chuyển sang `paid` |
| Người mua quên ghi mã | Giả lập với `connection_id` và `content` không chứa mã |
| Ngân hàng đẩy trùng | Giả lập hai lần cùng `reference` và `amount`: lần hai trả về `duplicate: true` và không thay đổi gì |
| Endpoint của bạn bị lỗi | Đặt `ipn_url` trỏ tới URL trả `500`, rồi xem `GET /payments/{id}/ipn-deliveries` |

> **Danger**
>
> Handler của bạn phải kiểm tra `environment`. Một payment `development` không bao giờ được mở khóa đơn hàng thật.

## Bước tiếp theo

- [Liên kết tài khoản ngân hàng](https://hatang.dev/vi/docs/guides/payments/bank-connections.md) ở production.
- [Tạo thanh toán](https://hatang.dev/vi/docs/guides/payments/create-payment.md).
- [Lên production](https://hatang.dev/vi/docs/guides/payments/go-live.md): chuyển sang production từng bước.

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