Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ django-timezone-field = "*"
braintree = "*"
pycountry = "*"
taxjar = "*"
usaddress-scourgify = "*"

[dev-packages]
platformdirs = "*"
Expand Down
1,890 changes: 1,009 additions & 881 deletions backend/Pipfile.lock

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions backend/api/serializers/model_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,12 @@ class Meta:
"subscriptions": {"required": True, "read_only": True},
"integrations": {"required": True, "read_only": True},
"default_currency": {"required": True, "read_only": True},
"payment_provider": {"required": True, "read_only": True},
"payment_provider": {
"required": True,
"read_only": True,
"allow_null": True,
"allow_blank": False,
},
"payment_provider_id": {
"required": True,
"read_only": True,
Expand All @@ -634,7 +639,7 @@ class Meta:
}

customer_id = serializers.CharField()
email = serializers.EmailField()
email = serializers.EmailField(allow_null=True)
subscriptions = serializers.SerializerMethodField()
invoices = serializers.SerializerMethodField()
total_amount_due = serializers.SerializerMethodField()
Expand Down Expand Up @@ -2756,3 +2761,4 @@ def create(self, validated_data):
sr.metadata = validated_data.get("metadata", {})
sr.save()
return sr
return sr
14 changes: 11 additions & 3 deletions backend/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,19 +240,25 @@ def get_queryset(self):
)
return qs

def get_serializer_class(self):
def get_serializer_class(self, default=None):
if self.action == "create":
return CustomerCreateSerializer
elif self.action == "archive":
return EmptySerializer

if default:
return default
return CustomerSerializer

@extend_schema(responses=CustomerSerializer)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
instance = self.perform_create(serializer)
customer_data = CustomerSerializer(instance).data

# return serializer
self.action = "retrieve"
customer_data = self.get_serializer(instance).data
customer_created_webhook(instance, customer_data=customer_data)
return Response(customer_data, status=status.HTTP_201_CREATED)

Expand Down Expand Up @@ -1525,9 +1531,11 @@ def get_queryset(self):

return Invoice.objects.filter(*args)

def get_serializer_class(self):
def get_serializer_class(self, default=None):
if self.action == "partial_update":
return InvoiceUpdateSerializer
if default:
return default
return InvoiceSerializer

@extend_schema(responses=InvoiceSerializer)
Expand Down
2 changes: 2 additions & 0 deletions backend/lotus/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@
CUSTOMER_ID_NAMESPACE = uuid.UUID("D1337E57-E6A0-4650-B1C3-D6487AFFB8CA")
EVENT_NAME_NAMESPACE = uuid.UUID("843D7005-63DE-4B72-B731-77E2866DCCFF")
IDEMPOTENCY_ID_NAMESPACE = uuid.UUID("904C0FFB-7005-414E-9B7D-8E3C5DDE266D")
# CRM Integration
VESSEL_API_KEY = config("VESSEL_API_KEY", default=None)

if SENTRY_DSN != "":
if not DEBUG:
Expand Down
32 changes: 24 additions & 8 deletions backend/lotus/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,14 @@
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
import api.views as api_views
from django.conf import settings
from django.conf.urls import include
from django.contrib import admin
from django.urls import path, re_path
from django.views.generic import TemplateView
from rest_framework import routers

import api.views as api_views
from metering_billing.views import auth_views, organization_views, webhook_views
from metering_billing.views.crm_views import CRMUnifiedAPIView
from metering_billing.views.model_views import (
ActionViewSet,
AddOnVersionViewSet,
Expand Down Expand Up @@ -60,6 +59,7 @@
TimezonesView,
TransferSubscriptionsView,
)
from rest_framework import routers

DEBUG = settings.DEBUG
PROFILER_ENABLED = settings.PROFILER_ENABLED
Expand Down Expand Up @@ -215,11 +215,6 @@
PaymentProcesorView.as_view(),
name="payment_providers",
),
# path(
# "app/experimental_to_active/",
# ExperimentalToActiveView.as_view(),
# name="expertimental-to-active",
# ),
path("app/login/", auth_views.LoginView.as_view(), name="api-login"),
path("app/demo_login/", auth_views.DemoLoginView.as_view(), name="api-demo-login"),
path("app/logout/", auth_views.LogoutView.as_view(), name="api-logout"),
Expand Down Expand Up @@ -254,6 +249,27 @@
path(
"stripe/webhook/", webhook_views.stripe_webhook_endpoint, name="stripe-webhook"
),
# crm
path(
"app/crm/link_token/",
CRMUnifiedAPIView.as_view({"post": "link_token"}),
name="link_token",
),
path(
"app/crm/store_token/",
CRMUnifiedAPIView.as_view({"post": "store_token"}),
name="store_token",
),
path(
"app/crm/",
CRMUnifiedAPIView.as_view({"get": "get_crms"}),
name="get_crms",
),
path(
"app/crm/set_customer_source/",
CRMUnifiedAPIView.as_view({"post": "update_crm_customer_source_of_truth"}),
name="set_customer_source",
),
]

if PROFILER_ENABLED:
Expand Down
18 changes: 18 additions & 0 deletions backend/metering_billing/exceptions/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ class OrganizationMismatch(APIException):
default_code = "authentication_failure"


class CRMNotSupported(APIException):
status_code = 400
default_detail = "CRM not supported"
default_code = "crm_not_supported"


class EnvironmentNotConnected(APIException):
status_code = 400
default_detail = "Environment not connected to Unified CRM API"
default_code = "environment_not_connected"


class CRMIntegrationNotAllowed(APIException):
status_code = 400
default_detail = "CRM Integration not allowed for Team"
default_code = "crm_integration_not_allowed"


class SubscriptionAlreadyEnded(APIException):
status_code = 400
default_detail = "Subscription already ended"
Expand Down
10 changes: 10 additions & 0 deletions backend/metering_billing/invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,9 @@ def generate_balance_adjustment_invoice(balance_adjustment, draft=False):


def generate_external_payment_obj(invoice):
from metering_billing.models import UnifiedCRMOrganizationIntegration
from metering_billing.views.crm_views import send_invoice_to_salesforce

customer = invoice.customer
pp = customer.payment_provider
if pp in PAYMENT_PROCESSOR_MAP and PAYMENT_PROCESSOR_MAP[pp].working():
Expand All @@ -760,6 +763,13 @@ def generate_external_payment_obj(invoice):
invoice.external_payment_obj_type = pp
invoice.save()
return invoice
if customer.salesforce_integration:
connection = customer.organization.unified_crm_organization_links.get(
crm_provider=UnifiedCRMOrganizationIntegration.CRMProvider.SALESFORCE
)
access_token = connection.access_token
accountId = customer.salesforce_integration.unified_account_id
send_invoice_to_salesforce(invoice, customer, accountId, access_token)
return None


Expand Down
6 changes: 6 additions & 0 deletions backend/metering_billing/management/commands/setup_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,9 @@ def handle(self, *args, **options):
task="metering_billing.tasks.check_past_due_invoices",
defaults={"interval": every_15_mins, "crontab": None},
)

PeriodicTask.objects.update_or_create(
name="Sync with CRM",
task="metering_billing.tasks.sync_all_crm_integrations",
defaults={"interval": every_hour, "crontab": None},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Generated by Django 4.0.5 on 2023-03-20 00:39

from django.db import migrations, models
import django.db.models.deletion
import metering_billing.utils.utils


class Migration(migrations.Migration):

dependencies = [
('metering_billing', '0228_auto_20230315_0342'),
]

operations = [
migrations.AddField(
model_name='historicalorganization',
name='crm_settings_provisioned',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='organization',
name='crm_settings_provisioned',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='team',
name='crm_integration_allowed',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='historicalorganizationsetting',
name='setting_group',
field=models.CharField(blank=True, choices=[('stripe', 'Stripe'), ('braintree', 'Braintree'), ('billing', 'Billing'), ('crm', 'CRM')], max_length=64, null=True),
),
migrations.AlterField(
model_name='historicalorganizationsetting',
name='setting_name',
field=models.CharField(choices=[('generate_customer_after_creating_in_lotus', 'Generate in Stripe after Lotus'), ('gen_cust_in_braintree_after_lotus', 'Generate in Braintree after Lotus'), ('subscription_filter_keys', 'Subscription Filter Keys'), ('payment_grace_period', 'Payment Grace Period'), ('crm_customer_source', 'CRM Customer Source')], max_length=64),
),
migrations.AlterField(
model_name='organizationsetting',
name='setting_group',
field=models.CharField(blank=True, choices=[('stripe', 'Stripe'), ('braintree', 'Braintree'), ('billing', 'Billing'), ('crm', 'CRM')], max_length=64, null=True),
),
migrations.AlterField(
model_name='organizationsetting',
name='setting_name',
field=models.CharField(choices=[('generate_customer_after_creating_in_lotus', 'Generate in Stripe after Lotus'), ('gen_cust_in_braintree_after_lotus', 'Generate in Braintree after Lotus'), ('subscription_filter_keys', 'Subscription Filter Keys'), ('payment_grace_period', 'Payment Grace Period'), ('crm_customer_source', 'CRM Customer Source')], max_length=64),
),
migrations.AlterField(
model_name='webhooktrigger',
name='trigger_name',
field=models.CharField(choices=[('customer.created', 'customer.created'), ('invoice.created', 'invoice.created'), ('invoice.paid', 'invoice.paid'), ('invoice.past_due', 'invoice.past_due'), ('subscription.created', 'subscription.created'), ('usage_alert.triggered', 'usage_alert.triggered'), ('subscription.cancelled', 'subscription.cancelled'), ('subscription.renewed', 'subscription.renewed')], max_length=40),
),
migrations.CreateModel(
name='UnifiedCRMOrganizationIntegration',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('crm_provider', models.IntegerField(choices=[(1, 'salesforce')])),
('access_token', models.TextField()),
('native_org_url', models.TextField()),
('native_org_id', models.TextField()),
('connection_id', models.TextField()),
('created', models.DateTimeField(default=metering_billing.utils.utils.now_utc)),
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='unified_crm_organization_links', to='metering_billing.organization')),
],
),
migrations.CreateModel(
name='UnifiedCRMInvoiceIntegration',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('crm_provider', models.IntegerField(choices=[(1, 'salesforce')])),
('native_invoice_id', models.TextField(null=True)),
('unified_note_id', models.TextField()),
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='unified_crm_invoice_links', to='metering_billing.organization')),
],
),
migrations.CreateModel(
name='UnifiedCRMCustomerIntegration',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('crm_provider', models.IntegerField(choices=[(1, 'salesforce')])),
('native_customer_id', models.TextField(null=True)),
('unified_account_id', models.TextField()),
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='unified_crm_customer_links', to='metering_billing.organization')),
],
),
migrations.AddField(
model_name='customer',
name='salesforce_integration',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customer', to='metering_billing.unifiedcrmcustomerintegration'),
),
migrations.AddField(
model_name='historicalcustomer',
name='salesforce_integration',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='metering_billing.unifiedcrmcustomerintegration'),
),
migrations.AddField(
model_name='historicalinvoice',
name='salesforce_integration',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='metering_billing.unifiedcrminvoiceintegration'),
),
migrations.AddField(
model_name='invoice',
name='salesforce_integration',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='metering_billing.unifiedcrminvoiceintegration'),
),
migrations.AddConstraint(
model_name='unifiedcrmorganizationintegration',
constraint=models.UniqueConstraint(fields=('organization', 'crm_provider'), name='unique_crm_provider'),
),
migrations.AddConstraint(
model_name='unifiedcrmcustomerintegration',
constraint=models.UniqueConstraint(condition=models.Q(('native_customer_id__isnull', False)), fields=('organization', 'crm_provider', 'native_customer_id'), name='unique_crm_customer_id_per_type'),
),
]
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,31 @@


class Migration(migrations.Migration):

dependencies = [
('metering_billing', '0228_auto_20230315_0342'),
("metering_billing", "0228_auto_20230315_0342"),
]

operations = [
migrations.AddField(
model_name='historicalsubscriptionrecord',
name='subscription_filters',
field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), size=2), default=list, size=None),
model_name="historicalsubscriptionrecord",
name="subscription_filters",
field=django.contrib.postgres.fields.ArrayField(
base_field=django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(), size=2
),
default=list,
size=None,
),
),
migrations.AddField(
model_name='subscriptionrecord',
name='subscription_filters',
field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), size=2), default=list, size=None),
),
migrations.AlterField(
model_name='webhooktrigger',
name='trigger_name',
field=models.CharField(choices=[('customer.created', 'customer.created'), ('invoice.created', 'invoice.created'), ('invoice.paid', 'invoice.paid'), ('invoice.past_due', 'invoice.past_due'), ('subscription.created', 'subscription.created'), ('usage_alert.triggered', 'usage_alert.triggered'), ('subscription.cancelled', 'subscription.cancelled'), ('subscription.renewed', 'subscription.renewed')], max_length=40),
model_name="subscriptionrecord",
name="subscription_filters",
field=django.contrib.postgres.fields.ArrayField(
base_field=django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(), size=2
),
default=list,
size=None,
),
),
]
14 changes: 14 additions & 0 deletions backend/metering_billing/migrations/0232_merge_20230320_0050.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Generated by Django 4.0.5 on 2023-03-20 00:50

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('metering_billing', '0229_historicalorganization_crm_settings_provisioned_and_more'),
('metering_billing', '0231_remove_subscriptionrecord_filters'),
]

operations = [
]
Loading