in quests/develop-apis-apigee/grpc-backend/server.js [488:614]
async function createTransaction(call, callback) {
const customerEmail = call.request.customerEmail;
const accountName = call.request.accountName;
const transactionType = call.request.transactionType;
const amount = call.request.amount;
const toCustomerEmail = call.request.toCustomerEmail;
const toAccountName = call.request.toAccountName;
if (!customerEmail) {
return callback(invalidArg('customerEmail missing'));
}
if (!accountName) {
return callback(invalidArg('accountName missing'));
}
if (!isValidTransactionType(transactionType)) {
return callback(invalidArg(`transactionType must be ${DEPOSIT}, ${WITHDRAWAL}, or ${TRANSFER}.`));
}
if (!isValidCurrency(amount) || amount <= 0) {
return callback(invalidArg(`amount must be positive with a maximum of 2 digits after the decimal point`));
}
if (transactionType === TRANSFER && !toCustomerEmail) {
return callback(invalidArg(`toCustomerEmail missing, required for TRANSFER transactions`));
}
if (transactionType === TRANSFER && !toAccountName) {
return callback(invalidArg(`toAccountName missing, required for TRANSFER transactions`));
}
if (transactionType !== TRANSFER && toCustomerEmail.length > 0) {
return callback(invalidArg(`toCustomerEmail specified, but only allowed for TRANSFER transactions`));
}
if (transactionType !== TRANSFER && toAccountName.length > 0) {
return callback(invalidArg(`toAccountName specified, but only allowed for TRANSFER transactions`));
}
const transactionDocRef = db.collection('transactions').doc();
const transactionDocId = transactionDocRef.id;
const customerAccountPath = `customers/${customerEmail}/accounts/${accountName}`
const transactionData = buildTransaction(call.request);
transactionData.id = transactionDocId;
console.log(`Creating initial transaction ${transactionDocId}: transactionData=${JSON.stringify(transactionData)}`);
await transactionDocRef.create(transactionData).then((result) => {
// success
}).catch((err) => {
const errMessage = `transaction ${transactionDocId}: transaction creation failure`;
return callback(unknown(errMessage));
});
console.log(`Starting firestore transaction`);
try {
await db.runTransaction(async (t) => {
// check account
const accountRef = db.doc(customerAccountPath);
console.log(`getting account ${customerAccountPath}`);
const accountDoc = await t.get(accountRef);
if (!accountDoc.exists) {
console.log(`account ${customerAccountPath} does not exist`);
// account must exist
const errMessage = `source account does not exist`;
t.update(transactionDocRef, { failureMessage: errMessage });
return callback(notFound(`transaction ${transactionDocId}: ${errMessage}`));
}
var accountData = accountDoc.data();
console.log(`accountData=${JSON.stringify(accountData)}`);
var accountBalance = accountData.balance;
var overdraftAllowed = accountData.overdraftAllowed;
console.log(`accountData: balance=${accountBalance}, overdraftAllowed=${overdraftAllowed}`);
var toAccountPath = "";
var toAccountRef = null;
var toAccountDoc = null;
var toAccountData = null;
if (transactionType === TRANSFER) {
// to account is required
toAccountPath = `customers/${toCustomerEmail}/accounts/${toAccountName}`
toAccountRef = db.doc(toAccountPath);
toAccountDoc = await t.get(toAccountRef);
if (!toAccountDoc.exists) {
// to account must exist
const errMessage = `destination account does not exist`;
t.update(transactionDocRef, { failureMessage: errMessage });
return callback(notFound(`transaction ${transactionDocId}: ${errMessage}`));
}
toAccountData = toAccountDoc.data();
}
// get transaction doc (GET required for a firestore transaction)
const transactionDoc = await t.get(transactionDocRef);
var transactionDocData = transactionDoc.data();
var balanceChange;
if (transactionType === TRANSFER || transactionType === WITHDRAWAL) {
balanceChange = -amount;
// check for overdraft
if (!overdraftAllowed && amount > accountBalance) {
// would be overdraft
const errMessage = `insufficient funds`;
t.update(transactionDocRef, { failureMessage: errMessage });
return callback(cancelled(`transaction ${transactionDocId}: ${errMessage}`));
}
t.update(accountRef, {
balance: admin.firestore.FieldValue.increment(balanceChange)
});
if (transactionType === TRANSFER) {
t.update(toAccountRef, {
balance: admin.firestore.FieldValue.increment(-balanceChange)
});
}
} else { // DEPOSIT
balanceChange = amount;
console.log(`depositing ${amount} into account ${account}`);
t.update(accountRef, {
balance: admin.firestore.FieldValue.increment(balanceChange)
});
console.log(`deposited ${amount} into account ${account}`);
}
transactionDocData.success = true;
console.log(`updating transaction ${transactionDocId} with success`);
t.update(transactionDocRef, { success: true, id: transactionDocId });
return callback(null, transactionDocData);
});
} catch (e) {
// transaction failure
return callback(unknown('transaction creation failure'));
}
}