From f8b39e09ad3ca69efbb791352f72d8aa98c4cd34 Mon Sep 17 00:00:00 2001 From: Clinton Ebadi Date: Sat, 22 Mar 2014 03:49:54 -0400 Subject: [PATCH] Initial support for Stripe (And Improve Paypal) * payment.mlt embeds Stripe checkout widget * stripe-payment.cgi charges the transaction setup by the widget and notifies the treasurer * Charged but unapplied payments are stored in a transaction log * Payments are applied semi-automatically, with processed payments stored in another log * Paypal amounts can be entered on the payment page directly --- create-stripe-tables.sql | 31 +++++++++++ money.mlt | 31 +++++++++++ money.sig | 7 +++ money.sml | 70 ++++++++++++++++++++++++ payment.mlt | 110 ++++++++++++++++++++++++++++++++++---- portal.mlt | 14 +++++ stripe/stripe-payment.cgi | 93 ++++++++++++++++++++++++++++++++ 7 files changed, 347 insertions(+), 9 deletions(-) create mode 100644 create-stripe-tables.sql rewrite payment.mlt (74%) create mode 100755 stripe/stripe-payment.cgi diff --git a/create-stripe-tables.sql b/create-stripe-tables.sql new file mode 100644 index 0000000..0adc68d --- /dev/null +++ b/create-stripe-tables.sql @@ -0,0 +1,31 @@ +BEGIN; + +CREATE TABLE stripe_payment +( + charge_id text not null primary key, + webuser_id integer not null references WebUser (id), + card_name text not null, + paid_on date not null, + gross integer not null, + fee integer not null +); + +CREATE TABLE stripe_join_payment +( + charge_id text not null primary key, + app_id integer not null references MemberApp (id) unique, + card_name text not null, + authorized_on date not null, + gross integer not null +-- no fee data, because an uncaptured payment does not have have a balance_transaction +); + +CREATE TABLE stripe_processed +( + stripe_charge_id text not null primary key, + transaction_id integer not null references transaction (id), + + foreign key (stripe_charge_id) references stripe_payment (charge_id) +); + +COMMIT; diff --git a/money.mlt b/money.mlt index 085c3aa..e846c3f 100644 --- a/money.mlt +++ b/money.mlt @@ -285,6 +285,11 @@ end %> %>

Payment transaction added.

+<% elseif $"cmd" = "stripeApply" then + val stripePmt = Money.lookupStripePayment ($"stripeId"); + val txid = Money.applyStripePayment stripePmt; +%>

Stripe Payment Processed (Transaction <% txid %>)

+ <% elseif $"modPay" <> "" then Group.requireGroupName "money"; showNormal := false; @@ -663,8 +668,34 @@ Co-op balance: $<% #amount (Balance.lookupBalance (valOf (Balance.balanceNameToI +

Apply Stripe Payments

+ + + + + + +<% foreach stripePmt in Money.listAllPendingStripePayments () do %> + + + + + + + +<% end %> +
DateMemberName on CardAmount (After Fees)
<% #name (Init.lookupUser (#webuser_id stripePmt)) %><% #paid_on stripePmt %><% #card_name stripePmt %> + $<% #net stripePmt %>
+ + + +
+
+

Most recent transactions

