Esempio n. 1
0
def curlPaypalIPN(url, headers, post_data=''):
    _debug('curlPaypalIPN url: {}'.format(url))
    buffer = BytesIO()
    c = pycurl.Curl()
    c.setopt(c.URL, url)
    c.setopt(c.HTTP_VERSION, pycurl.CURL_HTTP_VERSION_1_1)
    c.setopt(c.WRITEDATA, buffer)
    c.setopt(c.CAINFO, certifi.where())
    c.setopt(c.HTTPHEADER, headers)
    c.setopt(c.FORBID_REUSE, 1)
    c.setopt(c.POST, 1)
    if post_data:
        c.setopt(c.POSTFIELDS, post_data)
    c.setopt(c.VERBOSE, True)
    c.perform()
    status_code = c.getinfo(pycurl.HTTP_CODE)
    c.close()

    body = buffer.getvalue()
    # Body is a byte string.
    # We have to know the encoding in order to print it to a text file
    # such as standard output.
    if status_code >= 300:
        raise RuntimeError("curlPaypalipN request unsuccessful. Status Code: {}, Full body: {}".format(status_code, body.decode('utf-8')))
    # print("Curl to PayPal status code: {}({})".format(status_code, type(status_code)))
    # Here we deserialize the json into a python object
    # _debug('Success! curlPaypalIPN body: {}'.format(body.decode('utf-8')))
    return body.decode('utf-8')
Esempio n. 2
0
def curlPaypal(url, headers, userpwd='', post_data='', verb='GET'):
    _debug('curlPaypal url: {}'.format(url))
    buffer = BytesIO()
    c = pycurl.Curl()
    c.setopt(c.URL, url)
    c.setopt(c.WRITEDATA, buffer)
    c.setopt(c.CAINFO, certifi.where())
    c.setopt(c.HTTPHEADER, headers)
    if userpwd:
        c.setopt(c.USERPWD, userpwd)
    if post_data:
        c.setopt(c.POSTFIELDS, post_data)
    if verb == 'POST':
        c.setopt(c.CUSTOMREQUEST, 'POST')
    elif verb == 'PATCH':
        c.setopt(c.CUSTOMREQUEST, 'PATCH')
    c.setopt(c.VERBOSE, True)
    c.perform()
    status_code = c.getinfo(pycurl.HTTP_CODE)
    c.close()

    body = buffer.getvalue()
    # Body is a byte string.
    # We have to know the encoding in order to print it to a text file
    # such as standard output.
    if status_code == 204:
        _debug('curlPaypal returns 204')
        return {}
    if status_code >= 300:
        raise RuntimeError(
            "curlPaypal request unsuccessful. Status Code: {}, Full body: {}".
            format(status_code, body.decode('utf-8')))
    # print("Curl to PayPal status code: {}({})".format(status_code, type(status_code)))
    # Here we deserialize the json into a python object
    return json.loads(body.decode('utf-8'))
Esempio n. 3
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)
Esempio n. 4
0
def checkAccessTokenExpiry(session):
    paypalSettings = getPayPalSettings()
    token_uri = '/v1/oauth2/token'
    # check if the paypal_token_expiry stored in session has passed or not
    current_time = round(time.time())
    if 'paypal_token_expiry' not in session or current_time > session[
            'paypal_token_expiry']:
        saveNewAccessToken(session, paypalSettings.api_url + token_uri,
                           paypalSettings.client_id, paypalSettings.secret_key)
    _debug("Paypal Auth Token: " + session['paypal_token'])
Esempio n. 5
0
def isUpdateSubsFrequencyLimitationPassed(gatewayManager):
    site_settings = get_site_settings_from_default_site()
    if site_settings.limit_fiveactions_per_fivemins:
        # get count of the actions carried out by the same donor in the last 5 minutes
        nowdt = datetime.now(dt_timezone.utc)
        fiveminsbf = nowdt - timedelta(minutes=5)
        count = UserSubscriptionUpdatesLog.objects.filter(
            user=gatewayManager.subscription.user,
            created_at__gte=fiveminsbf).count()
        _debug(
            'Count of Subscription Actions done by {} within five minutes: {}'.
            format(gatewayManager.subscription.user.fullname, count))
        if count >= 5:
            return False
    return True
Esempio n. 6
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)
Esempio n. 7
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')
Esempio n. 8
0
def capture_paypal_order(donation, order_id):
    """Method to capture order using order_id"""
    paypalSettings = getPayPalSettings()
    client = PayPalHttpClient(paypalSettings.environment)

    req = OrdersCaptureRequest(order_id)
    _debug('PayPal: Capture Order')
    try:
        response = client.execute(req)
    except IOError as ioe:
        fail_reason = str(ioe)
        if isinstance(ioe, HttpError):
            # Something went wrong server-side
            httpError = json.loads(ioe.message)
            if 'details' in httpError and len(httpError['details']) > 0:
                fail_reason = process_capture_failure(
                    donation, httpError['details'][0]['issue'],
                    httpError['details'][0]['description'])
        # update donation status to failed
        donation.payment_status = STATUS_FAILED
        donation.save()
        raise IOError(fail_reason)
    return response.result
