When migrating to a new payment gateway: how to make the new customer ids
Published: 2023-02-16 19:07:04


**Problem**:Previously stripe was being used, new paymentgateway 'square' was introduced in the project.
Now New customer will have the squareID and stripeID.

But what about the backward compaitibilty (the old users)

**Solution** : 
- make a script that generates squareId for all the old users.
-Since paymentGateway have rate limit on customer account creation. 
- Delay the api calls
- Process the old customer in chunks
```js
const mongoose = require("mongoose");
const Customer = mongoose.model("Customer");

const { Client, Environment, ApiError } = require("square");
const crypto = require("crypto");
// const JSONBig = require("json-bigint");
const dev = require("../config/dev.js");

var _ = require('underscore');
const squareAccessToken = dev.squareAccessToken;
const squareLocationId = dev.squareLocationId;

const SQUARE_ACCESS_TOKEN = squareAccessToken;
const locationId = squareLocationId;

const client = new Client({
  accessToken: SQUARE_ACCESS_TOKEN,
  // change Envirnoment on production
  environment: Environment.Sandbox,
  httpClientOptions: {
    timeout: 10000,
    retryConfig: {
      maxNumberOfRetries: 2,
      maximumRetryWaitTime: 1000000,
    },
  },
});

/**
 * Creates square customer
 * @param {*} user 
 * @returns square customer
 */
async function createsquareUser(user) {
  try {
    let squareOBJ = getSquareCustomerOBJ(user);
    let createCustomerResponse = await client.customersApi.createCustomer(
      squareOBJ
    );
    const customer = createCustomerResponse.result.customer;
    // console.log("aquare cus", customer);
    return customer;
  } catch (error) {
    if (error instanceof ApiError) {
      error.result.errors.forEach(function (e) {
        console.log(e.category);
        console.log(e.code);
        console.log(e.detail);
      });
      throw {
        status: 400,
        message: error.result.errors,
      };
    } else {
      console.log("Unexpected error occurred: ", error);
      throw error;
    }
  }
}

/**
 * Make obj for square create customer request
 * @param {*} res 
 * @returns Object
 */
function getSquareCustomerOBJ(res) {
  let customer = {};
  customer.birthday = res.dob;
  customer.emailAddress = res.email;
  customer.givenName = res.name.first;
  customer.familyName = res.name.last;
  // phone number not used cuz square folllows is strict rules,chances of failure decrease on removing it
  // customer.phoneNumber = '+' + res.dialCode + res.phone;

  customer.idempotencyKey = crypto.randomBytes(20).toString("hex");
  // delete customer.phoneNumber;

  return customer;
}

/**
 * Return customer in chunks,cursor based pagination used
 * MAKES SQUARE Customer on each doc 
 * In Customer's doc a key will be added:  
 * paymentProviderUserIds:{squareID:'squareID',stripeID:'stripeID'}
 * @param {*} limit 
 * @param {*} cursor 
 * @returns Paginated data
 */

async function cursorBasedPaginationForCreatingSquareUserId(limit, cursor) {
  let decryptedCursor;
  let customerCollection;
  if (cursor) {
    // decryptedCursor = decrypt(cursor)
    // let decrypedDate = new Date(decryptedCursor * 1000)
    customerCollection = await Customer.find({
      __t:"Customer",
      _id: {
        $lt: mongoose.Types.ObjectId(cursor),
      },
    })
      .sort({ _id: -1 })
      .limit(limit + 1)
      .lean()
      .exec();
  } else {
    customerCollection = await Customer.find({__t:"Customer"})
      .sort({ _id: -1 })
      .lean()
      .limit(limit + 1);
  }
  const hasMore = customerCollection.length === limit + 1;
  let nextCursor = null;
  if (hasMore) {
    const nextCursorRecord = customerCollection[limit];
    nextCursor = nextCursorRecord._id;
    // customerCollection.pop();
  }
  //--------this is used to create SquareUsersId--------------
  
  const deferreds = customerCollection
  .map( customer =>
    // Wrap each request into a function, which will be called
    // sequentially and rate-limited

    // If you want to have both the payload and its results
    // available during post-processing, you can do something like:
    () => {
      // const {payload} = customer;
      return {
        _id:customer._id,
        result: createsquareUser(customer)
      }
    }
    // () => createsquareUser(customer) 
    );
    const result = [];
    for (const next of deferreds) {
      try {
        // Call the wrapper function, which returns `createSquareCustomer` Promise(api),
        const value = await next();
        result.push(value.result);
      } catch(err) {
        console.log(err);
      }
    
      // Rate limit
      await delay(2000);
    }
    // console.log("All result",result);
    let customerData=customerCollection;
    let SquareCustomer = (await Promise.all(result)).map(item => {
      return {
        sqId: item.id,
        emailAddress: item.emailAddress
      };
    });

    //join/merge data based on email
  let mergedData = customerData.map(o => Object.assign(
    {}, o, SquareCustomer.find(o2 => o2["emailAddress"] === o["email"])
));
  // console.log("SquareCustomer",SquareCustomer);
  // console.log("mergedData",mergedData);

    Customer.bulkWrite(
      mergedData.map(({ _id, sqId, stripeID }) => ({
        updateOne: {
          filter: { _id },
          update: {
            $set: {
              paymentProviderUserIds: {
                squareID: sqId,
                stripeID: stripeID,
              },
            },
          },
          upsert: true,
        },
      }))
    )
  return {
    data: customerCollection,
    paging: {
      hasMore,
      nextCursor,
    },
  };
}

/**
 * Recursive fuction loops through all pages in cursor based pagination
 * @param {*} cursor 
 * @param {*} data 
 * @param {*} payload 
 * @returns 
 */
async function getAllCustomerDataFromCursorBasedPagination(
  cursor,
  data = [],
  payload
) {
  try {
    const response = await cursorBasedPaginationForCreatingSquareUserId(
      payload.limit,
      cursor
    );
    if (response && response.data) {
      // if (data.length === 8) {
        
      //   return data;
      // }
      // console.log("response", response.data.length);
      if (response.paging.hasMore === false) {
        data.push(...response.data);

    // console.log("data length", data.length);
        return data;
      }
        data.push(...response.data);
// console.log("response.paging.nextCursor",response.paging.nextCursor);
        return await getAllCustomerDataFromCursorBasedPagination(
          response.paging.nextCursor,
          data,
          payload
        );
      
    }
    return [];
  } catch (error) {
    return error;
  }
}

/**
 * Loops through paginated data
 * @returns All the data
 */
async function getAllData() {
  return await getAllCustomerDataFromCursorBasedPagination(undefined,[],{limit:100});
}

// Helper to await for setTimeout
const delay = async (millis) =>
  new Promise(resolve => setTimeout(resolve, millis));


module.exports = {
  cursorBasedPagination: cursorBasedPaginationForCreatingSquareUserId,
  getAllData: getAllData,
};
```