unionpay/external/unionpay.js

'use strict';

var BraintreeError = require('../../lib/error');
var analytics = require('../../lib/analytics');

/**
 * @class
 * @param {object} options see {@link module:braintree-web/unionpay.create|unionpay.create}
 * @description <strong>Do not use this constructor directly. Use {@link module:braintree-web/unionpay.create|braintree-web.unionpay.create} instead.</strong>
 * @classdesc This class represents a UnionPay component. Instances of this class have methods for {@link UnionPay#fetchCapabilities fetching capabilities} of UnionPay cards, {@link UnionPay#enroll enrolling} a UnionPay card, and {@link UnionPay#tokenize tokenizing} a UnionPay card.
 */
function UnionPay(options) {
  this._options = options;
  this._merchantAccountId = options.client.getConfiguration().gatewayConfiguration.unionPay.merchantAccountId;
}

/**
 * @typedef {object} UnionPay~fetchCapabilitiesPayload
 * @property {boolean} isUnionPay Determines if this card is a UnionPay card
 * @property {boolean} isDebit Determines if this card is a debit card
 * @property {object} unionPay UnionPay specific properties
 * @property {boolean} unionPay.supportsTwoStepAuthAndCapture Determines if the card allows for an authorization, but settling the transaction later
 * @property {boolean} unionPay.isUnionPayEnrollmentRequired Notifies if {@link UnionPay#enroll|enrollment} should be completed
 */

/**
 * Fetches the capabilities of a card, including whether or not the card needs to be enrolled before use. If the card needs to be enrolled, use {@link UnionPay#enroll|enroll}
 * @public
 * @param {object} options UnionPay {@link UnionPay#fetchCapabilities fetchCapabilities} options
 * @param {object} options.cardNumber The card number to fetch capabilities for
 * @param {errback} errback The second argument, <code>data</code>, is a {@link UnionPay#fetchCapabilitiesPayload fetchCapabilitiesPayload}
 * @returns {void}
 */
UnionPay.prototype.fetchCapabilities = function (options, errback) {
  var client = this._options.client;
  var cardNumber = options.cardNumber;

  if (!cardNumber) {
    errback(new BraintreeError({
      type: BraintreeError.types.MERCHANT,
      message: 'A card number is required'
    }));
    return;
  }

  client.request({
    method: 'get',
    endpoint: 'payment_methods/credit_cards/capabilities',
    data: {
      _meta: {source: 'unionpay'},
      creditCard: {
        number: cardNumber
      }
    }
  }, function (err, response) {
    if (err) {
      errback(new BraintreeError({
        type: BraintreeError.types.NETWORK,
        message: 'Fetch capabilities network error',
        details: {
          originalError: err
        }
      }));
      analytics.sendEvent(client, 'web.unionpay.capabilities-failed');
      return;
    }

    analytics.sendEvent(client, 'web.unionpay.capabilities-received');
    errback(null, response);
  });
};

/**
 */

/**
 * @typedef {object} UnionPay~enrollPayload
 * @property {string} unionPayEnrollmentId UnionPay enrollment ID
 */

/**
 * Enrolls a UnionPay card. Only call this method if the card needs to be enrolled. Use {@link UnionPay#fetchCapabilities|fetchCapabilities} to determine if the user's card needs to be enrolled.
 * @public
 * @param {object} options UnionPay enrollment options
 * @param {object} options.card The card to enroll
 * @param {string} options.card.number Card number
 * @param {string} options.card.expirationMonth The card's expiration month
 * @param {string} options.card.expirationYear The card's expiration year
 * @param {string} options.card.mobileCountryCode Customer's mobile country code
 * @param {string} options.card.mobileNumber Customer's mobile phone number. This is the mobile phone number UnionPay will send an SMS auth code to
 * @param {errback} callback The second argument, <code>data</code>, is a {@link UnionPay~enrollPayload|enrollPayload}
 * @returns {void}
 */