Esempio n. 9
0
    def initGatewayByReturn(request):
        # a get param named 'token' contains the order_id
        paypalSettings = getPayPalSettings()
        client = PayPalHttpClient(paypalSettings.environment)
        donation_id = None
        subscription_obj = {}
        kwargs = {}

        if request.GET.get('subscription_id', None):
            # recurring payment
            subscription_obj = getSubscriptionDetails(
                request.session, request.GET.get('subscription_id'))
            kwargs['subscription_obj'] = subscription_obj
            if 'custom_id' in subscription_obj:
                donation_id = subscription_obj['custom_id']
            else:
                raise ValueError(
                    _('Missing custom_id(donation_id) in curlPaypal-returned subscription'
                      ))
        elif request.GET.get('token', None):
            # onetime payment
            req = OrdersGetRequest(request.GET.get('token'))
            # might throw IOError
            response = client.execute(req)
            _debug('PayPal: Returns from Gateway')
            _debug('PayPal: Order status: ' + response.result.status)
            donation_id = response.result.purchase_units[0].custom_id
            kwargs['order_id'] = request.GET.get('token')
            kwargs['order_status'] = response.result.status
            if not donation_id:
                raise ValueError(
                    _("Missing donation_id in purchase_units custom_id attribute"
                      ))
        else:
            raise ValueError(_("Missing token from PayPal request"))

        try:
            donation = Donation.objects.get(pk=donation_id)

            if request.GET.get('subscription_id', None):
                # raise error if paypal_subscription_id already found in DonationPaymentMeta
                dpm = DonationPaymentMeta.objects.filter(
                    donation=donation,
                    field_key='paypal_subscription_id',
                    field_value=request.GET.get('subscription_id'))
                if len(dpm) >= 1:
                    raise ValueError(
                        _("PayPal subscription id found. Return request from PayPal is already invalid."
                          ))
                else:
                    dpm = DonationPaymentMeta(
                        donation=donation,
                        field_key='paypal_subscription_id',
                        field_value=request.GET.get('subscription_id'))
                    dpm.save()
            elif request.GET.get('token', None):
                # raise error if paypal_token already found in DonationPaymentMeta
                dpm = DonationPaymentMeta.objects.filter(
                    donation=donation,
                    field_key='paypal_token',
                    field_value=request.GET.get('token'))
                if len(dpm) >= 1:
                    raise ValueError(
                        _("PayPal token found. Return request from PayPal is already invalid."
                          ))
                else:
                    dpm = DonationPaymentMeta(
                        donation=donation,
                        field_key='paypal_token',
                        field_value=request.GET.get('token'))
                    dpm.save()
            return Factory_Paypal.initGateway(request, donation, None,
                                              **kwargs)
        except Donation.DoesNotExist:
            raise ValueError(
                _("Donation object not found by id: ") + str(donation_id))
Esempio n. 10
0
    def process_webhook_response(self):
        initStripeApiKey()
        # Decide what actions to perform on Newstream's side according to the results/events from the Stripe notifications
        # Event: checkout.session.completed
        if self.event['type'] == EVENT_CHECKOUT_SESSION_COMPLETED:
            # Update payment status
            self.donation.payment_status = STATUS_COMPLETE
            # update donation_date
            self.donation.donation_date = datetime.now(timezone.utc)
            self.donation.save()

            # Since for recurring payment, subscription.updated event might lag behind checkout.session.completed
            if not self.donation.is_recurring:
                sendDonationReceiptToDonor(self.donation)
                sendDonationNotifToAdmins(self.donation)

            return HttpResponse(status=200)

        # Event: payment_intent.succeeded
        # Should be handled for onetime donations
        if self.event['type'] == EVENT_PAYMENT_INTENT_SUCCEEDED:
            # Update payment transaction_id as the charge id
            self.donation.transaction_id = self.payment_intent['charges'][
                'data'][0]['id']
            self.donation.save()

            return HttpResponse(status=200)

        # Event: invoice.created (for subscriptions, just return 200 here and do nothing - to signify to Stripe that it can proceed and finalize the invoice)
        # https://stripe.com/docs/billing/subscriptions/webhooks#understand
        if self.event['type'] == EVENT_INVOICE_CREATED and hasattr(
                self, 'subscription_obj') and hasattr(self, 'invoice'):
            return HttpResponse(status=200)

        # Event: invoice.paid (for subscriptions)
        if self.event['type'] == EVENT_INVOICE_PAID and hasattr(
                self, 'subscription_obj') and hasattr(self, 'invoice'):
            if self.invoice.status == 'paid':
                _debug("[stripe recurring] Invoice confirmed paid")
                # check if subscription has one or more invoices to determine it's a first time or renewal payment
                # self.subscription_obj here is the stripe subscription object
                try:
                    invoices = stripe.Invoice.list(
                        subscription=self.subscription_obj.id)
                except (stripe.error.RateLimitError,
                        stripe.error.InvalidRequestError,
                        stripe.error.AuthenticationError,
                        stripe.error.APIConnectionError,
                        stripe.error.StripeError) as e:
                    raise RuntimeError(
                        "Stripe API Error({}): Status({}), Code({}), Param({}), Message({})"
                        .format(
                            type(e).__name__, e.http_status, e.code, e.param,
                            e.user_message))
                # _debug("Stripe: Subscription {} has {} invoices.".format(self.subscription_obj.id, len(invoices['data'])))
                if len(invoices['data']) == 1:
                    _debug("[stripe recurring] First time subscription")
                    # save charge id as donation.transaction_id
                    self.donation.transaction_id = self.invoice.charge
                    self.donation.payment_status = STATUS_COMPLETE
                    self.donation.save()

                    # also save the invoice number as a DonationPaymentMeta
                    dpmeta = DonationPaymentMeta(
                        donation=self.donation,
                        field_key='stripe_invoice_number',
                        field_value=self.invoice.number)
                    dpmeta.save()
                elif len(invoices['data']) > 1:
                    _debug("[stripe recurring] About to add renewal donation")
                    # create a new donation record + then send donation receipt to user
                    # self.donation is the first donation made for a subscription
                    donation = Donation(
                        is_test=self.donation.is_test,
                        subscription=self.donation.subscription,
                        transaction_id=self.invoice.charge,
                        user=self.donation.user,
                        form=self.donation.form,
                        gateway=self.donation.gateway,
                        is_recurring=True,
                        donation_amount=formatDonationAmountFromGateway(
                            str(self.invoice.amount_paid),
                            self.donation.currency),
                        currency=self.donation.currency,
                        payment_status=STATUS_COMPLETE,
                        donation_date=datetime.now(timezone.utc),
                    )
                    donation.save()

                    dpmeta = DonationPaymentMeta(
                        donation=donation,
                        field_key='stripe_invoice_number',
                        field_value=self.invoice.number)
                    dpmeta.save()

                    # email notifications
                    sendRenewalReceiptToDonor(donation)
                    sendRenewalNotifToAdmins(donation)

                # log down the current subscription period span
                spmeta = SubscriptionPaymentMeta(
                    subscription=self.donation.subscription,
                    field_key='stripe_subscription_period',
                    field_value=str(self.subscription_obj.current_period_start)
                    + '-' + str(self.subscription_obj.current_period_end))
                spmeta.save()

                return HttpResponse(status=200)

        # Event: customer.subscription.updated
        if self.event[
                'type'] == EVENT_CUSTOMER_SUBSCRIPTION_UPDATED and hasattr(
                    self, 'subscription_obj'):
            # Subscription active after invoice paid
            if self.subscription_obj['status'] == 'active':
                if self.donation.subscription.recurring_status == STATUS_PROCESSING:
                    # save the new subscription, marked by profile_id
                    self.donation.subscription.profile_id = self.subscription_obj.id
                    self.donation.subscription.recurring_amount = formatDonationAmountFromGateway(
                        self.subscription_obj['items']['data'][0]['price']
                        ['unit_amount_decimal'], self.donation.currency)
                    self.donation.subscription.currency = self.donation.currency
                    self.donation.subscription.recurring_status = STATUS_ACTIVE
                    self.donation.subscription.save()

                    # set donation payment_status to complete(as this event may be faster than checkout.session.completed, for the email is sent next line)
                    self.donation.payment_status = STATUS_COMPLETE
                    self.donation.save()

                    # send the new recurring notifs to admins and donor as subscription is just active
                    sendNewRecurringNotifToAdmins(self.donation.subscription)
                    sendNewRecurringNotifToDonor(self.donation.subscription)
                else:
                    # check if pause_collection is marked_uncollectible
                    if self.subscription_obj[
                            'pause_collection'] and self.subscription_obj[
                                'pause_collection'][
                                    'behavior'] == 'mark_uncollectible':
                        self.donation.subscription.recurring_status = STATUS_PAUSED
                    else:
                        self.donation.subscription.recurring_status = STATUS_ACTIVE
                    self.donation.subscription.save()

                # price changes events should goes through the if-else block and returns 200 right here
                return HttpResponse(status=200)
            else:
                return HttpResponse(status=400)

        # Event: customer.subscription.deleted
        # self.donation is not initialized here, reason refer to Factory_Stripe.initGatewayByVerification
        if self.event[
                'type'] == EVENT_CUSTOMER_SUBSCRIPTION_DELETED and hasattr(
                    self, 'subscription_obj'):
            # update subscription recurring_status
            self.donation.subscription.recurring_status = STATUS_CANCELLED
            self.donation.subscription.save()

            # email notifications here because cancellation might occur manually at the stripe dashboard
            sendRecurringCancelledNotifToAdmins(self.donation.subscription)
            sendRecurringCancelledNotifToDonor(self.donation.subscription)

            return HttpResponse(status=200)

        # for other events:
        return HttpResponse(status=400)
