diff --git a/apps/billing/quotas.py b/apps/billing/quotas.py new file mode 100644 index 0000000..b61d7f1 --- /dev/null +++ b/apps/billing/quotas.py @@ -0,0 +1,12 @@ +from common.apps.billing.constants import FeatureCode, FeatureUsageScope +from common.apps.billing.mixins import BaseQuota + + +class WhitelabelQuota(BaseQuota): + reserve_actions = set() + rules = { + ("create", "update", "partial_update", "destroy"): { + "feature": FeatureCode.WHITELABEL_ENABLED, + "scope": FeatureUsageScope.ORGANIZATION, + }, + } diff --git a/apps/billing/services/quota_service.py b/apps/billing/services/quota_service.py new file mode 100644 index 0000000..cb4ad86 --- /dev/null +++ b/apps/billing/services/quota_service.py @@ -0,0 +1,52 @@ +from apps.billing.services.subscription import release_quota, reserve_quota +from apps.organization.models import Organization + + +class BillingQuotaService: + allow_missing_organization = False + + def _get_organization(self, organization_slug): + return Organization.objects.filter(slug_name=organization_slug).first() + + def reserve_quota( + self, + organization_slug, + feature, + amount=1, + scope_type=None, + scope_id=None, + ): + organization = self._get_organization(organization_slug) + if organization is None: + return False, "Organization context required." + + return reserve_quota( + organization, + feature, + amount, + scope_type=scope_type, + scope_id=scope_id, + ) + + def release_quota( + self, + organization_slug, + feature, + amount=1, + scope_type=None, + scope_id=None, + ): + organization = self._get_organization(organization_slug) + if organization is None: + return None + + return release_quota( + organization, + feature, + amount, + scope_type=scope_type, + scope_id=scope_id, + ) + + +billing_quota_service = BillingQuotaService() diff --git a/apps/custom_email/views.py b/apps/custom_email/views.py index 4541749..e5d8cb2 100644 --- a/apps/custom_email/views.py +++ b/apps/custom_email/views.py @@ -1,15 +1,17 @@ +from common.apps.billing.mixins import QuotaMixin from common.pagination.base_pagination import BasePagination from django.db.models import Prefetch from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import OrderingFilter +from apps.billing.quotas import WhitelabelQuota from apps.custom_email.models import OrganizationEmail from apps.custom_email.serializers import OrganizationEmailSerializer from apps.organization_setting.models import OrganizationTheme from utils.views import OrganizationListAPIView -class ListCustomEmailView(OrganizationListAPIView): +class ListCustomEmailView(QuotaMixin, OrganizationListAPIView): serializer_class = OrganizationEmailSerializer queryset = OrganizationEmail.objects.select_related( "organization", @@ -25,3 +27,4 @@ class ListCustomEmailView(OrganizationListAPIView): filterset_fields = ["email_type"] filter_backends = [DjangoFilterBackend, OrderingFilter] ordering = ["-created_at"] + quota_classes = [WhitelabelQuota] diff --git a/apps/custom_page/views.py b/apps/custom_page/views.py index faea208..58f99d8 100644 --- a/apps/custom_page/views.py +++ b/apps/custom_page/views.py @@ -1,16 +1,19 @@ +from common.apps.billing.mixins import QuotaMixin from common.pagination.base_pagination import BasePagination from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import OrderingFilter +from apps.billing.quotas import WhitelabelQuota from apps.custom_page.models import CustomPage from apps.custom_page.serializers import CustomPageSerializer from utils.views import OrganizationListAPIView -class ListCustomPageView(OrganizationListAPIView): +class ListCustomPageView(QuotaMixin, OrganizationListAPIView): serializer_class = CustomPageSerializer queryset = CustomPage.objects.select_related("organization").all() organization_field = "organization" pagination_class = BasePagination filter_backends = [DjangoFilterBackend, OrderingFilter] ordering = ["-created_at"] + quota_classes = [WhitelabelQuota] diff --git a/apps/organization_setting/views.py b/apps/organization_setting/views.py index 54fcf40..e8be844 100644 --- a/apps/organization_setting/views.py +++ b/apps/organization_setting/views.py @@ -1,7 +1,9 @@ +from common.apps.billing.mixins import QuotaMixin from django.shortcuts import get_object_or_404 from rest_framework import generics from rest_framework.response import Response +from apps.billing.quotas import WhitelabelQuota from apps.organization.models import Organization from apps.organization_setting.models import OrganizationSetting from apps.organization_setting.serializers import ( @@ -10,7 +12,7 @@ ) -class UpdateOrganizationSettingView(generics.UpdateAPIView): +class UpdateOrganizationSettingView(QuotaMixin, generics.UpdateAPIView): serializer_class = UpdateOrganizationSettingSerializer queryset = OrganizationSetting.objects.select_related( "organization" @@ -19,6 +21,7 @@ class UpdateOrganizationSettingView(generics.UpdateAPIView): "organization__organization_custom_emails", "organization__organization_custom_page", ) + quota_classes = [WhitelabelQuota] def get_object(self): organization = get_object_or_404( diff --git a/bootstrap_service/settings.py b/bootstrap_service/settings.py index f279c5e..932e865 100644 --- a/bootstrap_service/settings.py +++ b/bootstrap_service/settings.py @@ -243,6 +243,8 @@ def silky_intercept_func(request): EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER", "") EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD", "") +BILLING_QUOTA_SERVICE = "apps.billing.services.quota_service.billing_quota_service" + # Inbox that receives contact-sales requests for manual follow-up. SALES_CONTACT_EMAIL = os.getenv("SALES_CONTACT_EMAIL", "sales@spacedf.com")