Eliminate all 103 errors and 96 warnings from the architecture validator: Phase 1 - Validator rules & YAML: - Add NAM-001/NAM-002 exceptions for module-scoped router/service files - Fix API-004 to detect # public comments on decorator lines - Add module-specific exception bases to EXC-004 valid_bases - Exclude storefront files from AUTH-004 store context check - Add SVC-006 exceptions for loyalty service atomic commits - Fix _get_rule() to search naming_rules and auth_rules categories - Use plain # CODE comments instead of # noqa: CODE for custom rules Phase 2 - Billing module (5 route files): - Move _resolve_store_to_merchant to subscription_service - Move tier/feature queries to feature_service, admin_subscription_service - Extract 22 inline Pydantic schemas to billing/schemas/billing.py - Replace all HTTPException with domain exceptions Phase 3 - Loyalty module (4 routes + points_service): - Add 7 domain exceptions (Apple auth, enrollment, device registration) - Add service methods to card_service, program_service, apple_wallet_service - Move all db.query() from routes to service layer - Fix SVC-001: replace HTTPException in points_service with domain exception Phase 4 - Remaining modules: - tenancy: move store stats queries to admin_service - cms: move platform resolution to content_page_service, add NoPlatformSubscriptionException - messaging: move user/customer lookups to messaging_service - Add ConfigDict(from_attributes=True) to ContentPageResponse Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
176 lines
5.3 KiB
YAML
176 lines
5.3 KiB
YAML
# 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 <"
|