Skip to content
This repository has been archived by the owner on Feb 8, 2018. It is now read-only.

Commit

Permalink
Implement _charge
Browse files Browse the repository at this point in the history
  • Loading branch information
chadwhitacre committed Sep 10, 2017
1 parent b22c6f3 commit 403e7cb
Show file tree
Hide file tree
Showing 9 changed files with 379 additions and 53 deletions.
11 changes: 6 additions & 5 deletions deploy/before.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ BEGIN;
, ctime timestamptz NOT NULL DEFAULT now()

-- card charge
, amount bigint NOT NULL
, transaction_id text UNIQUE NOT NULL
, amount bigint NOT NULL
, transaction_id text UNIQUE DEFAULT NULL
, succeeded bool NOT NULL DEFAULT FALSE

-- contact info
, name text NOT NULL
, follow_up follow_up NOT NULL
, email_address text NOT NULL
, name text NOT NULL
, follow_up follow_up NOT NULL
, email_address text NOT NULL

-- promotion details
, promotion_name text NOT NULL DEFAULT ''
Expand Down
44 changes: 26 additions & 18 deletions gratipay/homepage.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""
from __future__ import absolute_import, division, print_function, unicode_literals

import braintree
from gratipay import utils
from gratipay.models.payment_for_open_source import PaymentForOpenSource

Expand All @@ -16,11 +17,15 @@ def _parse(raw):

# amount
amount = x('amount') or '0'
if (not amount.isdigit()) or (int(amount) < 50):
if (not amount.isdigit()) or (int(amount) < 10):
errors.append('amount')
amount = ''.join(x for x in amount.split('.')[0] if x.isdigit())

# TODO credit card token?
# credit card nonce
payment_method_nonce = x('payment_method_nonce')
if len(payment_method_nonce) > 36:
errors.append('payment_method_nonce')
payment_method_nonce = ''

# name
name = x('name')
Expand Down Expand Up @@ -63,6 +68,7 @@ def _parse(raw):
errors.append('promotion_message')

