22import json
33import urllib .parse
44from datetime import timedelta
5+ from unittest .mock import patch
56
67import pytest
78from django .urls import reverse
1415 PlanComponent ,
1516 PlanVersion ,
1617 PriceTier ,
18+ RecurringCharge ,
1719 SubscriptionRecord ,
1820)
1921from metering_billing .serializers .serializer_utils import DjangoJSONEncoder
22+ from metering_billing .tasks import check_past_due_invoices_inner
2023from metering_billing .utils import now_utc
2124from metering_billing .utils .enums import (
2225 CHARGEABLE_ITEM_TYPE ,
@@ -43,7 +46,11 @@ def subscription_test_common_setup(
4346 add_plan_to_product ,
4447):
4548 def do_subscription_test_common_setup (
46- * , num_subscriptions , auth_method , user_org_and_api_key_org_different = False
49+ * ,
50+ num_subscriptions ,
51+ auth_method ,
52+ user_org_and_api_key_org_different = False ,
53+ setup_grace_period_setting = False
4754 ):
4855 # set up organizations and api keys
4956 org , key = generate_org_and_api_key ()
@@ -121,6 +128,14 @@ def do_subscription_test_common_setup(
121128 cost_per_batch = cpb ,
122129 metric_units_per_batch = mupb ,
123130 )
131+ RecurringCharge .objects .create (
132+ organization = plan .organization ,
133+ plan_version = billing_plan ,
134+ charge_timing = RecurringCharge .ChargeTimingType .IN_ADVANCE ,
135+ charge_behavior = RecurringCharge .ChargeBehaviorType .PRORATE ,
136+ amount = 10 ,
137+ pricing_unit = billing_plan .pricing_unit ,
138+ )
124139 setup_dict ["billing_plan" ] = billing_plan
125140
126141 (customer ,) = add_customers_to_org (org , n = 1 )
@@ -137,6 +152,19 @@ def do_subscription_test_common_setup(
137152 setup_dict ["payload" ] = payload
138153 setup_dict ["customer" ] = customer
139154
155+ if setup_grace_period_setting :
156+ payload = {
157+ "payment_grace_period" : 0 ,
158+ }
159+ response = setup_dict ["client" ].patch (
160+ reverse (
161+ "organization-detail" ,
162+ kwargs = {"organization_id" : setup_dict ["org" ].organization_id },
163+ ),
164+ data = json .dumps (payload , cls = DjangoJSONEncoder ),
165+ content_type = "application/json" ,
166+ )
167+ assert response .status_code == status .HTTP_200_OK
140168 return setup_dict
141169
142170 return do_subscription_test_common_setup
@@ -749,3 +777,115 @@ def test_refresh_rate_metric_doesnt_fail(self, subscription_test_common_setup):
749777 setup_dict ["org" ].update_subscription_filter_settings (["email" ])
750778 except Exception as e :
751779 assert False , e
780+
781+
782+ @pytest .mark .django_db (transaction = True )
783+ class TestInvoiceWebhooks :
784+ def test_invoice_paid_webhook (
785+ self ,
786+ subscription_test_common_setup ,
787+ ):
788+ setup_dict = subscription_test_common_setup (
789+ num_subscriptions = 1 , auth_method = "session_auth"
790+ )
791+
792+ prev_invoices_len = Invoice .objects .all ().count ()
793+
794+ params = {
795+ "customer_id" : setup_dict ["customer" ].customer_id ,
796+ }
797+ payload = {
798+ "flat_fee_behavior" : FLAT_FEE_BEHAVIOR .CHARGE_FULL ,
799+ "bill_usage" : True ,
800+ }
801+ response = setup_dict ["client" ].post (
802+ reverse ("subscription-cancel" ) + "?" + urllib .parse .urlencode (params ),
803+ data = json .dumps (payload , cls = DjangoJSONEncoder ),
804+ content_type = "application/json" ,
805+ )
806+
807+ new_invoices_len = Invoice .objects .all ().count ()
808+ assert response .status_code == status .HTTP_200_OK
809+ assert new_invoices_len == prev_invoices_len + 1
810+
811+ payload = {
812+ "payment_status" : "paid" ,
813+ }
814+ invoice = Invoice .objects .last ()
815+ with patch ("metering_billing.models.invoice_paid_webhook" ) as mock_webhook :
816+ response = setup_dict ["client" ].patch (
817+ reverse (
818+ "invoice-detail" ,
819+ kwargs = {"invoice_id" : invoice .invoice_id },
820+ ),
821+ data = json .dumps (payload , cls = DjangoJSONEncoder ),
822+ content_type = "application/json" ,
823+ )
824+ mock_webhook .assert_called_once_with (invoice , invoice .organization )
825+ assert response .status_code == status .HTTP_200_OK
826+
827+ def test_invoice_past_due_webhook_org_setting_not_set (
828+ self ,
829+ subscription_test_common_setup ,
830+ ):
831+ setup_dict = subscription_test_common_setup (
832+ num_subscriptions = 1 ,
833+ auth_method = "session_auth" ,
834+ )
835+
836+ prev_invoices_len = Invoice .objects .all ().count ()
837+
838+ params = {
839+ "customer_id" : setup_dict ["customer" ].customer_id ,
840+ }
841+ payload = {
842+ "flat_fee_behavior" : FLAT_FEE_BEHAVIOR .CHARGE_FULL ,
843+ "bill_usage" : True ,
844+ }
845+ response = setup_dict ["client" ].post (
846+ reverse ("subscription-cancel" ) + "?" + urllib .parse .urlencode (params ),
847+ data = json .dumps (payload , cls = DjangoJSONEncoder ),
848+ content_type = "application/json" ,
849+ )
850+
851+ new_invoices_len = Invoice .objects .all ().count ()
852+ assert response .status_code == status .HTTP_200_OK
853+ assert new_invoices_len == prev_invoices_len + 1
854+
855+ with patch ("metering_billing.tasks.invoice_past_due_webhook" ) as mock_webhook :
856+ check_past_due_invoices_inner ()
857+ mock_webhook .assert_not_called ()
858+
859+ def test_invoice_past_due_webhook_org_setting_set (
860+ self ,
861+ subscription_test_common_setup ,
862+ ):
863+ setup_dict = subscription_test_common_setup (
864+ num_subscriptions = 1 ,
865+ auth_method = "session_auth" ,
866+ setup_grace_period_setting = True ,
867+ )
868+
869+ prev_invoices_len = Invoice .objects .all ().count ()
870+
871+ params = {
872+ "customer_id" : setup_dict ["customer" ].customer_id ,
873+ }
874+ payload = {
875+ "flat_fee_behavior" : FLAT_FEE_BEHAVIOR .CHARGE_FULL ,
876+ "bill_usage" : True ,
877+ }
878+ response = setup_dict ["client" ].post (
879+ reverse ("subscription-cancel" ) + "?" + urllib .parse .urlencode (params ),
880+ data = json .dumps (payload , cls = DjangoJSONEncoder ),
881+ content_type = "application/json" ,
882+ )
883+
884+ new_invoices_len = Invoice .objects .all ().count ()
885+ assert response .status_code == status .HTTP_200_OK
886+ assert new_invoices_len == prev_invoices_len + 1
887+ invoice = Invoice .objects .last ()
888+
889+ with patch ("metering_billing.tasks.invoice_past_due_webhook" ) as mock_webhook :
890+ check_past_due_invoices_inner ()
891+ mock_webhook .assert_called_once_with (invoice , invoice .organization )
0 commit comments