Exemplo n.º 1
0
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)
Exemplo n.º 2
0
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'))
Exemplo n.º 3
0
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')
Exemplo n.º 4
0
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'))
Exemplo n.º 5
0
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'))
Exemplo n.º 6
0
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
    })
Exemplo n.º 7
0
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)})
Exemplo n.º 8
0
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)
Exemplo n.º 9
0
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)})
Exemplo n.º 10
0
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')
Exemplo n.º 11
0
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)
Exemplo n.º 12
0
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')
Exemplo n.º 13
0
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)
Exemplo n.º 14
0
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')
Exemplo n.º 15
0
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')
Exemplo n.º 16
0
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')
Exemplo n.º 17
0
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))
Exemplo n.º 18
0
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
        })
Exemplo n.º 19
0
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
        })
Exemplo n.º 20
0
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)