Files
orion/app/modules/billing/models/merchant_subscription.py
Samir Boulahtit e9253fbd84 refactor: rename Wizamart to Orion across entire codebase
Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart
with Orion/orion/ORION across 184 files. This includes database
identifiers, email addresses, domain references, R2 bucket names,
DNS prefixes, encryption salt, Celery app name, config defaults,
Docker configs, CI configs, documentation, seed data, and templates.

Renames homepage-wizamart.html template to homepage-orion.html.
Fixes duplicate file_pattern key in api.yaml architecture rule.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 16:46:56 +01:00

165 lines
4.5 KiB
Python

# app/modules/billing/models/merchant_subscription.py
"""
Merchant-level subscription model.
Replaces StoreSubscription with merchant-level billing:
- One subscription per merchant per platform
- Merchant is the billing entity (not the store)
- Stores inherit features/limits from their merchant's subscription
"""
from datetime import UTC, datetime
from sqlalchemy import (
Boolean,
Column,
DateTime,
ForeignKey,
Index,
Integer,
String,
Text,
UniqueConstraint,
)
from sqlalchemy.orm import relationship
from app.core.database import Base
from app.modules.billing.models.subscription import SubscriptionStatus
from models.database.base import TimestampMixin
class MerchantSubscription(Base, TimestampMixin):
"""
Per-merchant, per-platform subscription tracking.
The merchant (legal entity) subscribes and pays, not the store.
A merchant can own multiple stores and subscribe per-platform.
Example:
Merchant "Boucherie Luxembourg" subscribes to:
- Orion OMS (Professional tier)
- Loyalty+ (Essential tier)
Their stores inherit features from the merchant's subscription.
"""
__tablename__ = "merchant_subscriptions"
id = Column(Integer, primary_key=True, index=True)
# Who pays
merchant_id = Column(
Integer,
ForeignKey("merchants.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
# Which platform
platform_id = Column(
Integer,
ForeignKey("platforms.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
# Which tier
tier_id = Column(
Integer,
ForeignKey("subscription_tiers.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
# Status
status = Column(
String(20),
default=SubscriptionStatus.TRIAL.value,
nullable=False,
index=True,
)
# Billing period
is_annual = Column(Boolean, default=False, nullable=False)
period_start = Column(DateTime(timezone=True), nullable=False)
period_end = Column(DateTime(timezone=True), nullable=False)
# Trial info
trial_ends_at = Column(DateTime(timezone=True), nullable=True)
# Stripe integration (per merchant)
stripe_customer_id = Column(String(100), nullable=True, index=True)
stripe_subscription_id = Column(String(100), nullable=True, index=True)
stripe_payment_method_id = Column(String(100), nullable=True)
# Payment failure tracking
payment_retry_count = Column(Integer, default=0, nullable=False)
last_payment_error = Column(Text, nullable=True)
# Cancellation
cancelled_at = Column(DateTime(timezone=True), nullable=True)
cancellation_reason = Column(Text, nullable=True)
# Relationships
merchant = relationship(
"Merchant",
backref="subscriptions",
foreign_keys=[merchant_id],
)
platform = relationship(
"Platform",
foreign_keys=[platform_id],
)
tier = relationship(
"SubscriptionTier",
foreign_keys=[tier_id],
)
__table_args__ = (
UniqueConstraint(
"merchant_id", "platform_id",
name="uq_merchant_platform_subscription",
),
Index("idx_merchant_sub_status", "merchant_id", "status"),
Index("idx_merchant_sub_platform", "platform_id", "status"),
)
def __repr__(self):
return (
f"<MerchantSubscription("
f"merchant_id={self.merchant_id}, "
f"platform_id={self.platform_id}, "
f"status='{self.status}'"
f")>"
)
# =========================================================================
# Status Checks
# =========================================================================
@property
def is_active(self) -> bool:
"""Check if subscription allows access."""
return self.status in [
SubscriptionStatus.TRIAL.value,
SubscriptionStatus.ACTIVE.value,
SubscriptionStatus.PAST_DUE.value,
SubscriptionStatus.CANCELLED.value,
]
@property
def is_trial(self) -> bool:
"""Check if currently in trial."""
return self.status == SubscriptionStatus.TRIAL.value
@property
def trial_days_remaining(self) -> int | None:
"""Get remaining trial days."""
if not self.is_trial or not self.trial_ends_at:
return None
remaining = (self.trial_ends_at - datetime.now(UTC)).days
return max(0, remaining)
__all__ = ["MerchantSubscription"]