//------------------------------------------------------------------------------------
//npm install --save axios moment dotenv base64url
const axios = require("axios");
const moment = require("moment");
const base64url = require("base64url").default;
const fs = require("fs");
const crypto = require("crypto"); //node standard crypto
require('dotenv').config()

//------------------------------------------------------------------------------------
/*
BEFORE RUNNING THIS EXAMPLE: 

* Set KEYID, CONNECTFI_CLIENTID, CONNECTFI_PASSWORD, and CONNECTFI_BASE_URL in an .env file 
(Speak to a support representative to be issued client credentials and URL after 
receiving access to the sandbox.)

* Set UNIQUE_REFERENCE_ID to a unique identifier.

*/
const CONNECTFI_CLIENTID = process.env.CONNECTFI_CLIENTID;
const CONNECTFI_PASSWORD = process.env.CONNECTFI_PASSWORD;
const CONNECTFI_BASE_URL = process.env.CONNECTFI_BASE_URL;
const KEYID = process.env.KEYID;

const UNIQUE_REFERENCE_ID = "exampleRef1017"; //Update this value so that it is a unique ID before running

//------------------------------------------------------------------------------------
//Get Authorization Token
/*
All other requests must have a valid authorization token in the request headers, 
so your first request in any workflow should be to /auth/get-token in order to receive 
an authorization token. A valid token should be included in the headers of all 
subsequent requests.

*/
async function getAuthToken() {
  const data = {
    "user": {
      "login": `${CONNECTFI_CLIENTID}`,
      "password": `${CONNECTFI_PASSWORD}`
    }
  };
  const config = {
    method: 'POST',
    url: `${CONNECTFI_BASE_URL}/auth/get-token`,
    headers: {
      'Content-Type': "application/json"
    },
    data
  };

  let result;
  try {
    result = await axios.request(config);
    if (result.status === 200) {
      return Promise.resolve(result.data.data);
    }
  } catch (err) {
    console.log({
      errCode: err.code,
      responseStatus: err.response && err.response.status,
      data: err.response && JSON.stringify(err.response.data)
    });
  }
}



//------------------------------------------------------------------------------------
//Encrypt Card Data
/*
Before making any /acquiring/ requests, you will need to encrypt the sensitive 
card data (including PAN, expiration date, and CVV) and register the card with 
connectFi. Instructions on how to encrypt the card data can be found in the 
"Encrypting Card Data" section of the Push To/Pull From Cards or Acquiring API 
Documentation.

*/
async function getEncryptedCard() {
  const myKey = fs.readFileSync("./public.key");

  const pan = "9400111999999990"; //test card
  const expiryYYYYMM = "203012";
  const cvv = "123";

  const rawData = `${pan}|${expiryYYYYMM}|${cvv}`; //1234567890123456|202211|123

  const encryptedData = crypto.publicEncrypt({
    key: myKey,
    padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
    oaepHash: "sha256",
  },
    Buffer.from(rawData)
  )

  const result = base64url(encryptedData);
  return result;
}



//------------------------------------------------------------------------------------
//Register Card
/*
After the sensitive card data has been encrypted, you will register the external card 
with a POST request to /acquiring/register. 

*/
async function registerCard(authToken, reference, encryptedCardData) {
  const data = {
    "reference": reference,
    "card": {
      "encryptedData": encryptedCardData,
      "keyId": KEYID
    },
    "owner": {
      "name": {
        "first": "John",
        "middle": "M",
        "last": "Doe",
        "suffix": "III"
      },
      "address": {
        "addressLine1": "1346 Pleasant Ave",
        "addressLine2": "Apt A123",
        "city": "Salt Lake City",
        "state": "UT",
        "postalCode": "12345",
        "country": "US"
      },
      "phone": {
        "countryCode": "1",
        "number": "5556667777"
      }
    }
  }
    ;
  const config = {
    method: 'POST',
    url: `${CONNECTFI_BASE_URL}/acquiring/register`,
    headers: {
      'Content-Type': "application/json",
      'x-connectfi-token': authToken //previously obtained authorization token is required
    },
    data
  };

  let result;
  try {
    result = await axios.request(config);

    if (result.status === 200) {
      return Promise.resolve(result.data.data);
    }
  } catch (err) {
    console.log({
      errCode: err.code,
      responseStatus: err.response && err.response.status,
      data: err.response && JSON.stringify(err.response.data)
    });
  }
}


//------------------------------------------------------------------------------------
//Get Card
/*
Previously registered card details can be retrieved using a GET request to 
/transfer-to/card/get/:cardId. When registering a card (or when retrieving the card 
details with a GET request), a successful response body will include boolean values 
indicating whether the card can accept push/pull transactions, the card expiration 
date, and the last four digits of the card.

*/
async function getCard(authToken, cFiCardId) {

  const config = {
    method: 'GET',
    url: `${CONNECTFI_BASE_URL}/transfer-to/card/get/${cFiCardId}`,
    headers: {
      'x-connectfi-token': authToken //previously obtained authorization token is required
    }
  };

  let result;
  try {
    result = await axios.request(config);

    if (result.status === 200) {
      return Promise.resolve(result.data.data);
    }
  } catch (err) {
    console.log({
      errCode: err.code,
      responseStatus: err.response && err.response.status,
      data: err.response && JSON.stringify(err.response.data)
    });
  }
}



