forked from gratipay/gratipay.com
-
Notifications
You must be signed in to change notification settings - Fork 0
/
masspay.py
executable file
·210 lines (167 loc) · 6.69 KB
/
masspay.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
#!/usr/bin/env python
"""This is a script for managing MassPay each week.
Most of our payouts are handled by Balanced, but they're limited to people in
the U.S. We need to payout to people outside the U.S. (#126), and while we work
on a long-term solution, we are using PayPal. However, we've grown past the
point that PayPal's Instant Transfer feature is workable. This script is for
interfacing with PayPal's MassPay feature.
This script provides for:
1. Computing an input CSV by hitting the Gittip database directly.
2. Computing two output CSVs (one to upload to PayPal, the second to use for POSTing
the exchanges back to Gittip)
3. POSTing the exchanges back to Gittip via the HTTP API.
The idea is that you run steps 1 and 2, then run through the MassPay UI on the
PayPal website using the appropriate CSV from step 2, then run step 3.
"""
from __future__ import absolute_import, division, print_function, unicode_literals
import csv
import datetime
import getpass
import os
import sys
from decimal import Decimal as D, ROUND_UP
import requests
os.chdir('../masspay')
ts = datetime.datetime.now().strftime('%Y-%m-%d')
INPUT_CSV = '{}.input.csv'.format(ts)
PAYPAL_CSV = '{}.output.paypal.csv'.format(ts)
GITTIP_CSV = '{}.output.gittip.csv'.format(ts)
def round_(d):
return d.quantize(D('0.01'))
def round_up(d):
return d.quantize(D('0.01'), ROUND_UP)
def print_rule():
print("-" * 80)
class Payee(object):
username = None
email = None
gross = None
gross_perc = None
fee = None
net = None
def __init__(self, rec):
self.username, self.email, amount = rec
self.gross = D(amount)
self.fee = D(0)
self.net = self.gross
def assess_fee(self, fee):
self.fee += fee
self.net -= fee
def compute_input_csv():
from gittip import wireup
db = wireup.db()
participants = db.all("""
SELECT participants.*::participants
FROM participants
WHERE paypal_email IS NOT null
AND balance > 0
ORDER BY balance DESC
""")
writer = csv.writer(open(INPUT_CSV, 'w+'))
print_rule()
print("{:<24}{:<32} {:^7} {:^7} {:^7}".format("username", "email", "balance", "tips", "amount"))
print_rule()
total_gross = 0
for participant in participants:
tips, total = participant.get_tips_and_total(for_payday=False)
amount = participant.balance - total
if amount <= 0:
continue
total_gross += amount
print("{:<24}{:<32} {:>7} {:>7} {:>7}".format( participant.username
, participant.paypal_email
, participant.balance
, total
, amount
))
row = (participant.username, participant.paypal_email, amount)
writer.writerow(row)
print(" "*72, "-"*7)
print("{:>80}".format(total_gross))
def compute_output_csvs():
payees = [Payee(rec) for rec in csv.reader(open(INPUT_CSV))]
payees.sort(key=lambda o: o.gross, reverse=True)
total_gross = sum([p.gross for p in payees])
total_fees = total_gross - round_(total_gross / D('1.02')) # 2% fee
total_net = D('0.00')
for payee in payees:
payee.gross_perc = payee.gross / total_gross
payee.assess_fee(round_(total_fees * payee.gross_perc))
fee_check = sum([p.fee for p in payees])
if fee_check != total_fees:
# Up to one penny per payee is okay.
fee_difference = total_fees - fee_check
fee_tolerance = D('0.0{}'.format(len((payees)))) # one penny per payee
print_rule()
print()
print("Fee rounding error tolerance: {}".format(fee_tolerance))
print("Accumulated fee rounding error: {}".format(fee_difference))
print()
assert fee_difference < fee_tolerance
# Distribute rounding errors.
for payee in reversed(payees):
allotment = round_up(fee_difference * payee.gross_perc)
payee.assess_fee(allotment)
print(" {} => {}".format(allotment, payee.email))
fee_difference -= allotment
if fee_difference == 0:
break
print()
total_net = sum([p.net for p in payees])
fee_check = sum([p.fee for p in payees])
assert fee_check == total_fees
assert total_net + total_fees == total_gross
paypal_csv = csv.writer(open(PAYPAL_CSV, 'w+'))
gittip_csv = csv.writer(open(GITTIP_CSV, 'w+'))
print_rule()
print("{:<24}{:<32} {:^7} {:^7} {:^7}".format("username", "email", "gross", "fee", "net"))
print_rule()
for payee in payees:
paypal_csv.writerow((payee.email, payee.net, "usd"))
gittip_csv.writerow(( payee.username
, payee.email
, payee.gross
, payee.fee
, payee.net
))
print("{username:<24}{email:<32} {gross:>7} {fee:>7} {net:>7}".format(**payee.__dict__))
print(" "*56, "-"*23)
print("{:>64} {:>7} {:>7}".format(total_gross, total_fees, total_net))
def record_exchanges_in_gittip():
try:
gittip_api_key = os.environ['GITTIP_API_KEY']
except KeyError:
gittip_api_key = getpass.getpass("Gittip API key: ")
try:
gittip_base_url = os.environ['GITTIP_BASE_URL']
except KeyError:
gittip_base_url = 'https://www.gittip.com'
for username, email, gross, fee, net in csv.reader(open(GITTIP_CSV)):
url = '{}/{}/history/record-an-exchange'.format(gittip_base_url, username)
note = 'PayPal MassPay to {}.'.format(email)
data = {'amount': '-' + net, 'fee': fee, 'note': note}
requests.post(url, auth=(gittip_api_key, ''), data=data)
print(note)
def main():
print("Looking for files for {} ...".format(ts))
for filename in (INPUT_CSV, PAYPAL_CSV, GITTIP_CSV):
print(" [{}] {}".format('x' if os.path.exists(filename) else ' ', filename))
if not sys.argv[1:]:
if raw_input("\nCompute input CSV? [y/N] ") == 'y':
compute_input_csv()
if raw_input("\nCompute output CSVs? [y/N] ") == 'y':
compute_output_csvs()
if raw_input("\nRecord exchanges in Gittip? [y/N] ") == 'y':
record_exchanges_in_gittip()
else:
if '-i' in sys.argv:
compute_input_csv()
if '-o' in sys.argv:
compute_output_csvs()
if '-r' in sys.argv:
record_exchanges_in_gittip()
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print()