Initial Stripe payment rejection support * Store rejected transactions * Command...
[hcoop/portal.git] / stripe / stripe-payment.cgi
1 #!/usr/bin/env python
2 # -*- python -*-
3
4 import sys
5 sys.path.insert(0, '/afs/hcoop.net/user/h/hc/hcoop/portal3/stripe/stripe-pkg/lib/python2.6/site-packages/')
6
7 import stripe, cgi, psycopg2, cgitb, datetime, smtplib
8 from email.mime.text import MIMEText
9 from contextlib import contextmanager
10
11 cgitb.enable()
12
13 def notify_payment (charge, member):
14 msg_text = """
15 A member has paid us via Stripe. Please visit the portal money
16 page at your earliest convenience to process the payment.
17
18 Member : {0}
19 Amount (cents) : {1}
20 Stripe Charge ID: {2}
21 """.format (member, charge.amount, charge.id)
22
23 msg = MIMEText(msg_text)
24 msg['Subject'] = 'Stripe payment received from {0}'.format(member)
25 msg['From'] = 'payment@hcoop.net'
26 msg['To'] = 'payment@hcoop.net'
27
28 s = smtplib.SMTP ('mail.hcoop.net')
29 s.sendmail ('payment@hcoop.net', ['payment@hcoop.net'], msg.as_string ())
30 s.quit ()
31
32 def notify_payment_rejected (charge, reason):
33 # TODO: notify member...
34 msg_text = """We have rejected a payment from a member.
35 Amount: {0}
36 Stripe Charge ID: {1}
37 Reason: {2}
38 """.format (charge.amount, charge.id, reason)
39
40 msg = MIMEText(msg_text)
41 msg['Subject'] = 'Stripe payment rejected'
42 msg['From'] = 'payment@hcoop.net'
43 msg['To'] = 'payment@hcoop.net'
44
45 s = smtplib.SMTP ('mail.hcoop.net')
46 s.sendmail ('payment@hcoop.net', ['payment@hcoop.net'], msg.as_string ())
47 s.quit ()
48
49 def stripe_key ():
50 keyfile = open ("/afs/hcoop.net/user/h/hc/hcoop/.portal-private/stripe", "r")
51 keystring = keyfile.read ()
52 keyfile.close ()
53 return keystring
54
55
56 @contextmanager
57 def stripe_error_handling ():
58 try:
59 yield
60 except stripe.error.CardError, e: # The card has been declined
61 print 'Status: 200 OK'
62 print
63 print '<html>'
64 print '<head><title>Transaction Failed</title></head>'
65 print '<body>'
66 print '<h1>Failed</h1><p>Reason: '
67 print e.json_body['error']['message']
68 print '</p>'
69 print '</body>'
70 print '</html>'
71 raise
72 except stripe.error.StripeError, e: # General stripe failure
73 print 'Status: 200 OK'
74 print
75 print '<html>'
76 print '<head><title>Stripe Error</title></head>'
77 print '<body>'
78 print '<h1>Failed</h1><p>Reason: '
79 print e.json_body['error']['message']
80 print '</p>'
81 print '</body>'
82 print '</html>'
83 raise
84
85 @contextmanager
86 def stripe_refund_on_error (charge):
87 try:
88 yield
89 except:
90 print 'Status: 200 OK'
91 print 'Content-Type: text/html'
92 print ''
93 print '<h1>Something went wrong after accepting payment!</h1>'
94 charge.refund ()
95 print '<p>The charge should be refunded. Please contact payment@hcoop.net if it was not!</p>'
96 raise
97
98 def stripe_success (redirect_to):
99 print 'Status: 303 See Other'
100 print 'Location: {0}'.format(redirect_to);
101 print ''
102 print '<a href="{0}">Go back to the portal</a>'.format(redirect_to)
103
104 # Set your secret key: remember to change this to your live secret key in production
105 # See your keys here https://manage.stripe.com/account
106
107 stripe.api_key = stripe_key ()
108 connstring = 'dbname=hcoop_portal3test user=hcoop host=postgres port=5433'
109
110 # Get the credit card details submitted by the form
111
112 request_params = cgi.FieldStorage()
113 request_command = request_params.getvalue ('cmd', 'none');
114
115 assert request_command != 'none', 'No command given.'
116
117 # Create the charge on Stripe's servers - this will charge the user's card
118
119 if request_command == 'member_payment':
120 token = request_params.getvalue ('stripeToken')
121 webuser_id = request_params.getvalue('webuser_id')
122 member_name = request_params.getvalue('webuser_name')
123 amount = request_params.getvalue('stripeDues')
124
125 with stripe_error_handling ():
126 charge = stripe.Charge.create( amount=amount,
127 currency="usd",
128 card=token,
129 description='Payment for member {0}'.format (member_name))
130
131 with stripe_refund_on_error (charge):
132 # assert charge.card.address_line1_check == 'pass', 'Address verification failed or unknown.'
133 assert charge.card.cvc_check == 'pass', 'CVC verification failed or unknown.'
134 # assert charge.card.address_zip_check == 'pass', 'Zipcode verification failed or unknown.'
135
136 balance = stripe.BalanceTransaction.retrieve (charge.balance_transaction)
137 conn = psycopg2.connect ('dbname=hcoop_portal3test user=hcoop host=postgres port=5433')
138 cur = conn.cursor ()
139 cur.execute ('insert into stripe_payment (charge_id, card_name, webuser_id, paid_on, gross, fee) values (%s, %s, %s, %s, %s, %s)',
140 (charge.id, charge.card.name, webuser_id, datetime.date.today (), charge.amount, balance.fee))
141 conn.commit ()
142
143 notify_payment (charge, member_name)
144 stripe_success ('/portal/portal?cmd=stripeSuccess')
145 elif request_command == 'reject_member_payment':
146 # todo: protect command using group file and separate cgi
147 charge_id = request_params.getvalue ('stripeChargeId');
148 reason = request_params.getvalue ('reason', 'none given');
149 with stripe_error_handling ():
150 conn = psycopg2.connect (connstring)
151 cur = conn.cursor ()
152
153 cur.execute ('SELECT charge_id FROM stripe_payment WHERE charge_id = %s', (charge_id,))
154 assert cur.fetchone() != None, 'Bad charge id'
155 cur.execute ('SELECT stripe_charge_id FROM stripe_handled WHERE stripe_charge_id = %s', (charge_id,))
156 assert cur.fetchone() == None, 'Cannot rejected a previously handled payment'
157
158 charge = stripe.Charge.retrieve (charge_id);
159 charge.refund ()
160 cur.execute ('insert into stripe_rejected (stripe_charge_id, refunded_on, reason) values (%s, %s, %s)',
161 (charge.id, datetime.date.today (), reason))
162 conn.commit ()
163
164 notify_payment_rejected (charge, reason)
165 stripe_success ('/portal/money?cmd=stripeRejected')
166 else:
167 assert False, 'Invalid command.'
168
169 # Use mod_authz_groupfile to store money/root
170 # (All hcoop members should be able to use this!)
171 # [support Satisfy? Satisfy: all is OK for now...]
172 # Whenever groups are updated in the portal, write the file
173 # make sure to store the file outside of the web root (duh)
174 # only users in money/root can do reject/adduser
175 # common code should go into a module (feh!)
176 # application_payment in one cgi (anyone)
177 # member_payment in another (only kerberos users)
178 # reject_payment / capture_application_payment (kerberos + inGroup {money, root})
179
180 # If there is a way to allow all and check the group info
181 # here... maybe investigate, but beware security holes
182 # alt: libapache2-mod-authnz-external + db helper script
183 # can use ExternalGroup, check kerberos user is in group specified in
184 # another env var
185