Esempio n. 11
0
    def process_webhook_response(self):
        # case one: donation is passed + not first_time_subscription = onetime donation
        if self.donation and not hasattr(self, 'first_time_subscription'):
            # change donation payment_status to 2c2p's payment_status, update recurring_status
            self.donation.payment_status = map2C2PPaymentStatus(
                self.data['payment_status'])
            self.donation.donation_date = datetime.now()
            self.donation.save()

            # email notifications
            if self.donation.payment_status == STATUS_REVOKED:
                sendDonationRevokedToDonor(self.donation)
                sendDonationRevokedToAdmins(self.donation)
            else:
                sendDonationReceiptToDonor(self.donation)
                sendDonationNotifToAdmins(self.donation)

            return HttpResponse(status=200)
        # case two: donation is passed + first_time_subscription: true
        if self.donation and self.first_time_subscription:
            # change donation payment_status to 2c2p's payment_status, update recurring_status
            self.donation.payment_status = map2C2PPaymentStatus(
                self.data['payment_status'])
            self.donation.save()

            if self.donation.payment_status == STATUS_COMPLETE:
                # create new Subscription object
                subscription = Subscription(
                    is_test=self.testing_mode,
                    profile_id=self.data['recurring_unique_id'],
                    user=self.donation.user,
                    gateway=self.donation.gateway,
                    recurring_amount=extract_payment_amount(
                        self.data['amount'], self.data['currency']),
                    currency=currencyCodeToKey(self.data['currency']),
                    recurring_status=STATUS_ACTIVE,
                    subscribe_date=datetime.now(timezone.utc))
                subscription.save()
                # link subscription to the donation
                self.donation.subscription = subscription
                self.donation.save()

                # send the donation receipt to donor and notification to admins if subscription is just created
                sendDonationReceiptToDonor(self.donation)
                sendDonationNotifToAdmins(self.donation)

                return HttpResponse(200)
            else:
                raise ValueError(
                    _("Cannot create subscription object due to donation payment_status: %(status)s"
                      ) % {'status': self.donation.payment_status})
        # case 3: renewals
        if not self.donation and self.subscription:
            # find the first donation made for this subscription
            fDonation = Donation.objects.filter(
                subscription=self.subscription).order_by('id').first()
            # Create new donation record from fDonation
            donation = Donation(
                is_test=self.testing_mode,
                subscription=self.subscription,
                transaction_id=self.data['order_id'],
                user=fDonation.user,
                form=fDonation.form,
                gateway=fDonation.gateway,
                is_recurring=True,
                donation_amount=extract_payment_amount(self.data['amount'],
                                                       self.data['currency']),
                currency=currencyCodeToKey(self.data['currency']),
                payment_status=map2C2PPaymentStatus(
                    self.data['payment_status']),
                donation_date=datetime.now(timezone.utc),
            )
            _debug('Save renewal Donation:' + self.data['order_id'])
            donation.save()

            # email notifications
            if donation.payment_status == STATUS_REVOKED:
                sendDonationRevokedToDonor(donation)
                sendDonationRevokedToAdmins(donation)
            else:
                sendRenewalReceiptToDonor(donation)
                sendRenewalNotifToAdmins(donation)

            return HttpResponse(200)
        else:
            raise RuntimeError(
                _("Unable to process_webhook_response after verifying 2C2P request"
                  ))
