Skip to content

Commit

Permalink
Support billing/due_date on subscription and invoice + support adding…
Browse files Browse the repository at this point in the history
… invoice items
  • Loading branch information
Paul424 committed Oct 19, 2017
1 parent 6c721ea commit 95145cd
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 4 deletions.
75 changes: 75 additions & 0 deletions pinax/stripe/actions/invoices.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,66 @@ def pay(invoice, send_receipt=True):
return False


def paid(invoice):
"""
Sometimes customers may want to pay with payment methods outside of Stripe, such as check.
In these situations, Stripe still allows you to keep track of the payment status of your invoices.
Once you receive an invoice payment from a customer outside of Stripe, you can manually
mark their invoices as paid.
Args:
invoice: the invoice object to close
"""
if not invoice.paid:
stripe_invoice = invoice.stripe_invoice
stripe_invoice.paid = True
stripe_invoice_ = stripe_invoice.save()
sync_invoice_from_stripe_data(stripe_invoice_)


def forgive(invoice):
"""
Forgiving an invoice instructs us to update the subscription status as if the invoice were
successfully paid. Once an invoice has been forgiven, it cannot be unforgiven or reopened.
Args:
invoice: the invoice object to close
"""
if not invoice.paid:
stripe_invoice = invoice.stripe_invoice
stripe_invoice.forgiven = True
stripe_invoice_ = stripe_invoice.save()
sync_invoice_from_stripe_data(stripe_invoice_)


def close(invoice):
"""
Cause an invoice to be closed; This prevents Stripe from automatically charging your customer for the invoice amount.
Args:
invoice: the invoice object to close
"""
if not invoice.closed:
stripe_invoice = invoice.stripe_invoice
stripe_invoice.closed = True
stripe_invoice_ = stripe_invoice.save()
sync_invoice_from_stripe_data(stripe_invoice_)


def open(invoice):
"""
(re)-open a closed invoice (which is hold for review)
Args:
invoice: the invoice object to open
"""
if invoice.closed:
stripe_invoice = invoice.stripe_invoice
stripe_invoice.closed = False
stripe_invoice_ = stripe_invoice.save()
sync_invoice_from_stripe_data(stripe_invoice_)


def create_invoice_item(customer, invoice, subscription, amount, currency, description, metadata=None):
"""
:param customer: The pinax-stripe Customer
Expand All @@ -80,6 +140,7 @@ def create_invoice_item(customer, invoice, subscription, amount, currency, descr
currency=currency,
description=description,
invoice=invoice.stripe_id,
discountable=True,
metadata=metadata,
subscription=subscription.stripe_id,
)
Expand Down Expand Up @@ -142,6 +203,7 @@ def sync_invoice_from_stripe_data(stripe_invoice, send_receipt=settings.PINAX_ST
attempt_count=stripe_invoice["attempt_count"],
amount_due=utils.convert_amount_for_db(stripe_invoice["amount_due"], stripe_invoice["currency"]),
closed=stripe_invoice["closed"],
forgiven=stripe_invoice["forgiven"],
paid=stripe_invoice["paid"],
period_end=period_end,
period_start=period_start,
Expand All @@ -154,7 +216,13 @@ def sync_invoice_from_stripe_data(stripe_invoice, send_receipt=settings.PINAX_ST
charge=charge,
subscription=subscription,
receipt_number=stripe_invoice["receipt_number"] or "",
metadata=stripe_invoice["metadata"]
)
if "billing" in stripe_invoice:
defaults.update({
"billing": stripe_invoice["billing"],
"due_date": utils.convert_tstamp(stripe_invoice, "due_date") if stripe_invoice.get("due_date", None) is not None else None
})
invoice, created = models.Invoice.objects.get_or_create(
stripe_id=stripe_invoice["id"],
defaults=defaults
Expand All @@ -180,6 +248,13 @@ def sync_invoices_for_customer(customer):
sync_invoice_from_stripe_data(invoice, send_receipt=False)


def sync_invoice(invoice):
"""
Syncronizes a specific invoice
"""
sync_invoice_from_stripe_data(invoice.stripe_invoice, send_receipt=False)


def sync_invoice_items(invoice, items):
"""
Syncronizes all invoice line items for a particular invoice
Expand Down
18 changes: 15 additions & 3 deletions pinax/stripe/actions/subscriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def cancel(subscription, at_period_end=True):
sync_subscription_from_stripe_data(subscription.customer, sub)


def create(customer, plan, quantity=None, trial_days=None, token=None, coupon=None, tax_percent=None):
def create(customer, plan, quantity=None, trial_days=None, token=None, coupon=None, tax_percent=None, **kwargs):
"""
Creates a subscription for the given customer
Expand All @@ -35,14 +35,15 @@ def create(customer, plan, quantity=None, trial_days=None, token=None, coupon=No
will be used
coupon: if provided, a coupon to apply towards the subscription
tax_percent: if provided, add percentage as tax
kwargs: any additional arguments are passed, easy for new features
Returns:
the data representing the subscription object that was created
"""
quantity = hooks.hookset.adjust_subscription_quantity(customer=customer, plan=plan, quantity=quantity)
cu = customer.stripe_customer

