Sometimes I'm getting duplicated documents from a callable function that looks like this:
const { default: Big } = require('big.js');
const { firestore } = require('firebase-admin');
const functions = require('firebase-functions');
const { createLog } = require('./utils/createLog');
const { payCart } = require('./utils/payCart');
const { unlockCart } = require('./utils/unlockCart');
exports.completeRechargedTransaction = functions.https.onCall(
async (data, context) => {
try {
if (!context.auth) {
throw new functions.https.HttpsError(
'unauthenticated',
'unauthenticated'
);
}
const requiredProperties = [
'foo',
'bar',
'etc'
];
const isDataValid = requiredProperties.every(prop => {
return Object.keys(data).includes(prop);
});
if (!isDataValid) {
throw new functions.https.HttpsError(
'failed-precondition',
'failed-precondition'
);
}
const transactionRef = firestore()
.collection('transactions')
.doc(data.transactionID);
const userRef = firestore().collection('users').doc(data.paidBy.userID);
let currentTransaction = null;
await firestore().runTransaction(async transaction => {
try {
const transactionSnap = await transaction.get(transactionRef);
if (!transactionSnap.exists) {
throw new functions.https.HttpsError(
'not-found',
'not-found'
);
}
const transactionData = transactionSnap.data();
if (transactionData.status !== 'recharged') {
throw new functions.https.HttpsError(
'invalid-argument',
'invalid-argument'
);
}
if (transactionData.type !== 'recharge') {
throw new functions.https.HttpsError(
'invalid-argument',
'invalid-argument'
);
}
if (transactionData.paidBy === null) {
throw new functions.https.HttpsError(
'invalid-argument',
'invalid-argument',
);
}
const userSnap = await transaction.get(userRef);
if (!userSnap.exists) {
throw new functions.https.HttpsError(
'not-found',
'not-found',
);
}
const userData = userSnap.data();
const newUserPoints = new Big(userData.points).plus(data.points);
if (!data.isGoldUser) {
transaction.update(userRef, {
points: parseFloat(newUserPoints.toFixed(2))
});
}
currentTransaction = {
...data,
remainingBalance: parseFloat(newUserPoints.toFixed(2)),
status: 'completed'
};
transaction.update(transactionRef, currentTransaction);
} catch (error) {
console.error(error);
throw error;
}
});
const { paymentMethod } = data.rechargeDetails;
let cashAmount = 0;
if (paymentMethod && paymentMethod.paymentMethod === 'cash') {
cashAmount = data.points;
}
let cartResponse = null;
if (
data.rechargeDetails.isProcessingCart &&
Boolean(data.paidBy.userID) &&
!data.isGoldUser
) {
cartResponse = await payCart(context, data.paidBy.userID, cashAmount);
// This is the function that does all the writes and for some reason it is getting
// called twice or thrice in some rare cases, and I'm pretty much sure that
// The Angular Client is only calling this function "completeRechargedTransaction " once.
}
await createLog({
message: 'Success',
createdAt: new Date(),
type: 'activity',
collectionName: 'transactions',
callerID: context.auth.uid || null,
docID: transactionRef.id
});
return {
code: 200,
message: 'Success',
transaction: currentTransaction,
cartResponse
};
} catch (error) {
console.error(error);
await unlockCart(data.paidBy.userID);
await createLog({
message: error.message,
createdAt: new Date(),
type: 'error',
collectionName: 'transactions',
callerID: context.auth.uid || null,
docID: data.transactionID,
errorSource:
'completeRechargedTransaction'
});
throw error;
}
}
);
I'm reading a lot of firebase documentation, but I can't find a solution to implement idempotency on my callable functions, the context parameter in callable function is very different from background functions and triggers, the callable context looks like this:
https://firebase.google.com/docs/reference/functions/providers_https_.callablecontext
I did find a helpful blogpost to implement idempotency with firebase triggers:
Cloud Functions pro tips: Building idempotent functions
But I don't fully understand this approach because I think it's assuming that the document writes are made on the client aka the front end application, and I don't really think that's a good approach because is it too reliant on the client and I'm afraid of security issues as well.
So yeah, I would like to know is there's a way to implement Idempotency on Callable Functions, I need something like an EventID but for callable functions to safely implement payments on my app and third party apis, such as stripe.
I will appreciate any help or hint you can give me.
from How can I make an Idempotent Callable Function with Firebase Firestore?
No comments:
Post a Comment