Repository: gratipay/gratipay.com

Issue:
history is screwed up, negative balance
@pjz noticed, not sure yet who else might be affected.



Discussion:
I checked with the backup I took before payday yesterday (#1107) and it's a problem there too. So this isn't something that snuck in yesterday during payday.

This may yet be a payday bug though. The history page starts with your current balance and adds/subtracts from that for each record in the transfers and exchanges table.

[IRC](https://botbot.me/freenode/gittip/msg/4292599/)

According to `transfers`, you've received $273.89 and given $48.75, so I'd expect your balance to be $225.14 instead of $4.75.

I count 58 affected accounts as of Gittip 56.

So we've got a bug.

(Though I guess we knew that.)

One implication we'll have to follow up on is that I've used the balance as reported on the history page to drive manual payouts.

I have a series of weekly backups from the past year. A good next step might be to run my checker script against each week's backup to find out when this behavior started.

Here's the number of affected record for each backup I've got:

```
       0 2012-07-05.psql.log
       0 2012-07-10.psql.log
       0 2012-07-13.psql.log
       2 2012-07-17.psql.log
       2 2012-07-20.psql.log
      73 2012-07-20b.psql.log
       0 2012-07-27.psql.log
       0 2012-07-27b.psql.log
       0 2012-08-03.psql.log
       0 2012-08-03b.psql.log
       0 2012-08-10.psql.log
       0 2012-08-16.psql.log
       0 2012-08-17.psql.log
       0 2012-08-23.psql.log
       2 2012-08-24.psql.log
       2 2012-08-28.psql.log
       2 2012-08-31.psql.log
       5 2012-09-06.psql.log
       6 2012-09-12.psql.log
       6 2012-09-13.psql.log
       7 2012-09-20.psql.log
       7 2012-09-27.psql.log
       9 2012-10-03.psql.log
       9 2012-10-04.psql.log
      13 2012-10-11.psql.log
      18 2012-10-25.psql.log
      20 2012-11-01.psql.log
      22 2012-11-05.psql.log
      22 2012-11-08.psql.log
      22 2012-11-15.psql.log
      22 2012-11-22.psql.log
      24 2012-11-29.psql.log
      26 2012-12-06.psql.log
      27 2012-12-11.psql.log
      27 2012-12-17.psql.log
      27 2012-12-20.psql.log
      27 2012-12-27.psql.log
      27 2013-01-03.psql.log
      27 2013-01-10.psql.log
      27 2013-01-17.psql.log
      30 2013-01-24.psql.log
      30 2013-01-31.psql.log
      34 2013-02-05.psql.log
      34 2013-02-07.psql.log
      36 2013-02-08.psql.log
      36 2013-02-14.psql.log
      39 2013-02-21.psql.log
      40 2013-02-28.psql.log
      42 2013-03-07.psql.log
      46 2013-03-14.psql.log
      49 2013-03-21.psql.log
      55 2013-03-28.psql.log
      59 2013-04-03.psql.log
      59 2013-04-04.psql.log
      62 2013-04-06.psql.log
      62 2013-04-10.psql.log
      62 2013-04-11.psql.log
      64 2013-04-14.psql.log
      64 2013-04-18.psql.log
      66 2013-04-25.psql.log
      67 2013-05-02.psql.log
      69 2013-05-09.psql.log
      69 2013-05-13.psql.log
      69 2013-05-16.psql.log
      75 2013-05-23.psql.log
      77 2013-05-30.psql.log
      79 2013-06-06.psql.log
      84 2013-06-13.psql.log
      85 2013-06-20.psql.log
      88 2013-06-27.psql.log
      88 2013-06-27b.psql.log
      90 2013-06-29.psql.log
      90 2013-07-01.psql.log
      90 2013-07-04.psql.log
```

So the problem seems to have been with us the whole time(!). It grew through week 7, which was a bit of a clusterhuck (#169). We apparently hit the reset button in cleaning that up, but it has since grown again. It is currently affecting 90 accounts out of 1559 with a balance (6%).

Presumably once an account is corrupted then the corruption carries through (rather than some accounts being corrupted some weeks, others the next).

The growth in corruption doesn't appear to line up with instances of the payday script crashing. We've had a handful of instances of that, and none in the past few months until this week, which isn't even represented above. The corruption appears to be part of payday in general and not triggered by crash-and-rerun.

We should make sure we're accounting for fees properly in the check script. I don't see that fees could be implicated in the corruption itself, because @pjz has zero exchanges and great corruption.

Phew. I wasn't subtracting payout fees from exchange amounts (the `exchanges` table is asymmetric: payin fees don't need to be subtracted from exchange amounts to compute balance but payout fees do). Corrected for that, here's what I'm now seeing:

```
       0 2012-07-05.psql.log
       0 2012-07-10.psql.log
       0 2012-07-13.psql.log
       2 2012-07-17.psql.log
       2 2012-07-20.psql.log
      73 2012-07-20b.psql.log
       0 2012-07-27.psql.log
       0 2012-07-27b.psql.log
       0 2012-08-03.psql.log
       0 2012-08-03b.psql.log
       0 2012-08-10.psql.log
       0 2012-08-16.psql.log
       0 2012-08-17.psql.log
       0 2012-08-23.psql.log
       0 2012-08-24.psql.log
       0 2012-08-28.psql.log
       0 2012-08-31.psql.log
       0 2012-09-06.psql.log
       0 2012-09-12.psql.log
       0 2012-09-13.psql.log
       0 2012-09-20.psql.log
       0 2012-09-27.psql.log
       0 2012-10-03.psql.log
       0 2012-10-04.psql.log
       0 2012-10-11.psql.log
       0 2012-10-25.psql.log
       0 2012-11-01.psql.log
       0 2012-11-05.psql.log
       0 2012-11-08.psql.log
       0 2012-11-15.psql.log
       0 2012-11-22.psql.log
       0 2012-11-29.psql.log
       0 2012-12-06.psql.log
       0 2012-12-11.psql.log
       1 2012-12-17.psql.log
       1 2012-12-20.psql.log
       1 2012-12-27.psql.log
       1 2013-01-03.psql.log
       1 2013-01-10.psql.log
       1 2013-01-17.psql.log
       2 2013-01-24.psql.log
       1 2013-01-31.psql.log
       1 2013-02-05.psql.log
       1 2013-02-07.psql.log
       1 2013-02-08.psql.log
       1 2013-02-14.psql.log
       1 2013-02-21.psql.log
       1 2013-02-28.psql.log
       1 2013-03-07.psql.log
       1 2013-03-14.psql.log
       1 2013-03-21.psql.log
       1 2013-03-28.psql.log
       1 2013-04-03.psql.log
       1 2013-04-04.psql.log
       1 2013-04-06.psql.log
       1 2013-04-10.psql.log
       1 2013-04-11.psql.log
       1 2013-04-14.psql.log
       1 2013-04-18.psql.log
       2 2013-04-25.psql.log
       2 2013-05-02.psql.log
       2 2013-05-09.psql.log
       2 2013-05-13.psql.log
       2 2013-05-16.psql.log
       2 2013-05-23.psql.log
       2 2013-05-30.psql.log
       2 2013-06-06.psql.log
       4 2013-06-13.psql.log
       4 2013-06-20.psql.log
       4 2013-06-27.psql.log
       4 2013-06-27b.psql.log
       4 2013-06-29.psql.log
       4 2013-07-01.psql.log
       4 2013-07-04.psql.log
       4 2013-07-11.psql.log
       5 2013-07-15.psql.log
```

One interesting datum is that for the four accounts affected, three of them get corrupted only once. @pjz got corrupted twice. But in other words it's only happening to a few users (4 / 1559 = 0.2%) and it's only happened five times total. It's a rare event, thankfully.

What is it? Do these corruptions line up with payday crashes?

The second in 2013-01-24 is explained by a blip in my handling of a manual payout.

Last payday 'worked'... in that it adjusted from my previous balance correctly.   I'm still down the $220ish amount, though.

[IRC ref](https://botbot.me/freenode/gittip/msg/4499456/)

I modified the [1116.sh](https://gist.github.com/whit537/c484b050567cbf4dbada) script I'm using to process backups (in concert with [1116.py](https://gist.github.com/whit537/8f1a3f484b250fbc66b0)). It now processes backup files named on the command line. If none are named it processes all backups in the current directory. I processed two more backup files and appended the result to https://github.com/gittip/www.gittip.com/issues/1116?source=c#issuecomment-20560285.

We picked up another corrupt record last week. We crashed two weeks ago 2013-07-04 (#1107). Each backup above generally is from immediately before payday is run, so the 2013-07-04 row above is the state of the db just before the 2013-07-04 payday, etc. 2013-07-11 went off without a hitch (#1131), except that after it we now have an additional corrupt record.

When we first experienced corruption during payday (#169) it turned out that the corruption entered in one week but didn't manifest until the next week. Is that the pattern here as well? How do the manifestations here line up with payday crashes?

(I manually added in the dates corresponding to the Gittip week number.)

```
$ ack Traceback
2012-07-20  gittip-7.log:145:pid-34940 thread-140735091989696 (MainThread) Traceback (most recent call last):
2012-08-24  gittip-12.0.log:1612:pid-35707 thread-140735091989696 (MainThread) Traceback (most recent call last):
2012-09-06  gittip-14.2.log:515:pid-30377 thread-140735081639104 (MainThread) Traceback (most recent call last):
2012-09-06  gittip-14.log:1202:pid-29946 thread-140735081639104 (MainThread) Traceback (most recent call last):
2012-09-27  gittip-17.log:1898:pid-2821 thread-140735081639104 (MainThread) Traceback (most recent call last):
2012-12-06  gittip-27.log:765:pid-74030 thread-140735090330816 (MainThread) Traceback (most recent call last):
2012-12-13  gittip-28.log:2088:pid-63480 thread-140735090330816 (MainThread) Traceback (most recent call last):
2013-01-03  gittip-31.log:1944:pid-734 thread-140735090330816 (MainThread) Traceback (most recent call last):
2013-01-10  gittip-32.log:1196:pid-93273 thread-140735090330816 (MainThread) Traceback (most recent call last):
2013-01-24  gittip-34.log:214:pid-51984 thread-140735090330816 (MainThread) Traceback (most recent call last):
2013-07-04  gittip-57.log:870:pid-29045 thread-140735088401600 (MainThread) Traceback (most recent call last):
2013-07-04  gittip-57.log:893:Traceback (most recent call last):
```

For kicks, here's the weeks we crashed, shown on the users chart:

![screen shot 2013-07-15 at 3 21 17 pm](https://f.cloud.github.com/assets/134455/800304/9b4585c8-ed86-11e2-98de-b79d975af5d7.png)

Actually, this is a good way to visualize this. Here's the weeks we saw corruption:

![screen shot 2013-07-15 at 3 40 40 pm](https://f.cloud.github.com/assets/134455/800307/aa9dd4b2-ed86-11e2-8221-774202c3c0d2.png)

Well, I'd say that rules out crashing as the root cause! Back to the drawing board. :-)

I believe pachinko first hit in Gittip 46 (#857). The frequency of corruption has increased since then, but we have that one case way back in week 29. Maybe there are multiple causes?

Here's the data for @pjz:

```
                  Exchanged       Received           Balance
Week     Date           Gave Away          Check           Difference
 53   2013-06-06     0    37.50   229.38   191.88     3.75   188.13
 54   2013-06-13     0    39.75   239.63   199.88    11.75   188.13
 55   2013-06-20     0    42.75   252.39   209.64     4.75   204.89
 56   2013-06-27     0    45.75   261.64   215.89    11.00   204.89
 57   2013-07-04     0    48.75   273.89   225.14     4.75   220.39
 58   2013-07-11     0    51.75   286.14   234.39    14.00   220.39
```

P.S. `Gave Away` and `Received` are cumulative, not per-week.

What is the pattern here? It's interesting that `difference` increases in stairstep (first by 16.76 and then by 15.50) while nothing else does. Where does the stairstep come from?

Okay! I believe (in @pjz's case at least) that this is due to a bug where we record an ach credit locally even if the Balanced API call fails. The reason we attempt to credit @pjz in the first place here is because he has a Balanced account connected (even though he has no credit card or bank account on file).

In other words, Gittip believes that @pjz has been withdrawing money all along. The stairstep effect is due to the minimum payout threshold of $10.

I keep logs for each payday. I inferred the above after seeing a `Crediting ... failed` log message for pjz in the log for week 53. Based on that, I just did an `ack` for `"Crediting.*failed"`, and it lines up perfectly with the corruption we're seeing.

The fix is somewhat tricky because we do want to call into `record_credit`, and the `UPDATE` to `participants` for `balance` also updates the `last_ach_result` as well. Need to write some tests and untangle the code paths here.

So is it fixed? Is it going to happen again? Can you fix my balance yet?


Resolving patch:
diff --git a/gittip/billing/payday.py b/gittip/billing/payday.py
--- a/gittip/billing/payday.py
+++ b/gittip/billing/payday.py
@@ -790,7 +790,7 @@ def record_credit(self, amount, fee, error, username):
 
             if error:
                 last_ach_result = error
-                amount = Decimal('0.00')
+                credit = fee = Decimal('0.00')  # ensures balance won't change
                 self.mark_ach_failed(cursor)
             else:
                 last_ach_result = ''
diff --git a/gittip/participant.py b/gittip/participant.py
--- a/gittip/participant.py
+++ b/gittip/participant.py
@@ -9,7 +9,6 @@
 from aspen import Response
 from aspen.utils import typecheck
 from psycopg2 import IntegrityError
-from gittip.models import community
 
 
 ASCII_ALLOWED_IN_USERNAME = set("0123456789"


Test patch:
diff --git a/tests/test_billing_payday.py b/tests/test_billing_payday.py
--- a/tests/test_billing_payday.py
+++ b/tests/test_billing_payday.py
@@ -702,3 +702,21 @@ def test_mark_transfer(self):
         expected = before_transfer['transfer_volume'] + amount
         actual = after_transfer['transfer_volume']
         assert_equals(actual, expected)
+
+    def test_record_credit_updates_balance(self):
+        alice = self.make_participant("alice")
+        self.payday.record_credit( amount=Decimal("-1.00")
+                                 , fee=Decimal("0.41")
+                                 , error=""
+                                 , username="alice"
+                                  )
+        assert_equals(alice.balance, Decimal("0.59"))
+
+    def test_record_credit_doesnt_update_balance_if_error(self):
+        alice = self.make_participant("alice")
+        self.payday.record_credit( amount=Decimal("-1.00")
+                                 , fee=Decimal("0.41")
+                                 , error="SOME ERROR"
+                                 , username="alice"
+                                  )
+        assert_equals(alice.balance, Decimal("0.00"))