From bd5d2441217811dbf5c1f8be9faad1be3a64ac49 Mon Sep 17 00:00:00 2001 From: Clinton Ebadi Date: Sat, 29 Mar 2014 04:32:41 -0400 Subject: [PATCH] Initial Stripe payment rejection support * Store rejected transactions * Command needs to be in a separate cgi only accessible by members of the money group --- create-stripe-tables.sql | 12 ++++ money.mlt | 21 +++++++ money.sml | 4 +- stripe/stripe-payment.cgi | 126 +++++++++++++++++++++++++++++++------- 4 files changed, 139 insertions(+), 24 deletions(-) diff --git a/create-stripe-tables.sql b/create-stripe-tables.sql index 0adc68d..be8d414 100644 --- a/create-stripe-tables.sql +++ b/create-stripe-tables.sql @@ -28,4 +28,16 @@ CREATE TABLE stripe_processed foreign key (stripe_charge_id) references stripe_payment (charge_id) ); +CREATE TABLE stripe_rejected +( + stripe_charge_id text not null primary key, + refunded_on date not null, + reason text not null, + + foreign key (stripe_charge_id) references stripe_payment (charge_id) +); + +CREATE VIEW stripe_handled AS + (select stripe_charge_id from stripe_processed) union (select stripe_charge_id from stripe_rejected); + COMMIT; diff --git a/money.mlt b/money.mlt index c6f92e3..63bfe0d 100644 --- a/money.mlt +++ b/money.mlt @@ -290,6 +290,21 @@ end %> val txid = Money.applyStripePayment stripePmt; %>

Stripe Payment Processed (Transaction <% txid %>)

+<% elseif $"cmd" = "stripeRejected" then +%>

Stripe Payment Rejected!

+ +<% elseif $"cmd" = "stripeReject" then + Group.requireGroupName "money"; + showNormal := false; +%> +

Are you sure you want to reject Stripe Payment <% $"stripeChargeId" %>?

+
+ + " /> + + +
+ <% elseif $"modPay" <> "" then Group.requireGroupName "money"; showNormal := false; @@ -698,6 +713,12 @@ Co-op balance: $<% #amount (Balance.lookupBalance (valOf (Balance.balanceNameToI +
+ + + +
+ <% end %> diff --git a/money.sml b/money.sml index 5697f3b..55330c2 100644 --- a/money.sml +++ b/money.sml @@ -371,12 +371,12 @@ fun mkStripeRow [charge_id, webuser_id, name, paid_on, gross, fee] = fun listUserPendingStripePayments uid = C.map (getDb ()) mkStripeRow ($`SELECT charge_id, webuser_id, card_name, paid_on, gross, fee FROM stripe_payment WHERE webuser_id = ^(C.intToSql uid) - AND charge_id NOT IN (SELECT stripe_charge_id FROM stripe_processed) + AND charge_id NOT IN (SELECT stripe_charge_id FROM stripe_handled) ORDER BY paid_on DESC`) fun listAllPendingStripePayments _ = C.map (getDb ()) mkStripeRow ($`SELECT charge_id, webuser_id, card_name, paid_on, gross, fee FROM stripe_payment - WHERE charge_id NOT IN (SELECT stripe_charge_id FROM stripe_processed) + WHERE charge_id NOT IN (SELECT stripe_charge_id FROM stripe_handled) ORDER BY paid_on DESC`) fun lookupStripePayment id = diff --git a/stripe/stripe-payment.cgi b/stripe/stripe-payment.cgi index ebb39c9..a6cd3e4 100755 --- a/stripe/stripe-payment.cgi +++ b/stripe/stripe-payment.cgi @@ -29,6 +29,23 @@ page at your earliest convenience to process the payment. s.sendmail ('payment@hcoop.net', ['payment@hcoop.net'], msg.as_string ()) s.quit () +def notify_payment_rejected (charge, reason): + # TODO: notify member... + msg_text = """We have rejected a payment from a member. + Amount: {0} + Stripe Charge ID: {1} + Reason: {2} +""".format (charge.amount, charge.id, reason) + + msg = MIMEText(msg_text) + msg['Subject'] = 'Stripe payment rejected' + msg['From'] = 'payment@hcoop.net' + msg['To'] = 'payment@hcoop.net' + + s = smtplib.SMTP ('mail.hcoop.net') + s.sendmail ('payment@hcoop.net', ['payment@hcoop.net'], msg.as_string ()) + s.quit () + def stripe_key (): keyfile = open ("/afs/hcoop.net/user/h/hc/hcoop/.portal-private/stripe", "r") keystring = keyfile.read () @@ -52,6 +69,18 @@ def stripe_error_handling (): print '' print '' raise + except stripe.error.StripeError, e: # General stripe failure + print 'Status: 200 OK' + print + print '' + print 'Stripe Error' + print '' + print '

Failed

Reason: ' + print e.json_body['error']['message'] + print '

' + print '' + print '' + raise @contextmanager def stripe_refund_on_error (charge): @@ -66,38 +95,91 @@ def stripe_refund_on_error (charge): print '

