def __init__(self, request, donation=None, subscription=None, **kwargs): super().__init__(request, donation, subscription) # set paypal settings object self.settings = getPayPalSettings() # init paypal http client self.client = PayPalHttpClient(self.settings.environment) # saves all remaining kwargs into the manager, e.g. order_id, order_status self.__dict__.update(kwargs)
def listProducts(session): checkAccessTokenExpiry(session) paypalSettings = getPayPalSettings() api_url = paypalSettings.api_url + '/v1/catalogs/products?page_size=20' if paypalSettings.sandbox_mode and session.get('negtest_listProducts', None): api_url += '&total_required=' + session.get('negtest_listProducts') return curlPaypal(api_url, common_headers(session['paypal_token']))
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'])
def getSubscriptionDetails(session, subscription_id): checkAccessTokenExpiry(session) paypalSettings = getPayPalSettings() api_url = paypalSettings.api_url + '/v1/billing/subscriptions/{}'.format( subscription_id) if paypalSettings.sandbox_mode and session.get( 'negtest_getSubscriptionDetails', None): api_url = paypalSettings.api_url + '/v1/billing/subscriptions/{}'.format( session.get('negtest_getSubscriptionDetails')) return curlPaypal(api_url, common_headers(session['paypal_token']))
def create_paypal_order(session, donation): paypalSettings = getPayPalSettings() client = PayPalHttpClient(paypalSettings.environment) req = OrdersCreateRequest() # set dictionary object in session['extra_test_headers'] in TestCases if session.get('extra_test_headers', None) and donation.is_test: for key, value in session.get('extra_test_headers').items(): req.headers[key] = value req.prefer('return=representation') req.request_body(build_onetime_request_body(donation)) return client.execute(req)
def suspendSubscription(session, subscription_id): checkAccessTokenExpiry(session) paypalSettings = getPayPalSettings() api_url = paypalSettings.api_url + '/v1/billing/subscriptions/{}/suspend'.format( subscription_id) if paypalSettings.sandbox_mode and session.get( 'negtest_suspendSubscription', None): api_url = paypalSettings.api_url + '/v1/billing/subscriptions/{}/suspend'.format( session.get('negtest_suspendSubscription')) return curlPaypal(api_url, common_headers(session['paypal_token']), verb='POST')
def cancelSubscription(session, subscription_id): '''paypal returns 404 if the subscription has not passed the approval stage''' checkAccessTokenExpiry(session) paypalSettings = getPayPalSettings() api_url = paypalSettings.api_url + '/v1/billing/subscriptions/{}/cancel'.format( subscription_id) if paypalSettings.sandbox_mode and session.get( 'negtest_cancelSubscription', None): api_url = paypalSettings.api_url + '/v1/billing/subscriptions/{}/cancel'.format( session.get('negtest_cancelSubscription')) return curlPaypal(api_url, common_headers(session['paypal_token']), verb='POST')
def createProduct(session, product_dict={}): checkAccessTokenExpiry(session) paypalSettings = getPayPalSettings() api_url = paypalSettings.api_url + '/v1/catalogs/products' if not product_dict: product_dict = { "name": "Newstream Donations", "description": "Newstream Donations(PayPal)", "type": "SERVICE", "category": "BOOKS_PERIODICALS_AND_NEWSPAPERS" } if paypalSettings.sandbox_mode and session.get('negtest_createProduct', None): product_dict['name'] = session.get('negtest_createProduct') return curlPaypal(api_url, common_headers(session['paypal_token']), post_data=json.dumps(product_dict))
def createSubscription(session, plan_id, donation, is_test=False): checkAccessTokenExpiry(session) paypalSettings = getPayPalSettings() api_url = paypalSettings.api_url + '/v1/billing/subscriptions' subscription_dict = { "plan_id": plan_id, "quantity": "1", "subscriber": { "name": { "given_name": getUserGivenName(donation.user), "surname": donation.user.last_name or '' }, "email_address": donation.user.email }, "application_context": { "brand_name": get_site_name(), "locale": "en-US", "shipping_preference": "NO_SHIPPING", "user_action": "SUBSCRIBE_NOW", "payment_method": { "payer_selected": "PAYPAL", "payee_preferred": "IMMEDIATE_PAYMENT_REQUIRED" }, "return_url": reverse_with_site_url('donations:return-from-paypal') if not is_test else 'http://example.com/return/', "cancel_url": reverse_with_site_url('donations:cancel-from-paypal') if not is_test else 'http://example.com/cancel/' }, "custom_id": str(donation.id) } if paypalSettings.sandbox_mode and session.get( 'negtest_createSubscription', None): subscription_dict['plan_id'] = session.get( 'negtest_createSubscription') return curlPaypal(api_url, common_headers(session['paypal_token']), post_data=json.dumps(subscription_dict))
def updateSubscription(session, subscription_id, new_amount, currency): checkAccessTokenExpiry(session) paypalSettings = getPayPalSettings() api_url = paypalSettings.api_url + '/v1/billing/subscriptions/{}'.format( subscription_id) patch_body = [{ "op": "replace", "path": "/plan/billing_cycles/@sequence==1/pricing_scheme/fixed_price", "value": { "currency_code": currency, "value": new_amount } }] if paypalSettings.sandbox_mode and session.get( 'negtest_updateSubscription', None): api_url = paypalSettings.api_url + '/v1/billing/subscriptions/{}'.format( session.get('negtest_updateSubscription')) return curlPaypal(api_url, common_headers(session['paypal_token']), post_data=json.dumps(patch_body), verb='PATCH')
def createPlan(session, product_id, donation): checkAccessTokenExpiry(session) paypalSettings = getPayPalSettings() api_url = paypalSettings.api_url + '/v1/billing/plans' plan_dict = { "product_id": product_id, "name": "Newstream Donation Plan for %s" % (donation.user.fullname), "description": "Newstream Donation Plan for %s" % (donation.user.fullname), "status": "ACTIVE", "billing_cycles": [{ "frequency": { "interval_unit": "MONTH", "interval_count": 1 }, "tenure_type": "REGULAR", "sequence": 1, "total_cycles": 0, "pricing_scheme": { "fixed_price": { "value": str(donation.donation_amount), "currency_code": donation.currency } } }], "payment_preferences": { "auto_bill_outstanding": 'true', "payment_failure_threshold": 3 } } if paypalSettings.sandbox_mode and session.get('negtest_createPlan', None): plan_dict['name'] = session.get('negtest_createPlan') return curlPaypal(api_url, common_headers(session['paypal_token']), post_data=json.dumps(plan_dict))
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
def create_paypal_transaction(request): """ When the user reaches last step after confirming the donation, user is redirected via gatewayManager.redirect_to_gateway_url(), which renders redirection_paypal.html This function calls to PayPal Api to create a PayPal subscription object or PayPal order(one-time donation), then this function returns the approval_link to frontend js and to redirect to PayPal's checkout page Sample (JSON) request: { 'csrfmiddlewaretoken': 'LZSpOsb364pn9R3gEPXdw2nN3dBEi7RWtMCBeaCse2QawCFIndu93fD3yv9wy0ij' } @todo: revise error handling, avoid catching all exceptions at the end """ errorObj = { "issue": "Exception", "description": "" } result = {} try: paypalSettings = getPayPalSettings() donation_id = request.session.pop('donation_id', None) if not donation_id: raise ValueError(_("Missing donation_id in session")) donation = Donation.objects.get(pk=int(donation_id)) if donation.is_recurring: # Product should have been created by admin manually at the dashboard/setup wizard # if no product exists, create one here(double safety net) # todo: make sure the product_id in site_settings has been set by some kind of configuration enforcement before site is launched product_list = listProducts(request.session) product = None if len(product_list['products']) == 0: product = createProduct(request.session) else: # get the product, should aim at the product with the specific product id for prod in product_list['products']: if prod['id'] == paypalSettings.product_id: product = prod if product == None: raise ValueError(_('Cannot initialize/get the paypal product object')) # Create plan and subscription plan = createPlan(request.session, product['id'], donation) if plan['status'] == 'ACTIVE': subscription = createSubscription(request.session, plan['id'], donation) result['subscription_id'] = subscription['id'] for link in subscription['links']: if link['rel'] == 'approve': result['approval_link'] = link['href'] else: raise ValueError(_("Newly created PayPal plan is not active, status: %(status)s") % {'status': plan['status']}) # else: one-time donation else: response = create_paypal_order(request.session, donation) ppresult = response.result _debug('PayPal: Order Created Status: '+ppresult.status) # set approval_link attribute for link in ppresult.links: _debug('PayPal: --- {}: {} ---'.format(link.rel, link.href)) if link.rel == 'approve': result['approval_link'] = link.href except ValueError as e: _exception(str(e)) errorObj['issue'] = "ValueError" errorObj['description'] = str(e) return JsonResponse(object_to_json(errorObj), status=500) except Donation.DoesNotExist: _exception("Donation.DoesNotExist") errorObj['issue'] = "Donation.DoesNotExist" errorObj['description'] = str(_("Donation object not found by id: %(id)s") % {'id': donation_id}) return JsonResponse(object_to_json(errorObj), status=500) except HttpError as ioe: # Catching exceptions from the paypalclient execution, HttpError is a subclass of IOError httpError = json.loads(ioe.message) if 'details' in httpError and len(httpError['details']) > 0: errorObj["issue"] = httpError['details'][0]['issue'] errorObj["description"] = httpError['details'][0]['description'] _exception(errorObj["description"]) # update donation status to failed donation.payment_status = STATUS_FAILED donation.save() return JsonResponse(object_to_json(errorObj), status=ioe.status_code) except Exception as error: errorObj['description'] = str(error) _exception(errorObj["description"]) return JsonResponse(object_to_json(errorObj), status=500) return JsonResponse(object_to_json(result))
def 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."))
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))