DEV Community

Sahil kashyap
Sahil kashyap

Posted on

1

When migrating to a new payment gateway: how to make the new customer ids

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
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,
};
Enter fullscreen mode Exit fullscreen mode

Image of Datadog

Create and maintain end-to-end frontend tests

Learn best practices on creating frontend tests, testing on-premise apps, integrating tests into your CI/CD pipeline, and using Datadog’s testing tunnel.

Download The Guide

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more