This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Using Makefile (recommended)
make build # Build the application
make test # Run all tests
make test-race # Run tests with race detector
make coverage # Generate HTML coverage report
make lint # Run golangci-lint
make security # Run gosec security scan
make sbom # Generate SBOM (CycloneDX format)
make all # Lint, test, and build
make install-tools # Install dev tools (golangci-lint, gosec, cyclonedx-gomod)
# Direct commands (for specific cases)
go test -v -run TestFunctionName ./path/to/package # Single test
go test -v -coverprofile=coverage.txt ./... # Coverage profile- Module:
fjacquet/camt-csv, Go 1.24+ - PDF support requires
poppler-utils(pdftotextCLI)
This is a Go CLI tool that converts financial statement formats (CAMT.053 XML, PDF, Revolut CSV, Selma CSV) into standardized CSV with AI-powered categorization.
The camt parser handles CAMT.053 (Bank to Customer Statement) files:
- Namespace:
urn:iso:std:iso:20022:tech:xsd:camt.053.001.02 - Standard: ISO 20022
- Structure defined in:
internal/models/iso20022.go
Supported CAMT Types:
graph TD
A["<b>CAMT Types</b>"]
B["<b>CAMT.052</b><br/>Bank to Customer Account Report<br/>❌ No"]
C["<b>CAMT.053</b><br/>Bank to Customer Statement<br/>✓ Yes v001.02"]
D["<b>CAMT.054</b><br/>Bank to Customer Debit/Credit Notification<br/>❌ No"]
A --> B
A --> C
A --> D
Known Limitations:
- Only version 001.02 tested (newer versions may have additional fields)
- No strict namespace validation (will attempt to parse any XML with matching structure)
- Swiss bank-specific extensions may not be fully supported
Parser Factory Pattern: Parsers implement segregated interfaces in internal/parser/parser.go:
type Parser interface {
Parse(r io.Reader) ([]Transaction, error)
}
type Validator interface {
ValidateFormat(filePath string) (bool, error)
}
type CSVConverter interface {
ConvertToCSV(inputFile, outputFile string) error
}
type LoggerConfigurable interface {
SetLogger(logger logging.Logger)
}
type CategorizerConfigurable interface {
SetCategorizer(categorizer models.TransactionCategorizer)
}
type BatchConverter interface {
BatchConvert(inputDir, outputDir string) (int, error)
}
// FullParser combines all capabilities
type FullParser interface {
Parser
Validator
CSVConverter
LoggerConfigurable
CategorizerConfigurable
BatchConverter
}New parsers are registered in internal/factory/factory.go. Important: CLI commands should get parsers from the DI Container (root.GetContainer().GetParser()), not directly from the factory, to ensure categorizers are properly wired.
Four-Tier Categorization (internal/categorizer/):
- Direct mapping - exact match from
database/creditors.yaml/database/debitors.yaml - Keyword matching - rules from
database/categories.yaml - Semantic search - vector embedding similarity via Gemini embeddings (
semantic_strategy.go) - AI fallback - Gemini API via
AIClientinterface (testable abstraction)
When --auto-learn is enabled, AI results save directly to YAML files. When disabled (default), suggestions go to staging files (database/staging_creditors.yaml, database/staging_debtors.yaml) for manual review.
Output Formatter Registry (internal/formatter/formatter.go):
- "standard" - 29-column, comma-delimited (backward-compatible)
- "icompta" - 10-column, semicolon-delimited, dd.MM.yyyy dates
CLI usage: --format standard or --format icompta. New formatters: implement OutputFormatter interface, register via registry.Register("name", formatter).
Command Lifecycle (Cobra hooks in cmd/root/root.go):
PersistentPreRun- Loads config, creates DI container- Command
RunE- Gets parser viaroot.GetContainer().GetParser(type) PersistentPostRun- Saves learned creditor/debtor mappings to YAML
cmd/- Cobra CLI commands (camt, pdf, batch, categorize, revolut, revolut-crypto, selma, debit, revolut-investment)internal/- Core application logic:*parser/packages - Format-specific parsers withadapter.goimplementing the interfacecategorizer/- Transaction categorization with AI integrationmodels/- Core data structures (Transaction,Category,Parserinterface)config/- Viper-based hierarchical configurationstore/- YAML category database managementcommon/- Shared CSV utilities
database/- YAML configuration files for categorization rules
Configuration loads in order (later overrides earlier):
- Config file:
~/.camt-csv/camt-csv.yamlor.camt-csv/config.yaml - Environment variables (see mapping below)
- CLI flags:
--log-level,--ai-enabled, etc.
Environment Variable Mapping:
graph LR
A["<b>Config Key</b>"]
B["log.level<br/>→ CAMT_LOG_LEVEL<br/>→ --log-level"]
C["ai.enabled<br/>→ CAMT_AI_ENABLED<br/>→ --ai-enabled"]
D["ai.model<br/>→ CAMT_AI_MODEL"]
E["ai.api_key<br/>→ GEMINI_API_KEY"]
A --> B
A --> C
A --> D
A --> E
Note: The .env file is auto-loaded from the current directory.
- Use
t.TempDir()for file system tests - Set
TEST_MODE=trueto disable real AI API calls - Use
SetTestCategoryStore()to inject mock stores in categorizer tests - Each parser has
_test.gowith table-driven tests
- Create package in
internal/{name}parser/ - Implement core parsing in
{name}parser.go - Create adapter implementing
parser.FullParserinadapter.go - Register in
internal/factory/factory.go - Add CLI command in
cmd/{name}/convert.go - Wire command in
main.go
Detailed patterns and examples: See
.claude/skills/golang-expert/for comprehensive Go patterns including functional programming, interface design, testing, concurrency, error handling, and performance optimization.
- KISS - Prefer the simplest solution. No abstraction until needed (Rule of Three).
- DRY - Single source of truth. Extract after 3 repetitions.
- No Global Mutable State - Use dependency injection via
Container. - Immutability - Private fields with getters, return new values.
- Pure Functions - Same input = same output, no side effects.
- Interface Segregation - Small, focused interfaces composed when needed.
All dependencies flow through the Container (internal/container/):
container, err := container.NewContainer(cfg)
logger := container.GetLogger()
parser, _ := container.GetParser(container.CAMT)
categorizer := container.GetCategorizer()IMPORTANT: Update CHANGELOG.md for every significant change.
Update the changelog when you:
- Add new features or commands
- Fix bugs
- Make breaking changes
- Change configuration options
- Modify public APIs or interfaces
- Add/remove dependencies
- Make security-related changes
-
Add entries under
## [Unreleased]section -
Use the appropriate category:
- Added - new features
- Changed - changes in existing functionality
- Deprecated - soon-to-be removed features
- Removed - removed features
- Fixed - bug fixes
- Security - vulnerability fixes
-
Write entries in imperative mood: "Add feature" not "Added feature"
-
Reference issues/PRs when relevant: "Fix parsing error (#123)"
When creating a release:
- Change
## [Unreleased]to## [X.Y.Z] - YYYY-MM-DD - Add new empty
## [Unreleased]section above - Update comparison links at bottom of file
- Follow semver: breaking=major, features=minor, fixes=patch
- OpenRouter model IDs have NO
openrouter/prefix: usemistralai/mistral-small-2603notopenrouter/mistralai/... - Default
ai.requests_per_minute: 5is correct for personal finance batch workloads (cost control) cleanCategoryhandles verbose AI responses: multi-line (takes last line) and**bold**anywhere in string
- Parser-internal categories (e.g., Selma investment types) are preserved —
categorization_helperskips external categorizer whentx.Categoryis already set and non-empty - Selma
tradetransactions:PartyName = Fund ISIN, Description =Buy/Sell <ISIN>
- Never use
math/rand— Semgrep blocks it (CWE-338); usetime.Now().UnixNano()for non-security jitter
- Config error message strings in tests go stale after refactors — use
assert.Containswith short substrings