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
GatewayNameenum (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 atdocs/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):
| Gateway | Description |
|---|---|
PEAK_GATEWAY | The Peak-managed processing gateway. Requires a DeviceType. |
CASH_ONLY | Cash-only terminal. Card transactions are auto-declined locally. |
DeviceType (only meaningful when gatewayName == PEAK_GATEWAY):
| Device | Status |
|---|---|
N62 | Nexgo N62 terminal, driven by NexgoSmartConnectInterface (newline-delimited JSON over TCP) |
N92 | Nexgo N92 terminal, driven by NexgoSmartConnectInterface (same protocol as N62) |
TapToPay | Recognized 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
| Component | Location | Responsibility |
|---|---|---|
GatewayName (enum) | payment/PaymentInterfaces.kt | Canonical gateway identifier (PEAK_GATEWAY, CASH_ONLY) |
DeviceType (enum) | payment/PaymentInterfaces.kt | Hardware variant under PeakGateway (N62, N92, TapToPay) |
PaymentInterface | payment/PaymentInterfaces.kt | Abstract contract all providers implement; exposes gatewayName |
PaymentInterfaceConfig | payment/PaymentInterfaces.kt | gatewayName, deviceType, apiKey, appIdentifier, environment, options |
PaymentInterfaceFactory | payment/PaymentInterfaces.kt | Dispatch table; create(gatewayName, deviceType) returns the interface |
PaymentManager | payment/PaymentManager.kt | Top-level orchestrator that delegates to the resolved PaymentInterface |
NexgoSmartConnectInterface | payment/nexgo/NexgoSmartConnectInterface.kt | Nexgo SmartConnect TCP socket implementation (used for both N62 and N92) |
CashOnlyPaymentInterface | payment/CashOnlyPaymentInterface.kt | No-op card path: every card transaction is locally declined |
UnsupportedPaymentInterface | payment/UnsupportedPaymentInterface.kt | Placeholder returned for valid-but-unimplemented combinations |
PaymentViewModel | payment/PaymentViewModel.kt | Android 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.
| Option | Default | Description |
|---|---|---|
terminalIp | required | IP address of the Nexgo terminal on the LAN |
port | 8765 | TCP port the SmartConnect app listens on |
processorName | TSYS | Processor identifier in each on-wire request |
receiptPreference | true | Whether 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:
-
New device under PeakGateway — add the variant to
enum class DeviceTypeand add a branch to the innerwhen (deviceType)inPaymentInterfaceFactory.create(). If it shares the SmartConnect protocol, route it toNexgoSmartConnectInterface; otherwise create a newPaymentInterfaceimplementation. -
New gateway — add the variant to
enum class GatewayNameand add a top-level branch toPaymentInterfaceFactory.create()returning the appropriate interface. Update the support-portal payment-config UI (apps/websites/support/src/pages/TerminalsPage.tsx) and the management-apiPaymentConfigRequest/Responseenum mapping to expose the new value. -
Contract test — extend
PaymentInterfaceContractTestso the new implementation passes the standard 5 contract tests (success, declined, tip adjust, terminal status, initialize-without-throw). -
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.