| 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 | |