# Застосунок Tapaya Terminal (/uk/in-store/integration/card_terminal)



Приймайте безконтактні платежі карткою у своєму застосунку для Android, передаючи взаємодію з карткою застосунку-плагіну
**Tapaya Terminal**. Продаж залишається під контролем вашого застосунку; плагін бере на себе прикладання картки через NFC, введення PIN і обробку
картки, а потім результат повертається через потік подій `Accept.payments.pay()`.

Використовуйте цю інтеграцію, якщо вам потрібен Tap to Pay без вбудовування платіжного процесора у ваш власний застосунок. Якщо для вашої
платформи натомість увімкнено вбудований Tap to Pay, див. посібник *Embedded Tap to Pay*.

## Варіанти інтеграції [#integration-options]

| Варіант                        | Найкраще підходить для                      | Отримання результату                                                                                                           |
| :----------------------------- | :------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------- |
| **Accept SDK** (рекомендовано) | Нативні застосунки для Android              | `Accept.payments.pay()` надсилає `Flow<PaymentEvent>`: створення, запуск і кінцевий результат із термінала обробляються за вас |
| **Deeplink**                   | WebView, кросплатформні фреймворки, QR-коди | Ви створюєте платіж через Tapaya API та періодично опитуєте його статус; плагін відкривається через deeplink                   |