The charge should be refunded. Please contact payment@hcoop.net if it was not!

' raise +def stripe_success (redirect_to): + print 'Status: 303 See Other' + print 'Location: {0}'.format(redirect_to); + print '' + print 'Go back to the portal'.format(redirect_to) + # Set your secret key: remember to change this to your live secret key in production # See your keys here https://manage.stripe.com/account stripe.api_key = stripe_key () +connstring = 'dbname=hcoop_portal3test user=hcoop host=postgres port=5433' # Get the credit card details submitted by the form request_params = cgi.FieldStorage() +request_command = request_params.getvalue ('cmd', 'none'); -token = request_params.getvalue ('stripeToken') -webuser_id = request_params.getvalue('webuser_id') -member_name = request_params.getvalue('webuser_name') -amount = request_params.getvalue('stripeDues') +assert request_command != 'none', 'No command given.' # Create the charge on Stripe's servers - this will charge the user's card -with stripe_error_handling (): - charge = stripe.Charge.create( amount=amount, - currency="usd", - card=token, - description='Payment for member {0}'.format (member_name)) - -with stripe_refund_on_error (charge): - with psycopg2.connect ('dbname=hcoop_portal3test user=hcoop host=postgres port=5433') as conn: - with conn.cursor () as cur: - balance = stripe.BalanceTransaction.retrieve (charge.balance_transaction); - cur.execute ('insert into stripe_payment (charge_id, card_name, webuser_id, paid_on, gross, fee) values (%s, %s, %s, %s, %s, %s)', - (charge.id, charge.card.name, webuser_id, datetime.date.today (), charge.amount, balance.fee)) - -notify_payment (charge, member_name) -print 'Status: 303 See Other' -print 'Location: /portal/portal?cmd=stripeSuccess' -print '' -print 'Go back to the portal' +if request_command == 'member_payment': + token = request_params.getvalue ('stripeToken') + webuser_id = request_params.getvalue('webuser_id') + member_name = request_params.getvalue('webuser_name') + amount = request_params.getvalue('stripeDues') + + with stripe_error_handling (): + charge = stripe.Charge.create( amount=amount, + currency="usd", + card=token, + description='Payment for member {0}'.format (member_name)) + + with stripe_refund_on_error (charge): +# assert charge.card.address_line1_check == 'pass', 'Address verification failed or unknown.' + assert charge.card.cvc_check == 'pass', 'CVC verification failed or unknown.' +# assert charge.card.address_zip_check == 'pass', 'Zipcode verification failed or unknown.' + + balance = stripe.BalanceTransaction.retrieve (charge.balance_transaction) + conn = psycopg2.connect ('dbname=hcoop_portal3test user=hcoop host=postgres port=5433') + cur = conn.cursor () + cur.execute ('insert into stripe_payment (charge_id, card_name, webuser_id, paid_on, gross, fee) values (%s, %s, %s, %s, %s, %s)', + (charge.id, charge.card.name, webuser_id, datetime.date.today (), charge.amount, balance.fee)) + conn.commit () + + notify_payment (charge, member_name) + stripe_success ('/portal/portal?cmd=stripeSuccess') +elif request_command == 'reject_member_payment': + # todo: protect command using group file and separate cgi + charge_id = request_params.getvalue ('stripeChargeId'); + reason = request_params.getvalue ('reason', 'none given'); + with stripe_error_handling (): + conn = psycopg2.connect (connstring) + cur = conn.cursor () + + cur.execute ('SELECT charge_id FROM stripe_payment WHERE charge_id = %s', (charge_id,)) + assert cur.fetchone() != None, 'Bad charge id' + cur.execute ('SELECT stripe_charge_id FROM stripe_handled WHERE stripe_charge_id = %s', (charge_id,)) + assert cur.fetchone() == None, 'Cannot rejected a previously handled payment' + + charge = stripe.Charge.retrieve (charge_id); + charge.refund () + cur.execute ('insert into stripe_rejected (stripe_charge_id, refunded_on, reason) values (%s, %s, %s)', + (charge.id, datetime.date.today (), reason)) + conn.commit () + + notify_payment_rejected (charge, reason) + stripe_success ('/portal/money?cmd=stripeRejected') +else: + assert False, 'Invalid command.' + +# Use mod_authz_groupfile to store money/root +# (All hcoop members should be able to use this!) +# [support Satisfy? Satisfy: all is OK for now...] +# Whenever groups are updated in the portal, write the file +# make sure to store the file outside of the web root (duh) +# only users in money/root can do reject/adduser +# common code should go into a module (feh!) +# application_payment in one cgi (anyone) +# member_payment in another (only kerberos users) +# reject_payment / capture_application_payment (kerberos + inGroup {money, root}) + +# If there is a way to allow all and check the group info +# here... maybe investigate, but beware security holes +# alt: libapache2-mod-authnz-external + db helper script +# can use ExternalGroup, check kerberos user is in group specified in +# another env var -- 2.20.1