Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
37f4a72
Add configurable HTTP bind host for containerized deployments
manana2520 May 24, 2026
1926aef
Add Dockerfile and .dockerignore for containerized deployment
manana2520 May 24, 2026
c695ec6
Bind-mount config.toml from the host instead of baking it in
manana2520 May 24, 2026
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
5d3abbe
Merge feature/docker-support into main
manana2520 May 26, 2026
e066712
Merge feature/multi-company into main
manana2520 May 26, 2026
8445174
Add design spec for flexible invoice sequence format
manana2520 May 26, 2026
9c6f3b8
Spec: builder exposes two separators (prefix-year and year-number)
manana2520 May 26, 2026
6d5e3f9
Spec: drop builder, use single template field with live preview
manana2520 May 26, 2026
e09ced0
Spec: address review feedback (token aliases, overflow, parity fixtures)
manana2520 May 26, 2026
c934a71
Add implementation plan for flexible invoice sequence format
manana2520 May 26, 2026
dddb0e2
Add format package for invoice number template rendering
manana2520 May 26, 2026
afd5db6
format: call ValidatePattern guard in Render
manana2520 May 26, 2026
d15ce1b
Wire format renderer into SequenceService
manana2520 May 26, 2026
5841f29
Render invoice numbers through format package in GetNextNumber
manana2520 May 26, 2026
e8fd6ab
Add TypeScript port of sequence format renderer
manana2520 May 26, 2026
03bb4e1
Live preview and pattern editing on sequences settings page
manana2520 May 26, 2026
3e8d0a3
Use TS JSON import for fixture instead of node:fs
manana2520 May 26, 2026
c2be6c8
Tests: live preview, invalid pattern, mid-year edit warning
manana2520 May 26, 2026
eab65da
Refresh Czech help content for invoice number format
manana2520 May 26, 2026
b3c5933
Address code review: empty-pattern guard on Update, drop dead export,…
manana2520 May 26, 2026
2f27d05
Merge pull request #1 from Maziak2520/feature/flexible-sequence-format
Maziak2520 May 27, 2026
7ca8e86
Allow short years (e.g. 26) in sequences create form
manana2520 May 27, 2026
dfb911e
Merge pull request #2 from Maziak2520/fix/sequences-year-validation
Maziak2520 May 27, 2026
fb2c986
Remove deleted sequence from local state immediately
manana2520 May 27, 2026
d4da2ab
Merge pull request #3 from Maziak2520/fix/sequence-delete-ui-refresh
Maziak2520 May 27, 2026
6159af1
Use partial unique index on invoice_sequences
manana2520 May 27, 2026
4a6fe52
Merge pull request #4 from Maziak2520/fix/sequences-partial-unique-index
Maziak2520 May 27, 2026
5112a9f
Unify identity source between firma and companies
manana2520 May 27, 2026
f418e73
Merge pull request #5 from Maziak2520/feature/firma-companies-unifica…
Maziak2520 May 27, 2026
df9261b
Add sequence picker to new-invoice form
manana2520 May 28, 2026
deff264
Merge pull request #6 from Maziak2520/feature/invoice-sequence-picker
Maziak2520 May 28, 2026
29c4d9b
Scope Dashboard, Reports and Audit-log to active company
manana2520 May 28, 2026
276f09f
Merge pull request #7 from Maziak2520/fix/multi-company-scoping-v2
Maziak2520 May 28, 2026
e803eb4
Mark draft invoices as sent after email + populate audit company_id
manana2520 May 28, 2026
90d3d83
Merge pull request #8 from Maziak2520/fix/email-sent-status-and-audit…
Maziak2520 May 28, 2026
81fb5ad
chore: fix CI baseline (prettier, gofmt, errcheck, pin golangci-lint)
manana2520 Jun 1, 2026
7629962
Make ProcessDue_DeactivatesAfterLastCycle time-relative
manana2520 Jun 1, 2026
e2fbffd
Merge pull request #10 from Maziak2520/chore/format-and-lint-baseline
Maziak2520 Jun 1, 2026
89ec1ac
Add recurring-invoice scheduler with optional auto-email
manana2520 Jun 1, 2026
18158ca
Fix lint/format issues surfaced by CI
manana2520 Jun 1, 2026
6757435
Merge pull request #9 from Maziak2520/feature/recurring-auto-send
Maziak2520 Jun 1, 2026
180b950
Let recurring invoices use a chosen invoice-number sequence
manana2520 Jun 1, 2026
e41d9a9
Stop tracking transient .claude/scheduled_tasks.lock
manana2520 Jun 1, 2026
4e041af
Merge pull request #11 from Maziak2520/feature/recurring-sequence-id
Maziak2520 Jun 1, 2026
cd7c908
Auto-send via an unsent-draft sweep (decoupled from generation)
manana2520 Jun 1, 2026
a44e419
Merge pull request #12 from Maziak2520/feature/recurring-autosend-sweep
Maziak2520 Jun 1, 2026
912a7cf
Default new recurring invoices to auto-send
manana2520 Jun 1, 2026
4842e08
Merge pull request #13 from Maziak2520/feature/recurring-autosend-def…
Maziak2520 Jun 1, 2026
e4d2d6a
Fix month-end recurring dates skipping a month
manana2520 Jun 1, 2026
16faf03
Merge pull request #14 from Maziak2520/fix/recurring-month-end-stepping
Maziak2520 Jun 1, 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
  •  
  •  
  •  
