A pure Haskell SDK that simulates a UPI payment lifecycle: initiate -> authenticate -> settle -> reconcile.
This project is intentionally designed with:
- Pure business logic in domain modules
- Explicit error handling with
Either TxnError a - A single IO boundary in
SDKfor in-memory state - Deterministic behavior suitable for tests and interviews
The SDK models core UPI payment behavior without networking or databases. It demonstrates how to design a clear, testable payment flow using functional patterns.
- Language: Haskell (GHC 9.6.5)
- Build tool: Stack
- Testing: Hspec
- Key libraries:
text,hashable,containers
.
|- src/
| |- Types.hs
| |- Transaction.hs
| |- UPI.hs
| |- Reconciliation.hs
| |- SDK.hs
|- test/
| |- TypesSpec.hs
| |- TransactionSpec.hs
| |- UPISpec.hs
| |- ReconciliationSpec.hs
| |- SDKSpec.hs
| |- Spec.hs
|- upi-payment-sdk.cabal
|- stack.yaml
- Defines domain wrappers and records:
VPA,Amount,TxnId,MPINTxnErrorTxnStateReceiptReconciliationReport
- Implements transaction state transitions using
TxnEvent. - Enforces valid state progression.
- Validates VPA format (
localpart@handle). - Validates MPIN rules (4 to 6 numeric digits).
- Simulates MPIN hashing and comparison.
- Builds debit-credit settlement legs.
- Aggregates receipts into:
- settled count
- failed count
- net settled amount
- duplicate transaction IDs
- Public API and only IO boundary.
- Manages transaction records in memory (
IORef). - Exposes:
initiateauthenticatesettlereconcile
This is the exact business flow implemented by the SDK.
- Client calls
initiate toVPA amount. - SDK validates
amount > 0. - SDK validates destination VPA and payer VPA.
- SDK creates a new
TxnIdand stores record with stateInitiated.
Expected output:
Right txnIdon successLeft InvalidAmountorLeft InvalidVPAon validation failure
- Client calls
authenticate txnId mpin. - SDK checks transaction exists.
- MPIN is validated and compared against stored MPIN hash.
- On success: state becomes
Authenticated. - On auth failure: transaction is marked
Failed.
Expected output:
Right txnIdon successful authenticationLeft AuthenticationFailedfor wrong MPINLeft TransactionNotFoundfor unknown ID
- Client calls
settle txnId. - SDK checks the transaction exists and is in a valid state.
- SDK transitions state to
Settled. - SDK generates a
Receiptwith timestamp and final state.
Expected output:
Right receipton successLeft InvalidTransitionfor invalid state transitionsLeft TransactionNotFoundfor unknown ID
- Client collects multiple receipts.
- Calls
reconcile receipts. - SDK returns summary report including duplicate IDs.
Expected output:
ReconciliationReportwith totals and duplicate detection
Valid transitions:
Initiated --MarkAuthenticated--> Authenticated
Initiated --MarkFailed---------> Failed
Authenticated --MarkSettled----> Settled
Authenticated --MarkFailed-----> Failed
Settled/Failed --any event-----> InvalidTransition
The project uses explicit return types for all business outcomes:
- No exceptions in core business logic
- Every invalid case is represented as
TxnError - Callers must handle success and failure branches
This improves predictability and testability.
- Stack installed and available in terminal
stack buildstack testCurrent expected test status:
- 14 examples
- 0 failures
import qualified Data.Text as T
import SDK (initiate, authenticate, settle, reconcile)
import Types (Amount(..), MPIN(..), VPA(..), Receipt)
demo :: IO ()
demo = do
started <- initiate (VPA (T.pack "merchant@bank")) (Amount 250)
case started of
Left err -> print err
Right txnId -> do
auth <- authenticate txnId (MPIN (T.pack "1234"))
case auth of
Left err -> print err
Right _ -> do
done <- settle txnId
case done of
Left err -> print err
Right receipt -> do
let report = reconcile [receipt]
print report- Simple to understand and debug
- Good separation between pure logic and stateful orchestration
- Easy to extend for retries, refunds, timeout handling, and persistence
- Strongly typed domain model reduces invalid states
- Persistent storage (SQLite/Postgres)
- Real cryptographic MPIN hashing
- API layer (Servant/Scotty)
- Idempotency keys for duplicate-safe requests
- Concurrency and race-condition safeguards
Types
wraps Amount and exposes integer payload [v]
supports TxnState equality checks [v]
wraps VPA text [v]
Transaction.transition
moves Initiated to Authenticated [v]
moves Authenticated to Settled [v]
rejects transitions from Failed [v]
UPI helpers
validates a proper VPA [v]
rejects malformed VPA [v]
accepts a valid MPIN [v]
rejects wrong MPIN against stored MPIN [v]
pairs debit and credit legs for positive amount [v]
Reconciliation.reconcileReceipts
computes settled, failed, net amount, and duplicate IDs [v]
SDK flow
runs initiate -> authenticate -> settle successfully [v]
fails authentication with wrong MPIN [v]
Finished in 0.0249 seconds
14 examples, 0 failures
To https://github.com/Asha0509/Simulated_UPI.git
<old_commit>..<new_commit> main -> main