# Ứng dụng Tapaya Terminal (/vi/in-store/integration/card_terminal)



Nhận thanh toán thẻ không tiếp xúc từ ứng dụng Android của bạn bằng cách chuyển phần tương tác với thẻ cho ứng dụng plugin **Tapaya Terminal**.
Ứng dụng của bạn vẫn kiểm soát giao dịch bán hàng; plugin xử lý việc chạm thẻ qua NFC, nhập PIN và xử lý
thẻ, sau đó kết quả được trả về qua luồng sự kiện của `Accept.payments.pay()`.

Hãy dùng cách tích hợp này khi bạn muốn có Tap to Pay mà không cần nhúng một đơn vị xử lý thanh toán vào ứng dụng của mình. Nếu
thay vào đó nền tảng của bạn được bật Tap to Pay nhúng, hãy xem hướng dẫn *Embedded Tap to Pay*.

## Các phương án tích hợp [#integration-options]

| Phương án                    | Phù hợp nhất cho                      | Cách trả kết quả                                                                                                           |
| :--------------------------- | :------------------------------------ | :------------------------------------------------------------------------------------------------------------------------- |
| **Accept SDK** (khuyến nghị) | Ứng dụng Android native               | `Accept.payments.pay()` phát ra một `Flow<PaymentEvent>`: việc tạo, khởi chạy và kết quả cuối cùng đều được xử lý thay bạn |
| **Deeplink**                 | WebView, framework đa nền tảng, mã QR | Bạn tạo khoản thanh toán qua Tapaya API và thăm dò (poll) trạng thái của nó; plugin được mở qua một deeplink               |

