gratipay / inside.gratipay.com

Here lieth a pioneer in open source sustainability. RIP
https://gratipay.news/the-end-cbfba8f50981
57 stars 38 forks source link

Respond to IRS letter #1164

Closed chadwhitacre closed 6 years ago

chadwhitacre commented 6 years ago

First time for everything, friends. :)

The income and payment information we have on file from sources such as employers or financial institutions doesn't match the information you reported on your tax return. If our information is correct, you will owe $9,220 (including interest), which you need to pay by October 5, 2017.

If you agree with the changes we made, [pay us].

If you don't agree with the changes, complete the Response form on Page 9, and send it to us along with a signed statement and any documentation that supports your claim so we receive it by October 5, 2017.

chadwhitacre commented 6 years ago

They are looking at Braintree's 1099-K for us which shows $36,428 income and we reported $18,758. In other words, they are counting our escrow as income to Gratipay. The reason we didn't file our own 1099-Ks for our own receivers is the "Exception for de minimis payments":

A TPSO is required to report any information concerning third party network transactions of any participating payee only if for the calendar year: The gross amount of total reportable payment transactions exceeds $20,000, and The total number of such transactions exceeds 200.

We won't have anyone over the $20,000 limit for 2015, but now that I think of it we may have people that exceed the 200 limit, since we are dealing in micro-payments.

rohitpaulk commented 6 years ago

I think of it we may have people that exceed the 200 limit, since we are dealing in micro-payments.

We only bill people once a week, no? i.e. maximum of 52 transactions per payee, per year.

chadwhitacre commented 6 years ago

@rohitpaulk But if someone has 10 givers that could be 520 transactions for the year. Also interacts with $10 minimum charge-in. Bottom line: we need to run the numbers.

We've got about two weeks to get a response in the (registered) mail to meet the October 5 deadline.

chadwhitacre commented 6 years ago

Just wrapped a conversation with a CPA, who serendipitously happened to be here at Paramount. I explained Gratipay to him, our status as a payment processor, etc. He reviewed the IRS letter, and he is going to put together an engagement letter to help us sort this out for an amount not to exceed $500. He doesn't think it'll escalate beyond that but of course who knows? :) He has experience with IRS audits (I guess technically this isn't an audit yet), and knows how to navigate the system (e.g., call the local office vs. responding by mail).

Engagement letter coming next week. He took the IRS letter with him. We also need to provide:

We don't need to worry about the number of transactions in 2015 at this point.

chadwhitacre commented 6 years ago

Emailed the CPA to establish contact. Started working up a query but it will take some thinking.

chadwhitacre commented 6 years ago

Need to account for transfers and payments (pre- and post-Gratipocalypse).

gratipay::DATABASE=> select sum(amount) from payments where direction='to-team' and "timestamp"::text >= '2015-01-01' and "timestamp"::text < '2016-01-01' and team != 'Gratipay';
┌──────────┐
│   sum    │
├──────────┤
│ 31244.01 │
└──────────┘
(1 row)
chadwhitacre commented 6 years ago

Sent the tax return. Need numbers ...

chadwhitacre commented 6 years ago

Can I repro the 18,758 number, first of all?

chadwhitacre commented 6 years ago

Phew. :)

gratipay::DATABASE=> \i irs.sql
┌──────────┐
│ ?column? │
├──────────┤
│ 18758.03 │
└──────────┘
(1 row)
select
(select sum(amount) from payments where direction='to-team' and team='Gratipay'
   and "timestamp"::text < '2016-01-01' and "timestamp"::text >= '2015-01-01')
+
(select sum(amount) from transfers where tippee='Gratipay'
   and "timestamp"::text < '2016-01-01' and "timestamp"::text >= '2015-01-01');
chadwhitacre commented 6 years ago

🤔

