diff --git a/backend/api/serializers/model_serializers.py b/backend/api/serializers/model_serializers.py index 951d56ac1..952b2958b 100644 --- a/backend/api/serializers/model_serializers.py +++ b/backend/api/serializers/model_serializers.py @@ -32,6 +32,7 @@ from metering_billing.payment_providers import PAYMENT_PROVIDER_MAP from metering_billing.serializers.serializer_utils import ( BalanceAdjustmentUUIDField, + FeatureUUIDField, InvoiceUUIDField, MetricUUIDField, PlanUUIDField, @@ -741,14 +742,21 @@ class FeatureSerializer( class Meta: model = Feature fields = ( + "feature_id", "feature_name", "feature_description", ) extra_kwargs = { - "feature_name": {"required": True}, - "feature_description": {"required": True}, + "feature_id": { + "required": True, + "read_only": True, + }, + "feature_name": {"required": True, "read_only": True}, + "feature_description": {"required": True, "read_only": True}, } + feature_id = FeatureUUIDField() + class PriceTierSerializer( ConvertEmptyStringToSerializerMixin, serializers.ModelSerializer @@ -1635,3 +1643,4 @@ class Meta: metric = MetricSerializer() plan_version = LightweightPlanVersionSerializer() plan_version = LightweightPlanVersionSerializer() + plan_version = LightweightPlanVersionSerializer() diff --git a/backend/metering_billing/serializers/model_serializers.py b/backend/metering_billing/serializers/model_serializers.py index dd63cf579..19cdaf4da 100644 --- a/backend/metering_billing/serializers/model_serializers.py +++ b/backend/metering_billing/serializers/model_serializers.py @@ -997,6 +997,12 @@ class Meta: price_adjustment_name = serializers.CharField(default="") +class FeatureCreateSerializer(serializers.ModelSerializer): + class Meta: + model = Feature + fields = ("feature_name", "feature_description") + + class PlanVersionCreateSerializer(serializers.ModelSerializer): class Meta: model = PlanVersion @@ -1038,7 +1044,13 @@ class Meta: components = PlanComponentCreateSerializer( many=True, allow_null=True, required=False, source="plan_components" ) - features = FeatureSerializer(many=True, allow_null=True, required=False) + features = SlugRelatedFieldWithOrganization( + slug_field="feature_id", + queryset=Feature.objects.all(), + many=True, + allow_null=True, + required=False, + ) price_adjustment = PriceAdjustmentSerializer(required=False) plan_id = SlugRelatedFieldWithOrganization( slug_field="plan_id", @@ -1135,16 +1147,17 @@ def create(self, validated_data): for component in components: component.plan_version = billing_plan component.save() - for feature_data in features_data: - feature_data["organization"] = org - description = feature_data.pop("description", None) - try: - f, created = Feature.objects.get_or_create(**feature_data) - if created and description: - f.description = description - f.save() - except Feature.MultipleObjectsReturned: - f = Feature.objects.filter(**feature_data).first() + for f in features_data: + # feature_data["organization"] = org + # description = feature_data.pop("description", None) + # try: + # f, created = Feature.objects.get_or_create(**feature_data) + # if created and description: + # f.description = description + # f.save() + # except Feature.MultipleObjectsReturned: + # f = Feature.objects.filter(**feature_data).first() + assert type(f) is Feature billing_plan.features.add(f) if price_adjustment_data: price_adjustment_data["organization"] = org diff --git a/backend/metering_billing/serializers/serializer_utils.py b/backend/metering_billing/serializers/serializer_utils.py index af950eb80..3f92563b5 100644 --- a/backend/metering_billing/serializers/serializer_utils.py +++ b/backend/metering_billing/serializers/serializer_utils.py @@ -15,6 +15,7 @@ def get_queryset(self): def to_internal_value(self, data): from metering_billing.models import ( CustomerBalanceAdjustment, + Feature, Metric, Plan, PlanVersion, @@ -28,11 +29,14 @@ def to_internal_value(self, data): data = PlanUUIDField().to_internal_value(data) elif self.queryset.model is PlanVersion: data = PlanVersionUUIDField().to_internal_value(data) + elif self.queryset.model is Feature: + data = FeatureUUIDField().to_internal_value(data) return super().to_internal_value(data) def to_representation(self, obj): from metering_billing.models import ( CustomerBalanceAdjustment, + Feature, Metric, Plan, PlanVersion, @@ -47,6 +51,8 @@ def to_representation(self, obj): return PlanUUIDField().to_representation(obj.plan_id) elif isinstance(obj, PlanVersion): return PlanVersionUUIDField().to_representation(obj.version_id) + elif isinstance(obj, Feature): + return FeatureUUIDField().to_representation(obj.feature_id) return repr @@ -144,6 +150,12 @@ def __init__(self, *args, **kwargs): super().__init__("plan_version_", *args, **kwargs) +@extend_schema_field(serializers.RegexField(regex=r"feature_[0-9a-f]{32}")) +class FeatureUUIDField(UUIDPrefixField): + def __init__(self, *args, **kwargs): + super().__init__("feature_", *args, **kwargs) + + @extend_schema_field(serializers.RegexField(regex=r"sub_[0-9a-f]{32}")) class SubscriptionUUIDField(UUIDPrefixField): def __init__(self, *args, **kwargs): diff --git a/backend/metering_billing/views/model_views.py b/backend/metering_billing/views/model_views.py index 8082a846d..dbdbdbe88 100644 --- a/backend/metering_billing/views/model_views.py +++ b/backend/metering_billing/views/model_views.py @@ -46,6 +46,7 @@ CustomerUpdateSerializer, EventSerializer, ExternalPlanLinkSerializer, + FeatureCreateSerializer, FeatureSerializer, MetricCreateSerializer, MetricSerializer, @@ -527,9 +528,15 @@ class FeatureViewSet( } queryset = Feature.objects.all() + def get_serializer_class(self): + if self.action == "create": + return FeatureCreateSerializer + return FeatureSerializer + def get_queryset(self): organization = self.request.organization - return Feature.objects.filter(organization=organization) + objs = Feature.objects.filter(organization=organization) + return objs def get_serializer_context(self): context = super().get_serializer_context() @@ -558,8 +565,16 @@ def dispatch(self, request, *args, **kwargs): ) return response + @extend_schema(responses=FeatureSerializer) + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + instance = self.perform_create(serializer) + feature_data = FeatureSerializer(instance).data + return Response(feature_data, status=status.HTTP_201_CREATED) + def perform_create(self, serializer): - serializer.save(organization=self.request.organization) + return serializer.save(organization=self.request.organization) class PlanVersionViewSet(PermissionPolicyMixin, viewsets.ModelViewSet): @@ -1131,3 +1146,4 @@ def get_serializer_context(self): organization = self.request.organization context.update({"organization": organization}) return context + return context diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 9385efc74..f34d77630 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -54,7 +54,7 @@ import { OrganizationType, PaginatedActionsType, } from "../types/account-type"; -import { FeatureType } from "../types/feature-type"; +import { FeatureType, CreateFeatureType } from "../types/feature-type"; import Cookies from "universal-cookie"; import { CreateBacktestType, @@ -408,7 +408,7 @@ export const PlansByCustomer = { export const Features = { getFeatures: (): Promise => requests.get("app/features/"), - createFeature: (post: FeatureType): Promise => + createFeature: (post: CreateFeatureType): Promise => requests.post("app/features/", post), }; diff --git a/frontend/src/components/Plans/FeatureDisplay.tsx b/frontend/src/components/Plans/FeatureDisplay.tsx index f155ff20b..1d0e415d9 100644 --- a/frontend/src/components/Plans/FeatureDisplay.tsx +++ b/frontend/src/components/Plans/FeatureDisplay.tsx @@ -21,7 +21,7 @@ export const FeatureDisplay: FC<{ type="text" icon={} danger - onClick={() => removeFeature(feature.feature_name)} + onClick={() => removeFeature(feature.feature_id)} />
diff --git a/frontend/src/components/Plans/FeatureForm.tsx b/frontend/src/components/Plans/FeatureForm.tsx index 357799f3f..31cf6d777 100644 --- a/frontend/src/components/Plans/FeatureForm.tsx +++ b/frontend/src/components/Plans/FeatureForm.tsx @@ -1,8 +1,9 @@ import React, { useState } from "react"; -import { FeatureType } from "../../types/feature-type"; +import { FeatureType, CreateFeatureType } from "../../types/feature-type"; import { Button, Divider, Modal, Select, Input, message } from "antd"; import { Features } from "../../api/api"; import { UseQueryResult, useQuery } from "react-query"; +import { useMutation, useQueryClient } from "react-query"; const { Option } = Select; @@ -15,7 +16,7 @@ const FeatureForm = (props: { const [createdFeatureName, setCreatedFeatureName] = useState(""); const [createdFeatureDescription, setCreatedFeatureDescription] = useState(""); - + const queryClient = useQueryClient(); const { data: features, isLoading, @@ -49,11 +50,15 @@ const FeatureForm = (props: { if (featureExists) { message.error("Feature already exists"); } else { - const newFeature: FeatureType = { + const newFeature: CreateFeatureType = { feature_name: createdFeatureName, feature_description: createdFeatureDescription, }; - setNewFeatures([...newFeatures, newFeature]); + + Features.createFeature(newFeature).then((res) => { + queryClient.invalidateQueries("feature_list"); + setNewFeatures([...newFeatures, res]); + }); setCreatedFeatureName(""); setCreatedFeatureDescription(""); } diff --git a/frontend/src/pages/CreatePlan.tsx b/frontend/src/pages/CreatePlan.tsx index 04a8edf2f..b980d4ab7 100644 --- a/frontend/src/pages/CreatePlan.tsx +++ b/frontend/src/pages/CreatePlan.tsx @@ -23,7 +23,7 @@ import { PlanType, } from "../types/plan-type"; import { Plan, Organization } from "../api/api"; -import { FeatureType } from "../types/feature-type"; +import { CreateFeatureType, FeatureType } from "../types/feature-type"; import FeatureForm from "../components/Plans/FeatureForm"; import LinkExternalIds from "../components/Plans/LinkExternalIds"; import { PageLayout } from "../components/base/PageLayout"; @@ -49,8 +49,8 @@ const durationConversion = { const CreatePlan = () => { const [componentVisible, setcomponentVisible] = useState(); const [allPlans, setAllPlans] = useState([]); - const [allCurrencies, setAllCurrencies] = useState([]); - const [selectedCurrency, setSelectedCurrency] = useState({ + const [allCurrencies, setAllCurrencies] = useState([]); + const [selectedCurrency, setSelectedCurrency] = useState({ symbol: "", code: "", name: "", @@ -112,7 +112,7 @@ const CreatePlan = () => { for (let i = 0; i < newFeatures.length; i++) { if ( planFeatures.some( - (feat) => feat.feature_name === newFeatures[i].feature_name + (feat) => feat.feature_id === newFeatures[i].feature_id ) ) { } else { @@ -129,9 +129,9 @@ const CreatePlan = () => { setFeatureVisible(true); }; - const removeFeature = (feature_name: string) => { + const removeFeature = (feature_id: string) => { setPlanFeatures( - planFeatures.filter((item) => item.feature_name !== feature_name) + planFeatures.filter((item) => item.feature_id !== feature_id) ); }; @@ -213,6 +213,14 @@ const CreatePlan = () => { } } + const featureIdList: string[] = []; + const features: any = Object.values(planFeatures); + if (features) { + for (let i = 0; i < features.length; i++) { + featureIdList.push(features[i].feature_id); + } + } + if (values.usage_billing_frequency === "yearly") { values.usage_billing_frequency = "end_of_period"; } @@ -222,7 +230,7 @@ const CreatePlan = () => { transition_to_plan_id: values.transition_to_plan_id, flat_rate: values.flat_rate, components: usagecomponentslist, - features: planFeatures, + features: featureIdList, usage_billing_frequency: values.usage_billing_frequency, currency_code: values.plan_currency, }; diff --git a/frontend/src/pages/EditPlan.tsx b/frontend/src/pages/EditPlan.tsx index a5cedf896..28fad58b0 100644 --- a/frontend/src/pages/EditPlan.tsx +++ b/frontend/src/pages/EditPlan.tsx @@ -61,7 +61,7 @@ const EditPlan = ({ type, plan, versionIndex }: Props) => { useState(false); const [activeVersion, setActiveVersion] = useState(false); const [activeVersionType, setActiveVersionType] = useState(); - const [allCurrencies, setAllCurrencies] = useState([]); + const [allCurrencies, setAllCurrencies] = useState([]); const navigate = useNavigate(); const [componentsData, setComponentsData] = useState([]); const [form] = Form.useForm(); @@ -80,7 +80,7 @@ const EditPlan = ({ type, plan, versionIndex }: Props) => { plan.versions[versionIndex].price_adjustment?.price_adjustment_type ?? "none" ); - const [selectedCurrency, setSelectedCurrency] = useState( + const [selectedCurrency, setSelectedCurrency] = useState( plan.versions[versionIndex].currency ?? { symbol: "", code: "", @@ -301,6 +301,16 @@ const EditPlan = ({ type, plan, versionIndex }: Props) => { usagecomponentslist.push(usagecomponent); } } + + const featureIdList: string[] = []; + const features: any = Object.values(planFeatures); + if (features) { + for (let i = 0; i < features.length; i++) { + featureIdList.push(features[i].feature_id); + } + } + + if (values.usage_billing_frequency === "yearly") { values.usage_billing_frequency = "end_of_period"; } @@ -311,7 +321,7 @@ const EditPlan = ({ type, plan, versionIndex }: Props) => { transition_to_plan_id: values.transition_to_plan_id, flat_rate: values.flat_rate, components: usagecomponentslist, - features: planFeatures, + features: featureIdList, usage_billing_frequency: values.usage_billing_frequency, currency_code: values.plan_currency.code, }; @@ -361,7 +371,7 @@ const EditPlan = ({ type, plan, versionIndex }: Props) => { transition_to_plan_id: values.transition_to_plan_id, flat_rate: values.flat_rate, components: usagecomponentslist, - features: planFeatures, + features: featureIdList, usage_billing_frequency: values.usage_billing_frequency, make_active: activeVersion, make_active_type: activeVersionType, diff --git a/frontend/src/types/feature-type.ts b/frontend/src/types/feature-type.ts index 0293c8888..d055938b2 100644 --- a/frontend/src/types/feature-type.ts +++ b/frontend/src/types/feature-type.ts @@ -1,4 +1,10 @@ export interface FeatureType { + feature_id: string; + feature_name: string; + feature_description: string; +} + +export interface CreateFeatureType { feature_name: string; feature_description: string; } diff --git a/frontend/src/types/plan-type.ts b/frontend/src/types/plan-type.ts index f85f3337d..7cdb3bce6 100644 --- a/frontend/src/types/plan-type.ts +++ b/frontend/src/types/plan-type.ts @@ -28,7 +28,7 @@ export interface PlanDetailType extends PlanType { export interface CreatePlanVersionType { description?: string; plan_id?: string; - features: FeatureType[]; + features: string[]; components: CreateComponent[]; flat_rate: number; usage_billing_frequency?: string; @@ -52,7 +52,10 @@ export interface PriceAdjustment { } export interface PlanVersionType - extends Omit { + extends Omit< + CreatePlanVersionType, + "components" | "currency_code" | "features" + > { description: string; plan_id: string; flat_fee_billing_type: string;