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

Commit

Permalink
Migrate transfer_tips to process_subscriptions
Browse files Browse the repository at this point in the history
Next up is implementing process_draws (for owners), then, probalby later
on, process_takes.
  • Loading branch information
chadwhitacre committed May 14, 2015
1 parent d9cd9aa commit d900c39
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 71 deletions.
15 changes: 8 additions & 7 deletions gratipay/billing/payday.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class Payday(object):
payin
prepare
create_card_holds
transfer_tips
process_subscriptions
transfer_takes
settle_card_holds
update_balances
Expand Down Expand Up @@ -159,7 +159,7 @@ def payin(self):
with self.db.get_cursor() as cursor:
self.prepare(cursor, self.ts_start)
holds = self.create_card_holds(cursor)
self.transfer_tips(cursor)
self.process_subscriptions(cursor)
self.transfer_takes(cursor, self.ts_start)
transfers = cursor.all("""
SELECT * FROM transfers WHERE "timestamp" > %s
Expand Down Expand Up @@ -272,10 +272,10 @@ def f(p):


@staticmethod
def transfer_tips(cursor):
"""Trigger the process_tip function for each row in payday_tips.
def process_subscriptions(cursor):
"""Trigger the process_subscription function for each row in payday_subscriptions.
"""
cursor.run("UPDATE payday_tips SET is_funded=true;")
cursor.run("UPDATE payday_subscriptions SET is_funded=true;")


@staticmethod
Expand Down Expand Up @@ -348,8 +348,9 @@ def update_balances(cursor):
log(p)
raise NegativeBalance()
cursor.run("""
INSERT INTO transfers (timestamp, tipper, tippee, amount, context)
SELECT * FROM payday_transfers;
INSERT INTO payments (timestamp, participant, team, amount, direction, payday)
SELECT *, (SELECT id FROM paydays WHERE extract(year from ts_end) = 1970)
FROM payday_payments;
""")
log("Updated the balances of %i participants." % len(participants))

Expand Down
112 changes: 56 additions & 56 deletions sql/payday.sql
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ CREATE UNIQUE INDEX ON payday_participants (id);
CREATE UNIQUE INDEX ON payday_participants (username);

CREATE TEMPORARY TABLE payday_teams ON COMMIT DROP AS
SELECT slug
SELECT t.id
, slug
, owner
, 0 AS balance
, 0::numeric(35, 2) AS balance
FROM teams t
JOIN participants p
ON t.owner = p.username
Expand All @@ -34,115 +35,114 @@ CREATE TEMPORARY TABLE payday_teams ON COMMIT DROP AS
AND p.is_closed IS NOT true
AND p.is_suspicious IS NOT true
AND (SELECT count(*)
FROM exchange_routes
WHERE participant = p.id
FROM exchange_routes er
WHERE er.participant = p.id
AND network IN ('balanced-ba', 'paypal')
AND error != ''
AND error = ''
) > 0
;

CREATE TEMPORARY TABLE payday_transfers_done ON COMMIT DROP AS
CREATE TEMPORARY TABLE payday_payments_done ON COMMIT DROP AS
SELECT *
FROM transfers t
WHERE t.timestamp > %(ts_start)s;
FROM payments p
WHERE p.timestamp > %(ts_start)s;

CREATE TEMPORARY TABLE payday_tips ON COMMIT DROP AS
SELECT tipper, tippee, amount
FROM ( SELECT DISTINCT ON (tipper, tippee) *
FROM tips
CREATE TEMPORARY TABLE payday_subscriptions ON COMMIT DROP AS
SELECT subscriber, team, amount
FROM ( SELECT DISTINCT ON (subscriber, team) *
FROM subscriptions
WHERE mtime < %(ts_start)s
ORDER BY tipper, tippee, mtime DESC
) t
JOIN payday_participants p ON p.username = t.tipper
JOIN payday_participants p2 ON p2.username = t.tippee
WHERE t.amount > 0
ORDER BY subscriber, team, mtime DESC
) s
JOIN payday_participants p ON p.username = s.subscriber
WHERE s.amount > 0
AND ( SELECT id
FROM payday_transfers_done t2
WHERE t.tipper = t2.tipper
AND t.tippee = t2.tippee
AND context = 'tip'
FROM payday_payments_done done
WHERE s.subscriber = done.participant
AND s.team = done.team
AND direction = 'to-team'
) IS NULL
ORDER BY p.claimed_time ASC, t.ctime ASC;
ORDER BY p.claimed_time ASC, s.ctime ASC;

CREATE INDEX ON payday_tips (tipper);
CREATE INDEX ON payday_tips (tippee);
ALTER TABLE payday_tips ADD COLUMN is_funded boolean;
CREATE INDEX ON payday_subscriptions (subscriber);
CREATE INDEX ON payday_subscriptions (team);
ALTER TABLE payday_subscriptions ADD COLUMN is_funded boolean;

ALTER TABLE payday_participants ADD COLUMN giving_today numeric(35,2);
UPDATE payday_participants
SET giving_today = COALESCE((
SELECT sum(amount)
FROM payday_tips
WHERE tipper = username
FROM payday_subscriptions
WHERE subscriber = username
), 0);

CREATE TEMPORARY TABLE payday_takes
( team text
, member text
, amount numeric(35,2)
) ON COMMIT DROP;
) ON COMMIT DROP;

