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

Multi vendor payment #1

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
20 changes: 15 additions & 5 deletions oscar_stripe/facade.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@


class Facade(object):
def __init__(self):
stripe.api_key = settings.STRIPE_SECRET_KEY
def __init__(self, api_key):
stripe.api_key = api_key

@staticmethod
def get_friendly_decline_message(error):
Expand All @@ -24,15 +24,25 @@ def charge(self,
description=None,
metadata=None,
**kwargs):
self.total = total
try:
return stripe.Charge.create(
amount=(total.incl_tax * 100).to_integral_value(),
self.charge_object = stripe.Charge.create(
amount=(total * 100).to_integral_value(),
currency=currency,
card=card,
description=description,
capture=False,
metadata=(metadata or {'order_number': order_number}),
**kwargs).id
**kwargs)
return self.charge_object.id
except stripe.CardError, e:
raise UnableToTakePayment(self.get_friendly_decline_message(e))
except stripe.StripeError, e:
raise InvalidGatewayRequestError(self.get_friendly_error_message(e))

def capture(self):
try:
self.charge_object.capture()
except stripe.StripeError, e:
raise InvalidGatewayRequestError(self.get_friendly_error_message(e))

164 changes: 147 additions & 17 deletions oscar_stripe/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import logging
logger = logging.getLogger(__name__)
from decimal import ROUND_FLOOR, Decimal as D

from django.conf import settings
from django.db.models import get_model
from django.utils.decorators import method_decorator
Expand All @@ -10,6 +14,7 @@

import forms

Partner = get_model('partner', 'Partner')
SourceType = get_model('payment', 'SourceType')
Source = get_model('payment', 'Source')

Expand All @@ -32,23 +37,148 @@ def get_context_data(self, **kwargs):
return ctx

def handle_payment(self, order_number, total, **kwargs):
stripe_ref = Facade().charge(
order_number,
total,
card=self.request.POST[STRIPE_TOKEN],
description=self.payment_description(order_number, total, **kwargs),
metadata=self.payment_metadata(order_number, total, **kwargs))

source_type, __ = SourceType.objects.get_or_create(name=PAYMENT_METHOD_STRIPE)
source = Source(
source_type=source_type,
currency=settings.STRIPE_CURRENCY,
amount_allocated=total.incl_tax,
amount_debited=total.incl_tax,
reference=stripe_ref)
self.add_payment_source(source)

self.add_payment_event(PAYMENT_EVENT_PURCHASE, total.incl_tax)
"""
Use the basket object that is passed in through kwargs
to generate a mapping of line items to partners so we can
generate separate charges to different vendors.
"""
# set up the default partner
default_partner = self.get_default_partner(**kwargs)

partners = {
default_partner: {
'stripe': self.get_stripe_token(default_partner),
'charges': [total.tax],
}
}
# Get all items and partners within the basket
for line in self.request.basket.all_lines():
partner = line.stockrecord.partner

# handle the default_parnter separately
if partner not in partners and partner != default_partner:
stripe_token = self.get_stripe_token(partner)

partners[partner] = {
'stripe': stripe_token,
'charges': []
}

default_charge, partner_charge = self.split_charge(line)

# default partner automatically gets the default_charge
partners[default_partner]['charges'].append(default_charge)

# if the partner has a stripe token, send them the partner_charge
if partners[partner]['stripe']:
partners[partner]['charges'].append(partner_charge)
# if the partner doesn't have a stripe token, send them the partner_charge
else:
partners[default_partner]['charges'].append(partner_charge)

chargeable_partners = []
for partner, info in partners.items():
if len(info['charges']):
chargeable_partners.append({partner: info})

shipping_cost = kwargs['shipping_cost']
shipping_charges = self.split_shipping_charge(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be calculated before partners are weeded out and the default partner gets the charges for those that don't have stripe accounts. This way kind of screws over a partner with a stripe account that's on the same order as one that doesn't.

shipping_cost, len(chargeable_partners)
)

facades = []
# Attempt to pull the access token from the auth
# Stripe account, defaulting to the key that is found
# in the settings file.
for index, info in enumerate(chargeable_partners):
try:
# We want to generate the charges first without capturing
# them (actually charging them). This allows us to confirm
# that everything is setup fine on the Stripe side after
# moving through all of the line items in the basket. If
# we find a single charge misbehaving, we can handle it
# separately.
partner = info.keys()[0]
stripe_access_token = info[partner]['stripe']
charges = info[partner]['charges']

# this partner doesn't have any lines, probably because they don't
# have stripe information, so skip them.
if len(charges) == 0:
continue

total = sum(charges, shipping_charges[index])

facade = Facade(api_key=stripe_access_token)
stripe_ref = facade.charge(
order_number,
total,
card=self.request.POST[STRIPE_TOKEN],
description=self.payment_description(order_number, total, **kwargs),
metadata=self.payment_metadata(order_number, total, **kwargs))
facades.append(facade)
except Exception, e:
logger.error(e, exc_info=True, extra={
})
raise

# Once all the stripe charges have been created we can
# then go ahead and capture them (actually charge them).
for facade in facades:
try:
# Figure out what the application_fee might be here
facade.capture()
source_type, __ = SourceType.objects.get_or_create(name=PAYMENT_METHOD_STRIPE)
source = Source(
source_type=source_type,
currency=settings.STRIPE_CURRENCY,
amount_allocated=facade.total,
amount_debited=facade.total,
reference=facade.charge_object.id)

self.add_payment_source(source)
self.add_payment_event(PAYMENT_EVENT_PURCHASE, facade.total)
except Exception, e:
logger.error(e, exc_info=True, extra={
})
raise

def split_charge(self, line):
stockrecord = line.stockrecord
return (
(stockrecord.price_excl_tax - stockrecord.cost_price) * line.quantity,
stockrecord.cost_price * line.quantity
)

def split_shipping_charge(self, shipping_charge, divisor):
per_charge = (shipping_charge/divisor).quantize(D('0.01'), ROUND_FLOOR)
charges = [per_charge] * divisor
extras = int((shipping_charge - sum(charges))*100)
for x in range(0, extras):
charges[x] += D('0.01')
return charges

def get_stripe_token(self, partner):
stripe_owner = partner.users.filter(social_auth__provider='stripe').all()
if len(stripe_owner) == 1:
stripe_info = stripe_owner.get().social_auth.get(provider='stripe')

if 'access_token' in stripe_info.tokens:
return stripe_info.tokens['access_token']
return None

def get_default_partner(self, **kwargs):
partner_code = kwargs.get('default_partner_code', None)
if not partner_code:
return None

try:
return Partner.objects.get(code=partner_code)
except Partner.DoesNotExist, e:
logger.error(e, exc_info=True, extra={
'partner_code': partner_code,
})
raise

def payment_description(self, order_number, total, **kwargs):
return self.request.POST[STRIPE_EMAIL]
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from setuptools import setup, find_packages

setup(name='django-oscar-stripe',
version='0.1',
version='0.2',
url='https://github.com/tangentlabs/django-oscar-stripe',
author="David Winterbottom",
author_email="[email protected]",
Expand Down