This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Start all services (app, nginx, postgres)
docker compose up --build
# Run in background
docker compose up -d --build
# View logs
docker compose logs -f payment-hook
# Stop services
docker compose down
# Reset database (removes all data)
docker compose down -v && docker compose up -d pg# Build production image
docker build -t payment-hook .
# Run with external PostgreSQL and nginx
docker run -d \
--name payment-hook \
--env-file .env \
-v $(pwd)/payment:/app/payment \
-v $(pwd)/receipt:/app/receipt \
-v $(pwd)/cert:/app/cert:ro \
-p 8000:8000 \
payment-hook# Run migrations manually (inside container)
docker compose exec payment-hook python migrate.py
# Check migration status
docker compose exec payment-hook python migrate.py status
# Create new migration
docker compose exec payment-hook python migrate.py create add_new_columndocker compose exec stripe-cli sh -c 'stripe trigger payment_intent.succeeded --override payment_intent:currency=eur --override payment_intent:amount=1234 --api-key $STRIPE_API_SECRET_KEY'# Generate PDF for specific receipt
docker compose exec payment-hook python fina_cli.py \
--generate-pdf 1 \
--template template.md \
--font RobotoMonoNerdFont-Medium
# Batch generate PDFs for all pending receipts
docker compose exec payment-hook python fina_cli.py \
--generate-pending-pdfs \
--template template.md \
--font RobotoMonoNerdFont-Medium
# Can be run via cron for automatic processing
# Example crontab: */5 * * * * docker compose exec -T payment-hook python fina_cli.py --generate-pending-pdfs --template template.md --font RobotoMonoNerdFont-MediumThis is a Python Flask application that processes payment webhooks and integrates them with fiscal systems. Currently supports Stripe → FINA flow, but designed for extensibility to support multiple payment providers and fiscal systems.
Current Implementation: Stripe payments → Croatian FINA fiscalization
app.py - Main Flask application
- Receives payment webhook events (currently
/stripe/payment-intent) - Validates webhook signatures and extracts payment data
- Routes payments to appropriate fiscal system (currently hardcoded to FINA)
- Saves raw webhook data to S3 storage in organized folders
fina.py - FINA fiscalization engine
- Handles FINA-specific database operations (
fina_receipttable) - Generates ZKI (protective code) using FINA certificate
- Builds XML receipt according to FINA schema
- Signs XML with PKCS#12 certificate
- Sends SOAP request to FINA endpoint
- Saves request/response files to S3 storage
s3_storage.py - S3-compatible storage module
- Handles file uploads to Hetzner Object Storage (S3-compatible)
- Supports both text and binary file uploads
- Provides logging for successful uploads and error handling
migrate.py - Database migration system
- Manages database schema changes over time
- Tracks applied migrations in
schema_migrationstable - Supports creating new migrations and checking status
- Runs automatically on container startup
- Stripe sends webhook →
app.py/stripe/payment-intentendpoint app.pyvalidates signature and extracts payment dataapp.pycreates organized folder structure and saves webhook data to S3app.pydetermines fiscal system routing (currently hardcoded to "fina")fina.pyhandles FINA-specific processing:- Gets next receipt number from
fina_receipttable - Calls
fiscalize()to generate fiscal receipt - Communicates with FINA endpoint and returns ZKI/JIR
- Saves successful transaction to
fina_receipttable - Saves fiscal receipt files to same S3 folder as webhook data
- Gets next receipt number from
- All transaction files are organized in S3 with consistent folder structure
After successful fiscalization, PDF receipts are generated asynchronously to avoid impacting the fiscalization process:
- Fiscalization completes →
pdf_statusset to 'pending' in database - CLI tool runs →
fina_cli.py --generate-pending-pdfs(can be triggered manually or via cron) - For each pending receipt:
- Fetch receipt data from database (JIR, ZKI, payment details)
- Generate Croatian tax authority verification URL with JIR
- Create QR code (20mm × 20mm, Error Correction Level L, ISO/IEC 15415 compliant)
- Render Jinja2 template with receipt data
- Convert Markdown → HTML → PDF using fpdf2
- Upload PDF to same S3 folder as fiscal receipt files
- Update
pdf_statusto 'completed' and setpdf_createdtimestamp
Key Features:
- Template-based: Markdown templates with Jinja2 variables (
{{ order_id }},{{ jir }}, etc.) - Unicode support: Uses TrueType fonts (e.g., RobotoMonoNerdFont-Medium) for Croatian characters
- Compliance: QR codes meet Croatian tax authority requirements (2×2cm, Level L error correction)
- Verification: QR code and clickable link to
https://porezna.gov.hr/rn?jir=...&datv=...&izn=... - Filename format:
fina-receipt-{order_id}.pdf(order_id sanitized for S3) - Idempotent: Can retry failed PDFs without affecting fiscal data
Important: receipt_updated timestamp is NOT modified during PDF generation - it only reflects when FINA fiscalization data was last updated.
app.py- Main Flask application (~400 lines)fina.py- FINA fiscalization logic (~500 lines)fina_cli.py- CLI tool for manual FINA operations and PDF generation (~650 lines)s3_storage.py- S3 storage module (~100 lines)migrate.py- Database migration system (~150 lines)test_ssl_connection.py- SSL connection testing utility (~180 lines)migrations/- SQL migration filestemplates/- Markdown templates for PDF receipt generationfonts/- TrueType fonts for PDF generation (Unicode support)doc/- FINA technical specifications and schemascert/- FINA certificates (not committed to git)
Files are stored in S3-compatible storage (Hetzner Object Storage) with organized folder structure:
YYYY-MM-DD-HH-MM-SS-stripe-payment-intent-{event_id}-{hostname}-{pid}/
├── stripe-webhook.json # Raw Stripe webhook payload
├── stripe-webhook.yaml # Parsed webhook data
├── fina-request.xml # SOAP request sent to FINA
├── fina-request.yaml # Parsed request data
├── fina-response.xml # SOAP response from FINA
├── fina-response.yaml # Parsed response data
└── fina-receipt-{order_id}.pdf # PDF receipt (generated asynchronously)
Folder naming convention:
- Timestamp in UTC (YYYY-MM-DD-HH-MM-SS)
- Payment provider and event type (e.g.,
stripe-payment-intent) - Stripe event ID for traceability
- Hostname to distinguish between environments (dev/production)
- Process ID to handle multiple workers/containers
This prevents file conflicts when the same Stripe event is sent to multiple environments (e.g., via stripe-cli to local dev and webhook to production).
fina_receipttable - Stores FINA fiscal receipt data- Primary key:
id(serial) - Unique constraint:
(year, receipt_number) - Fields:
year,location_id,register_id,receipt_number,order_id,stripe_id,amount,currency,zki,jir,payment_time,receipt_created,receipt_updated,status,s3_folder_path,pdf_status,pdf_created payment_time- The original Stripe payment timestamp (TIMESTAMPTZ)receipt_created- Database row creation timestamp (TIMESTAMPTZ)receipt_updated- Database row last update timestamp (TIMESTAMPTZ) - only updated when FINA data changesstatus- Receipt processing status ('pending', 'processing', 'completed', 'failed')s3_folder_path- Full S3 folder path where receipt files are stored (TEXT)pdf_status- PDF generation status ('pending', 'processing', 'completed', 'failed')pdf_created- Timestamp when PDF was successfully generated (TIMESTAMPTZ)
- Primary key:
schema_migrationstable - Tracks applied migrations
Note: Table was renamed from receipt to fina_receipt to support future fiscal systems (e.g., germany_receipt, etc.)
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET
PG_HOST, PG_PORT, PG_USER, PG_PASSWORD, PG_DB
P12_PATH, P12_PASSWORD (FINA certificate)
FINA_CA_DIR_PATH (directory containing FINA CA certificates in .pem format)
- For demo/test environment: cert/ca_demo
- For production environment: cert/ca (or cert/ca_prod)
FINA_TIMEZONE (e.g., Europe/Zagreb)
FINA_ENDPOINT (test/production URL)
- Demo: https://cistest.apis-it.hr:8449/FiskalizacijaServiceTest
- Production: https://cis.porezna-uprava.hr:8449/FiskalizacijaService
OIB_COMPANY, OIB_OPERATOR (Croatian tax IDs)
LOCATION_ID, REGISTER_ID (fiscal identifiers)
S3_ACCESS_KEY, S3_SECRET_KEY, S3_ENDPOINT_URL, S3_BUCKET_NAME (S3-compatible storage)
GUNICORN_WORKERS=2 (number of Gunicorn worker processes)
GUNICORN_TIMEOUT=60 (request timeout in seconds)
- fina_cli.py - Manual FINA operations and PDF generation
--retry-receipt <receipt_number>- Retry fiscalization for a failed receipt--create-receipt --amount <amount>- Create manual fiscal receipt (for non-Stripe payments)--generate-pdf <receipt_id> --template <template.md> --font <font_name>- Generate PDF receipt for specific receipt--generate-pending-pdfs --template <template.md> --font <font_name>- Batch generate PDFs for all pending receipts- Supports custom payment times, order IDs, and Stripe IDs
- Useful for fixing failed receipts, manual payments, testing, and PDF generation
- test_ssl_connection.py - SSL connection testing
--ca-dir <path>- Directory containing CA certificates--endpoint <url>- FINA endpoint to test- Tests SSL handshake without sending fiscal data
- Useful for verifying certificate configuration before deployment
- Development: Docker Compose with nginx proxy, app container, PostgreSQL, stripe-cli, and migration service
- Production: Single container with Gunicorn, external nginx and database
- Volumes:
cert/,migrations/,templates/,fonts/directories are mounted for persistence; all receipt data stored in S3 - Networking: App runs on port 8000 inside container, nginx proxies on port 8080
- Health checks: PostgreSQL health checks ensure database is ready before starting app
- Migration service: Runs once on startup, applies pending migrations, then exits
Python 3.12+ with Flask, Stripe SDK, PostgreSQL (psycopg2), cryptography, xmlsec, lxml for XML processing and FINA integration. boto3 for S3-compatible storage. Gunicorn for WSGI server. fpdf2, markdown-it-py, qrcode, and Pillow for PDF receipt generation with Unicode support. Jinja2 for template rendering. Development tools: black (formatter), isort (import sorter), flake8 (linter), mypy (type checker). No python-dotenv needed (Docker handles environment variables).
Manual formatting and checking:
# Format code
docker run --rm -v $(pwd):/src -w /src payment-hook black .
docker run --rm -v $(pwd):/src -w /src payment-hook isort .
# Check style and types
docker run --rm -v $(pwd):/src -w /src payment-hook flake8 .
docker run --rm -v $(pwd):/src -w /src payment-hook mypy .Git hooks: The project includes a pre-push hook that automatically runs all code quality checks:
# Set up git hooks (run once after cloning)
ln -s ../../.githooks/pre-push .git/hooks/pre-push
# The pre-push hook will automatically:
# 1. Build a test Docker image
# 2. Run black --check (formatting)
# 3. Run isort --check-only (imports)
# 4. Run flake8 (linting)
# 5. Run mypy (type checking)
# 6. Block push if any checks failWhen modifying or extending this codebase, follow these security recommendations:
- Always validate external inputs: Use validation functions for webhook data, API parameters, file paths
- S3 path validation: Use
validate_s3_key()froms3_storage.pyfor all S3 operations - Webhook validation: Extend
validate_webhook_data()inapp.pyfor new webhook fields - Database inputs: Continue using parameterized queries (already implemented)
- Never expose internal errors to clients - use generic error messages
- Log detailed errors server-side with
logger.error()for debugging - Catch specific exceptions rather than broad
Exceptioncatches when possible - Example pattern:
try: # risky operation except SpecificException as e: logger.error(f"Detailed error: {e}") return {"error": "Generic user message"}, 400
- SSL verification: Use
APP_ENVto control security features - Debug mode: Never hardcode
debug=True- use environment variables - Pattern:
os.environ.get("APP_ENV", "production").lower() in ["dev", "development"]
- Temporary files: Use
tempfile.NamedTemporaryFile()with defaultdelete=True - Sensitive data: Never log secrets, certificates, or personal data
- S3 storage: Validate all paths before storage operations
When adding new features:
- Validate all inputs from external sources
- Use parameterized SQL queries
- Implement proper error handling (log details, return generic messages)
- Test with invalid/malicious inputs
- Use environment variables for security-sensitive configuration
- Add appropriate logging for security events
- MD5 hash usage: Required by FINA specification (not changeable)
- Certificate handling: PKCS#12 certificates required for FINA integration
- Docker Compose is the recommended development and deployment method
- No test framework is currently configured
- Code formatting with Black, isort, flake8, and mypy configured in pyproject.toml
- Pre-push git hook automatically runs code quality checks before allowing pushes
- The system handles Croatian VAT-exempt transactions only
- All fiscal receipts and webhook data are stored permanently in S3 for audit compliance
- PDF receipts are generated asynchronously via CLI tool (can be automated with cron)
- Certificate files in cert/ directory are required but not committed to git
- Database migrations run automatically on
docker compose up - Files are stored in S3-compatible storage (Hetzner Object Storage) instead of local directories
- Templates and fonts directories support multiple clients with different receipt designs
- No virtual environment setup needed - Docker handles all dependencies