def get_donation_status_change_text(donation): donation_url = reverse_with_site_url( 'donations:my-renewals', kwargs={ 'id': donation.subscription.id }) if donation.is_recurring else reverse_with_site_url( 'donations:my-onetime-donations') if donation.user: url_text = str( _('Sign into our support page (click "forgot password?" if you have trouble logging in) to view your updated donation(%(url)s).' ) % {'url': donation_url}) + "\n" else: url_text = '' return _("""ONE-OFF DONATION STATUS UPDATED\n \n Dear %(name)s,\n %(url_text)s Details of your donation:\n \n Transaction ID: %(transaction_id)s\n Donation frequency: %(frequency)s\n Payment method: %(gateway)s\n Donation amount: %(amount)s\n Payment status: %(status)s\n \n Thank you,\n %(sitename)s""") % { 'name': donation.donor_name(), 'url_text': url_text, 'transaction_id': donation.transaction_id, 'frequency': donation.donation_frequency, 'gateway': displayGateway(donation), 'amount': displayDonationAmountWithCurrency(donation), 'status': donation.payment_status, 'sitename': get_site_name() }
def build_onetime_request_body(donation): """Method to create body with CAPTURE intent""" # returl_url or cancel_url params in application_context tested to be only useful if order not initiated by Javascript APK return \ { "intent": "CAPTURE", "application_context": { "brand_name": get_site_name(), "landing_page": "NO_PREFERENCE", "shipping_preference": "NO_SHIPPING", "user_action": "PAY_NOW", "return_url": reverse_with_site_url('donations:return-from-paypal'), "cancel_url": reverse_with_site_url('donations:cancel-from-paypal') }, "purchase_units": [ { "description": get_site_name() + str(_(" Onetime Donation")), "custom_id": donation.id, "amount": { "currency_code": donation.currency, "value": str(donation.donation_amount) } } ] }
def get_donation_receipt_text(donation): donation_url = reverse_with_site_url( 'donations:my-recurring-donations' ) if donation.is_recurring else reverse_with_site_url( 'donations:my-onetime-donations') if donation.user: url_text = str( _('Sign into our support page (click "forgot password?" if you have trouble logging in) to view your donation(%(url)s). Please email [email protected] if you have any further enquiries.' ) % {'url': donation_url}) else: url_text = '' return _("""NEW ONE-OFF DONATION\n \n Dear %(name)s,\n A big "thank you" for your kind %(amount)s donation - it is very much appreciated and it will go a long way in supporting our operations.\n Your contribution will be well-spent, allowing us to invest more in original reporting and safeguard press freedom. Please check out HKFP's latest Annual Report(https://hongkongfp.com/hong-kong-free-press-annual-report-2020/) - it includes our yearly, audited Transparency Report(https://hongkongfp.com/hong-kong-free-press-transparency-report-2019/), so you can see how carefully we spend our income.\n %(url_text)s\n From all of us, thank you for helping us keep independent media alive in Hong Kong!\n Details of your donation:\n \n Transaction ID: %(transaction_id)s\n Donation frequency: %(frequency)s\n Payment method: %(gateway)s\n Donation amount: %(amount)s\n Payment status: %(status)s\n %(recurring_status)s \n Thank you,\n %(sitename)s""") % { 'name': donation.donor_name(), 'url_text': url_text, 'transaction_id': donation.transaction_id, 'frequency': donation.donation_frequency, 'gateway': displayGateway(donation), 'amount': displayDonationAmountWithCurrency(donation), 'status': donation.payment_status, 'recurring_status': 'Recurring Status: ' + donation.subscription.recurring_status + "\n" if donation.is_recurring and donation.subscription else '', 'sitename': get_site_name() }
def get_donation_error_admin_text(donation, error_title, error_description): return _("""A Donation Error has occurred.\n \n Hi Admins,\n This email is to inform you that a donation error has occurred on your website:\n %(url)s\n \n Donation transaction ID: %(order)s\n Donor: %(name)s\n Error title: %(error_title)s\n Error description: %(error_description)s\n \n Thank you,\n %(sitename)s""") % { 'url': reverse_with_site_url('donations_donation_modeladmin_inspect', kwargs={'instance_pk': donation.id}), 'order': donation.transaction_id, 'name': donation.donor_name(), 'error_title': error_title, 'error_description': error_description, 'sitename': get_site_name() }
def get_recurring_cancelled_donor_text(subscription): return _("""DONATION CANCELLED\n \n Dear %(name)s,\n Thanks very much for your recent support.\n Your recurring donation to HKFP has been suspended at your request - no further payments will be processed.\n Sign into our support page(%(siteurl)s) (click "forgot password?" if you have trouble logging in) if you wish to support us again in the future. Please email [email protected] if you have any further enquiries.\n From all of us, thank you for backing our team and helping us keep independent media alive in Hong Kong!\n Details of your recurring donation:\n \n Donor: %(name)s\n Recurring donation identifier: %(profile_id)s\n Payment method: %(gateway)s\n Recurring donation amount: %(amount)s\n Recurring Status: %(recurring_status)s\n \n Thank you,\n %(sitename)s""") % { 'name': subscription.user.fullname, 'siteurl': get_site_url(), 'url': reverse_with_site_url('donations:my-recurring-donations'), 'profile_id': subscription.profile_id, 'gateway': displayGateway(subscription), 'amount': displayRecurringAmountWithCurrency(subscription), 'recurring_status': subscription.recurring_status, 'sitename': get_site_name() }
def get_recurring_cancel_request_admin_text(subscription): return _("""Cancellation to a Recurring Donation is requested\n \n Hi Admins,\n This email is to inform you that a cancellation to a recurring donation has been requested on your website. Please complete the request and manually change the subscription status to Cancelled at the link below:\n %(url)s\n \n Donor: %(name)s\n Recurring donation identifier: %(profile_id)s\n Payment method: %(gateway)s\n Recurring donation amount: %(amount)s\n Recurring Status: %(recurring_status)s\n \n Thank you,\n %(sitename)s""") % { 'url': reverse_with_site_url('donations_subscription_modeladmin_inspect', kwargs={'instance_pk': subscription.id}), 'name': subscription.user.fullname, 'profile_id': subscription.profile_id, 'gateway': subscription.gateway, 'amount': displayRecurringAmountWithCurrency(subscription), 'recurring_status': subscription.recurring_status, 'sitename': get_site_name() }
def get_recurring_resumed_donor_text(subscription): return _("""DONATION RESUMED\n \n Dear %(name)s,\n A big "thank you" for resuming your %(amount)s contribution - it is very much appreciated and it will go a long way in supporting our operations. Recurring donations, in particular, are vital to our sustainability. As an HKFP Patron, your contribution will be well-spent, allowing us to invest more in original reporting and safeguard press freedom. Please check out HKFP's latest Annual Report(https://hongkongfp.com/hong-kong-free-press-annual-report-2020/) - it includes our yearly, audited Transparency Report(https://hongkongfp.com/hong-kong-free-press-transparency-report-2019/), so you can see how carefully we spend our income.\n Sign into our support page (click "forgot password?" if you have trouble logging in) to adjust or suspend your donation(%(url)s). Please email [email protected] if you have any further enquiries.\n From all of us, thank you for helping us keep independent media alive in Hong Kong!\n Details of your recurring donation:\n \n Donor: %(name)s\n Recurring donation identifier: %(profile_id)s\n Payment method: %(gateway)s\n Recurring donation amount: %(amount)s\n Recurring Status: %(recurring_status)s\n \n Thank you,\n %(sitename)s""") % { 'name': subscription.user.fullname, 'url': reverse_with_site_url('donations:my-recurring-donations'), 'profile_id': subscription.profile_id, 'gateway': displayGateway(subscription), 'amount': displayRecurringAmountWithCurrency(subscription), 'recurring_status': subscription.recurring_status, 'sitename': get_site_name() }
def get_recurring_paused_admin_text(subscription): return _("""A Recurring Donation is paused\n \n Hi Admins,\n This email is to inform you that a recurring donation has been paused on your website:\n %(url)s\n \n Donor: %(name)s\n Recurring donation identifier: %(profile_id)s\n Payment method: %(gateway)s\n Recurring donation amount: %(amount)s\n Recurring Status: %(recurring_status)s\n \n Thank you,\n %(sitename)s""") % { 'url': reverse_with_site_url('donations_subscription_modeladmin_inspect', kwargs={'instance_pk': subscription.id}), 'name': subscription.user.fullname, 'profile_id': subscription.profile_id, 'gateway': subscription.gateway, 'amount': displayRecurringAmountWithCurrency(subscription), 'recurring_status': subscription.recurring_status, 'sitename': get_site_name() }
def get_donation_revoked_donor_text(donation): donation_url = reverse_with_site_url( 'donations:my-recurring-donations' ) if donation.is_recurring else reverse_with_site_url( 'donations:my-onetime-donations') if donation.user: url_text = str( _('Go to %(url)s to view your donation on the website.') % {'url': donation_url}) else: url_text = '' return _("""DONATION REVOKED\n \n Dear %(name)s,\n Your donation is unfortunately revoked by the payment gateway. %(url_text)s\n Here are the details of your donation:\n \n Transaction ID: %(transaction_id)s\n Donation frequency: %(frequency)s\n Payment method: %(gateway)s\n Donation amount: %(amount)s\n Payment status: %(status)s\n %(recurring_status)s \n Thank you,\n %(sitename)s""") % { 'name': donation.donor_name(), 'url_text': url_text, 'transaction_id': donation.transaction_id, 'frequency': donation.donation_frequency, 'gateway': displayGateway(donation), 'amount': displayDonationAmountWithCurrency(donation), 'status': donation.payment_status, 'recurring_status': 'Recurring Status: ' + donation.subscription.recurring_status + "\n" if donation.is_recurring and donation.subscription else '', 'sitename': get_site_name() }
def getUnsubscriptionLink(self, email): user = get_object_or_404(User, email=email) digest = generateIDSecretHash(user.id) return reverse_with_site_url('unsubscribe', kwargs={ 'email': email, 'hash': digest })
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 get_subscription_status_change_text(subscription): return _("""RECURRING DONATION STATUS UPDATED\n \n Dear %(name)s,\n Sign into our support page (click "forgot password?" if you have trouble logging in) to view your updated recurring donation(%(url)s).\n Details of your recurring donation:\n \n Profile ID: %(profile_id)s\n Payment method: %(gateway)s\n Recurring Amount: %(amount)s\n Status: %(status)s\n \n Thank you,\n %(sitename)s""") % { 'name': subscription.user.fullname, 'url': reverse_with_site_url('donations:my-recurring-donations'), 'profile_id': subscription.profile_id, 'gateway': displayGateway(subscription), 'amount': displayRecurringAmountWithCurrency(subscription), 'status': subscription.recurring_status, 'sitename': get_site_name() }
def get_donation_revoked_admin_text(donation): return _("""A Donation is revoked\n \n Hi Admins,\n This email is to inform you that a donation has been revoked on your website:\n %(url)s\n \n Donor: %(name)s\n Transaction ID: %(transaction_id)s\n Donation frequency: %(frequency)s\n Payment method: %(gateway)s\n Donation amount: %(amount)s\n Payment status: %(status)s\n %(recurring_status)s \n Thank you,\n %(sitename)s""") % { 'url': reverse_with_site_url('donations_donation_modeladmin_inspect', kwargs={'instance_pk': donation.id}), 'name': donation.donor_name(), 'transaction_id': donation.transaction_id, 'frequency': donation.donation_frequency, 'gateway': donation.gateway, 'amount': displayDonationAmountWithCurrency(donation), 'status': donation.payment_status, 'recurring_status': 'Recurring Status: ' + donation.subscription.recurring_status + "\n" if donation.is_recurring and donation.subscription else '', 'sitename': get_site_name() }
def redirect_to_gateway_url(self): """ Overriding parent implementation as 2C2P has to receive a form post from client browser. See docs https://developer.2c2p.com/docs/payment-requestresponse-parameters on recurring parameters behavior """ data = {} data['version'] = REDIRECT_API_VERSION data['merchant_id'] = self.settings.merchant_id data['order_id'] = self.donation.transaction_id data['currency'] = getCurrencyDictAt(self.donation.currency)['code'] # Beware: self.donation.donation_amount param is str in type data['amount'] = format_payment_amount(self.donation.donation_amount, self.donation.currency) # Apr 20 Tested result_url_1/2 working (such that merchant portal no need manual setting) after follow up with 2C2P Sum (an internal 2C2P settings needs to be turned on by them) # todo: Apr 21 2C2P server is not firing back the request from the new recurring payments (need follow up with Sum again) data['result_url_1'] = reverse_with_site_url( 'donations:return-from-2c2p') data['result_url_2'] = reverse_with_site_url( 'donations:verify-2c2p-response') data['user_defined_1'] = str(self.donation.id) if self.donation.is_recurring: data['payment_description'] = _( 'Recurring Donation for %(site)s') % { 'site': get_site_name() } data['request_3ds'] = 'Y' data['recurring'] = 'Y' data['order_prefix'] = gen_order_prefix_2c2p() data['recurring_amount'] = format_payment_amount( self.donation.donation_amount, self.donation.currency) data['allow_accumulate'] = 'N' data['recurring_count'] = 0 data['payment_option'] = 'A' # - daily recurring for testing data['recurring_interval'] = 1 data['charge_next_date'] = getNextDateFromRecurringInterval( data['recurring_interval'], '%d%m%Y') # - monthly recurring(normal behavior) # getRecurringDateNextMonth requires the superuser to set the timezone first # data['charge_on_date'] = getRecurringDateNextMonth('%d%m') # append order_prefix to donation metas for distinguishment # dpmeta = DonationPaymentMeta( # donation=self.donation, field_key='order_prefix', field_value=data['order_prefix']) # dpmeta.save() else: data['payment_description'] = _( 'Onetime Donation for %(site)s') % { 'site': get_site_name() } params = '' for key in getRequestParamOrder(): if key in data.keys(): params += str(data[key]) # python 3.6 code data['hash_value'] = hmac.new(bytes(self.settings.secret_key, 'utf-8'), bytes(params, 'utf-8'), hashlib.sha256).hexdigest() return render(self.request, 'donations/redirection_2c2p_form.html', { 'action': self.base_gateway_redirect_url(), 'data': data })
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)