Appearance
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.tsSPA served via Vite; production static build behind nginx. Dev proxy: frontend → Gateway → API.
Bank import UI
| Area | Location | Notes |
|---|---|---|
| Upload + import list + matching + rules | BankStatementsPage hub (`/bank-statements?tab=imports | matching |
| Post-upload | redirect to /bank-statements/:id | Detail workspace |
| Import detail | BankStatementDetailPage | Tabs: recognition, transactions, Quality report, duplicates, metadata |
| Quality report | detail quality tab | Confidence, row counts, informational balance check (bank = source of truth; mismatch does not block Syrve posting) |
| Duplicates | detail duplicates tab | Candidate 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:
| Group | Controllers | Notes |
|---|---|---|
| Auth | AuthController | JWT login, me, logout |
| Bank / matching | ApiControllers, NameMatchesController | Import, transactions, quality, duplicates, counterparty matches |
| Allocations | ApiControllers | Preview, confirm |
| Syrve orchestration | SyrveOrchestrationControllers | Bridge, health, sync, postings, evidence, reconcile |
| Anomalies / notifications | PaymentAnomalyControllers | Anomalies CRUD, BackOffice verification, notifications |
| Dashboard / audit | ApiControllers | Operational 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/importsfor failed imports and quality-warning imports.GET /api/bank-statements/quality-summaryfor aggregate duplicate candidates.GET /api/name-matches?status=Unmatchedfor unmatched counterparties.GET /api/name-matches?status=LowConfidencefor low-confidence matches.GET /api/payment-allocations?status=Previewfor allocations ready for operator review.GET /api/syrve/postings?status=Failedfor failed Syrve/iiko postings.GET /api/payment-anomalies?status=Open&pageSize=50for open payment anomalies.
Each block shows its current count, a short operator-facing explanation, and a link to the filtered workspace:
| Block | Target |
|---|---|
| Импорты с ошибками | /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 detailThe 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=Unmatchedand 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:mockedBank import
Status: production-oriented MVP with multiple sanitized-tested Indonesian bank adapters, quality reporting, duplicate detection, and operator UI.
| Component | Location |
|---|---|
IBankAdapter, BankAdapterDescriptor | Domain |
BankStatementFormatDetector | BankStatements |
BankStatementParser | BankStatements |
BankStatementImportService | BankStatements |
BankStatementSupportedBanksService | BankStatements |
BankStatementQualityService | BankStatements |
BankStatementDuplicateDetectionService | BankStatements |
| Adapters | BankStatements/Adapters — see list below |
| Entities | BankStatementImport, BankTransaction, BankAdapterDetection, BankStatementImportQualityReport, BankTransactionDuplicateCandidate |
Adapters (current):
| Adapter | Format |
|---|---|
CorporateExplicitDebitCreditCsvAdapter | CIMB account statement summary CSV |
MultiAccountCrDbCsvAdapter | BCA multi-account CR/DB CSV |
PermataCsvAdapter | Permata CSV |
PermataTextAdapter | Permata TXT |
PermataMt940LikeAdapter | Permata MT940-like |
MandiriAccountStatementCsvAdapter | Mandiri 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 allocationCoverage: 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.
| Piece | Role |
|---|---|
Organization, Location | Tenant model |
UserRoleAssignment | Scoped roles (org/location) |
OrganizationQuota | Per-org limits; legacy Syrve/iiko capability columns are deprecated and ignored |
OrganizationQuotaService | Enforces 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).
| Piece | Role |
|---|---|
ISyrveIikoConnectionBridge | Create/update bridge link |
IikoSyrveConnectionProvider | Resolve connection for reads/writes |
SyrveConnectionLiveChecker | Health + auth diagnostics |
PasswordSecretRef | pay-secret:{secretRecordId} for Pay-managed encrypted secrets, or env:VAR for deployment-managed secrets — no raw password in Pay DB/API |
SecretRecord | AES-GCM encrypted local secret payload (Ciphertext/Nonce/Tag) keyed by stable reference |
syrve.credentials.rotate | Scoped 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
| Layer | May reference IikoConnector? |
|---|---|
| Domain | No |
| Application | No |
| Api | No |
| Infrastructure | Yes (adapters, workers) |
| IikoConnector | Transport 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
→ SyrveReconciliationWorkerEntities: 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:
| Control | Requirement |
|---|---|
| Approval | Every new posting starts in ApprovalRequired; ApprovedByUserId is required |
| Scope | Scoped RBAC and owned organization/Outlet/connection |
| Business validity | Payable document, amount/allocation consistency, no blocking anomaly |
| Server applicability | Selected method is supported by the detected server/deployment |
| Transport readiness | Credentials, HTTP/RPC and method-specific preflight |
| Idempotency | Unique IdempotencyKey; RequestHash consistency |
| Technical limits | Retry, 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
| Component | Location |
|---|---|
PaymentAnomaly entity | Domain |
IPaymentAnomalyAppService | Application |
PaymentAnomalyAppServices | Infrastructure |
| API | PaymentAnomalyControllers |
| UI | PaymentAnomaliesPage, 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.
Manual corrections foundation (MHPAY-73)
| Component | Location |
|---|---|
ManualCorrection entity | Entities.cs |
ManualCorrectionTypes / ManualCorrectionStatuses | DomainEnums.cs |
IManualCorrectionService, DTOs | Application/Abstractions/ManualCorrections.cs |
ManualCorrectionService | Infrastructure/Services/ManualCorrectionService.cs |
| API | ManualCorrectionsController |
| Migration | 20260630160000_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)
| Component | Location |
|---|---|
| Cancel API | POST /api/manual-corrections/{id}/cancel |
CancelAsync | ManualCorrectionService |
| Contracts / hooks | frontend/src/api/contracts/manualCorrectionContracts.ts, useManualCorrectionsApi.ts |
| List / detail UI | ManualCorrectionsPage, ManualCorrectionDetailPage |
| Permissions (frontend) | manual_corrections.view / manual_corrections.manage → scoped mapping in permissions.ts |
| Nav | Hidden 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/MegaManagercan query global audit, optionally filtered by organization/location.OrganizationAdmin,Accountant, andViewercan read only audit entries inside their effective organization/location scope.- Explicit foreign
organizationIdorlocationIdfilters 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
| Component | Location |
|---|---|
AppNotification entity | Domain |
INotificationAppService | Application |
NotificationAppService | Infrastructure |
| UI | NotificationBell in AppLayout |
Types include payment_reconciliation_failed, payment_manual_review_required.
Tests and guards
| Test project | Purpose |
|---|---|
MyHoreca.Pay.ArchitectureTests | Layer references, UUID v7 policy |
MyHoreca.Pay.UnitTests | Bank 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
| Gap | Impact | Phase |
|---|---|---|
| Tenant scoping audit for all legacy endpoints | Possible cross-tenant leakage on older paths | 2 |
| Legacy role assumptions in older UI/API paths | Inconsistent scoped vs flat role behavior | 2 |
| Additional real bank adapters beyond current coverage | Cannot onboard every Indonesian bank yet | 1 |
| Real production sample validation | Local/staging only; repo uses sanitized fixtures | 1 |
bank-import-pipeline-review.md follow-up refresh | Doc may lag adapter/duplicate work | docs |
Quality report UI labels (Detection confidence, Parse confidence) | Partial English in Russian UI | 1 |
| DTO/API response audit | Controllers use named DTOs (MHPAY-21) | docs |
| OUTGOING_INVOICE readback | FAIL_RECONCILE | 4 |
| No transaction type guard on write response | Risk of silent mismatch | 4 |
| Production posting authorization procedure | Always-on capability must not replace scoped approval | 6 |
| Integration live reports not in VitePress sidebar | Discoverability (playbooks are linked) | docs |
Live test evidence
| Report | Result |
|---|---|
| Read-only 20260619-0928 | FAIL — auth; infra fixes |
| Read-only rerun 20260619-0955 | FAIL_AUTH — improved diagnostics |
| Test write 20260619-1530 | PASS_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
| Topic | Document |
|---|---|
| Layer/module graph | codegraph/manifest.yaml |
| Bank import pipeline review | bank-import-pipeline-review.md |
| Normalized bank transaction model | normalized-bank-transaction-model.md |
| Bank statement parsing | bank-statement-parsing.md |
| Scoped RBAC / tenancy | scoped-rbac-and-tenancy.md |
| Payment orchestration | syrve-payment-orchestration.md |
| Anomaly control | payment-anomaly-control.md |
| Identity / UUID v7 | identity-and-ids.md |
| API | api.md |
| Env vars | environment-variables.md |
| UAT / release | uat-checklist.md, production-readiness-report.md, release-checklist.md |
| Product scope | product-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.