Example #1
0
 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
Example #2
0
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)
Example #3
0
 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)
Example #4
0
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)
Example #5
0
 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
Example #6
0
 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,
     )
Example #7
0
 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)
Example #8
0
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)
Example #9
0
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,
    )
Example #10
0
 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,
     )
Example #11
0
 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
Example #12
0
 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
Example #13
0
 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
Example #14
0
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,
        )
Example #15
0
 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
Example #16
0
 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
Example #17
0
 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
Example #18
0
    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
Example #19
0
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)
Example #20
0
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)