gratipay::DATABASE=> \i irs.sql
┌──────────────┐
│ total volume │
├──────────────┤
│    212514.20 │
└──────────────┘
(1 row)
select
(select sum(amount) from payments where direction='to-team'
   and "timestamp"::text < '2016-01-01' and "timestamp"::text >= '2015-01-01')
+
(select sum(amount) from transfers where
   "timestamp"::text < '2016-01-01' and "timestamp"::text >= '2015-01-01')
as "total volume";
chadwhitacre commented 6 years ago

That's way more than the 36,428 that Braintree reported. I guess the rest was Balanced? Did they not report 1099-Ks for 2015 since they went out of business? Seems likely ...

chadwhitacre commented 6 years ago

Alright, can we repro the 36,428 figure coming in from Braintree?

chadwhitacre commented 6 years ago

Um ... when did we start charging on Braintree?

chadwhitacre commented 6 years ago

https://github.com/gratipay/gratipay.com/issues/3287

chadwhitacre commented 6 years ago

https://github.com/gratipay/gratipay.com/issues/3486#issuecomment-106706837

chadwhitacre commented 6 years ago

Alright, so our first Braintree charges were on 2015-05-29.

chadwhitacre commented 6 years ago

(We have charges against network='braintree' exchange_routes dating back before that because of the potential corruption mentioned at https://github.com/gratipay/gratipay.com/issues/4442#issuecomment-305864193.)

chadwhitacre commented 6 years ago

Hmm ... low by $7,203 relative to Braintree's 1099-K? What am I missing? 🤔

gratipay::DATABASE=> \i irs.sql
┌──────────┐
│   sum    │
├──────────┤
│ 29225.88 │
└──────────┘
(1 row)
  select sum(amount + fee)
    from exchanges e
    join exchange_routes er
      on e.route = er.id
   where "timestamp"::text < '2016-01-01' and "timestamp"::text >= '2015-05-29'
     and network = 'braintree-cc'
        ;
chadwhitacre commented 6 years ago

What does the Braintree dashboard report?

chadwhitacre commented 6 years ago

$27,671.68 if I constrain to status='succeeded'.

chadwhitacre commented 6 years ago

2,266 rows. Braintree reports 2,516 successful transactions for the time period 2015-01-01 through 2015-12-31 inclusive.

chadwhitacre commented 6 years ago

They begin on 2015-05-29 as expected.

chadwhitacre commented 6 years ago

Oh! My query includes some refunds ...

chadwhitacre commented 6 years ago

(https://gratipay.news/charging-in-arrears-18cacf779bee per note field.)

chadwhitacre commented 6 years ago

Good lord we stopped keeping ref at that point (2015-10-15)? 😞

chadwhitacre commented 6 years ago

Looking at amount > 0 gets me to $30,232.90, but then only 1,687 transactions.

chadwhitacre commented 6 years ago

Alright, filtering out refunds at Braintree drops it to 2,246. Do those sum to $36,428?

Card Type: All Created At: 01/01/2015 12:00 AM - 12/31/2015 11:59 PM Created Using: All Customer Location: All Payment Instrument Type: Credit Card, PayPal Account Status: authorized, submitted_for_settlement, settlement_pending, settling, settled, voided Transaction Source: All Transaction Type: sale

kaguillera commented 6 years ago

Now seeing this...don't we have the raw downloads for Braintree in raw https://github.com/gratipay/logs/tree/master/misc/braintree-import

chadwhitacre commented 6 years ago

Yeah, I've got a new one as well now. Thank goodness we at least have participant_id in there. 😅

chadwhitacre commented 6 years ago

Filtering to only settled status brings it down to 2,225.

Card Type: All
Created At: 01/01/2015 12:00 AM - 12/31/2015 11:59 PM
Created Using: All
Customer Location: All
Payment Instrument Type: Credit Card, PayPal Account
Status: settled
Transaction Source: All
Transaction Type: sale
chadwhitacre commented 6 years ago

Okay, close!

Summing the transactions in that last download gives me $37,538.56, off by $1,110.56.

chadwhitacre commented 6 years ago

What about looking at disbursements instead of transactions?

chadwhitacre commented 6 years ago

ಠ_ಠ

$ ./process-disbursements.py 
35550.43
$
chadwhitacre commented 6 years ago

Alright, will pick up there next time!

kaguillera commented 6 years ago

Aren't they charging tax on income? Would disbursements be an expense (sort of)? And should we be taxed for money paid to Gratipay team and not just the money passing through gratipay.com?

chadwhitacre commented 6 years ago

Yes but the question is how do we file with the IRS to show them that that is the situation, because clearly we didn't file in the best way in 2015 so presumably also for 2016.

chadwhitacre commented 6 years ago

Can I dredge up Braintree's 1099-K for us?

chadwhitacre commented 6 years ago

Yes!

Dashboard > Documents > Tax Statements > IRS 1099K - 2015

chadwhitacre commented 6 years ago

Got it! Needed to use "Settled date range" vs. "Creation date range."

Card Type: All Created Using: All Customer Location: All Payment Instrument Type: Credit Card, PayPal Account Settled At: 01/01/2015 12:00 AM - 12/31/2015 11:59 PM Status: settled Transaction Source: All Transaction Type: sale

screen shot 2017-09-20 at 1 02 48 pm

chadwhitacre commented 6 years ago

Transactions in download count to 2,165 and sum to $36,428.04. 👍

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import csv
from decimal import Decimal as D

inp = csv.reader(open('transaction_search.csv'))

headers = next(inp)

total = D('0.00')

for row in inp:
    rec = dict(zip(headers, row))
    amount = rec['Settlement Amount']
    date = rec['Settlement Date']
    participant_id = rec['participant_id']
    try:
        total += D(amount)
    except:
        import pdb; pdb.set_trace()
    print(date, participant_id, amount)
    if not participant_id:
        raise
print(total)
chadwhitacre commented 6 years ago

P.S. Forwarded 1099-K to accountant, still waiting for engagement letter from him.

chadwhitacre commented 6 years ago

Okay! Now to match Braintree transactions to the exchanges table ... :rage1:

chadwhitacre commented 6 years ago

Well, let's see how many we can match on ref, I guess, to start with?

chadwhitacre commented 6 years ago

744 / 2165 = 34% of refs in the Braintree transaction download from 2015 are missing from the exchanges table.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals

import csv
from decimal import Decimal as D
from gratipay import wireup

db = wireup.db(wireup.env())
inp = csv.reader(open('transaction_search.csv'))
headers = next(inp)

known_refs = set(db.all('''

    SELECT ref
      FROM exchanges
     WHERE "timestamp"::text >= '2015-01-01' and "timestamp"::text < '2016-01-01'

'''))

for row in inp:
    rec = dict(zip(headers, row))
    amount = D(rec['Settlement Amount'])
    date = rec['Settlement Date']
    participant_id = rec['participant_id']
    ref = rec['Transaction ID']
    if ref not in known_refs:
        print(ref)
chadwhitacre commented 6 years ago

Of the 1,421 transactions for which we have a ref, the amount and participant_idall match exactly, and the date matches within one day.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals

import csv
import datetime
from decimal import Decimal as D
from gratipay import wireup

db = wireup.db(wireup.env())
inp = csv.reader(open('transaction_search.csv'))
headers = next(inp)

exchanges = {e.ref: e for e in db.all('''

    SELECT ref
         , "timestamp"::date    date
         , amount + fee         amount
         , p.id                 participant_id
      FROM exchanges e
      JOIN participants p
        ON p.username = e.participant
     WHERE "timestamp"::text >= '2015-01-01' and "timestamp"::text < '2016-01-01'

''')}

for row in inp:
    rec = dict(zip(headers, row))
    amount = D(rec['Settlement Amount'])
    m,d,y = map(int, rec['Settlement Date'].split('/'))
    date = datetime.date(y,m,d)
    participant_id = int(rec['participant_id'])
    ref = rec['Transaction ID']

    ours = exchanges.get(ref)
    if ours:
        assert amount == ours.amount, (amount, ours.amount)
        assert (date - ours.date).days <= 1, (date, ours.date)
        assert participant_id == ours.participant_id, (participant_id, ours.participant_id)
chadwhitacre commented 6 years ago

Alright, so the scary possibility here is that we did not record 34% of the Braintree transactions that happened during 2015.

chadwhitacre commented 6 years ago

That is a disheartening thought.

[gratipay] $ run defaults.env local.env ./match-2015.py 
number unmatched: 744
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals

import csv
import datetime
import sys
from collections import defaultdict
from decimal import Decimal as D

from gratipay import wireup

db = wireup.db(wireup.env())
inp = csv.reader(open('transaction_search.csv'))
headers = next(inp)
one_day = datetime.timedelta(days=1)

# load up transactions known to us

gratipay = {e.ref: e for e in db.all('''

    SELECT e.id                 exchange_id
         , ref
         , "timestamp"::date    date
         , amount + fee         amount
         , p.id                 participant_id
      FROM exchanges e
      JOIN participants p
        ON p.username = e.participant
     WHERE "timestamp"::text >= '2015-01-01' and "timestamp"::text < '2016-01-01'

''')}

# loop through transactions known to braintree, looking for those that don't
# have a corresponding match on ref in exchanges: these are the ones we'll need
# to fuzzy-match

braintree = list()
for row in inp:
    rec = dict(zip(headers, row))

    participant_id = int(rec['participant_id'])
    m,d,y = map(int, rec['Settlement Date'].split('/'))
    date = datetime.date(y,m,d)
    amount = D(rec['Settlement Amount'])

    ref = rec['Transaction ID']
    ours = gratipay.pop(ref, None)

    if ours:
        assert participant_id == ours.participant_id, (participant_id, ours.participant_id)
        assert (date - ours.date).days <= 1, (date, ours.date)
        assert amount == ours.amount, (amount, ours.amount)
    else:
        braintree.append((ref, participant_id, date, amount))

# now loop through the ones we need to fuzzy-match, and try to match them up

gratipay_remaining = {(e.participant_id, e.date, e.amount): e.exchange_id for e in gratipay.values()}
possible_matches = defaultdict(list)
unmatched = []

for ref, participant_id, date, amount in braintree:
    guesses = [ (participant_id, date - one_day, amount)
              , (participant_id, date, amount)
              , (participant_id, date + one_day, amount)
               ]
    for guess in guesses:
        exchange_id = gratipay_remaining.get(guess)
        if exchange_id:
            possible_matches[ref].append(exchange_id)
    if ref not in possible_matches:
        unmatched.append(ref)

# look through the matches we found ... write out a csv of exchange_id,ref ...
# we'll use this to backfill the ref column in exchanges ...  if any matches
# failed then say so

ambiguous = []
out = csv.writer(open('matches.csv', 'w+'))
for ref, m in possible_matches.items():
    nmatches = len(m)
    if nmatches > 1:
        ambiguous.append(ref)
    exchange_id = m[0]
    out.writerow((ref, exchange_id))

if unmatched:
    print('number unmatched: {}'.format(len(unmatched)), file=sys.stderr)
if ambiguous:
    print('number ambiguous: {}'.format(len(ambiguous)), file=sys.stderr)
chadwhitacre commented 6 years ago

How about a spot-check of transactions for some given user?

chadwhitacre commented 6 years ago

The funny thing is that we do have participant_id for all of these ... right?

chadwhitacre commented 6 years ago

Okay, so this is encouraging. I grabbed a random item from unmatched, and manually dereferenced the transaction in the Braintree dashboard and the ~user history on Gratipay. I do see the transaction showing up in history as expected. Is there a bug in the match script that explains the misfire?