56 changes: 56 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# VCS
.git
.github
.gitignore

# Editor / agent metadata
.claude
.agents
.audit
.idea
.vscode
.envrc
.direnv

# Built binaries
/zfaktury
/zfaktury-server
*.exe
*.dll
*.so
*.dylib

# Frontend artifacts (re-built inside the image)
frontend/node_modules
frontend/build
frontend/.svelte-kit
frontend/coverage

# Embedded frontend output (re-generated inside the image; placeholder index.html stays)
web/frontend/build/_app
web/frontend/build/favicon.png

# Go test/coverage
*.test
*.out
coverage.html
/vendor

# Local data and runtime
*.db
*.db-shm
*.db-wal
logs
tmp
config.dev.toml
# Live deployment config (must NEVER be baked into the image — mounted at runtime)
/config.toml
.env
.env.*

# Docs and reports
docs/AGENT-REPORTS

# Nix / Rust sibling project (not needed by Go server build)
shell.nix
rust
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v7
with:
version: latest
# Pinned to avoid CI breaking when a newer linter version adds rules.
version: v2.12.2
args: --build-tags server

- name: Test
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ frontend/coverage/

# Local config
config.dev.toml
# Live deployment config (template: config.toml.example)
/config.toml

# Nix
.envrc
Expand All @@ -55,3 +57,6 @@ web/frontend/build/favicon.png
# Claude
docs/AGENT-REPORTS
.claude/worktrees/

# Claude Code transient scheduler lock
.claude/scheduled_tasks.lock
49 changes: 49 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# syntax=docker/dockerfile:1.7

# Build the SvelteKit frontend
FROM node:22-alpine AS frontend
WORKDIR /app/frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci --no-audit --no-fund
COPY frontend/ ./
RUN npm run build

# Build the Go server binary (CGO disabled, server build tag)
FROM golang:1.25-alpine AS backend
WORKDIR /src
RUN apk add --no-cache git ca-certificates
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Frontend output must live at web/frontend/build for //go:embed all:frontend/build (relative to web/)
RUN rm -rf web/frontend/build
COPY --from=frontend /app/frontend/build/ ./web/frontend/build/

ARG VERSION=docker
ARG COMMIT=unknown
ARG DATE=unknown
ENV CGO_ENABLED=0
RUN go build -tags server -trimpath \
-ldflags="-s -w \
-X github.com/zajca/zfaktury/internal/version.Version=${VERSION} \
-X github.com/zajca/zfaktury/internal/version.Commit=${COMMIT} \
-X github.com/zajca/zfaktury/internal/version.Date=${DATE}" \
-o /out/zfaktury-server ./cmd/zfaktury

# Runtime image: small Alpine with ca-certificates + timezone data
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata wget \
&& addgroup -g 1000 zfaktury \
&& adduser -u 1000 -G zfaktury -S -h /data -D zfaktury \
&& mkdir -p /data && chown -R 1000:1000 /data
COPY --from=backend --chown=1000:1000 /out/zfaktury-server /usr/local/bin/zfaktury-server
USER 1000:1000
ENV ZFAKTURY_DATA_DIR=/data \
TZ=Europe/Prague
WORKDIR /data
VOLUME ["/data"]
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -qO- http://127.0.0.1:8080/health >/dev/null 2>&1 || exit 1
ENTRYPOINT ["/usr/local/bin/zfaktury-server"]
CMD ["serve", "--host", "0.0.0.0", "--port", "8080"]
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,37 @@ make clean # Remove build artifacts
make install-hooks # Install git pre-commit hook
```

## Docker

A multi-stage `Dockerfile` is included for the server-only (headless) build:
node22 → golang1.25 → alpine, producing a ~33 MB image with no CGO and the
SvelteKit frontend embedded in the binary.

```bash
# Build the image
docker build -t zfaktury:latest .

# Create your runtime config (gitignored, never baked into the image)
cp config.toml.example config.toml
$EDITOR config.toml

