# Money Handling Architecture

## Overview

This document describes the architecture for handling monetary values in the application. Following industry best practices (Stripe, PayPal, Shopify), all monetary values are stored as **integers representing cents**.

## Why Integer Cents?

### The Problem with Floating-Point

```python
# Floating-point arithmetic is imprecise
>>> 0.1 + 0.2
0.30000000000000004

>>> 105.91 * 1
105.91000000000001
```

This causes issues in financial applications where precision is critical.

### The Solution: Integer Cents

```python
# Integer arithmetic is always exact
>>> 10 + 20  # 0.10 + 0.20 in cents
30

>>> 10591 * 1  # 105.91 EUR in cents
10591
```

**€105.91** is stored as **10591** (integer)

## Conventions

### Database Schema

All price/amount columns use `Integer` type with a `_cents` suffix:

```python
# Good - Integer cents
price_cents = Column(Integer, nullable=False, default=0)
total_amount_cents = Column(Integer, nullable=False)

# Bad - Never use Float for money
price = Column(Float)  # DON'T DO THIS
```

**Column naming convention:**
- `price_cents` - Product price in cents
- `sale_price_cents` - Sale price in cents
- `unit_price_cents` - Per-unit price in cents
- `total_price_cents` - Line total in cents
- `subtotal_cents` - Order subtotal in cents
- `tax_amount_cents` - Tax in cents
- `shipping_amount_cents` - Shipping cost in cents
- `discount_amount_cents` - Discount in cents
- `total_amount_cents` - Order total in cents

### Python Code

#### Using the Money Utility

```python
from app.utils.money import Money, cents_to_euros, euros_to_cents

# Creating money values
price = Money.from_euros(105.91)  # Returns 10591
price = Money.from_cents(10591)   # Returns 10591

# Converting for display
euros = cents_to_euros(10591)     # Returns 105.91
formatted = Money.format(10591)   # Returns "105.91"
formatted = Money.format(10591, currency="EUR")  # Returns "105.91 EUR"

# Parsing user input
cents = euros_to_cents("105.91")  # Returns 10591
cents = euros_to_cents(105.91)    # Returns 10591

# Arithmetic (always use integers)
line_total = unit_price_cents * quantity
order_total = subtotal_cents + shipping_cents - discount_cents
```

#### In Services

```python
from app.utils.money import euros_to_cents, cents_to_euros

class OrderService:
    def create_order(self, items: list):
        subtotal_cents = 0
        for item in items:
            line_total_cents = item.unit_price_cents * item.quantity
            subtotal_cents += line_total_cents

        # All arithmetic in cents - no precision loss
        total_cents = subtotal_cents + shipping_cents - discount_cents

        order = Order(
            subtotal_cents=subtotal_cents,
            total_amount_cents=total_cents,
        )
```

#### In Models (Properties)

```python
class Order(Base):
    total_amount_cents = Column(Integer, nullable=False)

    @property
    def total_amount(self) -> float:
        """Get total as euros (for display/API)."""
        return cents_to_euros(self.total_amount_cents)

    @total_amount.setter
    def total_amount(self, value: float):
        """Set total from euros."""
        self.total_amount_cents = euros_to_cents(value)
```

### Pydantic Schemas

#### Input Schemas (Accept Euros)

```python
from pydantic import BaseModel, field_validator
from app.utils.money import euros_to_cents

class ProductCreate(BaseModel):
    price: float  # Accept euros from API

    @field_validator('price')
    @classmethod
    def convert_to_cents(cls, v):
        # Validation only - actual conversion in service
        if v < 0:
            raise ValueError('Price must be non-negative')
        return v
```

#### Output Schemas (Return Euros)

```python
from pydantic import BaseModel, computed_field
from app.utils.money import cents_to_euros

class ProductResponse(BaseModel):
    price_cents: int  # Internal storage

    @computed_field
    @property
    def price(self) -> float:
        """Return price in euros for API consumers."""
        return cents_to_euros(self.price_cents)
```

### API Layer

APIs accept and return values in **euros** (human-readable), while internally everything is stored in **cents**.

```json
// API Request (euros)
{
    "price": 105.91,
    "quantity": 2
}

// API Response (euros)
{
    "price": 105.91,
    "total": 211.82
}

// Database (cents)
price_cents: 10591
total_cents: 21182
```

### Frontend/Templates

JavaScript helper for formatting:

```javascript
// In static/shared/js/money.js
const Money = {
    /**
     * Format cents as currency string
     * @param {number} cents - Amount in cents
     * @param {string} currency - Currency code (default: EUR)
     * @returns {string} Formatted price
     */
    format(cents, currency = 'EUR') {
        const euros = cents / 100;
        return new Intl.NumberFormat('de-DE', {
            style: 'currency',
            currency: currency
        }).format(euros);
    },

    /**
     * Convert euros to cents
     * @param {number|string} euros - Amount in euros
     * @returns {number} Amount in cents
     */
    toCents(euros) {
        return Math.round(parseFloat(euros) * 100);
    },

    /**
     * Convert cents to euros
     * @param {number} cents - Amount in cents
     * @returns {number} Amount in euros
     */
    toEuros(cents) {
        return cents / 100;
    }
};
```

In templates:

```html
<!-- Display price from cents -->
<span x-text="Money.format(product.price_cents)"></span>

<!-- Or using computed euro value from API -->
<span x-text="formatPrice(product.price, product.currency)"></span>
```

