Skip to content

Instantly share code, notes, and snippets.

@exoer
Last active June 16, 2020 09:47
Show Gist options
  • Save exoer/eee2aa37c86c06190f12ef4e2c6bd1c6 to your computer and use it in GitHub Desktop.
Save exoer/eee2aa37c86c06190f12ef4e2c6bd1c6 to your computer and use it in GitHub Desktop.
Mongo Transactions
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]
},
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})
},
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