Esempio n. 12
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))
Esempio n. 13
0
    def initGatewayByVerification(request):
        paypalSettings = getPayPalSettings()

        # The payload body sent in the webhook event
        event_body = request.body.decode()
        json_data = json.loads(request.body)
        _debug('Event Type: ' + json_data['event_type'])
        # printvars(request.headers)
        # Paypal-Transmission-Id in webhook payload header
        transmission_id = request.headers['Paypal-Transmission-Id']
        # Paypal-Transmission-Time in webhook payload header
        timestamp = request.headers['Paypal-Transmission-Time']
        # Webhook id created
        webhook_id = paypalSettings.webhook_id
        # Paypal-Transmission-Sig in webhook payload header
        actual_signature = request.headers['Paypal-Transmission-Sig']
        # Paypal-Cert-Url in webhook payload header
        cert_url = request.headers['Paypal-Cert-Url']
        # PayPal-Auth-Algo in webhook payload header
        auth_algo = request.headers['PayPal-Auth-Algo']

        response = WebhookEvent.verify(transmission_id, timestamp, webhook_id,
                                       event_body, cert_url, actual_signature,
                                       auth_algo)
        if not response:
            _debug('Webhook verification result: ' + str(response))

        if response:
            donation_id = None
            subscription = None
            subscription_obj = None
            kwargs = {}
            expected_events = [
                EVENT_PAYMENT_CAPTURE_COMPLETED,
                EVENT_BILLING_SUBSCRIPTION_ACTIVATED,
                EVENT_BILLING_SUBSCRIPTION_UPDATED,
                EVENT_PAYMENT_SALE_COMPLETED,
                EVENT_BILLING_SUBSCRIPTION_CANCELLED
            ]

            # one-time donation payment captured
            if json_data['event_type'] == EVENT_PAYMENT_CAPTURE_COMPLETED:
                if 'custom_id' in json_data['resource']:
                    donation_id = json_data['resource']['custom_id']
                else:
                    raise ValueError(
                        _('Missing custom_id(donation_id) in json_data.resource'
                          ))

            # subscription activated
            if json_data['event_type'] == EVENT_BILLING_SUBSCRIPTION_ACTIVATED:
                subscription_obj = json_data['resource']
                if 'custom_id' in json_data['resource']:
                    donation_id = json_data['resource']['custom_id']
                else:
                    raise ValueError(
                        _('Missing custom_id(donation_id) in json_data.resource'
                          ))

            # subscription updated
            if json_data['event_type'] == EVENT_BILLING_SUBSCRIPTION_UPDATED:
                subscription_obj = json_data['resource']
                if 'custom_id' in json_data['resource']:
                    donation_id = json_data['resource']['custom_id']
                else:
                    raise ValueError(
                        _('Missing custom_id(donation_id) in json_data.resource'
                          ))

            # subscription payment sale completed
            if json_data['event_type'] == EVENT_PAYMENT_SALE_COMPLETED:
                subscription_id = json_data['resource']['billing_agreement_id']
                subscription_obj = getSubscriptionDetails(
                    request.session, subscription_id)
                if 'custom_id' in subscription_obj:
                    donation_id = subscription_obj['custom_id']
                else:
                    raise ValueError(
                        _('Missing custom_id(donation_id) in curlPaypal-returned subscription_obj'
                          ))

            # subscription cancelled
            if json_data['event_type'] == EVENT_BILLING_SUBSCRIPTION_CANCELLED:
                subscription_obj = json_data['resource']
                if 'custom_id' in json_data['resource']:
                    donation_id = json_data['resource']['custom_id']
                else:
                    raise ValueError(
                        _('Missing custom_id(donation_id) in json_data.resource'
                          ))

            if json_data['event_type'] in expected_events and not donation_id:
                raise ValueError(
                    _("Missing donation_id after processing events from paypal"
                      ))
            if json_data['event_type'] not in expected_events:
                raise WebhookNotProcessedError(
                    _("PayPal Event not expected for processing at the moment")
                )
            try:
                donation = Donation.objects.get(pk=donation_id)
                kwargs['payload'] = json_data['resource']
                kwargs['event_type'] = json_data['event_type']
                kwargs['subscription_obj'] = subscription_obj
                return Factory_Paypal.initGateway(request, donation,
                                                  subscription, **kwargs)
            except Donation.DoesNotExist:
                raise ValueError(
                    _("Donation object not found by id: ") + str(donation_id))
        else:
            raise ValueError(_("PayPal Webhook verification failed."))
