Daimyo API documentation

Getting started

Fee Figters' Samurai payment gateway is currently in beta. Before joining the beta program and getting an account, it is highly recommended that you get your merchant account first. After signing up with Samurai, you will receive three HEX tokens:

  • Merchant key - used to identify your account on Samurai
  • API password - password for you Samurai account
  • Processor ID - the ID of the gateway that you will use for transactions

Initially, you will receive a sandbox processor ID. The sandbox is used for testing, and you cannot actually process transactions using the sandbox. Keep in mind that you should only run unit tests that come with Daimyo using the sandbox processor ID.

Overview

When using the Samurai payment gateway, you basically deal with two separate concepts: payment methods (cards) and transactions (making/loosing money). Daimyo's API reflects this dualism. It provides two main constructors that you will use most of the time: Card and Transaction.

Once created the card objects have the following methods:

  • card.create(): cretes a new payment methods
  • card.load(): fetches payment method data from the Samurai vault
  • card.update(): updates the payment method details
  • card.retain(): instructs Samurai to permanently save the payment method
  • card.redact(): instructs Samurai to remove the payment method from vault

(See notes about PCI compliance before you start using some of these methods.)

The transaction object is constructed using the Transaction constructor. The transaction object only has one method:

  • transaction.process()

This method takes a card object as its argument, and runs a transaction against the payment method associated with the card.

Notes on PCI compliance

There are two ways you can use the Samurai gateway with Daimyo in terms of payment methods management. One is server-to-server, where you handle the cardholder data, and pass them on to the gateway. Another method is transparent redirect, where you set up a web form that submits directly to the gateway, and you only receive a payment method token that is associated with whatever data the user submitted.

While the server-to-server method is useful in cases a web form required for transparent redirect method cannot be set up (for example, for single-page AJAX apps, where cross-site request restrictions apply), you have to be aware that full PCI compliance for class C merchants is still required. Class C PCI compliance may involve on-site audits and/or audits of any 3rd party infrastructure you might be using, and many other steps. You can read more about PCI compliance at www.pcisecuritystandards.org.

Also note that Daimyo itself has not been atested or tested for PCI compliance, so use of Daimyo in your environment may negatively affect your capacity to achieve PCI compliance. While Daimyo's author sincerely believes that Daimyo is reasonably safe (or, rather, will be when a full release is made), we do not, and cannot make any guarantees to that effect, either explicit or implied, as noted in the LICENSE. Daimyo is provided to you as is, with no implied and/or explicit warranties of any sort. In other words, you are on your own using Daimyo if you are looking for PCI compliance. Good news is, source code is availabe, so you can make any necessary adjustments. (Don't forget to send us a pull request if you do so.)

Ashigaru and single-page AJAX apps

If you have an AJAX-intesive website that cannot make regular POST requests using web forms, you might want to try using the Ashigaru jQuery plugin. The plugin is included in the Daimyo project directory under /support subdirectory. For more information on how to set up your server for use with Ashigaru, and basic usage of this plugin, take a look at Ashigaru's documentation. You can also find a functional demo online.

Ashigaru has been tested only on the latest browsers. Browsers that are currently supported by Ashigaru are:

  • Firefox 5.0+ (probably works on 4.0 and older as well)
  • Internet Explorer 8.0+ (probably works on older, but IE6 is probably broken)
  • Opera 11.0+ (not sure it would work in older releases)
  • Chrome 13.0+ (should work in most version of Chrome)

Basically, ashigaru is a very simple plugin which uses technology that has been available for quite some time, so there is no reason to believe it wouldn't work on older browsers. If you bump into problems with older browsers, however, please file a bug report.

Configuration

Before you use any of the Daimyo's functions, you should configure Daimyo.

var daimyo = require('daimyo');

daimyo.configure({
  merchantKey: 'xxxxxxxxxxxxxxxxxxxxxxxx',
  apiPassword: 'xxxxxxxxxxxxxxxxxxxxxxxx',
  processorId: 'xxxxxxxxxxxxxxxxxxxxxxxx'
});

Samurai gatway uses transparent redirect method to process credit card information. The way it works is, user submits the card and billing data directly to Samurai, and it redirects the user back to your site attaching a payment method token to the request. You have to access to credit card data in any part of the work flow. Daimyo provides a create method, which allows you to create a payment method without using the transparent redirect. You may use this method if you really cannot use the transparent redirect, and you find Ashigaru to be broken or otherwise unusable for you. You should keep in mind, though, that you have to ensure that sensitive data passing through your site is properly secured. Use SSL for every connection that passes sensitive data, and do not use GET requests for such requests. Also make sure that no sensitive data is logged or stored in any part of your application.

One of the configuration options is debug, which enables logging of all data that passes through Daimyo. While it is disabled by default, you should take utmost care to ensure it remains disabled in production. Double-check you app's configuration.

Configuration locking

