Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion apps/api/tests/contract/test_billing_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
from httpx import AsyncClient
from pytest import MonkeyPatch

from tests.support.contract_database import ContractDatabase
from shared.core.billing import BillingCalculator
from shared.core.config.billing import BillingConfig
from shared.utils.api_keys import hash_api_key
from tests.support.contract_database import ContractDatabase


def _utc_now() -> datetime:
Expand Down Expand Up @@ -57,6 +59,28 @@ async def _insert_api_key_for_user(user_id: str, api_key: str) -> None:
)


def test_billing_config_should_default_to_one_dollar_fifty_per_hundred_pages(
monkeypatch: MonkeyPatch,
) -> None:
monkeypatch.delenv("MICRO_DOLLARS_PER_PAGE", raising=False)

config: BillingConfig = BillingConfig(_env_file=None)

assert config.MICRO_DOLLARS_PER_PAGE == 15_000


def test_billing_calculator_should_charge_one_dollar_fifty_per_hundred_pages() -> None:
calculator: BillingCalculator = BillingCalculator(price_per_page_micros=15_000)
charge = calculator.calculate_page_cost(100)

assert charge.amount == 1_500_000
assert charge.to_credit() == 1.5
assert (
calculator.format_description(100, "contract.pdf")
== "Document processing: contract.pdf (100 pages @ $0.0150/page)"
)


@pytest.mark.asyncio
async def test_should_return_the_authenticated_users_initialized_credits_balance(
developer_api_client_factory: Callable[
Expand Down
8 changes: 4 additions & 4 deletions packages/shared-python/shared/core/billing/calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

Per-page model: Each page costs a fixed amount in micro-credits.
$1.00 = 1,000,000 micro-credits
Default: $0.0015/page = 1,500 micro-credits/page
Default: $0.015/page = 15,000 micro-credits/page
"""

from shared.core.config import settings
Expand All @@ -21,7 +21,7 @@ class BillingCalculator:
Example:
calc = BillingCalculator()
cost = calc.calculate_page_cost(10) # Returns MicroDollar
print(cost.amount) # 15,000 micro-credits
print(cost.amount) # 150,000 micro-credits
"""

def __init__(self, price_per_page_micros: int | None = None):
Expand Down Expand Up @@ -50,7 +50,7 @@ def calculate_page_cost(self, page_count: int) -> MicroDollar:
MicroDollar representing total cost

Example:
calc.calculate_page_cost(10) # -> MicroDollar(15000)
calc.calculate_page_cost(10) # -> MicroDollar(150000)
"""
return MicroDollar(page_count * self._price_per_page)

Expand All @@ -66,7 +66,7 @@ def format_description(self, page_count: int, filename: str | None = None) -> st
Human-readable description string

Example:
"Document processing: report.pdf (10 pages @ $0.0015/page)"
"Document processing: report.pdf (10 pages @ $0.0150/page)"
"""
price_dollars = self._price_per_page / 1_000_000
file_name = filename or "file"
Expand Down
10 changes: 5 additions & 5 deletions packages/shared-python/shared/core/billing/micro_dollar.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
MicroDollar value object for type-safe integer-based currency operations.

Standard: $1.00 = 1,000,000 micro-credits
This allows representing prices like $0.0015 as 1,500 integers.
This allows representing prices like $0.015 as 15,000 integers.

Usage:
# Create from int (e.g., from database value)
balance = MicroDollar(user.credits_balance)

# Calculations
cost = MicroDollar(1500) * 10 # 10 pages @ $0.0015/page
cost = MicroDollar(15000) * 10 # 10 pages @ $0.015/page

# Get raw int value (e.g., for database storage)
user.credits_balance = cost.amount
Expand All @@ -26,8 +26,8 @@ class MicroDollar:
All arithmetic is integer-based to prevent floating-point errors in billing.

Example:
price = MicroDollar(1_500) # $0.0015
total = price * 10 # 10 pages -> 15,000 micros ($0.015)
price = MicroDollar(15_000) # $0.015
total = price * 10 # 10 pages -> 150,000 micros ($0.15)
"""

SCALE = 1_000_000 # $1.00 = 1,000,000 micros
Expand Down Expand Up @@ -99,7 +99,7 @@ def __mul__(self, quantity: int) -> "MicroDollar":
"""
Multiply by a scalar quantity (integer only).

Example: 10 pages * $0.0015/page = $0.015
Example: 10 pages * $0.015/page = $0.15
"""
if not isinstance(quantity, int):
raise TypeError(
Expand Down
2 changes: 1 addition & 1 deletion packages/shared-python/shared/core/config/billing.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class BillingConfig(BaseSettings):

# Credits (Micro-Dollar System: $1.00 = 1,000,000 micro-credits)
MICRO_DOLLARS_PER_PAGE: int = Field(
default=1500, description="Micro dollars per page ($0.0015 = 1500 micros)"
default=15000, description="Micro dollars per page ($0.015 = 15000 micros)"
)
CREDITS_VALID_DAYS: int = Field(
default=365, description="Credit validity period in days"
Expand Down