Large diffs are not rendered by default.

Large diffs are not rendered by default.

This file was deleted.

@@ -7,37 +7,37 @@ interactions:
response:
body:
string: !!binary |
H4sIAAAAAAAAALxWbY/jNBD+KyYf0CG6bd7aJJUQEru38AWBYNGddFlFTjJpzTp2sJ3dK6v+d8ZJ
k6bt7goQIl9qz5vHzzwz7rPDSmftBV4QrZb+MprhPtNGOWvnKHRmjqA1oOx7RQ1r6A4lulAAIrtU
cFngWgqU4q5RsmIcsqNUtJzPnBIwAGsOhp27aUv4lgyRviAfAB74juC6BmE0MZI0IBsOhIqSGKC1
JjvZkhw4g0cgTMzxwFZxDLg1ptHrdJEuzLyQ6aJeQfj7T15Yh2iC0ZhhoJ31c2/e/eD20/Pfcv/c
YAJQZue2m0Pq6FKnC7QsmW443R0Mp2pUMlGywibxyZ35wf3+fn+GypjV/X7fAWmgMIDlqijXMHMq
ybl8AqWzQrbCYBkj30exYiDKURgssSRMo+MgidEIi0etiOLeuVWM/EgV8RPiBWtvuXYD8rWLH/Fd
L8BUK/ooW8UMjFFjBNoUmawqDbi98sLQdWeOYTVkf0phKfGe4qFKkDuUkXe//Uq+JNdU0JJ+hRE3
IDMQNOeT+zyCYhWbCLShptWTQxN7Fyo2GB2EMxhYpE7uc7dt8T474kbE99ehu3bj432WFno8Y5ms
QvwC3/XDlRuuomDC/Uslehn4bKNbWhJvGaQtRgtvpSJmC6RiShti7488LfHwJyD6gTUNlASxw+2U
zKTYFRzW5Jxnv4ifP3wE/+MPdI70J1QBKQEadNRSqd2crK/e2XtjxMJinLauGxSUbBVU36QdFfto
T8wg+B0RHaKAo1LInjBp7wV3vQ2ek5Nr7CBh+mjpgvYG9sqqFQWdko6JTNmEMiOzHv7Motk39YvK
HtJLA6ysesX3oHrN82T09Grk07AspFQlE5i1HkTYhAUc9cIolrdGqtFAgcECmQm7LeUt48c2Oh0b
W6q3hm667sR67Opc8sOmS94WGZt4EL09XY5Vf2u61FCytu5nyzBKrnIuN8MWJyzfNRquQr8qXSjc
MoH4Ygq9EcYy2l+djKbImyVhP5sGRI5cOKA2kTRSa5ZjlTQIjWA9wqg69u3+tAYXc4DpzCgqNKeo
fkmKwF44De9MTouHjcKa2XHHrb9z7d68f3/jvGjDarqBCdQd0k2u59hB9aZH6TU3nS7iYBmHSRTF
6SLJw4DmhefTKgrDuAIa+IVX0TyO/bwo83mD9387h6wr9knR/+dUDO4vIP0nGA3ZBFEc24nrelHo
RkESL1G2TGgYVFXuBUWFSxokob+KE68oiyR2q0xIVVN+lt2/QOe/TyKnQmBXn7fkq9Wx1pjA8S8U
rkM38aNV1D0l478iJh5GnrpuHH4XTrSalZBTleU40PD0we62+16www0frW6Q9LeriZV9vkZt0H0T
LU6tC1aNTCihoi032cH4NfmZV//cMNv2/ZTtBTjH/2hBGzsjzKAS0uDb3/9FPEzl/V8AAAD//wMA
EGb7G5wKAAA=
H4sIAAAAAAAAALxWW2/bNhT+K5wehg1zY+ouGRh2SdIBA4oNWIc9VIFAUZTDhiY1korrBf7vO5Qs
WbHjthuG+cU8Vx6e8/Gjnjxeeys/9MM0iYM4XYBcGqu9lXdUegtPkg0D3U+aWN6SHWgM1YzJ8twg
FIW1kqAFqdWq4YKVR63shFh4NYMEvD049uG2q9l3aMz0BfqV7DZMWoOIrBFotBICNUoj1TKJtko/
XMEGnRaQ4N7a1qyKZbG0V1QVy03Cove/+NEmAhdIwi1nxls9De79H4jvnj4r/EMLFbC6PPVdH0qF
kE2xBM+am1aQ3cFxbgYjlzWnroh3eBGEd/u7/UkXpqru9vu+cZZRy2A8DRGGLbwGzq+2TJuSqk5a
GFsaYlBrzmQ9KcMYRsANBI6azF94MCziVARk77Xm6A3RKMiRH678eIVD9A2GHwqwH0KpDXlUneaW
TVkzaLSlpWoaw0B85UcRhr0t37DyLyUdBG4JbKolegs69NXvv6Ev0TWRpCZfQ8Y1UyWTpBKz8zwy
zRs+UxhLbGdmm+YJnIXINWRn0hsdXKdeOM8OBQHC/irKVnF2PE/sWg97JLBM4jALIpwmYY4jPMP6
uRGiLPvgsv8AABRbsjMLtFMdokQiqqQl1KLOoFbzR6hE7NAjJ8h0bau0/X4+egSIbbtKcHpwOgXb
+kH8/IZsqvc12nJ7D3DfoT87ZhwqjIvmxoB4hVavXCsNjIa6hhcdxiGFfJo13xY9LoeskAUm0aPS
Q5oJMEo1oKcYotjbwQf9wSp0LQBAdshWLMng4M6vO0nJHIFclpq1YldaVQ6zKF1rhxv9onHo77kD
jFlfiD2YLkU+453BDOAal1QpXXMJVZtRBTeSsqNdWs2rzio9OWhmt4zZEXV4wL+D/3TRnnPIPTH3
lqz7qwrz2G0qJQ5CX7xjLTe7nmAu8uS55vOo+Mgjsb9IMBDJ4lN0dkTYR+kMwNdVA5mNAD4luQGK
xVKy7TnfXY4vugAHybPigZV8HA00OPb7iLTDTGaaVhnDK8CAYdLAKB7ZZDpSxP75hM8oh5vSaiKN
IGB+SQtjOwsan7CK0Ie1BkQ4ZhUu3rvGN7e3N96LPnxD1mzW5L7HbWWu4H5u1kObLoVBh7MwzqI8
TbNimVdRSCrqB6RJoyhrGAkD6jekyrKgonV11cL5P15D2Y/52bj/51IsyGct/Sc9GqsJ0yxz5I79
FNg6zLMYdHFOorBpKj+kDSxJmEdBkuU+rWme4aaUSm+IOKnuX3Tnvy+iIlICZ5xexovTcd5QwJES
YB3hPEiTNA3nH1xcPkw4xTiLfoxmVsNrVhFdVkCXsPvo97r/veAHgpi8bgD0r5OZl3spJ2vY/2ZW
4MQzVE1IAC4tIZr1jHSImIw1a0gn7Kf0JymHl447ThgIflDAE9K/qo5A7GiSysI3yPBpengQ9n8D
AAD//wMANpQPtxQLAAA=
headers:
cache-control: ['no-cache, no-store, must-revalidate, pre-check=0, post-check=0']
content-disposition: [attachment; filename=json.json]
content-encoding: [gzip]
content-length: ['1091']
content-length: ['1097']
content-type: [application/json;charset=utf-8]
expires: ['Tue, 31 Mar 1981 05:00:00 GMT']
last-modified: ['Thu, 21 May 2015 22:11:42 GMT']
last-modified: ['Wed, 01 Jul 2015 12:19:26 GMT']
pragma: [no-cache]
set-cookie: ['guest_id=v1%3A143224630224336617; Domain=.twitter.com; Path=/;
Expires=Sat, 20-May-2017 22:11:42 UTC']
set-cookie: ['guest_id=v1%3A143575316654394798; Domain=.twitter.com; Path=/;
Expires=Fri, 30-Jun-2017 12:19:26 UTC']
status: [200 OK]
strict-transport-security: [max-age=631138519]
status: {code: 200, message: OK}
@@ -58,10 +58,10 @@ interactions:
content-length: ['93']
content-type: [application/json;charset=utf-8]
expires: ['Tue, 31 Mar 1981 05:00:00 GMT']
last-modified: ['Thu, 21 May 2015 22:11:44 GMT']
last-modified: ['Wed, 01 Jul 2015 12:19:28 GMT']
pragma: [no-cache]
set-cookie: ['guest_id=v1%3A143224630470081097; Domain=.twitter.com; Path=/;
Expires=Sat, 20-May-2017 22:11:44 UTC']
set-cookie: ['guest_id=v1%3A143575316882139494; Domain=.twitter.com; Path=/;
Expires=Fri, 30-Jun-2017 12:19:28 UTC']
status: [404 Not Found]
strict-transport-security: [max-age=631138519]
status: {code: 404, message: Not Found}
@@ -119,9 +119,8 @@ interactions:
AfM0plMwAAAA
headers:
content-encoding: [gzip]
content-language: [en]
content-type: [application/json; charset=utf-8]
transfer-encoding: [chunked]
vary: ['Authorization, Accept-Language, Cookie']
vary: ['Authorization, Cookie']
status: {code: 404, message: NOT FOUND}
version: 1

Large diffs are not rendered by default.

This file was deleted.

@@ -7,14 +7,14 @@ interactions:
response:
body:
string: !!binary |
H4sIAAAAAAAAA52TP2+DMBDFv4tnEkMSmspS1W4du3TqEhlw4SRjW/5DlaJ+954xaUmGSnQCW/d+
9/x0NxKpW1CEkdZyD4afSUagIaw4Hg75cZ8RPnDP7SlYiUWd98YxStOl27bgu1AFJ2ytlRfKb2vd
00Bn9ePwsEceohMkggle3MAMzKCkRpqjCzud7+VN/9R3Kl8Uvmsp9Qeqb93+1YD+qNBY+gfV/oOA
qpFq3wkMC5/wFR8Ozq8zMylGGj8naCLDYfpWNKsMzRq086HQyUitMHqChcrVFowHrdYZu1IiSduW
K/jEmVlLQqVDQLS0zsKkQKUYcNDWSZNkpMbCwOtzjMKKWsCAwf4Dd6NFmj8bgbP9skglxg1enHjT
xwV759KJjCjex8Ln323DITZcnQlTQcqMVLiQi027jPc064iUup4yT4skeg5xL10wRlv/tKzG4g6s
4JXEhjMbdNKZUEmoTylRti8zMt9M80dYflkFXKbFCUd8OtWI9Rgd94jb5cVuk5eb4u41L1h+ZGX5
hr2Daa5qyk1+2OyOr8U9Kwu2P7yRr29XRjQ5fQQAAA==
H4sIAAAAAAAAA52TT0+EMBDFv0vPrAXWFdPE6M2jlz152RSoMElpm/5hsxK/u1OKinswwRO0mfeb
15eZiUjdgSKMdJZ7MPxCMgItYUV1e5tX+4zwkXtuT8FKLOq9N45Rmi7dTQe+D3VwwjZaeaH8TaMH
Guiifhwf9shDdIJEMMGLK5iBBZTUSHN0Zaf3g7zqn/rO5avCNy2lPqP62u1fDei3Co2lf1DdPwio
mqj2vcCw8Akf8eHg/DYzs2Ki8XOCNjIcpm9Fu8nQokE7Z4VOJmqF0TMs1K6xYDxotc3YLyWStO24
gnecma0kVDoEREvbLMwKVIoRB22bNEkmaiyMvLnEKKxoBIwY7D9wV1qk+YsRONsvq1Ri3ODFibdD
XLA3Lp3IiOJDLHz+2TYcYsPVhTAVpMxIjQu52rSv8Z5nHZFSN3PmaZHEwCHupQvGaOuf1tVY3IMV
vJbYcGGDTjoTagnNKSXK9lVGlpt5/gjLv1YBl2l1whGfTw1iPUbHPeLKvCh3+WFX3B3zguUVOxxe
sXcw7a+awy6/25XVsbhn+3tWlq/k4xOQ9WwgfQQAAA==
headers:
access-control-allow-credentials: ['true']
access-control-allow-origin: ['*']
@@ -25,8 +25,8 @@ interactions:
content-encoding: [gzip]
content-security-policy: [default-src 'none']
content-type: [application/json; charset=utf-8]
etag: [W/"e665f3b4dbb033e0c1c61fbfc987f7ce"]
last-modified: ['Mon, 27 Apr 2015 18:51:34 GMT']
etag: [W/"c9af3cd0085b6eda64b21753a149b3d9"]
last-modified: ['Sat, 27 Jun 2015 18:38:22 GMT']
status: [200 OK]
strict-transport-security: [max-age=31536000; includeSubdomains; preload]
transfer-encoding: [chunked]
@@ -81,14 +81,14 @@ interactions:
response:
body:
string: !!binary |
H4sIAAAAAAAAA52TP2+DMBDFv4tnEkMSmspS1W4du3TqEhlw4SRjW/5DlaJ+954xaUmGSnQCW/d+
9/x0NxKpW1CEkdZyD4afSUagIaw4Hg75cZ8RPnDP7SlYiUWd98YxStOl27bgu1AFJ2ytlRfKb2vd
00Bn9ePwsEceohMkggle3MAMzKCkRpqjCzud7+VN/9R3Kl8Uvmsp9Qeqb93+1YD+qNBY+gfV/oOA
qpFq3wkMC5/wFR8Ozq8zMylGGj8naCLDYfpWNKsMzRq086HQyUitMHqChcrVFowHrdYZu1IiSduW
K/jEmVlLQqVDQLS0zsKkQKUYcNDWSZNkpMbCwOtzjMKKWsCAwf4Dd6NFmj8bgbP9skglxg1enHjT
xwV759KJjCjex8Ln323DITZcnQlTQcqMVLiQi027jPc064iUup4yT4skeg5xL10wRlv/tKzG4g6s
4JXEhjMbdNKZUEmoTylRti8zMt9M80dYflkFXKbFCUd8OtWI9Rgd94jb5cVuk5eb4u41L1h+ZGX5
hr2Daa5qyk1+2OyOr8U9Kwu2P7yRr29XRjQ5fQQAAA==
H4sIAAAAAAAAA52TT0+EMBDFv0vPrAXWFdPE6M2jlz152RSoMElpm/5hsxK/u1OKinswwRO0mfeb
15eZiUjdgSKMdJZ7MPxCMgItYUV1e5tX+4zwkXtuT8FKLOq9N45Rmi7dTQe+D3VwwjZaeaH8TaMH
Guiifhwf9shDdIJEMMGLK5iBBZTUSHN0Zaf3g7zqn/rO5avCNy2lPqP62u1fDei3Co2lf1DdPwio
mqj2vcCw8Akf8eHg/DYzs2Ki8XOCNjIcpm9Fu8nQokE7Z4VOJmqF0TMs1K6xYDxotc3YLyWStO24
gnecma0kVDoEREvbLMwKVIoRB22bNEkmaiyMvLnEKKxoBIwY7D9wV1qk+YsRONsvq1Ri3ODFibdD
XLA3Lp3IiOJDLHz+2TYcYsPVhTAVpMxIjQu52rSv8Z5nHZFSN3PmaZHEwCHupQvGaOuf1tVY3IMV
vJbYcGGDTjoTagnNKSXK9lVGlpt5/gjLv1YBl2l1whGfTw1iPUbHPeLKvCh3+WFX3B3zguUVOxxe
sXcw7a+awy6/25XVsbhn+3tWlq/k4xOQ9WwgfQQAAA==
headers:
access-control-allow-credentials: ['true']
access-control-allow-origin: ['*']
@@ -99,8 +99,8 @@ interactions:
content-encoding: [gzip]
content-security-policy: [default-src 'none']
content-type: [application/json; charset=utf-8]
etag: [W/"e665f3b4dbb033e0c1c61fbfc987f7ce"]
last-modified: ['Mon, 27 Apr 2015 18:51:34 GMT']
etag: [W/"c9af3cd0085b6eda64b21753a149b3d9"]
last-modified: ['Sat, 27 Jun 2015 18:38:22 GMT']
status: [200 OK]
strict-transport-security: [max-age=31536000; includeSubdomains; preload]
transfer-encoding: [chunked]
@@ -155,14 +155,14 @@ interactions:
response:
body:
string: !!binary |
H4sIAAAAAAAAA52TP2+DMBDFv4tnEkMSmspS1W4du3TqEhlw4SRjW/5DlaJ+954xaUmGSnQCW/d+
9/x0NxKpW1CEkdZyD4afSUagIaw4Hg75cZ8RPnDP7SlYiUWd98YxStOl27bgu1AFJ2ytlRfKb2vd
00Bn9ePwsEceohMkggle3MAMzKCkRpqjCzud7+VN/9R3Kl8Uvmsp9Qeqb93+1YD+qNBY+gfV/oOA
qpFq3wkMC5/wFR8Ozq8zMylGGj8naCLDYfpWNKsMzRq086HQyUitMHqChcrVFowHrdYZu1IiSduW
K/jEmVlLQqVDQLS0zsKkQKUYcNDWSZNkpMbCwOtzjMKKWsCAwf4Dd6NFmj8bgbP9skglxg1enHjT
xwV759KJjCjex8Ln323DITZcnQlTQcqMVLiQi027jPc064iUup4yT4skeg5xL10wRlv/tKzG4g6s
4JXEhjMbdNKZUEmoTylRti8zMt9M80dYflkFXKbFCUd8OtWI9Rgd94jb5cVuk5eb4u41L1h+ZGX5
hr2Daa5qyk1+2OyOr8U9Kwu2P7yRr29XRjQ5fQQAAA==
H4sIAAAAAAAAA52TT0+EMBDFv0vPrAXWFdPE6M2jlz152RSoMElpm/5hsxK/u1OKinswwRO0mfeb
15eZiUjdgSKMdJZ7MPxCMgItYUV1e5tX+4zwkXtuT8FKLOq9N45Rmi7dTQe+D3VwwjZaeaH8TaMH
Guiifhwf9shDdIJEMMGLK5iBBZTUSHN0Zaf3g7zqn/rO5avCNy2lPqP62u1fDei3Co2lf1DdPwio
mqj2vcCw8Akf8eHg/DYzs2Ki8XOCNjIcpm9Fu8nQokE7Z4VOJmqF0TMs1K6xYDxotc3YLyWStO24
gnecma0kVDoEREvbLMwKVIoRB22bNEkmaiyMvLnEKKxoBIwY7D9wV1qk+YsRONsvq1Ri3ODFibdD
XLA3Lp3IiOJDLHz+2TYcYsPVhTAVpMxIjQu52rSv8Z5nHZFSN3PmaZHEwCHupQvGaOuf1tVY3IMV
vJbYcGGDTjoTagnNKSXK9lVGlpt5/gjLv1YBl2l1whGfTw1iPUbHPeLKvCh3+WFX3B3zguUVOxxe
sXcw7a+awy6/25XVsbhn+3tWlq/k4xOQ9WwgfQQAAA==
headers:
access-control-allow-credentials: ['true']
access-control-allow-origin: ['*']
@@ -173,8 +173,8 @@ interactions:
content-encoding: [gzip]
content-security-policy: [default-src 'none']
content-type: [application/json; charset=utf-8]
etag: [W/"e665f3b4dbb033e0c1c61fbfc987f7ce"]
last-modified: ['Mon, 27 Apr 2015 18:51:34 GMT']
etag: [W/"c9af3cd0085b6eda64b21753a149b3d9"]
last-modified: ['Sat, 27 Jun 2015 18:38:22 GMT']
status: [200 OK]
strict-transport-security: [max-age=31536000; includeSubdomains; preload]
transfer-encoding: [chunked]

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

This file was deleted.

@@ -10,14 +10,12 @@
from aspen.utils import typecheck
from gratipay.billing.exchanges import (
_prep_hit,
ach_credit,
cancel_card_hold,
capture_card_hold,
create_card_hold,
record_exchange,
record_exchange_result,
skim_credit,
sync_with_balanced,
skim_credit
)
from gratipay.exceptions import NegativeBalance, NotWhitelisted
from gratipay.models.exchange_route import ExchangeRoute
@@ -26,42 +24,6 @@
from gratipay.testing.billing import BillingHarness


class TestCredits(BillingHarness):

def test_ach_credit_withhold(self):
self.make_exchange('balanced-cc', 27, 0, self.homer)
withhold = D('1.00')
error = ach_credit(self.db, self.homer, withhold)
assert error == ''
homer = Participant.from_id(self.homer.id)
assert self.homer.balance == homer.balance == 1

def test_ach_credit_amount_under_minimum(self):
self.make_exchange('balanced-cc', 8, 0, self.homer)
r = ach_credit(self.db, self.homer, 0)
assert r is None

@mock.patch('gratipay.billing.exchanges.thing_from_href')
def test_ach_credit_failure(self, tfh):
tfh.side_effect = Foobar
self.make_exchange('balanced-cc', 20, 0, self.homer)
error = ach_credit(self.db, self.homer, D('1.00'))
homer = Participant.from_id(self.homer.id)
assert self.homer.get_bank_account_error() == error == "Foobar()"
assert self.homer.balance == homer.balance == 20

def test_ach_credit_no_bank_account(self):
self.make_exchange('balanced-cc', 20, 0, self.david)
error = ach_credit(self.db, self.david, D('1.00'))
assert error == 'No bank account'

def test_ach_credit_invalidated_bank_account(self):
bob = self.make_participant('bob', is_suspicious=False, balance=20,
last_ach_result='invalidated')
error = ach_credit(self.db, bob, D('1.00'))
assert error == 'No bank account'


class TestCardHolds(BillingHarness):

# create_card_hold
@@ -277,7 +239,7 @@ class TestRecordExchange(Harness):
def test_record_exchange_doesnt_update_balance_for_positive_amounts(self):
alice = self.make_participant('alice', last_bill_result='')
record_exchange( self.db
, ExchangeRoute.from_network(alice, 'balanced-cc')
, ExchangeRoute.from_network(alice, 'braintree-cc')
, amount=D("0.59")
, fee=D("0.41")
, participant=alice
@@ -287,9 +249,9 @@ def test_record_exchange_doesnt_update_balance_for_positive_amounts(self):
assert alice.balance == D('0.00')

def test_record_exchange_updates_balance_for_negative_amounts(self):
alice = self.make_participant('alice', balance=50, last_ach_result='')
alice = self.make_participant('alice', balance=50, last_paypal_result='')
record_exchange( self.db
, ExchangeRoute.from_network(alice, 'balanced-ba')
, ExchangeRoute.from_network(alice, 'paypal')
, amount=D('-35.84')
, fee=D('0.75')
, participant=alice
@@ -299,34 +261,34 @@ def test_record_exchange_updates_balance_for_negative_amounts(self):
assert alice.balance == D('13.41')

def test_record_exchange_fails_if_negative_balance(self):
alice = self.make_participant('alice', last_ach_result='')
ba = ExchangeRoute.from_network(alice, 'balanced-ba')
alice = self.make_participant('alice', last_paypal_result='')
ba = ExchangeRoute.from_network(alice, 'paypal')
with pytest.raises(NegativeBalance):
record_exchange(self.db, ba, D("-10.00"), D("0.41"), alice, 'pre')

def test_record_exchange_result_restores_balance_on_error(self):
alice = self.make_participant('alice', balance=30, last_ach_result='')
ba = ExchangeRoute.from_network(alice, 'balanced-ba')
alice = self.make_participant('alice', balance=30, last_paypal_result='')
ba = ExchangeRoute.from_network(alice, 'paypal')
e_id = record_exchange(self.db, ba, D('-27.06'), D('0.81'), alice, 'pre')
assert alice.balance == D('02.13')
record_exchange_result(self.db, e_id, 'failed', 'SOME ERROR', alice)
alice = Participant.from_username('alice')
assert alice.balance == D('30.00')

def test_record_exchange_result_restores_balance_on_error_with_invalidated_route(self):
alice = self.make_participant('alice', balance=37, last_ach_result='')
ba = ExchangeRoute.from_network(alice, 'balanced-ba')
e_id = record_exchange(self.db, ba, D('-32.45'), D('0.86'), alice, 'pre')
alice = self.make_participant('alice', balance=37, last_paypal_result='')
pp = ExchangeRoute.from_network(alice, 'paypal')
e_id = record_exchange(self.db, pp, D('-32.45'), D('0.86'), alice, 'pre')
assert alice.balance == D('3.69')
ba.update_error('invalidated')
pp.update_error('invalidated')
record_exchange_result(self.db, e_id, 'failed', 'oops', alice)
alice = Participant.from_username('alice')
assert alice.balance == D('37.00')
assert ba.error == alice.get_bank_account_error() == 'invalidated'
assert pp.error == alice.get_paypal_error() == 'invalidated'

def test_record_exchange_result_doesnt_restore_balance_on_success(self):
alice = self.make_participant('alice', balance=50, last_ach_result='')
ba = ExchangeRoute.from_network(alice, 'balanced-ba')
alice = self.make_participant('alice', balance=50, last_paypal_result='')
ba = ExchangeRoute.from_network(alice, 'paypal')
e_id = record_exchange(self.db, ba, D('-43.98'), D('1.60'), alice, 'pre')
assert alice.balance == D('4.42')
record_exchange_result(self.db, e_id, 'succeeded', None, alice)
@@ -335,56 +297,9 @@ def test_record_exchange_result_doesnt_restore_balance_on_success(self):

def test_record_exchange_result_updates_balance_for_positive_amounts(self):
alice = self.make_participant('alice', balance=4, last_bill_result='')
cc = ExchangeRoute.from_network(alice, 'balanced-cc')
cc = ExchangeRoute.from_network(alice, 'braintree-cc')
e_id = record_exchange(self.db, cc, D('31.59'), D('0.01'), alice, 'pre')
assert alice.balance == D('4.00')
record_exchange_result(self.db, e_id, 'succeeded', None, alice)
alice = Participant.from_username('alice')
assert alice.balance == D('35.59')


class TestSyncWithBalanced(BillingHarness):
@pytest.mark.xfail(reason="We don't use balanced for debits anymore")
def test_sync_with_balanced(self):
with mock.patch('gratipay.billing.exchanges.record_exchange_result') as rer:
rer.side_effect = Foobar()
hold, error = create_card_hold(self.db, self.janet, D('20.00'))
assert error == '' # sanity check
with self.assertRaises(Foobar):
capture_card_hold(self.db, self.janet, D('10.00'), hold)
exchange = self.db.one("SELECT * FROM exchanges")
assert exchange.status == 'pre'
sync_with_balanced(self.db)
exchange = self.db.one("SELECT * FROM exchanges")
assert exchange.status == 'succeeded'
assert Participant.from_username('janet').balance == 10

@pytest.mark.xfail(reason="We don't use balanced for debits anymore")
def test_sync_with_balanced_deletes_charges_that_didnt_happen(self):
with mock.patch('gratipay.billing.exchanges.record_exchange_result') as rer \
, mock.patch('balanced.CardHold.capture') as capture:
rer.side_effect = capture.side_effect = Foobar
hold, error = create_card_hold(self.db, self.janet, D('33.67'))
assert error == '' # sanity check
with self.assertRaises(Foobar):
capture_card_hold(self.db, self.janet, D('7.52'), hold)
exchange = self.db.one("SELECT * FROM exchanges")
assert exchange.status == 'pre'
sync_with_balanced(self.db)
exchanges = self.db.all("SELECT * FROM exchanges")
assert not exchanges
assert Participant.from_username('janet').balance == 0

def test_sync_with_balanced_reverts_credits_that_didnt_happen(self):
self.make_exchange('balanced-cc', 41, 0, self.homer)
with mock.patch('gratipay.billing.exchanges.record_exchange_result') as rer \
, mock.patch('gratipay.billing.exchanges.thing_from_href') as tfh:
rer.side_effect = tfh.side_effect = Foobar
with self.assertRaises(Foobar):
ach_credit(self.db, self.homer, 0, 0)
exchange = self.db.one("SELECT * FROM exchanges WHERE amount < 0")
assert exchange.status == 'pre'
sync_with_balanced(self.db)
exchanges = self.db.all("SELECT * FROM exchanges WHERE amount < 0")
assert not exchanges
assert Participant.from_username('homer').balance == 41
@@ -12,7 +12,7 @@
from gratipay.billing.payday import NoPayday, Payday
from gratipay.exceptions import NegativeBalance
from gratipay.models.participant import Participant
from gratipay.testing import Foobar, Harness
from gratipay.testing import Foobar
from gratipay.testing.billing import BillingHarness
from gratipay.testing.emails import EmailHarness

@@ -37,17 +37,17 @@ def test_payday_doesnt_move_money_from_a_suspicious_account(self, fch):
self.db.run("""
UPDATE participants
SET is_suspicious = true
WHERE username = 'janet'
WHERE username = 'obama'
""")
team = self.make_team(owner=self.homer, is_approved=True)
self.janet.set_subscription_to(team, '6.00') # under $10!
self.obama.set_subscription_to(team, '6.00') # under $10!
fch.return_value = {}
Payday.start().run()

janet = Participant.from_username('janet')
obama = Participant.from_username('obama')
homer = Participant.from_username('homer')

assert janet.balance == D('0.00')
assert obama.balance == D('0.00')
assert homer.balance == D('0.00')

@mock.patch.object(Payday, 'fetch_card_holds')
@@ -68,32 +68,6 @@ def test_payday_doesnt_move_money_to_a_suspicious_account(self, fch):
assert obama.balance == D('0.00')
assert homer.balance == D('0.00')

@mock.patch.object(Payday, 'fetch_card_holds')
def test_payday_moves_money_with_balanced(self, fch):
team = self.make_team(owner=self.homer, is_approved=True)
self.obama.set_subscription_to(team, '15.00')
fch.return_value = {}
Payday.start().run()

obama = Participant.from_username('obama')
homer = Participant.from_username('homer')

assert obama.balance == D('0.00')
assert homer.balance == D('0.00')

#janet_customer = balanced.Customer.fetch(obama.balanced_customer_href)
homer_customer = balanced.Customer.fetch(homer.balanced_customer_href)

created_at = balanced.Transaction.f.created_at

credit = homer_customer.credits.sort(created_at.desc()).first()
assert credit.amount == 1500
assert credit.description == 'homer'

#debit = janet_customer.debits.sort(created_at.desc()).first()
#assert debit.amount == 1576 # base amount + fee
#assert debit.description == 'obama'

@pytest.mark.xfail(reason="haven't migrated transfer_takes yet")
@mock.patch.object(Payday, 'fetch_card_holds')
@mock.patch('gratipay.billing.payday.create_card_hold')
@@ -428,8 +402,8 @@ def test_payin_doesnt_make_null_payments(self):

def test_process_subscriptions(self):
alice = self.make_participant('alice', claimed_time='now', balance=1)
hannibal = self.make_participant('hannibal', claimed_time='now', last_ach_result='')
lecter = self.make_participant('lecter', claimed_time='now', last_ach_result='')
hannibal = self.make_participant('hannibal', claimed_time='now', last_paypal_result='')
lecter = self.make_participant('lecter', claimed_time='now', last_paypal_result='')
A = self.make_team('The A Team', hannibal, is_approved=True)
B = self.make_team('The B Team', lecter, is_approved=True)
alice.set_subscription_to(A, D('0.51'))
@@ -486,7 +460,7 @@ def test_transfer_takes(self):

def test_process_draws(self):
alice = self.make_participant('alice', claimed_time='now', balance=1)
hannibal = self.make_participant('hannibal', claimed_time='now', last_ach_result='')
hannibal = self.make_participant('hannibal', claimed_time='now', last_paypal_result='')
A = self.make_team('The A Team', hannibal, is_approved=True)
alice.set_subscription_to(A, D('0.51'))

@@ -561,84 +535,6 @@ def test_payin_dumps_transfers_for_debugging(self, cch, fch):
assert filename.endswith('_payments.csv')
os.unlink(filename)


class TestPayout(Harness):

def test_payout_no_balanced_href_does_________what_question_mark(self):
self.make_participant('alice', claimed_time='now', is_suspicious=False,
balance=20)
Payday.start().payout()

@mock.patch('gratipay.billing.payday.ach_credit')
def test_payout_can_pay_out(self, ach):
alice = self.make_participant('alice', claimed_time='now', is_suspicious=False,
balanced_customer_href='foo',
last_ach_result='')
self.make_exchange('balanced-cc', 20, 0, alice)
self.make_team(owner='alice', is_approved=True)
Payday.start().payout()

assert ach.call_count == 1
assert ach.call_args_list[0][0][1].id == alice.id
assert ach.call_args_list[0][0][2] == 0

@mock.patch('gratipay.billing.payday.log')
def test_payout_skips_unreviewed(self, log):
self.make_participant('alice', claimed_time='now', is_suspicious=None,
balance=20, balanced_customer_href='foo',
last_ach_result='')
self.make_team(owner='alice', is_approved=True)
Payday.start().payout()
log.assert_any_call('UNREVIEWED: alice')

@mock.patch('gratipay.billing.payday.ach_credit')
def test_payout_ach_error_gets_recorded(self, ach_credit):
self.make_participant('alice', claimed_time='now', is_suspicious=False,
balance=20, balanced_customer_href='foo',
last_ach_result='')
self.make_team(owner='alice', is_approved=True)
ach_credit.return_value = 'some error'
Payday.start().payout()
payday = self.fetch_payday()
assert payday['nach_failing'] == 1

@mock.patch('gratipay.billing.payday.ach_credit')
def test_payout_pays_out_Gratipay_1_0_balance(self, ach):
alice = self.make_participant('alice', claimed_time='now', is_suspicious=False,
balanced_customer_href='foo', last_ach_result='',
balance=20, status_of_1_0_balance='pending-payout')
Payday.start().payout()

assert ach.call_count == 1
assert ach.call_args_list[0][0][1].id == alice.id
assert ach.call_args_list[0][0][2] == 0

@mock.patch('balanced.BankAccount.credit')
def test_paying_out_sets_1_0_status_to_resolved(self, credit):
alice = self.make_participant('alice', claimed_time='now', is_suspicious=False,
balanced_customer_href='foo', last_ach_result='',
balance=0, status_of_1_0_balance='pending-payout')
self.make_exchange('balanced-cc', 20, 0, alice) # sets balance, and satisfies self_check
Payday.start().payout()
alice = Participant.from_username('alice')
assert alice.status_of_1_0_balance == 'resolved'
assert alice.balance == 0

@mock.patch('balanced.BankAccount.credit')
def test_payout_ignores_unresolved(self, credit):
bob = self.make_participant('bob', claimed_time='now', is_suspicious=False,
balanced_customer_href='foo', last_ach_result='',
balance=13, status_of_1_0_balance='unresolved')
alice = self.make_participant('alice', claimed_time='now', is_suspicious=False,
balanced_customer_href='foo', last_ach_result='',
balance=0, status_of_1_0_balance='pending-payout')
self.make_exchange('balanced-cc', 20, 0, alice)
Payday.start().payout()
bob = Participant.from_username('bob')
assert bob.status_of_1_0_balance == 'unresolved'
assert bob.balance == 13


class TestNotifyParticipants(EmailHarness):

def test_it_notifies_participants(self):
@@ -31,7 +31,8 @@ def test_no_csrf_cookie_set_for_callbacks(self):

@patch('gratipay.billing.exchanges.record_exchange_result')
def test_credit_callback(self, rer):
alice = self.make_participant('alice', last_ach_result='')
alice = self.make_participant('alice')
ExchangeRoute.insert(alice, 'balanced-ba', '/bank/foo', '')
ba = ExchangeRoute.from_network(alice, 'balanced-ba')
for status in ('succeeded', 'failed'):
error = 'FOO' if status == 'failed' else None
@@ -92,7 +92,7 @@ def test_wbtba_withdraws_balance_to_bank_account(self, tfh):
alice = self.make_participant( 'alice'
, balance=D('10.00')
, is_suspicious=False
, last_ach_result=''
, last_paypal_result=''
)
alice.close('bank')

@@ -599,6 +599,7 @@ def test_get_teams_can_get_all_teams(self):

# giving, npatrons and receiving

@pytest.mark.xfail(reason="#3399")
def test_only_funded_tips_count(self):
alice = self.make_participant('alice', claimed_time='now', last_bill_result='')
bob = self.make_participant('bob', claimed_time='now')
@@ -618,6 +619,7 @@ def test_only_funded_tips_count(self):
funded_tip = self.db.one("SELECT * FROM subscriptions WHERE is_funded ORDER BY id")
assert funded_tip.subscriber == alice.username

@pytest.mark.xfail(reason="#3399")
def test_only_latest_tip_counts(self):
alice = self.make_participant('alice', claimed_time='now', last_bill_result='')
team = self.make_team(is_approved=True)
@@ -758,11 +760,4 @@ def test_archive_records_an_event(self):

def test_suggested_payment_is_zero_for_new_user(self):
alice = self.make_participant('alice')
assert alice.suggested_payment == 0

class TestGetBalancedAccount(Harness):
def test_get_balanced_account_creates_new_customer_href(self):
alice = self.make_participant('alice')
account = alice.get_balanced_account()
alice = Participant.from_username('alice')
assert alice.balanced_customer_href == account.href
assert alice.suggested_payment == 0
@@ -1,6 +1,5 @@
from __future__ import absolute_import, division, print_function, unicode_literals

import balanced
from braintree.test.nonces import Nonces
import mock

@@ -42,28 +41,28 @@ def test_associate_invalid_card(self):
@mock.patch.object(Participant, 'send_email')
def test_associate_paypal(self, mailer):
mailer.return_value = 1 # Email successfully sent
self.david.add_email('david@gmail.com')
self.db.run("UPDATE emails SET verified=true WHERE address='david@gmail.com'")
self.hit('david', 'associate', 'paypal', 'david@gmail.com')
assert ExchangeRoute.from_network(self.david, 'paypal')
assert self.david.has_payout_route
self.roman.add_email('roman@gmail.com')
self.db.run("UPDATE emails SET verified=true WHERE address='roman@gmail.com'")
self.hit('roman', 'associate', 'paypal', 'roman@gmail.com')
assert ExchangeRoute.from_network(self.roman, 'paypal')
assert self.roman.has_payout_route

def test_associate_paypal_invalid(self):
r = self.hit('david', 'associate', 'paypal', 'alice@gmail.com', expected=400)
assert not ExchangeRoute.from_network(self.david, 'paypal')
assert not self.david.has_payout_route
r = self.hit('roman', 'associate', 'paypal', 'alice@gmail.com', expected=400)
assert not ExchangeRoute.from_network(self.roman, 'paypal')
assert not self.roman.has_payout_route
assert "Only verified email addresses allowed." in r.body

def test_associate_bitcoin(self):
addr = '17NdbrSGoUotzeGCcMMCqnFkEvLymoou9j'
self.hit('david', 'associate', 'bitcoin', addr)
route = ExchangeRoute.from_network(self.david, 'bitcoin')
self.hit('roman', 'associate', 'bitcoin', addr)
route = ExchangeRoute.from_network(self.roman, 'bitcoin')
assert route.address == addr
assert route.error == ''

def test_associate_bitcoin_invalid(self):
self.hit('david', 'associate', 'bitcoin', '12345', expected=400)
assert not ExchangeRoute.from_network(self.david, 'bitcoin')
self.hit('roman', 'associate', 'bitcoin', '12345', expected=400)
assert not ExchangeRoute.from_network(self.roman, 'bitcoin')

def test_credit_card_page(self):
self.make_participant('alice', claimed_time='now')
@@ -90,38 +89,4 @@ def test_receipt_page_loads_for_braintree_cards(self):
ex_id = self.make_exchange(self.obama_route, 113, 30, self.obama)
url_receipt = '/~obama/receipts/{}.html'.format(ex_id)
actual = self.client.GET(url_receipt, auth_as='obama').body.decode('utf8')
assert self.bt_card.card_type in actual

# Remove once we've moved off balanced
def test_associate_balanced_card_should_fail(self):
card = balanced.Card(
number='4242424242424242',
expiration_year=2020,
expiration_month=12
).save()
customer = self.david.get_balanced_account()
self.hit('david', 'associate', 'balanced-cc', card.href, expected=400)

cards = customer.cards.all()
assert len(cards) == 0

def test_credit_card_page_loads_when_there_is_a_balanced_card(self):
expected = 'Your credit card is <em id="status">working'
actual = self.client.GET('/~janet/routes/credit-card.html', auth_as='janet').body.decode('utf8')
assert expected in actual

def test_credit_card_page_shows_details_for_balanced_cards(self):
response = self.client.GET('/~janet/routes/credit-card.html', auth_as='janet').body.decode('utf8')
assert self.card.number in response

def test_credit_card_page_shows_when_balanced_card_is_failing(self):
ExchangeRoute.from_network(self.janet, 'balanced-cc').update_error('Some error')
expected = 'Your credit card is <em id="status">failing'
actual = self.client.GET('/~janet/routes/credit-card.html', auth_as='janet').body.decode('utf8')
assert expected in actual

def test_receipt_page_loads_for_balanced_cards(self):
ex_id = self.make_exchange('balanced-cc', 113, 30, self.janet)
url_receipt = '/~janet/receipts/{}.html'.format(ex_id)
actual = self.client.GET(url_receipt, auth_as='janet').body.decode('utf8')
assert 'Visa' in actual
assert self.bt_card.card_type in actual
@@ -33,9 +33,11 @@ def test_api_returns_amount_and_totals(self):
first_data = json.loads(response1.body)
second_data = json.loads(response2.body)
assert first_data['amount'] == "1.50"
assert first_data['total_giving'] == "1.50"
assert second_data['amount'] == "3.00"
assert second_data['total_giving'] == "4.50"

# Bring these back when cached values are updated
# assert first_data['total_giving'] == "1.50"
# assert second_data['total_giving'] == "4.50"


def test_setting_subscription_out_of_range_gets_bad_amount(self):
@@ -42,14 +42,14 @@ def test_can_construct_from_id(self):
assert team.owner == 'hannibal'

def test_can_create_new_team(self):
self.make_participant('alice', claimed_time='now', email_address='', last_ach_result='')
self.make_participant('alice', claimed_time='now', email_address='', last_paypal_result='')
self.post_new(dict(self.valid_data))
team = self.db.one("SELECT * FROM teams")
assert team
assert team.owner == 'alice'

def test_all_fields_persist(self):
self.make_participant('alice', claimed_time='now', email_address='', last_ach_result='')
self.make_participant('alice', claimed_time='now', email_address='', last_paypal_result='')
self.post_new(dict(self.valid_data))
team = Team.from_slug('gratiteam')
assert team.name == 'Gratiteam'
@@ -76,23 +76,23 @@ def test_error_message_for_no_payout_route(self):
assert "You must attach a bank account or PayPal to apply for a new team." in r.body

def test_error_message_for_terms(self):
self.make_participant('alice', claimed_time='now', email_address='alice@example.com', last_ach_result='')
self.make_participant('alice', claimed_time='now', email_address='alice@example.com', last_paypal_result='')
data = dict(self.valid_data)
del data['agree_terms']
r = self.post_new(data, expected=400)
assert self.db.one("SELECT COUNT(*) FROM teams") == 0
assert "Please agree to the terms of service." in r.body

def test_error_message_for_missing_fields(self):
self.make_participant('alice', claimed_time='now', email_address='alice@example.com', last_ach_result='')
self.make_participant('alice', claimed_time='now', email_address='alice@example.com', last_paypal_result='')
data = dict(self.valid_data)
del data['name']
r = self.post_new(data, expected=400)
assert self.db.one("SELECT COUNT(*) FROM teams") == 0
assert "Please fill out the 'Team Name' field." in r.body

def test_error_message_for_slug_collision(self):
self.make_participant('alice', claimed_time='now', email_address='alice@example.com', last_ach_result='')
self.make_participant('alice', claimed_time='now', email_address='alice@example.com', last_paypal_result='')
self.post_new(dict(self.valid_data))
r = self.post_new(dict(self.valid_data), expected=400)
assert self.db.one("SELECT COUNT(*) FROM teams") == 1
@@ -111,7 +111,7 @@ def test_rejected_team_does_not_show_up_on_explore_teams(self):
assert 'The A Team' not in self.client.GET("/explore/teams/").body

def test_stripping_required_inputs(self):
self.make_participant('alice', claimed_time='now', email_address='alice@example.com', last_ach_result='')
self.make_participant('alice', claimed_time='now', email_address='alice@example.com', last_paypal_result='')
data = dict(self.valid_data)
data['name'] = " "
r = self.post_new(data, expected=400)