def verify_paypal_response(request): """ This endpoint should be set as the listening endpoint for the webhook set in PayPal's developer dashboard(within a REST API App) The verification of the incoming PayPal requests is done via Factory_Paypal.initGatewayByVerification(request) The webhook should be set with the following events only: Billing subscription activated Billing subscription cancelled Billing subscription updated Payment capture completed Payment sale completed For more info on how to build this webhook endpoint, refer to PayPal documentation: https://developer.paypal.com/docs/subscriptions/integrate/ @todo: revise error handling, avoid catching all exceptions at the end """ try: # Set up gateway manager object with its linking donation, session, etc... gatewayManager = Factory_Paypal.initGatewayByVerification(request) if gatewayManager: return gatewayManager.process_webhook_response() else: raise Exception(_('gatewayManager for paypal not initialized.')) except WebhookNotProcessedError as error: # beware: this exception should be reserved for the incoming but not processed webhook events _debug(str(error)) # return 200 to prevent resending of paypal server of those requests return HttpResponse(status=200) except ValueError as error: _exception(str(error)) return HttpResponse(status=500) except Exception as error: _exception(str(error)) return HttpResponse(status=500)
def set_subscription_status(request): try: if request.method == 'POST' and request.POST.get('id', None): id = int(request.POST.get('id')) subscription = get_object_or_404(Subscription, id=id) old_status = subscription.recurring_status if not request.POST.get('status', ''): raise _("Empty value submitted for subscription status update") subscription.recurring_status = request.POST.get('status').lower() subscription.save() new_status = subscription.recurring_status # add to the update actions log addUpdateSubsActionLog(subscription, SUBS_ACTION_MANUAL, action_notes='%s -> %s' % (old_status, new_status), user=request.user) # notify donor of action sendSubscriptionStatusChangeToDonor(subscription) messages.add_message( request, messages.SUCCESS, str( _('Subscription %(id)d status set to %(status)s.') % { 'id': id, 'status': subscription.recurring_status })) return redirect( reverse('donations_subscription_modeladmin_inspect', kwargs={'instance_pk': id})) except Exception as e: _exception(str(e)) messages.add_message(request, messages.ERROR, str(e)) return redirect(reverse('donations_subscription_modeladmin_index'))
def return_from_2c2p(request): try: gatewayManager = Factory_2C2P.initGatewayByReturn(request) request.session['return-donation-id'] = gatewayManager.donation.id if map2C2PPaymentStatus( gatewayManager.data['payment_status']) == STATUS_COMPLETE: return redirect('donations:thank-you') elif map2C2PPaymentStatus( gatewayManager.data['payment_status']) == STATUS_CANCELLED: return redirect('donations:cancelled') elif map2C2PPaymentStatus( gatewayManager.data['payment_status']) == STATUS_REVOKED: return redirect('donations:revoked') else: request.session['error-title'] = str(_("Unknown Error")) request.session['error-message'] = str( _("Could not determine how to treat payment status: " + map2C2PPaymentStatus(gatewayManager.data['payment_status']))) except ValueError as e: _exception(str(e)) request.session['error-title'] = str(_("ValueError")) request.session['error-message'] = str(e) except Exception as e: _exception(str(e)) request.session['error-title'] = str(_("Unknown Error")) request.session['error-message'] = str(e) return redirect('donations:thank-you')
def toggle_subscription(request): try: if request.method == 'POST' and request.POST.get('id', None): subscription_id = int(request.POST.get('id')) subscription = get_object_or_404(Subscription, id=subscription_id) gatewayManager = InitPaymentGateway(request, subscription=subscription) resultSet = gatewayManager.toggle_recurring_payment() # add to the update actions log addUpdateSubsActionLog( gatewayManager.subscription, SUBS_ACTION_PAUSE if resultSet['recurring-status'] == STATUS_PAUSED else SUBS_ACTION_RESUME, user=request.user) messages.add_message( request, messages.SUCCESS, str( _('Subscription %(id)d status is toggled to %(status)s.') % { 'id': subscription_id, 'status': resultSet['recurring-status'].capitalize() })) return redirect( reverse('donations_subscription_modeladmin_inspect', kwargs={'instance_pk': subscription_id})) except Exception as e: _exception(str(e)) messages.add_message(request, messages.ERROR, str(e)) return redirect(reverse('donations_subscription_modeladmin_index'))
def cancel_subscription(request): try: if request.method == 'POST' and request.POST.get('id', None): subscription_id = int(request.POST.get('id')) subscription = get_object_or_404(Subscription, id=subscription_id) gatewayManager = InitPaymentGateway(request, subscription=subscription) resultSet = gatewayManager.cancel_recurring_payment() # add to the update actions log addUpdateSubsActionLog(gatewayManager.subscription, SUBS_ACTION_CANCEL, user=request.user) messages.add_message( request, messages.SUCCESS, str( _('Subscription %(id)d status is cancelled.') % {'id': subscription_id})) return redirect( reverse('donations_subscription_modeladmin_inspect', kwargs={'instance_pk': subscription_id})) except Exception as e: _exception(str(e)) messages.add_message(request, messages.ERROR, str(e)) return redirect(reverse('donations_subscription_modeladmin_index'))
def edit_recurring(request, id): """ This is called when the user clicks the 'Edit recurring donation' button on page donations.views.my_recurring_donations We only update the subscription if it is owned by request.user Action is logged for the corresponding subscription, action logs can be viewed by inspecting the subscription's 'Action Log' tab Sample (Form) request: { 'recurring_amount': 10.00, 'billing_cycle_now': 'on' } * only Stripe's form has the billing_cycle_now option @todo: revise error handling, avoid catching all exceptions at the end """ try: subscription = get_object_or_404(Subscription, id=id) if subscription.user == request.user: # Form object is initialized according to the specific gateway and if request.method=='POST' form = InitEditRecurringPaymentForm(request.POST, request.method, subscription) if request.method == 'POST': if form.is_valid(): # use gatewayManager to process the data in form.cleaned_data as required gatewayManager = InitPaymentGateway( request, subscription=subscription) # check if frequency limitation is enabled and passed if not isUpdateSubsFrequencyLimitationPassed( gatewayManager): raise Exception( _('You have already carried out 5 subscription update action in the last 5 minutes, our current limit is 5 subscription update actions(edit/pause/resume) every 5 minutes.' )) original_value = gatewayManager.subscription.recurring_amount gatewayManager.update_recurring_payment(form.cleaned_data) new_value = gatewayManager.subscription.recurring_amount # add to the update actions log addUpdateSubsActionLog( gatewayManager.subscription, SUBS_ACTION_UPDATE, 'Recurring Amount: %s -> %s' % (str(original_value), str(new_value))) return redirect('donations:edit-recurring', id=id) else: raise PermissionError( _('You are not authorized to edit subscription %(id)d.') % {'id': id}) except PermissionError as e: _exception(str(e)) messages.add_message(request, messages.ERROR, str(e)) return redirect('donations:my-recurring-donations') except (ValueError, RuntimeError, Exception) as e: _exception(str(e)) messages.add_message(request, messages.ERROR, str(e)) return render(request, getEditRecurringPaymentHtml(subscription), { 'form': form, 'subscription': subscription })
def toggle_recurring(request): """ This is called when the user clicks the 'Pause/Resume recurring donation' button on page donations.views.my_recurring_donations We only update the subscription if it is owned by request.user Action is logged for the corresponding subscription, action logs can be viewed by inspecting the subscription's 'Action Log' tab Sample request: { 'subscription_id': 1, 'csrfmiddlewaretoken': 'LZSpOsb364pn9R3gEPXdw2nN3dBEi7RWtMCBeaCse2QawCFIndu93fD3yv9wy0ij' } @todo: revise error handling, avoid catching all exceptions at the end """ try: if request.method == 'POST': json_data = json.loads(request.body) if 'subscription_id' not in json_data: print("No subscription_id in JSON body", flush=True) return HttpResponse(status=400) subscription_id = int(json_data['subscription_id']) subscription = get_object_or_404(Subscription, id=subscription_id) if subscription.user == request.user: gatewayManager = InitPaymentGateway(request, subscription=subscription) # check if frequency limitation is enabled and passed if not isUpdateSubsFrequencyLimitationPassed(gatewayManager): raise Exception( _('You have already carried out 5 subscription update action in the last 5 minutes, our current limit is 5 subscription update actions(edit/pause/resume) every 5 minutes.' )) resultSet = gatewayManager.toggle_recurring_payment() # add to the update actions log addUpdateSubsActionLog( gatewayManager.subscription, SUBS_ACTION_PAUSE if resultSet['recurring-status'] == STATUS_PAUSED else SUBS_ACTION_RESUME) return JsonResponse({ 'status': 'success', 'button-text': resultSet['button-text'], 'recurring-status': str(_(resultSet['recurring-status'].capitalize())), 'success-message': resultSet['success-message'] }) else: raise PermissionError( _('You are not authorized to pause/resume subscription %(id)d.' ) % {'id': subscription_id}) else: return HttpResponse(400) except (ValueError, PermissionError, RuntimeError, Exception) as e: _exception(str(e)) return JsonResponse({'status': 'failure', 'reason': str(e)})
def verify_paypal_legacy_response(request): try: # Set up gateway manager object with its linking donation, session, etc... gatewayManager = Factory_Paypal_Legacy.initGatewayByVerification(request) if gatewayManager: return gatewayManager.process_webhook_response() else: raise Exception(_('gatewayManager for paypal-legacy not initialized.')) except ValueError as error: _exception(str(error)) return HttpResponse(status=500) except Exception as error: _exception(str(error)) return HttpResponse(status=500)
def cancel_recurring(request): """ This is called when the user clicks confirm when cancelling a recurring donation on page donations.views.my_recurring_donations We only cancel the subscription if it is owned by request.user Action is logged for the corresponding subscription, action logs can be viewed by inspecting the subscription's 'Action Log' tab Sample (JSON) request: { 'subscription_id': 1, 'csrfmiddlewaretoken': 'LZSpOsb364pn9R3gEPXdw2nN3dBEi7RWtMCBeaCse2QawCFIndu93fD3yv9wy0ij' } @todo: revise error handling, avoid catching all exceptions at the end """ try: if request.method == 'POST': json_data = json.loads(request.body) if 'subscription_id' not in json_data: print("No subscription_id in JSON body", flush=True) return HttpResponse(status=400) subscription_id = int(json_data['subscription_id']) subscription = get_object_or_404(Subscription, id=subscription_id) if subscription.user == request.user: gatewayManager = InitPaymentGateway(request, subscription=subscription) gatewayManager.cancel_recurring_payment() # add to the update actions log addUpdateSubsActionLog(gatewayManager.subscription, SUBS_ACTION_CANCEL) return JsonResponse({ 'status': 'success', 'button-text': str(_('View all renewals')), 'recurring-status': str(_(STATUS_CANCELLED.capitalize())), 'button-href': reverse('donations:my-renewals', kwargs={'id': subscription_id}) }) else: raise PermissionError( _('You are not authorized to cancel subscription %(id)d.') % {'id': subscription_id}) else: return HttpResponse(400) except (ValueError, PermissionError, RuntimeError, Exception) as e: _exception(str(e)) return JsonResponse({'status': 'failure', 'reason': str(e)})
def cancel_from_paypal(request): """ This endpoint is submitted as the cancel_url when creating the PayPal Subscription/Order at create_paypal_transaction(request) This url should receive GET params: 'token' and 'subscription_id'(only recurring payments); 'ba_token' is not used In Factory_PayPal.initGatewayByReturn(request), we save the subscription_id/token into the donationPaymentMeta data upon a successful request; exception will be raised if the endpoint is reached but a previous meta value is found, this is done to prevent this endpoint being called unlimitedly, which would set the payment status as Cancelled each time @todo: revise error handling, avoid catching all exceptions at the end """ try: gatewayManager = Factory_Paypal.initGatewayByReturn(request) request.session['return-donation-id'] = gatewayManager.donation.id # no need to carry out further actions for donation cancellation # 1. if it is a subscription, curlPaypal will fail to cancel it before any donor approval is given(resource not found error will be returned) # 2. there is no cancel-order endpoint for the Orders API except IOError as error: request.session['error-title'] = str(_("IOError")) request.session['error-message'] = str(error) _exception(str(error)) except ValueError as error: request.session['error-title'] = str(_("ValueError")) request.session['error-message'] = str(error) _exception(str(error)) except Exception as error: request.session['error-title'] = str(_("Exception")) request.session['error-message'] = str(error) _exception(str(error)) return redirect('donations:cancelled')
def verify_stripe_response(request): """ This endpoint should be set as the listening endpoint for the webhook set in Stripe's dashboard The verification of the incoming Stripe requests is done via Factory_Stripe.initGatewayByVerification(request) The webhook should be set with the following events only: payment_intent.succeeded customer.subscription.deleted customer.subscription.updated invoice.paid invoice.created checkout.session.completed For more info on how to build this webhook endpoint, refer to Stripe documentation: https://stripe.com/docs/webhooks @todo: revise error handling, avoid catching all exceptions at the end """ try: # Set up gateway manager object with its linking donation, session, etc... gatewayManager = Factory_Stripe.initGatewayByVerification(request) return gatewayManager.process_webhook_response() except WebhookNotProcessedError as error: # beware: this exception should be reserved for the incoming but not processed webhook events, or events processed but data not needed further action _debug(str(error)) # return 200 for attaining a higher rate of successful response rate at Stripe backend return HttpResponse(status=200) except ValueError as e: # Might be invalid payload from initGatewayByVerification # or missing donation_id/subscription_id or donation object not found _exception(str(e)) return HttpResponse(status=400) except stripe.error.SignatureVerificationError as e: # Invalid signature from initGatewayByVerification _exception(str(e)) return HttpResponse(status=400) except (stripe.error.RateLimitError, stripe.error.InvalidRequestError, stripe.error.AuthenticationError, stripe.error.APIConnectionError, stripe.error.StripeError) as e: _exception( "Stripe API Error({}): Status({}), Code({}), Param({}), Message({})" .format( type(e).__name__, e.http_status, e.code, e.param, e.user_message)) return HttpResponse(status=int(e.http_status)) except Exception as e: _exception(str(e)) return HttpResponse(status=500)
def my_renewals(request, id): # deleted=False should be valid whether soft-delete mode is on or off subscription = get_object_or_404(Subscription, id=id, deleted=False) try: if subscription.user == request.user: renewals = Donation.objects.filter( subscription=subscription, deleted=False).order_by('-donation_date') siteSettings = get_site_settings_from_default_site() return render( request, 'donations/my_renewals.html', { 'subscription': subscription, 'renewals': renewals, 'siteSettings': siteSettings }) else: raise PermissionError( _('You are not authorized to view renewals of subscription %(id)d.' ) % {'id': id}) except PermissionError as e: _exception(str(e)) messages.add_message(request, messages.ERROR, str(e)) return redirect('donations:my-recurring-donations')
def verify_2c2p_response(request): try: gatewayManager = Factory_2C2P.initGatewayByVerification(request) return gatewayManager.process_webhook_response() except ValueError as e: _exception(str(e)) return HttpResponse(status=400) except RuntimeError as e: _exception(str(e)) return HttpResponse(status=500) except Exception as e: _exception(str(e)) return HttpResponse(status=500)
def return_from_paypal(request): """ This endpoint is submitted as the return_url when creating the PayPal Subscription/Order at create_paypal_transaction(request) This url should receive GET params: 'token' and 'subscription_id'(only recurring payments); 'ba_token' is not used In Factory_PayPal.initGatewayByReturn(request), we save the subscription_id/token into the donationPaymentMeta data upon a successful request; exception will be raised if the endpoint is reached but a previous meta value is found, this is done to prevent this endpoint being called unlimitedly @todo: revise error handling, avoid catching all exceptions at the end """ try: gatewayManager = Factory_Paypal.initGatewayByReturn(request) request.session['return-donation-id'] = gatewayManager.donation.id # subscription donation updates are handled by webhooks # returning from paypal only needs to deal with onetime donations if not gatewayManager.donation.is_recurring: # further capture payment if detected order approved, if not just set payment as processing and leave it to webhook processing if gatewayManager.order_status == 'APPROVED': # might raise IOError/HttpError capture_response = capture_paypal_order(gatewayManager.donation, gatewayManager.order_id) if capture_response.status == 'COMPLETED': _debug('PayPal: Order Captured. Payment Completed.') gatewayManager.donation.payment_status = STATUS_COMPLETE gatewayManager.donation.donation_date = datetime.now(timezone.utc) gatewayManager.donation.transaction_id = capture_response.purchase_units[0].payments.captures[0].id gatewayManager.donation.save() else: _debug('PayPal: Order status after Paypal returns: '+gatewayManager.order_status) else: # save the subscription_id as profile_id gatewayManager.donation.subscription.profile_id = request.GET.get('subscription_id') gatewayManager.donation.subscription.save() except IOError as error: request.session['error-title'] = str(_("IOError")) request.session['error-message'] = str(error) _exception(str(error)) except ValueError as error: request.session['error-title'] = str(_("ValueError")) request.session['error-message'] = str(error) _exception(str(error)) except Exception as error: request.session['error-title'] = str(_("Exception")) request.session['error-message'] = str(error) _exception(str(error)) return redirect('donations:thank-you')
def cancel_from_stripe(request): """ This endpoint is submitted as the cancel_url when creating the Stripe session at create_checkout_session(request) This url should receive a single GET param: 'stripe_session_id' In Factory_Stripe.initGatewayByReturn(request), we save the stripe_session_id into the donationPaymentMeta data upon a successful request; exception will be raised if the endpoint is reached but a previous meta value is found, this is done to prevent this endpoint being called unlimitedly, which would set the payment status as Cancelled each time @todo: revise error handling, avoid catching all exceptions at the end """ try: gatewayManager = Factory_Stripe.initGatewayByReturn(request) request.session['return-donation-id'] = gatewayManager.donation.id if gatewayManager.session: if gatewayManager.session.mode == 'payment': stripe.PaymentIntent.cancel( gatewayManager.session.payment_intent) # for subscription mode, payment_intent is not yet created, so no need to cancel except ValueError as e: _exception(str(e)) request.session['error-title'] = str(_("ValueError")) request.session['error-message'] = str(e) except (stripe.error.RateLimitError, stripe.error.InvalidRequestError, stripe.error.AuthenticationError, stripe.error.APIConnectionError, stripe.error.StripeError) as e: _exception( "Stripe API Error({}): Status({}), Code({}), Param({}), Message({})" .format( type(e).__name__, e.http_status, e.code, e.param, e.user_message)) request.session['error-title'] = type(e).__name__ request.session['error-message'] = e.user_message except Exception as e: _exception(str(e)) request.session['error-title'] = str(_("Unknown Error")) request.session['error-message'] = str( _("Results returned from gateway is invalid.")) return redirect('donations:cancelled')
def return_from_stripe(request): """ This endpoint is submitted as the success_url when creating the Stripe session at create_checkout_session(request) This url should receive a single GET param: 'stripe_session_id' In Factory_Stripe.initGatewayByReturn(request), we save the stripe_session_id into the donationPaymentMeta data upon a successful request; exception will be raised if the endpoint is reached but a previous meta value is found, this is done to prevent this endpoint being called unlimitedly For more info on how to build this endpoint, refer to Stripe documentation: https://stripe.com/docs/payments/checkout/custom-success-page @todo: revise error handling, avoid catching all exceptions at the end """ try: gatewayManager = Factory_Stripe.initGatewayByReturn(request) request.session['return-donation-id'] = gatewayManager.donation.id except ValueError as e: _exception(str(e)) request.session['error-title'] = str(_("ValueError")) request.session['error-message'] = str(e) except (stripe.error.RateLimitError, stripe.error.InvalidRequestError, stripe.error.AuthenticationError, stripe.error.APIConnectionError, stripe.error.StripeError) as e: _exception( "Stripe API Error({}): Status({}), Code({}), Param({}), Message({})" .format( type(e).__name__, e.http_status, e.code, e.param, e.user_message)) request.session['error-title'] = type(e).__name__ request.session['error-message'] = e.user_message except Exception as e: _exception(str(e)) request.session['error-title'] = str(_("Unknown Exception")) request.session['error-message'] = str( _("Results returned from gateway is invalid.")) return redirect('donations:thank-you')
def create_paypal_transaction(request): """ When the user reaches last step after confirming the donation, user is redirected via gatewayManager.redirect_to_gateway_url(), which renders redirection_paypal.html This function calls to PayPal Api to create a PayPal subscription object or PayPal order(one-time donation), then this function returns the approval_link to frontend js and to redirect to PayPal's checkout page Sample (JSON) request: { 'csrfmiddlewaretoken': 'LZSpOsb364pn9R3gEPXdw2nN3dBEi7RWtMCBeaCse2QawCFIndu93fD3yv9wy0ij' } @todo: revise error handling, avoid catching all exceptions at the end """ errorObj = { "issue": "Exception", "description": "" } result = {} try: paypalSettings = getPayPalSettings() donation_id = request.session.pop('donation_id', None) if not donation_id: raise ValueError(_("Missing donation_id in session")) donation = Donation.objects.get(pk=int(donation_id)) if donation.is_recurring: # Product should have been created by admin manually at the dashboard/setup wizard # if no product exists, create one here(double safety net) # todo: make sure the product_id in site_settings has been set by some kind of configuration enforcement before site is launched product_list = listProducts(request.session) product = None if len(product_list['products']) == 0: product = createProduct(request.session) else: # get the product, should aim at the product with the specific product id for prod in product_list['products']: if prod['id'] == paypalSettings.product_id: product = prod if product == None: raise ValueError(_('Cannot initialize/get the paypal product object')) # Create plan and subscription plan = createPlan(request.session, product['id'], donation) if plan['status'] == 'ACTIVE': subscription = createSubscription(request.session, plan['id'], donation) result['subscription_id'] = subscription['id'] for link in subscription['links']: if link['rel'] == 'approve': result['approval_link'] = link['href'] else: raise ValueError(_("Newly created PayPal plan is not active, status: %(status)s") % {'status': plan['status']}) # else: one-time donation else: response = create_paypal_order(request.session, donation) ppresult = response.result _debug('PayPal: Order Created Status: '+ppresult.status) # set approval_link attribute for link in ppresult.links: _debug('PayPal: --- {}: {} ---'.format(link.rel, link.href)) if link.rel == 'approve': result['approval_link'] = link.href except ValueError as e: _exception(str(e)) errorObj['issue'] = "ValueError" errorObj['description'] = str(e) return JsonResponse(object_to_json(errorObj), status=500) except Donation.DoesNotExist: _exception("Donation.DoesNotExist") errorObj['issue'] = "Donation.DoesNotExist" errorObj['description'] = str(_("Donation object not found by id: %(id)s") % {'id': donation_id}) return JsonResponse(object_to_json(errorObj), status=500) except HttpError as ioe: # Catching exceptions from the paypalclient execution, HttpError is a subclass of IOError httpError = json.loads(ioe.message) if 'details' in httpError and len(httpError['details']) > 0: errorObj["issue"] = httpError['details'][0]['issue'] errorObj["description"] = httpError['details'][0]['description'] _exception(errorObj["description"]) # update donation status to failed donation.payment_status = STATUS_FAILED donation.save() return JsonResponse(object_to_json(errorObj), status=ioe.status_code) except Exception as error: errorObj['description'] = str(error) _exception(errorObj["description"]) return JsonResponse(object_to_json(errorObj), status=500) return JsonResponse(object_to_json(result))
def confirm_donation(request): try: siteSettings = get_site_settings_from_default_site() tmpd = TempDonation.objects.get( pk=request.session.get('temp_donation_id', None)) paymentMethod = displayGateway(tmpd) isGatewayHostedBool = isGatewayHosted(tmpd.gateway) if request.method == 'POST': # determine path based on submit-choice if request.POST.get('submit-choice', '') == 'change-submit': # goes back to step 1 which is donation details return redirect('donations:donate') elif request.POST.get('submit-choice', '') == 'confirm-submit': # proceed with the rest of the payment procedures # create processing donation transaction_id = gen_transaction_id(gateway=tmpd.gateway) donation = Donation( is_test=tmpd.is_test, transaction_id=transaction_id, user=request.user if request.user.is_authenticated else None, form=tmpd.form, gateway=tmpd.gateway, is_recurring=tmpd.is_recurring, donation_amount=tmpd.donation_amount, currency=tmpd.currency, guest_email=tmpd.guest_email if not request.user.is_authenticated else '', payment_status=STATUS_PROCESSING, metas=temp_donation_meta_to_donation_meta( tmpd.temp_metas.all()), donation_date=datetime.now(timezone.utc), ) # create a processing subscription if is_recurring if tmpd.is_recurring: # create new Subscription object, with a temporary profile_id created by uuidv4 # user should have been authenticated according to flow logic subscription = Subscription( is_test=tmpd.is_test, profile_id=uuid4_str(), user=request.user if request.user.is_authenticated else None, gateway=tmpd.gateway, recurring_amount=tmpd.donation_amount, currency=tmpd.currency, recurring_status=STATUS_PROCESSING, subscribe_date=datetime.now(timezone.utc)) subscription.save() # link subscription to the donation donation.subscription = subscription donation.save() request.session.pop('temp_donation_id') # delete temp donation instead of saving it as processed tmpd.delete() # tmpd.status = STATUS_PROCESSED # tmpd.save() if 'first_time_registration' in request.session: dpmeta = DonationPaymentMeta( donation=donation, field_key='is_user_first_donation', field_value=request.session['first_time_registration']) dpmeta.save() request.session.pop('first_time_registration') # redirect to payment_gateway gatewayManager = InitPaymentGateway(request, donation=donation) return gatewayManager.redirect_to_gateway_url() else: raise Exception( _('No valid submit-choice is being submitted.')) except TempDonation.DoesNotExist as e: messages.add_message( request, messages.ERROR, str( _('Session data has expired. Please enter the donation details again.' ))) return redirect('donations:donate') except Exception as e: # Should rarely happen, but in case some bugs or order id repeats itself _exception(str(e)) return render( request, 'donations/confirm_donation.html', { 'tmpd': tmpd, 'paymentMethod': paymentMethod, 'isGatewayHosted': isGatewayHostedBool })
def donate(request): try: siteSettings = get_site_settings_from_default_site() form_template = 'donations/donation_details_form.html' form_blueprint = siteSettings.donation_form if not form_blueprint: raise Exception(_('Donation Form not yet set.')) if request.method == 'POST': form = DonationDetailsForm(request.POST, request=request, blueprint=form_blueprint, label_suffix='') if form.is_valid(): # extract temp meta data temp_donation_metas = extract_temp_donation_meta(request.POST) # process donation amount if form.cleaned_data.get( 'donation_amount_custom', None ) and form.cleaned_data['donation_amount_custom'] > 0: is_amount_custom = True donation_amount = form.cleaned_data[ 'donation_amount_custom'] else: is_amount_custom = False donation_amount = form.cleaned_data['donation_amount'] # create/edit a pending temporary donation object payment_gateway = PaymentGateway.objects.get( pk=form.cleaned_data['payment_gateway']) if request.session.get('temp_donation_id', ''): temp_donation = TempDonation.objects.get( pk=request.session.get('temp_donation_id')) temp_donation.gateway = payment_gateway temp_donation.is_amount_custom = is_amount_custom temp_donation.is_recurring = True if form.cleaned_data[ 'donation_frequency'] == 'monthly' else False temp_donation.donation_amount = donation_amount temp_donation.currency = form.cleaned_data['currency'] temp_donation.temp_metas = temp_donation_metas temp_donation.guest_email = form.cleaned_data.get( 'email', '') temp_donation.save() else: temp_donation = TempDonation( is_test=siteSettings.sandbox_mode, form=form_blueprint, gateway=payment_gateway, is_amount_custom=is_amount_custom, is_recurring=True if form.cleaned_data['donation_frequency'] == 'monthly' else False, donation_amount=donation_amount, currency=form.cleaned_data['currency'], status=STATUS_PENDING, temp_metas=temp_donation_metas, guest_email=form.cleaned_data.get('email', ''), ) temp_donation.save() request.session['temp_donation_id'] = temp_donation.id # determine path based on submit-choice if request.POST.get('submit-choice', '') == 'guest-submit' or request.POST.get( 'submit-choice', '') == 'loggedin-submit': # skip to step 3 which is Donation Confirmation return redirect('donations:confirm-donation') elif request.POST.get('submit-choice', '') == 'register-submit': # proceed to step 2 which is Register or Login return redirect('donations:register-signin') else: raise Exception( _('No valid submit-choice is being submitted.')) else: form = DonationDetailsForm(request=request, blueprint=form_blueprint, label_suffix='') # see: https://docs.djangoproject.com/en/3.0/ref/forms/api/#django.forms.Form.field_order if form_blueprint.isAmountSteppedCustom(): form.order_fields([ 'donation_amount', 'donation_amount_custom', 'donation_frequency', 'payment_gateway', 'email' ]) else: form.order_fields([ 'donation_amount', 'donation_frequency', 'payment_gateway', 'email' ]) except Exception as e: # Should rarely happen, but in case some bugs or order id repeats itself _exception(str(e)) messages.add_message(request, messages.ERROR, str(e)) return redirect('donations:donate') # get offline gateway id and instructions text offline_gateway = PaymentGateway.objects.get(title=GATEWAY_OFFLINE) offline_gateway_id = offline_gateway.id offlineSettings = getOfflineSettings() # manually casting offline_instructions_text from LazyI18nString to str to avoid the "richtext expects a string" error in the template offline_instructions_html = str(offlineSettings.offline_instructions_text) return render( request, form_template, { 'form': form, 'donation_details_fields': DONATION_DETAILS_FIELDS, 'offline_gateway_id': offline_gateway_id, 'offline_instructions_html': offline_instructions_html })
def create_checkout_session(request): """ When the user reaches last step after confirming the donation, user is redirected via gatewayManager.redirect_to_gateway_url(), which renders redirection_stripe.html This function calls to Stripe Api to create a Stripe Session object, then this function returns the stripe session id to the stripe js api 'stripe.redirectToCheckout({ sessionId: session.id })' for the redirection to Stripe's checkout page Sample (JSON) request: { 'csrfmiddlewaretoken': 'LZSpOsb364pn9R3gEPXdw2nN3dBEi7RWtMCBeaCse2QawCFIndu93fD3yv9wy0ij' } @todo: revise error handling, avoid catching all exceptions at the end """ errorObj = {"issue": "Exception", "description": ""} try: initStripeApiKey() stripeSettings = getStripeSettings() donation_id = request.session.pop('donation_id', None) if not donation_id: raise ValueError(_("No donation_id in session")) # might throw DoesNotExist error donation = Donation.objects.get(pk=donation_id) # init session_kwargs with common parameters session_kwargs = { 'payment_method_types': ['card'], 'metadata': { 'donation_id': donation.id }, 'success_url': reverse_with_site_url('donations:return-from-stripe') + '?stripe_session_id={CHECKOUT_SESSION_ID}', 'cancel_url': reverse_with_site_url('donations:cancel-from-stripe') + '?stripe_session_id={CHECKOUT_SESSION_ID}', 'idempotency_key': uuid4_str() } # try to get existing stripe customer donor_email = donation.user.email if donation.user else donation.guest_email customers = stripe.Customer.list(email=donor_email, limit=1) if len(customers['data']) > 0: session_kwargs['customer'] = customers['data'][0]['id'] else: session_kwargs['customer_email'] = donor_email # Product should have been created by admin manually at the dashboard # todo: make sure the product_id in site_settings has been set by some kind of configuration enforcement before site is launched # stripe.error.InvalidRequestError would be raised if the product_id is either not found or empty/None product = stripe.Product.retrieve(stripeSettings.product_id) # ad-hoc price is used amount_str = formatDonationAmount(donation.donation_amount, donation.currency) adhoc_price = { 'unit_amount_decimal': amount_str, 'currency': donation.currency.lower(), 'product': product.id } if donation.is_recurring: adhoc_price['recurring'] = { 'interval': 'month', 'interval_count': 1 } session_kwargs['line_items'] = [{ 'price_data': adhoc_price, 'quantity': 1, }] # set session mode session_mode = 'payment' if donation.is_recurring: session_mode = 'subscription' session_kwargs['mode'] = session_mode # set metadata if donation.is_recurring: session_kwargs['subscription_data'] = { 'metadata': { 'donation_id': donation.id } } else: session_kwargs['payment_intent_data'] = { 'metadata': { 'donation_id': donation.id } } session = stripe.checkout.Session.create(**session_kwargs) # save payment_intent id for recognition purposes when receiving the payment_intent.succeeded webhook for onetime donations if session.payment_intent: dpm = DonationPaymentMeta(donation=donation, field_key='stripe_payment_intent_id', field_value=session.payment_intent) dpm.save() return JsonResponse({'id': session.id}) except ValueError as e: _exception(str(e)) errorObj['issue'] = "ValueError" errorObj['description'] = str(e) return JsonResponse(object_to_json(errorObj), status=500) except Donation.DoesNotExist: _exception("Donation.DoesNotExist") errorObj['issue'] = "Donation.DoesNotExist" errorObj['description'] = str( _("Donation object not found by id: %(id)s") % {'id': donation_id}) return JsonResponse(object_to_json(errorObj), status=500) except (stripe.error.RateLimitError, stripe.error.InvalidRequestError, stripe.error.AuthenticationError, stripe.error.APIConnectionError, stripe.error.StripeError) as e: _exception( "Stripe API Error({}): Status({}), Code({}), Param({}), Message({})" .format( type(e).__name__, e.http_status, e.code, e.param, e.user_message)) errorObj['issue'] = type(e).__name__ errorObj['description'] = 'Message is: %s' % e.user_message return JsonResponse(object_to_json(errorObj), status=int(e.http_status)) except Exception as e: errorObj['description'] = str(e) _exception(errorObj["description"]) return JsonResponse(object_to_json(errorObj), status=500)