def send_invoice_receipt(invoice_id): """Send out a receipt for an invoiced charge""" invoice = stripe_retry_on_error( stripe.Invoice.retrieve, invoice_id, ) try: charge = stripe_retry_on_error( stripe.Charge.retrieve, invoice.charge, ) except stripe.error.InvalidRequestError: # a free subscription has no charge attached # maybe send a notification about the renewal # but for now just handle the error return profile = Profile.objects.get(customer_id=invoice.customer) # send a receipt based on the plan customer = profile.customer() subscription = customer.subscriptions.retrieve(invoice.subscription) try: receipt_functions = { 'pro': receipts.pro_subscription_receipt, 'org': receipts.org_subscription_receipt } receipt_function = receipt_functions[subscription.plan.id] except KeyError: logger.warning('Invoice charged for unrecognized plan: %s', subscription.plan.name) receipt_function = receipts.generic_receipt receipt = receipt_function(profile.user, charge) receipt.send(fail_silently=False)
def cancel_pro_subscription(self): """Unsubscribe this profile from a professional plan. Return the cancelled subscription.""" customer = self.customer() subscription = None # subscription reference either exists as a saved field or inside the Stripe customer # if it isn't, then they probably don't have a subscription. in that case, just make # sure that we demote their account and reset them back to basic. try: if not self.subscription_id and not len(customer.subscriptions.data) > 0: raise AttributeError('There is no subscription to cancel.') if self.subscription_id: subscription_id = self.subscription_id else: subscription_id = customer.subscriptions.data[0].id subscription = stripe_retry_on_error( customer.subscriptions.retrieve, subscription_id, ) subscription = subscription.delete() customer = stripe_retry_on_error(customer.save, idempotency_key=True) except AttributeError as exception: logger.warn(exception) except stripe.error.StripeError as exception: logger.warn(exception) self.subscription_id = '' self.acct_type = 'basic' self.monthly_requests = settings.MONTHLY_REQUESTS.get('basic', 0) self.payment_failed = False self.save() return subscription
def buy_requests(request, username=None): """A purchaser buys requests for a recipient. The recipient can even be themselves!""" url_redirect = request.GET.get('next', 'acct-my-profile') bundles = int(request.POST.get('bundles', 1)) recipient = get_object_or_404(User, username=username) purchaser = request.user request_price = bundles * 2000 if purchaser.is_authenticated(): request_count = bundles * purchaser.profile.bundled_requests() else: request_count = bundles * 4 try: if request.POST: stripe_token = request.POST.get('stripe_token') stripe_email = request.POST.get('stripe_email') stripe_email = validate_stripe_email(stripe_email) if not stripe_token or not stripe_email: raise KeyError('Missing Stripe payment data.') # take from the purchaser stripe_retry_on_error( stripe.Charge.create, amount=request_price, currency='usd', source=stripe_token, metadata={ 'email': stripe_email, 'action': 'request-purchase', }, idempotency_key=True, ) # and give to the recipient recipient.profile.num_requests += request_count recipient.profile.save() # record the purchase request.session['ga'] = 'request_purchase' msg = 'Purchase successful. ' if recipient == purchaser: msg += '%d requests have been added to your account.' % request_count else: msg += '%d requests have been gifted to %s' % (request_count, recipient.first_name) gift_description = '%d requests' % request_count # notify the recipient with an email gift.delay(recipient, purchaser, gift_description) messages.success(request, msg) logger.info('%s purchased %d requests', purchaser.username, request_count) except KeyError as exception: msg = 'Payment error: %s' % exception messages.error(request, msg) logger.warn('Payment error: %s', exception, exc_info=sys.exc_info()) except stripe.CardError as exception: msg = 'Payment error: %s Your card has not been charged.' % exception messages.error(request, msg) logger.warn('Payment error: %s', exception, exc_info=sys.exc_info()) except (stripe.error.InvalidRequestError, stripe.error.APIError) as exc: msg = 'Payment error: Your card has not been charged.' messages.error(request, msg) logger.warn('Payment error: %s', exc, exc_info=sys.exc_info()) return redirect(url_redirect)
def make_payment(self, token, email, amount, show=False, user=None): """Creates a payment for the crowdfund""" # pylint: disable=too-many-arguments amount = Decimal(amount) if self.payment_capped and amount > self.amount_remaining(): amount = self.amount_remaining() # Try processing the payment using Stripe. # If the payment fails, do not catch the error. # Stripe represents currency as smallest-unit integers. stripe_amount = int(float(amount) * 100) charge = stripe_retry_on_error( stripe.Charge.create, amount=stripe_amount, source=token, currency='usd', metadata={ 'email': email, 'action': 'crowdfund-payment', 'crowdfund_id': self.id, 'crowdfund_name': self.name }, idempotency_key=True, ) payment = CrowdfundPayment.objects.create(amount=amount, crowdfund=self, user=user, show=show, charge_id=charge.id) cache.delete('cf:%s:crowdfund_widget_data' % self.pk) logging.info(payment) self.update_payment_received() return payment
def pay(self, token, amount, metadata, fee=PAYMENT_FEE): """ Creates a Stripe charge for the user. Should always expect a 1-cent based integer (e.g. $1.00 = 100) Should apply a baseline fee (5%) to all payments. """ # pylint: disable=no-self-use modified_amount = int(amount + (amount * fee)) if not metadata.get('email') or not metadata.get('action'): raise ValueError('The charge metadata is malformed.') stripe_retry_on_error( stripe.Charge.create, amount=modified_amount, currency='usd', source=token, metadata=metadata )
def customer(self): """Retrieve the customer from Stripe or create one if it doesn't exist. Then return it.""" # pylint: disable=redefined-variable-type try: if not self.customer_id: raise AttributeError('No Stripe ID') customer = stripe_retry_on_error( stripe.Customer.retrieve, self.customer_id, ) except (AttributeError, stripe.InvalidRequestError): customer = stripe_retry_on_error( stripe.Customer.create, description=self.user.username, email=self.user.email ) self.customer_id = customer.id self.save() return customer
def card(self): """Retrieve the default credit card from Stripe, if one exists.""" card = None customer = self.customer() if customer.default_source: card = stripe_retry_on_error( customer.sources.retrieve, customer.default_source, ) return card
def start_pro_subscription(self, token=None): """Subscribe this profile to a professional plan. Return the subscription.""" # create the stripe subscription customer = self.customer() if self.subscription_id: raise AttributeError('Only allowed one active subscription at a time.') if not token and not customer.default_source: raise AttributeError('No payment method provided for this subscription.') subscription = stripe_retry_on_error( customer.subscriptions.create, plan='pro', source=token, idempotency_key=True, ) stripe_retry_on_error(customer.save, idempotency_key=True) # modify the profile object (should this be part of a webhook callback?) self.subscription_id = subscription.id self.acct_type = 'pro' self.date_update = date.today() self.monthly_requests = settings.MONTHLY_REQUESTS.get('pro', 0) self.save() return subscription
def make_charge(self, token, amount, email): """Make a Stripe charge and catch any errors.""" charge = None error_msg = None try: charge = stripe_retry_on_error( stripe.Charge.create, amount=amount, currency='usd', source=token, description='Donation from %s' % email, metadata={ 'email': email, 'action': 'donation' }, idempotency_key=True, ) except stripe.error.CardError: # card declined logger.warn('Card was declined.') error_msg = 'Your card was declined' except ( stripe.error.InvalidRequestError, # Invalid parameters were supplied to Stripe's API stripe.error.AuthenticationError, # Authentication with Stripe's API failed stripe.error.APIConnectionError, # Network communication with Stripe failed stripe.error.StripeError, # Generic error ) as exception: logger.error(exception, exc_info=sys.exc_info()) error_msg = ('Oops, something went wrong on our end.' ' Sorry about that!') finally: if error_msg: messages.error(self.request, error_msg) else: self.request.session['donated'] = amount self.request.session['ga'] = 'donation' return charge
def failed_payment(invoice_id): """Notify a customer about a failed subscription invoice.""" invoice = stripe_retry_on_error( stripe.Invoice.retrieve, invoice_id, ) attempt = invoice.attempt_count subscription_type = get_subscription_type(invoice) profile = Profile.objects.get(customer_id=invoice.customer) user = profile.user # raise the failed payment flag on the profile profile.payment_failed = True profile.save() subject = u'Your payment has failed' org = None if subscription_type == 'org': org = Organization.objects.get(owner=user) if attempt == 4: # on last attempt, cancel the user's subscription and lower the failed payment flag if subscription_type == 'pro': profile.cancel_pro_subscription() elif subscription_type == 'org': org.cancel_subscription() profile.payment_failed = False profile.save() logger.info('%s subscription has been cancelled due to failed payment', user.username) subject = u'Your %s subscription has been cancelled' % subscription_type context = {'attempt': 'final', 'type': subscription_type, 'org': org} else: logger.info('Failed payment by %s, attempt %s', user.username, attempt) context = {'attempt': attempt, 'type': subscription_type, 'org': org} notification = TemplateEmail( user=user, extra_context=context, text_template='message/notification/failed_payment.txt', html_template='message/notification/failed_payment.html', subject=subject, ) notification.send(fail_silently=False)
def send_charge_receipt(charge_id): """Send out a receipt for a charge""" charge = stripe_retry_on_error( stripe.Charge.retrieve, charge_id, ) # if the charge was generated by an invoice, let the invoice handler send the receipt if charge.invoice: return # we should expect charges to have metadata attached when they are made try: user_email = charge.metadata['email'] user_action = charge.metadata['action'] except KeyError: logger.warning('Malformed charge metadata, no receipt sent: %s', charge) return # try getting the user based on the provided email # we know from Checkout purchases that logged in users have their email autofilled try: user = User.objects.get(email=user_email) except User.DoesNotExist: user = None try: receipt_functions = { 'request-purchase': receipts.request_purchase_receipt, 'request-fee': receipts.request_fee_receipt, 'crowdfund-payment': receipts.crowdfund_payment_receipt, 'donation': receipts.donation_receipt, } receipt_function = receipt_functions[user_action] except KeyError: logger.warning('Unrecognized charge: %s', user_action) receipt_function = receipts.generic_receipt receipt = receipt_function(user, charge) receipt.send(fail_silently=False)