## Data Import/Export

### CSV Import

When importing prices from CSV files:

```python
from app.utils.money import parse_price_to_cents

# Parse "19.99 EUR" or "19,99" to cents
price_cents = parse_price_to_cents("19.99 EUR")  # Returns 1999
price_cents = parse_price_to_cents("19,99")       # Returns 1999
```

### Marketplace Import (Letzshop)

When importing from Letzshop GraphQL API:

```python
from app.utils.money import euros_to_cents

# Letzshop returns prices as floats
letzshop_price = 105.91
price_cents = euros_to_cents(letzshop_price)  # Returns 10591
```

## Migration Strategy

### Database Migration

1. Add new `_cents` columns
2. Migrate data: `UPDATE table SET price_cents = ROUND(price * 100)`
3. Drop old float columns
4. Rename columns if needed

```python
# Migration example
def upgrade():
    # Add cents columns
    op.add_column('products', sa.Column('price_cents', sa.Integer()))

    # Migrate data
    op.execute('UPDATE products SET price_cents = ROUND(price * 100)')

    # Drop old columns
    op.drop_column('products', 'price')
```

## Currency Support

The system is designed for EUR but supports multiple currencies:

```python
class Money:
    # Currency decimal places (for future multi-currency support)
    CURRENCY_DECIMALS = {
        'EUR': 2,
        'USD': 2,
        'GBP': 2,
        'JPY': 0,  # Yen has no decimals
    }
```

## Testing

Always test with values that expose floating-point issues:

```python
def test_price_precision():
    # These values cause issues with float
    test_prices = [0.1, 0.2, 0.3, 19.99, 105.91]

    for price in test_prices:
        cents = euros_to_cents(price)
        back_to_euros = cents_to_euros(cents)
        assert back_to_euros == price
```

## VAT Handling

### Storage Strategy: Gross Prices (VAT-Inclusive)

The platform stores all prices as **gross** (VAT-inclusive) for B2C simplicity:

- Customers see the final price immediately (EU legal requirement)
- No calculation needed for display
- Profit is calculated by extracting net from gross

### Luxembourg VAT Rates

| Rate | Percentage | Applies To |
|------|------------|------------|
| Standard | **17%** | Most products (electronics, furniture, cosmetics) |
| Intermediate | **14%** | Wines, printed materials, heating oils |
| Reduced | **8%** | Utilities, hairdressing, small repairs |
| Super-reduced | **3%** | Food, books, children's clothing, medicine |
| Zero | **0%** | Exports, certain financial services |

### Database Fields

```python
class Product(Base):
    # Tax rate (0, 3, 8, 14, or 17 for Luxembourg)
    tax_rate_percent = Column(Integer, default=17, nullable=False)

    # Cost for profit calculation (what store pays to acquire)
    cost_cents = Column(Integer, nullable=True)
```

### Profit Calculation

```python
class Product(Base):
    @property
    def net_price_cents(self) -> int:
        """Calculate net price (excluding VAT) from gross price.

        Formula: Net = Gross * 100 / (100 + rate)
        Example: €119 gross at 17% VAT = €119 * 100 / 117 = €101.71 net
        """
        return int(self.effective_price_cents * 100 / (100 + self.tax_rate_percent))

    @property
    def vat_amount_cents(self) -> int:
        """VAT = Gross - Net"""
        return self.effective_price_cents - self.net_price_cents

    @property
    def profit_cents(self) -> int:
        """Profit = Net Revenue - Cost"""
        return self.net_price_cents - self.cost_cents

    @property
    def profit_margin_percent(self) -> float:
        """Margin% = (Profit / Net) * 100"""
        return round((self.profit_cents / self.net_price_cents) * 100, 2)
```

### Example Calculation

| Field | Value |
|-------|-------|
| Gross Price | €119.00 (11900 cents) |
| Tax Rate | 17% |
| Net Price | €101.71 (10171 cents) |
| VAT Amount | €17.29 (1729 cents) |
| Cost | €60.00 (6000 cents) |
| **Profit** | €41.71 (4171 cents) |
| **Margin** | 41.0% |

### Store Letzshop Settings

Stores have default settings for the Letzshop feed:

```python
class Store(Base):
    # Default VAT rate for new products
    letzshop_default_tax_rate = Column(Integer, default=17)

    # Product sort priority (0.0-10.0, higher = displayed first)
    letzshop_boost_sort = Column(String(10), default="5.0")

    # Delivery method: 'nationwide', 'package_delivery', 'self_collect'
    letzshop_delivery_method = Column(String(100), default="package_delivery")

    # Pre-order days before shipping (default 1 day)
    letzshop_preorder_days = Column(Integer, default=1)
```

## Summary

| Layer | Format | Example |
|-------|--------|---------|
| Database | Integer cents | `10591` |
| Python services | Integer cents | `10591` |
| Pydantic schemas | Float euros (I/O) | `105.91` |
| API request/response | Float euros | `105.91` |
| Frontend display | Formatted string | `"105,91 €"` |

**Golden Rules:**
1. All arithmetic happens with integers. Conversion to/from euros only at system boundaries.
2. Prices are stored as gross (VAT-inclusive). Net is calculated when needed.
3. Tax rate is stored per product, with store defaults for new products.