CREATE TEMPORARY TABLE payday_transfers
( timestamp timestamptz DEFAULT now()
, tipper text
, tippee text
, amount numeric(35,2)
, context context_type
) ON COMMIT DROP;
CREATE TEMPORARY TABLE payday_payments
( timestamp timestamptz DEFAULT now()
, participant text NOT NULL
, team text NOT NULL
, amount numeric(35,2) NOT NULL
, direction payment_direction NOT NULL
) ON COMMIT DROP;


-- Prepare a statement that makes and records a transfer
-- Prepare a statement that makes and records a payment

CREATE OR REPLACE FUNCTION transfer(text, text, numeric, context_type)
CREATE OR REPLACE FUNCTION pay(text, text, numeric, payment_direction)

This comment has been minimized.

Copy link
@rohitpaulk

rohitpaulk May 14, 2015

Contributor

If we have payment_direction as a column, it'd probably make sense to constrain amount to >0 and use payment_direction to decide where to deduct/add funds. Not saying that we should block payday on this, just something I might correct myself later :)

This comment has been minimized.

Copy link
@chadwhitacre

chadwhitacre May 15, 2015

Author Contributor

Good call. :)

RETURNS void AS $$
BEGIN
IF ($3 = 0) THEN RETURN; END IF;
UPDATE payday_participants
SET new_balance = (new_balance - $3)
WHERE username = $1;
UPDATE payday_participants
SET new_balance = (new_balance + $3)
WHERE username = $2;
INSERT INTO payday_transfers
(tipper, tippee, amount, context)
UPDATE payday_teams
SET balance = (balance + $3)
WHERE slug = $2;
INSERT INTO payday_payments
(participant, team, amount, direction)
VALUES ( ( SELECT p.username
FROM participants p
JOIN payday_participants p2 ON p.id = p2.id
WHERE p2.username = $1 )
, ( SELECT p.username
FROM participants p
JOIN payday_participants p2 ON p.id = p2.id
WHERE p2.username = $2 )
, ( SELECT t.slug
FROM teams t
JOIN payday_teams t2 ON t.id = t2.id
WHERE t2.slug = $2 )
, $3
, $4
);
);
END;
$$ LANGUAGE plpgsql;


-- Create a trigger to process tips
-- Create a trigger to process subscriptions

