This commit adds lookup functions that allows transaction histories to
be found using the Order ID from the customer. The Order ID is a string
that gets included in each email receipt and can be used in customer
support situations.
Currently the function is not exposed in any external APIs. It can only
be used interactively on a Node.js console.
From the perspective of the API and the server functionality, this commit should be a no-op.
-------
Testing
-------
Damus API: This commit
Coverage:
- All automated tests are passing (with no `.env`)
- Typescript type check has no regressions
- New function was tested by using it via Node.js console to help solve a real customer support request
Closes:
https://github.com/damus-io/api/issues/9
Changelog-Added: Add Order ID lookup functions to aid customer support
src/app_store_receipt_verifier.js | 127 +++++++++++++++++++++++-------
1 file changed, 99 insertions(+), 28 deletions(-)
diff --git a/src/app_store_receipt_verifier.js b/src/app_store_receipt_verifier.js
index fddcbfc..eabfc0f 100644
--- a/src/app_store_receipt_verifier.js
+++ b/src/app_store_receipt_verifier.js
@@ -12,6 +12,20 @@ const { current_time } = require("./utils")
const fs = require('fs')
const debug = require('debug')('api:iap')
+const DEFAULT_ROOT_CA_DIR = './apple-root-ca'
+/**
+ * Mock transaction history for testing purposes.
+ * @type {Transaction[]}
+ */
+const MOCK_TRANSACTION_HISTORY = [{
+ type: "iap",
+ id: "1",
+ start_date: current_time(),
+ end_date: current_time() + 60 * 60 * 24 * 30,
+ purchased_date: current_time(),
+ duration: null
+}];
+
/**
* Verifies the receipt data and returns the expiry date if the receipt is valid.
*
@@ -25,18 +39,11 @@ async function verify_receipt(receipt_data, authenticated_account_token) {
// Mocking logic for testing purposes
if (process.env.MOCK_VERIFY_RECEIPT == "true") {
debug("Mocking verify_receipt with expiry date 30 days from now");
- return [{
- type: "iap",
- id: "1",
- start_date: current_time(),
- end_date: current_time() + 60 * 60 * 24 * 30,
- purchased_date: current_time(),
- duration: null
- }]
+ return MOCK_TRANSACTION_HISTORY;
}
// Setup the environment and client
- const rootCaDir = process.env.IAP_ROOT_CA_DIR || './apple-root-ca'
+ const rootCaDir = process.env.IAP_ROOT_CA_DIR || DEFAULT_ROOT_CA_DIR;
const bundleId = process.env.IAP_BUNDLE_ID;
const environment = getAppStoreEnvironmentFromEnv();
const client = createAppStoreServerAPIClientFromEnv();
@@ -65,18 +72,11 @@ async function verify_transaction_id(transaction_id, authenticated_account_token
// Mocking logic for testing purposes
if (process.env.MOCK_VERIFY_RECEIPT == "true") {
debug("Mocking verify_receipt with expiry date 30 days from now");
- return [{
- type: "iap",
- id: "1",
- start_date: current_time(),
- end_date: current_time() + 60 * 60 * 24 * 30,
- purchased_date: current_time(),
- duration: null
- }];
+ return MOCK_TRANSACTION_HISTORY;
}
// Setup the environment and client
- const rootCaDir = process.env.IAP_ROOT_CA_DIR || './apple-root-ca'
+ const rootCaDir = process.env.IAP_ROOT_CA_DIR || DEFAULT_ROOT_CA_DIR;
const bundleId = process.env.IAP_BUNDLE_ID;
const environment = getAppStoreEnvironmentFromEnv();
const client = createAppStoreServerAPIClientFromEnv();
@@ -89,6 +89,35 @@ async function verify_transaction_id(transaction_id, authenticated_account_token
}
+/**
+ * Looks up the order id and returns the verified transaction history if valid
+ *
+ * @param {string} order_id - The order id to lookup
+ *
+ * @returns {Promise<Transaction[]|null>} The validated transactions
+ */
+async function lookup_order_id(order_id) {
+ debug("Looking up order id '%d'", order_id);
+ // Mocking logic for testing purposes
+ if (process.env.MOCK_VERIFY_RECEIPT == "true") {
+ debug("Mocking verify_receipt with expiry date 30 days from now");
+ return MOCK_TRANSACTION_HISTORY;
+ }
+
+ // Setup the environment and client
+ const rootCaDir = process.env.IAP_ROOT_CA_DIR || DEFAULT_ROOT_CA_DIR;
+ const bundleId = process.env.IAP_BUNDLE_ID;
+ const environment = getAppStoreEnvironmentFromEnv();
+ const client = createAppStoreServerAPIClientFromEnv();
+
+ // If the transaction ID is present, fetch the transaction history, verify the transactions, and return the latest expiry date
+ if (order_id != null) {
+ return await fetchValidatedTransactionsFromOrderId(client, order_id, rootCaDir, environment, bundleId, undefined);
+ }
+ return Promise.resolve(null);
+}
+
+
/**
* Fetches transaction history with the App Store API, verifies the transactions, and returns formatted transactions.
* It also verifies if the transaction belongs to the account who made the request.
@@ -114,17 +143,59 @@ async function fetchValidatedTransactions(client, transactionId, rootCaDir, envi
return null;
}
return validDecodedTransactions.map((decodedTransaction) => {
- return {
- type: "iap",
- id: decodedTransaction.transactionId,
- start_date: decodedTransaction.purchaseDate / 1000,
- end_date: decodedTransaction.expiresDate / 1000,
- purchased_date: decodedTransaction.purchaseDate / 1000,
- duration: null
- }
+ return transformDecodedTransactionToTransaction(decodedTransaction)
})
}
+/**
+ * Fetches transaction history associated with an Order ID with the App Store API, verifies the transactions, and returns formatted transactions.
+ * An Order ID is a unique identifier for a transaction that is generated by the App Store and is included in receipt emails, so it is useful for customer support.
+ *
+ * @param {AppStoreServerAPIClient} client - The App Store API client.
+ * @param {string} orderId - The order ID to fetch history for.
+ * @param {string} rootCaDir - The directory containing Apple root CA certificates for verification.
+ * @param {Environment} environment - The App Store environment.
+ * @param {string} bundleId - The bundle ID of the app.
+ * @param {string | undefined} authenticatedAccountToken - The UUID account token of the user who is authenticated in this request. If undefined, UUID authentication will be skipped.
+ *
+ * @returns {Promise<Transaction[]|null>} The validated transactions
+*/
+async function fetchValidatedTransactionsFromOrderId(client, orderId, rootCaDir, environment, bundleId, authenticatedAccountToken) {
+ const transactions = await client.lookUpOrderId(orderId);
+ debug("[Order ID: %s] Fetched transaction history; Found %d transactions", orderId, transactions.signedTransactions);
+ const rootCAs = readCertificateFiles(rootCaDir);
+ const decodedTransactions = await verifyAndDecodeTransactions(transactions.signedTransactions, rootCAs, environment, bundleId);
+ debug("[Order ID: %s] Verified and decoded %d transactions", orderId, decodedTransactions.length);
+ const validDecodedTransactions = authenticatedAccountToken == undefined ? decodedTransactions : filterTransactionsThatBelongToAccount(decodedTransactions, authenticatedAccountToken);
+ if (authenticatedAccountToken != undefined) {
+ debug("[Account token: %s] Filtered transactions that belong to the account UUID. Found %d matching transactions", authenticatedAccountToken, validDecodedTransactions.length);
+ }
+ if (validDecodedTransactions.length === 0) {
+ return null;
+ }
+ return validDecodedTransactions.map((decodedTransaction) => {
+ return transformDecodedTransactionToTransaction(decodedTransaction)
+ })
+}
+
+/**
+ * Transforms a JWSTransactionDecodedPayload into a Transaction object
+ *
+ * @param {JWSTransactionDecodedPayload} decodedTransaction - The decoded transaction to transform.
+ * @returns {Transaction} The transformed transaction.
+ *
+ */
+function transformDecodedTransactionToTransaction(decodedTransaction) {
+ return {
+ type: "iap",
+ id: decodedTransaction.transactionId,
+ start_date: decodedTransaction.purchaseDate / 1000,
+ end_date: decodedTransaction.expiresDate / 1000,
+ purchased_date: decodedTransaction.purchaseDate / 1000,
+ duration: null
+ }
+}
+
/**
* Filters out transactions that do not belong to the authorized account token.
*
@@ -168,7 +239,7 @@ function readCertificateFiles(directory) {
* @param {Buffer[]} rootCAs - Apple root CA certificate contents for verification.
* @param {Environment} environment - The App Store environment.
* @param {string} bundleId - The bundle ID of the app.
- * @returns {Promise<Object[]>} The decoded transactions.
+ * @returns {Promise<JWSTransactionDecodedPayload[]>} The decoded transactions.
*/
async function verifyAndDecodeTransactions(transactions, rootCAs, environment, bundleId) {
const verifier = new SignedDataVerifier(rootCAs, true, environment, bundleId);
@@ -254,5 +325,5 @@ function getAppStoreEnvironmentFromEnv() {
}
module.exports = {
- verify_receipt, verify_transaction_id
+ verify_receipt, verify_transaction_id, lookup_order_id
};
--
2.44.0