Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create invoice item #436

Open
wants to merge 24 commits into
base: original
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a03b657
ideal support; sry no tests yet!
Paul424 Apr 19, 2017
bae60ab
Extend webhooks required for ideal (source status) and add relevant f…
Paul424 Apr 20, 2017
c8acb98
Allow for source in update subscription
Paul424 Apr 21, 2017
c0acca1
Merge branch 'master' of github.com:Paul424/pinax-stripe
Paul424 May 29, 2017
3226b70
Merge branch 'master' of github.com:pinax/pinax-stripe
Paul424 May 29, 2017
8011951
Revert "Allow for source in update subscription"
Paul424 May 29, 2017
b9e56ed
Revert "Extend webhooks required for ideal (source status) and add re…
Paul424 May 29, 2017
810a066
Revert "ideal support; sry no tests yet!"
Paul424 May 29, 2017
4501b0d
Add action to create invoice item
Paul424 May 30, 2017
54bf5c1
Merge branch 'master' of github.com:pinax/pinax-stripe
Paul424 Oct 15, 2017
6c721ea
Merge branch 'master' into create-invoice-item
Paul424 Oct 15, 2017
95145cd
Support billing/due_date on subscription and invoice + support adding…
Paul424 Oct 19, 2017
9e472e9
Update after review
Paul424 Oct 26, 2017
9c1660c
Merge branch 'master' into create-invoice-item
Paul424 Oct 30, 2017
04a6d1c
Merge migrations
Paul424 Oct 30, 2017
3211821
Add tests
Paul424 Oct 31, 2017
975bb9b
Fix issue that subscription is not sync'ed when change made from Stri…
Paul424 Nov 6, 2017
e111401
Merge branch 'master' of github.com:pinax/pinax-stripe
Paul424 Nov 8, 2017
b586d3b
Merge branch 'master' of github.com:pinax/pinax-stripe
Paul424 Dec 5, 2017
027d1e1
Merge branch 'master' into create-invoice-item + fix migrations (0010…
Paul424 Dec 6, 2017
27bd049
Fix default in migration
Paul424 Dec 6, 2017
01c88b6
Fix new tests
Paul424 Dec 6, 2017
0c2ced3
Fix tests
Paul424 Dec 6, 2017
2e9b3ef
Revert fix migrations to enable delivery to master
Paul424 Dec 7, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/reference/webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ details on how to wire those up.
* `recipient.created` - Occurs whenever a recipient is created.
* `recipient.deleted` - Occurs whenever a recipient is deleted.
* `recipient.updated` - Occurs whenever a recipient is updated.
* `source.chargeable` - A Source object becomes chargeable after a customer has authenticated and verified a payment
* `source.canceled` - A Source object expired and cannot be used to create a charge
* `source.consumed` - A Source object that was single-use has already been charged
* `source.failed` - A Source object failed to become chargeable as your customer declined to authenticate the payment
* `sku.created` - Occurs whenever a SKU is created.
* `sku.updated` - Occurs whenever a SKU is updated.
* `transfer.created` - Occurs whenever a new transfer is created.
Expand Down
3 changes: 1 addition & 2 deletions pinax/stripe/actions/customers.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ def create(user, card=None, plan=settings.PINAX_STRIPE_DEFAULT_PLAN, charge_imme
email=user.email,
source=card,
plan=plan,
quantity=quantity,
trial_end=trial_end
)
try:
Expand Down Expand Up @@ -145,7 +144,7 @@ def set_default_source(customer, source):

def sync_customer(customer, cu=None):
"""
Syncronizes a local Customer object with details from the Stripe API
Synchronizes a local Customer object with details from the Stripe API

Args:
customer: a Customer object
Expand Down
54 changes: 54 additions & 0 deletions pinax/stripe/actions/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,45 @@ def sync_bitcoin(customer, source):
return utils.update_with_defaults(receiver, defaults, created)


def sync_ideal(customer, source):
"""
Syncronizes the data for an ideal source locally for a given customer

This is required since payment through ideal involves additional steps to be taken by the customer (select bank, enter codes, confirm, ...), updates will be made
available through webhooks or the return url and we then need to relate the payment to an instance of a source locally in order to process it further.

Args:
customer: the customer to create or update the source for
source: data reprenting the source from the Stripe API
"""
defaults = dict(
customer=customer,
status = source["status"] or "",
type = source["type"] or "",
usage = source["usage"] or "",
amount = utils.convert_amount_for_db(source["amount"], source["currency"]), # currency is in but in fact it's always eur
flow = source["flow"] or "",
livemode = source["livemode"],
owner_address = source["owner"]["address"] or "",
owner_email = source["owner"]["email"] or "",
owner_name = source["owner"]["name"] or "",
owner_phone = source["owner"]["phone"] or "",
owner_verified_address = source["owner"]["verified_address"] or "",
owner_verified_email = source["owner"]["verified_email"] or "",
owner_verified_name = source["owner"]["verified_name"] or "",
owner_verified_phone = source["owner"]["verified_phone"] or "",
redirect_return_url = source["redirect"]["return_url"] or "",
redirect_status = source["redirect"]["status"] or "",
redirect_url = source["redirect"]["url"] or "",
ideal_bank = source["ideal"]["bank"] or "",
)
o, created = models.Ideal.objects.get_or_create(
stripe_id=source["id"],
defaults=defaults
)
return utils.update_with_defaults(o, defaults, created)


def sync_payment_source_from_stripe_data(customer, source):
"""
Syncronizes the data for a payment source locally for a given customer
Expand All @@ -116,6 +155,9 @@ def sync_payment_source_from_stripe_data(customer, source):
"""
if source["id"].startswith("card_"):
return sync_card(customer, source)
elif source.get("type", None) == "ideal":
# ideal is created using Sources, only then will it have a type attribute
return sync_ideal(customer, source)
else:
return sync_bitcoin(customer, source)

Expand All @@ -140,3 +182,15 @@ def update_card(customer, source, name=None, exp_month=None, exp_year=None):
stripe_source.exp_year = exp_year
s = stripe_source.save()
return sync_payment_source_from_stripe_data(customer, s)


def create_ideal(customer, token):
"""
Attaches an ideal source to a customer

Args:
customer: the customer to create the source for
token: the token created from Stripe.js
"""
source = customer.stripe_customer.sources.create(source=token)
return sync_payment_source_from_stripe_data(customer, source)
8 changes: 7 additions & 1 deletion pinax/stripe/actions/subscriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,14 +171,18 @@ 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, token=None, prorate=True, coupon=None, charge_immediately=False):
"""
Updates a subscription

Args:
subscription: the subscription to update
plan: optionally, the plan to change the subscription to
quantity: optionally, the quantiy of the subscription to change
token: if provided, a token from Stripe.js that will be used as the
payment source for the subscription and set as the default
source for the customer, otherwise the current default source
will be used
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
Expand All @@ -188,6 +192,8 @@ def update(subscription, plan=None, quantity=None, prorate=True, coupon=None, ch
stripe_subscription.plan = plan
if quantity:
stripe_subscription.quantity = quantity
if token:
stripe_subscription.token = token
if not prorate:
stripe_subscription.prorate = False
if coupon:
Expand Down
10 changes: 9 additions & 1 deletion pinax/stripe/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
Subscription,
Card,
BitcoinReceiver,
Ideal,
Customer,
Event,
EventProcessingException,
Expand Down Expand Up @@ -195,6 +196,12 @@ class BitcoinReceiverInline(admin.TabularInline):
max_num = 0


class IdealInline(admin.TabularInline):
model = Ideal
extra = 0
max_num = 0


def subscription_status(obj):
return ", ".join([subscription.status for subscription in obj.subscription_set.all()])
subscription_status.short_description = "Subscription Status"
Expand Down Expand Up @@ -224,7 +231,8 @@ def subscription_status(obj):
inlines=[
SubscriptionInline,
CardInline,
BitcoinReceiverInline
BitcoinReceiverInline,
IdealInline,
]
)

Expand Down
47 changes: 47 additions & 0 deletions pinax/stripe/migrations/0008_ideal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2017-04-20 13:25
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone


class Migration(migrations.Migration):

dependencies = [
('pinax_stripe', '0007_auto_20170108_1202'),
]

operations = [
migrations.CreateModel(
name='Ideal',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('stripe_id', models.CharField(max_length=255, unique=True)),
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
('status', models.CharField(blank=True, max_length=32)),
('type', models.CharField(blank=True, max_length=32)),
('usage', models.CharField(blank=True, max_length=32)),
('amount', models.DecimalField(decimal_places=2, max_digits=9, null=True)),
('flow', models.CharField(max_length=32)),
('livemode', models.BooleanField(default=False)),
('owner_address', models.TextField(blank=True)),
('owner_email', models.EmailField(blank=True, max_length=254)),
('owner_name', models.TextField(blank=True)),
('owner_phone', models.TextField(blank=True)),
('owner_verified_address', models.TextField(blank=True)),
('owner_verified_email', models.EmailField(blank=True, max_length=254)),
('owner_verified_name', models.TextField(blank=True)),
('owner_verified_phone', models.TextField(blank=True)),
('redirect_return_url', models.URLField(max_length=1024)),
('redirect_status', models.TextField(blank=True)),
('redirect_url', models.URLField(max_length=1024)),
('ideal_bank', models.TextField(blank=True)),
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Customer')),
],
options={
'abstract': False,
},
),
]
23 changes: 23 additions & 0 deletions pinax/stripe/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,29 @@ class BitcoinReceiver(StripeObject):
used_for_payment = models.BooleanField(default=False)


