585 lines
20 KiB
Python
585 lines
20 KiB
Python
from rowers.models import Rower, PaidPlan, CoachingGroup, iDokladToken
|
|
from rowers.utils import ProcessorCustomerError
|
|
from rowsandall_app.settings import (
|
|
BRAINTREE_MERCHANT_ID, BRAINTREE_PUBLIC_KEY, BRAINTREE_PRIVATE_KEY,
|
|
BRAINTREE_SANDBOX_MERCHANT_ID, BRAINTREE_SANDBOX_PUBLIC_KEY,
|
|
BRAINTREE_SANDBOX_PRIVATE_KEY, BRAINTREE_MERCHANT_ACCOUNT_ID,
|
|
IDOKLAD_CLIENT_ID, IDOKLAD_CLIENT_SECRET, IDOKLAD_REDIRECT_URI,
|
|
)
|
|
import pandas as pd
|
|
from rowers.utils import dologging
|
|
from rowers import credits
|
|
from rowers.tasks import (
|
|
handle_send_email_transaction,
|
|
handle_send_email_subscription_update,
|
|
handle_send_email_subscription_create,
|
|
handle_send_email_failed_cancel,
|
|
# handle_send_email_transaction_notification,
|
|
)
|
|
from rowers.utils import myqueue
|
|
import rowers.idoklad as idoklad
|
|
from braintree.exceptions.invalid_signature_error import InvalidSignatureError
|
|
from braintree.exceptions.not_found_error import NotFoundError
|
|
import time
|
|
import sys, traceback
|
|
|
|
import braintree
|
|
from django.utils import timezone
|
|
import datetime
|
|
from django.conf import settings
|
|
import sys
|
|
import django_rq
|
|
queue = django_rq.get_queue('default')
|
|
queuelow = django_rq.get_queue('low')
|
|
queuehigh = django_rq.get_queue('high')
|
|
|
|
|
|
if settings.DEBUG or 'dev' in settings.SITE_URL: # pragma: no cover
|
|
gateway = braintree.BraintreeGateway(
|
|
braintree.Configuration(
|
|
braintree.Environment.Sandbox,
|
|
merchant_id=BRAINTREE_SANDBOX_MERCHANT_ID,
|
|
public_key=BRAINTREE_SANDBOX_PUBLIC_KEY,
|
|
private_key=BRAINTREE_SANDBOX_PRIVATE_KEY,
|
|
)
|
|
)
|
|
else:
|
|
gateway = braintree.BraintreeGateway(
|
|
braintree.Configuration(
|
|
braintree.Environment.Production,
|
|
merchant_id=BRAINTREE_MERCHANT_ID,
|
|
public_key=BRAINTREE_PUBLIC_KEY,
|
|
private_key=BRAINTREE_PRIVATE_KEY,
|
|
)
|
|
)
|
|
|
|
|
|
|
|
def process_webhook(notification):
|
|
if not settings.TESTING: # pragma: no cover
|
|
dologging('braintreewebhooks.log','{kind}'.format(kind=notification.kind))
|
|
if notification.kind == 'subscription_charged_successfully':
|
|
subscription = notification.subscription
|
|
rs = Rower.objects.filter(subscription_id=subscription.id)
|
|
if rs.count() == 0: # pragma: no cover
|
|
dologging('braintreewebhooks.log', 'Could not find rowers with subscription ID {id}'.format(
|
|
id=subscription.id
|
|
))
|
|
else:
|
|
r = rs[0]
|
|
transactions = subscription.transactions
|
|
if transactions:
|
|
amount = int(transactions[0].amount)
|
|
_ = credits.upgrade(amount, r)
|
|
return send_invoice(notification.subscription)
|
|
if notification.kind == 'subscription_canceled':
|
|
subscription = notification.subscription
|
|
rs = Rower.objects.filter(subscription_id=subscription.id)
|
|
if rs.count() == 0: # pragma: no cover
|
|
dologging('braintreewebhooks.log','Could not find rowers with subscription ID ' +
|
|
subscription.id+'\n')
|
|
return 0
|
|
r = rs[0]
|
|
result, mesg, errormsg = cancel_subscription(r, subscription.id)
|
|
if result:
|
|
dologging('braintreewebhooks.log','Subscription canceled {id}'.format(id=subscription.id))
|
|
return subscription.id
|
|
dologging('braintreewebhooks.log','Could not cancel Subscription: ' +
|
|
str(subscription.id)+'\n')
|
|
return 0 # pragma: no cover
|
|
if notification.kind == 'subscription_charged_unsuccessfully':
|
|
dologging('braintreewebhooks.log','Subscription charged unsuccessfully')
|
|
subscription = notification.subscription
|
|
rs = Rower.objects.filter(subscription_id=subscription.id)
|
|
if rs.count() == 0: # pragma: no cover
|
|
dologging('braintreewebhooks.log', 'Could not find rowers with subscription ID {id}'.format(
|
|
id=subscription.id
|
|
))
|
|
else:
|
|
r = rs[0]
|
|
result, mesg, errormsg = cancel_subscription(r, subscription.id)
|
|
dologging('braintreewebhooks.log','Subscription canceled for rower {r} with subscription ID {id}'.format(
|
|
r = r.user.email,
|
|
id = subscription.id
|
|
))
|
|
return 0
|
|
|
|
|
|
def send_invoice(subscription):
|
|
dologging('braintreewebhooks.log', 'Subscription ID '+str(subscription.id))
|
|
subscription_id = subscription.id
|
|
rs = Rower.objects.filter(subscription_id=subscription_id)
|
|
if rs.count() == 0: # pragma: no cover
|
|
return 0
|
|
else:
|
|
r = rs[0]
|
|
dologging('braintreewebhooks.log','Rower '+str(r)+'\n')
|
|
idoklad_contact_id = idoklad.get_contacts(r)
|
|
dologging('braintreewebhooks.log','Idoklad Contact ID '+str(idoklad_contact_id)+'\n')
|
|
if not idoklad_contact_id: # pragma: no cover
|
|
idoklad_contact_id = idoklad.create_contact(r)
|
|
dologging('braintreewebhooks.log','Created Idoklad Contact ID ' +
|
|
str(idoklad_contact_id)+'\n')
|
|
transactions = subscription.transactions
|
|
if transactions:
|
|
amount = transactions[0].amount
|
|
dologging('braintreewebhooks.log','Transaction amount '+str(amount)+'\n')
|
|
id = idoklad.create_invoice(r, amount, subscription_id, dosend=True,
|
|
contact_id=idoklad_contact_id)
|
|
return id
|
|
|
|
return 0 # pragma: no cover
|
|
|
|
|
|
def webhook(request):
|
|
try:
|
|
webhook_notification = gateway.webhook_notification.parse(
|
|
str(request.POST['bt_signature']),
|
|
request.POST['bt_payload'])
|
|
except InvalidSignatureError: # pragma: no cover
|
|
return 4
|
|
|
|
result = process_webhook(webhook_notification)
|
|
|
|
return result
|
|
|
|
|
|
def create_customer(rower, force=False):
|
|
if not rower.customer_id or force:
|
|
result = gateway.customer.create(
|
|
{
|
|
'first_name': rower.user.first_name,
|
|
'last_name': rower.user.last_name,
|
|
'email': rower.user.email,
|
|
})
|
|
if not result.is_success: # pragma: no cover
|
|
raise ProcessorCustomerError
|
|
else:
|
|
rower.customer_id = result.customer.id
|
|
rower.paymentprocessor = 'braintree'
|
|
rower.save()
|
|
return rower.customer_id
|
|
else: # pragma: no cover
|
|
return rower.customer_id
|
|
|
|
|
|
def get_client_token(rower):
|
|
try:
|
|
client_token = gateway.client_token.generate({
|
|
"customer_id": rower.customer_id,
|
|
})
|
|
except ValueError: # pragma: no cover
|
|
customer_id = create_customer(rower, force=True)
|
|
|
|
client_token = gateway.client_token.generate({
|
|
"customer_id": customer_id,
|
|
})
|
|
|
|
return client_token
|
|
|
|
|
|
def make_payment(rower, data):
|
|
nonce_from_the_client = data['payment_method_nonce']
|
|
nonce = gateway.payment_method_nonce.find(nonce_from_the_client)
|
|
info = nonce.three_d_secure_info
|
|
if nonce.type.lower() == 'creditcard': # pragma: no cover
|
|
if info is None or not info.liability_shifted:
|
|
dologging('braintree.log','Nonce info {info}'.format(info=info))
|
|
return False, 0
|
|
|
|
amount = data['amount']
|
|
amount = '{amount}'.format(amount=amount)
|
|
|
|
if 'plan' in data:
|
|
theplan = data['plan']
|
|
additional_text = 'Rowsandall Purchase Plan nr {theplan}'.format(
|
|
theplan=theplan)
|
|
else: # pragma: no cover
|
|
additional_text = 'Rowsandall Purchase'
|
|
|
|
result = gateway.transaction.sale({
|
|
"amount": amount,
|
|
"payment_method_nonce": nonce_from_the_client,
|
|
"options": {
|
|
"submit_for_settlement": True
|
|
}
|
|
})
|
|
if result.is_success:
|
|
transaction = result.transaction
|
|
amount = transaction.amount
|
|
name = '{f} {l}'.format(
|
|
f=rower.user.first_name,
|
|
l=rower.user.last_name,
|
|
)
|
|
|
|
idoklad_contact_id = idoklad.get_contacts(rower)
|
|
if not idoklad_contact_id:
|
|
idoklad_contact_id = idoklad.create_contact(rower)
|
|
|
|
_ = idoklad.create_invoice(rower, amount, transaction.id, dosend=True,
|
|
contact_id=idoklad_contact_id,
|
|
name=additional_text)
|
|
|
|
_ = myqueue(queuehigh, handle_send_email_transaction,
|
|
name, rower.user.email, amount)
|
|
return amount, True
|
|
else: # pragma: no cover
|
|
dologging('braintree.log','Payment failed {result}'.format(result=result))
|
|
return 0, False
|
|
|
|
|
|
def update_subscription(rower, data, method='up'):
|
|
planid = data['plan']
|
|
plan = PaidPlan.objects.get(id=planid)
|
|
nonce_from_the_client = data['payment_method_nonce']
|
|
nonce = gateway.payment_method_nonce.find(nonce_from_the_client)
|
|
info = nonce.three_d_secure_info
|
|
if nonce.type.lower() == 'creditcard': # pragma: no cover
|
|
if info is None or not info.liability_shifted:
|
|
dologging('braintree.log','Nonce info {info}'.format(info=info))
|
|
return False, 0
|
|
amount = data['amount']
|
|
amount = '{amount:.2f}'.format(amount=amount)
|
|
|
|
gatewaydata = {
|
|
"price": amount,
|
|
"plan_id": plan.external_id,
|
|
"payment_method_nonce": nonce_from_the_client,
|
|
"options": {
|
|
"prorate_charges": True,
|
|
},
|
|
}
|
|
|
|
if plan.paymenttype == 'single':
|
|
gatewaydata['number_of_billing_cycles'] = 1
|
|
else: # pragma: no cover
|
|
gatewaydata['never_expires'] = True
|
|
|
|
try:
|
|
result = gateway.subscription.update(
|
|
rower.subscription_id,
|
|
gatewaydata
|
|
)
|
|
except NotFoundError:
|
|
rower.subscription_id = None
|
|
rower.save()
|
|
return create_subscription(rower, data)
|
|
except BaseException as e: # pragma: no cover
|
|
exc_type, exc_value, exc_traceback = sys.exc_info()
|
|
dologging('braintree.log','Payment failed with error')
|
|
dologging('braintree.log','{t}, {v}'.format(t=exc_type,v=exc_value))
|
|
trace_back = traceback.extract_tb(exc_traceback)
|
|
for trace in trace_back:
|
|
dologging('braintree.log','{t}'.format(t=trace))
|
|
|
|
return False, 0
|
|
|
|
if result.is_success:
|
|
yesterday = (timezone.now()-datetime.timedelta(days=1)).date()
|
|
rower.paidplan = plan
|
|
amount_int = int(float(amount))
|
|
_ = credits.upgrade(amount_int, rower)
|
|
rower.planexpires = result.subscription.billing_period_end_date
|
|
rower.teamplanexpires = result.subscription.billing_period_end_date
|
|
rower.clubsize = 10
|
|
rower.paymenttype = plan.paymenttype
|
|
rower.rowerplan = plan.shortname
|
|
rower.subscription_id = result.subscription.id
|
|
rower.protrialexpires = yesterday
|
|
rower.plantrialexpires = yesterday
|
|
rower.save()
|
|
name = '{f} {l}'.format(
|
|
f=rower.user.first_name,
|
|
l=rower.user.last_name,
|
|
)
|
|
|
|
try:
|
|
coachgroup = rower.mycoachgroup
|
|
except CoachingGroup.DoesNotExist: # pragma: no cover
|
|
coachgroup = CoachingGroup()
|
|
coachgroup.save()
|
|
rower.mycoachgroup = coachgroup
|
|
rower.save()
|
|
|
|
athletes = Rower.objects.filter(
|
|
coachinggroups__in=[rower.mycoachgroup]).distinct()
|
|
for athlete in athletes: # pragma: no cover
|
|
athlete.coachinggroups.remove(rower.mycoachgroup)
|
|
|
|
if method == 'up':
|
|
transactions = result.subscription.transactions
|
|
|
|
if transactions:
|
|
amount = transactions[0].amount
|
|
else: # pragma: no cover
|
|
amount = 0
|
|
else: # pragma: no cover
|
|
amount = 0
|
|
|
|
_ = myqueue(queuehigh,
|
|
handle_send_email_subscription_update,
|
|
name, rower.user.email,
|
|
plan.name,
|
|
plan.paymenttype,
|
|
plan.price,
|
|
amount,
|
|
result.subscription.billing_period_end_date.strftime(
|
|
'%Y-%m-%d'),
|
|
method)
|
|
|
|
_ = idoklad.create_invoice(rower, amount, result.subscription.id, dosend=True)
|
|
|
|
return True, amount
|
|
else: # pragma: no cover
|
|
errors = result.errors.for_object("subscription")
|
|
codes = [str(e.code) for e in errors]
|
|
create_new = False
|
|
proceed_codes = ['81901', '81910','91920']
|
|
for c in codes:
|
|
if c in proceed_codes:
|
|
create_new = True
|
|
|
|
if create_new:
|
|
return create_subscription(rower, data)
|
|
|
|
dologging('braintree.log','Unrecognized proceed code {c}'.format(c=codes))
|
|
|
|
return False, 0
|
|
|
|
return False, 0 # pragma: no cover
|
|
|
|
|
|
def create_subscription(rower, data):
|
|
nonce_from_the_client = data['payment_method_nonce']
|
|
nonce = gateway.payment_method_nonce.find(nonce_from_the_client)
|
|
info = nonce.three_d_secure_info
|
|
# paymenttype = nonce.type
|
|
|
|
if nonce.type != 'PayPalAccount': # pragma: no cover
|
|
if info is None or not info.liability_shifted:
|
|
dologging('braintree.log','Create Subscription. Nonce info {info}'.format(info=info))
|
|
return False, 0
|
|
# amount = data['amount']
|
|
|
|
planid = data['plan']
|
|
plan = PaidPlan.objects.get(id=planid)
|
|
|
|
# create or find payment method
|
|
result = gateway.payment_method.create({
|
|
"customer_id": rower.customer_id,
|
|
"payment_method_nonce": nonce_from_the_client
|
|
})
|
|
|
|
if result.is_success:
|
|
payment_method_token = result.payment_method.token
|
|
else: # pragma: no cover
|
|
dologging('braintree.log','Payment failed {result}'.format(result=result))
|
|
return False, 0
|
|
|
|
result = gateway.subscription.create({
|
|
"payment_method_token": payment_method_token,
|
|
"plan_id": plan.external_id,
|
|
# "merchant_account_id": BRAINTREE_MERCHANT_ACCOUNT_ID,
|
|
})
|
|
|
|
if result.is_success:
|
|
yesterday = (timezone.now()-datetime.timedelta(days=1)).date()
|
|
rower.paidplan = plan
|
|
rower.planexpires = result.subscription.billing_period_end_date
|
|
rower.teamplanexpires = result.subscription.billing_period_end_date
|
|
rower.clubsize = plan.clubsize
|
|
rower.paymenttype = plan.paymenttype
|
|
rower.rowerplan = plan.shortname
|
|
rower.subscription_id = result.subscription.id
|
|
rower.protrialexpires = yesterday
|
|
rower.plantrialexpires = yesterday
|
|
|
|
rower.save()
|
|
name = '{f} {l}'.format(
|
|
f=rower.user.first_name,
|
|
l=rower.user.last_name,
|
|
)
|
|
|
|
recurring = plan.paymenttype
|
|
|
|
_ = myqueue(
|
|
queuehigh,
|
|
handle_send_email_subscription_create,
|
|
name, rower.user.email,
|
|
plan.name,
|
|
recurring,
|
|
plan.price,
|
|
plan.price,
|
|
result.subscription.billing_period_end_date.strftime('%Y-%m-%d')
|
|
)
|
|
return True, plan.price
|
|
else: # pragma: no cover
|
|
return False, 0
|
|
|
|
return False, 0 # pragma: no cover
|
|
|
|
|
|
def cancel_subscription(rower, id):
|
|
themessages = []
|
|
errormessages = []
|
|
try:
|
|
_ = gateway.subscription.cancel(id)
|
|
themessages.append("Subscription canceled")
|
|
except: # pragma: no cover
|
|
errormessages.append(
|
|
"We could not find the subscription record in our customer database."
|
|
" We have notified the site owner, who will contact you.")
|
|
|
|
name = '{f} {l}'.format(f=rower.user.first_name,
|
|
l=rower.user.last_name)
|
|
|
|
_ = myqueue(queuehigh,
|
|
handle_send_email_failed_cancel,
|
|
name, rower.user.email, rower.user.username, id)
|
|
|
|
return False, themessages, errormessages
|
|
|
|
basicplans = PaidPlan.objects.filter(price=0, paymentprocessor='braintree')
|
|
rower.paidplan = basicplans[0]
|
|
rower.teamplanexpires = timezone.now()
|
|
rower.planexpires = timezone.now()
|
|
rower.clubsize = 0
|
|
rower.rowerplan = 'basic'
|
|
rower.subscription_id = None
|
|
rower.save()
|
|
themessages.append("Your plan was reset to basic")
|
|
|
|
return True, themessages, errormessages
|
|
|
|
|
|
def find_subscriptions(rower):
|
|
try:
|
|
result = gateway.customer.find(rower.customer_id)
|
|
except: # pragma: no cover
|
|
raise ProcessorCustomerError(
|
|
"We could not find the customer in the database")
|
|
|
|
active_subscriptions = []
|
|
|
|
cards = result.credit_cards
|
|
for card in cards: # pragma: no cover
|
|
for subscription in card.subscriptions:
|
|
if subscription.status == 'Active':
|
|
active_subscriptions.append(subscription)
|
|
|
|
try:
|
|
paypal_accounts = result.paypal_accounts
|
|
for account in paypal_accounts: # pragma: no cover
|
|
for subscription in account.subscriptions:
|
|
if subscription.status == 'Active':
|
|
active_subscriptions.append(subscription)
|
|
except AttributeError: # pragma: no cover
|
|
pass
|
|
|
|
result = []
|
|
|
|
for subscription in active_subscriptions: # pragma: no cover
|
|
|
|
plan = PaidPlan.objects.filter(paymentprocessor="braintree",
|
|
external_id=subscription.plan_id)[0]
|
|
|
|
thedict = {
|
|
'end_date': subscription.billing_period_end_date,
|
|
'plan_id': subscription.plan_id,
|
|
'price': subscription.price,
|
|
'id': subscription.id,
|
|
'plan': plan.name,
|
|
'never_expires': subscription.never_expires
|
|
}
|
|
|
|
result.append(thedict)
|
|
|
|
return result
|
|
|
|
|
|
def get_transactions(start_date, end_date): # pragma: no cover
|
|
results = gateway.transaction.search(
|
|
braintree.TransactionSearch.created_at.between(
|
|
start_date,
|
|
end_date,
|
|
)
|
|
)
|
|
|
|
amounts = []
|
|
countries = []
|
|
addresses = []
|
|
card_countries = []
|
|
names = []
|
|
emails = []
|
|
dates = []
|
|
currencies = []
|
|
statuses = []
|
|
# ids = []
|
|
usernames = []
|
|
customerids = []
|
|
transactionids = []
|
|
subscriptionids = []
|
|
ownids = []
|
|
|
|
for transaction in results:
|
|
r = None
|
|
rs = Rower.objects.filter(
|
|
customer_id=transaction.customer['id'],
|
|
paymentprocessor='braintree')
|
|
if rs:
|
|
r = rs[0]
|
|
countries.append(r.country)
|
|
addresses.append('{street}, {city}, {postal_code}'.format(
|
|
street=r.street_address,
|
|
city=r.city,
|
|
postal_code=r.postal_code))
|
|
ownids.append(r.id)
|
|
usernames.append(r.user.username)
|
|
|
|
else:
|
|
countries.append(
|
|
transaction.credit_card_details.country_of_issuance)
|
|
ownids.append('unknown')
|
|
usernames.append('unknown')
|
|
addresses.append('')
|
|
|
|
emails.append(transaction.customer_details.email)
|
|
names.append('{f} {l}'.format(
|
|
f=transaction.customer['first_name'],
|
|
l=transaction.customer['last_name']
|
|
)
|
|
)
|
|
customerids.append(transaction.customer['id'])
|
|
transactionids.append(transaction.id)
|
|
subscriptionids.append(transaction.subscription_id)
|
|
amounts.append(transaction.amount)
|
|
dates.append(transaction.created_at)
|
|
currencies.append(transaction.currency_iso_code)
|
|
card_countries.append(
|
|
transaction.credit_card_details.country_of_issuance)
|
|
statuses.append(transaction.status)
|
|
|
|
df = pd.DataFrame({
|
|
'name': names,
|
|
'email': emails,
|
|
'date': dates,
|
|
'amount': amounts,
|
|
'currency': currencies,
|
|
'country': countries,
|
|
'card_country': card_countries,
|
|
'status': statuses,
|
|
'username': usernames,
|
|
'user_id': ownids,
|
|
'customer_id': customerids,
|
|
'transaction_id': transactionids,
|
|
'subscription_id': subscriptionids,
|
|
'address': addresses
|
|
}
|
|
)
|
|
|
|
return df
|
|
|
|
|
|
def mocktest(rower): # pragma: no cover
|
|
return '5'
|