Skip to content

Current Architecture State ​

Snapshot of MyHoreca Pay architecture after MHPAY-1..6 updates (2026-06-22). For phases and readiness, see Roadmap.

Backend layers ​

text
MyHoreca.Pay.Api          → controllers, Program.cs
MyHoreca.Pay.Gateway      → YARP reverse proxy (/api → Api)
MyHoreca.Pay.Application  → use cases, DTOs, abstractions (no Infrastructure refs)
MyHoreca.Pay.Domain       → entities, enums, bank statement interfaces
MyHoreca.Pay.Persistence  → EF Core, migrations, DatabaseSeeder
MyHoreca.Pay.BankStatements → IBankAdapter, import pipeline
MyHoreca.Pay.Infrastructure → auth, Syrve adapters, workers, tenancy
IikoConnector*            → Syrve/iiko transport (Infrastructure only)

Dependency rules enforced by MyHoreca.Pay.ArchitectureTests — see codegraph/manifest.yaml → dependency_rules.

API response safety: public endpoints prefer flat DTOs. ReferenceLoopHandling.Ignore is configured globally in Program.cs as a safety net, not as the primary response design — controllers must not rely on EF navigation serialization.

Frontend structure ​

text
frontend/src/
  api/           — httpClient, apiContracts, hooks (TanStack Query)
  components/    — reusable UI (DataTable, NotificationBell, …)
  pages/         — thin route pages
  routes/        — router.tsx, guards.tsx
  shared/        — statusMessages.ts

SPA served via Vite; production static build behind nginx. Dev proxy: frontend → Gateway → API.

Bank import UI ​

