-
Notifications
You must be signed in to change notification settings - Fork 0
Use Cases
Tiana_ edited this page May 30, 2026
·
1 revision
Functional capabilities of FinCore Engine - what the system must be able to do, expressed in user-centric terms. Each use case is independently testable and traces to one or more Epics.
- Actor: who initiates the use case (Client App, Operator, Webhook Producer, Internal Service, Scheduled Job)
- Preconditions: state required before the use case can run
- Main flow: happy path
- Alternative flows: variations and exceptions
- Postconditions: state after success
- Acceptance criteria: testable statements (used to derive contract tests)
| Field | Value |
|---|---|
| ID | UC-01 |
| Epic | Epic-01 Ledger |
| Actor | Client App (authenticated via OAuth2 client credentials) |
| Goal | Open a new ledger account with explicit type and currency |
- Client has a valid JWT with role
LEDGER_WRITER - Currency code is ISO 4217
- Client sends
POST /v1/accountswith{ name, type, currency, metadata? }andIdempotency-Keyheader - Service validates the request (currency in ISO 4217, type in allowed enum, name length 1..255)
- Service checks the idempotency key; if present and matches, returns the previous response
- Service generates
accountId = UUID.randomUUID() - Service persists the account with
status = ACTIVE,version = 0,created_at = now() - Service emits
account.createdevent via Outbox - Service returns
201 Createdwith the full account representation
-
A1 - Idempotency replay: same
Idempotency-Keyand same request body → return the original201response (cached for 24h) -
A2 - Idempotency conflict: same
Idempotency-Key, different request body →409 Conflict(RFC 7807) -
A3 - Validation error: invalid currency / unsupported type / empty name →
400 Bad Request -
A4 - Auth missing/invalid: →
401 Unauthorizedor403 Forbidden
- Row exists in
accountswith the returnedid - Row exists in
outbox_eventswithevent_type = account.created - Audit trail captures
actor_id,correlation_id,request_hash
- AC-01.1: Concurrent identical POSTs with same idempotency key produce exactly one row in
accounts - AC-01.2: Account
versionstarts at 0 and increments on each update - AC-01.3:
account.createdevent payload includesid,type,currency,created_at - AC-01.4: Response p99 latency < 200ms under normal load
- AC-01.5: All write paths logged with structured JSON including
correlation_id
| Field | Value |
|---|---|
| ID | UC-02 |
| Epic | Epic-01 Ledger |
| Actor | Client App (any authenticated reader) |
| Goal | Retrieve the current balance of an account in its native currency |
- Account exists and is not soft-deleted
- Caller has role
LEDGER_READERor higher
- Client sends
GET /v1/accounts/{id}/balance - Service loads the account; if missing →
404 - Service computes the balance from the materialized view
account_balances(sum of entries) - Service returns
200 OKwith{ accountId, currency, balance, asOf, version }
-
A1 - Fresh balance required: query parameter
?fresh=truetriggersREFRESH MATERIALIZED VIEW CONCURRENTLYbefore returning (rate-limited to once per second per account) -
A2 - Account closed:
status != ACTIVE→ balance still returned withstatusfield
- No state change
- Read counted in metrics
ledger.balance.reads_total
- AC-02.1: Balance == sum of all
entries.amountfor the account (per currency) - AC-02.2: Balance is consistent within a single read (snapshot isolation)
- AC-02.3: p99 latency < 50ms for cached reads, < 200ms for
fresh=true
| Field | Value |
|---|---|
| ID | UC-03 |
| Epic | Epic-01 Ledger |
| Actor | Client App (operator viewing transactions) |
| Goal | Retrieve a paginated list of entries (debits/credits) for an account |
- Account exists
- Caller has role
LEDGER_READER
- Client sends
GET /v1/accounts/{id}/entries?from=...&to=...&cursor=...&limit=50 - Service validates date range (max 90 days per request)
- Service queries entries with cursor-based pagination
- Service returns
200 OKwith{ items, nextCursor, hasMore }
- A1 - No range provided: defaults to last 30 days
-
A2 - Invalid cursor:
400 Bad Requestwith explicitcorrelation_id
- No state change
- AC-03.1: Entries returned in deterministic order (
created_at DESC, id DESC) - AC-03.2: Cursor is opaque, base64-encoded, includes integrity hash
- AC-03.3: Page returns
<= limititems;hasMore=trueonly if there exist more matching rows
| Field | Value |
|---|---|
| ID | UC-04 |
| Epic | Epic-01 Ledger |
| Actor | Internal service (Payments) or Client App with role LEDGER_POSTER
|
| Goal | Atomically debit one or more accounts and credit one or more accounts, preserving SUM(entries) = 0 per currency |
- All referenced accounts exist and are
ACTIVE - All entries are in the same currency (single-currency transaction); multi-currency requires separate FX flow
- Caller has role
LEDGER_POSTER
- Client sends
POST /v1/transactionswith{ reference, description?, entries: [{accountId, amount, direction}] }andIdempotency-Keyheader - Service validates: at least 2 entries, sum equals zero, no duplicate
accountId+directionpairs unless explicitly allowed - Service checks idempotency key
- In a single DB transaction (
REPEATABLE_READ):- Insert one
transactionsrow withstatus = POSTED - Insert N
entriesrows - Verify the invariant
SUM(entries.amount) = 0(deferred trigger check at commit) - Refresh affected
account_balancesMV rows - Append
outbox_eventsrow withevent_type = transaction.posted
- Insert one
- On commit, service returns
201 Createdwith the transaction representation
-
A1 - Invariant violation: trigger raises exception →
422 Unprocessable Entity, transaction rolled back -
A2 - Account inactive: any account
status != ACTIVE→409 Conflict -
A3 - Reference collision: duplicate
reference(unique constraint) →409 Conflict -
A4 - Multi-currency entries: →
422 Unprocessable Entity -
A5 - Optimistic lock failure: concurrent posting → retry up to 3 times, then
503
- Transaction with new
idexists - Account balances updated atomically
- Outbox event written
- AC-04.1: Concurrent posts to the same accounts always produce consistent balances (no lost updates)
- AC-04.2: Failed posts leave zero rows in
transactionsandentries - AC-04.3: Database-level trigger blocks any write that would violate
SUM = 0 - AC-04.4: Outbox event is committed in the same transaction as the ledger writes
- AC-04.5: p99 latency < 300ms with 100 concurrent writers
| Field | Value |
|---|---|
| ID | UC-05 |
| Epic | Epic-01 Ledger |
| Actor | Operator with role LEDGER_REVERSER
|
| Goal | Reverse a previously posted transaction by creating a compensating transaction |
- Original transaction exists with
status = POSTED - Original transaction has not been reversed before
- Caller has role
LEDGER_REVERSER
- Client sends
POST /v1/transactions/{id}/reversewith{ reason }andIdempotency-Key - Service loads the original transaction
- Service creates a new transaction with inverted
entries(debits become credits and vice versa) linked viareverses_id - Original transaction marked
status = REVERSED - Outbox event
transaction.reversedemitted
-
A1 - Already reversed:
409 Conflict -
A2 - Missing:
404 Not Found
- Compensating transaction exists with same magnitude opposite directions
- Both transactions linked via foreign keys
- AC-05.1: Reverse preserves invariants
- AC-05.2:
reasonis mandatory and stored in audit log - AC-05.3: Original transaction is not deleted (immutable journal)
| Field | Value |
|---|---|
| ID | UC-06 |
| Epic | Epic-02 Payments |
| Actor | Client App (authenticated user or system) |
| Goal | Initiate a payment from one account to another, possibly via an external bank provider |
- Both
from_accountandto_accountexist -
from_accounthas sufficient available balance (or overdraft allowed) - Caller has role
PAYMENTS_INITIATOR
- Client sends
POST /v1/paymentswith{ fromAccountId, toAccountId, amount, currency, reference? }andIdempotency-Key - Service validates the payment (amount > 0, currencies match accounts)
- Service creates a
Paymentaggregate in stateCREATED - Service runs synchronous pre-checks:
- Balance check
- Decision engine evaluation (
UC-13)
- If decision =
APPROVE, payment moves to statePROCESSINGand posts a ledger transaction (UC-04) - Outbox event
payment.createdis emitted - Service returns
202 Acceptedwith the payment representation
-
A1 - Insufficient balance:
422 Unprocessable Entity, payment moves to stateFAILED -
A2 - Decision = REJECT: payment moves to
REJECTED, returns202with explanation -
A3 - Decision = REVIEW: payment moves to
PENDING_REVIEW, returns202
-
paymentsrow exists with appropriate state - Ledger transaction exists if approved
- Outbox event committed
- AC-06.1: Payment lifecycle states follow the defined state machine (no illegal transitions)
- AC-06.2: Idempotent retries return the same payment
- AC-06.3: Decision engine is invoked exactly once per attempt
| Field | Value |
|---|---|
| ID | UC-07 |
| Epic | Epic-02 Payments |
| Actor | Client App |
| Goal | Retrieve the current status of a payment, including history |
- Client sends
GET /v1/payments/{id}(optionally?include=events) - Service returns
{ id, state, amount, ..., events?: [...] }where events show state transitions
- AC-07.1: Event log is append-only
- AC-07.2: All state transitions include actor, timestamp, reason
| Field | Value |
|---|---|
| ID | UC-08 |
| Epic | Epic-02 Payments |
| Actor | External Bank Provider (HMAC-signed webhook) |
| Goal | Update a payment status based on a callback from the bank |
- Webhook is HMAC-signed with shared secret per provider
- Provider is registered
- Provider POSTs to
/v1/webhooks/payments/{providerId}with signed body - Service verifies HMAC signature; if invalid →
401 Unauthorized(logged for forensic purposes) - Service deduplicates the webhook by
provider_event_id(stored inprocessed_webhookstable) - Service updates the corresponding payment via state machine
- If terminal state (
COMPLETEDorFAILED), ledger may post compensating entries - Outbox event
payment.<status>emitted - Service responds
200 OK
-
A1 - Duplicate webhook: deduplicated,
200 OKreturned (idempotent for provider) -
A2 - Unknown payment:
404 Not Found
- AC-08.1: Replay attacks blocked by deduplication
- AC-08.2: Signature verification mandatory; secret rotated quarterly
- AC-08.3: Webhook processing p99 < 500ms
| Field | Value |
|---|---|
| ID | UC-09 |
| Epic | Epic-02 Payments |
| Actor | Scheduled job or operator |
| Goal | Retry a payment that failed due to transient error |
- Payment is in state
FAILEDwithretryable = true - Retry attempts < max retries (default 5)
- Job loads pending retries with exponential backoff (1m, 5m, 15m, 1h, 6h)
- For each, re-invokes the bank adapter
- Updates payment state based on the response
- AC-09.1: Retries are idempotent - same
Idempotency-Keyreused - AC-09.2: After max retries, payment moves to
PERMANENTLY_FAILEDand emits alert
| Field | Value |
|---|---|
| ID | UC-10 |
| Epic | Epic-04 Compliance |
| Actor | Client App (during user onboarding) |
| Goal | Initiate identity verification through a pluggable provider |
- User is authenticated
- KYC provider is configured (sandbox by default)
- Client POSTs
/v1/kyc/sessionswith{ userId, providerId? } - Service creates a KYC session in state
PENDING - Service calls the configured
KycProvideradapter (sandbox returns mock URL) - Service returns
{ sessionId, providerSessionUrl, status }
-
A1 - Provider down: queue for retry, return
202withpending -
A2 - User uploads doc:
POST /v1/kyc/sessions/{id}/documents -
A3 - Provider callback: handled via webhook (
UC-08style), updates session toAPPROVED/REJECTED/MANUAL_REVIEW
- AC-10.1: PII is encrypted at rest (AES-256 via DB-level encryption)
- AC-10.2: Provider switching requires no API contract change
- AC-10.3: All session state transitions are auditable
| Field | Value |
|---|---|
| ID | UC-11 |
| Epic | Epic-04 Compliance |
| Actor | Internal service (triggered on every payment via Kafka consumer) |
| Goal | Evaluate a transaction against AML rules and flag suspicious activity |
- Transaction posted (UC-04)
- Compliance service consumes
transaction.postedevent - Loads applicable AML rules from
aml_rulestable - Evaluates rules sequentially (stop on first match or evaluate all per config)
- If matched, creates an
aml_alertrow and emitsaml.flaggedevent - Optionally creates a
compliance_casefor human review
- AC-11.1: Rules engine is deterministic - same input produces same output
- AC-11.2: Rule changes are versioned; old transactions evaluated against rules at time of post
- AC-11.3: p99 evaluation < 50ms
| Field | Value |
|---|---|
| ID | UC-12 |
| Epic | Epic-04 Compliance |
| Actor | Compliance officer (human) |
| Goal | Approve or reject a flagged transaction after manual investigation |
- Officer fetches
GET /v1/compliance/cases/{id} - Officer reviews transaction details, rule that fired, AI explanation (if available via plugin)
- Officer POSTs
/v1/compliance/cases/{id}/resolvewith{ decision: APPROVE|REJECT, reason } - Service updates the case state, optionally reverses the transaction (UC-05) if rejected
- Outbox event
compliance.case.resolvedemitted
- AC-12.1: Resolution requires
reason(audit trail) - AC-12.2: Resolved cases are immutable
| Field | Value |
|---|---|
| ID | UC-13 |
| Epic | Epic-05 Decision Engine |
| Actor | Internal service (Payments, Compliance, Onboarding) |
| Goal | Run a decision evaluation against active rules and return a deterministic result |
- Decision rules exist and are active
- Caller invokes the engine with
{ context: { ... }, ruleSetId? } - Engine loads active rules sorted by priority
- Engine evaluates conditions deterministically against the context
- Engine returns
{ decision: APPROVE|REJECT|REVIEW, matchedRules: [...], explanation } - Engine writes a row to
decision_logsfor audit
-
A1 - No rules match: returns
{ decision: APPROVE, matchedRules: [] }(configurable default) -
A2 - Rule error: catch and log, fail-safe to
REVIEW
- AC-13.1: Engine is pure (no side effects beyond audit log)
- AC-13.2: p99 evaluation < 10ms for typical rule sets (< 100 rules)
- AC-13.3: Every decision is reproducible from
decision_logs(input context + rule version)
| Field | Value |
|---|---|
| ID | UC-14 |
| Epic | Epic-05 Decision Engine |
| Actor | Risk operator with role DECISION_ADMIN
|
| Goal | Create, update, deactivate decision rules with full version history |
- Operator POSTs/PUTs to
/v1/decision/ruleswith JSON DSL definition - Service validates DSL syntax and references
- Service stores a new version in
rule_versions; the latest version perrule_idis whatdecision_logsreferences at evaluation time - Activation requires explicit
POST /v1/decision/rules/{id}/activate
- AC-14.1: Rules are versioned; old versions are queryable
- AC-14.2: Invalid DSL is rejected with explicit error pointing to the offending field
- AC-14.3: Rule changes are audited (who, when, what)
| Field | Value |
|---|---|
| ID | UC-15 |
| Epic | Epic-09 Developer Experience |
| Actor | Client App (webhook consumer) |
| Goal | Receive events about ledger / payments / compliance / decision lifecycle |
- Client POSTs
/v1/webhooks/subscriptionswith{ url, events: [...], secret } - Service stores subscription
- On every relevant outbox event, dispatcher signs the payload (HMAC) and POSTs to subscriber URL
- On 2xx response → mark delivered
- On non-2xx → retry with exponential backoff (1m, 5m, 30m, 6h, 24h, 3d, 7d) up to 7 attempts
- AC-15.1: Delivery is at-least-once with HMAC integrity
- AC-15.2: Subscriber can verify signature with their stored secret
- AC-15.3: Delivery success rate > 99.9% within 30s under normal conditions
- AC-15.4: Failed deliveries are inspectable via
GET /v1/webhooks/subscriptions/{id}/deliveries
| Use Case | Epic | Priority for v0.1.0 |
|---|---|---|
| UC-01 Create Account | Epic-01 Ledger | P0 |
| UC-02 Get Balance | Epic-01 Ledger | P0 |
| UC-03 List Entries | Epic-01 Ledger | P0 |
| UC-04 Post Transaction | Epic-01 Ledger | P0 |
| UC-05 Reverse Transaction | Epic-01 Ledger | P1 |
| UC-06 Initiate Payment | Epic-02 Payments | P1 (v0.2.0) |
| UC-07 Track Payment | Epic-02 Payments | P1 (v0.2.0) |
| UC-08 Webhook Handling | Epic-02 Payments | P1 (v0.2.0) |
| UC-09 Retry Payment | Epic-02 Payments | P2 (v0.2.0) |
| UC-10 KYC Verification | Epic-04 Compliance | P2 (v0.3.0) |
| UC-11 AML Check | Epic-04 Compliance | P2 (v0.3.0) |
| UC-12 Resolve Case | Epic-04 Compliance | P2 (v0.3.0) |
| UC-13 Evaluate Decision | Epic-05 Decision Engine | P1 (v0.2.0) |
| UC-14 Manage Rules | Epic-05 Decision Engine | P1 (v0.2.0) |
| UC-15 Webhook Subscriptions | Epic-09 Developer Experience | P2 (v0.4.0) |
The following are deliberately not part of FinCore Engine OSS:
- No ML risk scoring (only
RiskScorerplug-in interface; implementations are out of scope) - No Real bank/KYC provider integrations (only sandbox + plug-in interfaces)
- No Customer portal / user-facing UI beyond the dashboard sandbox
- No Multi-tenancy with tenant isolation guarantees (each adopter handles tenancy themselves)
- No Mobile SDKs (web SDK only)
- No FX rate management (delegated to bank provider)
- No Custodial wallet management for crypto
- No Direct integration with sanctioned-list providers (only
SanctionsProviderinterface)
- Overview
- Services
- Data Model
- Domain Model
- Event Flow
- Security
- Observability
- Resilience
- SLA / SLI / SLO