diff --git a/memberportal/api_billing/views.py b/memberportal/api_billing/views.py index 35f9bc7e..a3e537de 100644 --- a/memberportal/api_billing/views.py +++ b/memberportal/api_billing/views.py @@ -1,4 +1,5 @@ from asgiref.sync import sync_to_async +from django.http import HttpRequest from profile.models import Profile from access.models import Doors, Interlock @@ -207,102 +208,82 @@ class PaymentPlanSignup(StripeAPIView): post: attempts to sign the member up to a new payment plan. """ - def post(self, request, plan_id): - current_plan = request.user.profile.membership_plan - new_plan = PaymentPlan.objects.get(pk=plan_id) + def create_subscription( + self, request: HttpRequest, new_plan: PaymentPlan, attempts: int = 0 + ): + attempts += 1 - if current_plan: - return Response({"success": False}, status=status.HTTP_409_CONFLICT) + if attempts > 3: + request.user.log_event( + "Too many attempts while creating subscription.", + "stripe", + "", + ) + return Response( + { + "success": False, + "message": None, + }, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) - def create_subscription(attempts=0): - attempts += 1 + try: + return stripe.Subscription.create( + customer=request.user.profile.stripe_customer_id, + items=[ + {"price": new_plan.stripe_id}, + ], + ) + + except stripe.error.InvalidRequestError as e: + capture_exception(e) + error = e.json_body.get("error") - if attempts > 3: + if ( + error["code"] == "resource_missing" + and "default payment method" in error["message"] + ): request.user.log_event( - "Too many attempts while creating subscription.", + "InvalidRequestError (missing default payment method) from Stripe while creating subscription.", "stripe", - "", + error, + ) + + # try to set the default and try again + stripe.Customer.modify( + request.user.profile.stripe_customer_id, + invoice_settings={ + "default_payment_method": request.user.profile.stripe_payment_method_id, + }, ) + + return self.create_subscription(attempts) + + if ( + error["code"] == "resource_missing" + and "a similar object exists in live mode" in error["message"] + ): + request.user.log_event( + "InvalidRequestError (used test key with production object) from Stripe while " + "creating subscription.", + "stripe", + error, + ) + return Response( { "success": False, - "message": None, + "message": error["message"], }, status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) - try: - return stripe.Subscription.create( - customer=request.user.profile.stripe_customer_id, - items=[ - {"price": new_plan.stripe_id}, - ], - ) - - except stripe.error.InvalidRequestError as e: - capture_exception(e) - error = e.json_body.get("error") - - if ( - error["code"] == "resource_missing" - and "default payment method" in error["message"] - ): - request.user.log_event( - "InvalidRequestError (missing default payment method) from Stripe while creating subscription.", - "stripe", - error, - ) - - # try to set the default and try again - stripe.Customer.modify( - request.user.profile.stripe_customer_id, - invoice_settings={ - "default_payment_method": request.user.profile.stripe_payment_method_id, - }, - ) - - return create_subscription(attempts) - - if ( - error["code"] == "resource_missing" - and "a similar object exists in live mode" in error["message"] - ): - request.user.log_event( - "InvalidRequestError (used test key with production object) from Stripe while " - "creating subscription.", - "stripe", - error, - ) - - return Response( - { - "success": False, - "message": error["message"], - }, - status=status.HTTP_500_INTERNAL_SERVER_ERROR, - ) - - else: - request.user.log_event( - "InvalidRequestError from Stripe while creating subscription.", - "stripe", - error, - ) - return Response( - { - "success": False, - "message": None, - }, - status=status.HTTP_500_INTERNAL_SERVER_ERROR, - ) - - except Exception as e: + else: request.user.log_event( "InvalidRequestError from Stripe while creating subscription.", "stripe", - e, + error, ) - capture_exception(e) return Response( { "success": False, @@ -311,7 +292,29 @@ def create_subscription(attempts=0): status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) - new_subscription = create_subscription() + except Exception as e: + request.user.log_event( + "InvalidRequestError from Stripe while creating subscription.", + "stripe", + e, + ) + capture_exception(e) + return Response( + { + "success": False, + "message": None, + }, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + + def post(self, request, plan_id): + current_plan = request.user.profile.membership_plan + new_plan = PaymentPlan.objects.get(pk=plan_id) + + if current_plan: + return Response({"success": False}, status=status.HTTP_409_CONFLICT) + + new_subscription = self.create_subscription(request, new_plan) try: if new_subscription.status == "active": @@ -487,7 +490,7 @@ class SubscriptionInfo(StripeAPIView): def get(self, request): current_plan = request.user.profile.membership_plan - if not current_plan: + if not current_plan or not request.user.profile.stripe_subscription_id: return Response({"success": False}) else: @@ -527,14 +530,62 @@ def post(self, request, resume): ) else: - # this will modify the subscription to automatically cancel at the end of the current payment period - if resume: + if resume and not request.user.profile.stripe_subscription_id: + request.user.log_event( + "Member tried to resume a payment plan that doesn't exist - creating it.", + "stripe", + ) + new_subscription = PaymentPlanSignup().create_subscription( + request, current_plan + ) + + try: + if new_subscription.status == "active": + request.user.profile.stripe_subscription_id = ( + new_subscription.id + ) + request.user.profile.subscription_status = "active" + request.user.profile.save() + + request.user.log_event( + "Successfully created subscription in Stripe.", + "stripe", + "", + ) + + return Response({"success": True}) + + elif new_subscription.status == "incomplete": + # if we got here, that means the subscription wasn't successfully created + request.user.log_event( + f"Failed to create subscription in Stripe with status {new_subscription.status}.", + "stripe", + "", + ) + + return Response( + {"success": True, "message": "signup.subscriptionFailed"} + ) + + else: + request.user.log_event( + f"Failed to create subscription in Stripe with status {new_subscription.status}.", + "stripe", + "", + ) + return Response({"success": True}) + + except KeyError as e: + capture_exception(e) + return new_subscription or e + + elif resume: modified_subscription = stripe.Subscription.modify( request.user.profile.stripe_subscription_id, cancel_at_period_end=False, ) - if modified_subscription.cancel_at_period_end == False: + if not modified_subscription.cancel_at_period_end: request.user.profile.subscription_status = "active" request.user.profile.save() subject = f"{request.user.get_full_name()} resumed their cancelling membership plan." diff --git a/src-frontend/src/boot/routeGuards.ts b/src-frontend/src/boot/routeGuards.ts index 35a75203..c02667da 100644 --- a/src-frontend/src/boot/routeGuards.ts +++ b/src-frontend/src/boot/routeGuards.ts @@ -15,6 +15,7 @@ export default boot(({ router, store }) => { store.getters['profile/profile']?.memberStatus === 'Needs Induction' && to.name !== 'membershipPlan' && to.name !== 'webcams' && + to.name !== 'billing' && store.getters['config/features']?.enableMembershipPayments && to.meta.admin !== true ) { diff --git a/src-frontend/src/components/Billing/SelectTier.vue b/src-frontend/src/components/Billing/SelectTier.vue index da4dd3ef..9e49c4d7 100644 --- a/src-frontend/src/components/Billing/SelectTier.vue +++ b/src-frontend/src/components/Billing/SelectTier.vue @@ -332,8 +332,6 @@ export default defineComponent({ }, cardExistsHandler(value) { this.cardExists = value; - console.log('UPDATED CARD SAVED TO'); - console.log(value); }, }, }); diff --git a/src-frontend/src/pages/MembershipPlan.vue b/src-frontend/src/pages/MembershipPlan.vue index ee32472d..347c27b5 100644 --- a/src-frontend/src/pages/MembershipPlan.vue +++ b/src-frontend/src/pages/MembershipPlan.vue @@ -39,7 +39,10 @@
@@ -75,6 +78,7 @@ color="error" :label="$tc('paymentPlans.cancelButton')" /> +