Phương án SDK được trình bày trước; xem [Tích hợp không cần SDK](#integrate-without-the-sdk) để biết quy ước của
deeplink.

## Cách hoạt động [#how-it-works]

1. Ứng dụng của bạn bắt đầu một khoản thanh toán bằng `Accept.payments.pay(amount, currency)`.
2. SDK đăng ký khoản thanh toán với backend Tapaya và khởi chạy plugin Tapaya Terminal trên cùng thiết bị.
3. Khách hàng chạm thẻ hoặc điện thoại vào màn hình Tapaya Terminal.
4. Tapaya Terminal hoàn tất việc xử lý khoản thanh toán với backend Tapaya.
5. SDK theo dõi khoản thanh toán cho tới khi nó đạt trạng thái cuối cùng và phát ra `PaymentEvent.Result` kèm theo một
   `PayResult`. Nếu hoàn toàn không thể tạo khoản thanh toán, SDK sẽ phát ra `PaymentEvent.CreationFailed` thay vào đó, thường
   trong vòng một giây.

## Điều kiện tiên quyết [#prerequisites]

* **Plugin Tapaya Terminal** đã được cài đặt trên thiết bị và đã được **kích hoạt** cho merchant của bạn (xem
  [Cài đặt và kích hoạt máy thanh toán](#install-and-activate-the-terminal)).
* Thiết bị có NFC, chạy Android 11 (API 30) trở lên.
* Accept SDK đã được [cài đặt và khởi tạo](../integration/sdk_integration#initialization), với merchant
  đã [đăng nhập](../integration/sdk_integration#step-2-authenticate).
* Quyền truy cập vị trí (`ACCESS_FINE_LOCATION`) được ứng dụng của bạn cấp khi chạy.

## Cài đặt và kích hoạt máy thanh toán [#install-and-activate-the-terminal]

Kích hoạt một lần cho mỗi thiết bị. Việc kích hoạt liên kết plugin Tapaya Terminal với chính tài khoản merchant mà ứng dụng của bạn
đang đăng nhập.

<CodeBlockTabs defaultValue="kotlin" groupId="accept-language">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="kotlin">
      Android (Kotlin)
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="typescript">
      React Native (TypeScript)
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="kotlin">
    ```kotlin
    if (!Accept.plugin.isInstalled()) {
        // Opens the store listing so the user can install Tapaya Terminal
        Accept.plugin.install()
        return
    }

    // suspend; launches the plugin's activation flow and waits for the outcome
    when (val result = Accept.plugin.activateTerminal()) {
        ActivateTerminalResult.Success -> { /* device linked, ready to take payments */ }
        ActivateTerminalResult.Canceled -> { /* user backed out */ }
        is ActivateTerminalResult.Failed -> result.reason // ActivateTerminalFailureReason
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="typescript">
    ```typescript
    if (!Accept.plugin.isInstalled()) {
        // Opens the store listing so the user can install Tapaya Terminal
        await Accept.plugin.install();
        return;
    }

    // launches the plugin's activation flow and waits for the outcome
    const result = await Accept.plugin.activateTerminal();
    switch (result.type) {
        case 'success': /* device linked, ready to take payments */ break;
        case 'canceled': /* user backed out */ break;
        case 'failed': handle(result.reason); break; // ActivateTerminalFailureReason
    }
    ```
  </CodeBlockTab>
</CodeBlockTabs>

`ActivateTerminalFailureReason` là một trong các giá trị `INVALID_INTENT`, `HANDOVER_FAILED`, `CONFIRM_FAILED`,
`MERCHANT_NOT_ONBOARDED` hoặc `UNKNOWN`.

<Callout title="Lưu ý" type="info">
  `Accept.plugin.isInstalled()` trả về `false` khi thiếu plugin Tapaya Terminal. Bạn có thể đọc trạng thái hiện tại
  của plugin bất cứ lúc nào bằng `Accept.plugin.status()` và điều chỉnh giao diện thanh toán của mình cho phù hợp.
</Callout>

`activateTerminal()` cũng **ném ra** `NoTidsAvailableForMerchant` khi merchant không còn máy thanh toán (TID) nào chưa được gán
để kích hoạt, tách biệt với các kết quả `ActivateTerminalResult.Failed` ở trên. Hãy bọc lệnh gọi trong
`try-catch` nếu bạn muốn hiển thị riêng trường hợp này, chẳng hạn bằng cách nhắc merchant giải phóng hoặc
yêu cầu thêm một TID khác.

### Kiểm tra bản cập nhật plugin [#checking-for-plugin-updates]

`Accept.plugin.updateInfo()` cho biết liệu trên Google Play có bản build mới hơn của plugin Tapaya Terminal hay không,
để bạn có thể nhắc merchant cập nhật trước khi một khoản thanh toán gặp phải chênh lệch phiên bản.

<CodeBlockTabs defaultValue="kotlin" groupId="accept-language">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="kotlin">
      Android (Kotlin)
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="typescript">
      React Native (TypeScript)
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="kotlin">
    ```kotlin
    when (val info = Accept.plugin.updateInfo()) {
        PluginUpdateInfo.UpToDate -> { /* nothing to do */ }
        is PluginUpdateInfo.UpdateAvailable -> promptUpdate(info.priority) // Int, higher is more urgent
        PluginUpdateInfo.Unknown -> { /* not installed, or an older plugin build that predates this check */ }
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="typescript">
    ```typescript
    const info = await Accept.plugin.updateInfo();
    switch (info.type) {
        case 'upToDate': /* nothing to do */ break;
        case 'updateAvailable': promptUpdate(info.priority); break; // higher is more urgent
        case 'unknown': /* not installed, or a plugin build that predates this check */ break;
    }
    ```
  </CodeBlockTab>
</CodeBlockTabs>

<Callout title="Lưu ý" type="info">
  SDK không bao giờ truy vấn trực tiếp Google Play; ứng dụng plugin tự kiểm tra qua Play Core và báo kết quả qua
  kênh trạng thái hiện có của nó. Bản build plugin cũ hơn bản đã phát hành tính năng này luôn báo
  `Unknown`.
</Callout>

## Tạo khoản thanh toán [#create-a-payment]

Số tiền luôn tính theo **đơn vị nhỏ nhất của loại tiền tệ**: haléř với CZK, cent với EUR. `1500` với
loại tiền tệ `"CZK"` sẽ trừ 15,00 Kč. Loại tiền tệ dùng mã ISO 4217.

`pay()` trả về một `Flow<PaymentEvent>`; hãy collect luồng này để theo dõi tiến trình và kết quả cuối cùng. Hàm này không bao giờ ném ngoại lệ;
lỗi được báo dưới dạng `PaymentEvent.CreationFailed`.

<CodeBlockTabs defaultValue="kotlin" groupId="accept-language">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="kotlin">
      Android (Kotlin)
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="typescript">
      React Native (TypeScript)
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="kotlin">
    ```kotlin
    Accept.payments.pay(amount = 1500, currency = "CZK") // 15.00 Kč
        .collect { event ->
            when (event) {
                PaymentEvent.Creating -> { /* registering the payment */ }
                is PaymentEvent.Created -> { /* backend payment created: event.paymentToken */ }
                PaymentEvent.Launched -> { /* Tapaya Terminal is in the foreground */ }
                is PaymentEvent.Result -> when (val r = event.payResult) {
                    is PayResult.Success -> showReceipt(r.paymentToken, r.authCode)
                    is PayResult.Declined -> showDeclined(r.metadata)
                    is PayResult.Canceled -> showCancelled(r.metadata)
                    is PayResult.Failed -> showUnexpected(r.reason) // PayFailureReason
                }
                is PaymentEvent.CreationFailed -> handleError(event.cause) // AcceptException
            }
        }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="typescript">
    ```typescript
    Accept.payments.pay({ amount: 1500, currency: 'CZK' }, (event) => { // 15.00 Kč
        switch (event.type) {
            case 'creating': /* registering the payment */ break;
            case 'created': /* backend payment created: event.paymentToken */ break;
            case 'launched': /* Tapaya Terminal is in the foreground */ break;
            case 'result':
                switch (event.payResult.type) {
                    case 'success': showReceipt(event.payResult.paymentToken, event.payResult.authCode); break;
                    case 'declined': showDeclined(event.payResult.metadata); break;
                    case 'canceled': showCancelled(event.payResult.metadata); break;
                    case 'failed': showUnexpected(event.payResult.reason); break; // PayFailureReason
                }
                break;
            case 'creationFailed': handleError(event.error); break; // AcceptError payload
        }
    });
    ```
  </CodeBlockTab>
</CodeBlockTabs>

**Tham số:**

| Tham số              | Kiểu                    | Bắt buộc | Mô tả                                                                                                                                                                                                                                 |
| :------------------- | :---------------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `amount`             | `Long`                  | ✓        | Số tiền cần trừ, theo đơn vị nhỏ nhất của `currency` (ví dụ cent với USD, haléř với CZK).                                                                                                                                             |
| `currency`           | `String`                | ✓        | Mã tiền tệ ISO 4217.                                                                                                                                                                                                                  |
| `paymentToken`       | `String?`               |          | Tiếp tục một phiên thanh toán hiện có thay vì bắt đầu phiên mới. Mặc định là `null`.                                                                                                                                                  |
| `receiptConfig`      | `PaymentReceiptConfig?` |          | Ghi đè màn hình biên lai hiển thị sau một khoản thanh toán **được chấp thuận**, chỉ cho lần gọi này. Mặc định là `null`, khi đó sẽ dùng giá trị mặc định đã đặt bằng `Accept.payments.setOptions()`.                                  |
| `nfcPosition`        | `NfcPositionConfig?`    |          | Ghi đè vị trí mà hoạt ảnh tap-to-pay chỉ tới, chỉ cho lần gọi này. Mặc định là `null`, khi đó sẽ dùng giá trị mặc định đã đặt bằng `Accept.payments.setOptions()`.                                                                    |
| `metadata`           | `Map<String, String>`   |          | Dữ liệu dạng khóa-giá trị gắn vào khoản thanh toán. Được gửi tới plugin Tapaya Terminal trong intent `PAY` và trả lại trong `PayResult` tương ứng, nếu plugin trả về. Mặc định là `emptyMap()`.                                       |
| `tip`                | `Long?`                 |          | Tiền tip cố định, theo đơn vị nhỏ nhất của `currency`. Khi được đặt, plugin bỏ qua màn hình tip tương tác của riêng nó và trừ tiền `amount + tip`. Mặc định là `null` (plugin hỏi người thanh toán về tiền tip).                      |
| `transactionTimeout` | `Duration?`             |          | Ghi đè thời gian máy thanh toán chờ chạm thẻ, chỉ cho lần gọi này. Mặc định là `null`, khi đó sẽ dùng giá trị mặc định đã đặt bằng `Accept.payments.setOptions()`. Xem [Đặt thời gian chờ giao dịch](#setting-a-transaction-timeout). |
| `display`            | `PaymentDisplay?`       |          | Ghi đè màn hình chạy giao diện thanh toán của plugin, chỉ cho lần gọi này. Mặc định là `null`, khi đó sẽ dùng giá trị mặc định đã đặt bằng `Accept.payments.setOptions()`. Xem [Chọn màn hình hiển thị](#targeting-a-display).        |

Trong khi khoản thanh toán đang diễn ra, plugin Tapaya Terminal ở tiền cảnh. Ứng dụng của bạn tự động quay lại
khi khoản thanh toán kết thúc. Để tiếp tục một khoản thanh toán bị gián đoạn, hãy truyền lại `paymentToken` của nó vào
`pay(amount, currency, paymentToken)`.

<Callout title="metadata không được đảm bảo trả về" type="info">
  `metadata` chỉ được trả lại trong `PayResult` nếu plugin trả về, vì vậy hãy coi đây là ngữ cảnh bổ sung không được đảm bảo
  (ví dụ mã đơn hàng cho việc ghi log của riêng bạn), chứ không phải dữ liệu mà ứng dụng của bạn bắt buộc phải nhận lại.
</Callout>

### Chọn loại tiền tệ [#choosing-the-currency]

`pay()` trừ tiền bằng `currency` mà bạn truyền vào, dưới dạng mã ISO 4217 viết hoa, với `amount` theo đơn vị nhỏ nhất
của loại tiền tệ đó. [Loại tiền tệ được hỗ trợ](/merchants/supported_currencies) liệt kê mọi loại tiền tệ mà Tapaya hỗ trợ, nhưng một merchant
thường chỉ được bật một số loại trong đó.

Gọi `Accept.merchant.availableCurrencies()` để lấy các mã ISO 4217 đã được bật cho merchant. Thanh toán bằng bất kỳ loại tiền tệ
nào khác sẽ thất bại với `CurrencyNotAvailableForMerchant`.

Mỗi loại tiền tệ cũng có một số tiền thanh toán tối thiểu, được áp dụng trên toàn nền tảng. Lấy các mức tối thiểu hiện tại bằng
`Accept.sdk.minimumAmounts()`, hàm này trả về một `List<MinimumAmount>` (`currency: String, amount: Long`, theo đơn vị nhỏ nhất
của loại tiền tệ). Gọi `pay()` với số tiền dưới mức tối thiểu đó sẽ thất bại với
`PaymentEvent.CreationFailed(AmountBelowMinimum(minimum, currency))`. Xem
[Xử lý lỗi](./sdk_integration#error-handling) để biết danh sách ngoại lệ đầy đủ.

### Cấu hình màn hình biên lai [#configuring-the-receipt-screen]

`receiptConfig` kiểm soát những gì plugin Tapaya Terminal hiển thị cho người thanh toán sau một khoản thanh toán **được chấp thuận**, thời gian
màn hình đó hiển thị và việc có in biên lai hay không. Thiết lập này không ảnh hưởng tới các kết quả bị từ chối, đã đảo giao dịch (reversal) hoặc đang chờ
đảo giao dịch; các kết quả đó luôn hiển thị màn hình kết quả riêng, bất kể thiết lập này.

| `PaymentReceiptConfig`                                    | Mô tả                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| :-------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DismissImmediately`                                      | Không hiển thị gì cho người thanh toán.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `Show(showQrCodeReceipt, dismissBehavior, printBehavior)` | Hiển thị màn hình biên lai. `showQrCodeReceipt: Boolean = true` bật hoặc tắt mã QR liên kết tới biên lai. `dismissBehavior: DismissBehavior = DismissBehavior.Manual` kiểm soát cách màn hình đóng lại: `Manual` (giữ nguyên cho tới khi người thanh toán đóng) hoặc `Delayed(delay: Duration)` (tự động đóng sau `delay` nếu người thanh toán chưa đóng trước đó). `printBehavior: ReceiptPrintBehavior = ReceiptPrintBehavior.OnDemand` kiểm soát biên lai giấy: `Automatic` (in ngay khi màn hình xuất hiện), `OnDemand` (chỉ in khi thu ngân yêu cầu) hoặc `Disabled` (không bao giờ in). |

Đặt giá trị mặc định cho toàn thiết bị một lần bằng `Accept.payments.setOptions()`, và ghi đè cho một khoản thanh toán riêng qua
`pay(..., receiptConfig = ...)`. Cho tới khi `setOptions()` được gọi, giá trị mặc định là `Show` với biên lai QR, đóng
thủ công và in theo yêu cầu, trùng với giá trị dự phòng của chính plugin.

<CodeBlockTabs defaultValue="kotlin" groupId="accept-language">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="kotlin">
      Android (Kotlin)
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="typescript">
      React Native (TypeScript)
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="kotlin">
    ```kotlin
    // Set a default, e.g. once at app startup
    Accept.payments.setOptions(
        receiptConfig = PaymentReceiptConfig.Show(
            showQrCodeReceipt = true,
            dismissBehavior = DismissBehavior.Delayed(30.seconds),
            printBehavior = ReceiptPrintBehavior.Automatic,
        ),
    )

    // Override it for a single payment
    Accept.payments.pay(
        amount = 1500,
        currency = "CZK",
        receiptConfig = PaymentReceiptConfig.DismissImmediately,
    )
    ```
  </CodeBlockTab>

  <CodeBlockTab value="typescript">
    ```typescript
    // Set a default, e.g. once at app startup
    Accept.payments.setOptions({
        receiptConfig: {
            type: 'show',
            showQrCodeReceipt: true,
            dismissBehavior: { type: 'delayed', delayMs: 30_000 },
            printBehavior: 'automatic',
        },
    });

    // Override it for a single payment
    await Accept.payments.payAsync({
        amount: 1500,
        currency: 'CZK',
        receiptConfig: { type: 'dismissImmediately' },
    });
    ```
  </CodeBlockTab>
</CodeBlockTabs>

### Định vị hoạt ảnh chạm NFC [#positioning-the-nfc-tap-animation]

`nfcPosition` là một gợi ý cho plugin Tapaya Terminal biết ăng-ten NFC của thiết bị này nằm ở đâu, để plugin có thể đặt
hoạt ảnh “chạm vào đây” cho đúng chỗ. Trước tiên, plugin tự xác định vị trí ăng-ten (phát hiện trực tiếp qua nền tảng,
sau đó là cơ sở dữ liệu thiết bị đi kèm, rồi đến bất cứ thiết lập nào merchant đã cấu hình trực tiếp trong plugin); chỉ cung cấp giá trị này
để ghi đè kết quả xác định đó, chẳng hạn trên một thiết bị mà plugin không phát hiện được hoặc có thông tin sai trong cơ sở dữ liệu.

| `NfcPositionConfig`                       | Mô tả                                                                                                                                                    |
| :---------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Unspecified`                             | Không ghi đè (mặc định). Plugin tự xác định vị trí và giữ nguyên mọi vị trí mà merchant đã cấu hình ở đó.                                                |
| `OnDevice(surface, xFraction, yFraction)` | Ăng-ten nằm ngay trên thiết bị; hoạt ảnh chỉ tới `(xFraction, yFraction)` trên `surface`, là `OnDeviceSurface.BACK` hoặc `OnDeviceSurface.FRONT_SCREEN`. |
| `ExternalAbove`                           | Một mô-đun đầu đọc riêng nằm phía trên điện thoại (giá đỡ smart-POS); không có điểm nào trên thiết bị để chỉ tới.                                        |

Đặt giá trị mặc định cho toàn thiết bị bằng `Accept.payments.setOptions(nfcPosition = ...)`, hoặc ghi đè cho từng lần gọi qua
`pay(..., nfcPosition = ...)`. `setOptions()` chỉ thay đổi (các) tùy chọn mà bạn truyền vào, nên riêng `setOptions(nfcPosition = ...)`
sẽ không ảnh hưởng tới giá trị mặc định `receiptConfig` đã đặt trước đó, và ngược lại.

### Đặt thời gian chờ giao dịch [#setting-a-transaction-timeout]

`transactionTimeout` kiểm soát thời gian plugin Tapaya Terminal chờ khách hàng chạm thẻ trước khi
dừng lại. Tham số nhận bất kỳ `Duration` nào từ 0 tới 120 giây; mọi giá trị ngoài khoảng đó được báo dưới dạng
`TransactionTimeoutOutOfRange(transactionTimeout)` qua `PaymentEvent.CreationFailed`, vì `pay()` không bao giờ ném ngoại lệ
một cách đồng bộ.

<CodeBlockTabs defaultValue="kotlin" groupId="accept-language">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="kotlin">
      Android (Kotlin)
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="typescript">
      React Native (TypeScript)
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="kotlin">
    ```kotlin
    Accept.payments.pay(
        amount = 1500,
        currency = "CZK",
        transactionTimeout = 90.seconds,
    )
    ```
  </CodeBlockTab>

  <CodeBlockTab value="typescript">
    ```typescript
    await Accept.payments.payAsync({
        amount: 1500,
        currency: 'CZK',
        transactionTimeoutMs: 90_000, // milliseconds, 0…120000
    });
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Để nguyên giá trị mặc định `null` để dùng mức dự phòng của chính ứng dụng đi kèm (khoảng 60 giây), hoặc đặt giá trị mặc định
cho toàn thiết bị bằng `Accept.payments.setOptions(transactionTimeout = ...)`.

### Chọn màn hình hiển thị [#targeting-a-display]

Trên thiết bị hai mặt (“double-sided”), một màn hình hướng về phía merchant và màn hình thứ hai, dành cho khách hàng, hướng về phía
người thanh toán. `Accept.displays` cho biết thiết bị hiện có những màn hình nào, và tùy chọn `display` cho plugin biết
nên chạy giao diện thanh toán trên màn hình nào.

<CodeBlockTabs defaultValue="kotlin" groupId="accept-language">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="kotlin">
      Android (Kotlin)
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="typescript">
      React Native (TypeScript)
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="kotlin">
    ```kotlin
    if (Accept.displays.isDualScreen) {
        val customerPanel = Accept.displays.customerFacing() // AcceptDisplay?, null on single-screen devices
    }

    Accept.payments.pay(
        amount = 1500,
        currency = "CZK",
        display = PaymentDisplay.CustomerFacing,
    )
    ```
  </CodeBlockTab>

  <CodeBlockTab value="typescript">
    ```typescript
    if (Accept.displays.isDualScreen) {
        const customerPanel = Accept.displays.customerFacing(); // null on single-screen devices
    }

    await Accept.payments.payAsync({
        amount: 1500,
        currency: 'CZK',
        display: { type: 'customerFacing' },
    });
    ```
  </CodeBlockTab>
</CodeBlockTabs>

| `PaymentDisplay`      | Mô tả                                                                                             |
| :-------------------- | :------------------------------------------------------------------------------------------------ |
| `Default`             | Màn hình hiện tại của ứng dụng chủ (hành vi mặc định, không thay đổi trên thiết bị một màn hình). |
| `CustomerFacing`      | Màn hình phụ của thiết bị hai mặt.                                                                |
| `Specific(displayId)` | Một màn hình cụ thể, theo `id` được báo trong `Accept.displays.all()`.                            |

Một màn hình đích không thể đáp ứng, ví dụ `CustomerFacing` trên thiết bị một màn hình, sẽ âm thầm chuyển về
`Default`. Đặt giá trị mặc định cho toàn thiết bị bằng `Accept.payments.setOptions(display = ...)`, hoặc ghi đè cho từng lần gọi qua
`pay(..., display = ...)`.

<Callout title="Đặt màn hình đích trước khi xác thực" type="info">
  Quá trình khởi động trước (warm-up) của máy thanh toán do `Accept.auth.authenticate()` kích hoạt cũng dùng cùng màn hình đích này, vì vậy hãy gọi
  `setOptions(display = ...)` trước `authenticate()` nếu bạn muốn chính quá trình warm-up cũng không hiển thị trên
  màn hình của merchant.
</Callout>

Ứng dụng mẫu `:dualscreen` minh họa một quy trình thanh toán đầy đủ trên hai màn hình: merchant thao tác trên màn hình chính, trong khi
`PaymentDisplay.CustomerFacing` đưa giao diện thanh toán của plugin lên màn hình của khách hàng.

## Kết quả thanh toán [#payment-outcome]

Sự kiện cuối cùng `PaymentEvent.Result` mang theo một `PayResult`. Mọi biến thể đều mang `metadata`, trả lại những gì bạn
đã truyền vào `pay()` nếu plugin trả về (có thể là `null`):

| `PayResult`                                                 | Ý nghĩa                                                                                                                                                                                                               |
| :---------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Success(paymentToken, metadata, authCode, receiptDetails)` | Đã được cấp phép và thu tiền (capture). Tiền sẽ được quyết toán cho merchant. `authCode` là mã cấp phép của đơn vị xử lý do máy thanh toán báo về; chỉ mang tính thông tin, và có thể không có tùy theo đơn vị xử lý. |
| `Declined(metadata, receiptDetails)`                        | Bị đơn vị xử lý thẻ hoặc ngân hàng phát hành thẻ từ chối.                                                                                                                                                             |
| `Canceled(metadata)`                                        | Bị khách hàng hoặc merchant hủy trước khi hoàn tất.                                                                                                                                                                   |
| `Failed(reason, metadata)`                                  | Không hoàn tất; `reason` là một `PayFailureReason`.                                                                                                                                                                   |

`PayFailureReason` là một trong các giá trị `INVALID_INTENT`, `NOT_ACTIVATED`, `TERMINAL_INIT_FAILED`, `PAYMENT_SETUP_FAILED`,
`PLUGIN_UNAVAILABLE` hoặc `UNKNOWN`.

### Đọc chi tiết biên lai [#reading-receipt-details]

`Success` và `Declined` mang một `receiptDetails: ReceiptDetails?` tùy chọn, chứa dữ liệu biên lai thẻ có cấu trúc
do máy thanh toán báo về, dành cho bên gọi tự in biên lai thẻ chính thức của mình thay vì chỉ dựa vào
trang do Tapaya lưu trữ tại `PaymentStatus.receiptUrl`.

| Trường                 | Kiểu      | Mô tả                                        |
| :--------------------- | :-------- | :------------------------------------------- |
| `merchantId`           | `String?` | ID merchant do đơn vị xử lý báo về.          |
| `terminalId`           | `String?` | ID máy thanh toán do đơn vị xử lý báo về.    |
| `cardBrand`            | `String?` | Thương hiệu thẻ, ví dụ Visa hoặc Mastercard. |
| `appLabel`             | `String?` | Nhãn ứng dụng EMV.                           |
| `aid`                  | `String?` | Mã định danh ứng dụng EMV.                   |
| `maskedPan`            | `String?` | Số thẻ đã được che.                          |
| `responseCode`         | `String?` | Mã phản hồi của đơn vị xử lý.                |
| `transactionTimestamp` | `String?` | Dấu thời gian giao dịch theo ISO-8601.       |

Mọi trường đều là tùy chọn và không được đảm bảo; bên gọi phải sẵn sàng cho trường hợp bất kỳ trường nào, hoặc chính `receiptDetails`,
không có.

### Đọc chi tiết khoản thanh toán [#reading-payment-details]

`PayResult.Success` cung cấp cho bạn `paymentToken`. Lấy bản ghi đầy đủ, bao gồm `receiptUrl`, số tiền yêu cầu và
số tiền quyết toán, cùng các dấu thời gian, bằng `Accept.payments.status()`:

<CodeBlockTabs defaultValue="kotlin" groupId="accept-language">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="kotlin">
      Android (Kotlin)
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="typescript">
      React Native (TypeScript)
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="kotlin">
    ```kotlin
    val status: PaymentStatus = Accept.payments.status(paymentToken)
    status.state       // PENDING, SUCCESS, CANCELLED, REFUNDED, FAILED, ACTION_NEEDED
    status.receiptUrl  // for your receipt UI
    ```
  </CodeBlockTab>

  <CodeBlockTab value="typescript">
    ```typescript
    const status = await Accept.payments.status(paymentToken);
    status.state;      // PENDING, SUCCESS, CANCELLED, REFUNDED, FAILED, ACTION_NEEDED
    status.receiptUrl; // for your receipt UI
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Dừng một khoản thanh toán đang diễn ra bằng `Accept.payments.cancel(paymentToken)`, hàm này trả về `PaymentState` tương ứng.

### Hoàn tiền [#issuing-a-refund]

Hoàn tiền một khoản thanh toán đã xử lý xong bằng `Accept.payments.refund(paymentToken, reason)`, hàm này cũng trả về
`PaymentState` tương ứng.

<CodeBlockTabs defaultValue="kotlin" groupId="accept-language">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="kotlin">
      Android (Kotlin)
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="typescript">
      React Native (TypeScript)
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="kotlin">
    ```kotlin
    val state: PaymentState = Accept.payments.refund(paymentToken, RefundReason.CUSTOMER_REQUEST)
    ```
  </CodeBlockTab>

  <CodeBlockTab value="typescript">
    ```typescript
    const state = await Accept.payments.refund(paymentToken, 'CUSTOMER_REQUEST');
    ```
  </CodeBlockTab>
</CodeBlockTabs>

`RefundReason` là một trong các giá trị `CUSTOMER_REQUEST`, `DUPLICATE`, `FRAUDULENT` hoặc `EXPIRED_CHARGE`.

## Xử lý lỗi [#error-handling]

Một khoản thanh toán đã tới máy thanh toán nhưng không thành công **không** phải là ngoại lệ; nó đến dưới dạng
`PaymentEvent.Result` mang `PayResult.Declined`, `Canceled` hoặc `Failed(reason)`. Các lỗi khiến
khoản thanh toán không thể được tạo sẽ đến dưới dạng `PaymentEvent.CreationFailed(cause)`, trong đó `cause` là một
`AcceptException` có kiểu. Các ngoại lệ riêng của cách tích hợp này:

| Ngoại lệ                                           | Khi nào xảy ra                                                                              | Cần làm gì                                                                       |
| :------------------------------------------------- | :------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------- |
| `PluginUnavailable`                                | Plugin Tapaya Terminal chưa được cài đặt hoặc không thể liên kết (bind) với dịch vụ của nó. | Nhắc cài đặt bằng `Accept.plugin.install()`.                                     |
| `CurrencyNotAvailableForMerchant(code)`            | Merchant chưa được cấu hình cho loại tiền tệ được yêu cầu.                                  | Trừ tiền bằng một loại tiền tệ đã được bật, hoặc cập nhật cấu hình của merchant. |
| `AmountBelowMinimum(minimum, currency)`            | Số tiền thấp hơn mức tối thiểu đã cấu hình.                                                 | Trừ số tiền bằng hoặc cao hơn `minimum`.                                         |
| `TransactionTimeoutOutOfRange(transactionTimeout)` | `transactionTimeout` nằm ngoài khoảng 0 đến 120 giây.                                       | Truyền một giá trị trong khoảng cho phép, hoặc bỏ qua để dùng giá trị mặc định.  |
| `LocationPermissionRequired`                       | Ứng dụng của bạn chưa được cấp quyền truy cập vị trí.                                       | Yêu cầu `ACCESS_FINE_LOCATION`, sau đó thử lại.                                  |
| `LocationUnavailable` / `LocationTimeout`          | Không thể xác định vị trí của thiết bị.                                                     | Yêu cầu người dùng bật định vị và thử lại.                                       |
| `SessionExpired`                                   | Phiên đăng nhập đã hết hạn.                                                                 | Xác thực lại bằng `Accept.auth.authenticate()`.                                  |
| `NoInternetConnection`                             | Không có kết nối mạng.                                                                      | Yêu cầu người dùng kết nối lại và thử lại.                                       |
| `PaymentInProgress`                                | Một lệnh gọi `pay()` trước đó vẫn đang chờ kết quả.                                         | Chờ khoản thanh toán hiện tại kết thúc.                                          |

`Accept.payments.status()`, `cancel()` và `refund()` là các hàm `suspend` và **ném ra** các ngoại lệ này
(thay vì phát ra sự kiện); hãy bọc chúng trong `try-catch`. `status()`/`cancel()`/`refund()` cũng ném ra
`PaymentNotFound` với token không xác định. Xem danh sách đầy đủ trong
[Xử lý lỗi](../integration/sdk_integration#error-handling).

## Thời gian chờ [#timeouts]

Một khoản thanh toán bằng thẻ phải được hoàn tất trên máy thanh toán trong thời gian ngắn sau khi bắt đầu. Nếu khách hàng không hề chạm thẻ, hoặc
plugin Tapaya Terminal bị đóng giữa chừng, khoản thanh toán sẽ kết thúc với `PayResult.Canceled` hoặc
`PayResult.Failed`, và khoản thanh toán gốc bị hủy ở phía máy chủ và không bao giờ được xử lý xong. Luôn xác nhận
trạng thái cuối cùng bằng `Accept.payments.status()` trước khi coi một khoản thanh toán là đã hoàn tất.

## Tích hợp không cần SDK [#integrate-without-the-sdk]

Nếu bạn không thể nhúng Accept SDK (framework đa nền tảng, một POS shell mỏng, máy tính tiền chạy trên web), bạn có thể điều khiển
Tapaya Terminal trực tiếp bằng deeplink. Cách phân chia công việc thay đổi: **bạn** tạo khoản thanh toán qua
Tapaya API và **bạn** thăm dò trạng thái của nó để lấy kết quả; Tapaya Terminal chỉ xử lý phần tương tác với thẻ.

Quy trình:

1. Tạo một khoản thanh toán từ backend của bạn qua [Payments API](https://api.tapaya.com/openapi/index.html) và lưu lại
   `paymentId` của nó.
2. Mở Tapaya Terminal với `paymentId` đó bằng [deeplink thanh toán](#pay-deeplink).
3. Thăm dò trạng thái khoản thanh toán qua [Payments API](https://api.tapaya.com/openapi/index.html) cho tới khi nó rời khỏi
   `PENDING`. Deeplink không có kênh trả kết quả, nên thăm dò là nguồn thông tin đáng tin cậy duy nhất.

<Callout title="Lưu ý" type="info">
  Tapaya Terminal phải được cài đặt, đăng nhập vào tài khoản merchant và được cấu hình cho loại tiền tệ của
  khoản thanh toán. Chế độ này không có bước kích hoạt; tài khoản đang đăng nhập là bên bán chịu trách nhiệm giao dịch (merchant of record). Nếu cần, hãy
  đăng nhập cho merchant bằng [deeplink đăng nhập](#login-deeplink).
</Callout>

### Scheme của deeplink [#deeplink-schemes]

Scheme quyết định bản build Tapaya Terminal nào sẽ xử lý liên kết:

| Môi trường | Scheme                  |
| :--------- | :---------------------- |
| Production | `tapaya-accept`         |
| Sandbox    | `tapaya-accept-sandbox` |

Nếu một liên kết không mở được, nghĩa là bản build Tapaya Terminal đó chưa được cài đặt.

### Deeplink thanh toán [#pay-deeplink]

```text
tapaya-accept://pay?paymentId=<paymentId>&amount=<amount>&currency=<ISO4217>
```

| Tham số truy vấn | Bắt buộc | Ghi chú                                                                                                                             |
| :--------------- | :------- | :---------------------------------------------------------------------------------------------------------------------------------- |
| `paymentId`      | **Có**   | Khoản thanh toán đã tạo ở bước 1.                                                                                                   |
| `amount`         | **Có**   | Số tiền theo **đơn vị chính** dưới dạng số nguyên: `amount=15` sẽ trừ 15,00. Lưu ý điều này khác với SDK, vốn nhận đơn vị nhỏ nhất. |
| `currency`       | **Có**   | Mã tiền tệ ISO 4217, ví dụ `CZK`.                                                                                                   |

<CodeBlockTabs defaultValue="kotlin" groupId="accept-language">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="kotlin">
      Android (Kotlin)
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="typescript">
      React Native (TypeScript)
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="kotlin">
    ```kotlin
    val uri = Uri.parse("tapaya-accept://pay?paymentId=$paymentId&amount=15&currency=CZK")
    try {
        startActivity(Intent(Intent.ACTION_VIEW, uri).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
    } catch (e: ActivityNotFoundException) {
        // Tapaya Terminal is not installed, send the user to Google Play
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="typescript">
    ```typescript
    import { Linking } from 'react-native';

    const url = `tapaya-accept://pay?paymentId=${paymentId}&amount=15&currency=CZK`;
    try {
        await Linking.openURL(url);
    } catch {
        // Tapaya Terminal is not installed, send the user to Google Play
    }
    ```
  </CodeBlockTab>
</CodeBlockTabs>

### Deeplink đăng nhập [#login-deeplink]

Đăng nhập cho merchant vào Tapaya Terminal bằng `merchantToken` của họ, tức định danh merchant mà bạn đặt qua `POST /merchant/auth/register`:

```text
tapaya-accept://login?token=<merchantToken>
```

| Tham số truy vấn | Bắt buộc | Ghi chú                                                                                                                         |
| :--------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------ |
| `token`          | **Có**   | `merchantToken` của merchant. Đây là ID merchant ổn định của bạn, không phải token SDK ngắn hạn từ `POST /merchant/auth/login`. |
| `activationCode` | Không    | Điền sẵn mã kích hoạt máy thanh toán.                                                                                           |

## Kiểm thử tích hợp [#test-the-integration]

1. Khởi tạo với `isProduction = false` (mặc định); các khoản thanh toán trong sandbox không bao giờ chuyển tiền thật.
2. Cài đặt plugin Tapaya Terminal trên thiết bị thử nghiệm, đăng nhập bằng một merchant sandbox và kích hoạt.
3. Thực hiện một khoản thanh toán với số tiền nhỏ và kiểm tra toàn bộ luồng `Success` từ đầu đến cuối, bao gồm cả giao diện biên lai của bạn.
4. Thử các tình huống lỗi: từ chối một khoản thanh toán trên máy thanh toán, hủy một khoản, và bắt đầu một khoản với loại tiền tệ không được hỗ trợ;
   phần xử lý `PayResult`/`CreationFailed` của bạn cần bao quát từng trường hợp.

Khi bạn sẵn sàng vận hành chính thức, hãy đặt `isProduction = true` trong `initialize` và xác thực bằng thông tin đăng nhập
production. Mã thanh toán không thay đổi.
