Root cause: Old code used rules.get('max_vehicles') which only searched
JSONB root level. Corporate tiers store limits under rules['allowances'].
Changes:
1. Added _extract_limit() helper (3-level search: direct → allowances → limits)
2. Replaced single-sub .limit(1) with multi-sub .all() for true aggregation
3. Fixed fallback path to use _extract_limit() instead of rules.get()
4. Added clamping: asset_limit = max(asset_limit, 1) right before
SubscriptionSummary creation on BOTH code paths
5. Added 'type' column to SubscriptionTier model (base vs addon)
253 lines
11 KiB
Python
Executable File
253 lines
11 KiB
Python
Executable File
# /opt/docker/dev/service_finder/backend/app/models/core_logic.py
|
|
from typing import Optional, List, Any
|
|
from datetime import datetime # Python saját típusa a típusjelöléshez
|
|
from sqlalchemy import String, Integer, ForeignKey, Boolean, DateTime, Numeric, text, Float
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
from sqlalchemy.dialects.postgresql import JSONB
|
|
from sqlalchemy.sql import func
|
|
|
|
# MB 2.0: A központi aszinkron adatbázis motorból húzzuk be a Base-t
|
|
from app.database import Base
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# P0 SYSTEM ARCHITECTURE: Global Region Registry
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
class RegionConfig(Base):
|
|
"""
|
|
🌍 Global Region Configuration Registry.
|
|
|
|
Central table for L10n/i18n configuration. Drives all UI formatting
|
|
(dates, numbers, currencies) and financial calculations (VAT, pricing).
|
|
|
|
Each row represents a country/region with its own locale, currency,
|
|
VAT rate, and timezone settings.
|
|
|
|
Schema: system.region_config
|
|
"""
|
|
__tablename__ = "region_config"
|
|
__table_args__ = {"schema": "system"}
|
|
|
|
country_code: Mapped[str] = mapped_column(
|
|
String(10), primary_key=True, index=True,
|
|
comment="ISO 3166-1 alpha-2 country code, e.g. 'HU', 'GB', or 'DEFAULT' for fallback"
|
|
)
|
|
name: Mapped[str] = mapped_column(
|
|
String, nullable=False,
|
|
comment="Human-readable region name, e.g. 'Hungary', 'United Kingdom'"
|
|
)
|
|
currency: Mapped[str] = mapped_column(
|
|
String(3), nullable=False,
|
|
comment="ISO 4217 currency code, e.g. 'HUF', 'GBP', 'EUR'"
|
|
)
|
|
default_vat_rate: Mapped[float] = mapped_column(
|
|
Float, nullable=False, default=0.0,
|
|
comment="Default VAT rate as decimal, e.g. 27.0 for 27%"
|
|
)
|
|
locale_code: Mapped[str] = mapped_column(
|
|
String(10), nullable=False,
|
|
comment="BCP 47 locale code, e.g. 'hu-HU', 'en-GB', 'de-DE'"
|
|
)
|
|
timezone: Mapped[str] = mapped_column(
|
|
String(50), nullable=False,
|
|
comment="IANA timezone, e.g. 'Europe/Budapest', 'Europe/London'"
|
|
)
|
|
is_active: Mapped[bool] = mapped_column(
|
|
Boolean, nullable=False, default=True, server_default=text("true"),
|
|
comment="Soft-toggle for region availability"
|
|
)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now()
|
|
)
|
|
updated_at: Mapped[Optional[datetime]] = mapped_column(
|
|
DateTime(timezone=True), onupdate=func.now()
|
|
)
|
|
|
|
|
|
class SubscriptionTier(Base):
|
|
"""
|
|
Előfizetési csomagok definíciója (pl. Free, Premium, VIP).
|
|
A csomagok határozzák meg a korlátokat (pl. max járműszám).
|
|
|
|
P0 MULTI-SUBSCRIPTION AGGREGATION:
|
|
A `type` mező különbözteti meg a Base tier-eket ('private', 'corporate', 'business')
|
|
az Add-on tier-ektől ('addon'). Ez lehetővé teszi az "1 Base + N Add-on" modellt.
|
|
"""
|
|
__tablename__ = "subscription_tiers"
|
|
__table_args__ = {"schema": "system"}
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
name: Mapped[str] = mapped_column(String, unique=True, index=True) # pl. 'premium'
|
|
rules: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb")) # pl. {"max_vehicles": 5}
|
|
is_custom: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
|
|
# ── P0 MULTI-SUBSCRIPTION AGGREGATION: Base vs Add-on típus ──
|
|
# 'base' = core tier (individual, corporate, business)
|
|
# 'addon' = add-on tier (extra vehicles, branches, users)
|
|
# None/empty = treated as 'base' for backward compatibility
|
|
type: Mapped[Optional[str]] = mapped_column(
|
|
String(20),
|
|
nullable=True,
|
|
default=None,
|
|
server_default=text("NULL"),
|
|
comment="Tier type: 'base' for core subscriptions, 'addon' for add-on tiers. NULL = base (backward compat)."
|
|
)
|
|
|
|
# ── P0 3D CAPABILITY MATRIX: feature_capabilities JSONB ──
|
|
# Defines what features/capabilities this subscription tier unlocks.
|
|
# Example: {"can_export_data": True, "can_use_analytics": True, "max_vehicles": 50}
|
|
feature_capabilities: Mapped[dict] = mapped_column(
|
|
JSONB,
|
|
nullable=False,
|
|
default=dict,
|
|
server_default=text("'{}'::jsonb")
|
|
)
|
|
|
|
# ── P0 AUDIT FIX: Database-driven tier level instead of hardcoded string mapping ──
|
|
# Numeric level for the entitlement hierarchy: 0=free, 1=premium, 2=enterprise.
|
|
# This replaces the hardcoded _map_tier_name() string-matching logic.
|
|
# New tiers can be added with any name; the tier_level determines their position.
|
|
tier_level: Mapped[int] = mapped_column(
|
|
Integer,
|
|
nullable=False,
|
|
default=0,
|
|
server_default=text("0"),
|
|
comment="Entitlement hierarchy level: 0=free, 1=premium, 2=enterprise"
|
|
)
|
|
|
|
# ── P0 SINGLETON PACKAGE RULES: is_default_fallback ──
|
|
# Ha True, ez a csomag az alapértelmezett fallback csomag, amit a
|
|
# subscription_service használ, ha a user előfizetése lejárt.
|
|
# MAXIMUM EGY csomag lehet True értékkel (singleton).
|
|
is_default_fallback: Mapped[bool] = mapped_column(
|
|
Boolean,
|
|
nullable=False,
|
|
default=False,
|
|
server_default=text("false"),
|
|
comment="Singleton: maximum one tier can be the default fallback package"
|
|
)
|
|
|
|
# ── P0 SINGLETON PACKAGE RULES: trial_days_on_signup ──
|
|
# Ha > 0, ez a csomag a próbaidős csomag, amit új regisztrációnál
|
|
# automatikusan hozzárendelünk a felhasználóhoz.
|
|
# MAXIMUM EGY csomag lehet > 0 értékkel (singleton).
|
|
trial_days_on_signup: Mapped[int] = mapped_column(
|
|
Integer,
|
|
nullable=False,
|
|
default=0,
|
|
server_default=text("0"),
|
|
comment="Singleton: maximum one tier can have trial_days_on_signup > 0"
|
|
)
|
|
|
|
class OrganizationSubscription(Base):
|
|
"""
|
|
Szervezetek aktuális előfizetései és azok érvényessége.
|
|
"""
|
|
__tablename__ = "org_subscriptions"
|
|
__table_args__ = {"schema": "finance"}
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
|
|
# Kapcsolat a szervezettel (fleet séma)
|
|
org_id: Mapped[int] = mapped_column(Integer, ForeignKey("fleet.organizations.id"), nullable=False)
|
|
|
|
# Kapcsolat a csomaggal (system séma)
|
|
tier_id: Mapped[int] = mapped_column(Integer, ForeignKey("system.subscription_tiers.id"), nullable=False)
|
|
|
|
valid_from: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
|
valid_until: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
|
|
# ── P0 BOOSTER ARCHITECTURE: extra_allowances JSONB ──
|
|
# Tárolja a darabáron vett extra kvótákat a csomag keretein felül.
|
|
# Példa: {"extra_vehicles": 1, "extra_garages": 1, "extra_credits": 100}
|
|
extra_allowances: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"), default=dict)
|
|
|
|
# ── P0 FEATURE FLAG: relationship a SubscriptionTier-hez ──
|
|
tier: Mapped[Optional["SubscriptionTier"]] = relationship(
|
|
"SubscriptionTier",
|
|
foreign_keys=[tier_id],
|
|
lazy="selectin",
|
|
)
|
|
|
|
class UserSubscription(Base):
|
|
"""
|
|
Felhasználók aktuális előfizetései és azok érvényessége.
|
|
Tábla: finance.user_subscriptions
|
|
"""
|
|
__tablename__ = "user_subscriptions"
|
|
__table_args__ = {"schema": "finance"}
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
|
|
# Kapcsolat a felhasználóval (identity séma)
|
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
|
|
|
|
# Kapcsolat a csomaggal (system séma)
|
|
tier_id: Mapped[int] = mapped_column(Integer, ForeignKey("system.subscription_tiers.id"), nullable=False)
|
|
|
|
valid_from: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
|
valid_until: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
|
|
# ── P0 FEATURE FLAG: relationship a SubscriptionTier-hez ──
|
|
tier: Mapped[Optional["SubscriptionTier"]] = relationship(
|
|
"SubscriptionTier",
|
|
foreign_keys=[tier_id],
|
|
lazy="selectin",
|
|
)
|
|
|
|
|
|
class CreditTransaction(Base):
|
|
"""
|
|
Kreditnapló (Pontok, kreditek vagy virtuális egyenleg követése).
|
|
"""
|
|
__tablename__ = "credit_logs"
|
|
__table_args__ = {"schema": "finance"}
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
|
|
# Kapcsolat a szervezettel (fleet séma)
|
|
org_id: Mapped[int] = mapped_column(Integer, ForeignKey("fleet.organizations.id"), nullable=False)
|
|
|
|
amount: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False)
|
|
description: Mapped[Optional[str]] = mapped_column(String)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
class ServiceSpecialty(Base):
|
|
"""
|
|
Hierarchikus fa struktúra a szerviz szolgáltatásokhoz (pl. Motor -> Futómű).
|
|
"""
|
|
__tablename__ = "service_specialties"
|
|
__table_args__ = {"schema": "marketplace"}
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
|
|
# Önmagára mutató idegen kulcs a hierarchiához
|
|
parent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("marketplace.service_specialties.id"))
|
|
|
|
name: Mapped[str] = mapped_column(String, nullable=False)
|
|
slug: Mapped[str] = mapped_column(String, unique=True, index=True)
|
|
|
|
# Kapcsolat az ős-szolgáltatással (Self-referential relationship)
|
|
parent: Mapped[Optional["ServiceSpecialty"]] = relationship("ServiceSpecialty", remote_side=[id], backref="children")
|
|
|
|
|
|
class ServiceCatalog(Base):
|
|
"""
|
|
Szolgáltatás Katalógus (Service Catalog).
|
|
A rendszer által kínált szolgáltatások definíciói, pl. 'SRV_DATA_EXPORT'.
|
|
Tábla: system.service_catalog
|
|
"""
|
|
__tablename__ = "service_catalog"
|
|
__table_args__ = {"schema": "system"}
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
service_code: Mapped[str] = mapped_column(String, unique=True, index=True, nullable=False) # pl. 'SRV_DATA_EXPORT'
|
|
name: Mapped[str] = mapped_column(String, nullable=False)
|
|
description: Mapped[Optional[str]] = mapped_column(String)
|
|
credit_cost: Mapped[int] = mapped_column(Integer, default=0) # alapértelmezett ár kreditben
|
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True) |