Skip to main content

Module Design

Module Design Document

Project: {{PROJECT_NAME}}Drop Module: {{MODULE_NAME}}Payments Module (transactions + recipients + exchange_rates) Service: {{SERVICE_NAME}}drop-api — src/drop-api/src/routes/ Version: {{VERSION}}1.0 Date: {{DATE}}2026-02-23 Author: {{AUTHOR}}Petter Graff, Senior Enterprise Architect Status: Draft | In Review | Approved Reviewers: {{REVIEWERS}}Alem Bašić (CEO), John (AI Director)

Document History

Version Date Author Changes
0.1 {{DATE}}2026-02-23 {{AUTHOR}}Petter Graff Initial draft from source code analysis

1. Module Overview & Responsibility

Module: {{MODULE_NAME}}payments Layer: Application (routes) + Domain |(business Applicationlogic) |+ Infrastructure |(DB Presentationaccess) Repository: {{REPO_OR_MONOREPO_PATH}}src/drop-api/src/routes/transactions.ts, src/drop-api/src/routes/recipients.ts, src/drop-api/src/routes/rates.ts Team Owner: {{TEAM_NAME}}ALAI — Backend

Single Responsibility Statement:

{{THE_MODULE_IS_RESPONSIBLE_FOR_ONE_THING}}The Payments module orchestrates all financial operations — remittance (international money transfer) and QR merchant payments — using the PSD2 pass-through model, meaning Drop never holds funds but initiates payments from the user's bank via PISP.

This module owns:

  • {{OWNED_RESOURCE_1}}Transaction lifecycle (datacreate, +status update, list, fetch)
  • Remittance business logic)rules (fee calculation, FX rate application, PSD2 disclosure)
  • {{OWNED_RESOURCE_2}}QR payment business rules (merchant fee calculation, HMAC QR validation)
  • Recipient management (CRUD for saved remittance contacts)
  • Exchange rate lookup and caching (6 corridors)
  • Idempotency enforcement for payment operations
  • Pre-payment disclosure (PSD2 Art. 45/46)

This module does NOT own:

  • {{NOT_OWNED_1}}User authentication and session management — owned by {{OTHER_MODULE}}auth module (routes/auth.ts, lib/bankid.ts)
  • {{NOT_OWNED_2}}KYC/AML status — owned by {{OTHER_MODULE}}compliance module (routes/user.ts + Sumsub integration)
  • Bank account linking (AISP consent) — owned by banking module (routes/bank-accounts.ts)
  • Merchant registration — owned by merchants module (routes/merchants.ts)
  • Notifications delivery — owned by notifications module (routes/notifications.ts)

Why this is a separate module: {{RATIONALE_FOR_SEPARATION}}Payments is e.g.,the "Separatecore revenue-generating bounded context,context differentfor teamDrop. ownership,It differenthas scalingdistinct requirements"business rules (PSD2 compliance, FX calculation, PISP orchestration), its own data domain (transactions, recipients, exchange_rates), and its own compliance requirements (PSD2 Art. 45/46 disclosure, idempotency, AML monitoring). Separating it enables independent testing, focused security review, and future extraction to a dedicated service if transaction volume demands it (per ADR-005 extraction triggers).


2. Interface Definition (Public API)

2.1 Exported Service Interface

// PublicRemittance
interface exported by this module
export interface I{{ModuleName}}ServiceIRemittanceService {
  /**
   * Calculate pre-payment disclosure (fee, FX rate, receive amount)
   * Required by PSD2 Art. 45/46 before every payment
   * @throws {NotFoundError} if recipientId not found or not owned by user
   * @throws {METHOD_1_DESCRIPTION}}NotFoundError} if exchange rate for currency not found
   */
  calculateDisclosure(dto: DisclosureDto, userId: string): Promise<DisclosureResult>;

  /**
   * Initiate international remittance via PISP
   * @throws {ForbiddenError} if KYC not approved
   * @throws {ForbiddenError} if insufficient balance
   * @throws {NotFoundError} if recipient not found
   * @throws {ConflictError} if idempotency key collision (returns existing tx)
   * @throws {ValidationError} if dtoamount isoutside invalid100-50000 *NOK @throws {ConflictError} if {{UNIQUE_FIELD}} already existsrange
   */
  create(initiateRemittance(dto: Create{{Entity}}Dto,RemittanceDto, context:userId: RequestContext)string): Promise<{{Entity}}Transaction>;