//------------------------------------------------------------------------------------
//Pull From Card
/*
Once the card is registered, you may request pull transactions using a 
/acquiring/pull request in order to transfer funds from the external card 
into your settlement account.

*/
async function pullFromCard(authToken, cFiCardId, amount, reference) {
  const data = {
    "reference": reference,
    "cFiCardId": cFiCardId,
    "amount": amount,
    "currency": "USD",
    "narrative": "For invoice #123",
    "softDescriptor": {
      "name": "Sample Merchant",
      "address": {
        "addressLine1": "1346 Pleasant Ave",
        "addressLine2": "Apt A123",
        "city": "Salt Lake City",
        "state": "PA",
        "postalCode": "12345",
        "country": "US"
      },
      "phone": {
        "countryCode": "1",
        "number": "5556667777"
      },
      "email": "merchant@sample.com"
    }
  };
  const config = {
    method: 'POST',
    url: `${CONNECTFI_BASE_URL}/acquiring/pull`,
    headers: {
      'Content-Type': "application/json",
      'x-connectfi-token': authToken //previously obtained authorization token is required
    },
    data
  };

  let result;
  try {
    result = await axios.request(config);

    if (result.status === 200) {
      return Promise.resolve(result.data.data);
    }
  } catch (err) {
    console.log({
      errCode: err.code,
      responseStatus: err.response && err.response.status,
      data: err.response && JSON.stringify(err.response.data)
    });
  }
}



//------------------------------------------------------------------------------------
//Query
/*
You may query the current status of up to 20 card transactions at a time using an 
/acquiring/transactions-query request. It is recommended that you run an 
/acquiring/transactions-query only once or twice per day. The conditions 
under which an /acquiring/transactions-query should be made are as follows:

* A status change was expected to have already occurred (For example, a card transaction 
  was sent. It is now 12 hours later and a status change to "Complete" or "Declined" 
  was expected to have already occurred.)
* Do not send multiple queries for the same transaction within a 12 hour period. If a 
  query confirms that the status of a card transaction has not moved when expected, 
  contact customer support with the cFiTransactionId, current status, expected status, 
  and date that the card transaction was created.
* If you have more than one card transaction to query, do not send multiple queries 
  with one transaction at a time. The queries should be sent in batches of 20 
  cFiTransactionIds/reference IDs per request. If you have less than 20 transactions 
  to query, they can all be queried in a single request.

*/
async function query(authToken, cFiTransactionIds) {

  const data = {
    "cFiTransactionIds": [...cFiTransactionIds]
  };
  const config = {
    method: 'POST',
    url: `${CONNECTFI_BASE_URL}/acquiring/transactions-query`,
    headers: {
      'Content-Type': "application/json",
      'x-connectfi-token': authToken //previously obtained authorization token is required
    },
    data
  };

  let result;
  try {
    result = await axios.request(config);
    if (result.status === 200) {
      return Promise.resolve(result.data.data);
    }
  } catch (err) {
    console.log({
      errCode: err.code,
      responseStatus: err.response && err.response.status,
      data: err.response && JSON.stringify(err.response.data)
    });
  }
}


//------------------------------------------------------------------------------------
//List
/*
It is possible to list up to 1000 card transactions at a time using search criteria 
with an /acquiring/list request. Search criteria may include a date range, a status 
value, or a combination of these search criteria. Use case examples for an 
/acquiring/list request include but are not limited to the following.

* Listing all card transactions that still have an "Initiated" status.
* Listing all card transactions that have a "Declined" status with a date range of 
  "2023-05-10T12:00:30.000Z" to "2023-05-11T12:00:00.000Z".
* Listing all transactions that have a "Completed" status in batches of 150. (You 
  can use the "numberOfRecords" property and the "skipRecords" property to list 
  up to 1000 transactions at a time or to skip a certain number of transactions.)

*/
async function list(authToken, status, numRecords, numToSkip) {

  const data = {
    "dateCreateFrom": `${new Date(moment(new Date()).add(-1, "hour")).toISOString()}`,
    "dateCreateTo": `${new Date(moment(new Date()).add(1, "hour")).toISOString()}`,
    "status": status,
    "numberOfRecords": numRecords,
    "skipRecords": numToSkip
  };
  const config = {
    method: 'POST',
    url: `${CONNECTFI_BASE_URL}/acquiring/list`,
    headers: {
      'Content-Type': "application/json",
      'x-connectfi-token': authToken //previously obtained authorization token is required
    },
    data
  };

  let result;
  try {
    result = await axios.request(config);
    if (result.status === 200) {
      return Promise.resolve(result.data.data);
    }
  } catch (err) {
    console.log({
      errCode: err.code,
      responseStatus: err.response && err.response.status,
      data: err.response && JSON.stringify(err.response.data)
    });
  }
}