subscription_params = {}
subscription_params = kwargs
if trial_days:
subscription_params["trial_end"] = datetime.datetime.utcnow() + datetime.timedelta(days=trial_days)
if token:
Expand Down Expand Up @@ -156,6 +157,11 @@ def sync_subscription_from_stripe_data(customer, subscription):
trial_start=utils.convert_tstamp(subscription["trial_start"]) if subscription["trial_start"] else None,
trial_end=utils.convert_tstamp(subscription["trial_end"]) if subscription["trial_end"] else None
)
if "billing" in subscription:
defaults.update({
"billing": subscription["billing"],
"days_until_due": subscription["days_until_due"] if "days_until_due" in subscription else None,
})
sub, created = models.Subscription.objects.get_or_create(
stripe_id=subscription["id"],
defaults=defaults
Expand All @@ -164,7 +170,7 @@ def sync_subscription_from_stripe_data(customer, subscription):
return sub


def update(subscription, plan=None, quantity=None, prorate=True, coupon=None, charge_immediately=False):
def update(subscription, plan=None, quantity=None, prorate=True, coupon=None, charge_immediately=False, billing=None, days_until_due=None):
"""
Updates a subscription
Expand All @@ -175,6 +181,8 @@ def update(subscription, plan=None, quantity=None, prorate=True, coupon=None, ch
prorate: optionally, if the subscription should be prorated or not
coupon: optionally, a coupon to apply to the subscription
charge_immediately: optionally, whether or not to charge immediately
billing: Either charge_automatically or send_invoice
days_until_due: Number of days a customer has to pay invoices generated by this subscription. Only valid for subscriptions where billing=send_invoice.
"""
stripe_subscription = subscription.stripe_subscription
if plan:
Expand All @@ -188,6 +196,10 @@ def update(subscription, plan=None, quantity=None, prorate=True, coupon=None, ch
if charge_immediately:
if stripe_subscription.trial_end is not None and utils.convert_tstamp(stripe_subscription.trial_end) > timezone.now():
stripe_subscription.trial_end = "now"
if billing is not None:
stripe_subscription.billing = billing
if days_until_due is not None:
stripe_subscription.days_until_due = days_until_due
sub = stripe_subscription.save()
customer = models.Customer.objects.get(pk=subscription.customer.pk)
sync_subscription_from_stripe_data(customer, sub)
51 changes: 51 additions & 0 deletions pinax/stripe/migrations/0011_auto_20171019_1321.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-10-19 13:21
from __future__ import unicode_literals

from django.db import migrations, models
import jsonfield.fields


class Migration(migrations.Migration):

dependencies = [
('pinax_stripe', '0010_connect'),
]

operations = [
migrations.AddField(
model_name='invoice',
name='billing',
field=models.CharField(default='charge_automatically', max_length=32),
),
migrations.AddField(
model_name='invoice',
name='due_date',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='invoice',
name='forgiven',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='invoice',
name='metadata',
field=jsonfield.fields.JSONField(null=True),
),
migrations.AddField(
model_name='subscription',
name='billing',
field=models.CharField(default='charge_automatically', max_length=32),
),
migrations.AddField(
model_name='subscription',
name='days_until_due',
field=models.IntegerField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='subscription',
name='application_fee_percent',
field=models.DecimalField(blank=True, decimal_places=2, default=None, max_digits=3, null=True),
),
]
8 changes: 7 additions & 1 deletion pinax/stripe/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ class Subscription(StripeObject):
STATUS_CURRENT = ["trialing", "active"]

customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
application_fee_percent = models.DecimalField(decimal_places=2, max_digits=3, default=None, null=True)
application_fee_percent = models.DecimalField(decimal_places=2, max_digits=3, default=None, blank=True, null=True)
cancel_at_period_end = models.BooleanField(default=False)
canceled_at = models.DateTimeField(blank=True, null=True)
current_period_end = models.DateTimeField(blank=True, null=True)
Expand All @@ -240,6 +240,8 @@ class Subscription(StripeObject):
status = models.CharField(max_length=25) # trialing, active, past_due, canceled, or unpaid
trial_end = models.DateTimeField(blank=True, null=True)
trial_start = models.DateTimeField(blank=True, null=True)
billing = models.CharField(max_length=32, default=u'charge_automatically') # charge_automatically or send_invoice
days_until_due = models.IntegerField(default=None, blank=True, null=True)

@property
def stripe_subscription(self):
Expand Down Expand Up @@ -278,6 +280,7 @@ class Invoice(StripeObject):
statement_descriptor = models.TextField(blank=True)
currency = models.CharField(max_length=10, default="usd")
closed = models.BooleanField(default=False)
forgiven = models.BooleanField(default=False)
description = models.TextField(blank=True)
paid = models.BooleanField(default=False)
receipt_number = models.TextField(blank=True)
Expand All @@ -289,6 +292,9 @@ class Invoice(StripeObject):
total = models.DecimalField(decimal_places=2, max_digits=9)
date = models.DateTimeField()
webhooks_delivered_at = models.DateTimeField(null=True)
billing = models.CharField(max_length=32, default=u'charge_automatically') # charge_automatically or send_invoice
due_date = models.DateTimeField(null=True, blank=True)
metadata = JSONField(null=True)

@property
def status(self):
Expand Down
5 changes: 5 additions & 0 deletions pinax/stripe/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,11 @@ def process_webhook(self):
)


class InvoiceUpcomingWebhook(InvoiceWebhook):
name = "invoice.upcoming"
description = "Occurs X number of days before a subscription is scheduled to create an invoice that is charged automatically, where X is determined by your subscriptions settings."


class InvoiceCreatedWebhook(InvoiceWebhook):
name = "invoice.created"
description = "Occurs whenever a new invoice is created. If you are using webhooks, Stripe will wait one hour after they have all succeeded to attempt to pay the invoice; the only exception here is on the first invoice, which gets created and paid immediately when you subscribe a customer to a plan. If your webhooks do not all respond successfully, Stripe will continue retrying the webhooks every hour and will not attempt to pay the invoice. After 3 days, Stripe will attempt to pay the invoice regardless of whether or not your webhooks have succeeded. See how to respond to a webhook."
Expand Down

0 comments on commit 95145cd

Please sign in to comment.