Overview
NetPOS uses Retrofit for RESTful API communication with multiple backend services. The app integrates with Storm API, payment gateways (Zenith, Providus, FCMB), and NIBSS services.
Service Configuration
All services are provided via Dagger Hilt dependency injection in di/Module.kt:38.
Base URLs
Default Service
Zenith Pay-by-Transfer
QR Payment Service
@Named ( "defaultBaseUrl" )
fun providesDefaultBaseUrl (): String = BuildConfig.STRING_DEFAULT_BASE_URL
Core API Services
StormApiService
Main backend API for authentication, transactions, and merchant operations.
network/StormApiService.kt:9
interface StormApiService {
@POST ( "api/token" )
fun appToken ( @Body credentials: JsonObject ?): Single < TokenResp >
@POST ( "api/auth" )
fun userToken ( @Body credentials: JsonObject ?): Single < AppLoginResponse >
@GET ( "api/agents/{stormId}" )
fun getAgentDetails ( @Path ( "stormId" ) stormId: String ?): Single < User >
@POST ( "api/passwordReset" )
fun passwordReset ( @Body payload: JsonObject ?): Single < Response < Any ?>?>
@GET ( "/api/nip-notifications" )
fun getNotificationByReference (
@Query ( "referenceNo" ) reference: String ,
@Header ( "X-CLIENT-ID" ) clientId: String ,
@Header ( "X-ACCESSCODE" ) accessCode: String
): Single < NipNotification >
@POST ( "/pos_transaction" )
fun logTransactionBeforeConnectingToNibss (
@Body dataToLog: TransactionToLogBeforeConnectingToNibbs
): Single < ResponseBodyAfterLoginToBackend >
@PUT ( "/pos_transaction/{rrn}" )
fun updateLogAfterConnectingToNibss (
@Path ( "rrn" ) rrn: String ,
@Body data : DataToLogAfterConnectingToNibss
): Single < Response < LogToBackendResponse >>
@GET ( "/pos_transactions/terminal/{terminalId}/btw/{from}/{to}/{page}/{pageSize}" )
fun getTransactionsFromNewService (
@Path ( "terminalId" ) terminalId: String ,
@Path ( "from" ) from: String ,
@Path ( "to" ) to: String ,
@Path ( "page" ) page: Int ,
@Path ( "pageSize" ) pageSize: Int
): Single < GetEndOfDayModelFromNewServer >
}
StormApiService Endpoints
Get application-level authentication token Request: {
"appId" : "netpos-app" ,
"appSecret" : "***"
}
Response: TokenResp { success: Boolean, token: String }Authenticate merchant user and retrieve JWT token Request: {
"username" : "[email protected] " ,
"password" : "***" ,
"deviceId" : "optional-device-id"
}
Response: AppLoginResponse { success, token, data }Log transaction before NIBSS connection Returns: RRN for transaction tracking
GET /pos_transactions/terminal/{terminalId}/btw/{from}/{to}
Retrieve transactions for EOD report Query Params:
terminalId: Terminal identifier
from: Start date (yyyy-MM-dd HH:mm:ss)
to: End date
page: Page number
pageSize: Results per page
ZenithPayByTransferService
Zenith Bank pay-by-transfer integration for account details and transaction queries.
network/ZenithPayByTransferService.kt:11
interface ZenithPayByTransferService {
@GET ( "api/getUserAccount/{terminalId}" )
fun getUserAccount (
@Path ( "terminalId" ) terminalId: String
): Single < GetPayByTransferUserAccount >
@GET ( "api/queryTransactions/{requestParameters}" )
fun getTransactions (
@Path ( "requestParameters" ) requestParameters: String
): Single < GetZenithPayByTransferUserTransactions >
@POST ( "api/addFirebaseToken" )
fun registerDeviceToken (
@Body req: ZenithPayByTransferRegisterDeviceTokenModel
): Single < String >
}
This service uses Bearer token authentication via zenithPayByTransferHeaderInterceptor in di/Module.kt:92
QrPaymentService
Contactless QR code payment processing.
network/QrPaymentService.kt:11
interface QrPaymentService {
@POST ( "contactlessQr" )
fun payWithQr (
@Body payWithQrRequest: PayWithQrRequest
): Single < Response < String >>
}
CheckoutService
Payment checkout and session management.
interface CheckoutService {
@POST ( "checkout/initialize" )
fun initializeCheckout (
@Body checkOutModel: CheckOutModel
): Single < CheckOutResponse >
@GET ( "checkout/verify/{reference}" )
fun verifyCheckout (
@Path ( "reference" ) reference: String
): Single < CheckOutResponse >
}
SubmitComplaintsService
Customer feedback and complaints submission.
interface SubmitComplaintsService {
@POST ( "api/complaints" )
fun submitComplaint (
@Body feedbackRequest: FeedbackRequest
): Single < Response < String >>
}
Bank Integration Services
ProvidusMerchantsAccountService
Providus Bank merchant account operations.
interface ProvidusMerchantsAccountService {
@GET ( "merchant/account/{accountNumber}" )
fun getMerchantAccount (
@Path ( "accountNumber" ) accountNumber: String
): Single < GetPayByTransferUserAccount >
@GET ( "merchant/transactions" )
fun getTransactions (
@Query ( "accountNumber" ) accountNumber: String ,
@Query ( "from" ) from: String ,
@Query ( "to" ) to: String
): Single < List < Transaction >>
}
FcmbMerchantsAccountService
FCMB Bank merchant account integration.
interface FcmbMerchantsAccountService {
@GET ( "merchant/account/{accountNumber}" )
fun getMerchantAccount (
@Path ( "accountNumber" ) accountNumber: String
): Single < GetPayByTransferUserAccount >
}
OkHttp Configuration
Default HTTP Client
@Named ( "defaultOkHttpClient" )
fun providesDefaultOkHttpClient (
@Named ( "loginInterceptor" ) loggingInterceptor: Interceptor
): OkHttpClient =
OkHttpClient (). newBuilder ()
. connectTimeout ( 120 , TimeUnit.SECONDS)
. readTimeout ( 120 , TimeUnit.SECONDS)
. writeTimeout ( 120 , TimeUnit.SECONDS)
. retryOnConnectionFailure ( true )
. addInterceptor (loggingInterceptor)
. build ()
Timeout Configuration:
Connect: 120 seconds
Read: 120 seconds
Write: 120 seconds
Auto-retry on connection failure: Enabled
Authenticated HTTP Client
@Named ( "zenithPayByTransferOkHttp" )
fun providesZenithOkHttpClient (
@ApplicationContext context: Context ,
@Named ( "loginInterceptor" ) loggingInterceptor: Interceptor ,
@Named ( "zenithPayByTransferHeaderInterceptor" ) authInterceptor: Interceptor
): OkHttpClient =
OkHttpClient (). newBuilder ()
. connectTimeout ( 120 , TimeUnit.SECONDS)
. readTimeout ( 120 , TimeUnit.SECONDS)
. addInterceptor (authInterceptor)
. addInterceptor (loggingInterceptor)
. build ()
Interceptors
Logging Interceptor
@Named ( "loginInterceptor" )
fun providesLoginInterceptor (): Interceptor =
HttpLoggingInterceptor (). apply {
setLevel (HttpLoggingInterceptor.Level.BODY)
}
Set logging level to BASIC or NONE in production to prevent sensitive data exposure.
Authorization Interceptor
@Named ( "zenithPayByTransferHeaderInterceptor" )
fun providesZenithPayByTransferHeaderInterceptor (): Interceptor =
Interceptor { chain ->
val originalRequest = chain. request ()
val requestWithAuth = originalRequest. newBuilder ()
. addHeader ( "Authorization" , "Bearer ${ Prefs. getString (PREF_USER_TOKEN, "" ) } " )
. build ()
chain. proceed (requestWithAuth)
}
Retrofit Configuration
Default Retrofit Instance
@Named ( "defaultRetrofit" )
fun providesDefaultRetrofit (
@Named ( "defaultOkHttpClient" ) okhttp: OkHttpClient ,
@Named ( "defaultBaseUrl" ) baseUrl: String
): Retrofit =
Retrofit. Builder ()
. addConverterFactory (GsonConverterFactory. create ())
. addCallAdapterFactory (RxJava2CallAdapterFactory. create ())
. baseUrl (baseUrl)
. client (okhttp)
. build ()
Converters:
GsonConverterFactory : JSON serialization/deserialization
RxJava2CallAdapterFactory : Reactive API calls returning Single<T>, Observable<T>, etc.
Service Injection
Services are injected via Dagger Hilt:
@Provides @Singleton
fun providesStormApiService (
@Named ( "defaultRetrofit" ) retrofit: Retrofit
): StormApiService = retrofit. create (StormApiService:: class .java)
@Provides @Singleton
fun providesZenithPayByTransferService (
@Named ( "zenithPayByTransferRetrofit" ) retrofit: Retrofit
): ZenithPayByTransferService = retrofit. create (ZenithPayByTransferService:: class .java)
Repository Pattern
ZenithPayByTransferRepository
Repository wrapping service calls with business logic:
class ZenithPayByTransferRepository (
private val service: ZenithPayByTransferService ,
private val dao: ZenithPayByTransferUserTransactionsDao
) {
fun getUserAccount (terminalId: String ): Single < GetPayByTransferUserAccount > {
return service. getUserAccount (terminalId)
. subscribeOn (Schedulers. io ())
}
fun getTransactionsWithCache (
params: String
): Single < GetZenithPayByTransferUserTransactions > {
return service. getTransactions (params)
. doOnSuccess { response ->
// Cache to local database
dao. insertTransactions (response.transactions)
}
. onErrorResumeNext { error ->
// Fallback to cached data
dao. getAllTransactions (). map {
GetZenithPayByTransferUserTransactions (it)
}
}
}
}
RxJava Call Patterns
Basic API Call
stormApiService. userToken (credentials)
. subscribeOn (Schedulers. io ())
. observeOn (AndroidSchedulers. mainThread ())
. subscribe { response, error ->
response?. let { handleSuccess (it) }
error?. let { handleError (it) }
}
. disposeWith (compositeDisposable)
Chained Calls
stormApiService. userToken (credentials)
. flatMap { loginResponse ->
val token = loginResponse.token
Prefs. putString (PREF_USER_TOKEN, token)
stormApiService. getAgentDetails ( getStormId (token))
}
. flatMap { user ->
// Save to database
userDao. insert (user)
}
. subscribeOn (Schedulers. io ())
. observeOn (AndroidSchedulers. mainThread ())
. subscribe { _, error ->
// Handle final result
}
Retry Logic
stormApiService. getTransactions (terminalId)
. retry ( 3 )
. retryWhen { errors ->
errors. zipWith (Observable. range ( 1 , 3 )) { error, retryCount ->
if (retryCount < 3 ) retryCount else throw error
}. flatMap { retryCount ->
Observable. timer (retryCount * 2L , TimeUnit.SECONDS)
}
}
. subscribe { response, error ->
// Handle response
}
Error Handling
HTTP Exception Handling
apiService. getData ()
. subscribe { response, error ->
error?. let {
when ( val httpException = it as ? HttpException) {
null -> Timber. e ( "Network error: ${ it.message } " )
else -> {
when (httpException. code ()) {
401 -> handleUnauthorized ()
404 -> handleNotFound ()
500 -> handleServerError ()
else -> handleGenericError (httpException)
}
}
}
}
}
Parsing Error Body
val httpException = error as ? HttpException
val errorBody = httpException?. response ()?. errorBody ()?. string ()
val errorMessage = try {
Gson (). fromJson (errorBody, ErrorResponse:: class .java).message
} catch (e: Exception ) {
"An unexpected error occurred"
}
Testing Services
MockWebServer Example
class StormApiServiceTest {
private lateinit var mockWebServer: MockWebServer
private lateinit var apiService: StormApiService
@Before
fun setup () {
mockWebServer = MockWebServer ()
mockWebServer. start ()
val retrofit = Retrofit. Builder ()
. baseUrl (mockWebServer. url ( "/" ))
. addConverterFactory (GsonConverterFactory. create ())
. addCallAdapterFactory (RxJava2CallAdapterFactory. create ())
. build ()
apiService = retrofit. create (StormApiService:: class .java)
}
@Test
fun `login returns success response` () {
val mockResponse = MockResponse ()
. setResponseCode ( 200 )
. setBody ( """{"success":true,"token":"abc123"}""" )
mockWebServer. enqueue (mockResponse)
val credentials = JsonObject (). apply {
addProperty ( "username" , "[email protected] " )
addProperty ( "password" , "password" )
}
val response = apiService. userToken (credentials). blockingGet ()
assertTrue (response.success)
assertEquals ( "abc123" , response.token)
}
}
Best Practices
Always use Schedulers Subscribe on Schedulers.io(), observe on AndroidSchedulers.mainThread()
Dispose subscriptions Use CompositeDisposable and dispose in onCleared() or onDestroy()
Handle errors gracefully Provide user-friendly error messages, log technical details
Use repositories Abstract network calls in repository layer for testability
Architecture Dependency injection setup
Models Request/response models
ViewModels Service consumption patterns
Security Authentication and encryption