Files
Chad Whitacre be8dc4f8ce Implement UI for recording manual exchanges; #53
Sometimes we do manual payouts (maybe payins someday?) and we need a way
to record those in the database so we don't have to keep doing them
directly in SQL(!). This adds a form to the history page for site admins
to use. It extends the exchanges schema to keep track of who made manual
adjustments and why.
2013-03-05 12:02:33 -05:00

293 lines
11 KiB
HTML

from decimal import Decimal
from aspen import Response, log
from aspen.utils import to_age
from gittip import db, AMOUNTS
from gittip.utils import get_participant
class Paydays(object):
def __init__(self, participant_id, balance):
self._participant_id = participant_id
self._paydays = list(db.fetchall("SELECT ts_start, ts_end FROM paydays "
"ORDER BY ts_end DESC"))
self._npaydays = len(self._paydays)
self._exchanges = list(db.fetchall( "SELECT * FROM exchanges "
"WHERE participant_id=%s "
"ORDER BY timestamp ASC"
, (participant_id,)
))
self._transfers = list(db.fetchall( "SELECT * FROM transfers "
"WHERE tipper=%s OR tippee=%s "
"ORDER BY timestamp ASC"
, (participant_id, participant_id)
))
self._balance = balance
def __iter__(self):
"""Yield iterators of events.
Each payday is expected to encompass 0 or 1 exchanges and 0 or more
transfers per participant. Here we knit them together along with start
and end events for each payday. If we have exchanges or transfers that
fall outside of a payday, then we have a logic bug. I am 50% confident
that this will manifest some day.
"""
_i = 1
for payday in self._paydays:
if not (self._exchanges or self._transfers):
# Show all paydays since the user started really participating.
break
payday_start = { 'event': 'payday-start'
, 'timestamp': payday['ts_start']
, 'number': self._npaydays - _i
, 'balance': Decimal('0.00')
}
payday_end = { 'event': 'payday-end'
, 'timestamp': payday['ts_end']
, 'number': self._npaydays - _i
}
received = { 'event': 'received'
, 'amount': Decimal('0.00')
, 'n': 0
}
_i += 1
events = []
while (self._exchanges or self._transfers):
# Take the next event, either an exchange or transfer.
# ====================================================
# We do this by peeking at both lists, and popping the list
# that has the next event.
exchange = self._exchanges[-1] if self._exchanges else None
transfer = self._transfers[-1] if self._transfers else None
if exchange is None:
event = self._transfers.pop()
elif transfer is None:
event = self._exchanges.pop()
elif transfer['timestamp'] > exchange['timestamp']:
event = self._transfers.pop()
else:
event = self._exchanges.pop()
if 'fee' in event:
if event['amount'] > 0:
event['event'] = 'charge'
else:
event['event'] = 'credit'
else:
event['event'] = 'transfer'
# Record the next event.
# ======================
if event['timestamp'] < payday_start['timestamp']:
if event['event'] == 'exchange':
back_on = self._exchanges
else:
back_on = self._transfers
back_on.append(event)
break
if event['event'] == 'transfer':
if event['tippee'] == self._participant_id:
# Don't leak details about who tipped you. Only show
# aggregates for that.
received['amount'] += event['amount']
received['n'] += 1
#continue # Don't leak!
events.append(event)
if not events:
continue
# Calculate balance.
# ==================
prev = events[0]
prev['balance'] = self._balance
for event in events[1:] + [payday_start]:
if prev['event'] == 'charge':
balance = prev['balance'] - prev['amount']
elif prev['event'] == 'credit':
balance = prev['balance'] - prev['amount'] + prev['fee']
elif prev['event'] == 'transfer':
if prev['tippee'] == self._participant_id:
balance = prev['balance'] - prev['amount']
else:
balance = prev['balance'] + prev['amount']
event['balance'] = balance
prev = event
self._balance = payday_start['balance']
yield payday_start
for event in reversed(events):
yield event
yield payday_end
# This should catch that logic bug.
if self._exchanges or self._transfers:
log("These should be empty:", self._exchanges, self._transfers)
raise "Logic bug in payday timestamping."
# ========================================================================== ^L
participant = get_participant(request, restrict=True)
paydays = Paydays(participant.id, participant.balance)
hero = "History"
title = "%s - %s" % (participant.id, hero)
locked = False
# ========================================================================== ^L
{% extends templates/profile.html %}
{% block page %}
<table id="history" class="centered">
<tr><td colspan="7">
<h2>{{ participant.id == user.id and "You" or participant.id }} joined
{{ to_age(participant.claimed_time) }}.</h2>
</td></tr>
<tr><td colspan="7">
<h2>{{ participant.id == user.id and "Your" or "Their" }} balance is
${{ participant.balance }}.</h2>
{% if user.ADMIN %}
<h2>Record an Exchange.</h2>
<form style="text-align: left;" action="record-an-exchange"
method="POST">
<input name="csrf_token" type="hidden" value="{{ csrf_token }}" />
<input name="amount" placeholder="amount" />
<input name="fee" placeholder="fee" />
<br />
<input name="note" placeholder="note" style="width: 230px" />
<button type="submit">Submit</button>
</form>
{% end %}
</td></tr>
{% for event in paydays %}
{% if event['event'] == 'payday-start' %}
<tr>
<th colspan="7"><h2>Gittip #{{ event['number'] }}
&mdash;{{ event['timestamp'].strftime("%B %d, %Y").replace(' 0', ' ') }}
({{ to_age(event['timestamp']) }})</h2></th>
</tr>
<tr class="head">
<td colspan="3" class="outside">&larr; Outside</td>
<td colspan="4" class="inside">Inside Gittip &rarr;</td>
</tr>
<tr class="head">
<td class="bank">Bank</td>
<td class="card">Card</td>
<td class="fees">Fees</td>
<td class="credits">Credits</td>
<td class="debits">Debits</td>
<td class="balance">Balance</td>
<td class="notes">Notes</td>
</tr>
<tr>
<td class="bank"></td>
<td class="card"></td>
<td class="fees"></td>
<td class="credits"></td>
<td class="debits"></td>
<td class="balance">{{ event['balance'] }}</td>
<td class="notes"></td>
</tr>
{% elif event['event'] == 'balance' %}
<tr>
<td class="bank"></td>
<td class="card"></td>
<td class="fees"></td>
<td class="credits"></td>
<td class="debits"></td>
<td class="balance">{{ event['balance'] }}</td>
<td class="notes"></td>
</tr>
{% elif event['event'] == 'credit' %}
<tr>
<td class="bank">{{ -event['amount'] }}</td>
<td class="card"></td>
<td class="fees">{{ event['fee'] }}</td>
<td class="credits"></td>
<td class="debits">{{ -event['amount'] + event['fee'] }}</td>
<td class="balance">{{ event['balance'] }}</td>
{% if event['recorder'] is None %}
<td class="notes">automatic withdrawal</td>
{% else %}
<td class="notes">
&ldquo;{{ escape(event['note']) }}&rdquo;&mdash;<a
href="/{{ event['recorder'] }}/">{{ event['recorder'] }}</a>
</td>
{% end %}
</tr>
{% elif event['event'] == 'charge' %}
<tr>
<td class="bank"></td>
<td class="card">{{ event['amount'] + event['fee'] }}</td>
<td class="fees">{{ event['fee'] }}</td>
<td class="credits">{{ event['amount'] }}</td>
<td class="debits"></td>
<td class="balance">{{ event['balance'] }}</td>
{% if event['recorder'] is None %}
<td class="notes">automatic charge</td>
{% else %}
<td class="notes">
&ldquo;{{ escape(event['note']) }}&rdquo;&mdash;<a
href="/{{ event['recorder'] }}/">{{ event['recorder'] }}</a>
</td>
{% end %}
</tr>
{% elif event['event'] == 'transfer' %}
<tr>
<td class="bank"></td>
<td class="card"></td>
<td class="fees"></td>
{% if event['tippee'] == participant.id %}
<td class="credits">{{ event['amount'] }}</td>
<td class="debits"></td>
{% else %}
<td class="credits"></td>
<td class="debits">{{ event['amount'] }}</td>
{% end %}
<td class="balance">{{ event['balance'] }}</td>
{% if event['tippee'] == participant.id %}
{% if user.ADMIN and (participant.id != user.id or 'override' in qs) %}
<td class="notes">from <a href="/{{ event['tipper'] }}/history.html">{{ event['tipper'] }}</a></td>
{% else %}
<td class="notes">from someone</td>
{% end %}
{% else %}
{% if user.ADMIN %}
<td class="notes">to
<a href="/{{ event['tippee'] }}/history.html">{{ event['tippee'] }}</a></td>
{% else %}
<td class="notes">to
<a href="/{{ event['tippee'] }}/">{{ event['tippee'] }}</a></td>
{% end %}
{% end %}
</tr>
{% end %}
{% end %}
</table>
{% end %}