# Run it
docker run -d --name zfaktury --restart unless-stopped \
-p 8080:8080 \
-v "$PWD/data:/data" \
-v "$PWD/config.toml:/data/.zfaktury/config.toml:ro" \
zfaktury:latest
```

By default the container binds `0.0.0.0:8080` (the `serve --host` flag is
configurable; the desktop build still defaults to `127.0.0.1`). The SQLite
database, uploaded documents, and backups live in the mounted `/data`
directory.

See `deploy/docker-compose.yml` for an equivalent Compose setup, and
`config.toml.example` for the full set of configurable sections (SMTP, FIO,
OCR, S3 backups).

## Testing

```bash
Expand All @@ -110,6 +141,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 +287,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.
4 changes: 4 additions & 0 deletions config.dev.dist.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@
host = "localhost"
port = 1025
from = "dev@zfaktury.local"

# [scheduler]
# enabled = false # Generate due recurring invoices automatically once a day
# hour = 7 # Local-time hour (0-23) of the daily run
69 changes: 69 additions & 0 deletions config.toml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# ZFaktury runtime configuration template.
#
# Copy this file to `config.toml` at the repository root and fill in the
# sections you actually use. `config.toml` is gitignored and dockerignored,
# so the secrets it contains never enter the image or version control.
#
# Apply changes by restarting the container:
# docker restart zfaktury

# --- HTTP server -------------------------------------------------------------
# When running in a container the CMD already passes --host 0.0.0.0, so this
# section can stay commented. Uncomment only to override.
# [server]
# host = "0.0.0.0"
# port = 8080

# --- Database (defaults are fine) -------------------------------------------
# [database]
# path = "" # Custom SQLite path (default: <data_dir>/zfaktury.db)
# journal_mode = "wal" # wal (default) or delete

# --- Logging ----------------------------------------------------------------
# [log]
# path = "" # Empty = stderr only
# level = "info" # debug, info, warn, error

# --- Email sending (invoices, reminders) ------------------------------------
# Gmail/M365 require an "app password", not your account password.
# [smtp]
# host = "smtp.seznam.cz" # or smtp.gmail.com, smtp.office365.com
# port = 587 # 587 STARTTLS (usual) or 465 implicit TLS
# username = "you@example.com"
# password = "app-password-here"
# from = "you@example.com"

# --- FIO Bank API import (Czech) --------------------------------------------
# Generate the token in FIO Internetbanking -> Nastaveni -> API.
# [fio]
# api_token = ""

# --- Backups ----------------------------------------------------------------
# [backup]
# destination = "" # Local fallback dir (default: <data_dir>/backups)
# schedule = "0 2 * * *" # Cron expression; empty disables scheduled backups
# retention_count = 14 # Keep last N; 0 = keep all
#
# Off-site copy to S3-compatible storage (AWS S3, MinIO, Backblaze B2, Cloudflare R2).
# [backup.s3]
# endpoint = "s3.eu-central-1.amazonaws.com"
# region = "eu-central-1"
# bucket = ""
# access_key = ""
# secret_key = ""
# use_ssl = true

# --- OCR for expense document extraction ------------------------------------
# Pick ONE provider. Leave api_key empty to disable OCR entirely.
#
# Defaults per provider:
# openai -> gpt-4o
# claude -> claude-sonnet-4-20250514
# gemini -> gemini-2.0-flash
# openrouter -> google/gemini-2.0-flash-001
# mistral -> pixtral-large-latest
# [ocr]
# provider = "openai" # openai | openrouter | gemini | mistral | claude
# api_key = ""
# model = "" # Optional override of the per-provider default
# base_url = "" # Optional, for proxies / self-hosted endpoints
43 changes: 43 additions & 0 deletions deploy/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Example docker-compose.yml for running zfaktury in a container.
#
# This file is a starting point — copy it to your deployment directory
# (outside the repo) and adjust host paths, the published port, and
# the `user:` UID/GID to your environment.
#
# Pair it with a `config.toml` at the repository root (gitignored).
# Edit that file in your editor, then `docker compose restart` to apply.

name: zfaktury

services:
zfaktury:
build:
context: ..
dockerfile: Dockerfile
args:
VERSION: docker-local
image: zfaktury:latest
container_name: zfaktury
hostname: zfaktury
restart: unless-stopped

# Override the image's default 1000:1000 if your host data directory
# is owned by a different UID/GID (common on NAS systems).
# user: "1024:100"

ports:
- "8080:8080"

environment:
- TZ=Europe/Prague
- ZFAKTURY_DATA_DIR=/data

volumes:
# Persistent state: SQLite DB, uploaded documents, backups, lock file.
# Point this at any host directory you want to keep across rebuilds.
- ./data:/data

# Live configuration. Edit the host file in your editor and
# `docker compose restart zfaktury` to apply changes.
# The :ro suffix makes the mount read-only inside the container.
- ../config.toml:/data/.zfaktury/config.toml:ro
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.
Loading