CREATE OR REPLACE FUNCTION process_tip() RETURNS trigger AS $$
CREATE OR REPLACE FUNCTION process_subscription() RETURNS trigger AS $$
DECLARE
tipper payday_participants;
subscriber payday_participants;
BEGIN
tipper := (
subscriber := (
SELECT p.*::payday_participants
FROM payday_participants p
WHERE username = NEW.tipper
WHERE username = NEW.subscriber
);
IF (NEW.amount <= tipper.new_balance OR tipper.card_hold_ok) THEN
EXECUTE transfer(NEW.tipper, NEW.tippee, NEW.amount, 'tip');
IF (NEW.amount <= subscriber.new_balance OR subscriber.card_hold_ok) THEN
EXECUTE pay(NEW.subscriber, NEW.team, NEW.amount, 'to-team');
RETURN NEW;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER process_tip BEFORE UPDATE OF is_funded ON payday_tips
CREATE TRIGGER process_subscription BEFORE UPDATE OF is_funded ON payday_subscriptions
FOR EACH ROW
WHEN (NEW.is_funded IS true AND OLD.is_funded IS NOT true)
EXECUTE PROCEDURE process_tip();
EXECUTE PROCEDURE process_subscription();


-- Create a trigger to process takes
Expand Down
39 changes: 31 additions & 8 deletions tests/py/test_billing_payday.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import balanced
import mock
import pytest

from gratipay.billing.exchanges import create_card_hold
from gratipay.billing.payday import NoPayday, Payday
Expand All @@ -17,6 +18,7 @@

class TestPayday(BillingHarness):

@pytest.mark.xfail(reason="haven't migrated transfer_takes yet")
@mock.patch.object(Payday, 'fetch_card_holds')
def test_payday_moves_money(self, fch):
self.janet.set_tip_to(self.homer, '6.00') # under $10!
Expand All @@ -29,6 +31,7 @@ def test_payday_moves_money(self, fch):
assert homer.balance == D('6.00')
assert janet.balance == D('3.41')

@pytest.mark.xfail(reason="haven't migrated transfer_takes yet")
@mock.patch.object(Payday, 'fetch_card_holds')
def test_payday_doesnt_move_money_from_a_suspicious_account(self, fch):
self.db.run("""
Expand All @@ -46,6 +49,7 @@ def test_payday_doesnt_move_money_from_a_suspicious_account(self, fch):
assert janet.balance == D('0.00')
assert homer.balance == D('0.00')

@pytest.mark.xfail(reason="haven't migrated transfer_takes yet")
@mock.patch.object(Payday, 'fetch_card_holds')
def test_payday_doesnt_move_money_to_a_suspicious_account(self, fch):
self.db.run("""
Expand All @@ -63,6 +67,7 @@ def test_payday_doesnt_move_money_to_a_suspicious_account(self, fch):
assert janet.balance == D('0.00')
assert homer.balance == D('0.00')

@pytest.mark.xfail(reason="haven't migrated transfer_takes yet")
@mock.patch.object(Payday, 'fetch_card_holds')
def test_payday_moves_money_with_balanced(self, fch):
self.janet.set_tip_to(self.homer, '15.00')
Expand All @@ -88,6 +93,7 @@ def test_payday_moves_money_with_balanced(self, fch):
assert debit.amount == 1576 # base amount + fee
assert debit.description == 'janet'

@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')
def test_ncc_failing(self, cch, fch):
Expand Down Expand Up @@ -264,7 +270,8 @@ def test_hold_amount_includes_negative_balance(self, cch, fch):
self.db.run("""
UPDATE participants SET balance = -10 WHERE username='janet'
""")
self.janet.set_tip_to(self.homer, 25)
team = self.make_team('The A Team', is_approved=True)
self.janet.set_subscription_to(team, 25)
fch.return_value = {}
cch.return_value = (None, 'some error')
self.create_card_holds()
Expand All @@ -280,6 +287,7 @@ def test_payin_fetches_and_uses_existing_holds(self):
self.create_card_holds()
assert not cch.called, cch.call_args_list

@pytest.mark.xfail(reason="haven't migrated transfer_takes yet")
@mock.patch.object(Payday, 'fetch_card_holds')
def test_payin_cancels_existing_holds_of_insufficient_amounts(self, fch):
self.janet.set_tip_to(self.homer, 30)
Expand Down Expand Up @@ -313,6 +321,7 @@ def test_fetch_card_holds_handles_extra_holds(self, cancel, CardHold):
cancel.assert_called_with(fake_hold)
assert len(holds) == 0

@pytest.mark.xfail(reason="haven't migrated transfer_takes yet")
@mock.patch('gratipay.billing.payday.log')
def test_payin_cancels_uncaptured_holds(self, log):
self.janet.set_tip_to(self.homer, 42)
Expand Down Expand Up @@ -342,6 +351,7 @@ def test_payin_cant_make_balances_more_negative(self):
with self.assertRaises(NegativeBalance):
payday.update_balances(cursor)

@pytest.mark.xfail(reason="haven't migrated transfer_takes yet")
@mock.patch.object(Payday, 'fetch_card_holds')
@mock.patch('gratipay.billing.exchanges.thing_from_href')
def test_card_hold_error(self, tfh, fch):
Expand All @@ -352,6 +362,7 @@ def test_card_hold_error(self, tfh, fch):
payday = self.fetch_payday()
assert payday['ncc_failing'] == 1

@pytest.mark.xfail(reason="haven't migrated transfer_takes yet")
def test_payin_doesnt_make_null_transfers(self):
alice = self.make_participant('alice', claimed_time='now')
alice.set_tip_to(self.homer, 1)
Expand All @@ -362,19 +373,28 @@ def test_payin_doesnt_make_null_transfers(self):
transfers0 = self.db.all("SELECT * FROM transfers WHERE amount = 0")
assert not transfers0

def test_transfer_tips(self):
def test_pay_subscriptions(self):
alice = self.make_participant('alice', claimed_time='now', balance=1)
alice.set_tip_to(self.janet, D('0.51'))
alice.set_tip_to(self.homer, D('0.50'))
hannibal = self.make_participant('hannibal', claimed_time='now', last_ach_result='')
lecter = self.make_participant('lecter', claimed_time='now', last_ach_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'))
alice.set_subscription_to(B, D('0.50'))

payday = Payday.start()
with self.db.get_cursor() as cursor:
payday.prepare(cursor, payday.ts_start)
payday.transfer_tips(cursor)
payday.process_subscriptions(cursor)
assert cursor.one("select balance from payday_teams where slug='TheATeam'") == D('0.51')
assert cursor.one("select balance from payday_teams where slug='TheBTeam'") == 0
payday.update_balances(cursor)

assert Participant.from_id(alice.id).balance == D('0.49')
assert Participant.from_id(self.janet.id).balance == D('0.51')
assert Participant.from_id(self.homer.id).balance == 0
assert Participant.from_username('hannibal').balance == 0
assert Participant.from_username('lecter').balance == 0

@pytest.mark.xfail(reason="haven't migrated_transfer_takes yet")
def test_transfer_takes(self):
a_team = self.make_participant('a_team', claimed_time='now', number='plural', balance=20)
alice = self.make_participant('alice', claimed_time='now')
Expand Down Expand Up @@ -407,6 +427,7 @@ def test_transfer_takes(self):
else:
assert p.balance == 0

@pytest.mark.xfail(reason="haven't migrated_transfer_takes yet")
@mock.patch.object(Payday, 'fetch_card_holds')
def test_transfer_takes_doesnt_make_negative_transfers(self, fch):
hold = balanced.CardHold(amount=1500, meta={'participant_id': self.janet.id},
Expand All @@ -422,6 +443,7 @@ def test_transfer_takes_doesnt_make_negative_transfers(self, fch):
assert Participant.from_id(self.homer.id).balance == 10
assert Participant.from_id(self.janet.id).balance == 0

@pytest.mark.xfail(reason="haven't migrated take_over_balances yet")
def test_take_over_during_payin(self):
alice = self.make_participant('alice', claimed_time='now', balance=50)
bob = self.make_participant('bob', claimed_time='now', elsewhere='twitter')
Expand All @@ -431,7 +453,7 @@ def test_take_over_during_payin(self):
payday.prepare(cursor, payday.ts_start)
bruce = self.make_participant('bruce', claimed_time='now')
bruce.take_over(('twitter', str(bob.id)), have_confirmation=True)
payday.transfer_tips(cursor)
payday.process_subscriptions(cursor)
bruce.delete_elsewhere('twitter', str(bob.id))
billy = self.make_participant('billy', claimed_time='now')
billy.take_over(('github', str(bruce.id)), have_confirmation=True)
Expand All @@ -441,6 +463,7 @@ def test_take_over_during_payin(self):
assert Participant.from_id(bruce.id).balance == 0
assert Participant.from_id(billy.id).balance == 18

@pytest.mark.xfail(reason="haven't migrated transfer_takes yet")
@mock.patch.object(Payday, 'fetch_card_holds')
@mock.patch('gratipay.billing.payday.capture_card_hold')
def test_payin_dumps_transfers_for_debugging(self, cch, fch):
Expand Down
Loading

0 comments on commit d900c39

Please sign in to comment.