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?) with transaction.atomic(): profile = Profile.objects.select_for_update().get(pk=self.pk) profile.subscription_id = subscription.id profile.acct_type = 'pro' profile.date_update = date.today() profile.monthly_requests = settings.MONTHLY_REQUESTS.get('pro', 0) profile.save() return subscription
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 try: profile = Profile.objects.get(customer_id=invoice.customer) except Profile.DoesNotExist: user = None try: customer = stripe_retry_on_error( stripe.Customer.retrieve, invoice.customer, ) charge.metadata['email'] = customer.email except stripe.error.InvalidRequestError: logger.error('Could not retrieve customer') return else: user = profile.user plan = get_subscription_type(invoice) try: receipt_functions = { 'pro': receipts.pro_subscription_receipt, 'org': receipts.org_subscription_receipt, 'donate': receipts.donation_receipt, } receipt_function = receipt_functions[plan] except KeyError: if plan.startswith('crowdfund'): receipt_function = receipts.crowdfund_payment_receipt charge.metadata['crowdfund_id'] = plan.split('-')[1] recurring_payment = RecurringCrowdfundPayment.objects.filter( subscription_id=invoice.subscription, ).first() if recurring_payment: recurring_payment.log_payment(charge) else: logger.error( 'No recurring crowdfund payment for: %s', invoice.subscription, ) else: logger.warning('Invoice charged for unrecognized plan: %s', plan) receipt_function = receipts.generic_receipt receipt = receipt_function(user, charge) receipt.send(fail_silently=False)
def cancel(self): """Cancel the recurring donation""" self.active = False self.deactivated_datetime = timezone.now() self.save() subscription = stripe_retry_on_error(stripe.Subscription.retrieve, self.subscription_id) stripe_retry_on_error(subscription.delete)
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 try: customer = stripe_retry_on_error( stripe.Customer.retrieve, invoice.customer, ) charge.metadata['email'] = customer.email except stripe.error.InvalidRequestError: logger.error('Could not retrieve customer') return plan = get_subscription_type(invoice) if plan == 'donate': receipt_function = receipts.donation_receipt elif plan.startswith('crowdfund'): receipt_function = receipts.crowdfund_payment_receipt charge.metadata['crowdfund_id'] = plan.split('-')[1] recurring_payment = RecurringCrowdfundPayment.objects.filter( subscription_id=invoice.subscription, ).first() if recurring_payment: recurring_payment.log_payment(charge) else: logger.error( 'No recurring crowdfund payment for: %s', invoice.subscription, ) else: # other types are handled by squarelet return receipt = receipt_function(None, charge) receipt.send(fail_silently=False)
def make_recurring_payment(self, token, email, amount, show, user): """Make a recurring payment for the crowdfund""" # pylint: disable=too-many-arguments plan = self._get_stripe_plan() customer = stripe_get_customer( user, email, 'Crowdfund {} for {}'.format(self.pk, email), ) subscription = stripe_retry_on_error( customer.subscriptions.create, plan=plan, source=token, quantity=amount, idempotency_key=True, ) RecurringCrowdfundPayment.objects.create( user=user, crowdfund=self, email=email, amount=amount, show=show, customer_id=customer.id, subscription_id=subscription.id, ) return subscription
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, ) return self.log_payment( amount, user, show, charge, )
def buy_requests(self, recipient): """Buy the requests""" num_requests = self.cleaned_data['num_requests'] stripe_retry_on_error( stripe.Charge.create, amount=self.get_price(num_requests), currency='usd', source=self.cleaned_data['stripe_token'], metadata={ 'email': self.cleaned_data['stripe_email'], 'action': 'request-purchase', 'amount': num_requests, }, idempotency_key=True, ) recipient.profile.add_requests(num_requests)
def send_charge_receipt(charge_id): """Send out a receipt for a charge""" logger.info("Charge Receipt for %s", charge_id) 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: # squarelet charges will not have matching metadata 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 logger.info("Charge Receipt User: %s", user) try: receipt_functions = { "crowdfund-payment": receipts.crowdfund_payment_receipt, "donation": receipts.donation_receipt, } receipt_function = receipt_functions[user_action] except KeyError: # squarelet charges will be handled on squarelet logger.warning("Unrecognized charge: %s", user_action) receipt_function = receipts.generic_receipt receipt = receipt_function(user, charge) receipt.send(fail_silently=False)
def stripe_get_customer(email, description): """Get a customer for an authenticated or anonymous user""" return stripe_retry_on_error( stripe.Customer.create, description=description, email=email, idempotency_key=True, )
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. """ 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, idempotency_key=True, )
def _get_stripe_plan(self): """Ensure there is a stripe plan created for this crowdfund""" plan = "crowdfund-{}".format(self.pk) try: stripe_retry_on_error(stripe.Plan.retrieve, plan) except stripe.InvalidRequestError: # default to $1 (100 cents) and then use the quantity # on the subscription to set the amount stripe_retry_on_error( stripe.Plan.create, id=plan, amount=100, currency="usd", interval="month", name=self.name, statement_descriptor="MuckRock Crowdfund", ) return plan
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 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, idempotency_key=True, ) self.customer_id = customer.id self.save() return customer
def stripe_get_customer(user, email, description): """Get a customer for an authenticated or anonymous user""" if user and user.is_authenticated: return user.profile.customer() else: return stripe_retry_on_error( stripe.Customer.create, description=description, email=email, idempotency_key=True, )
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) with transaction.atomic(): profile = Profile.objects.select_for_update().get(pk=self.pk) profile.subscription_id = '' profile.acct_type = 'basic' profile.monthly_requests = settings.MONTHLY_REQUESTS.get('basic', 0) profile.payment_failed = False profile.save() self.refresh_from_db() 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' mixpanel_event( self.request, 'Donate', {'Amount': amount / 100}, charge=amount / 100, ) return charge
def make_subscription(self, token, amount, email): """Start a subscription for recurring donations""" subscription = None quantity = amount / 100 customer = stripe_get_customer( self.request.user, email, 'Donation for {}'.format(email), ) if self.request.user.is_authenticated: user = self.request.user else: user = None try: subscription = stripe_retry_on_error( customer.subscriptions.create, plan='donate', source=token, quantity=quantity, idempotency_key=True, ) except stripe.error.CardError: logger.warn('Card was declined.') messages.error(self.request, 'Your card was declined') except stripe.error.StripeError as exception: logger.error(exception, exc_info=sys.exc_info()) messages.error( self.request, 'Oops, something went wrong on our end. Sorry about that!', ) else: RecurringDonation.objects.create( user=user, email=email, amount=quantity, customer_id=customer.id, subscription_id=subscription.id, ) mixpanel_event( self.request, 'Recurring Donation', {'Amount': quantity}, ) return subscription
def activate_subscription(self, token, num_seats): """Subscribes the owner to the org plan, given a variable quantity""" if self.active: raise AttributeError('Cannot activate an active organization.') if num_seats < settings.ORG_MIN_SEATS: raise ValueError( 'Cannot have an organization with less than three member seats.' ) quantity = self.compute_monthly_cost(num_seats) / 100 customer = self.owner.profile.customer() subscription = stripe_retry_on_error( customer.subscriptions.create, plan='org', source=token, quantity=quantity, idempotency_key=True, ) with transaction.atomic(): org = Organization.objects.select_for_update().get(pk=self.pk) org.update_num_seats(num_seats) org.num_requests = org.monthly_requests org.stripe_id = subscription.id org.active = True org.save() # If the owner has a pro account, cancel it. # Assume the pro user has an active subscription. # On the off chance that they don't, just silence the error. if self.owner.profile.acct_type == 'pro': try: self.owner.profile.cancel_pro_subscription() except AttributeError: pass self.owner.profile.subscription_id = subscription.id self.owner.profile.save() return subscription
def failed_payment(invoice_id): """Notify a customer about a failed subscription invoice.""" # pylint: disable=too-many-branches # pylint: disable=too-many-statements invoice = stripe_retry_on_error(stripe.Invoice.retrieve, invoice_id) attempt = invoice.attempt_count subscription_type = get_subscription_type(invoice) recurring_donation = None crowdfund = None email_to = [] if subscription_type == "donate": recurring_donation = RecurringDonation.objects.filter( subscription_id=invoice.subscription).first() if recurring_donation: user = recurring_donation.user if user is None: email_to = [recurring_donation.email] recurring_donation.payment_failed = True recurring_donation.save() else: user = None logger.error("No recurring crowdfund found for %s", invoice.subscription) elif subscription_type.startswith("crowdfund"): recurring_payment = RecurringCrowdfundPayment.objects.filter( subscription_id=invoice.subscription).first() if recurring_payment: user = recurring_payment.user if user is None: email_to = [recurring_payment.email] crowdfund = recurring_payment.crowdfund recurring_payment.payment_failed = True recurring_payment.save() else: user = None logger.error("No recurring crowdfund found for %s", invoice.subscription) else: # squarelet handles other types return subject = "Your payment has failed" context = { "attempt": attempt, "type": subscription_type, "crowdfund": crowdfund } if subscription_type.startswith("crowdfund"): context["type"] = "crowdfund" if attempt == 4: # on last attempt, cancel the user's subscription and lower the failed payment flag if subscription_type == "donate" and recurring_donation: recurring_donation.cancel() elif subscription_type.startswith("crowdfund") and recurring_payment: recurring_payment.cancel() logger.info("%s subscription has been cancelled due to failed payment", user) subject = "Your %s subscription has been cancelled" % subscription_type context["attempt"] = "final" else: logger.info("Failed payment by %s, attempt %s", user, attempt) notification = TemplateEmail( user=user, to=email_to, 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 failed_payment(invoice_id): """Notify a customer about a failed subscription invoice.""" # pylint: disable=too-many-branches # pylint: disable=too-many-statements invoice = stripe_retry_on_error( stripe.Invoice.retrieve, invoice_id, ) attempt = invoice.attempt_count subscription_type = get_subscription_type(invoice) recurring_donation = None profile = None crowdfund = None if subscription_type == 'donate': recurring_donation = RecurringDonation.objects.filter( subscription_id=invoice.subscription, ).first() if recurring_donation: user = recurring_donation.user recurring_donation.payment_failed = True recurring_donation.save() else: user = None logger.error( 'No recurring crowdfund found for %s', invoice.subscription, ) elif subscription_type.startswith('crowdfund'): recurring_payment = RecurringCrowdfundPayment.objects.filter( subscription_id=invoice.subscription, ).first() if recurring_payment: user = recurring_payment.user crowdfund = recurring_payment.crowdfund recurring_payment.payment_failed = True recurring_payment.save() else: user = None logger.error( 'No recurring crowdfund found for %s', invoice.subscription, ) else: 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) context = { 'attempt': attempt, 'type': subscription_type, 'org': org, 'crowdfund': crowdfund, } if subscription_type.startswith('crowdfund'): context['type'] = 'crowdfund' 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() elif subscription_type == 'donate' and recurring_donation: recurring_donation.cancel() elif subscription_type.startswith('crowdfund') and recurring_payment: recurring_payment.cancel() if subscription_type in ('pro', 'org'): profile.payment_failed = False profile.save() logger.info('%s subscription has been cancelled due to failed payment', user) subject = u'Your %s subscription has been cancelled' % subscription_type context['attempt'] = 'final' else: logger.info('Failed payment by %s, attempt %s', user, attempt) 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)