  /**
   * Initiate QR merchant payment via PISP (domestic)
   * @throws {ForbiddenError} if KYC not approved
   * @throws {METHOD_2_DESCRIPTION}}NotFoundError} if merchant not found or inactive
   */
  initiateQRPayment(dto: QRPaymentDto, userId: string): Promise<Transaction>;

  /**
   * List user's transactions with pagination and optional filters
   */
  listTransactions(filter: TransactionFilter, userId: string): Promise<PaginatedTransactions>;

  /**
   * Get a single transaction by ID (must belong to requesting user)
   * @throws {NotFoundError} if not found or access denied
   */
  findById(id:getTransaction(txId: string, context:userId: RequestContext)string): Promise<{{Entity}}Transaction>;
}

/**/ *Recipients
interface IRecipientsService {{METHOD_3_DESCRIPTION}}
  */createRecipient(dto: findAll(filter:CreateRecipientDto, {{Filter}}Dto,userId: context: RequestContext)string): Promise<PaginatedResult<{{Entity}}>Recipient>;
  /**listRecipients(userId: * {{METHOD_4_DESCRIPTION}}
   * @throws {NotFoundError} if not found
   * @throws {ForbiddenError} if user lacks permission
   */
  update(id: string, dto: Update{{Entity}}Dto, context: RequestContext)string): Promise<{{Entity}}Recipient[]>;
  /**
   * {{METHOD_5_DESCRIPTION}}
   * @throws {NotFoundError} if not found
   */
  delete(id:getRecipient(recipientId: string, context:userId: RequestContext)string): Promise<Recipient>;
  deleteRecipient(recipientId: string, userId: string): Promise<void>;
}

// DTOs exported for consumersTypes
export type Create{{Entity}}DtoRemittanceDto = {
  recipientId: string;
  amount: number; /*/ ...NOK, *100-50000
  bankAccountId: string;
  currency?: string; // Default: 'NOK'
};

export type Update{{Entity}}DtoQRPaymentDto = {
  merchantId: string;
  amount: number; /*/ ...NOK, */positive
};

export type {{Filter}}DtoDisclosureDto = {
  /*type: ...'remittance';
  */amount: number;
  recipientId: string;
};

export type {{Entity}}Transaction = {
  id: string; /*/ ...tx_<hex16>
  */userId: string;
  type: 'remittance' | 'qr_payment';
  status: 'processing' | 'completed' | 'failed';
  amount: number;
  fee: number;
  receiveAmount?: number;
  receiveCurrency?: string;
  exchangeRate?: number;
  recipientId?: string;
  merchantId?: string;
  idempotencyKey?: string;
  createdAt: string;
};

2.2 HTTP Endpoints (if applicable)

Get
Method Path AuthRate Limit Description
POST /api/v{{V}}/{{resource}}v1/transactions/remittance JWT Create10/IP {{entity}}+ 3/user per 60sInitiate remittance
POST/v1/transactions/qr-paymentJWT10/IP + 3/user per 60sInitiate QR payment
POST/v1/transactions/disclosureJWTNone specificPre-payment fee disclosure
GET /api/v{{V}}/{{resource}}v1/transactions JWT None specificList {{entities}}user transactions
GET /api/v{{V}}/{{resource}}/v1/transactions/:id JWT None specificGet transaction by ID
PUTGET /api/v{{V}}v1/recipientsJWTNone specificList saved recipients
POST/{{resource}}v1/recipientsJWTNone specificAdd recipient
GET/v1/recipients/:id JWT FullNone update
PATCHspecific /api/v{{V}}/{{resource}}/:id JWTPartial updaterecipient
DELETE /api/v{{V}}/{{resource}}/v1/recipients/:id JWT SoftNone deletespecificDelete recipient
GET/v1/rates/:currencyJWT120 req/60s per IPGet exchange rate

2.3 Events Published

The Payments module does not use an async event bus (monolith-first architecture per ADR-005). Side effects (notifications, audit log) are written synchronously within the same DB transaction.