AreaLocationNotes
Upload + import list + matching + rulesBankStatementsPage hub (`/bank-statements?tab=importsmatching
Post-uploadredirect to /bank-statements/:idDetail workspace
Import detailBankStatementDetailPageTabs: recognition, transactions, Quality report, duplicates, metadata
Quality reportdetail quality tabConfidence, row counts, informational balance check (bank = source of truth; mismatch does not block Syrve posting)
Duplicatesdetail duplicates tabCandidate cards (reason, matched signals, current vs existing summary), confirm/dismiss

See Bank statement parsing, Normalized bank transaction model, Bank import pipeline review.

API surface ​

Full endpoint list: API reference. Codegraph api_surface maps endpoints to controllers, hooks, and pages.

Groups:

GroupControllersNotes
AuthAuthControllerJWT login, me, logout
Bank / matchingApiControllers, NameMatchesControllerImport, transactions, quality, duplicates, counterparty matches
AllocationsApiControllersPreview, confirm
Syrve orchestrationSyrveOrchestrationControllersBridge, health, sync, postings, evidence, reconcile
Anomalies / notificationsPaymentAnomalyControllersAnomalies CRUD, BackOffice verification, notifications
Dashboard / auditApiControllersOperational summary, audit log

Review Queue ​

/review-queue is the operator action queue. It is implemented as a frontend aggregation page, not as a new bounded context or backend endpoint.

The page uses existing hooks and APIs:

  • GET /api/bank-statements/imports for failed imports and quality-warning imports.
  • GET /api/bank-statements/quality-summary for aggregate duplicate candidates.
  • GET /api/name-matches?status=Unmatched for unmatched counterparties.
  • GET /api/name-matches?status=LowConfidence for low-confidence matches.
  • GET /api/payment-allocations?status=Preview for allocations ready for operator review.
  • GET /api/syrve/postings?status=Failed for failed Syrve/iiko postings.
  • GET /api/payment-anomalies?status=Open&pageSize=50 for open payment anomalies.

Each block shows its current count, a short operator-facing explanation, and a link to the filtered workspace:

BlockTarget
Импорты с ошибками/bank-statements?tab=imports (filter status Failed)
Предупреждения качества/bank-statements?tab=imports (quality warnings)
Возможные дубли/bank-statements?tab=imports (duplicates)
Несопоставленные контрагенты/bank-statements?tab=matching&status=Unmatched
Низкая уверенность/bank-statements?tab=matching&status=LowConfidence
Готово к распределению оплаты/payment-allocations?status=Preview
Ошибки проведения/syrve/postings?status=Failed
Аномалии платежей/payment-anomalies?status=Open

Target pages read these query parameters and reflect or apply the selected filter. If all counts are zero, the page displays Очередь пуста.

MHPAY-1..13 Mocked E2E Smoke ​

frontend/tests/mhpay-e2e-smoke.spec.ts proves that the bank import, quality review, duplicate review, name matching, payment candidates, allocation preview, allocation confirmation, and allocation detail screens work together as one operator scenario.

The scenario is fully mocked and uses the sanitized fixture frontend/scripts/samples/sample-bank-statement.csv. It does not require live backend services, real bank files, real Syrve/iiko credentials, or live Syrve/iiko calls.

Flow covered by the test:

text
login
  -> upload bank statement
  -> open import
  -> check quality report
  -> resolve duplicate candidate
  -> open unmatched transaction
  -> manual counterparty match
  -> open payment candidates
  -> create allocation preview
  -> confirm allocation
  -> open allocation detail

The mocked API surface is provided by frontend/tests/helpers/mockApi.ts, including:

  • bank import, import detail, transactions, quality report, and duplicates;
  • GET /api/name-matches?status=Unmatched and manual match mutation;
  • GET /api/payment-candidates/{bankTransactionId};
  • POST /api/payment-candidates/build/{bankTransactionId};
  • POST /api/payment-allocations/preview;
  • POST /api/payment-allocations/confirm;
  • GET /api/payment-allocations/{id}.

Run it with the mocked suite:

bash
cd frontend
npm run test:mocked

Bank import ​

Status: production-oriented MVP with multiple sanitized-tested Indonesian bank adapters, quality reporting, duplicate detection, and operator UI.

ComponentLocation
IBankAdapter, BankAdapterDescriptorDomain
BankStatementFormatDetectorBankStatements
BankStatementParserBankStatements
BankStatementImportServiceBankStatements
BankStatementSupportedBanksServiceBankStatements
BankStatementQualityServiceBankStatements
BankStatementDuplicateDetectionServiceBankStatements
AdaptersBankStatements/Adapters — see list below
EntitiesBankStatementImport, BankTransaction, BankAdapterDetection, BankStatementImportQualityReport, BankTransactionDuplicateCandidate

Adapters (current):

AdapterFormat
CorporateExplicitDebitCreditCsvAdapterCIMB account statement summary CSV
MultiAccountCrDbCsvAdapterBCA multi-account CR/DB CSV
PermataCsvAdapterPermata CSV
PermataTextAdapterPermata TXT
PermataMt940LikeAdapterPermata MT940-like
MandiriAccountStatementCsvAdapterMandiri account statement CSV

Pipeline:

text
upload
  -> detect bank/format
  -> parse through adapter
  -> normalize transactions
  -> persist import, transactions, quality report
  -> run duplicate detection
  -> show upload result / quality report / duplicate candidates in UI
  -> continue to counterparty matching and allocation

Coverage: current adapters cover Permata CSV/TXT/MT940-like, corporate explicit debit/credit CSV, and multi-account CR/DB-style CSV. More real bank adapters are still needed, but the adapter pattern and sanitized fixtures are now established.

Documentation: bank-import-pipeline-review.md (MHPAY-1), normalized-bank-transaction-model.md (MHPAY-2), bank-statement-parsing.md (MHPAY-3). Pipeline review exists but may need follow-up refresh after adapter/fixture/duplicate work.

Tenancy and scoped RBAC ​

Organization/Location foundation and scoped RBAC are in place. See Scoped RBAC and tenancy.

PieceRole
Organization, LocationTenant model
UserRoleAssignmentScoped roles (org/location)
OrganizationQuotaPer-org limits; legacy Syrve/iiko capability columns are deprecated and ignored
OrganizationQuotaServiceEnforces quotas; creates default OrganizationQuota when organization exists but quota row is missing

Payment write safety remains enforced by orchestration independently of deprecated capability columns: scoped authorization, mandatory manual approval, idempotency, business checks, server applicability and technical preflight.

Tenant isolation hardening: older endpoints were reviewed so list endpoints apply organization/location scope, detail endpoints check scoped access, and mutations verify the target resource before write. Regression coverage lives in backend/tests/MyHoreca.Pay.UnitTests/ScopedRbac/TenantIsolationTests.cs. Remaining tenancy work is mainly admin UX for quota configuration and broader API integration coverage.

Syrve bridge ​

Pay stores operational SyrveConnection (portal registry). IikoConnector stores IikoRestaurantConnection (runtime sessions).

PieceRole
ISyrveIikoConnectionBridgeCreate/update bridge link
IikoSyrveConnectionProviderResolve connection for reads/writes
SyrveConnectionLiveCheckerHealth + auth diagnostics
PasswordSecretRefpay-secret:{secretRecordId} for Pay-managed encrypted secrets, or env:VAR for deployment-managed secrets — no raw password in Pay DB/API
SecretRecordAES-GCM encrypted local secret payload (Ciphertext/Nonce/Tag) keyed by stable reference
syrve.credentials.rotateScoped permission for secure credentials rotation; granted to MegaAdmin, MegaManager, OrganizationAdmin

API: POST /api/syrve/connections/{id}/bridge, GET .../health, POST .../credentials/rotate.

Local Pay-managed Syrve passwords require PaySecrets__EncryptionKey in API configuration. The bridge and IikoCredentialsProvider resolve credentials through ISecretResolver; env: fallback remains supported for externally managed secrets, and legacy secret: aliases normalize to env:. Credential rotation uses the encrypted local secret store, preserves BaseUrl, invalidates Redis runtime token/session key mhpay:{env}:org:{organizationId}:syrve:runtime-token:{connectionId}, syncs the bridge, and tests the connection after save.

See Syrve payment orchestration.

IikoConnector boundaries ​

LayerMay reference IikoConnector?
DomainNo
ApplicationNo
ApiNo
InfrastructureYes (adapters, workers)
IikoConnectorTransport only

Pay API host uses AddIikoConnectorForPayHost() from the current portable connector. The host includes session, catalog/entity/document/warehouse/employee read services, OLAP/reconciliation read facades and the existing Pay payment-write bridge. Compatibility booleans reintroduced in portable IikoOptions are forced on inside this Pay-host registration, so they do not become deployment capability switches. Planning product-candidate discovery and bank-statement recovery are deliberately not registered because their persistence ports belong to the EnterCo product model; Pay must add its own typed adapters before enabling those two runtimes.

External JSON DTOs use [JsonProperty] under IikoConnector External/. Pay stack uses PayJsonSerializer with typed DTOs.

Payment orchestration ​

text
SyrveConnection
  → SyrveDocumentSyncWorker / manual sync
  → SyrveDocumentSnapshot
  → PaymentCandidate
  → PaymentAllocation (+ lines)
  → SyrvePaymentPosting (outbox)
  → PaymentPostingWorker
  → SyrveReconciliationWorker

Entities: SyrveOrchestrationEntities.cs — connection, snapshot, sync run, candidate.

Application abstractions: SyrveOrchestration.cs, SyrveAppServices.cs, SyrvePaymentWritePreflight.cs.

Infrastructure: SyrveOrchestrationServices.cs, IikoSyrveAppServices.cs, workers in SyrveWorkers.cs.

Formal Import, Transaction, Allocation, Posting, and Organization states/transitions are documented in Workflow State Machines. Backend guards, API conflict codes, and frontend status mapping should stay aligned to that page.

Payment write safety controls ​

Checked before worker executes a write:

ControlRequirement
ApprovalEvery new posting starts in ApprovalRequired; ApprovedByUserId is required
ScopeScoped RBAC and owned organization/Outlet/connection
Business validityPayable document, amount/allocation consistency, no blocking anomaly
Server applicabilitySelected method is supported by the detected server/deployment
Transport readinessCredentials, HTTP/RPC and method-specific preflight
IdempotencyUnique IdempotencyKey; RequestHash consistency
Technical limitsRetry, payload and response bounds

Flow: queue → dry-run → approve → worker → evidence.

Historical live result: PASS_WRITE on test iiko OutgoingInvoice under the configuration model that existed on 2026-06-19. Those removed flags are not current operating instructions.

Anomaly control ​

ComponentLocation
PaymentAnomaly entityDomain
IPaymentAnomalyAppServiceApplication
PaymentAnomalyAppServicesInfrastructure
APIPaymentAnomalyControllers
UIPaymentAnomaliesPage, posting detail

Lifecycle: Open → Acknowledged → Resolved / ResolvedManually / Dismissed.

Reconciliation retry delays: Syrve__Reconciliation__ReadbackRetryDelaysSeconds.

Verified live: overpaid_after_write Critical anomaly, notifications, retry block on open anomaly.

See Payment anomaly control.

Manual corrections foundation (MHPAY-73) ​

ComponentLocation
ManualCorrection entityEntities.cs
ManualCorrectionTypes / ManualCorrectionStatusesDomainEnums.cs
IManualCorrectionService, DTOsApplication/Abstractions/ManualCorrections.cs
ManualCorrectionServiceInfrastructure/Services/ManualCorrectionService.cs
APIManualCorrectionsController
Migration20260630160000_ManualCorrectionFoundation

Purpose: first-class persisted correction records with before/after JSON snapshots, scoped RBAC, and audit log on create. Status lifecycle supports Draft / Applied / Cancelled; create endpoint defaults to Applied for minimal audit trail.

Manual corrections UI + cancel (MHPAY-74) ​

ComponentLocation
Cancel APIPOST /api/manual-corrections/{id}/cancel
CancelAsyncManualCorrectionService
Contracts / hooksfrontend/src/api/contracts/manualCorrectionContracts.ts, useManualCorrectionsApi.ts
List / detail UIManualCorrectionsPage, ManualCorrectionDetailPage
Permissions (frontend)manual_corrections.view / manual_corrections.manage → scoped mapping in permissions.ts
NavHidden from sidebar until create UI and/or auto-wiring from match/quality/allocation exist. Routes /manual-corrections remain for API, audit deep-links, and future work.

Cancel MVP marks correction Cancelled with audit + rowVersion concurrency; does not auto-revert target business entities. UI shows before/after/metadata JSON with defensive pretty-print.

Product note (2026-07-19): for accountants the list is almost always empty — nothing in the live workflow writes ManualCorrection rows, and there is no create form in the SPA. Keeping the nav item was UI noise; hide until the journal is actually populated.

Out of scope (future): wiring existing counterparty match / quality override flows to auto-create corrections.

Manual corrections reason policy (MHPAY-75) ​

Manual correction create/apply and cancel now use a centralized IManualCorrectionReasonPolicy / ManualCorrectionReasonPolicy. The policy normalizes accepted reasons before persistence/audit, enforces 10..500 characters, rejects generic placeholders, and rejects secret-like terms (password, secret, token, credential, apiKey, authorization, bearer).

Validation failures return validation.failed with fieldErrors.reason; rejected raw reasons are not emitted in API error details. The same policy shape is intended to be reused by future counterparty match, quality override, and allocation note dangerous flows.

MHPAY-74 cancel behavior remains unchanged otherwise: status Applied → Cancelled, rowVersion guard, audit entry, no automatic rollback of target business entities.

Append-only audit + tombstones (MHPAY-86) ​

Audit log writes remain append-only through normal EF/API paths. AuditService.LogAsync is the normal writer, PayDbContext.SaveChanges / SaveChangesAsync reject tracked AuditLogEntry modifications and deletions, and there is no public audit update/delete endpoint. Critical operational writers should include typed DetailsJson (ids, amounts, names, { changes } / { reason }) and org/location scope when known.

Bank import balance: BalanceMatches is calculated and shown on the Quality tab as informational status only. BankImportQualityGate.RequiresBalanceReviewOverride always returns false — mismatch does not block posting or workbench actions (bank statement is source of truth). DB fields and the override API remain for compatibility.

GET /api/audit-log is read-only and scoped:

  • MegaAdmin / MegaManager can query global audit, optionally filtered by organization/location.
  • OrganizationAdmin, Accountant, and Viewer can read only audit entries inside their effective organization/location scope.
  • Explicit foreign organizationId or locationId filters return a safe empty result.

Deletion lifecycle now includes a service-only final hard delete foundation (IDeletionLifecycleService.HardDeleteAsync). It requires PendingHardDelete, allows only MegaAdmin, deletes Pay-owned data only, cancels unfinished Pay jobs/outbox work before removal, and writes an AuditAction.HardDeleted tombstone that remains queryable after the Pay entity is removed. Tombstone details include minimal actor/scope/entity/state/reason/correlation/preflight/job-cancellation data and redact sensitive terms/full account numbers; raw bank lines, raw bank payloads, secrets, Syrve/iiko credentials, and full private customer payloads remain forbidden.

Notifications ​

ComponentLocation
AppNotification entityDomain
INotificationAppServiceApplication
NotificationAppServiceInfrastructure
UINotificationBell in AppLayout

Types include payment_reconciliation_failed, payment_manual_review_required.

Tests and guards ​

Test projectPurpose
MyHoreca.Pay.ArchitectureTestsLayer references, UUID v7 policy
MyHoreca.Pay.UnitTestsBank import, allocation, Syrve orchestration, anomaly, always-on runtime safety, scoped RBAC/quota

Backend unit tests (bank import): adapter detection/parsing, sanitized fixtures, import service, quality reports, duplicate detection. BankStatementFixtureSanitizationTests prevents committing raw bank/customer samples to the repo.

Key safety tests cover always-on runtime registration, payment approval/preflight/idempotency/anomaly controls, live bridge behavior and scoped RBAC.

Frontend Playwright (mocked): smoke.spec.ts, bank-statements-ui.spec.ts, bank-statements-import-flow.spec.ts, bank-statement-duplicates.spec.ts cover login/dashboard, supported banks widget, bank statement upload, drag-and-drop upload, upload result block, import details, Quality report, duplicate candidates confirm/dismiss, expired token redirect/login flow.

Bank statement testing uses sanitized automated fixtures — not live bank data committed to the repo.

Known gaps ​

GapImpactPhase
Tenant scoping audit for all legacy endpointsPossible cross-tenant leakage on older paths2
Legacy role assumptions in older UI/API pathsInconsistent scoped vs flat role behavior2
Additional real bank adapters beyond current coverageCannot onboard every Indonesian bank yet1
Real production sample validationLocal/staging only; repo uses sanitized fixtures1
bank-import-pipeline-review.md follow-up refreshDoc may lag adapter/duplicate workdocs
Quality report UI labels (Detection confidence, Parse confidence)Partial English in Russian UI1
DTO/API response auditControllers use named DTOs (MHPAY-21)docs
OUTGOING_INVOICE readbackFAIL_RECONCILE4
No transaction type guard on write responseRisk of silent mismatch4
Production posting authorization procedureAlways-on capability must not replace scoped approval6
Integration live reports not in VitePress sidebarDiscoverability (playbooks are linked)docs

Live test evidence ​

ReportResult
Read-only 20260619-0928FAIL — auth; infra fixes
Read-only rerun 20260619-0955FAIL_AUTH — improved diagnostics
Test write 20260619-1530PASS_WRITE / FAIL_RECONCILE

Syrve evidence above remains the live integration baseline. Bank import is validated via sanitized unit/Playwright fixtures, not committed production bank files.

Reference map ​

TopicDocument
Layer/module graphcodegraph/manifest.yaml
Bank import pipeline reviewbank-import-pipeline-review.md
Normalized bank transaction modelnormalized-bank-transaction-model.md
Bank statement parsingbank-statement-parsing.md
Scoped RBAC / tenancyscoped-rbac-and-tenancy.md
Payment orchestrationsyrve-payment-orchestration.md
Anomaly controlpayment-anomaly-control.md
Identity / UUID v7identity-and-ids.md
APIapi.md
Env varsenvironment-variables.md
UAT / releaseuat-checklist.md, production-readiness-report.md, release-checklist.md
Product scopeproduct-scope.md

Mistral OCR document recognition vertical ​

PDF/JPEG/PNG/WEBP extend the existing bank-statement import lifecycle. Originals use the existing local BankStatementFile storage and SHA-256 duplicate boundary. A leased PostgreSQL worker streams each original once to Mistral Files API and performs one Mistral OCR request with strict document annotation, OCR pages and blocks. Typed output is persisted as draft recognition rows. The React detail workspace visually links deterministically matched, approximate normalized source boxes to immutable recognized and editable current values.

Confirmation is the sole boundary that creates BankTransaction and runs duplicate, matching and quality processing. Confirmed final correction deltas create tenant-scoped candidates; anonymization/privacy validation produces platform-approved global examples. Immutable dataset versions, typed evaluation metrics, deterministic import-id canary selection, one active Mistral model per input kind, activation and rollback form one global—not per-tenant—learning contour. See ADR.