Skip to main content

Android Payment Interface

This document describes the Android POS app's payment interface dispatch layer — how a (GatewayName, DeviceType) pair on a terminal resolves to a concrete PaymentInterface, and how to add a new payment provider.

Terminology note. "Gateway" in this document refers to the Android-side GatewayName enum (PEAK_GATEWAY, CASH_ONLY) — i.e. which payment interface does this terminal use. This is not the same as the pinpointpos gateway product (the separate hosted-payments product the POS integrates with). For the POS↔gateway-product boundary, see the Gateway Integration Charter at docs/superpowers/specs/2026-04-13-gateway-integration-charter-design.md.

Overview

PeakPOS supports a small, fixed set of payment gateways. Every terminal has exactly one active gateway, identified by the GatewayName enum, and (for card-capable gateways) one hardware DeviceType. The pair (gatewayName, deviceType) deterministically resolves to a concrete PaymentInterface via PaymentInterfaceFactory.create(...).

PaymentViewModel


PaymentManager


PaymentInterfaceFactory.create(gatewayName, deviceType)

├── PEAK_GATEWAY + N62/N92 → NexgoSmartConnectInterface (TCP → Nexgo terminal)
├── PEAK_GATEWAY + TapToPay → UnsupportedPaymentInterface (recognized, not yet implemented)
├── PEAK_GATEWAY + null → UnsupportedPaymentInterface (no device configured)
└── CASH_ONLY → CashOnlyPaymentInterface (auto-declines all card tx)

Supported gateways and devices

GatewayName (apps/android/app/src/main/java/com/myriad/pinpointpos/payment/PaymentInterfaces.kt):

GatewayDescription
PEAK_GATEWAYThe Peak-managed processing gateway. Requires a DeviceType.
CASH_ONLYCash-only terminal. Card transactions are auto-declined locally.

DeviceType (only meaningful when gatewayName == PEAK_GATEWAY):

DeviceStatus
N62Nexgo N62 terminal, driven by NexgoSmartConnectInterface (newline-delimited JSON over TCP)
N92Nexgo N92 terminal, driven by NexgoSmartConnectInterface (same protocol as N62)
TapToPayRecognized by the factory but not yet implemented — card tx return decline

There are no other gateways. The previous multi-interface map plus separate processorType / preferredInterface fields have been replaced by the single (gatewayName, deviceType) pair carried in PaymentInterfaceConfig.

Key components

ComponentLocationResponsibility
GatewayName (enum)payment/PaymentInterfaces.ktCanonical gateway identifier (PEAK_GATEWAY, CASH_ONLY)
DeviceType (enum)payment/PaymentInterfaces.ktHardware variant under PeakGateway (N62, N92, TapToPay)
PaymentInterfacepayment/PaymentInterfaces.ktAbstract contract all providers implement; exposes gatewayName
PaymentInterfaceConfigpayment/PaymentInterfaces.ktgatewayName, deviceType, apiKey, appIdentifier, environment, options
PaymentInterfaceFactorypayment/PaymentInterfaces.ktDispatch table; create(gatewayName, deviceType) returns the interface
PaymentManagerpayment/PaymentManager.ktTop-level orchestrator that delegates to the resolved PaymentInterface
NexgoSmartConnectInterfacepayment/nexgo/NexgoSmartConnectInterface.ktNexgo SmartConnect TCP socket implementation (used for both N62 and N92)
CashOnlyPaymentInterfacepayment/CashOnlyPaymentInterface.ktNo-op card path: every card transaction is locally declined
UnsupportedPaymentInterfacepayment/UnsupportedPaymentInterface.ktPlaceholder returned for valid-but-unimplemented combinations
PaymentViewModelpayment/PaymentViewModel.ktAndroid ViewModel; consumes the abstraction

PaymentInterface contract

abstract class PaymentInterface(protected val context: Context) {
abstract val gatewayName: GatewayName
open val requiresActivityContext: Boolean = false
open val supportsTipAdjust: Boolean = true
open val supportsBatchSettlement: Boolean = true
open val processorName: String? = null
open val pendingPaymentDao: PendingPaymentRequestDao? = null

open fun setActivity(activity: Activity?) {}
abstract fun initialize(config: PaymentInterfaceConfig)
open suspend fun connectDevice() {}
abstract suspend fun performTransaction(
amount: Double,
transactionType: String,
paymentType: PaymentType = PaymentType.CREDIT,
referenceId: String? = null,
): TransactionResult
abstract suspend fun tipAdjust(referenceId: String, tipAmount: Double): TransactionResult
open suspend fun batchSettle(): SettlementResult =
throw UnsupportedOperationException("Batch settlement is not supported by this payment interface")
abstract fun terminalStatus(): DeviceInfo
}

