method/adminCreate.js

/**
 * @module method/adminCreate
 */

const admin = require('firebase-admin');
const functions = require('firebase-functions');
const axios = require('axios');
const {assign, has, pick} = require('lodash');
const {format} = require('date-fns');

const db = admin.firestore();

const {loandisk} = functions.config();
const config = {
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Basic ${loandisk.authcode}`,
  },
};

const errorMessages = require('../error/error.json');

/**
 * Create a new user on Firebase with borrower creation on Loandisk
 *
 * @param  {object} user - User object
 * @param  {string} user.uid - Firebase User ID, e.g. hk_wx5555556.
 * @param  {number} user.branchID - Branch ID of target branch where the user belongs to.
 * @param  {string} user.country - Country of employment, e.g. HK.
 * @param  {string} user.idNumber - ID number at country of employment, e.g. WX555555(6).
 * @param  {string} user.passwordHash - Hashed password.
 *
 * @return {userMeta} User’s user meta.
 */
const create = {
  run: (user) => {
    const requiredParams = [
      'uid',
      'branchID',
      'country',
      'idNumber',
      'passwordHash',
    ];

    // Throw error if there are missing parameters
    requiredParams.forEach((param) => {
      if (!has(user, param)) {
        // Log the error
        console.error('[Failure] Error during user creation for UID ' +
            user.uid +
            ': The function has been called with missing parameter (' +
            param + ')'
        );

        throw new functions.https.HttpsError(
          'invalid-argument',
          'The function has been called with missing parameter: ' + param,
        );
      }
    })

    // Create new auth user on Firebase
    return create.newFirebaseUser(user).then((userRecord) =>
      // Create a new borrower on Loandisk
      create.newLoandiskBorrower(user)
    ).then((borrowerID) => {
      const userMeta = pick(user, [
        'branchID',
        'country',
        'idNumber',
        'passwordHash',
      ]);

      userMeta['borrowerID'] = Number(borrowerID);

      const userMetaDoc = db.collection('appUserMeta').doc(user.uid);

      // Create userMeta on Firebase
      return db.runTransaction((transaction) => {
        return transaction.get(userMetaDoc).then((doc) => {
          if (!doc.exists) {
            return transaction.set(userMetaDoc, userMeta);
          }
        })
      });
    }).then((result) => {
      // Log the success
      console.info('[Success] User created for: ' + user.uid);

      return true;
    }).catch((error) => {
      // Assign error response to response
      let response = {
        "code": "unknown",
        "message": "Sorry, there are some issues on our side",
        "details": "Please try again in a few minutes. If it’s still not working, call us at +852 9847 4943."
      };

      // Check if an HttpError has been thrown
      if (has(error, 'code')) {
        // Assign response
        response = error;
      }

      // Check if an cloud function error has been thrown
      if (has(error, 'errorInfo.code')) {
        if (has(errorMessages, error.errorInfo.code)) {
          // Continue if error code is not auth/uid-already-exists
          if (error.errorInfo.code !== 'auth/uid-already-exists') {
            // Delete the created auth user
            admin.auth().deleteUser(user.uid);
          }

          // Assign response to error
          response = errorMessages[error.errorInfo.code];
        }
      }

      // Throw final error message
      throw new functions.https.HttpsError(
          response.code,
          response.message,
          response.details
      );
    });
  },
  // Create new Firebase user
  newFirebaseUser: ({uid}) => {
    // Set up retry function
    const retry = async (fn, retriesLeft = 5, interval = 1000, exponential = false) => {
      try {
        const val = await fn();
        return val;
      } catch (error) {
        if (retriesLeft) {
          // Hold the function from rerunning for an interval
          await new Promise((resolve) => setTimeout(resolve, interval));

          // Rerun the function
          return retry(fn, retriesLeft - 1, exponential ? interval * 2 : interval, exponential);
        } else {
          throw error;
        }
      }
    }

    return retry(
      () => admin.auth().createUser({
        uid: uid
      }), 5, 200
    ).then((userRecord) => {
      // console.info('[Success] Created new Firebase user: ', userRecord.uid);
      return userRecord;
    }).catch((error) => {
      console.error('[Failure] The provided UID already exist on Firebase: ' + uid);
      throw error;
    });
  },
  // Create new Loandisk borrower
  newLoandiskBorrower: ({country, idNumber, uid, branchID}) => {

    const borrowerData = {
      'borrower_country': country,
      'borrower_dob': null,
      'borrower_firstname': 'New',
      'borrower_gender': 'Female',
      'borrower_lastname': 'User',
      'borrower_unique_number': idNumber,
      'custom_field_3012': uid,
    };

    return axios.post(
        `${loandisk.url}/${loandisk.publickey}/${branchID}/borrower`,
        borrowerData,
        config
    ).then((response) => {
      const {data} = response;

      // [Loandisk bypass] Throw error if error key exists
      if (data.error) {
        throw new functions.https.HttpsError(
          'unknown',
          "An issue occured while communicating with Loandisk.",
          data.error.message
        );
      }

      if (data.response.Errors) {
        throw new functions.https.HttpsError(
          "unknown",
          "An issue occured while communicating with Loandisk.",
          data.response.Errors,
        );
      }

      // return borrower ID from Loandisk
      // console.info('[Success] Created new borrower with borrower ID: ', data.response.borrower_id);
      return data.response.borrower_id;
    }).catch((error) => {
      console.error('[Failure] An issue occured while communicating with Loandisk:', error.details);

      throw error;
    })
  }
};


module.exports = create.run;