From cf8e73299213d995b68ceaf4de81a232e94f55d2 Mon Sep 17 00:00:00 2001 From: Codewriter90x Date: Wed, 8 Jul 2026 09:54:09 +0200 Subject: [PATCH 01/10] docs(testing): triage excluded database tests --- Docs/testing/db-test-triage.md | 174 ++++++++++++++++++ .../OpenCashFlow.Test.csproj | 6 +- 2 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 Docs/testing/db-test-triage.md diff --git a/Docs/testing/db-test-triage.md b/Docs/testing/db-test-triage.md new file mode 100644 index 0000000..51c9f93 --- /dev/null +++ b/Docs/testing/db-test-triage.md @@ -0,0 +1,174 @@ +# Database Test Triage + +Date: 2026-07-08 +Branch: `test/db-legacy-test-triage` + +## Summary + +The database-level tests under `tests/OpenCashFlow.Test/Tests/db/**/*.cs` are not part of the default test suite. + +`tests/OpenCashFlow.Test/OpenCashFlow.Test.csproj` excludes them with: + +```xml + +``` + +That means `dotnet test OpenCashFlow.sln --configuration Release --no-build` reports zero skipped tests because these files are not compiled or discovered. This is different from ordinary xUnit skipped tests. The files still contain test drafts, skipped facts, direct `ApplicationDbContext` usage, and assumptions about seed data/database constraints. + +This document classifies them so they are no longer silent debt. + +## Inventory + +| File | Declared Tests | `Skip =` Attributes | Area | Current Status | +| --- | ---: | ---: | --- | --- | +| `tests/OpenCashFlow.Test/Tests/db/Company_Tests.cs` | 8 | 7 | Company persistence | Excluded from compilation | +| `tests/OpenCashFlow.Test/Tests/db/Employee_Tests.cs` | 12 | 12 | Employee/Company_Staff persistence | Excluded from compilation | +| `tests/OpenCashFlow.Test/Tests/db/Lookup_Tests.cs` | 30 | 28 | Payment methods, document types, lookup persistence | Excluded from compilation | +| `tests/OpenCashFlow.Test/Tests/db/Payment_Tests.cs` | 21 | 15 | Payment persistence | Excluded from compilation | +| `tests/OpenCashFlow.Test/Tests/db/Registration_Tests.cs` | 0 | 0 | Registration TODO notes | Excluded from compilation | +| **Total** | **71** | **62** | Company, employee, lookup, payment, registration notes | **Not discovered by test runner** | + +## Covered Areas + +The excluded files cover these intended database-level concerns: + +- Company required columns, optional columns, contract date chronology, FK delete restrictions, soft delete, `DateIns`, and `DateEdit`. +- Employee/Company_Staff required FK fields, missing FK targets, duplicate user/company assignments, multi-company assignment rules, delete restrictions, `CreatedBy`, and UTC timestamps. +- Payment method and document type required fields, duplicate names, global lookup semantics, tenant FK behavior, visibility, soft delete, and delete restrictions when referenced by payments. +- Payment required fields, FK references to payment method/document type/company/user, negative amount, optional fields, audit timestamps, update, soft delete, query filtering, and related-data loading. +- Registration file contains TODO notes only and no executable tests. + +## Existing Coverage Overlap + +### Company + +Current API/Application tests provide meaningful coverage for many public contract behaviors: + +- create company success; +- required `CompanyName` validation; +- duplicate `CompanyName` validation; +- duplicate TIN validation; +- list/detail authorization; +- tenant isolation for detail/update; +- update validation; +- soft delete; +- active-relation delete failure; +- active-state/name/TIN/revenue filtering; +- self-hosted expired-contract behavior; +- `MaxUsers` employee-create enforcement. + +Remaining database-specific value: + +- direct persistence defaults for optional columns; +- date chronology constraints if they are meant to be database-enforced; +- `DateIns`/`DateEdit` persistence behavior; +- hard FK delete behavior beneath the API soft-delete contract. + +Recommended action: convert only the remaining persistence-specific checks into a dedicated DB integration suite. Do not duplicate Company API contract tests. + +### Payments + +Current API/Application tests provide meaningful coverage for: + +- create/update/delete payment flows; +- authentication and role enforcement; +- tenant isolation and cross-company lookup rejection; +- negative amount validation; +- locked field mutation rejection; +- list/detail/filter behavior; +- cash ledger and daily payment orchestration at Application level. + +Remaining database-specific value: + +- raw FK constraint behavior for invalid payment method, document type, tenant and user IDs; +- EF persistence of optional fields and timestamps; +- direct query filter behavior if required below repository level. + +Recommended action: convert FK/timestamp persistence checks into a dedicated DB integration suite. Treat API-level behavior as already covered and avoid duplicating it. + +### Payment Methods And Document Types + +Current Application tests cover use-case validation and reader/writer interaction for payment methods and document types. The API lookup test file exists, but many compiled `Lookup_Tests.cs` facts contain only `// todo: implement test`, so they do not provide meaningful behavioral coverage. + +Remaining valuable checks: + +- required fields at database level; +- duplicate name constraints, if the schema is intended to enforce them; +- tenant-scoped versus global lookup semantics; +- soft delete persistence; +- FK delete restrictions when lookups are referenced by payments; +- visibility query semantics, if implemented below the API layer. + +Recommended action: prioritize this group after Company/Payment persistence because overlap is weaker. + +### Employees / Company_Staff + +Application tests cover some employee use-case behavior, and registration API tests contain real assertions for registration/user/staff/company creation. However, `tests/OpenCashFlow.Test/Tests/API/Empolyee_Tests.cs` contains placeholder facts with `// todo: implement test`, so it does not provide meaningful API coverage for the scenarios it names. + +Remaining valuable checks: + +- `Company_Staff` FK constraints; +- duplicate `(UserID, TenantID)` rules; +- whether a user may belong to multiple companies in the current self-hosted model; +- employee delete restrictions when referenced by payments; +- audit/timestamp persistence. + +Recommended action: decide the intended user-to-company cardinality first. Then convert constraints into DB integration tests and replace placeholder API Employee tests with real API tests. + +### Registration + +The excluded DB registration file contains TODO notes only. Current API registration tests contain real assertions around successful registration, validation failures, duplicate behavior, roles, approval flags, timestamps and email sending. + +Recommended action: archive or delete the excluded DB registration TODO file after confirming every note maps to an existing API/Application test or a tracked backlog item. + +## Recommended Migration Path + +Do not re-enable `Tests/db/**/*.cs` in the default test project as-is. The files are not ready: + +- they depend directly on `ApplicationDbContext`; +- they assume seeded data through `.First()` calls; +- several assertions describe missing desired behavior rather than current enforced behavior; +- several tests use placeholder wording such as `TBF`, `WIP`, and `TO BE FIXED`; +- some scenarios duplicate newer API/Application tests; +- some scenarios need explicit product decisions before they can be asserted safely. + +Recommended phases: + +1. Create a dedicated database integration test project, for example `tests/OpenCashFlow.Infrastructure.Tests` or `tests/OpenCashFlow.Database.Tests`. +2. Use Testcontainers PostgreSQL consistently, like the existing high-value API integration tests. +3. Move only persistence-specific tests, not API contract duplicates. +4. Replace seed assumptions with per-test data builders. +5. Remove all `Skip = ...` attributes from migrated tests. +6. Archive or delete the old excluded files only after migrated coverage exists. +7. Add a CI job or optional workflow that can run the DB integration suite explicitly. + +## Group Decisions + +| Group | Decision | Reason | +| --- | --- | --- | +| Company DB tests | Convert selected tests into DB integration tests | Several API contract behaviors are covered, but persistence defaults/timestamps/FK restrictions still have value. | +| Payment DB tests | Convert selected tests into DB integration tests | API/Application coverage is strong, but raw FK and timestamp persistence behavior remains useful. | +| Lookup DB tests | Convert many into DB integration tests | Current lookup API tests are mostly placeholders, so database/use-case coverage remains weak. | +| Employee DB tests | Convert after product decisions | User/company cardinality and delete semantics need explicit decisions before assertions are safe. | +| Registration DB TODO file | Archive or delete after mapping TODOs | It contains no executable tests; current API registration tests already cover much of the valuable behavior. | + +## Immediate Change Made + +The test project exclusion was made explicit: + +```xml + + + +``` + +This keeps the default suite stable while making the excluded files visible as non-compiled test artifacts. + +## Remaining Risk + +The default test result can still say `0 skipped` while these historical DB test drafts remain outside compilation. The risk is now documented, but not eliminated. + +The most important remaining coverage gap is not "make skipped count non-zero"; it is converting valuable persistence behaviors into real Testcontainers-backed integration tests with deterministic setup and assertions. diff --git a/tests/OpenCashFlow.Test/OpenCashFlow.Test.csproj b/tests/OpenCashFlow.Test/OpenCashFlow.Test.csproj index 61a9c7e..70981ef 100644 --- a/tests/OpenCashFlow.Test/OpenCashFlow.Test.csproj +++ b/tests/OpenCashFlow.Test/OpenCashFlow.Test.csproj @@ -38,9 +38,13 @@ - + + From 092a648202fe3cdee0f3e76998aa47875630d3d5 Mon Sep 17 00:00:00 2001 From: Codewriter90x Date: Wed, 8 Jul 2026 10:02:51 +0200 Subject: [PATCH 02/10] docs: polish public repository presentation --- .github/ISSUE_TEMPLATE/bug_report.md | 27 --------- Docs/ROADMAP.md | 71 +++++++++++++---------- Docs/quality/quality-gates-report.md | 8 ++- Docs/ui/payments-index-refactor-report.md | 3 +- README.md | 2 +- 5 files changed, 47 insertions(+), 64 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 160740e..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: bug -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Additional context** -Add any other context about the problem here. diff --git a/Docs/ROADMAP.md b/Docs/ROADMAP.md index 4e1726b..c8bc9f1 100644 --- a/Docs/ROADMAP.md +++ b/Docs/ROADMAP.md @@ -16,35 +16,40 @@ serious public self-hosted product. ## Short-Term Priorities -1. Repository hygiene - - Remove tracked local/internal residue. - - Remove or archive legacy Admin source outside active `src`. - - Keep only sanitized environment examples. - -2. Test reliability - - Reduce skipped tests. - - Document remaining skipped tests with explicit risk and required fixes. - - Prioritize authorization, tenant isolation, company CRUD, payment isolation, cash ledger, and setup. - -3. Security posture - - Keep dependency advisories at zero. - - Continue CSP and frontend hardening. - - Document threat model, secrets management, backup/restore, and production hardening. - -4. Developer experience - - Improve quickstart reliability. - - Add conservative CI quality gates. - - Document common local troubleshooting paths. - -5. UI maintainability - - Split very large Razor views. - - Move inline scripts/styles into static assets where practical. - - Preserve existing behavior while reducing maintenance risk. - -6. Architecture cleanup - - Continue moving orchestration out of API services. - - Keep WebApp independent from Infrastructure. - - Keep Contracts limited to stable API/shared contracts. +1. Database integration test migration + - Move historical database-level tests into an explicit integration-test suite or archive them with evidence. + - Prefer a repeatable Testcontainers-backed path for persistence constraints, tenant isolation, and migration checks. + - Keep excluded DB test coverage visible until it is migrated or intentionally retired. + +2. Production hardening + - Validate TLS/reverse proxy, cookie/JWT settings, SMTP, logging, rate limiting, and operational defaults. + - Keep Docker Compose defaults clearly scoped to local evaluation. + - Turn production-readiness claims into evidence-backed checks before any stable release. + +3. Backup/restore drill + - Execute a real backup and restore against a fresh database. + - Document RPO/RTO assumptions and operator steps. + - Verify restored instances can pass a minimal smoke test. + +4. Upgrade/migration drill + - Exercise EF migrations against a copied database. + - Document rollback expectations and release migration notes. + - Keep schema changes out of stable releases unless migration behavior is proven. + +5. GitHub alert closeout + - Reconcile GitHub dependency alerts against the default branch and current dependency files. + - Classify any remaining alerts by NuGet, Docker base image, GitHub Action, frontend package, or stale removed file. + - Keep advisory status documented until the GitHub Security tab is clean or intentionally dismissed. + +6. Frontend/static asset cleanup + - Continue extracting inline Razor scripts/styles into versioned static assets. + - Remove remaining CDN dependencies where local assets are available. + - Keep CSP free of `unsafe-inline` and `unsafe-eval`. + +7. Infrastructure/Contracts boundary reduction + - Review Infrastructure dependencies on public Contracts and remove DTO coupling where practical. + - Keep Contracts limited to stable API/shared boundary types. + - Keep EF entities owned by Infrastructure. ## Medium-Term Direction @@ -58,12 +63,14 @@ serious public self-hosted product. Before a production-ready claim, the project needs: -- no high-risk skipped tests without tracked justification; +- explicit integration-test handling for database-level coverage; - documented backup and restore process; -- documented upgrade/migration policy; -- production secrets guidance; +- verified backup and restore drill; +- verified upgrade/migration drill; +- production secrets and hardening guidance validated against real deployment settings; - security review of auth, reset-password, PIN/fast-login, tenant isolation, and authorization; - at least one repeatable full Docker smoke test; +- GitHub dependency alert closeout; - clear release artifacts and versioning policy. ## Release Policy diff --git a/Docs/quality/quality-gates-report.md b/Docs/quality/quality-gates-report.md index a14dc0f..fe400ac 100644 --- a/Docs/quality/quality-gates-report.md +++ b/Docs/quality/quality-gates-report.md @@ -79,9 +79,10 @@ Deferred until they can be introduced safely: - stricter analyzers; - warning-as-error policy; - SBOM generation as a required gate; -- making skipped tests fail CI. +- failing CI automatically on any future skipped test. -Skipped tests are still visible in CI output. They should be reduced or tracked in `Docs/testing/skipped-tests-backlog.md` once that backlog branch is merged. +Skipped tests remain visible in CI output. The current skipped-test status is tracked in +`Docs/testing/skipped-tests-backlog.md`; any future skipped test should include a precise reason, risk, and follow-up. ## Local Verification @@ -98,7 +99,8 @@ Results: - `git diff --check`: passed. - `dotnet build OpenCashFlow.sln --configuration Release --no-restore`: passed with 0 warnings and 0 errors. -- `dotnet test OpenCashFlow.sln --configuration Release --no-build`: passed with 225 passed, 30 skipped, 0 failed. +- `dotnet test OpenCashFlow.sln --configuration Release --no-build`: passed. See current CI output and + `Docs/testing/skipped-tests-backlog.md` for the current test count. - `docker compose config`: passed. Local note: diff --git a/Docs/ui/payments-index-refactor-report.md b/Docs/ui/payments-index-refactor-report.md index 72a94c5..3b5e6e6 100644 --- a/Docs/ui/payments-index-refactor-report.md +++ b/Docs/ui/payments-index-refactor-report.md @@ -98,4 +98,5 @@ Results: - `git diff --check`: passed. - `dotnet build OpenCashFlow.sln --configuration Release --no-restore`: passed with 0 warnings and 0 errors. -- `dotnet test OpenCashFlow.sln --configuration Release --no-build`: passed with 225 passed, 30 skipped, 0 failed. +- `dotnet test OpenCashFlow.sln --configuration Release --no-build`: passed. See current CI output and + `Docs/testing/skipped-tests-backlog.md` for the current test count. diff --git a/README.md b/README.md index 4b0bc63..8de92b7 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ Planned module work is documented in [Docs/modules/architecture.md](Docs/modules ### Build And Test ```bash -git clone https://github.com//OpenCashFlow.git +git clone https://github.com/Reckonry/OpenCashFlow.git cd OpenCashFlow dotnet restore OpenCashFlow.sln From 605de654f8837b715c978e5e18418b7ef0d1cc4b Mon Sep 17 00:00:00 2001 From: Codewriter90x Date: Wed, 8 Jul 2026 10:24:20 +0200 Subject: [PATCH 03/10] test(db): add integration test foundation --- Docs/testing/db-integration-tests.md | 70 ++++++++++++ Docs/testing/db-test-triage.md | 42 ++++++- OpenCashFlow.sln | 15 +++ .../CompanyPersistenceTests.cs | 60 ++++++++++ .../DatabaseTestFixture.cs | 40 +++++++ .../OpenCashFlow.Database.Tests.csproj | 34 ++++++ .../PaymentPersistenceTests.cs | 104 ++++++++++++++++++ .../PersistenceTestData.cs | 91 +++++++++++++++ 8 files changed, 452 insertions(+), 4 deletions(-) create mode 100644 Docs/testing/db-integration-tests.md create mode 100644 tests/OpenCashFlow.Database.Tests/CompanyPersistenceTests.cs create mode 100644 tests/OpenCashFlow.Database.Tests/DatabaseTestFixture.cs create mode 100644 tests/OpenCashFlow.Database.Tests/OpenCashFlow.Database.Tests.csproj create mode 100644 tests/OpenCashFlow.Database.Tests/PaymentPersistenceTests.cs create mode 100644 tests/OpenCashFlow.Database.Tests/PersistenceTestData.cs diff --git a/Docs/testing/db-integration-tests.md b/Docs/testing/db-integration-tests.md new file mode 100644 index 0000000..c42cbf3 --- /dev/null +++ b/Docs/testing/db-integration-tests.md @@ -0,0 +1,70 @@ +# Database Integration Tests + +OpenCashFlow has a dedicated database integration-test project: + +```text +tests/OpenCashFlow.Database.Tests +``` + +The project uses PostgreSQL Testcontainers and the real `ApplicationDbContext` migrations from +`OpenCashFlow.Infrastructure`. It is intended for persistence behavior that cannot be trusted with EF InMemory tests, +API-only tests, or pure Application tests. + +## Scope + +Use this project for: + +- database foreign-key behavior; +- delete/cascade/restrict behavior; +- persistence defaults and nullable column behavior; +- migration-backed schema behavior; +- tenant-related persistence constraints when those constraints are actually modeled in EF/schema. + +Do not use it to duplicate API contract tests or Application use-case tests. + +## Current Coverage + +The initial foundation covers: + +- Company soft-delete flag persistence; +- Company nullable optional fields; +- Payment foreign-key failures for unknown payment method, document type, and user; +- current-schema cascade behavior when deleting a referenced payment method. + +The cascade test documents observed behavior. It does not decide whether cascade is the desired product rule. + +## Running + +Docker must be available because Testcontainers starts PostgreSQL containers. + +```bash +dotnet test tests/OpenCashFlow.Database.Tests/OpenCashFlow.Database.Tests.csproj --configuration Release +``` + +The default solution test command also includes this project: + +```bash +dotnet test OpenCashFlow.sln --configuration Release --no-build +``` + +## Test Data Rules + +Database integration tests should: + +- create deterministic data per test; +- avoid `.First()` seed assumptions; +- use unique names/emails/IDs; +- avoid `Skip`; +- assert current schema behavior, not desired behavior without an implemented schema/model rule; +- document any surprising current behavior in `Docs/testing/db-test-triage.md`. + +## Historical DB Tests + +The old files under: + +```text +tests/OpenCashFlow.Test/Tests/db/**/*.cs +``` + +remain excluded from compilation. They are historical drafts and should not be re-enabled in bulk. Valuable scenarios +should be migrated one at a time into `OpenCashFlow.Database.Tests` after confirming the intended product/schema rule. diff --git a/Docs/testing/db-test-triage.md b/Docs/testing/db-test-triage.md index 51c9f93..9435500 100644 --- a/Docs/testing/db-test-triage.md +++ b/Docs/testing/db-test-triage.md @@ -17,6 +17,10 @@ That means `dotnet test OpenCashFlow.sln --configuration Release --no-build` rep This document classifies them so they are no longer silent debt. +Follow-up foundation work has started in `tests/OpenCashFlow.Database.Tests`. That project runs real PostgreSQL +Testcontainers tests for a small set of high-value persistence behaviors. The old `Tests/db/**/*.cs` files remain +excluded and should not be re-enabled in bulk. + ## Inventory | File | Declared Tests | `Skip =` Attributes | Area | Current Status | @@ -142,13 +146,33 @@ Recommended phases: 6. Archive or delete the old excluded files only after migrated coverage exists. 7. Add a CI job or optional workflow that can run the DB integration suite explicitly. +## Integration Test Foundation Added + +`tests/OpenCashFlow.Database.Tests` now provides the first real database integration-test foundation. + +Current migrated coverage: + +- Company soft-delete flags persist to PostgreSQL. +- Company optional nullable fields persist as `NULL`. +- Payment insert fails for an unknown `PaymentMethodID`. +- Payment insert fails for an unknown `DocumentTypeID`. +- Payment insert fails for an unknown `UserID`. +- Deleting a referenced payment method cascades to the related payment with the current EF schema. + +The cascade lookup test is intentionally named as current-schema behavior. It is not a recommendation that lookup +deletes should cascade in the product. If the intended product rule is "restrict lookup delete while referenced", that +requires an explicit schema/model change and migration in a later slice. + +The new tests use deterministic per-test data builders and avoid `.First()` seed assumptions. They do not carry any +`Skip` attributes. + ## Group Decisions | Group | Decision | Reason | | --- | --- | --- | -| Company DB tests | Convert selected tests into DB integration tests | Several API contract behaviors are covered, but persistence defaults/timestamps/FK restrictions still have value. | -| Payment DB tests | Convert selected tests into DB integration tests | API/Application coverage is strong, but raw FK and timestamp persistence behavior remains useful. | -| Lookup DB tests | Convert many into DB integration tests | Current lookup API tests are mostly placeholders, so database/use-case coverage remains weak. | +| Company DB tests | Partially converted into DB integration tests | Soft-delete and nullable-field persistence are covered. Date chronology and hard company delete rules remain product/schema decisions. | +| Payment DB tests | Partially converted into DB integration tests | FK behavior for payment method, document type, and user is covered. Tenant FK is not asserted because the current EF model does not define a Company FK on `Payment.TenantID`. | +| Lookup DB tests | Partially converted through payment lookup FK behavior | Referenced payment-method delete currently cascades; restrict-delete behavior would require a deliberate schema change. | | Employee DB tests | Convert after product decisions | User/company cardinality and delete semantics need explicit decisions before assertions are safe. | | Registration DB TODO file | Archive or delete after mapping TODOs | It contains no executable tests; current API registration tests already cover much of the valuable behavior. | @@ -171,4 +195,14 @@ This keeps the default suite stable while making the excluded files visible as n The default test result can still say `0 skipped` while these historical DB test drafts remain outside compilation. The risk is now documented, but not eliminated. -The most important remaining coverage gap is not "make skipped count non-zero"; it is converting valuable persistence behaviors into real Testcontainers-backed integration tests with deterministic setup and assertions. +The most important remaining coverage gap is not "make skipped count non-zero"; it is continuing to convert valuable +persistence behaviors into real Testcontainers-backed integration tests with deterministic setup and assertions. + +Remaining migration candidates: + +- Company hard-delete behavior once product/schema rules are explicit. +- Company `DateIns`/`DateEdit` persistence behavior. +- Payment timestamp persistence. +- Document-type referenced-delete behavior. +- Employee/company-staff cardinality, FK behavior, and delete semantics after a product decision. +- Archival or deletion of the registration DB TODO file after mapping its notes to current tests/backlog. diff --git a/OpenCashFlow.sln b/OpenCashFlow.sln index f8b21f7..76fb574 100644 --- a/OpenCashFlow.sln +++ b/OpenCashFlow.sln @@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenCashFlow.Application.Te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenCashFlow.Contracts", "src\OpenCashFlow.Contracts\OpenCashFlow.Contracts.csproj", "{EE9F0C09-E4A8-4A66-916B-3857F4A14A38}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenCashFlow.Database.Tests", "tests\OpenCashFlow.Database.Tests\OpenCashFlow.Database.Tests.csproj", "{E492D93A-CAE0-4C4F-B249-6F093E527E31}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -143,6 +145,18 @@ Global {EE9F0C09-E4A8-4A66-916B-3857F4A14A38}.Release|x64.Build.0 = Release|Any CPU {EE9F0C09-E4A8-4A66-916B-3857F4A14A38}.Release|x86.ActiveCfg = Release|Any CPU {EE9F0C09-E4A8-4A66-916B-3857F4A14A38}.Release|x86.Build.0 = Release|Any CPU + {E492D93A-CAE0-4C4F-B249-6F093E527E31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E492D93A-CAE0-4C4F-B249-6F093E527E31}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E492D93A-CAE0-4C4F-B249-6F093E527E31}.Debug|x64.ActiveCfg = Debug|Any CPU + {E492D93A-CAE0-4C4F-B249-6F093E527E31}.Debug|x64.Build.0 = Debug|Any CPU + {E492D93A-CAE0-4C4F-B249-6F093E527E31}.Debug|x86.ActiveCfg = Debug|Any CPU + {E492D93A-CAE0-4C4F-B249-6F093E527E31}.Debug|x86.Build.0 = Debug|Any CPU + {E492D93A-CAE0-4C4F-B249-6F093E527E31}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E492D93A-CAE0-4C4F-B249-6F093E527E31}.Release|Any CPU.Build.0 = Release|Any CPU + {E492D93A-CAE0-4C4F-B249-6F093E527E31}.Release|x64.ActiveCfg = Release|Any CPU + {E492D93A-CAE0-4C4F-B249-6F093E527E31}.Release|x64.Build.0 = Release|Any CPU + {E492D93A-CAE0-4C4F-B249-6F093E527E31}.Release|x86.ActiveCfg = Release|Any CPU + {E492D93A-CAE0-4C4F-B249-6F093E527E31}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -157,6 +171,7 @@ Global {21D55372-03A9-4FDE-9165-7F659BFC7D32} = {9BE36780-75DB-4BFE-BCA3-1799549F4556} {CAF17142-CCB8-40C1-A765-3B94BFCCF347} = {9BE36780-75DB-4BFE-BCA3-1799549F4556} {EE9F0C09-E4A8-4A66-916B-3857F4A14A38} = {165EE37F-DFDD-4A81-8772-333E2E004B84} + {E492D93A-CAE0-4C4F-B249-6F093E527E31} = {9BE36780-75DB-4BFE-BCA3-1799549F4556} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {96084FEB-7ED7-4016-933A-7C16D6B806C8} diff --git a/tests/OpenCashFlow.Database.Tests/CompanyPersistenceTests.cs b/tests/OpenCashFlow.Database.Tests/CompanyPersistenceTests.cs new file mode 100644 index 0000000..aba42d6 --- /dev/null +++ b/tests/OpenCashFlow.Database.Tests/CompanyPersistenceTests.cs @@ -0,0 +1,60 @@ +using Microsoft.EntityFrameworkCore; + +namespace OpenCashFlow.Database.Tests; + +public sealed class CompanyPersistenceTests(DatabaseTestFixture fixture) : IClassFixture +{ + [Fact] + public async Task Company_soft_delete_flags_are_persisted() + { + await using var db = fixture.CreateDbContext(); + var company = PersistenceTestData.Company($"Soft Delete {Guid.NewGuid():N}"); + var deletedBy = Guid.NewGuid(); + var deletedAt = DateTime.UtcNow; + + db.Company_DS.Add(company); + await db.SaveChangesAsync(); + + company.IsDeleted = true; + company.IsDeletedBy = deletedBy; + company.IsDeletedWhy = "database integration test"; + company.DateDeleted = deletedAt; + await db.SaveChangesAsync(); + + var persisted = await db.Company_DS + .AsNoTracking() + .SingleAsync(x => x.TenantID == company.TenantID); + + Assert.True(persisted.IsDeleted); + Assert.Equal(deletedBy, persisted.IsDeletedBy); + Assert.Equal("database integration test", persisted.IsDeletedWhy); + Assert.NotNull(persisted.DateDeleted); + } + + [Fact] + public async Task Company_optional_fields_can_be_null() + { + await using var db = fixture.CreateDbContext(); + var company = PersistenceTestData.Company($"Optional Nulls {Guid.NewGuid():N}"); + company.BusinessCategory = null; + company.Website = null; + company.EstimatedAnnualRevenue = null; + company.SocialLinks = null; + company.IBAN = null; + company.DefaultCurrency = null; + + db.Company_DS.Add(company); + await db.SaveChangesAsync(); + + var persisted = await db.Company_DS + .AsNoTracking() + .SingleAsync(x => x.TenantID == company.TenantID); + + Assert.Null(persisted.BusinessCategory); + Assert.Null(persisted.Website); + Assert.Null(persisted.EstimatedAnnualRevenue); + Assert.Null(persisted.SocialLinks); + Assert.Null(persisted.IBAN); + Assert.Null(persisted.DefaultCurrency); + } +} diff --git a/tests/OpenCashFlow.Database.Tests/DatabaseTestFixture.cs b/tests/OpenCashFlow.Database.Tests/DatabaseTestFixture.cs new file mode 100644 index 0000000..31539ea --- /dev/null +++ b/tests/OpenCashFlow.Database.Tests/DatabaseTestFixture.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore; +using OpenCashFlow.Infrastructure.Persistence; +using Testcontainers.PostgreSql; + +namespace OpenCashFlow.Database.Tests; + +public sealed class DatabaseTestFixture : IAsyncLifetime +{ + private readonly PostgreSqlContainer _container = new PostgreSqlBuilder() + .WithImage("postgres:16-alpine") + .WithDatabase("opencashflow_tests") + .WithUsername("opencashflow") + .WithPassword("opencashflow") + .WithCleanUp(true) + .Build(); + + public async Task InitializeAsync() + { + await _container.StartAsync(); + + await using var db = CreateDbContext(); + await db.Database.MigrateAsync(); + } + + public async Task DisposeAsync() + { + await _container.DisposeAsync(); + } + + public ApplicationDbContext CreateDbContext() + { + var options = new DbContextOptionsBuilder() + .UseNpgsql( + _container.GetConnectionString(), + npgsql => npgsql.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)) + .Options; + + return new ApplicationDbContext(options); + } +} diff --git a/tests/OpenCashFlow.Database.Tests/OpenCashFlow.Database.Tests.csproj b/tests/OpenCashFlow.Database.Tests/OpenCashFlow.Database.Tests.csproj new file mode 100644 index 0000000..64ce4c5 --- /dev/null +++ b/tests/OpenCashFlow.Database.Tests/OpenCashFlow.Database.Tests.csproj @@ -0,0 +1,34 @@ + + + + net10.0 + enable + enable + false + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/tests/OpenCashFlow.Database.Tests/PaymentPersistenceTests.cs b/tests/OpenCashFlow.Database.Tests/PaymentPersistenceTests.cs new file mode 100644 index 0000000..7ca8416 --- /dev/null +++ b/tests/OpenCashFlow.Database.Tests/PaymentPersistenceTests.cs @@ -0,0 +1,104 @@ +using Microsoft.EntityFrameworkCore; + +namespace OpenCashFlow.Database.Tests; + +public sealed class PaymentPersistenceTests(DatabaseTestFixture fixture) : IClassFixture +{ + [Fact] + public async Task Payment_insert_with_unknown_payment_method_fails_fk_constraint() + { + await using var db = fixture.CreateDbContext(); + var seed = await SeedPaymentGraphAsync(db); + var payment = PersistenceTestData.Payment( + seed.CompanyId, + seed.UserId, + Guid.NewGuid(), + seed.DocumentTypeId); + + db.Payment_DS.Add(payment); + + await Assert.ThrowsAsync(() => db.SaveChangesAsync()); + } + + [Fact] + public async Task Payment_insert_with_unknown_document_type_fails_fk_constraint() + { + await using var db = fixture.CreateDbContext(); + var seed = await SeedPaymentGraphAsync(db); + var payment = PersistenceTestData.Payment( + seed.CompanyId, + seed.UserId, + seed.PaymentMethodId, + Guid.NewGuid()); + + db.Payment_DS.Add(payment); + + await Assert.ThrowsAsync(() => db.SaveChangesAsync()); + } + + [Fact] + public async Task Payment_insert_with_unknown_user_fails_fk_constraint() + { + await using var db = fixture.CreateDbContext(); + var seed = await SeedPaymentGraphAsync(db); + var payment = PersistenceTestData.Payment( + seed.CompanyId, + Guid.NewGuid(), + seed.PaymentMethodId, + seed.DocumentTypeId); + + db.Payment_DS.Add(payment); + + await Assert.ThrowsAsync(() => db.SaveChangesAsync()); + } + + [Fact] + public async Task Deleting_referenced_payment_method_cascades_payment_with_current_schema() + { + await using var db = fixture.CreateDbContext(); + var seed = await SeedPaymentGraphAsync(db); + var payment = PersistenceTestData.Payment( + seed.CompanyId, + seed.UserId, + seed.PaymentMethodId, + seed.DocumentTypeId); + + db.Payment_DS.Add(payment); + await db.SaveChangesAsync(); + + var method = await db.PaymentMethod_DS.SingleAsync(x => x.PaymentMethodID == seed.PaymentMethodId); + db.PaymentMethod_DS.Remove(method); + await db.SaveChangesAsync(); + + var paymentExists = await db.Payment_DS.AnyAsync(x => x.PaymentID == payment.PaymentID); + + Assert.False(paymentExists); + } + + private static async Task SeedPaymentGraphAsync(OpenCashFlow.Infrastructure.Persistence.ApplicationDbContext db) + { + var suffix = Guid.NewGuid().ToString("N"); + var company = PersistenceTestData.Company($"Payment Company {suffix}"); + var user = PersistenceTestData.User($"payment-{suffix}@example.test"); + var method = PersistenceTestData.PaymentMethod(company.TenantID, $"Method {suffix}"); + var documentType = PersistenceTestData.DocumentType(company.TenantID, $"Doc {suffix}"); + + db.Company_DS.Add(company); + db.AspNetUser_DS.Add(user); + db.PaymentMethod_DS.Add(method); + db.Payment_DocumentType_DS.Add(documentType); + await db.SaveChangesAsync(); + + return new PaymentSeed( + company.TenantID, + user.UserID, + method.PaymentMethodID, + documentType.DocumentTypeID); + } + + private sealed record PaymentSeed( + Guid CompanyId, + Guid UserId, + Guid PaymentMethodId, + Guid DocumentTypeId); +} diff --git a/tests/OpenCashFlow.Database.Tests/PersistenceTestData.cs b/tests/OpenCashFlow.Database.Tests/PersistenceTestData.cs new file mode 100644 index 0000000..702cfef --- /dev/null +++ b/tests/OpenCashFlow.Database.Tests/PersistenceTestData.cs @@ -0,0 +1,91 @@ +using OpenCashFlow.Infrastructure.Persistence.Entities; +using OpenCashFlow.Infrastructure.Persistence.Entities.Identity; + +namespace OpenCashFlow.Database.Tests; + +internal static class PersistenceTestData +{ + public static Company Company(string name) + { + return new Company + { + TenantID = Guid.NewGuid(), + CompanyName = name, + MaxUsers = 10, + PriorityLevel = 1, + StartingContract = DateTime.UtcNow.AddDays(-1), + EndingContract = DateTime.UtcNow.AddYears(1), + GdprConsent = true, + ContractAcepted = true, + CompanySecret = $"secret-{Guid.NewGuid():N}", + IsActive = true + }; + } + + public static AspNetUser User(string email) + { + return new AspNetUser + { + UserID = Guid.NewGuid(), + UserName = email, + UserFirstName = "Integration", + UserLastName = "Tester", + Email = email, + EmailConfirmed = true, + IsApproved = true, + PasswordHash = "hash", + PasswordSalt = "salt" + }; + } + + public static Payment_Method_LookUps PaymentMethod(Guid tenantId, string name) + { + return new Payment_Method_LookUps + { + PaymentMethodID = Guid.NewGuid(), + TenantID = tenantId, + PaymentMethodName = name, + PaymentMethodDescription = "Integration test method", + Visible = true, + DisplayOrder = 10, + IsDeleted = false, + DateIns = DateTime.UtcNow + }; + } + + public static Payment_DocumentType_LookUp DocumentType(Guid tenantId, string name) + { + return new Payment_DocumentType_LookUp + { + DocumentTypeID = Guid.NewGuid(), + TenantID = tenantId, + DocumentTypeName = name, + DocumentTypeDescription = "Integration test doc", + Visible = true, + DisplayOrder = 10, + IsDeleted = false, + DateIns = DateTime.UtcNow + }; + } + + public static Payment Payment( + Guid tenantId, + Guid userId, + Guid paymentMethodId, + Guid documentTypeId) + { + return new Payment + { + PaymentID = Guid.NewGuid(), + TenantID = tenantId, + RequestId = Guid.NewGuid(), + Amount = 42.5, + EntryType = "Income", + PaymentMethodID = paymentMethodId, + DocumentTypeID = documentTypeId, + UserID = userId, + Description = "Database integration test payment", + DateIns = DateTime.UtcNow + }; + } +} From 967d2c627af8a1cbe69f60e5de63bb885e272a39 Mon Sep 17 00:00:00 2001 From: Codewriter90x Date: Wed, 8 Jul 2026 10:33:10 +0200 Subject: [PATCH 04/10] test(e2e): add clean install smoke path --- Docs/testing/clean-install-smoke.md | 102 ++++++ scripts/smoke/clean-install-smoke.sh | 305 ++++++++++++++++++ .../smoke/docker-compose.clean-install.yml | 76 +++++ 3 files changed, 483 insertions(+) create mode 100644 Docs/testing/clean-install-smoke.md create mode 100755 scripts/smoke/clean-install-smoke.sh create mode 100644 scripts/smoke/docker-compose.clean-install.yml diff --git a/Docs/testing/clean-install-smoke.md b/Docs/testing/clean-install-smoke.md new file mode 100644 index 0000000..729d896 --- /dev/null +++ b/Docs/testing/clean-install-smoke.md @@ -0,0 +1,102 @@ +# Clean Install Smoke Test + +OpenCashFlow has a scripted clean-install smoke path: + +```text +scripts/smoke/clean-install-smoke.sh +``` + +The script starts a disposable Docker Compose stack, applies the normal application startup path, completes initial +setup, logs in, creates a payment, and verifies the cash effect. + +## Scope + +The smoke currently verifies: + +- clean PostgreSQL database starts from an empty volume; +- API `/health` becomes healthy; +- WebApp `/Login` renders; +- `/v1/Setup/status` reports setup is required; +- `/v1/Setup` creates the first company/admin; +- `/v1/Authentication/login` returns a JWT; +- JWT contains `TenantID` and `UserID`; +- payment method and document type lookups are readable with the JWT; +- `/v1/Payment` creates a payment; +- `/v1/Payments` returns the created payment; +- `/v1/admin/cash/current` reports a positive balance after the cash payment; +- `/v1/admin/cash/ledger` contains an entry for the created payment. + +This is a smoke test, not a full browser E2E suite. It intentionally tests the public HTTP path with deterministic data +and a disposable database. + +## Prerequisites + +- Docker and Docker Compose; +- `curl`; +- `python3` for JSON/JWT parsing in the script; +- local ports available: + - API: `15100`; + - WebApp: `15200`; + - PostgreSQL: `15432`. + +Override ports if needed: + +```bash +SMOKE_API_PORT=16100 SMOKE_WEB_PORT=16200 SMOKE_DB_PORT=16432 scripts/smoke/clean-install-smoke.sh +``` + +## Running + +```bash +scripts/smoke/clean-install-smoke.sh +``` + +The script uses this Compose project name by default: + +```text +opencashflow-smoke +``` + +It uses a dedicated Compose file: + +```text +scripts/smoke/docker-compose.clean-install.yml +``` + +The smoke Compose file has isolated container names and host ports so it does not collide with the default OpenCashFlow +local stack. + +## Cleanup + +The script runs: + +```bash +docker compose -p opencashflow-smoke -f scripts/smoke/docker-compose.clean-install.yml down -v --remove-orphans +``` + +on exit by default. This removes the disposable smoke database volume. + +To keep the stack after a failure for inspection: + +```bash +SMOKE_KEEP_STACK=1 scripts/smoke/clean-install-smoke.sh +``` + +Then inspect logs with: + +```bash +docker compose -p opencashflow-smoke -f scripts/smoke/docker-compose.clean-install.yml logs +``` + +## Known Limits + +- It does not drive the Razor UI with a browser. +- It does not verify every dashboard widget. +- It does not verify password reset, fast-login PIN, exports, or admin settings. +- It assumes setup admin login remains available immediately after setup. + +Recommended future work: + +- add a browser-level smoke for Login, Setup, Payments, and Cash pages; +- run this smoke in CI as an optional or scheduled job with Docker available; +- add a backup/restore drill that runs after the smoke creates data. diff --git a/scripts/smoke/clean-install-smoke.sh b/scripts/smoke/clean-install-smoke.sh new file mode 100755 index 0000000..e62dda7 --- /dev/null +++ b/scripts/smoke/clean-install-smoke.sh @@ -0,0 +1,305 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +PROJECT_NAME="${SMOKE_PROJECT_NAME:-opencashflow-smoke}" +API_PORT="${SMOKE_API_PORT:-15100}" +WEB_PORT="${SMOKE_WEB_PORT:-15200}" +API_URL="http://localhost:${API_PORT}" +WEB_URL="http://localhost:${WEB_PORT}" +ADMIN_EMAIL="${SMOKE_ADMIN_EMAIL:-smoke-admin@example.test}" +ADMIN_PASSWORD="${SMOKE_ADMIN_PASSWORD:-SmokeP@ssw0rd!2026}" +COMPANY_NAME="${SMOKE_COMPANY_NAME:-OpenCashFlow Smoke Company}" +KEEP_STACK="${SMOKE_KEEP_STACK:-0}" +WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/opencashflow-smoke.XXXXXX")" + +COMPOSE_FILES=( + -f "${ROOT_DIR}/scripts/smoke/docker-compose.clean-install.yml" +) + +cleanup() { + local exit_code=$? + if [[ "${KEEP_STACK}" == "1" && "${exit_code}" != "0" ]]; then + echo "Smoke failed. Stack kept for inspection because SMOKE_KEEP_STACK=1." + echo "Collect logs with: docker compose -p ${PROJECT_NAME} ${COMPOSE_FILES[*]} logs" + else + docker compose -p "${PROJECT_NAME}" "${COMPOSE_FILES[@]}" down -v --remove-orphans >/dev/null 2>&1 || true + fi + rm -rf "${WORK_DIR}" +} +trap cleanup EXIT + +compose() { + docker compose -p "${PROJECT_NAME}" "${COMPOSE_FILES[@]}" "$@" +} + +request() { + local method="$1" + local url="$2" + local body="${3:-}" + local output="$4" + local token="${5:-}" + local expected="${6:-200}" + local status + local args=(-fsS -X "${method}" -o "${output}" -w "%{http_code}" -H "Accept: application/json") + + if [[ -n "${token}" ]]; then + args+=(-H "Authorization: Bearer ${token}") + fi + + if [[ -n "${body}" ]]; then + args+=(-H "Content-Type: application/json" --data "${body}") + fi + + status="$(curl "${args[@]}" "${url}")" + if [[ "${status}" != "${expected}" ]]; then + echo "Unexpected HTTP status for ${method} ${url}: expected ${expected}, got ${status}" >&2 + echo "Response body:" >&2 + cat "${output}" >&2 || true + exit 1 + fi +} + +json_get() { + local file="$1" + shift + python3 - "$file" "$@" <<'PY' +import json +import sys + +def lookup(value, path): + current = value + for part in path: + if isinstance(current, list): + current = current[int(part)] + continue + if not isinstance(current, dict): + raise KeyError(part) + match = next((key for key in current.keys() if key.lower() == part.lower()), None) + if match is None: + raise KeyError(part) + current = current[match] + return current + +with open(sys.argv[1], "r", encoding="utf-8") as handle: + data = json.load(handle) + +result = lookup(data, sys.argv[2:]) +if isinstance(result, bool): + print(str(result).lower()) +elif result is None: + print("") +else: + print(result) +PY +} + +json_first_id() { + local file="$1" + local id_key="$2" + local preferred_name_key="${3:-}" + local preferred_name="${4:-}" + python3 - "$file" "$id_key" "$preferred_name_key" "$preferred_name" <<'PY' +import json +import sys + +with open(sys.argv[1], "r", encoding="utf-8") as handle: + data = json.load(handle) + +id_key = sys.argv[2].lower() +preferred_name_key = sys.argv[3].lower() +preferred_name = sys.argv[4].lower() + +if not isinstance(data, list) or not data: + raise SystemExit("Expected a non-empty JSON array") + +def get_case_insensitive(obj, key): + return next((value for current_key, value in obj.items() if current_key.lower() == key), None) + +selected = None +if preferred_name_key and preferred_name: + for item in data: + value = get_case_insensitive(item, preferred_name_key) + if isinstance(value, str) and value.lower() == preferred_name: + selected = item + break + +selected = selected or data[0] +identifier = get_case_insensitive(selected, id_key) +if not identifier: + raise SystemExit(f"Could not find id key {sys.argv[2]}") + +print(identifier) +PY +} + +jwt_claim() { + local token="$1" + local claim="$2" + python3 - "$token" "$claim" <<'PY' +import base64 +import json +import sys + +token = sys.argv[1] +claim = sys.argv[2].lower() +payload = token.split(".")[1] +payload += "=" * (-len(payload) % 4) +data = json.loads(base64.urlsafe_b64decode(payload.encode("ascii"))) +match = next((key for key in data.keys() if key.lower() == claim), None) +if match is None: + raise SystemExit(f"Missing JWT claim {sys.argv[2]}") +print(data[match]) +PY +} + +uuid() { + python3 - <<'PY' +import uuid +print(uuid.uuid4()) +PY +} + +wait_for_url() { + local url="$1" + local label="$2" + local attempts="${3:-60}" + for _ in $(seq 1 "${attempts}"); do + if curl -fsS "${url}" >/dev/null 2>&1; then + echo "${label} is ready" + return 0 + fi + sleep 2 + done + + echo "Timed out waiting for ${label} at ${url}" >&2 + compose logs --tail=200 >&2 || true + exit 1 +} + +cd "${ROOT_DIR}" + +echo "Starting clean OpenCashFlow smoke stack (${PROJECT_NAME})" +compose down -v --remove-orphans >/dev/null 2>&1 || true +compose up -d --build + +wait_for_url "${API_URL}/health" "API health" +wait_for_url "${WEB_URL}/Login" "WebApp login page" + +setup_status="${WORK_DIR}/setup-status.json" +request GET "${API_URL}/v1/Setup/status" "" "${setup_status}" "" 200 +requires_setup="$(json_get "${setup_status}" requiresSetup)" +if [[ "${requires_setup}" != "true" ]]; then + echo "Expected a clean database to require setup. Status response:" >&2 + cat "${setup_status}" >&2 + exit 1 +fi + +setup_body="$(cat < Date: Wed, 8 Jul 2026 10:39:08 +0200 Subject: [PATCH 05/10] docs(ops): add production hardening and recovery drills --- Docs/ops/backup-restore-drill.md | 111 +++++++++++++++++++++ Docs/ops/production-hardening.md | 147 ++++++++++++++++++++++++++++ Docs/ops/secrets-management.md | 104 ++++++++++++++++++++ Docs/ops/upgrade-migration-drill.md | 90 +++++++++++++++++ 4 files changed, 452 insertions(+) create mode 100644 Docs/ops/backup-restore-drill.md create mode 100644 Docs/ops/production-hardening.md create mode 100644 Docs/ops/secrets-management.md create mode 100644 Docs/ops/upgrade-migration-drill.md diff --git a/Docs/ops/backup-restore-drill.md b/Docs/ops/backup-restore-drill.md new file mode 100644 index 0000000..22a8d35 --- /dev/null +++ b/Docs/ops/backup-restore-drill.md @@ -0,0 +1,111 @@ +# Backup And Restore Drill + +This drill verifies that an OpenCashFlow PostgreSQL database can be backed up, restored into a clean database, and used +by the application after restore. + +It is written for local Docker Compose evaluation. Production operators must adapt hostnames, credentials, retention, +encryption, and storage targets to their environment. + +## Local Evaluation Drill + +Prerequisites: + +- Docker and Docker Compose; +- a running local evaluation stack started with `docker compose up --build`; +- an explicitly disposable local database volume, or written approval to test against the current local volume. + +Representative data should include: + +- first company/admin; +- successful login; +- payment; +- cash balance and cash ledger entry. + +## Commands + +Start the local evaluation stack: + +```bash +docker compose up --build +``` + +Create a dump from the local database: + +```bash +mkdir -p reports/ops +docker compose exec -T db pg_dump -U postgres -d opencashflow --format=custom \ + > reports/ops/opencashflow-local.dump +``` + +Create a restore target database inside the same disposable PostgreSQL container: + +```bash +docker compose exec -T db createdb -U postgres opencashflow_restore +``` + +Restore the dump: + +```bash +docker compose exec -T db pg_restore -U postgres -d opencashflow_restore --clean --if-exists \ + < reports/ops/opencashflow-local.dump +``` + +Verify representative restored data: + +```bash +docker compose exec -T db psql -U postgres -d opencashflow_restore \ + -tAc 'select count(*) from "Companies"; select count(*) from "Payments"; select count(*) from "CashLedgers";' +``` + +Expected result after the smoke data load: + +```text +1 +1 +1 +``` + +Clean up: + +```bash +docker compose exec -T db dropdb -U postgres opencashflow_restore +``` + +## Local Result - 2026-07-08 + +Status: **not performed in this branch**. + +Reason: + +- the current `development` branch does not contain a dedicated isolated clean-install smoke stack; +- the root `docker-compose.yml` uses fixed container names and standard host ports, so starting or resetting a second + destructive stack is unsafe on a workstation that may already contain local OpenCashFlow data; +- this branch documents the procedure and intentionally does not run `docker compose down -v` against the standard + local volume. + +Future exact local proof: + +1. Start an explicitly disposable stack or use a dedicated smoke compose file with isolated project name, ports and + volumes. +2. Create representative data: company/admin, login, payment and cash ledger. +3. Run `pg_dump --format=custom`. +4. Restore into `opencashflow_restore`. +5. Verify counts for `Companies`, `Payments` and `CashLedgers`. +6. Destroy only the disposable stack/volume. + +Until this proof is run, backup/restore remains a production-readiness blocker. + +## Production Requirements + +Production backup design must define: + +- backup frequency; +- retention period; +- encryption at rest; +- off-host/off-region storage; +- restore owner; +- restore target environment; +- RPO and RTO; +- alerting when backups fail or become stale. + +At least one restore drill should be performed before any production-ready claim. diff --git a/Docs/ops/production-hardening.md b/Docs/ops/production-hardening.md new file mode 100644 index 0000000..7843c3d --- /dev/null +++ b/Docs/ops/production-hardening.md @@ -0,0 +1,147 @@ +# Production Hardening + +OpenCashFlow is a **Developer Preview / Early Self-Hosted Preview**. This document describes the minimum hardening work +expected before a real production deployment. It is not a production-readiness claim. + +## Local Defaults Versus Production + +The root `docker-compose.yml` is for local evaluation. + +Local defaults include: + +- PostgreSQL user/password `postgres/postgres`; +- a development JWT fallback secret; +- plain HTTP ports; +- `AUTO_MIGRATE=true`; +- empty SMTP settings; +- no external backup target; +- no reverse proxy configuration. + +Production deployments must replace all of those defaults. + +## Minimum Production Requirements + +Before exposing an instance: + +- terminate TLS at a trusted reverse proxy or load balancer; +- use a strong `JWT_SECRET` from a secret manager; +- use externally managed PostgreSQL or a hardened PostgreSQL service; +- disable public database port exposure; +- configure automated backups and restore testing; +- configure SMTP or intentionally disable flows that depend on email delivery; +- set explicit trusted origins and cookie domains; +- retain structured application logs; +- monitor API health, WebApp availability, PostgreSQL health, disk usage, and backup freshness; +- document an upgrade and rollback process. + +## TLS And Reverse Proxy + +OpenCashFlow containers expose HTTP internally. Production TLS should be terminated by a reverse proxy such as nginx, +Caddy, Traefik, a cloud load balancer, or an equivalent ingress controller. + +Production requirements: + +- force HTTPS; +- enable HSTS at the edge; +- forward `X-Forwarded-Proto` and `X-Forwarded-For`; +- restrict allowed hosts; +- keep API and WebApp origins explicit; +- avoid exposing API, WebApp, and PostgreSQL directly to the public internet unless intentionally designed. + +Recommended production shape: + +```text +Internet -> TLS reverse proxy -> WebApp/API containers -> private PostgreSQL +``` + +## JWT Secret Rotation + +`JwtSettings__SecretKey` signs authentication tokens. Treat it as a high-value secret. + +Rotation procedure: + +1. Generate a new high-entropy secret. +2. Store it in the deployment secret manager. +3. Restart API and WebApp with the same new value. +4. Expect existing JWT sessions to fail validation. +5. Force users to log in again. +6. Revoke or delete the old secret from the secret manager. + +Emergency rotation after suspected compromise should happen immediately. + +## Database Credentials + +Production must not use `postgres/postgres`. + +Requirements: + +- use a least-privilege application database role; +- store credentials in a secret manager or runtime secret injection mechanism; +- rotate credentials on a documented schedule; +- avoid committing `.env` files with real values; +- do not expose PostgreSQL on a public interface; +- require TLS to PostgreSQL when the database is outside the private host/network. + +## External PostgreSQL + +For production, prefer external PostgreSQL managed by the operator or platform. + +Recommended settings: + +- point `DEFAULT_CONN_STRING` at the production PostgreSQL endpoint; +- set `AUTO_MIGRATE=false` unless using a deliberately controlled startup migration process; +- run migrations as a separate release step; +- require regular `pg_dump` or storage-level backups; +- monitor replication, disk, connections, and slow queries. + +## AUTO_MIGRATE Guidance + +`AUTO_MIGRATE=true` is useful for local evaluation and disposable smoke tests. + +Production recommendation: + +- set `AUTO_MIGRATE=false`; +- run migrations in a controlled maintenance step; +- back up before applying migrations; +- run migrations against a staging copy first; +- record migration IDs before and after release; +- keep rollback expectations explicit. + +Use automatic startup migrations only when the operator has accepted the risk that application startup may modify the +database schema. + +## Logging And Observability Minimums + +Minimum signals: + +- API `/health`; +- WebApp login page availability; +- PostgreSQL connection health; +- authentication failures; +- password reset events; +- setup completion; +- payment create/update/delete events; +- cash ledger adjustments; +- background migration attempts; +- unhandled exceptions. + +Minimum operational retention: + +- application logs retained long enough for incident triage; +- reverse proxy access logs with sensitive values removed; +- PostgreSQL logs for connection and error events; +- backup job logs; +- restore drill logs. + +Do not log passwords, JWTs, reset tokens, PINs, SMTP credentials, or raw secret values. + +## Production Blockers Remaining + +The project should not be described as production-ready until at least these items are proven: + +- backup and restore drill repeated on the intended production topology; +- upgrade/migration drill repeated against a production-like database copy; +- GitHub dependency alerts fully closed or formally dismissed with evidence; +- browser-level clean-install smoke coverage; +- documented reverse proxy configuration tested end to end; +- security review of auth, fast-login/PIN, reset password, authorization, and tenant isolation. diff --git a/Docs/ops/secrets-management.md b/Docs/ops/secrets-management.md new file mode 100644 index 0000000..104663a --- /dev/null +++ b/Docs/ops/secrets-management.md @@ -0,0 +1,104 @@ +# Secrets Management + +OpenCashFlow local evaluation uses environment variables and `.env.example` templates. Production deployments must use a +real secret-management process. + +Do not commit real `.env` files, passwords, tokens, private URLs, SMTP credentials, or database connection strings. + +## Local Evaluation Defaults + +The root Docker Compose file includes local defaults: + +- PostgreSQL `postgres/postgres`; +- fallback JWT secret; +- empty SMTP values; +- local HTTP origins. + +These values are acceptable only for disposable local evaluation. + +## Required Production Secrets + +At minimum, production operators must manage: + +- `JwtSettings__SecretKey`; +- PostgreSQL username/password or managed database credentials; +- SMTP username/password, if email is enabled; +- reverse proxy TLS private keys or certificate automation credentials; +- any external observability sink credentials; +- backup storage credentials. + +## Storage Requirements + +Use one of: + +- cloud secret manager; +- orchestrator secrets; +- encrypted deployment variables; +- managed platform secrets. + +Avoid: + +- checked-in `.env` files; +- secrets embedded in Docker images; +- secrets in command history; +- secrets in issue reports or logs; +- shared static secrets across environments. + +## JWT Secret Rotation + +Normal rotation: + +1. Generate a new random value with at least 256 bits of entropy. +2. Store it in the secret manager. +3. Restart API and WebApp with the new value. +4. Confirm new logins work. +5. Remove the old value. + +Impact: + +- existing JWTs become invalid; +- users must log in again; +- fast-login flows should be rechecked. + +Emergency rotation after leak: + +1. Rotate immediately. +2. Restart all app nodes. +3. Review logs for suspicious access. +4. Notify affected operators/users according to the incident process. + +## Database Credential Rotation + +Recommended approach: + +1. Create a new database role or password. +2. Grant required permissions. +3. Update secret manager. +4. Restart application services. +5. Verify `/health`. +6. Revoke the old credential. +7. Confirm old credential no longer connects. + +Do not rotate by editing committed files. + +## SMTP Credential Rotation + +SMTP is optional. If enabled: + +1. Rotate credentials at the provider. +2. Update runtime secrets. +3. Restart services. +4. Run password-reset and notification checks. +5. Remove old credentials from the provider. + +## Incident Handling + +If a secret is committed: + +1. Treat it as compromised. +2. Rotate it immediately. +3. Remove it from active configuration. +4. Remove or rewrite repository history only after coordinating with maintainers. +5. Document the incident privately if exploitable details are involved. + +Never post real tokens, reset links, PINs, JWTs, or database credentials in public issues. diff --git a/Docs/ops/upgrade-migration-drill.md b/Docs/ops/upgrade-migration-drill.md new file mode 100644 index 0000000..979ea01 --- /dev/null +++ b/Docs/ops/upgrade-migration-drill.md @@ -0,0 +1,90 @@ +# Upgrade And Migration Drill + +OpenCashFlow uses EF Core migrations in `OpenCashFlow.Infrastructure`. This document defines the minimum migration drill +expected before a production release. + +The project is not production-ready until upgrade and rollback behavior is proven against a production-like database +copy. + +## Production Migration Policy + +Recommended production settings: + +- set `AUTO_MIGRATE=false`; +- take a database backup before applying migrations; +- run migrations as an explicit release step; +- record migration IDs before and after; +- validate application health after migrations; +- keep rollback expectations documented. + +Automatic startup migrations are acceptable for local evaluation and disposable smoke tests, but they are risky in +production because application startup can alter schema. + +## Inspection Commands + +List migrations: + +```bash +dotnet ef migrations list \ + --project src/OpenCashFlow.Infrastructure/OpenCashFlow.Infrastructure.csproj \ + --startup-project src/OpenCashFlow.API/OpenCashFlow.API.csproj +``` + +Record applied migrations from a running database: + +```bash +docker compose exec -T db psql -U postgres -d opencashflow \ + -tAc 'select "MigrationId" from "__EFMigrationsHistory" order by "MigrationId";' +``` + +## Drill Plan + +For each release candidate: + +1. Restore a recent production backup into a staging database. +2. Record current migration IDs. +3. Apply pending migrations with the release build. +4. Start API and WebApp against the migrated copy. +5. Run clean-install or post-upgrade smoke checks. +6. Verify critical flows: + - login; + - setup status; + - payment list/detail; + - payment create/update/delete; + - cash balance/ledger; + - employee/company access; + - audit log write path. +7. Record migration IDs after upgrade. +8. Decide rollback path: + - restore backup for destructive migrations; + - or apply explicit down migration only if it has been tested. + +## Local Result - 2026-07-08 + +Status: **not fully performed in this branch**. + +Not performed: + +- clean install with an isolated disposable smoke stack; +- upgrade from an older released database snapshot; +- rollback test; +- migration against a copied production-like dataset. + +Reason: + +- no stable production release or archived production-like database snapshot exists in the repository; +- the current `development` branch does not contain a dedicated isolated clean-install smoke stack suitable as a + migration drill harness; +- the root Docker Compose file is suitable for local evaluation, but its fixed container names and standard ports make + destructive automated migration drills unsafe on shared developer machines. + +Future exact test plan: + +1. Preserve a database dump from the previous release candidate. +2. Restore it into a disposable PostgreSQL instance. +3. Run the new release with `AUTO_MIGRATE=false`. +4. Apply migrations explicitly. +5. Run the clean-install smoke equivalent against the upgraded data. +6. Restore the pre-upgrade dump to prove rollback by restore. + +Until this drill is performed with a real prior schema/data snapshot, production upgrade readiness remains unproven. From f61525ad22963673d764c1cc1f3d3c346f0416d7 Mon Sep 17 00:00:00 2001 From: Codewriter90x Date: Wed, 8 Jul 2026 11:17:59 +0200 Subject: [PATCH 06/10] docs(ops): reconcile hardening docs after smoke merge --- Docs/ops/backup-restore-drill.md | 22 ++++++++++++---------- Docs/ops/upgrade-migration-drill.md | 7 +++---- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Docs/ops/backup-restore-drill.md b/Docs/ops/backup-restore-drill.md index 22a8d35..adc94ea 100644 --- a/Docs/ops/backup-restore-drill.md +++ b/Docs/ops/backup-restore-drill.md @@ -8,12 +8,15 @@ encryption, and storage targets to their environment. ## Local Evaluation Drill -Prerequisites: +Prerequisites for the root Docker Compose stack: - Docker and Docker Compose; - a running local evaluation stack started with `docker compose up --build`; - an explicitly disposable local database volume, or written approval to test against the current local volume. +For an isolated clean-install smoke stack, see `Docs/testing/clean-install-smoke.md` and +`scripts/smoke/clean-install-smoke.sh`. + Representative data should include: - first company/admin; @@ -73,21 +76,20 @@ docker compose exec -T db dropdb -U postgres opencashflow_restore ## Local Result - 2026-07-08 -Status: **not performed in this branch**. +Status: **not performed as part of the original ops documentation branch**. Reason: -- the current `development` branch does not contain a dedicated isolated clean-install smoke stack; -- the root `docker-compose.yml` uses fixed container names and standard host ports, so starting or resetting a second - destructive stack is unsafe on a workstation that may already contain local OpenCashFlow data; -- this branch documents the procedure and intentionally does not run `docker compose down -v` against the standard - local volume. +- the clean-install smoke stack is now present in `development`, but this backup/restore drill has not yet been + executed against it; +- the root `docker-compose.yml` uses fixed container names and standard host ports, so destructive testing against the + default stack remains unsafe on a workstation that may already contain local OpenCashFlow data; +- the documented procedure intentionally avoids `docker compose down -v` against the standard local volume. Future exact local proof: -1. Start an explicitly disposable stack or use a dedicated smoke compose file with isolated project name, ports and - volumes. -2. Create representative data: company/admin, login, payment and cash ledger. +1. Start the isolated smoke stack with `SMOKE_KEEP_STACK=1 scripts/smoke/clean-install-smoke.sh`. +2. Confirm representative data exists: company/admin, login, payment and cash ledger. 3. Run `pg_dump --format=custom`. 4. Restore into `opencashflow_restore`. 5. Verify counts for `Companies`, `Payments` and `CashLedgers`. diff --git a/Docs/ops/upgrade-migration-drill.md b/Docs/ops/upgrade-migration-drill.md index 979ea01..add68e6 100644 --- a/Docs/ops/upgrade-migration-drill.md +++ b/Docs/ops/upgrade-migration-drill.md @@ -65,7 +65,7 @@ Status: **not fully performed in this branch**. Not performed: -- clean install with an isolated disposable smoke stack; +- upgrade using the isolated clean-install smoke stack as a post-migration verification harness; - upgrade from an older released database snapshot; - rollback test; - migration against a copied production-like dataset. @@ -73,8 +73,7 @@ Not performed: Reason: - no stable production release or archived production-like database snapshot exists in the repository; -- the current `development` branch does not contain a dedicated isolated clean-install smoke stack suitable as a - migration drill harness; +- the isolated clean-install smoke stack proves only a fresh install path, not an upgrade from older schema/data; - the root Docker Compose file is suitable for local evaluation, but its fixed container names and standard ports make destructive automated migration drills unsafe on shared developer machines. @@ -84,7 +83,7 @@ Future exact test plan: 2. Restore it into a disposable PostgreSQL instance. 3. Run the new release with `AUTO_MIGRATE=false`. 4. Apply migrations explicitly. -5. Run the clean-install smoke equivalent against the upgraded data. +5. Run the clean-install smoke checks or equivalent post-upgrade checks against the upgraded data. 6. Restore the pre-upgrade dump to prove rollback by restore. Until this drill is performed with a real prior schema/data snapshot, production upgrade readiness remains unproven. From 2ad5f30b2f3193294eb928da2368fc06937fb6af Mon Sep 17 00:00:00 2001 From: Codewriter90x Date: Thu, 9 Jul 2026 16:54:33 +0200 Subject: [PATCH 07/10] feat(cash): add custody transfer contracts --- Docs/analysis/cash-custody-domain-analysis.md | 1023 ++++++++++++++++ Docs/analysis/multi-cash-account-model.md | 1038 +++++++++++++++++ Docs/product/CASH_CUSTODY_DECISION_RECORD.md | 363 ++++++ .../product/CASH_INTEGRITY_DECISION_RECORD.md | 470 ++++++++ .../Commands/CashIntegrityCommands.cs | 96 ++ .../Integrity/Models/CashIntegrityModels.cs | 109 ++ .../Integrity/Ports/ICashAccountReader.cs | 14 + .../Integrity/Ports/ICashAccountWriter.cs | 12 + .../Ports/ICashIntegrityAuditWriter.cs | 8 + .../Integrity/Ports/ICashMovementReader.cs | 14 + .../Integrity/Ports/ICashMovementWriter.cs | 18 + .../Ports/ICashReconciliationReader.cs | 8 + .../Ports/ICashReconciliationWriter.cs | 11 + .../Integrity/Ports/ICashSessionReader.cs | 14 + .../Integrity/Ports/ICashSessionWriter.cs | 11 + .../Integrity/Ports/ICashTransferReader.cs | 14 + .../Integrity/Ports/ICashTransferWriter.cs | 13 + src/OpenCashFlow.Domain/Cash/CashAccount.cs | 59 + .../Cash/CashAccountType.cs | 11 + .../Cash/CashDiscrepancy.cs | 57 + src/OpenCashFlow.Domain/Cash/CashMovement.cs | 284 +++++ .../Cash/CashMovementDirection.cs | 7 + .../Cash/CashMovementStatus.cs | 9 + .../Cash/CashReconciliation.cs | 84 ++ .../Cash/CashReconciliationStatus.cs | 9 + src/OpenCashFlow.Domain/Cash/CashSession.cs | 73 ++ .../Cash/CashSessionStatus.cs | 8 + src/OpenCashFlow.Domain/Cash/CashTransfer.cs | 390 +++++++ .../Cash/CashTransferSide.cs | 7 + .../Cash/CashTransferStatus.cs | 9 + .../Cash/CashTransferTests.cs | 224 ++++ 31 files changed, 4467 insertions(+) create mode 100644 Docs/analysis/cash-custody-domain-analysis.md create mode 100644 Docs/analysis/multi-cash-account-model.md create mode 100644 Docs/product/CASH_CUSTODY_DECISION_RECORD.md create mode 100644 Docs/product/CASH_INTEGRITY_DECISION_RECORD.md create mode 100644 src/OpenCashFlow.Application/Cash/Integrity/Commands/CashIntegrityCommands.cs create mode 100644 src/OpenCashFlow.Application/Cash/Integrity/Models/CashIntegrityModels.cs create mode 100644 src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashAccountReader.cs create mode 100644 src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashAccountWriter.cs create mode 100644 src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashIntegrityAuditWriter.cs create mode 100644 src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashMovementReader.cs create mode 100644 src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashMovementWriter.cs create mode 100644 src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashReconciliationReader.cs create mode 100644 src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashReconciliationWriter.cs create mode 100644 src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashSessionReader.cs create mode 100644 src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashSessionWriter.cs create mode 100644 src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashTransferReader.cs create mode 100644 src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashTransferWriter.cs create mode 100644 src/OpenCashFlow.Domain/Cash/CashAccount.cs create mode 100644 src/OpenCashFlow.Domain/Cash/CashAccountType.cs create mode 100644 src/OpenCashFlow.Domain/Cash/CashDiscrepancy.cs create mode 100644 src/OpenCashFlow.Domain/Cash/CashMovement.cs create mode 100644 src/OpenCashFlow.Domain/Cash/CashMovementDirection.cs create mode 100644 src/OpenCashFlow.Domain/Cash/CashMovementStatus.cs create mode 100644 src/OpenCashFlow.Domain/Cash/CashReconciliation.cs create mode 100644 src/OpenCashFlow.Domain/Cash/CashReconciliationStatus.cs create mode 100644 src/OpenCashFlow.Domain/Cash/CashSession.cs create mode 100644 src/OpenCashFlow.Domain/Cash/CashSessionStatus.cs create mode 100644 src/OpenCashFlow.Domain/Cash/CashTransfer.cs create mode 100644 src/OpenCashFlow.Domain/Cash/CashTransferSide.cs create mode 100644 src/OpenCashFlow.Domain/Cash/CashTransferStatus.cs create mode 100644 tests/OpenCashFlow.Domain.Tests/Cash/CashTransferTests.cs diff --git a/Docs/analysis/cash-custody-domain-analysis.md b/Docs/analysis/cash-custody-domain-analysis.md new file mode 100644 index 0000000..99d4a48 --- /dev/null +++ b/Docs/analysis/cash-custody-domain-analysis.md @@ -0,0 +1,1023 @@ +# Cash Custody Domain Analysis + +Branch: `analysis/cash-custody-domain` + +Date: 2026-07-08 + +Status: analysis only. No production code, schema, API, UI, or contract changes are implemented by this document. + +## Purpose + +OpenCashFlow is trying to become the self-hosted cash cockpit for small businesses. The current Cash Integrity direction says the system must prove what cash moved, who handled it, why it moved, and whether reality matches the expected balance. + +This document challenges that language. + +The central question is: + +> Is the real business concept cash moving, or responsibility for cash changing hands? + +The answer matters before more code is written. If OpenCashFlow models only movement, it can become a better ledger. If it models custody, it can become the system that proves who was responsible for company money at every point in time. + +## Inputs Reviewed + +Primary inputs: + +- `Docs/product/CASH_INTEGRITY_DECISION_RECORD.md` +- `Docs/analysis/cash-integrity-foundation-analysis.md` +- `Docs/analysis/multi-cash-account-model.md` +- current runtime cash model on `development` +- proposed Cash Integrity domain/application contracts on `feature/cash-integrity-domain-contracts` + +Important repository state: + +- `development` is currently at `f61525a`. +- The Cash Integrity contract branch exists locally and remotely. +- The Cash Integrity contracts are not part of local `development` at the time of this analysis, so they are treated as proposed baseline, not current runtime behavior. + +## Executive Summary + +Cash Movement is not wrong, but it is not the deepest domain concept. + +The deeper concept is Cash Custody: + +> A named party or source becomes responsible for company money until that responsibility is transferred, spent, deposited, returned, reconciled, corrected, or closed. + +Recommended decision: + +- keep `CashMovement` as the event/ledger-level fact; +- introduce `CashCustody` as the product pillar and domain language; +- treat `CashCustodyChain` or `CashCustodyRecord` as the conceptual aggregate around which movement, transfer, reconciliation, discrepancy, and audit make sense; +- model transfers as custody transfers, implemented by linked movement entries when persistence is designed; +- make reconciliation a proof step in a custody chain, not just a balance comparison. + +Business reasoning: + +Small business owners do not wake up asking, "How many movements did I record?" They ask: + +- Who has the cash? +- Why do they have it? +- Should they still have it? +- Did they bring it back? +- Is the cash box short? +- Can I prove what happened? + +That is custody language. + +## Current Model + +### Runtime Model On `development` + +The current runtime cash model is company-level: + +- `CashBalance` + - one balance per company; + - stores current balance only. + +- `CashLedger` + - records deltas against a company; + - references payment/admin adjustment events; + - does not identify a cash account; + - does not identify a physical cash handler; + - does not model responsibility transfer; + - does not model session/reconciliation/discrepancy. + +This is a ledger foundation, not a custody foundation. + +### Proposed Cash Integrity Contracts + +The proposed Cash Integrity branch introduces: + +- `CashAccount` +- `CashMovement` +- `CashSession` +- `CashReconciliation` +- `CashDiscrepancy` +- movement commands and read/write ports + +The proposed model improves the system materially: + +- cash is source-bound through `CashAccountId`; +- cash has direction through `CashMovementDirection`; +- physical handling can require `EmployeeCashHandlerId`; +- entered/approved users are separate fields; +- sessions and reconciliations become explicit; +- posted movements become immutable through reversal/correction. + +However, the proposed model is still primarily movement-centered. It can answer "what happened?" but it does not make "who is currently responsible?" the first-class question. + +## Proposed Model + +The product should evolve from: + +> Cash Integrity = prove cash movements and balances. + +to: + +> Cash Custody = prove responsibility for company money across its lifecycle. + +Cash Integrity remains a capability. Cash Custody becomes the stronger domain pillar. + +Recommended conceptual model: + +- Cash Account: where company-controlled cash is held or reported. +- Custody Holder: who or what currently has responsibility for the money. +- Custody Event: an auditable fact that changes or proves custody. +- Custody Transfer: an event that moves responsibility from one holder to another. +- Cash Movement: the accounting/ledger consequence of a custody event. +- Cash Session: a time-boxed proof window for one account/source. +- Cash Reconciliation: the proof that expected custody matches actual custody. +- Cash Discrepancy: evidence that the custody chain is broken or incomplete. + +The model should not discard movements. It should subordinate them to custody. + +## Aggregate Root + +Candidate aggregate roots: + +- `CashMovement` +- `CashTransfer` +- `CashCustody` +- `CashSession` +- `CashLedger` + +### CashMovement + +Strengths: + +- simple; +- maps well to ledger rows; +- supports inflow/outflow; +- good for expected balance calculation; +- good persistence boundary for append-only records. + +Weaknesses: + +- too event-specific; +- does not naturally answer who currently has responsibility; +- a transfer is awkward because it requires coordination across two movements; +- a reconciliation is only indirectly related; +- can become a technical ledger concept rather than business language. + +Verdict: useful entity/event, not the best aggregate root for the business problem. + +### CashTransfer + +Strengths: + +- captures movement between two holders/sources; +- naturally links source and destination; +- fits employee float and bank deposit scenarios. + +Weaknesses: + +- not all cash events are transfers; +- customer receipts, supplier payments, owner deposits, adjustments and discrepancies are broader than transfer; +- transfer is a subtype of custody change, not the whole domain. + +Verdict: important aggregate or command boundary for transfer operations, but not the top-level model. + +### CashCustody + +Strengths: + +- directly represents responsibility for company money; +- covers receipt, holding, transfer, spend, return, deposit, reconciliation and discrepancy; +- matches owner concerns; +- gives strong language for audit and proof; +- naturally supports "every euro must have a current responsible holder"; +- can include movement as a consequence rather than confusing movement with responsibility. + +Weaknesses: + +- more abstract than movement; +- must be carefully scoped to avoid over-modeling every individual banknote; +- needs clear implementation rules to avoid a complex event-sourcing system too early. + +Verdict: best business aggregate/root concept. It should be introduced as the conceptual aggregate and product pillar. + +### CashSession + +Strengths: + +- strong boundary for opening/closing/reconciliation; +- useful for daily operations; +- aligns with the Monday/evening cash routine. + +Weaknesses: + +- custody can cross session boundaries; +- an employee float can remain open for more than one day; +- a session proves custody for a period, but does not own the full custody lifecycle. + +Verdict: session is a proof window, not the root concept. + +### CashLedger + +Strengths: + +- append-only; +- deterministic balance calculation; +- familiar audit/accounting shape. + +Weaknesses: + +- technical artifact; +- weak product language; +- does not answer responsibility without additional semantics. + +Verdict: ledger is an implementation/projection, not a domain aggregate root. + +### Aggregate Root Decision + +The real aggregate root should be: + +> Cash Custody + +More precisely, implementation may use records such as `CashCustodyChain`, `CashCustodyEvent`, and `CashCustodyTransfer`, but the domain root should be the custody chain for company money within a tenant/account context. + +Pragmatic implementation guidance: + +- Do not model every euro as a separate object in MVP. +- Model custody at the transaction/batch level. +- A custody chain starts when company money enters a controlled cash source or is assigned to a holder. +- The chain advances through custody events. +- The chain closes when the money is spent, deposited, returned, reconciled into a session, or otherwise resolved. + +## Who Owns Money? + +Strictly, the company owns company money. What changes is custody/responsibility. + +Recommended language: + +- Owner: the company/legal entity. +- Custodian: the party/source currently responsible for the money. +- Handler: the person physically receiving or carrying cash. +- Recorder: the user who entered the event. +- Approver: the user who authorized the event. +- Reconciler: the user who verified expected vs actual cash. + +The system should avoid saying "employee owns the cash." The employee holds custody or responsibility. + +## Complete Custody Chain + +A small-business custody chain can look like this: + +1. Customer pays 200 EUR. +2. Employee X receives the cash. +3. Employee X records or reports the receipt. +4. The company cash account expected balance increases. +5. Employee X deposits the cash into the main cash box. +6. A manager reconciles the main cash box. +7. The cash is later deposited into a bank account. +8. The bank confirms the deposit. +9. The chain is closed for physical custody and continues as bank custody. + +Another chain: + +1. Main cash box assigns 300 EUR to Employee Z as float. +2. Employee Z holds custody. +3. Employee Z spends 100 EUR on `acquisto F`. +4. Employee Z returns 200 EUR. +5. The float is reconciled. +6. The discrepancy is zero, so the chain is proven. + +Broken chain: + +1. Main cash box assigns 300 EUR to Employee Z. +2. Employee Z reports 100 EUR spent. +3. Employee Z returns 150 EUR. +4. Expected return is 200 EUR. +5. Actual return is 150 EUR. +6. Discrepancy is -50 EUR. +7. The custody chain remains open/discrepant until explained or resolved. + +## Money Moving Or Responsibility Moving? + +The product should model responsibility moving first. + +Money movement is the visible surface: + +- inflow; +- outflow; +- transfer; +- reversal; +- correction. + +Responsibility movement is the business truth: + +- who had company money before; +- who has it now; +- why responsibility changed; +- who authorized that change; +- whether responsibility was later proven by reconciliation. + +This distinction is crucial. + +When money moves from the main cash box to an employee float, the company still owns the money. The meaningful business event is not only "outflow from account A, inflow to account B." It is: + +> responsibility moved from the main cash custodian to Employee Z. + +## Chain Of Custody Questions + +Every custody record should answer: + +- Where did the money come from? +- Who or what had responsibility before? +- Who or what has responsibility now? +- Why did responsibility change? +- When did it happen? +- Who physically handled it? +- Who recorded it? +- Who authorized it, if approval was required? +- Which cash account/source changed? +- Was it reconciled? +- Is there a discrepancy? +- Can the chain be proven without mutating history? + +If any of these are unanswerable, OpenCashFlow cannot honestly claim cash custody. + +## Concept Relationships + +### Cash Account + +A company-controlled source or container of cash value. + +Examples: + +- main cash box; +- workshop cash box; +- petty cash; +- employee float; +- POS transit; +- bank account. + +Relationship: + +- a Cash Account can be a Custody Holder; +- a Cash Account can have sessions; +- a Cash Account has expected balances derived from custody events/movements; +- a Cash Account can be reconciled independently. + +### Cash Custodian + +The person or organizational actor currently responsible for money. + +Examples: + +- Employee X; +- Employee Z; +- manager; +- external bank as institutional custodian; +- supplier/customer only at boundary moments. + +Relationship: + +- a custodian can be a person or institutional holder; +- not every custodian is a user; +- not every user is a custodian. + +### Cash Handler + +The person physically touching or carrying cash. + +Relationship: + +- often an employee; +- may differ from entered-by; +- required when physical cash is handled; +- is evidence in the custody chain. + +### Entered By + +The authenticated user who records the event. + +Relationship: + +- accountable for data entry; +- may be the same as handler, but must not be assumed. + +### Approved By + +The user who authorizes a custody event. + +Relationship: + +- optional for low-risk ordinary entry in MVP; +- required for reversal, correction, reconciliation close, discrepancy resolution, and high-risk transfers. + +### Reconciled By + +The user who verifies expected vs actual custody. + +Relationship: + +- should be distinct in permission model from ordinary movement entry; +- proves or challenges custody. + +### Cash Movement + +The event-level ledger fact that changes expected balance for one account. + +Relationship: + +- generated by or attached to a custody event; +- affects exactly one Cash Account; +- should be append-only after posting; +- remains necessary for balance derivation. + +### Cash Transfer + +A business operation where custody moves between two holders/accounts. + +Relationship: + +- conceptually one custody transfer; +- operationally produces linked source/destination movement facts; +- must be atomic. + +### Cash Session + +A proof window for one account/source over a business date or operating period. + +Relationship: + +- contains opening expected/actual state; +- includes posted movements/custody events; +- closes with reconciliation. + +### Cash Ledger + +An append-only projection of posted custody events/movements. + +Relationship: + +- should support balance calculation; +- should not be the user-facing domain language; +- should never be the only evidence of custody. + +### Cash Reconciliation + +The act of comparing expected custody with actual counted/reported custody. + +Relationship: + +- proves custody when balanced; +- creates or references discrepancy when not balanced; +- is per account/session. + +### Cash Discrepancy + +Evidence that the custody chain does not fully explain reality. + +Relationship: + +- discrepancy is not just a math difference; +- it is a broken or incomplete custody proof; +- it remains visible until explained/resolved. + +## Is Transfer Two Movements Or Custody Transfer? + +Business answer: + +> A transfer is a transfer of custody. + +Implementation answer: + +> A transfer should produce two linked movements. + +This distinction should be explicit. + +Example: + +- Transfer 300 EUR from Main Cash Box to Employee Float. + +Domain event: + +- Custody transferred from `Main Cash Box` to `Employee Z`. + +Ledger consequences: + +- Main Cash Box: outflow 300. +- Employee Float: inflow 300. + +Company-level consequence: + +- net zero. + +Risk if modeled as only two movements: + +- one side can be missing; +- the business intent is lost; +- reversal is ambiguous; +- audit trail does not prove custody transfer. + +Risk if modeled as only one abstract transfer: + +- per-account balances become harder to derive; +- reconciliation by account becomes weaker; +- reporting needs projections anyway. + +Recommended model: + +- `CashCustodyTransfer` or `CashTransfer` is the business object; +- it owns/links two posted movement facts; +- both sides share a `TransferId`; +- creation is atomic; +- reversal creates an inverse linked transfer, not deletion. + +## Can Every Movement Be Expressed As Custody Change? + +Most cash events can be expressed as custody changes. + +Examples: + +- Customer payment received: custody moves from customer to company-controlled holder. +- Supplier payment: custody moves from company-controlled holder to supplier. +- Employee float assigned: custody moves from main cash box to employee. +- Employee returns cash: custody moves from employee to main cash box. +- Bank deposit: custody moves from physical cash holder to bank. +- Owner deposit: custody moves from owner to company account. +- Owner withdrawal: custody moves from company account to owner. +- Correction: custody proof is amended by reversal plus replacement. +- Reconciliation: custody is proven, disputed, explained, or resolved. + +Important nuance: + +Some accounting-like events do not represent physical custody transfer. Bank import corrections, fees, or settlement adjustments may update an account without a human handler. They are still custody events because responsibility or proof state changes, but they may not require `CashHandler`. + +## Ubiquitous Language + +Recommended product language: + +| Current/technical term | Recommended domain term | Decision | +| --- | --- | --- | +| Cash Integrity | Cash Custody | Use Cash Custody as long-term pillar; keep Cash Integrity as capability. | +| CashMovement | Custody Event / Cash Movement | Keep `CashMovement` for ledger/event implementation; use Custody Event in product language when responsibility changes. | +| CashTransfer | Custody Transfer | Prefer Custody Transfer in product language; `CashTransfer` is acceptable in code if clearer for developers. | +| EmployeeCashHandler | Cash Handler | Keep employee-specific field where applicable; product term is Cash Handler. | +| EnteredBy | Recorded By | Prefer Recorded By in UI/product copy. | +| ApprovedBy | Approved By | Keep. | +| ReconciledBy | Reconciled By | Keep. | +| CashAccount | Cash Source / Cash Account | Product can say Cash Source; domain can keep Cash Account. | +| CashLedger | Custody Ledger / Cash Ledger | Ledger is a projection; not primary user-facing language. | +| Discrepancy | Custody Gap / Discrepancy | Keep Discrepancy; optionally explain as a custody gap. | + +Naming decision: + +- Use `Cash Custody` for positioning and product pillar. +- Use `Cash Account` in domain/implementation because it is clear and concrete. +- Use `Custody Holder` for any person/account/institution currently responsible. +- Use `Custody Event` for the generic event family. +- Use `Cash Movement` for balance-affecting posted facts. +- Use `Custody Transfer` for responsibility transfer between holders. + +Avoid: + +- `CashPossession`: too physical; bank and POS transit are not possession. +- `CashResponsible`: awkward noun. +- `CashHolder`: useful but less precise than Custody Holder. +- `MoneyOwner`: wrong for employees; the company owns the money. + +## Lifecycle + +Recommended custody lifecycle: + +1. `Draft` + - Event is being recorded. + - No expected balance effect. + +2. `Posted` + - Custody event is accepted. + - Expected balance/custody state changes. + - Event becomes immutable. + +3. `InCustody` + - A holder/account is currently responsible for money. + - This can be an implied state derived from posted events. + +4. `Transferred` + - Responsibility moved to another holder/account. + - Original custody obligation is reduced or closed. + +5. `Spent` + - Money left company custody for a business reason. + +6. `Returned` + - Money returned from employee/external temporary custody to a company cash account. + +7. `Reconciled` + - Expected custody matched actual custody. + +8. `Discrepant` + - Expected custody did not match actual custody. + - Chain remains open/problematic. + +9. `Explained` + - Difference has an explanation but may not be financially corrected. + +10. `Resolved` + - Difference has been corrected, accepted, or otherwise closed under policy. + +11. `Archived` + - Historical chain is closed for operational purposes and remains auditable. + +Implementation note: + +Do not force all these as status values on one entity in MVP. Some are event types or derived states. For example, `InCustody` may be derived from current open obligations rather than stored directly. + +## Invariants + +Core custody invariants: + +1. Every custody event is tenant-scoped. + +2. Every posted custody event has a business reason/category. + +3. Every posted custody event identifies the company cash source or custody holder affected. + +4. Physical cash handling requires a cash handler. + +5. Recorded By is always required. + +6. Approval is required for reversal, correction, reconciliation close, and discrepancy resolution. + +7. Posted custody events cannot be edited or deleted. + +8. Corrections use reversal plus replacement. + +9. Responsibility cannot disappear silently. + +10. Cash cannot become orphaned. + +11. A custody transfer has exactly one source holder and one destination holder. + +12. Transfer source and destination cannot be the same. + +13. Transfer source and destination must be in the same tenant. + +14. Same-currency transfers must net to zero at company level. + +15. Every transfer side must be linked by one transfer id. + +16. Reversal of a transfer reverses both sides. + +17. Account/session expected balance is derived from posted movement facts. + +18. Reconciliation is per cash account/source and business session. + +19. Balanced reconciliation requires actual minus expected equals zero. + +20. Discrepancy is a broken or incomplete custody chain and must remain visible until explained or resolved. + +21. Historical custody records remain visible after account deactivation or employee status changes. + +22. Tenant A cannot see or alter Tenant B custody records. + +23. Unauthorized users cannot create, transfer, reverse, correct, reconcile, or resolve custody. + +## Domain Model + +Recommended conceptual model: + +```text +Company/Tenant + owns many Cash Accounts + owns many Custody Holders + +Cash Account + is a Custody Holder + has sessions + has posted movement facts + has expected balance projections + +Custody Holder + can be Cash Account + can be Employee + can be Bank + can be Customer/Supplier boundary actor + +Custody Event + belongs to Tenant + has reason/category + has recorded-by + may have approved-by + may have physical handler + changes custody state + may create one or more Cash Movements + +Custody Transfer + is a Custody Event + has source holder + has destination holder + has amount/currency + creates linked source/destination movements + +Cash Session + proves one Cash Account over a business period + calculates expected balance + closes through reconciliation + +Cash Reconciliation + compares expected vs actual + proves or challenges custody + may create discrepancy + +Cash Discrepancy + marks unresolved custody gap + requires explanation/resolution workflow +``` + +Potential implementation records later: + +- `CashCustodyHolder` +- `CashCustodyEvent` +- `CashCustodyTransfer` +- `CashCustodyObligation` +- `CashCustodyChain` + +MVP can avoid all of those names in code if that creates churn. It must still preserve the semantics. + +## Migration Impact + +### From Current Runtime Model + +Current `CashBalance` and `CashLedger` are balance-centric. + +Migration direction: + +1. Create default Cash Account per company. +2. Link historical `CashLedger` entries to the default account. +3. Treat existing ledger rows as historical movement facts with limited custody semantics. +4. Mark historical rows as `LegacyImported` or equivalent in migration documentation/projections. +5. Do not claim old rows can prove handler/approver/custody if the data was never captured. + +### From Proposed Cash Integrity Contracts + +The proposed contracts need conceptual adjustment, but not wholesale rejection. + +Recommended adjustments: + +- keep `CashAccount`; +- keep `CashMovement`; +- keep `CashSession`; +- keep `CashReconciliation`; +- keep `CashDiscrepancy`; +- add transfer concept before persistence; +- add custody language to documentation and API semantics; +- add holder/source/destination vocabulary; +- clarify `EmployeeCashHandlerId` is not the same as current custodian in every case; +- consider adding `CustodyHolderType` and holder reference later; +- treat `CashMovement` as one event fact within a broader custody chain. + +## Compatibility Impact + +Public product language should evolve carefully: + +- Do not abruptly remove Cash Integrity references. +- Introduce Cash Custody as the stronger umbrella. +- Explain Cash Integrity as the measurable capability of the Cash Custody model. + +Suggested wording: + +> Cash Custody is the chain of responsibility for company money. Cash Integrity is the proof that the chain is complete, reconciled, and explainable. + +API compatibility: + +- existing payment/cash endpoints should not be broken by analysis; +- future endpoints can use `/cash/accounts`, `/cash/movements`, `/cash/transfers`, `/cash/reconciliations`; +- product docs can describe these as custody workflows even if endpoint names stay cash-oriented. + +Data compatibility: + +- old company-level balance should be migrated into default account; +- old ledger rows should remain auditable but labeled as limited legacy evidence; +- no silent rewriting of history. + +## Product Impact + +### Cash Integrity + +Cash Integrity becomes the proof layer of Cash Custody. + +It answers: + +- Does expected match actual? +- Can every discrepancy be explained? +- Are movements immutable and auditable? + +### Safe-to-Pay + +Safe-to-Pay becomes stronger if based on custody: + +- money in employee float is not equally safe as money in bank; +- unreconciled cash should reduce confidence; +- open discrepancies should affect safe cash; +- custody uncertainty becomes an explicit risk signal. + +### Forecast + +Forecast should separate: + +- expected future cash events; +- actual custody state; +- confidence level based on reconciliation quality. + +Without custody, forecast can look precise but be operationally untrusted. + +### Cash Visibility + +Cash visibility improves from "balance by account" to: + +- balance by account; +- cash currently held by employees; +- unreconciled sessions; +- open discrepancies; +- transfers in transit; +- custody obligations due back. + +### Weekly Review + +The weekly review should show: + +- cash sources; +- unreconciled custody; +- employee-held cash; +- open discrepancies; +- upcoming commitments; +- safe cash confidence. + +### Owner Dashboard + +The dashboard should not only show "cash balance." It should show: + +- cash available; +- cash committed; +- cash held by employees; +- cash in transit; +- cash not yet reconciled; +- discrepancies requiring action. + +## Risks + +### Over-Modeling Risk + +Custody can become too complex if the system tries to track every euro as an object. + +Mitigation: + +- track custody at transaction/batch level; +- keep MVP focused on operational cash proof; +- avoid accounting-grade subledger ambitions. + +### Naming Churn Risk + +Renaming all code from Cash Movement to Cash Custody too early would create churn without product value. + +Mitigation: + +- change product language first; +- add custody concepts only where they clarify behavior; +- keep stable technical names where they remain accurate. + +### ERP Drift Risk + +Custody could invite broad inventory/procurement/accounting workflows. + +Mitigation: + +- model responsibility for money only; +- reject stock, payroll, CRM, full accounts payable, and full bank accounting workflows. + +### Migration Evidence Risk + +Historical cash ledger rows lack handler/source/approver data. + +Mitigation: + +- preserve them as legacy ledger evidence; +- do not backfill fake custody facts; +- start full custody proof from migration date. + +### User Friction Risk + +Custody workflows can become too bureaucratic for small businesses. + +Mitigation: + +- ordinary entry remains fast; +- stronger proof is required at transfer, correction, reconciliation and discrepancy resolution; +- UI emphasizes "who has it and why" rather than compliance jargon. + +## Recommended Implementation Order + +Do not start with database tables. + +Recommended sequence: + +1. Product language update + - update decision record and product principles to introduce Cash Custody as umbrella language. + +2. Contract adjustment analysis + - add transfer/custody holder concepts to the planned Cash Integrity contracts before persistence. + +3. Domain contracts + - add `CashTransfer` or `CashCustodyTransfer`; + - add holder/source/destination semantics; + - clarify handler vs custodian vs recorded-by. + +4. Use cases + - create custody movement; + - transfer custody; + - reverse/correct custody event; + - open/close/reconcile account session; + - explain discrepancy. + +5. Persistence schema + - cash accounts; + - movements; + - transfer links; + - sessions; + - reconciliations; + - discrepancies; + - audit. + +6. API contracts + - account list/detail; + - movement entry; + - transfer; + - reconciliation; + - discrepancy. + +7. WebApp workflows + - owner cash overview; + - employee custody entry; + - transfer between accounts/holders; + - daily reconciliation; + - discrepancy action queue. + +8. Migration + - create default account per company; + - link legacy ledger to default account; + - preserve legacy limitations. + +9. Safe-to-Pay integration + - treat unreconciled custody and discrepancies as confidence reducers. + +## Should Cash Integrity Become Cash Custody? + +Decision: + +Yes, at the product/domain level. + +Cash Integrity should evolve into Cash Custody as the broader product pillar. + +Precise relationship: + +- Cash Custody is the business domain: who is responsible for company money and how responsibility changes. +- Cash Integrity is the proof quality: whether the custody chain is complete, immutable, reconciled, and explainable. + +Why Cash Custody is stronger: + +- it speaks to business risk, not just data correctness; +- it captures human responsibility; +- it explains employee floats, cash boxes, bank deposits, transfers and discrepancies in one language; +- it supports emotional value for owners: "I know where the money is and who is responsible"; +- it creates a sharper identity than generic cash ledger/reporting. + +Why not replace all technical names immediately: + +- `CashMovement` remains useful for balance-affecting facts; +- `CashAccount` remains clear; +- `CashSession` and `CashReconciliation` remain clear; +- a full rename would add churn before behavior exists. + +Recommendation: + +Adopt Cash Custody in product documents and future design. Keep Cash Movement as a lower-level concept inside the custody model. + +## If OpenCashFlow Disappeared Tomorrow + +Would "Cash Custody" be a stronger long-term identity than "Cash Integrity"? + +Yes. + +Business reasoning: + +Cash Integrity sounds like an internal quality promise. It says the data is correct. That matters, but it does not immediately describe the owner's pain. + +Cash Custody describes the lived problem: + +- employees receive money; +- cash moves through boxes, floats, banks and suppliers; +- owners worry about missing cash; +- managers need to know who is responsible; +- discrepancies create stress and conflict; +- weekly cash decisions depend on trust. + +If OpenCashFlow became known for one thing, "it proves who has the money and why" is more memorable than "it has accurate cash movements." The first is a business outcome. The second is a system property. + +The strongest long-term identity is: + +> OpenCashFlow is the cash cockpit that proves custody of company money from receipt to reconciliation. + +That identity is narrower than ERP, stronger than generic cash tracking, and directly aligned with the weekly cash ritual. diff --git a/Docs/analysis/multi-cash-account-model.md b/Docs/analysis/multi-cash-account-model.md new file mode 100644 index 0000000..d5af121 --- /dev/null +++ b/Docs/analysis/multi-cash-account-model.md @@ -0,0 +1,1038 @@ +# Multi-Cash-Account Model Analysis + +Branch: `analysis/multi-cash-account-model` + +Date: 2026-07-08 + +## Scope + +This is an analysis and design document only. It does not implement schema, migrations, API endpoints, WebApp screens, or production code. + +Important local context: + +- `development` was pulled and was up to date with `origin/development`. +- The requested prerequisite commit from `feature/cash-integrity-domain-contracts` was not an ancestor of local `development` at the time of this analysis. +- The current Cash Integrity domain/application contracts were inspected from `feature/cash-integrity-domain-contracts` as the intended next baseline. + +## Goal + +OpenCashFlow must support more than one cash source per company. + +Examples: + +- main office cash box; +- workshop cash box; +- employee float; +- bank account; +- POS/transit account; +- petty cash; +- cash temporarily held by an employee. + +The product must prove: + +- where money came from; +- where money went; +- who handled it; +- which cash account/source changed; +- expected balance per cash account; +- actual counted balance per cash account; +- discrepancies per cash account; +- transfers between cash accounts. + +## Current State + +### Production State On `development` + +The current runtime model is still company-level: + +- `CashBalance` + - key: `CompanyId`; + - fields: `Balance`, `LastUpdatedUtc`, `RowVersion`; + - one cash balance per company. + +- `CashLedger` + - fields: `CompanyId`, `RefType`, `RefId`, `OriginalPaymentId`, `Delta`, `Reason`, `CreatedBy`, `CreatedAtUtc`; + - no `CashAccountId`; + - no transfer group; + - no session or reconciliation link. + +`CashWriter` applies deltas to the company balance and inserts ledger entries. `CashReader` reads the company balance and company ledger. Current payment cash handling updates this single company-level balance. + +### Intended Cash Integrity Contracts + +The domain/application contract branch introduces: + +- `CashAccount`; +- `CashMovement`; +- `CashSession`; +- `CashReconciliation`; +- `CashDiscrepancy`; +- `CashAccountType`; +- `CashMovementDirection`; +- `CashMovementStatus`; +- `CashSessionStatus`; +- `CashReconciliationStatus`; +- Application commands/results/ports under `Application/Cash/Integrity`. + +These contracts already point in the right direction: + +- `CashAccount` has `TenantId`, `Name`, `Type`, `Currency`, `IsDefault`, `IsActive`; +- `CashMovement` affects exactly one `CashAccountId`; +- `CashSession` is per `CashAccountId` and business date; +- `CashReconciliation` is per session/account; +- `CashDiscrepancy` is linked to a reconciliation. + +The main missing concept is transfer: a business object that coordinates two opposite movements across two cash accounts. + +## Decisions + +## 1. Can One Company Have Many Cash Accounts? + +Decision: yes. + +One tenant/company can have many Cash Accounts. + +Rationale: + +A real small business may hold money in several operational places. A company-level balance hides operational risk. Safe-to-Pay needs source-level visibility because cash in a bank account, physical cash box, POS transit account, and employee float do not have the same availability or confidence. + +## 2. Is There Exactly One Default Cash Account? + +Decision: yes, exactly one active default account per company and currency. + +MVP simplification: + +- one company currency, initially `EUR`; +- therefore one active default account per company. + +Later multi-currency: + +- one active default account per company/currency. + +Enforcement: + +- domain can express `IsDefault`; +- application/infrastructure must enforce uniqueness because it requires querying existing accounts; +- database should add a filtered unique index for active default account per tenant/currency when schema is implemented. + +## 3. Which Cash Account Types Are Supported? + +Decision: + +Supported product types: + +- `PhysicalCash`; +- `Bank`; +- `EmployeeFloat`; +- `POS/transit`; +- `PettyCash`; +- `Other`. + +Current contract adjustment needed: + +- `CashAccountType` currently includes `PhysicalCash`, `Bank`, `EmployeeFloat`, `Other`; +- add `PosTransit` and `PettyCash` before persistence implementation, or map both to `Other` in MVP and document the limitation. + +Recommendation: + +Add explicit `PosTransit` and `PettyCash` enum values before schema generation. They are not ERP drift; they are cash source semantics. + +## 4. Can Cash Accounts Be Deactivated? + +Decision: yes. + +Deactivation means: + +- account no longer accepts ordinary new movements; +- account remains visible historically; +- account remains available in reports, audit, reconciliations and old movements; +- account may still receive system correction/reversal movements needed to preserve integrity. + +## 5. Can Cash Accounts Be Deleted? + +Decision: no physical delete after creation if any movement/session/reconciliation exists. + +MVP behavior: + +- account with no history may be deleted only if product needs it, but deletion is not required for MVP; +- account with history can only be deactivated. + +Recommendation: + +Avoid delete endpoints in the first implementation. Use deactivate only. + +## 6. Can Cash Accounts Have Opening Balances? + +Decision: yes, but opening balance belongs to the first Cash Session or initialization movement, not only to the account row. + +Recommended model: + +- `CashAccount` stores identity/configuration; +- opening cash for a day is recorded in `CashSession`; +- initial migration/import creates an opening session or initialization movement for the default account. + +Rationale: + +Opening balances are historical facts. If stored only as mutable account properties, they are hard to audit. + +## 7. Can Cash Accounts Have Currencies? + +Decision: yes, Cash Account has currency. + +MVP: + +- enforce single tenant/company currency, likely `EUR`; +- transfers are allowed only between accounts with the same currency. + +Excluded from MVP: + +- FX conversion; +- multi-currency Safe-to-Pay; +- exchange gains/losses; +- transfer with currency conversion. + +## 8. Can Cash Accounts Be Reconciled Independently? + +Decision: yes. + +Reconciliation is per: + +- tenant; +- cash account; +- business date/session. + +A discrepancy in `Main Cash Box` must not make `Workshop Cash Box` discrepant. + +## 9. Can A Daily Session Exist Per Cash Account? + +Decision: yes. + +There can be one active/open session per tenant, cash account and business date. + +This allows: + +- main cash box reconciled daily; +- employee float reconciled when returned; +- bank/POS source reconciled when imported or settled; +- independent discrepancy lifecycle. + +## 10. Can One User Access Only Specific Cash Accounts? + +Decision: + +Not in MVP, but the model should not block it. + +MVP: + +- tenant-level role permissions decide access; +- `CompanyAdmin` / `InstanceAdmin` can manage/reconcile; +- authorized cash operators can enter movements if introduced. + +Later: + +- per-account access policy for employee floats, branch cash boxes or sensitive bank accounts. + +## Transfer Model + +Transfers are critical because they move value between cash sources without changing company-level cash. + +Examples: + +- move 300 EUR from main cash box to employee float; +- move 1,000 EUR from physical cash to bank deposit; +- move 200 EUR from employee float back to main cash box. + +## Transfer Decision + +Decision: + +A transfer is one business object that creates two linked cash movements. + +Implementation concept: + +- `CashTransfer` + - `CashTransferId`; + - tenant; + - source cash account; + - destination cash account; + - amount; + - currency; + - reason/category; + - entered by; + - employee cash handler, when physical custody is involved; + - occurred/posting timestamps; + - status; + - reversal/correction group. + +Posting a transfer creates: + +- source movement: + - account = source; + - direction = outflow; + - amount = transfer amount; + - `TransferId = CashTransferId`; + +- destination movement: + - account = destination; + - direction = inflow; + - amount = transfer amount; + - `TransferId = CashTransferId`. + +The pair must be committed atomically. + +## Why One Transfer Object Plus Two Movements? + +Rejected option: model transfer as one movement with source and destination fields. + +Reason rejected: + +- every movement invariant says one movement affects exactly one cash account; +- account balances and sessions are easier when each movement belongs to exactly one account; +- reconciliation per account needs source and destination to appear independently. + +Rejected option: model transfer as two unrelated movements. + +Reason rejected: + +- audit cannot prove the two sides belong together; +- reversal could accidentally reverse only one side; +- users cannot understand transfer lifecycle. + +Accepted option: + +- one transfer aggregate; +- two linked movements; +- one transaction boundary. + +## Transfer Invariants + +- transfer belongs to exactly one tenant; +- source and destination accounts belong to the same tenant; +- source and destination cannot be the same account; +- source and destination must use the same currency in MVP; +- amount must be greater than zero; +- transfer creates exactly two posted movements; +- source side is an outflow; +- destination side is an inflow; +- both sides share the same `CashTransferId`; +- company-level net effect is zero; +- both sides are posted atomically; +- neither side can be edited directly after posting; +- transfer reversal reverses both sides; +- transfer correction preserves original transfer, reversal pair and replacement pair. + +## Transfer Reversal + +Decision: + +Transfer reversal creates a linked reversal pair, not a physical delete. + +For original transfer: + +- source A -> destination B, amount 300. + +Reversal creates: + +- B outflow 300; +- A inflow 300; +- both linked to original transfer/reversal group. + +Rationale: + +This restores both account expected balances and preserves audit. + +## Transfer Reconciliation + +Decision: + +Each side reconciles with its own cash account/session. + +If only one side is counted/reconciled: + +- only that account/session can become balanced/discrepant; +- the other account remains unreconciled; +- transfer is visible in both accounts; +- Safe-to-Pay confidence should treat unreconciled side as lower confidence. + +Example: + +- 1,000 EUR moved from cash box to bank deposit. +- Cash box is counted and balanced. +- Bank account has not imported deposit yet. + +Result: + +- cash box session can close balanced; +- bank account session remains expected/unconfirmed until bank import/count confirms; +- no silent global balance assumption. + +## Multi-Account Invariants + +1. Every Cash Account is tenant-scoped. +2. A company can have many Cash Accounts. +3. A company has exactly one active default Cash Account per currency. +4. Cash Account names are unique per tenant among active accounts. +5. Cash Account currency is immutable after first movement. +6. Cash Account type can be changed only while no movements exist, or only through an explicit admin action with audit. +7. Inactive Cash Accounts cannot receive new ordinary movements. +8. Historical movements remain visible after account deactivation. +9. Every Cash Movement affects exactly one Cash Account. +10. Every Cash Movement account belongs to the same tenant as the movement. +11. Cash account balance is derived from posted movements, not mutable user input. +12. A Cash Session is per tenant/account/business date. +13. Reconciliation is per tenant/account/session. +14. Discrepancy in account A does not affect account B. +15. Transfer affects exactly two Cash Accounts. +16. Transfer source and destination cannot be equal. +17. Transfer must net to zero at company level. +18. Transfer source and destination movements are linked and atomic. +19. Transfer reversal reverses both sides. +20. Cross-tenant transfer is forbidden. +21. Unauthorized role cannot create account, transfer or reconciliation. + +## MVP Scope + +Confirmed MVP: + +- multiple active cash accounts per company; +- exactly one default account per company/currency; +- physical cash box account type; +- employee float account type; +- petty cash account type; +- POS/transit account type if enum is adjusted before persistence; +- bank account as manual/import-ready source, not automated connector; +- manual transfers between accounts; +- per-account expected balance; +- per-account daily session; +- per-account reconciliation; +- per-account discrepancy visibility; +- tenant-level authorization; +- no account-specific permissions initially. + +Excluded from MVP: + +- multi-currency transfers; +- FX rates and conversion; +- bank automation; +- POS integration; +- full treasury workflows; +- per-account user permissions; +- delete account with history; +- partial transfer reversal; +- cross-company transfers; +- certified accounting reconciliation. + +## Required Data Model + +## CashAccount + +Recommended fields: + +- `CashAccountId`; +- `TenantId`; +- `Name`; +- `Type`; +- `Currency`; +- `IsDefault`; +- `IsActive`; +- `CreatedByUserId`; +- `CreatedAtUtc`; +- `DeactivatedByUserId`; +- `DeactivatedAtUtc`; + +Indexes: + +- unique active account name per tenant; +- unique active default per tenant/currency; +- tenant/type for filtering. + +## CashMovement + +Recommended fields: + +- `CashMovementId`; +- `TenantId`; +- `CashAccountId`; +- `CashTransferId`; +- `Amount`; +- `Currency`; +- `Direction`; +- `ReasonCategory`; +- `ReasonText`; +- `EmployeeCashHandlerUserId`; +- `EnteredByUserId`; +- `ApprovedByUserId`; +- `OccurredAtUtc`; +- `PostedAtUtc`; +- `Status`; +- `PhysicalCashHandled`; +- `OriginalMovementId`; +- `CorrectionGroupId`; + +Indexes: + +- tenant/account/posting date; +- tenant/transfer id; +- original movement id; +- correction group id. + +## CashTransfer + +Recommended fields: + +- `CashTransferId`; +- `TenantId`; +- `SourceCashAccountId`; +- `DestinationCashAccountId`; +- `Amount`; +- `Currency`; +- `ReasonCategory`; +- `ReasonText`; +- `EnteredByUserId`; +- `EmployeeCashHandlerUserId`; +- `OccurredAtUtc`; +- `PostedAtUtc`; +- `Status`; +- `OriginalTransferId`; +- `CorrectionGroupId`. + +## CashSession + +Recommended fields: + +- `CashSessionId`; +- `TenantId`; +- `CashAccountId`; +- `BusinessDate`; +- `OpeningExpectedBalance`; +- `OpeningActualBalance`; +- `Currency`; +- `OpenedByUserId`; +- `OpenedAtUtc`; +- `ClosedByUserId`; +- `ClosedAtUtc`; +- `Status`. + +Index: + +- unique session per tenant/account/business date. + +## CashReconciliation + +Recommended fields: + +- `CashReconciliationId`; +- `TenantId`; +- `CashSessionId`; +- `CashAccountId`; +- `ExpectedBalance`; +- `ActualBalance`; +- `Discrepancy`; +- `Currency`; +- `Status`; +- `ReconciledByUserId`; +- `ReconciledAtUtc`. + +## CashDiscrepancy + +Recommended fields: + +- `CashDiscrepancyId`; +- `TenantId`; +- `CashReconciliationId`; +- `Amount`; +- `Currency`; +- `Category`; +- `Explanation`; +- `CreatedByUserId`; +- `CreatedAtUtc`; +- `ResolvedByUserId`; +- `ResolvedAtUtc`. + +## API Proposal + +Use `/v1/cash` for the new product API. Keep existing `/v1/admin/cash` as legacy/admin compatibility until migrated. + +## Cash Accounts + +- `GET /v1/cash/accounts` +- `GET /v1/cash/accounts/{id}` +- `POST /v1/cash/accounts` +- `PATCH /v1/cash/accounts/{id}` +- `PATCH /v1/cash/accounts/{id}/deactivate` +- `GET /v1/cash/accounts/{id}/balance` + +Contract behavior: + +- create rejects duplicate active names per tenant; +- create can set default only if it becomes the only default; +- setting a new default unsets previous default in same transaction; +- deactivate rejects default account unless another default is selected. + +## Cash Movements + +- `POST /v1/cash/accounts/{id}/movements` +- `GET /v1/cash/accounts/{id}/movements` +- `GET /v1/cash/movements/{movementId}` +- `POST /v1/cash/movements/{movementId}/reverse` +- `POST /v1/cash/movements/{movementId}/correct` + +Contract behavior: + +- movement writes only to the selected account; +- inactive account rejects ordinary movement; +- posted movement cannot be edited/deleted. + +## Transfers + +- `POST /v1/cash/transfers` +- `GET /v1/cash/transfers/{transferId}` +- `POST /v1/cash/transfers/{transferId}/reverse` +- `POST /v1/cash/transfers/{transferId}/correct` + +Contract behavior: + +- source and destination required; +- source and destination cannot match; +- both accounts must be active; +- both accounts must belong to tenant; +- both accounts must use same currency in MVP; +- source outflow and destination inflow are atomic. + +## Sessions And Reconciliation + +- `GET /v1/cash/accounts/{id}/sessions/current` +- `POST /v1/cash/accounts/{id}/sessions/open` +- `GET /v1/cash/accounts/{id}/sessions/{sessionId}` +- `GET /v1/cash/accounts/{id}/sessions/{sessionId}/expected-balance` +- `POST /v1/cash/accounts/{id}/sessions/{sessionId}/reconcile` +- `GET /v1/cash/accounts/{id}/sessions/{sessionId}/reconciliation` +- `POST /v1/cash/reconciliations/{reconciliationId}/discrepancies` +- `PATCH /v1/cash/reconciliations/{reconciliationId}/discrepancies/{discrepancyId}/resolve` + +## UI Proposal + +## Cash Accounts List + +Shows: + +- account name; +- type; +- currency; +- current expected balance; +- last reconciliation status; +- default marker; +- active/inactive marker. + +Actions: + +- create account; +- edit account; +- set default; +- deactivate. + +## Create/Edit Cash Account + +Fields: + +- name; +- type; +- currency; +- default flag; +- active flag. + +Warnings: + +- currency cannot change after first movement; +- deactivation preserves history. + +## Account Balances Overview + +Shows all accounts: + +- main office cash box; +- workshop cash box; +- employee floats; +- bank/manual accounts; +- POS/transit; +- petty cash. + +Also shows: + +- company total; +- unreconciled account warnings; +- accounts with open discrepancies. + +## Movement Entry + +Fields: + +- cash account selector; +- employee cash handler; +- amount; +- inflow/outflow; +- reason/category; +- note; +- optional payment/document link. + +Default behavior: + +- preselect default cash account; +- require explicit account if multiple accounts exist and user changed context. + +## Transfer Between Cash Accounts + +Fields: + +- source account; +- destination account; +- amount; +- reason/category; +- employee handler if physical cash custody changes; +- note. + +UI must preview: + +- source decreases by amount; +- destination increases by amount; +- company total net effect is zero. + +## Per-Account Daily Reconciliation + +Shows: + +- account name; +- opening expected; +- posted movements; +- expected closing; +- actual counted/imported amount; +- discrepancy. + +Status: + +- balanced; +- discrepant; +- explained; +- resolved. + +## Account Discrepancy View + +Shows: + +- account; +- date/session; +- expected; +- actual; +- discrepancy amount; +- explanation status; +- linked movements/transfers. + +## Employee Float Summary + +Shows: + +- employee; +- assigned float account; +- current expected balance; +- open sessions; +- unresolved discrepancies; +- movements handled by employee. + +## Test Matrix + +## Domain Tests + +- company can have many Cash Accounts; +- default account uniqueness is represented and enforced at service/repository level; +- account requires name, tenant, currency and type; +- account currency normalizes to ISO code; +- inactive account cannot receive ordinary movement; +- movement affects exactly one account; +- transfer source and destination cannot match; +- transfer amount must be positive; +- transfer creates source outflow and destination inflow; +- transfer net effect is zero; +- transfer reversal restores both accounts; +- session expected balance includes only posted movements for that account; +- reconciliation discrepancy is per account. + +## Application Tests + +- create multiple cash accounts for same company; +- duplicate account names rejected per company; +- exactly one active default account; +- setting new default unsets old default; +- default account cannot be deactivated without replacement; +- inactive account rejects new movement; +- movement updates only selected account; +- transfer source decreases and destination increases; +- transfer nets to zero at company level; +- transfer cannot use same source/destination; +- transfer cannot cross tenant; +- transfer cannot cross currency in MVP; +- transfer reversal restores both accounts; +- reconciliation is independent per account; +- discrepancy in account A does not affect account B; +- tenant A cannot see tenant B accounts/movements/transfers; +- unauthorized role cannot create account, transfer, movement or reconciliation. + +## API Tests + +- `GET /cash/accounts` returns only tenant accounts; +- `POST /cash/accounts` creates account; +- duplicate name returns `409 Conflict`; +- invalid type/currency returns `400 Bad Request`; +- deactivate account preserves read access; +- movement into inactive account returns `409 Conflict`; +- transfer source equal destination returns `400 Bad Request`; +- transfer cross-tenant account returns `403` or `404` consistently; +- reconciliation for account A does not alter account B; +- unauthorized role returns `403`. + +## Database Integration Tests + +- unique account name per tenant; +- unique active default per tenant/currency; +- movement FK requires account; +- transfer FK requires source and destination accounts; +- session unique per tenant/account/date; +- reconciliation FK requires session/account; +- historical movements remain queryable after account deactivation; +- rollback of transfer transaction leaves no one-sided movement. + +## Migration Strategy From Current Single CashBalance + +Current state: + +- one `CashBalance` row per company; +- `CashLedger` entries have only `CompanyId`, no `CashAccountId`; +- existing UI/API display company-level current balance. + +## Migration Decision + +Decision: + +Create one default Cash Account per company and migrate existing cash data into that account. + +## Default Account Creation + +For each company: + +- create default account named `Default cash account` or localized equivalent; +- type = `PhysicalCash`; +- currency = tenant/company currency, default `EUR` if no currency exists; +- `IsDefault = true`; +- `IsActive = true`. + +## Existing CashBalance + +Recommended approach: + +- preserve `CashBalance` during transition as compatibility cache/view; +- initialize default account expected opening from existing `CashBalance.Balance`; +- mark that opening as a migration/import event or first session opening. + +Rejected approach: + +- drop `CashBalance` immediately. + +Reason rejected: + +- current payment/cash UI and tests depend on company-level balance; +- immediate removal increases migration risk. + +## Existing CashLedger Entries + +Recommended phased approach: + +1. Add `CashAccountId` to ledger/movement model in future migration. +2. Backfill every existing ledger row with the tenant's default Cash Account. +3. Keep old ledger entries visible as legacy imported movements. +4. Later, convert old `CashLedger` to a compatibility projection of `CashMovement`/ledger entries. + +If a new `CashMovement` table is introduced: + +- create one imported movement per legacy ledger row where feasible; +- preserve `RefType`, `RefId`, `OriginalPaymentId`, `Reason`, `CreatedBy`, `CreatedAtUtc`; +- map missing reason to category `LegacyImport` and reason text from ref fields; +- do not invent employee cash handlers for historical data. + +## CashBalance Future + +Options: + +1. Keep `CashBalance` as company-level cache. +2. Replace with `CashAccountBalance` per account. +3. Derive balances on read from posted movements and use caches only for performance. + +Recommendation: + +- MVP implementation should introduce `CashAccountBalance` cache per account, or derive per-account balance if volume is low; +- keep old `CashBalance` temporarily as sum of active account balances for compatibility; +- plan deprecation once UI/API migrates to account-aware cash views. + +## Rollback Plan + +Use additive migration first: + +- add Cash Accounts; +- add account-aware tables/columns; +- backfill default accounts; +- keep existing `CashBalance` and `CashLedger`; +- do not delete legacy columns/tables in the first release. + +Rollback: + +- disable new account-aware UI/API; +- continue reading existing `CashBalance` and `CashLedger`; +- account data can remain unused; +- no irreversible data loss. + +Do not ship destructive cleanup until: + +- account-aware flows are stable; +- backup/restore drill covers new tables; +- migration has been tested on realistic data. + +## Current Cash Integrity Contracts: Required Adjustments + +The current intended contracts are a good foundation but need small adjustments before schema/API implementation: + +1. Add `CashTransfer` domain model. +2. Add transfer commands: + - `CreateCashTransferCommand`; + - `ReverseCashTransferCommand`; + - `CorrectCashTransferCommand`. +3. Add transfer result model and ports: + - `ICashTransferReader`; + - `ICashTransferWriter`. +4. Add transfer linkage to `CashMovementResult`, e.g. `CashTransferId`. +5. Add `PosTransit` and `PettyCash` to `CashAccountType`, or explicitly defer them. +6. Decide whether `CashAccount` needs `CreatedBy`, timestamps and deactivation metadata in domain result contracts. +7. Add account default management command, because exactly-one-default requires orchestration. +8. Add account balance result per account. +9. Add currency policy: single currency MVP, reject cross-currency transfer. + +## Rejected Options + +## One Global Company Cash Balance + +Rejected because it cannot prove source-level cash integrity. + +## One Movement With Source And Destination + +Rejected because it violates the clean invariant that a movement affects exactly one account and makes per-account reconciliation harder. + +## Two Unlinked Movements For Transfer + +Rejected because it breaks auditability and can leave one-sided reversals. + +## Physical Delete For Cash Accounts + +Rejected because account history must remain readable. + +## Multi-Currency Transfers In MVP + +Rejected because FX conversion and accounting implications are outside the Cash Cockpit MVP. + +## Account-Level Permissions In MVP + +Deferred. Useful later, but tenant-level roles are enough to implement source-level Cash Integrity first. + +## Implementation Sequence + +## PR 1 - Contract Adjustment + +Add: + +- `CashTransfer`; +- transfer enums/status if needed; +- transfer commands/results/ports; +- account balance result; +- missing account types; +- default-account command/result. + +No EF schema. + +## PR 2 - Domain/Application Use Cases + +Add use cases with fake-port tests: + +- create/deactivate/set default account; +- create movement; +- create transfer; +- reverse transfer; +- open session; +- reconcile per account. + +No EF schema. + +## PR 3 - Persistence Schema + +Add EF entities/migration: + +- `CashAccounts`; +- `CashMovements`; +- `CashTransfers`; +- `CashSessions`; +- `CashReconciliations`; +- `CashDiscrepancies`; +- optional `CashAccountBalances`. + +Backfill default accounts. + +## PR 4 - Infrastructure Implementations + +Implement repositories/readers/writers. + +Preserve existing payment behavior by routing legacy payment cash effects into the default account. + +## PR 5 - API Contracts + +Add `/v1/cash` account-aware endpoints. + +Keep `/v1/admin/cash` compatibility until WebApp moves. + +## PR 6 - WebApp UX + +Add: + +- account list; +- transfer form; +- movement form with account selector; +- per-account balance/reconciliation. + +## PR 7 - Migration Cutover + +Move payment cash effects from company-level ledger to account-aware movement/ledger. + +Deprecate old company-level balance only after compatibility period. + +## Risks + +- Migration risk from one company balance to per-account balances. +- Payment cash effects currently lack account selection; default-account fallback is necessary. +- Transfers must be atomic or they can corrupt expected balances. +- Cross-account reconciliation can confuse users if transfer timing differs between physical and bank sources. +- Employee float accounts can become pseudo-HR if scope is not constrained. +- Bank accounts can become full bank reconciliation/accounting if product boundaries are not enforced. +- Default account uniqueness needs infrastructure/database enforcement, not only domain records. +- Historical data cannot reliably reconstruct employee cash handlers. + +## Final Recommendation + +Model Cash Accounts as first-class company-owned cash sources. + +Keep the invariant: + +> one Cash Movement affects exactly one Cash Account. + +Model transfers as: + +> one CashTransfer business object that atomically creates two linked Cash Movements: source outflow and destination inflow. + +Migration should be additive and conservative: + +> create one default Cash Account per company, map existing `CashBalance` and `CashLedger` history to that default account, and keep legacy `CashBalance` as a temporary compatibility cache. + +This keeps OpenCashFlow aligned with Cash Integrity without turning it into accounting software or ERP treasury management. diff --git a/Docs/product/CASH_CUSTODY_DECISION_RECORD.md b/Docs/product/CASH_CUSTODY_DECISION_RECORD.md new file mode 100644 index 0000000..15c2730 --- /dev/null +++ b/Docs/product/CASH_CUSTODY_DECISION_RECORD.md @@ -0,0 +1,363 @@ +# Cash Custody Decision Record + +Date: 2026-07-08 + +Status: Accepted for product/domain direction. Not yet implemented. + +Source analysis: + +- `Docs/analysis/cash-custody-domain-analysis.md` +- `Docs/analysis/multi-cash-account-model.md` +- `Docs/product/CASH_INTEGRITY_DECISION_RECORD.md` + +## Decision Summary + +OpenCashFlow will treat Cash Custody as the umbrella product and domain pillar for operational cash. + +Cash Custody means: + +> The provable chain of responsibility for company money from receipt to reconciliation. + +Cash Integrity remains required, but it is not the umbrella concept. It is the proof capability inside the custody model. + +The product decision is: + +> OpenCashFlow must prove who or what is responsible for company money, why that responsibility exists, how it changed, and whether it reconciles with reality. + +This decision must be made before schema, contracts, API, or UI are expanded further. Otherwise the product risks implementing a ledger that records movements but does not answer the owner's real question: who has the money and can we prove it? + +## 1. Cash Custody As Umbrella Domain + +Decision: + +Cash Custody is the broader domain language for OpenCashFlow operational cash. + +Cash Custody covers: + +- cash received from customers; +- cash held in a cash box; +- cash held by an employee; +- cash assigned as float; +- cash paid to suppliers; +- cash deposited to a bank; +- cash in POS/transit; +- cash returned to the company; +- cash reconciled at the end of a day/session; +- discrepancies that prove the chain is incomplete. + +Rationale: + +Small business owners do not only need to know that cash moved. They need to know who was responsible for the money at each point, why, and whether the money was later proven. + +Product language: + +> Cash Custody is the chain of responsibility for company money. + +## 2. Cash Integrity As Proof Capability + +Decision: + +Cash Integrity remains a required product capability, but it is subordinate to Cash Custody. + +Cash Integrity means: + +- custody events are explicit; +- movements are source-bound; +- reasons are recorded; +- handlers, recorders, approvers and reconcilers are distinguishable; +- posted facts are immutable; +- corrections use reversal plus replacement; +- expected cash can be derived; +- actual cash can be compared; +- discrepancies remain visible until explained or resolved. + +Rationale: + +Cash Custody describes the business problem. Cash Integrity describes the quality bar required to trust the custody chain. + +Product language: + +> Cash Integrity proves that the custody chain is complete, reconciled, and explainable. + +## 3. Cash Movement As Balance-Affecting Fact + +Decision: + +Cash Movement remains a valid lower-level concept. + +A Cash Movement is a posted, balance-affecting fact for one Cash Account. + +It answers: + +- which account/source changed; +- amount; +- direction; +- reason/category; +- handler when physical cash is handled; +- recorded by; +- approved by when required; +- timestamp; +- reversal/correction links. + +Rationale: + +Balances and reconciliations need movement facts. The mistake would be treating movement as the whole business concept. Movement is the ledger consequence of a custody event. + +Implementation implication: + +Do not rename all future code to custody terminology blindly. Keep `CashMovement` where it accurately represents a balance-affecting fact. Add custody concepts where responsibility must be represented. + +## 4. Cash Account As Cash Source And Custody Holder + +Decision: + +Cash Account is the domain object for a company-controlled cash source. + +It is also a type of Custody Holder. + +Examples: + +- main office cash box; +- workshop cash box; +- petty cash; +- employee float; +- bank account; +- POS/transit account. + +Rationale: + +A cash account is not just a reporting bucket. It can be responsible for money. For example, the main cash box holds custody until money is assigned to an employee, deposited to bank, paid out, or reconciled. + +MVP rule: + +Every posted Cash Movement affects exactly one Cash Account. + +## 5. Terminology + +Use this language consistently. + +| Term | Meaning | +| --- | --- | +| Cash Custody | The chain of responsibility for company money. | +| Cash Integrity | Proof that the custody chain is complete, reconciled, immutable and explainable. | +| Cash Account | Company-controlled cash source, such as a cash box, bank account, or employee float. | +| Cash Source | User-facing synonym for Cash Account. | +| Custody Holder | Person, account, institution, or boundary actor responsible for money at a point in the chain. | +| Custodian | The person or source currently responsible for money. | +| Cash Handler | The person physically receiving, carrying, counting, or handing over cash. | +| Recorded By | The authenticated user who records the event in OpenCashFlow. | +| Approved By | The user who authorizes a high-risk custody event. | +| Reconciled By | The user who verifies expected cash against actual counted/reported cash. | +| Cash Movement | Balance-affecting fact for one cash account. | +| Custody Transfer | Business event where responsibility moves from one holder/source to another. | +| Expected Cash | Cash that should exist based on opening state plus posted events. | +| Actual Cash | Counted cash or imported/reported account balance. | +| Discrepancy | Actual cash minus expected cash; evidence of an incomplete or broken custody chain. | + +Naming decisions: + +- use `Cash Custody` for product/domain pillar; +- keep `Cash Integrity` for proof quality; +- keep `Cash Movement` as event/ledger fact; +- use `Recorded By`, not `Entered By`, in user-facing copy; +- use `Cash Handler` for physical handling; +- use `Custody Holder` when the responsible party might be an account, employee, bank, customer, supplier, or other actor. + +## 6. Transfer As Custody Transfer + +Decision: + +A transfer is a transfer of custody. + +It should be implemented as two linked Cash Movements: + +- source movement: outflow from the source account/holder; +- destination movement: inflow to the destination account/holder. + +Both sides must share a transfer id and be created atomically. + +Examples: + +- main cash box to employee float; +- employee float back to main cash box; +- physical cash box to bank deposit; +- POS/transit settlement to bank. + +Rules: + +- source and destination cannot be the same; +- source and destination must belong to the same tenant; +- same-currency transfers net to zero at company level; +- reversal of a transfer reverses both sides; +- one side must never exist without the other. + +Rationale: + +One abstract transfer alone is not enough for per-account balance and reconciliation. Two unrelated movements are not enough to prove business intent. The correct model is one custody transfer with two linked movement facts. + +## 7. Multi-Cash-Account MVP Scope + +Decision: + +The MVP supports multiple cash accounts per company. + +Included: + +- one active default cash account per company; +- physical cash boxes; +- employee float; +- bank account as cash source/context; +- POS/transit account; +- petty cash; +- deactivate account; +- per-account expected balance; +- per-account daily/session reconciliation; +- transfer between accounts; +- discrepancy per account/session. + +Excluded from MVP: + +- multi-currency transfers; +- FX conversion; +- bank automation; +- per-account permission rules; +- full accounting bank ledger replacement; +- treasury workflows. + +Rationale: + +Multiple cash sources are required to prove custody. Multi-currency, automation, and permission-per-account can wait until the core custody chain is proven. + +## 8. Chain Of Custody Invariant + +Decision: + +Every euro tracked by OpenCashFlow must have a provable custody chain from the moment it enters the system's operational scope. + +Minimum invariant: + +Every custody event must have: + +- tenant/company; +- source or origin; +- current custodian or destination; +- cash account/source; +- amount and currency; +- reason/category; +- recorded-by actor; +- cash handler when physical cash is handled; +- approval where policy requires it; +- audit trail; +- reconciliation status or path to reconciliation. + +Operational rule: + +Cash must never become orphaned. Responsibility cannot disappear through edit, delete, import, or partial transfer. + +Discrepancy rule: + +A discrepancy is a broken or incomplete custody chain. It must remain visible until explained or resolved. + +## 9. Historical Legacy Ledger Limitation + +Decision: + +Historical ledger data must not be upgraded into fake custody data. + +Current legacy cash data can prove: + +- company-level ledger deltas; +- payment/admin adjustment references; +- created-by for some ledger rows; +- balance reconstruction. + +It cannot fully prove: + +- exact physical cash handler; +- exact custodian chain; +- approval; +- source/account before migration; +- reconciliation status; +- discrepancy lifecycle. + +Migration rule: + +When historical data is migrated, assign it to a default Cash Account only as legacy evidence. Do not invent handler, approver, or reconciliation facts that were never captured. + +Product rule: + +Full Cash Custody proof begins from the implementation/migration date forward. + +## 10. Forecast And Safe-To-Pay Gate + +Decision: + +Forecast and Safe-to-Pay must treat unreconciled custody as lower confidence. + +Rules: + +- reconciled cash can be high-confidence; +- unreconciled cash can be visible but lower-confidence; +- open discrepancies reduce safe cash confidence; +- employee-held cash is not equivalent to bank-confirmed cash; +- transfers in transit must reduce confidence until both sides are reconciled or settled; +- forecast/Safe-to-Pay must remain WIP/Experimental until custody events, transfers, reconciliation and discrepancy status exist. + +Rationale: + +Safe-to-Pay is only valuable if users trust it. A recommendation built on unreconciled or unexplained cash is dangerous. + +Product language: + +> Safe-to-Pay can only be trusted when the custody chain is trusted. + +## MVP Acceptance Criteria + +The Cash Custody MVP is acceptable only when OpenCashFlow can prove this scenario: + +1. Employee X receives 200 EUR for `rinnovo K`. +2. Employee Z receives 100 EUR for `acquisto F`. +3. Both events identify source/account, handler, recorded-by, reason and tenant. +4. Expected cash for the account/session is 300 EUR higher. +5. Actual cash of 300 EUR reconciles as balanced. +6. Actual cash of 250 EUR creates a visible -50 EUR discrepancy. +7. A posted event cannot be edited. +8. Correction uses reversal plus replacement. +9. Another tenant cannot see or alter the chain. +10. Unauthorized roles cannot enter, transfer, reverse, correct, reconcile or resolve custody. + +## Implementation Implications + +Do not implement this decision by renaming everything. + +Recommended technical implications: + +- add custody/transfer concepts before persistence schema; +- keep movement as a balance-affecting fact; +- introduce transfer id/grouping before account balances are persisted; +- ensure sessions/reconciliations are per cash account; +- ensure legacy ledger migration is explicit about limited evidence; +- connect reconciliation/discrepancy state to future forecast confidence. + +Recommended next implementation branch: + +`feature/cash-custody-transfer-contracts` + +Purpose: + +- adjust domain/application contracts to include custody transfer and holder semantics; +- avoid schema/API/UI changes until the terminology and invariants are stable. + +## Final Decision + +Cash Custody is the product/domain pillar. + +Cash Integrity is the proof capability. + +Cash Movement is the balance-affecting fact. + +Cash Account is the cash source and possible custody holder. + +Transfer is custody transfer implemented by linked movements. + +Safe-to-Pay remains gated until custody is reconciled, explainable and confidence-aware. diff --git a/Docs/product/CASH_INTEGRITY_DECISION_RECORD.md b/Docs/product/CASH_INTEGRITY_DECISION_RECORD.md new file mode 100644 index 0000000..b3a50ba --- /dev/null +++ b/Docs/product/CASH_INTEGRITY_DECISION_RECORD.md @@ -0,0 +1,470 @@ +# Cash Integrity Decision Record + +Date: 2026-07-08 + +Status: Accepted as proof capability inside Cash Custody. Not yet implemented. + +Source analysis: `Docs/analysis/cash-integrity-foundation-analysis.md` + +Superseding umbrella decision: `Docs/product/CASH_CUSTODY_DECISION_RECORD.md` + +## Decision Summary + +Cash Integrity is a required proof capability for OpenCashFlow. + +OpenCashFlow cannot credibly provide Safe-to-Pay guidance, cash forecasts, cash confidence, or owner payment decisions unless it can prove the operational cash facts behind those decisions. + +The broader product/domain pillar is now Cash Custody: + +> the chain of responsibility for company money from receipt to reconciliation. + +The product decision is: + +> Cash movements must become explicit, auditable, source-bound, employee-aware, and reconciliation-ready before forecast and Safe-to-Pay move beyond WIP/Experimental. + +## Product Language + +Use these terms consistently. + +| Term | Meaning | +| --- | --- | +| Cash Custody | The chain of responsibility for company money from receipt to reconciliation. | +| Cash Integrity | The ability to prove the custody chain is complete, immutable, reconciled, and explainable. | +| Cash Account | A cash source controlled by the company, such as a physical cash box, bank account, or employee float. | +| Cash Source | User-facing synonym for Cash Account. | +| Cash Movement | A business event where cash is received, paid, moved, corrected, or reversed. | +| Employee Cash Handler | The employee who physically received or took the money. | +| Entered By | The user who records the movement in OpenCashFlow. | +| Approved By | The user who authorizes a movement when approval is required. | +| Expected Cash | Opening cash plus posted movements for the session/account. | +| Actual Cash | Counted cash or imported bank/account balance. | +| Discrepancy | Actual cash minus expected cash. | +| Reversal | A movement that cancels a posted movement without deleting it. | +| Correction | A reversal plus replacement movement. | + +## 1. What Is A Cash Account? + +Decision: + +OpenCashFlow will model cash sources as Cash Accounts. + +MVP includes: + +- one default physical cash box per company; +- optional additional physical cash boxes; +- optional employee floats; +- optional bank accounts as cash sources, but only for balance/import/reconciliation context. + +MVP excludes: + +- full bank ledger replacement; +- bank transaction enrichment beyond cash decision needs; +- chart-of-accounts accounting behavior; +- interbank treasury workflows. + +Rationale: + +The current company-level cash balance is too coarse. A business can have a front desk cash box, a workshop cash box, employee-held cash, and a bank account. Safe-to-Pay cannot be trusted if all sources collapse into one unexplained number. + +Product rule: + +Every posted cash movement must affect exactly one Cash Account. + +## 2. What Is A Cash Movement? + +Decision: + +A Cash Movement is the canonical product event for operational cash. + +It must answer: + +- who physically received or took the money; +- who entered it; +- who approved it, if approval is required; +- how much moved; +- whether it was inflow or outflow; +- why it moved; +- which Cash Account it affected; +- when it happened; +- which tenant owns it. + +Required fields: + +- tenant/company; +- cash account; +- amount; +- direction; +- reason/category; +- employee cash handler when a person physically handles the cash; +- entered-by user; +- occurred/posting timestamp. + +Optional fields: + +- payment reference; +- document type; +- free text note; +- approved-by user; +- external import id; +- attachment/reference evidence. + +Rationale: + +Payments alone are not enough. A payment may describe a business transaction, but Cash Integrity needs an explicit cash movement with handler, source, reason and audit semantics. + +## 3. Who Physically Receives Or Takes Money? + +Decision: + +The person who physically receives or takes money is the Employee Cash Handler. + +This is not automatically the same as: + +- the user who enters the movement; +- the user who created a payment; +- the administrator who later reconciles the day. + +Rationale: + +The core proof scenario requires: Employee X received 200 EUR and Employee Z received 100 EUR. The current `UserID`/`CreatedBy` fields are not semantically clear enough to prove that. + +Product rule: + +When cash is physically handled by an employee, Employee Cash Handler is required. + +## 4. Who Enters It? + +Decision: + +Every movement records Entered By. + +Entered By is the authenticated user who records the movement in OpenCashFlow. + +Rationale: + +The system must distinguish operational responsibility from data-entry responsibility. A manager may enter a movement on behalf of an employee; an employee may enter their own receipt; both cases must be auditable. + +## 5. Who Approves It? + +Decision: + +Approval is optional in MVP for ordinary movement entry, but the model must support it. + +MVP behavior: + +- low-risk movement entry can be posted by authorized cash roles without separate approval; +- reversal, correction, end-of-day reconciliation and discrepancy resolution require stronger roles; +- future configuration may require approval above a threshold. + +Rationale: + +Mandatory approval for every movement could block adoption. The schema and use cases must still support approval because corrections and reconciliation are control points. + +## 6. Reason And Category + +Decision: + +Every Cash Movement requires a reason/category. + +MVP categories: + +- customer payment received; +- supplier payment; +- owner withdrawal/deposit; +- employee float; +- bank deposit/withdrawal; +- cash count adjustment; +- correction; +- other. + +Free-text reason is required for: + +- manual adjustments; +- corrections; +- reversals; +- discrepancy explanations; +- category `other`. + +Rationale: + +Cash without reason is not trustworthy. A ledger delta that says only `Payment` is insufficient for owner decisions and audit review. + +## 7. Immutability Policy + +Decision: + +Posted cash movements cannot be edited or deleted. + +Corrections must use: + +1. reversal of the original movement; +2. replacement movement with corrected facts; +3. audit trail linking original, reversal and replacement. + +Rationale: + +OpenCashFlow must preserve the historical cash story. Silent mutation breaks trust, reconciliation, audit and Safe-to-Pay confidence. + +Allowed states: + +- `Draft`; +- `Posted`; +- `Reversed`; +- `Corrected`. + +Product rule: + +If money has affected expected cash, the original record remains visible. + +## 8. Daily Session + +Decision: + +OpenCashFlow will model a daily cash session per Cash Account. + +Session lifecycle: + +1. Open session. +2. Record opening balance. +3. Post movements during the day. +4. Calculate expected balance. +5. Count or import actual balance. +6. Reconcile. +7. Close as balanced or discrepant. + +Required session facts: + +- cash account; +- business date; +- opening expected balance; +- opening actual balance when counted; +- posted movements; +- expected closing balance; +- actual counted/imported balance; +- close status. + +Close statuses: + +- `Open`; +- `Balanced`; +- `Discrepant`; +- `Explained`; +- `Resolved`. + +Rationale: + +A current balance is not the same as a day that was opened, operated, counted and closed. + +## 9. Reconciliation + +Decision: + +Reconciliation compares Expected Cash with Actual Cash. + +Definitions: + +- Expected Cash = opening balance + posted movements + corrections. +- Actual Cash = counted physical cash or imported bank/source balance. +- Discrepancy = actual cash - expected cash. + +Statuses: + +- `Balanced`: discrepancy is zero. +- `Discrepant`: discrepancy is non-zero and not explained. +- `Explained`: discrepancy has a documented explanation but may still represent real loss/overage. +- `Resolved`: follow-up action has closed the discrepancy. + +Product rule: + +A discrepancy is never hidden. It remains visible until explained or resolved. + +Rationale: + +Owner trust depends on showing the mismatch. The system must not convert unexplained differences into generic adjustments without preserving the discrepancy. + +## 10. Roles + +Decision: + +Cash Integrity needs explicit permissions. + +MVP role behavior: + +| Action | Allowed roles | +| --- | --- | +| View own/company cash movements | CompanyAdmin, InstanceAdmin, authorized employee role | +| Enter cash movement | CompanyAdmin, InstanceAdmin, authorized cash operator | +| Reconcile daily session | CompanyAdmin, InstanceAdmin | +| Reverse/correct posted movement | CompanyAdmin, InstanceAdmin | +| Resolve discrepancy | CompanyAdmin, InstanceAdmin | +| Export cash ledger/audit | CompanyAdmin, InstanceAdmin | +| Cross-company cash access | InstanceAdmin only, with explicit company context | + +Implementation can map these to current roles first, then introduce narrower permissions later. + +Rationale: + +The current role split is enough for admin-only cash operations, but movement entry and reconciliation need clearer separation before broader employee workflows. + +## 11. Forecast And Safe-To-Pay Status + +Decision: + +Forecast and Safe-to-Pay are WIP/Experimental until Cash Custody exists and Cash Integrity can prove it. + +They may exist as prototypes, concept screens, or planning documents, but must not be presented as reliable payment recommendations until: + +- Cash Accounts exist; +- Cash Movements exist; +- custody transfer and holder semantics exist where responsibility changes; +- posted movements are immutable; +- daily sessions exist; +- reconciliation exists; +- discrepancies are visible; +- tenant isolation and authorization tests pass; +- cash confidence can distinguish reconciled from unreconciled cash. + +Product rule: + +Unreconciled custody must reduce forecast confidence. + +## 12. MVP Scope + +Included now: + +- default physical cash account per company; +- cash movement model; +- employee cash handler; +- required reason/category; +- immutable posting; +- reversal/correction; +- daily session; +- expected vs actual reconciliation; +- discrepancy explanation; +- audit trail; +- tenant isolation; +- role enforcement; +- WebApp screens for daily movement entry and reconciliation. + +Explicitly excluded from MVP: + +- full accounting ledger; +- certified bank reconciliation; +- payroll; +- POS; +- inventory/MRP; +- tax compliance; +- automated bank connectors; +- multi-step approval workflows beyond basic admin control; +- advanced anomaly detection; +- cash forecasting marketed as stable. + +## 13. Required Test Scenarios + +### Scenario A - Balanced Cash Day + +Given: + +- opening cash is 0; +- Employee X receives 200 EUR for `rinnovo K`; +- Employee Z receives 100 EUR for `acquisto F`; +- actual counted cash is 300. + +Expected: + +- X is linked to the 200 movement; +- Z is linked to the 100 movement; +- both reasons are recorded; +- expected cash is 300; +- actual cash is 300; +- reconciliation status is `Balanced`; +- audit trail exists. + +### Scenario B - Discrepancy + +Given: + +- same movements as Scenario A; +- actual counted cash is 250. + +Expected: + +- expected cash is 300; +- discrepancy is -50; +- status is `Discrepant`; +- discrepancy remains visible until explained or resolved. + +### Scenario C - Reversal And Correction + +Given: + +- a posted movement is wrong. + +Expected: + +- direct edit is rejected; +- original movement remains visible; +- reversal is created; +- replacement movement is created; +- audit links all records. + +### Scenario D - Tenant Isolation + +Given: + +- a user from Tenant A attempts to read or alter Tenant B cash movements. + +Expected: + +- access is rejected; +- no data leaks; +- no ledger entry changes. + +### Scenario E - Unauthorized Role + +Given: + +- a user without cash permission attempts movement entry, reconciliation, correction or export. + +Expected: + +- request is rejected; +- no movement, session, reconciliation or audit mutation occurs except optional security audit. + +### Scenario F - Posted Movement Deletion + +Given: + +- a posted movement exists. + +Expected: + +- physical delete is forbidden; +- soft delete is forbidden for posted movement; +- reversal is the correction path. + +## 14. Implementation Gate + +No implementation branch should add forecast/Safe-to-Pay production behavior until it can state which Cash Custody chain +and Cash Integrity guarantees it relies on. + +Recommended implementation sequence: + +1. domain/application contracts and invariants; +2. persistence model and migration; +3. cash movement use cases; +4. session/reconciliation use cases; +5. API contracts; +6. WebApp workflow; +7. test coverage; +8. forecast confidence integration. + +## Final Decision + +OpenCashFlow will not treat cash as an anonymous balance. + +It will treat cash as a custody chain proven by accountable movements: + +> source + custodian + cash handler + reason + actor + posting + audit + reconciliation. + +That chain is the foundation for Safe-to-Pay. diff --git a/src/OpenCashFlow.Application/Cash/Integrity/Commands/CashIntegrityCommands.cs b/src/OpenCashFlow.Application/Cash/Integrity/Commands/CashIntegrityCommands.cs new file mode 100644 index 0000000..c20832a --- /dev/null +++ b/src/OpenCashFlow.Application/Cash/Integrity/Commands/CashIntegrityCommands.cs @@ -0,0 +1,96 @@ +namespace OpenCashFlow.Application.Cash.Integrity.Commands; + +public sealed record CreateCashMovementCommand( + Guid TenantId, + Guid CashAccountId, + decimal Amount, + string Currency, + string Direction, + string ReasonCategory, + string? ReasonText, + Guid? CashHandlerUserId, + Guid RecordedByUserId, + DateTimeOffset OccurredAtUtc, + bool PhysicalCashHandled); + +public sealed record ReverseCashMovementCommand( + Guid TenantId, + Guid CashMovementId, + Guid RecordedByUserId, + string ReasonCategory, + string ReasonText, + DateTimeOffset OccurredAtUtc); + +public sealed record CorrectCashMovementCommand( + Guid TenantId, + Guid CashMovementId, + decimal ReplacementAmount, + string Currency, + string ReplacementDirection, + string ReasonCategory, + string ReasonText, + Guid RecordedByUserId, + DateTimeOffset OccurredAtUtc); + +public sealed record CreateCashTransferCommand( + Guid TenantId, + Guid SourceCashAccountId, + Guid DestinationCashAccountId, + decimal Amount, + string Currency, + string ReasonCategory, + string? ReasonText, + Guid RecordedByUserId, + Guid? ApprovedByUserId, + DateTimeOffset OccurredAtUtc); + +public sealed record ReverseCashTransferCommand( + Guid TenantId, + Guid CashTransferId, + Guid RecordedByUserId, + string ReasonCategory, + string ReasonText, + DateTimeOffset OccurredAtUtc); + +public sealed record CorrectCashTransferCommand( + Guid TenantId, + Guid CashTransferId, + Guid ReplacementSourceCashAccountId, + Guid ReplacementDestinationCashAccountId, + decimal ReplacementAmount, + string Currency, + string ReasonCategory, + string ReasonText, + Guid RecordedByUserId, + DateTimeOffset OccurredAtUtc); + +public sealed record OpenCashSessionCommand( + Guid TenantId, + Guid CashAccountId, + DateOnly BusinessDate, + decimal OpeningExpectedBalance, + decimal? OpeningActualBalance, + string Currency, + Guid OpenedByUserId, + DateTimeOffset OpenedAtUtc); + +public sealed record ReconcileCashSessionCommand( + Guid TenantId, + Guid CashSessionId, + Guid CashAccountId, + decimal ExpectedBalance, + decimal ActualBalance, + string Currency, + Guid ReconciledByUserId, + DateTimeOffset ReconciledAtUtc); + +public sealed record ExplainCashDiscrepancyCommand( + Guid TenantId, + Guid CashReconciliationId, + Guid CashDiscrepancyId, + decimal Amount, + string Currency, + string Category, + string Explanation, + Guid CreatedByUserId, + DateTimeOffset CreatedAtUtc); diff --git a/src/OpenCashFlow.Application/Cash/Integrity/Models/CashIntegrityModels.cs b/src/OpenCashFlow.Application/Cash/Integrity/Models/CashIntegrityModels.cs new file mode 100644 index 0000000..d2272ad --- /dev/null +++ b/src/OpenCashFlow.Application/Cash/Integrity/Models/CashIntegrityModels.cs @@ -0,0 +1,109 @@ +using OpenCashFlow.Domain.Cash; + +namespace OpenCashFlow.Application.Cash.Integrity.Models; + +public sealed record CashAccountResult( + Guid CashAccountId, + Guid TenantId, + string Name, + CashAccountType Type, + string Currency, + bool IsDefault, + bool IsActive); + +public sealed record CashAccountBalanceResult( + Guid TenantId, + Guid CashAccountId, + decimal ExpectedBalance, + string Currency, + DateTimeOffset CalculatedAtUtc); + +public sealed record CashMovementResult( + Guid CashMovementId, + Guid TenantId, + Guid CashAccountId, + decimal Amount, + string Currency, + CashMovementDirection Direction, + string ReasonCategory, + string? ReasonText, + Guid? CashHandlerUserId, + Guid RecordedByUserId, + Guid? ApprovedByUserId, + DateTimeOffset OccurredAtUtc, + DateTimeOffset? PostedAtUtc, + CashMovementStatus Status, + bool PhysicalCashHandled, + Guid? OriginalMovementId, + Guid? CorrectionGroupId, + Guid? CashTransferId, + CashTransferSide? TransferSide); + +public sealed record CashTransferResult( + Guid CashTransferId, + Guid TenantId, + Guid SourceCashAccountId, + Guid DestinationCashAccountId, + decimal Amount, + string Currency, + string ReasonCategory, + string? ReasonText, + Guid RecordedByUserId, + Guid? ApprovedByUserId, + DateTimeOffset OccurredAtUtc, + DateTimeOffset? PostedAtUtc, + CashTransferStatus Status, + Guid? OriginalTransferId, + Guid? CorrectionGroupId, + CashMovementResult? SourceMovement, + CashMovementResult? DestinationMovement); + +public sealed record CashTransferCorrectionResult( + CashTransferResult Original, + CashTransferResult Reversal, + CashTransferResult Replacement); + +public sealed record CashSessionResult( + Guid CashSessionId, + Guid TenantId, + Guid CashAccountId, + DateOnly BusinessDate, + decimal OpeningExpectedBalance, + decimal? OpeningActualBalance, + string Currency, + Guid OpenedByUserId, + DateTimeOffset OpenedAtUtc, + CashSessionStatus Status); + +public sealed record CashReconciliationResult( + Guid CashReconciliationId, + Guid TenantId, + Guid CashSessionId, + Guid CashAccountId, + decimal ExpectedBalance, + decimal ActualBalance, + decimal Discrepancy, + string Currency, + CashReconciliationStatus Status, + Guid ReconciledByUserId, + DateTimeOffset ReconciledAtUtc); + +public sealed record CashDiscrepancyResult( + Guid CashDiscrepancyId, + Guid TenantId, + Guid CashReconciliationId, + decimal Amount, + string Currency, + string Category, + string Explanation, + Guid CreatedByUserId, + DateTimeOffset CreatedAtUtc); + +public sealed record CashIntegrityAuditEvent( + Guid TenantId, + string EventType, + string Resource, + Guid ResourceId, + Guid ActorUserId, + DateTimeOffset OccurredAtUtc, + string? Details); diff --git a/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashAccountReader.cs b/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashAccountReader.cs new file mode 100644 index 0000000..d0ab465 --- /dev/null +++ b/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashAccountReader.cs @@ -0,0 +1,14 @@ +using OpenCashFlow.Application.Cash.Integrity.Models; + +namespace OpenCashFlow.Application.Cash.Integrity.Ports; + +public interface ICashAccountReader +{ + Task> GetAccountsAsync(Guid tenantId, CancellationToken cancellationToken = default); + + Task GetByIdAsync(Guid tenantId, Guid cashAccountId, CancellationToken cancellationToken = default); + + Task GetDefaultAsync(Guid tenantId, string currency, CancellationToken cancellationToken = default); + + Task GetExpectedBalanceAsync(Guid tenantId, Guid cashAccountId, CancellationToken cancellationToken = default); +} diff --git a/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashAccountWriter.cs b/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashAccountWriter.cs new file mode 100644 index 0000000..0ebc875 --- /dev/null +++ b/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashAccountWriter.cs @@ -0,0 +1,12 @@ +using OpenCashFlow.Application.Cash.Integrity.Models; + +namespace OpenCashFlow.Application.Cash.Integrity.Ports; + +public interface ICashAccountWriter +{ + Task CreateAsync(CashAccountResult account, CancellationToken cancellationToken = default); + + Task UpdateAsync(CashAccountResult account, CancellationToken cancellationToken = default); + + Task DeactivateAsync(Guid tenantId, Guid cashAccountId, CancellationToken cancellationToken = default); +} diff --git a/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashIntegrityAuditWriter.cs b/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashIntegrityAuditWriter.cs new file mode 100644 index 0000000..3e357f2 --- /dev/null +++ b/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashIntegrityAuditWriter.cs @@ -0,0 +1,8 @@ +using OpenCashFlow.Application.Cash.Integrity.Models; + +namespace OpenCashFlow.Application.Cash.Integrity.Ports; + +public interface ICashIntegrityAuditWriter +{ + Task WriteAsync(CashIntegrityAuditEvent auditEvent, CancellationToken cancellationToken = default); +} diff --git a/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashMovementReader.cs b/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashMovementReader.cs new file mode 100644 index 0000000..6b676cf --- /dev/null +++ b/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashMovementReader.cs @@ -0,0 +1,14 @@ +using OpenCashFlow.Application.Cash.Integrity.Models; + +namespace OpenCashFlow.Application.Cash.Integrity.Ports; + +public interface ICashMovementReader +{ + Task GetByIdAsync(Guid tenantId, Guid cashMovementId, CancellationToken cancellationToken = default); + + Task> GetByAccountAsync( + Guid tenantId, + Guid cashAccountId, + DateOnly? businessDate = null, + CancellationToken cancellationToken = default); +} diff --git a/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashMovementWriter.cs b/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashMovementWriter.cs new file mode 100644 index 0000000..b608835 --- /dev/null +++ b/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashMovementWriter.cs @@ -0,0 +1,18 @@ +using OpenCashFlow.Application.Cash.Integrity.Commands; +using OpenCashFlow.Application.Cash.Integrity.Models; + +namespace OpenCashFlow.Application.Cash.Integrity.Ports; + +public interface ICashMovementWriter +{ + Task CreateAsync(CreateCashMovementCommand command, CancellationToken cancellationToken = default); + + Task ReverseAsync(ReverseCashMovementCommand command, CancellationToken cancellationToken = default); + + Task CorrectAsync(CorrectCashMovementCommand command, CancellationToken cancellationToken = default); +} + +public sealed record CashMovementCorrectionResult( + CashMovementResult Original, + CashMovementResult Reversal, + CashMovementResult Replacement); diff --git a/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashReconciliationReader.cs b/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashReconciliationReader.cs new file mode 100644 index 0000000..5f69107 --- /dev/null +++ b/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashReconciliationReader.cs @@ -0,0 +1,8 @@ +using OpenCashFlow.Application.Cash.Integrity.Models; + +namespace OpenCashFlow.Application.Cash.Integrity.Ports; + +public interface ICashReconciliationReader +{ + Task GetBySessionAsync(Guid tenantId, Guid cashSessionId, CancellationToken cancellationToken = default); +} diff --git a/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashReconciliationWriter.cs b/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashReconciliationWriter.cs new file mode 100644 index 0000000..0bb2f79 --- /dev/null +++ b/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashReconciliationWriter.cs @@ -0,0 +1,11 @@ +using OpenCashFlow.Application.Cash.Integrity.Commands; +using OpenCashFlow.Application.Cash.Integrity.Models; + +namespace OpenCashFlow.Application.Cash.Integrity.Ports; + +public interface ICashReconciliationWriter +{ + Task ReconcileAsync(ReconcileCashSessionCommand command, CancellationToken cancellationToken = default); + + Task ExplainDiscrepancyAsync(ExplainCashDiscrepancyCommand command, CancellationToken cancellationToken = default); +} diff --git a/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashSessionReader.cs b/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashSessionReader.cs new file mode 100644 index 0000000..9c8a180 --- /dev/null +++ b/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashSessionReader.cs @@ -0,0 +1,14 @@ +using OpenCashFlow.Application.Cash.Integrity.Models; + +namespace OpenCashFlow.Application.Cash.Integrity.Ports; + +public interface ICashSessionReader +{ + Task GetByIdAsync(Guid tenantId, Guid cashSessionId, CancellationToken cancellationToken = default); + + Task GetCurrentAsync( + Guid tenantId, + Guid cashAccountId, + DateOnly businessDate, + CancellationToken cancellationToken = default); +} diff --git a/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashSessionWriter.cs b/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashSessionWriter.cs new file mode 100644 index 0000000..c70cc2e --- /dev/null +++ b/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashSessionWriter.cs @@ -0,0 +1,11 @@ +using OpenCashFlow.Application.Cash.Integrity.Commands; +using OpenCashFlow.Application.Cash.Integrity.Models; + +namespace OpenCashFlow.Application.Cash.Integrity.Ports; + +public interface ICashSessionWriter +{ + Task OpenAsync(OpenCashSessionCommand command, CancellationToken cancellationToken = default); + + Task CloseAsync(Guid tenantId, Guid cashSessionId, Guid closedByUserId, CancellationToken cancellationToken = default); +} diff --git a/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashTransferReader.cs b/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashTransferReader.cs new file mode 100644 index 0000000..6b68011 --- /dev/null +++ b/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashTransferReader.cs @@ -0,0 +1,14 @@ +using OpenCashFlow.Application.Cash.Integrity.Models; + +namespace OpenCashFlow.Application.Cash.Integrity.Ports; + +public interface ICashTransferReader +{ + Task GetByIdAsync(Guid tenantId, Guid cashTransferId, CancellationToken cancellationToken = default); + + Task> GetByAccountAsync( + Guid tenantId, + Guid cashAccountId, + DateOnly? businessDate = null, + CancellationToken cancellationToken = default); +} diff --git a/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashTransferWriter.cs b/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashTransferWriter.cs new file mode 100644 index 0000000..f7d4f7f --- /dev/null +++ b/src/OpenCashFlow.Application/Cash/Integrity/Ports/ICashTransferWriter.cs @@ -0,0 +1,13 @@ +using OpenCashFlow.Application.Cash.Integrity.Commands; +using OpenCashFlow.Application.Cash.Integrity.Models; + +namespace OpenCashFlow.Application.Cash.Integrity.Ports; + +public interface ICashTransferWriter +{ + Task CreateAsync(CreateCashTransferCommand command, CancellationToken cancellationToken = default); + + Task ReverseAsync(ReverseCashTransferCommand command, CancellationToken cancellationToken = default); + + Task CorrectAsync(CorrectCashTransferCommand command, CancellationToken cancellationToken = default); +} diff --git a/src/OpenCashFlow.Domain/Cash/CashAccount.cs b/src/OpenCashFlow.Domain/Cash/CashAccount.cs new file mode 100644 index 0000000..284b172 --- /dev/null +++ b/src/OpenCashFlow.Domain/Cash/CashAccount.cs @@ -0,0 +1,59 @@ +using OpenCashFlow.Domain.Common; + +namespace OpenCashFlow.Domain.Cash; + +public sealed record CashAccount( + Guid CashAccountId, + TenantId TenantId, + string Name, + CashAccountType Type, + string Currency, + bool IsDefault, + bool IsActive) +{ + public static CashAccount Create( + Guid cashAccountId, + TenantId tenantId, + string name, + CashAccountType type, + string currency = "EUR", + bool isDefault = false) + { + if (cashAccountId == Guid.Empty) + { + throw new ArgumentException("Cash account id is required.", nameof(cashAccountId)); + } + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Cash account name is required.", nameof(name)); + } + + return new CashAccount( + cashAccountId, + tenantId, + name.Trim(), + type, + NormalizeCurrency(currency), + isDefault, + IsActive: true); + } + + public CashAccount Deactivate() => this with { IsActive = false, IsDefault = false }; + + private static string NormalizeCurrency(string currency) + { + if (string.IsNullOrWhiteSpace(currency)) + { + throw new ArgumentException("Currency is required.", nameof(currency)); + } + + var normalizedCurrency = currency.Trim().ToUpperInvariant(); + if (normalizedCurrency.Length != 3) + { + throw new ArgumentException("Currency must be a 3-letter ISO code.", nameof(currency)); + } + + return normalizedCurrency; + } +} diff --git a/src/OpenCashFlow.Domain/Cash/CashAccountType.cs b/src/OpenCashFlow.Domain/Cash/CashAccountType.cs new file mode 100644 index 0000000..c6a05dd --- /dev/null +++ b/src/OpenCashFlow.Domain/Cash/CashAccountType.cs @@ -0,0 +1,11 @@ +namespace OpenCashFlow.Domain.Cash; + +public enum CashAccountType +{ + PhysicalCash = 1, + Bank = 2, + EmployeeFloat = 3, + PosTransit = 4, + PettyCash = 5, + Other = 99 +} diff --git a/src/OpenCashFlow.Domain/Cash/CashDiscrepancy.cs b/src/OpenCashFlow.Domain/Cash/CashDiscrepancy.cs new file mode 100644 index 0000000..d8daeb7 --- /dev/null +++ b/src/OpenCashFlow.Domain/Cash/CashDiscrepancy.cs @@ -0,0 +1,57 @@ +using OpenCashFlow.Domain.Common; + +namespace OpenCashFlow.Domain.Cash; + +public sealed record CashDiscrepancy( + Guid CashDiscrepancyId, + Guid CashReconciliationId, + Money Amount, + string Category, + string Explanation, + UserId CreatedByUserId, + DateTimeOffset CreatedAtUtc) +{ + public static CashDiscrepancy Explain( + Guid cashDiscrepancyId, + Guid cashReconciliationId, + Money amount, + string category, + string explanation, + UserId createdByUserId, + DateTimeOffset createdAtUtc) + { + if (cashDiscrepancyId == Guid.Empty) + { + throw new ArgumentException("Cash discrepancy id is required.", nameof(cashDiscrepancyId)); + } + + if (cashReconciliationId == Guid.Empty) + { + throw new ArgumentException("Cash reconciliation id is required.", nameof(cashReconciliationId)); + } + + if (amount.Amount == 0m) + { + throw new ArgumentOutOfRangeException(nameof(amount), amount.Amount, "Cash discrepancy amount cannot be zero."); + } + + if (string.IsNullOrWhiteSpace(category)) + { + throw new ArgumentException("Cash discrepancy category is required.", nameof(category)); + } + + if (string.IsNullOrWhiteSpace(explanation)) + { + throw new ArgumentException("Cash discrepancy explanation is required.", nameof(explanation)); + } + + return new CashDiscrepancy( + cashDiscrepancyId, + cashReconciliationId, + amount, + category.Trim(), + explanation.Trim(), + createdByUserId, + createdAtUtc); + } +} diff --git a/src/OpenCashFlow.Domain/Cash/CashMovement.cs b/src/OpenCashFlow.Domain/Cash/CashMovement.cs new file mode 100644 index 0000000..d078fd9 --- /dev/null +++ b/src/OpenCashFlow.Domain/Cash/CashMovement.cs @@ -0,0 +1,284 @@ +using OpenCashFlow.Domain.Common; + +namespace OpenCashFlow.Domain.Cash; + +public sealed record CashMovement( + Guid CashMovementId, + TenantId TenantId, + Guid CashAccountId, + Money Amount, + CashMovementDirection Direction, + string ReasonCategory, + string? ReasonText, + UserId? CashHandlerUserId, + UserId RecordedByUserId, + UserId? ApprovedByUserId, + DateTimeOffset OccurredAtUtc, + DateTimeOffset? PostedAtUtc, + CashMovementStatus Status, + bool PhysicalCashHandled, + Guid? OriginalMovementId, + Guid? CorrectionGroupId, + Guid? CashTransferId, + CashTransferSide? TransferSide) +{ + public static CashMovement CreateDraft( + Guid cashMovementId, + TenantId tenantId, + Guid cashAccountId, + Money amount, + CashMovementDirection direction, + string reasonCategory, + string? reasonText, + UserId? cashHandlerUserId, + UserId recordedByUserId, + DateTimeOffset occurredAtUtc, + bool physicalCashHandled) + { + ValidateCore(cashMovementId, cashAccountId, amount, reasonCategory, cashHandlerUserId, physicalCashHandled); + + return new CashMovement( + cashMovementId, + tenantId, + cashAccountId, + amount, + direction, + reasonCategory.Trim(), + NormalizeOptional(reasonText), + cashHandlerUserId, + recordedByUserId, + ApprovedByUserId: null, + occurredAtUtc, + PostedAtUtc: null, + CashMovementStatus.Draft, + physicalCashHandled, + OriginalMovementId: null, + CorrectionGroupId: null, + CashTransferId: null, + TransferSide: null); + } + + public static CashMovement CreatePostedTransferSide( + Guid cashMovementId, + TenantId tenantId, + Guid cashAccountId, + Money amount, + CashMovementDirection direction, + Guid cashTransferId, + CashTransferSide transferSide, + string reasonCategory, + string? reasonText, + UserId recordedByUserId, + UserId? approvedByUserId, + DateTimeOffset occurredAtUtc, + DateTimeOffset postedAtUtc) + { + if (cashTransferId == Guid.Empty) + { + throw new ArgumentException("Cash transfer id is required.", nameof(cashTransferId)); + } + + return CreateDraft( + cashMovementId, + tenantId, + cashAccountId, + amount, + direction, + reasonCategory, + reasonText, + cashHandlerUserId: null, + recordedByUserId, + occurredAtUtc, + physicalCashHandled: false).Post(postedAtUtc, approvedByUserId) with + { + CashTransferId = cashTransferId, + TransferSide = transferSide + }; + } + + public CashMovement Post(DateTimeOffset postedAtUtc, UserId? approvedByUserId = null) + { + if (Status != CashMovementStatus.Draft) + { + throw new InvalidOperationException("Only draft cash movements can be posted."); + } + + return this with + { + Status = CashMovementStatus.Posted, + PostedAtUtc = postedAtUtc, + ApprovedByUserId = approvedByUserId + }; + } + + public CashMovement UpdateDraftReason(string reasonCategory, string? reasonText) + { + if (Status != CashMovementStatus.Draft) + { + throw new InvalidOperationException("Posted cash movements cannot be edited."); + } + + if (string.IsNullOrWhiteSpace(reasonCategory)) + { + throw new ArgumentException("Cash movement reason/category is required.", nameof(reasonCategory)); + } + + return this with + { + ReasonCategory = reasonCategory.Trim(), + ReasonText = NormalizeOptional(reasonText) + }; + } + + public CashMovement CreatePostedReversal( + Guid reversalMovementId, + UserId recordedByUserId, + string reasonCategory, + string reasonText, + DateTimeOffset occurredAtUtc, + DateTimeOffset postedAtUtc) + { + if (Status != CashMovementStatus.Posted) + { + throw new InvalidOperationException("Only posted cash movements can be reversed."); + } + + if (string.IsNullOrWhiteSpace(reasonText)) + { + throw new ArgumentException("Reversal reason is required.", nameof(reasonText)); + } + + var reversal = CreateDraft( + reversalMovementId, + TenantId, + CashAccountId, + Amount, + Reverse(Direction), + reasonCategory, + reasonText, + CashHandlerUserId, + recordedByUserId, + occurredAtUtc, + PhysicalCashHandled); + + return reversal.Post(postedAtUtc) with + { + OriginalMovementId = CashMovementId, + CorrectionGroupId = CorrectionGroupId ?? Guid.NewGuid(), + CashTransferId = CashTransferId, + TransferSide = TransferSide + }; + } + + public CashMovementCorrection CreateCorrection( + Guid reversalMovementId, + Guid replacementMovementId, + Money replacementAmount, + CashMovementDirection replacementDirection, + string reasonCategory, + string reasonText, + UserId recordedByUserId, + DateTimeOffset occurredAtUtc, + DateTimeOffset postedAtUtc) + { + if (Status != CashMovementStatus.Posted) + { + throw new InvalidOperationException("Only posted cash movements can be corrected."); + } + + var correctionGroupId = Guid.NewGuid(); + var reversal = CreatePostedReversal( + reversalMovementId, + recordedByUserId, + reasonCategory, + reasonText, + occurredAtUtc, + postedAtUtc) with + { + CorrectionGroupId = correctionGroupId + }; + + var original = this with + { + Status = CashMovementStatus.Corrected, + CorrectionGroupId = correctionGroupId + }; + + var replacement = CreateDraft( + replacementMovementId, + TenantId, + CashAccountId, + replacementAmount, + replacementDirection, + reasonCategory, + reasonText, + CashHandlerUserId, + recordedByUserId, + occurredAtUtc, + PhysicalCashHandled).Post(postedAtUtc) with + { + OriginalMovementId = CashMovementId, + CorrectionGroupId = correctionGroupId, + CashTransferId = CashTransferId, + TransferSide = TransferSide + }; + + return new CashMovementCorrection(original, reversal, replacement); + } + + public decimal SignedAmount => Direction == CashMovementDirection.Inflow ? Amount.Amount : -Amount.Amount; + + public static CashMovementDirection Reverse(CashMovementDirection direction) + { + return direction switch + { + CashMovementDirection.Inflow => CashMovementDirection.Outflow, + CashMovementDirection.Outflow => CashMovementDirection.Inflow, + _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, "Unsupported cash movement direction.") + }; + } + + private static void ValidateCore( + Guid cashMovementId, + Guid cashAccountId, + Money amount, + string reasonCategory, + UserId? cashHandlerUserId, + bool physicalCashHandled) + { + if (cashMovementId == Guid.Empty) + { + throw new ArgumentException("Cash movement id is required.", nameof(cashMovementId)); + } + + if (cashAccountId == Guid.Empty) + { + throw new ArgumentException("Cash account id is required.", nameof(cashAccountId)); + } + + if (amount.Amount <= 0m) + { + throw new ArgumentOutOfRangeException(nameof(amount), amount.Amount, "Cash movement amount must be greater than zero."); + } + + if (string.IsNullOrWhiteSpace(reasonCategory)) + { + throw new ArgumentException("Cash movement reason/category is required.", nameof(reasonCategory)); + } + + if (physicalCashHandled && cashHandlerUserId is null) + { + throw new ArgumentException("Cash handler is required when physical cash is handled.", nameof(cashHandlerUserId)); + } + } + + private static string? NormalizeOptional(string? value) + { + return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + } +} + +public sealed record CashMovementCorrection( + CashMovement Original, + CashMovement Reversal, + CashMovement Replacement); diff --git a/src/OpenCashFlow.Domain/Cash/CashMovementDirection.cs b/src/OpenCashFlow.Domain/Cash/CashMovementDirection.cs new file mode 100644 index 0000000..09ac657 --- /dev/null +++ b/src/OpenCashFlow.Domain/Cash/CashMovementDirection.cs @@ -0,0 +1,7 @@ +namespace OpenCashFlow.Domain.Cash; + +public enum CashMovementDirection +{ + Inflow = 1, + Outflow = 2 +} diff --git a/src/OpenCashFlow.Domain/Cash/CashMovementStatus.cs b/src/OpenCashFlow.Domain/Cash/CashMovementStatus.cs new file mode 100644 index 0000000..6738edf --- /dev/null +++ b/src/OpenCashFlow.Domain/Cash/CashMovementStatus.cs @@ -0,0 +1,9 @@ +namespace OpenCashFlow.Domain.Cash; + +public enum CashMovementStatus +{ + Draft = 1, + Posted = 2, + Reversed = 3, + Corrected = 4 +} diff --git a/src/OpenCashFlow.Domain/Cash/CashReconciliation.cs b/src/OpenCashFlow.Domain/Cash/CashReconciliation.cs new file mode 100644 index 0000000..afd9e60 --- /dev/null +++ b/src/OpenCashFlow.Domain/Cash/CashReconciliation.cs @@ -0,0 +1,84 @@ +using OpenCashFlow.Domain.Common; + +namespace OpenCashFlow.Domain.Cash; + +public sealed record CashReconciliation( + Guid CashReconciliationId, + TenantId TenantId, + Guid CashSessionId, + Guid CashAccountId, + Money ExpectedBalance, + Money ActualBalance, + Money Discrepancy, + CashReconciliationStatus Status, + UserId ReconciledByUserId, + DateTimeOffset ReconciledAtUtc) +{ + public static CashReconciliation Create( + Guid cashReconciliationId, + TenantId tenantId, + Guid cashSessionId, + Guid cashAccountId, + Money expectedBalance, + Money actualBalance, + UserId reconciledByUserId, + DateTimeOffset reconciledAtUtc) + { + if (cashReconciliationId == Guid.Empty) + { + throw new ArgumentException("Cash reconciliation id is required.", nameof(cashReconciliationId)); + } + + if (cashSessionId == Guid.Empty) + { + throw new ArgumentException("Cash session id is required.", nameof(cashSessionId)); + } + + if (cashAccountId == Guid.Empty) + { + throw new ArgumentException("Cash account id is required.", nameof(cashAccountId)); + } + + var discrepancy = actualBalance.Subtract(expectedBalance); + var status = discrepancy.Amount == 0m + ? CashReconciliationStatus.Balanced + : CashReconciliationStatus.Discrepant; + + return new CashReconciliation( + cashReconciliationId, + tenantId, + cashSessionId, + cashAccountId, + expectedBalance, + actualBalance, + discrepancy, + status, + reconciledByUserId, + reconciledAtUtc); + } + + public CashReconciliation MarkExplained(CashDiscrepancy discrepancy) + { + if (Status == CashReconciliationStatus.Balanced) + { + throw new InvalidOperationException("Balanced reconciliation cannot be explained as discrepant."); + } + + if (discrepancy.CashReconciliationId != CashReconciliationId) + { + throw new InvalidOperationException("Discrepancy belongs to another reconciliation."); + } + + return this with { Status = CashReconciliationStatus.Explained }; + } + + public CashReconciliation MarkResolved() + { + if (Status == CashReconciliationStatus.Balanced) + { + throw new InvalidOperationException("Balanced reconciliation is already resolved by balance."); + } + + return this with { Status = CashReconciliationStatus.Resolved }; + } +} diff --git a/src/OpenCashFlow.Domain/Cash/CashReconciliationStatus.cs b/src/OpenCashFlow.Domain/Cash/CashReconciliationStatus.cs new file mode 100644 index 0000000..5140763 --- /dev/null +++ b/src/OpenCashFlow.Domain/Cash/CashReconciliationStatus.cs @@ -0,0 +1,9 @@ +namespace OpenCashFlow.Domain.Cash; + +public enum CashReconciliationStatus +{ + Balanced = 1, + Discrepant = 2, + Explained = 3, + Resolved = 4 +} diff --git a/src/OpenCashFlow.Domain/Cash/CashSession.cs b/src/OpenCashFlow.Domain/Cash/CashSession.cs new file mode 100644 index 0000000..76e8e8a --- /dev/null +++ b/src/OpenCashFlow.Domain/Cash/CashSession.cs @@ -0,0 +1,73 @@ +using OpenCashFlow.Domain.Common; + +namespace OpenCashFlow.Domain.Cash; + +public sealed record CashSession( + Guid CashSessionId, + TenantId TenantId, + Guid CashAccountId, + DateOnly BusinessDate, + Money OpeningExpectedBalance, + Money? OpeningActualBalance, + UserId OpenedByUserId, + DateTimeOffset OpenedAtUtc, + CashSessionStatus Status) +{ + public static CashSession Open( + Guid cashSessionId, + TenantId tenantId, + Guid cashAccountId, + DateOnly businessDate, + Money openingExpectedBalance, + Money? openingActualBalance, + UserId openedByUserId, + DateTimeOffset openedAtUtc) + { + if (cashSessionId == Guid.Empty) + { + throw new ArgumentException("Cash session id is required.", nameof(cashSessionId)); + } + + if (cashAccountId == Guid.Empty) + { + throw new ArgumentException("Cash account id is required.", nameof(cashAccountId)); + } + + return new CashSession( + cashSessionId, + tenantId, + cashAccountId, + businessDate, + openingExpectedBalance, + openingActualBalance, + openedByUserId, + openedAtUtc, + CashSessionStatus.Open); + } + + public Money CalculateExpectedBalance(IEnumerable movements) + { + var balance = OpeningExpectedBalance; + + foreach (var movement in movements) + { + if (movement.TenantId != TenantId || movement.CashAccountId != CashAccountId) + { + throw new InvalidOperationException("Cash session can only calculate movements from the same tenant and cash account."); + } + + if (movement.Status != CashMovementStatus.Posted) + { + continue; + } + + balance = movement.Direction == CashMovementDirection.Inflow + ? balance.Add(movement.Amount) + : balance.Subtract(movement.Amount); + } + + return balance; + } + + public CashSession WithStatus(CashSessionStatus status) => this with { Status = status }; +} diff --git a/src/OpenCashFlow.Domain/Cash/CashSessionStatus.cs b/src/OpenCashFlow.Domain/Cash/CashSessionStatus.cs new file mode 100644 index 0000000..a5a4581 --- /dev/null +++ b/src/OpenCashFlow.Domain/Cash/CashSessionStatus.cs @@ -0,0 +1,8 @@ +namespace OpenCashFlow.Domain.Cash; + +public enum CashSessionStatus +{ + Open = 1, + Reconciled = 2, + Closed = 3 +} diff --git a/src/OpenCashFlow.Domain/Cash/CashTransfer.cs b/src/OpenCashFlow.Domain/Cash/CashTransfer.cs new file mode 100644 index 0000000..5794953 --- /dev/null +++ b/src/OpenCashFlow.Domain/Cash/CashTransfer.cs @@ -0,0 +1,390 @@ +using OpenCashFlow.Domain.Common; + +namespace OpenCashFlow.Domain.Cash; + +public sealed record CashTransfer( + Guid CashTransferId, + TenantId TenantId, + Guid SourceCashAccountId, + Guid DestinationCashAccountId, + Money Amount, + string ReasonCategory, + string? ReasonText, + UserId RecordedByUserId, + UserId? ApprovedByUserId, + DateTimeOffset OccurredAtUtc, + DateTimeOffset? PostedAtUtc, + CashTransferStatus Status, + Guid? OriginalTransferId, + Guid? CorrectionGroupId) +{ + public static CashTransfer CreateDraft( + Guid cashTransferId, + CashAccount sourceAccount, + CashAccount destinationAccount, + Money amount, + string reasonCategory, + string? reasonText, + UserId recordedByUserId, + DateTimeOffset occurredAtUtc) + { + ValidateAccounts(sourceAccount, destinationAccount); + ValidateCore(cashTransferId, amount, reasonCategory); + + return new CashTransfer( + cashTransferId, + sourceAccount.TenantId, + sourceAccount.CashAccountId, + destinationAccount.CashAccountId, + amount, + reasonCategory.Trim(), + NormalizeOptional(reasonText), + recordedByUserId, + ApprovedByUserId: null, + occurredAtUtc, + PostedAtUtc: null, + CashTransferStatus.Draft, + OriginalTransferId: null, + CorrectionGroupId: null); + } + + public CashTransferPosting Post( + Guid sourceMovementId, + Guid destinationMovementId, + DateTimeOffset postedAtUtc, + UserId? approvedByUserId = null) + { + if (Status != CashTransferStatus.Draft) + { + throw new InvalidOperationException("Only draft cash transfers can be posted."); + } + + var postedTransfer = this with + { + Status = CashTransferStatus.Posted, + PostedAtUtc = postedAtUtc, + ApprovedByUserId = approvedByUserId + }; + + var sourceMovement = CashMovement.CreatePostedTransferSide( + sourceMovementId, + TenantId, + SourceCashAccountId, + Amount, + CashMovementDirection.Outflow, + CashTransferId, + CashTransferSide.Source, + ReasonCategory, + ReasonText, + RecordedByUserId, + approvedByUserId, + OccurredAtUtc, + postedAtUtc); + + var destinationMovement = CashMovement.CreatePostedTransferSide( + destinationMovementId, + TenantId, + DestinationCashAccountId, + Amount, + CashMovementDirection.Inflow, + CashTransferId, + CashTransferSide.Destination, + ReasonCategory, + ReasonText, + RecordedByUserId, + approvedByUserId, + OccurredAtUtc, + postedAtUtc); + + return new CashTransferPosting(postedTransfer, sourceMovement, destinationMovement); + } + + public CashTransferReversal CreatePostedReversal( + Guid reversalTransferId, + Guid sourceReversalMovementId, + Guid destinationReversalMovementId, + UserId recordedByUserId, + string reasonCategory, + string reasonText, + DateTimeOffset occurredAtUtc, + DateTimeOffset postedAtUtc) + { + if (Status != CashTransferStatus.Posted) + { + throw new InvalidOperationException("Only posted cash transfers can be reversed."); + } + + if (string.IsNullOrWhiteSpace(reasonText)) + { + throw new ArgumentException("Transfer reversal reason is required.", nameof(reasonText)); + } + + var correctionGroupId = CorrectionGroupId ?? Guid.NewGuid(); + var original = this with + { + Status = CashTransferStatus.Reversed, + CorrectionGroupId = correctionGroupId + }; + + var reversal = CreateRawPostedReversal( + reversalTransferId, + sourceAccountId: DestinationCashAccountId, + destinationAccountId: SourceCashAccountId, + amount: Amount, + tenantId: TenantId, + reasonCategory, + reasonText, + recordedByUserId, + occurredAtUtc, + postedAtUtc, + originalTransferId: CashTransferId, + correctionGroupId); + + var sourceMovement = CashMovement.CreatePostedTransferSide( + sourceReversalMovementId, + TenantId, + reversal.SourceCashAccountId, + Amount, + CashMovementDirection.Outflow, + reversal.CashTransferId, + CashTransferSide.Source, + reasonCategory, + reasonText, + recordedByUserId, + approvedByUserId: null, + occurredAtUtc, + postedAtUtc); + + var destinationMovement = CashMovement.CreatePostedTransferSide( + destinationReversalMovementId, + TenantId, + reversal.DestinationCashAccountId, + Amount, + CashMovementDirection.Inflow, + reversal.CashTransferId, + CashTransferSide.Destination, + reasonCategory, + reasonText, + recordedByUserId, + approvedByUserId: null, + occurredAtUtc, + postedAtUtc); + + return new CashTransferReversal(original, reversal, sourceMovement, destinationMovement); + } + + public CashTransferCorrection CreateCorrection( + Guid reversalTransferId, + Guid reversalSourceMovementId, + Guid reversalDestinationMovementId, + Guid replacementTransferId, + Guid replacementSourceMovementId, + Guid replacementDestinationMovementId, + CashAccount replacementSourceAccount, + CashAccount replacementDestinationAccount, + Money replacementAmount, + string reasonCategory, + string reasonText, + UserId recordedByUserId, + DateTimeOffset occurredAtUtc, + DateTimeOffset postedAtUtc) + { + if (Status != CashTransferStatus.Posted) + { + throw new InvalidOperationException("Only posted cash transfers can be corrected."); + } + + var correctionGroupId = Guid.NewGuid(); + var reversal = CreatePostedReversal( + reversalTransferId, + reversalSourceMovementId, + reversalDestinationMovementId, + recordedByUserId, + reasonCategory, + reasonText, + occurredAtUtc, + postedAtUtc); + + var original = reversal.Original with + { + Status = CashTransferStatus.Corrected, + CorrectionGroupId = correctionGroupId + }; + + var correctedReversal = reversal.Reversal with { CorrectionGroupId = correctionGroupId }; + var replacementPosting = CreateDraft( + replacementTransferId, + replacementSourceAccount, + replacementDestinationAccount, + replacementAmount, + reasonCategory, + reasonText, + recordedByUserId, + occurredAtUtc).Post( + replacementSourceMovementId, + replacementDestinationMovementId, + postedAtUtc) with + { + Transfer = CreateRawPostedReplacement( + replacementTransferId, + replacementSourceAccount, + replacementDestinationAccount, + replacementAmount, + reasonCategory, + reasonText, + recordedByUserId, + occurredAtUtc, + postedAtUtc, + originalTransferId: CashTransferId, + correctionGroupId) + }; + + return new CashTransferCorrection( + original, + correctedReversal, + replacementPosting.Transfer, + reversal.SourceMovement, + reversal.DestinationMovement, + replacementPosting.SourceMovement, + replacementPosting.DestinationMovement); + } + + public decimal CompanyLevelNetEffect => 0m; + + private static void ValidateAccounts(CashAccount sourceAccount, CashAccount destinationAccount) + { + if (sourceAccount.CashAccountId == Guid.Empty) + { + throw new ArgumentException("Source cash account id is required.", nameof(sourceAccount)); + } + + if (destinationAccount.CashAccountId == Guid.Empty) + { + throw new ArgumentException("Destination cash account id is required.", nameof(destinationAccount)); + } + + if (sourceAccount.CashAccountId == destinationAccount.CashAccountId) + { + throw new ArgumentException("Source and destination cash accounts cannot be the same.", nameof(destinationAccount)); + } + + if (sourceAccount.TenantId != destinationAccount.TenantId) + { + throw new InvalidOperationException("Cash transfer cannot cross tenants."); + } + + if (!string.Equals(sourceAccount.Currency, destinationAccount.Currency, StringComparison.Ordinal)) + { + throw new InvalidOperationException("Cash transfer cannot cross currencies in MVP."); + } + } + + private static void ValidateCore(Guid cashTransferId, Money amount, string reasonCategory) + { + if (cashTransferId == Guid.Empty) + { + throw new ArgumentException("Cash transfer id is required.", nameof(cashTransferId)); + } + + if (amount.Amount <= 0m) + { + throw new ArgumentOutOfRangeException(nameof(amount), amount.Amount, "Cash transfer amount must be greater than zero."); + } + + if (string.IsNullOrWhiteSpace(reasonCategory)) + { + throw new ArgumentException("Cash transfer reason/category is required.", nameof(reasonCategory)); + } + } + + private static CashTransfer CreateRawPostedReversal( + Guid cashTransferId, + Guid sourceAccountId, + Guid destinationAccountId, + Money amount, + TenantId tenantId, + string reasonCategory, + string reasonText, + UserId recordedByUserId, + DateTimeOffset occurredAtUtc, + DateTimeOffset postedAtUtc, + Guid originalTransferId, + Guid correctionGroupId) + { + ValidateCore(cashTransferId, amount, reasonCategory); + + return new CashTransfer( + cashTransferId, + tenantId, + sourceAccountId, + destinationAccountId, + amount, + reasonCategory.Trim(), + reasonText.Trim(), + recordedByUserId, + ApprovedByUserId: null, + occurredAtUtc, + postedAtUtc, + CashTransferStatus.Posted, + originalTransferId, + correctionGroupId); + } + + private static CashTransfer CreateRawPostedReplacement( + Guid cashTransferId, + CashAccount sourceAccount, + CashAccount destinationAccount, + Money amount, + string reasonCategory, + string reasonText, + UserId recordedByUserId, + DateTimeOffset occurredAtUtc, + DateTimeOffset postedAtUtc, + Guid originalTransferId, + Guid correctionGroupId) + { + ValidateAccounts(sourceAccount, destinationAccount); + ValidateCore(cashTransferId, amount, reasonCategory); + + return new CashTransfer( + cashTransferId, + sourceAccount.TenantId, + sourceAccount.CashAccountId, + destinationAccount.CashAccountId, + amount, + reasonCategory.Trim(), + reasonText.Trim(), + recordedByUserId, + ApprovedByUserId: null, + occurredAtUtc, + postedAtUtc, + CashTransferStatus.Posted, + originalTransferId, + correctionGroupId); + } + + private static string? NormalizeOptional(string? value) + { + return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + } +} + +public sealed record CashTransferPosting( + CashTransfer Transfer, + CashMovement SourceMovement, + CashMovement DestinationMovement); + +public sealed record CashTransferReversal( + CashTransfer Original, + CashTransfer Reversal, + CashMovement SourceMovement, + CashMovement DestinationMovement); + +public sealed record CashTransferCorrection( + CashTransfer Original, + CashTransfer Reversal, + CashTransfer Replacement, + CashMovement ReversalSourceMovement, + CashMovement ReversalDestinationMovement, + CashMovement ReplacementSourceMovement, + CashMovement ReplacementDestinationMovement); diff --git a/src/OpenCashFlow.Domain/Cash/CashTransferSide.cs b/src/OpenCashFlow.Domain/Cash/CashTransferSide.cs new file mode 100644 index 0000000..0b848d4 --- /dev/null +++ b/src/OpenCashFlow.Domain/Cash/CashTransferSide.cs @@ -0,0 +1,7 @@ +namespace OpenCashFlow.Domain.Cash; + +public enum CashTransferSide +{ + Source = 1, + Destination = 2 +} diff --git a/src/OpenCashFlow.Domain/Cash/CashTransferStatus.cs b/src/OpenCashFlow.Domain/Cash/CashTransferStatus.cs new file mode 100644 index 0000000..053810c --- /dev/null +++ b/src/OpenCashFlow.Domain/Cash/CashTransferStatus.cs @@ -0,0 +1,9 @@ +namespace OpenCashFlow.Domain.Cash; + +public enum CashTransferStatus +{ + Draft = 1, + Posted = 2, + Reversed = 3, + Corrected = 4 +} diff --git a/tests/OpenCashFlow.Domain.Tests/Cash/CashTransferTests.cs b/tests/OpenCashFlow.Domain.Tests/Cash/CashTransferTests.cs new file mode 100644 index 0000000..0b3f521 --- /dev/null +++ b/tests/OpenCashFlow.Domain.Tests/Cash/CashTransferTests.cs @@ -0,0 +1,224 @@ +using OpenCashFlow.Domain.Cash; +using OpenCashFlow.Domain.Common; + +namespace OpenCashFlow.Domain.Tests.Cash; + +public sealed class CashTransferTests +{ + private static readonly TenantId Tenant = TenantId.New(); + private static readonly UserId User = UserId.New(); + private static readonly DateTimeOffset OccurredAt = new(2026, 7, 9, 8, 0, 0, TimeSpan.Zero); + private static readonly DateTimeOffset PostedAt = new(2026, 7, 9, 8, 1, 0, TimeSpan.Zero); + + [Fact] + public void Create_rejects_same_source_and_destination_account() + { + var account = Account("Main cash"); + + var exception = Assert.Throws(() => + CashTransfer.CreateDraft( + Guid.NewGuid(), + account, + account, + Money.Positive(100m), + "employee-float", + "Assign float", + User, + OccurredAt)); + + Assert.Contains("cannot be the same", exception.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void Create_rejects_cross_tenant_transfer() + { + var source = Account("Main cash"); + var destination = Account("Other tenant", TenantId.New()); + + var exception = Assert.Throws(() => + CashTransfer.CreateDraft( + Guid.NewGuid(), + source, + destination, + Money.Positive(100m), + "employee-float", + null, + User, + OccurredAt)); + + Assert.Equal("Cash transfer cannot cross tenants.", exception.Message); + } + + [Fact] + public void Create_rejects_cross_currency_transfer() + { + var source = Account("Main cash", currency: "EUR"); + var destination = Account("USD box", currency: "USD"); + + var exception = Assert.Throws(() => + CashTransfer.CreateDraft( + Guid.NewGuid(), + source, + destination, + Money.Positive(100m, "EUR"), + "employee-float", + null, + User, + OccurredAt)); + + Assert.Equal("Cash transfer cannot cross currencies in MVP.", exception.Message); + } + + [Fact] + public void Create_rejects_non_positive_amount() + { + var source = Account("Main cash"); + var destination = Account("Employee float", type: CashAccountType.EmployeeFloat); + + var exception = Assert.Throws(() => + CashTransfer.CreateDraft( + Guid.NewGuid(), + source, + destination, + Money.From(0m), + "employee-float", + null, + User, + OccurredAt)); + + Assert.Contains("greater than zero", exception.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void Posted_transfer_produces_two_linked_movement_facts() + { + var source = Account("Main cash"); + var destination = Account("Employee float", type: CashAccountType.EmployeeFloat); + var transferId = Guid.NewGuid(); + + var posting = CashTransfer.CreateDraft( + transferId, + source, + destination, + Money.Positive(300m), + "employee-float", + "Assign float", + User, + OccurredAt).Post(Guid.NewGuid(), Guid.NewGuid(), PostedAt); + + Assert.Equal(CashTransferStatus.Posted, posting.Transfer.Status); + Assert.Equal(source.CashAccountId, posting.SourceMovement.CashAccountId); + Assert.Equal(destination.CashAccountId, posting.DestinationMovement.CashAccountId); + Assert.Equal(CashMovementDirection.Outflow, posting.SourceMovement.Direction); + Assert.Equal(CashMovementDirection.Inflow, posting.DestinationMovement.Direction); + Assert.Equal(transferId, posting.SourceMovement.CashTransferId); + Assert.Equal(transferId, posting.DestinationMovement.CashTransferId); + Assert.Equal(CashTransferSide.Source, posting.SourceMovement.TransferSide); + Assert.Equal(CashTransferSide.Destination, posting.DestinationMovement.TransferSide); + } + + [Fact] + public void Posted_transfer_nets_to_zero_at_company_level() + { + var source = Account("Main cash"); + var destination = Account("Bank", type: CashAccountType.Bank); + + var posting = CashTransfer.CreateDraft( + Guid.NewGuid(), + source, + destination, + Money.Positive(1_000m), + "bank-deposit", + "Deposit cash to bank", + User, + OccurredAt).Post(Guid.NewGuid(), Guid.NewGuid(), PostedAt); + + var movementNet = posting.SourceMovement.SignedAmount + posting.DestinationMovement.SignedAmount; + + Assert.Equal(0m, movementNet); + Assert.Equal(0m, posting.Transfer.CompanyLevelNetEffect); + } + + [Fact] + public void Reversal_reverses_both_transfer_sides() + { + var source = Account("Main cash"); + var destination = Account("Employee float", type: CashAccountType.EmployeeFloat); + var transfer = CashTransfer.CreateDraft( + Guid.NewGuid(), + source, + destination, + Money.Positive(200m), + "employee-float", + "Assign float", + User, + OccurredAt).Post(Guid.NewGuid(), Guid.NewGuid(), PostedAt).Transfer; + + var reversal = transfer.CreatePostedReversal( + Guid.NewGuid(), + Guid.NewGuid(), + Guid.NewGuid(), + User, + "reversal", + "Employee returned float", + OccurredAt.AddHours(1), + PostedAt.AddHours(1)); + + Assert.Equal(CashTransferStatus.Reversed, reversal.Original.Status); + Assert.Equal(transfer.CashTransferId, reversal.Reversal.OriginalTransferId); + Assert.Equal(destination.CashAccountId, reversal.SourceMovement.CashAccountId); + Assert.Equal(source.CashAccountId, reversal.DestinationMovement.CashAccountId); + Assert.Equal(CashMovementDirection.Outflow, reversal.SourceMovement.Direction); + Assert.Equal(CashMovementDirection.Inflow, reversal.DestinationMovement.Direction); + Assert.Equal(0m, reversal.SourceMovement.SignedAmount + reversal.DestinationMovement.SignedAmount); + } + + [Fact] + public void Correction_preserves_original_reversal_and_replacement_transfers() + { + var source = Account("Main cash"); + var destination = Account("Employee float", type: CashAccountType.EmployeeFloat); + var transfer = CashTransfer.CreateDraft( + Guid.NewGuid(), + source, + destination, + Money.Positive(200m), + "employee-float", + "Assign float", + User, + OccurredAt).Post(Guid.NewGuid(), Guid.NewGuid(), PostedAt).Transfer; + + var correction = transfer.CreateCorrection( + Guid.NewGuid(), + Guid.NewGuid(), + Guid.NewGuid(), + Guid.NewGuid(), + Guid.NewGuid(), + Guid.NewGuid(), + source, + destination, + Money.Positive(250m), + "correction", + "Correct float amount", + User, + OccurredAt.AddMinutes(10), + PostedAt.AddMinutes(10)); + + Assert.Equal(CashTransferStatus.Corrected, correction.Original.Status); + Assert.Equal(transfer.CashTransferId, correction.Reversal.OriginalTransferId); + Assert.Equal(transfer.CashTransferId, correction.Replacement.OriginalTransferId); + Assert.Equal(correction.Original.CorrectionGroupId, correction.Reversal.CorrectionGroupId); + Assert.Equal(correction.Original.CorrectionGroupId, correction.Replacement.CorrectionGroupId); + Assert.Equal(250m, correction.Replacement.Amount.Amount); + Assert.Equal(0m, correction.ReplacementSourceMovement.SignedAmount + correction.ReplacementDestinationMovement.SignedAmount); + } + + private static CashAccount Account( + string name, + TenantId? tenant = null, + string currency = "EUR", + CashAccountType type = CashAccountType.PhysicalCash) + { + return CashAccount.Create(Guid.NewGuid(), tenant ?? Tenant, name, type, currency); + } +} From c2235cc8cb16e398def88754144e6591a286bef8 Mon Sep 17 00:00:00 2001 From: Codewriter90x Date: Thu, 9 Jul 2026 17:03:19 +0200 Subject: [PATCH 08/10] docs(product): add remaining strategy and audit context --- .../cash-integrity-foundation-analysis.md | 963 +++++++++++ .../opencashflow-product-strategy-review.md | 1222 ++++++++++++++ .../opencashflow-customer-adoption-audit.md | 299 ++++ .../opencashflow-enterprise-cto-audit.md | 316 ++++ .../opencashflow-executive-board-review.md | 321 ++++ Docs/audits/opencashflow-maintainer-audit.md | 350 ++++ Docs/audits/opencashflow-security-review.md | 369 +++++ Docs/audits/opencashflow-sre-audit.md | 374 +++++ Docs/product/COMPETITOR_POSITIONING.md | 225 +++ Docs/product/FEATURE_FILTER.md | 154 ++ Docs/product/FIRST_DEMO.md | 183 +++ Docs/product/LEAN_ROADMAP.md | 472 ++++++ Docs/product/MONDAY_MORNING.md | 374 +++++ Docs/product/OWNER_DASHBOARD.md | 197 +++ Docs/product/POSITIONING.md | 149 ++ Docs/product/PRODUCT_DECISIONS.md | 138 ++ Docs/product/PRODUCT_MANIFESTO.md | 198 +++ Docs/product/PRODUCT_PRINCIPLES.md | 163 ++ Docs/product/PRODUCT_ROADMAP.md | 173 ++ Docs/product/PRODUCT_STRATEGY_SUMMARY.md | 192 +++ Docs/product/WEEKLY_CASH_RITUAL.md | 210 +++ Docs/product/WOW_FEATURE_DISCOVERY.md | 1425 +++++++++++++++++ 22 files changed, 8467 insertions(+) create mode 100644 Docs/analysis/cash-integrity-foundation-analysis.md create mode 100644 Docs/analysis/opencashflow-product-strategy-review.md create mode 100644 Docs/audits/opencashflow-customer-adoption-audit.md create mode 100644 Docs/audits/opencashflow-enterprise-cto-audit.md create mode 100644 Docs/audits/opencashflow-executive-board-review.md create mode 100644 Docs/audits/opencashflow-maintainer-audit.md create mode 100644 Docs/audits/opencashflow-security-review.md create mode 100644 Docs/audits/opencashflow-sre-audit.md create mode 100644 Docs/product/COMPETITOR_POSITIONING.md create mode 100644 Docs/product/FEATURE_FILTER.md create mode 100644 Docs/product/FIRST_DEMO.md create mode 100644 Docs/product/LEAN_ROADMAP.md create mode 100644 Docs/product/MONDAY_MORNING.md create mode 100644 Docs/product/OWNER_DASHBOARD.md create mode 100644 Docs/product/POSITIONING.md create mode 100644 Docs/product/PRODUCT_DECISIONS.md create mode 100644 Docs/product/PRODUCT_MANIFESTO.md create mode 100644 Docs/product/PRODUCT_PRINCIPLES.md create mode 100644 Docs/product/PRODUCT_ROADMAP.md create mode 100644 Docs/product/PRODUCT_STRATEGY_SUMMARY.md create mode 100644 Docs/product/WEEKLY_CASH_RITUAL.md create mode 100644 Docs/product/WOW_FEATURE_DISCOVERY.md diff --git a/Docs/analysis/cash-integrity-foundation-analysis.md b/Docs/analysis/cash-integrity-foundation-analysis.md new file mode 100644 index 0000000..c671aaa --- /dev/null +++ b/Docs/analysis/cash-integrity-foundation-analysis.md @@ -0,0 +1,963 @@ +# Cash Integrity Foundation Analysis + +Branch: `analysis/cash-integrity-foundation` + +Date: 2026-07-08 + +## Purpose + +OpenCashFlow is positioning itself as a cash cockpit. That positioning is not credible unless the system can prove the cash facts behind every cash decision. + +The minimum proof chain is: + +- who received or took money; +- how much money moved; +- why it moved; +- who entered it; +- who authorized it, if authorization is required; +- which cash source/account was affected; +- what the expected cash balance should be; +- whether counted or bank-reported cash matches the expected balance; +- where any difference comes from. + +This analysis intentionally does not implement production code. It evaluates the current model and defines the foundation required before forecast and Safe-to-Pay features can be treated as reliable. + +## Current State + +### Payment Flow + +Payment create/update/delete is now orchestrated in `OpenCashFlow.Application`. + +Observed behavior: + +- `CreatePaymentOrchestrator` validates payment input, checks idempotency by `RequestId`, validates payment method/document type, creates the payment, updates daily payment aggregation, applies cash ledger entries for cash-like methods, and writes payment audit. +- `UpdatePaymentOrchestrator` updates payment state, reverses/reapplies daily aggregation, updates/voids/reapplies cash ledger effects when the payment method or amount changes, and writes payment audit. +- `DeletePaymentOrchestrator` soft-deletes the payment, removes daily aggregation effect, voids cash ledger effect for cash-like methods, and writes payment audit. +- Cash-like detection is based on a system payment method id (`00000000-0000-0000-0000-000000000002`) or payment method names `Cash` / `Contanti`. +- Payment persistence stores `TenantID`, `Amount`, `EntryType`, `PaymentMethodID`, `DocumentTypeID`, `Description`, `UserID`, `CreatedBy`, `EditedBy`, soft-delete fields and timestamps. + +What this gives today: + +- a payment can identify tenant, amount, direction, method, document type, description and user; +- cash-like payments produce ledger deltas; +- create/update/delete are transaction-scoped through `IUnitOfWork`; +- audit exists for payment create/update/delete. + +What it does not give today: + +- a distinct employee/cash handler separate from the actor who entered the payment; +- an approver; +- an explicit cash source/account; +- a posted/immutable cash movement concept; +- a daily cash session; +- actual cash count; +- reconciliation status; +- discrepancy explanation; +- reversal-first correction policy. + +### Cash Ledger + +Current persistence entities: + +- `CashBalance` + - key: `CompanyId`; + - fields: `Balance`, `LastUpdatedUtc`, `RowVersion`; + - represents one current balance per company. + +- `CashLedger` + - fields: `CompanyId`, `RefType`, `RefId`, `OriginalPaymentId`, `Delta`, `Reason`, `CreatedBy`, `CreatedAtUtc`; + - stores ledger deltas for payment events, voids, reapplications, refunds and manual adjustments. + +Current infrastructure behavior: + +- cash balance is incrementally updated when a ledger delta is inserted; +- duplicate ledger effects are suppressed by `(CompanyId, RefType, RefId)` checks; +- cash balance rebuild recalculates `CashBalance.Balance` from sum of `CashLedgers.Delta`; +- `CashWriter` uses a local transaction when there is no ambient EF transaction; +- `CashWriter` retries once on `DbUpdateConcurrencyException`; +- `AdminAdjustAsync` requires a non-empty reason; +- payment-driven ledger entries usually do not store a reason. + +What this gives today: + +- a deterministic company-level running cash balance; +- a ledger of deltas from payment and adjustment events; +- basic idempotence for ref-based ledger writes; +- a repair path through rebuild. + +What it does not give today: + +- multiple cash accounts/sources; +- opening balance by day/source; +- closing balance by day/source; +- counted actual balance; +- reconciliation discrepancy; +- required reason for every movement; +- employee receiver/taker; +- approval metadata; +- immutable posting state; +- explicit reversal/correction entries; +- durable link from ledger entry to business reason except for manual adjustments. + +### Daily Payment Aggregation + +Current `Payment_DailyPayments` stores: + +- `TenantID`; +- `CashDate`; +- `Total`. + +It is updated from payment create/update/delete. This is a payment reporting aggregate, not a cash integrity or reconciliation model. + +Limitations: + +- uses `double` while the ledger uses `decimal`; +- represents daily net payment total, not expected cash by source; +- does not store opening balance, closing count, discrepancy or close status; +- cannot explain differences. + +### Cash Admin API And WebApp + +Current API endpoint area: + +- `GET /v1/admin/cash/current`; +- `GET /v1/admin/cash/ledger`; +- `POST /v1/admin/cash/adjust`; +- `POST /v1/admin/cash/rebuild`; +- `GET /v1/admin/cash/ledger/export`. + +Authorization: + +- `CashAdminController` is restricted to `CompanyAdmin,InstanceAdmin`; +- non-instance users are constrained to the `TenantID` claim; +- `InstanceAdmin` must provide `companyId`. + +Current WebApp view: + +- displays current cash balance; +- lists ledger rows; +- allows administrators to add a positive/negative adjustment with reason; +- attempts a non-blocking user-id to employee-name lookup for display. + +What is missing in the UI: + +- employee cash movement entry; +- daily opening balance; +- daily closing count; +- reconciliation screen; +- discrepancy workflow; +- approval workflow; +- movement reversal/correction screen; +- per-source cash view. + +### Audit + +Current audit model: + +- `Admin_AuditLog` stores generic event type, resource, resource id, action, user id, username, changes JSON, IP/user-agent, timestamp, severity, additional info and tenant id. + +Current payment audit: + +- payment created; +- payment updated; +- payment deleted. + +Current cash admin audit: + +- manual adjustment; +- rebuild; +- export. + +Limitations: + +- cash ledger entries are not audited as first-class cash movement events; +- audit does not encode cash source, receiver/taker, approver or reconciliation context; +- audit is not yet the canonical integrity trail for cash lifecycle events. + +## Core Scenario Evaluation + +Scenario: + +- Employee X receives 200 EUR for `rinnovo K`. +- Employee Z receives 100 EUR for `acquisto F`. +- Expected end-of-day cash movement is 300 EUR. +- Actual cash count is compared with expected cash. + +Current system can partially prove: + +- two cash-like payments can be recorded with amounts 200 and 100; +- each payment can store a description such as `rinnovo K` or `acquisto F`; +- each payment has a tenant and `UserID`; +- cash-like payment creation can write cash ledger deltas; +- the company cash balance can become 300 if both are inflows and no other movements exist; +- payment create audit can be written. + +Current system cannot fully prove: + +- that Employee X physically received exactly 200; +- that Employee Z physically received exactly 100; +- whether `UserID` means receiver, operator, creator or owner of the payment; +- which cash box, bank account, till or source was affected; +- who approved the cash movement; +- that the movement is immutable after posting; +- opening cash for the day; +- expected cash for the day by source; +- counted actual cash; +- balanced vs discrepant reconciliation; +- why a discrepancy exists; +- whether discrepancy remains open or has been resolved. + +Verdict: the current system is a useful payment/cash-ledger foundation, but it cannot yet prove the full cash integrity scenario. + +## Gap Analysis + +### Already Implemented + +- Tenant-scoped payments. +- Payment create/update/delete use cases in Application. +- Transaction boundary through `IUnitOfWork` for payment create/update/delete. +- Cash-like payment ledger effect. +- Company-level current cash balance. +- Ledger append for payment/void/reapply/update/adjustment. +- Cash balance rebuild from ledger deltas. +- Admin cash adjustment with required reason. +- Cash admin API authorization for `CompanyAdmin` and `InstanceAdmin`. +- Tenant claim isolation in cash admin endpoints. +- Generic audit log. +- Payment create/update/delete audit. +- Cash adjustment/rebuild/export audit at API level. +- Tests for payment orchestrator cash ledger application/void/update paths. +- Domain tests for simple cash balance arithmetic. + +### Partially Implemented + +- Actor tracking: `UserID`, `CreatedBy` and ledger `CreatedBy` exist, but semantics are ambiguous. +- Reason tracking: payment description and adjustment reason exist, but reason is not mandatory for every cash movement. +- Ledger determinism: deltas are deterministic for payment flows, but there is no explicit posting/correction policy. +- Daily totals: `Payment_DailyPayments` exists, but it is not a reconciliation or cash-day model. +- Audit trail: payment and cash admin actions are audited, but cash movements are not modeled as auditable lifecycle events. +- Authorization: admin cash endpoints are role-protected, but there is no domain-level permission model for cashier vs reconciler vs approver. + +### Missing + +- `CashAccount` / cash source. +- `CashMovement` aggregate. +- `CashSession` or `CashDay`. +- Opening cash balance per day/source. +- Actual counted balance. +- End-of-day reconciliation. +- Reconciliation discrepancy. +- Discrepancy explanation workflow. +- Approval metadata. +- Cash receiver/taker field distinct from creator/actor. +- Movement status (`Draft`, `Posted`, `Reversed`, `Corrected`). +- Reversal/correction model. +- Cash movement audit as first-class use case. +- API contracts for movement entry, expected balance, close day, reconcile, explain discrepancy, reverse/correct. +- WebApp screens for the daily cash workflow. +- Heavy tenant/authorization/reconciliation tests. + +### Risky Or Ambiguous + +- Payment uses `double` in persistence while Application records and cash ledger use `decimal`. +- Cash-like method detection depends on fixed id/name aliases rather than a durable method classification. +- `CashLedger.RefType` is a string with implicit values. +- `CashLedger.Reason` is nullable. +- `CashLedger.CreatedBy` is a string rather than a typed user reference. +- A payment can be updated or soft-deleted after creating cash effects; ledger compensates, but the product has not defined whether posted cash movement editing should be allowed. +- `Payment_DailyPayments.Total` can drift semantically from actual cash because it is a reporting aggregate, not a reconciliation source. +- Cash balance is company-wide, which is insufficient for multiple cash boxes, registers, banks or employee-held cash. +- Manual adjustment can explain a delta but does not create a structured discrepancy lifecycle. + +### Overcomplicated + +- Existing payment update/delete cash reapplication logic is necessary for current behavior, but it is compensating for the absence of a first-class immutable cash movement model. +- Daily payment aggregation and cash ledger are separate but similarly named concepts; this can confuse future forecast/reconciliation work. +- Cash admin UI mixes operational adjustment and ledger inspection, but not daily workflow. + +### Needs Product Decision + +- Is `Payment.UserID` the employee who received/took money, the creator, or the payment owner? +- Must every cash movement be approved? +- Which roles can create, approve, reverse and reconcile? +- Can cash be negative? +- Should posted movements be immutable? +- Are corrections always reversal-based? +- How many cash accounts/sources are needed in MVP: one default cash box, multiple cash boxes, bank accounts, employee wallets? +- Does bank account reconciliation belong in MVP, or only physical cash? +- Should cash ledger include bank-like movements, or should bank reconciliation be separate? +- Is end-of-day reconciliation mandatory before forecasting is considered reliable? + +## Cash Integrity Invariants + +These invariants should become explicit product and domain rules. + +1. Every cash movement is tenant-scoped. +2. Every cash movement affects exactly one cash account/source. +3. Every cash movement has a signed monetary delta using decimal precision. +4. Every cash movement has a direction or type derived from the delta. +5. Every cash movement has a required business reason/category. +6. Every cash movement identifies the actor who entered it. +7. Every cash movement identifies the employee/cash handler who received or took the money when that differs from the actor. +8. Every posted movement is immutable. +9. Changes to posted movements happen through reversal/correction entries. +10. Every reversal references the original movement. +11. Every correction preserves the original movement, the reversing movement and the corrected movement. +12. Every movement has a timestamp generated by the system. +13. Every movement has an audit event. +14. Every reconciliation has an opening expected balance. +15. Every reconciliation has a calculated expected balance. +16. Every reconciliation records actual counted balance or imported bank balance. +17. Every discrepancy remains visible until explained or resolved. +18. Discrepancy explanations are audit logged. +19. A daily cash session cannot be silently overwritten. +20. Tenant isolation must be enforced in read, write, correction and reconciliation paths. +21. Unauthorized roles cannot create, reverse, approve or reconcile movements. +22. Forecast and Safe-to-Pay must not treat unreconciled cash as high-confidence cash. + +## Minimum Product Workflow + +### Morning + +1. A company admin or authorized cashier opens the daily cash session. +2. The system shows yesterday's closing balance as today's opening expected balance. +3. If no previous closing exists, the user records an opening cash count with reason. +4. The opening balance is locked for the session. + +### During The Day + +1. Employee receives or takes money. +2. User records: + - employee/cash handler; + - amount; + - direction; + - reason/category; + - optional payment/document reference; + - cash account/source. +3. System validates authorization and tenant. +4. Movement is posted. +5. Ledger entry is appended. +6. Expected cash balance updates. +7. Audit event is written. + +### Evening + +1. Authorized user starts reconciliation for the cash account/source. +2. System calculates expected balance: + - opening balance; + - plus posted inflows; + - minus posted outflows; + - plus/minus corrections. +3. User enters actual counted cash or imported bank balance. +4. System calculates discrepancy. +5. If discrepancy is zero, session can close as balanced. +6. If discrepancy is non-zero, discrepancy remains open until explained. +7. Explanation is required before marking as explained/resolved. +8. Close/reconcile action is audit logged. + +## Proposed Model + +### CashAccount + +Represents a source of cash/bank value. + +Suggested fields: + +- `CashAccountId`; +- `TenantId`; +- `Name`; +- `Type` (`PhysicalCash`, `Bank`, `EmployeeFloat`, `Other`); +- `Currency`; +- `IsDefault`; +- `IsActive`; +- `CreatedBy`; +- `CreatedAtUtc`; +- `ClosedAtUtc`. + +MVP can start with one default physical cash account per company, but the model should not block multiple accounts later. + +### CashMovement + +Canonical business movement. + +Suggested fields: + +- `CashMovementId`; +- `TenantId`; +- `CashAccountId`; +- `Amount`; +- `Direction`; +- `ReasonCode`; +- `ReasonText`; +- `EmployeeId` / `CashHandlerUserId`; +- `EnteredByUserId`; +- `ApprovedByUserId`; +- `PaymentId`; +- `DocumentTypeId`; +- `OccurredAtUtc`; +- `PostedAtUtc`; +- `Status` (`Draft`, `Posted`, `Reversed`, `Corrected`); +- `OriginalMovementId`; +- `CorrectionGroupId`; +- `IdempotencyKey`. + +### CashLedgerEntry + +Append-only accounting effect of a posted movement. + +Suggested fields: + +- `CashLedgerEntryId`; +- `TenantId`; +- `CashAccountId`; +- `CashMovementId`; +- `Delta`; +- `BalanceAfter`; +- `CreatedAtUtc`. + +`CashLedger` should stop being the only cash business model. It should become the append-only effect log of posted movements. + +### CashSession / CashDay + +Represents a daily operating period per cash account. + +Suggested fields: + +- `CashSessionId`; +- `TenantId`; +- `CashAccountId`; +- `BusinessDate`; +- `OpeningExpectedBalance`; +- `OpeningActualBalance`; +- `OpenedByUserId`; +- `OpenedAtUtc`; +- `ClosedByUserId`; +- `ClosedAtUtc`; +- `Status` (`Open`, `Reconciled`, `ClosedWithDiscrepancy`). + +### Reconciliation + +Represents expected vs actual comparison. + +Suggested fields: + +- `ReconciliationId`; +- `TenantId`; +- `CashSessionId`; +- `CashAccountId`; +- `ExpectedBalance`; +- `ActualBalance`; +- `Discrepancy`; +- `Status` (`Balanced`, `Discrepant`, `Explained`, `Resolved`); +- `ReconciledByUserId`; +- `ReconciledAtUtc`. + +### ReconciliationDiscrepancy + +Structured discrepancy explanation. + +Suggested fields: + +- `DiscrepancyId`; +- `ReconciliationId`; +- `Amount`; +- `Explanation`; +- `Category`; +- `CreatedByUserId`; +- `CreatedAtUtc`; +- `ResolvedByUserId`; +- `ResolvedAtUtc`. + +### AuditEvent + +Current `Admin_AuditLog` can be reused initially, but cash integrity should introduce semantic audit writes for: + +- movement created; +- movement posted; +- movement reversed; +- movement corrected; +- session opened; +- session reconciled; +- discrepancy recorded; +- discrepancy resolved; +- export. + +## Proposed Application Use Cases + +Cash movement: + +- `CreateCashMovementUseCase`; +- `PostCashMovementUseCase`; +- `ReverseCashMovementUseCase`; +- `CorrectCashMovementUseCase`; +- `GetCashMovementDetailUseCase`; +- `GetCashMovementsByDayUseCase`. + +Cash account: + +- `GetCashAccountsUseCase`; +- `CreateCashAccountUseCase`; +- `DeactivateCashAccountUseCase`. + +Cash session: + +- `OpenCashSessionUseCase`; +- `GetCashSessionUseCase`; +- `GetDailyExpectedCashUseCase`; +- `CloseCashSessionUseCase`. + +Reconciliation: + +- `StartCashReconciliationUseCase`; +- `RecordActualCashCountUseCase`; +- `ExplainCashDiscrepancyUseCase`; +- `ResolveCashDiscrepancyUseCase`; +- `GetCashReconciliationStatusUseCase`. + +Audit: + +- `ICashAuditWriter`; +- cash event records in Application. + +## Proposed API Contracts + +Initial API should be explicit and deterministic. + +### Cash Accounts + +- `GET /v1/cash/accounts` +- `POST /v1/cash/accounts` +- `PATCH /v1/cash/accounts/{cashAccountId}/deactivate` + +### Cash Movements + +- `POST /v1/cash/movements` +- `GET /v1/cash/movements?date=&cashAccountId=&employeeId=` +- `GET /v1/cash/movements/{cashMovementId}` +- `POST /v1/cash/movements/{cashMovementId}/reverse` +- `POST /v1/cash/movements/{cashMovementId}/correct` + +### Daily Cash + +- `POST /v1/cash/sessions/open` +- `GET /v1/cash/sessions/current` +- `GET /v1/cash/sessions/by-date?date=&cashAccountId=` +- `GET /v1/cash/sessions/{cashSessionId}/expected-balance` +- `POST /v1/cash/sessions/{cashSessionId}/close` + +### Reconciliation + +- `POST /v1/cash/sessions/{cashSessionId}/reconcile` +- `GET /v1/cash/sessions/{cashSessionId}/reconciliation` +- `POST /v1/cash/reconciliations/{reconciliationId}/discrepancies` +- `PATCH /v1/cash/reconciliations/{reconciliationId}/discrepancies/{discrepancyId}/resolve` + +### Audit + +- `GET /v1/cash/movements/{cashMovementId}/audit` +- `GET /v1/cash/sessions/{cashSessionId}/audit` + +## Proposed WebApp Screens + +### Employee Cash Movement Entry + +Purpose: quick entry during operations. + +Fields: + +- employee/cash handler; +- cash account/source; +- amount; +- direction; +- reason/category; +- free-text note; +- optional payment/document reference. + +### Daily Cash Movements + +Purpose: review all movements for a day. + +Shows: + +- opening expected balance; +- movement list; +- employee summaries; +- cash account/source filters; +- current expected balance. + +### End-Of-Day Reconciliation + +Purpose: close the day. + +Shows: + +- expected cash; +- actual counted cash input; +- calculated discrepancy; +- close as balanced or record discrepancy. + +### Discrepancy View + +Purpose: keep differences visible. + +Shows: + +- discrepancy amount; +- source/day; +- linked movements; +- explanation status; +- explanation history; +- resolution status. + +### Employee Money Received Summary + +Purpose: prove employee-level handling. + +Shows: + +- employee; +- amounts received/taken by day; +- reasons; +- linked cash movements; +- reconciled/unreconciled status. + +### Audit/History View + +Purpose: prove lifecycle. + +Shows: + +- movement creation/posting/reversal/correction; +- session open/close; +- reconciliation events; +- discrepancy explanation/resolution. + +## Test Plan + +### Domain Unit Tests + +- Cash movement requires tenant. +- Cash movement requires cash account. +- Cash movement requires amount not zero. +- Cash movement requires reason/category. +- Cash movement requires actor. +- Cash movement requires cash handler when movement type needs it. +- Posted movement cannot be edited. +- Reversal references original movement. +- Correction preserves original and creates reversal/new movement. +- Expected balance equals opening balance plus posted deltas. +- Discrepancy equals actual minus expected. +- Balanced reconciliation requires discrepancy zero. +- Discrepant reconciliation cannot be hidden without explanation. + +### Application Use Case Tests + +- Create cash movement validates tenant, account, user, employee and reason. +- Post movement appends ledger and writes audit in transaction. +- Reverse movement creates reversal and audit. +- Correct movement creates reversal and replacement movement. +- Open session derives opening balance from prior close. +- Reconcile session calculates expected balance. +- Reconcile balanced closes as balanced. +- Reconcile discrepant leaves discrepancy open. +- Explain discrepancy records explanation and audit. +- Unauthorized role cannot create/reconcile/reverse. +- Cross-tenant access is rejected. + +### API Tests + +- `POST /cash/movements` returns 201 for valid movement. +- Missing amount/reason/account returns 400. +- Cross-tenant account returns 403/404 consistently. +- Unauthorized role returns 403. +- Posted movement update endpoint does not exist or returns deterministic rejection. +- Reversal endpoint creates reversal, not physical delete. +- Reconciliation balanced returns expected status. +- Reconciliation discrepant returns discrepancy detail. +- Discrepancy explanation is visible on subsequent reads. + +### Database Integration Tests + +- Movement requires existing tenant/company. +- Movement requires existing cash account. +- Movement requires existing actor/user. +- Movement requires existing employee/cash handler if provided. +- Ledger entry requires movement. +- Reconciliation requires session. +- Discrepancy requires reconciliation. +- Unique session per tenant/account/business date. +- Reversal references original movement. +- Cash ledger balance after concurrent movement posting remains deterministic. + +### Tenant Isolation Tests + +- Tenant A cannot list Tenant B cash accounts. +- Tenant A cannot create movement in Tenant B account. +- Tenant A cannot reverse Tenant B movement. +- Tenant A cannot reconcile Tenant B session. +- Tenant A cannot read Tenant B audit trail. + +### Authorization Tests + +- Employee/cashier can create movement only if product allows it. +- Employee/cashier cannot reconcile. +- CompanyAdmin can reconcile. +- InstanceAdmin requires explicit company context. +- Unauthorized roles cannot export cash ledger. + +### Immutability And Reversal Tests + +- Posted movement edit is rejected. +- Posted movement delete is rejected. +- Correction creates reversal and replacement. +- Audit includes original/reversal/replacement ids. +- Original movement remains visible. + +### End-Of-Day Reconciliation Tests + +- Session opening from prior close. +- Expected balance from opening + movements. +- Actual count zero with expected non-zero creates discrepancy. +- Balanced close prevents further movement unless next session opens or explicit late adjustment policy is used. + +### Audit Trail Tests + +- Movement created audit. +- Movement posted audit. +- Movement reversed audit. +- Movement corrected audit. +- Reconciliation audit. +- Discrepancy explanation audit. +- Export audit. + +## Concrete Test Scenarios + +### Scenario A - Balanced Day + +Given: + +- opening cash: 0; +- employee X receives 200 for `rinnovo K`; +- employee Z receives 100 for `acquisto F`. + +Expected: + +- two posted movements exist; +- X is linked to the 200 movement; +- Z is linked to the 100 movement; +- reasons are stored; +- expected cash is 300; +- actual cash count is 300; +- reconciliation status is `Balanced`; +- audit trail includes movement and reconciliation events. + +### Scenario B - Discrepancy + +Given: + +- same movements as Scenario A; +- actual cash count is 250. + +Expected: + +- expected cash is 300; +- actual cash is 250; +- discrepancy is -50; +- reconciliation status is `Discrepant`; +- discrepancy remains visible until explained; +- forecast/Safe-to-Pay confidence is degraded. + +### Scenario C - Posted Movement Edit + +Given: + +- a posted movement exists. + +Expected: + +- direct edit is rejected; +- or system creates reversal/correction entries; +- original movement remains visible. + +### Scenario D - Cross-Tenant Access + +Given: + +- Tenant A employee attempts to see or alter Tenant B movements. + +Expected: + +- read/write/reverse/reconcile actions are rejected; +- no data leaks in response shape. + +### Scenario E - Unauthorized Role + +Given: + +- user without cash permission attempts movement creation or reconciliation. + +Expected: + +- action returns 403; +- no ledger entry is written; +- optional security audit is written. + +### Scenario F - Delete Posted Movement + +Given: + +- posted movement exists. + +Expected: + +- physical delete is forbidden; +- soft delete is also forbidden for posted cash effect; +- reversal is the only correction path. + +### Scenario G - Correction Audit + +Given: + +- movement 200 was posted but should be 180. + +Expected: + +- original 200 remains; +- reversal -200 is created; +- corrected +180 is created; +- expected balance changes by -20; +- audit links all three records. + +## Forecast And Safe-To-Pay Status + +Forecast and Safe-to-Pay should remain WIP/Experimental until Cash Integrity is implemented. + +Reason: + +- current cash balance can be calculated, but not reconciled; +- expected cash cannot be tied to actual counted/bank balance; +- discrepancies cannot be tracked or explained; +- cash confidence cannot be scored; +- user/employee/cash source semantics are ambiguous. + +Safe-to-Pay can be prototyped as a product concept, but it should not present cash recommendations as reliable until: + +1. cash movement model exists; +2. daily reconciliation exists; +3. discrepancy status feeds forecast confidence; +4. tenant/authorization/immutability tests are green. + +## Release Blockers For Cash Integrity + +These are blockers before cash forecasting can be marketed as trustworthy: + +1. No cash account/source model. +2. No cash movement aggregate. +3. No explicit employee cash handler field. +4. No immutable posted movement policy. +5. No reversal/correction model. +6. No daily cash session. +7. No actual-vs-expected reconciliation. +8. No discrepancy workflow. +9. No cash movement audit lifecycle. +10. No heavy tenant/authorization/reconciliation test suite. + +## Recommended Implementation PR Sequence + +### PR 1 - Cash Integrity Product Decision Record + +Document decisions: + +- cash account scope for MVP; +- movement roles; +- authorization roles; +- immutable/reversal policy; +- reconciliation requirements. + +No schema changes. + +### PR 2 - Domain Model And Application Contracts + +Add neutral domain/application records: + +- `CashAccount`; +- `CashMovement`; +- `CashSession`; +- `CashReconciliation`; +- `CashDiscrepancy`; +- reversal/correction concepts. + +Add domain tests for invariants. + +### PR 3 - Persistence Schema And Repositories + +Add EF entities and migrations for: + +- cash accounts; +- cash movements; +- sessions; +- reconciliations; +- discrepancies. + +Add database integration tests. + +### PR 4 - Movement Use Cases + +Implement: + +- create/post movement; +- list by day; +- reverse/correct movement; +- audit writer. + +Add application and API tests. + +### PR 5 - Daily Session And Reconciliation Use Cases + +Implement: + +- open session; +- expected balance; +- reconcile actual count; +- discrepancy explanation/resolution. + +Add application/API/database tests. + +### PR 6 - WebApp Daily Cash Workflow + +Add screens: + +- movement entry; +- daily movement list; +- end-of-day reconciliation; +- discrepancy view. + +Keep current cash ledger admin page as an admin/diagnostic view until replaced. + +### PR 7 - Forecast Confidence Gate + +Connect reconciliation status to future forecast/Safe-to-Pay confidence: + +- unreconciled cash lowers confidence; +- open discrepancy creates warning; +- balanced sessions can be used as high-confidence actuals. + +## Final Assessment + +The current system can partially support the sample scenario as payments and ledger entries, but it cannot prove the scenario at cash-integrity level. + +It can show that: + +- two cash-like payments were entered; +- their amounts affected company cash balance; +- payment audit exists. + +It cannot prove that: + +- specific employees physically received the money; +- a specific cash source was affected; +- the day opened and closed correctly; +- actual cash matched expected cash; +- discrepancies were explained. + +Therefore Cash Integrity should be treated as a foundational product track, and forecast/Safe-to-Pay features should remain WIP/Experimental until the cash movement, reconciliation and audit model is complete. diff --git a/Docs/analysis/opencashflow-product-strategy-review.md b/Docs/analysis/opencashflow-product-strategy-review.md new file mode 100644 index 0000000..c86b4b0 --- /dev/null +++ b/Docs/analysis/opencashflow-product-strategy-review.md @@ -0,0 +1,1222 @@ +# OpenCashFlow Product Strategy Review + +Date: 2026-07-08 + +Perspective: product strategist, SaaS founder, ERP consultant, and software investor. + +Scope: product strategy only. This review intentionally assumes engineering quality, architecture, CI, tests, +documentation, repository hygiene, and governance are already good enough. It does not review code. + +Core question: + +> What should OpenCashFlow become in order to be the obvious choice over competing self-hosted business management +> software? + +## Executive Answer + +OpenCashFlow should not try to become another ERP. + +The self-hosted business software market already has broad ERP suites, accounting tools, invoicing tools, and personal +finance tools. OpenCashFlow will not win by being a smaller Odoo, a smaller ERPNext, a smaller Dolibarr, or a weaker +Akaunting. + +The winning position is: + +> The self-hosted cash command center for small operators who run businesses from bank accounts, spreadsheets, invoices, +> supplier payments, and gut feel, but do not want to adopt a full ERP. + +That means OpenCashFlow should become the product that answers: + +- How much cash do we really have? +- What money is coming in? +- What money is going out? +- What happens if a big customer pays late? +- Can we make payroll, suppliers, taxes, rent, loan payments, and materials purchases this month? +- Which commitments are real, forecasted, overdue, or risky? +- What should the owner do this week? + +If OpenCashFlow becomes "cash visibility and short-term survival planning for small businesses", it has a reason to +exist. + +If it becomes "generic ERP modules", it loses. + +## Product Positioning + +## Current Positioning + +Current product statement: + +- open-source; +- self-hosted; +- cash-flow management; +- payments; +- cash ledger; +- company setup; +- users/roles; +- audit; +- basic reporting. + +This is clear but not yet sharp. + +It explains category and ownership model, but it does not yet create a memorable reason to switch. A small business owner +will ask: + +- Is this accounting? +- Is this invoicing? +- Is this ERP? +- Is this budgeting? +- Is this just a ledger? +- Why not use Odoo, ERPNext, Dolibarr, Akaunting, Invoice Ninja, Excel, or my accountant? + +Today, the answer is not strong enough. + +## Better Positioning + +Recommended positioning: + +> OpenCashFlow is a self-hosted cash visibility and working-capital control system for small businesses that need to know +> what cash is available, what is due, what is late, and what decisions must be made before liquidity becomes a problem. + +Shorter: + +> Self-hosted cash control for small businesses. + +Sharper: + +> The cash cockpit for owner-operated businesses. + +More operational: + +> Know what cash you have, what cash is coming, what cash is leaving, and what can break next. + +## Product Category + +Do not call it a full ERP. + +Do not call it accounting software. + +Do not call it budgeting software. + +Recommended category: + +> Cash-flow operations system. + +This creates space between: + +- accounting software, which records financial truth; +- ERP, which manages the whole business; +- invoicing tools, which bill customers; +- personal finance tools, which manage household budgets; +- spreadsheets, which are flexible but fragile. + +## Target Customer + +Best initial customer: + +Small, owner-operated companies with real operational cash pressure: + +- workshops; +- manufacturers under 100 employees; +- distributors; +- field-service companies; +- accounting studios managing many small clients; +- consultants with contractors and recurring receivables; +- small companies leaving expensive SaaS tools; +- companies that already use accounting software but still run cash planning in spreadsheets. + +This market does not want abstract "finance analytics". It wants to survive next month. + +## Differentiation + +## Competitive Reality + +OpenCashFlow enters a crowded market. + +Observable competitor positioning: + +- ERPNext presents itself as a comprehensive open-source ERP with accounting, procurement, sales, CRM, stock, + manufacturing, projects, POS, HR/payroll, support, no-code builder, hosting, marketplace, customer stories and broad + business scope. +- Odoo is a broad business app suite with CRM, accounting, eCommerce, inventory, manufacturing, HR, project management, + point of sale, website and a large app ecosystem. +- Dolibarr is a modular ERP/CRM for small businesses and freelancers, with modules for commercial operations, products, + stock, bank accounts, invoices, payments and more. +- Akaunting targets small-business accounting, invoicing, expenses and online/cloud accounting workflows. +- Invoice Ninja targets invoicing, payments, clients, quotes, expenses and billing operations. +- Firefly III and Actual Budget are closer to personal finance/budgeting than business ERP, but they compete for + self-hosted finance mindshare. + +Against that field, OpenCashFlow cannot win on breadth. + +It must win on focus. + +## Why Would Someone Choose OpenCashFlow? + +Today, most buyers would not choose it over the established alternatives unless they specifically want: + +- self-hosted; +- narrow cash-flow tracking; +- simpler setup than a full ERP; +- AGPL/community alignment; +- a product focused on cash rather than accounting compliance. + +That is a real wedge, but it is not yet enough. + +To become the obvious choice, OpenCashFlow must promise something competitors do not prioritize: + +> We do not run your whole business. We keep your cash decisions sane. + +## Competitor-by-Competitor Positioning + +## Odoo + +Odoo is broad, mature, modular, commercial, and ecosystem-driven. + +Why choose Odoo: + +- need ERP breadth; +- need CRM, inventory, manufacturing, accounting, sales, website, HR, POS; +- need partner ecosystem; +- accept complexity or vendor/commercial model. + +Why choose OpenCashFlow instead: + +- you do not want ERP implementation; +- you do not want to model every business process; +- you need owner-level liquidity visibility fast; +- you want a small self-hosted system dedicated to cash decisions. + +Strategic lesson: + +Do not out-Odoo Odoo. Become the lightweight cash cockpit Odoo users still need because ERP reports are too slow, +generic, or accounting-centered. + +## ERPNext + +ERPNext is the hardest competitor for self-hosted/open-source business management. It has broad modules, manufacturing, +stock, accounting, procurement, CRM, HR, hosting, documentation, customers and ecosystem. + +Why choose ERPNext: + +- need full ERP; +- manufacturing, stock, procurement, sales and accounting must live in one system; +- willing to adopt Frappe/ERPNext operating model. + +Why choose OpenCashFlow instead: + +- ERPNext is too large for the problem; +- the company already has accounting/inventory tools; +- the owner wants a cash-control layer, not a business operating system migration. + +Strategic lesson: + +OpenCashFlow should integrate around ERPs, not replace them. + +## Dolibarr + +Dolibarr is simple, modular ERP/CRM for small businesses. + +Why choose Dolibarr: + +- want a lightweight ERP/CRM; +- need invoicing, products, stock, bank accounts, commercial workflows; +- want a broad but simple modular suite. + +Why choose OpenCashFlow instead: + +- cash-flow visibility is the main pain; +- the business does not need CRM/product/catalog modules; +- owner wants daily cash decisions, not a suite. + +Strategic lesson: + +OpenCashFlow must be even simpler and more decision-oriented than Dolibarr. + +## Akaunting + +Akaunting focuses on accounting, invoicing and expenses for small businesses. + +Why choose Akaunting: + +- need small-business accounting; +- need invoices, expenses, financial records; +- want accounting-like workflows. + +Why choose OpenCashFlow instead: + +- accounting system already exists; +- accountant owns compliance; +- owner needs operational cash runway and commitments; +- focus is "what can we pay?" rather than "what does the ledger say?" + +Strategic lesson: + +OpenCashFlow should not become accounting. It should connect accounting reality to owner decisions. + +## Invoice Ninja + +Invoice Ninja is strong for billing, clients, quotes, invoices and payment collection. + +Why choose Invoice Ninja: + +- invoicing is the main problem; +- client billing and online payment collection matter; +- service businesses need quote-to-cash. + +Why choose OpenCashFlow instead: + +- cash planning includes suppliers, taxes, payroll, bank balances, loans and inventory purchases; +- invoices are only one signal; +- the business needs a cash calendar, not only invoicing. + +Strategic lesson: + +OpenCashFlow should ingest invoice data but not compete as an invoicing-first product. + +## Firefly III + +Firefly III is excellent self-hosted personal finance software. + +Why choose Firefly III: + +- personal budgets; +- household finances; +- individual transaction tracking. + +Why choose OpenCashFlow instead: + +- multi-user business workflows; +- company roles; +- approval, audit, cash commitments, invoices, customers, suppliers. + +Strategic lesson: + +OpenCashFlow should borrow personal-finance usability but stay business-focused. + +## Actual Budget + +Actual Budget is local-first budgeting. + +Why choose Actual Budget: + +- personal or household budgeting; +- envelope-style planning; +- simple local-first finance. + +Why choose OpenCashFlow instead: + +- business cash-flow operations; +- team roles; +- receivables/payables; +- company-level cash planning. + +Strategic lesson: + +Actual wins on simplicity. OpenCashFlow should learn from that: fewer modules, better daily workflow. + +## Differentiation Thesis + +The winning sentence: + +> OpenCashFlow is not your ERP or your accounting system. It is the self-hosted cash decision layer that tells small +> business owners what cash is safe, what cash is committed, and what cash is at risk. + +## Feature Gaps Ranked By Adoption Impact + +## 1. Cash Forecast Calendar + +Highest-value missing feature. + +Users need a calendar showing: + +- expected incoming payments; +- expected outgoing payments; +- overdue receivables; +- overdue payables; +- payroll dates; +- rent, loans, taxes; +- supplier commitments; +- projected cash balance by day/week/month. + +This is the heart of the product. + +Without it, OpenCashFlow is a ledger. + +With it, OpenCashFlow becomes a decision system. + +## 2. Accounts Receivable And Accounts Payable Lite + +Not full accounting. + +Needed: + +- customer invoices imported or entered as expected receivables; +- supplier bills imported or entered as expected payables; +- due dates; +- partial payment tracking; +- overdue aging; +- promised payment date; +- confidence level; +- cash impact. + +This is the bridge between payments and forecast. + +## 3. Bank Import And Reconciliation + +Adoption requires reducing manual data entry. + +Minimum: + +- CSV import from banks; +- rule-based categorization; +- match imported bank movements to expected payments; +- unmatched transactions queue; +- duplicate detection. + +Later: + +- open banking integrations by country; +- Plaid/GoCardless/TrueLayer/Stripe Financial Connections depending on market. + +Self-hosted buyers will tolerate CSV first if the workflow is clean. + +## 4. Cash Risk Alerts + +The product should tell the owner what matters. + +Examples: + +- "You will go negative in 12 days if customer X pays late." +- "Payroll and VAT are due in the same week." +- "Three supplier payments exceed available safe cash." +- "This purchase order consumes 40% of free cash." +- "Your top customer is 19 days late." + +This is where OpenCashFlow becomes memorable. + +## 5. Scenario Planning + +Simple what-if planning: + +- customer pays 15 days late; +- buy machine/materials now versus next month; +- supplier payment split; +- hire contractor; +- tax bill due; +- loan repayment starts. + +This should be visual and simple, not a financial modeling tool. + +## 6. Owner Dashboard + +Not a generic dashboard. + +It should show: + +- cash now; +- safe cash; +- committed cash; +- cash runway; +- top overdue receivables; +- next seven critical outflows; +- worst-case balance; +- actions needed today. + +The dashboard must be the product. + +## 7. Accountant/Advisor Workspace + +Accounting firms are a strong channel. + +Needed: + +- multi-client view; +- client cash health summary; +- invite client; +- notes/recommendations; +- export pack for accountant; +- monthly cash review workflow. + +This creates distribution. + +## 8. Integrations And Importers + +Do not build full modules first. Build importers. + +Priority: + +- CSV bank import; +- generic invoice CSV import; +- Odoo export import; +- ERPNext export import; +- Invoice Ninja export import; +- Akaunting export import; +- QuickBooks/Xero export import only if legally and commercially practical. + +The message: + +> Keep your current system. Use OpenCashFlow for cash decisions. + +## 9. Approval And Commitment Workflow + +Small businesses need control before cash leaves. + +Features: + +- planned outgoing payment; +- requested payment; +- approved payment; +- scheduled payment; +- paid; +- cancelled/deferred. + +This is especially valuable for workshops and manufacturers. + +## 10. Manufacturing/Workshop Cash Pack + +Avoid full manufacturing ERP. + +Create a cash-focused pack: + +- job/project cash estimate; +- materials commitment; +- deposit received; +- supplier due dates; +- customer milestone payments; +- margin/cash exposure by job; +- cash impact of work in progress. + +This is differentiated and useful. + +## Unique Selling Proposition + +## If OpenCashFlow Disappeared Tomorrow, What Would The World Lose? + +Today: + +Probably not much unique. + +The world already has ERPNext, Odoo, Dolibarr, Akaunting, Invoice Ninja, Firefly III, Actual Budget, spreadsheets and +accountants. + +OpenCashFlow's current uniqueness is mostly: + +- self-hosted; +- narrow cash-flow focus; +- clean modern stack; +- AGPL; +- early promise. + +That is not enough. + +## What It Should Become + +The world would lose something if OpenCashFlow became: + +> The simplest self-hosted operational cash cockpit for businesses that do not want a full ERP. + +Unique promise: + +- not accounting; +- not ERP; +- not personal budgeting; +- not invoicing; +- not a spreadsheet; +- not a SaaS lock-in; +- specifically for operational cash decisions. + +Potential slogan: + +> Your cash truth, before the bank surprises you. + +Better B2B slogan: + +> Self-hosted cash control for owner-led businesses. + +Sharper manufacturing slogan: + +> See whether jobs, suppliers, payroll and late invoices will break your cash before they do. + +## Market Strategy + +## Who Should Not Use OpenCashFlow + +Do not target: + +- companies needing full double-entry accounting; +- companies needing tax-certified accounting; +- companies needing payroll; +- companies needing full ERP/MRP immediately; +- companies needing point-of-sale; +- companies needing e-commerce; +- companies needing built-in banking automation from day one; +- enterprises with mature treasury systems; +- consumers managing household budgets; +- teams unwilling to self-host or pay for managed hosting. + +Saying no is important. + +## Who Should Definitely Use OpenCashFlow + +Best-fit users: + +1. Owner-led small companies with 5-100 staff. +2. Workshops and light manufacturers with deposits, supplier purchases and late customer payments. +3. Distributors managing cash between inventory purchases and receivables. +4. Accounting firms advising small businesses on cash planning. +5. Consultants with contractor payouts and delayed receivables. +6. Small companies that use invoicing/accounting software but still forecast in Excel. +7. Privacy-conscious companies that want self-hosted business finance tools. + +## Initial Beachhead + +Recommended first niche: + +> Small manufacturers and workshops that already have accounting/invoicing but still manage cash manually. + +Why: + +- real pain; +- cash timing matters; +- ERP adoption is expensive; +- accounting reports are lagging indicators; +- bank balance alone is misleading; +- owners will pay for clarity; +- consultants/accountants can help sell it. + +## Monetization Without Changing AGPL + +## Enterprise Support + +Viable: yes. + +What to sell: + +- installation support; +- upgrade support; +- incident response; +- prioritized bug fixes; +- security advisories; +- backup/restore validation; +- email support. + +Best for: + +- companies self-hosting; +- accounting firms; +- IT providers. + +Risk: + +- requires operational maturity and response capacity. + +Recommendation: + +Launch after stable release, not before. + +## Hosted Services + +Viable: yes, but dangerous too early. + +What to sell: + +- managed OpenCashFlow hosting; +- automatic backups; +- upgrades; +- email delivery; +- secure reverse proxy; +- monitoring. + +Best for: + +- companies that want self-hosted values but not server maintenance. + +Risk: + +- turns the project into an ops company; +- creates customer data liability; +- requires security, backups, support and compliance. + +Recommendation: + +Offer later as "managed hosting" after the product has stable operational runbooks. + +## Consulting + +Viable: yes, immediately. + +What to sell: + +- cash-flow workflow design; +- migration from spreadsheets; +- setup for accounting firms; +- import templates; +- training; +- custom reports. + +Best for: + +- early revenue; +- learning customer workflows; +- product discovery. + +Risk: + +- can become service business distraction. + +Recommendation: + +Use consulting deliberately to discover repeatable product patterns. + +## Onboarding + +Viable: yes. + +Productized onboarding packages: + +- "Spreadsheet to OpenCashFlow"; +- "30-day cash control setup"; +- "Accounting firm client onboarding"; +- "Workshop cash calendar setup". + +Recommendation: + +This is the best early monetization path. + +## Migration Services + +Viable: high. + +Migration from: + +- Excel/Google Sheets; +- Invoice Ninja exports; +- Akaunting exports; +- Odoo/ERPNext CSV exports; +- bank CSV history. + +Recommendation: + +Build repeatable import templates and charge for assisted migration. + +## Plugins + +Viable: later. + +Potential paid/official plugins: + +- bank connector packs; +- country-specific tax calendar packs; +- manufacturing/workshop pack; +- accountant dashboard pack; +- forecasting pack; +- advanced reporting pack. + +Risk: + +- plugin marketplace too early creates complexity. + +Recommendation: + +Start with official modules, not an open marketplace. + +## Certified Builds + +Viable: yes. + +What to sell: + +- signed releases; +- supported Docker images; +- security-patched builds; +- compatibility matrix; +- upgrade assurance. + +This fits AGPL and self-hosted buyers. + +Recommendation: + +Strong long-term monetization. Needs release discipline. + +## Training + +Viable: yes. + +Formats: + +- owner cash-control course; +- accountant advisory course; +- self-hosted administrator course; +- workshop/manufacturing cash planning course. + +Recommendation: + +Useful after product-market fit. Do not overbuild training before the workflow is proven. + +## Support Subscriptions + +Viable: yes. + +Tiers: + +- Community: forum/GitHub only. +- Professional: business-hours support. +- Business: priority support plus upgrade help. +- Partner: accounting firm/multi-client support. + +Recommendation: + +Tie subscriptions to outcome: "keep your cash system running and upgraded." + +## Growth Strategy + +## First 100 Users + +Goal: + +Get real workflows, not vanity stars. + +Actions: + +1. Pick one niche: workshops/light manufacturers. +2. Create a landing page around "cash forecast for small workshops". +3. Publish a demo dataset showing late customer, supplier bills, payroll and projected cash shortfall. +4. Build CSV import from bank and invoices. +5. Offer free assisted setup to 10-20 companies. +6. Interview every user weekly. +7. Publish one honest case study. +8. Create templates: + - cash calendar; + - supplier payments; + - customer receivables; + - workshop job cash plan. + +Success metric: + +10 companies use it weekly to make cash decisions. + +## First 1,000 Users + +Goal: + +Become a known self-hosted cash-flow tool. + +Actions: + +1. Launch stable `1.0`. +2. Provide one-command Docker install. +3. Create hosted demo. +4. Publish migration templates for Excel, Invoice Ninja, Akaunting, Odoo and ERPNext. +5. Build accountant workspace. +6. Partner with small accounting firms and fractional CFOs. +7. Publish comparison pages: + - OpenCashFlow vs spreadsheets; + - OpenCashFlow vs Odoo for cash planning; + - OpenCashFlow vs ERPNext for non-ERP companies; + - OpenCashFlow vs accounting software. +8. Create YouTube/live walkthroughs: + - "Run your weekly cash meeting in 15 minutes"; + - "How to stop being surprised by supplier payments"; + - "Cash forecast for workshops." + +Success metric: + +100 active instances and 20 accounting/consulting partners. + +## First 10,000 Users + +Goal: + +Own the self-hosted cash-operations category. + +Actions: + +1. Launch managed hosting. +2. Launch certified builds. +3. Build official modules: + - forecasting; + - accountant/advisor portal; + - workshop/manufacturing cash pack; + - bank connectors; + - advanced reports. +4. Create marketplace/partner ecosystem only after official modules prove demand. +5. Build import/connectors ecosystem. +6. Publish anonymized benchmark reports: + - average DSO by business type; + - cash runway patterns; + - supplier pressure signals. +7. Create partner certification for accountants and implementation consultants. +8. Localize for key regions. + +Success metric: + +1,000 active instances, repeatable revenue, partner channel, recognized category. + +## Community Strategy + +## Who Contributes? + +Likely contributors: + +- self-hosters who need a lightweight cash tool; +- accountants/fractional CFOs who want templates; +- .NET developers who dislike PHP/Python ERP stacks; +- small-business owners with technical ability; +- consultants building integrations; +- localization contributors; +- people who want to replace spreadsheets. + +## What Motivates Them? + +Motivators: + +- "I need this for my business." +- "I want a self-hosted alternative." +- "I can build a connector for my local bank/accounting system." +- "I can help with localization." +- "I can build templates for my industry." +- "I want to sell services around it." + +## How To Build Contributors + +Do this: + +1. Publish a product manifesto. +2. Create a "cash-flow templates" contribution path. +3. Create sample data packs by industry. +4. Add import connector contribution docs. +5. Maintain a public module roadmap. +6. Run monthly roadmap calls. +7. Create "good first workflow" issues, not only code issues. +8. Showcase community templates and integrations. +9. Accept non-code contributions: cash plans, workflows, translations, docs, sample reports. +10. Build partner pages for accountants/consultants. + +Do not do this too early: + +- broad plugin marketplace; +- complex governance; +- enterprise certification; +- full ERP module explosion. + +## New Product Roadmap + +Ignore the current engineering roadmap. This is the product roadmap. + +## Next Month + +Theme: find the wedge. + +1. Rewrite product positioning: + - "Self-hosted cash control for small businesses." + - "The cash cockpit for owner-led companies." +2. Build a demo dataset: + - workshop/manufacturer; + - late customer; + - supplier payments; + - payroll; + - material purchase; + - projected cash shortfall. +3. Build the owner dashboard: + - cash now; + - committed cash; + - expected cash; + - cash runway; + - next risky outflows. +4. Create manual CSV import for: + - bank movements; + - receivables; + - payables. +5. Publish one landing page: + - "Stop running cash flow from spreadsheets." +6. Talk to 20 target users. + +Do not build ERP modules this month. + +## Next Quarter + +Theme: make cash forecast useful every week. + +1. Cash forecast calendar. +2. Receivables/payables lite. +3. Overdue aging. +4. Payment confidence levels. +5. Scenario planning v1. +6. Weekly cash meeting view. +7. Bank CSV reconciliation. +8. Accountant export pack. +9. First industry template: workshop/light manufacturing. +10. First case study from a real pilot. + +Success metric: + +5 businesses use it weekly for real cash planning. + +## Next 6 Months + +Theme: build adoption loops. + +1. Accountant/advisor workspace. +2. Multi-client dashboard for accounting firms. +3. Import templates for Odoo, ERPNext, Dolibarr, Akaunting, Invoice Ninja and spreadsheets. +4. Alerts: + - late customer risk; + - negative cash forecast; + - tax/payroll collision; + - supplier payment overload. +5. Workshop cash pack: + - job cash exposure; + - deposits; + - materials commitments; + - milestone payments. +6. Hosted demo. +7. Stable `1.0` candidate if product workflow is proven. +8. Partner program for accountants/consultants. + +Success metric: + +50 active companies or advisors using it monthly. + +## Next Year + +Theme: become the self-hosted cash operations category leader. + +1. Stable release. +2. Certified Docker builds. +3. Managed hosting beta. +4. Support subscriptions. +5. Bank connector strategy by region. +6. Official forecasting module. +7. Official accountant module. +8. Official workshop/manufacturing module. +9. Localization for first target countries. +10. Content engine: + - cash planning guides; + - workshop financial survival playbooks; + - accountant advisory templates. + +Success metric: + +1,000 users, 100 active instances, 20 partners. + +## Next 3 Years + +Theme: own the operational cash layer. + +1. OpenCashFlow Cloud for managed hosting. +2. Certified self-hosted builds. +3. Partner marketplace. +4. Official regional compliance calendars. +5. Bank integrations by country. +6. AI-assisted cash risk explanation, not AI accounting. +7. Benchmarking and anonymized operational insights. +8. API ecosystem for ERPs/accounting tools. +9. Partner certification. +10. Recognized as the default self-hosted cash cockpit for small businesses. + +Success metric: + +10,000 users, 1,000 active installations, sustainable revenue from hosting/support/migration/training/modules. + +## Brutal Truth + +The biggest risk is not technical. + +The biggest risk is that OpenCashFlow becomes "yet another business app" with no urgent reason to exist. + +Small businesses already have: + +- spreadsheets; +- accountants; +- bank portals; +- Odoo; +- ERPNext; +- Dolibarr; +- Akaunting; +- Invoice Ninja; +- QuickBooks/Xero exports; +- generic dashboards. + +They do not wake up wanting "open-source cash-flow management software". + +They wake up worried about: + +- payroll; +- late customers; +- supplier pressure; +- tax bills; +- buying materials; +- low bank balance; +- whether one bad month breaks the company. + +If OpenCashFlow is positioned as software, it is weak. + +If OpenCashFlow is positioned as "the weekly cash decision ritual for owner-led companies", it can win. + +The product must create a habit: + +> Every Monday morning, the owner opens OpenCashFlow before making payment decisions. + +If that habit does not exist, adoption will be shallow. + +## Investment Decision + +Would I invest my own money? + +Not yet as a venture-style bet. + +I would invest time or a small angel/check only if the founder committed to a sharp wedge: + +- cash cockpit; +- workshops/manufacturing/distributors; +- weekly cash decision workflow; +- integrations/imports, not full ERP; +- support/migration/hosting revenue; +- accountant partner channel. + +I would not invest in "open-source ERP alternative". That market is already crowded and brutally hard. + +I would invest in: + +> Open-source, self-hosted cash intelligence for small operators who are too complex for spreadsheets and too small for +> ERP implementation. + +Investment thesis: + +- pain is real; +- SaaS fatigue and self-hosting interest exist; +- accountants/consultants can distribute it; +- AGPL core plus services/hosting/support can work; +- focus can beat breadth. + +Investment blocker: + +No product proof yet. The positioning and workflow must be validated with real small businesses. + +## Strategic Recommendations + +## 1. Stop Saying "Business Management" + +Business management is too broad. + +Say: + +> Cash-flow operations. + +## 2. Do Not Build Full Accounting + +Accounting is compliance-heavy and region-specific. + +Integrate with accounting. Do not replace it. + +## 3. Do Not Build Full ERP + +ERP means years of scope. + +OpenCashFlow should be the layer above bank/accounting/ERP that helps owners make cash decisions. + +## 4. Build Around Weekly Workflow + +Product loop: + +1. Import bank/invoices/payables. +2. Review forecast. +3. Resolve alerts. +4. Decide what to pay. +5. Update expected dates. +6. Export/share with accountant. +7. Repeat next week. + +This is more important than modules. + +## 5. Sell To Accountants And Fractional CFOs + +They already advise multiple companies. + +They have distribution. + +They feel the spreadsheet pain. + +They can onboard clients. + +## 6. Make The Demo Unforgettable + +Demo should show: + +- bank balance looks healthy; +- late customer creates future cash crisis; +- supplier and payroll collide; +- scenario planning shows fix; +- owner takes action. + +That demo sells the product better than feature lists. + +## 7. Create A New Roadmap Page For Product, Not Engineering + +Separate: + +- engineering readiness roadmap; +- product adoption roadmap; +- module roadmap. + +Customers buy product outcomes, not architecture. + +## 8. Make OpenCashFlow Complement Existing Systems + +Position: + +> Keep your accounting software. Keep your invoices. Use OpenCashFlow to see cash risk before it hurts. + +## 9. Build Importers Before Integrations + +CSV importers get adoption faster than API integrations. + +Start ugly but useful. + +## 10. Win One Niche Before Expanding + +Recommended niche: + +> Light manufacturing and workshops with supplier payments, customer deposits and late receivables. + +If OpenCashFlow wins there, it can expand to distributors, consultants and accounting firms. + +## Product Scorecard + +| Area | Score | Notes | +| --- | ---: | --- | +| Current positioning | 4/10 | Clear category, weak urgency and differentiation. | +| Differentiation | 3/10 today, 8/10 possible | Must become cash cockpit, not mini ERP. | +| Feature focus | 5/10 | Core exists, but missing forecast/reconciliation/alerts. | +| Market wedge | 6/10 | Workshops/manufacturing/accountants are plausible. | +| Monetization potential | 7/10 | Support, onboarding, hosting, certified builds and modules fit AGPL. | +| Community potential | 6/10 | Strong if templates/importers/localization become contribution paths. | +| Competitive threat | 8/10 high | Broad ERPs and accounting tools already own mindshare. | +| Investment attractiveness | 5/10 now, 8/10 if wedge validated | Needs proof of weekly usage habit. | + +## Final Recommendation + +OpenCashFlow should become: + +> The self-hosted cash cockpit for small businesses that need weekly cash decisions, not a full ERP migration. + +The next product milestone should not be "more modules". + +The next product milestone should be: + +> A workshop owner can import bank data, enter expected receivables/payables, see a 90-day cash forecast, identify a cash +> risk, run a scenario, and decide what to pay this week. + +That is the wedge. + +Everything else should wait. + +## Public Sources Consulted + +- ERPNext official site: https://erpnext.com/ +- Dolibarr official site: https://www.dolibarr.org/ +- Akaunting official site: https://akaunting.com/ +- Odoo overview source surfaced during review: https://en.wikipedia.org/wiki/Odoo +- Invoice Ninja official site: https://www.invoiceninja.com/ +- Firefly III official site: https://www.firefly-iii.org/ +- Actual Budget official site: https://actualbudget.org/ diff --git a/Docs/audits/opencashflow-customer-adoption-audit.md b/Docs/audits/opencashflow-customer-adoption-audit.md new file mode 100644 index 0000000..8acfd66 --- /dev/null +++ b/Docs/audits/opencashflow-customer-adoption-audit.md @@ -0,0 +1,299 @@ +# OpenCashFlow Customer Adoption Audit + +Date: 2026-07-08 + +Perspective: CTO of a manufacturing company looking for a self-hosted cash-flow management system. + +Scope: first visit to the GitHub repository. Observable evidence only. This review intentionally ignores code style and focuses on trust, professionalism, documentation, roadmap, maintenance signals, risk and adoption confidence. + +## Executive Customer Verdict + +I would not migrate company financial data into OpenCashFlow today. + +I would keep it on a shortlist for technical evaluation because the maintainers are unusually honest about project status, risks and production blockers. The repository reads like a serious Developer Preview, not like abandonware or a toy. It has a clear README, roadmap, security policy, contribution guide, changelog, Docker quickstart, tests, quality gates, smoke testing and backup/restore documentation. + +However, as a manufacturing company CTO, I would not ask my CEO to approve operational use yet. The project explicitly says it is not production-ready. It has no stable release, no proven upgrade/migration drill, no production-ready deployment model, no support offering, no published SLA, and no evidence that production restore, security hardening or long-term maintenance have been proven with real customer data. + +Decision: evaluate only with synthetic data. Do not adopt for live finance operations yet. + +## First Customer Impression + +The repository answers the basic customer questions quickly: + +- what it is: self-hosted cash-flow management; +- who it is for: small companies, consultants, accounting studios and teams; +- what it is not: not a production-ready accounting suite, not fiscal/tax/payroll/invoicing certification, not SaaS/Stripe dependent; +- maturity: Developer Preview / Early Self-Hosted Preview; +- how to run: .NET 10 and Docker Compose quickstart; +- license: AGPL-3.0. + +That clarity builds trust. The project does not oversell itself. + +The same honesty also blocks adoption. A customer seeking a stable cash-flow system will immediately see repeated warnings that this should not be used for business-critical financial operations. + +## Would I Trust These Developers? + +Partially, for evaluation. + +Trust-building evidence: + +- README is explicit that the project is not production-ready. +- Roadmap lists hardening work rather than pretending it is done. +- Security docs describe dependency hygiene, headers, CSP, auth, tenant isolation and remaining security-sensitive areas. +- Contributing docs set boundaries and discourage random framework churn. +- Test backlog says skipped tests are currently zero in the compiled suite. +- Backup/restore docs distinguish local proof from production proof. +- Upgrade/migration docs explicitly say the drill is not fully performed. + +Trust-reducing evidence: + +- There is no stable release. +- There is no commercial support statement. +- There is no documented maintainer support model. +- There is no production reference deployment. +- Some docs still mention historical/migration-era concerns. +- Repository hygiene is not perfect; for example, `Docs/.DS_Store` is still present in the current checkout. + +Customer trust score: moderate for engineering honesty, low for business dependency. + +## Would I Believe This Project Will Still Exist In Three Years? + +I cannot conclude that from the repository alone. + +Positive signals: + +- The roadmap is coherent and conservative. +- The project has open-source packaging: contributing guide, code of conduct, changelog, security policy and issue/PR templates. +- The architecture and documentation suggest sustained engineering effort. +- The project has moved to .NET 10 and has active hardening work documented. + +Missing signals: + +- no stable releases; +- no release cadence; +- no public governance model; +- no named maintainers/support commitments in the reviewed docs; +- no adoption/community evidence visible from the local repository; +- no long-term support policy; +- no public funding/support model beyond repository metadata. + +Three-year continuity confidence: uncertain. + +## Would I Migrate Company Data Into It? + +No. + +Reason: + +- README explicitly says not to use it for regulated or business-critical financial operations. +- Production hardening docs list blockers before production. +- Backup/restore is proven only against a disposable local smoke stack. +- Upgrade/migration drill is explicitly not fully performed. +- There is no stable release or migration compatibility promise. +- Support and recovery obligations are not defined. + +I would only load synthetic or anonymized sample data for evaluation. + +## Would I Convince My CEO To Approve It? + +Not for production. + +I could justify a small engineering evaluation if the business wants a self-hosted cash-flow option and accepts that this is a preview. I would not present it to the CEO as a deployable finance system. + +CEO-facing message: + +> OpenCashFlow is promising and transparent, but it is not ready to hold our real financial data. We can evaluate it in a lab. We should not depend on it operationally until stable releases, support, backup/restore, upgrade and security evidence improve. + +## Would I Pay For Support? + +Not yet. + +I might pay for a professional assessment, implementation spike or hardening engagement if the maintainers offered it. I would not pay recurring production support until: + +- stable release exists; +- support terms and SLAs are published; +- upgrade policy is proven; +- production deployment guide is validated; +- backup/restore and incident response are production-ready; +- security review/pentest status is clear. + +## Would I Recommend It To Another Company? + +I would recommend it only as an open-source project to watch or evaluate. + +I would not recommend it as an operational accounting/cash-flow system for another company’s real data. + +## Trust + +Strengths: + +- Honest maturity statement. +- Clear self-hosted scope. +- Clear warning against production use. +- Good open-source docs. +- Visible testing and quality-gate culture. + +Weaknesses: + +- No stable release. +- No support model. +- No production readiness proof. +- No customer references or adoption signal in the repository. +- No demonstrated three-year maintenance guarantee. + +Trust score: 5.5/10. + +## Professionalism + +The public presentation is professional for a Developer Preview: + +- polished README; +- roadmap; +- security policy; +- contributing guide; +- changelog; +- code of conduct; +- quality gates; +- testing docs; +- operational docs. + +Professionalism is reduced by: + +- developer-preview caveats throughout; +- migration/hardening docs that show substantial work remains; +- a small repository hygiene issue (`Docs/.DS_Store`); +- no stable releases. + +Professionalism score: 7/10 for a preview, 4/10 for a vendor-grade product. + +## Documentation + +Documentation is a strength. + +Customer-useful docs: + +- README explains project status and scope. +- `Docs/ROADMAP.md` is conservative and clear. +- `Docs/installation.md` exists. +- `Docs/ops/production-hardening.md` explains local defaults versus production. +- `Docs/ops/backup-restore-drill.md` documents a real local drill. +- `Docs/ops/upgrade-migration-drill.md` documents what is not yet proven. +- `SECURITY.md` explains security posture and reporting. + +Documentation gap: + +- no customer-oriented “Can I use this in production?” decision guide beyond warnings; +- no support/SLA document; +- no migration guide between releases because stable releases do not exist; +- no production reference architecture validated end-to-end. + +Documentation score: 7/10. + +## Roadmap + +The roadmap is credible because it is conservative. It does not promise magic. It names hard work: + +- database integration migration; +- production hardening; +- backup/restore drill; +- upgrade/migration drill; +- GitHub alert closeout; +- frontend/static asset cleanup; +- Infrastructure/Contracts boundary reduction. + +Customer concern: + +Most roadmap items are things I would want completed before production adoption. That means the roadmap is useful, but it also confirms the product is not ready for my company’s financial operations. + +Roadmap score: 6/10. + +## Signals Of Long-Term Maintenance + +Positive: + +- .NET 10 baseline; +- active security/dependency documentation; +- quality-gate workflow; +- ZAP workflows; +- tests and smoke scripts; +- open-source project files. + +Negative or missing: + +- no stable release; +- no release cadence; +- no LTS/support policy; +- no clear maintainer roster; +- no public support channel policy beyond general contribution/security docs; +- no evidence of commercial backing in the repository; +- no customer adoption references. + +Maintenance confidence score: 4.5/10. + +## Risk + +Customer adoption risks: + +1. Data migration risk: no stable release or proven upgrade path. +2. Recovery risk: local backup/restore proof exists, but production backup/restore is not proven. +3. Security risk: auth, tenant isolation, PIN/fast-login and reset password are still called out as stabilization areas. +4. Operational risk: production deployment docs are requirements, not a validated production deployment. +5. Support risk: no SLA or support model. +6. Continuity risk: no evidence from the repository that the project will be maintained for three years. +7. Compliance risk: README says it is not certified fiscal, tax, payroll or invoicing software. + +Risk score: 7/10 high for production adoption, acceptable for lab evaluation. + +## Confidence + +Confidence as a codebase worth evaluating: moderate-high. + +Confidence as a system to run a manufacturing company’s cash-flow data today: low. + +Confidence score: 4/10 for adoption, 7/10 for evaluation. + +## Customer Decision Matrix + +| Question | Answer | Rationale | +| --- | --- | --- | +| Would I trust these developers? | Partially | They are honest and organized, but there is no stable/support evidence. | +| Would I believe this project will still exist in three years? | Unknown | Repository signals effort, but no governance, release cadence or support commitments. | +| Would I migrate company data into it? | No | Project says not production-ready; upgrade/recovery not proven. | +| Would I convince my CEO to approve it? | Only for evaluation | Not for live finance operations. | +| Would I pay for support? | Not yet | No support offering/SLA/stable release evidence. | +| Would I recommend it to another company? | Watch/evaluate only | Not production use. | + +## What Would Change My Mind + +Before customer adoption, I would need: + +1. A stable release with clear release notes. +2. A production deployment guide validated end-to-end. +3. A proven upgrade/migration drill from a previous release. +4. A production-like backup/restore drill with RPO/RTO. +5. A security review or pentest summary. +6. Clear support/SLA options. +7. A maintainer and governance statement. +8. Dependency alert closure. +9. A customer-safe migration/import/export story. +10. Clear documentation for what happens if the project is abandoned. + +## Scores + +| Area | Score | Customer Interpretation | +| --- | ---: | --- | +| Trust | 5.5 | Honest and transparent, but not yet proven for business reliance. | +| Professionalism | 7.0 | Strong preview presentation; not vendor-grade maturity. | +| Documentation | 7.0 | Clear and extensive, with good warnings. | +| Roadmap | 6.0 | Credible but mostly lists blockers before production readiness. | +| Long-term maintenance signals | 4.5 | Good engineering signals, weak governance/support/release signals. | +| Risk | 7.0 | High for real financial data. | +| Confidence | 4.0 | Low confidence for adoption, higher for evaluation. | + +## Final Customer Adoption Verdict + +As a manufacturing company CTO, I would not adopt OpenCashFlow for live company cash-flow management today. + +I would approve a sandbox evaluation by an internal technical team if we are looking for a self-hosted option and willing to participate early. I would not migrate real data, ask the CEO for production approval, pay for ongoing support, or recommend it to another company as a production system until the project reaches a stable release with proven operations, security and support commitments. + +OpenCashFlow earns interest. It does not yet earn business trust. diff --git a/Docs/audits/opencashflow-enterprise-cto-audit.md b/Docs/audits/opencashflow-enterprise-cto-audit.md new file mode 100644 index 0000000..08e7977 --- /dev/null +++ b/Docs/audits/opencashflow-enterprise-cto-audit.md @@ -0,0 +1,316 @@ +# OpenCashFlow Enterprise CTO Audit + +Date: 2026-07-08 + +Perspective: CTO evaluation for potential adoption by a software company serving 120 companies with important financial data and expensive downtime. + +Scope: observable evidence in the repository only. This report does not rely on prior reviews or project history outside the current checkout. + +## Executive Decision + +I would not approve OpenCashFlow today for business-critical production use or paying customers. + +I would approve a constrained technical pilot with non-production data if the goal is to evaluate product fit, architecture, developer experience and hardening cost. The repository shows serious engineering work: a clean .NET 10 solution, separated projects, passing Release build, passing tests, Docker evaluation path, quality gates, ZAP workflows, clean-install smoke automation and a local backup/restore drill. Those are meaningful signals. + +The gap is operational proof. For 120 companies and financial data, I need verified upgrade/migration behavior, production-grade backup and restore procedures on a production-like topology, dependency alert closure, browser-level end-to-end coverage, stronger database integration coverage, observability guidance tested in practice, and a production deployment reference that does not rely on local evaluation defaults. + +Current status: suitable for engineering evaluation and limited pilot. Not suitable for customer production. + +## Evidence Collected + +Commands run locally: + +```bash +git status --short --branch +dotnet sln OpenCashFlow.sln list +dotnet build OpenCashFlow.sln --configuration Release --no-restore +dotnet test OpenCashFlow.sln --configuration Release --no-build +docker compose config +dotnet list OpenCashFlow.sln package --vulnerable --include-transitive +``` + +Observed results: + +- Current branch during audit: `ops/backup-restore-smoke-drill`. +- Working tree was clean before creating this audit report. +- Solution contains 10 active projects: + - `OpenCashFlow.API` + - `OpenCashFlow.Application` + - `OpenCashFlow.Contracts` + - `OpenCashFlow.Domain` + - `OpenCashFlow.Infrastructure` + - `OpenCashFlow.WebApp` + - four test projects. +- Release build passed with 0 warnings and 0 errors. +- Release tests passed: 261 passed, 0 failed, 0 skipped. +- `docker compose config` passed. +- `dotnet list OpenCashFlow.sln package --vulnerable --include-transitive` did not complete in this local session and had to be terminated. The repository contains a CI dependency audit gate and a dependency-alert reconciliation document, but this audit cannot independently confirm a clean local vulnerability scan. + +Repository evidence inspected: + +- `README.md` +- `SECURITY.md` +- `Docs/ROADMAP.md` +- `Docs/testing/skipped-tests-backlog.md` +- `Docs/testing/db-integration-tests.md` +- `Docs/testing/clean-install-smoke.md` +- `Docs/ops/production-hardening.md` +- `Docs/ops/backup-restore-drill.md` +- `Docs/ops/upgrade-migration-drill.md` +- `Docs/ops/secrets-management.md` +- `Docs/security/dependency-alert-reconciliation.md` +- `.github/workflows/quality-gates.yml` +- `.github/workflows/zap-baseline.yml` +- `.github/workflows/zap-full.yml` +- `docker-compose.yml` +- project references in `.csproj` files. + +## Technical Quality + +The technical baseline is materially better than a prototype. The solution builds cleanly on .NET 10, the project structure is recognizable, and the test suite is not skipped into a false green state. The explicit layer split into Domain, Application, Infrastructure, Contracts, API and WebApp is a strong maintainability signal. + +The codebase also includes dedicated Application tests, Domain tests, API/integration-style tests and a PostgreSQL Testcontainers project. That is the right direction for software that handles financial data. + +Concerns: + +- The repository still contains `Docs/.DS_Store`, which is minor but shows hygiene is not perfect. +- Historical DB tests remain under `tests/OpenCashFlow.Test/Tests/db/**/*.cs` and are excluded from compilation. They are documented as historical drafts, but they contain many skipped test markers around lookup, payment, employee and company persistence. This is not invisible risk anymore, but it is still unresolved test debt. +- The database integration project currently documents only an initial foundation. Six DB tests are not enough for high confidence in financial persistence, tenant isolation and migration safety. + +CTO view: technically credible for evaluation; not yet proven enough for operational dependence. + +## Architecture + +The architecture is visibly intentional: + +- `OpenCashFlow.Application` references `Domain` only. +- `OpenCashFlow.Domain` appears isolated from infrastructure packages. +- `Infrastructure` owns EF Core, PostgreSQL, email, JWT and persistence concerns. +- `WebApp` references `Contracts` only, not Infrastructure. +- `API` composes Contracts, Application and Infrastructure. + +This is a reasonable Clean Architecture direction. The repository also documents boundary decisions and migration maps. + +Observed architecture debt: + +- `OpenCashFlow.Infrastructure` still references `OpenCashFlow.Contracts`. That is not fatal, but it means the persistence/integration layer still knows about public boundary types in some areas. +- `OpenCashFlow.Contracts` is a public boundary project, so it must remain very stable. Any DTO leakage into persistence or business rules would become expensive later. +- The docs still contain migration-era architecture material. It is useful for maintainers, but it can make the project feel like it is still mid-refactor. + +CTO view: architecture is a positive signal, but I would require one more boundary-hardening pass before treating it as a stable platform. + +## Operational Maturity + +Positive evidence: + +- Docker Compose configuration exists and validates. +- README clearly says Docker Compose defaults are local evaluation only. +- Production hardening guidance exists. +- Secrets management guidance exists. +- Clean-install smoke script exists and verifies setup, login, payment creation and cash ledger effect through HTTP paths. +- Backup/restore smoke drill exists and was documented as executed against an isolated smoke stack. + +Critical gaps: + +- `Docs/ops/upgrade-migration-drill.md` explicitly says the upgrade/migration drill was not fully performed. +- Backup/restore proof is local and disposable. It proves `pg_dump`/`pg_restore` for representative smoke data, not production RPO, RTO, off-host storage, encryption, scheduling, alerting or restore ownership. +- `docker-compose.yml` uses local-evaluation defaults: `postgres/postgres`, fallback JWT secret, HTTP ports, exposed PostgreSQL and `AUTO_MIGRATE=true`. +- There is no demonstrated production topology with TLS reverse proxy, external PostgreSQL, secret manager, backup target, monitoring and rollback. +- Observability is guidance, not an implemented operating model. + +CTO view: operational maturity is the main blocker. For 120 companies, I would not accept this without staging/prod runbooks proven by drills. + +## Maintainability + +Positive evidence: + +- Project boundaries are clear. +- Build has 0 warnings. +- Tests are green with 0 skipped in the compiled suite. +- Quality gates exist for restore, vulnerability audit, Release build, tests and Docker Compose config. +- Documentation is extensive and mostly honest about preview status. + +Risks: + +- Large legacy/static frontend assets remain under `wwwroot/libs`; searches can be noisy and security review of vendored assets is harder. +- Historical DB tests are excluded rather than migrated or removed. +- Some docs are operationally useful but numerous; new maintainers may need a curated “operator path” versus “architecture migration archive” separation. + +CTO view: maintainable for a motivated engineering team; not yet low-friction for enterprise operations. + +## Security + +Positive evidence: + +- README and ops docs warn the project is not production-ready. +- Rate limiting is present in API startup and applied to auth-related endpoints. +- WebApp emits HSTS in non-development mode and CSP/security headers. +- ZAP baseline and full scan workflows exist and run on non-draft PRs to `main`. +- The ZAP baseline gate parses JSON and fails on Medium/High findings. +- Secrets-management docs explicitly forbid committed real secrets and describe rotation. +- Dependency reconciliation documentation exists. + +Concerns: + +- The local vulnerability scan did not complete during this audit, so I cannot independently confirm a clean dependency state from this run. +- The ZAP full scan quality gate counts `Medium` and `High` strings in HTML reports, which is less reliable than parsing structured JSON/XML. +- `SECURITY.md` contains broad security posture claims, including encryption-at-rest and protection layers, that are partly operator/platform responsibilities rather than fully proven repository features. +- Production auth, reset-password, PIN/fast-login, authorization and tenant isolation are documented as requiring further security review before production readiness. +- GitHub dependency alert closeout is documented as a remaining work item. + +CTO view: acceptable for a controlled pilot; not enough assurance for customer financial production. + +## Deployment + +The deployment story is currently local-evaluation focused. Dockerfiles and Compose exist, and Compose config validates. That is useful for onboarding and smoke testing. + +For enterprise adoption, missing evidence includes: + +- production compose or Kubernetes reference; +- TLS/reverse proxy configuration validated end to end; +- external PostgreSQL deployment path; +- secret-manager integration; +- migration job or controlled release process; +- monitoring and alerting examples; +- backup automation with retention and off-host storage. + +CTO view: developer deployment is real; production deployment is not mature. + +## Recovery And Business Continuity + +Positive evidence: + +- Backup/restore documentation exists. +- A backup/restore smoke drill script exists. +- The current documented local result shows `Companies`, `Payments` and `CashLedgers` restore counts equal to 1 for smoke-created records. + +Insufficient evidence: + +- No production-like restore drill. +- No RPO/RTO evidence. +- No off-host backup target. +- No scheduled backup verification. +- No upgrade rollback drill. +- No disaster recovery procedure tested for an environment with 120 companies. + +CTO view: recovery is started, not enterprise-ready. + +## Documentation + +Documentation is one of the stronger areas. README is direct about “Developer Preview / Early Self-Hosted Preview” and “not production-ready”. There are docs for roadmap, security, installation, testing, quality gates, backup/restore, upgrade/migration and secrets. + +The problem is not absence of docs. The problem is proof. Several docs correctly say “not proven yet”. That honesty is good, but from a CTO adoption standpoint it means I cannot approve production. + +CTO view: good for evaluating and contributing; not yet sufficient as an operator handbook for production service ownership. + +## Supportability + +Supportability is moderate: + +- Build/test commands are simple. +- README quickstart is clear. +- CI gates are conservative. +- Test backlog says 0 skipped in compiled suite. +- Clean-install smoke gives a repeatable support diagnostic path. + +But: + +- No documented SLOs. +- No incident response runbook beyond secret guidance. +- No production metrics/logging reference implementation. +- No support matrix for versions, database versions or upgrade windows. +- No stable release policy in action because there are no stable releases. + +CTO view: supportable by the project team; not yet supportable at enterprise/customer scale. + +## Business Continuity Risk + +For 120 companies and financial data, the largest risks are: + +1. Upgrade risk: migrations and rollback are not proven against historical data. +2. Recovery risk: backup/restore is only proven locally with a small smoke dataset. +3. Security risk: auth/tenant/PIN/reset flows need explicit production security review. +4. Dependency risk: local vulnerability scan did not complete in this audit; GitHub alert closeout is documented as work. +5. Persistence risk: historical DB tests remain excluded, and DB integration coverage is still small. +6. Operations risk: no production deployment reference with monitoring, backup storage, TLS and secret management. + +These are not cosmetic. They directly affect downtime, data integrity and reputation. + +## Scores + +Scale: 0 is absent or unacceptable; 10 is mature and proven for enterprise use. + +| Area | Score | Rationale | +| --- | ---: | --- | +| Engineering maturity | 6.5 | Clean build, separated projects, 0-warning Release build, meaningful tests, clear architecture direction. Still has excluded historical DB tests and boundary debt. | +| Operational maturity | 4.0 | Local smoke and backup/restore drill exist. Production operations, observability, RPO/RTO and migration drills are not proven. | +| Deployment maturity | 4.0 | Docker evaluation path works. Production topology is guidance, not validated implementation. | +| Supportability | 5.0 | Good docs and smoke commands, but no SLOs, production runbooks, incident process or support matrix. | +| Risk | 7.0 | High residual risk for business-critical financial use. Risk is lower for a non-production pilot. | +| Confidence | 6.0 | Strong build/test evidence, but vulnerability scan did not complete locally and production drills are incomplete. | +| Overall adoption score | 4.5 | Worth piloting; not ready for real customer dependency. | + +## CTO Approval Answers + +### Would I allow my own engineering team to depend on this project? + +For evaluation and controlled internal experimentation: yes. + +For a production service dependency: no, not yet. The engineering team would need to own hardening, recovery, migration testing and security review before depending on it. + +### Would I allow customers to depend on it? + +No. The repository itself states it is not production-ready, and the observable evidence supports that caution. + +### Would I approve this as CTO? + +I would approve a limited pilot with synthetic or non-critical data. I would not approve business-critical production. + +## Rollout Decisions + +### Approve a pilot? + +Yes, with constraints. + +Conditions: + +- non-production data only; +- isolated environment; +- engineering team assigned to test setup, payment, cash ledger, auth and backup/restore; +- no customer dependency; +- clear exit criteria. + +### Approve an internal rollout? + +Not for financial operations. I might approve a small internal evaluation by engineering or finance operations using synthetic data. I would not approve company-wide internal reliance on it. + +### Approve paying customers? + +No. Paying customers create support, uptime, security and data integrity obligations that the current repository has not proven. + +### Approve business-critical production? + +No. The upgrade/migration drill is explicitly not complete, production backup/restore is not proven, dependency alerts are not fully closed by direct evidence in this audit, and production deployment hardening is documented rather than validated. + +## Adoption Requirements Before Production + +Minimum next steps before reconsidering production: + +1. Run and document an upgrade/migration drill against an older realistic database snapshot. +2. Run backup/restore against a production-like topology with off-host encrypted storage and defined RPO/RTO. +3. Convert or retire the historical excluded DB tests with traceability to real integration coverage. +4. Expand DB integration tests for tenant isolation, delete restrictions, financial constraints and lookup behavior. +5. Add browser-level E2E smoke for login, setup, payment, cash ledger and recovery-facing workflows. +6. Close or formally dismiss GitHub dependency alerts with evidence. +7. Replace fragile ZAP full-scan HTML grep gate with structured report parsing. +8. Provide a production deployment reference with TLS, external PostgreSQL, secret injection, `AUTO_MIGRATE=false`, monitoring and backup jobs. +9. Perform focused security review of auth, password reset, PIN/fast-login, tenant isolation and authorization. +10. Remove repository residue such as `Docs/.DS_Store`. + +## Final CTO Verdict + +OpenCashFlow looks like a serious engineering project in developer preview, not a production platform. + +I would not stake company reputation or customer financial operations on it today. I would assign a small team to run a structured pilot if the product direction is strategically interesting. The pilot should measure hardening cost, not just feature fit. + +For 120 companies, the current risk is too high. diff --git a/Docs/audits/opencashflow-executive-board-review.md b/Docs/audits/opencashflow-executive-board-review.md new file mode 100644 index 0000000..200fa83 --- /dev/null +++ b/Docs/audits/opencashflow-executive-board-review.md @@ -0,0 +1,321 @@ +# OpenCashFlow Executive Board Review + +Date: 2026-07-08 + +Scope: executive board review of OpenCashFlow based only on observable repository evidence. + +Board members: + +- CTO +- CIO +- CISO +- Principal Software Architect +- SRE Manager +- Lead QA Engineer +- Enterprise Customer +- Open Source Maintainer + +Decision options: + +- `REJECT` +- `PROMISING BUT IMMATURE` +- `APPROVED FOR PILOT` +- `APPROVED FOR INTERNAL USE` +- `READY FOR PUBLIC DEVELOPER PREVIEW` +- `READY FOR STABLE RELEASE` +- `READY FOR ENTERPRISE ADOPTION` + +## Evidence Reviewed + +Observable evidence includes: + +- `README.md` +- `CONTRIBUTING.md` +- `CODE_OF_CONDUCT.md` +- `CHANGELOG.md` +- `SECURITY.md` +- `LICENSE` +- `.github/ISSUE_TEMPLATE/*.yml` +- `.github/pull_request_template.md` +- `.github/workflows/quality-gates.yml` +- `.github/workflows/zap-baseline.yml` +- `.github/workflows/zap-full.yml` +- `.github/workflows/README.md` +- `Docs/ROADMAP.md` +- `Docs/testing/skipped-tests-backlog.md` +- `Docs/testing/db-test-triage.md` +- `Docs/testing/db-integration-tests.md` +- `Docs/testing/clean-install-smoke.md` +- `Docs/ops/production-hardening.md` +- `Docs/ops/backup-restore-drill.md` +- `Docs/ops/upgrade-migration-drill.md` +- solution structure containing API, Application, Contracts, Domain, Infrastructure, WebApp and test projects. + +Visible repository state during review: + +- current branch: `ops/backup-restore-smoke-drill`; +- several audit reports are untracked in the current checkout; +- `Docs/.DS_Store` is visible in the Docs tree. + +## Independent Reviewer Verdicts + +Each reviewer evaluates independently before board discussion. + +## CTO Verdict + +Decision: `APPROVED FOR PILOT` + +Rationale: + +OpenCashFlow has credible engineering signals: .NET 10 baseline, clean architecture project split, explicit self-hosted +scope, quality gates, ZAP workflows, tests, database integration-test foundation, clean-install smoke documentation and +backup/restore drill documentation. + +I would approve a tightly scoped technical pilot using synthetic or non-critical data. I would not approve internal +operational use or customer-facing production use. The README and roadmap explicitly say the project is not +production-ready. Stable release, upgrade proof, production hardening, dependency alert closeout and support model are +not complete. + +Key concern: + +The project looks serious, but it is still an engineering stabilization effort. + +## CIO Verdict + +Decision: `PROMISING BUT IMMATURE` + +Rationale: + +From an IT portfolio perspective, the project has clear documentation and a self-hosted deployment path, but it lacks the +operational guarantees needed to become an approved business system. There is no stable release, no support SLA, no +proven production deployment topology, no long-term maintenance model and no documented vendor-style release process. + +I would allow evaluation in an isolated lab. I would not approve it as part of the enterprise application portfolio. + +Key concern: + +Business continuity ownership is not defined. + +## CISO Verdict + +Decision: `PROMISING BUT IMMATURE` + +Rationale: + +The repository has a security policy, security headers/CSP work, ZAP workflows, dependency audit workflow, and explicit +warnings around auth, reset password, PIN/fast-login, tenant isolation and production hardening. That transparency is +positive. + +However, the project itself states security-sensitive areas are active stabilization work. Production hardening docs list +unproven requirements. Dependency alert closeout remains on the roadmap. The project is not ready for internet-exposed +customer data without a targeted security review, penetration test and production configuration validation. + +Key concern: + +The security posture is documented, not yet proven for production. + +## Principal Software Architect Verdict + +Decision: `READY FOR PUBLIC DEVELOPER PREVIEW` + +Rationale: + +The solution shape is coherent: Domain, Application, Infrastructure, Contracts, API, WebApp and dedicated tests. The +architecture docs describe boundary cleanup and current dependencies. Application and Domain are intended to remain free +of EF/API/Infrastructure coupling. WebApp no longer depends on Infrastructure according to documentation. The roadmap +openly lists remaining Infrastructure/Contracts boundary reduction. + +This is enough for a public developer preview because contributors can understand the intended architecture and help +stabilize it. It is not enough for stable release because boundary debt and migration-era documentation remain. + +Key concern: + +The architecture is directionally good but still recently refactored and not fully settled. + +## SRE Manager Verdict + +Decision: `PROMISING BUT IMMATURE` + +Rationale: + +Operations documentation has improved: production hardening, secrets management, backup/restore drill, upgrade/migration +drill, Docker Compose validation and clean-install smoke path are documented. The backup/restore drill proves a local +isolated smoke database can be backed up and restored. + +That is not enough for operational approval. Production deployment is not validated end to end. Restore RPO/RTO, +off-host backup storage, monitoring, alerting, rollback, reverse proxy configuration and upgrade-from-prior-release are +not proven. The upgrade/migration drill explicitly says it is not fully performed. + +Key concern: + +An operations team still could not rely on this at 3 AM for business-critical recovery. + +## Lead QA Engineer Verdict + +Decision: `APPROVED FOR PILOT` + +Rationale: + +The compiled suite is documented as having zero skipped tests after company coverage work. There are multiple test +projects, including Application, Domain, API/integration-style tests and a dedicated PostgreSQL Testcontainers database +test project. P0/P1 company, payment and tenant isolation gaps appear to have been addressed according to testing docs. + +The remaining risk is that historical DB test files remain excluded from compilation. This is documented rather than +hidden, and the DB integration-test foundation has begun. For a pilot, that is acceptable. For stable release, it is not. + +Key concern: + +Test maturity is improving, but persistence coverage is not complete enough for stable release confidence. + +## Enterprise Customer Verdict + +Decision: `PROMISING BUT IMMATURE` + +Rationale: + +As a customer, I appreciate the clear README, AGPL license, quickstart, roadmap and honesty about maturity. I would +understand what the product is and what it is not. + +I would not migrate company financial data into it today. The project explicitly says not to use it for regulated or +business-critical financial operations. There is no stable release, no support SLA, no proven production deployment, no +customer-safe upgrade path and no enterprise support model. + +Key concern: + +The project earns interest, not business trust. + +## Open Source Maintainer Verdict + +Decision: `READY FOR PUBLIC DEVELOPER PREVIEW` + +Rationale: + +OpenCashFlow has the basics a contributor expects: README, CONTRIBUTING, Code of Conduct, changelog, issue forms, PR +template, funding file, security policy, quality gates and roadmap. The contribution guide names useful work areas and +protects architectural boundaries. CI is conservative and does not auto-deploy. + +I would star it and consider a small PR. I would not become a maintainer yet. Governance is thin: no maintainer roster, +no review SLA, no decision-making model, no release cadence and no process for becoming a maintainer. + +Key concern: + +Contributor entry is viable, but maintainer governance is not mature. + +## Board Discussion + +The board agrees the project should not be rejected outright. The repository shows substantial engineering effort, +honest maturity labeling and meaningful hardening work. + +The CTO and Lead QA Engineer are willing to approve a technical pilot because tests, architecture and smoke/backup +documentation are present. The pilot must use synthetic or non-critical data and must not be presented as operational +adoption. + +The Principal Software Architect and Open Source Maintainer argue that the repository is ready for public developer +preview. Their reasoning is that public contributors can understand the system, run it, read the roadmap and make useful +improvements without being misled about maturity. + +The CIO, CISO, SRE Manager and Enterprise Customer block any stronger decision. Their objections are consistent: + +- no stable release; +- no production support model; +- no proven production deployment; +- no complete upgrade/migration drill; +- no production-grade backup/restore/RPO/RTO evidence; +- security-sensitive areas remain active stabilization work; +- GitHub dependency alert closeout remains on the roadmap; +- historical database tests remain excluded, even though documented; +- governance and release ownership are not mature. + +The board rejects `APPROVED FOR INTERNAL USE`, `READY FOR STABLE RELEASE` and `READY FOR ENTERPRISE ADOPTION`. + +The board also rejects plain `APPROVED FOR PILOT` as the final public classification because that phrase could imply +customer or operational pilot readiness. The appropriate public classification is narrower: the repository is ready to +be shown and improved as a Developer Preview, while any deployment pilot must remain isolated and non-critical. + +## Board Decision + +Decision: `READY FOR PUBLIC DEVELOPER PREVIEW` + +This is not approval for production, enterprise adoption, stable release, customer deployment or business-critical +internal use. + +## Reasons + +The board grants `READY FOR PUBLIC DEVELOPER PREVIEW` because: + +1. The project clearly describes itself as Developer Preview / Early Self-Hosted Preview. +2. README explains purpose, scope, non-goals, quickstart, architecture, security and license. +3. Open-source packaging exists: contributing guide, code of conduct, changelog, issue templates and PR template. +4. CI quality gates exist and use read-only permissions. +5. ZAP baseline and full scan workflows exist. +6. The solution has recognizable Clean Architecture layering. +7. The compiled test suite is documented as having zero skipped tests. +8. A dedicated PostgreSQL database test project exists. +9. Clean-install smoke and backup/restore drill documentation exists. +10. The roadmap is honest and does not claim production readiness. + +## Remaining Blockers + +Blockers before stable release or enterprise adoption: + +1. No stable release or release cadence. +2. No maintainer governance model. +3. No support/SLA policy. +4. No validated production deployment topology. +5. No fully performed upgrade/migration drill from a previous release database. +6. Production backup/restore/RPO/RTO not proven. +7. GitHub dependency alert closeout remains unresolved or not fully evidenced. +8. Security-sensitive auth/tenant/PIN/reset-password posture needs formal review. +9. Historical DB tests remain excluded from compilation, despite documentation. +10. Infrastructure/Contracts boundary debt remains on the roadmap. +11. Frontend static asset/CSP cleanup remains on the roadmap. +12. Repository polish issue remains visible: `Docs/.DS_Store`. + +## Required Actions + +Required before `APPROVED FOR INTERNAL USE`: + +1. Prove production-like deployment behind TLS/reverse proxy. +2. Prove backup/restore with off-host storage and documented RPO/RTO. +3. Prove upgrade/migration from at least one prior release or production-like snapshot. +4. Close or formally dismiss dependency alerts with evidence. +5. Complete a targeted security review of authentication, authorization, tenant isolation, reset password and PIN flows. +6. Expand database integration tests for remaining high-value persistence rules. +7. Publish an incident response and operational runbook. + +Required before `READY FOR STABLE RELEASE`: + +1. Publish a stable release policy and release cadence. +2. Create versioned release artifacts and changelog entries. +3. Define compatibility and migration guarantees. +4. Resolve remaining architecture boundary debt or document accepted exceptions. +5. Remove repository residue and stale migration-era public noise. +6. Establish maintainer governance and review expectations. + +Required before `READY FOR ENTERPRISE ADOPTION`: + +1. Provide support/SLA model. +2. Provide production reference architecture. +3. Provide security assessment or pentest evidence. +4. Provide disaster recovery drill evidence. +5. Provide upgrade and rollback evidence. +6. Provide customer-safe data export/import/migration story. +7. Provide long-term maintenance policy. + +## Overall Confidence + +| Evaluation Target | Confidence | +| --- | ---: | +| Public Developer Preview | 7/10 | +| Non-critical technical pilot | 6/10 | +| Internal business use | 3/10 | +| Stable release | 2/10 | +| Enterprise adoption | 2/10 | +| Customer production deployment | 1.5/10 | + +## Final Statement + +OpenCashFlow is credible enough to be public, reviewed and improved by developers. It is not credible enough yet to carry +business-critical financial operations. + +The executive board decision is `READY FOR PUBLIC DEVELOPER PREVIEW`. diff --git a/Docs/audits/opencashflow-maintainer-audit.md b/Docs/audits/opencashflow-maintainer-audit.md new file mode 100644 index 0000000..8c08449 --- /dev/null +++ b/Docs/audits/opencashflow-maintainer-audit.md @@ -0,0 +1,350 @@ +# OpenCashFlow Maintainer Audit + +Date: 2026-07-08 + +Perspective: long-time maintainer of a successful .NET open-source project evaluating whether to contribute to +OpenCashFlow. + +Scope: repository evidence only. This review focuses on repository, issues, roadmap, CI, tests, architecture, +documentation, project governance, and contribution experience. It does not judge code style. + +## Executive Maintainer Verdict + +I would star OpenCashFlow and consider a small first pull request. + +I would not become a maintainer, sponsor it, or recommend that new contributors invest significant time until the project +has clearer governance, a release cadence, and fewer production-readiness blockers. + +The repository has enough structure to be worth watching: clear README, AGPL license, contribution guide, code of +conduct, issue forms, PR template, quality gates, ZAP workflows, roadmap, tests, database integration-test foundation, +and operational documentation. It reads like a serious Developer Preview. + +The repository is not yet a mature open-source contributor ecosystem. Observable gaps remain: no maintainer guide, no +decision-making process, no public release cadence, no stable version, no documented triage policy, no project board +evidence in the local repository, no contributor recognition policy, and some repository residue such as `Docs/.DS_Store` +visible in the tree. + +## Observable Evidence Reviewed + +Reviewed files and structure: + +- `README.md` +- `CONTRIBUTING.md` +- `CODE_OF_CONDUCT.md` +- `CHANGELOG.md` +- `SECURITY.md` +- `LICENSE` +- `.github/ISSUE_TEMPLATE/*.yml` +- `.github/pull_request_template.md` +- `.github/workflows/quality-gates.yml` +- `.github/workflows/zap-baseline.yml` +- `.github/workflows/zap-full.yml` +- `.github/workflows/README.md` +- `Docs/ROADMAP.md` +- `Docs/quality/quality-gates-report.md` +- `Docs/testing/skipped-tests-backlog.md` +- `Docs/testing/db-test-triage.md` +- `Docs/testing/db-integration-tests.md` +- solution structure from `dotnet sln OpenCashFlow.sln list` +- project references from `*.csproj` +- repository status from `git status --short --branch` + +Current observable worktree note: + +- The current branch is `ops/backup-restore-smoke-drill`. +- Several audit reports are untracked in the current checkout. +- `Docs/.DS_Store` is still visible in the Docs tree. + +Those do not invalidate the project, but they reduce first-impression polish for a maintainer. + +## Repository + +Strengths: + +- The top-level README explains project purpose, status, scope, quickstart, architecture and license. +- The project is explicit about being a Developer Preview / Early Self-Hosted Preview. +- The solution structure is understandable: + - `OpenCashFlow.API` + - `OpenCashFlow.Application` + - `OpenCashFlow.Contracts` + - `OpenCashFlow.Domain` + - `OpenCashFlow.Infrastructure` + - `OpenCashFlow.WebApp` + - multiple test projects. +- Legacy SaaS/Admin/Stripe runtime is described as removed from the active core. +- The repository has an AGPL-3.0 license, which is appropriate for a networked self-hosted application if that is the + intended governance model. + +Weaknesses: + +- There is still visible cleanup residue (`Docs/.DS_Store`). +- There are many historical/migration/hardening documents. They are useful, but they make the project feel like it is + still mid-transition. +- The README is honest, but a new contributor still has to navigate many docs to find the highest-value starting point. + +Maintainer view: worth exploring, not yet frictionless. + +## Issues + +The local repository includes useful issue forms: + +- bug report form with reproduction steps, environment and safety check; +- feature request form focused on user/operator problem, risks and area; +- config disables blank issues; +- security contact routes vulnerability reports away from public issues. + +What is not observable locally: + +- active issue count; +- stale issue ratio; +- maintainer response time; +- labels actually used in practice; +- project board or milestone discipline; +- good-first-issue inventory. + +Maintainer view: the templates are good, but issue hygiene cannot be verified from the local repository alone. + +## Roadmap + +The roadmap is unusually honest. It clearly says the project is not production-ready and names concrete work: + +- database integration test migration; +- production hardening; +- backup/restore drill; +- upgrade/migration drill; +- GitHub alert closeout; +- frontend/static asset cleanup; +- Infrastructure/Contracts boundary reduction. + +Strength: + +- The roadmap avoids false maturity claims. +- It names real engineering work rather than vague feature wishes. + +Weakness: + +- The roadmap is mostly a blocker list. It does not yet show release milestones, ownership, target dates or how + contributors can claim work. +- There is no visible milestone policy for `0.x`, `1.0`, or stable release readiness. + +Maintainer view: good direction, incomplete execution model. + +## CI + +Positive signals: + +- `quality-gates.yml` runs on non-draft PRs to `main` and `development`. +- It restores, audits NuGet vulnerabilities, builds Release, runs Release tests, and validates Docker Compose config. +- Permissions are read-only. +- Legacy promotion workflows were removed according to `.github/workflows/README.md`. +- ZAP baseline and full scans exist for non-draft PRs to `main`. +- Workflow docs explain active and removed workflows. + +Limitations: + +- Formatting is documented as deferred; `dotnet format --verify-no-changes` is not a required gate. +- Stricter analyzers and warning-as-error are deferred. +- SBOM publishing is deferred. +- The vulnerability audit depends on GitHub-hosted access to NuGet advisory data; local docs note a sandboxed attempt did + not complete in one earlier run. +- ZAP scans only target `main`, not `development`. + +Maintainer view: conservative and sane for a preview. Not yet mature enough for a large contributor base without more +automated hygiene. + +## Tests + +Positive signals: + +- The solution has multiple test projects: + - Domain tests; + - Application tests; + - Database tests; + - broader API/integration-style tests. +- `Docs/testing/skipped-tests-backlog.md` states the compiled suite has `0 skipped`. +- The skipped test backlog keeps historical context rather than silently hiding risk. +- `OpenCashFlow.Database.Tests` exists and uses PostgreSQL Testcontainers. +- Database integration docs distinguish real persistence tests from historical excluded DB drafts. + +Risks: + +- `tests/OpenCashFlow.Test/Tests/db/**/*.cs` are still excluded from compilation and documented as historical drafts. +- `Docs/testing/db-test-triage.md` lists 71 declared historical DB tests, 62 skip attributes, and multiple high-value + persistence scenarios not fully migrated. +- The database integration foundation is useful but still small. +- The docs themselves warn that zero skipped tests in the compiled suite does not eliminate excluded DB-test debt. + +Maintainer view: the test culture is improving and transparent, but there is still meaningful hidden-domain risk in +historical DB coverage. + +## Architecture + +Positive signals: + +- The solution has explicit Clean Architecture-style projects. +- `Application` references `Domain`, not API/Infrastructure/Contracts. +- `Domain` is isolated. +- `WebApp` references `Contracts`, not `Infrastructure`. +- `Infrastructure` owns EF/persistence implementation. +- Contribution docs explicitly state architectural boundaries and warn against reintroducing `OpenCashFlow.Shared`. + +Concerns: + +- `Infrastructure` still references `Contracts`; docs identify Infrastructure/Contracts boundary reduction as remaining + work. +- `API` references `Contracts`, `Application`, and `Infrastructure`, which is expected for composition but keeps API as a + broad integration surface. +- Many migration documents imply significant architecture cleanup has happened recently and some debt remains. + +Maintainer view: architecture is understandable and moving in the right direction. It is not yet boring or fully settled. + +## Documentation + +Strong areas: + +- README is clear and honest. +- Contribution guide is practical. +- Roadmap is conservative. +- Security documentation exists. +- Quality gate report explains CI choices. +- Testing docs explain skipped/excluded test state. +- Ops docs exist for production hardening, backup/restore, upgrade/migration and secrets. + +Weak areas: + +- Documentation volume is high and can feel like an audit archive rather than a curated contributor path. +- Some documents still read like migration-phase records. +- There is no concise maintainer guide. +- There is no "architecture decision index" for contributors to quickly find current policy versus historical notes. + +Maintainer view: much better than most early projects, but curation is the next bottleneck. + +## Project Governance + +Observable strengths: + +- AGPL license is explicit. +- Code of Conduct exists. +- Security reporting route exists. +- Funding file exists. +- PR and issue templates exist. + +Observable gaps: + +- No maintainer roster. +- No decision-making model. +- No release manager/triage ownership. +- No documented review SLA or expectations. +- No governance model for breaking changes. +- No contributor recognition path. +- No documented process for becoming a maintainer. +- No public support policy beyond general docs. + +Maintainer view: governance is the largest blocker to deeper contribution. + +## Contribution Experience + +What would help me open a first PR: + +- Clear local build/test commands. +- Conservative contribution rules. +- PR template asks for risk and checks. +- Issue forms request useful reproduction detail. +- Good small areas are suggested in `CONTRIBUTING.md`. +- The codebase is split into recognizable layers. + +What would slow me down: + +- Many docs to read before knowing what is current. +- Production-readiness blockers are mixed with architecture and migration history. +- No visible list of beginner-ready issues in the local repository. +- No maintainer guide explaining review standards or ownership. +- Some remaining repository residue weakens confidence in hygiene. + +Maintainer view: I would open a small PR, but I would not yet invest in a large feature. + +## Would I Star It? + +Yes. + +Reason: the project is honest, structured, self-hosted, AGPL-licensed, and has credible early engineering discipline. +Starring is appropriate as a signal to watch the project. + +## Would I Fork It? + +Maybe. + +I would fork it to experiment or prepare a focused PR. I would not fork it to build a production derivative yet because +release stability and operational maturity are not proven. + +## Would I Open A PR? + +Yes, for a small bounded change. + +Good first PR candidates: + +- remove repository residue; +- improve docs curation; +- migrate one historical DB test group into `OpenCashFlow.Database.Tests`; +- add a missing integration test; +- reduce Infrastructure/Contracts coupling in a narrow area; +- clean frontend static assets where behavior can be preserved. + +I would avoid large features until governance and review expectations are clearer. + +## Would I Become A Maintainer? + +No, not from current evidence. + +Reason: the project does not yet publish a maintainer process, ownership model, review expectations, release discipline or +governance. Becoming a maintainer without those would carry high process risk. + +## Would I Sponsor It? + +Not yet. + +Reason: funding links exist, but the repository does not show a support roadmap, funding goals, maintainer commitments or +clear use of sponsorship funds. I might sponsor after seeing consistent releases and issue/PR responsiveness. + +## Would I Recommend Contributors Join? + +Qualified yes. + +I would recommend experienced .NET contributors join if they are comfortable with early-preview cleanup, tests, +documentation, security hardening and architecture work. + +I would not recommend it yet to new contributors expecting a highly curated onboarding path or fast maintainer feedback. + +## Scores + +| Area | Score | Maintainer Interpretation | +| --- | ---: | --- | +| Repository | 7.0 | Clear structure and purpose, with minor hygiene residue. | +| Issues | 6.0 | Good templates; live issue health not observable locally. | +| Roadmap | 6.5 | Honest and concrete, but lacks milestones and ownership. | +| CI | 7.0 | Conservative quality gates, read-only permissions, no deployment automation. | +| Tests | 6.5 | Compiled suite has zero skips and DB foundation exists; historical DB tests remain excluded. | +| Architecture | 7.0 | Good layer split; some boundary debt remains documented. | +| Documentation | 7.0 | Extensive and honest, but needs curation for contributors. | +| Governance | 4.0 | Code of Conduct and templates exist; maintainer/release process missing. | +| Contribution Experience | 6.0 | Good for focused PRs; not yet frictionless for sustained contribution. | +| Overall Contributor Attractiveness | 6.5 | Worth watching and contributing small fixes; not yet a mature maintainer ecosystem. | + +## Maintainer Risk Register + +1. Governance risk: no visible maintainer process or decision model. +2. Release risk: no stable release or cadence. +3. Test risk: historical DB tests remain excluded, even though documented. +4. Scope risk: many production-readiness blockers remain open. +5. Documentation sprawl: strong docs exist, but contributors need clearer "current truth" paths. +6. Hygiene risk: visible residue such as `Docs/.DS_Store` should be removed. +7. Architecture debt: Infrastructure/Contracts coupling remains on roadmap. + +## Final Maintainer Verdict + +OpenCashFlow is a credible early open-source project for experienced contributors who like stabilization work. It has +enough discipline to justify a star and a small PR. It does not yet have enough governance, release maturity or +maintainer process clarity to justify becoming a maintainer, sponsoring it, or recommending broad contributor adoption. + +The next best step is not a new feature. It is contributor-operability work: maintainer guide, issue triage policy, +release milestone plan, curated current-doc index, DB test migration, and removal of final repository residue. diff --git a/Docs/audits/opencashflow-security-review.md b/Docs/audits/opencashflow-security-review.md new file mode 100644 index 0000000..c102fe0 --- /dev/null +++ b/Docs/audits/opencashflow-security-review.md @@ -0,0 +1,369 @@ +# OpenCashFlow Application Security Review + +Date: 2026-07-08 + +Perspective: Senior Application Security Engineer. + +Scope: observable repository evidence only. This review intentionally ignores code style and focuses on authentication, authorization, tenant isolation, secrets, dependency hygiene, Docker, OWASP, headers, CSP, supply chain, GitHub workflows, credential management and attack surface. + +## Executive Security Verdict + +OpenCashFlow is not ready to be exposed to the public Internet or deployed for customers without a focused security hardening pass. + +It is ready for a controlled external penetration test against a disposable pre-production environment. A pentest would be useful now because the application has enough real security surface to test: JWT auth, role policies, tenant scoping, password reset, PIN/fast-login, setup, payments, cash ledger, audit, CSP, Docker deployment and ZAP workflows. + +It is not ready for a public bug bounty. A public bounty would create noise and reputational risk before production deployment assumptions, dependency alerts, reverse-proxy/TLS behavior, Docker hardening and operational security are proven. + +## Evidence Reviewed + +Reviewed areas: + +- `src/OpenCashFlow.API/AppStart/05_Auth.cs` +- `src/OpenCashFlow.API/AppStart/04_RateLimiting.cs` +- `src/OpenCashFlow.API/AppStart/11_Cors.cs` +- `src/OpenCashFlow.API/AppStart/14_MiddlewarePipeline.cs` +- `src/OpenCashFlow.WebApp/Program.cs` +- auth use cases under `src/OpenCashFlow.Application/Auth` +- auth infrastructure under `src/OpenCashFlow.Infrastructure/Auth` +- API controllers for Company, Payment, Authentication and DevEmail +- Dockerfiles and `docker-compose.yml` +- GitHub workflows under `.github/workflows` +- dependency files and package references +- security/ops docs. + +Observable baseline: + +- JWT bearer auth validates issuer, audience, signing key, lifetime and expiration. +- API has role policies: `InstanceAdmin`, `CompanyAdmin`, `CompanyMember`. +- Auth endpoints use a specific `auth-limiter`. +- WebApp emits CSP, frame, content-type, referrer and permissions headers. +- ZAP baseline and full scan workflows exist for PRs to `main`. +- Quality gate includes `dotnet list OpenCashFlow.sln package --vulnerable --include-transitive`. +- No active `package.json`, `package-lock.json`, `yarn.lock`, or `pnpm-lock.yaml` files were found. +- Root Docker Compose is clearly local-evaluation oriented, with local defaults. + +## Authentication + +### Strengths + +- JWT validation is configured for issuer, audience, signing key, lifetime and expiration. +- Token clock skew is narrowed to 1 minute. +- API rejects missing JWT secret and secrets shorter than 32 characters. +- Login use case returns generic invalid credential failure for missing or incorrect credentials. +- Password reset token flow uses token generation, token storage, validity checks and invalidation after reset. +- Auth endpoints are rate-limited. +- WebApp auth cookie is `HttpOnly` where token-bearing cookies are set. + +### Concerns + +- API sets `RequireHttpsMetadata = false` in JWT bearer configuration. That is acceptable in local evaluation, but unsafe as a production default if not overridden or justified. +- WebApp sets cookie `Secure = Request.IsHttps` in at least the normal login flow. No `UseForwardedHeaders` configuration was found. Behind a TLS-terminating reverse proxy, the app may see HTTP and issue non-Secure cookies unless proxy headers are configured correctly. +- Fast-login/PIN exists as an alternative auth flow. It has cookie signing and rate/lockout logic in the WebApp, but it remains a high-value feature that deserves dedicated adversarial testing before Internet exposure. +- Password reset returns success only when a user is found in the Application use case. Depending on controller behavior, this can create account enumeration risk. This needs explicit verification. +- There is no observed server-side JWT revocation/blacklist. Logout appears cookie/client driven. + +### Security Rating + +Authentication is credible for a preview, but not ready for hostile Internet traffic without proxy, cookie, reset-token and fast-login review. + +## Authorization + +### Strengths + +- API controllers use `[Authorize]`, role attributes and policies. +- Company write/delete endpoints require `CompanyAdmin` or `InstanceAdmin`. +- Company tenant access checks compare route tenant ID to the `TenantID` claim, with `InstanceAdmin` bypass. +- Tests exist for unauthorized company/payment/lookup access, forbidden cross-company access and locked payment field behavior. + +### Concerns + +- `CompanyController.GetAllCompanies` is protected only by `CompanyMember`; comments indicate a TODO for `InstanceAdmin`. If this endpoint returns all companies, it is a potential horizontal data exposure unless the service layer scopes results for non-admin users. This should be treated as high priority. +- `PaymentController.GetPayments` honors caller-supplied `IncludeAllUsers` and tenant filters are set only if missing. This must be proven to prevent client-supplied tenant override. Tests exist around cross-company behavior, but this remains a sensitive access-control surface. +- Some tests accept either `Forbidden` or `NotFound`, which may be intentional anti-enumeration, but also makes exact access-control semantics less crisp. + +### Security Rating + +Authorization has meaningful coverage, but company listing and payment filtering should be reviewed manually and with negative tests before exposure. + +## Tenant Isolation + +### Strengths + +- Tenant ID is embedded in JWT claims. +- Company APIs have explicit `CanAccessTenant` checks. +- Infrastructure queries frequently filter by `TenantID`. +- Payment and company tests cover wrong-company and cross-company scenarios. + +### Concerns + +- Tenant isolation appears implemented at service/query level rather than guaranteed by database row-level security. That can be acceptable, but it puts more burden on test coverage and code review. +- Historical DB tests remain excluded from compilation under `tests/OpenCashFlow.Test/Tests/db/**/*.cs`; they include persistence and cross-company concerns. Some of the valuable scenarios have been migrated, but this remains a residual assurance gap. +- `InstanceAdmin` bypass is broad. It needs explicit audit logging, operational controls and clear business rules. + +### Security Rating + +Tenant isolation is promising but should be a primary pentest target. + +## Secrets And Credential Management + +### Strengths + +- `.env.example` exists. +- Only example/local env files were found in the shallow search. +- Secret management docs explicitly forbid real checked-in secrets and describe rotation. +- Root Compose uses env vars for JWT/SMTP overrides. + +### Concerns + +- Root Docker Compose defaults include: + - `POSTGRES_PASSWORD=postgres` + - API connection string with `Password=postgres` + - fallback JWT secret. +- There is no secret-manager integration, Docker secrets, Kubernetes secrets or SOPS-based workflow. +- Rotation procedures are documented, not automated. +- No evidence of secret scanning workflow was found. + +### Security Rating + +Acceptable for local evaluation; not acceptable for production. + +## Dependency Hygiene And Supply Chain + +### Strengths + +- Core packages target .NET 10. +- Microsoft packages are aligned on `10.0.9`. +- Quality gate runs NuGet vulnerability audit. +- Dependency alert reconciliation document exists. +- No npm manifests or lockfiles were found, reducing false npm advisory surface from vendored static assets. + +### Concerns + +- This review did not independently complete a local `dotnet list ... --vulnerable` scan. CI should be treated as authoritative for that gate. +- Docker base images use floating tags such as `mcr.microsoft.com/dotnet/aspnet:10.0`, `sdk:10.0` and `postgres:16-alpine`; images are not digest-pinned. +- GitHub Actions use floating major tags such as `actions/checkout@v4`, `actions/setup-dotnet@v4`, `actions/upload-artifact@v4`. +- No SBOM generation workflow was observed. +- No container image vulnerability scan workflow was observed. +- Vendored frontend libraries remain under `wwwroot/libs`; package manifests are gone, but static asset provenance still needs periodic review. + +### Security Rating + +Good NuGet direction; incomplete supply-chain hardening for production. + +## Docker Security + +### Strengths + +- Multi-stage Dockerfiles reduce final image size. +- Compose has PostgreSQL healthcheck. +- Separate smoke Compose exists for disposable test runs. + +### Concerns + +- Dockerfiles do not set a non-root `USER`. +- Containers have no read-only filesystem, dropped capabilities, `no-new-privileges`, seccomp profile or resource limits. +- PostgreSQL is exposed on host port `5432`. +- Compose is production-looking enough to be dangerous but contains local-only defaults. +- No image signing or provenance evidence. + +### Security Rating + +Docker is suitable for local evaluation. It is not hardened for Internet-facing production. + +## OWASP Review + +### A01 Broken Access Control + +Primary risk. Tenant isolation and role enforcement exist, but company listing, payment filters and `InstanceAdmin` bypass require adversarial testing. + +### A02 Cryptographic Failures + +JWT validation is present. Risks remain around fallback secrets, cookie Secure behavior behind proxies, no observed server-side token revocation and operational secret handling. + +### A03 Injection + +EF Core reduces SQL injection risk. Payment export uses CSV escaping for quotes. Dynamic filtering dependencies exist (`System.Linq.Dynamic.Core`), so filter/sort inputs should stay constrained. + +### A04 Insecure Design + +Fast-login/PIN and self-hosted setup are sensitive design areas. They may be valid, but require threat-model review and abuse testing. + +### A05 Security Misconfiguration + +High risk in deployment defaults: exposed DB port, default DB password, fallback JWT secret, `AUTO_MIGRATE=true`, direct HTTP ports and missing forwarded-header handling. + +### A06 Vulnerable And Outdated Components + +CI includes advisory scan, but Docker/GitHub Action/container image scanning are incomplete. + +### A07 Identification And Authentication Failures + +Rate limiting exists. Password reset and fast-login remain priority review areas. Account enumeration behavior must be verified. + +### A08 Software And Data Integrity Failures + +No digest pinning, SBOM or image signing. GitHub workflows are conservative but not fully supply-chain hardened. + +### A09 Security Logging And Monitoring Failures + +Audit logging exists for application actions, but operational security monitoring, alerting and incident response are not complete. + +### A10 SSRF + +No obvious SSRF-heavy surface was identified in this pass, but email/template/link generation and any future import/export features should be reviewed. + +## Headers And CSP + +### Strengths + +- WebApp emits: + - `Content-Security-Policy` + - `X-Frame-Options: DENY` + - `X-Content-Type-Options: nosniff` + - `Referrer-Policy: strict-origin-when-cross-origin` + - `Permissions-Policy`. +- CSP avoids `unsafe-inline` and `unsafe-eval`. +- CSP uses per-request nonce. +- ZAP baseline does not appear to allowlist CSP warnings. + +### Concerns + +- CSP still allows `https://cdn.jsdelivr.net` for scripts and styles. +- CSP nonce is still needed for inline Razor scripts/styles in some areas. +- API does not appear to emit the same full security header set. +- `connect-src` includes localhost and host.docker.internal values intended for local/container evaluation. +- No explicit `Strict-Transport-Security` header was observed in API; WebApp uses HSTS outside development. + +### Security Rating + +CSP is materially better than many preview apps, but still not final production posture. + +## GitHub Workflows + +### Strengths + +- Quality gates run on non-draft PRs and manual dispatch. +- Permissions are minimal in quality gate (`contents: read`). +- Quality gate includes restore, vulnerability audit, Release build, Release tests and Compose validation. +- ZAP baseline and full scans run on non-draft PRs to `main`. +- Legacy promotion/deploy workflows are absent from current workflow list. + +### Concerns + +- ZAP full scan quality gate parses HTML by counting `Medium` and `High` strings, which is less reliable than structured JSON/XML parsing. +- Actions are not SHA-pinned. +- No explicit dependency-review action was observed. +- No CodeQL workflow was observed. +- No secret scanning workflow or documentation gate was observed, though GitHub may provide secret scanning outside repository workflows. +- No SBOM/container scan workflow was observed. + +### Security Rating + +Good PR baseline. Not yet enterprise-grade supply-chain CI. + +## Attack Surface + +High-value surfaces: + +- Login and JWT issuing. +- Fast-login/PIN flow. +- Password reset token generation/validation. +- Registration and account confirmation. +- Setup/bootstrap first admin flow. +- Company CRUD and tenant boundary. +- Payment create/update/delete and export. +- Cash ledger. +- Audit log. +- Swagger/OpenAPI if exposed in production. +- Docker-exposed PostgreSQL port. +- Vendored frontend libraries. + +Primary attack classes to test: + +- tenant ID tampering; +- role escalation; +- payment method/document type cross-tenant references; +- JWT audience/issuer confusion; +- cookie security behind reverse proxy; +- reset-token enumeration/replay; +- PIN brute force and lockout bypass; +- CSRF-like risks around cookie-authenticated WebApp actions; +- CSV injection in exports; +- CSP bypass; +- exposed dev/setup endpoints. + +## Decision Answers + +### Would I approve this for an external penetration test? + +Yes, for a controlled non-production environment. + +The app has enough security control surface to make a pentest valuable. Scope should include auth, tenant isolation, payment/cash flows, setup, reset password, fast-login/PIN, Docker config and CSP. Test data must be synthetic. + +### Would I approve a bug bounty? + +Not a public bug bounty. + +I would approve a private invite-only assessment after fixing obvious deployment defaults, reverse-proxy cookie behavior, company listing authorization ambiguity and ZAP full scan parsing. A public bounty before Internet readiness would produce avoidable noise and risk. + +### Would I expose this to the Internet? + +No, not as-is. + +Before Internet exposure, I would require: + +- production reverse proxy with forwarded headers correctly configured; +- no fallback JWT/database secrets; +- PostgreSQL not publicly exposed; +- `AUTO_MIGRATE=false`; +- production CSP without CDN dependency or with justified source policy; +- full dependency/container scan; +- reviewed reset/PIN flows; +- explicit production security headers for API and WebApp. + +### Would I deploy it for customers? + +No. + +For customers, the app needs stronger supply-chain controls, production secret management, Docker hardening, security monitoring, incident response, penetration test results and explicit closure of high-risk auth/tenant questions. + +## Risk Ratings + +| Area | Rating | Notes | +| --- | --- | --- | +| Authentication | Medium | Good JWT validation; fast-login/PIN, reset-token and reverse-proxy cookie behavior need review. | +| Authorization | Medium-High | Role policies exist; company list and filter override surfaces need proof. | +| Tenant isolation | Medium-High | Tests exist, but isolation is mostly application-layer and should be pentested. | +| Secrets | High for production | Defaults are local-only; no secret-manager integration. | +| Dependency hygiene | Medium | NuGet gate exists; Docker/action/SBOM/container scan gaps remain. | +| Docker | High for production | Local Compose is not hardened and exposes DB. | +| Headers/CSP | Medium | Strong CSP direction, but CDN and nonce debt remain. | +| GitHub workflows | Medium | Good gates, but no CodeQL/SBOM/dependency-review and unpinned actions. | +| Overall AppSec posture | Medium-High | Good developer-preview foundation; not Internet/customer ready. | + +## Required Fixes Before Internet Exposure + +1. Add and verify forwarded-header handling for API/WebApp behind TLS reverse proxy. +2. Ensure auth cookies are always `Secure` in production, independent of direct `Request.IsHttps` when behind proxy. +3. Remove or strictly scope company-wide list access for non-InstanceAdmin users. +4. Add explicit negative tests for client-supplied tenant/filter override in payment list/report/export paths. +5. Review password reset response behavior for account enumeration. +6. Threat-model and pentest fast-login/PIN. +7. Remove production fallback secrets and provide a production deployment template that requires explicit secret injection. +8. Disable public PostgreSQL exposure in production examples. +9. Set `AUTO_MIGRATE=false` in production examples. +10. Add Docker hardening: non-root user, resource limits and reduced privileges where possible. +11. Add container image vulnerability scanning. +12. Add CodeQL or equivalent SAST. +13. Add SBOM generation. +14. Pin GitHub Actions and container images by digest or document an update policy. +15. Replace ZAP full-scan HTML grep gate with structured report parsing. +16. Close or formally reconcile GitHub dependency alerts. + +## Final Security Conclusion + +OpenCashFlow has a serious security foundation for a developer preview. It is not a casual toy project: JWT validation, role policies, tenant tests, rate limiting, CSP, ZAP and dependency gates are visible. + +That said, I would not expose it to the Internet or deploy it for customers today. The remaining issues are not cosmetic; they touch authentication transport assumptions, access-control ambiguity, production secrets, Docker hardening, supply-chain controls and operational security monitoring. + +Security approval status: controlled pentest yes; public bug bounty no; Internet exposure no; customer deployment no. diff --git a/Docs/audits/opencashflow-sre-audit.md b/Docs/audits/opencashflow-sre-audit.md new file mode 100644 index 0000000..33e7373 --- /dev/null +++ b/Docs/audits/opencashflow-sre-audit.md @@ -0,0 +1,374 @@ +# OpenCashFlow SRE Audit + +Date: 2026-07-08 + +Perspective: Senior Site Reliability Engineer evaluating whether OpenCashFlow can actually be operated. This review ignores software architecture except where it affects operability. + +Scope: observable evidence in the repository only. + +## Executive SRE Verdict + +An operations team should not sleep comfortably with OpenCashFlow in business-critical production today. + +The project has a credible local operations foundation: Docker Compose, API health endpoint, PostgreSQL container healthcheck, Release build/test gates, clean-install smoke automation, and an isolated backup/restore smoke drill. Those are useful and unusually concrete for a developer preview. + +However, the production operations story is not complete. The root Compose file is explicitly local-evaluation oriented, secrets are environment-variable examples rather than secret-manager integration, `AUTO_MIGRATE=true` is the default in Compose, PostgreSQL is exposed on the host, observability is mostly logging guidance, Sentry is present but commented out, backup/restore is proven only against a disposable smoke stack, and upgrade/rollback is explicitly not fully performed. + +If production goes down at 3 AM, an experienced engineer could probably inspect logs, check `/health`, restart containers and restore a small local-style database from a manual dump. They would not have a proven production-grade runbook, RPO/RTO target, alerting path, migration rollback drill or disaster recovery process. + +## Evidence Reviewed + +Files and areas inspected: + +- `docker-compose.yml` +- `scripts/smoke/docker-compose.clean-install.yml` +- `scripts/smoke/clean-install-smoke.sh` +- `scripts/smoke/backup-restore-smoke-drill.sh` +- `Docs/ops/production-hardening.md` +- `Docs/ops/secrets-management.md` +- `Docs/ops/backup-restore-drill.md` +- `Docs/ops/upgrade-migration-drill.md` +- `Docs/testing/clean-install-smoke.md` +- `.github/workflows/quality-gates.yml` +- `.github/workflows/zap-baseline.yml` +- `.github/workflows/zap-full.yml` +- API/WebApp startup files for health, migrations, logging and headers. + +Observed local verification from the current working tree: + +- Release build previously passed with 0 warnings and 0 errors. +- Release tests previously passed with 261 passed, 0 failed, 0 skipped. +- `docker compose config` previously passed. +- Backup/restore smoke drill documentation records successful restore verification for `Companies`, `Payments`, and `CashLedgers` counts. + +## Deployment + +Current deployment maturity is local-evaluation focused. + +Positive evidence: + +- Root `docker-compose.yml` starts PostgreSQL, API and WebApp. +- API and WebApp Dockerfiles exist and are used by Compose. +- Docker Compose has a PostgreSQL healthcheck. +- Compose config validates. +- README and ops docs clearly warn that Compose defaults are local-evaluation defaults. + +Operational concerns: + +- PostgreSQL is exposed on host port `5432`. +- PostgreSQL credentials default to `postgres/postgres`. +- API connection string defaults to the same superuser-style credentials. +- `AUTO_MIGRATE=true` is set in root Compose. +- HTTP ports are exposed directly. +- No production Compose, Helm chart, Terraform, systemd unit, or reference reverse-proxy deployment is present. +- No demonstrated external PostgreSQL deployment path. +- No demonstrated blue/green, canary, rolling restart, or maintenance-window process. + +SRE judgement: deployable locally; not production deployment-ready. + +## Docker + +Docker is useful but not hardened. + +Strengths: + +- Isolated smoke Compose stack exists with separate ports and project name. +- Smoke stack avoids colliding with the default local stack. +- `docker compose config` is a quality gate. + +Weaknesses: + +- Fixed container names in root Compose make multiple local instances awkward. +- Root Compose is not safe as a production template without substantial changes. +- No resource limits, restart policies, log driver settings, read-only filesystem settings, healthcheck for API/WebApp containers, or backup sidecar/job. +- Database data lives in a local Docker volume without retention or off-host replication. + +SRE judgement: adequate for developer evaluation; insufficient for operations. + +## Secrets + +Secrets handling is documented, not operationalized. + +Positive evidence: + +- `Docs/ops/secrets-management.md` lists required production secrets and rotation procedures. +- README warns to replace secrets before exposure. +- `.env.example` exists. + +Risks: + +- Compose uses fallback JWT secret `local-development-secret-key-change-before-production-0001`. +- Compose uses `postgres/postgres`. +- No integration with Docker secrets, Kubernetes secrets, Vault, cloud secret manager, SOPS, 1Password, AWS/GCP/Azure secret managers, or equivalent. +- Rotation is a manual runbook, not an implemented operational procedure. + +SRE judgement: acceptable for local evaluation; production secret management is unimplemented. + +## Logging + +Logging exists but is not operationally complete. + +Positive evidence: + +- API and WebApp use Serilog. +- WebApp logs to console and a file path under `../Logs`. +- API has Serilog startup configuration. +- Docs list operational signals that should be retained. + +Concerns: + +- No centralized log collection configuration. +- No structured log schema contract. +- No log retention policy implemented. +- No log redaction tests. +- Sentry packages/config exist, but Sentry startup/tracing appears commented out. +- Slack logging examples are commented and not a production alerting plan. + +SRE judgement: enough for local debugging; not enough for incident response. + +## Monitoring And Alerting + +Monitoring maturity is low. + +Positive evidence: + +- API exposes `/health`. +- Docker Compose checks PostgreSQL health. +- CI waits for `/health` in smoke/ZAP flows. + +Missing: + +- WebApp health endpoint. +- Readiness/liveness separation. +- Database dependency health in API health output. +- Metrics endpoint. +- Dashboards. +- Alert thresholds. +- Pager policy. +- Synthetic monitoring. +- Backup freshness monitoring. +- Disk, CPU, memory and queue monitoring. +- Error-budget/SLO definitions. + +SRE judgement: operators would detect problems manually or by external tooling they build themselves. + +## Backups + +Backup maturity has improved but remains local. + +Positive evidence: + +- `Docs/ops/backup-restore-drill.md` documents `pg_dump --format=custom`. +- `scripts/smoke/backup-restore-smoke-drill.sh` automates backup and restore using an isolated smoke stack. +- The documented local result verifies: + - `Companies: 1` + - `Payments: 1` + - `CashLedgers: 1` + - smoke-created company/payment/cash ledger matches. + +Missing: + +- Scheduled backup job. +- Off-host/off-region backup target. +- Encryption at rest for backup artifacts. +- Retention policy. +- Backup failure alerting. +- Backup restore ownership. +- RPO/RTO targets. +- Production database-size restore timing. + +SRE judgement: backup concept is proven locally; production backup system is not present. + +## Restore + +Restore is partially proven. + +Positive evidence: + +- Restore into `opencashflow_restore` is automated in the smoke drill. +- Verification queries confirm representative smoke data. + +Limitations: + +- Restore is into a second database in the same disposable PostgreSQL container. +- No restore into a separate host/container/service. +- No restored application startup against the restored database is proven in the script. +- No production-like dataset. +- No restore timing measurement. +- No operator checklist for deciding restore point and communicating impact. + +SRE judgement: encouraging local proof; not enough for a 3 AM production recovery guarantee. + +## Migration + +Migration readiness is a blocker. + +Positive evidence: + +- EF Core migrations are present in Infrastructure. +- `AUTO_MIGRATE` is configurable. +- Docs recommend `AUTO_MIGRATE=false` in production and explicit migration steps. + +Critical gap: + +- `Docs/ops/upgrade-migration-drill.md` explicitly states the upgrade/migration drill was not fully performed. +- No prior release database snapshot exists in the repo. +- No migration runbook with exact precheck, apply, verify and abort commands has been proven. +- No automated migration smoke against copied historical data. + +SRE judgement: not production-ready. + +## Rollback + +Rollback is not proven. + +Current documented rollback position is effectively “restore the pre-upgrade backup” for destructive migrations. That can be a valid strategy, but it must be drilled against realistic data and timed. + +Missing: + +- tested rollback procedure; +- data-loss decision tree; +- operator checklist; +- restore-point selection; +- customer communication plan; +- rollback validation script; +- failed-migration recovery procedure. + +SRE judgement: rollback readiness is inadequate. + +## Health Checks + +Current state: + +- API has `/health`. +- PostgreSQL container has `pg_isready`. +- CI and smoke scripts use API `/health`. +- WebApp reachability is checked by HTTP status in smoke workflows. + +Gaps: + +- API health appears minimal; no evidence from this audit of full dependency breakdown. +- WebApp has no dedicated `/health`. +- No readiness endpoint to block traffic before dependencies are ready. +- No liveness endpoint distinction. +- No health check auth/tenant/payment/cash dependency probe. + +SRE judgement: good minimum for local smoke; insufficient for orchestrated production operations. + +## Recovery And Disaster Recovery + +Disaster recovery is documented as a need, not implemented. + +There is no observable evidence of: + +- DR environment; +- restore into alternate region/host; +- backup replication; +- DNS failover; +- runbook for database corruption; +- runbook for lost admin access beyond installation notes; +- incident commander checklist; +- communications template; +- post-incident review template. + +SRE judgement: DR is not ready. + +## Incident Response + +Incident response is partial. + +Positive evidence: + +- `Docs/ops/secrets-management.md` includes steps for leaked secrets. +- Security docs warn not to publish exploitable details. +- Production hardening docs list signals to monitor. + +Missing: + +- severity levels; +- escalation paths; +- on-call ownership; +- contact list; +- incident timeline template; +- customer notification criteria; +- recovery decision matrix; +- audit evidence preservation procedure. + +SRE judgement: no complete incident response system. + +## Runbooks + +Existing runbooks/docs: + +- production hardening; +- secrets management; +- backup/restore drill; +- upgrade/migration drill plan; +- clean-install smoke; +- database recovery and connection guide. + +Runbook quality: + +- Good for maintainers and evaluators. +- Too incomplete for on-call production operators. +- Some docs are plans rather than executed procedures. +- No single “3 AM outage” runbook exists. + +SRE judgement: a good start, not enough for reliable operations. + +## Can Operations Sleep At Night? + +Not for production. + +They can sleep if OpenCashFlow is running as a disposable evaluation environment with no critical data. They cannot sleep if it holds financial data for many companies without additional operational engineering. + +## If Production Goes Down At 3 AM + +Could they recover? + +- Maybe, if the failure is simple container restart, local database issue, or known health endpoint failure. +- Not confidently for data corruption, bad migration, secret compromise, database volume failure, or regional outage. + +Could they restore? + +- They have a local `pg_dump`/`pg_restore` pattern and smoke proof. +- They do not have a production backup system, off-host backups, restore timing, or RPO/RTO evidence. + +Would they know what to do? + +- For local smoke/evaluation: mostly yes. +- For production incident: not reliably. The repository lacks a complete incident runbook. + +## Scores + +Scale: 0 is absent; 10 is production-grade and proven. + +| Area | Score | Rationale | +| --- | ---: | --- | +| Operations readiness | 4.0 | Local run paths and docs exist, but no production operating model, alerting, SLOs or on-call runbooks. | +| Recovery readiness | 4.0 | Local backup/restore smoke is proven; production backup/restore, RPO/RTO and DR are not proven. | +| Deployment maturity | 3.5 | Docker Compose works for evaluation, but root Compose is not a production deployment artifact. | +| Operational risk | 7.5 | High risk for business-critical use due to unproven migration, rollback, monitoring, DR and production restore. | + +## Required SRE Work Before Production + +1. Add production deployment reference with TLS reverse proxy, external PostgreSQL, secret injection and `AUTO_MIGRATE=false`. +2. Add API readiness/liveness checks and WebApp health endpoint. +3. Add structured centralized logging guidance and a working example. +4. Add metrics and alerting baseline: uptime, error rate, latency, DB connectivity, disk, backup freshness. +5. Implement scheduled backup automation with off-host storage and encryption. +6. Run restore drill into a separate environment and start the app against restored data. +7. Define and test RPO/RTO. +8. Execute upgrade/migration drill against a realistic previous-version database. +9. Execute rollback-by-restore drill and document timing. +10. Write a 3 AM incident runbook with severity, escalation, communication and recovery checklists. +11. Add DR plan for host/volume loss and database corruption. +12. Replace local-evaluation defaults in any production example. + +## Final SRE Decision + +OpenCashFlow can be operated as a developer-preview evaluation stack. + +OpenCashFlow cannot yet be responsibly operated as a business-critical financial system without a dedicated SRE hardening phase. + +I would not put customers or 120 companies on it until backup, restore, migration, rollback, monitoring and incident response are proven under production-like conditions. diff --git a/Docs/product/COMPETITOR_POSITIONING.md b/Docs/product/COMPETITOR_POSITIONING.md new file mode 100644 index 0000000..f4102a4 --- /dev/null +++ b/Docs/product/COMPETITOR_POSITIONING.md @@ -0,0 +1,225 @@ +# Competitor Positioning + +OpenCashFlow should not compete by copying broad business suites. + +It should compete by being the clearest self-hosted cash cockpit for small companies that do not want a full ERP. + +## Odoo + +## Why Customers Choose Odoo + +- broad business app suite; +- CRM, accounting, inventory, manufacturing, ecommerce, HR, projects, website, POS; +- large ecosystem and marketplace; +- partner network; +- commercial support and hosted options. + +## Why Customers Should Choose OpenCashFlow Instead + +- they do not want an ERP implementation; +- they already have accounting or operations tools; +- they need cash clarity faster than ERP rollout allows; +- they want a focused self-hosted cash decision layer. + +## What We Should Never Copy + +- app-suite sprawl; +- marketplace complexity before product-market fit; +- turning every adjacent business workflow into a module; +- positioning as "all-in-one". + +## What We Should Learn + +- strong ecosystem matters; +- industry templates matter; +- partner channels matter; +- modules are easier to sell when the core story is clear. + +## ERPNext + +## Why Customers Choose ERPNext + +- complete open-source ERP; +- manufacturing, stock, procurement, sales, accounting, CRM, HR, projects and POS; +- self-hosted and hosted options; +- active ecosystem; +- broad industry fit. + +## Why Customers Should Choose OpenCashFlow Instead + +- ERPNext is too broad for companies that only need cash decisions; +- the business does not want to migrate operations; +- cash planning can sit above existing systems; +- weekly owner workflow matters more than full business modeling. + +## What We Should Never Copy + +- full ERP scope; +- no-code/customization platform as the initial differentiator; +- trying to satisfy every industry process. + +## What We Should Learn + +- manufacturing and distribution use cases are valuable; +- partner and hosting models can fund open-source software; +- import/export and integration stories matter. + +## Dolibarr + +## Why Customers Choose Dolibarr + +- simple modular ERP/CRM; +- useful for freelancers and small businesses; +- invoicing, products, CRM, bank accounts, payments, stock and commercial workflows; +- easier than large ERPs. + +## Why Customers Should Choose OpenCashFlow Instead + +- they need cash control, not a suite; +- they want fewer modules and more decisions; +- they already have invoicing/CRM; +- owner dashboard and forecast matter more than broad operations. + +## What We Should Never Copy + +- becoming a generic small-business suite; +- adding CRM/product/catalog features just because they are common. + +## What We Should Learn + +- simplicity is a competitive advantage; +- modularity is useful when it does not dilute the core; +- small businesses value practical workflows. + +## Akaunting + +## Why Customers Choose Akaunting + +- small-business accounting; +- invoicing and expenses; +- finance records; +- cloud/accounting workflow. + +## Why Customers Should Choose OpenCashFlow Instead + +- accounting is already handled elsewhere; +- the owner needs cash runway and commitment visibility; +- operational timing matters more than ledger reporting; +- OpenCashFlow is a cash decision tool, not the accounting system. + +## What We Should Never Copy + +- becoming accounting-first; +- taking on regional accounting compliance as a core burden; +- presenting as a bookkeeper replacement. + +## What We Should Learn + +- small businesses need simple financial language; +- invoices and expenses are important inputs; +- export to accountants matters. + +## Invoice Ninja + +## Why Customers Choose Invoice Ninja + +- invoicing and quotes; +- online payments; +- client portal; +- recurring billing; +- expenses and vendors; +- bank sync and integrations; +- strong small-business/freelancer positioning. + +## Why Customers Should Choose OpenCashFlow Instead + +- invoicing is only one part of cash risk; +- supplier payments, payroll, taxes, loans and materials matter too; +- the business needs a cash calendar and decision cockpit; +- OpenCashFlow can ingest invoice data without becoming invoice-first. + +## What We Should Never Copy + +- centering the product around invoice creation; +- competing on invoice templates; +- becoming a payment gateway product. + +## What We Should Learn + +- clear small-business positioning works; +- onboarding speed matters; +- payment collection has obvious business value; +- customer portals can become later inputs, not core identity. + +## Firefly III + +## Why Customers Choose Firefly III + +- self-hosted personal finance; +- transaction tracking; +- budgets and categories; +- privacy and ownership. + +## Why Customers Should Choose OpenCashFlow Instead + +- OpenCashFlow is for business cash decisions; +- multi-user roles, companies, audit and supplier/customer context matter; +- receivables/payables and operational commitments are business workflows. + +## What We Should Never Copy + +- personal finance framing; +- consumer budgeting language; +- household-style category management as the core. + +## What We Should Learn + +- privacy/self-hosting is emotionally important; +- finance products must feel trustworthy; +- transaction clarity matters. + +## Actual Budget + +## Why Customers Choose Actual Budget + +- simple budgeting; +- local-first finance; +- fast interface; +- envelope-budgeting model; +- personal control over data. + +## Why Customers Should Choose OpenCashFlow Instead + +- OpenCashFlow is for teams and businesses; +- the problem is operational cash timing, not personal envelopes; +- supplier/customer commitments and accountant collaboration are business needs. + +## What We Should Never Copy + +- personal budgeting as the product model; +- consumer-first language; +- feature simplicity that ignores business audit and role needs. + +## What We Should Learn + +- speed and clarity win; +- local-first/self-hosted ownership is a strong value; +- users like simple mental models. + +## Positioning Summary + +Competitors win on breadth, maturity, accounting, invoicing, ERP, or personal finance. + +OpenCashFlow should win on one thing: + +> weekly operational cash decisions for small businesses. + +Reference signals reviewed: + +- ERPNext: https://erpnext.com/ +- Odoo overview: https://en.wikipedia.org/wiki/Odoo +- Dolibarr: https://www.dolibarr.org/ +- Akaunting: https://akaunting.com/ +- Invoice Ninja: https://www.invoiceninja.com/ +- Firefly III: https://www.firefly-iii.org/ +- Actual Budget: https://actualbudget.org/ diff --git a/Docs/product/FEATURE_FILTER.md b/Docs/product/FEATURE_FILTER.md new file mode 100644 index 0000000..65aaeff --- /dev/null +++ b/Docs/product/FEATURE_FILTER.md @@ -0,0 +1,154 @@ +# Feature Filter + +OpenCashFlow should reject good ideas that weaken the product identity. + +The product is: + +> the self-hosted cash cockpit for small businesses. + +Every proposed feature must pass this filter. + +## The Six Questions + +## 1. Does It Improve Cash Visibility? + +Accept if the feature helps users see: + +- current cash; +- expected cash; +- committed cash; +- overdue cash; +- safe cash; +- cash runway; +- cash risk. + +Reject if it only adds generic business data. + +## 2. Does It Improve Decisions? + +Accept if the feature helps users decide: + +- what to pay; +- what to delay; +- what to chase; +- what to reserve; +- what to buy; +- what not to buy. + +Reject if it creates reports without decisions. + +## 3. Does It Reduce Uncertainty? + +Accept if the feature makes assumptions visible: + +- payment confidence; +- expected date; +- source; +- scenario; +- owner; +- status. + +Reject if it adds numbers nobody can trust or explain. + +## 4. Does It Help The Weekly Workflow? + +Accept if the feature makes the weekly cash ritual faster, clearer, or more complete. + +Reject if it is useful only once during setup or belongs in administration. + +## 5. Does It Integrate With Existing Systems? + +Accept if the feature helps OpenCashFlow work with: + +- bank exports; +- spreadsheets; +- accounting systems; +- invoicing tools; +- ERPs; +- accountant workflows. + +Reject if it tries to replace those systems without a compelling cash reason. + +## 6. Does It Strengthen Cash Custody And Integrity? + +Accept if the feature makes responsibility for company money more provable: + +- source/origin; +- custodian or destination; +- cash source; +- employee cash handler; +- reason/category; +- actor; +- audit trail; +- expected vs actual cash; +- discrepancy status. + +Reject or redesign if it creates cash numbers without a custody chain that can be reconciled and explained. + +## ERP Drift Test + +Ask: + +> Are we building this because cash decisions require it, or because ERPs usually have it? + +If the answer is "because ERPs usually have it", reject. + +## Default Rejection List + +Reject from core unless a later product decision overrides: + +- full CRM; +- full inventory; +- full MRP; +- payroll; +- ecommerce; +- POS; +- HR suite; +- tax compliance engine; +- certified accounting; +- project management; +- help desk; +- marketing automation. + +## Acceptable Core Areas + +Accept in core when scoped tightly: + +- payments; +- cash ledger; +- cash forecast; +- receivables/payables lite; +- commitments; +- scenarios; +- alerts; +- owner dashboard; +- accountant summary; +- imports/exports; +- audit. + +## Feature Scoring + +Score each proposal 0-2: + +| Question | 0 | 1 | 2 | +| --- | --- | --- | --- | +| Cash visibility | No effect | Indirect | Direct | +| Decisions | No decision | Informs decision | Drives action | +| Uncertainty | Adds ambiguity | Neutral | Clarifies assumptions | +| Weekly workflow | No role | Occasional | Weekly habit | +| Integration | Isolated | Export/import later | Connects existing system | +| Cash custody/integrity | Weakens proof | Neutral | Strengthens proof | + +Decision: + +- 10-12: strong candidate; +- 7-9: consider if small; +- 0-4: reject or move outside core. + +## Product Rule + +When in doubt, choose the feature that helps an owner decide what to do this week. + +Cash features must pass one extra rule: + +> If the custody chain cannot be reconciled, do not use the number as trusted cash. diff --git a/Docs/product/FIRST_DEMO.md b/Docs/product/FIRST_DEMO.md new file mode 100644 index 0000000..2b34015 --- /dev/null +++ b/Docs/product/FIRST_DEMO.md @@ -0,0 +1,183 @@ +# First Public Demo + +The first demo should make the product obvious in five minutes. + +It should not show every feature. + +It should tell one story: + +> A small workshop looks healthy by bank balance, but cash risk is hiding in late customers, supplier payments, payroll, +> VAT, and a machine purchase. OpenCashFlow reveals the risk before it becomes a crisis. + +## Demo Company + +Name: + +> Northside Workshop + +Business: + +- small metal fabrication shop; +- 18 employees; +- serves local industrial customers; +- buys materials before customer payments arrive; +- relies on a few large customers; +- tracks cash in spreadsheets today. + +## Starting Situation + +Bank balance: + +> 84,000 + +At first glance, the company looks safe. + +But upcoming commitments: + +- payroll: 38,000 due Friday; +- VAT/tax: 21,000 due next week; +- steel supplier: 27,500 due in 10 days; +- rent: 6,000 due this month; +- loan payment: 4,200 due this month. + +Expected receivables: + +- ACME: 52,000 due today, likely late; +- Bright Machines: 18,500 due next week; +- Local Construction: 9,400 overdue by 12 days. + +Optional decision: + +- buy a used machine for 32,000 this week. + +## Demo Flow + +## 1. The Bank Balance Looks Fine + +Open the dashboard. + +Show: + +- cash now: 84,000; +- safe cash: much lower; +- committed cash: high; +- alert count: meaningful. + +Message: + +> The bank balance is not the answer. + +## 2. ACME Is Late + +Open receivables. + +Show: + +- ACME payment expected today; +- confidence low; +- historical delay note; +- projected impact. + +Message: + +> One late customer changes the next two weeks. + +## 3. Supplier And Payroll Collide + +Open commitments. + +Show: + +- payroll due Friday; +- supplier due soon; +- tax/VAT due next week; +- projected cash low point. + +Message: + +> The problem is timing, not profitability. + +## 4. Run Scenario: ACME Pays 15 Days Late + +Scenario: + +- ACME payment delayed 15 days. + +Result: + +- projected low balance turns negative or below reserve; +- safe cash disappears; +- alert explains why. + +Message: + +> OpenCashFlow shows the future before the bank does. + +## 5. Run Scenario: Delay Machine Purchase + +Scenario: + +- machine purchase delayed 30 days. + +Result: + +- runway improves; +- payroll and supplier can be paid; +- tax reserve survives. + +Message: + +> Decisions become visible. + +## 6. Run Scenario: Split Supplier Payment + +Scenario: + +- pay supplier 50% now, 50% after ACME pays. + +Result: + +- cash low point improves; +- supplier risk noted; +- action created. + +Message: + +> The owner can negotiate before the crisis. + +## 7. Weekly Action Summary + +Show: + +- call ACME today; +- ask supplier for split payment; +- delay machine purchase; +- reserve VAT; +- approve payroll; +- send summary to accountant. + +Message: + +> This is not a report. It is a decision ritual. + +## Demo Ending + +End with: + +> OpenCashFlow helped the owner avoid a cash surprise without replacing accounting, invoicing, or ERP. + +## What Not To Demo + +Do not lead with: + +- settings; +- architecture; +- user roles; +- generic payment list; +- admin screens; +- module catalogs; +- technical setup. + +Those matter later. + +The first demo must make the cash pain obvious. diff --git a/Docs/product/LEAN_ROADMAP.md b/Docs/product/LEAN_ROADMAP.md new file mode 100644 index 0000000..6dee820 --- /dev/null +++ b/Docs/product/LEAN_ROADMAP.md @@ -0,0 +1,472 @@ +# Lean Product Roadmap + +Date: 2026-07-08 + +Role: Product Guardian. + +Goal: + +> Remove anything that does not make OpenCashFlow a better Cash Cockpit. + +This roadmap is intentionally smaller than `PRODUCT_ROADMAP.md`. + +It cuts feature ambition in favor of one habit: + +> Every week, the owner opens OpenCashFlow to answer: what can we safely pay? + +## Product Guardrail + +Keep only work that directly strengthens at least one of these: + +- Safe-to-Pay Radar; +- weekly cash decisions; +- cash visibility; +- uncertainty reduction; +- trust in the forecast; +- cash custody and cash integrity; +- import of existing cash facts; +- owner action this week. + +Move to Later if useful but not essential to the first habit. + +Delete if it drifts toward ERP, marketplace, broad business management, or premature go-to-market machinery. + +## The Lean Roadmap + +## Now + +Goal: + +Make the Cash Cockpit identity impossible to misunderstand. + +Keep: + +1. Adopt "self-hosted cash cockpit" across product messaging. +2. Define Safe-to-Pay Radar as the core feature. +3. Define the weekly cash ritual as the primary workflow. +4. Define owner dashboard around safe cash, committed cash, expected cash, risk, and weekly actions. +5. Keep the feature filter explicit. +6. Define Cash Custody as the required foundation for trusted cash, with Cash Integrity as the proof capability. + +Cut from Now: + +- broad demo story work unless it directly demonstrates Safe-to-Pay Radar. + +Why: + +The product does not need many stories. It needs one unavoidable story: "Can I safely pay this?" + +## Next Month + +Goal: + +Make one owner understand the product in one realistic cash decision. + +Keep: + +1. Cash Custody foundation: + - cash account/source; + - custodian/holder; + - employee cash handler; + - immutable movement; + - custody transfer; + - reversal/correction; + - daily reconciliation; + - discrepancy visibility. +2. Safe-to-Pay Radar concept and prototype, clearly marked WIP until Cash Custody and Cash Integrity are implemented. +3. Workshop demo dataset only if it supports the Radar: + - late customer; + - supplier payment; + - payroll reserve; + - VAT/tax reserve; + - optional machine purchase; + - projected cash shortfall. +4. Owner dashboard concept reduced to: + - bank cash; + - safe cash; + - committed cash; + - expected cash; + - projected low point; + - recommended weekly actions. +5. Manual import templates for the minimum facts: + - bank balance/movements; + - expected receivables; + - supplier commitments; + - reserves. +6. Interview 10 target users specifically about payment approval anxiety and end-of-day cash mismatch anxiety. + +Move to Later: + +- full homepage copy system; +- broad interviews across four segments. + +Delete: + +- generic product-marketing polish that does not test Safe-to-Pay Radar. + +Why: + +The next month must validate the painful decision, not the brand system. Safe-to-Pay cannot be trusted until the cash +custody chain is auditable and reconcilable. + +## Next Quarter + +Goal: + +Make Safe-to-Pay Radar useful in a real weekly review. + +Keep: + +1. Payment Decision Queue: + - safe to pay; + - risky; + - unsafe; + - split recommended; + - delay recommended. +2. Safe Cash model: + - current cash; + - protected reserves; + - committed cash; + - expected cash; + - risky expected cash. +3. Receivables/payables lite, only as inputs to cash decisions. +4. Cash confidence levels: + - confirmed; + - expected; + - risky; + - hypothetical. +5. Basic scenarios: + - customer pays late; + - supplier split payment; + - payment delayed; + - tax/payroll reserve protected. +6. Cash Collision Timeline, only inside the Radar. +7. Weekly Action Summary: + - pay; + - delay; + - split; + - chase; + - reserve. + +Move to Later: + +- full cash forecast calendar; +- overdue aging as a standalone module; +- accountant/advisor summary export; +- CSV reconciliation queue. + +Delete: + +- any receivables/payables screen that looks like accounting software; +- any scenario builder that becomes financial modeling; +- any alert feed not tied to a payment decision. + +Why: + +The quarter should deliver a working answer to "what can we safely pay?", not a finance suite. + +## Next 6 Months + +Goal: + +Make the weekly habit repeatable and trusted. + +Keep: + +1. Bank CSV import and match queue. +2. Recurring commitments: + - payroll reserve; + - tax/VAT reserve; + - rent; + - loans; + - recurring supplier obligations. +3. What changed since last week: + - new commitments; + - changed due dates; + - changed confidence; + - forecast low point changed. +4. Customer payment promise tracker, only as forecast confidence input. +5. Decision log: + - paid; + - delayed; + - split; + - chased; + - reserved. +6. One accountant/share summary, limited to the weekly cash review. + +Move to Later: + +- accountant/advisor workspace; +- multi-client dashboard; +- importers from Odoo, ERPNext, Dolibarr, Akaunting, Invoice Ninja; +- workshop cash pack; +- public hosted demo; +- case studies. + +Delete: + +- adoption-loop features that do not improve the weekly owner workflow; +- broad partner tooling before the owner habit is proven. + +Why: + +If owners do not return weekly, accountant workspace and case studies are premature. + +## Next Year + +Goal: + +Turn a proven weekly cash ritual into a product people can adopt confidently. + +Keep: + +1. Stable Safe-to-Pay Radar. +2. Reliable import/export around the Radar. +3. Stable owner dashboard. +4. Cash review history. +5. Narrow accountant handoff: + - weekly summary; + - notes; + - export. +6. Comparison content only if it explains the focused category: + - OpenCashFlow vs spreadsheets; + - OpenCashFlow vs ERP for cash decisions; + - OpenCashFlow vs accounting reports. + +Move to Later: + +- productized onboarding packages; +- support subscription offer; +- certified self-hosted builds; +- managed hosting beta; +- official forecasting module; +- official accountant module; +- official workshop/manufacturing module; +- localization. + +Delete: + +- "become the default open-source cash cockpit" as a roadmap goal. + +Why: + +Market position is an outcome. The roadmap should describe product work. Do not build commercialization machinery before +the core weekly habit is proven. + +## Next 3 Years + +Goal: + +Expand only after Safe-to-Pay Radar becomes a trusted category-defining workflow. + +Keep: + +1. Regional bank connector packs, if they reduce manual cash uncertainty. +2. Country-specific tax calendar packs, if limited to cash reserve dates and not tax compliance. +3. Partner templates for weekly cash review. +4. API ecosystem for importing cash facts from existing systems. + +Move to Later: + +- managed hosting; +- certified releases and upgrade assurance; +- partner program; +- official training and certification; +- anonymized benchmarking; +- marketplace. + +Delete: + +- broad marketplace as a strategic goal until the core workflow has real pull; +- community templates for industries unless they directly support Safe-to-Pay Radar. + +Why: + +OpenCashFlow should earn expansion by owning one habit first. + +## Kept Roadmap Items + +These survive because they directly strengthen the Cash Cockpit. + +| Item | Why It Stays | +| --- | --- | +| Self-hosted cash cockpit positioning | Clarifies product identity. | +| Safe-to-Pay Radar | Core "I need this" feature. | +| Weekly cash ritual | Creates habit. | +| Owner dashboard | Main decision surface. | +| Feature filter | Prevents ERP drift. | +| Cash Custody | Proves who or what is responsible for company money before forecast recommendations. | +| Cash Integrity | Proves the custody chain is complete, immutable, reconciled, and explainable. | +| Workshop demo dataset | Demonstrates one painful cash decision. | +| Manual imports for bank/receivables/commitments/reserves | Feeds the Radar without integrations. | +| Payment Decision Queue | Turns cash visibility into action. | +| Safe Cash model | Separates bank balance from spendable cash. | +| Receivables/payables lite | Necessary inputs, not accounting replacement. | +| Cash confidence levels | Reduces false certainty. | +| Basic scenarios | Lets owners test decisions. | +| Cash Collision Timeline | Shows why a payment is risky. | +| Weekly Action Summary | Ends with decisions, not reports. | +| Bank CSV import | Reduces manual work and improves trust. | +| Recurring commitments | Protects payroll, tax, rent, loans. | +| What changed since last week | Builds trust and weekly habit. | +| Payment promise tracker | Improves forecast confidence. | +| Decision log | Records why cash decisions were made. | +| Narrow accountant summary | Supports weekly cash conversation. | +| Cash review history | Makes weekly ritual cumulative. | +| Focused comparison content | Explains category without feature bloat. | +| Bank connector packs | Later, only to reduce cash uncertainty. | +| Tax calendar packs | Later, only as reserve-date helpers. | +| Cash fact import API | Helps integrate without replacing systems. | + +## Moved To Later + +These may be useful, but not before Safe-to-Pay Radar proves the weekly habit. + +| Item | Why Later | +| --- | --- | +| Full homepage copy system | Useful, but messaging should follow validated feature pull. | +| Broad interviews across multiple segments | Too diffuse; start with payment anxiety in one niche. | +| Full cash forecast calendar | Useful, but can distract from payment decisions. | +| Overdue aging as standalone module | Risks becoming accounting/invoicing. Keep only as Radar input. | +| Accountant/advisor summary export | Valuable, but only after owner workflow is useful. | +| CSV reconciliation queue | Important, but not first decision value. | +| Accountant/advisor workspace | Distribution feature; premature before weekly owner habit. | +| Multi-client dashboard | Could become advisor SaaS before core is proven. | +| Importers from Odoo/ERPNext/Dolibarr/Akaunting/Invoice Ninja | Useful after manual import proves demand. | +| Workshop cash pack | Good niche packaging, but depends on Radar. | +| Public hosted demo | Useful after demo story stabilizes. | +| Case studies | Need real usage first. | +| Productized onboarding packages | Commercialization after habit proof. | +| Support subscription | Commercialization after stable adoption. | +| Certified self-hosted builds | Trust layer, not product wedge. | +| Managed hosting beta | Operations business; later. | +| Official forecasting module | Risk of module sprawl; keep forecast inside Radar first. | +| Official accountant module | Later if accountant summary proves pull. | +| Official workshop/manufacturing module | Later if workshop demo converts. | +| Localization | Later after core workflow is stable. | +| Partner program | Later after repeatable adoption. | +| Official training/certification | Later after product maturity. | +| Anonymized benchmarking | Interesting, not essential to weekly cash decisions. | +| Marketplace | Much later; marketplace amplifies demand, it does not create it. | + +## Deleted Items + +These should be removed from the active product roadmap. + +| Deleted Item | Why It Is Deleted | +| --- | --- | +| Generic product-marketing polish | Does not directly improve cash decisions. | +| Broad demo stories | One sharp Safe-to-Pay story is better than many demos. | +| Receivables/payables as standalone accounting-like screens | Drifts toward accounting software. | +| Scenario builder as financial modeling | Risks becoming complex planning software. | +| Generic alert feed | Alerts must be tied to payment decisions or they become noise. | +| Adoption-loop features before weekly habit | Growth before retention is waste. | +| Broad partner tooling before owner workflow | Optimizes distribution before product pull. | +| "Become the default open-source cash cockpit" as roadmap item | Outcome, not a roadmap item. | +| Community templates for industries not tied to Radar | Risks generic template marketplace. | +| Broad marketplace as strategic goal | Creates ERP/platform drift before category pull exists. | + +## Roadmap Cut Summary + +Original product roadmap direction contained roughly: + +- positioning work; +- demo work; +- dashboard work; +- imports; +- interviews; +- forecast/calendar; +- receivables/payables; +- confidence levels; +- scenarios; +- accountant export; +- reconciliation; +- advisor workspace; +- multi-client dashboard; +- system importers; +- alerts; +- workshop pack; +- hosted demo; +- case studies; +- onboarding; +- support; +- certified builds; +- managed hosting; +- official modules; +- localization; +- comparison pages; +- bank connectors; +- tax packs; +- benchmarking; +- API ecosystem; +- templates; +- training; +- marketplace. + +Lean roadmap keeps the core cash-decision system and moves or deletes most commercialization, marketplace, module, and +partner expansion work. + +Estimated cut: + +- Kept now/active: about 40%. +- Moved to Later: about 40%. +- Deleted: about 20%. + +Active focus reduced by more than 50%. + +## The New Roadmap In One Page + +## Phase 1: Prove The Pain + +Build the product around one question: + +> What can we safely pay this week? + +Deliver: + +- Safe-to-Pay Radar concept; +- workshop cash decision demo; +- manual cash inputs; +- safe cash model; +- owner dashboard. + +## Phase 2: Make It Useful Weekly + +Deliver: + +- payment decision queue; +- receivables/payables lite as inputs; +- reserves; +- confidence levels; +- scenarios; +- cash collision timeline; +- weekly actions. + +## Phase 3: Make It Trusted + +Deliver: + +- bank CSV import; +- recurring commitments; +- what changed since last week; +- payment promises; +- decision log; +- accountant summary. + +## Phase 4: Expand Carefully + +Only after weekly use is proven: + +- selected importers; +- narrow accountant workflow; +- workshop packaging; +- bank connectors; +- tax calendar packs. + +## Final Product Guardian Decision + +Build fewer things. + +Build Safe-to-Pay Radar first. + +Reject anything that does not help an owner decide what to pay this week. diff --git a/Docs/product/MONDAY_MORNING.md b/Docs/product/MONDAY_MORNING.md new file mode 100644 index 0000000..7ad1031 --- /dev/null +++ b/Docs/product/MONDAY_MORNING.md @@ -0,0 +1,374 @@ +# Monday Morning + +It is Monday. + +07:30. + +The workshop is still quiet. + +Coffee on the desk. + +You open OpenCashFlow before opening email. + +You are not here to browse reports. + +You are here to answer one question: + +> What can we safely pay this week? + +## The Company + +Company: + +> Northside Workshop + +Business: + +- small metal workshop; +- 18 employees; +- mostly B2B customers; +- buys steel and components before customers pay; +- uses an accountant for books; +- still plans cash in a spreadsheet. + +Today is Monday, August 5. + +## What You See First + +The first screen is not a chart wall. + +It is the Safe-to-Pay Radar. + +```text +Safe-to-Pay Radar Monday, Aug 5, 07:30 + +Bank cash 84,260 +Safe cash 18,340 +Committed this week 71,900 +Expected this week 52,000 +Risk level High + +This week: +You can safely approve 3 of 7 payments. +2 payments should be delayed. +1 supplier should be called. +1 customer is creating payroll risk. +``` + +The number that matters is not `84,260`. + +That is just the bank balance. + +The number that matters is `18,340`. + +That is safe cash after protecting payroll, VAT, rent, and commitments already approved. + +The screen says: + +> ACME late payment can break payroll reserve on Aug 12. + +That is the moment you lean forward. + +## What You Click First + +You click: + +> Review payment queue + +OpenCashFlow shows the payments due this week. + +```text +Payment Decision Queue + +Payee Due Amount Recommendation +SteelCo Aug 7 27,500 Split recommended +Rent Aug 8 6,000 Safe to pay +Payroll reserve Aug 9 38,000 Protected +Machine purchase Aug 9 32,000 Delay recommended +Tooling supplier Aug 10 4,900 Safe to pay +Insurance Aug 12 2,800 Safe to pay +Fasteners Ltd Aug 12 3,700 Hold until ACME pays +``` + +You expected to pay SteelCo in full. + +OpenCashFlow says not yet. + +## What You Learn + +You click SteelCo. + +OpenCashFlow explains: + +```text +SteelCo - 27,500 due Aug 7 + +If paid in full: +- Safe cash drops from 18,340 to -9,160 +- Payroll reserve is breached +- Worst projected balance: -6,800 on Aug 12 +- Risk depends on ACME paying this week + +Recommended action: +Pay 12,000 now. +Pay 15,500 after ACME lands. +``` + +This is not an accounting report. + +This is a decision. + +You now understand that paying SteelCo in full is only safe if ACME pays on time. + +ACME has not confirmed. + +## The Customer That Worries You + +You click the alert: + +> ACME payment creates payroll risk. + +OpenCashFlow shows: + +```text +Customer: ACME Components +Amount expected: 52,000 +Original due date: Aug 2 +Days late: 3 +Confidence: Risky +Last promise: "Should go out early next week" +Cash impact if 15 days late: Payroll reserve breach Aug 12 +``` + +This customer worries you because: + +- it is a large amount; +- they have slipped before; +- payroll is due Friday; +- SteelCo is also due this week; +- the machine purchase only works if ACME pays. + +You do not need a CRM. + +You need to know that ACME is the customer to call today. + +## The Scenario You Run + +You click: + +> Scenario: ACME pays 15 days late + +OpenCashFlow updates the forecast. + +```text +Scenario: ACME pays Aug 20 instead of Aug 5 + +Projected low point: -6,800 +Payroll reserve breached: Yes +Unsafe payments this week: SteelCo full payment, machine purchase + +Fix options: +1. Split SteelCo: low point becomes 9,400 +2. Delay machine purchase: low point becomes 18,300 +3. Collect 10,000 deposit from Bright Machines: low point becomes 19,400 +``` + +The decision is obvious now. + +You do not need a bigger dashboard. + +You need to not spend money that only looks available. + +## The Decision You Make + +You decide: + +1. Pay rent. +2. Pay tooling supplier. +3. Pay insurance. +4. Protect payroll. +5. Split SteelCo. +6. Delay machine purchase. +7. Hold Fasteners Ltd until ACME pays. +8. Call ACME before 09:00. + +You click: + +> Approve safe payments + +OpenCashFlow marks: + +```text +Approved today: +- Rent: 6,000 +- Tooling supplier: 4,900 +- Insurance: 2,800 +- SteelCo partial: 12,000 + +Delayed: +- Machine purchase: 32,000 delayed 30 days +- Fasteners Ltd: 3,700 held pending ACME +- SteelCo remaining: 15,500 after ACME receipt +``` + +## The Payment You Postpone + +The machine purchase hurts. + +It is a good machine. + +The price is fair. + +But OpenCashFlow shows: + +```text +Machine purchase - 32,000 + +If approved this week: +- Safe cash drops below zero +- ACME delay creates a negative balance +- Payroll reserve depends on customer timing + +Recommendation: +Delay until ACME is received or deposit collected. +``` + +You postpone it. + +Not because the machine is bad. + +Because this week is not the week. + +## The Supplier You Call + +You call SteelCo at 08:15. + +You do not call vaguely. + +You have a plan. + +You say: + +> We can send 12,000 today and the remaining 15,500 once ACME clears. Can you hold the next steel delivery if we confirm +> the second payment by Friday? + +This is a better conversation than: + +> We are tight this week. + +OpenCashFlow gave you a specific ask. + +## The Customer You Call + +You call ACME at 08:45. + +You are polite, but direct. + +You say: + +> Your 52,000 payment was due Friday. We need confirmation today because it affects this week's supplier schedule. + +They say: + +> It should go out Wednesday. + +You update the expected date to Wednesday and mark confidence as medium, not confirmed. + +OpenCashFlow updates the Radar. + +```text +ACME expected: Aug 7 +Confidence: Medium +Risk level: Medium +Safe cash after decisions: 14,900 +Payroll reserve: Protected +``` + +You are not relaxed. + +But you are no longer guessing. + +## What Happens Next + +At 09:10, you send the weekly cash summary to your accountant. + +```text +Weekly cash summary + +Cash now: 84,260 +Safe cash after approved payments: 14,900 +Protected payroll: 38,000 +Protected VAT reserve: 21,000 + +Approved: +- Rent +- Tooling supplier +- Insurance +- SteelCo partial + +Delayed: +- Machine purchase +- Fasteners Ltd +- SteelCo remaining + +Watch: +- ACME 52,000 expected Wednesday, medium confidence + +Question: +Can we safely reduce VAT reserve by 5,000 until Friday if ACME slips again? +``` + +The accountant has context before giving advice. + +At 10:30, SteelCo agrees to the split. + +At 11:15, ACME sends payment confirmation for Wednesday. + +You do not approve the machine purchase yet. + +You wait for cash to land. + +## What OpenCashFlow Actually Did + +OpenCashFlow did not run the company. + +It did not replace accounting. + +It did not manage inventory. + +It did not become CRM. + +It did one job: + +> It stopped the owner from making a payment decision that looked safe but was not safe. + +## Why This Monday Matters + +Before OpenCashFlow: + +- bank balance looked fine; +- spreadsheet was probably stale; +- SteelCo would have been paid in full; +- machine purchase might have been approved; +- ACME delay would have become a Friday problem; +- payroll reserve would have been at risk. + +After OpenCashFlow: + +- safe cash was visible; +- ACME risk was explicit; +- SteelCo got a concrete split proposal; +- machine purchase was delayed; +- payroll stayed protected; +- accountant saw the decision context; +- the owner started the week with control. + +## Product Test + +If OpenCashFlow can produce this Monday morning, it is the right product. + +If it cannot, it is still too generic. + +The product should be judged by whether it can make this sentence true: + +> I opened OpenCashFlow before making payments, and it changed what I did. diff --git a/Docs/product/OWNER_DASHBOARD.md b/Docs/product/OWNER_DASHBOARD.md new file mode 100644 index 0000000..824feb2 --- /dev/null +++ b/Docs/product/OWNER_DASHBOARD.md @@ -0,0 +1,197 @@ +# Owner Dashboard + +The owner dashboard should be the main product surface. + +It should not be a generic analytics page. + +It should answer: + +> What cash is safe, what cash is committed, what cash is at risk, and what should I do this week? + +## Dashboard Priority + +Priority order: + +1. Decision. +2. Risk. +3. Cash reality. +4. Commitments. +5. Forecast. +6. Detail. + +The owner should not need to interpret ten charts before finding the problem. + +## Top Section: This Week + +Widgets: + +- Cash available now. +- Safe cash. +- Committed cash. +- Expected cash this week. +- Required payments this week. +- Projected end-of-week balance. +- Number of actions needed. + +Primary question: + +> Can we get through this week safely? + +## Cash Runway + +Show: + +- current runway in days; +- forecast low point; +- date of projected cash pressure; +- best/expected/worst case. + +Good copy: + +> Expected runway: 42 days. Worst-case runway: 18 days if ACME pays late. + +## Safe Cash + +Safe cash is not bank balance. + +Safe cash should subtract: + +- payroll reserve; +- taxes/VAT reserve; +- approved supplier payments; +- recurring commitments; +- loan payments; +- minimum cash buffer. + +Show: + +- bank cash; +- reserved cash; +- safe cash; +- explanation. + +## Committed Cash + +Show outgoing commitments by status: + +- approved; +- due soon; +- overdue; +- planned; +- disputed; +- deferrable. + +Prioritize by impact and due date. + +## Upcoming Commitments + +Calendar or list: + +- payroll; +- rent; +- taxes; +- supplier bills; +- loan payments; +- materials purchases; +- subscriptions; +- contractor payments. + +Highlight collisions: + +> Payroll and VAT are due within the same week. + +## Late Customers + +Show: + +- customer; +- amount; +- days late; +- promised date; +- confidence; +- cash impact; +- owner action. + +Do not just show aging. Show risk. + +## Cash Forecast + +Time horizons: + +- 7 days; +- 30 days; +- 60 days; +- 90 days. + +Views: + +- expected; +- conservative; +- worst case. + +The chart should emphasize the low point and why it happens. + +## Worst Scenarios + +Show the top scenarios that matter: + +- biggest customer pays 15 days late; +- supplier payment cannot move; +- tax payment due in full; +- machine purchase approved; +- payroll increases. + +Each scenario should show: + +- projected low balance; +- runway; +- decision options. + +## Alerts + +Alerts should be serious and action-oriented. + +Examples: + +- "Safe cash drops below payroll reserve in 9 days." +- "ACME late payment creates a negative balance on August 12." +- "Supplier batch exceeds safe cash by 4,200." +- "Tax reserve is 38% under target." + +Avoid: + +- generic reminders; +- decorative notifications; +- low-value warnings. + +## Weekly Actions + +The dashboard should end with actions: + +- chase customer; +- approve payment; +- defer payment; +- reserve tax cash; +- update promised date; +- run scenario; +- share summary with accountant. + +Each action should connect to a cash risk. + +## Accountant Summary + +One click should generate: + +- current cash; +- safe cash; +- overdue receivables; +- upcoming commitments; +- forecast low point; +- decisions made; +- questions for accountant. + +This turns the product into a collaboration surface. + +## Design Principle + +If the dashboard does not change what the owner does this week, it is not good enough. diff --git a/Docs/product/POSITIONING.md b/Docs/product/POSITIONING.md new file mode 100644 index 0000000..d175b59 --- /dev/null +++ b/Docs/product/POSITIONING.md @@ -0,0 +1,149 @@ +# OpenCashFlow Positioning + +## Current Positioning + +OpenCashFlow is currently described as an open-source, self-hosted cash-flow management system for small companies, +consultants, accounting studios, and teams. + +That is accurate, but not yet memorable. + +It says what category the product belongs to. It does not yet say why a business owner should switch from spreadsheets, +accounting reports, bank portals, Odoo, ERPNext, Dolibarr, Akaunting, Invoice Ninja, or a generic dashboard. + +## New Positioning + +OpenCashFlow is the self-hosted cash cockpit for small businesses. + +It helps owner-led companies understand: + +- what cash they have; +- what cash is expected; +- what cash is committed; +- what is late; +- what can break; +- what decisions must be made this week. + +## Target Audience + +Primary audience: + +- small companies; +- workshops; +- light manufacturers; +- distributors; +- accounting firms; +- consultants; +- owner-led service businesses; +- companies leaving expensive SaaS products; +- companies that want self-hosted financial operations tools. + +Best initial wedge: + +> Small manufacturers and workshops that already have accounting or invoicing software, but still manage operational cash +> planning in spreadsheets. + +## Who Should Never Use OpenCashFlow + +OpenCashFlow is not for: + +- companies looking for certified accounting; +- companies needing payroll; +- companies needing full ERP/MRP; +- companies needing ecommerce; +- companies needing POS; +- enterprises with treasury departments; +- consumers managing personal budgets; +- companies unwilling to self-host or pay for managed hosting; +- teams expecting tax/legal/accounting compliance without professional review. + +## Who Should Absolutely Use OpenCashFlow + +OpenCashFlow is for businesses where this sentence sounds familiar: + +> The accountant says the books are fine, the bank balance looks fine, but I still do not know what we can safely pay. + +Ideal users: + +- owner-operators who make payment decisions weekly; +- accounting firms advising many small clients; +- workshops juggling deposits, materials, suppliers, payroll, and late customers; +- distributors buying stock before customer cash arrives; +- consultants managing contractor payouts and delayed receivables; +- companies that trust spreadsheets less every month. + +## Unique Selling Proposition + +OpenCashFlow is not an ERP and not accounting software. + +It is the self-hosted cash decision layer that shows what cash is safe, what cash is committed, and what cash is at risk. + +## One-Sentence Pitch + +OpenCashFlow is the self-hosted cash cockpit that helps small businesses decide what they can safely pay this week. + +## 30-Second Pitch + +OpenCashFlow helps small businesses stop running cash decisions from fragile spreadsheets. It brings bank movements, +expected receivables, supplier commitments, payroll, taxes, and scenarios into one self-hosted cash cockpit, so owners can +see what is safe, what is late, what is committed, and what needs action before cash becomes a crisis. + +## Elevator Pitch + +Small businesses usually have accounting software, bank portals, invoices, supplier bills, payroll dates, and a +spreadsheet that only one person trusts. OpenCashFlow does not try to replace all of that. It sits above it as a +self-hosted cash cockpit. Every week, the owner can review expected cash, committed cash, overdue payments, supplier +pressure, tax and payroll dates, and worst-case scenarios. The goal is simple: make better cash decisions before the bank +balance surprises you. + +## Homepage Headline + +Self-hosted cash control for small businesses. + +## Homepage Subheadline + +See what cash is real, what cash is committed, what cash is at risk, and what decisions you need to make this week. + +## GitHub Description + +Self-hosted cash cockpit for small businesses: payments, cash ledger, receivables, commitments, forecasts, and weekly +cash decisions. + +## Tagline Options + +- Your cash truth, before the bank surprises you. +- The weekly cash cockpit for owner-led businesses. +- Stop guessing what you can pay. +- Cash visibility without ERP complexity. +- Know what cash is safe. +- The self-hosted layer between accounting and decisions. + +## Marketing Keywords + +Primary: + +- self-hosted cash flow; +- cash cockpit; +- small business cash management; +- cash forecast; +- working capital visibility; +- owner dashboard; +- weekly cash planning. + +Secondary: + +- receivables tracking; +- payables planning; +- supplier commitments; +- late customer alerts; +- cash runway; +- accounting firm advisory; +- workshop cash planning; +- manufacturing cash flow. + +Avoid leading with: + +- ERP; +- accounting suite; +- invoicing software; +- budgeting app; +- finance tracker. diff --git a/Docs/product/PRODUCT_DECISIONS.md b/Docs/product/PRODUCT_DECISIONS.md new file mode 100644 index 0000000..8331d4d --- /dev/null +++ b/Docs/product/PRODUCT_DECISIONS.md @@ -0,0 +1,138 @@ +# Product Decisions + +This document states what OpenCashFlow will not become. + +The goal is to protect the product from becoming a generic ERP. + +## Decision 1: No Full ERP + +OpenCashFlow will not become a full ERP. + +Reason: + +ERP scope is too broad and already served by mature competitors. OpenCashFlow wins by being focused on operational cash +decisions. + +Implication: + +ERP-like modules must prove they directly strengthen cash visibility or remain outside the core. + +## Decision 2: No Payroll + +OpenCashFlow will not run payroll. + +Reason: + +Payroll is compliance-heavy, region-specific, and high-risk. + +Implication: + +OpenCashFlow may track payroll commitments and cash impact, but payroll calculation and filing belong elsewhere. + +## Decision 3: No Accounting Replacement + +OpenCashFlow will not replace accounting software. + +Reason: + +Accounting requires compliance, reconciliation, tax rules, reporting standards, and professional review. + +Implication: + +OpenCashFlow imports from, exports to, and collaborates with accounting systems. + +## Decision 4: No CRM + +OpenCashFlow will not become a CRM. + +Reason: + +CRM optimizes sales pipelines and customer relationships. OpenCashFlow optimizes cash decisions. + +Implication: + +Customer records exist only as needed for receivables, payment risk, and cash context. + +## Decision 5: No Inventory ERP + +OpenCashFlow will not become an inventory ERP. + +Reason: + +Inventory requires item masters, warehouses, stock movements, costing, purchasing, MRP, and operational complexity. + +Implication: + +OpenCashFlow may track cash commitments for inventory purchases, but not stock operations. + +## Decision 6: No Ecommerce Platform + +OpenCashFlow will not become ecommerce. + +Reason: + +Ecommerce is a separate product category. + +Implication: + +OpenCashFlow may import orders, invoices, payouts, and expected cash from ecommerce systems. + +## Decision 7: No HR Suite + +OpenCashFlow will not become HR software. + +Reason: + +HR includes hiring, time off, payroll, compliance, performance, documents, and employee lifecycle management. + +Implication: + +OpenCashFlow may track payroll cash commitments and user permissions, not HR operations. + +## Decision 8: No Tax Compliance Engine + +OpenCashFlow will not calculate or file taxes as a compliance engine. + +Reason: + +Tax rules vary by country, change often, and require specialist accountability. + +Implication: + +OpenCashFlow may reserve cash for taxes, track tax due dates, and import estimated tax obligations. + +## Decision 9: No Marketplace Until The Core Habit Works + +OpenCashFlow will not launch a broad plugin marketplace early. + +Reason: + +Marketplaces amplify a product that already has demand. They do not create demand. + +Implication: + +Start with official modules and templates. Add community marketplace later. + +## Decision 10: Cash Cockpit First + +Every product decision should reinforce: + +> weekly operational cash decisions. + +If a feature does not support that, it should wait. + +## Decision 11: Cash Custody Before Safe-To-Pay + +OpenCashFlow will not present forecast or Safe-to-Pay recommendations as reliable until Cash Custody is implemented and +Cash Integrity can prove it. + +Reason: + +Safe-to-Pay depends on trusted cash. Trusted cash requires a custody chain: source, custodian, reason, actor, audit, +immutable posted facts, transfer links, reversal/correction, reconciliation, visible discrepancies, and confidence +status. + +Implication: + +Forecast and Safe-to-Pay may be designed and prototyped, but must remain WIP/Experimental until the Cash Custody decision +record and Cash Integrity proof requirements are implemented and covered by tests. diff --git a/Docs/product/PRODUCT_MANIFESTO.md b/Docs/product/PRODUCT_MANIFESTO.md new file mode 100644 index 0000000..6a4e0eb --- /dev/null +++ b/Docs/product/PRODUCT_MANIFESTO.md @@ -0,0 +1,198 @@ +# OpenCashFlow Product Manifesto + +OpenCashFlow exists because small businesses do not fail on paper. + +They fail in the gap between what the books say, what the bank says, what customers promised, what suppliers expect, +and what the owner has to decide this week. + +Accounting can be correct and still not answer the question: + +> Can we safely pay this bill today? + +ERP can be powerful and still not answer the question quickly enough: + +> What will break if this customer pays two weeks late? + +Spreadsheets can be flexible and still become the most dangerous system in the company: + +> One stale formula, one forgotten invoice, one hidden payment, and the owner is guessing again. + +OpenCashFlow is built for the moment when a business owner, manager, accountant, or consultant needs a clear view of +cash before making decisions. + +It is not here to replace professional accounting. + +It is not here to run every department. + +It is not here to become a full ERP. + +It is here to make cash visible, commitments explicit, and decisions calmer. + +## Why Spreadsheets Fail + +Spreadsheets start as freedom. + +Then they become dependency. + +At first, the owner tracks bank balance, invoices, supplier payments, payroll, taxes, and expected collections in a +simple sheet. It works because the business is small and the owner remembers the context. + +Then the business grows. + +More invoices. More suppliers. More payment dates. More people. More exceptions. More versions of the same file. + +The spreadsheet still opens, but trust is gone. + +Common failure modes: + +- the bank balance is updated but overdue invoices are not; +- a supplier payment is forgotten; +- tax or payroll is not modeled; +- customer promises are treated as cash; +- old rows are duplicated; +- formulas drift; +- only one person understands the file; +- no audit trail explains who changed what; +- the owner makes decisions from memory. + +The problem is not that spreadsheets are bad. + +The problem is that cash decisions deserve a system. + +## Why Accounting Software Is Not Enough + +Accounting software records financial truth. + +That is essential. + +But business owners often need operational truth before accounting truth is finalized. + +Accounting tells you: + +- what was invoiced; +- what was paid; +- what is posted; +- what belongs in the ledger; +- what reports must reconcile. + +Cash decisions ask: + +- what is likely to arrive this week; +- what can wait; +- what cannot wait; +- what happens if a customer slips; +- what commitments are already made; +- what cash is safe to spend; +- what risk is hidden in the next 30, 60, or 90 days. + +Accounting software is a source of truth. + +OpenCashFlow should be the cash decision layer built around that truth. + +## Why ERPs Are Too Large For Many Businesses + +ERPs are built to model the business. + +That is valuable when a company is ready for it. + +But many small businesses do not need a full ERP implementation to answer urgent cash questions. They do not want to +model every warehouse, workflow, manufacturing order, quote, CRM stage, approval rule, and accounting configuration just +to understand whether supplier payments and payroll collide next Friday. + +For these businesses, ERP adoption can be heavier than the pain. + +OpenCashFlow should not compete by becoming a smaller ERP. + +It should win by doing one job with less ceremony: + +> Turn cash uncertainty into weekly decisions. + +## Cash Decisions Are Different From Accounting + +Accounting is about correctness. + +Cash decisions are about timing, risk, and action. + +Accounting asks: + +- is this transaction classified correctly? +- does the ledger reconcile? +- is the report compliant? + +Cash decisions ask: + +- can we pay this now? +- what if they pay late? +- should we delay this purchase? +- which customer must we call today? +- how long is our runway? +- what is the worst week in the next quarter? + +Both matter. + +They are not the same product. + +## The Problem OpenCashFlow Solves + +OpenCashFlow should solve this problem: + +> Small businesses need a self-hosted way to see current cash, expected cash, committed cash, cash risk, and the decisions +> required this week. + +The product should make it obvious: + +- what cash is in the bank; +- what cash is already committed; +- what receivables are expected; +- what payables are coming; +- what is overdue; +- what scenarios create risk; +- what actions the owner should take now. + +## Why Cash Custody Matters + +Cash is not only a number. + +Cash is also responsibility. + +If an employee receives 200 for a customer renewal, the owner needs to know more than "cash increased by 200." The owner +needs to know: + +- who received it; +- why they received it; +- where it went; +- whether it was returned, spent, deposited, or reconciled; +- whether any difference remains unexplained. + +That is Cash Custody. + +OpenCashFlow should prove the chain of responsibility for company money from receipt to reconciliation. Cash Integrity is +the proof that this chain is complete, immutable, reconciled, and explainable. + +## The Problem OpenCashFlow Does Not Solve + +OpenCashFlow does not solve every business-management problem. + +It should not try to become: + +- a certified accounting system; +- a payroll system; +- a full ERP; +- a CRM; +- an inventory/MRP suite; +- an ecommerce platform; +- a bank; +- a tax compliance engine; +- a replacement for an accountant. + +Those are different products with different burdens. + +OpenCashFlow should integrate with them, import from them, export to them, and make their cash impact visible. + +## The Promise + +OpenCashFlow should give the owner a weekly moment of clarity: + +> I know what cash is real, what cash is promised, what cash is committed, what can go wrong, and what I need to decide. + +That is the product. diff --git a/Docs/product/PRODUCT_PRINCIPLES.md b/Docs/product/PRODUCT_PRINCIPLES.md new file mode 100644 index 0000000..66f0eaa --- /dev/null +++ b/Docs/product/PRODUCT_PRINCIPLES.md @@ -0,0 +1,163 @@ +# OpenCashFlow Product Principles + +These principles define what OpenCashFlow should become and what it must refuse. + +## 1. We Are Not An ERP + +OpenCashFlow should not manage every business process. + +ERPs model the whole company. OpenCashFlow should model cash decisions. + +If a feature requires building a full CRM, inventory system, manufacturing suite, HR system, ecommerce engine, or +accounting ledger, it is probably outside the core product. + +## 2. We Are Not Accounting Software + +Accounting records financial truth. + +OpenCashFlow helps people make cash decisions from operational truth. + +The product should integrate with accounting systems, import from them, export to them, and respect them. It should not +replace the accountant. + +## 3. Cash First + +Every important screen should answer one of these questions: + +- how much cash do we have? +- how much is safe? +- what is committed? +- what is expected? +- what is late? +- what can break? + +If the feature does not improve cash clarity, it is probably secondary. + +## 4. Decision First + +Reports are not enough. + +OpenCashFlow should help users decide: + +- pay or wait; +- chase or ignore; +- buy now or delay; +- split payment or pay in full; +- reserve cash or spend it. + +The product must move from information to action. + +## 5. Weekly Over Occasional + +The product should create a weekly habit. + +The ideal use case is Monday morning cash review, not monthly reporting after the fact. + +Features that support weekly use are more valuable than features that only look good in a settings page. + +## 6. Simple Before Complete + +A partial workflow that owners actually use beats a complete module nobody configures. + +Prefer: + +- CSV import before bank integrations; +- receivables/payables lite before full accounting; +- cash forecast before full financial planning; +- workshop cash pack before full manufacturing. + +## 7. Operational Over Theoretical + +Owners do not need abstract financial theory during a cash crunch. + +They need: + +- dates; +- amounts; +- commitments; +- risk; +- actions. + +Language should be practical. + +## 8. Integrate, Do Not Replace + +OpenCashFlow should work with existing systems. + +It should accept imports from accounting tools, invoicing tools, ERPs, bank exports, and spreadsheets. + +The product should say: + +> Keep your systems. Use OpenCashFlow to see cash risk and decisions. + +## 9. Owner Trust Is The Feature + +The owner must trust the dashboard. + +Trust comes from: + +- clear sources; +- visible assumptions; +- audit trail; +- matching bank reality; +- confidence levels; +- explainable forecasts. + +No black-box magic. + +## 10. Cash Custody Before Forecast + +Forecasts are only as trustworthy as the cash facts underneath them. + +Before OpenCashFlow presents Safe-to-Pay as reliable, it must be able to prove: + +- who handled the cash; +- who or what currently has custody; +- which cash source moved; +- why it moved; +- who entered it; +- whether the day reconciled; +- whether discrepancies remain open. + +Cash Custody is the business pillar: the chain of responsibility for company money. + +Cash Integrity is the proof capability: the custody chain is complete, immutable, reconciled, and explainable. + +Unreconciled custody must reduce confidence. A forecast that hides cash uncertainty is worse than no forecast. + +## 11. Fewer Alerts, Better Alerts + +Alert fatigue kills usage. + +Alerts should be rare, serious, and tied to action. + +Bad alert: + +> Payment due soon. + +Good alert: + +> This supplier payment will reduce safe cash below payroll reserve. + +## 12. Self-Hosted Must Remain A Product Value + +Self-hosted is not just a deployment option. + +It matters because businesses trust the system with cash, payments, customers, suppliers, and sensitive timing data. + +OpenCashFlow should remain useful without mandatory SaaS services. + +## 13. The Core Must Stay Focused + +The core product should stay narrow: + +- cash position; +- expected cash; +- committed cash; +- forecast; +- alerts; +- decisions; +- audit; +- imports/exports. + +Everything else should prove it strengthens the cash cockpit. diff --git a/Docs/product/PRODUCT_ROADMAP.md b/Docs/product/PRODUCT_ROADMAP.md new file mode 100644 index 0000000..9d5b01a --- /dev/null +++ b/Docs/product/PRODUCT_ROADMAP.md @@ -0,0 +1,173 @@ +# Product Roadmap + +This roadmap is product-only. + +It ignores engineering milestones and focuses on strengthening OpenCashFlow's identity as: + +> the self-hosted cash cockpit for small businesses. + +## Now + +Goal: make the product identity unmistakable. + +Priorities: + +1. Adopt the phrase "self-hosted cash cockpit" across product messaging. +2. Stop positioning against ERP breadth. +3. Define the weekly cash ritual as the primary workflow. +4. Define the owner dashboard as the primary product surface. +5. Create a public demo story around a small workshop. +6. Make the feature filter explicit: cash visibility, decisions, uncertainty reduction, weekly workflow. + +## Next Month + +Goal: prove the product can explain itself in one demo. + +Priorities: + +1. Workshop demo dataset: + - late customer; + - supplier payment; + - payroll; + - VAT/tax; + - machine purchase; + - cash shortfall scenario. +2. Owner dashboard concept: + - cash now; + - safe cash; + - committed cash; + - expected cash; + - projected low point; + - weekly actions. +3. Manual import templates: + - bank CSV; + - expected receivables; + - supplier commitments. +4. Product homepage copy focused on cash decisions. +5. Interview 20 target users: + - workshops; + - accounting firms; + - distributors; + - consultants. + +Success metric: + +People understand the product in under two minutes. + +## Next Quarter + +Goal: make the weekly cash ritual useful. + +Priorities: + +1. Cash forecast calendar. +2. Receivables/payables lite. +3. Overdue receivables and payables aging. +4. Supplier commitments. +5. Cash confidence levels: + - confirmed; + - expected; + - risky; + - hypothetical. +6. Basic scenario planning: + - customer late; + - supplier split payment; + - purchase delayed; + - tax reserved. +7. Weekly cash summary export for accountant/advisor. +8. CSV reconciliation queue. + +Success metric: + +Five businesses use OpenCashFlow weekly to make real cash decisions. + +## Next 6 Months + +Goal: create adoption loops. + +Priorities: + +1. Accountant/advisor workspace. +2. Multi-client cash health summary for accounting firms. +3. Importers from common systems: + - spreadsheets; + - Invoice Ninja exports; + - Akaunting exports; + - Odoo/ERPNext CSV exports; + - bank exports. +4. Alerts: + - negative forecast; + - low safe cash; + - late customer risk; + - payroll/tax collision; + - supplier overload. +5. Workshop cash pack: + - deposit tracking; + - materials commitment; + - job cash exposure; + - milestone payments. +6. Public hosted demo with sample data. +7. First case studies. + +Success metric: + +Fifty companies or advisors use it monthly. + +## Next Year + +Goal: become the default open-source cash cockpit for small businesses. + +Priorities: + +1. Stable release candidate. +2. Productized onboarding packages. +3. Support subscription offer. +4. Certified self-hosted builds. +5. Managed hosting beta. +6. Official forecasting module. +7. Official accountant module. +8. Official workshop/manufacturing module. +9. Localization for first priority countries. +10. Comparison pages: + - OpenCashFlow vs spreadsheets; + - OpenCashFlow vs ERP; + - OpenCashFlow vs accounting software; + - OpenCashFlow vs invoicing tools. + +Success metric: + +One hundred active instances and twenty accounting/consulting partners. + +## Next 3 Years + +Goal: own the operational cash layer for self-hosted small businesses. + +Priorities: + +1. Managed OpenCashFlow hosting. +2. Certified releases and upgrade assurance. +3. Partner program for accountants and consultants. +4. Bank connector packs by region. +5. Country-specific tax calendar packs. +6. Cash benchmarking and anonymized insights. +7. API ecosystem for ERPs and accounting tools. +8. Community templates for industries. +9. Official training and certification. +10. Marketplace only after official modules prove demand. + +Success metric: + +Ten thousand users, one thousand active installations, sustainable revenue from hosting, support, onboarding, migration, +training, and official modules. + +## Roadmap Guardrail + +Every item must answer at least one question: + +- Does it improve cash visibility? +- Does it improve a cash decision? +- Does it reduce uncertainty? +- Does it strengthen the weekly cash ritual? +- Does it help integrate existing systems? + +If not, it does not belong on the product roadmap. diff --git a/Docs/product/PRODUCT_STRATEGY_SUMMARY.md b/Docs/product/PRODUCT_STRATEGY_SUMMARY.md new file mode 100644 index 0000000..814ac38 --- /dev/null +++ b/Docs/product/PRODUCT_STRATEGY_SUMMARY.md @@ -0,0 +1,192 @@ +# Product Strategy Summary + +Date: 2026-07-08 + +This summary consolidates the product positioning sprint. + +## Documents Created + +- `Docs/product/PRODUCT_MANIFESTO.md` +- `Docs/product/POSITIONING.md` +- `Docs/product/WEEKLY_CASH_RITUAL.md` +- `Docs/product/PRODUCT_PRINCIPLES.md` +- `Docs/product/COMPETITOR_POSITIONING.md` +- `Docs/product/PRODUCT_ROADMAP.md` +- `Docs/product/FEATURE_FILTER.md` +- `Docs/product/OWNER_DASHBOARD.md` +- `Docs/product/FIRST_DEMO.md` +- `Docs/product/PRODUCT_DECISIONS.md` +- `Docs/product/PRODUCT_STRATEGY_SUMMARY.md` +- `Docs/product/CASH_CUSTODY_DECISION_RECORD.md` + +## Core Strategic Decision + +OpenCashFlow should not become another ERP. + +OpenCashFlow should become: + +> the self-hosted cash cockpit for small businesses. + +The product should focus on weekly operational cash decisions: + +- what cash is real; +- what cash is expected; +- what cash is committed; +- what is late; +- what can break; +- what decisions must be made this week. + +Cash Custody is now the product/domain pillar underneath trusted cash: + +> OpenCashFlow should prove who or what is responsible for company money from receipt to reconciliation. + +Cash Integrity remains the proof capability that makes the custody chain complete, immutable, reconciled, and explainable. + +## Key Strategic Decisions + +1. Position around cash operations, not generic business management. +2. Treat accounting systems and ERPs as integration sources, not enemies to replace. +3. Build the weekly cash ritual as the primary habit. +4. Make the owner dashboard the primary product surface. +5. Target workshops, small manufacturers, distributors, accounting firms, and owner-led companies first. +6. Reject features that create ERP drift. +7. Treat Cash Custody as a gate before trusted Safe-to-Pay recommendations. +8. Prioritize forecast calendar, receivables/payables lite, bank import, risk alerts, and scenarios. +9. Monetize through support, onboarding, migration, hosted services, certified builds, training, and official modules. +10. Build importers before deep integrations. +11. Prove one niche before expanding. + +## Biggest Positioning Improvements + +Before: + +> Open-source, self-hosted cash-flow management system. + +After: + +> Self-hosted cash control for small businesses. + +Better: + +> The cash cockpit for owner-led companies. + +Sharpest: + +> Know what cash is safe, what is committed, what is at risk, and what to do this week. + +## Contradictions Found + +No hard contradictions were introduced. + +Existing README messaging is aligned with the new strategy because it already says OpenCashFlow is: + +- self-hosted; +- cash-flow focused; +- not an accounting suite; +- not a full ERP; +- not SaaS/Stripe dependent. + +Minor tension: + +- README currently includes "business management" style language through scope and modules. Future copy should emphasize + cash operations first and treat modules as supporting capabilities. + +## Recommended README Updates + +Update the opening paragraph from broad cash-flow management language to: + +> OpenCashFlow is a self-hosted cash cockpit for small businesses. It helps owner-led teams see current cash, expected +> cash, committed cash, overdue payments, projected risk, and the decisions they need to make this week. + +Add a "Product Focus" section: + +```text +OpenCashFlow is not an ERP and not accounting software. It is the cash decision layer between your bank, accounting +system, invoices, supplier commitments, and weekly payment decisions. +``` + +Add a "Best Fit" section: + +```text +OpenCashFlow is best for small companies, workshops, light manufacturers, distributors, consultants, and accounting firms +that still plan cash in spreadsheets. +``` + +Add a "Not For" section: + +```text +OpenCashFlow is not for companies looking for certified accounting, payroll, full ERP/MRP, ecommerce, POS, or tax filing. +``` + +## Recommended Website Homepage Copy + +Headline: + +> Self-hosted cash control for small businesses. + +Subheadline: + +> See what cash is real, what cash is committed, what cash is at risk, and what decisions you need to make this week. + +Primary call to action: + +> Try the demo + +Secondary call to action: + +> Read the manifesto + +Section: The Problem + +> Your bank balance is not your cash position. Customer promises, supplier payments, payroll, taxes, loans, and purchases +> all compete for the same money. Spreadsheets hide the risk until it is too late. + +Section: The Product + +> OpenCashFlow brings bank movements, expected receivables, supplier commitments, taxes, payroll, and scenarios into one +> self-hosted cash cockpit. + +Section: Weekly Ritual + +> Every week, review cash reality, expected receipts, upcoming commitments, alerts, scenarios, and payment decisions. + +Section: Not Another ERP + +> Keep your accounting system. Keep your invoices. Keep your ERP if you have one. Use OpenCashFlow to understand cash +> risk and decisions before the bank surprises you. + +## Recommended First Demo Copy + +> Northside Workshop has 84,000 in the bank. It looks safe. But payroll, VAT, a supplier bill, and one late customer turn +> next week into a cash crunch. OpenCashFlow shows the risk, tests the scenarios, and helps the owner decide what to pay, +> what to delay, and who to call. + +## Product North Star + +The north-star metric should be: + +> weekly cash reviews completed. + +Supporting metrics: + +- active companies running weekly review; +- receivables/payables imported; +- scenarios run; +- cash alerts resolved; +- accountant summaries shared; +- decisions recorded. + +Avoid vanity metrics: + +- modules enabled; +- dashboard views; +- raw transactions entered; +- generic user count. + +## Final Product Direction + +Build fewer features. + +Make one habit unavoidable: + +> Every Monday, the owner opens OpenCashFlow before making payment decisions. diff --git a/Docs/product/WEEKLY_CASH_RITUAL.md b/Docs/product/WEEKLY_CASH_RITUAL.md new file mode 100644 index 0000000..a635359 --- /dev/null +++ b/Docs/product/WEEKLY_CASH_RITUAL.md @@ -0,0 +1,210 @@ +# The Weekly Cash Ritual + +OpenCashFlow should become part of the owner's weekly routine. + +The product should not be something users visit only when entering transactions. It should become the Monday morning +ritual before payment decisions are made. + +## The Job + +Every week, the owner needs to know: + +- what cash is actually available; +- which incoming payments are expected; +- which customers are late; +- which supplier commitments are due; +- which taxes, payroll, rent, loans, and recurring costs are coming; +- what the projected balance looks like; +- what could go wrong; +- what can be paid safely; +- what should be delayed, negotiated, or chased. + +## Monday Morning Flow + +## 1. Import Latest Bank Movements + +Start with reality. + +The user imports bank movements or confirms synced bank activity. + +OpenCashFlow should show: + +- new money in; +- new money out; +- unmatched transactions; +- possible duplicates; +- transactions that match expected payments; +- unexpected cash movements. + +Outcome: + +The current cash position is trusted. + +## 2. Review Expected Receivables + +The user reviews money expected from customers. + +Show: + +- due this week; +- overdue; +- promised payment dates; +- high-value receivables; +- risky customers; +- partial payments; +- confidence level. + +Outcome: + +The owner knows what cash is likely, uncertain, or late. + +## 3. Review Supplier Commitments + +The user reviews outgoing supplier payments. + +Show: + +- due this week; +- overdue; +- essential suppliers; +- deferrable payments; +- payments already approved; +- payments waiting for decision. + +Outcome: + +Supplier pressure becomes explicit. + +## 4. Review Payroll + +Payroll is not optional cash. + +Show: + +- next payroll date; +- estimated payroll amount; +- employer costs; +- contractor payouts; +- payroll collision with supplier or tax dates. + +Outcome: + +The owner sees whether people can be paid safely. + +## 5. Review Taxes + +Taxes often create cash surprises. + +Show: + +- VAT/sales tax estimates; +- income or corporate tax reminders; +- payroll tax; +- payment deadlines; +- amount reserved versus amount needed. + +Outcome: + +Tax cash stops being accidentally spent. + +## 6. Review Projected Balance + +The product shows the next 30, 60, and 90 days. + +Views: + +- best case; +- expected case; +- conservative case; +- worst week; +- projected low point; +- cash runway. + +Outcome: + +The owner sees the future, not just the bank balance. + +## 7. Review Alerts + +Alerts should be few and serious. + +Examples: + +- "Customer ACME is 19 days late and creates a negative balance next week." +- "Payroll and VAT fall within three days." +- "This supplier payment consumes 42% of safe cash." +- "Cash runway drops below 21 days." +- "Three expected receivables have low confidence." + +Outcome: + +The owner knows what deserves attention. + +## 8. Run Scenarios + +The user tests decisions. + +Scenarios: + +- customer pays late; +- supplier accepts split payment; +- machine purchase delayed; +- tax paid in full versus reserved; +- customer deposit collected early; +- contractor hire postponed. + +Outcome: + +The owner can compare decisions before acting. + +## 9. Approve Payment Decisions + +The user chooses: + +- pay now; +- pay later; +- split payment; +- chase customer; +- negotiate supplier; +- reserve cash; +- hold purchase. + +Outcome: + +Cash decisions become deliberate. + +## 10. Share Summary With Accountant Or Advisor + +The ritual ends with a simple summary. + +The summary should include: + +- current cash; +- safe cash; +- committed cash; +- projected low point; +- overdue receivables; +- upcoming commitments; +- decisions made; +- risks requiring advice. + +Outcome: + +The accountant or advisor becomes part of a cash conversation, not only a compliance process. + +## Weekly Ritual Success Criteria + +The ritual works if: + +- it takes less than 15 minutes after setup; +- the owner trusts the numbers; +- alerts are actionable; +- scenarios are easy; +- payment decisions are clearer; +- the owner returns every week. + +## Product Implication + +Every major feature should support this ritual. + +If a feature does not improve the weekly cash ritual, it should be delayed or rejected. diff --git a/Docs/product/WOW_FEATURE_DISCOVERY.md b/Docs/product/WOW_FEATURE_DISCOVERY.md new file mode 100644 index 0000000..7fed295 --- /dev/null +++ b/Docs/product/WOW_FEATURE_DISCOVERY.md @@ -0,0 +1,1425 @@ +# Wow Feature Discovery + +Date: 2026-07-08 + +Role: Product Guardian. + +Mission: + +> Find one feature that makes a small business owner say: "I need this." + +Constraints: + +- must fit the Cash Cockpit vision; +- must not become ERP; +- must not become accounting; +- must not become CRM; +- must not require hundreds of screens; +- must create weekly habit; +- must create emotional value; +- must solve an expensive problem. + +Product filter applied: + +- `PRODUCT_MANIFESTO.md` +- `PRODUCT_PRINCIPLES.md` +- `FEATURE_FILTER.md` +- `PRODUCT_DECISIONS.md` + +## The Standard + +A "wow" feature for OpenCashFlow is not a clever widget. + +It is a feature that answers a painful owner question: + +> Can I safely pay this, or will it create a cash problem later? + +The feature must create relief. + +It must turn vague anxiety into a clear action. + +## Candidate Features + +## 1. Safe-to-Pay Radar + +Problem solved: + +Owners approve payments from bank balance, memory, and fear. They need to know whether paying a supplier today will +endanger payroll, taxes, rent, or another commitment later. + +Who benefits: + +Owners, office managers, accountants, workshop managers, distributors, consultants managing contractors. + +Why competitors do not solve it well: + +ERPs and accounting systems can show payables and reports, but they usually do not frame the decision as "safe to pay +this week?" with expected cash, reserves, late customers, and scenario impact in one owner-facing view. + +Development complexity: + +Medium. Requires commitments, expected receivables, reserves, forecast logic, and action workflow. Does not require full +accounting or ERP. + +Business value: + +Very high. Avoiding one bad payment decision can protect payroll, supplier relationships, and owner confidence. + +Adoption impact: + +Very high. The pain is immediate and easy to demonstrate. + +Frequency of use: + +Weekly, often daily in cash-tight businesses. + +Competitive advantage: + +Strong. It is a decision feature, not a recordkeeping feature. + +## 2. Cash Collision Calendar + +Problem solved: + +Cash crises happen when inflows and outflows collide on the same dates. + +Who benefits: + +Small manufacturers, workshops, distributors, accountants. + +Why competitors do not solve it well: + +Accounting calendars exist, but they rarely combine confidence-weighted receivables, supplier commitments, payroll, +taxes, and forecast low points in a simple operating view. + +Development complexity: + +Medium. + +Business value: + +Very high. + +Adoption impact: + +High. + +Frequency of use: + +Weekly. + +Competitive advantage: + +Strong, especially if visual and scenario-based. + +## 3. Late Customer Risk Meter + +Problem solved: + +One late customer can create a cash shortfall, but owners often see the lateness before they see the downstream damage. + +Who benefits: + +Businesses with concentrated receivables. + +Why competitors do not solve it well: + +Invoicing systems show overdue invoices; they do not always show what the delay will break. + +Development complexity: + +Low-medium. + +Business value: + +High. + +Adoption impact: + +High for service, manufacturing, and distribution businesses. + +Frequency of use: + +Weekly. + +Competitive advantage: + +Good, but narrower than Safe-to-Pay Radar. + +## 4. Payroll Protection Reserve + +Problem solved: + +Owners accidentally treat payroll cash as spendable cash. + +Who benefits: + +Any company with employees or contractors. + +Why competitors do not solve it well: + +Payroll systems calculate payroll; accounting systems record it. Neither necessarily protects payroll cash inside weekly +payment decisions. + +Development complexity: + +Low-medium. + +Business value: + +High. + +Adoption impact: + +High emotional value. + +Frequency of use: + +Weekly or per payroll cycle. + +Competitive advantage: + +Good as a component, not strong enough as the flagship. + +## 5. Tax/VAT Cash Reserve + +Problem solved: + +Businesses spend cash that should be reserved for tax. + +Who benefits: + +Small companies, consultants, accounting firms. + +Why competitors do not solve it well: + +Accounting systems can calculate liabilities, but owners need an operational reserve visible in cash decisions. + +Development complexity: + +Medium because tax differs by country. MVP can use manual reserve rules. + +Business value: + +High. + +Adoption impact: + +Medium-high. + +Frequency of use: + +Weekly/monthly. + +Competitive advantage: + +Good if implemented as cash reserve, not tax engine. + +## 6. Customer Promise Tracker + +Problem solved: + +Owners track payment promises in email, memory, and notes. + +Who benefits: + +Anyone with receivables. + +Why competitors do not solve it well: + +CRMs track interactions; invoicing tools track due dates. Neither turns payment promises into forecast confidence. + +Development complexity: + +Low. + +Business value: + +High. + +Adoption impact: + +Medium-high. + +Frequency of use: + +Weekly. + +Competitive advantage: + +Moderate. Needs connection to cash forecast to become powerful. + +## 7. Cash Confidence Score + +Problem solved: + +Expected cash is not equal to reliable cash. + +Who benefits: + +Owners and accountants. + +Why competitors do not solve it well: + +Many systems show due amounts but do not distinguish confirmed, expected, risky, or hypothetical cash. + +Development complexity: + +Low-medium. + +Business value: + +High. + +Adoption impact: + +Medium. + +Frequency of use: + +Weekly. + +Competitive advantage: + +Strong as a supporting model. + +## 8. Weekly Cash Review + +Problem solved: + +Owners lack a repeatable cash meeting workflow. + +Who benefits: + +Owner-led businesses and accounting firms. + +Why competitors do not solve it well: + +Most tools provide screens. They do not prescribe a weekly operating ritual. + +Development complexity: + +Medium. + +Business value: + +High. + +Adoption impact: + +High if it becomes habit. + +Frequency of use: + +Weekly. + +Competitive advantage: + +Strong, but it is a workflow wrapper. Needs a sharp core decision feature. + +## 9. Supplier Pressure Board + +Problem solved: + +Owners need to know which suppliers can be paid, delayed, split, or negotiated with. + +Who benefits: + +Manufacturers, workshops, distributors. + +Why competitors do not solve it well: + +Payables lists do not usually combine supplier relationship risk with safe cash and forecast impact. + +Development complexity: + +Medium. + +Business value: + +High. + +Adoption impact: + +Medium-high. + +Frequency of use: + +Weekly. + +Competitive advantage: + +Good for manufacturing/distribution. + +## 10. Scenario Snapshots + +Problem solved: + +Owners need to test "what if customer pays late?" or "what if we delay purchase?" without building a spreadsheet. + +Who benefits: + +Owners, CFO consultants, accountants. + +Why competitors do not solve it well: + +ERPs can report; spreadsheets can model; few lightweight self-hosted tools make scenarios part of weekly cash decisions. + +Development complexity: + +Medium. + +Business value: + +High. + +Adoption impact: + +High. + +Frequency of use: + +Weekly/monthly. + +Competitive advantage: + +Strong as part of Safe-to-Pay Radar. + +## 11. Cash Runway Forecast + +Problem solved: + +Owners need to know how many days of cash they have under expected and worst-case assumptions. + +Who benefits: + +All cash-sensitive companies. + +Why competitors do not solve it well: + +Accounting reports are backward-looking; runway is operational and forward-looking. + +Development complexity: + +Medium. + +Business value: + +High. + +Adoption impact: + +High. + +Frequency of use: + +Weekly. + +Competitive advantage: + +Moderate by itself; strong inside dashboard. + +## 12. Accountant Cash Summary + +Problem solved: + +Accountants see books; owners need cash advice. The summary bridges the conversation. + +Who benefits: + +Accounting firms, owners. + +Why competitors do not solve it well: + +Accounting software is accountant-centered; ERP is operations-centered. This is advisor conversation-centered. + +Development complexity: + +Low-medium. + +Business value: + +Medium-high. + +Adoption impact: + +High through accounting firms. + +Frequency of use: + +Weekly/monthly. + +Competitive advantage: + +Good distribution advantage. + +## 13. Bank CSV Match Queue + +Problem solved: + +Manual entry kills trust and adoption. + +Who benefits: + +All users. + +Why competitors do not solve it well: + +Many competitors have bank sync, but self-hosted/local setups often struggle with practical import workflows. + +Development complexity: + +Medium. + +Business value: + +High. + +Adoption impact: + +High. + +Frequency of use: + +Weekly. + +Competitive advantage: + +Necessary but not emotionally differentiated. + +## 14. "What Changed Since Last Week?" + +Problem solved: + +Owners need to know why the cash forecast changed. + +Who benefits: + +Owners and advisors. + +Why competitors do not solve it well: + +Most systems show current state, not changes in assumptions. + +Development complexity: + +Medium. + +Business value: + +Medium-high. + +Adoption impact: + +Medium. + +Frequency of use: + +Weekly. + +Competitive advantage: + +Strong for trust, not enough as the flagship. + +## 15. Purchase Delay Advisor + +Problem solved: + +Owners need to decide whether to buy equipment/materials now or wait. + +Who benefits: + +Workshops, manufacturers, distributors. + +Why competitors do not solve it well: + +Inventory/ERP systems manage purchases; they rarely tell a small owner the cash safety impact in plain language. + +Development complexity: + +Medium. + +Business value: + +High. + +Adoption impact: + +Medium-high in target niche. + +Frequency of use: + +Occasional/weekly. + +Competitive advantage: + +Good niche advantage. + +## 16. Minimum Cash Buffer Guard + +Problem solved: + +Owners need a line they should not cross. + +Who benefits: + +All small businesses. + +Why competitors do not solve it well: + +Bank balance shows absolute cash; it does not separate cash that must remain protected. + +Development complexity: + +Low. + +Business value: + +High. + +Adoption impact: + +Medium. + +Frequency of use: + +Weekly/daily. + +Competitive advantage: + +Useful but simple; best as part of Safe-to-Pay Radar. + +## 17. Critical Week Finder + +Problem solved: + +Owners need to know which future week is the most dangerous. + +Who benefits: + +Cash-constrained businesses. + +Why competitors do not solve it well: + +Reports show ranges; they often do not highlight the week that will hurt. + +Development complexity: + +Medium. + +Business value: + +High. + +Adoption impact: + +High. + +Frequency of use: + +Weekly. + +Competitive advantage: + +Strong as part of forecast UX. + +## 18. Cash Decision Log + +Problem solved: + +Owners forget why they delayed or approved payments. + +Who benefits: + +Owners, managers, accountants. + +Why competitors do not solve it well: + +Accounting records transactions, not decision rationale. + +Development complexity: + +Low-medium. + +Business value: + +Medium. + +Adoption impact: + +Medium. + +Frequency of use: + +Weekly. + +Competitive advantage: + +Good trust feature, not a wow feature alone. + +## 19. Customer Concentration Warning + +Problem solved: + +One customer can dominate cash risk. + +Who benefits: + +Small B2B companies with a few large customers. + +Why competitors do not solve it well: + +CRM/accounting tools may show revenue concentration; they often do not show cash timing concentration. + +Development complexity: + +Medium. + +Business value: + +High. + +Adoption impact: + +Medium. + +Frequency of use: + +Monthly/weekly. + +Competitive advantage: + +Good analytic feature, not universal enough. + +## 20. Cash Stress Test + +Problem solved: + +Owners need to know whether the business survives common shocks. + +Who benefits: + +Owners, advisors, lenders. + +Why competitors do not solve it well: + +Small-business tools rarely offer simple operational stress tests. + +Development complexity: + +Medium-high. + +Business value: + +High. + +Adoption impact: + +Medium-high. + +Frequency of use: + +Monthly/quarterly. + +Competitive advantage: + +Strong for advisors, but less weekly than Safe-to-Pay Radar. + +## 21. Payment Split Planner + +Problem solved: + +Owners often need to split supplier payments without losing sight of cash impact. + +Who benefits: + +Workshops, distributors, companies with supplier pressure. + +Why competitors do not solve it well: + +Payables tools track payment terms; they do not usually model negotiation options against forecast risk. + +Development complexity: + +Medium. + +Business value: + +High in cash-tight businesses. + +Adoption impact: + +Medium. + +Frequency of use: + +Weekly. + +Competitive advantage: + +Useful inside Safe-to-Pay Radar. + +## 22. Cash Forecast Confidence Timeline + +Problem solved: + +Forecasts feel fake when assumptions are hidden. + +Who benefits: + +Owners and advisors. + +Why competitors do not solve it well: + +Forecasts are often presented as precise. Small businesses need confidence-aware forecasts. + +Development complexity: + +Medium. + +Business value: + +Medium-high. + +Adoption impact: + +Medium. + +Frequency of use: + +Weekly. + +Competitive advantage: + +Good trust layer. + +## Ranking + +| Rank | Candidate | Why | +| ---: | --- | --- | +| 1 | Safe-to-Pay Radar | Directly answers the owner's most urgent cash question and creates weekly/daily habit. | +| 2 | Cash Collision Calendar | Strong visual way to reveal future cash crises. | +| 3 | Scenario Snapshots | Converts anxiety into decisions. | +| 4 | Late Customer Risk Meter | Expensive, emotional, easy to understand. | +| 5 | Weekly Cash Review | Important habit wrapper, but needs a core decision engine. | +| 6 | Cash Runway Forecast | High value, but common enough to need stronger framing. | +| 7 | Supplier Pressure Board | Strong in workshops/distribution, less universal. | +| 8 | Payroll Protection Reserve | Emotional and valuable, but narrower. | +| 9 | Critical Week Finder | Excellent insight, best as part of the radar. | +| 10 | Bank CSV Match Queue | Necessary adoption feature, not the emotional hook. | +| 11 | Tax/VAT Cash Reserve | Valuable but country-sensitive. | +| 12 | Cash Confidence Score | Strong model, not a standalone feature. | +| 13 | Customer Promise Tracker | Useful input, not the whole product. | +| 14 | Payment Split Planner | Strong supporting action. | +| 15 | Cash Stress Test | Powerful for advisors, less weekly. | +| 16 | Accountant Cash Summary | Distribution feature, not the owner wow moment. | +| 17 | Purchase Delay Advisor | Valuable niche feature. | +| 18 | Minimum Cash Buffer Guard | Simple and useful, not enough alone. | +| 19 | What Changed Since Last Week? | Trust feature, not first-order pain. | +| 20 | Customer Concentration Warning | Important but less frequent. | +| 21 | Cash Decision Log | Supports accountability, not acquisition. | +| 22 | Cash Forecast Confidence Timeline | Good sophistication, not immediate pain. | + +## Chosen Feature + +## Safe-to-Pay Radar + +Single strongest feature: + +> Safe-to-Pay Radar tells a business owner what can be paid this week without breaking payroll, taxes, supplier +> commitments, reserves, or future cash runway. + +This is the feature most likely to make a small business owner say: + +> I need this. + +## Why This Wins + +It is not a report. + +It is not another dashboard. + +It answers a stressful decision: + +> Can I pay this? + +The owner does not need more data. The owner needs confidence. + +Safe-to-Pay Radar creates that confidence by combining: + +- current cash; +- committed cash; +- protected reserves; +- expected receivables; +- overdue receivables; +- due payables; +- confidence levels; +- scenario outcomes; +- recommended actions. + +## Product Guardian Evaluation + +Does it strengthen the Cash Cockpit vision? + +Yes. It is the clearest expression of cash cockpit: cash position plus decision. + +Does it improve weekly cash decisions? + +Yes. It becomes the weekly payment approval surface. + +Does it improve cash visibility? + +Yes. It separates bank cash, safe cash, committed cash, protected cash, and risky expected cash. + +Does it reduce uncertainty? + +Yes. It exposes assumptions, confidence levels, and future collisions. + +Does it move OpenCashFlow toward ERP? + +No, if scoped correctly. It does not require inventory, CRM, accounting, payroll calculation, or purchasing modules. It +only models cash commitments and decision outcomes. + +## UX Design + +## Primary Screen: Safe-to-Pay Radar + +Purpose: + +The owner opens this screen before approving payments. + +Top-level question: + +> What can we safely pay this week? + +Primary sections: + +1. Safe Cash Summary. +2. Payment Decision Queue. +3. Cash Collision Timeline. +4. Risk Explanation. +5. Scenario Drawer. +6. Weekly Action Summary. + +## UX Principle + +The screen must never ask the owner to interpret raw finance. + +It should say: + +- safe; +- risky; +- unsafe; +- why; +- what to do next. + +## Workflow + +## 1. Open Weekly Review + +The owner starts the Monday review. + +OpenCashFlow shows: + +- bank cash; +- safe cash; +- committed cash; +- protected reserves; +- expected cash; +- risky expected cash. + +## 2. Review Payment Queue + +The owner sees payments due this week: + +- supplier; +- amount; +- due date; +- status; +- impact if paid; +- recommendation. + +Recommendations: + +- Safe to pay. +- Pay only if receivable lands. +- Split recommended. +- Delay recommended. +- Unsafe: breaks reserve. + +## 3. Inspect A Payment + +Click a payment. + +Show: + +- current cash impact; +- forecast after payment; +- affected future commitments; +- protected reserve impact; +- late receivables dependency; +- scenarios. + +## 4. Run Scenario + +Scenario options: + +- customer pays 7 days late; +- customer pays 15 days late; +- split supplier payment; +- delay payment; +- reserve tax first; +- delay purchase. + +## 5. Decide + +Possible decisions: + +- approve; +- delay; +- split; +- hold pending customer; +- mark as critical; +- negotiate; +- reject. + +Each decision updates the forecast and decision log. + +## 6. Share Summary + +The owner exports or shares: + +- payments approved; +- payments delayed; +- cash risk; +- customer follow-ups; +- accountant questions. + +## Conceptual Data Model + +This is product data, not implementation. + +## CashAccount + +Represents a bank or cash source. + +Fields: + +- `CashAccountId` +- `Name` +- `CurrentBalance` +- `LastUpdatedAt` +- `Source` + +## CashReserve + +Represents protected cash. + +Examples: + +- payroll reserve; +- tax reserve; +- rent reserve; +- minimum buffer. + +Fields: + +- `CashReserveId` +- `Name` +- `Amount` +- `Priority` +- `ProtectedUntil` +- `Reason` + +## ExpectedCashIn + +Represents money expected to arrive. + +Fields: + +- `ExpectedCashInId` +- `Counterparty` +- `Amount` +- `ExpectedDate` +- `OriginalDueDate` +- `Confidence` +- `Status` +- `Source` +- `Notes` + +## CashCommitment + +Represents money expected or required to leave. + +Fields: + +- `CashCommitmentId` +- `Payee` +- `Amount` +- `DueDate` +- `Category` +- `Criticality` +- `Deferrable` +- `Status` +- `Source` +- `Notes` + +## PaymentDecision + +Represents an owner decision. + +Fields: + +- `PaymentDecisionId` +- `CashCommitmentId` +- `Decision` +- `DecisionDate` +- `DecisionBy` +- `Reason` +- `ExpectedImpact` + +Decision values: + +- approve; +- delay; +- split; +- hold; +- negotiate; +- reject. + +## ForecastPoint + +Represents projected balance on a date. + +Fields: + +- `Date` +- `ExpectedBalance` +- `ConservativeBalance` +- `WorstCaseBalance` +- `SafeCash` +- `CommittedCash` + +## CashCollision + +Represents a future risk event. + +Fields: + +- `CashCollisionId` +- `Date` +- `Severity` +- `Trigger` +- `AffectedReserve` +- `ProjectedShortfall` +- `Explanation` +- `SuggestedActions` + +## Scenario + +Represents a temporary what-if assumption. + +Fields: + +- `ScenarioId` +- `Name` +- `Assumptions` +- `ProjectedLowPoint` +- `RunwayDays` +- `AffectedCommitments` + +## WeeklyCashReview + +Represents the weekly ritual. + +Fields: + +- `WeeklyCashReviewId` +- `WeekStartDate` +- `StartedBy` +- `CashAtStart` +- `SafeCashAtStart` +- `ActionsTaken` +- `Summary` +- `CompletedAt` + +## Screenshots (Concept) + +These are wireframe concepts, not implementation screens. + +## Screen 1: Safe-to-Pay Overview + +```text ++--------------------------------------------------------------+ +| Safe-to-Pay Radar Week of Aug 5 | ++--------------------------------------------------------------+ +| Bank Cash Safe Cash Committed Expected Risk | +| 84,000 18,300 61,500 79,900 HIGH | ++--------------------------------------------------------------+ +| This week: You can safely approve 3 of 7 payments. | +| Warning: ACME late payment can break payroll reserve Aug 12. | ++--------------------------------------------------------------+ +| Payment Queue | +| Supplier Due Amount Recommendation | +| SteelCo Aug 7 27,500 Split recommended | +| Payroll Aug 9 38,000 Protected | +| Rent Aug 10 6,000 Safe to pay | +| Machine purchase Aug 11 32,000 Delay recommended | ++--------------------------------------------------------------+ +``` + +## Screen 2: Payment Decision Detail + +```text ++--------------------------------------------------------------+ +| SteelCo - 27,500 due Aug 7 | ++--------------------------------------------------------------+ +| If paid in full: | +| - Safe cash drops to -9,200 | +| - Payroll reserve is breached | +| - Lowest projected balance: Aug 12 | ++--------------------------------------------------------------+ +| Recommended action: Split payment | +| Pay 12,000 now, 15,500 after ACME receipt | ++--------------------------------------------------------------+ +| [Approve split] [Delay] [Run scenario] [Mark critical] | ++--------------------------------------------------------------+ +``` + +## Screen 3: Cash Collision Timeline + +```text +Aug 5 Aug 7 Aug 9 Aug 12 Aug 18 +84,000 56,500 18,500 -6,800 45,200 + SteelCo Payroll ACME late ACME paid + reserve collision + protected +``` + +## Screen 4: Scenario Drawer + +```text ++--------------------------------------------------------------+ +| Scenario: ACME pays 15 days late | ++--------------------------------------------------------------+ +| Expected low point: -6,800 | +| Safe cash breach: Yes | +| Payroll at risk: Yes | +| Supplier at risk: SteelCo | ++--------------------------------------------------------------+ +| Fix options | +| 1. Split SteelCo payment: low point becomes 9,400 | +| 2. Delay machine purchase: low point becomes 18,300 | +| 3. Collect 10,000 deposit: low point becomes 3,200 | ++--------------------------------------------------------------+ +``` + +## MVP + +## MVP Goal + +Answer: + +> What can we safely pay this week? + +## MVP Scope + +Inputs: + +- current cash balance; +- manual expected cash-in; +- manual cash commitments; +- manual reserves; +- confidence level; +- due dates. + +Core calculations: + +- safe cash; +- committed cash; +- projected daily balance; +- reserve breach; +- payment impact; +- basic recommendation. + +Screens: + +1. Safe-to-Pay Overview. +2. Payment Decision Detail. +3. Simple Scenario Drawer. +4. Weekly Summary. + +Recommendations: + +- safe to pay; +- risky; +- unsafe; +- split/delay suggested. + +Out of scope: + +- bank sync; +- full accounting; +- payroll calculation; +- tax filing; +- inventory; +- CRM; +- automated supplier negotiation; +- AI recommendations. + +## MVP Success Criteria + +The MVP works if an owner can: + +1. enter cash, receivables, commitments, and reserves; +2. open the Radar on Monday; +3. see which payments are safe; +4. understand why one payment is risky; +5. run one scenario; +6. decide what to pay or delay. + +## Version 2 + +Goal: + +Reduce manual work and increase trust. + +Add: + +- bank CSV import; +- invoice/payables CSV import; +- match expected payments to bank movements; +- recurring commitments; +- decision log; +- accountant summary export; +- "what changed since last week"; +- customer payment promise tracker. + +Do not add: + +- full invoicing; +- full accounting; +- supplier portal; +- inventory. + +## Version 3 + +Goal: + +Make Safe-to-Pay Radar the operating system for weekly cash decisions. + +Add: + +- import templates for Odoo, ERPNext, Dolibarr, Akaunting, Invoice Ninja, spreadsheets; +- advisor/accountant multi-client view; +- cash risk alerts; +- workshop/manufacturing cash pack; +- regional bank connector packs where commercially practical; +- certified cash review reports; +- scenario library. + +Do not add: + +- ERP module marketplace; +- payroll; +- tax compliance engine; +- broad CRM. + +## Why This Feature Creates Emotional Value + +Small business owners do not lose sleep because they lack another report. + +They lose sleep because they are unsure whether they can pay people, suppliers, taxes, and still survive the next +customer delay. + +Safe-to-Pay Radar gives them a concrete answer. + +It replaces: + +- "I think we are okay" + +with: + +- "We can pay rent and payroll. We should split SteelCo. Do not buy the machine until ACME pays." + +That is emotional value. + +## Why This Solves An Expensive Problem + +Bad cash decisions create expensive consequences: + +- payroll stress; +- supplier holds; +- late fees; +- emergency borrowing; +- missed discounts; +- owner panic; +- damaged trust; +- bad purchases; +- tax surprises. + +If Safe-to-Pay Radar prevents even one avoidable cash crunch, it can justify the product. + +## Why Competitors Will Not Easily Own This + +Accounting software is ledger-first. + +ERP is process-first. + +Invoicing software is billing-first. + +Personal finance software is budget-first. + +OpenCashFlow can be decision-first. + +Safe-to-Pay Radar is not a module bolted onto an ERP. It is the product's central job. + +## Could This Make OpenCashFlow Famous? + +Yes. + +If OpenCashFlow became famous, Safe-to-Pay Radar could be the reason. + +The phrase is concrete: + +> It tells me what I can safely pay. + +That is stronger than: + +- cash-flow dashboard; +- financial report; +- budget view; +- payment list; +- forecast chart. + +The feature is memorable because it maps directly to owner anxiety. + +## If Not This, What Would Be Better? + +The only stronger alternative would be an even sharper version: + +> Pay This Week + +A screen that simply says: + +- Pay these. +- Delay these. +- Call these customers. +- Reserve this cash. +- Do not buy this yet. + +That may become the final UX name. + +But the underlying feature remains Safe-to-Pay Radar. + +## Final Decision + +Build: + +> Safe-to-Pay Radar + +Position it as: + +> The weekly answer to "what can we safely pay?" + +Reject any feature that distracts from this until the Radar is useful, trusted, and habit-forming. From e4dda1319a4861f970023a37fb0979eca97031cd Mon Sep 17 00:00:00 2001 From: Codewriter90x Date: Thu, 9 Jul 2026 17:25:31 +0200 Subject: [PATCH 09/10] feat(setup): add first-run setup wizard --- Docs/setup/first-run-setup-wizard.md | 93 ++++++++++++++ README.md | 4 + .../Controllers/SetupController.cs | 15 ++- .../CompleteSetup/CompleteSetupCommand.cs | 1 - .../CompleteSetup/CompleteSetupResult.cs | 9 +- .../CompleteSetup/CompleteSetupUseCase.cs | 55 +++++++-- .../Setup/Ports/ISetupWriter.cs | 5 +- src/OpenCashFlow.Contracts/DTOs/Setup_DTO.cs | 17 ++- .../Setup/SetupWriter.cs | 8 +- .../Controllers/HomeController.Setup.cs | 19 ++- .../Models/Setup/SetupCompleteViewModel.cs | 7 ++ .../Services/SetupAPIService.cs | 9 +- .../Views/Home/Setup.cshtml | 19 +-- .../Views/Home/SetupComplete.cshtml | 43 +++++++ .../Setup/SetupUseCaseTests.cs | 26 +++- .../Factories/CustomWebApplicationFactory.cs | 9 +- .../Tests/API/Setup_Tests.cs | 116 ++++++++++++++++++ 17 files changed, 398 insertions(+), 57 deletions(-) create mode 100644 Docs/setup/first-run-setup-wizard.md create mode 100644 src/OpenCashFlow.WebApp/Models/Setup/SetupCompleteViewModel.cs create mode 100644 src/OpenCashFlow.WebApp/Views/Home/SetupComplete.cshtml create mode 100644 tests/OpenCashFlow.Test/Tests/API/Setup_Tests.cs diff --git a/Docs/setup/first-run-setup-wizard.md b/Docs/setup/first-run-setup-wizard.md new file mode 100644 index 0000000..8ce7901 --- /dev/null +++ b/Docs/setup/first-run-setup-wizard.md @@ -0,0 +1,93 @@ +# First-Run Setup Wizard + +OpenCashFlow includes a first-run setup wizard for fresh self-hosted installations. + +The wizard configures application data only. It does not create PostgreSQL users, create databases, change database permissions, or perform infrastructure provisioning. Database connectivity must already be configured through Docker Compose, environment variables, or the host deployment configuration. + +## Local Docker Flow + +```bash +cp .env.example .env +docker compose up --build +``` + +Then open: + +```text +http://localhost:5200 +``` + +If no company and no administrator exist, the WebApp redirects to: + +```text +/Setup +``` + +## Setup Inputs + +The first-run setup form asks for: + +- company name; +- owner/admin first name; +- optional owner/admin last name; +- admin email; +- language; +- currency; +- country; +- timezone. + +The setup wizard does not ask for an administrator password. OpenCashFlow generates a strong temporary password server-side. + +## What Setup Creates + +On a fresh instance, setup creates: + +- the first company/tenant; +- the first administrator user; +- standard self-hosted roles; +- the administrator role assignments; +- the company staff link for the administrator; +- the administrator contact email; +- an initial zero cash balance for the company. + +Cash Custody domain contracts exist, but there is no persisted multi-cash-account schema yet. Until that implementation lands, setup seeds the current legacy company-level `CashBalance` record rather than a new `CashAccount`. + +## Temporary Password + +After successful setup, the WebApp shows the generated temporary administrator password exactly once in the setup POST response. + +Store it immediately. Refreshing or revisiting setup after completion does not show the password again. + +The password is: + +- generated server-side with a cryptographic random generator; +- hashed before storage; +- never stored as plaintext; +- never logged intentionally by setup code; +- marked as temporary by setting the first admin to require password change after login. + +## Setup Lock + +Setup is only available while the instance has no company and no administrator user. + +After setup completes: + +- `GET /v1/Setup/status` reports `RequiresSetup = false`; +- `POST /v1/Setup` returns a conflict instead of creating another instance; +- the WebApp `/Setup` page redirects to login. + +If the database is partially configured, for example a company exists but no admin user exists, setup refuses to continue. That state requires an explicit recovery procedure rather than silent repair. + +## First Login + +Use the administrator email and the temporary password shown after setup completes. + +After login, OpenCashFlow redirects the user to the password-change screen because the first administrator is created with `UserMustChangePassword = true`. + +## Security Notes + +- Do not expose an unconfigured instance publicly. +- Set real secrets and database credentials before any public or shared deployment. +- Treat `.env.example` as a template only. +- Do not paste generated setup passwords into issue reports, logs, screenshots, or support channels. +- If the temporary password is lost before first login, use a controlled password reset or database recovery procedure. diff --git a/README.md b/README.md index 8de92b7..76fe54a 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,10 @@ Local endpoints: The default Docker Compose configuration is for local evaluation. Change secrets, database credentials, TLS, backups, reverse proxy configuration, and operational settings before exposing any instance. +On a fresh database, opening the WebApp redirects to `/Setup`. The first-run wizard creates the first company and admin +user, generates a temporary password, shows it once, and then requires a password change after login. See +[Docs/setup/first-run-setup-wizard.md](Docs/setup/first-run-setup-wizard.md). + ### Run Manually Configure `DEFAULT_CONN_STRING` or `ConnectionStrings:DefaultConnectionString`, then run: diff --git a/src/OpenCashFlow.API/Controllers/SetupController.cs b/src/OpenCashFlow.API/Controllers/SetupController.cs index 3862f6d..bd2768f 100644 --- a/src/OpenCashFlow.API/Controllers/SetupController.cs +++ b/src/OpenCashFlow.API/Controllers/SetupController.cs @@ -1,6 +1,7 @@ using Asp.Versioning; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.RateLimiting; using OpenCashFlow.Application.Setup.CompleteSetup; using OpenCashFlow.Application.Setup.GetSetupStatus; using OpenCashFlow.Contracts.DTOs; @@ -23,6 +24,7 @@ public async Task> Status(CancellationToken cancel } [HttpPost] + [EnableRateLimiting("auth-limiter")] public async Task Create([FromBody] SetupRequest_DTO request, CancellationToken cancellationToken) { if (!ModelState.IsValid) @@ -33,7 +35,6 @@ public async Task Create([FromBody] SetupRequest_DTO request, Can var result = await completeSetupUseCase.ExecuteAsync(new CompleteSetupCommand( request.CompanyName, request.AdminEmail, - request.AdminPassword, request.AdminFirstName, request.AdminLastName, request.Language, @@ -43,7 +44,7 @@ public async Task Create([FromBody] SetupRequest_DTO request, Can if (result.Success && result.Status is not null) { - return CreatedAtAction(nameof(Status), ToDto(result.Status)); + return CreatedAtAction(nameof(Status), ToCompletedDto(result.Status, request.AdminEmail, result.TemporaryAdminPassword!)); } return result.Failure switch @@ -64,5 +65,15 @@ private static SetupStatus_DTO ToDto(SetupStatusResult status) HasAdminUsers = status.HasAdminUsers }; } + + private static SetupCompleted_DTO ToCompletedDto(SetupStatusResult status, string adminEmail, string temporaryAdminPassword) + { + return new SetupCompleted_DTO + { + Status = ToDto(status), + AdminEmail = adminEmail.Trim(), + TemporaryAdminPassword = temporaryAdminPassword + }; + } } } diff --git a/src/OpenCashFlow.Application/Setup/CompleteSetup/CompleteSetupCommand.cs b/src/OpenCashFlow.Application/Setup/CompleteSetup/CompleteSetupCommand.cs index acacb64..d9d7eef 100644 --- a/src/OpenCashFlow.Application/Setup/CompleteSetup/CompleteSetupCommand.cs +++ b/src/OpenCashFlow.Application/Setup/CompleteSetup/CompleteSetupCommand.cs @@ -3,7 +3,6 @@ namespace OpenCashFlow.Application.Setup.CompleteSetup; public sealed record CompleteSetupCommand( string CompanyName, string AdminEmail, - string AdminPassword, string AdminFirstName, string? AdminLastName, string Language, diff --git a/src/OpenCashFlow.Application/Setup/CompleteSetup/CompleteSetupResult.cs b/src/OpenCashFlow.Application/Setup/CompleteSetup/CompleteSetupResult.cs index 3a1fc21..8fc4bcc 100644 --- a/src/OpenCashFlow.Application/Setup/CompleteSetup/CompleteSetupResult.cs +++ b/src/OpenCashFlow.Application/Setup/CompleteSetup/CompleteSetupResult.cs @@ -15,11 +15,12 @@ public sealed record CompleteSetupResult( bool Success, CompleteSetupFailure Failure, string? Message, - SetupStatusResult? Status) + SetupStatusResult? Status, + string? TemporaryAdminPassword) { - public static CompleteSetupResult Ok(SetupStatusResult status) - => new(true, CompleteSetupFailure.None, null, status); + public static CompleteSetupResult Ok(SetupStatusResult status, string temporaryAdminPassword) + => new(true, CompleteSetupFailure.None, null, status, temporaryAdminPassword); public static CompleteSetupResult Fail(CompleteSetupFailure failure, string message) - => new(false, failure, message, null); + => new(false, failure, message, null, null); } diff --git a/src/OpenCashFlow.Application/Setup/CompleteSetup/CompleteSetupUseCase.cs b/src/OpenCashFlow.Application/Setup/CompleteSetup/CompleteSetupUseCase.cs index 41d9c76..f4927ee 100644 --- a/src/OpenCashFlow.Application/Setup/CompleteSetup/CompleteSetupUseCase.cs +++ b/src/OpenCashFlow.Application/Setup/CompleteSetup/CompleteSetupUseCase.cs @@ -1,4 +1,5 @@ using OpenCashFlow.Application.Setup.Ports; +using System.Security.Cryptography; namespace OpenCashFlow.Application.Setup.CompleteSetup; @@ -28,24 +29,52 @@ public async Task ExecuteAsync(CompleteSetupCommand command return CompleteSetupResult.Fail(CompleteSetupFailure.PartiallyConfigured, "Setup cannot continue because this instance is partially configured."); } - if (!IsStrongPassword(command.AdminPassword)) + var temporaryAdminPassword = GenerateTemporaryPassword(); + var completedStatus = await setupWriter.CompleteAsync(command, temporaryAdminPassword, cancellationToken); + return CompleteSetupResult.Ok(completedStatus, temporaryAdminPassword); + } + + private static string GenerateTemporaryPassword() + { + const string upper = "ABCDEFGHJKLMNPQRSTUVWXYZ"; + const string lower = "abcdefghijkmnopqrstuvwxyz"; + const string digits = "23456789"; + const string symbols = "!@#$%^&*()-_=+"; + const string all = upper + lower + digits + symbols; + + Span password = + [ + Pick(upper), + Pick(lower), + Pick(digits), + Pick(symbols), + Pick(all), + Pick(all), + Pick(all), + Pick(all), + Pick(all), + Pick(all), + Pick(all), + Pick(all), + Pick(all), + Pick(all), + Pick(all), + Pick(all), + Pick(all), + Pick(all) + ]; + + for (var i = password.Length - 1; i > 0; i--) { - return CompleteSetupResult.Fail( - CompleteSetupFailure.WeakPassword, - "Admin password must be at least 8 characters and include upper, lower, digit, and special characters."); + var j = RandomNumberGenerator.GetInt32(i + 1); + (password[i], password[j]) = (password[j], password[i]); } - var completedStatus = await setupWriter.CompleteAsync(command, cancellationToken); - return CompleteSetupResult.Ok(completedStatus); + return new string(password); } - private static bool IsStrongPassword(string password) + private static char Pick(string source) { - return !string.IsNullOrWhiteSpace(password) - && password.Length >= 8 - && password.Any(char.IsUpper) - && password.Any(char.IsLower) - && password.Any(char.IsDigit) - && password.Any(ch => !char.IsLetterOrDigit(ch)); + return source[RandomNumberGenerator.GetInt32(source.Length)]; } } diff --git a/src/OpenCashFlow.Application/Setup/Ports/ISetupWriter.cs b/src/OpenCashFlow.Application/Setup/Ports/ISetupWriter.cs index 79b7e89..923dad3 100644 --- a/src/OpenCashFlow.Application/Setup/Ports/ISetupWriter.cs +++ b/src/OpenCashFlow.Application/Setup/Ports/ISetupWriter.cs @@ -5,5 +5,8 @@ namespace OpenCashFlow.Application.Setup.Ports; public interface ISetupWriter { - Task CompleteAsync(CompleteSetupCommand command, CancellationToken cancellationToken = default); + Task CompleteAsync( + CompleteSetupCommand command, + string temporaryAdminPassword, + CancellationToken cancellationToken = default); } diff --git a/src/OpenCashFlow.Contracts/DTOs/Setup_DTO.cs b/src/OpenCashFlow.Contracts/DTOs/Setup_DTO.cs index 1fd0af1..4044814 100644 --- a/src/OpenCashFlow.Contracts/DTOs/Setup_DTO.cs +++ b/src/OpenCashFlow.Contracts/DTOs/Setup_DTO.cs @@ -19,12 +19,11 @@ public class SetupRequest_DTO [EmailAddress] public required string AdminEmail { get; set; } - [Required] - [StringLength(128, MinimumLength = 8)] - public required string AdminPassword { get; set; } + // Kept optional for older clients. First-run setup now generates the temporary password server-side. + public string? AdminPassword { get; set; } - [Compare(nameof(AdminPassword))] - public required string ConfirmPassword { get; set; } + // Kept optional for older clients. First-run setup now generates the temporary password server-side. + public string? ConfirmPassword { get; set; } [Required] [StringLength(80)] @@ -49,4 +48,12 @@ public class SetupRequest_DTO [StringLength(2, MinimumLength = 2)] public string Country { get; set; } = "IT"; } + + public class SetupCompleted_DTO + { + public required SetupStatus_DTO Status { get; set; } + public required string AdminEmail { get; set; } + public required string TemporaryAdminPassword { get; set; } + public string Message { get; set; } = "Setup completed. Store the temporary password now; it is shown only once."; + } } diff --git a/src/OpenCashFlow.Infrastructure/Setup/SetupWriter.cs b/src/OpenCashFlow.Infrastructure/Setup/SetupWriter.cs index 4eae6cc..f87e7bd 100644 --- a/src/OpenCashFlow.Infrastructure/Setup/SetupWriter.cs +++ b/src/OpenCashFlow.Infrastructure/Setup/SetupWriter.cs @@ -14,7 +14,10 @@ namespace OpenCashFlow.Infrastructure.Setup; public sealed class SetupWriter(ApplicationDbContext db, ILogger logger) : ISetupWriter { - public async Task CompleteAsync(CompleteSetupCommand command, CancellationToken cancellationToken = default) + public async Task CompleteAsync( + CompleteSetupCommand command, + string temporaryAdminPassword, + CancellationToken cancellationToken = default) { var normalizedEmail = command.AdminEmail.Trim(); var now = DateTime.UtcNow; @@ -65,7 +68,8 @@ public async Task CompleteAsync(CompleteSetupCommand command, PrivacyPolicyAcepted = true, PrivacyPolicyAcceptedDate = now, PasswordSalt = salt, - PasswordHash = PasswordHasher.HashPasswordArgon2(command.AdminPassword, salt), + PasswordHash = PasswordHasher.HashPasswordArgon2(temporaryAdminPassword, salt), + UserMustChangePassword = true, DateIns = now }); diff --git a/src/OpenCashFlow.WebApp/Controllers/HomeController.Setup.cs b/src/OpenCashFlow.WebApp/Controllers/HomeController.Setup.cs index 27696c0..eb85bfc 100644 --- a/src/OpenCashFlow.WebApp/Controllers/HomeController.Setup.cs +++ b/src/OpenCashFlow.WebApp/Controllers/HomeController.Setup.cs @@ -1,5 +1,6 @@ using OpenCashFlow.Contracts.DTOs; using Microsoft.AspNetCore.Mvc; +using OpenCashFlow.WebApp.Models.Setup; using OpenCashFlow.WebApp.Services; namespace OpenCashFlow.WebApp.Controllers @@ -22,8 +23,6 @@ public async Task Setup(CancellationToken cancellationToken) { CompanyName = string.Empty, AdminEmail = string.Empty, - AdminPassword = string.Empty, - ConfirmPassword = string.Empty, AdminFirstName = string.Empty, Language = "it", Currency = "EUR", @@ -42,6 +41,9 @@ public async Task Setup([Bind] SetupRequest_DTO model, Cancellati return View(model); } + model.AdminPassword = null; + model.ConfirmPassword = null; + var result = await _setupAPIService.CompleteSetupAsync(model, cancellationToken); if (!result.Success) { @@ -49,8 +51,17 @@ public async Task Setup([Bind] SetupRequest_DTO model, Cancellati return View(model); } - TempData["SetupCompleted"] = "Setup completed. Sign in with the administrator account."; - return RedirectToAction(nameof(Login)); + if (result.Setup is null || string.IsNullOrWhiteSpace(result.Setup.TemporaryAdminPassword)) + { + ViewBag.ErrorMessage = "Setup completed but the temporary password was not returned. Reset the admin password before signing in."; + return View(model); + } + + return View("SetupComplete", new SetupCompleteViewModel + { + AdminEmail = result.Setup.AdminEmail, + TemporaryAdminPassword = result.Setup.TemporaryAdminPassword + }); } } } diff --git a/src/OpenCashFlow.WebApp/Models/Setup/SetupCompleteViewModel.cs b/src/OpenCashFlow.WebApp/Models/Setup/SetupCompleteViewModel.cs new file mode 100644 index 0000000..79d2e5b --- /dev/null +++ b/src/OpenCashFlow.WebApp/Models/Setup/SetupCompleteViewModel.cs @@ -0,0 +1,7 @@ +namespace OpenCashFlow.WebApp.Models.Setup; + +public sealed class SetupCompleteViewModel +{ + public required string AdminEmail { get; init; } + public required string TemporaryAdminPassword { get; init; } +} diff --git a/src/OpenCashFlow.WebApp/Services/SetupAPIService.cs b/src/OpenCashFlow.WebApp/Services/SetupAPIService.cs index 08fcc0c..592b395 100644 --- a/src/OpenCashFlow.WebApp/Services/SetupAPIService.cs +++ b/src/OpenCashFlow.WebApp/Services/SetupAPIService.cs @@ -22,22 +22,23 @@ public class SetupAPIService(IHttpClientFactory httpClientFactory, ILogger CompleteSetupAsync(SetupRequest_DTO request, CancellationToken cancellationToken = default) + public async Task<(bool Success, string? Message, SetupCompleted_DTO? Setup)> CompleteSetupAsync(SetupRequest_DTO request, CancellationToken cancellationToken = default) { var response = await _httpClient.PostAsJsonAsync("/v1/Setup", request, cancellationToken); if (response.IsSuccessStatusCode) { - return (true, null); + var setup = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + return (true, null, setup); } if (response.StatusCode == HttpStatusCode.Conflict) { - return (false, "This instance is already configured."); + return (false, "This instance is already configured.", null); } var body = await response.Content.ReadAsStringAsync(cancellationToken); _logger.LogWarning("Setup failed with status {Status}: {Body}", response.StatusCode, body); - return (false, "Unable to complete setup. Check the fields and try again."); + return (false, "Unable to complete setup. Check the fields and try again.", null); } } } diff --git a/src/OpenCashFlow.WebApp/Views/Home/Setup.cshtml b/src/OpenCashFlow.WebApp/Views/Home/Setup.cshtml index 617344a..765b939 100644 --- a/src/OpenCashFlow.WebApp/Views/Home/Setup.cshtml +++ b/src/OpenCashFlow.WebApp/Views/Home/Setup.cshtml @@ -21,7 +21,7 @@ @Html.AntiForgeryToken()

First setup

-

Create the first company and instance administrator.

+

Create the first company and instance administrator. OpenCashFlow will generate a temporary password after setup.

@if (!string.IsNullOrWhiteSpace(ViewBag.ErrorMessage as string)) { @@ -55,19 +55,6 @@
-
-
- - - -
-
- - - -
-
-
@@ -94,6 +81,10 @@
+
+ Database connection and PostgreSQL credentials are read from the container/environment configuration. This wizard only creates application data. +
+ diff --git a/src/OpenCashFlow.WebApp/Views/Home/SetupComplete.cshtml b/src/OpenCashFlow.WebApp/Views/Home/SetupComplete.cshtml new file mode 100644 index 0000000..689079f --- /dev/null +++ b/src/OpenCashFlow.WebApp/Views/Home/SetupComplete.cshtml @@ -0,0 +1,43 @@ +@model OpenCashFlow.WebApp.Models.Setup.SetupCompleteViewModel +@{ + Layout = "_BlankLayout"; + ViewData["Title"] = "Setup complete"; +} + +@section PageStyles { + +} + +
+
+ + +
+
+

Setup complete

+

Store this temporary administrator password now. It will not be shown again.

+ +
+ + +
+ +
+ + +
+ +
+ You will be asked to change this password after the first login. +
+ + Go to login +
+
+
+
diff --git a/tests/OpenCashFlow.Application.Tests/Setup/SetupUseCaseTests.cs b/tests/OpenCashFlow.Application.Tests/Setup/SetupUseCaseTests.cs index c3a2dc4..26b9b3c 100644 --- a/tests/OpenCashFlow.Application.Tests/Setup/SetupUseCaseTests.cs +++ b/tests/OpenCashFlow.Application.Tests/Setup/SetupUseCaseTests.cs @@ -33,16 +33,16 @@ public async Task CompleteSetup_WhenAlreadyConfigured_Fails() } [Fact] - public async Task CompleteSetup_WithWeakPassword_FailsBeforeWriter() + public async Task CompleteSetup_WithInvalidInput_FailsBeforeWriter() { var reader = new FakeSetupReader(new SetupStatusResult(true, false, false)); var writer = new FakeSetupWriter(); var useCase = new CompleteSetupUseCase(reader, writer); - var result = await useCase.ExecuteAsync(ValidCommand() with { AdminPassword = "weak" }); + var result = await useCase.ExecuteAsync(ValidCommand() with { CompanyName = " " }); Assert.False(result.Success); - Assert.Equal(CompleteSetupFailure.WeakPassword, result.Failure); + Assert.Equal(CompleteSetupFailure.InvalidInput, result.Failure); Assert.False(writer.Called); } @@ -58,6 +58,9 @@ public async Task CompleteSetup_WithValidEmptyInstance_CallsWriter() Assert.True(result.Success); Assert.True(writer.Called); Assert.False(result.Status!.RequiresSetup); + Assert.NotNull(result.TemporaryAdminPassword); + Assert.Equal(result.TemporaryAdminPassword, writer.TemporaryAdminPassword); + Assert.True(IsStrongPassword(result.TemporaryAdminPassword)); } private static CompleteSetupCommand ValidCommand() @@ -65,7 +68,6 @@ private static CompleteSetupCommand ValidCommand() return new CompleteSetupCommand( "OpenCashFlow Test", "admin@example.local", - "Str0ng!Pass", "Admin", "User", "it", @@ -88,11 +90,25 @@ public Task GetStatusAsync(CancellationToken cancellationToke private sealed class FakeSetupWriter : ISetupWriter { public bool Called { get; private set; } + public string? TemporaryAdminPassword { get; private set; } - public Task CompleteAsync(CompleteSetupCommand command, CancellationToken cancellationToken = default) + public Task CompleteAsync( + CompleteSetupCommand command, + string temporaryAdminPassword, + CancellationToken cancellationToken = default) { Called = true; + TemporaryAdminPassword = temporaryAdminPassword; return Task.FromResult(new SetupStatusResult(false, true, true)); } } + + private static bool IsStrongPassword(string password) + { + return password.Length >= 16 + && password.Any(char.IsUpper) + && password.Any(char.IsLower) + && password.Any(char.IsDigit) + && password.Any(ch => !char.IsLetterOrDigit(ch)); + } } diff --git a/tests/OpenCashFlow.Test/Factories/CustomWebApplicationFactory.cs b/tests/OpenCashFlow.Test/Factories/CustomWebApplicationFactory.cs index afbc8f7..8bd2848 100644 --- a/tests/OpenCashFlow.Test/Factories/CustomWebApplicationFactory.cs +++ b/tests/OpenCashFlow.Test/Factories/CustomWebApplicationFactory.cs @@ -19,11 +19,13 @@ public class CustomWebApplicationFactory : WebApplicationFactory private readonly bool _useFakeAuth; private readonly string _dbIdentifier; private readonly bool _usePostgres; + private readonly bool _seedTestData; - public CustomWebApplicationFactory(string dbIdentifier, bool useFakeAuth = true) + public CustomWebApplicationFactory(string dbIdentifier, bool useFakeAuth = true, bool seedTestData = true) { _dbIdentifier = dbIdentifier; _useFakeAuth = useFakeAuth; + _seedTestData = seedTestData; // If dbIdentifier contains connection string keywords, use PostgreSQL _usePostgres = dbIdentifier.Contains("Host=") || dbIdentifier.Contains("Server="); } @@ -111,7 +113,10 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) db.Database.EnsureCreated(); } - TestHelpers.SeedAllTestData(db); + if (_seedTestData) + { + TestHelpers.SeedAllTestData(db); + } }); } diff --git a/tests/OpenCashFlow.Test/Tests/API/Setup_Tests.cs b/tests/OpenCashFlow.Test/Tests/API/Setup_Tests.cs new file mode 100644 index 0000000..588eb32 --- /dev/null +++ b/tests/OpenCashFlow.Test/Tests/API/Setup_Tests.cs @@ -0,0 +1,116 @@ +using System.Net; +using System.Net.Http.Json; +using Microsoft.EntityFrameworkCore; +using OpenCashFlow.Contracts.Auth; +using OpenCashFlow.Contracts.Core; +using OpenCashFlow.Contracts.DTOs; +using OpenCashFlow.Test.Factories; + +namespace OpenCashFlow.Test.Tests.API; + +[Trait("Layer", "API")] +[Trait("Feature", "Setup")] +[Trait("Type", "Integration")] +public sealed class SetupApiTests : IDisposable +{ + private readonly CustomWebApplicationFactory _factory = new( + $"SetupDb_{Guid.NewGuid():N}", + useFakeAuth: false, + seedTestData: false); + + [Fact] + public async Task Status_WithFreshDatabase_RequiresSetup() + { + var client = _factory.CreateClient(); + + var status = await client.GetFromJsonAsync("/v1/Setup/status"); + + Assert.NotNull(status); + Assert.True(status!.RequiresSetup); + Assert.False(status.HasCompanies); + Assert.False(status.HasAdminUsers); + } + + [Fact] + public async Task Create_WithFreshDatabase_CreatesFirstAdminAndTemporaryPassword() + { + var client = _factory.CreateClient(); + var request = ValidRequest(); + + var response = await client.PostAsJsonAsync("/v1/Setup", request); + + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + var completed = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(completed); + Assert.Equal(request.AdminEmail, completed!.AdminEmail); + Assert.True(IsStrongPassword(completed.TemporaryAdminPassword)); + Assert.False(completed.Status.RequiresSetup); + + using var db = _factory.CreateDbContext(); + var company = await db.Company_DS.AsNoTracking().SingleAsync(); + var admin = await db.AspNetUser_DS.AsNoTracking().SingleAsync(); + var cashBalance = await db.CashBalances.AsNoTracking().SingleAsync(); + + Assert.Equal(request.CompanyName, company.CompanyName); + Assert.Equal(request.AdminEmail, admin.Email); + Assert.True(admin.UserMustChangePassword); + Assert.Equal(company.TenantID, cashBalance.CompanyId); + + var secondResponse = await client.PostAsJsonAsync("/v1/Setup", request); + Assert.Equal(HttpStatusCode.Conflict, secondResponse.StatusCode); + + var loginResponse = await client.PostAsJsonAsync("/v1/Authentication/login", new + { + Username = request.AdminEmail, + Password = completed.TemporaryAdminPassword + }); + + Assert.Equal(HttpStatusCode.OK, loginResponse.StatusCode); + var login = await loginResponse.Content.ReadFromJsonAsync>(); + Assert.NotNull(login?.Data); + Assert.True(login!.Data!.Success); + Assert.True(login.Data.RequiresPasswordChange); + Assert.False(string.IsNullOrWhiteSpace(login.Data.Token)); + } + + [Fact] + public async Task Create_WithInvalidData_ReturnsBadRequest() + { + var client = _factory.CreateClient(); + var request = ValidRequest(); + request.CompanyName = string.Empty; + + var response = await client.PostAsJsonAsync("/v1/Setup", request); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + public void Dispose() + { + _factory.Dispose(); + } + + private static SetupRequest_DTO ValidRequest() + { + return new SetupRequest_DTO + { + CompanyName = "OpenCashFlow Fresh Install", + AdminEmail = $"owner-{Guid.NewGuid():N}@example.local", + AdminFirstName = "Owner", + AdminLastName = "Admin", + Language = "it", + Currency = "EUR", + Timezone = "Europe/Rome", + Country = "IT" + }; + } + + private static bool IsStrongPassword(string password) + { + return password.Length >= 16 + && password.Any(char.IsUpper) + && password.Any(char.IsLower) + && password.Any(char.IsDigit) + && password.Any(ch => !char.IsLetterOrDigit(ch)); + } +} From 9e2b144fea16c99efcdd8f61c82e3c750e3a46d6 Mon Sep 17 00:00:00 2001 From: Codewriter90x Date: Thu, 9 Jul 2026 17:40:31 +0200 Subject: [PATCH 10/10] docs(setup): add first-run setup smoke report --- Docs/setup/first-run-setup-smoke.md | 132 ++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 Docs/setup/first-run-setup-smoke.md diff --git a/Docs/setup/first-run-setup-smoke.md b/Docs/setup/first-run-setup-smoke.md new file mode 100644 index 0000000..6b10981 --- /dev/null +++ b/Docs/setup/first-run-setup-smoke.md @@ -0,0 +1,132 @@ +# First-Run Setup Smoke + +Date: 2026-07-09 + +Branch: `feature/first-run-setup-wizard` + +## Scope + +This smoke validated the first-run setup wizard against a clean Docker Compose stack. + +The smoke used the local Docker Compose services: + +- WebApp: `http://localhost:5200` +- API: `http://localhost:5100` +- PostgreSQL: Docker Compose `db` service with a fresh `opencashflow_pgdata` volume + +No deployment was performed. + +## Commands Used + +Clean stack: + +```bash +docker compose down -v +docker compose up -d --build +``` + +Fast repeat of the WebApp form path after images were built: + +```bash +docker compose down -v +docker compose up -d +``` + +Health and setup status: + +```bash +curl -i http://localhost:5100/health +curl -i http://localhost:5200/ +curl -i http://localhost:5200/Login +curl -i http://localhost:5100/v1/Setup/status +``` + +WebApp setup form: + +```bash +curl -sS -c /private/tmp/ocf-web.cookies \ + -o /private/tmp/ocf-web-setup.html \ + http://localhost:5200/Setup +``` + +The antiforgery token was read from the setup form and posted back to `POST /Setup` with: + +- company name: `Web Smoke Workshop SRL` +- admin email: `web-owner-smoke@example.local` +- admin first name: `Web` +- admin last name: `Owner` +- language: `it` +- currency: `EUR` +- country: `IT` +- timezone: `Europe/Rome` + +The temporary password was captured from the setup completion response and was not written to this document. + +API login and password change: + +```bash +curl -sS -X POST http://localhost:5100/v1/Authentication/login \ + -H "Content-Type: application/json" \ + -d '{"username":"web-owner-smoke@example.local","password":""}' + +curl -sS -i -X POST http://localhost:5100/v1/Authentication/change-password-required \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer " \ + -d '{"newPassword":""}' + +curl -sS -X POST http://localhost:5100/v1/Authentication/login \ + -H "Content-Type: application/json" \ + -d '{"username":"web-owner-smoke@example.local","password":""}' +``` + +Log check: + +```bash +docker logs opencashflow-api +docker logs opencashflow-webapp +``` + +The generated temporary password was searched explicitly in API and WebApp logs and was not found. + +## Results + +| Check | Result | +| --- | --- | +| Clean DB/container state | Passed. `docker compose down -v` removed containers and `opencashflow_pgdata`. | +| Docker Compose startup | Passed. `api`, `webapp`, and `db` started. | +| API health | Passed. `GET /health` returned `{"status":"healthy","database":"ok"}`. | +| WebApp root | Passed. `GET /` returned `302` to `http://localhost:5200/Login`. | +| Unconfigured login redirect | Passed. `GET /Login` returned `302 Location: /Setup`. | +| Setup status before setup | Passed. API returned `requiresSetup=true`, `hasCompanies=false`, `hasAdminUsers=false`. | +| WebApp setup form | Passed. `GET /Setup` returned the first setup page with antiforgery token and no password input fields. | +| Complete setup through WebApp form | Passed. `POST /Setup` returned `200 OK` with the `Setup complete` page. | +| Temporary password shown once | Passed. Password appeared in the setup completion response. After setup, `GET /Setup` returned `302 Location: /Account/Login` and did not replay the password. | +| Login with temporary password | Passed. Login returned `success=true` and `requiresPasswordChange=true`. | +| Change password | Passed. `POST /v1/Authentication/change-password-required` returned `200 OK`. | +| Login with new password | Passed. Login returned `success=true` and `requiresPasswordChange=false`. | +| Setup locked after configuration | Passed. `GET /Setup` redirected to login after configuration. | +| Second setup POST | Passed. `POST /v1/Setup` returned `409 Conflict`. | +| Plaintext generated password in logs | Passed. Exact generated temporary password was not present in API or WebApp logs. | + +## Issues Found + +The API container logs this startup message: + +```text +Cannot load library libgssapi_krb5.so.2 +Error: libgssapi_krb5.so.2: cannot open shared object file: No such file or directory +``` + +The application still started and the health endpoint reported the database as healthy. This should be tracked separately because it creates noisy operational logs and may indicate a missing native package in the runtime image. + +During one repeated invalid-login check immediately after several auth attempts, the auth rate limiter returned `503 Service Unavailable`. That is consistent with rate limiting behavior during smoke repetition, not a setup failure. + +## Screenshots + +No screenshots were captured. The smoke used HTTP checks and saved local response HTML under `/private/tmp` during the run. + +## Remaining Gaps + +- The smoke did not exercise a full browser UI interaction beyond HTTP form submission. +- The setup wizard currently creates the legacy company-level `CashBalance`, not a persisted Cash Custody `CashAccount`; the Cash Custody persistence model is not implemented yet. +- The `libgssapi_krb5.so.2` startup log should be investigated in a separate Docker/runtime hardening task.