Configuration

Provider selection is config-driven via PaymentInterfaceConfig:

data class PaymentInterfaceConfig(
val gatewayName: GatewayName,
val deviceType: DeviceType? = null,
val apiKey: String = "",
val appIdentifier: String = "",
val environment: String = "production", // "production" | "sandbox" | "test"
val options: Map<String, String> = emptyMap(),
)

The backend hands this to the device verbatim — terminal-api flattens gatewayName, deviceType, apiKey, appIdentifier, environment, and options as top-level keys on the payment-config response, and the Android ProvisioningManager.parseInterfaceConfig reads them back into a PaymentInterfaceConfig.

NexgoSmartConnect (N62 / N92) options

When gatewayName == PEAK_GATEWAY and deviceType is N62 or N92, the factory wires up NexgoSmartConnectInterface, which talks newline-delimited JSON over a TCP socket to the Nexgo terminal. The terminal itself handles EMV certification, PCI scope, and processor communication (TSYS on the wire to the device); the POS app only sends transaction requests and parses results.

OptionDefaultDescription
terminalIprequiredIP address of the Nexgo terminal on the LAN
port8765TCP port the SmartConnect app listens on
processorNameTSYSProcessor identifier in each on-wire request
receiptPreferencetrueWhether SmartConnect should print a receipt

Note: processorName here describes the on-device wire protocol with the Nexgo terminal — it is not a backend gateway choice. The only backend gateway choices are PEAK_GATEWAY and CASH_ONLY.

Backward-compat note: interface_name SQL column

The on-device SQLite recovery log (pending_smartconnect_requests) still has an interface_name column with a default of 'NEXGO_SMARTCONNECT'. This column is intentionally retained for backward compatibility with rows written by pre-refactor app versions and is unrelated to the GatewayName enum. Do not touch the column or its migration.

Adding a new gateway or device

The supported set is intentionally narrow. Adding a new option is a deliberate change:

  1. New device under PeakGateway — add the variant to enum class DeviceType and add a branch to the inner when (deviceType) in PaymentInterfaceFactory.create(). If it shares the SmartConnect protocol, route it to NexgoSmartConnectInterface; otherwise create a new PaymentInterface implementation.

  2. New gateway — add the variant to enum class GatewayName and add a top-level branch to PaymentInterfaceFactory.create() returning the appropriate interface. Update the support-portal payment-config UI (apps/websites/support/src/pages/TerminalsPage.tsx) and the management-api PaymentConfigRequest/Response enum mapping to expose the new value.

  3. Contract test — extend PaymentInterfaceContractTest so the new implementation passes the standard 5 contract tests (success, declined, tip adjust, terminal status, initialize-without-throw).

  4. Verify with:

    bazel build --config=android_arm64 //apps/android:android_app
    bazel test //apps/microservices/management-api:core_test //apps/microservices/merchant-api:core_test

Decision log

Why two enums (GatewayName + DeviceType) instead of one flat list? The original design had a single string interfaceName plus a sibling processorType field, which let any combination be configured (and several were never valid). Splitting into two enums lets the dispatch table reject invalid pairs (e.g. CASH_ONLY + N62) at compile time and keeps the gateway-vs-hardware concerns separate.

Why factory instead of DI? PaymentInterfaceFactory is a singleton object rather than a Spring bean because the Android app uses manual DI in the payment module. The factory keeps provider selection centralized and testable.

Why SmartConnect (TCP) instead of an SDK for N62/N92? Nexgo SmartConnect exposes a simple JSON-over-TCP protocol that avoids SDK version coupling. The terminal handles EMV certification, PCI scope, and processor communication — the POS app only sends transaction requests and parses results.

Why CASH_ONLY is its own gateway instead of "no payment provider"? Treating cash-only as a real PaymentInterface means the rest of the app (PaymentManager, PaymentViewModel, recovery logic) has a single uniform code path: every terminal has a gateway, every card transaction goes through a PaymentInterface, and "cash only" simply auto-declines.