Org Web Adapter

pages/mmpy_149_handle_transfer_rejections_due_to_bad_account_info.org

ID
ea00767b-3ee4-44a3-a18d-7f64afbf4885

MMPY-149 Handle Transfer rejections due to bad account info

- source :: https://multimediallc.atlassian.net/browse/MMPY-149

- tags :: Falcon Nest Project Falcon Nest Project Story

- status :: Status - Complete

Tasks

DONE Consider whether transition is needed

workmmpy149thinking
ID
56b0b60f-2f3d-9b90-8a17-b71550003c13

- Note taken on

Not sure what I'm doing here. Trying to watch tennis and work at the same time.

- Note taken on

I think it's a good idea to have transitions on the Transfer model,

as it starts to build consistent APIs for managing things that have

state in our repo.

DONE Review work for last week and setup tasks to get it done

workchoremmpy149
ID
4e1c0856-0b21-5991-a333-576e74073906
LINK
https://multimediallc.atlassian.net/browse/MMPY-149

DONE Open pull request for MMPY-149

workprojectmmpy149
LINK
https://multimediallc.atlassian.net/browse/MMPY-149
ID
3a206e7a-95da-ca96-7ee7-cec165bcb7f6

DONE Get transfer rejected on bad send test working

workprojectmmpy149tests
ID
42021020-3027-f43f-f87a-3639cc9b5635

- Note taken on

Finally got tests passing. Once I got the cocktail of mocking correct (using

decorator patches for setup and with patch for the important exception

raising).

Finally, there was actually an implementation bug where I wasn't using

continue to get out of the loop when the transfer errored out.

[[file:~/src/github.com/multimediallc/mm-pay-internal/nest/payout/tests/test_tasks.py::def test_transfers_rejected_on_bad_send(newrelic):][test_transfers_rejected_on_bad_send]]

DONE Make sure we're actually going to get a PayoutError on failed sends

workprojectmmpy149test
ID
d0af9afc-15d7-7b7a-e9a1-6f033d2a975c

DONE Look into query jump on tests

workprojectmmpy149testsqueries
ID
1a6e4752-9902-9e74-7d6a-87713f5ec3f9

- Note taken on

Oh man, [[https://github.com/multimediallc/mm-pay-internal/pull/287#discussion_r2136608756][Gemini actually got it right!]] My logic for when to close accounts also blew up queries as it was checking every payout request and closing accounts. Now I need to write some tests to make sure this all is sane.

DONE Fix missing code coverage

workprojectmmpy149testscoverage
ID
c489c012-ab92-a840-9d23-a477f93291c0

DONE Figure out how to monkeypatch the `create_payout` method on processor in models.py

workproejctmmpy149tests
ID
7a6de3cb-844d-1888-ad20-678769c50d34

DONE Try to get local dev running and setup Stripe locally

workchoremmpy149localdev
ID
b3a03d10-2045-b47d-85ae-054c538672cb

- Note taken on

Okay, so I have the local install running by hijacking the CORS settings in my

local settings file. I think I need to add those to my personal envrc file

and/or figure out why the container is not reading the .env file.

In the meantime, I still don't really know why I can't access the Stripe

sandbox that everyone else has access to. But that's for another day.

DONE Make sure coverage is 100% before undrafting PR

workprojectmmpy149tests
ID
885ea458-7c58-7723-ce0e-61fab46e2e85

DONE Work through the flow for releasing payouts and approving tranfsers

workprojectchoremmpy149
ID
e29b6d0d-24f1-f769-d425-05f44d8e1325

- Note taken on

I mostly figured this out. There some weirdness with getting backends setup properly, but for the most part,

I can create a payout request from the API and then, via the console set the PayoutRequest status to "APPROVED" and

then run `approve_payouts()`, `make_transfers()` and `send_transfers()` and it all goes up to Stripe.

DONE Add robust way to handle different PayoutErrors via a dictionary grouping them, maybe?

featuremmpy149
ID
faa6352f-5009-fb55-129d-88ad63a07ed7

- Note taken on

#+begin_src python

from nest.payout.tasks import *

from nest.payout.tasks import *

from django.utils import timezone

def go():

su = SourceApplicationUser.objects.all().first()

#Hold.objects.filter(application_user=su, removed_at__isnull=True).update(removed_at=timezone.now())

approve_payouts()

#make_transfers()

#send_transfers()

go()

#+end_src

- Note taken on

I *think* the idea here is to have a simple map between what sort of status we

want to catch "bad account info" and what the various flavors of errors could

be given the backend. So if the backend is Stripe, have a

BadAccountInfo("stripe") constant that provides a list of all the errors we

could receive that qualify as bad accounts. Then you can just keep creating

different exceptions for different types, all keyed to the expected response

from the backend.

DONE Fix small PR changes [1/2]

workchoremmpy149
ID
b6c93bcb-9f1a-22c7-49e4-e0cd1b18c366

- [ ] Log actual errors for further investigation

- [X] Create actual BadAccountError instead of PayoutRejectedError

Background

The difference between “rejected”, “returned” and “canceled” has to do with how

far the transfer got before failure. All are terminal states. Notably,

rejections indicate we were not charged for the payout by the processor because

they didn’t accept the request. Returns indicate they tried to pay, but then

something occurred after they accepted the request. With a return, we will

charge the user a fee 2x, since we are charged 2x. With a rejection, the user is

only charged the fee 1x.

For stripe, we can’t be given a “bad account” since the account has to be linked

to our system.

To emulate a “bad account”, we should have an account that is “active” and in an

“open” status, but is pending or rejected on the Stripe side. This could happen

if we miss a “closed account” webhook.

All processors will have this flow.

Requirements

GIVEN

A transfer in released is being sent to be processed

WHEN

The transfer is rejected due to bad account information.

THEN

Mark the transfer as rejected

Detach the payouts

Create inverted ledgers

Close the account

Send an email to the payee telling them their account has been closed. Separate

message for “no other accounts” and “try the other account on file”

This will be a separate ticket. A stub can be put in, or left out entirely.

If no other open account exists, create no valid account hold

if it does exist, mark as active

Add an audit log on the transfer and payout explaining the rejection

Footnotes