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

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: gratipay/gratipay.com
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 52b0413372ef588f7bd1001045497419c08c5c1b
Choose a base ref
..
head repository: gratipay/gratipay.com
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 013a505c17d1d77076a1a4eb1b541d3d2a93b040
Choose a head ref
Showing with 89 additions and 52 deletions.
  1. +32 −20 gratipay/billing/payday.py
  2. +10 −9 sql/payday.sql
  3. +47 −23 tests/py/test_billing_payday.py
52 changes: 32 additions & 20 deletions gratipay/billing/payday.py
Original file line number Diff line number Diff line change
@@ -264,25 +264,25 @@ def process_takes(cursor, ts_start):
cursor.run("""
INSERT INTO payday_takes
SELECT team_id, participant_id, ntakes
FROM ( SELECT DISTINCT ON (team_id, participant_id)
team_id, participant_id, ntakes, ctime
FROM takes
WHERE mtime < %(ts_start)s
ORDER BY team_id, participant_id, mtime DESC
) t
WHERE t.ntakes > 0
AND t.team_id IN (SELECT id FROM payday_teams)
AND t.participant_id IN (SELECT id FROM payday_participants)
AND ( SELECT ppd.id
FROM payday_payments_done ppd
JOIN participants ON participants.id = t.participant_id
JOIN teams ON teams.id = t.team_id
WHERE participants.username = ppd.participant
AND teams.slug = ppd.team
AND direction = 'to-participant'
) IS NULL
ORDER BY t.team_id, t.ntakes ASC;
SELECT team_id, participant_id, ntakes
FROM ( SELECT DISTINCT ON (team_id, participant_id)
team_id, participant_id, ntakes, ctime
FROM takes
WHERE mtime < %(ts_start)s
ORDER BY team_id, participant_id, mtime DESC
) t
WHERE t.ntakes > 0
AND t.team_id IN (SELECT id FROM payday_teams)
AND t.participant_id IN (SELECT id FROM payday_participants)
AND ( SELECT ppd.id
FROM payday_payments_done ppd
JOIN participants ON participants.id = t.participant_id
JOIN teams ON teams.id = t.team_id
WHERE participants.username = ppd.participant
AND teams.slug = ppd.team
AND direction = 'to-participant'
) IS NULL
ORDER BY t.team_id, t.ntakes ASC;
""", dict(ts_start=ts_start))

@@ -292,7 +292,19 @@ def process_draws(cursor):
"""Send whatever remains after payroll to the team owner.
"""
log("Processing draws.")
cursor.run("UPDATE payday_teams SET is_drained=true;")
cursor.run("""
UPDATE payday_teams pt
SET is_drained=true
WHERE ( SELECT ppd.id
FROM payday_payments_done ppd
JOIN teams ON teams.slug = ppd.team
WHERE ppd.team = pt.slug -- from the team
AND ppd.participant = teams.owner -- to the owner
AND direction = 'to-participant'
) IS NULL
""")


def settle_card_holds(self, cursor, holds):
19 changes: 10 additions & 9 deletions sql/payday.sql
Original file line number Diff line number Diff line change
@@ -30,6 +30,7 @@ CREATE TABLE payday_teams AS
, slug
, owner
, ntakes
, baseline
, 0::numeric(35, 2) AS balance
, false AS is_drained
FROM teams t
@@ -206,19 +207,19 @@ CREATE TRIGGER process_payment_instruction BEFORE UPDATE OF is_funded ON payday_
EXECUTE PROCEDURE process_payment_instruction();


-- Create a trigger to process distributions based on takes
-- Create a trigger to process distributions to team members

CREATE OR REPLACE FUNCTION process_distribution() RETURNS trigger AS $$
CREATE OR REPLACE FUNCTION process_member_distribution() RETURNS trigger AS $$
DECLARE
amount numeric(35,2);
team_balance numeric(35,2);
team_ntakes int;
amount numeric(35,2);
available numeric(35,2);
team_ntakes int;
BEGIN
team_balance := (SELECT balance FROM payday_teams WHERE id = NEW.team_id);
IF (team_balance <= 0) THEN RETURN NULL; END IF;
available := (SELECT balance - baseline FROM payday_teams WHERE id = NEW.team_id);
IF (available <= 0) THEN RETURN NULL; END IF;

team_ntakes := (SELECT ntakes FROM payday_teams WHERE id = NEW.team_id);
amount := (NEW.ntakes::numeric / team_ntakes::numeric) * team_balance;
amount := (NEW.ntakes::numeric / team_ntakes::numeric) * available;
amount := round(amount, 2);

-- `pay` decrements the team balance, so we need to decrement ntakes as
@@ -240,7 +241,7 @@ CREATE OR REPLACE FUNCTION process_distribution() RETURNS trigger AS $$
$$ LANGUAGE plpgsql;

CREATE TRIGGER process_takes AFTER INSERT ON payday_takes
FOR EACH ROW EXECUTE PROCEDURE process_distribution();
FOR EACH ROW EXECUTE PROCEDURE process_member_distribution();