Спершу описано шлях через SDK; контракт deeplink див. у розділі [Інтеграція без SDK](#integrate-without-the-sdk).

## Як це працює [#how-it-works]

1. Ваш застосунок починає платіж викликом `Accept.payments.pay(amount, currency)`.
2. SDK реєструє платіж у бекенді Tapaya і запускає плагін Tapaya Terminal на тому самому пристрої.
3. Клієнт прикладає свою картку або телефон до екрана Tapaya Terminal.
4. Tapaya Terminal завершує обробку платежу з бекендом Tapaya.
5. SDK відстежує платіж, доки той не досягне кінцевого стану, і надсилає `PaymentEvent.Result` із
   `PayResult`. Якщо платіж узагалі неможливо створити, натомість надсилається `PaymentEvent.CreationFailed`, зазвичай
   протягом секунди.

## Передумови [#prerequisites]

* **Плагін Tapaya Terminal**, установлений на пристрої та **активований** для вашого мерчанта (див.
  [Установлення й активація термінала](#install-and-activate-the-terminal)).
* Пристрій із NFC під керуванням Android 11 (API 30) або новішої версії.
* Accept SDK [установлено й ініціалізовано](../integration/sdk_integration#initialization), а мерчант
  [увійшов у систему](../integration/sdk_integration#step-2-authenticate).
* Дозвіл на доступ до геолокації (`ACCESS_FINE_LOCATION`), наданий вашому застосунку під час виконання.

## Установлення й активація термінала [#install-and-activate-the-terminal]

Активацію виконують один раз для кожного пристрою. Активація пов'язує плагін Tapaya Terminal із тим самим обліковим записом мерчанта,
під яким автентифіковано ваш застосунок.

<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` набуває одного зі значень `INVALID_INTENT`, `HANDOVER_FAILED`, `CONFIRM_FAILED`,
`MERCHANT_NOT_ONBOARDED` або `UNKNOWN`.

<Callout title="Примітка" type="info">
  `Accept.plugin.isInstalled()` повертає `false`, коли плагін Tapaya Terminal відсутній. Поточний стан плагіна
  можна будь-коли дізнатися за допомогою `Accept.plugin.status()`; відповідно до нього захистіть свій платіжний UI.
</Callout>

`activateTerminal()` також **викидає** `NoTidsAvailableForMerchant`, коли в мерчанта не залишилося жодного непризначеного термінала
(TID) для активації, окремо від наведених вище результатів `ActivateTerminalResult.Failed`. Обгорніть виклик у
`try-catch`, якщо хочете окремо показувати цей випадок, наприклад запропонувати мерчанту звільнити або
запросити ще один TID.

### Перевірка оновлень плагіна [#checking-for-plugin-updates]

`Accept.plugin.updateInfo()` повідомляє, чи доступна в Google Play новіша збірка плагіна Tapaya Terminal,
щоб ви могли запропонувати мерчанту оновитися до того, як платіж наштовхнеться на розбіжність версій.

<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="Примітка" type="info">
  SDK ніколи не звертається до Google Play напряму; застосунок-плагін сам перевіряє оновлення через Play Core і повідомляє результат
  своїм наявним каналом статусу. Збірка плагіна, старіша за ту, у якій з'явилася ця функція, завжди повідомляє
  `Unknown`.
</Callout>

## Створення платежу [#create-a-payment]

Суми завжди вказуються в **найменшій одиниці валюти**: гелерах для CZK, центах для EUR. `1500` з
валютою `"CZK"` стягує 15,00 Kč. Валюти вказуються кодами ISO 4217.

`pay()` повертає `Flow<PaymentEvent>`; збирайте його, щоб стежити за перебігом і кінцевим результатом. Він ніколи не викидає винятків;
збої надходять як `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>

**Параметри:**

| Параметр             | Тип                     | Обов'язковий | Опис                                                                                                                                                                                                                                                                                          |
| :------------------- | :---------------------- | :----------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `amount`             | `Long`                  | ✓            | Сума до стягнення в найменшій одиниці валюти `currency` (наприклад, центи для USD, гелери для CZK).                                                                                                                                                                                           |
| `currency`           | `String`                | ✓            | Код валюти ISO 4217.                                                                                                                                                                                                                                                                          |
| `paymentToken`       | `String?`               |              | Відновлює наявну платіжну сесію замість того, щоб починати нову. Значення за замовчуванням: `null`.                                                                                                                                                                                           |
| `receiptConfig`      | `PaymentReceiptConfig?` |              | Перевизначає екран чека, що показується після **схваленого** платежу, лише для цього виклику. Значення за замовчуванням: `null`, тоді використовується типове налаштування, задане через `Accept.payments.setOptions()`.                                                                      |
| `nfcPosition`        | `NfcPositionConfig?`    |              | Перевизначає, куди вказує анімація прикладання картки, лише для цього виклику. Значення за замовчуванням: `null`, тоді використовується типове налаштування, задане через `Accept.payments.setOptions()`.                                                                                     |
| `metadata`           | `Map<String, String>`   |              | Дані у форматі «ключ-значення», прикріплені до платежу. Надсилаються плагіну Tapaya Terminal в intent `PAY` і повертаються у відповідному `PayResult`, якщо плагін їх повертає. Значення за замовчуванням: `emptyMap()`.                                                                      |
| `tip`                | `Long?`                 |              | Фіксовані чайові в найменшій одиниці валюти `currency`. Якщо задано, плагін пропускає власний інтерактивний екран чайових і стягує `amount + tip`. Значення за замовчуванням: `null` (плагін запитує платника про чайові).                                                                    |
| `transactionTimeout` | `Duration?`             |              | Перевизначає, скільки часу термінал чекає на прикладання картки, лише для цього виклику. Значення за замовчуванням: `null`, тоді використовується типове налаштування, задане через `Accept.payments.setOptions()`. Див. [Налаштування тайм-ауту транзакції](#setting-a-transaction-timeout). |
| `display`            | `PaymentDisplay?`       |              | Перевизначає, на якому екрані працює платіжний UI плагіна, лише для цього виклику. Значення за замовчуванням: `null`, тоді використовується типове налаштування, задане через `Accept.payments.setOptions()`. Див. [Вибір дисплея](#targeting-a-display).                                     |

Поки платіж триває, плагін Tapaya Terminal перебуває на передньому плані. Коли платіж завершується, ваш застосунок
автоматично відновлює роботу. Щоб відновити перерваний платіж, передайте його `paymentToken` назад у
`pay(amount, currency, paymentToken)`.

<Callout title="Повернення metadata не гарантоване" type="info">
  `metadata` повертаються в `PayResult` лише тоді, коли плагін їх повертає, тож розглядайте їх як додатковий контекст без гарантій
  (наприклад, ID замовлення для власного логування), а не як дані, від отримання яких залежить ваш застосунок.
</Callout>

### Вибір валюти [#choosing-the-currency]

`pay()` стягує оплату у валюті `currency`, яку ви передаєте як код ISO 4217 великими літерами, а `amount` вказується в найменшій
одиниці цієї валюти. На сторінці [Підтримувані валюти](/merchants/supported_currencies) наведено всі валюти, які підтримує Tapaya, але мерчанту
зазвичай увімкнено лише деякі з них.

Викличте `Accept.merchant.availableCurrencies()`, щоб отримати коди ISO 4217, увімкнені для мерчанта. Платіж у будь-якій іншій
валюті завершується помилкою `CurrencyNotAvailableForMerchant`.

Кожна валюта також має мінімальну суму платежу, яка діє на рівні всієї платформи. Поточні мінімуми можна отримати за допомогою
`Accept.sdk.minimumAmounts()`, який повертає `List<MinimumAmount>` (`currency: String, amount: Long`, у найменшій
одиниці валюти). Виклик `pay()` із сумою, нижчою за цей мінімум, завершується помилкою
`PaymentEvent.CreationFailed(AmountBelowMinimum(minimum, currency))`. Повний перелік винятків див. у розділі
[Обробка помилок](./sdk_integration#error-handling).

### Налаштування екрана чека [#configuring-the-receipt-screen]

`receiptConfig` визначає, що плагін Tapaya Terminal показує платнику після **схваленого** платежу, як довго
це залишається на екрані та чи друкується чек. Він не впливає на відхилені й сторновані платежі чи платежі, що очікують
на сторнування; для них завжди показується власний екран результату незалежно від цього налаштування.

| `PaymentReceiptConfig`                                    | Опис                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| :-------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DismissImmediately`                                      | Платнику нічого не показується.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `Show(showQrCodeReceipt, dismissBehavior, printBehavior)` | Показує екран чека. `showQrCodeReceipt: Boolean = true` вмикає або вимикає QR-код із посиланням на чек. `dismissBehavior: DismissBehavior = DismissBehavior.Manual` визначає, як екран закривається: `Manual` (залишається, доки платник його не закриє) або `Delayed(delay: Duration)` (закривається автоматично після `delay`, якщо платник не закрив його раніше). `printBehavior: ReceiptPrintBehavior = ReceiptPrintBehavior.OnDemand` керує паперовим чеком: `Automatic` (друкується одразу, щойно з'являється екран), `OnDemand` (лише коли друк запускає касир) або `Disabled` (ніколи не друкується). |

Задайте типове налаштування для всього пристрою один раз за допомогою `Accept.payments.setOptions()` і перевизначайте його для окремого платежу
через `pay(..., receiptConfig = ...)`. Доки `setOptions()` не викликано, типовим значенням є `Show` із QR-чеком, ручним
закриттям і друком на вимогу, що відповідає власному резервному налаштуванню плагіна.

<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>

### Розташування анімації прикладання NFC [#positioning-the-nfc-tap-animation]

`nfcPosition` є підказкою, яка повідомляє плагіну Tapaya Terminal, де розташована NFC-антена цього пристрою, щоб він міг
правильно розмістити анімацію «прикладіть тут». Плагін спершу сам визначає розташування антени (виявленням на платформі в реальному часі,
потім за вбудованою базою даних пристроїв, а потім за будь-якими налаштуваннями, які мерчант задав безпосередньо в плагіні); вказуйте це значення лише
тоді, коли потрібно перевизначити результат цього визначення, наприклад на пристрої, який плагін не може розпізнати або для якого в його базі даних указано неправильні дані.

| `NfcPositionConfig`                       | Опис                                                                                                                                                                              |
| :---------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Unspecified`                             | Без перевизначення (за замовчуванням). Плагін сам визначає розташування й зберігає будь-яке розташування, яке мерчант уже налаштував у ньому.                                     |
| `OnDevice(surface, xFraction, yFraction)` | Антена розташована на самому пристрої; анімація вказує на точку `(xFraction, yFraction)` на поверхні `surface`, якою є `OnDeviceSurface.BACK` або `OnDeviceSurface.FRONT_SCREEN`. |
| `ExternalAbove`                           | Окремий модуль зчитувача розташований над телефоном (конструкція smart-POS); на пристрої немає точки, на яку слід указувати.                                                      |

Задайте типове налаштування для всього пристрою за допомогою `Accept.payments.setOptions(nfcPosition = ...)` або перевизначте його для окремого виклику
через `pay(..., nfcPosition = ...)`. `setOptions()` змінює лише передані вами параметри, тож окремий виклик `setOptions(nfcPosition = ...)`
не порушить раніше задане типове значення `receiptConfig`, і навпаки.

### Налаштування тайм-ауту транзакції [#setting-a-transaction-timeout]

`transactionTimeout` визначає, скільки часу плагін Tapaya Terminal чекає, поки клієнт прикладе картку, перш ніж
припинити спробу. Він приймає будь-яке значення `Duration` від нуля до 120 секунд; будь-яке значення поза цим діапазоном надходить як
`TransactionTimeoutOutOfRange(transactionTimeout)` через `PaymentEvent.CreationFailed`, оскільки `pay()` ніколи не викидає винятків
синхронно.

<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>

Залиште типове значення `null`, щоб використовувати власний резервний тайм-аут супутнього застосунку (близько 60 секунд), або задайте типове
значення для всього пристрою за допомогою `Accept.payments.setOptions(transactionTimeout = ...)`.

### Вибір дисплея [#targeting-a-display]

На двосторонньому («double-sided») пристрої одна панель повернута до мерчанта, а друга, клієнтська панель, повернута
до платника. `Accept.displays` повідомляє, які дисплеї пристрій має зараз, а параметр `display` вказує плагіну,
на якій панелі запускати свій платіжний UI.

<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`      | Опис                                                                                         |
| :-------------------- | :------------------------------------------------------------------------------------------- |
| `Default`             | Поточний дисплей застосунку-хоста (типова поведінка, без змін на пристроях з одним екраном). |
| `CustomerFacing`      | Вторинна панель двостороннього пристрою.                                                     |
| `Specific(displayId)` | Конкретний дисплей за `id`, який повертає `Accept.displays.all()`.                           |

Ціль, яку неможливо застосувати, наприклад `CustomerFacing` на пристрої з одним екраном, непомітно замінюється на
`Default`. Задайте типове налаштування для всього пристрою за допомогою `Accept.payments.setOptions(display = ...)` або перевизначте його для окремого виклику
через `pay(..., display = ...)`.

<Callout title="Задайте ціль перед автентифікацією" type="info">
  Попередня підготовка термінала (warm-up), яку запускає `Accept.auth.authenticate()`, використовує ту саму ціль, тож викличте
  `setOptions(display = ...)` перед `authenticate()`, якщо хочете, щоб і сама підготовка не відбувалася на
  екрані мерчанта.
</Callout>

Демонстраційний застосунок `:dualscreen` показує повний процес оплати на двох панелях: мерчант працює на основній панелі, а
`PaymentDisplay.CustomerFacing` виводить платіжний UI плагіна на екран клієнта.

## Результат платежу [#payment-outcome]

Кінцева подія `PaymentEvent.Result` містить `PayResult`. Кожен варіант містить `metadata`, повертаючи те, що ви
передали в `pay()`, якщо плагін їх повертає (може бути `null`):

| `PayResult`                                                 | Значення                                                                                                                                                                                                                     |
| :---------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Success(paymentToken, metadata, authCode, receiptDetails)` | Авторизовано й кошти списано. Кошти буде зараховано мерчанту під час розрахунку. `authCode` є кодом авторизації процесора, який повідомляє термінал; має інформаційний характер і залежно від процесора може бути відсутнім. |
| `Declined(metadata, receiptDetails)`                        | Відхилено платіжним процесором або банком-емітентом картки.                                                                                                                                                                  |
| `Canceled(metadata)`                                        | Скасовано клієнтом або мерчантом до завершення.                                                                                                                                                                              |
| `Failed(reason, metadata)`                                  | Не завершено; `reason` має тип `PayFailureReason`.                                                                                                                                                                           |

`PayFailureReason` набуває одного зі значень `INVALID_INTENT`, `NOT_ACTIVATED`, `TERMINAL_INIT_FAILED`, `PAYMENT_SETUP_FAILED`,
`PLUGIN_UNAVAILABLE` або `UNKNOWN`.

### Отримання даних чека [#reading-receipt-details]

`Success` і `Declined` містять необов'язкове поле `receiptDetails: ReceiptDetails?` зі структурованими даними карткового чека,
які повідомляє термінал. Воно призначене для тих, хто друкує власний офіційний картковий чек, а не покладається лише на
розміщену сторінку `PaymentStatus.receiptUrl`.

| Поле                   | Тип       | Опис                                         |
| :--------------------- | :-------- | :------------------------------------------- |
| `merchantId`           | `String?` | ID мерчанта, який повідомляє процесор.       |
| `terminalId`           | `String?` | ID термінала, який повідомляє процесор.      |
| `cardBrand`            | `String?` | Бренд картки, наприклад Visa або Mastercard. |
| `appLabel`             | `String?` | Мітка застосунку EMV.                        |
| `aid`                  | `String?` | Ідентифікатор застосунку EMV.                |
| `maskedPan`            | `String?` | Маскований номер картки.                     |
| `responseCode`         | `String?` | Код відповіді процесора.                     |
| `transactionTimestamp` | `String?` | Позначка часу транзакції у форматі ISO-8601. |

Кожне поле є необов'язковим і заповнюється без гарантій; код, що викликає, має бути готовим до того, що будь-яке з них або саме `receiptDetails`
буде відсутнім.

### Отримання даних платежу [#reading-payment-details]

`PayResult.Success` надає вам `paymentToken`. Повний запис, зокрема `receiptUrl`, запитану
суму й суму розрахунку, а також позначки часу, можна отримати за допомогою `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>

Зупинити платіж, що триває, можна за допомогою `Accept.payments.cancel(paymentToken)`, який повертає підсумковий `PaymentState`.

### Повернення коштів [#issuing-a-refund]

Повернути кошти за завершеним платежем можна за допомогою `Accept.payments.refund(paymentToken, reason)`, який також повертає підсумковий
`PaymentState`.

<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` набуває одного зі значень `CUSTOMER_REQUEST`, `DUPLICATE`, `FRAUDULENT` або `EXPIRED_CHARGE`.

## Обробка помилок [#error-handling]

Платіж, який дійшов до термінала, але не був успішним, **не** є винятком; він надходить як
`PaymentEvent.Result` із `PayResult.Declined`, `Canceled` або `Failed(reason)`. Збої, що не дають
створити платіж, надходять як `PaymentEvent.CreationFailed(cause)`, де `cause` є типізованим
`AcceptException`. Винятки, специфічні для цієї інтеграції:

| Виняток                                            | Коли виникає                                                                       | Що робити                                                                                             |
| :------------------------------------------------- | :--------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- |
| `PluginUnavailable`                                | Плагін Tapaya Terminal не встановлено або не вдалося підключитися до його сервісу. | Запропонуйте встановлення за допомогою `Accept.plugin.install()`.                                     |
| `CurrencyNotAvailableForMerchant(code)`            | Для мерчанта не налаштовано запитану валюту.                                       | Стягуйте оплату в увімкненій валюті або оновіть конфігурацію мерчанта.                                |
| `AmountBelowMinimum(minimum, currency)`            | Сума нижча за налаштований мінімум.                                                | Стягуйте суму, що дорівнює `minimum` або перевищує його.                                              |
| `TransactionTimeoutOutOfRange(transactionTimeout)` | `transactionTimeout` виходив за межі діапазону від 0 до 120 секунд.                | Передайте значення в межах діапазону або не вказуйте його, щоб використати значення за замовчуванням. |
| `LocationPermissionRequired`                       | Вашому застосунку не надано дозволу на доступ до геолокації.                       | Запросіть `ACCESS_FINE_LOCATION`, а потім повторіть спробу.                                           |
| `LocationUnavailable` / `LocationTimeout`          | Не вдалося визначити геолокацію пристрою.                                          | Попросіть користувача ввімкнути геолокацію та повторіть спробу.                                       |
| `SessionExpired`                                   | Строк дії автентифікованої сесії минув.                                            | Повторно автентифікуйтеся за допомогою `Accept.auth.authenticate()`.                                  |
| `NoInternetConnection`                             | Немає підключення до мережі.                                                       | Попросіть користувача знову підключитися та повторіть спробу.                                         |
| `PaymentInProgress`                                | Попередній виклик `pay()` досі очікує на результат.                                | Зачекайте, доки поточний платіж завершиться.                                                          |

`Accept.payments.status()`, `cancel()` і `refund()` є функціями `suspend` і **викидають** ці винятки
(а не надсилають події); обгортайте їх у `try-catch`. `status()`/`cancel()`/`refund()` також викидають
`PaymentNotFound` для невідомого токена. Повний перелік див. у розділі
[Обробка помилок](../integration/sdk_integration#error-handling).

## Тайм-аути [#timeouts]

Платіж карткою потрібно завершити на терміналі невдовзі після його початку. Якщо клієнт так і не прикладе картку або
плагін Tapaya Terminal закриється посеред платежу, платіж завершиться як `PayResult.Canceled` або
`PayResult.Failed`, а відповідний платіж буде скасовано на стороні сервера, і він ніколи не буде проведений. Завжди перевіряйте
кінцевий стан за допомогою `Accept.payments.status()`, перш ніж вважати платіж завершеним.

## Інтеграція без SDK [#integrate-without-the-sdk]

Якщо ви не можете вбудувати Accept SDK (кросплатформний фреймворк, тонка POS-оболонка, вебкаса), ви можете керувати
Tapaya Terminal напряму за допомогою deeplink. Розподіл роботи змінюється: **ви** створюєте платіж через
Tapaya API і **ви** опитуєте його статус, щоб отримати результат; Tapaya Terminal відповідає лише за взаємодію з карткою.

Послідовність:

1. Створіть платіж зі свого бекенда через [Payments API](https://api.tapaya.com/openapi/index.html) і збережіть
   його `paymentId`.
2. Відкрийте Tapaya Terminal із цим `paymentId` за допомогою [платіжного deeplink](#pay-deeplink).
3. Опитуйте статус платежу через [Payments API](https://api.tapaya.com/openapi/index.html), доки він не вийде зі стану
   `PENDING`. Deeplink не має зворотного каналу, тому опитування є єдиним надійним джерелом результату.

<Callout title="Примітка" type="info">
  Tapaya Terminal має бути встановлено, у ньому має бути виконано вхід в обліковий запис мерчанта, і його має бути налаштовано для валюти
  платежу. У цьому режимі немає кроку активації; мерчантом, відповідальним за транзакцію (merchant of record), є обліковий запис, у який виконано вхід. За потреби
  виконайте вхід мерчанта за допомогою [deeplink для входу](#login-deeplink).
</Callout>

### Схеми deeplink [#deeplink-schemes]

Схема визначає, яка збірка Tapaya Terminal обробляє посилання:

| Середовище          | Схема                   |
| :------------------ | :---------------------- |
| Робоче середовище   | `tapaya-accept`         |
| Пісочниця (sandbox) | `tapaya-accept-sandbox` |

Якщо посилання не відкривається, цю збірку Tapaya Terminal не встановлено.

### Платіжний deeplink [#pay-deeplink]

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

| Параметр запиту | Обов'язковий | Примітки                                                                                                                                          |
| :-------------- | :----------- | :------------------------------------------------------------------------------------------------------------------------------------------------ |
| `paymentId`     | **Так**      | Платіж, створений на кроці 1.                                                                                                                     |
| `amount`        | **Так**      | Сума в **основних одиницях** як ціле число: `amount=15` стягує 15,00. Зверніть увагу, що це відрізняється від SDK, який приймає найменші одиниці. |
| `currency`      | **Так**      | Код валюти ISO 4217, наприклад `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 для входу [#login-deeplink]

Виконайте вхід мерчанта в Tapaya Terminal за допомогою його `merchantToken`, ідентифікатора мерчанта, який ви задаєте через `POST /merchant/auth/register`:

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

| Параметр запиту  | Обов'язковий | Примітки                                                                                                             |
| :--------------- | :----------- | :------------------------------------------------------------------------------------------------------------------- |
| `token`          | **Так**      | `merchantToken` мерчанта. Це ваш незмінний ID мерчанта, а не короткоживучий токен SDK з `POST /merchant/auth/login`. |
| `activationCode` | Ні           | Попередньо заповнює код активації термінала.                                                                         |

## Тестування інтеграції [#test-the-integration]

1. Ініціалізуйте з `isProduction = false` (значення за замовчуванням); платежі в пісочниці ніколи не переказують реальні гроші.
2. Установіть плагін Tapaya Terminal на тестовий пристрій, увійдіть як мерчант пісочниці та виконайте активацію.
3. Проведіть платіж на невелику суму й перевірте весь сценарій `Success` від початку до кінця, зокрема ваш UI чека.
4. Перевірте сценарії збоїв: відхиліть платіж на терміналі, скасуйте інший і почніть ще один у непідтримуваній
   валюті; ваша обробка `PayResult`/`CreationFailed` має охоплювати кожен із них.

Коли будете готові перейти в робочий режим, задайте `isProduction = true` в `initialize` і автентифікуйтеся з обліковими даними
робочого середовища. Код платежів не змінюється.
