Initial support for Stripe (And Improve Paypal)
authorClinton Ebadi <clinton@unknownlamer.org>
Sat, 22 Mar 2014 07:49:54 +0000 (03:49 -0400)
committerClinton Ebadi <clinton@unknownlamer.org>
Sat, 22 Mar 2014 07:49:54 +0000 (03:49 -0400)
* 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 [new file with mode: 0644]
money.mlt
money.sig
money.sml
payment.mlt
portal.mlt
stripe/stripe-payment.cgi [new file with mode: 0755]

diff --git a/create-stripe-tables.sql b/create-stripe-tables.sql
new file mode 100644 (file)
index 0000000..0adc68d
--- /dev/null
@@ -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;
index 085c3aa..e846c3f 100644 (file)
--- a/money.mlt
+++ b/money.mlt
@@ -285,6 +285,11 @@ end %></textarea></td> </tr>
 
        %><h3>Payment transaction added.</h3>
 
+<% elseif $"cmd" = "stripeApply" then
+        val stripePmt = Money.lookupStripePayment ($"stripeId");
+        val txid = Money.applyStripePayment stripePmt;
+%><h3>Stripe Payment Processed (Transaction <% txid %>)</h3>
+
 <% elseif $"modPay" <> "" then
        Group.requireGroupName "money";
        showNormal := false;
@@ -663,8 +668,34 @@ Co-op balance: $<% #amount (Balance.lookupBalance (valOf (Balance.balanceNameToI
 <input name="rname"> <input type="submit" value="Look up">
 </form>
 
+<h3>Apply Stripe Payments</h3>
+
+<table>
+<tr><td><strong>Date</strong></td><td><strong>Member</strong></td>
+    <td><strong>Name on Card</strong></td>
+    <td><strong>Amount</strong> (After Fees)</td><td><td></td>
+</tr>
+<% foreach stripePmt in Money.listAllPendingStripePayments () do %>
+
+<tr>
+  <td><% #name (Init.lookupUser (#webuser_id stripePmt)) %></td>
+  <td><% #paid_on stripePmt %></td>
+  <td><% #card_name stripePmt %>
+  <td>$<% #net stripePmt %></td>
+  <td><form method="post">
+        <input type="hidden" name="cmd" value="stripeApply" />
+        <input type="hidden" name="stripeId" value="<% #charge_id stripePmt %>" />
+        <input type="submit" value="Apply to Balance" /> <!-- also, refund? -->
+      </form>
+  </td>
+</tr>
+<% end %>
+</table>
+
 <h3>Most recent transactions</h3>
 
+
+
 <table>
 <tr> <td><b>Date</b></td> <td><b>Description</b></td> <td><b>Amount</b></td> <td><b>Participants</b></td> <td><b>Replace</b></td> <td><b>Delete</b></td> </tr>
 <% foreach trn in Money.listTransactionsLimit 20 do %>