Esempio n. 14
0
    def initGatewayByVerification(request):
        '''
        This method verifies webhooks of either Newstream-created transactions or Givewp-created subscriptions
        In givewp, $event_json->data->object->charge is ensured to be not empty before processing the webhooks,
        but I cannot find the 'charge' attribute in my Newstream 'customer.subscription.deleted' event,
        thus I will not apply the same checking here but run the same stripe_signature header check instead.
        '''
        initStripeApiKey()
        stripeSettings = getStripeSettings()

        payload = request.body
        sig_header = request.META['HTTP_STRIPE_SIGNATURE']
        donation_id = None
        can_skip_donation_id = False
        event = None
        session = None
        payment_intent = None
        donation = None
        subscription = None
        subscription_obj = None
        invoice = None
        kwargs = {}
        expected_events = [
            EVENT_CHECKOUT_SESSION_COMPLETED, EVENT_PAYMENT_INTENT_SUCCEEDED,
            EVENT_INVOICE_CREATED, EVENT_INVOICE_PAID,
            EVENT_CUSTOMER_SUBSCRIPTION_UPDATED,
            EVENT_CUSTOMER_SUBSCRIPTION_DELETED
        ]

        # the following call will raise ValueError/stripe.error.SignatureVerificationError
        event = stripe.Webhook.construct_event(payload, sig_header,
                                               stripeSettings.webhook_secret)

        # for events not being processed
        if event['type'] not in expected_events:
            raise WebhookNotProcessedError(
                _("Stripe Event not expected for processing at the moment"))
        _debug("[stripe recurring] Incoming Event type:" + event['type'])

        # Intercept the checkout.session.completed event
        if event['type'] == EVENT_CHECKOUT_SESSION_COMPLETED:
            session = event['data']['object']
            # Fulfill the purchase...
            if session:
                if session.mode == 'payment' and session.payment_intent:
                    payment_intent = stripe.PaymentIntent.retrieve(
                        session.payment_intent)
                    if 'donation_id' in payment_intent.metadata:
                        donation_id = payment_intent.metadata['donation_id']
                    else:
                        raise ValueError(
                            _('Missing donation_id in payment_intent.metadata')
                        )
                elif session.mode == 'subscription' and session.subscription:
                    subscription_obj = stripe.Subscription.retrieve(
                        session.subscription)
                    if 'donation_id' in subscription_obj.metadata:
                        donation_id = subscription_obj.metadata['donation_id']
                    else:
                        raise ValueError(
                            _('Missing donation_id in subscription_obj.metadata'
                              ))

        # Intercept the payment_intent.succeeded event
        if event['type'] == EVENT_PAYMENT_INTENT_SUCCEEDED:
            payment_intent = event['data']['object']
            # Fulfill the purchase...
            if payment_intent:
                try:
                    dpm = DonationPaymentMeta.objects.get(
                        field_key='stripe_payment_intent_id',
                        field_value=payment_intent.id)
                    donation = dpm.donation
                    can_skip_donation_id = True
                except DonationPaymentMeta.DoesNotExist:
                    # should be renewal payments since only one-time payments have saved stripe_payment_intent_id
                    raise WebhookNotProcessedError(
                        _('Payment Intent Id not found in DonationPaymentMeta:'
                          ) + payment_intent.id)

        # Intercept the invoice created event for subscriptions(a must for instant invoice finalization)
        # if it's givewp subscription -> no donation_id in metadata is set -> query subscription from profile_id -> then get first donation as parent donation
        # if it's newstream-created subscription -> has donation_id in metadata -> query donation is enough(since profile_id might not have yet been set in subscription)
        # https://stripe.com/docs/billing/subscriptions/webhooks#understand
        if event['type'] == EVENT_INVOICE_CREATED:
            invoice = event['data']['object']
            subscription_id = invoice.subscription

            if subscription_id:
                subscription_obj = stripe.Subscription.retrieve(
                    subscription_id)
                if 'donation_id' in subscription_obj.metadata:
                    # newstream-created subscription
                    donation_id = subscription_obj.metadata['donation_id']
                else:
                    # givewp subscription
                    try:
                        subscription = Subscription.objects.get(
                            profile_id=subscription_id)
                        donation = Donation.objects.filter(
                            subscription=subscription).order_by('id').first()
                        can_skip_donation_id = True
                        if not donation:
                            raise ValueError(
                                _('Missing parent donation queried via Subscription, subscription_id: '
                                  ) + subscription_id)
                    except Subscription.DoesNotExist:
                        raise ValueError(
                            _('No matching Subscription found, profile_id: ') +
                            subscription_id)
            else:
                raise ValueError(_('Missing subscription_id'))

        # Intercept the invoice paid event for subscriptions
        # At Givewp, invoice.payment_succeeded is processed instead
        # but I checked that the payloads of both invoice.paid and invoice.payment_succeeded are the same
        # so I will process only the invoice.paid event for both Newstream or Givewp Subscriptions
        # if it's givewp subscription -> no donation_id in metadata is set -> query subscription from profile_id -> then get first donation as parent donation
        # if it's newstream-created subscription -> has donation_id in metadata -> query donation is enough(since profile_id might not have yet been set in subscription)
        if event['type'] == EVENT_INVOICE_PAID:
            invoice = event['data']['object']
            subscription_id = invoice.subscription

            if subscription_id:
                subscription_obj = stripe.Subscription.retrieve(
                    subscription_id)
                if 'donation_id' in subscription_obj.metadata:
                    # newstream-created subscription
                    donation_id = subscription_obj.metadata['donation_id']
                else:
                    # givewp subscription
                    try:
                        subscription = Subscription.objects.get(
                            profile_id=subscription_id)
                        donation = Donation.objects.filter(
                            subscription=subscription).order_by('id').first()
                        can_skip_donation_id = True
                        if not donation:
                            raise ValueError(
                                _('Missing parent donation queried via Subscription, subscription_id: '
                                  ) + subscription_id)
                    except Subscription.DoesNotExist:
                        raise ValueError(
                            _('No matching Subscription found, profile_id: ') +
                            subscription_id)
            else:
                raise ValueError(_('Missing subscription_id'))

        # The subscription created event is not to be used for subscription model init, so as to prevent race condition with the subscription model init in updated event
        # That is because there is no guarantee which event hits first, it's better to let one event handles the model init as well.

        # Intercept the subscription updated event
        # if it's givewp subscription -> no donation_id in metadata is set -> query subscription from profile_id -> then get first donation as parent donation
        # if it's newstream-created subscription -> has donation_id in metadata -> query donation is enough(since profile_id might not have yet been set in subscription)
        if event['type'] == EVENT_CUSTOMER_SUBSCRIPTION_UPDATED:
            subscription_obj = event['data']['object']

            if subscription_obj:
                if 'donation_id' in subscription_obj.metadata:
                    # newstream-created subscription
                    donation_id = subscription_obj.metadata['donation_id']
                else:
                    # givewp subscription
                    subscription_id = subscription_obj.id
                    try:
                        subscription = Subscription.objects.get(
                            profile_id=subscription_id)
                        donation = Donation.objects.filter(
                            subscription=subscription).order_by('id').first()
                        can_skip_donation_id = True
                        if not donation:
                            raise ValueError(
                                _('Missing parent donation queried via Subscription, subscription_id: '
                                  ) + subscription_id)
                    except Subscription.DoesNotExist:
                        raise ValueError(
                            _('No matching Subscription found, profile_id: ') +
                            subscription_id)

        # Intercept the subscription deleted event
        # This event links to either Newstream or Givewp created subscriptions
        # if it's givewp subscription -> no donation_id in metadata is set -> query subscription from profile_id -> then get first donation as parent donation
        # if it's newstream-created subscription -> has donation_id in metadata -> query donation is enough(since profile_id might not have yet been set in subscription)
        if event['type'] == EVENT_CUSTOMER_SUBSCRIPTION_DELETED:
            subscription_obj = event['data']['object']

            if subscription_obj:
                if 'donation_id' in subscription_obj.metadata:
                    # newstream-created subscription
                    donation_id = subscription_obj.metadata['donation_id']
                else:
                    # givewp subscription
                    subscription_id = subscription_obj.id
                    try:
                        subscription = Subscription.objects.get(
                            profile_id=subscription_id)
                        donation = Donation.objects.filter(
                            subscription=subscription).order_by('id').first()
                        can_skip_donation_id = True
                        if not donation:
                            raise ValueError(
                                _('Missing parent donation queried via Subscription, subscription_id: '
                                  ) + subscription_id)
                    except Subscription.DoesNotExist:
                        raise ValueError(
                            _('No matching Subscription found, profile_id: ') +
                            subscription_id)

        # Finally init and return the Stripe Gateway Manager
        if not donation_id and not can_skip_donation_id:
            raise ValueError(_('Missing donation_id'))

        try:
            # no need to query donation object if can skip
            if not can_skip_donation_id and not donation:
                donation = Donation.objects.get(pk=donation_id)
            kwargs['session'] = session
            kwargs['event'] = event
            kwargs['payment_intent'] = payment_intent
            kwargs['subscription_obj'] = subscription_obj
            kwargs['invoice'] = invoice
            return Factory_Stripe.initGateway(request, donation, subscription,
                                              **kwargs)
        except Donation.DoesNotExist:
            raise ValueError(
                _('No matching Donation found, donation_id: ') +
                str(donation_id))