-- Create a trigger to process draws
70 changes: 47 additions & 23 deletions tests/py/test_billing_payday.py
Original file line number Diff line number Diff line change
@@ -537,7 +537,7 @@ def test_payin_dumps_transfers_for_debugging(self, cch, fch):
os.unlink(filename)


class TestTakes(BillingHarness):
class TestPayout(BillingHarness):

tearDownClass = None

@@ -547,70 +547,94 @@ def setUp(self):
self.TT = self.db.one("SELECT id FROM countries WHERE code2='TT'")

picard = Participant.from_username(self.enterprise.owner)
picard.store_identity_info(self.TT, 'nothing-enforced', {})
picard.set_identity_verification(self.TT, True)
self.identify(picard)

def make_member(self, username, ntakes):
member = self.make_participant(username, email_address=username+'@x.y', claimed_time='now')
member.store_identity_info(self.TT, 'nothing-enforced', {})
member.set_identity_verification(self.TT, True)
self.identify(member)
self.enterprise.set_ntakes_for(member, ntakes)
return member

def identify(self, participant):
participant.store_identity_info(self.TT, 'nothing-enforced', {})
participant.set_identity_verification(self.TT, True)

payday = None
def start_payday(self):
self.payday = Payday.start()

def run_through_takes(self):
def process_distributions(self):
if not self.payday:
self.start_payday()
with self.db.get_cursor() as cursor:
self.payday.prepare(cursor)
cursor.run("UPDATE payday_teams SET balance=99")
self.payday.process_takes(cursor, self.payday.ts_start)
self.payday.process_draws(cursor)
self.payday.update_balances(cursor)


# pt - process_takes
# pd - process_distributions -- This is a pseudo-function, if you will; I'm not
# taking the time to refactor Payday right now.

def test_pt_processes_takes(self):
def test_pd_processes_takes(self):
self.make_member('crusher', 500)
self.make_member('bruiser', 400)
self.run_through_takes()
self.process_distributions()
assert Participant.from_username('crusher').balance == D('49.50')
assert Participant.from_username('bruiser').balance == D('39.60')
assert Participant.from_username('picard').balance == D(' 0.00')
assert Participant.from_username('picard').balance == D(' 9.90')

def test_pt_ignores_takes_set_after_the_start_of_payday(self):
def test_pd_ignores_takes_set_after_the_start_of_payday(self):
self.make_member('crusher', 500)
self.start_payday()
self.make_member('bruiser', 400)
self.run_through_takes()
self.process_distributions()
assert Participant.from_username('crusher').balance == D('49.50')
assert Participant.from_username('bruiser').balance == D(' 0.00')
assert Participant.from_username('picard').balance == D(' 0.00')
assert Participant.from_username('picard').balance == D('49.50')

def test_pt_ignores_takes_that_have_already_been_processed(self):
def test_pd_ignores_takes_that_have_already_been_processed(self):
self.make_member('crusher', 500)
self.start_payday()
self.make_member('bruiser', 400)
self.run_through_takes()
self.run_through_takes()
self.run_through_takes()
self.run_through_takes()
self.run_through_takes()
self.process_distributions()
self.process_distributions()
self.process_distributions()
self.process_distributions()
self.process_distributions()
assert Participant.from_username('crusher').balance == D('49.50')
assert Participant.from_username('bruiser').balance == D(' 0.00')
assert Participant.from_username('picard').balance == D(' 0.00')
assert Participant.from_username('picard').balance == D('49.50')

def test_pt_is_happy_to_deal_the_owner_in(self):
def test_pd_is_happy_to_deal_the_owner_in(self):
self.make_member('crusher', 500)
self.make_member('bruiser', 400)
self.enterprise.set_ntakes_for(Participant.from_username(self.enterprise.owner), 50)
self.run_through_takes()
self.process_distributions()
assert Participant.from_username('crusher').balance == D('49.50')
assert Participant.from_username('bruiser').balance == D('39.60')
assert Participant.from_username('picard').balance == D(' 4.95')
assert Participant.from_username('picard').balance == D(' 9.90')

def test_pd_respects_baseline(self):
self.db.run("UPDATE teams SET baseline=33")
self.make_member('crusher', 500)
self.process_distributions()
assert Participant.from_username('crusher').balance == D('33.00')
assert Participant.from_username('picard').balance == D('66.00')

def test_pd_leaves_the_rest_for_the_owner(self):
self.make_member('crusher', 500)
self.process_distributions()
assert Participant.from_username('crusher').balance == D('49.50')
assert Participant.from_username('picard').balance == D('49.50')

def test_pd_handles_baseline_and_draw_gracefully(self):
self.db.run("UPDATE teams SET baseline=33")
self.make_member('crusher', 500)
self.process_distributions()
assert Participant.from_username('crusher').balance == D('33.00')
assert Participant.from_username('picard').balance == D('66.00')


class TestNotifyParticipants(EmailHarness):