parsed = { 'amount': amount
, 'payment_method_nonce': payment_method_nonce
, 'name': name
, 'email_address': email_address
, 'follow_up': follow_up
Expand All @@ -74,34 +80,36 @@ def _parse(raw):
return parsed, errors


def _charge(app, parsed):
raise NotImplementedError
def _store(parsed):
return PaymentForOpenSource.insert(**parsed)


def _store(parsed, transaction_id):
return PaymentForOpenSource.insert(transaction_id=transaction_id, **parsed)
def _charge(amount, payment_method_nonce, _sale=braintree.Transaction.sale):
return _sale({ 'amount': amount
, 'payment_method_nonce': payment_method_nonce
, 'options': {'submit_for_settlement': True}
})


def _send(app, parsed, payment_for_open_source):
def _send(app, email_address, payment_for_open_source):
app.email_queue.put( to=None
, template='paid-for-open-source'
, email=parsed['email_address']
, email=email_address
, amount=payment_for_open_source.amount
, receipt_url=payment_for_open_source.receipt_url
)


def pay_for_open_source(app, raw, _parse=_parse, _charge=_charge, _send=_send, _store=_store):
def pay_for_open_source(app, raw):
parsed, errors = _parse(raw)
payment_method_nonce = parsed.pop('payment_method_nonce')
payment_for_open_source = _store(parsed)
if not errors:
transaction_id = _charge(app, parsed)
if not transaction_id:
result = _charge(parsed['amount'], payment_method_nonce)
payment_for_open_source.process_result(result)
if not payment_for_open_source.succeeded:
errors.append('charging')
if not errors:
payment_for_open_source = _store(parsed, transaction_id)
if parsed['email_address']:
sent = _send(app, parsed['email_address'], payment_for_open_source)
if not sent:
errors.append('sending')
parsed= {}
if not errors and parsed['email_address']:
_send(app, parsed['email_address'], payment_for_open_source)
parsed = {} # no need to populate form anymore
return {'parsed': parsed, 'errors': errors}
26 changes: 22 additions & 4 deletions gratipay/models/payment_for_open_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ def receipt_url(self):

@classmethod
def from_uuid(cls, uuid, cursor=None):
"""Take a uuid and return an object.
"""
return (cursor or cls.db).one("""
SELECT pfos.*::payments_for_open_source
FROM payments_for_open_source pfos
Expand All @@ -29,15 +31,31 @@ def from_uuid(cls, uuid, cursor=None):


@classmethod
def insert(cls, amount, transaction_id, name, follow_up, email_address,
def insert(cls, amount, name, follow_up, email_address,
promotion_name, promotion_url, promotion_twitter, promotion_message,
cursor=None):
"""Take baseline info and insert into the database.
"""
uuid = uuidlib.uuid4().hex
return (cursor or cls.db).one("""
INSERT INTO payments_for_open_source
(uuid, amount, transaction_id, name, follow_up, email_address,
(uuid, amount, name, follow_up, email_address,
promotion_name, promotion_url, promotion_twitter, promotion_message)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
VALUES (%s, %s, %s, %s, %s,
%s, %s, %s, %s)
RETURNING payments_for_open_source.*::payments_for_open_source
""", (uuid, amount, transaction_id, name, follow_up, email_address,
""", (uuid, amount, name, follow_up, email_address,
promotion_name, promotion_url, promotion_twitter, promotion_message))


def process_result(self, result):
"""Take a Braintree API result and update the database.
"""
transaction_id = result.transaction.id if result.transaction else None
self.db.run("""
UPDATE payments_for_open_source
SET transaction_id=%s
, succeeded=%s
WHERE uuid=%s
""", (transaction_id, result.is_success, self.uuid))
self.set_attributes(transaction_id=transaction_id, succeeded=result.is_success)
1 change: 0 additions & 1 deletion gratipay/testing/harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ def clear_tables(self):
def make_payment_for_open_source(self, **info):
defaults = dict( amount='1000'
, name='Alice Liddell'
, transaction_id='deadbeef'
, email_address='[email protected]'
, follow_up='monthly'
, promotion_name='Wonderland'
Expand Down
44 changes: 44 additions & 0 deletions tests/py/fixtures/BadCharge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
interactions:
- request:
body: !!python/unicode <transaction><amount>10</amount><type>sale</type><options><submit_for_settlement
type="boolean">true</submit_for_settlement></options><payment_method_nonce>fake-valid-nonce</payment_method_nonce></transaction>
headers: {}
method: POST
uri: https://api.sandbox.braintreegateway.com:443/merchants/j9gwdfjdkxymhdgr/transactions
response:
body:
string: !!binary |
H4sIAP0Ls1kAA+RYS2/jNhC+51cYvjOSnWzjBIqCoosteuheNtnDXgJKHFuMKVLlw7Hz6zuUZFmK
qCQ9tFigN2vm45AznMdHJ3f7Usx2oA1X8na+OI/nM5C5YlxubucP91/Ian6XniVWU2lobhGVns1m
CWfpE9W7NSyWSYQfXmYstc6kxmUltxbY41rpRwPWCihB2iRqAR5rDxWkhgpIovqnl+VOa9z7QLhR
BI8A6cO3z0k0FnswLZWTNl3E53GcRO2XV5Sg84JKS2ieeyHB0xmbXW8vr15esqel/uXTUxKFULUP
LiMB3UxycTu32sE8anahxoL+EFRphsiAItdAMUyE2pmPwe2c4aflJczTZby4IvE1iVf3y8XNRXxz
cfUDI9EtqNe7iv2z9acFbbyNVeiB/6iv9PUJUbjm2lgiaQkBpaDTulyVFZWHgAZKykVA/gyZ4TZk
qyqUDMnXdD8KatT3Ksm4EJjJ/7GHxmoAzAnGNBgTCsHegmT+JiYhQuVUcBsyr2GDZRiKk8IKE02N
XF8u4qsk6ouOx8Y81Ydprxq1X0GoqAq6/BDq4j2UdHgpPB9fWO+O0LW1kyxULJ3GtMlOtaaHgRLj
2WtTISMV1ZZjOE5N6dWKkHHqbKE0f3nffM9sRm1eBDEFr6r/ZUq+kSA/TS72b6ftj2TNQTDT5sLO
ENBaaYIxqpQ0EHStxvVcH6LTP3FgvQk4mhje2ivQH42VNzG1G7vdeP+x0EM3OB6e6QE1T9BkOU4c
M77YpNIqx90wDsfqoDW8tnT/9fdPP37D3vMWaGhleJRF7Gf6lHZipcUMTn+tULMDFlxdI+rQMsb9
STD4Y9jI153iub+gNV48rsDcyUCPI+I8E8BdmnE/gbJ0TxquElTBHsrqOM0zpQRQOU/XVBjPkzrA
kT2gFySnmrUpbtUWQjWYcZlexovlauWbrez3kct0sVotkqj9aEsFTZKalX3nhmKudN/HVlFx3Vxl
qaQtUs8BR8IR9gBUIzFZxgNwLW33bSc38Y2mZpoP307z/CQ9nbJQog52uH3wkm6AOC3SwtrK3EQR
NdiizXmmKZe+bNp8P8e+GVX04Dv3YwmYq+xRqI2Kduj/eSU3dyB3XCvpAbeGSpapPdLezn7b6zRU
FHnkV+XTr/ndaAqgwhZ4YqS0civVs0yinqwBMci4Pembz1blNF4c5uDGCc/geqjXmm4QeGqKs+4E
7cna89KDVqKHOAra8BnjsBXiKJPbE2YgHbZWtSZeS2UOqd9uLD3GSTGX15T7tPVJ1oCc5H85aOsI
xRh5jp14VFK+QEGWihi2nSicTt8SxGHhtM8WUnDMM30YEIBueNYIQEPtjfhqQ6KNirL6IPnu8J2F
9jl04hf9F1KNmHrkNBEySHK1KritqBNbrMFa0p2vx1WMwu4EKa04nmMsb7yMxm7+y55/5KX4E8Sh
k7Qp0vR7QcMc0GUm17ya5Ig9fdefawJMKuQkihGkYcSHNNjJB0g8lrZBLB751T5+6BGcbwGCy7ip
izWog8aKOjbpiV479UTD7jg+29Aokkf/xEe/Juq30zdzD9/bEsZW8c53flCvAaZGrN9WPZPmNkda
DEPmtGn4OwOLr1Rz7L8DVfhueuQ/vP0QM/pX44Nw2PsA4NzR4WP4hxBmKlLWkEGX5wFujzcy4bv3
vHIW3noXNXOcsicc1H59CNsOV8Il0lXXPLw8oWh68aPvxUk0BRoSvl5Qhrywz/kmQe/bqlnie7Y6
KmkLpBEES9HnKODR12oY3UGjSc/+BgAA//8DAFQWy6JwEwAA
headers:
cache-control: ['max-age=0, private, must-revalidate']
content-encoding: [gzip]
content-type: [application/xml; charset=utf-8]
etag: [W/"48d3905da353929ec2b8cd3d54737edd"]
strict-transport-security: [max-age=31536000; includeSubDomains]
transfer-encoding: [chunked]
vary: [Accept-Encoding]
status: {code: 201, message: Created}
version: 1
25 changes: 25 additions & 0 deletions tests/py/fixtures/GoodCharge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
interactions:
- request:
body: !!python/unicode <transaction><amount>10</amount><type>sale</type><options><submit_for_settlement
type="boolean">true</submit_for_settlement></options><payment_method_nonce>deadbeef</payment_method_nonce></transaction>
headers: {}
method: POST
uri: https://api.sandbox.braintreegateway.com:443/merchants/j9gwdfjdkxymhdgr/transactions
response:
body:
string: !!binary |
H4sIAP0Ls1kAA6RSu27DMAzc8xWCdkfJkKIFFGXLFzRzQMdMatR6gKLb+O8rP/OA26UbyaNOdyfp
3dVW4gsplt5t5Xq5kgLdyRelu2zl4X2fvcqdWWgIZYZEnjLCGLyLaBZC6G4U23JqBDcBtxKIoJFq
gJjARThxuqSfzK+P2Ije+jRJotC8rTcvG626+h4EZirzmnHgi43NfSVNgMai46NF/vDF0Xl3Qq2m
7QcOizHCBc3BfTr/7YQngddQEhZijmap1XjiJls96B7aMSD1FMM9rAMQ2PhHXmB97disV0l/X45I
a9lEqJKzrhznPrQE8U5erHNbcnZO7xiRucLWlWGq09F5bHLyRKaHSLI+kqyLxBQIRY541moW/i2G
m/f/PELKZeaX/gAAAP//AwB33+lL4gIAAA==
headers:
cache-control: [no-cache]
content-encoding: [gzip]
content-type: [application/xml; charset=utf-8]
strict-transport-security: [max-age=31536000; includeSubDomains]
transfer-encoding: [chunked]
vary: [Accept-Encoding]
status: {code: 422, message: Unprocessable Entity}
version: 1
Loading

0 comments on commit 403e7cb

Please sign in to comment.