Last active
June 16, 2020 09:47
-
-
Save exoer/eee2aa37c86c06190f12ef4e2c6bd1c6 to your computer and use it in GitHub Desktop.
Mongo Transactions
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const AccountCollection = mongoDb.collection('Accounts') | |
await AccountCollection.createIndex({ userId: 1 }); | |
const accountStore = { | |
createAccount: async({name, userId, balance=0, id= Id.makeId(), createdAt=Date.now(), updatedAt=Date.now(), isDefault=true}) => | |
{ | |
if (!userId) throw new Error('createAccount userId missing') | |
if ( isNaN(Number(balance))) throw new Error('invalid balance NaN') | |
const account = { | |
id, | |
balance: Number(balance), | |
giftedCredits: 0, | |
userId: userId, | |
name: name, | |
pendingTransactions: [], | |
createdAt, | |
updatedAt, | |
isDefault, | |
} | |
const {id: _id, ...rest} = account | |
const account_doc = {_id, ...rest} | |
const response = await AccountCollection.insertOne(account_doc) | |
const a = response.ops ? _normalizeMongo(response.ops[0]) : null | |
return a | |
}, | |
debitAccount: async({transaction})=> { | |
if (!transaction.id) throw new Error(`debitAccount: Error transction.id missing ${transaction.id}`,) | |
const acc = await AccountCollection.findOneAndUpdate( | |
{ | |
_id: transaction.source, | |
pendingTransactions: {$ne: transaction.id}, | |
balance: {$gte: transaction.amount} | |
}, | |
{ | |
$inc: {balance: -transaction.amount}, | |
$push: {pendingTransactions: transaction.id}, | |
$currentDate: {updatedAt: true} | |
}, | |
{returnOriginal: false} | |
) | |
return acc && acc.value ? _normalizeMongo(acc.value) : null | |
}, | |
debitOverdrawAccount: async({transaction})=> { | |
if (!transaction.id) throw new Error(`debitAccount: Error transction.id missing ${transaction.id}`,) | |
const acc = await AccountCollection.findOneAndUpdate( | |
{ | |
_id: transaction.source, | |
pendingTransactions: {$ne: transaction.id}, | |
// balance: {$gte: transaction.amount} | |
}, | |
{ | |
$inc: {balance: -transaction.amount}, | |
$push: {pendingTransactions: transaction.id}, | |
$currentDate: {updatedAt: true} | |
}, | |
{returnOriginal: false} | |
) | |
return acc && acc.value ? _normalizeMongo(acc.value) : null | |
}, | |
reverseDebitAccount: async({transaction})=> { | |
if (!transaction.id) throw new Error(`reverseDebitAccount: Error transction.id missing ${transaction.id}`,) | |
const acc = await AccountCollection.findOneAndUpdate( | |
{ | |
_id: transaction.source, | |
pendingTransactions: {$in: [transaction.id] } }, | |
{ | |
$inc: {balance: transaction.amount}, | |
$pull: {pendingTransactions: transaction.id}, | |
$currentDate: {updatedAt: true}}, | |
{returnOriginal: false} | |
) | |
return acc && acc.value ? _normalizeMongo(acc.value) : null | |
}, | |
creditAccount: async ({transaction, accountId=null})=> { | |
if (!transaction.id) throw new Error('creditAccount transction.id missing') | |
const amt = transaction.fee ? transaction.amount - transaction.fee : transaction.amount | |
const giftedCredits = transaction.gifted ? transaction.amount : 0 | |
const acc = await AccountCollection.findOneAndUpdate( | |
{ | |
_id: accountId || transaction.destination, | |
pendingTransactions: {$ne: transaction.id } | |
}, | |
{ | |
$inc: {balance: amt, giftedCredits: giftedCredits }, | |
$push: {pendingTransactions: transaction.id}, | |
$currentDate: {updatedAt: true}}, | |
{returnOriginal: false} | |
) | |
return acc && acc.value ? _normalizeMongo(acc.value) : null | |
}, | |
creditAccountFee: async ({transaction, accountId })=> { | |
if (!transaction.id) throw new Error('creditAccount transction.id missing') | |
const acc = await AccountCollection.findOneAndUpdate( | |
{ | |
_id: accountId, | |
pendingTransactions: {$ne: transaction.id } | |
}, | |
{ | |
$inc: {balance: transaction.fee }, | |
$push: {pendingTransactions: transaction.id}, | |
$currentDate: {updatedAt: true}}, | |
{returnOriginal: false} | |
) | |
return acc && acc.value ? _normalizeMongo(acc.value) : null | |
}, | |
reverseCreditAccount: async({transaction, accountId=null})=> { | |
const amt = transaction.fee ? transaction.amount - transaction.fee : transaction.amount | |
const giftedCredits = transaction.gifted ? transaction.amount : 0 | |
const acc = await AccountCollection.findOneAndUpdate( | |
{ | |
_id: accountId || transaction.destination, | |
pendingTransactions: {$in: [transaction.id] } | |
}, | |
{ | |
$inc: {balance: -amt, giftedCredits: -giftedCredits}, | |
$pull: {pendingTransactions: transaction.id}, | |
$currentDate: {updatedAt: true} | |
}, | |
{returnOriginal: false} | |
) | |
return acc && acc.value ? _normalizeMongo(acc.value) : null | |
}, | |
reverseGiftCreditAccount: async({transaction})=> { | |
const acc = await AccountCollection.findOneAndUpdate( | |
{ | |
_id: transaction.destination, | |
pendingTransactions: {$in: [transaction.id] } | |
}, | |
{ | |
$inc: {balance: -transaction.amount}, | |
$inc: {giftedCredits: -transaction.amount}, | |
$pull: {pendingTransactions: transaction.id}, | |
$currentDate: {updatedAt: true} | |
}, | |
{returnOriginal: false} | |
) | |
return acc && acc.value ? _normalizeMongo(acc.value) : null | |
}, | |
removePending: async({transaction, accountId=null}) => { | |
const accS = await AccountCollection.findOneAndUpdate( | |
{_id: transaction.source}, | |
{$pull: {pendingTransactions: transaction.id}, $currentDate: {updatedAt: true}}, | |
{returnOriginal: false} | |
) | |
const source = accS && accS.value ? _normalizeMongo(accS.value) : null | |
const accD = await AccountCollection.findOneAndUpdate( | |
{_id: transaction.destination}, | |
{$pull: {pendingTransactions: transaction.id}, $currentDate: {updatedAt: true}}, | |
{returnOriginal: false} | |
) | |
const dest = accD && accD.value ? _normalizeMongo(accD.value) : null | |
if (accountId) { | |
const accX = await AccountCollection.findOneAndUpdate( | |
{_id: accountId}, | |
{$pull: {pendingTransactions: transaction.id}, $currentDate: {updatedAt: true}}, | |
{returnOriginal: false} | |
) | |
} | |
return [source, dest] | |
}, |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
async function _transferCredits({source, destination, toUserId, fromUserId, amount, fee, descriptionSource, descriptionDestination, overdraw=false, gifted=false, idempotencyKey}) { | |
if (!source) throw new Error('creditsAccess transferCredits source missing') | |
if (!COMPANY_ACCOUNTS.includes(source) && !Id.isValidId(source)) throw new Error(`ransferCredit source must be of type Id ${COMPANY_ACCOUNTS.includes(source)}, ${Id.isValidId(source)}, ${source}`) | |
if (!destination) throw new Error('creditsAccess transferCredits destination missing') | |
if (!Id.isValidId(destination)) throw new Error('transferCredit destination must be of type Id') | |
if (!amount) throw new Error('creditsAccess transferCredits amount missing') | |
let transaction | |
try { | |
transaction = await transactionStore.insertTransaction({source, destination, toUserId, fromUserId, amount, fee, descriptionDestination, descriptionSource, gifted, idempotencyKey}) | |
const transaction_pending= await transactionStore.patchTransaction({transaction: transaction, change: {state: 'pending'}}) | |
if (!transaction_pending) throw new PendingError() | |
const debAccount = overdraw ? await accountStore.debitOverdrawAccount({transaction}) : await accountStore.debitAccount({transaction}) | |
if (!debAccount) throw new DebitError() | |
const credAccount = await accountStore.creditAccount({transaction}) | |
if (!credAccount) throw new CreditError() | |
if (fee && fee>0) { | |
const feeAcc = await accountStore.creditAccountFee({transaction, accountId: FEE_ACCOUNT}) | |
if (!feeAcc) throw new CreditFeeError() | |
} | |
const transaction_comitted = await transactionStore.patchTransaction({transaction: transaction, change: {state: 'comitted'}}) | |
if (!transaction_comitted) throw new ComittingError() | |
const [src, dest] = await accountStore.removePending({transaction: transaction_comitted}) | |
if (!src || !dest) throw new RemovePendingError() | |
const transaction_done = await transactionStore.patchTransaction({transaction: transaction, change: {state: 'done'}}) | |
return transaction_done | |
} catch (e) { | |
if (e instanceof PendingError) { | |
const transaction_cancel = await transactionStore.patchTransaction({transaction: transaction, change: {state: 'cancel'}}) | |
return transaction_cancel | |
} | |
else if (e instanceof DebitError) { | |
const revDeb = await accountStore.reverseDebitAccount({transaction}) | |
const transaction_cancel = await transactionStore.patchTransaction({transaction: transaction, change: {state: 'cancel'}}) | |
return transaction_cancel | |
} | |
else if (e instanceof CreditError) { | |
const revDeb = await accountStore.reverseDebitAccount({transaction}) | |
const revCred = await accountStore.reverseCreditAccount({transaction}) | |
const transaction_cancel = await transactionStore.patchTransaction({transaction: transaction, change: {state: 'cancel'}}) | |
return transaction_cancel | |
} | |
else if (e instanceof CreditFeeError) { | |
const revDeb = await accountStore.reverseDebitAccount({transaction}) | |
const revCred = await accountStore.reverseCreditAccount({transaction}) | |
const revCredFee = await accountStore.reverseCreditAccount({transaction, accountId: FEE_ACCOUNT}) | |
const transaction_cancel = await transactionStore.patchTransaction({transaction: transaction, change: {state: 'cancel'}}) | |
return transaction_cancel | |
} | |
else if (e instanceof ComittingError) { | |
const revDeb = await accountStore.reverseDebitAccount({transaction}) | |
const revAcc = await accountStore.reverseCreditAccount({transaction}) | |
const transaction_cancel = await transactionStore.patchTransaction({transaction: transaction, change: {state: 'cancel'}}) | |
return transaction_cancel | |
} | |
else if (e instanceof RemovePendingError) { | |
const revDeb = await accountStore.reverseDebitAccount({transaction}) | |
const revAcc = await accountStore.reverseCreditAccount({transaction}) | |
const transaction_cancel = await transactionStore.patchTransaction({transaction: transaction, change: {state: 'cancel'}}) | |
return transaction_cancel | |
} else { | |
console.error(e, transaction) | |
throw new Error(e) | |
} | |
} | |
} | |
.... | |
addGiftCredits : async({destination, toUserId, amount, description}) => { | |
if (toUserId && !destination) { | |
const destinationAccount = await accountStore.getDefaultAccountForUser({userId: toUserId}) | |
destination = destinationAccount.id | |
} | |
if (destination && !toUserId) { | |
const destinationAccount = await accountStore.findOne({id: destination}) | |
toUserId = destinationAccount ? destinationAccount.userId : 'unknown' | |
} | |
if (!Id.isValidId(destination)) throw new Error('transferCredit destination must be of type Id') | |
if (!amount) throw new Error('creditsAccess transferCredits amount missing') | |
const source = GIFTED_CREDITS_ACCOUNT | |
return _transferCredits({source, destination, toUserId, fromUserId: '7Stride gift', amount, fee: 0, descriptionSource:'gifted credist', descriptionDestination: description, overdraw: true, gifted: true}) | |
}, |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const TransactionCollection = mongoDb.collection('Transactions') | |
TransactionCollection.createIndex({source: 1, destination: 1, idempotencyKey: 1}) | |
const transactionStore = { | |
insertTransaction: async ({id= Id.makeId(), source, destination, toUserId, fromUserId, amount, fee, state='initial', descriptionSource, descriptionDestination, gifted=false, createdAt=Date.now(), updatedAt= Date.now(), idempotencyKey})=> { | |
if (source === destination) throw new Error('insertTransaction source cannot equal destination') | |
const doc = { | |
_id: id, | |
amount: amount, | |
fee: fee, | |
source, | |
destination, | |
toUserId, | |
fromUserId, | |
state, | |
createdAt, | |
updatedAt, | |
gifted, | |
descriptionSource, | |
descriptionDestination, | |
idempotencyKey | |
} | |
const response = await TransactionCollection.insertOne(doc) | |
const t = response.ops ? _normalizeMongo(response.ops[0]) : null | |
return t | |
}, | |
patchTransaction: async({transaction, change}) => { | |
const {id: _id, ...rest} = transaction | |
const query = _id ? {_id: _id} : rest | |
const t = await TransactionCollection.findOneAndUpdate(query, {$set: change, $currentDate: {updatedAt: true}}, {returnOriginal: false}) | |
return t && t.value ? _normalizeMongo(t.value) : null | |
}, |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment