Skip to content

Commit

Permalink
use stripe invoice statuses (#712)
Browse files Browse the repository at this point in the history
  • Loading branch information
diego-escobedo authored Mar 20, 2023
1 parent 9939628 commit 3852fa0
Show file tree
Hide file tree
Showing 15 changed files with 154 additions and 41 deletions.
1 change: 1 addition & 0 deletions backend/api/serializers/model_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@ class Meta:
"payment_status",
"external_payment_obj_id",
"external_payment_obj_type",
"external_payment_obj_status",
"line_items",
"customer",
"due_date",
Expand Down
39 changes: 24 additions & 15 deletions backend/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,13 @@
Tag,
)
from metering_billing.permissions import HasUserAPIKey, ValidOrganization
from metering_billing.serializers.model_serializers import DraftInvoiceSerializer, MetricDetailSerializer
from metering_billing.serializers.model_serializers import (
DraftInvoiceSerializer,
MetricDetailSerializer,
)
from metering_billing.serializers.request_serializers import (
DraftInvoiceRequestSerializer,
CostAnalysisRequestSerializer,
PeriodRequestSerializer,
)
from metering_billing.serializers.response_serializers import CostAnalysisSerializer
from metering_billing.serializers.serializer_utils import (
Expand All @@ -127,14 +130,14 @@
SubscriptionUUIDField,
)
from metering_billing.utils import (
calculate_end_date,
convert_to_datetime,
now_utc,
dates_bwn_two_dts,
calculate_end_date,
convert_to_date,
convert_to_datetime,
convert_to_decimal,
dates_bwn_two_dts,
make_all_dates_times_strings,
make_all_decimals_floats,
make_all_dates_times_strings
now_utc,
)
from metering_billing.utils.enums import (
CUSTOMER_BALANCE_ADJUSTMENT_STATUS,
Expand Down Expand Up @@ -339,20 +342,22 @@ def draft_invoice(self, request, customer_id=None):
invoice.delete()
response = {"invoices": serializer or []}
return Response(response, status=status.HTTP_200_OK)

@extend_schema(
request=CostAnalysisRequestSerializer,
parameters=[CostAnalysisRequestSerializer],
request=None,
parameters=[PeriodRequestSerializer],
responses={200: CostAnalysisSerializer},
)
@action(detail=True, methods=["get"], url_path="cost_analysis", url_name="cost_analysis")
@action(
detail=True, methods=["get"], url_path="cost_analysis", url_name="cost_analysis"
)
def cost_analysis(self, request, customer_id=None):
organization = request.organization
serializer = CostAnalysisRequestSerializer(
serializer = PeriodRequestSerializer(
data=request.query_params, context={"organization": organization}
)
serializer.is_valid(raise_exception=True)
customer = serializer.validated_data["customer"]
customer = self.get_object()
start_date, end_date = (
serializer.validated_data.get(key, None)
for key in ["start_date", "end_date"]
Expand Down Expand Up @@ -390,9 +395,9 @@ def cost_analysis(self, request, customer_id=None):
}
per_day_dict[date]["cost_data"][metric.billable_metric_name][
"cost"
] += usage
] += usage
for date, items in per_day_dict.items():
items["cost_data"] = [v for k,v in items["cost_data"].items()]
items["cost_data"] = [v for k, v in items["cost_data"].items()]
subscriptions = (
SubscriptionRecord.objects.filter(
Q(start_date__range=[start_time, end_time])
Expand Down Expand Up @@ -2576,3 +2581,7 @@ def get(self, request, format=None):
metrics,
status=status.HTTP_200_OK,
)
return Response(
metrics,
status=status.HTTP_200_OK,
)
3 changes: 2 additions & 1 deletion backend/metering_billing/invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -757,10 +757,11 @@ def generate_external_payment_obj(invoice):
customer_conn = pp_connector.customer_connected(customer)
org_conn = pp_connector.organization_connected(invoice.organization)
if customer_conn and org_conn:
external_id = pp_connector.create_payment_object(invoice)
external_id, external_status = pp_connector.create_payment_object(invoice)
if external_id:
invoice.external_payment_obj_id = external_id
invoice.external_payment_obj_type = pp
invoice.external_payment_obj_status = external_status
invoice.save()
return invoice
if customer.salesforce_integration:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.0.5 on 2023-03-20 03:34

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('metering_billing', '0232_merge_20230320_0050'),
]

operations = [
migrations.AddField(
model_name='historicalinvoice',
name='external_payment_obj_status',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='invoice',
name='external_payment_obj_status',
field=models.TextField(blank=True, null=True),
),
]
1 change: 1 addition & 0 deletions backend/metering_billing/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1853,6 +1853,7 @@ class PaymentStatus(models.IntegerChoices):
external_payment_obj_type = models.CharField(
choices=PAYMENT_PROCESSORS.choices, max_length=40, blank=True, null=True
)
external_payment_obj_status = models.TextField(blank=True, null=True)
salesforce_integration = models.OneToOneField(
"UnifiedCRMInvoiceIntegration", on_delete=models.SET_NULL, null=True, blank=True
)
Expand Down
22 changes: 13 additions & 9 deletions backend/metering_billing/payment_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import datetime
import logging
from decimal import Decimal
from typing import Literal, Optional
from typing import Literal, Optional, Tuple
from urllib.parse import urlencode

import braintree
Expand Down Expand Up @@ -153,8 +153,8 @@ def create_customer_flow(self, customer) -> None:
pass

@abc.abstractmethod
def create_payment_object(self, invoice) -> Optional[str]:
"""This method will be called when an external payment object needs to be generated (this can vary greatly depending on the payment processor). It should return the id of this object as a string so that the status of the payment can later be updated."""
def create_payment_object(self, invoice) -> Tuple[Optional[str], Optional[str]]:
"""This method will be called when an external payment object needs to be generated (this can vary greatly depending on the payment processor). It should return the id of this object and its status as a tuple of strings."""
pass

# FRONTEND REQUEST METHODS
Expand Down Expand Up @@ -476,7 +476,7 @@ def create_customer_flow(self, customer) -> None:
"Invalid value for generate_customer_after_creating_in_lotus setting"
)

def create_payment_object(self, invoice) -> Optional[str]:
def create_payment_object(self, invoice) -> Tuple[Optional[str], Optional[str]]:
gateway = self._get_gateway(invoice.organization)
# check everything works as expected + build invoice item
assert (
Expand Down Expand Up @@ -515,11 +515,12 @@ def create_payment_object(self, invoice) -> Optional[str]:
result = gateway.transaction.sale(invoice_kwargs)
if result.is_success:
invoice.external_payment_obj_id = result.transaction.id
invoice.external_payment_obj_status = result.transaction.status
invoice.save()
return result.transaction.id
return result.transaction.id, result.transaction.status
else:
logger.error("Ran into error:", result.message)
return None
return None, None

def update_payment_object_status(self, organization, payment_object_id):
from metering_billing.models import Invoice
Expand Down Expand Up @@ -1034,6 +1035,7 @@ 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_PROCESSORS.STRIPE,
"external_payment_obj_status": stripe_invoice.status,
"organization": customer.organization,
}
lotus_invoice = Invoice.objects.create(**invoice_kwargs)
Expand Down Expand Up @@ -1085,7 +1087,7 @@ def create_customer_flow(self, customer) -> None:
"Invalid value for generate_customer_after_creating_in_lotus setting"
)

def create_payment_object(self, invoice) -> Optional[str]:
def create_payment_object(self, invoice) -> Tuple[Optional[str], Optional[str]]:
from metering_billing.models import Organization

organization = invoice.organization
Expand Down Expand Up @@ -1114,6 +1116,7 @@ def create_payment_object(self, invoice) -> Optional[str]:
), "Organization does not have a Stripe account ID"
invoice_kwargs["stripe_account"] = org_stripe_acct

stripe_invoice = stripe.Invoice.create(**invoice_kwargs)
for line_item in invoice.line_items.all().order_by(
F("associated_subscription_record").desc(nulls_last=True)
):
Expand Down Expand Up @@ -1141,12 +1144,13 @@ def create_payment_object(self, invoice) -> Optional[str]:
"currency": invoice.currency.code.lower(),
"tax_behavior": tax_behavior,
"metadata": metadata,
"invoice": stripe_invoice.id,
}
if not self.self_hosted:
inv_dict["stripe_account"] = org_stripe_acct
stripe.InvoiceItem.create(**inv_dict)
stripe_invoice = stripe.Invoice.create(**invoice_kwargs)
return stripe_invoice.id

return stripe_invoice.id, stripe_invoice.status

def get_post_data_serializer(self) -> serializers.Serializer:
class StripePostRequestDataSerializer(serializers.Serializer):
Expand Down
2 changes: 1 addition & 1 deletion backend/metering_billing/serializers/model_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1987,7 +1987,7 @@ def get_payment_provider_url(
self, obj
) -> serializers.URLField(allow_null=True, required=True):
if obj.stripe_integration:
return f"https://dashboard.stripe.com/customers/{obj.stripe_customer_id}"
return f"https://dashboard.stripe.com/customers/{obj.stripe_integration.stripe_customer_id}"
return None

def get_crm_provider_url(
Expand Down
3 changes: 2 additions & 1 deletion backend/metering_billing/views/model_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1745,10 +1745,11 @@ def send(self, request, *args, **kwargs):
if customer.payment_provider and invoice.external_payment_obj_type is None:
connector = PAYMENT_PROCESSOR_MAP.get(customer.payment_provider)
if connector:
external_id = connector.create_payment_object(invoice)
external_id, external_status = connector.create_payment_object(invoice)
if external_id:
invoice.external_payment_obj_id = external_id
invoice.external_payment_obj_type = customer.payment_provider
invoice.external_payment_obj_status = external_status
invoice.save()
serializer = self.get_serializer(invoice)
return Response(serializer.data, status=status.HTTP_200_OK)
Expand Down
19 changes: 16 additions & 3 deletions backend/metering_billing/views/webhook_views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import stripe
from django.conf import settings
from django.views.decorators.csrf import csrf_exempt
from metering_billing.models import Invoice
from metering_billing.utils.enums import PAYMENT_PROCESSORS
from rest_framework import status
from rest_framework.decorators import (
api_view,
Expand All @@ -9,9 +11,6 @@
)
from rest_framework.response import Response

from metering_billing.models import Invoice
from metering_billing.utils.enums import PAYMENT_PROCESSORS

STRIPE_WEBHOOK_SECRET = settings.STRIPE_WEBHOOK_SECRET
STRIPE_TEST_SECRET_KEY = settings.STRIPE_TEST_SECRET_KEY
STRIPE_LIVE_SECRET_KEY = settings.STRIPE_LIVE_SECRET_KEY
Expand All @@ -28,6 +27,17 @@ def _invoice_paid_handler(event):
matching_invoice.save()


def _invoice_updated_handler(event):
invoice = event["data"]["object"]
id = invoice.id
matching_invoice = Invoice.objects.filter(
external_payment_obj_type=PAYMENT_PROCESSORS.STRIPE, external_payment_obj_id=id
).first()
if matching_invoice:
matching_invoice.external_payment_obj_status = invoice.status
matching_invoice.save()


@api_view(http_method_names=["POST"])
@csrf_exempt
@permission_classes([])
Expand All @@ -51,5 +61,8 @@ def stripe_webhook_endpoint(request):
if event["type"] == "invoice.paid":
_invoice_paid_handler(event)

if event["type"] == "invoice.updated":
_invoice_updated_handler(event)

# Passed signature verification
return Response(status=status.HTTP_200_OK)
1 change: 1 addition & 0 deletions backend/scripts/start_backend.dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ while ! nc -q 1 svix-server 8071 </dev/null; do sleep 5; done
python3 manage.py wait_for_db && \
python3 manage.py migrate && \
python3 manage.py initadmin && \
python3 manage.py demo_up && \
python3 manage.py setup_tasks && \
python3 manage.py runserver 0.0.0.0:8000
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@tanstack/react-query": "4.3.9",
"@tremor/react": "^1.0.7",
"@types/node": "18.7.3",
"@types/react-syntax-highlighter": "^15.5.6",
"@typescript-eslint/eslint-plugin": "^5.48.2",
"@typescript-eslint/parser": "^5.48.2",
"@vesselapi/react-vessel-link": "^1.1.6",
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,8 @@ export const Customer = {
start_date: string,
end_date: string
): Promise<CustomerCostType> {
return requests.get(`app/cost_analysis/`, {
params: { customer_id, start_date, end_date },
return requests.get(`app/customers/${customer_id}/cost_analysis/`, {
params: { start_date, end_date },
});
},
createSubscription: (
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/Customers/CustomerInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ const CustomerInfoView: FC<CustomerInfoViewProps> = ({
</div>
<div className="Inter">
{data.default_currency.symbol}
{cost_data.total_revenue.toFixed(2)}
{(cost_data.total_revenue || 0).toFixed(2)}
</div>
</CustomerCard.Item>

Expand All @@ -646,7 +646,7 @@ const CustomerInfoView: FC<CustomerInfoViewProps> = ({
</div>
<div className="Inter">
{data.default_currency.symbol}
{cost_data.total_cost.toFixed(2)}
{(cost_data.total_cost || 0).toFixed(2)}
</div>
</CustomerCard.Item>

Expand Down
Loading

0 comments on commit 3852fa0

Please sign in to comment.