Low-Level Design (LLD)
Low-Level Design Document
Project:
Drop{{PROJECT_NAME}} Module/Component:Transactions Module (Remittance + QR Payment){{MODULE_NAME}} Version:1.0{{VERSION}} Date:2026-02-23{{DATE}} Author:Petter Graff, Senior Enterprise Architect{{AUTHOR}} Status: Draft | In Review | Approved Reviewers:Alem Bašić (CEO), John (AI Director){{REVIEWERS}} Related HLD: HLD Document
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1 | Initial draft | ||
1. Module Overview
Module:
Service/Repo: transactions{{MODULE_NAME}}drop-api —
Team Owner: src/drop-api/src/routes/transactions.ts{{REPO_NAME}}ALAI — Backend{{TEAM_NAME}}
Single Responsibility: Processes all financial operations — remittance (international money transfer via PISP) and QR payments (domestic merchant payments via PISP) — using the PSD2 pass-through model where Drop never holds funds.{{ONE_SENTENCE_WHAT_THIS_MODULE_DOES}}
Boundaries:
- Owns:
Transaction records (transactionstable), exchange rates (exchange_ratestable), fee calculation, pre-payment disclosure, idempotency enforcement, PISP payment initiation orchestration{{WHAT_THIS_MODULE_OWNS}} - Does NOT own:
User authentication (auth module), recipient management (recipientstable owned by recipients route), bank account balance display (bank_accounts route / AISP), merchant registration (merchants route){{WHAT_THIS_MODULE_DOES_NOT_OWN}} - Delegates to:
BankID auth middleware (JWT validation), Open Banking PISP API (actual payment execution), audit_log (compliance side-effect), notifications (user alerting){{WHAT_IT_DELEGATES}}
Key Business Rules:
Drop never deducts money from the Drop DB balance except as a cached AISP value — all real deductions happen at the user's bank via PISP{{BUSINESS_RULE_1}}Every transaction requireskyc_status = 'approved'on the initiating user{{BUSINESS_RULE_2}}Remittance amounts: 100 NOK minimum, 50,000 NOK maximum; fee = 0.5% of send amountQR payments: fee = merchantfee_rate(default 1%); validated via HMAC QR codeidempotency_key(unique index ontransactions) prevents double-charging on retry{{BUSINESS_RULE_3}}
2. Class / Module Diagram
classDiagram
class TransactionsRoute{{ServiceClassName}} {
-authMiddleware:repository: Middleware{{RepositoryInterface}}
-rateLimiter:eventBus: MiddlewareEventBus
-logger: Logger
+POSTcreate(dto: /v1/transactions/remittance(body: RemittanceDto)Create{{Entity}}Dto): ResponsePromise~{{Entity}}~
+POSTfindById(id: /v1/transactions/qr-payment(body: QRPaymentDto)string): ResponsePromise~{{Entity}} | null~
+POSTfindAll(filter: /v1/transactions/disclosure(body: DisclosureDto){{Filter}}Dto): ResponsePromise~PaginatedResult~{{Entity}}~~
+GETupdate(id: /v1/transactions(query:string, TransactionFilter)dto: Update{{Entity}}Dto): ResponsePromise~{{Entity}}~
+GETdelete(id: /v1/transactions/:id()string): ResponsePromise~void~
-validateRemittanceInput(validate(dto: RemittanceDto)unknown): void
-validateQRPaymentInput(dto:publishEvent(event: QRPaymentDto){{DomainEvent}}): voidPromise~void~
}
class RemittanceDto{{RepositoryInterface}} {
+recipientId: string<<interface>>
+amount:findById(id: numberstring): Promise~{{Entity}} | null~
+bankAccountId:findMany(filter: string{{Filter}}): Promise~{{Entity}}[]~
+currency:create(data: stringPartial~{{Entity}}~): Promise~{{Entity}}~
+update(id: string, data: Partial~{{Entity}}~): Promise~{{Entity}}~
+delete(id: string): Promise~void~
+count(filter: {{Filter}}): Promise~number~
}
class QRPaymentDto {
+merchantId: string
+amount: number
+qrData: string
{Entity}}
class Transaction {
+id: string
+user_id:createdAt: stringDate
+type:updatedAt: "remittance"Date
+deletedAt: Date | "qr_payment"null
{{FIELD_1}}: {{TYPE_1}}
{{FIELD_2}}: {{TYPE_2}}
+status:isDeleted(): "processing" | "completed" | "failed"boolean
+amount:toJSON(): number
+currency: string
+fee: number
+recipient_id: string
+merchant_id: string
+exchange_rate: number
+send_amount: number
+receive_amount: number
+receive_currency: string
+idempotency_key: string
+created_at: string{{EntityJSON}}
}
class DatabaseCreate{{Entity}}Dto {
<<abstraction>>
+query(sql, params){{FIELD_1}}: T[]{{TYPE_1}}
+getOne(sql, params){{FIELD_2}}: T
+run(sql, params): RunResult
+transaction(fn): void{{TYPE_2}}
}
class PISPClient{{Controller}} {
<<external>>-service: {{ServiceClassName}}
+initiatePayment(paymentRequest)POST /{{resource}}(body: Create{{Entity}}Dto): PaymentResponseResponse
+getPaymentStatus(paymentId)GET /{{resource}}/:id(): PaymentStatusResponse
+GET /{{resource}}(query: {{Filter}}Dto): Response
+PUT /{{resource}}/:id(body: Update{{Entity}}Dto): Response
+DELETE /{{resource}}/:id(): Response
}
TransactionsRoute{{ServiceClassName}} --> RemittanceDto{{RepositoryInterface}}
TransactionsRoute{{ServiceClassName}} --> QRPaymentDto{{Entity}}
TransactionsRoute{{Controller}} --> Transaction{{ServiceClassName}}
TransactionsRoute{{RepositoryInterface}} --..> Database
TransactionsRoute --> PISPClient{{Entity}}
3. Database Schema
3.1 Tables
transactions{{TABLE_NAME_1}}
Purpose: Records all financial operations. Append-only — status updates are the only writes after creation.{{TABLE_PURPOSE}}
| Column | Type | Nullable | Default | Constraints | Description |
|---|---|---|---|---|---|
id |
| | |||
| | | |||
| | ||||
| |
NO | |
||
| | ||||
| | | |||
| | | |||
| | | |||
| | | |||
| | ||||
| | ||||
| | ||||
| | ||||
| | ||||
| | ||||
created_at |
|
NO | |
||
updated_at |
TIMESTAMPTZ |
NO | NOW() |
Last update timestamp | |
deleted_at |
TIMESTAMPTZ |
YES | NULL |
Soft delete timestamp | |
{{COLUMN_1}} |
{{TYPE}} |
{{YES/NO}} | {{DEFAULT}} |
{{CONSTRAINTS}} | {{DESCRIPTION}} |
{{COLUMN_2}} |
{{TYPE}} |
{{YES/NO}} | {{DEFAULT}} |
{{CONSTRAINTS}} | {{DESCRIPTION}} |
{{FK_COLUMN}} |
UUID |
NO | FK → {{OTHER_TABLE}}(id) |
Reference to {{OTHER_TABLE}} |
Indexes:
| Index Name | Columns | Type | Rationale |
|---|---|---|---|
|
id |
B-tree (PK) | Primary key lookup |
|
|
B-tree | |
|
|
||
| | ||
| |
exchange_rates{{TABLE_NAME_2}}
Purpose: Stores current NOK-to-foreign currency exchange rates for the 6 supported remittance corridors.{{TABLE_PURPOSE}}
| Column | Type | Nullable | Default | Constraints | Description |
|---|---|---|---|---|---|
id |
|
NO | gen_random_uuid() |
PK | |
|
|
||||
|
{{CONSTRAINTS}} |
||||
| | ||||
| |
Indexes:
| |
3.2 Enums (CHECK constraints in SQLite, native ENUMs in PostgreSQL migration)
--CREATE transactionTYPE type{{ENUM_NAME}} CHECK(typeAS INENUM (
'remittance'{{VALUE_1}}',
'qr_payment'))
-- transaction status
CHECK(status IN ({{VALUE_2}}'processing',
'completed',{{VALUE_3}}'
'failed'));
3.3 Migration Notes
Migration:MigrationIncluded infile:db.ts{{MIGRATION_FILE_NAME}}.sqlinitializeDatabase()— runs on startup (SQLite) or via separate migration script (PostgreSQL)- Zero-downtime:
YES{{YES_NO}} —onlyINSERTandUPDATE statusneeded;CREATE INDEX CONCURRENTLYfor PostgreSQL{{NOTES}} - Backfill required:
NO{{YES_NO}} —new tables{{BACKFILL_STRATEGY}} - Estimated migration time:
< 1 second (SQLite), < 5 seconds (PostgreSQL){{ESTIMATE}}
4. API Contract
Base Path: /v1/transactionsapi/v{{VERSION}}/{{RESOURCE}}
POST /v1/transactions/remittance{{resource}}
Summary: Initiate international money transfer (PISP) from user's bank account toCreate a savednew recipient{{entity}}
Authentication: Bearer JWT required (authMiddleware)
Rate Limit: 10 req/60s per IP + 3 req/60s per user
Request Body:
{
"recipientId"{{field1}}": "rec_abc123def456gh78"{{type_and_example}}",
"amount": 2000,
{{field2}}"bankAccountId": "ba_abc123def456gh78",
{{type_and_example}}"currency": "NOK"
}
Success Response — 201 Created:
{
"data": {
"id": "tx_rem_abc123def456gh78"550e8400-e29b-41d4-a716-446655440000",
"type"{{field1}}": "remittance",
{{value}}"status": "processing",
"amount": 2000,
"fee": 10,
"receiveAmount": 20340,
"receiveCurrency": "RSD",
"exchangeRate": 10.17,
"estimatedDelivery": "2-4 business days",
"scaRedirect": "https://dnb.no/sca/pay/abc123",
"createdAt": "2026-02-23T10:2024-01-01T00:00:00.000Z"
}
}
Error Responses:
| Status | Code | Description |
|---|---|---|
400 |
|
|
401 |
|
Missing or |
403 |
|
|
| | |
| | |
409 |
|
|
422 |
|
|
| | |
| | |
500 |
|
Unexpected server error |
POSTGET /v1/transactions/qr-payment{{resource}}/:id
Summary: InitiateRetrieve QRa merchant{{entity}} paymentby (PISP) from user's bank accountID
Authentication:Path Bearer JWT required
Request Body:Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
UUID |
The { |
Success Response — :201200 CreatedOK
{
"data": {
"id": "tx_qr_abc123def456gh78"550e8400-e29b-41d4-a716-446655440000",
"type"{{field1}}": "qr_payment",
{{value}}"status": "completed",
"amount": 450,
"fee": 4.5,
"merchantName": "Café Oslo AS",
"createdAt": "2026-02-23T10:2024-01-01T00:00:00.000Z"
}
}
Error Responses:
| Status | Code | Description |
|---|---|---|
|
|
Missing or invalid |
| | |
| | |
404 |
|
|
| |
POST /v1/transactions/disclosure
Summary: Pre-payment disclosure — returns fee, exchange rate, receive amount (PSD2 Art. 45/46 compliance)
Authentication: Bearer JWT required
Request Body:
{
"type": "remittance",
"amount": 2000,
"recipientId": "rec_abc123def456gh78"
}
Success Response — 200 OK:
{
"data": {
"sendAmount": 2000,
"sendCurrency": "NOK",
"fee": 10,
"feePercentage": 0.5,
"exchangeRate": 10.17,
"receiveAmount": 20340,
"receiveCurrency": "RSD",
"totalCost": 2010,
"estimatedDelivery": "2-4 business days"
}
}
GET /v1/transactions{{resource}}
Summary: List authenticated user's transactions{{entities}} with pagination and filtering
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page |
integer |
1 |
Page number (1-based) |
limit |
integer |
20 |
Items per page ( |
|
string |
createdAt:desc |
field direction |
|
|
— | {{filter1}} |
Success Response — 200 OK:
{
"data": {
"transactions": [
{
"id": "tx_rem_abc123",
"type": "remittance",
"status": "completed",
"amount": 2000,
"fee": 10,
"receiveAmount": 20340,
"receiveCurrency": "RSD",
"recipientName": "Marko Petrovic",
"createdAt": "2026-02-23T10:00:00.000Z"
}
],
"total"pagination": 15,{
"page": 1,
"limit": 2020,
"total": 0,
"totalPages": 0
}
}
5. Algorithm Specifications
5.1 Fee Calculation — Remittance{{ALGORITHM_NAME}}
Purpose: Calculate 0.5% fee on remittance, rounded to 2 decimal places{{WHAT_IT_DOES}}
Complexity: Time O(1){{TIME_COMPLEXITY}}) | Space O(1){{SPACE_COMPLEXITY}})
function calculateRemittanceFee(sendAmountNOK:{{algorithmName}}(input: number){{InputType}}): number
FEE_RATE = 0.005{{OutputType}}
// 0.5%Step fee1: =Validate sendAmountNOKinput
*if FEE_RATEinput returnis Math.round(feenull *or 100)invalid /then
100throw ValidationError("{{VALIDATION_MESSAGE}}")
end if
// RoundStep to2: 2{{STEP_2_DESCRIPTION}}
decimal places
function calculateReceiveAmount(sendAmountNOK: number, exchangeRate: number): number
netSendresult = sendAmountNOKinitialize()
for each item in input.items do
if item.satisfies({{CONDITION}}) then
result.add(transform(item))
end if
end for
// FeeStep taken3: from{{STEP_3_DESCRIPTION}}
send amount, not receive
receivesorted = netSend * exchangeRate
return Math.round(receive)result.sortBy({{SORT_CRITERIA}})
// RoundStep to4: wholeApply unitsbusiness ofrules
targetfor currencyeach rule in {{BUSINESS_RULES}} do
sorted = rule.apply(sorted)
end for
return sorted
end function
Edge Cases:
MinimumEmptyamount:input:100ReturnNOKempty→result,feedo=not0.50throw- Single item: {{SINGLE_ITEM_BEHAVIOR}}
- Maximum
amount:items:50,000 NOK → fee = 250 NOK Rate not found:Return 404 — do not proceedLimit totransaction{{MAX_ITEMS}}, log warning if exceeded
5.2 Idempotency Key Generation
Purpose: Prevent double-charging on network retry or duplicate form submission
Format: {userId}:{amount}:{recipientId}:{minuteTimestamp}
function generateIdempotencyKey(userId, amount, recipientId): string
minuteTimestamp = Math.floor(Date.now() / 60000) // Changes every 60s
key = `${userId}:${amount}:${recipientId}:${minuteTimestamp}`
return key
// Unique index on transactions.idempotency_key prevents duplicate
// If INSERT fails with UNIQUE constraint → return existing transaction
6. Sequence Diagrams
6.1 RemittanceCreate Initiation{{Entity}} Flow
sequenceDiagram
autonumber
actor Client
as Client (Web/Mobile)
participant RLGW as RateAPI LimiterGateway
participant Auth as Auth Middleware
participant RouteSVC as Transactions Route{{ServiceClassName}}
participant DB as Database
participant PISPMQ as OpenMessage BankingQueue
PISPparticipant Email as Email Service
Client->>RL:GW: POST /v1/transactions/remittance{{resource}} RL->>RL:{body}
Check rate_limits (10/IP, 3/user per 60s)
alt Rate limit exceeded
RL-->>Client: 429 Too Many Requests
end
RL-GW->>Auth: Forward request
Auth->>Auth: ExtractValidate JWT from Bearer header / cookie
Auth->>DB: SELECT session WHERE token_hash = ? AND revoked = 0
Auth->>DB: SELECT user WHERE id = ? AND deleted_at IS NULL
Auth-->>Route:GW: userUser context {userId, role,roles}
kycStatus}
Route-GW->>Route:SVC: Validatecreate(dto, body:userContext)
recipientId,SVC->>SVC: amount (100-50000), bankAccountIdvalidate(dto)
alt Validation fails
Route-SVC-->>Client: 400 validation_errorValidationError
end
alt kyc_status != 'approved'
Route-->>Client: 403 kyc_required
end
Route->>DB: SELECT * FROM recipients WHERE id = ? AND user_id = ?
alt Recipient not found
Route-->>Client: 404 recipient_not_found
end
Route->>DB: SELECT rate FROM exchange_rates WHERE to_currency = ?
Route->>DB: SELECT * FROM bank_accounts WHERE id = ? AND user_id = ? AND is_primary = 1
Route->>Route: Calculate fee (0.5%), total cost, receive amount
alt balance < totalCost
Route-->>Client: 403 insufficient_balance
end
Route-SVC->>DB: BEGIN TRANSACTION
Route->>DB: UPDATE bank_accounts SET balance = balance - totalCostInOere WHERE balance >= ?
Route-SVC->>DB: INSERT INTO transactions{{TABLE_NAME_1}}
(status='processing', idempotency_key=?)
Route-DB-->>DB:SVC: INSERT{{entity}} INTOrecord
audit_log (action='transaction.create')
Route->>DB: INSERT INTO notifications (title='Overføring startet')
Route-SVC->>DB: COMMIT
Route-SVC->>PISP:MQ: POSTpublish("{{ENTITY_CREATED_EVENT}}", /v1/payments/cross-border-credit-transfersevent)
PISP-MQ->>Email: Trigger confirmation email
Email-->>Route:Client: Email delivered async
SVC-->>GW: {paymentId,{entity}} transactionStatus:dto
"RCVD", scaRedirect}
Route-GW-->>Client: 201 {transactionId, status: "processing", scaRedirect}Created
6.2 QR Payment{{SECONDARY_FLOW_NAME}} Flow
sequenceDiagram
autonumber
actor Client
participant SVC as Client (Mobile){{ServiceClassName}}
participant RouteCache as Transactions RouteRedis
participant DB as Database
Client->>Route:SVC: POSTGET /v1/transactions/qr-payment{{resource}}/:id
SVC->>Cache: GET {merchantId,{resource}}:{{id}}
amount}alt Route-Cache hit
Cache-->>DB:SVC: VerifyCached JWT{{entity}}
sessionSVC-->>Client: Route-200 OK (from cache)
else Cache miss
Cache-->>SVC: null
SVC->>DB: SELECT * FROM merchants{{TABLE_NAME_1}} WHERE id = ? AND status = 'active'$1
alt Merchant notNot found
Route-DB-->>SVC: null
SVC-->>Client: 404 merchant_not_foundNot Found
end
Route-DB-->>DB:SVC: SELECT{{entity}} *record
FROM bank_accounts WHERE user_id = ? AND is_primary = 1
Route-SVC->>Route: Calculate fee = amount * merchant.fee_rate
Route->>Route: Calculate total = amount + fee
Route->>DB: BEGIN TRANSACTION
Route->>DB: UPDATE bank_accountsCache: SET balance{{resource}}:{{id}} =TTL={{CACHE_TTL}}
balance - total
Route->>DB: INSERT INTO transactions (type='qr_payment', status='completed')
Route->>DB: INSERT INTO audit_log (action='qr_payment.create')
Route->>DB: INSERT INTO notifications (title='Betaling registrert')
Route->>DB: COMMIT
Route-SVC-->>Client: 201200 {transactionId,OK
status: "completed", merchantName}end
7. State Diagrams
stateDiagram-v2
[*] --> processing:{{STATE_DRAFT}} POST: /v1/transactions/remittanceCreated
(PISP initiated)
processing{{STATE_DRAFT}} --> completed:{{STATE_PENDING}} PISP: webhooksubmit()
— payment confirmed by bank
processing --> failed: PISP webhook — payment rejected (insufficient funds, SCA timeout, SCA cancelled)
processing --> failed: 5-minute SCA timeout — no callback received
completed{{STATE_DRAFT}} --> [*] failed: delete()
{{STATE_PENDING}} --> {{STATE_ACTIVE}} : approve()
{{STATE_PENDING}} --> {{STATE_REJECTED}} : reject(reason)
{{STATE_PENDING}} --> {{STATE_DRAFT}} : recall()
{{STATE_ACTIVE}} --> {{STATE_SUSPENDED}} : suspend(reason)
{{STATE_ACTIVE}} --> {{STATE_COMPLETED}} : complete()
{{STATE_ACTIVE}} --> {{STATE_CANCELLED}} : cancel(reason)
{{STATE_SUSPENDED}} --> {{STATE_ACTIVE}} : reactivate()
{{STATE_SUSPENDED}} --> {{STATE_CANCELLED}} : cancel(reason)
{{STATE_REJECTED}} --> {{STATE_DRAFT}} : revise()
{{STATE_COMPLETED}} --> [*]
{{STATE_CANCELLED}} --> [*]
Note: QR payments go directly from creation to completed (synchronous in MVP — no PISP webhook for domestic transfers in mock mode).
State Transition Rules:
| From | To | Trigger | Guard Condition | Side Effect |
|---|---|---|---|---|
submit() |
||||
approve() |
{{PERMISSION}} role |
|||
suspend(reason) |
8. Error Handling Strategy
8.1 Error Classification
| Error Type | HTTP Status | Retry? | Log Level | Alert? |
|---|---|---|---|---|
| ValidationError | 400 | No | INFO | No |
| UnauthorizedError | 401 | No | WARN | No |
| 403 | No | |||
| 404 | No | INFO | No | |
| 409 | No | |||
| WARN | No | |||
| 502 | Yes ( |
ERROR | Yes (if |
|
| DatabaseError | 500 | Yes (1x) | ERROR | Yes |
| UnexpectedError | 500 | No | ERROR | Yes |
8.2 Error Response Format (RFC 7807)
{
"error"type": "kyc_required",
"message": "Du må fullføre identitetsverifisering før du kan sende penger.https://api.{{DOMAIN}}/errors/{{ERROR_CODE}}",
"details"title": []"{{Human-readable error title}}",
"status": 400,
"detail": "{{Specific error message}}",
"instance": "/{{resource}}/{{id}}",
"traceId": "{{TRACE_ID}}"
}
8.3 Retry & Fallback Strategy
PISPExternal API call failure:
→ Retry with exponential backoff: [1s, 2s, 4s]
→ Max retries: 3
→ Circuit breaker: Open after 35 failures in 60s window
→ 60s cooldown
→ Fallback: Return 502 to client — payment cannot proceed without PISP{{FALLBACK_BEHAVIOR}}
→ Alert: Sentry alertPagerDuty if circuit remains open > 5 minutes
→ Idempotency: PISP call uses X-Request-ID = idempotency_key to prevent double-payment on retry
9. Concurrency & Thread Safety
| Concern | Scenario | Mitigation |
|---|---|---|
Unique |
||
| Two |
|
|
10. Performance Considerations
| Operation | Target (p99) | Current Baseline | Optimization |
|---|---|---|---|
|
< |
||
GET / (list) |
< |
query |
|
POST / |
< |
||
import (1000 items) |
< |
Known bottlenecks:
PISP{{BOTTLENECK_1}}:API latency: 200-2000ms external call — mitigated by async SCA redirect pattern{{MITIGATION}}Exchange{{BOTTLENECK_2}}:rate reads: High frequency but 6 rows only — fully cached in PostgreSQL buffer pool{{MITIGATION}}
11. Dependencies
Internal Dependencies
| Dependency | Type | Purpose | Fallback if unavailable |
|---|---|---|---|
|
Synchronous | ||
|
|||
|
External Dependencies
| Dependency | Version | Purpose | Fallback if unavailable |
|---|---|---|---|
| PostgreSQL | {{VERSION}} |
Primary data store | |
{{VERSION}} |
|||
{{EXTERNAL_API}} |
v{{VERSION}} |
|
12. Configuration Parameters
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
|
|
100 |
No |
|
|
|
|
No | Cache |
|
integer |
3 |
No | External API retry count |
{{EXTERNAL_API_KEY_VAR}} |
string |
— | Yes |
|
|
string |
— | Yes |
|
| string |
13. Testing Approach
| Test Type | Tool | Coverage Target | Location |
|---|---|---|---|
| Unit tests | Jest / Vitest | > |
src/ |
| Integration tests | Supertest + TestContainers | Key |
src/ |
| Contract tests | Pact | All public APIs | src/{{module}}/__tests__/contract/ |
Key test scenarios:
-
RemittanceCreate— success path (mock PISP) Remittance — KYC not approved → 403Remittance — amount < 100 NOK → 422Remittance — amount > 50,000 NOK → 422Remittance — recipient not found → 404Remittance — duplicate request (idempotency key) → 409 with existing transactionQR payment{{entity}} — success path-
QRCreatepayment{{entity}} —merchantvalidationinactiveerror→(missing404required field) -
DisclosureCreate {{entity}} —calculatesconflictfee,(duplicateexchange rate, receive amount correctly{{UNIQUE_FIELD}}) -
PISPGet {{entity}} by ID — found - Get {{entity}} by ID — not found (404)
- List {{entities}} — with filters, pagination, sorting
- Update {{entity}} — success
- Update {{entity}} — not found
- Delete {{entity}} — success (soft delete)
- State transition — valid transition
- State transition — invalid transition (guard fails)
- External service failure — circuit breaker
— 3 failures → open → 502 (integration test, Phase 2)triggers - Concurrent
paymentcreation —balanceduplicaterace conditionconstraint handledcorrectly (Phase 2)
Approval
| Role | Name | Date | Signature |
|---|---|---|---|
| Author | |||
| Module Owner | |||
| Security Review | |||
| Tech Lead |