+ + <% foreach trn in Money.listTransactionsLimit 20 do %> diff --git a/money.sig b/money.sig index 4195a87..d3d591d 100644 --- a/money.sig +++ b/money.sig @@ -14,6 +14,13 @@ sig val listUsers : int -> (bool * Init.user) list (* List users and indicate whether they participated in a transaction *) + type stripePayment = {charge_id : string, webuser_id : int, card_name : string, paid_on : string, gross_cents : int, fee_cents : int, net : real} + + val listUserPendingStripePayments : int -> stripePayment list + val listAllPendingStripePayments : unit -> stripePayment list + val lookupStripePayment : string -> stripePayment + val applyStripePayment : stripePayment -> int + val lookupHostingUsage : int -> string option type charge = {trn : int, usr : int, amount : real} diff --git a/money.sml b/money.sml index 8fa5ed7..5697f3b 100644 --- a/money.sml +++ b/money.sml @@ -358,4 +358,74 @@ fun billDues {descr, base, date} = applyCharges receive end +(* Stripe *) + +type stripePayment = {charge_id : string, webuser_id : int, card_name : string, paid_on : string, gross_cents : int, fee_cents : int, net : real} + +fun mkStripeRow [charge_id, webuser_id, name, paid_on, gross, fee] = + {charge_id = C.stringFromSql charge_id, webuser_id = C.intFromSql webuser_id, + card_name = C.stringFromSql name, paid_on = C.stringFromSql paid_on, + gross_cents = C.intFromSql gross, fee_cents = C.intFromSql fee, net = real (C.intFromSql gross - C.intFromSql fee) / 100.0 } + | mkStripeRow row = Init.rowError ("stripe_payment", row) + +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) + 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) + ORDER BY paid_on DESC`) + +fun lookupStripePayment id = + let + val c = getDb () + in + (case C.oneOrNoRows c ($`SELECT charge_id, webuser_id, card_name, paid_on, gross, fee FROM stripe_payment WHERE charge_id = ^(C.stringToSql id)`) of + NONE => raise Fail "Stripe Payment Not Found" + | SOME r => mkStripeRow r) + end + +(* Not Used *) +val stripeNotify : stripePayment -> bool = +fn pmt => + let + val user = Init.lookupUser (#webuser_id pmt) + val mail = Mail.mopen () + in + Mail.mwrite (mail, "From: Hcoop Support System \nTo: payment"); + Mail.mwrite (mail, emailSuffix); + Mail.mwrite (mail, "\n"); + Mail.mwrite (mail, "Subject: Stripe Payment Received"); + Mail.mwrite (mail, "\n\n"); + + Mail.mwrite (mail, "A member has paid us via Stripe. Visit the money page to process the payment."); + Mail.mwrite (mail, "Member: "); + Mail.mwrite (mail, #name user); + Mail.mwrite (mail, "\n"); + Mail.mwrite (mail, "Amount (after fees): "); + Mail.mwrite (mail, Real.toString (#net pmt)); + Mail.mwrite (mail, "\n\n"); + + OS.Process.isSuccess (Mail.mclose mail) + end + +val applyStripePayment : stripePayment -> int = + fn pmt => + let + val _ = Group.requireGroupName "money"; + val amount = #net pmt; + val txid = addTransaction ("Stripe", amount, #paid_on pmt) + in + addCharge {trn = txid, usr = #webuser_id pmt, amount = amount}; + applyCharges txid; + C.dml (getDb ()) ($`INSERT INTO stripe_processed (stripe_charge_id, transaction_id) + VALUES (^(C.stringToSql (#charge_id pmt)), ^(C.intToSql txid))`); + txid + end end + diff --git a/payment.mlt b/payment.mlt dissimilarity index 74% index 2e22165..aac7c71 100644 --- a/payment.mlt +++ b/payment.mlt @@ -1,9 +1,101 @@ -<% val you = Init.getUser () %> - -

Add to your balance with PayPal

- -<% switch #paypal you of - NONE => %>

You haven't set a PayPal e-mail address. If you are going to send a payment by PayPal, please set a PayPal e-mail address on the Preferences page first to ensure that you are credited promptly and accurately.

<% -end %> - -

Remember that we credit member balances for PayPal payments after subtracting PayPal's service fees. This means that, to increase your balance by a particular amount, you must make a larger payment than just that amount. You should consult the PayPal fees page to figure out how much extra to send. We have a business account, which means, for example, a 2.9% plus 30 cent fee for payments from the USA. This means that you can calculate the amount x to send from the amount y you want us to receive with this formula: x = (y + .3) / (1 - .029). The fees may be different for other countries.

\ No newline at end of file +<% val you = Init.getUser () %> + +<% if $"cmd" = "stripeSuccess" then %> +
+ +

Stripe Payment Succeeded

+ +

Your payment via Stripe must still be applied to your balance +manually at present, and will be applied by the treasurer within a few +days.

+ +
+<% end %> + +

Add To Your Balance

+ + + + +

Remember that we credit member balances for payments after subtracting service fees. This means that, to increase your balance by a particular amount, you must make a larger payment than just that amount.

+ +

Add to your balance with PayPal

+ +

You should consult the PayPal fees page to figure out how much extra to send. We have a business account, which means, for example, a 2.9% plus 30 cent fee for payments from the USA. This means that you can calculate the amount x to send from the amount y you want us to receive with this formula: x = (y + .3) / (1 - .029). The fees may be different for other countries.

+ +
+ + + + + + + + + + + + +<% switch #paypal you of + NONE => %>

You haven't set a PayPal e-mail address. If you are going to send a payment by PayPal, please set a PayPal e-mail address on the Preferences page first to ensure that you are credited promptly and accurately.

<% +end %> + +

Add to your balance with Stripe

+ +

Stripe fees are $0.30 + 2.9% for each transaction regardless of country.

+ +
+ + + + + + + + + + + + diff --git a/portal.mlt b/portal.mlt index 37f66a4..c0f634e 100644 --- a/portal.mlt +++ b/portal.mlt @@ -34,6 +34,20 @@ end %> Balance: $<% showBal %>
Deposit: $<% deposit %> (3 months of dues at the minimal pledge level) +

Pending Stripe Payments

+ +
Date Description Amount Participants Replace Delete
+ +<% foreach stripePmt in Money.listUserPendingStripePayments + (Init.getUserId () ) do %> + + + + +<% end %> +
DateNet Amount (After Fees)
<% #paid_on stripePmt %>$<% #net stripePmt %>
+ + <% val polls = Poll.listCurrentPolls (); diff --git a/stripe/stripe-payment.cgi b/stripe/stripe-payment.cgi new file mode 100755 index 0000000..130bac2 --- /dev/null +++ b/stripe/stripe-payment.cgi @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# -*- python -*- + +import sys +sys.path.insert(0, '/afs/hcoop.net/user/h/hc/hcoop/portal3/stripe/stripe-pkg/lib/python2.6/site-packages/') + +import stripe, cgi, psycopg2, cgitb, datetime, smtplib +from email.mime.text import MIMEText + +cgitb.enable() + +def notify_payment (charge, member): + msg_text = """ +A member has paid us via Stripe. Please visit the portal money +page at your earliest convenience to process the payment. + + Member : {0} + Amount (cents) : {1} + Stripe Charge ID: {2} +""".format (member, charge.amount, charge.id) + + msg = MIMEText(msg_text) + msg['Subject'] = 'Stripe payment received from {0}'.format(member) + 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 () + keyfile.close () + return keystring + +# 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 () + +# Get the credit card details submitted by the form + +request_params = cgi.FieldStorage() + +token = request_params.getvalue ('stripeToken') +webuser_id = request_params.getvalue('webuser_id') +member_name = request_params.getvalue('webuser_name') +amount = request_params.getvalue('stripeDues') + +# Create the charge on Stripe's servers - this will charge the user's card + +try: + charge = stripe.Charge.create( amount=amount, + currency="usd", + card=token, + description='Payment for member {0}'.format (member_name)) +except stripe.error.CardError, e: # The card has been declined + print 'Status: 200 OK' + print + print '' + print 'Transaction Failed' + print '' + print '

Failed

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

' + print '' + print '' +else: + try: + 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)) + except: + print 'Status: 200 OK' + print 'Content-Type: text/html' + print '' + print '

Something went wrong after accepting payment!

' + charge.refund () + conn.rollback () + print '

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

' + raise + else: + conn.commit () + cur.close () + conn.close () + notify_payment (charge, member_name) + print 'Status: 303 See Other' + print 'Location: /portal/portal?cmd=stripeSuccess' + print '' + print 'Go back to the portal' -- 2.20.1