index 4195a87..d3d591d 100644 (file)
--- 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}
index 8fa5ed7..5697f3b 100644 (file)
--- 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 <support");
+       Mail.mwrite (mail, emailSuffix);
+       Mail.mwrite (mail, ">\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
+
dissimilarity index 74%
index 2e22165..aac7c71 100644 (file)
@@ -1,9 +1,101 @@
-<% val you = Init.getUser () %>
-
-<h3><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&business=payment@hcoop.net&item_name=Member+payment+for+<% Init.getUserName () %>">Add to your balance with PayPal</a></h3>
-
-<% switch #paypal you of
-   NONE => %><p>You haven't set a PayPal e-mail address.  If you are going to send a payment by PayPal, please <a href="pref">set a PayPal e-mail address on the Preferences page</a> first to ensure that you are credited promptly and accurately.</p><%
-end %>
-
-<p>Remember that we credit member balances for PayPal payments <b>after subtracting PayPal's service fees</b>. This means that, to increase your balance by a particular amount, you must make a <b>larger</b> payment than just that amount. You should consult <a href="https://www.paypal.com/us/cgi-bin/webscr?cmd=_display-fees-outside">the PayPal fees page</a> 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 <i>x</i> to send from the amount <i>y</i> you want us to receive with this formula: <i>x</i> = (<i>y</i> + .3) / (1 - .029). The fees may be different for other countries.</p>
\ No newline at end of file
+<% val you = Init.getUser () %>
+
+<% if $"cmd" = "stripeSuccess" then %>
+<div class="notice">
+
+<h3>Stripe Payment Succeeded</h3>
+
+<p>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.</p>
+
+</div>
+<% end %>
+
+<h3>Add To Your Balance</h3>
+
+<script src="https://code.jquery.com/jquery-1.11.0.min.js"></script>
+<script src="https://checkout.stripe.com/checkout.js"></script>
+
+<p>Remember that we credit member balances for payments <b>after subtracting service fees</b>. This means that, to increase your balance by a particular amount, you must make a <b>larger</b> payment than just that amount.</p>
+
+<h4>Add to your balance with <a href="https://paypal.com">PayPal</a></h4>
+
+<p>You should consult <a href="https://www.paypal.com/us/cgi-bin/webscr?cmd=_display-fees-outside">the PayPal fees page</a> 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 <em>x</em> to send from the amount <em>y</em> you want us to receive with this formula: <em>x</em> = (<em>y</em> + .3) / (1 - .029). The fees may be different for other countries.</p>
+
+<form method="GET" action="https://www.paypal.com/cgi-bin/webscr">
+  <input type="hidden" name="cmd" value="_xclick" />
+  <input type="hidden" name="business" value="payment@hcoop.net" />
+  <input type="hidden" name="item_name" value="Member payment for <% Init.getUserName () %>" />
+
+  <select id="paypalDuesEasy">
+    <option value="" selected="selected">---</option>
+    <% foreach (amt) in [10, 15, 20, 25, 30, 35, 40, 45, 50, 60, 70, 80, 90, 100] do %>
+    <option value="<% amt %>.00">$<% amt %>.00</option>
+    <% end %>
+  </select>
+
+  <script>
+    $("#paypalDuesEasy").change (function () {
+     $("#paypalDues").val ($(this).val ());
+   });
+  </script>
+
+  <label>$<input id="paypalDues" type="text" name="amount" pattern="^\\d+\\.\\d\\d$" /></label>
+  <input type="submit" value="Add to Your Balance With Paypal" />
+</form>
+
+<% switch #paypal you of
+   NONE => %><p>You haven't set a PayPal e-mail address.  If you are going to send a payment by PayPal, please <a href="pref">set a PayPal e-mail address on the Preferences page</a> first to ensure that you are credited promptly and accurately.</p><%
+end %>
+
+<h4>Add to your balance with <a href="https://stripe.com/">Stripe</a></h4>
+
+<p>Stripe fees are $0.30 + 2.9% for each transaction regardless of country.</p>
+
+<form id="stripeForm" action="/stripe/stripe-payment.cgi" method="POST">
+  <select id="stripeDuesEasy">
+    <option value="" selected="selected">---</option>
+    <% foreach (amt) in [10, 15, 20, 25, 30, 35, 40, 45, 50, 60, 70, 80, 90, 100] do %>
+    <option value="<% amt %>.00">$<% amt %>.00</option>
+    <% end %>
+  </select>
+
+  <script>
+    $("#stripeDuesEasy").change (function () {
+     $("#stripeDues").val ($(this).val ());
+   });
+  </script>
+  <label>$<input type="text" name="stripeDues" id="stripeDues" pattern="^\\d+\\.\\d\\d$"/></label>
+  <input type="hidden" name="webuser_id" value="<% #id you %>" />
+  <input type="hidden" name="webuser_name" value="<% #name you %>" />
+  
+  <button id="stripePay">Add To Your Balance With Stripe</button>
+
+  <script>
+   $("#stripePay").click (function(e) {
+     if ($("#stripeDues")[0].validity.valid) { 
+       var token = function(res){
+        var $input = $('<input type=hidden name=stripeToken />').val(res.id);
+        $("#stripeDues").val($("#stripeDues").val() * 100);
+        $('#stripeForm').append($input).submit();
+       };
+       
+       // Open Checkout with further options
+       StripeCheckout.open({
+        key: 'pk_test_sJkMs1I4fVK4JQu9QkFDjOMs',
+        image: '/globe.gif',
+        name: 'Hcoop',
+        description: 'Dues',
+        amount:  ($("#stripeDues").val() * 100),
+        currency: 'usd',
+        address: true,
+        panelLabel: 'Pay {{amount}}',
+        token: token
+       });
+     }
+     return false;
+   });
+  </script>
+</form>
+
index 37f66a4..c0f634e 100644 (file)
@@ -34,6 +34,20 @@ end %>
 <b>Balance: $<% showBal %></b><br>
 <b>Deposit: $<% deposit %></b> (3 months of dues at the minimal <a href="pledge">pledge level</a>)
 
+<h3>Pending Stripe Payments</h3>
+
+<table>
+<tr><td><strong>Date</strong></td><td><strong>Net Amount</strong> (After Fees)</td></tr>
+<% foreach stripePmt in Money.listUserPendingStripePayments
+       (Init.getUserId () ) do %>
+<tr>
+  <td><% #paid_on stripePmt %></td>
+  <td>$<% #net stripePmt %></td>
+</tr>
+<% end %>
+</table>
+
+
 <!--b>Balance: $<% #amount bal %></b-->
 
 <% val polls = Poll.listCurrentPolls ();
diff --git a/stripe/stripe-payment.cgi b/stripe/stripe-payment.cgi
new file mode 100755 (executable)
index 0000000..130bac2
--- /dev/null
@@ -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 '<html>'
+    print '<head><title>Transaction Failed</title></head>'
+    print '<body>'
+    print '<h1>Failed</h1><p>Reason: '
+    print e.json_body['error']['message']
+    print '</p>'
+    print '</body>'
+    print '</html>'
+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 '<h1>Something went wrong after accepting payment!</h1>'
+        charge.refund ()
+        conn.rollback ()
+        print '<p>The charge should be refunded. Please contact payment@hcoop.net if it was not!</p>'
+        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 '<a href="/portal/portal?cmd=stripeSuccess">Go back to the portal</a>'