Esempio n. 15
0
 def initGatewayByVerification(request):
     settings = get2C2PSettings()
     data = {}
     # debugging POST params from 2C2P
     for key, value in request.POST.items():
         _debug(key + ': ' + value)
     for key in getResponseParamOrder():
         if key in request.POST:
             data[key] = request.POST[key]
     if 'hash_value' in request.POST and request.POST['hash_value']:
         hash_value = request.POST['hash_value']
         checkHashStr = ''
         for key in getResponseParamOrder():
             if key in data.keys():
                 checkHashStr += data[key]
         checkHash = hmac.new(bytes(settings.secret_key, 'utf-8'),
                              bytes(checkHashStr, 'utf-8'),
                              hashlib.sha256).hexdigest()
         if hash_value.lower() == checkHash.lower():
             # distinguish between various cases
             # case one: onetime payment response
             if request.POST['user_defined_1'] and not request.POST[
                     'recurring_unique_id']:
                 try:
                     donation = Donation.objects.get(
                         pk=int(request.POST['user_defined_1']))
                     return Factory_2C2P.initGateway(request,
                                                     donation,
                                                     None,
                                                     data=data)
                 except Donation.DoesNotExist:
                     raise ValueError(
                         _('Cannot identify donation record from 2C2P request, id: %(id)s'
                           ) % {'id': request.POST['user_defined_1']})
             # case two: either first time subscription or renewal donation
             elif request.POST['recurring_unique_id']:
                 try:
                     subscription = Subscription.objects.get(
                         profile_id=str(
                             request.POST['recurring_unique_id']),
                         gateway__title=GATEWAY_2C2P)
                     _debug(
                         '--2C2P initGatewayByVerification: subscription found--'
                     )
                     # subscription object found, indicating this is a renewal request
                     return Factory_2C2P.initGateway(request,
                                                     None,
                                                     subscription,
                                                     data=data)
                 except Subscription.DoesNotExist:
                     # Subscription object not created yet, indicating this is the first time subscription
                     try:
                         donation = Donation.objects.get(
                             pk=int(request.POST['user_defined_1']))
                         return Factory_2C2P.initGateway(
                             request,
                             donation,
                             None,
                             data=data,
                             first_time_subscription=True)
                     except Donation.DoesNotExist:
                         raise ValueError(
                             _('Cannot identify donation record from 2C2P request, id: %(id)s'
                               ) % {'id': request.POST['user_defined_1']})
         else:
             _debug("hash_value: " + hash_value)
             _debug("checkHash: " + checkHash)
             printvars(data)
             raise ValueError(
                 _("hash_value does not match with checkHash, cannot verify request from 2C2P."
                   ))
     else:
         raise ValueError(
             _("No hash_value in request.POST, cannot verify request from 2C2P."
               ))
