Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
4f5a1c5
Add design spec for multi-company support
manana2520 May 24, 2026
8f137a0
Address review feedback (spec v2)
manana2520 May 24, 2026
76eae7f
Archive multi-company spec review trail
manana2520 May 24, 2026
e556a23
Add implementation plan for multi-company support
manana2520 May 24, 2026
1d5c23f
Add test scaffolding for multi-company migration and leak detection
manana2520 May 24, 2026
0397547
T1 review fix: align production-sized test docstring with planned gating
manana2520 May 24, 2026
414636d
Add Company domain struct with validation and sentinel errors
manana2520 May 24, 2026
bce50b5
Add CompanyRepository with create/get/list/update/soft-delete
manana2520 May 24, 2026
614fce8
T3 review fix: align CompanyRepository naming with project convention
manana2520 May 24, 2026
df8768a
Add CompanyService with last-company and in-use delete protection
manana2520 May 24, 2026
fcbf7ee
T4 review fix: audit oldValues + reject zero IDs
manana2520 May 24, 2026
fb4e490
Set up failing migration tests for multi-company
manana2520 May 24, 2026
46fea5b
T5 review fix: TestMain goose setup, FK-off during migrations, richer…
manana2520 May 24, 2026
479c777
Migration 025: create companies table and seed default from settings
manana2520 May 24, 2026
3e7cf4a
T6 review fix: RFC3339 timestamps, ICO presence guard, legal_name com…
manana2520 May 24, 2026
97effe1
Migration 025: partition contacts by company_id
manana2520 May 24, 2026
89f4ec1
T7 review fix: reversible contacts partition + SeedContact takes comp…
manana2520 May 24, 2026
5ab5de1
Migration 025: partition 19 leaf-style per-company tables
manana2520 May 24, 2026
8535c66
Migration 025: rebuild invoice_sequences with UNIQUE per company
manana2520 May 24, 2026
faae52b
Migration 025: partition invoice graph with composite FK on items
manana2520 May 24, 2026
aadcf36
Migration 025: partition expense graph with composite FK on items
manana2520 May 24, 2026
c44d63b
Migration 025: partition VAT filings with composite FK on return lines
manana2520 May 24, 2026
74ffaf0
Migration 025: partition income tax return line tables
manana2520 May 24, 2026
55172c9
Migration 025: partition settings; add nullable company_id to audit_log
manana2520 May 24, 2026
b313de7
Add production-sized migration test (env-gated)
manana2520 May 24, 2026
da9d368
Add WithCompany middleware and typed request-context key
manana2520 May 24, 2026
2f610b9
Add CompanyHandler with full CRUD + status code mapping
manana2520 May 24, 2026
1aeeb49
Audit log: accept optional company_id filter
manana2520 May 24, 2026
02ee17b
Restructure routes into global + per-company tiers
manana2520 May 24, 2026
e42e1b6
Thread companyID through contacts/categories/sequences
manana2520 May 24, 2026
bf6d70c
Thread companyID through invoice/expense verticals
manana2520 May 24, 2026
bdabd2f
Thread companyID through tax/VAT/investment and populate end-to-end test
manana2520 May 24, 2026
2aaeb4b
Frontend: add Company type and client.companies.* methods
manana2520 May 24, 2026
faa5a64
Frontend: currentCompany store with localStorage persistence
manana2520 May 24, 2026
404168c
Frontend: CompanyHeader component with dropdown switcher
manana2520 May 24, 2026
f74bc19
Frontend: thread currentCompany through every per-company API method
manana2520 May 24, 2026
729a842
Frontend: root layout bootstraps companies and mounts the switcher
manana2520 May 24, 2026
e5fd244
Frontend: companies management routes (list, new, edit)
manana2520 May 24, 2026
cfc37ee
Frontend: list/detail pages refetch on currentCompany change
manana2520 May 24, 2026
2cdda59
Add multi-company manual smoke test checklist
manana2520 May 24, 2026
2b7513e
Document multi-company support and upgrade path
manana2520 May 24, 2026
792cb67
Fix /companies/{id} CRUD routes shadowed by per-company subrouter
manana2520 May 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,17 @@ cd frontend && npm test
make coverage-frontend
```

The production-sized migration test (`TestMultiCompanyMigrationProductionSized`)
seeds ~22k rows into a v024 fixture and runs migration 025 end-to-end. It is
gated behind the env var `ZFAKTURY_RUN_BIG_MIGRATION_TEST=1` so CI runs stay
fast; enable it locally before merging schema changes that touch existing
migrations:

```bash
ZFAKTURY_RUN_BIG_MIGRATION_TEST=1 CGO_ENABLED=0 go test ./internal/database \
-run TestMultiCompanyMigrationProductionSized -v -timeout 90s
```

### Coverage

Go backend test coverage is enforced at **80% minimum** via a pre-commit hook. Run `make install-hooks` after cloning to enable it.
Expand Down Expand Up @@ -245,6 +256,33 @@ Without the `[ocr]` section (or with an empty `api_key`), the import still works

Maximum file size: 20 MB. Up to 10 documents per expense.

## Multi-Company Support

zfaktury can manage multiple legal entities — for example, an OSVČ
and an s.r.o. owned by the same person — from a single install.

The active company is selectable from the header dropdown and
persisted across sessions. Each company has its own contacts,
invoices, expenses, tax filings, and sequences. Switching is
instantaneous (no reload).

When upgrading from a single-company install, the database
migration creates a default company from your existing settings
(name, IČO, DIČ, bank details, etc.) and backfills every existing
record into it. You can rename it or add more companies after
the first launch.

To run the production-sized migration test locally before merging
schema changes:

```bash
ZFAKTURY_RUN_BIG_MIGRATION_TEST=1 CGO_ENABLED=0 go test ./internal/database -run TestMultiCompanyMigrationProductionSized -v
```

See [docs/UPGRADING.md](docs/UPGRADING.md) for upgrade notes,
including the known tax-year-tables limitation and the destructive
downgrade caveat.

## License

Private project.
48 changes: 48 additions & 0 deletions docs/UPGRADING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Upgrading to multi-company (v0.0.51 and later)

Migration `025_multi_company.sql` is the first multi-company migration.
It rewrites the database in-place — back up your `~/.zfaktury/zfaktury.db`
before upgrading.

## What happens

1. A `companies` table is created.
2. A default company (id = 1) is created from your existing settings
(company_name, ico, dic, address, bank details, etc.).
3. Every per-company table (contacts, invoices, expenses, tax filings,
etc.) gains a `company_id` column, backfilled to 1.
4. Composite foreign keys are added to invoice_items, expense_items,
recurring_invoice_items, vat_return_invoices, and vat_return_expenses
for physical cross-company isolation.

## Known limitation: tax-year tables

The following tables retain their original `UNIQUE(year)` constraint
from migrations 015-017 and are not yet partitioned for multi-company
UPSERT collisions:

- `tax_year_settings`
- `tax_prepayments`
- `tax_spouse_credits`
- `tax_child_credits`
- `tax_personal_credits`
- `tax_deductions`

Reads from these tables ARE properly scoped by `company_id`, so cross-
company leakage is impossible. The limitation only manifests if two
companies try to UPSERT the same year row simultaneously. A follow-up
migration will extend the unique constraints to `(company_id, year)`.

## Downgrade is destructive

The `Down` migration restores the 17 identity keys from id = 1 only.
Any data you created in companies other than the first will be lost
on downgrade. Back up your database before testing the downgrade
path.

## Renaming the default company

After upgrade, visit `/companies/[id]` (clickable from the header
dropdown's "Spravovat firmy →" link) to rename or fill in additional
fields. You can add more companies from "+ Přidat firmu" — IČO
lookup against ARES auto-fills most fields.
48 changes: 48 additions & 0 deletions docs/superpowers/manual-tests/multi-company.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Multi-Company Manual Smoke Test

Run before merging the multi-company branch. Exercises the full feature
end-to-end through the UI.

## Setup

1. Fresh DB: `rm -f ~/.zfaktury/zfaktury.db` (or set `ZFAKTURY_DATA_DIR=/tmp/zft-smoke` to a fresh path)
2. `make dev` (Vite + Go server)
3. Open http://localhost:5173

## Checklist

- [ ] **Empty state**: app routes to `/companies/new` (no companies exist yet)
- [ ] **Create first company**: enter IČO `12345678` (or any valid 8-digit), click "Načíst z ARES" — name/address pre-fill from ARES. Save. App routes to `/` dashboard with "Manas OSVČ" (or whatever ARES returned) shown in the header.
- [ ] **Add a contact**: go to `/contacts/new`, create "Keboola s.r.o." (ICO 25620916). Verify it appears in the contacts list.
- [ ] **Add an invoice**: go to `/invoices/new`, customer = the contact, add one line item. Save. Verify it appears in `/invoices` with status "Vystaveno".
- [ ] **Add second company**: click the dropdown header → "+ Přidat firmu". Create "Test s.r.o." with a different IČO. The dropdown should now list both companies.
- [ ] **Switch via dropdown**: click "Test s.r.o." in the dropdown. Verify:
- The header label changes to "Test s.r.o."
- The contacts list becomes empty (different company)
- The invoice list becomes empty
- The dashboard totals change to 0
- [ ] **Create contact in s.r.o.**: add "Acme spol. s r.o." (only visible while s.r.o. is active)
- [ ] **Sequence collision check**: in s.r.o., create an invoice. The next invoice number should be FV20260001 (NOT FV20260002) — each company has its own sequence.
- [ ] **Switch back to OSVČ**: original contact and invoice should reappear.
- [ ] **Cross-company contact rejection**: in OSVČ, try to create an invoice via the API directly using s.r.o.'s contact ID:
```bash
curl -sS -X POST http://localhost:5173/api/v1/companies/1/invoices \
-H 'content-type: application/json' \
-d '{"customer_id": <s.r.o.-contact-id>, ...}'
```
Should return 4xx with a not-found-style error (the contact doesn't belong to OSVČ).
- [ ] **In-use delete protection**: from `/companies`, click "Smazat" on OSVČ (which has an invoice). Should error with "cannot delete: still in use".
- [ ] **Last-company delete protection**: soft-delete all data in s.r.o. (or just remember it's the only OTHER company). Delete s.r.o. successfully. Then try to delete OSVČ — should error with "cannot delete the last company".
- [ ] **Reload persistence**: after switching to OSVČ, hit browser refresh. The header should still show OSVČ (localStorage restored).

## What to look for

- Header dropdown closes when clicking outside
- Switching companies shows a brief loading state (no flicker of stale data)
- Error messages are in Czech
- Forms validate before submitting
- `X-Company-Id` response header is present on every per-company response (open browser devtools Network panel)

## If anything fails

Document in the PR description as a regression to address before merge.
Loading