Developer Guide¶
Table of Contents¶
- Getting Started
- Development Environment
- Architecture Overview
- Adding New Parsers
- Adding Categorization Strategies
- Testing Guidelines
- Code Quality Standards
- Debugging and Troubleshooting
- Performance Considerations
- Contributing Guidelines
Getting Started¶
Prerequisites¶
- Go 1.24.2 or higher: Download Go
- Git: For version control
- pdftotext: For PDF processing (
brew install poppleron macOS) - golangci-lint: For code quality checks
- IDE/Editor: VS Code, GoLand, or similar with Go support
Initial Setup¶
# Clone the repository
git clone https://github.com/fjacquet/camt-csv.git
cd camt-csv
# Install dependencies
go mod download
go mod tidy
# Build the application
go build -o camt-csv
# Run tests to verify setup
go test ./...
# Install development tools
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
Project Structure¶
camt-csv/
├── cmd/ # CLI command implementations
│ ├── root/ # Root cobra command
│ ├── camt/ # CAMT.053 XML conversion
│ ├── pdf/ # PDF conversion
│ └── ... # Other format commands
├── internal/ # Private application code
│ ├── models/ # Core data structures
│ ├── parser/ # Parser interfaces and base
│ ├── categorizer/ # Transaction categorization
│ ├── logging/ # Logging abstraction
│ ├── container/ # Dependency injection
│ └── ... # Format-specific parsers
├── database/ # Configuration YAML files
├── docs/ # Documentation
├── samples/ # Sample input files
└── main.go # Application entry point
Development Environment¶
IDE Configuration¶
VS Code Settings (.vscode/settings.json):
{
"go.lintTool": "golangci-lint",
"go.lintOnSave": "package",
"go.testFlags": ["-v"],
"go.testTimeout": "30s",
"editor.formatOnSave": true,
"go.formatTool": "goimports"
}
Recommended Extensions: - Go (Google) - Go Test Explorer - YAML (Red Hat) - Markdown All in One
Development Commands¶
# Format code
go fmt ./...
goimports -w .
# Run linters
golangci-lint run
# Run tests with coverage
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
# Build with debug info
go build -gcflags="all=-N -l" -o camt-csv-debug
# Run specific tests
go test -run TestParserName ./internal/camtparser/
go test -v ./internal/categorizer/ -run TestCategorizer
# Benchmark tests
go test -bench=. ./internal/categorizer/
Architecture Overview¶
Dependency Injection Pattern¶
The application uses dependency injection to eliminate global state and improve testability:
// Container manages all dependencies
type Container struct {
Logger logging.Logger
Config *config.Config
Store *store.CategoryStore
Categorizer *categorizer.Categorizer
Parsers map[parser.ParserType]parser.FullParser
}
// All components receive dependencies through constructors
func NewMyParser(logger logging.Logger) *MyParser {
return &MyParser{
BaseParser: parser.NewBaseParser(logger),
}
}
// Usage example with container
func main() {
// Load configuration
cfg, err := config.Load()
if err != nil {
log.Fatal(err)
}
// Create container with all dependencies
container, err := container.NewContainer(cfg)
if err != nil {
log.Fatal(err)
}
defer container.Close()
// Use dependencies from container
parser, err := container.GetParser(container.CAMT)
if err != nil {
log.Fatal(err)
}
// Process files
transactions, err := parser.Parse(ctx, inputReader)
if err != nil {
container.GetLogger().Error("Parse failed", logging.Field{Key: "error", Value: err})
return
}
// Categorize transactions
cat := container.GetCategorizer()
for i, tx := range transactions {
category, err := cat.Categorize(ctx, tx.GetCounterparty(), tx.IsDebit(),
tx.Amount.String(), tx.Date.String(), tx.Description)
if err != nil {
container.GetLogger().Warn("Categorization failed",
logging.Field{Key: "transaction", Value: tx.Number},
logging.Field{Key: "error", Value: err})
continue
}
transactions[i].Category = category.Name
}
}
Interface Segregation¶
Parsers implement only the interfaces they need:
type Parser interface {
Parse(ctx context.Context, r io.Reader) ([]models.Transaction, error)
}
type Validator interface {
ValidateFormat(filePath string) (bool, error)
}
type CSVConverter interface {
ConvertToCSV(ctx context.Context, inputFile, outputFile string) error
}
type BatchConverter interface {
BatchConvert(ctx context.Context, inputDir, outputDir string) (int, error)
}
type FullParser interface {
Parser
Validator
CSVConverter
LoggerConfigurable
CategorizerConfigurable
BatchConverter
}
BaseParser Foundation¶
All parsers embed BaseParser for common functionality:
type MyParser struct {
parser.BaseParser // Provides logging and CSV writing
// parser-specific fields
}
Transaction Model and Backward Compatibility¶
Core Transaction Structure¶
The models.Transaction struct is the central data structure representing financial transactions:
type Transaction struct {
// Core fields
Date time.Time `csv:"Date"`
ValueDate time.Time `csv:"ValueDate"`
Amount decimal.Decimal `csv:"Amount"`
Currency string `csv:"Currency"`
Description string `csv:"Description"`
// Party information
Payer string `csv:"-"` // Internal field
Payee string `csv:"-"` // Internal field
PartyName string `csv:"PartyName"`
PartyIBAN string `csv:"PartyIBAN"`
// Transaction direction
CreditDebit string `csv:"CreditDebit"`
DebitFlag bool `csv:"IsDebit"`
// Additional fields...
}
Party Access Methods¶
Use GetCounterparty() to get the other party in a transaction:
// Returns the "other party" based on transaction direction
counterparty := tx.GetCounterparty()
// For debit: returns payee (who receives money)
// For credit: returns payer (who sent money to us)
For direct field access when you know the direction: tx.Payer and tx.Payee.
v2.0.0 Breaking Change:
GetPayee(),GetPayer(),GetAmountAsFloat(),SetPayerInfo(),SetPayeeInfo(),SetAmountFromFloat(), andToBuilder()were removed. UseGetCounterparty(),GetAmountAsDecimal(), and theTransactionBuilderpattern instead.
TransactionBuilder Pattern¶
For creating new transactions, use the builder pattern:
tx, err := models.NewTransactionBuilder().
WithDate("2025-01-15").
WithAmountFromFloat(100.50, "CHF").
WithDescription("Payment to supplier").
WithPayer("John Doe", "CH1234567890").
WithPayee("Acme Corp", "CH0987654321").
WithCategory("Business Expenses").
AsDebit().
Build()
if err != nil {
return fmt.Errorf("failed to build transaction: %w", err)
}
Migration Guidelines¶
Legacy Code (Still Works):
// These methods continue to work with enhanced logic
payee := tx.GetPayee()
payer := tx.GetPayer()
amount := tx.GetAmountAsFloat() // Deprecated but functional
Modern Code (Recommended):
// Direct field access for clarity
payee := tx.Payee
payer := tx.Payer
amount := tx.GetAmountAsDecimal() // Precise decimal arithmetic
// Or use counterparty for "other party" logic
counterparty := tx.GetCounterparty()
// Use builder for new transactions
tx, err := models.NewTransactionBuilder().
// ... builder methods
Build()
Conversion Between Formats¶
The Transaction model supports conversion to/from the new decomposed structure:
// Convert to new format
core := tx.ToTransactionCore()
withParties := tx.ToTransactionWithParties()
categorized := tx.ToCategorizedTransaction()
// Convert from new format
var tx models.Transaction
tx.FromCategorizedTransaction(categorized)
Adding New Parsers¶
Step-by-Step Guide¶
1. Create Parser Package¶
2. Define Parser Structure¶
File: internal/myformatparser/myformatparser.go
package myformatparser
import (
"io"
"github.com/fjacquet/camt-csv/internal/logging"
"github.com/fjacquet/camt-csv/internal/models"
"github.com/fjacquet/camt-csv/internal/parser"
"github.com/fjacquet/camt-csv/internal/parsererror"
)
// MyFormatParser handles parsing of MyFormat files
type MyFormatParser struct {
parser.BaseParser
// Add parser-specific fields here
}
// NewMyFormatParser creates a new MyFormat parser with dependency injection
func NewMyFormatParser(logger logging.Logger) *MyFormatParser {
return &MyFormatParser{
BaseParser: parser.NewBaseParser(logger),
}
}
// Parse implements the parser.Parser interface
func (p *MyFormatParser) Parse(r io.Reader) ([]models.Transaction, error) {
p.GetLogger().Info("Starting MyFormat parsing")
// Read and validate input
data, err := io.ReadAll(r)
if err != nil {
return nil, &parsererror.ParseError{
Parser: "MyFormat",
Field: "input",
Err: err,
}
}
// Parse the data
transactions, err := p.parseData(data)
if err != nil {
return nil, err
}
p.GetLogger().Info("MyFormat parsing completed",
logging.Field{Key: "count", Value: len(transactions)})
return transactions, nil
}
// ValidateFormat implements the parser.Validator interface (optional)
func (p *MyFormatParser) ValidateFormat(filePath string) (bool, error) {
// Implement format validation logic
return true, nil
}
// parseData contains the core parsing logic
func (p *MyFormatParser) parseData(data []byte) ([]models.Transaction, error) {
var transactions []models.Transaction
// Implement parsing logic here
// Use TransactionBuilder for creating transactions:
tx, err := models.NewTransactionBuilder().
WithDate("2025-01-15").
WithAmount(decimal.NewFromFloat(100.50), "CHF").
WithPayer("John Doe", "CH1234567890").
WithPayee("Acme Corp", "CH0987654321").
AsDebit().
Build()
if err != nil {
return nil, &parsererror.ParseError{
Parser: "MyFormat",
Field: "transaction",
Err: err,
}
}
transactions = append(transactions, tx)
return transactions, nil
}
3. Create Adapter¶
File: internal/myformatparser/adapter.go
package myformatparser
import (
"github.com/fjacquet/camt-csv/internal/logging"
"github.com/fjacquet/camt-csv/internal/parser"
)
// Adapter implements the parser interfaces for MyFormat files
type Adapter struct {
parser.BaseParser
}
// NewAdapter creates a new adapter for MyFormat parser
func NewAdapter(logger logging.Logger) *Adapter {
return &Adapter{
BaseParser: parser.NewBaseParser(logger),
}
}
// Parse delegates to the MyFormatParser
func (a *Adapter) Parse(r io.Reader) ([]models.Transaction, error) {
parser := NewMyFormatParser(a.GetLogger())
return parser.Parse(r)
}
4. Add Comprehensive Tests¶
File: internal/myformatparser/myformatparser_test.go
package myformatparser
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/fjacquet/camt-csv/internal/logging"
)
func TestMyFormatParser_Parse(t *testing.T) {
tests := []struct {
name string
input string
expected int
expectError bool
}{
{
name: "valid input",
input: "sample,data,here",
expected: 1,
expectError: false,
},
{
name: "invalid input",
input: "invalid",
expected: 0,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create mock logger
logger := &MockLogger{}
parser := NewMyFormatParser(logger)
// Execute
reader := strings.NewReader(tt.input)
transactions, err := parser.Parse(reader)
// Assert
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Len(t, transactions, tt.expected)
}
})
}
}
// MockLogger for testing
type MockLogger struct {
messages []string
}
func (m *MockLogger) Info(msg string, fields ...logging.Field) {
m.messages = append(m.messages, msg)
}
// Implement other Logger interface methods...
5. Add CLI Command¶
File: cmd/myformat/convert.go
package myformat
import (
"github.com/spf13/cobra"
"github.com/fjacquet/camt-csv/internal/container"
"github.com/fjacquet/camt-csv/internal/factory"
)
// NewConvertCmd creates the myformat conversion command
func NewConvertCmd() *cobra.Command {
var inputFile, outputFile string
cmd := &cobra.Command{
Use: "myformat",
Short: "Convert MyFormat files to CSV",
Long: "Convert MyFormat files to standardized CSV format with transaction categorization",
RunE: func(cmd *cobra.Command, args []string) error {
// Create container with dependencies
container, err := container.NewContainer(config.GetGlobalConfig())
if err != nil {
return err
}
// Get parser from container
parser, err := container.GetParser(factory.MyFormat)
if err != nil {
return err
}
// Execute conversion
return parser.ConvertToCSV(inputFile, outputFile)
},
}
cmd.Flags().StringVarP(&inputFile, "input", "i", "", "Input MyFormat file")
cmd.Flags().StringVarP(&outputFile, "output", "o", "", "Output CSV file")
cmd.MarkFlagRequired("input")
cmd.MarkFlagRequired("output")
return cmd
}
6. Register Parser in Factory¶
File: internal/factory/factory.go
const (
// ... existing parser types
MyFormat ParserType = "myformat"
)
func GetParserWithLogger(parserType ParserType, logger logging.Logger) (models.Parser, error) {
switch parserType {
// ... existing cases
case MyFormat:
return myformatparser.NewAdapter(logger), nil
default:
return nil, fmt.Errorf("unknown parser type: %s", parserType)
}
}
7. Add Sample Files¶
8. Update Documentation¶
Update README.md and docs/user-guide.md to include the new parser.
Parser Best Practices¶
Error Handling¶
// Use custom error types with context
if err != nil {
return nil, &parsererror.ParseError{
Parser: "MyFormat",
Field: "amount",
Value: rawValue,
Err: err,
}
}
// Log warnings for recoverable issues
if amount.IsZero() {
p.GetLogger().Warn("Zero amount detected, continuing",
logging.Field{Key: "line", Value: lineNumber})
}
Constants Usage¶
// Use constants instead of magic strings
transaction.CreditDebit = models.TransactionTypeDebit
transaction.Category = models.CategoryUncategorized
Structured Logging¶
p.GetLogger().Info("Processing transaction",
logging.Field{Key: "file", Value: filename},
logging.Field{Key: "line", Value: lineNumber},
logging.Field{Key: "amount", Value: amount.String()})
Adding Categorization Strategies¶
Strategy Interface¶
type CategorizationStrategy interface {
Categorize(ctx context.Context, tx Transaction) (Category, bool, error)
Name() string
}
Example Implementation¶
File: internal/categorizer/my_strategy.go
package categorizer
import (
"context"
"strings"
"github.com/fjacquet/camt-csv/internal/logging"
"github.com/fjacquet/camt-csv/internal/models"
)
// MyStrategy implements a custom categorization strategy
type MyStrategy struct {
logger logging.Logger
rules map[string]string
}
// NewMyStrategy creates a new instance of MyStrategy
func NewMyStrategy(logger logging.Logger) *MyStrategy {
return &MyStrategy{
logger: logger,
rules: make(map[string]string),
}
}
// Name returns the strategy name for logging
func (s *MyStrategy) Name() string {
return "MyStrategy"
}
// Categorize attempts to categorize a transaction using custom logic
func (s *MyStrategy) Categorize(ctx context.Context, tx models.Transaction) (models.Category, bool, error) {
// Implement categorization logic
description := strings.ToLower(tx.Description)
for pattern, category := range s.rules {
if strings.Contains(description, pattern) {
s.logger.Debug("Transaction categorized",
logging.Field{Key: "strategy", Value: s.Name()},
logging.Field{Key: "pattern", Value: pattern},
logging.Field{Key: "category", Value: category})
return models.Category{Name: category}, true, nil
}
}
return models.Category{}, false, nil
}
Register Strategy¶
File: internal/categorizer/categorizer.go
func NewCategorizer(store *store.CategoryStore, aiClient AIClient, logger logging.Logger) *Categorizer {
c := &Categorizer{
store: store,
logger: logger,
}
// Initialize strategies in priority order
c.strategies = []CategorizationStrategy{
NewDirectMappingStrategy(store, logger),
NewKeywordStrategy(store, logger),
NewMyStrategy(logger), // Add your strategy
NewAIStrategy(aiClient, logger),
}
return c
}
Testing Guidelines¶
Unit Testing Structure¶
func TestMyFunction(t *testing.T) {
tests := []struct {
name string
input InputType
expected ExpectedType
expectError bool
}{
{
name: "valid case",
input: validInput,
expected: expectedOutput,
expectError: false,
},
{
name: "error case",
input: invalidInput,
expected: ExpectedType{},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup
mockDeps := setupMocks()
// Execute
result, err := MyFunction(tt.input, mockDeps)
// Assert
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expected, result)
}
})
}
}
Mock Dependencies¶
type MockLogger struct {
entries []LogEntry
}
type LogEntry struct {
Level string
Message string
Fields []logging.Field
}
func (m *MockLogger) Info(msg string, fields ...logging.Field) {
m.entries = append(m.entries, LogEntry{
Level: "INFO",
Message: msg,
Fields: fields,
})
}
Integration Testing¶
func TestEndToEndConversion(t *testing.T) {
// Create temporary directories
tempDir := t.TempDir()
inputFile := filepath.Join(tempDir, "input.xml")
outputFile := filepath.Join(tempDir, "output.csv")
// Create test input
testData := `<xml>test data</xml>`
err := os.WriteFile(inputFile, []byte(testData), 0644)
require.NoError(t, err)
// Execute conversion
container, err := container.NewContainer(config.GetGlobalConfig())
require.NoError(t, err)
parser, err := container.GetParser(factory.CAMT)
require.NoError(t, err)
err = parser.ConvertToCSV(inputFile, outputFile)
require.NoError(t, err)
// Verify output
assert.FileExists(t, outputFile)
// Verify content
content, err := os.ReadFile(outputFile)
require.NoError(t, err)
assert.Contains(t, string(content), "expected,content")
}
Test Coverage¶
# Generate coverage report
go test -coverprofile=coverage.out ./...
# View coverage in browser
go tool cover -html=coverage.out
# Check coverage percentage
go tool cover -func=coverage.out | grep total
Code Quality Standards¶
Linting Configuration¶
File: .golangci.yml
linters-settings:
govet:
check-shadowing: true
golint:
min-confidence: 0
gocyclo:
min-complexity: 15
maligned:
suggest-new: true
dupl:
threshold: 100
goconst:
min-len: 2
min-occurrences: 2
linters:
enable:
- bodyclose
- deadcode
- depguard
- dogsled
- dupl
- errcheck
- gochecknoinits
- goconst
- gocyclo
- gofmt
- goimports
- golint
- gosec
- gosimple
- govet
- ineffassign
- interfacer
- maligned
- misspell
- nakedret
- scopelint
- staticcheck
- structcheck
- stylecheck
- typecheck
- unconvert
- unparam
- unused
- varcheck
- whitespace
run:
timeout: 5m
Code Formatting¶
# Format all Go files
go fmt ./...
# Organize imports
goimports -w .
# Run all linters
golangci-lint run
Documentation Standards¶
// Package mypackage provides functionality for handling MyFormat files.
//
// This package implements the parser interface for MyFormat financial data,
// supporting both parsing and validation of input files.
package mypackage
// MyFunction performs a specific operation on the input data.
//
// It takes an input parameter and returns the processed result along with
// any error that occurred during processing.
//
// Parameters:
// - input: The data to be processed
// - config: Configuration options for processing
//
// Returns:
// - ProcessedData: The result of processing
// - error: Any error that occurred during processing
//
// Example:
//
// result, err := MyFunction(inputData, config)
// if err != nil {
// log.Fatal(err)
// }
// fmt.Printf("Result: %v\n", result)
func MyFunction(input InputData, config Config) (ProcessedData, error) {
// Implementation
}
Debugging and Troubleshooting¶
Debug Logging¶
// Enable debug logging
logger := logging.NewLogrusAdapter("debug", "text")
// Add debug information
logger.Debug("Processing entry",
logging.Field{Key: "entry_id", Value: entry.ID},
logging.Field{Key: "amount", Value: entry.Amount},
logging.Field{Key: "raw_data", Value: string(rawData)})
Common Issues¶
1. Parser Not Found¶
Error: unknown parser type: myformat
Solution: Ensure parser is registered in factory:
2. Dependency Injection Issues¶
Error: nil pointer dereference
Solution: Ensure all dependencies are properly injected:
func NewMyParser(logger logging.Logger) *MyParser {
if logger == nil {
logger = logging.GetLogger() // Fallback
}
return &MyParser{
BaseParser: parser.NewBaseParser(logger),
}
}
3. Test Failures¶
Error: Tests fail with mock dependencies
Solution: Ensure mocks implement all interface methods:
Debugging Tools¶
# Run with race detection
go test -race ./...
# Profile CPU usage
go test -cpuprofile=cpu.prof -bench=.
go tool pprof cpu.prof
# Profile memory usage
go test -memprofile=mem.prof -bench=.
go tool pprof mem.prof
# Debug with delve
dlv debug
Performance Considerations¶
Memory Optimization¶
// Pre-allocate slices with known capacity
transactions := make([]models.Transaction, 0, expectedCount)
// Use strings.Builder for string concatenation
var builder strings.Builder
builder.Grow(estimatedSize)
CPU Optimization¶
// Use sync.Pool for frequently allocated objects
var transactionPool = sync.Pool{
New: func() interface{} {
return &models.Transaction{}
},
}
func getTransaction() *models.Transaction {
return transactionPool.Get().(*models.Transaction)
}
func putTransaction(tx *models.Transaction) {
// Reset transaction
*tx = models.Transaction{}
transactionPool.Put(tx)
}
Benchmarking¶
func BenchmarkMyFunction(b *testing.B) {
input := setupBenchmarkInput()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := MyFunction(input)
if err != nil {
b.Fatal(err)
}
}
}
Contributing Guidelines¶
Pull Request Process¶
- Fork and Clone: Fork the repository and clone your fork
- Create Branch: Create a feature branch from
main - Implement Changes: Follow the coding standards and patterns
- Add Tests: Ensure comprehensive test coverage
- Update Documentation: Update relevant documentation
- Run Quality Checks: Ensure all linters and tests pass
- Submit PR: Create a pull request with clear description
Commit Message Format¶
Types: feat, fix, docs, style, refactor, test, chore
Examples:
feat(parser): add support for MyFormat files
Implements parser for MyFormat financial data with validation
and error handling following established patterns.
Fixes #123
Code Review Checklist¶
- [ ] Follows established architecture patterns
- [ ] Uses dependency injection properly
- [ ] Implements proper error handling
- [ ] Includes comprehensive tests
- [ ] Updates documentation
- [ ] Passes all quality checks
- [ ] Maintains backward compatibility
Migration from v1.x to v2.0.0¶
Breaking Changes¶
v2.0.0 removes all deprecated APIs that were flagged for removal:
Removed Transaction Methods¶
| Removed | Replacement |
|---|---|
GetPayee() |
GetCounterparty() or tx.Payee directly |
GetPayer() |
GetCounterparty() or tx.Payer directly |
GetAmountAsFloat() |
GetAmountAsDecimal() |
GetDebitAsFloat() |
tx.Debit (decimal.Decimal) |
GetCreditAsFloat() |
tx.Credit (decimal.Decimal) |
GetFeesAsFloat() |
GetFeesAsDecimal() |
ToBuilder() |
Create new NewTransactionBuilder() and copy fields |
SetPayerInfo() |
TransactionBuilder.WithPayer() |
SetPayeeInfo() |
TransactionBuilder.WithPayee() |
SetAmountFromFloat() |
TransactionBuilder.WithAmountFromFloat() or SetAmountFromDecimal() |
Removed Functions¶
| Removed | Replacement |
|---|---|
root.GetConfig() |
root.GetContainer().GetConfig() |
common.ProcessFileLegacy() |
common.ProcessFileWithErrorFormatted() |
common.SaveMappings() |
Use container-based categorizer |
mock.Entries() |
mock.GetEntries() |
Removed Constants¶
| Removed | Replacement |
|---|---|
xmlutils.XPath* constants |
xmlutils.DefaultCamt053XPaths() struct |
Configuration Migration¶
If migrating from environment variables to config file:
# ~/.camt-csv/camt-csv.yaml
log:
level: "info" # was CAMT_LOG_LEVEL
format: "text" # was CAMT_LOG_FORMAT
csv:
delimiter: "," # was CAMT_CSV_DELIMITER
ai:
enabled: false # was CAMT_AI_ENABLED
model: "gemini-2.0-flash"
categorization:
auto_learn: false # NEW in v1.2 — controls AI auto-learning
staging:
enabled: true # NEW — save AI suggestions to staging files when auto-learn is off
creditors_file: "staging_creditors.yaml"
debtors_file: "staging_debtors.yaml"
backup:
enabled: true # backups before YAML overwrites
Environment variables still work — config file is optional.
Version Information¶
The binary embeds version info via ldflags. Variables defined in main.go:
main.version— git tag (e.g.,v2.1.0)main.commit— short commit hashmain.date— build timestamp
Check with ./camt-csv --version. GoReleaser injects these automatically during releases.
Debtor File Rename¶
The debtor mapping file was renamed from debitors.yaml to debtors.yaml for standard English spelling. Rename your file if you have one. The application loads debtors.yaml by default.
This developer guide provides the foundation for contributing to the CAMT-CSV project while maintaining code quality and architectural consistency.