Esempio n. 16
0
    def process_webhook_response(self):
        # Event: EVENT_PAYMENT_CAPTURE_COMPLETED (This alone comes after the onetime donation is captured)
        if self.event_type == EVENT_PAYMENT_CAPTURE_COMPLETED:
            # update transaction_id
            self.donation.transaction_id = self.payload['id']
            self.donation.save()
            # payment should have been completed after successful capture at the moment of returning to this site
            # only run below code if somehow payment_status is still not complete(e.g. donor did not return to site)
            if self.donation.payment_status != STATUS_COMPLETE:
                self.donation.payment_status = STATUS_COMPLETE
                self.donation.save()

            # send email notifs
            sendDonationReceiptToDonor(self.donation)
            sendDonationNotifToAdmins(self.donation)

            return HttpResponse(status=200)

        # Event: EVENT_BILLING_SUBSCRIPTION_ACTIVATED
        if self.event_type == EVENT_BILLING_SUBSCRIPTION_ACTIVATED and hasattr(
                self, 'subscription_obj'):
            if self.subscription_obj['status'] == 'ACTIVE':
                if self.donation.subscription.recurring_status == STATUS_PROCESSING:
                    # save the new subscription, marked by profile_id
                    self.donation.subscription.profile_id = self.subscription_obj[
                        'id']
                    self.donation.subscription.recurring_amount = Decimal(
                        self.subscription_obj['billing_info']['last_payment']
                        ['amount']['value'])
                    self.donation.subscription.currency = self.subscription_obj[
                        'billing_info']['last_payment']['amount'][
                            'currency_code']
                    self.donation.subscription.recurring_status = STATUS_ACTIVE
                    self.donation.subscription.save()

                    # send the new recurring notifs to admins and donor as subscription is just active
                    sendNewRecurringNotifToAdmins(self.donation.subscription)
                    sendNewRecurringNotifToDonor(self.donation.subscription)

                return HttpResponse(status=200)
            else:
                raise ValueError(
                    _("EVENT_BILLING_SUBSCRIPTION_ACTIVATED but subscription status is %(status)s"
                      ) % {'status': self.subscription_obj['status']})

        # Event: EVENT_BILLING_SUBSCRIPTION_UPDATED
        if self.event_type == EVENT_BILLING_SUBSCRIPTION_UPDATED and hasattr(
                self, 'subscription_obj'):
            if self.subscription_obj[
                    'status'] == 'SUSPENDED' or self.subscription_obj[
                        'status'] == 'ACTIVE':
                subscription = Subscription.objects.filter(
                    profile_id=self.subscription_obj['id']).first()
                if not subscription:
                    raise ValueError(
                        _("Cannot find subscription object in database with profile_id %(id)s"
                          ) % {'id': self.subscription_obj['id']})
                subscription.recurring_amount = Decimal(
                    self.subscription_obj['plan']['billing_cycles'][0]
                    ['pricing_scheme']['fixed_price']['value'])
                subscription.save()

                return HttpResponse(status=200)
            else:
                raise ValueError(
                    _("EVENT_BILLING_SUBSCRIPTION_UPDATED but subscription status is %(status)s"
                      ) % {'status': self.subscription_obj['status']})

        # Event: EVENT_PAYMENT_SALE_COMPLETED
        if self.event_type == EVENT_PAYMENT_SALE_COMPLETED and hasattr(
                self, 'subscription_obj'):
            if self.payload['state'] == 'completed':
                # check if this is first time subscription payment or a renewal payment
                donationPMs = DonationPaymentMeta.objects.filter(
                    donation=self.donation, field_key='paypal_first_cycle')
                renewals = Donation.objects.filter(
                    subscription__profile_id=self.subscription_obj['id'])
                _debug("Number of donation PMs: " + str(len(donationPMs)))
                _debug("Number of renewals for subscription_id({}): ".format(
                    self.subscription_obj['id']) + str(len(renewals)))
                if len(donationPMs) == 1 or len(renewals) >= 2:
                    # this is already a renewal payment
                    # self.donation is the first donation associated with the subscription
                    if not self.donation.subscription:
                        raise ValueError(
                            _("Missing subscription linkage/object for donation %(id)s"
                              ) % {'id': self.donation.id})
                    donation = Donation(
                        is_test=self.donation.is_test,
                        subscription=self.donation.subscription,
                        transaction_id=self.payload['id'],
                        user=self.donation.user,
                        form=self.donation.form,
                        gateway=self.donation.gateway,
                        is_recurring=True,
                        donation_amount=Decimal(
                            self.payload['amount']['total']),
                        currency=self.payload['amount']['currency'],
                        payment_status=STATUS_COMPLETE,
                        donation_date=datetime.now(timezone.utc),
                    )
                    # save new donation as a record of renewal donation
                    donation.save()

                    # email notifications
                    sendRenewalReceiptToDonor(donation)
                    sendRenewalNotifToAdmins(donation)
                else:
                    # this is a first time subscription payment
                    self.donation.payment_status = STATUS_COMPLETE
                    self.donation.transaction_id = self.payload['id']
                    self.donation.save()

                    # save DonationPaymentMeta as proof of first time subscription payment
                    dpmeta = DonationPaymentMeta(
                        donation=self.donation,
                        field_key='paypal_first_cycle',
                        field_value='completed')
                    dpmeta.save()

                return HttpResponse(status=200)
            else:
                raise ValueError(
                    _("EVENT_PAYMENT_SALE_COMPLETED but payment state is %(state)s"
                      ) % {'state': self.payload['state']})

        # Event: EVENT_BILLING_SUBSCRIPTION_CANCELLED
        if self.event_type == EVENT_BILLING_SUBSCRIPTION_CANCELLED and hasattr(
                self, 'subscription_obj'):
            if self.subscription_obj['status'] == 'CANCELLED':
                self.donation.subscription.recurring_status = STATUS_CANCELLED
                self.donation.subscription.save()

                # email notifications
                sendRecurringCancelledNotifToAdmins(self.donation.subscription)
                sendRecurringCancelledNotifToDonor(self.donation.subscription)

                return HttpResponse(status=200)
            else:
                raise ValueError(
                    _("EVENT_BILLING_SUBSCRIPTION_CANCELLED but subscription status is %(status)s"
                      ) % {'status': self.subscription_obj['status']})

        # return 400 for all other events
        return HttpResponse(status=400)
