diff --git a/backend/metering_billing/invoice.py b/backend/metering_billing/invoice.py index 7ca0cfc17..f980827a9 100644 --- a/backend/metering_billing/invoice.py +++ b/backend/metering_billing/invoice.py @@ -15,7 +15,6 @@ now_utc, ) from metering_billing.utils.enums import FLAT_FEE_BILLING_TYPE, INVOICE_STATUS -from rest_framework import serializers from .webhooks import invoice_created_webhook @@ -27,11 +26,6 @@ def generate_invoice(subscription, draft=False, charge_next_plan=False): Generate an invoice for a subscription. """ from metering_billing.models import CustomerBalanceAdjustment, Invoice, PlanVersion - from metering_billing.serializers.internal_serializers import ( - InvoiceCustomerSerializer, - InvoiceOrganizationSerializer, - InvoiceSubscriptionSerializer, - ) from metering_billing.serializers.model_serializers import InvoiceSerializer issue_date = now_utc() @@ -185,15 +179,12 @@ def generate_invoice(subscription, draft=False, charge_next_plan=False): summary_dict = make_all_dates_times_strings(summary_dict) # create kwargs for invoice - org_serializer = InvoiceOrganizationSerializer(organization) - customer_serializer = InvoiceCustomerSerializer(customer) - subscription_serializer = InvoiceSubscriptionSerializer(subscription) invoice_kwargs = { "cost_due": amount, "issue_date": issue_date, - "organization": org_serializer.data, - "customer": customer_serializer.data, - "subscription": make_all_dates_times_strings(subscription_serializer.data), + "organization": organization, + "customer": customer, + "subscription": subscription, "payment_status": INVOICE_STATUS.DRAFT if draft else INVOICE_STATUS.UNPAID, "external_payment_obj_id": None, "external_payment_obj_type": None, diff --git a/backend/metering_billing/migrations/0067_rename_customer_historicalinvoice_old_customer_and_more.py b/backend/metering_billing/migrations/0067_rename_customer_historicalinvoice_old_customer_and_more.py new file mode 100644 index 000000000..97abe87f3 --- /dev/null +++ b/backend/metering_billing/migrations/0067_rename_customer_historicalinvoice_old_customer_and_more.py @@ -0,0 +1,51 @@ +# Generated by Django 4.0.5 on 2022-11-10 21:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("metering_billing", "0066_change_unique_rate_to_count"), + ] + + operations = [ + migrations.RenameField( + model_name="historicalinvoice", + old_name="customer", + new_name="old_customer", + ), + migrations.RenameField( + model_name="historicalinvoice", + old_name="organization", + new_name="old_organization", + ), + migrations.RenameField( + model_name="historicalinvoice", + old_name="subscription", + new_name="old_subscription", + ), + migrations.RenameField( + model_name="invoice", + old_name="customer", + new_name="old_customer", + ), + migrations.RenameField( + model_name="invoice", + old_name="organization", + new_name="old_organization", + ), + migrations.RenameField( + model_name="invoice", + old_name="subscription", + new_name="old_subscription", + ), + migrations.RemoveField( + model_name="historicalinvoice", + name="external_payment_obj", + ), + migrations.RemoveField( + model_name="invoice", + name="external_payment_obj", + ), + ] diff --git a/backend/metering_billing/migrations/0068_historicalinvoice_customer_and_more.py b/backend/metering_billing/migrations/0068_historicalinvoice_customer_and_more.py new file mode 100644 index 000000000..750d20f30 --- /dev/null +++ b/backend/metering_billing/migrations/0068_historicalinvoice_customer_and_more.py @@ -0,0 +1,83 @@ +# Generated by Django 4.0.5 on 2022-11-10 21:01 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "metering_billing", + "0067_rename_customer_historicalinvoice_old_customer_and_more", + ), + ] + + operations = [ + migrations.AddField( + model_name="historicalinvoice", + name="customer", + field=models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="metering_billing.customer", + ), + ), + migrations.AddField( + model_name="historicalinvoice", + name="organization", + field=models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="metering_billing.organization", + ), + ), + migrations.AddField( + model_name="historicalinvoice", + name="subscription", + field=models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="metering_billing.subscription", + ), + ), + migrations.AddField( + model_name="invoice", + name="customer", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="invoices", + to="metering_billing.customer", + ), + ), + migrations.AddField( + model_name="invoice", + name="organization", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="invoices", + to="metering_billing.organization", + ), + ), + migrations.AddField( + model_name="invoice", + name="subscription", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="invoices", + to="metering_billing.subscription", + ), + ), + ] diff --git a/backend/metering_billing/migrations/0069_transfer_invoice_to_fk.py b/backend/metering_billing/migrations/0069_transfer_invoice_to_fk.py new file mode 100644 index 000000000..7becd7071 --- /dev/null +++ b/backend/metering_billing/migrations/0069_transfer_invoice_to_fk.py @@ -0,0 +1,72 @@ +# Generated by Django 4.0.5 on 2022-11-10 21:02 + +from django.db import migrations + + +def migrate_jsonfields_to_fk(apps, schema_editor): + Invoice = apps.get_model("metering_billing", "Invoice") + Organization = apps.get_model("metering_billing", "Organization") + Subscription = apps.get_model("metering_billing", "Subscription") + Customer = apps.get_model("metering_billing", "Customer") + + invoice_org_dict = {} + invoice_customer_dict = {} + invoice_sub_dict = {} + for invoice in Invoice.objects.all(): + try: + invoice_org = invoice.old_organization["company_name"] + except: + print(invoice.__dict__) + raise + invoice_customer = invoice.old_customer["customer_id"] + invoice_sub = invoice.old_subscription["subscription_id"] + + if invoice_org in invoice_org_dict: + invoice_org_object = invoice_org_dict[invoice_org] + else: + try: + invoice_org_object = Organization.objects.get(company_name=invoice_org) + except Organization.DoesNotExist: + invoice_org_object = None + print(f"Invoice {invoice.pk} has no organization") + invoice_org_dict[invoice_org] = invoice_org_object + invoice.organization = invoice_org_object + + if invoice_customer in invoice_customer_dict: + invoice_customer_object = invoice_customer_dict[invoice_customer] + else: + try: + invoice_customer_object = Customer.objects.get( + organization=invoice_org_object, customer_id=invoice_customer + ) + except: + invoice_customer_object = None + print(f"Invoice {invoice.pk} has no customer") + invoice_customer_dict[invoice_customer] = invoice_customer_object + invoice.customer = invoice_customer_object + + if invoice_sub in invoice_sub_dict: + invoice_sub_object = invoice_sub_dict[invoice_sub] + else: + try: + invoice_sub_object = Subscription.objects.get( + organization=invoice_org_object, subscription_id=invoice_sub + ) + except: + invoice_sub_object = None + print(f"Invoice {invoice.pk} has no subscription") + invoice_sub_dict[invoice_sub] = invoice_sub_object + invoice.subscription = invoice_sub_object + + invoice.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("metering_billing", "0068_historicalinvoice_customer_and_more"), + ] + + operations = [ + migrations.RunPython(migrate_jsonfields_to_fk), + ] diff --git a/backend/metering_billing/migrations/0070_remove_historicalinvoice_old_customer_and_more.py b/backend/metering_billing/migrations/0070_remove_historicalinvoice_old_customer_and_more.py new file mode 100644 index 000000000..f8469875b --- /dev/null +++ b/backend/metering_billing/migrations/0070_remove_historicalinvoice_old_customer_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 4.0.5 on 2022-11-10 21:22 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("metering_billing", "0069_transfer_invoice_to_fk"), + ] + + operations = [ + migrations.RemoveField( + model_name="historicalinvoice", + name="old_customer", + ), + migrations.RemoveField( + model_name="historicalinvoice", + name="old_organization", + ), + migrations.RemoveField( + model_name="historicalinvoice", + name="old_subscription", + ), + migrations.RemoveField( + model_name="invoice", + name="old_customer", + ), + migrations.RemoveField( + model_name="invoice", + name="old_organization", + ), + migrations.RemoveField( + model_name="invoice", + name="old_subscription", + ), + ] diff --git a/backend/metering_billing/models.py b/backend/metering_billing/models.py index 19015431a..0ea9e325a 100644 --- a/backend/metering_billing/models.py +++ b/backend/metering_billing/models.py @@ -537,15 +537,20 @@ class Invoice(models.Model): invoice_id = models.CharField( max_length=100, null=False, blank=True, default=invoice_uuid, unique=True ) - external_payment_obj = models.JSONField(default=dict, blank=True, null=True) external_payment_obj_id = models.CharField(max_length=200, blank=True, null=True) external_payment_obj_type = models.CharField( choices=PAYMENT_PROVIDERS.choices, max_length=40, null=True, blank=True ) line_items = models.JSONField() - organization = models.JSONField() - customer = models.JSONField() - subscription = models.JSONField() + organization = models.ForeignKey( + Organization, on_delete=models.CASCADE, null=True, related_name="invoices" + ) + customer = models.ForeignKey( + Customer, on_delete=models.CASCADE, null=True, related_name="invoices" + ) + subscription = models.ForeignKey( + "Subscription", on_delete=models.CASCADE, null=True, related_name="invoices" + ) history = HistoricalRecords() def __str__(self): diff --git a/backend/metering_billing/payment_providers.py b/backend/metering_billing/payment_providers.py index b320f46f7..c5389024c 100644 --- a/backend/metering_billing/payment_providers.py +++ b/backend/metering_billing/payment_providers.py @@ -225,10 +225,6 @@ def import_payment_objects(self, organization): def _import_payment_objects_for_customer(self, customer): from metering_billing.models import Invoice - from metering_billing.serializers.internal_serializers import ( - InvoiceCustomerSerializer, - InvoiceOrganizationSerializer, - ) stripe.api_key = self.secret_key invoices = stripe.PaymentIntent.list( @@ -244,7 +240,7 @@ def _import_payment_objects_for_customer(self, customer): Decimal(stripe_invoice.amount) / 100, stripe_invoice.currency ) invoice_kwargs = { - "customer": InvoiceCustomerSerializer(customer).data, + "customer": customer, "cost_due": cost_due, "issue_date": datetime.datetime.fromtimestamp( stripe_invoice.created, pytz.utc @@ -253,10 +249,8 @@ def _import_payment_objects_for_customer(self, customer): "cust_connected_to_payment_provider": True, "external_payment_obj_id": stripe_invoice.id, "external_payment_obj_type": PAYMENT_PROVIDERS.STRIPE, - "organization": InvoiceOrganizationSerializer( - customer.organization - ).data, - "subscription": {}, + "organization": customer.organization, + "subscription": None, "line_items": {}, } lotus_invoice = Invoice.objects.create(**invoice_kwargs) @@ -315,12 +309,7 @@ def create_payment_object(self, invoice) -> str: stripe.api_key = self.secret_key # check everything works as expected + build invoice item assert invoice.external_payment_obj_id is None - organization = Organization.objects.get( - company_name=invoice.organization["company_name"] - ) - customer = Customer.objects.get( - organization=organization, customer_id=invoice.customer["customer_id"] - ) + customer = invoice.customer stripe_customer_id = customer.integrations.get( PAYMENT_PROVIDERS.STRIPE, {} ).get("id") diff --git a/backend/metering_billing/serializers/internal_serializers.py b/backend/metering_billing/serializers/internal_serializers.py index c547372ea..299cd664e 100644 --- a/backend/metering_billing/serializers/internal_serializers.py +++ b/backend/metering_billing/serializers/internal_serializers.py @@ -28,55 +28,3 @@ class PlanVersionNameSerializer(serializers.ModelSerializer): class Meta: model = PlanVersion fields = ("name",) - - -# Invoice Serializers -class InvoiceOrganizationSerializer(serializers.ModelSerializer): - class Meta: - model = Organization - fields = ("company_name",) - - -class InvoiceCustomerSerializer(serializers.ModelSerializer): - class Meta: - model = Customer - fields = ( - "customer_name", - "customer_id", - ) - - -class InvoicePlanVersionSerializer(serializers.ModelSerializer): - class Meta: - model = PlanVersion - fields = ( - "name", - "description", - "version", - "interval", - "flat_rate", - "flat_fee_billing_type", - ) - - flat_rate = serializers.SerializerMethodField() - name = serializers.CharField(source="plan.plan_name") - interval = serializers.CharField(source="plan.plan_duration") - - def get_flat_rate(self, obj): - return float(obj.flat_rate.amount) - - -class InvoiceSubscriptionSerializer(serializers.ModelSerializer): - class Meta: - model = Subscription - fields = ("start_date", "end_date", "billing_plan", "subscription_id") - - billing_plan = InvoicePlanVersionSerializer() - start_date = serializers.SerializerMethodField() - end_date = serializers.SerializerMethodField() - - def get_start_date(self, obj): - return obj.start_date - - def get_end_date(self, obj): - return obj.end_date diff --git a/backend/metering_billing/tasks.py b/backend/metering_billing/tasks.py index ae9cdf9d3..83b72db48 100644 --- a/backend/metering_billing/tasks.py +++ b/backend/metering_billing/tasks.py @@ -77,7 +77,7 @@ def calculate_invoice(): Invoice.objects.filter( issue_date__lt=now, payment_status=INVOICE_STATUS.DRAFT, - subscription__subscription_id=old_subscription.subscription_id, + subscription=old_subscription, ).delete() # Renew the subscription if old_subscription.auto_renew: diff --git a/backend/project_schema.yaml b/backend/project_schema.yaml index c76366884..9cebf6db0 100644 --- a/backend/project_schema.yaml +++ b/backend/project_schema.yaml @@ -2250,21 +2250,18 @@ components: type: object additionalProperties: {} organization: - type: object - additionalProperties: {} + type: integer + nullable: true customer: - type: object - additionalProperties: {} + type: integer + nullable: true subscription: - type: object - additionalProperties: {} + type: integer + nullable: true required: - cost_due - cost_due_currency - - customer - line_items - - organization - - subscription Event: type: object properties: @@ -2456,6 +2453,29 @@ components: InitialPlanVersion: type: object properties: + version: + type: integer + readOnly: true + description: + type: string + nullable: true + maxLength: 200 + price_adjustment: + $ref: '#/components/schemas/PriceAdjustment' + created_on: + type: string + format: date-time + readOnly: true + status: + allOf: + - $ref: '#/components/schemas/StatusEe1Enum' + readOnly: true + active_subscriptions: + type: integer + readOnly: true + created_by: + type: string + readOnly: true flat_rate: type: number format: double @@ -2463,23 +2483,13 @@ components: minimum: -10000000000 exclusiveMaximum: true exclusiveMinimum: true - version: - type: integer - readOnly: true - created_by: - type: string - readOnly: true components: type: array items: $ref: '#/components/schemas/PlanComponent' nullable: true - description: + version_id: type: string - nullable: true - maxLength: 200 - active_subscriptions: - type: integer readOnly: true flat_fee_billing_type: $ref: '#/components/schemas/FlatFeeBillingTypeEnum' @@ -2488,19 +2498,6 @@ components: items: $ref: '#/components/schemas/Feature' nullable: true - created_on: - type: string - format: date-time - readOnly: true - version_id: - type: string - readOnly: true - status: - allOf: - - $ref: '#/components/schemas/StatusEe1Enum' - readOnly: true - price_adjustment: - $ref: '#/components/schemas/PriceAdjustment' required: - active_subscriptions - created_by @@ -2542,22 +2539,19 @@ components: type: object additionalProperties: {} organization: - type: object - additionalProperties: {} + type: integer + nullable: true customer: - type: object - additionalProperties: {} + type: integer + nullable: true subscription: - type: object - additionalProperties: {} + type: integer + nullable: true required: - cost_due - cost_due_currency - - customer - line_items - - organization - payment_status - - subscription KpisEnum: enum: - total_revenue @@ -3026,55 +3020,55 @@ components: PlanDetail: type: object properties: - plan_duration: - $ref: '#/components/schemas/PlanDurationEnum' - num_versions: - type: integer + target_customer: + allOf: + - $ref: '#/components/schemas/CustomerNameAndID' readOnly: true - created_by: + created_on: type: string + format: date-time readOnly: true - product_id: - type: string - nullable: true external_links: type: array items: $ref: '#/components/schemas/ExternalPlanLink' readOnly: true - target_customer: - allOf: - - $ref: '#/components/schemas/CustomerNameAndID' + status: + $ref: '#/components/schemas/Status13eEnum' + versions: + type: array + items: + $ref: '#/components/schemas/PlanVersion' readOnly: true + product_id: + type: string + nullable: true active_subscriptions: type: integer readOnly: true - created_on: - type: string - format: date-time - readOnly: true - initial_external_links: - type: array - items: - $ref: '#/components/schemas/InitialExternalPlanLink' - writeOnly: true - plan_name: - type: string - maxLength: 100 parent_plan: allOf: - $ref: '#/components/schemas/PlanNameAndID' readOnly: true - versions: - type: array - items: - $ref: '#/components/schemas/PlanVersion' + plan_name: + type: string + maxLength: 100 + created_by: + type: string readOnly: true - status: - $ref: '#/components/schemas/Status13eEnum' plan_id: type: string maxLength: 100 + initial_external_links: + type: array + items: + $ref: '#/components/schemas/InitialExternalPlanLink' + writeOnly: true + num_versions: + type: integer + readOnly: true + plan_duration: + $ref: '#/components/schemas/PlanDurationEnum' required: - active_subscriptions - created_by