Note that Daimyo performs configuration-locking after you call daimyo.configure() for the first time (and if that's successful). This means that you will not be able to call daimyo.configure() multiple times to set different options. You need to set all options beforehand.

This is a security feature that prevents accidental/malicious resetting of critical options.

Calling daimyo.option() will also fail after configuration has been locked. You can use multiple calls to daimyo.options() to set an option multiple times and it won't lead to locking if the configuration has not been locked already.

Althoug using daimyo.option() may sound more convenient, you should set all critical core options (including debug, enabled, and sandbox) using the daimyo.configure() method for security reasons.

Future version of Daimyo may simply lock any option that has been set once without errors using either daimyo.configure() or daimyo.option(), so you should not rely on the behavior of daimyo.option() to circumvent configuration locking.

Currently, the only exception to configuration locking is the currency parameter, which can be set any number of times. Future version of Daimyo may include more such non-critical options.

See the config module documentation for more information.

Card object

If you chose to use the server-to-server method of creating payment methods, you can create a new payment method using the create method. Suppse you have received billing and creadit card data from your user. You can now create a new Card object use that data.

var card = new daimyo.Card({
  number: data.cardNumber,
  csc: data.csc,
  firstName: data.firstName,
  lastName: data.lastName,
  year: data.expirationYear,
  month: data.expirationMonth,
  ....
});

The card object has following fields:

  • number: card number
  • csc: card security code (called CCV, CVC, CVV, and various other names)
  • year: expiration year (if any)
  • month: expiration month
  • firstName: card holder's first name
  • lastName: card holder's last name
  • address1: billing address line 1
  • address2: billing address line 2
  • city: billing address city
  • state: billing address state/region
  • zip: billing address zip/postal code
  • custom: JSON-serializable object containing arbitrary data you may want to store with your payment method (e.g., user ID)

You cannot create a card object unless you supply it a payment token, or credit card number and CSC. If you supply it a token, all other fields are ignored. Otherwise, card number and CSC are required, and you will get an error if you do not specify them. If you create a card object with credit card and billing details, you will get one more field:

  • issuer: name of the credit card's issuer

The issuer is detected from the card number, and you should not set the field manually (or allow the user to set it).

Here is an example of initializing a new card object:

var card = new daimyo.Card({
  number: '4111-1111-1111-1111',
  csc: '123',
  year: 2012,
  month: 11,
  firstName: 'John',
  lastName: 'Doe',
  address1: '123 Payment St',
  custom: {
    email: 'jdoe@example.com',
    timestamp: new Date();
  }
});

// The card now also has an issuer property:
console.log(card.issuer); // This logs 'Visa'

Basic validation

Before you call the create method, you can perform basic validation to increase the likelyhood of successful transaction.

card.isValid(); // returns true if card is valid
card.isExpired(); // returns true if expired

In addition to these two methods, you should generally ensure that user supplies correct address and zip code (or postal number), and that expiration date is in future. Note that you are allowed to forward the expiration date into future, as banks do not usually check if it's correct as long as it's in future. Generally, the more data you supply, your liability will be lower, so it's a good idea to supply as much data as possible to the gateway if you cannot trust your users 100%.

Creating the payment method server-side

The card, when initilized, is still not a valid payment method. You have to actually create in on Samurai gateway in order to make purchases. You can do that like so:

card.create(function(err) {
  // Handle errors
  // Card now has a payment method token associated with it
  console.log(card.token);
});

If there are any errors during the creation process, they will be passed to the callback function. The error object will have following properties:

  • category: Category of the error ('system' in most cases)
  • message: Error message
  • details: Any data that give you more details about the error

Common error messages may include:

  • 'Error making create payment method request': Daimyo is not properly configured (e.g., missing Samurai gateway credentials), or the request failed for some reason and the response was unreadable (e.g., Samurai was offline)
  • 'Gateway responded with non-302 status': This is rare, but means that Samurai received malformed data. If this happens, please report it as a bug.
  • 'Gateway failed to send the location header': This is a Samurai bug, but report it to us anyway.

In any case, you should consider receiving an error object with 'system' category a critical failure, and act accordingly. Depending on the nature of the error, you may want to retry, too.

It is important to note that error related to the actual card information will not be passed in error object. These will be available once you load the card from the gateway, or when you make a transaction.

Loading the payment method

Now that the card object has a token associated with it, you can either save the token, or perform transactions with it. So, let's say you have stored the payment token, either when doing the transparent redirect, or after you created the payment method using the create method. You can now use the load method to fetch payment method details from Samurai server.

var myToken = 'xxxxxxxxxxxxxxxxxxxxxxxx';
var card = new daimyo.Card({token: myToken});
card.load(function(err) {
  // Handle error
});

The card object has all the fields populated. There are also two new fields:

  • messages: contains any Samurai gateway messages about the card
  • method: contains meta-information about the payment method

See the API documentation for details on what these fields contain. The error object passed to the callback has the same format as the error object passed to the create method. All methods except create also have two more possible error messages:

  • 'Cannot ACTION payment method without token': (where ACTION is the action you were trying to perform), this means that no token was provided and action requires a valid payment method token.
  • 'Loaded token does not equal the token that was requested': Gateway responded with a token that does not match our token. This indicates a high likelyhood of MITM attack, and you should immediately take the site offline and perform security checks and forensic analysis to ensure no sensitive data has been leaked. (Note that you will not be able to easily uncover a successful MITM attack.)

In addition, the following error is different from the create method:

  • 'Gateway responded with non-200 status': This means that the request was malformed, and it is most likely due to a Daimyo bug. Please report full error details along with your bug report.

Updating the payment method

If you want to update the card details, you can do so using the update method:

card.firstName = 'Foo';
card.lastName = 'Bar';
card.address1 = '241 Bar St.';
card.city = 'Fooville';
card.update(function(err) {
  // Handle errors here
});

Error messages are the same ones expected in load method callback.

Retention and redaction

Samurai has a built-in vault that can safely store your payment methods. Well, they aren't yours, but... you know what I mean. :) Usage of this vault is pretty much automatic. As soon as you create a new card, it is stored in the vault.

By default, the stored payment methods will be deleted after 48 hours. If you wish to keep the payment methods stored for longer periods, you can use the retain() method. Let's say you have a newly created card. To retain it, simply instruct the Samurai to do so:

card.retain(function(err) {
  // Error handling here
  // The card now has a method property, 
  // which contains metadata about
  // the payment method. It has a `retained`
  // property which is now set to true:
  console.log(card.method.retained); // => true
});

When your user supplies you a new card, or simply wants you to remove their records, use the redact() method to have the card removed from the Samurai vault.

card.redact(function(err) {
   // The card.method.retained is still true, but
   // card.method.redacted is now also true.
});

Generally, you should not keep using a redacted payment method, so make sure you check if the card.method.redacted is true. Updating the card data is usually more efficient using the update() method. While it is more effiicient, if you are using the transparent redirect method, you should redact the old card, and let the user enter a new one. Only use update() if you are using the server-to-server method.

Again, error messages are the same as ones for the load and update methods.

Making transactions

Transactions are made using the daimyo.Transaction constructor and resulting transaction objects.

There are currenly 5 transaction types supported by Samurai:

  • purchase: make immediate authorization and capture of the funds (immediately debit the user's credit card)
  • authorize: instruct the bank to put a hold on specified amount (the funds are reserved, but you do not get the money until you capture)
  • capture: capture the funds that have been authorized
  • void: void a previously authorized transaction (this releases the hold on the funds)
  • credit: reverse the capture (return funds to the user's card in the amount that has been captured via capture or purchase).

The transaction object constructor takes an object with transaction options, and the type option selects one of the transaction types. Depending on the transaction type, other options may differ.

Transaction-specific data are specified in data property of the constructor options object. The data to be passed via this parameter depends on the transaction type, and they are discussed further below. The layout of the options object looks like this:

var transaction = new daimyo.Transaction({
  type: 'purchase',
  data: {
    amount: 123.45,
    currency: 'USD',
    billingReference: 'AX-0002-13',
    customerReference: 'ab551f23',
    descriptor: 'Spiffy bike',
    custom: {originalDate: new Date('2011-06-12 UTC')}
  }
});

Purchase and Authorize transactions

For purchase and authorize transactions, options other than type are the following:

  • amount: The transaction amount
  • currency: Optional (defaults to the one set in your initial configuration)
  • billingReference
  • customerReference
  • descriptor: description of transaction that will appear in the user's bank statement (if supported by the bank)
  • custom: JSON-serializable object containing any data you want to attack to a transaction

Capture, Void, and Credit transactions

These transactions require none of the extra options required by the purchase and authorize transactions. Instead they require a transaction ID (returned by successful purchase/authorize transactions, see details further below).

  • transactionId

In addition, capture and credit take an amount option.

Transaction data locking

Transaction data cannot be modified after the transaction object is initialized. This is done to prevent tampering by malicious code.

Processing transactions

Transaction constructor will not check if you have specified the appropriate options depending on the type of tranasaction. You are expected to be mindful about what options are passed.

Once you have created the transaction object, you will also need a card object with a token (card has a token after creation, or after load). Card object is not required for credit and void transactions. You may pass null instead of the card object, or omit the first argument for those transactions.

Given a transaction object, you can now:

transaction.process(card, function(err) { // Success! });

Once the transaction is completed, and there are no errors, the transaction object will gain a receipt property which should contain data about the transaction, including transactionId, and success property. The latter tells you if the transaction was successful.

In addition, transaction object will contain a messages property, which will contain more details about the transaction if it failed.

AVS status messages

AVS (Address Verification System) can return as many as 26 different messages. If you simply want to know if the address was verified or not, you can check if transaction.messages.errors object contains an address key:

transaction.process(card, function(err) {
  if (transaction.messages.error && transaction.messages.error.address) {
    console.error('AVS check failed!');
  }
});

In some cases, AVS will fail despite the card being valid. This is a common case with non-US cards, or when the issuing bank doesn't support AVS. You can test these conditions by checking the first character of the avs message:

transaction.process(card, function(err) {
  var inf = transaction.messages.info || {};
  if (inf && inf.avs && ['G','E','S','U'].indexOf(inf.avs[0][0] > -1) {
    // Try updating the card by removing the address
    // (but not postal code!)
  } else if (inf && inf.avs) {
    // Transaction failed because of AVS
  }
});

For more information on AVS, check the Wikipedia aticle.

config

lib/config.js

config

Copyright (c)2011, by Branko Vukelic

Configuration methods and settings for Daimyo. All startup configuration settings are set using the config.configure() and config.option() methods. Most options can only be set once, and subsequent attempts to set them will result in an error. To avoid this, pass the allowMultipleSetOption option to config.configure() and set it to true. (The option has a long name to prevent accidental usage.)

var config = exports;
var util = require('util');
var DaimyoError = require('./error');
var samurayKeyRe = /^[0-9a-f]{24}$/;
var isConfigured = false;

config.DAIMYO_VERSION = '0.1.5';

settings

Master configuration settings for Daimyo

The settings object contains all the core configuration options that affect the way certain things work (or not), and the Samurai gateway credentials. You should not access this object directly. The correct way to access and set the settings is through either configure() or option() methods.

Settings are expected to contain following keys with their default values:

  • merchantKey: Samurai gateway Merchant Key (default: '')
  • apiPassword: Samurai gateway API Password (default: '')
  • processorId: Processor (gateway) ID; be sure to set this to a sandbox ID for testing (default: '')
  • currency: Default currency for all transactions (can be overriden by specifying the appropriate options in transaction objects)
  • allowedCurrencies: Array containing the currencies that can be used in transactions. (default: ['USD'])
  • sandbox: All new payment methods will be sandbox payment methods (default: false)
  • enabled: Whether to actually make requests to gateway (default: true)
  • debug: Whether to log to STDOUT; it is highly recommended that you disable this in production, to remain PCI comliant, and to avoid performance issues (default: true)

Only currency option can be set multiple times. All other options can only be set once using the config.configure() method.

The apiVersion setting is present for conveinence and is should be treated as a constant (i.e., read-only).

var settings = {};
settings.merchantKey = '';
settings.apiPassword = '';
settings.processorId = '';
settings.currency = 'USD';
settings.allowedCurrencies = ['USD'];
settings.sandbox = false;
settings.enabled = true; // Does not make any actual API calls if false
settings.debug = false; // Enables *blocking* debug output to STDOUT
settings.apiVersion = 1; // Don't change this... unless you need to
settings.allowMultipleSetOption = false;

config.configure(opts)

Set global Daimyo configuration options

This method should be used before using any of the Daimyo's functions. It sets the options in the settings object, and performs basic validation of the options before doing so.

Unless you also pass it the allowMultipleSetOption option with value set to true, you will only be able to call this method once. This is done to prevent accidental calls to this method to modify critical options that may affect the security and/or correct operation of your system.

This method depends on config.option() method to set the individual options.

If an invalid option is passed, it will throw an error.

  • param: Object Configuration options

config.configure = function(opts) {
  debug('Configuring Daimyo with: \n' + util.inspect(opts));
  if (!opts.merchantKey || !opts.apiPassword || !opts.processorId) {
    throw new DaimyoError('system', 'Incomplete Samurai API credentials', opts);
  }
  Object.keys(opts).forEach(function(key) {
    config.option(key, opts[key]);
  });
  isConfigured = true;
};

config.option(name, [value])

Returns or sets a single configuration option

If value is not provided this method returns the value of the named configuration option key. Otherwise, it sets the value and returns it.

Setting values can only be set once for most options. An error will be thrown if you try to set an option more than once. This restriction exist to prevent accidental and/or malicious manipulation of critical Daimyo configuration options.

During testing, you may set the allowMultipleSetOption to true in order to enable multiple setting of protected options. Note that once this option is set to false it can no longer be set to true.

Samurai API credentials are additionally checked for consistency. If they do not appear to be valid keys, an error will be thrown.

  • param: String option Name of the option key

  • param: Object value New value of the option

  • returns: Object Value of the option key

config.option = function(option, value) {
  if (typeof value !== 'undefined') {
    debug('Setting Daimyo key `' + option + '` to `' + value.toString() + '`');
    
    // Do not allow an option to be set twice unless it's `currency`
    if (isConfigured && 
        !settings.allowMultipleSetOption && 
        option !== 'currency') {
      throw new DaimyoError(
        'system', 
        'Option ' + option + ' is already locked', 
        option);
    }

    switch (option) {
    case 'merchantKey':
    case 'apiPassword':
    case 'processorId':
      // Throw if doesn't look like valid key
      if (!samurayKeyRe.exec(value)) {
        throw new DaimyoError('system', 'Invalid setting', option);
      }
      settings[option] = value;
      break;
    case 'currency':
      settings[option] = value;
      break;
    case 'sandbox':
    case 'enabled':
    case 'debug':
    case 'allowMultipleSetOption':
      settings[option] = Boolean(value);
      break;
    case 'allowedCurrencies':
      if (!Array.isArray(value)) {
        throw new DaimyoError('system', 'Allowed currencies must be an array', null);
      }
      if (value.indexOf(settings.currency) < 0) {
        value.push(settings.currency);
      }
      settings.allowedCurrencies = value;
      break;
    default:
      // Do not allow unknown options to be set
      throw new DaimyoError('system', 'Unrecognized configuration option', option);
    }
  }
  return settings[option];
};

daimyo

lib/daimyo.js

daimyo

Copyright (c)2011, by Branko Vukelic branko@herdhound.com

Core Daimyo objects and functions. This module contains the core Daimyo API, which is actually used by your application.

var config = require('./config');
var debug = config.debug;
var check = require('./check');
var DaimyoError = require('./error');
var authpost = require('./authpost');
var xmlutils = require('./xmlutils');
var messages = require('./messages');
var tokenRe = /^[0-9a-f]{24}$/;
var daimyo = exports;

daimyo.Card(opts)

Creates a new payment method instance

The payment method options can be either a single token: - token: Payment method token

or full credit card details:

  • number: Credit card number
  • csc: Card security code
  • year: Expiration year
  • month: Expiration month
  • firstName: Card holder's first name
  • lastName: Card holder's last name
  • address1: Address line 1
  • address2: Address line 2
  • city: City
  • state: State
  • zip: Zip code
  • custom: Any custom object (will be stored as JSON, so use JSON-serializable data)

If you supply both token, and card details, token will take presedence, and credit card details will be completely ignored.

The constructor will refuse to accept credit card details if number and/or csc properties are missing. An error will be thrown in such cases.

  • param: Object opts Payment method options

  • constructo: r

function Card(opts) {
  var self = this;

  // Add accessors in a close to prevent tampering
  (function(self) {
    // This property contains the original/initial values of all fields
    // Do not motify this object directly. Use _dirty setter.
    var originalValues = {
      number: undefined,
      csc: undefined,
      year: undefined,
      month: undefined,
      firstName: '',
      lastName: '',
      address1: '',
      address2: '',
      city: '',
      state: '',
      zip: '',
      custom: undefined
    };

    // Update _originalValues keys if matching keys are found in object argument
    self.__defineSetter__('_dirty', function(object) {
      Object.keys(originalValues).forEach(function(field) {
        if (object[field]) { originalValues[field] = object[field]; }
      });
    });

    // Check fields for dirtiness and return the names of dirty fields as Array
    self.__defineGetter__('_dirty', function() {
      var dirtyFields = [];
     
      // Check all fields in _originalValues and determine the dirty fields
      Object.keys(originalValues).forEach(function(field) {
        if (self[field] !== originalValues[field]) { 
          dirtyFields.push(field); 
        }
      });

      return dirtyFields;
    });

    // Resets dirty fields
    self._resetDirty = function() {
      var self = this;
      self._dirty = {
        number: self.number,
        csc: self.csc,
        year: self.year,
        month: self.month,
        firstName: self.firstName,
        lastName: self.lastName,
        address1: self.address1,
        address2: self.address2,
        city: self.city,
        state: self.state,
        zip: self.zip
      };
    };

  }(self));

  // Setters and getters for `year`, `month`, and `number` properties
  (function(self, check) {
    
    // Original values
    var yearVal;
    var monthVal;
    var numberVal;
    var custVal;

    // Helper for year normalization
    function normalizeYear(order, year) {
      return (Math.floor(new Date().getFullYear() / order) * order) + year;
    }

    self.__defineSetter__('year', function(year) {
      if (!year) { return; }

      year = parseInt(year, 10);
      if (year < 10) {
        yearVal = normalizeYear(10, year);
      } else if (year >= 10 && year < 100) {
        yearVal = normalizeYear(100, year);
      } else {
        yearVal = year;
      }
    });

    self.__defineGetter__('year', function() {
      return yearVal;
    });

    self.__defineSetter__('month', function(month) {
      month = parseInt(month, 10);
      if (isNaN(month) || month < 1 || month > 12) {
        monthVal = null;
      } else {
        monthVal = month;
      }
    });

    self.__defineGetter__('month', function() {
      return monthVal;
    });

    self.__defineSetter__('number', function(value) {
      numberVal = check.extractDigits(value);
      self.issuer = check.getIssuer(value);
    });

    self.__defineGetter__('number', function() {
      return numberVal;
    });

    self.__defineSetter__('custom', function(value) {
      if (!value) {
        return;
      }
      try {
        custVal = JSON.stringify(value);
      } catch(e) {

This always fails silently

}
    });

    self.__defineGetter__('custom', function() {
      try {
        return JSON.parse(custVal);
      } catch(e) {

Fail silently

}
    });

    self.__defineGetter__('customJSON', function() {
      return custVal;
    });

  }(self, check));

  // Process the options
  if (opts) {
    if (typeof opts.token !== 'undefined') {
      if (!tokenRe.test(opts.token)) {
        throw new DaimyoError('system', 'Invalid token', null);
      } 

      debug('Using payment token instead of credit card details.');
      self.token = opts.token;

    } else {
      debug('No valid token found. Using credit card details.');
      self.number = opts.number;
      self.csc = opts.csc;

      // Card number and CSC are required
      if (!self.number) {
        throw new DaimyoError('system', 'Card number is required', null);
      }
      if (!self.csc) {
        throw new DaimyoError('system', 'CSC is required', null);
      }

      self.year = opts.year;
      self.month = parseInt(opts.month, 10);
      self.firstName = opts.firstName || '';
      self.lastName = opts.lastName || '';
      self.address1 = opts.address1 || '';
      self.address2 = opts.address2 || '';
      self.city = opts.city || '';
      self.state = opts.state || '';
      self.zip = opts.zip || '';
      self.custom = opts.custom;

      // Set card issuer
      self.issuer = check.getIssuer(self.number) || '';
    }
  }
}

daimyo.Card.isValid()

Validate the card data

This method validates the correctness of the card number and CSC. It uses the industry-standard Luhn Mod-10 check to ensure that the card number's checksum is correct. It also makes sure that the CSC has the correct number of digits (currently, AMEX cards have 4-digit CSC, while others use 3 digits).

Note that the card may still fail to clear for any number of reasons. The same check as this one is performed in the Samurai gateway, as well, however if you create payment methods using server-to-server requests, rather than letting Samurai create payment methods by submitting the payment form directly to Samurai, then this can speed up processing as you can trap some common errors without sending a request to Samurai.

  • returns: Boolean Validation result

Card.prototype.isValid = function() {
  if (!check.mod10check(this.number)) {
    return false;
  }

  if (!check.cscCheck(this.number, this.csc)) {
    return false;
  }

  return true;
};

daimyo.Card.isExpired()

Checks the card expiration year/month

If the year and month are not specified, the check will return true.

This method does not correct the expiration year/month.

You should be aware that correcting the expiration year/month by setting them to a future date is acceptable practice, and banks will not decline a card based on expiration year/month. If the card fails this test, you should manually forward the expiration date to increase the chance of the transaction succeeding.

Note that the same check will be performed in Samurai gateway, but it is more efficient to do it yourself if you are not using the transparent redirect method as it saves at least one request.

  • returns: Boolean Check result

Card.prototype.isExpired = function() {
  var expYear = new Date().getFullYear();
  var expMonth = new Date().getMonth() + 1; // 0-indexed

  if (!this.year || !this.month) { return true; }

  // Expired card should not be last month this year or older
  if (this.year < expYear) { return true; }
  if (this.year === expYear && this.month < expMonth) { return true; }

  return false;
};

daimyo.Card.create(callback)

Sends a request to create a new payment method

Creates a new payment method in the Samurai vault, and sets the token property to the received payment method token.

The callback should accept a single error object that is null if no error occured.

Example

var cardData = {...};
var card = new daimyo.Card(cardData);
card.create(function(err) {
  if (err) {
    console.log(err);
    return;
  }
  console.log('Payment token: ' + card.token);
});

  • param: Function Optional callback function (expects err)

Card.prototype.create = function(callback) {
  var querystring = require('querystring');
  var self = this;
  var paymentTokenRe = /payment_method_token=([0-9a-f]{24})/;

  if (this.token) {
    callback(null, this.token);
    return;
  }

  paymentData = {
    'redirect_url': 'http://example.com', // Bogus URL, required by API
    'merchant_key': config.option('merchantKey'),
    'custom': this.customJSON || '',
    'credit_card[first_name]': this.firstName || '',
    'credit_card[last_name]': this.lastName || '',
    'credit_card[address_1]': this.address1 || '',
    'credit_card[address_2]': this.address2 || '',
    'credit_card[city]': this.city || '',
    'credit_card[state]': this.state || '',
    'credit_card[zip]': this.zip || '',
    'credit_card[card_type]':  this.issuer || '',
    'credit_card[card_number]': this.number || '',
    'credit_card[cvv]': this.csc || '',
    'credit_card[expiry_month]': this.month || '',
    'credit_card[expiry_year]': this.year || ''
  };

  // Add sandbox parameter if the sandbox config option is enabled
  if (config.option('sandbox')) {
    paymentData.sandbox = true;
  }

  // Reformat our data as required by the API
  paymentData = querystring.stringify(paymentData);

  authpost.makeRequest({
    method: 'POST',
    payload: paymentData
  }, function(err, res) {
    var errMsg;

    if (err && err instanceof DaimyoError) {
      callback(err);
    }
    if (err) {
      callback(new DaimyoError('system', 'Error making create payment method request', err));
      return;
    }
    if (res.status !== 302) {
      // Parse the body to find the error
      callback(new DaimyoError(
        'system', 
        'Gateway responded with non-302 status', 
        {
          status: res.status, 
          messages: messages.translateAll(xmlutils.extractMessages(res.body))
        }
      ));
      return;
    }
    if (!res.headers.location) {
      callback(new DaimyoError('system', 'Gateway failed to send the location header', res.headers));
      return;
    }

    // Parse the location header to extract the token
    self.token = paymentTokenRe.exec(res.headers.location)[1];
    self._resetDirty();
    callback(null);
  });
 
};

Fail silently

}
  }

  self._resetDirty();

  return true;
};

daimyo.Card.load(callback)

Load payment method data from Samurai server

This method is used to load a created/retained payment method from the Samurai gateway. The returned data immediately updates the fields in the card instance, and also creates a new method property, which contains metadata about the payment method.

The method property has the following properties:

  • valid: Whether the card has passed Samurai's validation (this is the same type of validation as those used in isValid() method.
  • updatedAt: The timestamp of payment method's last modification
  • createdAt: Payment method creation timestamp
  • retained: Whether payment method is permanent
  • redacted: Whether payment mehtod was modified
  • custom: Custom fields (setting them is not yet supported by Daimyo)

load method also creates a messages property which contains any messages that the gateway emitted. These may include validation errors.

The callback should expect a single error object.

Example

// E.g., you have a payment token stored in a database
var userToken = 'xxxxxxxxxxxxxxxxxxxxxxxx';
var card = new daimyo.Card({token: userToken});
card.load(function(err) {
   // Now card's fields are populated with billing details
});

  • param: Function callback Expects err object

Card.prototype.load = function(callback) {
  var self = this;

  self._rejectNoToken(callback);

  self._executeRequest({
    path: self._getTokenPath()
  }, callback);

};

daimyo.Card.update(callback)

Update payment method with modified data

After changing the properties of a card object, you can call this method to sync the data with the Samurai gateway. This method is 'smart' enough to not waste bandwidth when there are no changed properties, so if the if you allow users to edit the data, you don't need to check if they have entered anything new. It also sends only the changed fields.

Once the update is complete, all fields will be marked as clean.

The callback should expect a single error object.

Example

// You load the card from Samurai using the load() method
var loadedCard;
// User makes modification to data
loadedCard.firstName = 'Foo';
// Now you can update it
loadedCard.update(function(err) {
   // The payment method is now updated
});

  • param: Function callback Called with err object

Card.prototype.update = function(callback) {
  var updatedFields = {};
  var payload = '';
  var self = this;

  self._rejectNoToken(callback);

  // If nothing was updated, just call the callback and pretend it was updated
  if (!self._dirty.length) {
    callback(null);
    return;
  }

  // Get all dirty fields
  self._dirty.forEach(function(field) {
    updatedFields[field] = self[field];
  });

  // We have to rename a few fields
  updatedFields = xmlutils.toSamurai(updatedFields, daimyo.MAPPINGS);

  // Construct the XML payload
  payload = xmlutils.toXML(updatedFields, 'payment_method');

  self._executeRequest({
    path: self._getTokenPath(),
    method: 'PUT',
    payload: payload
  }, callback);

};

daimyo.Card.retain()

Instructs Samurai to ratain (save permanently) the payment method

This is simple method that makes the payment method permanent on the gateway. The payment method will be stored for future use after this method is called.

By default, payment method that hasn't been retained will be deleted after 48 hours. You can delete a payment method (redact in Samurai parlance) at any time by calling the redact() method.

  • param: Function callback Called with err object

Card.prototype.retain = function(callback) {
  var self = this;

  self._rejectNoToken(callback);

  self._executeRequest({
    path: self._getTokenPath('/retain'),
    method: 'POST'
  }, callback);

};

daimyo.Card.redact()

Instructs Samurai to redact (delete) the payment method

When a user wants to use a new card, you should always redact (delete) the existing payment method before accepting the new card. An alternative way is to update the existing card by modifying the billing/card data using the update() method.

Updating a card is usually more efficient, as it requires only one request.

Note that the card that is redacted will still have the retained flag set to true in the method property. You should not, and cannot keep using the redacted payment method for transactions.

  • param: Function callback Called with err object

Card.prototype.redact = function(callback) {
  var self = this;

  self._rejectNoToken(callback);

  self._executeRequest({
    path: self._getTokenPath('/redact'),
    method: 'POST'
  }, callback);

};

daimyo.Card = Card;

transaction

lib/transaction.js

transaction

Copyright (c)2011, by Branko Vukelic branko@herdhound.com

var config = require('./config');
var DaimyoError = require('./error');
var xmlutils = require('./xmlutils');
var authpost = require('./authpost');
var messages = require('./messages');
var ducttape = require('./ducttape');
var daimyoRecipes = require('./daimyo').recipes;
var debug = config.debug;
var transaction = exports;

var VALID_TRANSACTIONS = ['purchase', 'authorize', 'capture', 'void', 'credit'];

MAPPINGS

Mappings between Daimyo fields and Samurai node names

transaction.MAPPINGS = {
  currency: 'currency_code'
};

getTransactionPath(type, [transactionId])

Builds transaction path

  • param: String subpath Subpath of the transaction

  • param: String [transactionId] Transaction ID for capture, void and fetch

function getTransactionPath(type, transactionId) {
  var subpath = '/processors/';
  if (transactionId) {
    subpath = '/transactions/';
  }
  return subpath + (transactionId || config.option('processorId')) + '/' + 
    type + '.xml';
}

transaction.Transaction(opts)

Low-level transaction object

Options for the transaction object are:

  • type: Transaction type
  • data: Transaction data
  • [transactionId]: (optional) Transaction ID

Note that the Transaction constructor is not meant to be used directly. However, if you find that other higher-level specialized constructors do not perform the kind of transaction you are looking for, you can use the generic Transaction constructor to make the transactions.

To use the generic constructor, supply the type of the transaction (it should be named after the request URI component that identifies the transaction type (e.g, if the URI ends in /credit.xml, the transaction type is credit), and the transactionId option as necessary. The data option should be an object that contains key-value mappings such that each key corresponds to an XML node of the request payload.

When naming the keys, feel free to use camel-case names, as those will get decamelized prior to XML conversion. For example if the node is called payment_method_token, you may call it either that, or paymentMethodToken. Either way will work just fine (if it doesn't, file a bug, please).

  • param: Object opts Transaction options

  • constructo: r

transaction.Transaction = function(opts) {
  // opts are required
  if (!opts) {
    throw new DaimyoError('system', 'Missing options object', null);
  }
  // opts.type are required
  if (!opts.type || VALID_TRANSACTIONS.indexOf(opts.type) < 0) {
    throw new DaimyoError('system', 'Missing or invalid transaction type', null);
  }
  // opts.data are required
  if (!opts.data) {
    throw new DaimyoError('system', 'Missing payload data');
  }

  // Make data and path tamper-free properties
  ducttape.addOneTimeAccessor(this, 'data');
  ducttape.addOneTimeAccessor(this, 'type');
  ducttape.addOneTimeAccessor(this, 'path');
  ducttape.addOneTimeAccessor(this, 'useToken');

  this.type = opts.type;

  this.useToken = ['credit', 'void'].indexOf(this.type) === -1;
    
  // Some defaults for complex transactions
  if (!opts.transactionId) {
    opts.data.type = opts.type;
    opts.data.currency = opts.data.currency || config.option('currency');
  }

  this.data = opts.data;
  this.path = getTransactionPath(opts.type, opts.transactionId);

  // Add methods to remember and check the response validation hash
  (function(self, ducttape) {
    var hash;

    // Remember the hash (works only the first time)
    self.rememberHash = function(val) {
      if (!hash) {
        hash = val;
      }
    };

    // Compare given value to the remembered hash
    self.checkHash = function(val) {

      // Always fail if no hash is remembered
      if (!hash) { return false; }

      return val === hash;
    };

  }(this, ducttape));

};

Fail silently

}
  }

  self.receipt.referenceId = transData.reference_id;
  self.receipt.transactionId = transData.transaction_token;
  self.receipt.createdAt = transData.created_at;
  self.receipt.descriptor = transData.descriptor;
  self.receipt.type = transData.transaction_type.toLowerCase();
  self.receipt.amount = transData.amount;
  self.receipt.currency = transData.currency_code;
  self.receipt.customerReference = transData.customer_reference;
  self.receipt.billingReference = transData.billing_reference;
  self.receipt.success = transData.success;

  // Update the card object from payment method response information
  if (self.useToken) {
    card.method = {};
    card.method.valid = methodData.is_sensitive_data_valid;
    card.method.updatedAt = methodData.updated_at;
    card.method.createdAt = methodData.created_at;
    card.method.retained = methodData.is_retained;
    card.method.redacted = methodData.is_redacted;
    card.last4 = methodData.last_four_digits;
    card.issuer = methodData.card_type;
    card.year = methodData.expiry_year;
    card.month = methodData.expiry_month;
    card.firstName = methodData.first_name;
    card.lastName = methodData.last_name;
    card.address1 = methodData.address_1;
    card.address2 = methodData.address_2;
    card.city = methodData.city;
    card.state = methodData.state;
    card.zip = methodData.zip;
    card.country = methodData.country;
  }

  // Parse card's custom data
  if (methodData.custom) {
    try {
      card.custom = JSON.parse(methodData.custom);
    } catch (err2) {

Fail silently

}
  }

  return true;
};

transaction.Transaction.process([card], callback)

Process the transaction using the given card

After a transaction is processed, the transaction object gains one new property, receipt, which contains useful details on the status of the transaction:

  • referenceId: reference ID (this is not the transaction ID)
  • transactionId: this token can be used later to void or credit
  • cretedAt: time the transaction took place
  • descriptor: if gateway supports descriptors, it will be given here
  • custom: any custom fields you have set
  • type: transaction type (this should match the original transaction type)
  • amount: amount in the transaction (this may differ from original amount due to rounding errors; you should make sure this is correct)
  • currency: currency used in the transaction
  • success: boolean success flag (true if all went well)

The transaction object will also gain a property messages which will contain any messages generated by the gateway. In case of a successful transaction, A message will be returned for the field 'transaction', which will read 'Success'. Other messages are assigned to individual fields for which there were any errors.

The card object used for the transaction will also be update after processing a transaction. The card will gain the method property, and have all its fields populated with data from the gateway. This is effectively like doing a card.load(). Refer to documentaiton for the daimyo.Card.load() method.

The card object is not required for credit and void transactions. For those transactions, you can safely pass null as the first argument, or just omit the first argument.

  • param: daimyo.Card [card] Payment method to use for the transaction

  • param: Function callback Callback function

transaction.Transaction.prototype.process = function(card, callback) {
  var self = this;
  var transactionData;
  
  if (typeof card === 'function') {
    callback = card;
    card = null;
  }

  if (self.useToken && (!card || !card.token)) {
    callback(new DaimyoError('system', 'Card has no token', null));
    return;
  }

  // Augment transaction data with token
  transactionData = self.data;

  // Currency check
  if (transactionData.currency &&
      config.option('allowedCurrencies').indexOf(transactionData.currency) < 0) {
    
    callback(new DaimyoError(
      'system', 
      'Currency not allowed', 
      transactionData.currency
    ));
    return;

  }


  if (!transactionData.custom) {
    transactionData.custom = {};
  }

  // Add response validation hash and remember it
  transactionData.custom._check = ducttape.randomHash();
  self.rememberHash(transactionData.custom._check);

  // JSONify custom field
  try {
    transactionData.custom = JSON.stringify(transactionData.custom);
  } catch(e) {
    // If JSONification failes, remove the custom data completely
    delete transactionData.custom;
  }

  if (self.useToken) {
    transactionData.paymentMethodToken = card.token;
  }

  transactionData = xmlutils.toSamurai(transactionData, transaction.MAPPINGS);

  authpost.makeRequest({
    path: self.path,
    payload: xmlutils.toXML(transactionData, 'transaction'),
    method: 'POST'
  }, function(err, res) {
    // Error handling

    if (err && err.category === 'system') {
      callback(err);
      return;
    }

    if (res.status !== 200) {
      callback(new DaimyoError(
        'system',
        'Gateway responded with non-200 status',
        {status: res.status, body: res.body, 
         messages: err && messages.translateAll(err.details)}
      ));
      return;
    }

    // This should always be true unless there were system errors
    if (err && err.category == 'gateway') {
      self.messages = messages.translateAll(err.details, 'en_US');
    }

    // Load transaction data and payment method data
    if (!self._loadFromResponse(res.body, card)) {
      callback(new DaimyoError(
        'transaction',
        'Request data does not match the response data',
        null
      ));
    }

    // Exit with no error
    callback(null);
  });

};

ashigaru

support/ashigaru.js

Ashigaru

Copyright (c)2011, by Branko Vukelic branko@herdhound.com

jQuery plugin for making requests to Samurai payment gateway via a hidden iframe.

Ashigaru is part of Daimyo (https//github.com/HerdHound/Daimyo).

About the JSON parser

Ashigaru requires either a native JSON parser, or a 3rd party solution.

If your environment (browser(s) you intend to support) does not provide the native JSON object, you can use the Crockford's implementation found on on Github.

Preparing the server

Before you can use this plugin, you must make sure that a valid redirect URL is handled on your server. Once Samurai gateway receives data from this plugin, it will redirect the user to the redirect URL. The redirect URL will have an URL parameter called payment_method_token which should be a 24-digit hexdigest. That token identifies the payment method created by the user. (See the documentation for the daimyo module for more information on how to use this token.)

Once you have performed any operations you want, you can respond to the request that was made to the redirect URL. The response should be text/html, and it should contain at least the <body> tag, and a single <script> tag that contains the JSON response. A typical success response may look like this:

<body><script>({"status": "ok"})</script></body>

Note that the JSON is wrapped in brackets to make it a valid object expression. Otherwise, the browsers will complain.

There is no need to add standard HTML markup because this response will never be rendered. Once you have set up the server-side handler to work this way, you are ready to start using Ashigaru.

Basic usage

Generally, it is expected that you will take care of the user-facing form yourself, validate the input, and extract the data into an object. The object should have the following properties:

  • firstName: cardholder's first name (optional)
  • lastName: cardholder's last name (optional)
  • address1: cardholder's address line 1 (optional)
  • address2: cardholder's address line 2 (optional)
  • city: city (optional)
  • state: state or region (optional)
  • country: country (optional)
  • zip: zip/postal code (optional)
  • number: card number (required)
  • csc: card security code, CCV, CVV, CVC, etc (required)
  • year: expiration year (optional)
  • month: expiration month (optional)
  • custom: JSON-serializable object containing any custom data that you want to have saved in the payment method (optional)
  • sandbox: Whether the payment method will be marked as sandbox method (sandbox methods cannot be used to process transactions)

As you can see, only card number and CSC (a.k.a CCV, CVC, CVV) are required, and other fields are optional. To increase the chance of transactions actually clearing, and minimize your liability in case of trouble, you should try to collect as much data as possible, but cardholder name, zip, and address line 1 are probably bare minimum.

Once you have created the object containing the cardholder data, you can call Ashigaru like this:

$.ashigaru(cardData, merchantKey, redirectURL, function(err, data) {
  // err means there was an error parsing the JSON or no data
  // was ever returned. If all went fine, err should be null.
  // data will contain a fully parsed JSON response.
});

Ashigaru currently has no mechanisms for retrying a failed connection. You should make sure you handle such unforeseen problems yourself.

(function($, JSON, window) {

  var originalDomain = window.document.domain;
  var samuraiURI = 'https://api.samurai.feefighters.com/v1/payment_methods';
  var timeoutLabel;
  var timedOut = false;
  var dataReceived = false;
  var timeoutInterval = 40000; // 40 seconds

Remove the hidden form

function removeForm() {
    $('#samurai-form').remove();
    removeIframe();
  }

jQuery.ashigaru(data, merchantKey, redirectURI, callback)

Create and submit hidden form to make request with

The data argument is used to populate form with data. It can have any of the following fields:

  • firstName
  • lastName
  • address1
  • address2
  • city
  • state
  • zip
  • number
  • csc
  • year
  • month
  • custom

The callback is called from the load event handler on the iframe. See the details of the onResultLoad function in this module.

  • param: Object data Object containing the data for the form

  • param: String merchantKey Samurai gateway merchant key

  • param: String redirectURI URI of the transparent redirect handler

  • param: Function callback Called with error and results

$.ashigaru = function(data, merchantKey, redirectURI, callback) {
    var form;

    if (!data || !merchantKey || !redirectURI || !callback) {
      throw ('All arguments are required');
    }

    var fullData = {
      firstName: '',
      lastName: '',
      address1: '',
      address2: '',
      city: '',
      state: '',
      zip: '',
      number: '',
      csc: '',
      year: '',
      month: '',
      custom: {}
    };

    $.extend(fullData, data);

    // Serialize custom data and ignore it on failure
    try {
      // JSON-ify data
      fullData.custom = JSON.stringify(fullData.custom);
      // Escape single quotes
      fullData.custom = fullData.custom.replace(&quot;'", "\\'&quot;);
    } catch(e) {

Fail silently

}

    var formHTML = '<form id="samurai-form" style="display:none" ' +
      'action="$requestURI" method="POST" target="samurai-iframe">' + 
      '<input type="hidden" name="merchant_key" value="$merchantkey">'+
      '<input type="hidden" name="redirect_url" value="$redirecturl">' +
      '<input type="hidden" name="custom" value=\'$custom\'>' +
      (data.sandbox &amp;&amp; '<input type="hidden" name="sandbox" value="true">' || '') +
      '<input type="hidden" name="credit_card[first_name]" value="$firstName">'+
      '<input type="hidden" name="credit_card[last_name]" value="$lastName">' +
      '<input type="hidden" name="credit_card[address_1]" value="$address1">' +
      '<input type="hidden" name="credit_card[address_2]" value="$address2">' +
      '<input type="hidden" name="credit_card[city]" value="$city">' +
      '<input type="hidden" name="credit_card[state]" value="$state">' +
      '<input type="hidden" name="credit_card[zip]" value="$zip">' +
      '<input type="hidden" name="credit_card[card_number]" value="$number">' +
      '<input type="hidden" name="credit_card[card_type]" value="$issuer">' +
      '<input type="hidden" name="credit_card[cvv]" value="$csc">' +
      '<input type="hidden" name="credit_card[expiry_year]" value="$year">' +
      '<input type="hidden" name="credit_card[expiry_month]" value="$month">' +
      '</form>';

    // Replace non-data field placeholders and form attribute placeholders
    formHTML = formHTML.replace('$requestURI', samuraiURI);
    formHTML = formHTML.replace('$merchantkey', merchantKey);
    formHTML = formHTML.replace('$redirecturl', redirectURI);

    // Replace placeholders in form HTML with real data
    for (var key in fullData) {
      if (fullData.hasOwnProperty(key)) {
        formHTML = formHTML.replace('$' + key, fullData[key] || '');
      }
    }

    // Inject this into DOM
    form = $(formHTML);

    // Sanity check
    if (form.attr('action') === '$requestURI') { 
      throw 'Form error'; 
    }

    // Attach the form to body
    form.appendTo('body');

    // Submit the form
    form.unbind('submit');
    form.submit(function(e) {
      e.stopPropagation();
      return createIframe(callback);
    });

    // Fire the timout clock at 3 seconds
    timeoutLabel = window.setTimeout(function() {

      // Set the timeout flag
      timedOut = true;

      // Execute callback with error message
      if (!dataReceived) {
        callback('Connection timed out');
      }

    }, timeoutInterval);

    form.submit();
  };
 
}(jQuery, JSON, this));

check

lib/check.js

check

Copyright (c)2011, by Branko Vukelic branko@herdhound.com

This module contains functions for performing various credit card checks. The checks it performs are:

  • Luhn Mod-10 check (verifies if card number is valid)
  • CSC check (verifies if the CSC/CVV/CCV number has correct number of digits)
  • Issuer check (determines the issuer of the card)

This module can be converted for use in browsers by using the checkamd target in the makefile:

make checkamd

The above command converts this module into an AMD module loadable with AMD-compliant loaders like RequireJS.

var check = exports;
var NON_DIGIT_CHARS = /[^\d]+/g;

check.issuers

Issuer names with regexes for checking the card numbers and CSCs

Issuer check regexes are currently based on the following resources:

These regexes are bound to change as issuers landscape changes over time. They are also not meant to be comprehensive Issuer check. They are meant to catch major cards for further CVC (CCV) check.

Currently, the following issuers are detected by this module:

  • Visa
  • MasterCard
  • American Express
  • Diners Club
  • Discover
  • JCB (Japan Credit Bureau)
  • China UnionPay

Note that China UnionPay cards will fail the Luhn Mod-10 check.

check.issuers = {
  VISA: ['Visa', /^4(\d{12}|\d{15})$/, /^\d{3}$/],
  MAST: ['MasterCard', /^5[1-5]\d{14}$/, /^\d{3}$/],
  AMEX: ['American Express', /^3[47]\d{13}$/, /^\d{4}$/],
  DINA: ['Diners Club', /^3(00|05|6\d|8\d)\d{11}$/, /^\d{3}$/],
  DISC: ['Discover', /^(622[1-9]|6011|64[4-9]\d|65\d{2})\d{12}$/, /^\d{3}$/], 
  JCB: ['JCB', /^35(28|29|[3-8]\d)\d{12}$/, /^\d{3}$/], 
  CHIN: ['China UnionPay', /^62\d{14}/, /^\d{3}$/]
};

check.extractDigits(s)

Removes all non-digit characters from a given string

This function replaces all non-digit strings with an empty string. The resulting string contains only digits.

check.extractDigits() is used throughout the check module to make sure that the card numbers and CSCs are parsed correctly.

Example

var n = '1234-5678-1234-5678';
console.log(check.extractDigits(n);
// outputs: '1234567812345678'
n = 'abcd';
console.log(check.extractDigits(n);
// outputs: ''

  • param: String s String to operate on

  • returns: String String with stripped out non-digit characters

check.extractDigits = function(s) {
  if (typeof s !== 'string') {
    return '';
  }
  return s.replace(NON_DIGIT_CHARS, '');
};

check.getIssuer(card, [full])

Returns the issuer of the card

The card number is stripped of any non-digit characters prior to checking. The full flag can be used to retrieve all issuer detauls (regex for checking card numbers and CSC) or just the issuer name.

Example

check.getIssuer('4111111111111111');
// returns: 'Visa'
check.getIssuer('4111111111111111', true);
// returns: ['Visa', /^4(\d{12}|\d{15})$/, /^\d{3}$/]

  • param: String card Card number

  • param: Boolean [full] Whether to return the issuer details instead of name

  • returns: String String representing the issuer name

check.getIssuer = function(card, full) {
  var lastMatch = full ? ['Unknown', /^\d{16}$/, /^\d{3}$/] : 'Unknown';
  card = check.extractDigits(card);
  if (!card) {
    return 'Unknown';
  }
  Object.keys(check.issuers).forEach(function(issuer) {
    if (check.issuers[issuer][1].test(card)) {
      lastMatch = full ? check.issuers[issuer] : check.issuers[issuer][0];
    }
  });
  return lastMatch;
};

check.mod10check(card)

Checks the validity of credit card using the Luhn Mod-10 algorithm

The card number is checked after all non-digit characters are removed from the original string. If the check succeeds, the sanitized number is returned. Otherwise, null is returned.

Please note that China UnionPay cards always fail the Luhn Mod-10 check. If you need to support China UnionPay cards, you need to make an exception for them when validating the card number:

if (check.getIssuer(cardNumber) === 'China UnionPay') {
  return cardNumber;
} else {
  return check.mod10check(cardNumber);
}

Since we have no access to valid test numbers for China UnionPay cards, we don't know if Samurai gateway itself will accept them.

Example

check.mod10check('4111-1111-1111-1111');
// returns: '4111111111111111'
check.mod10check('123-bogus');
// returns: null
check.mod10check();
// returns: null

  • param: String card Card number in string format

  • return: Object Sanitized number if valid, otherwise null

check.mod10check = function(card) {
    var sum = 0;
    var totalDigits;
    var oddEven;
    var digits;
    var i;
    var current;

    card = check.extractDigits(card);  

    if (!card) {
      // Card number contains no digits
      return null;
    }

    totalDigits = card.length;
    oddEven = totalDigits &amp; 1;
    digits = card.split(''); // Convert to array

    for (i = totalDigits; i; --i) {
      current = totalDigits - i; 
      digit = parseInt(digits[current], 10);

      if (!((current &amp; 1) ^ oddEven)) {
        digit = digit * 2;
      }
      if (digit &gt; 9) {
        digit -= 9;
      }

      sum = sum + digit;
    }

    return (sum % 10) === 0 &amp;&amp; card;
};

check.cscCheck(card, csc)

Checks the card security code (CSC) given card number and the code

Card number is required because the CSC is not the same format for all issuers. Currently, American Express cards have a 4-digit CSC, while all other issuers (that we know of) have a 3-digit CSC.

Example

check.cscCheck('4111111111111111', '111');
// returns: true
check.cscCheck('4111111111111111', '11');
// returns: false

  • param: String card Credit card number

  • param: String csc Card security code

  • returns: Boolean Boolean value of the test status

check.cscCheck = function(card, csc) {
  var issuerDetails;

  csc = check.extractDigits(csc);
  if (!csc) { return false; }

  issuerDetails = check.getIssuer(card, true);
  return issuerDetails[2].test(csc);
};

xmlutils

lib/xmlutils.js

xmlutils

Copyright (c)2011, by Branko Vukelic branko@herdhound.com

Utitilities for parsing Samurai XML responses. These utilities are not general-purpose XML parsers and generators. Some of them can be quite useful outside the Daimyo context, but there's no guarantee of that.

In general, you shouldn't need to touch these methods yoursef ever, unless you are considering developing Daimyo.

var xmlutils = exports;

xmlutils.toInt(s)

Convert a string to integer

  • param: String s String to be parsed

  • returns: Number Converted integer

xmlutils.toInt = function(s) {
  return parseInt(s, 10);
};

xmlutils.toFloat(s)

Convert string to a float

  • param: String s String to convert

  • returns: Number Converted float

xmlutils.toFloat = function(s) {
  return parseFloat(s);
};

xmlutils.toDate(s)

Convert a string to Date object

  • param: String s String to be parsed

  • returns: Date Converted date

xmlutils.toDate = function(s) {
  return new Date(s);
};

xmlutils.toBool(s)

Convert a string to Bool

This function treats 'true' as true, and everything else as false.

  • param: String s String to be parsed

  • returns: Bool Converted boolean

xmlutils.toBool = function(s) {
  return s === 'true' ? true : false;
};

xmlutils.toString(s)

Pass-through function that does nothing

  • param: String s Strning to be passed through

  • returns: String Same string that came in

xmlutils.toString = function(s) {
  return s;
};

xmlutils.extractData(xml, recipe)

Extract data from XML string using recipe mappings

Recipe object contains a mapping between tag names and coercion functions like xmlutils.toInt and xmlutils.toDate. Each key in the recipe object will be treated as a valid tag name, and searched for within the XML. Data found within the XML string will be coerced and assigned to a key in the result object. The key name in the result object will match the key name from the recipe object.

An example recipe object:

{
  'payment_method_token': xmlutils.toString,
  'created_at': xmlutils.toDate,
  ....
}

A recipe may contain a special key '@' that can hold the name of the top-level node that should be searched (otherwise the recipe is applied to the whole XML source. For example:

{
  '@': 'payment_method',
  .....
}

  • param: String xml The source XML string

  • param: Object recipe The recipe object

  • returns: Object Name-value pairs of extracted data

xmlutils.extractData = function(xml, recipe) {
  var result = {};

  // Should we only parse a single node?
  if (recipe['@']) {
    xml = getInnerXML(xml, recipe['@']);
  }

  Object.keys(recipe).forEach(function(key) {
    if (key === '@') {
      // Ignore this key
      return;
    }

    // Get the Regex for the recipe
    var rxp = getTagRe(key);
    var matches = matchAll(xml, rxp);

    matches.forEach(function(match) {
      if (!result.hasOwnProperty(key)) {
        result[key] = recipe[key](match.content);
      }
    });

    // If there's only one match, then return the single match w/o array
    if (result[key] &amp;&amp; result[key].length &lt; 2) {
      result[key] = result[key][0];
    }

  });

  return result;
};

xmlutils.hasMessages(xml)

Checks the XML to see if there is a <messsages> block

Returns true if there is a <messages> block.

  • param: String xml The XML string

  • returns: Boolean Whether it found the <messages> block

xmlutils.hasMessages = function(xml) {
  return Boolean((/<messages[^>]*>/).exec(xml));
};

xmlutils.extractMessages(xml)

Extract messages from XML string

The messages can be of the following classes:

  • error: errors and declines
  • info: success and other information

The messages will also have a context in which it was transmitted, and the message that describes the details.

The return object may look like this:

{
  error: [{'input.card_number': 'failed_checksum'}]
}

  • param: String xml The XML string to be parsed

  • returns: Array Array of error messages.

xmlutils.extractMessages = function(xml) {
  var matches = matchAll(xml, getTagRe('message'));
  var messages = [];

  matches.forEach(function(match) {
    var message = {};

    message.cls = match.attributes.subclass;
    message.context = match.attributes.context;
    message.key = match.attributes.key;
    message.text = match.content;

    messages.push(message);
  });

  return messages;
};

xmlutils.decamelize(s)

Converts 'CamelCase' to 'camel_case'

  • param: String s String to convert

  • returns: String Decamelized string

xmlutils.decamelize = function(s) {
  s = s.replace(/^[A-Z]/, function(match) {
    return match.toLowerCase();
  });
  s = s.replace(/[A-Z]/g, function(match) {
    return '_' + match.toLowerCase();
  });
  return s;
};

xmlutils.toXML(o, [topLevel], [keys])

Shallow conversion of JavaScript object to XML

This method converts each key to a single XML node and converts the contents of the key to a text node by calling toString on it.

Optionally, the whole XML string can be wrapped in a top-level tag.

This function does not care about the order of keys, so if you need the nodes to be in a certain order, you should supply an array of keys.

  • param: Object o Object to be converted

  • param: String [topLevel] Top level tag

  • param: Array [keys] Array of keys to use so that nodes are ordered

  • returns: String XML version of the object

xmlutils.toXML = function(o, topLevel, keys) {
  var xml = '';

  // Was topLevel arg skipped?
  if (typeof topLevel != 'string') {
    keys = topLevel;
    topLevel = null;
  } else {
    xml += '<' + xmlutils.decamelize(topLevel) + '>\n';
  }

  keys = keys || Object.keys(o);

  keys.forEach(function(key) {
    var tagName = xmlutils.decamelize(key);
    xml += '<' + tagName + '>' + o[key].toString() + '</' + tagName + '>\n';
  });

  if (topLevel) {
    xml += '</' + xmlutils.decamelize(topLevel) + '>\n';
  }
  
  return xml;
};

xmlutils.toSamurai(o, mappings)

Remaps the Daimyo field names to Samurai node names in an object

Given an object with Daimyo field names, this method converts only the fields found in MAPPINGS and renames the fields to Samurai node names returning a new object with the renamed fields.

Example

var daimyoObj = {
   fullName: 'Foo',
   lastName: 'Bar',
   year: 2012
}
xmlutils.toSamurai(daimyoObj, daimyo.MAPPINGS);
// returns: 
//   {
//     fullName: 'Foo',
//     lastName: 'Bar',
//     expiry_year: 2012
//   }

  • param: Object o Daimyo object

  • param: Object mappings The mappings for he field names

  • returns: Object Object with remapped fields

xmlutils.toSamurai = function(o, mappings) {
  var remapped = {};

  Object.keys(o).forEach(function(key) {
    if (mappings.hasOwnProperty(key)) {
      remapped[mappings[key]] = o[key];
    } else {
      remapped[key] = o[key];
    }
  });

  return remapped;
};

authpost

lib/authpost.js

authpost

Copyright (c)2011, by Branko Vukelic

This library contains low-level methods for communicating with the Samurai payment gateway. You should never need to access these methods directly.

The makeRequest method is used to send requests to the gateway using the credentials that have been set using the config.configure() and config.option() methods.

var http = require('http');
var config = require('./config');
var debug = config.debug;
var xmlutils = require('./xmlutils');
var DaimyoError = require('./error');
var API_VER = config.option('apiVersion');
var API_HOST = 'api.samurai.feefighters.com';
var API_URL = '/v' + API_VER;
var DEFAULT_PATH = '/payment_methods';
var DAIMYO_VERSION = require('./config').DAIMYO_VERSION;

authpost.makeRequest([opts], callback)

Low-level abstraction of http client functionality

All options are optional (no pun intended):

  • path: Path under the main gateway URL
  • method: Either 'POST' or 'GET'
  • payload: XML or urlencoded payload to send
  • headers: Custom headers

An example usage:

authpost.makeRequest({
  path: '/payment_methods/xxxxxxxxxxxxxxxxxxxxxxxx.xml',
  method: 'PUT',
  payload: xmlData
}, function(err, res) {
  if (err) {
    // There was an error with the gateway
    console.log(err);
    return;
  }
  if (res.status !== 200) {
    // Bad status code
  }
  // etc...
});

If the options are not provided, the request path will default to /payment_methods, and the request method will default to 'GET'. There is no data that you can get from the default request, though, but it can be used to test the connection in general.

This method automatically sets the appropritate Content-Length and Content-Type headers. Content-Type can be either text/xml or application/x-www-form-urlencoded. The Content-Type is set based on the first character of the payload: if it is an lesser-than (<) character, it is set to text/xml.

  • param: Object [opts] Request options

  • param: Function callback Callback that will be called with err and res

exports.makeRequest = function(opts, callback) {
  var https = require('https');
  var path;
  var method;
  var payload;
  var headers;
  var requestOptions;
  var request;

  if (!config.option('merchantKey') || !config.option('apiPassword')) {
    callback(new DaimyoError('system', 'Daimyo is not configured', null));
  }

  if (!callback &amp;&amp; typeof opts === 'function') {
    callback = opts;
    opts = {};
  }

  // Is callback found?
  if (typeof callback !== 'function') {
    throw new Error('Callback function is required');
  }

  path = API_URL + (opts.path || DEFAULT_PATH);
  method = opts.method || 'GET'; // defaults to GET
  payload = opts.payload;

  debug(method + ' ' + path + ' ' + (payload ? 'with' : 'without') + ' payload');
  debug(payload);

  // Set headers
  headers = {
    'Authorization': generateCreds(),
    'User-Agent': 'Daimyo/' + DAIMYO_VERSION + ' Node.js/' + process.version,
    'Accepts': 'text/plain; q=0.5, text/xml; q=0.8, text/html',
    'Date': new Date().toString(),
    'Content-Length': 0
  };

  if (payload) {
    headers['Content-Length'] = payload.length;

    if (payload[0] === '<') {
      headers['Content-Type'] = 'text/xml';
    } else {
      headers['Content-Type'] = 'application/x-www-form-urlencoded';
    }
  }

  // Set request options
  requestOptions = {
    host: API_HOST,
    path: path,
    method: method,
    headers: headers
  };

  // Initialize the request object
  request = https.request(requestOptions, function(response) {
    var res = {};
  
    res.base = response;
    res.status = response.statusCode;
    res.headers = response.headers;
    res.body = '';

    // response.setEncoding('UTF-8');

    // Buffer the response body
    response.on('data', function(chunk) {
      res.body += chunk;
    });

    response.on('end', function(err) {
      var error;

      // System error
      if (err) {
        error = new DaimyoError('system', 'There was an error with the request', err);
      } else {
        error = parseMessages(res.body);
      }
      
      debug('Finished request to ' + path + ' with body: ' + res.body);
      callback(error, res);
    });
  });

  request.on('error', function(err) {
    callback(new DaimyoError('system', 'There was an error with the request', err));
  });

  if (payload) { request.write(payload); }
  request.end();
};

ducttape

lib/ducttape.js

ducttape

Copyright (c)2011, by Branko Vukelic branko@herdhound.com

Miscellaneous helper functions for boring tasks.

var ducttape = exports;

ducttape.copyArray(arr)

Deep-copies an array

This method supports clones of simple types only. Objects created with custom constructors will be treated as normal objects.

  • param: Array arr Array to be cloned

  • returns: Array Cloned array

ducttape.copyArray = function copyArray(arr) {
  var clone = [];

  // Iterate `arr` and clone members to `clone`
  arr.forEach(function(item) {
    if (typeof item !== 'object') {
      // First take care of simple types
      clone.push(item);
    } else if (Array.isArray(item)) {
      // Take care of arrays
      clone.push(copyArray(item)); // recurse
    } else {
      // Take care of normal objects
      clone.push(ducttape.deepCopy(item));
    }
  });

  return clone;
};

ducttape.deepCopy(obj)

Deep-copies an object

This method only supports clones of simple types. Objects created with custom constructors are not supported.

  • param: Object obj Object to clone

  • returns: Object The cloned object

ducttape.deepCopy = function deepCopy(obj) {
  var clone = {};

  // Iterate `obj`'s keys and clone them to `clone`
  Object.keys(obj).forEach(function(key) {
    
    // Take care of simple types
    if (typeof obj[key] !== 'object') {
      clone[key] = obj[key];
    } else if (Array.isArray(obj[key])) {
      clone[key] = ducttape.copyArray(obj[key]);
    } else {
      clone[key] = deepCopy(obj[key]);
    }
  });

  return clone;
};

addOneTimeAccessor(obj, property, [validator, failSilently]) One-timer accessors with optional validator to an object

The setter function allows setting of a value only once, and it should fail silently on subsequent attempts. Getter works as usual. This gives you a completely tamper-free property on the given object. Once set, the property is cemented, and there is no way of modifying the original value variable from outside.

If the value is an object, it is never returned directly. A clone will be created first. Same applies to arrays. This prevents tampering with the objects that are used as values.

  • param: Object obj Object to which to attach the accessors

  • param: String property Name of the property

  • param: Function [validator] Optional validator function

  • param: Boolean [failSilently] Whether to throw on validation error

ducttape.addOneTimeAccessor = function(obj, property, validator, failSilently) {
  // Protect the original value within the closure
  var val;
  var set = false;

  obj.__defineSetter__(property, function(value) {
    console.log(set);

    // Already set
    if (set) {
      return;
    }

    // Illegal value
    if (typeof validatro === 'function' &amp;&amp; !validator(value)) {
      if (failSilently === true) {
        return;
      }
      throw new Error('Illegal value for property ' + property);
    }

  // Set the value and seal it
  val = value;
  set = true;
  });

  obj.__defineGetter__(property, function() {
    if (typeof val !== 'object') {
      return val;
    } else if (Array.isArray(val)) {
      return ducttape.copyArray(val);
    } else {
      return ducttape.deepCopy(val);
    }
  });
};

ducttape.randomString(len, [pool])

Creates a random string of specified length

This function requires Mersenne Twister random number generator implemented in the node-mersenne package.

  • param: Number len Length of the generated string

  • param: String [pool] Pool of characters to use for result string

  • returns: String Random string

ducttape.randomString = function(len, pool) {
  var random = require('mersenne');
  var poolLength;
  var output = [];

  pool = pool || 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345' +
    '67890!@#$%^&*()_+{}][;"\'/?.,&lt;&gt;';
  poolLength = pool.length;

  for (var i = poolLength; i; --i) {
    output.push(pool[random.rand(poolLength)]);
  }

  return output.join('');
};

ducttape.randomHash()

Creates SHA1 hexdigest of a 100-character random string

  • returns: String Random SHA1 hexdigest

ducttape.randomHash = function() {
  var crypto = require('crypto');
  var hash = crypto.createHash('sha1');
  var rstr = ducttape.randomString(100);

  hash.update(rstr);

  return hash.digest('hex');
};

messages

lib/messages.js

messages

Copyright (c)2011, by Branko Vukelic branko@herdhound.com

Translation of various gateway error messages to human-readable format.

All error messages generated by Samurai gateway are translated using the messages.translateAll and messages.translate methods. The former translates all messages returned by authpost.makeRequest method, and the latter translates individual messages.

In either case, what we end up with is a mapping between fields and messages. Depending on what you are trying to do, the fields can be card data fields, transaction details fields, or virtual fields such as 'gateway', or 'bank'.

var msg = exports;

messages.translate(message, lang)

Returns a human readable string in given language

The return value is an object that maps a Daimyo field name to human-readable translation. The field name can be a virtual field like 'system' or 'gateway', which indicates the error pretains to components of payment processing workflow, rather than a field.

  • param: Object message Message object to be translated

  • param: String [lang] Optional language code (defaults to en_US)

  • returns: Object Object containing the field name, and human readable string

msg.translate = function(message, lang) {
  try {
    var mapping = msg.mappings[message.cls][message.context][message.key];
    lang = lang || 'en_US';
    return {
      field: mapping[0],
      message: mapping[1](lang)
    };
  } catch(e) {
    // We have an unknown message
    return {
      field: 'unknown',
      message: message.cls + ':' + message.context + ':' + message.key
    };
  }
};

messages.translateAll(messages, lang)

Translate multiple messages, and return a single field-message mapping

  • param: Array messages Array of message objects

  • param: String [lang] Optional language code (defaults to en_US);

  • returns: Object Object containing the field names, and human readable strings

msg.translateAll = function(messages, lang) {
  var errors = messages.filter(function(item) {
    return item.cls === 'error';
  });
  var info = messages.filter(function(item) {
    return item.cls === 'info';
  });
  function build(msgs) {
    var translations = {};
    msgs.forEach(function(message) {
      translation = msg.translate(message, lang);
      if (translations.hasOwnProperty(translation.field) &amp;&amp; 
          translations[translation.field].indexOf(translation.message) &lt; 0) {
        translations[translation.field].push(translation.message);
      } else {
        translations[translation.field] = [translation.message];
      }
    });
    return translations;
  }
  return {
    errors: build(errors),
    info: build(info)
  };
};

error

lib/error.js

error

Copyright (c)2011, by Branko Vukelic branko@herdhound.com

DaimyoError object used as the main error object throughout the Daimyo implementation.

var util = require('util');

error.DaimyoError

Daimyo error object

  • constructo: r

function DaimyoError(category, message, details) {
  Error.call(this);
  Error.captureStackTrace(this, this.constructor);
  this.category = category;
  this.message = message;
  this.details = details;
  this.name = this.constructor.name;
}

util.inherits(DaimyoError, Error);

DaimyoError.prototype.toString = function() {
  return this.category + ': ' + this.message + ': ' +
    util.inspect(this.details);
};

module.exports = DaimyoError;