Esempio n. 17
0
    def process_webhook_response(self):
        if self.request.POST.get('txn_type', None) == 'subscr_payment':
            # copy what process_paypal_subscr_payment does in givewp to here
            # at this stage, Donation(parent) object is already populated from the custom param
            # Subscription object should exist also, since it is linked to Donation either via parent_payment_id
            # update profile_id if it's empty
            if not self.subscription.profile_id:
                self.subscription.profile_id = self.request.POST.get(
                    'subscr_id', None)
                self.subscription.save()

            transaction_id_dpm = ''
            try:
                # either it returns a record with empty field_value or not found(which raises the DoesNotExist exception)
                transaction_id_dpm = DonationPaymentMeta.objects.get(
                    donation=self.donation,
                    field_key='_give_payment_transaction_id')
            except DonationPaymentMeta.DoesNotExist as e:
                # will re-add transaction_id_dpm down below
                pass
            # parse 'payment_date' from ipn_data into python date object for comparison
            ipn_payment_date = self.request.POST.get('payment_date', None)
            pac_tz = pytz.timezone('US/Pacific')
            naive_date = re.sub(r'\s(PST|PDT)$', '', ipn_payment_date)
            ipn_payment_datetime = pac_tz.localize(
                datetime.strptime(naive_date, '%H:%M:%S %b %d, %Y'))
            ipn_payment_datetime_utc = ipn_payment_datetime.astimezone(
                pytz.utc)
            is_today = self.subscription.created_at.date(
            ) == ipn_payment_datetime_utc.date()

            # Look to see if payment is same day as sign up and we haven't set the transaction ID on the parent payment yet.
            if is_today and (not transaction_id_dpm or transaction_id_dpm
                             and not transaction_id_dpm.field_value):
                _debug("[paypal legacy recurring] is_today clause")
                # Verify the amount paid.
                initial_amount = round_half_up(
                    self.subscription.recurring_amount, 2)
                paid_amount = round_half_up(
                    self.request.POST.get('mc_gross', None), 2)

                if paid_amount < initial_amount:
                    # save DonationPaymentMeta as note of failure, but no need to save parent payment's status to failed
                    dpmeta = DonationPaymentMeta(
                        donation=self.donation,
                        field_key='IPN FAILURE',
                        field_value=str(
                            _('Payment failed due to invalid amount in PayPal Recurring IPN.'
                              )))
                    dpmeta.save()

                    raise Exception(
                        str(
                            _('Invalid payment amount in IPN subscr_payment response. IPN data: %(data)s'
                              ) %
                            {'data': json.dumps(self.request.POST.dict())}))

                # This is the very first payment so set the transaction ID.
                if transaction_id_dpm:
                    transaction_id_dpm.field_value = self.request.POST.get(
                        'txn_id', '')
                else:
                    transaction_id_dpm = DonationPaymentMeta(
                        donation=self.donation,
                        field_key='_give_payment_transaction_id',
                        field_value=self.request.POST.get('txn_id', ''))
                transaction_id_dpm.save()

                return HttpResponse(status=200)

            # Is this payment already recorded?
            try:
                transaction_id_dpm = DonationPaymentMeta.objects.get(
                    field_key='_give_payment_transaction_id',
                    field_value=self.request.POST.get('txn_id', ''))
                # Payment already recorded
                _debug("[paypal legacy recurring] payment already recorded")
                return HttpResponse(status=200)
            except DonationPaymentMeta.DoesNotExist as e:
                # continue code execution
                pass

            # add renewal payment to subscription
            # transaction_id uses the txn_id from paypal directly, just for convenience, value is the same as _give_payment_transaction_id in donationPaymentMeta
            renewal = Donation(
                is_test=self.testing_mode,
                subscription=self.donation.subscription,
                transaction_id=self.request.POST.get('txn_id')
                if self.request.POST.get('txn_id', '') else gen_transaction_id(
                    self.donation.gateway),
                user=self.donation.user,
                form=self.donation.form,
                gateway=self.donation.gateway,
                is_recurring=True,
                donation_amount=round_half_up(
                    self.request.POST.get('mc_gross', None), 2),
                currency=self.donation.currency,
                payment_status=STATUS_COMPLETE,
                donation_date=datetime.now(timezone.utc),
            )
            # save new donation as a record of renewal donation
            renewal.save()

            # email notifications
            sendRenewalReceiptToDonor(renewal)
            sendRenewalNotifToAdmins(renewal)

            # also save required DonationPaymentMetas, purchase key is skipped here as it should be saved at the parent payment/subscription
            transaction_id_dpm = DonationPaymentMeta(
                donation=renewal,
                field_key='_give_payment_transaction_id',
                field_value=self.request.POST.get('txn_id', ''))
            transaction_id_dpm.save()
            # skip out the renew method from givewp as we don't do that here

            return HttpResponse(status=200)
        elif self.request.POST.get('txn_type', None) == 'subscr_cancel':
            self.donation.subscription.recurring_status = STATUS_CANCELLED
            self.donation.subscription.save()

            # email notifications
            sendRecurringCancelledNotifToDonor(self.donation.subscription)
            sendRecurringCancelledNotifToAdmins(self.donation.subscription)

            return HttpResponse(status=200)
        else:
            _debug("[paypal legacy recurring] ipn txn_type not handled: " +
                   self.request.POST.get('txn_type', None))
            return HttpResponse(status=400)