//------------------------------------------------------------------------------------
//Reverse Transaction
/*
It is possible to reverse a pull transaction using a POST request to 
/acquiring/reversal. 

*/
async function reverseTransaction(authToken, cFiTransactionId) {

  const data = {
    "cFiTransactionId": cFiTransactionId
  };
  const config = {
    method: 'POST',
    url: `${CONNECTFI_BASE_URL}/acquiring/reversal`,
    headers: {
      'Content-Type': "application/json",
      'x-connectfi-token': authToken //previously obtained authorization token is required
    },
    data
  };

  let result;
  try {
    result = await axios.request(config);

    if (result.status === 200) {
      return Promise.resolve(result.data.data);
    }
  } catch (err) {
    console.log({
      errCode: err.code,
      responseStatus: err.response && err.response.status,
      data: err.response && JSON.stringify(err.response.data)
    });
  }
}




//------------------------------------------------------------------------------------
//Run the walkthrough
async function acquiringWalkthrough() {

  //Get Authorization Token
  console.log(`Start /auth/get-token example.\n`);
  const authObject = await getAuthToken();
  let authToken = undefined;

  if (authObject) {
    console.log(`Successfully obtained authorization: ${JSON.stringify(authObject)}\n`);
    authToken = authObject.token;
    console.log(`Authorization token: ${authToken}\n`);
  } else {
    console.log(`Error getting authorization token\n`)
  }
  console.log(`End /auth/get-token example.\n`);

  //Register Card 
  console.log(`Start /acquiring/register example.\n`);
  let registerResult;
  if (authToken) {
    registerResult = await registerCard(authToken, UNIQUE_REFERENCE_ID, `${await getEncryptedCard()}`);
  } else {
    console.log(`Authorization token is required.\n`)
  }
  if (registerResult) {
    console.log(`Successfully registered external card: ${JSON.stringify(registerResult)}\n`);
  }
  console.log(`End /acquiring/register example.\n`);

  //Get Card
  console.log(`Start /transfer-to/card/get/:cardId example.\n`);
  let getCardResult;
  if (authToken && registerResult && registerResult.cFiCardId) {
    getCardResult = await getCard(authToken, registerResult.cFiCardId);
  } else {
    console.log(`Authorization token and a valid cFiCardId are required.\n`)
  }
  if (getCardResult) {
    console.log(`Successfully retrieved card details: ${JSON.stringify(getCardResult)}\n`);
  }
  console.log(`End /transfer-to/card/get/:cardId example.\n`);

  //Pull From Card
  console.log(`Start /acquiring/pull example.\n`);
  let pullFromCardResult;
  if (authToken && registerResult && registerResult.cFiCardId) {
    pullFromCardResult = await pullFromCard(authToken, registerResult.cFiCardId, 1.01, `Pull${UNIQUE_REFERENCE_ID}`);
  } else {
    console.log(`Authorization token and a valid cFiCardId are required.\n`)
  }
  if (pullFromCardResult) {
    console.log(`Successfully pulled funds from card: ${JSON.stringify(pullFromCardResult)}\n`);
  }
  console.log(`End /acquiring/pull example.\n`);

  //Query
  console.log(`Start /acquiring/transactions-query.\n`);
  let pushPullQueryResult;
  if (authToken && pullFromCardResult && pullFromCardResult.cFiTransactionId) {
    pushPullQueryResult = await query(authToken, [pullFromCardResult.cFiTransactionId]);
  } else {
    console.log(`Authorization token and cFiTransactionId(s) are required.\n`)
  }
  if (pushPullQueryResult) {
    console.log(`Successfully queried transactions: ${JSON.stringify(pushPullQueryResult)}\n`);
  }
  console.log(`End /acquiring/transactions-query.\n`);

  //List
  console.log(`Start /acquiring/list example.\n`);
  let listTransactionsResult;
  if (authToken) {
    listTransactionsResult = await list(authToken, "Complete", 5, 0);
  } else {
    console.log(`Authorization token is required.\n`)
  }
  if (listTransactionsResult) {
    console.log(`Successfully retrieved list of transactions matching criteria: ${JSON.stringify(listTransactionsResult)}\n`);
  }
  console.log(`End /acquiring/list example.\n`);

  //Reverse pull transaction
  console.log(`Start /acquiring/reversal example.\n`);
  let reverseResult;
  if (authToken && pullFromCardResult && pullFromCardResult.cFiTransactionId) {
    reverseResult = await reverseTransaction(authToken, pullFromCardResult.cFiTransactionId);
  } else {
    console.log(`Authorization token and cFiTransactionId are required.\n`)
  }
  if (reverseResult) {
    console.log(`Successfully reversed pull transaction: ${JSON.stringify(reverseResult)}\n`);
  }
  console.log(`End /acquiring/reversal.\n`);

}


if (process.env.CONNECTFI_CLIENTID && process.env.CONNECTFI_PASSWORD && process.env.CONNECTFI_BASE_URL) {
  acquiringWalkthrough();
} else {
  console.log("Before running the walkthrough, set the required .env variables.");
}