Every Transactioncompleted, TriggeredAMLengine(highamount,high-risk
EventSide Effect Topic/QueueTable Written SchemaTriggered ByTrigger
Audit trail{{entity}}.createdaudit_log {{TOPIC}} Seetransaction §5POST endpointcreation
User notification{{entity}}.updatednotifications {{TOPIC}} Seecreated, §5 PUT/PATCH endpointfailed
AML monitoring{{entity}}.deletedaml_alerts {{TOPIC}} Seeby §5 DELETErules endpoint
{{entity}}.{{CUSTOM_EVENT}} {{TOPIC}}See §5{{BUSINESS_TRIGGER}}corridor)

3. Internal Structure

{{MODULE_NAME}}/routes/
├── controllers/
│   └── {{entity}}.controller.transactions.ts         # HTTP request handling, inputrate parsinglimiting, routes
├── services/recipients.ts           # Recipient CRUD routes
└── rates.ts                # Exchange rate lookup

lib/
├── db.ts                   # Dual-driver DB abstraction (query, run, transaction)
├── middleware/
│   ├── auth.ts             # JWT verification, session validation
│   └── {{entity}}.service.rate-limit.ts       # BusinessDB-backed logicIP ├──+ repositories/user rate ├── {{entity}}.repository.ts      # Data access interface
│limiting
└── {{entity}}.repository.pg.ts   # PostgreSQL implementation
├── domain/
│   ├── {{entity}}.entity.ts          # Domain entity / value objects
│   ├── {{entity}}.events.ts          # Domain events
│openbanking/
    └── {{entity}}.errors.neonomics.ts        # Domain-specificPISP errorsclient ├──(Phase dto/2  ├──currently create-{{entity}}.dto.ts
│   ├── update-{{entity}}.dto.ts
│   └── {{entity}}-filter.dto.ts
├── mappers/
│   └── {{entity}}.mapper.ts          # DB record ↔ domain entity ↔ DTO
├── __tests__/
│   ├── unit/
│   └── integration/
└── {{entity}}.module.ts              # Module registration / DI wiringmock)

Layer rules (enforced by linting):rules:

  • ControllersRoutes only call Servicesdb.ts functions (neverno Repositoriesraw directly)SQL strings outside db.ts)
  • ServicesAll onlySQL calluses Repositoriesparameterized andqueries publish Eventsno string interpolation
  • DomainBusiness entitieslogic have(fee nocalculation, frameworkvalidation) dependencies
  • lives
  • Mappersin liveroute athandlers serviceor layerdedicated helper functions — not in controllersdb.ts
  • External API calls (Neonomics PISP) called after DB transaction commits to ensure idempotency key is stored before external call

4. Database Schema

Primary Table: {{table_name}}transactions

