Source code for ferrosoft.apps.ferrobase.models.subscription

#  Copyright (c) 2025 Ferrosoft GmbH. All rights reserved.
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _

from ferrosoft.apps.ferrobase.models.base import UUIDModel
from ferrosoft.decimal import zero


[docs] class AppFeatureManager(models.Manager): """Manager for ``AppFeature`` with a bulk upsert keyed on ``code``."""
[docs] def upsert(self, objects): return self.bulk_create( objects, update_conflicts=True, update_fields=["name"], unique_fields=["code"], )
[docs] class AppFeature(UUIDModel): """A toggleable platform feature identified by a stable string ``code``. Features are assigned to ``SubscriptionPlan`` instances via ``FeatureAssignment``, optionally with a numeric limit. """ class Meta: verbose_name = _("App Feature") verbose_name_plural = _("App Features") constraints = [ models.UniqueConstraint(name="unique_app_feature", fields=["code"]) ] indexes = [ models.Index(fields=["name"]), ] objects = AppFeatureManager() code = models.CharField( verbose_name=_("Code Name"), ) name = models.CharField( verbose_name=_("Name"), ) def __str__(self): return self.name
[docs] class SubscriptionPlanManager(models.Manager): """Manager for ``SubscriptionPlan`` with a bulk upsert keyed on ``id``."""
[docs] def upsert(self, objects): return self.bulk_create( objects, update_conflicts=True, update_fields=["name", "max_tonnage", "enable_all_features"], unique_fields=["id"], )
[docs] class SubscriptionPlan(UUIDModel): """Defines a subscription tier that controls which features and tonnage cap an organisation gets. When ``enable_all_features`` is ``True``, feature-gate checks are bypassed regardless of the ``FeatureAssignment`` entries. """ class Meta: verbose_name = _("Subscription Plan") verbose_name_plural = _("Subscription Plans") constraints = [ models.UniqueConstraint( fields=["name"], name="uniq_subscription_plan_name", ) ] objects = SubscriptionPlanManager() name = models.CharField( verbose_name=_("Name"), ) max_tonnage = models.IntegerField( blank=True, null=True, help_text=_( "Set maximum allowed tonnage in metric ton or leave blank for unlimited tonnage." ), verbose_name=_("Maximum Tonnage"), ) enable_all_features = models.BooleanField( default=False, help_text=_("Allow the use of all features regardless of assigned features."), verbose_name=_("Enable All Features"), ) features = models.ManyToManyField( AppFeature, verbose_name=_("Features"), related_name="+", through="FeatureAssignment", ) def __str__(self): return self.name
[docs] def get_absolute_url(self): return reverse("ferrobase:subscriptionplan_update", args=[self.pk])
[docs] def get_assigned_feature(self, feature_code: str) -> "FeatureAssignment | None": return FeatureAssignment.objects.filter( plan=self, feature__code=feature_code ).first()
[docs] class FeatureAssignmentModel(models.Manager): """Manager for ``FeatureAssignment`` that pre-fetches the related feature and supports bulk upsert."""
[docs] def get_queryset(self): return super().get_queryset().select_related("feature")
[docs] def upsert(self, objects): return self.bulk_create( objects, update_conflicts=True, update_fields=["limit"], unique_fields=["plan", "feature"], )
[docs] class FeatureAssignment(models.Model): """Through model linking an ``AppFeature`` to a ``SubscriptionPlan`` with an optional numeric limit. The meaning of ``limit`` is feature-specific (e.g. maximum number of allowed users). Setting ``limit`` to ``0`` disables any limit check for that feature. """ class Meta: verbose_name = _("Feature Assignment") verbose_name_plural = _("Feature Assignments") constraints = [ models.UniqueConstraint( name="unique_feature_assignment", fields=["plan", "feature"], ) ] objects = FeatureAssignmentModel() plan = models.ForeignKey( SubscriptionPlan, related_name="assigned_features", on_delete=models.CASCADE, ) feature = models.ForeignKey( AppFeature, related_name="+", on_delete=models.CASCADE, ) limit = models.IntegerField( verbose_name=_("Limit"), help_text=_( "Meaning of limit is up to the code enforcing it. Set to 0 to disable limit." ), default=0, blank=True, ) def __str__(self): if self.limit == zero: return str(self.feature) return f"{self.feature} (limit: {self.limit})"
[docs] def get_absolute_url(self): return reverse("ferrobase:featureassignment_update", args=[self.pk])
[docs] def with_plan(self, plan: SubscriptionPlan) -> "FeatureAssignment": self.plan = plan return self