Daimyo API documentationGetting 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 methodscard.load() : fetches payment method data from the Samurai vaultcard.update() : updates the payment method detailscard.retain() : instructs Samurai to permanently save the payment methodcard.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:
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).
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. | |
| 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.1';
|
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;
settings.debug = false;
settings.apiVersion = 1;
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.
|
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() + '`');
if (isConfigured &&
!settings.allowMultipleSetOption &&
option !== 'currency') {
throw new DaimyoError(
'system',
'Option ' + option + ' is already locked',
option);
}
switch (option) {
case 'merchantKey':
case 'apiPassword':
case 'processorId':
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:
throw new DaimyoError('system', 'Unrecognized configuration option', option);
}
}
return settings[option];
};
|
| 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.
|
function Card(opts) {
var self = this;
(function(self) {
var originalValues = {
number: undefined,
csc: undefined,
year: undefined,
month: undefined,
firstName: '',
lastName: '',
address1: '',
address2: '',
city: '',
state: '',
zip: '',
custom: undefined
};
self.__defineSetter__('_dirty', function(object) {
Object.keys(originalValues).forEach(function(field) {
if (object[field]) { originalValues[field] = object[field]; }
});
});
self.__defineGetter__('_dirty', function() {
var dirtyFields = [];
Object.keys(originalValues).forEach(function(field) {
if (self[field] !== originalValues[field]) {
dirtyFields.push(field);
}
});
return dirtyFields;
});
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));
(function(self, check) {
var yearVal;
var monthVal;
var numberVal;
var custVal;
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));
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;
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;
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.
|
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.
|
Card.prototype.isExpired = function() {
var expYear = new Date().getFullYear();
var expMonth = new Date().getMonth() + 1;
if (!this.year || !this.month) { return true; }
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);
});
|
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',
'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 || ''
};
if (config.option('sandbox')) {
paymentData.sandbox = true;
}
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) {
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;
}
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
});
|
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
});
|
Card.prototype.update = function(callback) {
var updatedFields = {};
var payload = '';
var self = this;
self._rejectNoToken(callback);
if (!self._dirty.length) {
callback(null);
return;
}
self._dirty.forEach(function(field) {
updatedFields[field] = self[field];
});
updatedFields = xmlutils.toSamurai(updatedFields, daimyo.MAPPINGS);
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.
|
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.
|
Card.prototype.redact = function(callback) {
var self = this;
self._rejectNoToken(callback);
self._executeRequest({
path: self._getTokenPath('/redact'),
method: 'POST'
}, callback);
};
daimyo.Card = Card;
|
| 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 = '/gateways/';
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).
|
transaction.Transaction = function(opts) {
if (!opts) {
throw new DaimyoError('system', 'Missing options object', null);
}
if (!opts.type || VALID_TRANSACTIONS.indexOf(opts.type) < 0) {
throw new DaimyoError('system', 'Missing or invalid transaction type', null);
}
if (!opts.data) {
throw new DaimyoError('system', 'Missing payload data');
}
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;
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);
(function(self, ducttape) {
var hash;
self.rememberHash = function(val) {
if (!hash) {
hash = val;
}
};
self.checkHash = function(val) {
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;
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;
}
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.
|
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;
}
transactionData = self.data;
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 = {};
}
transactionData.custom._check = ducttape.randomHash();
self.rememberHash(transactionData.custom._check);
try {
transactionData.custom = JSON.stringify(transactionData.custom);
} catch(e) {
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) {
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: messages.translateAll(err.details)}
));
return;
}
if (err && err.category == 'gateway') {
self.messages = messages.translateAll(err.details, 'en_US');
}
if (!self._loadFromResponse(res.body, card)) {
callback(new DaimyoError(
'transaction',
'Request data does not match the response data',
null
));
}
callback(null);
});
};
|
| 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://samurai.feefighters.com/v1/payment_methods';
var timeoutLabel;
var timedOut = false;
var dataReceived = false;
var timeoutInterval = 40000;
|
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);
try {
fullData.custom = JSON.stringify(fullData.custom);
fullData.custom = fullData.custom.replace("'", "\\'");
} 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 && '<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>';
formHTML = formHTML.replace('$requestURI', samuraiURI);
formHTML = formHTML.replace('$merchantkey', merchantKey);
formHTML = formHTML.replace('$redirecturl', redirectURI);
for (var key in fullData) {
if (fullData.hasOwnProperty(key)) {
formHTML = formHTML.replace('$' + key, fullData[key] || '');
}
}
form = $(formHTML);
if (form.attr('action') === '$requestURI') {
throw 'Form error';
}
form.appendTo('body');
form.unbind('submit');
form.submit(function(e) {
e.stopPropagation();
return createIframe(callback);
});
timeoutLabel = window.setTimeout(function() {
timedOut = true;
if (!dataReceived) {
callback('Connection timed out');
}
}, timeoutInterval);
form.submit();
};
}(jQuery, JSON, this));
|
| 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: ''
|
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) {
return null;
}
totalDigits = card.length;
oddEven = totalDigits & 1;
digits = card.split('');
for (i = totalDigits; i; --i) {
current = totalDigits - i;
digit = parseInt(digits[current], 10);
if (!((current & 1) ^ oddEven)) {
digit = digit * 2;
}
if (digit > 9) {
digit -= 9;
}
sum = sum + digit;
}
return (sum % 10) === 0 && 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);
};
|
| 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
|
xmlutils.toInt = function(s) {
return parseInt(s, 10);
};
|
xmlutils.toFloat(s)
Convert string to a float
|
xmlutils.toFloat = function(s) {
return parseFloat(s);
};
|
xmlutils.toDate(s)
Convert a string to Date object
|
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.
|
xmlutils.toBool = function(s) {
return s === 'true' ? true : false;
};
|
xmlutils.toString(s)
Pass-through function that does nothing
|
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 = {};
if (recipe['@']) {
xml = getInnerXML(xml, recipe['@']);
}
Object.keys(recipe).forEach(function(key) {
if (key === '@') {
return;
}
var rxp = getTagRe(key);
var matches = matchAll(xml, rxp);
matches.forEach(function(match) {
if (!result.hasOwnProperty(key)) {
result[key] = recipe[key](match.content);
}
});
if (result[key] && result[key].length < 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.
|
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'}]
}
|
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'
|
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 = '';
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;
};
|
| 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 .
|
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 && typeof opts === 'function') {
callback = opts;
opts = {};
}
if (typeof callback !== 'function') {
throw new Error('Callback function is required');
}
path = API_URL + (opts.path || DEFAULT_PATH);
method = opts.method || 'GET';
payload = opts.payload;
debug(method + ' ' + path + ' ' + (payload ? 'with' : 'without') + ' payload');
debug(payload);
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';
}
}
requestOptions = {
host: API_HOST,
path: path,
method: method,
headers: headers
};
request = https.request(requestOptions, function(response) {
var res = {};
res.base = response;
res.status = response.statusCode;
res.headers = response.headers;
res.body = '';
response.on('data', function(chunk) {
res.body += chunk;
});
response.on('end', function(err) {
var 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();
};
|
| 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.
|
ducttape.copyArray = function copyArray(arr) {
var clone = [];
arr.forEach(function(item) {
if (typeof item !== 'object') {
clone.push(item);
} else if (Array.isArray(item)) {
clone.push(copyArray(item));
} else {
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.
|
ducttape.deepCopy = function deepCopy(obj) {
var clone = {};
Object.keys(obj).forEach(function(key) {
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) {
var val;
var set = false;
obj.__defineSetter__(property, function(value) {
console.log(set);
if (set) {
return;
}
if (typeof validatro === 'function' && !validator(value)) {
if (failSilently === true) {
return;
}
throw new Error('Illegal value for property ' + property);
}
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!@#$%^&*()_+{}][;"\'/?.,<>';
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
|
ducttape.randomHash = function() {
var crypto = require('crypto');
var hash = crypto.createHash('sha1');
var rstr = ducttape.randomString(100);
hash.update(rstr);
return hash.digest('hex');
};
|
| 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) {
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) &&
translations[translation.field].indexOf(translation.message) < 0) {
translations[translation.field].push(translation.message);
} else {
translations[translation.field] = [translation.message];
}
});
return translations;
}
return {
errors: build(errors),
info: build(info)
};
};
|
| 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
|
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;
|