class Ideal(StripeObject):

customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
status = models.CharField(max_length=32, blank=True)
type = models.CharField(max_length=32, blank=True)
usage = models.CharField(max_length=32, blank=True)
amount = models.DecimalField(decimal_places=2, max_digits=9, null=True)
flow = models.CharField(max_length=32)
livemode = models.BooleanField(default=False)
owner_address = models.TextField(blank=True)
owner_email = models.EmailField(blank=True)
owner_name = models.TextField(blank=True)
owner_phone = models.TextField(blank=True)
owner_verified_address = models.TextField(blank=True)
owner_verified_email = models.EmailField(blank=True)
owner_verified_name = models.TextField(blank=True)
owner_verified_phone = models.TextField(blank=True)
redirect_return_url = models.URLField(max_length=1024)
redirect_status = models.TextField(blank=True)
redirect_url = models.URLField(max_length=1024)
ideal_bank = models.TextField(blank=True)


class Subscription(StripeObject):

customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
Expand Down
29 changes: 29 additions & 0 deletions pinax/stripe/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,35 @@ class SKUUpdatedWebhook(Webhook):
description = "Occurs whenever a SKU is updated."


class SourceWebhook(Webhook):

def process_webhook(self):
sources.sync_payment_source_from_stripe_data(
self.event.customer,
self.event.validated_message["data"]["object"]
)


class SourceChargeableWebhook(SourceWebhook):
name = "source.chargeable"
description = "A Source object becomes chargeable after a customer has authenticated and verified a payment."


class SourceCanceledWebhook(SourceWebhook):
name = "source.canceled"
description = "A Source object expired and cannot be used to create a charge."


class SourceConsumedWebhook(SourceWebhook):
name = "source.consumed"
description = "A Source object that was single-use has already been charged."


class SourceFailedWebhook(SourceWebhook):
name = "source.failed"
description = "A Source object failed to become chargeable as your customer declined to authenticate the payment."


class TransferWebhook(Webhook):

def process_webhook(self):
Expand Down