payfrit-works/cfpayment/api/gateway/stripe/stripe.cfc

907 lines
42 KiB
Text

<!---
Original code from Phil Cruz's Stripe.cfc from https://github.com/philcruz/Stripe.cfc/blob/master/stripe/Stripe.cfc
Added Stripe Connect/Marketplace support in 2015 by Chris Mayes & Brian Ghidinelli (http://www.ghidinelli.com)
Licensed under the Apache License, Version 2.0 (the "License"); you
may not use this file except in compliance with the License. You may
obtain a copy of the License at:
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
--->
<cfcomponent displayname="Stripe Gateway" extends="cfpayment.api.gateway.base" hint="Stripe Gateway" output="false">
<cfset variables.cfpayment.GATEWAY_NAME = "Stripe" />
<cfset variables.cfpayment.GATEWAY_VERSION = "1.0.7" />
<cfset variables.cfpayment.API_VERSION = "2015-08-19" />
<!--- stripe test mode uses different credentials instead of different urls --->
<cfset variables.cfpayment.GATEWAY_URL = "https://api.stripe.com/v1" />
<cffunction name="getSecretKey" access="public" output="false" returntype="string">
<cfif getTestMode()>
<cfreturn variables.cfpayment.TestSecretKey />
<cfelse>
<cfreturn variables.cfpayment.LiveSecretKey />
</cfif>
</cffunction>
<cffunction name="getLiveSecretKey" access="public" output="false" returntype="string">
<cfreturn variables.cfpayment.LiveSecretKey />
</cffunction>
<cffunction name="setLiveSecretKey" access="public" output="false" returntype="void">
<cfargument name="LiveSecretKey" type="string" required="true" />
<cfset variables.cfpayment.LiveSecretKey = arguments.LiveSecretKey />
</cffunction>
<cffunction name="getTestSecretKey" access="public" output="false" returntype="string">
<cfreturn variables.cfpayment.TestSecretKey />
</cffunction>
<cffunction name="setTestSecretKey" access="public" output="false" returntype="void">
<cfargument name="TestSecretKey" type="string" required="true" />
<cfset variables.cfpayment.TestSecretKey = arguments.TestSecretKey />
</cffunction>
<cffunction name="getPublishableKey" access="public" output="false" returntype="string">
<cfif getTestMode()>
<cfreturn variables.cfpayment.TestPublishableKey />
<cfelse>
<cfreturn variables.cfpayment.LivePublishableKey />
</cfif>
</cffunction>
<cffunction name="getLivePublishableKey" access="public" output="false" returntype="string">
<cfreturn variables.cfpayment.LivePublishableKey />
</cffunction>
<cffunction name="setLivePublishableKey" access="public" output="false" returntype="void">
<cfargument name="LivePublishableKey" type="string" required="true" />
<cfset variables.cfpayment.LivePublishableKey = arguments.LivePublishableKey />
</cffunction>
<cffunction name="getTestPublishableKey" access="public" output="false" returntype="string">
<cfreturn variables.cfpayment.TestPublishableKey />
</cffunction>
<cffunction name="setTestPublishableKey" access="public" output="false" returntype="void">
<cfargument name="TestPublishableKey" type="string" required="true" />
<cfset variables.cfpayment.TestPublishableKey = arguments.TestPublishableKey />
</cffunction>
<cffunction name="getApiVersion" access="public" output="false" returntype="string">
<cfreturn variables.cfpayment.API_VERSION />
</cffunction>
<cffunction name="setApiVersion" access="public" output="false" returntype="void">
<cfset variables.cfpayment.API_VERSION = arguments[1] />
</cffunction>
<cffunction name="authorize" output="false" access="public" returntype="any" hint="Authorize but don't capture a credit card">
<cfargument name="money" type="any" required="true" />
<cfargument name="account" type="any" required="false" />
<cfargument name="options" type="struct" required="false" default="#structNew()#" />
<cfset arguments.options["capture"] = false />
<cfreturn purchase(argumentCollection = arguments) />
</cffunction>
<cffunction name="capture" output="false" access="public" returntype="any" hint="Capture a previously authorized charge">
<cfargument name="transactionId" type="string" required="true" />
<cfargument name="options" type="struct" required="false" default="#structNew()#" />
<cfreturn process(gatewayUrl = getGatewayUrl("/charges/#arguments.transactionId#/capture"), payload = post, options = options) />
</cffunction>
<cffunction name="purchase" output="false" access="public" returntype="any" hint="Authorize + Capture in one step">
<cfargument name="money" type="any" required="true" />
<cfargument name="account" type="any" required="false" hint="source to be charged - a credit card, bank account or a tokenized source" />
<cfargument name="options" type="struct" required="false" default="#structNew()#" hint="Key options are customer, ConnectedAccount, destination and application_fee" />
<cfset var post = {} />
<cfset var response = "" />
<cfset post["amount"] = arguments.money.getCents() />
<cfset post["currency"] = lCase(arguments.money.getCurrency()) /><!--- iso currency code must be lower case? --->
<cfif structKeyExists(arguments, "account")>
<cfswitch expression="#getService().getAccountType(arguments.account)#">
<cfcase value="creditcard">
<cfset post = addCreditCard(post = post, account = arguments.account) />
</cfcase>
<cfcase value="token">
<cfset post = addToken(post = post, account = arguments.account) />
</cfcase>
<cfdefaultcase>
<cfthrow type="cfpayment.InvalidAccount" message="The account type #getService().getAccountType(arguments.account)# is not supported by this gateway." />
</cfdefaultcase>
</cfswitch>
</cfif>
<cfreturn process(gatewayUrl = getGatewayUrl("/charges"), payload = post, options = options) />
</cffunction>
<cffunction name="refund" access="public" output="false" returntype="any" hint="Returns an amount back to the previously charged account. Default is to refund the full amount.">
<cfargument name="money" type="any" required="false" hint="The amount to refund, if omitted, defaults to the full amount" />
<cfargument name="transactionId" type="any" required="true" />
<cfargument name="refund_application_fee" type="boolean" required="false" hint="For a destination or Connect charge, whether to refund the application_fee; defaults to false" />
<cfargument name="reverse_transfer" type="boolean" required="false" hint="For destination charges, whether to pull back funds from the connected account; defaults to false" />
<cfargument name="options" type="struct" required="false" default="#structNew()#" />
<cfset local.post = structNew() />
<!--- default is to refund full amount --->
<cfif structKeyExists(arguments, "money")>
<cfset post["amount"] = abs(arguments.money.getCents()) />
</cfif>
<!--- self-documenting --->
<cfif structKeyExists(arguments, "refund_application_fee")>
<cfset post["refund_application_fee"] = arguments.refund_application_fee />
</cfif>
<cfif structKeyExists(arguments, "reverse_transfer")>
<cfset post["reverse_transfer"] = arguments.reverse_transfer />
</cfif>
<cfreturn process(gatewayUrl = getGatewayURL("/charges/#arguments.transactionId#/refunds"), payload = post, options = options) />
</cffunction>
<cffunction name="search" access="public" output="false" returntype="any" hint="Find transactions using gateway-supported criteria">
<cfargument name="options" type="struct" required="true" />
<cfthrow message="Method not implemented." type="cfpayment.MethodNotImplemented" />
</cffunction>
<cffunction name="status" access="public" output="false" returntype="any" hint="Reconstruct a response object for a previously executed transaction">
<cfargument name="transactionId" type="any" required="true" />
<cfargument name="options" type="struct" required="false" default="#structNew()#" />
<cfreturn process(gatewayUrl = getGatewayURL("/charges/#arguments.transactionId#"), payload = structNew(), options = arguments.options, method = "GET") />
</cffunction>
<cffunction name="validate" output="false" access="public" returntype="any" hint="Convert payment details to a one-time token for charging once. To store payment details for repeat use, convert to a customer object with store().">
<cfargument name="account" type="any" required="true" />
<cfargument name="money" type="any" required="false" />
<cfset var post = "" />
<cfif getService().getAccountType(account) EQ "creditcard">
<cfset post = addCreditCard(post = structNew(), account = arguments.account) />
<cfelseif getService().getAccountType(account) EQ "eft">
<cfset post = addBankAccount(post = structNew(), account = arguments.account) />
</cfif>
<cfreturn process(gatewayUrl = getGatewayURL("/tokens"), payload = post) />
</cffunction>
<cffunction name="store" output="false" access="public" returntype="any" hint="Convert a one-time token (from validate() or Stripe.js) into a Customer object for charging one or more times in the future">
<cfargument name="account" type="any" required="true" />
<cfargument name="options" type="struct" required="false" default="#structNew()#" />
<cfset var post = {} />
<cfif getService().getAccountType(account) EQ "creditcard">
<cfset post = addCreditCard(post = post, account = account) />
<cfelseif getService().getAccountType(account) EQ "eft">
<cfset post = addBankAccount(post = post, account = account) />
<cfelse>
<cfset post["source"] = arguments.account.getID() />
</cfif>
<!--- optional things to add --->
<cfif structKeyExists(arguments.options, "coupon")>
<cfset post["coupon"] = arguments.options.coupon />
</cfif>
<cfif structKeyExists(arguments.options, "account_balance")>
<cfset post["account_balance"] = arguments.options.account_balance />
</cfif>
<cfif structKeyExists(arguments.options, "plan")>
<cfset post["plan"] = arguments.options.plan />
</cfif>
<cfif structKeyExists(arguments.options, "trial_end")>
<cfset post["trial_end"] = dateToUTC(arguments.options.trial_end) />
</cfif>
<cfif structKeyExists(arguments.options, "quantity")>
<cfset post["quantity"] = arguments.options.quantity />
</cfif>
<cfreturn process(gatewayUrl = getGatewayURL("/customers"), payload = post, options = options) />
</cffunction>
<cffunction name="unstore" output="false" access="public" returntype="any">
<cfargument name="tokenId" type="string" required="true" />
<cfreturn process(gatewayUrl = getGatewayURL("/customers/#arguments.tokenId#"), method = "DELETE") />
</cffunction>
<cffunction name="getCustomer" output="false" access="public" returntype="any">
<cfargument name="tokenId" type="string" required="true" />
<cfreturn process(gatewayUrl = getGatewayURL("/customers/#arguments.tokenId#"), method = "GET") />
</cffunction>
<cffunction name="listCharges" output="false" access="public" returntype="any">
<cfargument name="count" type="numeric" required="false" />
<cfargument name="offset" type="numeric" required="false" />
<cfargument name="tokenId" type="string" required="false" />
<cfset var payload = {} />
<cfloop collection="#arguments#" item="key">
<cfif structKeyExists(arguments, key)>
<cfset payload[lcase(key)] = arguments[key] />
</cfif>
</cfloop>
<cfreturn process(gatewayUrl = getGatewayUrl("/charges"), method = "GET", payload = payload) />
</cffunction>
<cffunction name="listApplicationFees" output="false" access="public" returntype="any" hint="Retrieve a list of application fees">
<cfargument name="options" type="struct" required="false" default="#structNew()#" hint="options include: charge, created, ending_before, limit, starting_after" />
<cfreturn process(gatewayUrl = getGatewayURL("/application_fees"), payload = {}, options = arguments.options, method = "GET") />
</cffunction>
<cffunction name="getApplicationFee" output="false" access="public" returntype="any" hint="Retrieve details about an application fee">
<cfargument name="id" type="any" required="true" />
<cfargument name="options" type="struct" required="false" default="#structNew()#" />
<cfreturn process(gatewayUrl = getGatewayURL("/application_fees/#arguments.id#"), payload = {}, options = arguments.options, method = "GET") />
</cffunction>
<cffunction name="refundApplicationFee" output="false" access="public" returntype="any" hint="Refund all or part of an application fee">
<cfargument name="id" type="any" required="true" />
<cfargument name="money" type="any" required="false" hint="The amount to refund, if omitted, defaults to the full amount" />
<cfargument name="options" type="struct" required="false" default="#structNew()#" />
<cfset local.post = {} />
<!--- default is to refund full amount --->
<cfif structKeyExists(arguments, "money")>
<cfset post["amount"] = abs(arguments.money.getCents()) />
</cfif>
<cfreturn process(gatewayUrl = getGatewayURL("/application_fees/#arguments.id#/refunds"), payload = post, options = arguments.options, method = "POST") />
</cffunction>
<cffunction name="getBalance" output="false" access="public" returntype="any" hint="Retrieve current Stripe account balance when automatic transfers are disabled">
<cfargument name="options" type="struct" required="false" default="#structNew()#" />
<cfreturn process(gatewayUrl = getGatewayURL("/balance"), payload = {}, options = arguments.options, method = "GET") />
</cffunction>
<cffunction name="getBalanceTransaction" output="false" access="public" returntype="any" hint="Retrieve details about a single balance transfer">
<cfargument name="id" type="any" required="true" />
<cfargument name="options" type="struct" required="false" default="#structNew()#" />
<cfreturn process(gatewayUrl = getGatewayURL("/balance/history/#arguments.id#"), payload = {}, options = arguments.options, method = "GET") />
</cffunction>
<cffunction name="listBalanceHistory" output="false" access="public" returntype="any" hint="Retrieve a list of balance transfers">
<cfargument name="options" type="struct" required="false" default="#structNew()#" />
<cfreturn process(gatewayUrl = getGatewayURL("/balance/history"), payload = {}, options = arguments.options, method = "GET") />
</cffunction>
<cffunction name="createToken" output="false" access="public" returntype="any" hint="Convert a credit card or bank account into a one-time Stripe token for charging/attaching to a customer, or disbursing/attaching to a recipient (note, using this rather than Stripe.js means you are responsible for ALL PCI DSS compliance)">
<cfargument name="account" type="any" required="true" />
<cfargument name="options" type="struct" required="false" default="#structNew()#" />
<cfset var post = {} />
<cfswitch expression="#getService().getAccountType(arguments.account)#">
<cfcase value="creditcard">
<cfset post = addCreditCard(post = post, account = arguments.account) />
</cfcase>
<cfcase value="eft">
<cfset post = addBankAccount(post = post, account = arguments.account) />
</cfcase>
<cfdefaultcase>
<cfthrow type="cfpayment.InvalidAccount" message="The account type #getService().getAccountType(arguments.account)# is not supported by createToken()" />
</cfdefaultcase>
</cfswitch>
<cfreturn process(gatewayUrl = getGatewayURL("/tokens"), payload = post, options = options) />
</cffunction>
<cffunction name="createTokenInConnectedAccount" output="false" access="public" returntype="any" hint="Get a token for an existing customer)">
<cfargument name="customer" type="any" required="true" />
<cfargument name="ConnectedAccount" type="any" required="true" />
<cfreturn process(gatewayUrl = getGatewayURL("/tokens"), payload = {}, options = {"ConnectedAccount": arguments.ConnectedAccount, "customer": arguments.customer}) />
</cffunction>
<cffunction name="getAccountToken" output="false" access="public" returntype="any" hint="Retrieve details about a one-time use token">
<cfargument name="id" type="any" required="true" />
<cfreturn process(gatewayUrl = getGatewayURL("/tokens/#arguments.id#"), payload = {}, options = {}, method = "GET") />
</cffunction>
<cffunction name="listConnectedAccounts" output="false" access="public" returntype="any" hint="List Connect accounts for a platform">
<cfreturn process(gatewayUrl = getGatewayURL("/accounts"), payload = structNew(), method = "GET") />
</cffunction>
<cffunction name="createConnectedAccount" output="false" access="public" returntype="any" hint="Provisions a marketplace account">
<cfargument name="country" type="string" required="true" />
<cfargument name="managed" type="boolean" required="true" />
<cfargument name="email" type="string" required="false" />
<cfargument name="options" type="struct" required="false" default="#structNew()#" />
<!--- two set-only and important fields: country, managed --->
<cfset local.post = {} />
<cfset post["country"] = arguments.country />
<cfset post["managed"] = arguments.managed />
<cfif NOT arguments.managed AND NOT structKeyExists(arguments, "email")>
<cfthrow type="cfpayment.InvalidArguments" message="Stripe requires an email address when creating an unmanaged account" />
<cfelseif structKeyExists(arguments, "email")>
<cfset post["email"] = arguments.email />
</cfif>
<cfreturn process(gatewayUrl = getGatewayURL("/accounts"), payload = post, options = options) />
</cffunction>
<cffunction name="getConnectedAccount" output="false" access="public" returntype="any" hint="">
<cfargument name="ConnectedAccount" type="any" required="true" />
<cfreturn process(gatewayUrl = getGatewayURL("/accounts/#arguments.ConnectedAccount.getID()#"), payload = structNew(), method = "GET") />
</cffunction>
<cffunction name="updateConnectedAccount" output="false" access="public" returntype="any" hint="">
<cfargument name="ConnectedAccount" type="any" required="true" />
<cfargument name="options" type="struct" required="false" default="#structNew()#" />
<cfreturn process(gatewayUrl = getGatewayURL("/accounts/#arguments.ConnectedAccount.getID()#"), payload = structNew(), options = options) />
</cffunction>
<cffunction name="deleteConnectedAccount" output="false" access="public" returntype="any" hint="">
<cfargument name="ConnectedAccount" type="any" required="true" />
<cfargument name="options" type="struct" required="false" default="#structNew()#" />
<cfreturn process(gatewayUrl = getGatewayURL("/accounts/#arguments.ConnectedAccount.getID()#"), payload = structNew(), options = options, method = "DELETE") />
</cffunction>
<cffunction name="listBankAccounts" output="false" access="public" returntype="any" hint="">
<cfargument name="ConnectedAccount" type="any" required="true" />
<cfreturn process(gatewayUrl = getGatewayURL("/accounts/#arguments.ConnectedAccount.getID()#/bank_accounts"), payload = structNew(), method = "GET") />
</cffunction>
<cffunction name="deleteBankAccount" output="false" access="public" returntype="any" hint="">
<cfargument name="ConnectedAccount" type="any" required="false" />
<cfargument name="bankAccountId" type="any" required="false" />
<cfreturn process(gatewayUrl = getGatewayURL("/accounts/#arguments.ConnectedAccount.getID()#/bank_accounts/#arguments.bankAccountId#"), payload = {}, method = "DELETE") />
</cffunction>
<cffunction name="createBankAccount" output="false" access="public" returntype="any" hint="">
<cfargument name="ConnectedAccount" type="any" required="false" />
<cfargument name="account" type="any" required="false" hint="Either a token or EFT" />
<cfargument name="currency" type="string" required="true" hint="A 3-letter ISO currency code" />
<cfset local.post = structNew() />
<cfif getService().getAccountType(account) EQ "token">
<cfset post["bank_account"] = arguments.account.getID() />
<cfelseif getService().getAccountType(account) EQ "eft">
<cfset post = addBankAccount(post = post, account = account) />
<cfset post["bank_account[currency]"] = lcase(arguments.currency) />
<cfelse>
<cfthrow type="cfpayment.InvalidAccount" message="The account type #getService().getAccountType(arguments.account)# is not supported by this gateway." />
</cfif>
<cfreturn process(gatewayUrl = getGatewayURL("/accounts/#arguments.ConnectedAccount.getID()#/bank_accounts"), payload = local.post) />
</cffunction>
<cffunction name="setDefaultBankAccountForCurrency" output="false" access="public" returntype="any" hint="">
<cfargument name="ConnectedAccount" type="any" required="false" />
<cfargument name="bankAccountId" type="any" required="false" />
<cfset local.post = {"default_for_currency": true} />
<cfreturn process(gatewayUrl = getGatewayURL("/accounts/#arguments.ConnectedAccount.getID()#/bank_accounts/#arguments.bankAccountId#"), payload = local.post) />
</cffunction>
<cffunction name="uploadFile" output="false" access="public" returntype="any" hint="Stripe allows file uploads for identity verification and chargeback dispute evidence - first upload and then assign the file id to its intended object">
<cfargument name="file" type="any" required="false" hint="An absolute path to a file" />
<cfargument name="purpose" type="string" required="true" hint="Allowed values - from https://stripe.com/docs/api##create_file_upload" />
<cfargument name="options" type="struct" required="false" default="#structNew()#" hint="Pass a ConnectedAccount if attaching to a Connect account" />
<cfif NOT listFind("identity_document,dispute_evidence", arguments.purpose)>
<cfthrow type="cfpayment.InvalidArguments" message="Purpose must be one of: identity_document, dispute_evidence" />
</cfif>
<cfset local.files = {"file": arguments.file} />
<cfset local.post = {"purpose": arguments.purpose} />
<cfreturn process(gatewayUrl = "https://uploads.stripe.com/v1/files", payload = local.post, files = local.files, options = arguments.options) />
</cffunction>
<cffunction name="attachIdentityFile" output="false" access="public" returntype="any" hint="For attaching Connect account identity documents">
<cfargument name="ConnectedAccount" type="any" required="true" />
<cfargument name="fileId" type="any" required="true" />
<cfargument name="options" type="struct" required="false" default="#structNew()#" hint="Pass a ConnectedAccount if attaching to a Connect account" />
<cfset local.post = {"legal_entity[verification][document]": arguments.fileId} />
<cfreturn process(gatewayUrl = getGatewayURL("/accounts/#arguments.ConnectedAccount.getID()#"), payload = local.post, options = arguments.options) />
</cffunction>
<cffunction name="updateDispute" output="false" access="public" returntype="any" hint="">
<cfargument name="transactionId" type="any" required="false" />
<cfargument name="options" type="struct" required="false" default="#structNew()#" hint="Disputes can include file references generated from uploadFile() for evidence" />
<cfreturn process(gatewayUrl = getGatewayURL("/charges/#arguments.transactionId#/disputes"), payload = structNew(), options = arguments.options) />
</cffunction>
<cffunction name="getTransfer" output="false" access="public" returntype="any" hint="Retrieve details about a single transfer">
<cfargument name="id" type="any" required="true" />
<cfreturn process(gatewayUrl = getGatewayURL("/transfers/#arguments.id#"), payload = {}, method = "GET") />
</cffunction>
<cffunction name="listTransfers" output="false" access="public" returntype="any" hint="">
<cfargument name="options" type="struct" required="false" default="#structNew()#" />
<cfreturn process(gatewayUrl = getGatewayURL("/transfers"), payload = {}, options = arguments.options, method = "GET") />
</cffunction>
<cffunction name="transfer" output="false" access="public" returntype="any" hint="">
<cfargument name="money" type="any" required="true" />
<cfargument name="destination" type="any" required="true" />
<cfargument name="options" type="struct" required="false" default="#structNew()#" />
<cfset local.post = structNew() />
<cfset local.post["amount"] = arguments.money.getCents() />
<cfset local.post["currency"] = lCase(arguments.money.getCurrency()) />
<cfset local.post["destination"] = arguments.destination.getID() />
<cfreturn process(gatewayUrl = getGatewayURL("/transfers"), payload = local.post, options = arguments.options) />
</cffunction>
<cffunction name="transferReverse" output="false" access="public" returntype="any">
<cfargument name="transferId" type="string" required="true" hint="The transfer to reverse" />
<cfargument name="money" type="any" required="false" hint="Amount to refund, if omitted, the default is the entire amount" />
<cfargument name="refund_application_fee" type="boolean" required="false" hint="For a destination or Connect charge, whether to refund the application_fee; defaults to false" />
<cfargument name="options" type="struct" required="false" default="#structNew()#" />
<cfset local.post = structNew() />
<cfif structKeyExists(arguments, "money")>
<cfset post["amount"] = abs(arguments.money.getCents()) />
</cfif>
<!--- self-documenting --->
<cfif structKeyExists(arguments, "refund_application_fee")>
<cfset post["refund_application_fee"] = arguments.refund_application_fee />
</cfif>
<cfreturn process(gatewayUrl = getGatewayURL("/transfers/#arguments.transferId#/reversals"), payload = post, options = arguments.options) />
</cffunction>
<cffunction name="testTLS12" output="false" access="public" returntype="any" hint="Test for TLS 1.2 support - a 200 OK response means your server is OK. See https://stripe.com/blog/upgrading-tls">
<cfreturn process(gatewayUrl = "https://api-tls12.stripe.com/v1/charges", payload = {}) />
</cffunction>
<!--- determine capability of this gateway --->
<cffunction name="getIsCCEnabled" access="public" output="false" returntype="boolean" hint="determine whether or not this gateway can accept credit card transactions">
<cfreturn true />
</cffunction>
<!--- process wrapper with gateway/transaction error handling --->
<cffunction name="process" output="false" access="private" returntype="any">
<cfargument name="gatewayUrl" type="string" required="true" />
<cfargument name="payload" type="struct" required="true" />
<cfargument name="options" type="struct" required="false" default="#structNew()#" />
<cfargument name="headers" type="struct" required="false" default="#structNew()#" />
<cfargument name="method" type="string" required="false" default="post" />
<cfargument name="files" type="struct" required="false" default="#structNew()#" />
<cfset var results = "" />
<cfset var response = "" />
<cfset var p = arguments.payload /><!--- shortcut (by reference) --->
<!--- process standard and common CFPAYMENT mappings into gateway-specific values --->
<cfif structKeyExists(arguments.options, "description")>
<cfset p["description"] = arguments.options.description />
</cfif>
<cfif structKeyExists(arguments.options, "tokenId")>
<cfset p["customer"] = arguments.options.tokenId />
</cfif>
<!--- add baseline authentication --->
<cfset headers["authorization"] = "Bearer #getSecretKey()#" />
<!--- add connect authentication on behalf of a Connect/Marketplace customer --->
<cfif structKeyExists(arguments.options, "ConnectedAccount")>
<cfif NOT isObject(arguments.options.ConnectedAccount)>
<cfthrow type="cfpayment.InvalidArguments" message="ConnectedAccount must be a cfpayment token object" />
</cfif>
<cfset headers["Stripe-Account"] = arguments.options.ConnectedAccount.getID() />
<cfset structDelete(arguments.options, "ConnectedAccount") />
</cfif>
<!--- if we want to override the stripe API version, we can set it in the config with "ApiVersion". Using 'latest' overrides to current version --->
<cfif len(getApiVersion())>
<!--- https://groups.google.com/a/lists.stripe.com/forum/#!topic/api-discuss/V4sYRlHwalc --->
<cfset headers["Stripe-Version"] = getApiVersion() />
</cfif>
<!--- help track where this request was made from --->
<cfset headers["User-Agent"] = "Stripe/v1 cfpayment/#variables.cfpayment.GATEWAY_VERSION#" />
<!--- add dynamic statement descriptors of up to 22 chars which show up on CC statement alongside merchant name: https://stripe.com/docs/api#create_charge --->
<cfif structKeyExists(arguments.options, "statement_descriptor")>
<cfset p["statement_descriptor"] = left(reReplace(arguments.options.statement_descriptor, "[<>""']", "", "ALL"), 22) />
<cfset structDelete(arguments.options, "statement_descriptor") />
</cfif>
<!--- application_fee is a money object, just like the amount to be charged --->
<cfif structKeyExists(arguments.options, "application_fee")>
<cfif NOT isObject(arguments.options.application_fee)>
<cfthrow type="cfpayment.InvalidArguments" message="application_fee must be a cfpayment money object" />
</cfif>
<cfset p["application_fee"] = arguments.options.application_fee.getCents() />
<cfset structDelete(arguments.options, "application_fee") />
</cfif>
<!--- if a card is converted to a customer, you can optionally pass a customer to many requests to charge their default account instead --->
<cfif structKeyExists(arguments.options, "customer")>
<cfif NOT isObject(arguments.options.customer)>
<cfthrow type="cfpayment.InvalidArguments" message="Customer must be a cfpayment token object" />
</cfif>
<cfset p = addCustomer(post = p, customer = arguments.options.customer) />
<cfset structDelete(arguments.options, "customer") />
</cfif>
<cfif structKeyExists(arguments.options, "destination")>
<cfif NOT isObject(arguments.options.destination)>
<cfthrow type="cfpayment.InvalidArguments" message="Destination must be a cfpayment token object" />
</cfif>
<cfset p["destination"] = arguments.options.destination.getID() />
<cfset structDelete(arguments.options, "destination") />
</cfif>
<!--- finally, copy in any additional keys like customer, destination, etc, stripe always wants lower-case --->
<cfloop collection="#arguments.options#" item="local.key">
<cfset p[lcase(key)] = arguments.options[key] />
</cfloop>
<!--- Stripe returns errors with http status like 400,402 or 404 (https://stripe.com/docs/api#errors) --->
<cfset response = createResponse(argumentCollection = super.process(url = arguments.gatewayUrl, payload = payload, headers = headers, method = arguments.method, files = files)) />
<cfif isJSON(response.getResult())>
<cfset results = deserializeJSON(response.getResult()) />
<cfset response.setParsedResult(results) />
<!--- take object-specific IDs like tok_*, ch_*, re_*, etc and always put it as the transaction id --->
<cfif structKeyExists(results, "id")>
<cfset response.setTransactionID(results.id) />
</cfif>
<!--- the available 'types': list, customer, charge, token, card, bank_account, refund, application_fee, transfer, transfer_reversal, account, file_upload --->
<cfif structKeyExists(results, "object")>
<cfswitch expression="#results.object#">
<cfcase value="account">
<cfset response.setTokenID(results.id) />
</cfcase>
<cfcase value="bank_account">
<cfset response.setTokenID(results.id) />
</cfcase>
<cfcase value="charge">
<cfset response.setCVVCode(normalizeCVV(results.source)) />
<cfset response.setAVSCode(normalizeAVS(results.source)) />
<!--- if you authorize without capture, you use the charge id to capture it later, which is the same as the transaction id, but for normality, put it here --->
<cfif structKeyExists(results, "captured") AND NOT results.captured AND structKeyExists(results, "id")>
<cfset response.setAuthorization(results.id) />
</cfif>
</cfcase>
<cfcase value="customer">
<!--- customers have a "sources" key with, by default, one card on file
you can add more cards to a customer using the card api, but otherwise
adding a new one actually replaces the previous one on file.
we make the assumption today that we only have one until someone needs more
--->
<cfset response.setCVVCode(normalizeCVV(results.sources.data[1])) />
<cfset response.setAVSCode(normalizeAVS(results.sources.data[1])) />
<cfset response.setTokenID(results.id) />
</cfcase>
<cfcase value="token">
<!--- stripe does not check AVS/CVV at the token stage - only once converted to a customer or in a charge --->
<!--- could be results.source.object EQ card or bank_account --->
<cfset response.setTokenID(results.id) />
</cfcase>
</cfswitch>
</cfif>
</cfif>
<!--- now add custom handling of status codes for Stripe which overrides base.cfc --->
<cfset handleHttpStatus(response = response) />
<cfreturn response />
</cffunction>
<cffunction name="normalizeCVV" output="false" access="private" returntype="string">
<cfargument name="source" type="any" required="true" hint="A structure that contains a cvc_check key" />
<!--- translate to normalized cfpayment CVV codes --->
<cfif structKeyExists(arguments.source, "cvc_check") AND arguments.source.cvc_check EQ "pass">
<cfreturn "M" />
<cfelseif structKeyExists(arguments.source, "cvc_check") AND arguments.source.cvc_check EQ "fail">
<cfreturn "N" />
<cfelseif structKeyExists(arguments.source, "cvc_check") AND arguments.source.cvc_check EQ "unchecked">
<cfreturn "U" />
<cfelseif NOT structKeyExists(arguments.source, "cvc_check")>
<!--- indicates it wasn't checked --->
<cfreturn "" />
<cfelse>
<cfreturn "P" />
</cfif>
</cffunction>
<cffunction name="normalizeAVS" output="false" access="private" returntype="string">
<cfargument name="source" type="any" required="true" hint="A structure that contains address_line1_check and address_zip_check keys" />
<!--- translate to normalized cfpayment AVS codes. Options are pass, fail, unavailable and unchecked. Watch out that either address_line1_check or address_zip_check can be null OR "unchecked"; null throws error trying to access --->
<cfif structKeyExists(arguments.source, "address_zip_check") AND arguments.source.address_zip_check EQ "pass"
AND structKeyExists(arguments.source, "address_line1_check") AND arguments.source.address_line1_check EQ "pass">
<cfreturn "M" />
<cfelseif structKeyExists(arguments.source, "address_zip_check") AND arguments.source.address_zip_check EQ "pass">
<cfreturn "P" />
<cfelseif structKeyExists(arguments.source, "address_line1_check") AND arguments.source.address_line1_check EQ "pass">
<cfreturn "B" />
<cfelseif (structKeyExists(arguments.source, "address_zip_check") AND arguments.source.address_zip_check EQ "unchecked")
OR (structKeyExists(arguments.source, "address_line1_check") AND arguments.source.address_line1_check EQ "unchecked")>
<cfif arguments.source.country EQ "US">
<cfreturn "S" />
<cfelse>
<cfreturn "G" />
</cfif>
<cfelseif NOT structKeyExists(arguments.source, "address_zip_check") AND NOT structKeyExists(arguments.source, "address_line1_check")>
<!--- indicates it wasn't checked --->
<cfreturn "" />
<cfelse>
<cfreturn "N" />
</cfif>
</cffunction>
<!---
//Stripe returns errors with http status like 400, 402 or 404 (https://stripe.com/docs/api#errors)
//so we need to override http status handling in base.cfc process()
--->
<cffunction name="handleHttpStatus" access="private" returntype="any" output="false" hint="Override base HTTP status code handling with Stripe-specific results">
<cfargument name="response" type="any" required="true" />
<!---
HTTP Status Code Summary
200 OK - Everything worked as expected.
400 Bad Request - Often missing a required parameter.
401 Unauthorized - No valid API key provided.
402 Request Failed - Parameters were valid but request failed.
404 Not Found - The requested item doesn't exist.
500, 502, 503, 504 Server errors - something went wrong on Stripe's end.
Errors
Invalid Request Errors
Type: invalid_request_error
API Errors
Type: api_error
Card Errors
Type: card_error
Code Details
incorrect_number The card number is incorrect
invalid_number The card number is not a valid credit card number
invalid_expiry_month The card's expiration month is invalid
invalid_expiry_year The card's expiration year is invalid
invalid_cvc The card's security code is invalid
expired_card The card has expired
incorrect_cvc The card's security code is incorrect
card_declined The card was declined.
missing There is no card on a customer that is being charged.
processing_error An error occurred while processing the card.
--->
<cfscript>
var status = response.getStatusCode();
var res = response.getParsedResult();
switch(status)
{
case "200": // OK - Everything worked as expected.
response.setStatus(getService().getStatusSuccessful());
break;
case "401": // Unauthorized - No valid API key provided.
response.setMessage("There is a configuration error preventing the transaction from completing successfully. (Original issue: Invalid API key)");
response.setStatus(getService().getStatusFailure());
break;
case "402": // Request Failed - Parameters were valid but request failed. e.g. invalid card, cvc failed, etc.
response.setStatus(getService().getStatusDeclined());
break;
case "400": // Bad Request - Often missing a required parameter, includes parameter not allowed or params not lowercase
case "404": // Not Found - The requested item doesn't exist. i.e. no charge for that id
response.setStatus(getService().getStatusFailure());
break;
case "500": // Server errors - something went wrong on Stripe's end.
case "502":
case "503":
case "504":
response.setStatus(getService().getStatusFailure());
break;
}
if (response.hasError() AND isStruct(res) AND structKeyExists(res, "error"))
{
if (structKeyExists(res.error, "message"))
response.setMessage(res.error.message);
if (structKeyExists(res.error, "param"))
response.setMessage(response.getMessage() & " (#res.error.param#)");
if (structKeyExists(res.error, "code"))
{
switch (res.error.code)
{
case "incorrect_number":
case "invalid_number":
case "invalid_expiry_month":
case "invalid_expiry_year":
case "invalid_cvc":
case "expired_card":
case "incorrect_cvc":
case "card_declined":
case "missing":
case "processing_error":
// can do more involved translation to human-speak here
response.setMessage(response.getMessage() & " [#res.error.code#]");
break;
default:
response.setMessage(response.getMessage() & " [#res.error.code#]");
}
}
else if (NOT structKeyExists(res.error, "message"))
{
response.setMessage("Gateway returned unknown response: #status#");
}
}
</cfscript>
<cfreturn response />
</cffunction>
<cffunction name="getGatewayURL" access="public" output="false" returntype="any" hint="Append to Gateway URL to return the appropriate url for the API endpoint">
<cfargument name="endpoint" type="string" required="false" default="" />
<cfreturn variables.cfpayment.GATEWAY_URL & arguments.endpoint />
</cffunction>
<!--- HELPER FUNCTIONS --->
<cffunction name="addCreditCard" output="false" access="private" returntype="any" hint="Add payment source fields to the request object">
<cfargument name="post" type="struct" required="true" />
<cfargument name="account" type="any" required="true" />
<cfscript>
post["card[number]"] = arguments.account.getAccount();
post["card[exp_month]"] = arguments.account.getMonth();
post["card[exp_year]"] = arguments.account.getYear();
post["card[cvc]"] = arguments.account.getVerificationValue();
post["card[name]"] = arguments.account.getName();
post["card[address_line1]"] = arguments.account.getAddress();
post["card[address_line2]"] = arguments.account.getAddress2();
post["card[address_zip]"] = arguments.account.getPostalCode();
post["card[address_state]"] = arguments.account.getRegion();
post["card[address_country]"] = arguments.account.getCountry();
</cfscript>
<cfreturn post />
</cffunction>
<cffunction name="addBankAccount" output="false" access="private" returntype="any" hint="Add payment source fields to the request object">
<cfargument name="post" type="struct" required="true" />
<cfargument name="account" type="any" required="true" />
<cfscript>
post["bank_account[country]"] = arguments.account.getCountry();
post["bank_account[routing_number]"] = arguments.account.getRoutingNumber();
post["bank_account[account_number]"] = arguments.account.getAccount();
</cfscript>
<cfreturn post />
</cffunction>
<cffunction name="addToken" output="false" access="private" returntype="any" hint="Add payment source fields to the request object">
<cfargument name="post" type="struct" required="true" />
<cfargument name="account" type="any" required="true" />
<cfset arguments.post["source"] = arguments.account.getID() />
<cfreturn arguments.post />
</cffunction>
<cffunction name="addCustomer" output="false" access="private" returntype="any" hint="Add payment source fields to the request object">
<cfargument name="post" type="struct" required="true" />
<cfargument name="customer" type="any" required="true" />
<cfset arguments.post["customer"] = arguments.customer.getID() />
<cfreturn arguments.post />
</cffunction>
<cffunction name="dateToUTC" output="false" access="public" returntype="any" hint="Take a date and return the number of seconds since the Unix Epoch">
<cfargument name="date" type="any" required="true" />
<cfreturn dateDiff("s", dateConvert("utc2Local", "January 1 1970 00:00"), arguments.date) />
</cffunction>
<cffunction name="UTCToDate" output="false" access="public" returntype="date" hint="Take a UTC timestamp and convert it to a ColdFusion date object">
<cfargument name="utcdate" required="true" />
<cfreturn dateAdd("s", arguments.utcDate, dateConvert("utc2Local", "January 1 1970 00:00")) />
</cffunction>
<!--- stripe createResponse() overrides the getSuccess/hasError() responses --->
<cffunction name="createResponse" access="public" output="false" returntype="any" hint="Create a Stripe response object with status set to unprocessed">
<cfreturn createObject("component", "response").init(argumentCollection = arguments, service = getService()) />
</cffunction>
</cfcomponent>