Column Type Nullable Default Constraints Description
id UUIDTEXTNOPK, tx_<hex16>Transaction ID
user_idTEXTNOFK → users(id)Initiating user
typeTEXTNOCHECK('remittance','qr_payment')Payment type
statusTEXT NO gen_random_uuid('processing'CHECK('processing','completed','failed')Payment status
amountREALNONOT NULLAmount in NOK
currencyTEXTYES'NOK'Source currency
feeREALYES0Fee in NOK
recipient_idTEXTYESNULLFK → recipients(id)Remittance target
merchant_idTEXTYESNULLFK → merchants(id)QR payment target
send_amountREALYESNULLAmount in source currency
receive_amountREALYESNULLAmount in target currency
receive_currencyTEXTYESNULLTarget currency
exchange_rateREALYESNULLRate at payment time
descriptionTEXTYESNULLUser description
idempotency_keyTEXTYESNULLUNIQUEDuplicate prevention
created_atTEXTNOdatetime('now')Creation timestamp

Indexes:

-- PostgreSQL
CREATE INDEX CONCURRENTLY idx_transactions_user_id ON transactions(user_id);
-- Rationale: Every list query filters by user_id

CREATE UNIQUE INDEX idx_tx_idempotency ON transactions(idempotency_key)
    WHERE idempotency_key IS NOT NULL;
-- Rationale: Prevent duplicate payments on retry

CREATE INDEX CONCURRENTLY idx_transactions_recipient ON transactions(recipient_id)
    WHERE recipient_id IS NOT NULL;

CREATE INDEX CONCURRENTLY idx_transactions_merchant ON transactions(merchant_id)
    WHERE merchant_id IS NOT NULL;

Secondary Table: recipients

ColumnTypeNullableDefaultConstraintsDescription
idTEXTNOPK, rec_<hex16>Recipient ID
user_idTEXTNOFK → users(id)Owner
nameTEXTNONOT NULLRecipient full name
countryTEXTNONOT NULLCountry code (RS, BA, PL, PK, TR, EU)
currencyTEXTNONOT NULLTarget currency
bank_accountTEXTNONOT NULLIBAN or local account number
bank_nameTEXTYESNULLBank name (optional)
created_atTEXTNOdatetime('now')Created timestamp

Secondary Table: exchange_rates

creationtime
ColumnTypeNullableDefaultConstraintsDescription
idINTEGERNOauto PK Surrogate key
created_atfrom_currency TIMESTAMPTZTEXT NO NOT NULLAlways 'NOK' at MVP
NOW()to_currency TEXT ImmutableNO NOT NULLRSD, BAM, PLN, PKR, TRY, EUR
rateREALNONOT NULL1 NOK = N target units
updated_at TIMESTAMPTZNONOW()Auto-updated on write
deleted_atTIMESTAMPTZTEXT YES NULL SoftLast deleteupdate marker
versionINTEGERNO1Optimistic lock version
{{FIELD_1}}{{TYPE}}{{YES/NO}}{{DEFAULT}}{{CHECK/UNIQUE/FK}}{{DESCRIPTION}}
{{FIELD_2}}{{TYPE}}{{YES/NO}}{{DEFAULT}}{{CHECK/UNIQUE/FK}}{{DESCRIPTION}}
{{FK_ID}}UUIDNOFK → {{other_table}}(id){{RELATIONSHIP}}timestamp

Indexes:

CREATE INDEX CONCURRENTLY idx_{{table_name}}_{{column}} ON {{table_name}}({{column}})
    WHERE deleted_at IS NULL;
-- Rationale: {{WHY_THIS_INDEX}}

CREATE UNIQUE INDEX idx_{{table_name}}_{{unique_column}} ON {{table_name}}({{unique_column}})
    WHERE deleted_at IS NULL;
-- Rationale: Enforce uniqueness for active records only

RLS (Row-Level Security):

-- TODO: Enable if multi-tenant
-- ALTER TABLE {{table_name}} ENABLE ROW LEVEL SECURITY;
-- CREATE POLICY {{table_name}}_tenant_isolation ON {{table_name}}
--     USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

5. API Endpoints (Detailed)

POST /api/v{{V}}/{{resource}}v1/transactions/remittance

Request:

{
  "{{field1}}"recipientId": "string (required, max 255 chars)"rec_abc123def456gh78",
  "{{field2}}amount": 2000,
  "bankAccountId": "number (required, > 0)"ba_abc123def456gh78",
  "{{field3}}"currency": "string (optional, enum: [A, B, C])"NOK"
}

Validation:

  • recipientId: required, string, must exist in recipients WHERE user_id = currentUser
  • amount: required, number, 100 ≤ amount ≤ 50000
  • bankAccountId: required, string, must exist in bank_accounts WHERE user_id = currentUser

Success 201:

{
  "data": {
    "id": "uuid"tx_rem_abc123def456gh78",
    "{{field1}}"type": "value"remittance",
    "{{field2}}"status": 0,"processing",
    "amount": 2000,
    "fee": 10,
    "receiveAmount": 20340,
    "receiveCurrency": "RSD",
    "exchangeRate": 10.17,
    "estimatedDelivery": "2-4 business days",
    "scaRedirect": "https://bank.no/sca/pay/...",
    "createdAt": "ISO8601"2026-02-23T10:00:00.000Z"
  }
}

POST /v1/transactions/disclosure

Request:

{
  "type": "remittance",
  "amount": 2000,
  "recipientId": "rec_abc123def456gh78"
}

Event published:Success {{entity}}.created200:

{
  "specversion": "1.0",
  "type": "{{entity}}.created",
  "source": "/{{resource}}",
  "id": "{{EVENT_UUID}}",
  "time": "ISO8601",
  "data": {
    "entityId"sendAmount": 2000,
    "sendCurrency": "uuid"NOK",
    "tenantId"fee": 10,
    "feePercentage": 0.5,
    "exchangeRate": 10.17,
    "receiveAmount": 20340,
    "receiveCurrency": "uuid"RSD",
    "createdBy"totalCost": 2010,
    "estimatedDelivery": "uuid",2-4 "{{field1}}":business "value"days"
  }
}

6. Business Logic Specifications

6.1 Business Rules

Rule ID Rule Enforced In Error
BR-001 {{RULE_1_DESCRIPTION}}User must have kyc_status = 'approved' before initiating any payment ServiceRoute handler {{ERROR_CODE}}kyc_required (403)
BR-002 {{RULE_2_DESCRIPTION}}Remittance amount must be 100–50,000 NOK DomainRoute Entityvalidation {{ERROR_CODE}}amount_out_of_range (422)
BR-003 {{RULE_3_DESCRIPTION}}Fee = 0.5% of send amount for remittance ServiceRoute calculationN/A (business calculation)
BR-004QR payment fee = merchants.fee_rate (default 1%)Route calculationN/A
BR-005Recipient must belong to the authenticated userDB query WHERE user_idrecipient_not_found (404)
BR-006Cached bank balance must cover amount + feeRoute checkinsufficient_balance (403)
BR-007Idempotency key uniqueness prevents duplicate payment on retryUNIQUE DB constraint Return existing transaction (409 or 200)
BR-008Pre-payment disclosure must be shown before every remittance (PSD2 Art. 45/46)Frontend enforces; API provides via ConflictError/disclosureN/A
BR-009Drop never initiates PISP without recording transaction in DB firstAtomic transaction: INSERT tx → PISP callRollback on PISP error

6.2 Validation Rules

A,B,orC"
Field Type Required Validation Error Message
{{field1}}recipientId string Yes MinExists 1,in Maxrecipients 255WHERE charsuser_id "{{field1}}Mottaker mustikke be 1-255 characters"funnet"
{{field2}}amount number Yes Positive100 integer≤ amount ≤ 50000, positive "{{field2}}Beløp must bevære amellom positive100 integer"og 50 000 kr"
{{field3}}bankAccountId enumstring NoYes OneExists of:in [A,bank_accounts B,WHERE C]user_id "{{field3}}Bankkonto mustikke befunnet"
merchantId stringYes (QR)Exists in merchants WHERE status='active'"Butikk ikke funnet"

6.3 Authorization Rules

===Anyauthenticated
Operation Required Role Additional Conditions
CreateInitiate remittance {{ROLE}}user kyc_status = 'approved'
ReadInitiate QR paymentuserkyc_status = 'approved'
Get disclosureuserMust own recipient
List transactionsuserOnly own transactions (user_id = currentUser)
Add recipientuser Any authenticated user
Delete recipientuserIduser Must resource.createdByown recipient
ReadView anyexchange rates {{ADMIN_ROLE}}user
Update{{ROLE}}userId === resource.createdBy OR isAdmin
Delete{{ADMIN_ROLE}}Soft delete only
Hard deleteSUPER_ADMINRequires 2FA confirmationuser

7. Event Publishing / Consuming

The Payments module operates in a monolith (ADR-005) — no async message bus. All side effects are synchronous within DB transactions:

7.1 EventsSide PublishedEffects on Payment Creation

amount} post-commit
EventSide Effect WhenTable Written Payload SchemaMethod Idempotency KeyNotes
Audit trail{{entity}}.createdaudit_log AfterINSERT successfulwithin createtransaction {entityId,action='transaction.create', tenantId, ...}entityIdresource_type='transaction'
User notification{{entity}}.updatednotifications AfterINSERT successfulwithin updatetransaction {entityId,title='Overføring changes:startet', body='Din overføring på {...}} entityIdkr +er versionunder behandling'
AML monitoring{{entity}}.deletedaml_alerts AfterINSERT softif deleterule triggered {entityId,Checked deletedAt} entityIdby AML rules engine

7.2 Events Consumed

The

modulereceivesPISPstatusupdates
  • HTTP
  • Event Sourcepayment Module Handler Processingvia:

    Guarantee
    {{other_entity}}.deleted{{OTHER_MODULE}}Cascade soft-delete related recordsAt-least-once
    {{other_entity}}.updated{{OTHER_MODULE}}Update denormalized cacheAt-least-once

    Idempotency strategy:webhooks Allfrom consumersOpen checkBanking provider (Neonomics in production) → processed_eventsPOST /v1/webhooks/openbanking table

  • Polling beforein processing.mock Duplicatemode events aretransaction loggedstatus andchecked skipped.

    by frontend polling GET /v1/transactions/:id

  • 8. Dependencies

    8.1 Upstream (what this module depends on)

    OpenBanking
    Dependency Type Coupling Reason
    AuthModulemiddleware/auth.ts Internal module LooseHard (interface)required for every route) JWT validation, user contextidentity
    {{OTHER_MODULE}}middleware/rate-limit.ts Internal module LooseHard (events)required for payment routes) {{REASON}}IP + user rate limiting
    lib/db.tsShared libraryHard (required)All data access
    PostgreSQL / SQLite Infrastructure RequiredHard Primary storage
    Redis Infrastructure OptionalCachingPISP (degrades gracefully)
    {{EXTERNAL_SDK}}Neonomics) External libraryAPI Hard (prod) {{REASON}}Payment initiation — module unavailable if PISP down

    8.2 Downstream (what depends on this module)

    fetch
    Consumer What they use Notes
    {{OTHER_MODULE}}drop-web (Next.js)REST API /v1/transactions/*, /v1/recipients/* {{entity}}.createdVia events Read-onlywith consumercookie auth
    {{FRONTEND}}drop-mobile (Expo) HTTPREST API /v1/transactions/*, /v1/recipients/* Via APIfetch gatewaywith Bearer token
    compliance/aml moduletransactions table readsAML rules engine monitors transaction patterns

    9. Error Handling & Recovery

    idempotency
    Error Scenario Handling User Impact Recovery
    DB connection lost Retry 3x with backoff,1x, then 503 Request fails gracefully— user retries Auto-recover when DB reconnects
    ExternalPISP API timeout Return cached502, datatransaction stays processingPayment may or 503may not have gone through DegradedPISP feature Asynckey retry,prevents alertdouble on-callcharge; poll status endpoint
    Duplicate submission Detect via uniqueUNIQUE constraint,constraint returnon 409idempotency_key ClearReturn errorexisting messagetransaction NoneNo user action needed
    InvalidKYC statenot transitionapproved Return 422403 with state machine errorimmediately Clear error message with KYC link User correctscompletes inputKYC verification
    EventExchange publishrate failuremissing LogReturn to404 retry queue,payment return 202blocked AsyncClear delayerror: corridor not supported BackgroundAdmin retryupdates exchange_rates table
    Balance insufficientReturn 403Clear error with balance shownUser reduces amount or top-up bank account

    10. Configuration & Feature Flags

    Environment Variables

    limit
    Variable Type Default Description
    {{MODULE_NAME}}_CACHE_TTLNEXT_PUBLIC_SERVICE_MODE numberstring 300mock Cachemock TTL= secondssimulated PISP; production = real Neonomics calls
    {{MODULE_NAME}}_MAX_LIST_SIZEOPEN_BANKING_API_URL numberstring 100 MaxNeonomics itemsbase perURL page(production)
    {{MODULE_NAME}}_RATE_LIMIT_RPMOPEN_BANKING_CLIENT_ID numberstringeIDAS client ID for Neonomics
    OPEN_BANKING_CLIENT_SECRET 60string Rate eIDAS perclient minutesecret (from Secrets Manager)

    Feature Flags (environment variables)

    Flag Type Default Description
    {{MODULE_NAME}}_ENABLE_CACHEFEATURE_QR_ENABLED boolean true Toggle RedisQR cachingpayment feature
    {{MODULE_NAME}}_ENABLE_{{FEATURE}}FEATURE_WITHDRAW_ENABLED boolean false GradualToggle rolloutwithdrawal offeature {{FEATURE}}(Phase 3)

    11. Monitoring & Health Checks

    Health Check Endpoint

    GET /health/{{module-name}}v1/health (owned by health route, includes payment module indicators)

    {
      "status": "healthy | degraded | unhealthy"ok",
      "checks": {
        "database"version": "healthy"0.1.0",
      "cache"uptime": 3600,
      "db": "healthy | degraded"connected",
      "externalApi"dbLatencyMs": 1,
      "timestamp": "healthy | degraded | unhealthy"
      },
      "latency": {
        "database_ms": 5,
        "cache_ms": 1
      }2026-02-23T10:00:00.000Z"
    }
    

    Key Metrics (via Sentry + CloudWatch)

    (PISP
    Metric Type Alert Threshold Dashboard
    Remittance {{module}}_requests_total201 rate Counter Drop > 50% over 5m {{DASHBOARD_LINK}}Sentry Issues
    Remittance {{module}}_request_duration_ms502 Histogram p99 > {{THRESHOLD}}ms{{DASHBOARD_LINK}}
    {{module}}_errors_totalCounterError rate > {{THRESHOLD}}%{{DASHBOARD_LINK}}
    {{module}}_cache_hit_rateGauge< {{THRESHOLD}}% for 5min{{DASHBOARD_LINK}}
    {{module}}_db_pool_exhausteddown) Counter Any occurrence {{DASHBOARD_LINK}}Sentry Alert
    Transaction processing timeHistogramp99 > 2000msCloudWatch
    kyc_required 403 rateCounter> 20% of remittance attemptsSentry
    insufficient_balance 403 rateCounter> 30% of remittance attemptsSentry

    12. Primary Flow — Sequence Diagram

    sequenceDiagram
        autonumber
        participant C as ControllerClient (Web/Mobile)
        participant SRL as ServiceRate Limiter
        participant VAuth as ValidatorAuth Middleware
        participant RRoute as RepositoryTransactions Route
        participant DB as PostgreSQL
        participant EBPISP as EventOpen BusBanking participantPISP Cache as Redis(Neonomics)
    
        C->>V:RL: validate(dto)POST /v1/transactions/remittance
        RL->>RL: Check rate_limits (10/IP, 3/user per 60s)
        RL->>Auth: Forward if within limits
        Auth->>DB: Verify JWT + session + user
        Auth-->>Route: {userId, role, kycStatus}
    
        Route->>Route: Validate: kycStatus='approved', amount 100-50000
        alt InvalidValidation inputfails
            V-Route-->>C: ValidationError
            C-->>Client: 400 Bad Request400/403/422
        end
    
        V-Route->>DB: SELECT recipient WHERE id=? AND user_id=?
        Route->>DB: SELECT exchange_rate WHERE to_currency=?
        Route->>DB: SELECT bank_account WHERE id=? AND user_id=?
        Route->>Route: Calculate fee, totalCost, receiveAmount
        alt balance < totalCost
            Route-->>C: Validated403 DTO
    
        C->>S: create(dto, context)
        S->>S: checkBusinessRules(dto)
        alt Business rule violation
            S-->>C: BusinessRuleError
            C-->>Client: 422 Unprocessableinsufficient_balance
        end
    
        S-Route->>DB: BEGIN TRANSACTION
        S-Route->>R:DB: create(entityData)UPDATE R-bank_accounts SET balance = balance - totalCostInOere
        Route->>DB: INSERT INTO {{table_name}}transactions DB--(status='processing', idempotency_key=?)
        Route->>R:DB: InsertedINSERT recordINTO R--audit_log
        Route->>S:DB: {{Entity}}INSERT domainINTO objectnotifications
        S-Route->>DB: COMMIT
    
        S-Route->>EB:PISP: publish("{{entity}}.created",POST event)/v1/payments/sepa-credit-transfers Note(with overidempotency_key)
        EB: Async — does not block response
    
        S-PISP-->>Cache:Route: INVALIDATE{paymentId, relatedscaRedirect, keystransactionStatus: S-"RCVD"}
    
        Route-->>C: {{Entity}} DTO
        C-->>Client: 201 Created{transactionId, status: "processing", scaRedirect}
    

    Approval

    Role Name Date Signature
    Author Petter Graff 2026-02-23
    Module Owner John (AI Director)
    Tech Lead John (AI Director)
    Reviewer Alem Bašić