How to properly account for system failure when deducting a users balance and calling a 3rd party service? - postgresql

Presume a user would like to withdraw $5.00 balance on a website for PayPal balance. So they hit a withdraw endpoint which makes sure they have enough balance, calls PayPal pay api, and deducts the users on-site balance in a single serializable transaction.
What would happen if the database server drops and the transaction fails to commit after the PayPal pay request is executed successfully and the users gets their on-site balance back?
Is there a way I can encapsulate all of these calls in one atomic transaction?

I assume you are looking for two-phase commit
https://www.postgresql.org/docs/current/static/sql-prepare-transaction.html
PREPARE TRANSACTION prepares the current transaction for two-phase
commit. After this command, the transaction is no longer associated
with the current session; instead, its state is fully stored on disk,
and there is a very high probability that it can be committed
successfully, even if a database crash occurs before the commit is
requested.
Once prepared, a transaction can later be committed or rolled back
with COMMIT PREPARED or ROLLBACK PREPARED, respectively.
(empasis mine)

You shouldn't try to do this atomically. The nature of internet APIs makes this impossible.
You should probably do something resembling this pseudocode:
payment_id = random_payment_id()
try:
db:
insert into payments (payment_id, order_id, payment_amount, status, created)
values (:payment_id, :order_id, :payment_amount, 'pending', now());
commit;
remote.create_payment(payment_id, payment_amount);
except remote.error:
throw payment_error
On payment confirmation:
try:
remote.execute_payment(payment_id);
db:
update payments set status='completed' where payment_id=:payment_id;
commit;
except remote.error:
throw payment_error
And periodically you have to check a status of 'pending' payments, as you can't be sure that you'll receive all payment confirmations:
db:
select payment_id from payments
where status='pending' and created<now()-'10 minutes';
for payment_id in db.result:
if remote.payment_status(payment_id) == 'approved':
remote.execute_payment(payment_id);
db:
update payments set status='completed' where payment_id=:payment_id;
commit;
You should also periodically clean expired unconfirmed payments:
db:
select payment_id from payments
where status='pending' and created<now()-'10 days';
for payment_id in db.result:
remote.cancel_payment(payment_id);
db:
update payments set status='failed' where payment_id=:payment_id;
commit;

Related

making multiple operations atomic in sqflite

I am creating a mobile application in flutter where I need to do entries in the transaction table and also need to update the balance in the user table. Whenever a new transaction is to be added I first check the current balance of the user, add the entry in the transaction table and update the balance. My question is how can I make the entire operation atomic. Say if the update balance fails because for any reason how can I roll back from the transaction table also.
That is exactly what SQLite transactions are for! (Sorry for the unfortunate name of your table which is also called transaction)
More info on how to use transactions in sqflite here:
https://github.com/tekartik/sqflite/blob/master/sqflite/doc/sql.md#transaction
some copy/pasted information for convenience:
transaction
transaction handle the 'all or nothing' scenario. If one command fails (and throws an error), all other commands are reverted.
await db.transaction((txn) async {
await txn.insert('my_table', {'name': 'my_name'});
await txn.delete('my_table', where: 'name = ?', whereArgs: ['cat']);
});
Make sure to sure the inner transaction object - txn in the code above - is used in a transaction (using the db object itself will cause a deadlock),
You can throw an error during a transaction to cancel a transaction,
When an error is thrown during a transaction, the action is cancelled right away and previous commands in the transaction are reverted,
No other concurrent modification on the database (even from an outside process) can happen during a transaction,
The inner part of the transaction is called only once, it is up to the developer to handle a try-again loop - assuming it can succeed at some point.

How to merge a sponsored stellar account with '0' XLM balance

I create a sponsored account with '0' XLM balance. To remove it using the sponsoring account, I use the accountMerge operation, but the subsequent endSponsoringFutureReserves operation fails to access the merged account - preventing the whole transaction from running. If I create a sponsored account with '0.0002' XLM balance, I can merge it when it is the transaction's source. Can the merging be done by the sponsoring account instead? TIA
You can create a regular (non-sponsored) transaction with the account_merge operation and then wrap that transaction in a FeeBumpTransaction paid by any (e.g. the sponsoring) account.

Database model design for single entry transaction between two accounts?

