# Architecture Rules - Money Handling # Rules for monetary value handling across the codebase # # Reference: docs/architecture/money-handling.md money_handling_rules: - id: "MON-001" name: "Database columns for money must use Integer cents" severity: "error" description: | All monetary values in database models MUST be stored as integers representing cents (or smallest currency unit). This prevents floating-point precision errors. CORRECT: price_cents = Column(Integer) total_amount_cents = Column(Integer) sale_price_cents = Column(Integer, nullable=True) INCORRECT: price = Column(Float) total_amount = Column(Numeric(10, 2)) Column naming convention: Use `_cents` suffix for all monetary columns. pattern: file_pattern: - "models/database/**/*.py" - "app/modules/*/models/**/*.py" required_patterns: - "_cents = Column(Integer" anti_patterns: - "price = Column(Float" - "amount = Column(Float" - "total = Column(Float" - "Column(Numeric" - id: "MON-002" name: "Use Money utility for conversions" severity: "error" description: | All euro/cents conversions MUST use the Money utility class or the euros_to_cents/cents_to_euros helper functions. Never use manual multiplication/division for money conversions. CORRECT: from app.utils.money import Money, euros_to_cents, cents_to_euros price_cents = euros_to_cents(19.99) display_price = cents_to_euros(price_cents) formatted = Money.format(price_cents, "EUR") INCORRECT: price_cents = int(price * 100) display_price = price_cents / 100 pattern: file_pattern: - "app/**/*.py" - "models/**/*.py" required_imports: - "from app.utils.money import" anti_patterns: - "* 100" # Manual cents conversion - "/ 100" # Manual euros conversion exceptions: - "app/utils/money.py" # The utility itself - id: "MON-003" name: "API responses use euros for display" severity: "error" description: | Pydantic response schemas MUST expose monetary values as floats (euros) for frontend consumption. Use computed properties to convert from _cents. CORRECT (in schema): price_cents: int # Internal @computed_field @property def price(self) -> float: return cents_to_euros(self.price_cents) Or use model validators to convert before response serialization. pattern: file_pattern: - "models/schema/**/*.py" - "app/modules/*/schemas/**/*.py" check: "money_response_format" - id: "MON-004" name: "Frontend must use Money.format() for display" severity: "error" description: | All monetary value display in frontend MUST use the Money utility. This ensures consistent formatting across locales. CORRECT (JavaScript): Money.format(order.total_amount_cents, 'EUR') Money.formatEuros(order.total_amount) INCORRECT: `${order.total_amount} EUR` order.total_amount.toFixed(2) pattern: file_pattern: - "static/**/*.js" - "app/templates/**/*.html" required_patterns: - "Money.format" - "formatPrice" # Helper function wrapping Money.format discouraged_patterns: - "toFixed(2)" - "${.*} EUR" - id: "MON-005" name: "All arithmetic on money uses integers" severity: "error" description: | All monetary calculations MUST be performed on integer cents values. Convert to euros ONLY for display purposes, never for calculations. CORRECT: subtotal_cents = sum(item.unit_price_cents * item.quantity for item in items) tax_cents = int(subtotal_cents * Decimal("0.17")) total_cents = subtotal_cents + tax_cents INCORRECT: subtotal = sum(item.price * item.quantity for item in items) tax = subtotal * 0.17 # Floating point! total = subtotal + tax pattern: file_pattern: - "app/services/**/*.py" - "app/modules/*/services/**/*.py" check: "money_arithmetic" - id: "MON-006" name: "CSV/External imports must convert to cents" severity: "error" description: | When importing monetary values from CSV files or external APIs, always convert to cents immediately upon parsing. CORRECT: price_cents = euros_to_cents(parsed_price) product.price_cents = price_cents INCORRECT: product.price = parsed_price # Float assignment pattern: file_pattern: - "app/utils/csv_processor.py" - "app/services/**/import*.py" required_patterns: - "euros_to_cents" - "_cents" - id: "MON-007" name: "Test assertions use cents" severity: "warning" description: | Test assertions for monetary values should use integer cents to avoid floating-point comparison issues. CORRECT: assert order.total_amount_cents == 10591 assert product.price_cents == 1999 INCORRECT: assert order.total_amount == 105.91 # Float comparison pattern: file_pattern: "tests/**/*.py" encouraged_patterns: - "_cents ==" - "_cents >" - "_cents <"