UnionPay.prototype.enroll = function (options, callback) {
  var client = this._options.client;
  var card = options.card;
  var data = {
    _meta: {source: 'unionpay'},
    unionPayEnrollment: {
      number: card.number,
      expirationMonth: card.expirationMonth,
      expirationYear: card.expirationYear,
      mobileCountryCode: card.mobileCountryCode,
      mobileNumber: card.mobileNumber
    }
  };

  if (this._merchantAccountId) {
    data.merchantAccountId = this._merchantAccountId;
  }

  client.request({
    method: 'post',
    endpoint: 'union_pay_enrollments',
    data: data
  }, function (err, response) {
    var message;

    if (err) {
      // TODO: We cannot get response codes, so we can't know whose "fault" this error is.
      // This requires a new feature of braintree-request which we are waiting on.
      if (err.type === BraintreeError.types.CUSTOMER) {
        message = 'Enrollment invalid due to customer input error';
      } else {
        err.type = BraintreeError.types.NETWORK;
        message = 'Enrollment network error';
      }

      analytics.sendEvent(client, 'web.unionpay.enrollment-failed');
      callback(new BraintreeError({
        type: err.type,
        message: message,
        details: {
          originalError: err
        }
      }));
      return;
    }

    analytics.sendEvent(client, 'web.unionpay.enrollment-succeeded');
    callback(null, {unionPayEnrollmentId: response.unionPayEnrollmentId});
  });
};

/**
 * @typedef {object} UnionPay~tokenizePayload
 * @property {string} nonce The payment method nonce
 * @property {string} type Always <code>CreditCard</code>
 * @property {object} details Additional account details
 * @property {string} details.cardType Type of card, ex: Visa, MasterCard
 * @property {string} details.lastTwo Last two digits of card number
 * @property {string} description A human-readable description
 */

/**
 * Tokenizes a UnionPay card, returning a nonce payload!
 * @public
 * @param {object} options UnionPay tokenization options
 * @param {object} options.card The card to enroll
 * @param {string} options.card.number Card number
 * @param {string} options.card.expirationMonth The card's expiration month
 * @param {string} options.card.expirationYear The card's expiration year
 * @param {string} options.card.cvv The card's security number
 * @param {object} options.options Additional options
 * @param {string} options.options.id The enrollment id if {@link UnionPay#enroll} was required
 * @param {string} options.options.smsCode The SMS code recieved from the user if {@link UnionPay#enroll} was required
 * @param {errback} callback The second argument, <code>data</code>, is a {@link UnionPay~tokenizePayload|tokenizePayload}
 * @returns {void}
 */
UnionPay.prototype.tokenize = function (options, callback) {
  var tokenizedCard;
  var client = this._options.client;

  client.request({
    method: 'post',
    endpoint: 'payment_methods/credit_cards',
    data: {
      _meta: {source: 'unionpay'},
      creditCard: {
        number: options.card.number,
        expirationMonth: options.card.expirationMonth,
        expirationYear: options.card.expirationYear,
        cvv: options.card.cvv
      },
      options: options.options
    }
  }, function (err, response) {
    if (err) {
      analytics.sendEvent(client, 'web.unionpay.nonce-failed');
      // TODO: We cannot get response codes, so we can't know whose "fault" this error is.
      // This requires a new feature of braintree-request which we are waiting on.
      callback(new BraintreeError({
        type: BraintreeError.types.NETWORK,
        message: 'Tokenization network error',
        details: {
          originalError: err
        }
      }));
      return;
    }

    tokenizedCard = response.creditCards[0];
    delete tokenizedCard.consumed;
    delete tokenizedCard.threeDSecureInfo;
    delete tokenizedCard.type;

    analytics.sendEvent(client, 'web.unionpay.nonce-received');
    callback(null, tokenizedCard);
  });
};

module.exports = UnionPay;