I am building an app that helps people transfer money from one account to another. I have two tables "Users" and "Transactions". The way I am currently handling transfers is by
Check if the sender has enough balance to make a transfer.
Deduct the balance from the sender and update the sender's balance.
Add the amount deducted from the sender's account to the recipient's account and then update the balance of the recipient.
Then finally write the transaction record on the "Transactions" table as a single entry like below:
id | transactionId | senderAccount | recipientAccount | Amount |
—--+---------------+---------------+------------------+--------+
1 | ijiej33 | A | B | 100 |
so my question is, is recording a transaction as a single entry like above a good practice or will this kind of database model design produce future challenges?
Thanks
Check if the sender has enough balance to make a transfer.
Deduct the balance from the sender and update the sender's balance.
Yes, but.
If two concurrent connections attempt to deduct money from the sender at the same time, they may both successfully check that there is enough money for each transaction on its own, then succeed even though the balance is insufficient for both transactions to succeed.
You must use a SELECT FOR UPDATE when checking. This will lock the row for the duration of the transaction (until COMMIT or ROLLBACK), and any concurrent connection attempting to also SELECT FOR UPDATE on the same row will have to wait.
Presumably the receiver account can always receive money, so there is no need to lock it explicitly, but the UPDATE will lock it anyway. And locks must always be acquired in the same order or you will get deadlocks.
For example if a transatcion locks rows 1 then 2, while another locks rows 2 then 1: the first one will lock 1, the second will lock 2, then the first will try to lock 2 but it is already locked, and the second will try to lock 1 but it is also already locked by the other transaction. Both transactions will wait for each other forever until the deadlock detector nukes one of them.
One simple way to dodge this is to use ORDER BY:
SELECT ... FROM users WHERE user_id IN (sender_id,receiver_id)
ORDER BY user_id FOR UPDATE;
This will lock both rows in the order of their user_ids, which will always be the same.
Then you can do the rest of the procedure.
Since it is always a good idea to hold locks for the shortest amount of time, I'd recommend to put the whole thing inside a plpgsql stored procedure, including the COMMIT/ROLLBACK and error handling. Try to make the stored procedure failsafe and atomic.
Note, for security purposes, you should:
Store the balance of both accounts before the money transfer occured into the transactions table. You're already SELECT'ing it in the SELECT for update, might as well use it. It will be useful for auditing.
For security, if a user gets their password stolen there's not much you can do, but if your application gets hacked it would be nice if the hacker was not able to issue global UPDATEs to all the account balances, mess with the audit tables, etc. This means you need to read up on this and create several postgres users/roles with suitable permissions for backup, web application, etc. Some tables and especially the transactions table should have all UPDATE privileges revoked, and INSERT allowed only for the transactions stored procs, for example. The aim is to make the audit tables impossible to modify, basically append-only from the point of view of the application code.
Likewise you can handle updates to balance via stored procedures and forbid the web application role from messing with it. You could even add take a user-specific security token passed as a parameter to the stored proc, to authenticate the app user to the database, so the database only allows transfers from the account of the user who is logged in, not just any user.
Basically if it involves money, then it involves legislation, and you have to think about how not to go to jail when your web app gets hacked.

Paypal Billing Agreement ID validity

I want to know that, however the reference transaction must have occurred within the past 730 days because the Billing Agreement ID may not be available after a two years. So if first transaction is done before expiry lets say after 600 days so the ID will be available until 130 days or it will again available for 730 days?
If you have the billing agreement id then its valid untill you or your customer cancels it . So its best practice to use the BA id for PayPal transactions .
For direct credit card payment you have to use the transaction id(as BA id is only if some one pays via Paypal).
Whenever you use the reference transaction on an existing transaction, the resulting new transaction id will have the new validity for another 730 days and this way you won't have to worry for 730 days limitation. So it's best to update your database with the latest transaction id whenever you do the reference transaction .
https://developer.paypal.com/webapps/developer/docs/classic/express-checkout/integration-guide/ECReferenceTxns/#id094TB0Y0J5Z__id094TB4003HS

associate fee reverstal with transaction

I am using transaction search to return a list of transaction. With refunds it is returning two lines the refund and the fee reversal. There is no detailed info available for the fee reversal transaction, so how can I associate the reversal with the refund?
Use the oarent_txn field. It refers to the txn_id of the original transaction which has been reversed, and which you have already received and should have saved.