diff --git a/gratipay/billing/payday.py b/gratipay/billing/payday.py index 85afdbf353..d01e7db4d9 100644 --- a/gratipay/billing/payday.py +++ b/gratipay/billing/payday.py @@ -19,7 +19,7 @@ import aspen.utils from aspen import log from gratipay.billing.exchanges import ( - cancel_card_hold, capture_card_hold, create_card_hold, upcharge, + cancel_card_hold, capture_card_hold, create_card_hold, upcharge, MINIMUM_CHARGE, ) from gratipay.exceptions import NegativeBalance from gratipay.models import check_db @@ -220,17 +220,23 @@ def f(p): if p.old_balance < 0: amount -= p.old_balance if p.id in holds: - charge_amount = upcharge(amount)[0] - if holds[p.id].amount >= charge_amount: - return + if amount >= MINIMUM_CHARGE: + charge_amount = upcharge(amount)[0] + if holds[p.id].amount >= charge_amount: + return + else: + # The amount is too low, cancel the hold and make a new one + cancel_card_hold(holds.pop(p.id)) else: - # The amount is too low, cancel the hold and make a new one + # not up to minimum charge level. cancel the hold cancel_card_hold(holds.pop(p.id)) - hold, error = create_card_hold(self.db, p, amount) - if error: - return 1 - else: - holds[p.id] = hold + return + if amount >= MINIMUM_CHARGE: + hold, error = create_card_hold(self.db, p, amount) + if error: + return 1 + else: + holds[p.id] = hold threaded_map(f, participants) # Update the values of card_hold_ok in our temporary table @@ -341,6 +347,7 @@ def update_balances(cursor): SELECT *, (SELECT id FROM paydays WHERE extract(year from ts_end) = 1970) FROM payday_payments; """) + log("Updated the balances of %i participants." % len(participants)) diff --git a/gratipay/models/participant.py b/gratipay/models/participant.py index 7e0c3dc7c4..702fdcce24 100644 --- a/gratipay/models/participant.py +++ b/gratipay/models/participant.py @@ -854,10 +854,13 @@ def set_payment_instruction(self, team, amount, update_self=True, update_team=Tr """ args = dict(participant=self.username, team=team.slug, amount=amount) t = (cursor or self.db).one(NEW_PAYMENT_INSTRUCTION, args) + t_dict = t._asdict() if update_self: # Update giving amount of participant self.update_giving(cursor) + # Carry over any existing due + self.update_due(t_dict['team'], t_dict['id'], cursor) if update_team: # Update receiving amount of team team.update_receiving(cursor) @@ -890,6 +893,24 @@ def get_payment_instruction(self, team): """, (self.username, team.slug), back_as=dict, default=default) + def get_due(self, team): + """Given a slug, return a Decimal. + """ + if not isinstance(team, Team): + team, slug = Team.from_slug(team), team + if not team: + raise NoTeam(slug) + + return self.db.one("""\ + + SELECT due + FROM current_payment_instructions + WHERE participant = %s + AND team = %s + + """, (self.username, team.slug)) + + def get_giving_for_profile(self): """Return a list and a Decimal. """ @@ -984,6 +1005,31 @@ def update_giving(self, cursor=None): return updated + def update_due(self, team, id, cursor=None): + """Transfer existing due value to newly inserted record + """ + # Copy due to new record + (cursor or self.db).run(""" + UPDATE payment_instructions p + SET due = COALESCE(( + SELECT due + FROM payment_instructions s + WHERE participant=%(username)s + AND team = %(team)s + AND due > 0 + ), 0) + WHERE p.id = %(id)s + """, dict(username=self.username,team=team,id=id)) + + # Reset older due values to 0 + (cursor or self.db).run(""" + UPDATE payment_instructions p + SET due = 0 + WHERE participant = %(username)s + AND team = %(team)s + AND due > 0 + AND p.id != %(id)s + """, dict(username=self.username,team=team,id=id)) def update_taking(self, cursor=None): (cursor or self.db).run(""" diff --git a/sql/branch.sql b/sql/branch.sql new file mode 100644 index 0000000000..beec938b48 --- /dev/null +++ b/sql/branch.sql @@ -0,0 +1,27 @@ +BEGIN; + + ALTER TABLE payment_instructions ADD COLUMN due numeric(35,2) DEFAULT 0; + + -- Recreate the current_payment_instructions view to pick up due. + DROP VIEW current_payment_instructions; + CREATE VIEW current_payment_instructions AS + SELECT DISTINCT ON (participant, team) * + FROM payment_instructions + ORDER BY participant, team, mtime DESC; + + -- Allow updating is_funded and due via the current_payment_instructions view for convenience. + DROP FUNCTION update_payment_instruction(); + CREATE FUNCTION update_payment_instruction() RETURNS trigger AS $$ + BEGIN + UPDATE payment_instructions + SET is_funded = NEW.is_funded + , due = NEW.due + WHERE id = NEW.id; + RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + CREATE TRIGGER update_current_payment_instruction + INSTEAD OF UPDATE ON current_payment_instructions + FOR EACH ROW EXECUTE PROCEDURE update_payment_instruction(); +END; diff --git a/sql/payday.sql b/sql/payday.sql index 1853ce5f3b..cd1b581254 100644 --- a/sql/payday.sql +++ b/sql/payday.sql @@ -55,7 +55,7 @@ CREATE TABLE payday_payments_done AS DROP TABLE IF EXISTS payday_payment_instructions; CREATE TABLE payday_payment_instructions AS - SELECT participant, team, amount + SELECT s.id, participant, team, amount, due FROM ( SELECT DISTINCT ON (participant, team) * FROM payment_instructions WHERE mtime < %(ts_start)s @@ -77,11 +77,11 @@ CREATE INDEX ON payday_payment_instructions (team); ALTER TABLE payday_payment_instructions ADD COLUMN is_funded boolean; ALTER TABLE payday_participants ADD COLUMN giving_today numeric(35,2); -UPDATE payday_participants +UPDATE payday_participants pp SET giving_today = COALESCE(( - SELECT sum(amount) + SELECT sum(amount + due) FROM payday_payment_instructions - WHERE participant = username + WHERE participant = pp.username ), 0); DROP TABLE IF EXISTS payday_takes; @@ -108,6 +108,7 @@ RETURNS void AS $$ DECLARE participant_delta numeric; team_delta numeric; + payload json; BEGIN IF ($3 = 0) THEN RETURN; END IF; @@ -125,6 +126,17 @@ RETURNS void AS $$ UPDATE payday_teams SET balance = (balance + team_delta) WHERE slug = $2; + UPDATE current_payment_instructions + SET due = 0 + WHERE participant = $1 + AND team = $2 + AND due > 0; + IF ($4 = 'to-team') THEN + payload = '{"action":"pay","participant":"' || $1 || '", "team":"' + || $2 || '", "amount":' || $3 || '}'; + INSERT INTO events(type, payload) + VALUES ('payday',payload); + END IF; INSERT INTO payday_payments (participant, team, amount, direction) VALUES ( ( SELECT p.username @@ -141,6 +153,27 @@ RETURNS void AS $$ END; $$ LANGUAGE plpgsql; +-- Add payments that were not met on to due + +CREATE OR REPLACE FUNCTION park(text, text, numeric) +RETURNS void AS $$ + DECLARE payload json; + BEGIN + IF ($3 = 0) THEN RETURN; END IF; + + UPDATE current_payment_instructions + SET due = $3 + WHERE participant = $1 + AND team = $2; + + payload = '{"action":"due","participant":"' || $1 || '", "team":"' + || $2 || '", "due":' || $3 || '}'; + INSERT INTO events(type, payload) + VALUES ('payday',payload); + + END; +$$ LANGUAGE plpgsql; + -- Create a trigger to process payment_instructions @@ -153,9 +186,12 @@ CREATE OR REPLACE FUNCTION process_payment_instruction() RETURNS trigger AS $$ FROM payday_participants p WHERE username = NEW.participant ); - IF (NEW.amount <= participant.new_balance OR participant.card_hold_ok) THEN - EXECUTE pay(NEW.participant, NEW.team, NEW.amount, 'to-team'); + IF (NEW.amount + NEW.due <= participant.new_balance OR participant.card_hold_ok) THEN + EXECUTE pay(NEW.participant, NEW.team, NEW.amount + NEW.due, 'to-team'); RETURN NEW; + ELSE + EXECUTE park(NEW.participant, NEW.team, NEW.amount + NEW.due); + RETURN NULL; END IF; RETURN NULL; END; @@ -166,7 +202,6 @@ CREATE TRIGGER process_payment_instruction BEFORE UPDATE OF is_funded ON payday_ WHEN (NEW.is_funded IS true AND OLD.is_funded IS NOT true) EXECUTE PROCEDURE process_payment_instruction(); - -- Create a trigger to process takes CREATE OR REPLACE FUNCTION process_take() RETURNS trigger AS $$ diff --git a/tests/py/fixtures/TestPayday.yml b/tests/py/fixtures/TestPayday.yml index 0c18fd6cb6..4b858f3956 100644 --- a/tests/py/fixtures/TestPayday.yml +++ b/tests/py/fixtures/TestPayday.yml @@ -9,35 +9,35 @@ interactions: response: body: string: !!binary | - H4sIAKDak1UAA9xXS2/cNhC+51cYe6cl7cPeGLKMtEWBHtJL4hbtJaCo2RWzEqmQ1HrXvz5DUdJK - FuW4hwJFb9LMx+G8Zxg/nMri6ghKcynuF9F1uLgCwWTGxf5+8fj5V7JdPCTvYqOo0JQZRCXvrq5i - niWbJ3U4V3GAn5aiDTW1Tmhtcqn4M2Rx0JIs15wrSDQtIA6aT0tjtVJ415lwLQleCcnjp1/iYEq2 - YFrKWpgkCq/DMA7aP8soQbGcCkMoY5ZIUB9t0veH9e3zc/p1qW42X+PAh7KnpcpA4c+V4MX9wqga - FoFTTgE1kBFqrqzC94sMfw0vYZEsw2hDwlsSRp+j5V20vVuHf6Pa/YHmfF1l/+z85UDrHG0kam1/ - nL+jaL1ebZbrzuNI3XGlDRG0hJf6I7Og8zwmy4qKs4cDJeWFh/4EqebGJ6vKpfDRd/Q08WowNCtO - eVFgnl1MXD/9u8ZpowAwB7JMgdY+608GRGajMAspJKMFNz7xCvZYHz4XSSyFwiXz+3UU3sbBkNSp - jXmpzvNWObY9QWhR5XT5JtTqRyhRYzw4m8ZqEB40bVeLzFcoPUe3iU6VoucRE/056B8+IRqMKaAE - LMyUGpZ7MTmvqmG6+HLuf5kzr0TwP5Msw+i0zYvsOBSZ7uyhynDGK+qa7xJLYEwZ9IfByZgeNQGl - pCLo3EoKDV6fNLiBz8bo5CPOjFcBnYhxuF+AfnNSXsU09h+P05NTooXusek/0TNyvoIrD5wjepoR - caUkw9vQD92IpQ28kfTTnz9vPv6FLn0NNJYyViUK7Vid486cNJj6yYcKOUc77ucQjWuzjFtN0PlT - 2MTWo+TMBmiHgccTmHQpqKlHajvT8RY3xGdQhp6IWxe8LDhBWXUzOpWyACoWyY4W2q4qPaDbCdAK - wqjqZpSRBxBJesirbyXCmz/HSblI1mG03G5tIxXDFrROou02ioP2p60yFEqa1egPrilmS//fdZmK - KxfMUgqTJxFW0IQ4wZ6BKlw4luEI3FDbe9uBTGyPata7x0+XMX2hXrTMZdG42995eEn3QGpVJLkx - lb4LAqqxu+vrVFEubOG0GX+NLRdbwNk2/S8lYLZmXwq5l8ER7b+uxP4BxJErKSzgXlORpfKE60Ev - 391XC/6thjb42EYQzLF7qGS9Ytt1tMsYW7HV7uZmdbPZwSYKVwy2kDIMy+zRtmEpqCj2pd+lTW33 - 7Tg50MLk6AvcWMVByCcRBwOaA2WQcnPhu9+WVStMCczvfV3YnW+Aesnpp5NdYDktLtABrWuwZyWL - AaIjtIHRusb+jPNVHC6YEXXc7+WOWC4VzC7mH+yNU0bnKpnVrOnil9svNAc6gigl0dlhpth6fjsJ - xsXWvi9IzjEz1Xm0a/STukEACmo9bSsUV25klNUb1/Ae30t49WHTIObeJi5BNWaUkjk3Fa2LA1Zt - Q+n1GyxGWmJHg4RWHPWY0p2VwUsze0rrGtcbC+pftOpUM8Wr2UVswO87WbMGkgoHv8wI7jrEOtFT - +y+QqJYyXiyq/OIeOyAIzgLPDplx3WSelwdOiuyKdqYrzb1RsI9MdRsLxQ3NvkjRrpm87fluRuAr - U8BUKsb8aIfaDmBuHNlr5RNx0Zxw0Q1prbRbkjMw+EzrVqsxyx+bwYbtv36MmTzC3wiHk3UAdmjl - V8M+BzBTcbvzCawZ8yzQGJEZ263lVW3AlxrtaCFc4LpWN5/NOHV95YvtK3EwBxovPANDx3vRcOeZ - Bf1YVrMl/UhWv0qZHIcowfKyeQeo+k6OPTZqHsm77wAAAP//AwDGldve4xEAAA== + H4sIAEFl11UAA9xXS2/bOBC+51cEvjOy/EidQlFQoFhggd29tOmhl4KiRhZjiVRJyrH763coSrJk + UWn3sMBib9LMx+G8Zxg9ncri9ghKcykeF+HdcnELgsmUi/3j4vnzb2S3eIpvIqOo0JQZRMU3t7cR + T2P2cMq/b6IAPy1FG2pqHdPa5FLxH5BGQUuyXHOuINa0gChoPi2N1UrhXWfCtSR4JcTPnz5GwZRs + wbSUtTBxuLxbLqOg/bOMEhTLqTCEMmaJBPXRJnk4bN79+JG8rNT99iUKfCh7WqoUFP7cCl48Loyq + YRE45RRQAymh5tYq/LhI8dfwEhbxahluyXJHVuHn8N377er9MvyKavcHmvN1lf6z85cDrXO0kai1 + /XH+DsPNZr1d9R5HasaVNkTQEq71R2ZB53lMlhUVZw8HSsoLD/0VEs2NT1aVS+GjZ/Q08WowNCtK + eFFgnl1M3Lz+u8ZpowAwB9JUgdY+608GRGqjMAspJKMFNz7xCvZYHz4XSSyFwiXzwyZcvouCIalT + G/NSneetcmx7gtCiyunql1Drn6FEjfHgbBqrQXjQtKwWqa9Qeo5uE50qRc8jJvpz0D98QjQYU0AJ + WJgJNSz3YnJeVcN08eXc/zJn3ojgfyZZhtFpmxfJOBSp7uyhynDGK+qa7wpLYEwZ9IfByYgeNQGl + pCLo3EoKDV6fNLiBz8bo+E+cGW8COhHjcF+BfndS3sQ09h+P05NTooXusem/0jNyXsCVB84RPc2I + qFKS4W3oh27E0gbeSPq4/ePrJ+vSt0BjKWNVwqUdq3PcmZMGUz/+UCHnaMf9HKJxbZpyqwk6fwqb + 2HqUnNkAZRh4PIFJl4CaeqS2Mx1vcUN8BmXoibh1wcuCE5RVN6MTKQugYhFntNB2VekB3U6AVhBG + VTejjDyAiJNDXn0vEd78OU7CRbxZhqvdzjZSMWxBmzjc7cIoaH/aKkOhpFmNvnBNMVv6/67LVFy5 + YJZSmDwOMdwT4gR7Bqpw4VgtR+CG2t7bDmRie1Sz3j1/uozpC/WiZS6Lxt3+zsNLugdSqyLOjan0 + +yCgGru7vksU5cIWTpvxd9hysQWcbdP/VgJma/qtkHsZHNH+u0rsn0AcuZLCAh41FWkiT7ge9PLb + tqKgotg9/pI2Ad234+RAC5OjxrhXioOQryIKBjQHSiHh5sJ3vy2rVhg4zMJ9XdjNbIC65vQzxK6Z + nBYX6IDWtcGzksUA0RFa92ldYxfFKSgOF8yIOu7KMiOWSwWz6/MHe+OU0blKpjVreu3l9gvNgWrB + v9fQFhOS0fkcu7GKN2u224RZytiarbP7+/X9NoNtuFwz2EHCMM1njzrJRxClJDo9zBRbz28nwbjY + 2vcFyTlmpjqPdo1+UjcIQEFtDG2F4sqNjLL6xTW8x/cS3nzYNIi5t4lzqEYPKJlzU9G6OGDVNpRe + v8FipCV2NIhpxVGPKd1ZGVyb2VNa17jeWFD/olUnmilezS5iA37fyZo1kFQ4+GVKcNch1ome2r9C + olrKeLGo8tU9dkAQnAWeHTLluslpLw+cFNkl2UxXmnujYB+Z6jYWihuafZGiXTN52/PdjMBXpoCp + VIz50Q61DGBuHNlr5Stx0Zxw0Q1JrbRbklMw+EzrVqsxyx+bwYbtv36MmTzCfxEOJ+sA7NDKr4Z9 + DmCm4nbnE1gz5lmgMSIztlvLq9qALzXa0UK4wHWtbj6bcer6yjfbV6JgDjReeAaGjvei4c4zC/q5 + rGZL+pmsfpUyOQ5RguVl8w5Q9UyOPTZqHvHN3wAAAP//AwB/dUO/4xEAAA== headers: cache-control: ['max-age=0, private, must-revalidate'] content-encoding: [gzip] content-type: [application/xml; charset=utf-8] - etag: ['"71d1e845e57808f30699e79e7761faf2"'] + etag: ['"4e915be350b75c10e38db3c0270e5465"'] strict-transport-security: [max-age=31536000, max-age=31536000; includeSubDomains] transfer-encoding: [chunked] vary: [Accept-Encoding] @@ -46,81 +46,314 @@ interactions: body: 10.0 headers: {} method: PUT - uri: https://api.sandbox.braintreegateway.com:443/merchants/j9gwdfjdkxymhdgr/transactions/5wrkyp/submit_for_settlement + uri: https://api.sandbox.braintreegateway.com:443/merchants/j9gwdfjdkxymhdgr/transactions/c9xhq4/submit_for_settlement response: body: string: !!binary | - H4sIAKLak1UAA9xYS4/bNhC+51csfOdK8mPjBFoFaYsCPaSXJC3ay4KiRhZjiVRIymvn13coSrK0 - ojYLFAWC3qyZj+S8+M3Q8btzVd6cQGkuxf0qug1XNyCYzLg43K8+f/qV7FfvklexUVRoygyiklc3 - NzHPkt2jOl7qOMCfVqINNY1OdJNW3BjIHnKpHjQYU0IFwsRBB7BYc6kh0bSEOGh/WhlrlMKTL4Rr - SdAASD5//CUO5mILppVshEmi8DYM46D7sooKFCuoMIQyZoUErdMmfXPcvv72Lf2yVne7L3HgQ9nV - UmWg8ONG8PJ+ZVQDq8AZp4CiT4SaG2vw/SrDT8MrWCXrMNqR8DUJo0/R+m20f7sN/0azhwXt+qbO - Xr5+jeuvC7rgaCPRavvhoh9F2+1mt9728UdpzpU2RNAKntqPypIu65isaiouHg1UlJce+SOkmhvf - XnUhhU+e0/MsqsHYrTjlZYlVd3Vx+/jfOqeNAsAayDIFWvu8PxsQmc3CIqSUjJbc+LZXcMDb4guR - xKtQumJ+s43C13EwFvVmY12qy7JXTm1XEFrWBV2/CLX5Hko0mA/O5rkapQddyxuR+S7KoNFdoVOl - 6GWixHiO2MS3yZU1SEoNK7yYgtf1uFx8Nfe/rJlnMvjDFMs4Ox15kZxDmeneH6oMZ7ymjnzXeAWm - khE/jFbG9KQJKCUVweDWUmjwxqTFjWI2RScfsGc8C+i3mKb7Ceg3t8uzmNb/02m+ci600AOS/iO9 - oOYLuOuBfUTPKyKulWR4GsaBNqaQin+jLbzd6ac/f959+AtD+hxousvUlCi0bXVJu7DSYOkn72vU - nCDzrm4RbWizjFtLMPhz2MzXk+TMJijHxOMKLLoU1Dwije3peIpr4gsoQ8/EjQteFZyhqvsenUpZ - AhWrJKeltqPKAOhnAvSCMKr6HmXkEUSSHov6a4Xw9stpUi6SbRit93tLpGJMQdsk2u+jOOg+uluG - m5J2NPqDa4rVMnz3LFNz5ZJZSWGKJMIbNBPOsBegCgeOdTgBt9Lu3K4hE8tR7bD3+eO1TV+lVysL - Wbbh9jMPr+gBSKPKpDCm1m+DgGpkd32bKsqFvThdxd8i5SIFXCzpP1SA1Zo9lPIggxP6f1uLwzsQ - J66ksIB7TUWWyjOOB8P+7rxG8K8NdMlHGkEwR/ZQyXbD9tsozxjbsE1+d7e52+Wwi8INgz2kDNOy - uLQjLAU1RV76XdrSdr+dpgBamgJjgROrOAr5KOJgJHOgDFJurnr32akahSWB9X1oSjvzjVBPNUN3 - sgMsp+UVOpL1BHtRshwhekGXGK0b5Gfsr+J4xUykU76XObFaKpgdzN/bE+eKPlQya1jL4tfTrzIH - OoGoJNHZceGyDfquE0wvW/faIAXHylSXyawxdOoWAbhRF2l7Q3HkRkVVv3CMH/DDDt0rpidUy3TX - h02LWHqbuALVWFFKFtzUtCmPeGtbyWDfaDDSEhkNElpztGMud14Gczf/vefr5zx/yQPvB4jDIOlK - xPWIkvoHzibVTPF6cSAd6QdGb8dhUuMAJDOCMx+xIfVw4BMkmqWMF4smPznHNkqCPdEzS2dctzfQ - qwO3i+zJa4Gdl95qyKdz26ab4qRqX+bo18L9HfSuV+JrW8B8V8z5yTb3HGCpLdtj5SNx2ZxpMQxp - o7R7LGRg8Lnaj5hTlT83o5eG//gpZvZnxAvhcLYBwE6l/GbYZxFWKk65vg0bxjwPCczIgu/W87ox - 4CuNrsUSLnBsbdqf7Vjh+PXB8mscLIGmg9/I0el8OJ79FkHf36udFr+31zBSmgKHCYLXy9YdoOm5 - nEZsQh7Jq38AAAD//wMAL2sjl/kSAAA= + H4sIAENl11UAA9xYS2/bOBC+91cEvjOy/EidQFFRoFhggd29pN1DLwFFjSzWEqmSlGPn1+9QlGQp + opIAiwWKvVkzH4fznqGjT6eyuDqC0lyK+0V4vVxcgWAy5WJ/v/j29TeyW3yKP0RGUaEpM4iKP1xd + RTyN2e0p/7mJAvxpKdpQU+tY10nJjYH0MZPqUYMxBZQgTBS0AIs15wpiTQuIguanpbFaKbz5TLiW + BBWA+NvDlyiYki2YlrIWJg6X18tlFLRfllGCYjkVhlDGLJGgdtokt4fNx+fn5MdK3Wx/RIEPZU9L + lYLCjyvBi/uFUTUsAqecAoo2EWqurML3ixQ/DS9hEa+W4ZYsd2QVfg0/3m1Xd8vwO6rdH2jO11X6 + /vNrPH850DpHG4la2w/n/TDcbNbbVe9/pGZcaUMELeGl/sgs6DyPybKi4uzhQEl54aE/QaK58cmq + cil89IyeJl4NhmZFCS8KzLqLiZun/9Y4bRQA5kCaKtDaZ/3JgEhtFGYhhWS04MYnXsEeq8XnIoml + ULhkvt2Ey49RMCR1amNeqvO8VY5tTxBaVDldvQu1fgslaowHZ9NYDcKDpmW1SH2F0nN0m+hUKXoe + MdGfg27iE3LpGiShhuVeTM6rapguvpz7X+bMKxH8ZZJlGJ22eZGMQ5Hqzh6qDGe8oq75rrAExpRB + fxicjOhRE1BKKoLOraTQ4PVJgxv4bIyO/8SZ8SqgEzEO9wvQ707Kq5jG/uNxenJKtNA9Nv0nekbO + D3DlgXNETzMiqpRkeBv6gdYml4o/0wbeSPqy/eP7g3Xpa6CxlLEq4dKO1TnuzEmDqR9/rpBzhNR7 + ukE0rk1TbjVB509hE1uPkjMboAwDjycw6RJQU4/UdqbjLW6Iz6AMPRG3LnhZcIKy6mZ0ImUBVCzi + jBbario9oNsJ0ArCqOpmlJEHEHFyyKufJcKbL8dJuIg3y3C129lGKoYtaBOHu10YBe1HW2UolDSr + 0d9cU8yW/rvrMhVXLpilFCaPQwz3hDjBnoEqXDhWyxG4obb3tgOZ2B7VLHvfHi5j+kK9aJnLonG3 + v/Pwku6B1KqIc2MqfRcEVGN319eJolzYwmkz/hpbLraAs236jyVgtqaPhdzL4Ij2X1di/wnEkSsp + LOBeU5Em8oTrQS+/bSsKKord4y9pE9D9dpwcaGFy1Bj3SnEQ8klEwYDmQCkk3Fz47rNl1QoDh1m4 + rwu7mQ1QLzn9DLFrJqfFBTqgdW3wrGQxQHSE1n1a19hFcQqKwwUzoo67ssyI5VLB7Pr82d44ZXSu + kmnNml57uf1Cc6Ba8J81tMWEZHQ+x26s4s2a7TZhljK2Zuvs5mZ9s81gGy7XDHaQMEzz2aNO8hFE + KYlODzPF1vPbSTAutva1QXKOmanOo12jn9QNAlBQG0NbobhyI6Os3rnG9/heQvuK6Rqq7XSXh02D + mHubOIdq9ICSOTcVrYsDVm1D6fUbLEZaYkeDmFYc9ZjSnZXB1Mx/b/n6Ncvf88D7BfzQU9oUcTOi + oP6Fs040U7yaXUgH/L6jN+swqXABkinBnY9Yl3p64AskqqWMF4sqv7jHDkqCM9GzS6dcN7Xt5YGT + Irtim+nOc2817KdT3cZCcVO1L3O0a6Z+e76blfjaFjCVijE/2uGeAcyNZXutfCIumhMuuiGplXaP + hRQMPle7FXPM8sdm8NLwXz/GTP6MeCccTtYBOKmUXw37LMJMxS3XJ7BmzPOQwIjM2G4tr2oDvtRo + RyzhAtfWuvnZrBWuvz7a/hoFc6Dx4jcwdLwfDne/WdDbsppt8S1Z/UppclwmCJaXzTtA1TM59tio + ecQf/gEAAP//AwAk7QPi+RIAAA== headers: cache-control: ['max-age=0, private, must-revalidate'] content-encoding: [gzip] content-type: [application/xml; charset=utf-8] - etag: ['"fe03106fc8b6e028558d8c56b9af470e"'] + etag: ['"921b3efc34f3fdba32afee3246beb849"'] strict-transport-security: [max-age=31536000, max-age=31536000; includeSubDomains] transfer-encoding: [chunked] vary: [Accept-Encoding] status: {code: 200, message: OK} - request: - body: !!python/unicode 'authorized' + body: !!python/unicode 'bkhpqm10.6111443524salefalse2' headers: {} method: POST - uri: https://api.sandbox.braintreegateway.com:443/merchants/j9gwdfjdkxymhdgr/transactions/advanced_search_ids + uri: https://api.sandbox.braintreegateway.com:443/merchants/j9gwdfjdkxymhdgr/transactions response: body: string: !!binary | - H4sIAKTak1UAA7IpTk0sSs7QLUotLs0pKbbjUlCwKUhMT9UtzqxKVSipLEi1VcrMK0lNTy1SsjM1 - sNGHS4KVZqYUQxUlFhUlViqBBEHAJrMkNdfOtLwou7LARh/MASnXB6q347LRR7cUAAAA//8DAKYQ - ZieGAAAA + H4sIAEVl11UAA9xXTW/kNgy9768I5q54PB/JbOA42KIo0ALdyyZ76CWQZc5YGVtyJHkyk19fyrI9 + diwn6aFA0ZtNPlEkRZFP0d2xyC8OoDSX4nYWXs5nFyCYTLnY3c4e7n8jm9ld/CUyigpNmUFU/OXi + IuJpvF8/r5SJAvy0Em2oqXRMK5NJxV8hjYJGZLXmVEKsaQ5RUH9aGauUwr1OhGtJcEuIH378GgVj + sQXTQlbCxOH88iqMgubPKgpQLKPCEMqYFRL0R5vk6351/fqaPC3U1fopCnwou1qqFBT+XAie386M + qmAWOOcUUAMpoebCOnw7S/HX8AJm8WIersl8QxbhfXh9s17czFd/odvdgnp9Vab/bP15QZMcbSR6 + bX9cvsNwtVquF6s24yjdcqUNEbSAt/6jMqfTOiaLkoqTRwMF5blH/gKJ5sZnq8yk8Mm39DjKatAP + K0p4nmOdnUNcvfy7wWmjALAG0lSB1r7ojwZEak9hEpJLRnNufOYV7PB++FIk8Srkrpi/rsL5dRT0 + Ra3bWJfqNB2VU9sVhOZlRhefQi0/QokKz4Oz8Vn1jgdD21Yi9V2UTqObQqdK0dNAifns9Q+fEQ3G + 5FAAXsyEGpZ5MRkvy365+Gruf1kz75zgf6ZY+qfTNC+y5ZCnuo2HKsMZL6lrvgu8AkNJrz/0Vkb0 + oAkoJRXB5JZSaPDmpMb1cjZEx3/izHgX0JoYHvcb0O/OyruYOv7DYbxyLLTQHTb9F3pCzRO464Fz + RI8rIiqVZLgb5qEdsbSG15Z++ePq+z0OkXdBQytDV8L5fN5fPnbUozNY+vG3EjUHO+6nEHVq05Rb + TzD5Y9go1oPkzB7QFg8eV2DRJaDGGansTMdd3BCfQBl6JI4ueFVwhKJsZ3QiZQ5UzOItzbWlKh2g + 5QQYBWFUtTPKyD2IONln5XOB8PrPaRIu4tU8XGw2tpGKfgtaxeFmgySm+WluGRolNTX6yTXFaun+ + 2y5TcuUOs5DCZHGIN2gkHGFPQBUSjsV8AK6lzb7NQCa2R9X07uHHeUyfpWcvM5nX6fZ3Hl7QHZBK + 5XFmTKlvgoBq7O76MlGUC3txmoq/xJaLLeBkm/5jAVit6WMudzI4YPyXpdjdgThwJYUF3Goq0kQe + kR509pu2oqCk2D2+S1uA7ttpMqC5ydBj5JViL+SLiIKezIFSSLg5691vo6oUHhxW4a7KLTProd5q + uhliaSan+Rnak7Vt8KRk3kO0giZ9WlfYRXEKiv0ZM5AOu7LcEqulgln6/M3uOFa0qZJpxepee979 + LHOgSvDnCprLhGJMPsdurOLVkm1W4TZlbMmW26ur5dV6C+twvmSwgYRhmU8udZYPIApJdLqfuGyd + vpkEw8vWvC9IxrEy1WnANbpJXSMADTVnaG8oUm5UFOUnaXiH7yy8+7CpEVNvE5dQjRlQMuOmpFW+ + x1tbSzr/esRIS+xoENOSox9juYsyeBtmJ2lS43pjTv1Eq0o0U7ycJGI9fdfJahpIShz8MiXIdYhN + oufuv0GiW8p4sejym33sgCA4CzwcMuW6rmmvDpwV2RbZRFeaeqNgHxn7NjSKDM2+SDGuibrt9G5G + 4CtTwNgqnvnBDrUtwNQ4stvKF+JOc6TFNCSV0o4kp2DwmdZSq6HKfzY9hu3ffogZPcI/CYejTQB2 + aOV3wz4HsFKR3fkMVox5CDSeyETsNvKyMuArjWa0EC6QrlX1Zz1OXV95tH0lCqZAQ8LTC3TIi/qc + ZxL0sa2aJX1kq6NSJsMhSvB62boDdH0rhxkbNI/4y98AAAD//wMANf2w7uMRAAA= headers: cache-control: ['max-age=0, private, must-revalidate'] content-encoding: [gzip] content-type: [application/xml; charset=utf-8] - etag: ['"e1b3f49eca179caaa350c90bceb5b1e3"'] + etag: ['"4fe130aca1c632e3673daf450217b4a8"'] + strict-transport-security: [max-age=31536000, max-age=31536000; includeSubDomains] + transfer-encoding: [chunked] + vary: [Accept-Encoding] + status: {code: 201, message: Created} +- request: + body: 10.61 + headers: {} + method: PUT + uri: https://api.sandbox.braintreegateway.com:443/merchants/j9gwdfjdkxymhdgr/transactions/k5q4rt/submit_for_settlement + response: + body: + string: !!binary | + H4sIAEZl11UAA9xYS4/bNhC+51csfOfK8itOoNUiRVGgBZpLsj30sqCokcW1RCok5bXz6zsUJVla + UZsFigJBb9bMx+G8Z+jo/lwWNydQmktxtwhvl4sbEEymXBzuFg9ffyP7xX38LjKKCk2ZQVT87uYm + 4ml83H7bKBMF+NNStKGm1rGuk5IbA+ljJtWjBmMKKEEgrgVYrLlUEGtaQBQ0Py2N1UrhzRfCtSSo + AMQPX36NginZgmkpa2HicHm7C6Og/bKMEhTLqTCEMmaJBLXTJvlw3Lz//j15Wqnd9ikKfCh7WqoU + FH7cCF7cLYyqYRE45RRQtIlQc2MVvluk+Gl4CYt4tQy3ZLknq/Br+P7jdvVxufkb1e4PNOfrKn37 + +R2evx5onaONRK3th/N+GG426+1q0/kfqRlX2hBBS3ipPzILOs9jsqyouHg4UFJeeOjPkGhufLKq + XAofPaPniVeDoVlRwosCs+5q4ub5vzVOGwWAOZCmCrT2WX82IFIbhVlIIRktuPGJV3DAavG5SGIp + FC6ZP2zC5fsoGJI6tTEv1WXeKse2Jwgtqpyu3oRa/wglaowHZ9NYDcKDpmW1SH2F0nN0m+hUKXoZ + MdGfg27iE3LtGiShhuVeTM6rapguvpz7X+bMKxH8aZJlGJ22eZGMQ5Hqzh6qDGe8oq75rrAExpRB + fxicjOhJE1BKKoLOraTQ4PVJgxv4bIyO/8SZ8SqgEzEO9wvQ707Kq5jG/tNpenJKtNADNv1nekHO + E7jywDmipxkRVUoyvA39QGuTS8W/0wbeSPrlj93nrzhEXgWNpYxVCZfL5fD4VFEPz2Dqx58q5Jwg + 9Z5uEI1r05RbTdD5U9jE1pPkzAYow8DjCUy6BNTUI7Wd6XiLG+IzKEPPxK0LXhacoay6GZ1IWQAV + izijhbarSg/odgK0gjCquhll5BFEnBzz6luJ8ObLcRIu4s0yXO33tpGKYQvaxOF+j0tM+9FWGQol + zWr0F9cUs6X/7rpMxZULZimFyeMQK2hCnGAvQBUuHKvlCNxQ23vbgUxsj2qWvYcv1zF9pV61zGXR + uNvfeXhJD0BqVcS5MZX+GARUY3fXt4miXNjCaTP+FlsutoCLbfqPJWC2po+FPMjghPbfVuJwD+LE + lRQWcKepSBN5xvWgl9+2FQUVxe7xWdoEdL8dJwdamBw1xr1SHIV8FlEwoDlQCgk3V777bFm1wsBh + Fh7qwm5mA9RLTj9D7JrJaXGFDmhdG7woWQwQHaF1n9Y1dlGcguJ4xYyo464sM2K5VDC7Pn+yN04Z + natkWrOm115vv9IcqBb8Ww1tMSEZnc+xG6t4s2b7TZiljK3ZOtvt1rttBttwuWawh4Rhms8edZJP + IEpJdHqcKbae306CcbG1rw2Sc8xMdRntGv2kbhCAgtoY2grFlRsZZfXGNb7H9xLaV0zXUG2nuz5s + GsTc28Q5VKMHlMy5qWhdHLFqG0qv32Ax0hI7GsS04qjHlO6sDKZm/nvLd69Z/pYH3k/gh57Spoib + EQX1L5x1opni1exCOuD3Hb1Zh0mFC5BMCe58xLrU0wNfIFEtZbxYVPnFPXZQEpyJnl065bqpbS8P + nBTZFdtMd557q2E/neo2Foqbqn2Zo10z9dvz3azE17aAqVSM+ckO9wxgbizba+UzcdGccNENSa20 + eyykYPC52q2YY5Y/NoOXhv/6MWbyZ8Qb4XC2DsBJpfxq2GcRZipuuT6BNWOehwRGZMZ2a3lVG/Cl + RjtiCRe4ttbNz2atcP310fbXKJgDjRe/gaHj/XC4+82Cfiyr2RZ/JKtfKU2OywTB8rJ5B6h6Jsce + GzWP+N0/AAAA//8DAJrR5wn5EgAA + headers: + cache-control: ['max-age=0, private, must-revalidate'] + content-encoding: [gzip] + content-type: [application/xml; charset=utf-8] + etag: ['"1696567c11946b3cedf1f1aa7a6ef90a"'] strict-transport-security: [max-age=31536000, max-age=31536000; includeSubDomains] transfer-encoding: [chunked] vary: [Accept-Encoding] status: {code: 200, message: OK} - request: - body: !!python/unicode 'authorized5wrkyp' + body: !!python/unicode 'bkhpqm12.6711443524salefalse2' headers: {} method: POST - uri: https://api.sandbox.braintreegateway.com:443/merchants/j9gwdfjdkxymhdgr/transactions/advanced_search + uri: https://api.sandbox.braintreegateway.com:443/merchants/j9gwdfjdkxymhdgr/transactions + response: + body: + string: !!binary | + H4sIAEll11UAA9xXwXLbNhC9+ys8usMUJdlWMjQzaTud6SG5OO6hlwwIrEREJMAAoCz567sgSIo0 + QSc9dKbTG7n7sNhdLHYfkg+nsrg+gjZCyYdFfLNcXINkigu5f1g8ffmdbBcf0qvEaioNZRZR6dX1 + dSJ4ylf8e2aTCD+dxFhqa5PS2uZKixfgSdSKnNaeK0gNLSCJmk8nY7XWuNeZCKMIbgnp0+NvSTQV + OzAtVS1tGq9u7u6TqP1zihI0y6m0hDLmhAT9MTZ7d9jcv7xk31b67vZbEoVQbrXSHDT+XEtRPCys + rmEReec0UAucUHvtHH5YcPy1ooRFulrGt2S5Jav4S3z//nb1fvnuL3S7X9Csryv+z9ZfFrTJMVah + 1+7H5zuON5v17WrTZRylO6GNJZKW8Np/VBZ0XsdUWVF5DmigpKIIyJ8hM8KGbFW5kiH5jp4mWY2G + YSWZKAqss0uIm+d/NzhjNQDWAOcajAlFf7IguTuFWUihGC2EDZnXsMf7EUqRwqtQ+GJ+t4mXWMFD + Uec21qU+z0fl1W4FoUWV09VPodY/Qskaz0Ow6VkNjgdD29WShy5KrzFtoVOt6XmkxHwO+kfIiAFr + CygBL2ZGLcuDmFxU1bBcQjX3v6yZN07wP1Msw9NpmxfZCSi46eKh2gomKuqb7wqvwFgy6A+DlQk9 + GgJaK00wuZWSBoI5aXCDnI3R6SecGW8COhPj434F+sNbeRPTxH88TldOhQ66x6b/TM+o+Qb+euAc + MdOKSCqtGO6GeehGLG3gjaVfPn/69XGLKX0LNLYydiVeLpfD5VNHAzqLpZ9+rFBzdON+DtGklnPh + PMHkT2GTWI9KMHdAOzx4XIFFl4GeZqR2Mx138UN8BmXpiXi6EFTBCcqqm9GZUgVQuUh3tDCOqvSA + jhNgFIRR3c0oqw4g0+yQV99LhDd/XpMJmW6W8Wq7dY1UDlvQJo232ziJ2p/2lqFR0lCjP4WhWC39 + f9dlKqH9YZZK2hyZUBJNhBPsGahGwrFajsCNtN23HcjE9aiG3j09Xsb0RXrxMldFk+5w5xEl3QOp + dZHm1lbmfRRRg93d3GSaCukuTlvxN9hysQWcXdP/WgJWK/9aqL2Kjhj/TSX3H0AehVbSAR4MlTxT + J6QHvf22rWioKHaPz8oVoP/2mhxoYXP0GHmlPEj1LJNoIPMgDpmwF73/bVW1xoPDKtzXhWNmA9Rr + TT9DHM0UtLhAB7KuDZ61KgaITtCmz5gauyhOQXm4YEbScVdWO+K0VDJHnz+6HaeKLlWK16zptZfd + LzIPqqX4XkN7mVCMyRfYjXW6WbPtJt5xxtZsvbu7W9/d7uA2Xq4ZbCFjWOazS73lI8hSEcMPM5et + 17eTYHzZ2vcFyQVWpj6PuEY/qRsEoKH2DN0NRcqNirL6SRre43sLbz5sGsTc28Qn1GAGtMqFrWhd + HPDWNpLevwExMgo7GqS0EujHVO6jjF6H2Uva1PjeWNAw0aozw7SoZonYQN93soYGkgoHv+IEuQ5x + SQzc/VdIdEvbIBZdfrWPGxAEZ0GAQ3JhmpoO6sBbUV2RzXSluTcK9pGpb2OjyNDcixTjmqnbXu9n + BL4yJUyt4pkf3VDbAcyNI7eteib+NCdaTENWa+NJMgeLz7SOWo1V4bMZMOzw9mPM5BH+k3A4uQRg + h9ZhN9xzACsV2V3IYM1YgEDjiczE7iKvaguh0mhHCxES6VrdfDbj1PeVr66vJNEcaEx4BoGOedGQ + 88yCfmyrYUk/stVTKZvjECV4vVzdAbq+U+OMjZpHevU3AAAA//8DAJJzbEXjEQAA + headers: + cache-control: ['max-age=0, private, must-revalidate'] + content-encoding: [gzip] + content-type: [application/xml; charset=utf-8] + etag: ['"b7ad6f02284b9610452d6e76b6a7f3ab"'] + strict-transport-security: [max-age=31536000, max-age=31536000; includeSubDomains] + transfer-encoding: [chunked] + vary: [Accept-Encoding] + status: {code: 201, message: Created} +- request: + body: 12.67 + headers: {} + method: PUT + uri: https://api.sandbox.braintreegateway.com:443/merchants/j9gwdfjdkxymhdgr/transactions/d2dqbt/submit_for_settlement + response: + body: + string: !!binary | + H4sIAEtl11UAA9xYwW7bOBC99ysC3xlZtpM4haKgu4sF9tBe2vSwl4CiRhZjiVRJyrHz9TsUJVmK + qDTAYoFib9bM43BmOJx5dHR/LIuLAyjNpbhbhJfLxQUIJlMudneLh29/ku3iPv4QGUWFpswgKv5w + cRHxNE5X6Y/ERAH+tBJtqKl1rOuk5MZA+phJ9ajBmAJKEIhrARZrThXEmhYQBc1PK2O1UrjziXAt + CToA8cPXP6JgKrZgWspamDhcXV7fREH7ZRUlKJZTYQhlzAoJeqdNcrvf3Ly8JE8rdX31FAU+lF0t + VQoKPy4EL+4WRtWwCJxzCijGRKi5sA7fLVL8NLyERbxahldkuSWr8Ft48/Fq9XF5+ze63S9o1tdV + +u714RLXnxe0ydFGotf2w2U/DDeb9dVq0+UfpRlX2hBBS3jtPyoLOq9jsqyoOHk0UFJeeOTPkGhu + fLaqXAqfPKPHSVaDYVhRwosCq+4c4ub5vw1OGwWANZCmCrT2RX80IFJ7CrOQQjJacOMzr2CHt8WX + IolXoXDFfLsJl1jBQ1HnNtalOs1H5dR2BaFFldPVu1Drn6FEjefB2fSsBseDoWW1SH0XpdfottCp + UvQ0UmI+B93EZ+TcNUhCDcu9mJxX1bBcfDX3v6yZN07wlymW4em0zYtkHIpUd/FQZTjjFXXNd4VX + YCwZ9IfByogeNAGlpCKY3EoKDd6cNLhBzsbo+DPOjDcBnYnxcb8C/eWsvIlp4j8cpiunQgvdYdN/ + pifUPIG7HjhH9LQiokpJhrthHmhtcqn4C23gjaXfvnz+/esWU/oWaGxl7Eq4XC6Hy6eOenQGSz/+ + VKHmAKl3dYNoUpum3HqCyZ/CJrEeJGf2gDI8eFyBRZeAmmaktjMdd3FDfAZl6JE4uuBVwRHKqpvR + iZQFULGIM1poS1V6QMcJMArCqOpmlJF7EHGyz6sfJcKbL6dJuIg3y3C13dpGKoYtaBOH220YBe1H + e8vQKGmo0XeuKVZL/911mYord5ilFCZHJhQFE+EEewKqkHCsliNwI233bQcysT2qIXsPX89j+iw9 + e5nLokm3v/Pwku6A1KqIc2Mq/TEIqMburi8TRbmwF6et+EtsudgCTrbpP5aA1Zo+FnIngwPGf1mJ + 3T2IA1dSWMCdpiJN5BHpQW+/bSsKKord44u0Beh+O00OtDA5eoy8UuyFfBZRMJA5UAoJN2e9+2xV + tcKDwyrc1YVlZgPUa00/QyzN5LQ4Qweyrg2elCwGiE7Qpk/rGrsoTkGxP2NG0nFXlhmxWiqYpc+f + 7I5TRZcqmdas6bXn3c8yB6oF/1FDe5lQjMnn2I1VvFmz7SbMUsbWbJ1dX6+vrzK4CpdrBltIGJb5 + 7FJn+QCilESn+5nL1uvbSTC+bO1rg+QcK1OdRlyjn9QNAtBQe4b2hiLlRkVZvZPG9/jeQvuK6Rqq + 7XTnh02DmHubuIRqzICSOTcVrYs93tpG0vs3IEZaYkeDmFYc/ZjKXZTBNMx/HXnzAJmN/D0PvF8g + D72kLRE3IwrqJ5x1opni1SwhHej7jt7QYVIhAZIpQc5HbEo9PfAVEt1SxotFl1/tYwclwZno4dIp + 183d9urAWZHdZZvpznNvNeynU9/GRpGp2pc5xjVzf3u9m5X42hYwtYpnfrDDPQOYG8t2W/lM3GlO + tJiGpFbaPRZSMPhc7SjmWOU/m8FLw7/9GDP5M+KdcDjaBOCkUn437LMIKxVZrs9gzZjnIYEnMhO7 + jbyqDfhKox2xhAukrXXzs6EVrr8+2v4aBXOgMfEbBDrmh0PuNwv6ua2GLf7MVk8pTY5kguD1snUH + 6HomxxkbNY/4wz8AAAD//wMA3daZuvkSAAA= + headers: + cache-control: ['max-age=0, private, must-revalidate'] + content-encoding: [gzip] + content-type: [application/xml; charset=utf-8] + etag: ['"402b7c851b4b54dc0265856b84823af0"'] + strict-transport-security: [max-age=31536000, max-age=31536000; includeSubDomains] + transfer-encoding: [chunked] + vary: [Accept-Encoding] + status: {code: 200, message: OK} +- request: + body: !!python/unicode 'bkhpqm15.7611443524salefalse2' + headers: {} + method: POST + uri: https://api.sandbox.braintreegateway.com:443/merchants/j9gwdfjdkxymhdgr/transactions + response: + body: + string: !!binary | + H4sIAExl11UAA9xXS2/jNhC+51cEvjOy/EicQFGwQFGgBbqH7qZAewkoamwxlkgtSTl2fn2HoiRL + EZWkhwJFb9LMx+G8Zxg9HIv88gBKcynuZ+HVfHYJgsmUi9397PH7z2Qze4gvIqOo0JQZRMUXl5cR + T+PlRt2qTRTgp6VoQ02lY1qZTCr+CmkUNCTLNacSYk1ziIL609JYpRTedSJcS4JXQvz47acoGJMt + mBayEiYO11c311HQ/FlGAYplVBhCGbNEgvpok9zuVzevr8nzQl2vn6PAh7KnpUpB4c+l4Pn9zKgK + ZoFTTgE1kBJqLq3C97MUfw0vYBYv5uGazDdkEX4Pb+7Wi7tw8Req3R2oz1dl+s/Onw80ztFGotb2 + x/k7DFer5Xqxaj2O1C1X2hBBC3irPzJzOs1jsiipOHk4UFCee+gvkGhufLLKTAoffUuPI68GfbOi + hOc55tnZxNXLv2ucNgoAcyBNFWjts/5oQKQ2CpOQXDKac+MTr2CH9eFzkcRSyF0y367C+U0U9Emt + 2piX6jRtlWPbE4TmZUYXn0ItP0KJCuPB2ThWvfCgadtKpL5C6Ti6SXSqFD0NmOjPXv/wCdFgTA4F + YGEm1LDMi8l4WfbTxZdz/8uceSeC/5lk6UenaV5kyyFPdWsPVYYzXlLXfBdYAkNKrz/0Tkb0oAko + JRVB55ZSaPD6pMb1fDZEx7/hzHgX0IoYhvsN6Bcn5V1Mbf/hMD45JlroDpv+Cz0h5xlceeAc0eOM + iEolGd6GfmhHLK3htaTl4uuvv/+JLn0PNJQyVCWcz+f942NFPTyDqR9/KZFzsON+ClG7Nk251QSd + P4aNbD1IzmyAthh4PIFJl4Aae6SyMx1vcUN8AmXokbh1wcuCIxRlO6MTKXOgYhZvaa7tqtIB2p0A + rSCMqnZGGbkHESf7rPxRILz+c5yEi3g1DxebjW2kot+CVnG42YRR0Pw0VYZCSb0a/cE1xWzp/tsu + U3LlgllIYbI4xAoaEUfYE1CFC8diPgDX1ObeZiAT26Pq9e7x23lMn6lnLTOZ1+72dx5e0B2QSuVx + Zkyp74KAauzu+ipRlAtbOE3GX2HLxRZwsk3/qQDM1vQplzsZHND+q1LsHkAcuJLCAu41FWkij7ge + dPKbtqKgpNg9vkqbgO7bcTKguclQY9wrxV7IFxEFPZoDpZBwc+a734ZVKQwcZuGuyu1m1kO95XQz + xK6ZnOZnaI/WtsGTknkP0RIa92ldYRfFKSj2Z8yAOuzKcksslwpm1+cv9sYxo3WVTCtW99rz7Wea + A1WC/6igKSYko/M5dmMVr5Zsswq3KWNLttxeXy+v11tYh/Mlgw0kDNN88qiTfABRSKLT/USxdfxm + EgyLrXlfkIxjZqrTYNfoJnWNABTUxNBWKK7cyCjKT67hHb6T8O7DpkZMvU2cQzV6QMmMm5JW+R6r + tqZ0+vUWIy2xo0FMS456jOnOyuCtmR2lcY3rjTn1L1pVopni5eQi1uN3naxeA0mJg1+mBHcdYp3o + qf03SFRLGS8WVX5zjx0QBGeBZ4dMua5z2ssDJ0W2STbRlabeKNhHxroNheKGZl+kaNdE3nZ8NyPw + lSlgLBVjfrBDbQswNY7stfKFuGiOuOiGpFLaLckpGHymtavVkOWPTW/D9l8/xIwe4Z+Ew9E6ADu0 + 8qthnwOYqbjd+QRWjHkWaIzIhO3W8rIy4EuNZrQQLnBdq+rPepy6vvJk+0oUTIGGC0/P0OFe1N95 + JkEfy6q3pI9kdauUyXCIEiwvm3eAqm/l0GOD5hFf/A0AAP//AwA1Er+c4xEAAA== + headers: + cache-control: ['max-age=0, private, must-revalidate'] + content-encoding: [gzip] + content-type: [application/xml; charset=utf-8] + etag: ['"88a31c27144fe72295f3982f930dbfa1"'] + strict-transport-security: [max-age=31536000, max-age=31536000; includeSubDomains] + transfer-encoding: [chunked] + vary: [Accept-Encoding] + status: {code: 201, message: Created} +- request: + body: 15.76 + headers: {} + method: PUT + uri: https://api.sandbox.braintreegateway.com:443/merchants/j9gwdfjdkxymhdgr/transactions/38r9r8/submit_for_settlement + response: + body: + string: !!binary | + H4sIAE1l11UAA9xY3W/bNhB/z18R+J2R5Y/ELRQVBYYBG7A+rE2B7SWgqJPFWCJVknLs/vU7ipIs + RVQaYBhQ7M26+/F433d09OFUFtdHUJpLcb8Ib5aLaxBMplzs7xcPX34lu8WH+CoyigpNmUFUfHV9 + HfE0Xu/UO7WLAvxpKdpQU+tY10nJjYH0MZPqUYMxBZQgTBS0AIs15wpiTQuIguanpbFaKbz5TLiW + BBWA+OHzL1EwJVswLWUtTBxub+5uo6D9sowSFMupMIQyZokEtdMmeXfY3H3/njyt1O32KQp8KHta + qhQUflwLXtwvjKphETjlFFC0iVBzbRW+X6T4aXgJi3i1DLdkuSOr8Et49367eh+u/ka1+wPN+bpK + 335+jecvB1rnaCNRa/vhvB+Gm816u9p0/kdqxpU2RNASXuqPzILO85gsKyrOHg6UlBce+jMkmhuf + rCqXwkfP6Gni1WBoVpTwosCsu5i4ef5vjdNGAWAOpKkCrX3WnwyI1EZhFlJIRgtufOIV7LFafC6S + WAqFS+Z3m3B5FwVDUqc25qU6z1vl2PYEoUWV09WbUOsfoUSN8eBsGqtBeNC0rBapr1B6jm4TnSpF + zyMm+nPQTXxCLl2DJNSw3IvJeVUN08WXc//LnHklgj9Nsgyj0zYvknEoUt3ZQ5XhjFfUNd8VlsCY + MugPg5MRPWoCSklF0LmVFBq8PmlwA5+N0fEfODNeBXQixuF+AfrNSXkV09h/PE5PTokWusem/0zP + yHkCVx44R/Q0I6JKSYa3oR9obXKp+HfawBtJ69Wn3//8C136GmgsZaxKuFwuh8eninp4BlM//lgh + 5wip93SDaFybptxqgs6fwia2HiVnNkAZBh5PYNIloKYeqe1Mx1vcEJ9BGXoibl3wsuAEZdXN6ETK + AqhYxBkttF1VekC3E6AVhFHVzSgjDyDi5JBX30qEN1+Ok3ARb5bharezjVQMW9AmDne7MAraj7bK + UChpVqOvXFPMlv676zIVVy6YpRQmj0OsoAlxgj0DVbhwrJYjcENt720HMrE9qln2Hj5fxvSFetEy + l0Xjbn/n4SXdA6lVEefGVPp9EFCN3V3fJIpyYQunzfgbbLnYAs626T+WgNmaPhZyL4Mj2n9Tif0H + EEeupLCAe01FmsgTrge9/LatKKgodo9P0iag++04OdDC5Kgx7pXiIOSziIIBzYFSSLi58N1ny6oV + Bg6zcF8XdjMboF5y+hli10xOiwt0QOva4FnJYoDoCK37tK6xi+IUFIcLZkQdd2WZEculgtn1+aO9 + ccroXCXTmjW99nL7heZAteDfamiLCcnofI7dWMWbNdttwixlbM3W2e3t+nabwTZcrhnsIGGY5rNH + neQjiFISnR5miq3nt5NgXGzta4PkHDNTnUe7Rj+pGwSgoDaGtkJx5UZGWb1xje/xvYT2FdM1VNvp + Lg+bBjH3NnEO1egBJXNuKloXB6zahtLrN1iMtMSOBjGtOOoxpTsrg6mZ/97y9WuWv+WB9xP4oae0 + KeJmREH9C2edaKZ4NbuQDvh9R2/WYVLhAiRTgjsfsS719MAXSFRLGS8WVX5xjx2UBGeiZ5dOuW5q + 28sDJ0V2xTbTnefeathPp7qNheKmal/maNdM/fZ8NyvxtS1gKhVjfrTDPQOYG8v2WvlMXDQnXHRD + UivtHgspGHyudivmmOWPzeCl4b9+jJn8GfFGOJysA3BSKb8a9lmEmYpbrk9gzZjnIYERmbHdWl7V + Bnyp0Y5YwgWurXXzs1krXH99tP01CuZA48VvYOh4PxzufrOgH8tqtsUfyepXSpPjMkGwvGzeAaqe + ybHHRs0jvvoHAAD//wMAI/OhsvkSAAA= + headers: + cache-control: ['max-age=0, private, must-revalidate'] + content-encoding: [gzip] + content-type: [application/xml; charset=utf-8] + etag: ['"664739933d0de13a6dda2c493e4aa259"'] + strict-transport-security: [max-age=31536000, max-age=31536000; includeSubDomains] + transfer-encoding: [chunked] + vary: [Accept-Encoding] + status: {code: 200, message: OK} +- request: + body: !!python/unicode 'authorized' + headers: {} + method: POST + uri: https://api.sandbox.braintreegateway.com:443/merchants/j9gwdfjdkxymhdgr/transactions/advanced_search_ids response: body: string: !!binary | - H4sIAKXak1UAA3TOwQrCMAwG4FcZuddtB8FD1918An2A2oVSaNORZkN9essQkaHH5P9+Ej3eU2xW - 5BIyDdAfOmiQXJ4C+QGul7M6wWi0Y5yCKGd5UsKWinVSC6WRx4wDuBwjbhuodmFGEjVbj4qWdEN+ - s0CCHhlMr9sfyuhtKOGJ+8Kx0+0nNFqy2KiCYCp7WeFXauqhP6+bFwAAAP//AwCNtt0D+wAAAA== + H4sIAE9l11UAA7IpTk0sSs7QLUotLs0pKbbjUlCwKUhMT9UtzqxKVSipLEi1VcrMK0lNTy1SsjM1 + sNGHS4KVZqYUQxUlFhUlViqBBfWBonZcNvroRgMAAAD//wMA9crnoGwAAAA= headers: cache-control: ['max-age=0, private, must-revalidate'] content-encoding: [gzip] content-type: [application/xml; charset=utf-8] - etag: ['"7c74627404e838039762954c15067a74"'] + etag: ['"bef84d7df48ddc2ffdffbc7459d3f5cc"'] strict-transport-security: [max-age=31536000, max-age=31536000; includeSubDomains] transfer-encoding: [chunked] vary: [Accept-Encoding] diff --git a/tests/py/test_billing_payday.py b/tests/py/test_billing_payday.py index b8bf61884a..878f63fd61 100644 --- a/tests/py/test_billing_payday.py +++ b/tests/py/test_billing_payday.py @@ -8,7 +8,7 @@ import mock import pytest -from gratipay.billing.exchanges import create_card_hold +from gratipay.billing.exchanges import create_card_hold, MINIMUM_CHARGE from gratipay.billing.payday import NoPayday, Payday from gratipay.exceptions import NegativeBalance from gratipay.models.participant import Participant @@ -19,9 +19,9 @@ class TestPayday(BillingHarness): - def test_payday_moves_money(self): + def test_payday_moves_money_above_min_charge(self): Enterprise = self.make_team(is_approved=True) - self.obama.set_payment_instruction(Enterprise, '6.00') # under $10! + self.obama.set_payment_instruction(Enterprise, MINIMUM_CHARGE) # must be >= MINIMUM_CHARGE with mock.patch.object(Payday, 'fetch_card_holds') as fch: fch.return_value = {} Payday.start().run() @@ -29,8 +29,90 @@ def test_payday_moves_money(self): obama = Participant.from_username('obama') picard = Participant.from_username('picard') - assert picard.balance == D('6.00') - assert obama.balance == D('3.41') + assert picard.balance == D(MINIMUM_CHARGE) + assert obama.balance == D('0.00') + assert obama.get_due('TheEnterprise') == D('0.00') + + @mock.patch.object(Payday, 'fetch_card_holds') + def test_payday_moves_money_cumulative_above_min_charge(self, fch): + Enterprise = self.make_team(is_approved=True) + self.obama.set_payment_instruction(Enterprise, '5.00') # < MINIMUM_CHARGE + # simulate already due amount + self.db.run(""" + + UPDATE payment_instructions ppi + SET due = '5.00' + WHERE ppi.participant = 'obama' + AND ppi.team = 'TheEnterprise' + + """) + + fch.return_value = {} + Payday.start().run() + + obama = Participant.from_username('obama') + picard = Participant.from_username('picard') + + assert picard.balance == D('10.00') + assert obama.balance == D('0.00') + assert obama.get_due('TheEnterprise') == D('0.00') + + @mock.patch.object(Payday, 'fetch_card_holds') + def test_payday_preserves_due_until_charged(self, fch): + Enterprise = self.make_team(is_approved=True) + self.obama.set_payment_instruction(Enterprise, '2.00') # < MINIMUM_CHARGE + + fch.return_value = {} + Payday.start().run() # payday 0 + + assert self.obama.get_due('TheEnterprise') == D('2.00') + + self.obama.set_payment_instruction(Enterprise, '3.00') # < MINIMUM_CHARGE + self.obama.set_payment_instruction(Enterprise, '2.50') # cumulatively still < MINIMUM_CHARGE + + fch.return_value = {} + Payday.start().run() # payday 1 + + assert self.obama.get_due('TheEnterprise') == D('4.50') + + fch.return_value = {} + Payday.start().run() # payday 2 + + assert self.obama.get_due('TheEnterprise') == D('7.00') + + self.obama.set_payment_instruction(Enterprise, '1.00') # cumulatively still < MINIMUM_CHARGE + + fch.return_value = {} + Payday.start().run() # payday 3 + + assert self.obama.get_due('TheEnterprise') == D('8.00') + + self.obama.set_payment_instruction(Enterprise, '4.00') # cumulatively > MINIMUM_CHARGE + + fch.return_value = {} + Payday.start().run() # payday 4 + + obama = Participant.from_username('obama') + picard = Participant.from_username('picard') + + assert picard.balance == D('12.00') + assert obama.balance == D('0.00') + assert obama.get_due('TheEnterprise') == D('0.00') + + @mock.patch.object(Payday, 'fetch_card_holds') + def test_payday_does_not_move_money_below_min_charge(self, fch): + Enterprise = self.make_team(is_approved=True) + self.obama.set_payment_instruction(Enterprise, '6.00') # not enough to reach MINIMUM_CHARGE + fch.return_value = {} + Payday.start().run() + + obama = Participant.from_username('obama') + picard = Participant.from_username('picard') + + assert picard.balance == D('0.00') + assert obama.balance == D('0.00') + assert obama.get_due('TheEnterprise') == D('6.00') + @mock.patch.object(Payday, 'fetch_card_holds') def test_payday_doesnt_move_money_from_a_suspicious_account(self, fch): @@ -40,7 +122,7 @@ def test_payday_doesnt_move_money_from_a_suspicious_account(self, fch): WHERE username = 'obama' """) team = self.make_team(owner=self.homer, is_approved=True) - self.obama.set_payment_instruction(team, '6.00') # under $10! + self.obama.set_payment_instruction(team, MINIMUM_CHARGE) # >= MINIMUM_CHARGE! fch.return_value = {} Payday.start().run() @@ -58,7 +140,7 @@ def test_payday_doesnt_move_money_to_a_suspicious_account(self, fch): WHERE username = 'homer' """) team = self.make_team(owner=self.homer, is_approved=True) - self.obama.set_payment_instruction(team, '6.00') # under $10! + self.obama.set_payment_instruction(team, MINIMUM_CHARGE) # >= MINIMUM_CHARGE! fch.return_value = {} Payday.start().run() @@ -165,10 +247,10 @@ def create_card_holds(self): def test_payin_pays_in(self, sale, sfs, fch): fch.return_value = {} team = self.make_team('Gratiteam', is_approved=True) - self.obama.set_payment_instruction(team, 1) + self.obama.set_payment_instruction(team, MINIMUM_CHARGE) # >= MINIMUM_CHARGE txn_attrs = { - 'amount': 1, + 'amount': MINIMUM_CHARGE, 'tax_amount': 0, 'status': 'authorized', 'custom_fields': {'participant_id': self.obama.id}, @@ -186,7 +268,7 @@ def test_payin_pays_in(self, sale, sfs, fch): Payday.start().payin() payments = self.db.all("SELECT amount, direction FROM payments") - assert payments == [(1, 'to-team'), (1, 'to-participant')] + assert payments == [(MINIMUM_CHARGE, 'to-team'), (MINIMUM_CHARGE, 'to-participant')] @mock.patch('braintree.Transaction.sale') def test_payin_doesnt_try_failed_cards(self, sale): diff --git a/tests/py/test_history.py b/tests/py/test_history.py index 10e7c0a875..0cb8fdac16 100644 --- a/tests/py/test_history.py +++ b/tests/py/test_history.py @@ -42,7 +42,7 @@ def test_iter_payday_events(self): Payday().start().run() Enterprise = self.make_team(is_approved=True) - self.obama.set_payment_instruction(Enterprise, '6.00') # under $10! + self.obama.set_payment_instruction(Enterprise, '10.00') # >= MINIMUM_CHARGE! for i in range(2): with patch.object(Payday, 'fetch_card_holds') as fch: fch.return_value = {} @@ -57,11 +57,12 @@ def test_iter_payday_events(self): SET timestamp = "timestamp" - interval '1 week'; """) + obama = Participant.from_username('obama') picard = Participant.from_username('picard') - assert obama.balance == D('6.82') - assert picard.balance == D('12.00') + assert obama.balance == D('0.00') + assert picard.balance == D('20.00') Payday().start() # to demonstrate that we ignore any open payday? @@ -69,15 +70,15 @@ def test_iter_payday_events(self): assert len(events) == 7 assert events[0]['kind'] == 'totals' assert events[0]['given'] == 0 - assert events[0]['received'] == 12 + assert events[0]['received'] == 20 assert events[1]['kind'] == 'day-open' assert events[1]['payday_number'] == 2 - assert events[2]['balance'] == 12 + assert events[2]['balance'] == 20 assert events[-1]['kind'] == 'day-close' assert events[-1]['balance'] == 0 events = list(iter_payday_events(self.db, obama)) - assert events[0]['given'] == 12 + assert events[0]['given'] == 20 assert len(events) == 11 def test_iter_payday_events_with_failed_exchanges(self):