CakePHP Braintree Plugin
This plugin is based completely around capturing credit card information to the Braintree vault using Braintree's Transpaent Redirect method. It helps you easily output a form that can capture billing information to a vault record, and then use that record to make charges against at any point in the future. This is useful for traditional ecommerce, or for subscription-based payments. But because it does not interact directly with Braintree's subscription features (instead opting to just store everything in the vault), you must develop your own scheduling system should you want to manage recurring payments. This is great if you need flexibility, as it allows you to develop the system exactly how you want it, and then use the plugin to easily trigger a transaction of any amount, at any point in time.
A group of local MySQL tables are created and used to synchronize your CakePHP application with the Braintree vault. This allows for maximum performance and flexibility; you will have a local record of everything stored in the vault that you can query against, which leaves the use cases up to your imagination -- link credit card IDs up to recurring transaction rows in the same database, find & email all customers with credit cards that are about to expire, etc.
When in shell mode, the sandbox or production environment is used based on the debug setting in CakePHP. Make sure it is zero if you want to use production, or anything greater than zero if you would like to test in the sandbox.
You can use one or more Braintree accounts. All API credentials are stored in the braintree_merchants table, so populate that table with your information. In order to let Braintree know which account you want to use at any given time, you must set the credentials before any calls to the API. Preferably, this logic lives in the beforeFilter()
of any controller/action which accesses Braintree:
$merchant_id = 1; // the merchant ID you want to use
$braintree_configs = ClassRegistry::init('Braintree.BraintreeMerchant')->find('first', array(
'conditions' => array(
'BraintreeMerchant.id' => $merchant_id
),
'contain' => false
));
BraintreeConfig::set(array(
'merchantId' => $merchant_id,
'publicKey' => $braintree_configs['BraintreeMerchant']['braintree_public_key'],
'privateKey' => $braintree_configs['BraintreeMerchant']['braintree_private_key'],
'environment' => 'production' // 'sandbox' or 'production'
));
In order to output the credit card intake form, you must first have a customer record to associate the credit card data with. The best way to do this is to use a convenience method in the BraintreeCustomer model from within the controller action that will output the intake form.
Example:
public function credit_card_intake () {
// $app_customer contains an array of information about the customer record in our application
$braintree_customer_id = ClassRegistry::init('Braintree.BraintreeCustomer')->getOrCreateCustomerId(
'Customer', // A model in your app that you want to associate the Braintree customer with
$app_customer['id'], // A foreign_id in your app that you want to associate the Braintree customer with
array(
'first_name' => $app_customer_first_name,
'last_name' => $app_customer['last_name'],
'company' => $app_customer['company'],
'email' => $app_customer['email'],
'phone' => $app_customer['phone'],
'fax' => $app_customer['fax'],
'website' => $app_customer['website']
)
);
$this->set('braintree_customer_id', $braintree_customer_id);
}
There is an element that makes it extremely simple to output a nicely styled credit card intake form that submits directly to Braintree and redirects back to your website with status & token information. All of the options for the form are documented at the top of the element, so be sure to look at the "elements/credit_cards/vault_form.ctp" file.
Example:
echo $this->element(
'credit_cards/vault_form',
array(
'plugin' => 'braintree',
'braintree_merchant_id' => 1, // the merchant ID you want to use
'braintree_customer_id' => $braintree_customer_id,
'billing_address' => array(
'first_name' => $this->Session->read('previously_input_first_name'),
'last_name' => $this->Session->read('previously_input_last_name'),
'company' => $this->Session->read('previously_input_company'),
'address1' => $this->Session->read('previously_input_address1'),
'address2' => $this->Session->read('previously_input_address2'),
'city' => $this->Session->read('previously_input_city'),
'state' => $this->Session->read('previously_input_state'),
'postal_code' => $this->Session->read('previously_input_postal_code'),
'country_code_alpha_2' => $this->Session->read('previously_input_country_code_alpha_2')
),
'verify_credit_card' => true, // do you want to verify credit cards before accepting them? (costs more, but makes life easier)
'foreign_model' => 'Order', // A model in your app that you want to associate the Braintree vault record with
'foreign_id' => $this->Session->read('order_id_in_progress') // A foreign_id in your app that you want to associate the Braintree vault record with
)
);
The credit card intake form is set up to redirect back to the same controller & action it came from. But the controller needs to understand how to process it. So, in the controller containing the credit_card_intake() action, the BraintreeCallback component needs to be included.
Example:
public $components = array(
'Braintree.BraintreeCallback' => array(
'callback_actions' => array(
'credit_card_intake' => array( // The name of the action you'd like the BraintreeCallback component to watch for incoming Braintree data
'redirect' => array( // The URL you'd like to redirect to after successfully storing the vault token
'action' => 'review_order'
)
)
)
)
);
If there are errors storing the credit card information in the vault, they are stored in the BraintreeCallback's "braintree_error" property and can be used however you'd like. For example, in the credit_card_intake() action, the following logic could be inserted:
if (!empty($this->BraintreeCallback->braintree_error)) {
$this->Session->setFlash(
nl2br($this->BraintreeCallback->braintree_error),
'error'
);
$this->redirect(array('action' => $this->params['action']));
}
You may want to do some custom process with the Braintree status/token result. You can easily extend the component into your own component and take advantage of the various callbacks available. For example
App::import('Component', 'Braintree.BraintreeCallback');
class CustomBraintreeCallbackComponent extends BraintreeCallbackComponent {
public function onSuccess ($result) {
if (!parent::onSuccess($result)) {
return false;
}
// do some custom "success" logic with the $result here
return true;
}
}
Once you have a customer & that customer's billing information captured to a vault record, you will most likely want to charge transactions against it. You may want to do this once (in the case of a simple order), or many times over the course of months/years, using your own home-brewed subscription logic. Either are possible. To create a transaction, simply use the BraintreeTransaction model. For example:
$result = ClassRegistry::init('BraintreeTransaction')->save(array(
'BraintreeTransaction' => array(
'braintree_customer_id' => $the_customer_id_you_are_creating_a_transaction_against,
'braintree_credit_card_id' => $the_credit_card_token_you_are_creating_a_transaction_against,
'model' => 'Order', // A model in your app that you want to associate the Braintree vault record with
'foreign_id' => 1, // A foreign_id in your app that you want to associate the Braintree transaction with
'type' => 'sale',
'amount' => '50.00'
)
));
Authorization, captures, refunds, and voids can be done in a similar fashion. For example:
- Authorization: Use
type
= "authorization" instead oftype
= "sale" - Refund: Use
type
= "credit" instead oftype
= "sale", with thebraintree_transaction_id
equal to the transaction ID being refunded
For any transaction acting as an update to a previous transaction, ensure the ID of the transaction is set before saving:
- Capture: Set
type
= "sale" on a transaction previously saved astype
= "authorization" - Void: Set
status
= "voided" on a transaction previously saved astype
= "submitted_for_settlement"
Transaction statuses change over the course of time and your application needs a way to know about this. For example, if you submit a transaction right now, it may not settle for another few hours. That is the purpose of the shell script, which will update your local database with the freshest statuses whenever it is run. The command is:
/path/to/app/../cake/console/cake.php braintree update_transaction_statuses
The script takes an optional argument for updating transactions in either the sandbox or production. For example:
/path/to/app/../cake/console/cake.php braintree update_transaction_statuses production
Remember that a plugin is nothing more than an object-oriented mini-application.
Every model in this plugin can be queried to allow for some very advanced usage. For example, query the BraintreeCreditCard model using custom conditions to find all credit cards that are expiring. Anything you set your mind to.
For maximum flexibility/understanding every piece of code in this plugin can be analyzed and then extended to suit; this wiki is merely a guide, not a rule book.
Please email me at contact@anthonyputignano.com with any questions or suggestions.