diff --git a/.cfconfig.json b/.cfconfig.json index f9b3828..f404985 100644 --- a/.cfconfig.json +++ b/.cfconfig.json @@ -6,21 +6,5 @@ "inspectTemplate":"always", "requestTimeout":"0,0,0,90", "robustExceptionEnabled":true, - "datasources": { - "coolblog": { - "class":"${DB_CLASS}", - "dbdriver": "MySQL", - "dsn":"jdbc:mysql://{host}:{port}/{database}", - "custom":"useUnicode=true&characterEncoding=UTF8&serverTimezone=UTC&useLegacyDatetimeCode=true&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true", - "host":"${DB_HOST:127.0.0.1}", - "username": "${DB_USER:root}", - "password": "${DB_PASSWORD}", - "database": "coolblog", - "port": "${DB_PORT:3306}", - "storage":"false", - "bundleName": "${DB_BUNDLENAME}", - "bundleVersion": "${DB_BUNDLEVERSION}", - "validate":"false" - } - } + "datasources": {} } diff --git a/.env.template b/.env.template index c50097f..3a30764 100644 --- a/.env.template +++ b/.env.template @@ -1,7 +1,2 @@ -DB_HOST=127.0.0.1 -DB_PORT=3306 -DB_USER=root -DB_PASSWORD=mysql -DB_CLASS=com.mysql.cj.jdbc.Driver -DB_BUNDLEVERSION=8.0.19 -DB_BUNDLENAME=com.mysql.cj \ No newline at end of file +STRIPE_API_KEY= +STRIPE_PUBLISHABLE_KEY= \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 35214ae..338c4e8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ on: type: boolean env: - MODULE_ID: @MODULE_SLUG@ + MODULE_ID: cbpayments SNAPSHOT: ${{ inputs.snapshot || false }} jobs: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 46c40a6..7c695d6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing Guide -Hola amigo! I'm really excited that you are interested in contributing to @MODULE_NAME@. Before submitting your contribution, please make sure to take a moment and read through the following guidelines: +Hola amigo! I'm really excited that you are interested in contributing to cbPayments. Before submitting your contribution, please make sure to take a moment and read through the following guidelines: - [Code Of Conduct](#code-of-conduct) - [Bug Reporting](#bug-reporting) @@ -98,11 +98,11 @@ You can support ColdBox and all of our Open Source initiatives at Ortus Solution ## Contributors -Thank you to all the people who have already contributed to @MODULE_NAME@! We :heart: :heart: :heart: love you! +Thank you to all the people who have already contributed to cbPayments! We :heart: :heart: :heart: love you! - - + + Made with [contributors-img](https://contrib.rocks) diff --git a/ModuleConfig.cfc b/ModuleConfig.cfc index 92257e3..961227e 100644 --- a/ModuleConfig.cfc +++ b/ModuleConfig.cfc @@ -6,42 +6,38 @@ component { // Module Properties - this.title = "@MODULE_NAME@"; - this.author = "Ortus Solutions"; - this.webURL = "https://www.ortussolutions.com"; - this.description = "@MODULE_DESCRIPTION@"; - this.version = "@build.version@+@build.number@"; + this.title = "cbPayments"; + this.author = "Ortus Solutions"; + this.webURL = "https://www.ortussolutions.com"; + this.description = "A module providing a common interface and API for processing payments and subscriptions'"; + this.version = "@build.version@+@build.number@"; // Model Namespace - this.modelNamespace = "@MODULE_SLUG@"; + this.modelNamespace = "cbpayments"; // CF Mapping - this.cfmapping = "@MODULE_SLUG@"; + this.cfmapping = "cbpayments"; // Dependencies - this.dependencies = []; + this.dependencies = []; /** * Configure Module */ function configure(){ - settings = { - - }; + settings = {}; } /** * Fired when the module is registered and activated. */ function onLoad(){ - } /** * Fired when the module is unregistered and unloaded */ function onUnload(){ - } } diff --git a/box.json b/box.json index 47146f3..e09aa89 100644 --- a/box.json +++ b/box.json @@ -1,54 +1,64 @@ { - "name" : "@MODULE_NAME@", - "version" : "1.0.0", - "location" : "https://downloads.ortussolutions.com/ortussolutions/coldbox-modules/@MODULE_SLUG@/@build.version@/@MODULE_SLUG@-@build.version@.zip", - "author" : "Ortus Solutions ", - "homepage" : "https://github.com/coldbox-modules/@MODULE_SLUG@", - "documentation" : "https://github.com/coldbox-modules/@MODULE_SLUG@", - "repository" : { "type" : "git", "url" : "https://github.com/coldbox-modules/@MODULE_SLUG@" }, - "bugs" : "https://github.com/coldbox-modules/@MODULE_SLUG@", - "shortDescription" : "Description goes here", - "slug" : "@MODULE_SLUG@", - "type" : "modules", - "keywords":"", - "license" : [ - { "type" : "Apache2", "url" : "http://www.apache.org/licenses/LICENSE-2.0.html" } - ], - "contributors" : [ - ], - "dependencies" :{ - }, - "devDependencies" :{ - "commandbox-cfformat":"*", + "name":"cbPayments", + "version":"1.0.0", + "location":"https://downloads.ortussolutions.com/ortussolutions/coldbox-modules/cbpayments/@build.version@/cbpayments-@build.version@.zip", + "author":"Ortus Solutions ", + "homepage":"https://github.com/coldbox-modules/cbpayments", + "documentation":"https://github.com/coldbox-modules/cbpayments", + "repository":{ + "type":"git", + "url":"https://github.com/coldbox-modules/cbpayments" + }, + "bugs":"https://github.com/coldbox-modules/cbpayments", + "shortDescription":"Description goes here", + "slug":"cbpayments", + "type":"modules", + "keywords":"", + "license":[ + { + "type":"Apache2", + "url":"http://www.apache.org/licenses/LICENSE-2.0.html" + } + ], + "contributors":[], + "dependencies":{ + "stripecfml":"^3.6.0", + "mementifier":"^3.4.0+2" + }, + "devDependencies":{ + "commandbox-cfformat":"*", "commandbox-docbox":"*", - "commandbox-dotenv":"*", + "commandbox-dotenv":"*", "commandbox-cfconfig":"*" - }, - "ignore":[ + }, + "ignore":[ "**/.*", "test-harness", - "/server*.json" + "/server*.json" ], - "scripts":{ - "setupTemplate": "task run taskFile=build/SetupTemplate.cfc", - "build:module":"task run taskFile=build/Build.cfc :projectName=`package show slug` :version=`package show version`", - "build:docs":"task run taskFile=build/Build.cfc target=docs :projectName=`package show slug` :version=`package show version`", - "install:dependencies":"install && cd test-harness && install", - "release":"recipe build/release.boxr", + "scripts":{ + "build:module":"task run taskFile=build/Build.cfc :projectName=`package show slug` :version=`package show version`", + "build:docs":"task run taskFile=build/Build.cfc target=docs :projectName=`package show slug` :version=`package show version`", + "install:dependencies":"install --force && cd test-harness && install --force", + "release":"recipe build/release.boxr", "format":"cfformat run helpers,models,test-harness/tests/,ModuleConfig.cfc --overwrite", "format:watch":"cfformat watch helpers,models,test-harness/tests/,ModuleConfig.cfc ./.cfformat.json", "format:check":"cfformat check helpers,models,test-harness/tests/,ModuleConfig.cfc ./.cfformat.json", - "start:lucee" : "server start serverConfigFile=server-lucee@5.json", - "start:2018" : "server start serverConfigFile=server-adobe@2018.json", - "start:2021" : "server start serverConfigFile=server-adobe@2021.json", - "stop:lucee" : "server stop serverConfigFile=server-lucee@5.json", - "stop:2018" : "server stop serverConfigFile=server-adobe@2018.json", - "stop:2021" : "server stop serverConfigFile=server-adobe@2021.json", - "logs:lucee" : "server log serverConfigFile=server-lucee@5.json --follow", - "logs:2018" : "server log serverConfigFile=server-adobe@2018.json --follow", - "logs:2021" : "server log serverConfigFile=server-adobe@2021.json --follow" + "start:lucee":"server start serverConfigFile=server-lucee@5.json", + "start:2018":"server start serverConfigFile=server-adobe@2018.json", + "start:2021":"server start serverConfigFile=server-adobe@2021.json", + "stop:lucee":"server stop serverConfigFile=server-lucee@5.json", + "stop:2018":"server stop serverConfigFile=server-adobe@2018.json", + "stop:2021":"server stop serverConfigFile=server-adobe@2021.json", + "logs:lucee":"server log serverConfigFile=server-lucee@5.json --follow", + "logs:2018":"server log serverConfigFile=server-adobe@2018.json --follow", + "logs:2021":"server log serverConfigFile=server-adobe@2021.json --follow" }, - "testbox":{ + "testbox":{ "runner":"http://localhost:60299/tests/runner.cfm" + }, + "installPaths":{ + "stripecfml":"modules/stripecfml/", + "mementifier":"modules/mementifier/" } } diff --git a/build/SetupTemplate.cfc b/build/SetupTemplate.cfc index ca18f24..1e01306 100644 --- a/build/SetupTemplate.cfc +++ b/build/SetupTemplate.cfc @@ -39,7 +39,7 @@ component { command( "tokenReplace" ) .params( path = "/#variables.cwd#/**", - token = "@MODULE_NAME@", + token = "cbPayments", replacement = moduleName ) .run(); @@ -47,7 +47,7 @@ component { command( "tokenReplace" ) .params( path = "/#variables.cwd#/**", - token = "@MODULE_SLUG@", + token = "cbpayments", replacement = moduleSlug ) .run(); @@ -55,7 +55,7 @@ component { command( "tokenReplace" ) .params( path = "/#variables.cwd#/**", - token = "@MODULE_DESCRIPTION@", + token = "A module providing a common interface and API for processing payments and subscriptions'", replacement = moduleDescription ) .run(); diff --git a/changelog.md b/changelog.md index 46f833a..082aa3e 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [1.0.0] => 2021-JAN-01 +## [1.0.0] => 2024-JUL-23 * First iteration of this module diff --git a/models/processor/AuthorizeNETProcessor.cfc b/models/processor/AuthorizeNETProcessor.cfc new file mode 100755 index 0000000..e779f76 --- /dev/null +++ b/models/processor/AuthorizeNETProcessor.cfc @@ -0,0 +1,8 @@ +/** + * A cool processor/AuthorizeNET entity + */ +component extends="BaseProcessor" { + + +} + diff --git a/models/processor/BaseProcessor.cfc b/models/processor/BaseProcessor.cfc new file mode 100755 index 0000000..25f6771 --- /dev/null +++ b/models/processor/BaseProcessor.cfc @@ -0,0 +1,258 @@ +/** + * A Base processor utility + */ +component accessors="true" { + + // Global DI + property name="wirebox" inject="wirebox"; + property name="log" inject="logbox:logger:{this}"; + + /** + * Get a new reponse object + */ + function newResponse() provider="ProcessorResponse@cbpayments"{} + + /** + * Retrieve a human readable name for the processor + */ + function getName(){ + throw( "This method must be implemented in the child processor" ); + } + + /** + * If there is a version attached to the processor then return it here. + */ + function getVersion(){ + throw( "This method must be implemented in the child processor" ); + } + + /** + * Get the payment processor SDK library implementation. This will be getting the raw processor. + */ + any function getProcessor(){ + throw( "This method must be implemented in the child processor" ); + } + + /** + * Pre-authorizes a transaction on the processor without capture + * + * @amount The amount in cents to charge, example: $20 = 2000, $20.5 = 2050, it is required + * @source A payment source to be charged, usually this is a card token, a customer token, etc. It is required + * @currency Usually the three-letter ISO Currency code (Optional) + * @customerId A customer identifier to attach to the charge (Optional) + * @description The description of the charge (Optional) + * @headers A struct of headers to send with the processor (Optional) + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function preAuthorize( + required numeric amount, + required source, + currency = "usd", + customerId, + description = "", + struct metadata = {} + ){ + throw( "This method must be implemented in the child processor" ); + } + + /** + * Make a charge on the processor. Please note that any EXTRA arguments added to a processor + * The processor implementation must take care of them. + * + * @amount The amount in cents to charge, example: $20 = 2000, $20.5 = 2050, it is required + * @source A payment source to be charged, usually this is a card token, a customer token, etc. It is required + * @currency Usually the three-letter ISO Currency code (Optional) + * @customerId A customer identifier to attach to the charge (Optional) + * @description The description of the charge (Optional) + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function charge( + required numeric amount, + required source, + currency, + customerId, + description, + boolean capture = true, + struct metadata = {} + ){ + throw( "This method must be implemented in the child processor" ); + } + + /** + * Make a refund on the processor. Please note that any EXTRA arguments added to a processor + * The processor implementation must take care of them. + * + * @charge The identifier of the charge to refund. + * @amount The amount in cents to refund, if not sent then the entire charge is refunded (Optional) + * @reason A reason of why the refund (Optional) + * @headers A struct of headers to send with the processor (Optional) + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function refund( + required charge, + numeric amount, + reason, + struct metadata = {} + ){ + throw( "This method must be implemented in the child processor" ); + } + + /** + * Create a subscription in the provider + * The processor implementation must take care of them. + * + * @providerCustomerId The provider customer Id + * @planId Plan Id in the provider + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function createSubscription( + required providerCustomerId, + required planId, + numeric quantity, + struct metadata = {} + ){ + throw( "This method must be implemented in the child processor" ); + } + + /** + * Get the subscription from the provider + * The processor implementation must take care of them. + * + * @subscriptionId The Subscription Id + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function getSubscription( required subscriptionId, struct metadata = {} ){ + throw( "This method must be implemented in the child processor" ); + } + + /** + * Cancel a subscription in the provider. + * Cancelling a subscription retains access through the end of the billing period. + * + * @subscriptionId Subscription Id + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function cancelSubscription( required subscriptionId, struct metadata = {} ){ + throw( "This method must be implemented in the child processor" ); + } + + /** + * Resume a subscription in the provider. + * + * @subscriptionId Subscription Id + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function resumeSubscription( required subscriptionId, struct metadata = {} ){ + throw( "This method must be implemented in the child processor" ); + } + + /** + * Update the subscription quantity in the provider. + * Cancelling a subscription retains access through the end of the billing period. + * + * @subscriptionId Subscription Id + * @quantity The new quantity, the subscription cost will be calculated based on this number + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function updateSubscriptionQuantity( + required subscriptionId, + required numeric quantity, + struct metadata = {} + ){ + throw( "This method must be implemented in the child processor" ); + } + + /** + * Delete a subscription in the provider + * The processor implementation must take care of them. + * + * @subscriptionId Subscription Id + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function deleteSubscription( required subscriptionId, struct metadata = {} ){ + throw( "This method must be implemented in the child processor" ); + } + + /** + * Convenience method to retrieve the customer associated with a subscription + * + * @subscriptionId The subscription Id + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function getSubscriptionCustomer( required subscriptionId, struct metadata ){ + throw( "This method must be implemented in the child processor" ); + } + + /** + * Create a customer in the provider + * The processor implementation must take care of them. + * + * @email Email of the new customer + * @paymentMethodId Payment Method Id of the provider + * @description Customer description, very handy info that could be found in the provider + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function createCustomer( + required email, + required paymentMethodId, + description, + struct metadata = {} + ){ + throw( "This method must be implemented in the child processor" ); + } + + /** + * Get the customer struct from the provider + * The processor implementation must take care of them. + * + * @providerCustomerId The provider customer Id + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function getCustomer( required providerCustomerId, struct metadata = {} ){ + throw( "This method must be implemented in the child processor" ); + } + + /** + * Get the customer payment method + * The processor implementation must take care of them. + * + * @providerCustomerId The provider customer Id + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function getPaymentMethod( required providerCustomerId, struct metadata = {} ){ + throw( "This method must be implemented in the child processor" ); + } + + /** + * Update the payment method associated to a customer + * The processor implementation must take care of them. + * + * @customerId The customer Id in the provider + * @paymentMethodId The Payment Method Id generated by the provider + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function updatePaymentMethod( + required providerCustomerId, + required paymentMethodId, + struct metadata = {} + ){ + throw( "This method must be implemented in the child processor" ); + } + + /** + * Change from one subscription plan to another one + * + * @planId The provider plan Id to change to + * @subscriptionId The subscription Id in the provider + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function changeSubscriptionPlan( + required planId, + required subscriptionId, + struct metadata = {} + ){ + throw( "This method must be implemented in the child processor" ); + } + +} + diff --git a/models/processor/IPaymentProcessor.cfc b/models/processor/IPaymentProcessor.cfc new file mode 100644 index 0000000..c733f8f --- /dev/null +++ b/models/processor/IPaymentProcessor.cfc @@ -0,0 +1,212 @@ +/** + * This is the interface that every processor must implement in order to work with forgebox + */ +interface { + + /** + * Retrieve a human readable name for the processor + */ + function getName(); + + /** + * If there is a version attached to the processor then return it here. + */ + function getVersion(); + + /** + * Get the payment processor SDK library implementation. This will be getting the raw processor. + */ + any function getProcessor(); + + /** + * Pre-authorizes a transaction on the processor without capture + * + * @amount The amount in cents to charge, example: $20 = 2000, $20.5 = 2050, it is required + * @source A payment source to be charged, usually this is a card token, a customer token, etc. It is required + * @currency Usually the three-letter ISO Currency code (Optional) + * @customerId A customer identifier to attach to the charge (Optional) + * @description The description of the charge (Optional) + * @headers A struct of headers to send with the processor (Optional) + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function preAuthorize( + required numeric amount, + required source, + currency = "usd", + customerId, + description = "", + struct metadata = {} + ); + + /** + * Make a charge on the processor. Please note that any EXTRA arguments added to a processor + * The processor implementation must take care of them. + * + * @amount The amount in cents to charge, example: $20 = 2000, $20.5 = 2050, it is required + * @source A payment source to be charged, usually this is a card token, a customer token, etc. It is required + * @currency Usually the three-letter ISO Currency code (Optional) + * @customerId A customer identifier to attach to the charge (Optional) + * @description The description of the charge (Optional) + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function charge( + required numeric amount, + required source, + currency, + customerId, + description, + boolean capture = true, + struct metadata = {} + ); + + /** + * Make a refund on the processor. Please note that any EXTRA arguments added to a processor + * The processor implementation must take care of them. + * + * @charge The identifier of the charge to refund. + * @amount The amount in cents to refund, if not sent then the entire charge is refunded (Optional) + * @reason A reason of why the refund (Optional) + * @headers A struct of headers to send with the processor (Optional) + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function refund( + required charge, + numeric amount, + reason, + struct metadata = {} + ); + + /** + * Create a subscription in the provider + * The processor implementation must take care of them. + * + * @providerCustomerId The provider customer Id + * @planId Plan Id in the provider + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function createSubscription( + required providerCustomerId, + required planId, + numeric quantity, + struct metadata = {} + ); + + /** + * Get the subscription from the provider + * The processor implementation must take care of them. + * + * @subscriptionId The Subscription Id + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function getSubscription( required subscriptionId, struct metadata = {} ); + + /** + * Cancel a subscription in the provider. + * Cancelling a subscription retains access through the end of the billing period. + * + * @subscriptionId Subscription Id + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function cancelSubscription( required subscriptionId, struct metadata = {} ); + + /** + * Resume a subscription in the provider. + * + * @subscriptionId Subscription Id + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function resumeSubscription( required subscriptionId, struct metadata = {} ); + + /** + * Update the subscription quantity in the provider. + * Cancelling a subscription retains access through the end of the billing period. + * + * @subscriptionId Subscription Id + * @quantity The new quantity, the subscription cost will be calculated based on this number + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function updateSubscriptionQuantity( + required subscriptionId, + required numeric quantity, + struct metadata = {} + ); + + /** + * Delete a subscription in the provider + * The processor implementation must take care of them. + * + * @subscriptionId Subscription Id + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function deleteSubscription( required subscriptionId, struct metadata = {} ); + + /** + * Convenience method to retrieve the customer associated with a subscription + * + * @subscriptionId The subscription Id + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function getSubscriptionCustomer( required subscriptionId, struct metadata ); + + /** + * Create a customer in the provider + * The processor implementation must take care of them. + * + * @email Email of the new customer + * @paymentMethodId Payment Method Id of the provider + * @description Customer description, very handy info that could be found in the provider + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function createCustomer( + required email, + required paymentMethodId, + description, + struct metadata = {} + ); + + /** + * Get the customer struct from the provider + * The processor implementation must take care of them. + * + * @providerCustomerId The provider customer Id + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function getCustomer( required providerCustomerId, struct metadata = {} ); + + /** + * Get the customer payment method + * The processor implementation must take care of them. + * + * @providerCustomerId The provider customer Id + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function getPaymentMethod( required providerCustomerId, struct metadata = {} ); + + /** + * Update the payment method associated to a customer + * The processor implementation must take care of them. + * + * @customerId The customer Id in the provider + * @paymentMethodId The Payment Method Id generated by the provider + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function updatePaymentMethod( + required providerCustomerId, + required paymentMethodId, + struct metadata = {} + ); + + /** + * Change from one subscription plan to another one + * + * @planId The provider plan Id to change to + * @subscriptionId The subscription Id in the provider + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function changeSubscriptionPlan( + required planId, + required subscriptionId, + struct metadata = {} + ); + +} diff --git a/models/processor/PayPalProcessor.cfc b/models/processor/PayPalProcessor.cfc new file mode 100755 index 0000000..c247328 --- /dev/null +++ b/models/processor/PayPalProcessor.cfc @@ -0,0 +1,8 @@ +/** + * A cool processor/PayPal entity + */ +component extends="BaseProcessor" { + + +} + diff --git a/models/processor/ProcessorResponse.cfc b/models/processor/ProcessorResponse.cfc new file mode 100644 index 0000000..1b21767 --- /dev/null +++ b/models/processor/ProcessorResponse.cfc @@ -0,0 +1,35 @@ +/** + * This response object is a standardized response for all payment processor gateways + */ +component accessors="true" { + + /** + * The error flag if there was an exception in the call + */ + property + name ="error" + type ="boolean" + default="false"; + + /** + * The raw content returned from the gateway, this can be any format + */ + property + name ="content" + type ="any" + default=""; + + /** + * Constructor + */ + function init(){ + // Init properties + variables.content = ""; + variables.error = false; + + return this; + } + + this.memento.defaultIncludes = [ "content", "error" ]; + +} diff --git a/models/processor/StripeProcessor.cfc b/models/processor/StripeProcessor.cfc new file mode 100755 index 0000000..18de368 --- /dev/null +++ b/models/processor/StripeProcessor.cfc @@ -0,0 +1,675 @@ +/** + * A cool processor/Stripe entity + */ +component + extends ="BaseProcessor" + delegates="DateTime@coreDelegates" + implements="IPaymentProcessor" + singleton +{ + + // DI + property name="stripe" inject="stripe@stripecfml"; + + /** + * Constructor + */ + function init(){ + return this; + } + + /** + * Retrieve a human readable name for the processor + */ + function getName(){ + return "Stripe CFML"; + } + + /** + * If there is a version attached to the processor then return it here. + */ + function getVersion(){ + return "1.x.x"; + } + + /** + * Get the payment processor SDK library implementation. This will be getting the raw processor. + */ + any function getProcessor(){ + return variables.stripe; + } + + /** + * Pre-authorizes a transaction on the processor without capture + * + * @amount The amount in cents to charge, example: $20 = 2000, $20.5 = 2050, it is required + * @source A payment source to be charged, usually this is a card token, a customer token, etc. It is required + * @currency Usually the three-letter ISO Currency code (Optional) + * @customerId A customer identifier to attach to the charge (Optional) + * @description The description of the charge (Optional) + * @headers A struct of headers to send with the processor (Optional) + * @metadata A struct of metadata to send to the processor (Optional) + * + * @return a struct containing the error and, if no error, the content of the [charge object](https://stripe.com/docs/api/charges/object) + */ + ProcessorResponse function preAuthorize( + required numeric amount, + required source, + currency = "usd", + customerId, + description = "", + struct metadata = {} + ){ + arguments.capture = false; + return charge( argumentCollection = arguments ); + } + + /** + * Captures a charge created via pre-authorization + * + * @chargeId + * + * @return a struct containing the error and, if no error, the content of the [charge object](https://stripe.com/docs/api/charges/object) + */ + ProcessorResponse function capture( required chargeId ){ + var oResponse = newResponse(); + + if ( log.canDebug() ) { + log.debug( "Stripe capture starting: #serializeJSON( arguments )#" ); + } + + var processorResponse = variables.stripe.charges.capture( charge_id = arguments.chargeId ); + + // Capture it baby! + oResponse.setContent( + formatChargeResponse( processorResponse.content ) + ); + + // Check for errors + if ( processorResponse.status >= 300 ) { + oResponse.setError( true ); + } + + if ( log.canDebug() ) { + log.debug( "Stripe capture response: #serializeJSON( oResponse.getContent() )#" ); + } + + return oResponse; + } + + /** + * Make a charge on the processor + * + * @amount The amount in cents to charge, example: $20 = 2000, $20.5 = 2050, it is required + * @source A payment source to be charged, usually this is a card token, a customer token, etc. It is required + * @currency Usually the three-letter ISO Currency code (Optional) + * @customerId A customer identifier to attach to the charge (Optional) + * @description The description of the charge (Optional) + * @headers A struct of headers to send with the processor (Optional) + * @metadata A struct of metadata to send to the processor (Optional) + * + * @return a struct containing the error and, if no error, the content of the [charge object](https://stripe.com/docs/api/charges/object) + */ + ProcessorResponse function charge( + required numeric amount, + required source, + currency = "usd", + customerId, + description = "", + boolean capture = true, + struct metadata = {} + ){ + var oResponse = newResponse(); + + if ( log.canDebug() ) { + log.debug( "Stripe charge starting: #serializeJSON( arguments )#" ); + } + + var processorResponse = variables.stripe.charges.create( argumentCollection = arguments ); + // Charge it baby! + oResponse.setContent( + formatChargeResponse( processorResponse.content ) + ); + + // Check for errors + if ( processorResponse.status >= 300 ) { + oResponse.setError( true ); + } + + if ( log.canDebug() ) { + log.debug( "Stripe charge response: #serializeJSON( oResponse.getContent() )#" ); + } + + return oResponse; + } + + /** + * Make a refund on the processor + * @charge The identifier of the charge to refund. + * @amount The amount in cents to refund, if not sent then the entire charge is refunded (Optional) + * @reason A reason of why the refund (Optional) + * @headers A struct of headers to send with the processor (Optional) + * @metadata A struct of metadata to send to the processor (Optional) + * + * @return a struct containing the error and, if no error, the content of the [refund object](https://stripe.com/docs/api/refunds/object) + */ + ProcessorResponse function refund( + required charge, + numeric amount, + reason = "", + struct metadata = {} + ){ + var oResponse = newResponse(); + + if ( log.canDebug() ) { + log.debug( "Stripe refund starting: #serializeJSON( arguments )#" ); + } + + var processorResponse = variables.stripe.refunds.create( argumentCollection = arguments ); + + // Charge it baby! + oResponse.setContent( processorResponse.content ); + + // Check for errors + if ( processorResponse.status >= 300 ) { + oResponse.setError( true ); + } + + if ( log.canDebug() ) { + log.debug( "Stripe refund response: #serializeJSON( oResponse.getContent() )#" ); + } + + return oResponse; + } + + /** + * Create a customer on Stripe so we can associate to a plan subscription + * + * @email email of the customer we are creating + * @paymentMethod Token provided by the stripe form with the payment method + * @description Customer description, very handy info that could be found in the provider + * @metadata A struct of metadata to send to the processor (Optional) + * + * @return a struct containing the error and, if no error, the content of the [customer object](https://docs.stripe.com/api/customers/object) + */ + ProcessorResponse function createCustomer( + required email, + required paymentMethodId, + description = "", + struct metadata = {} + ){ + var invoiceSettings = { "default_payment_method" : arguments.paymentMethodId }; + + var oResponse = newResponse(); + + if ( log.canDebug() ) { + log.debug( "Stripe customer creation starting: #serializeJSON( arguments )#" ); + } + + var processorResponse = variables.stripe.customers.create( + email = arguments.email, + payment_method = arguments.paymentMethodId, + description = arguments.description, + invoice_settings = invoiceSettings + ); + + // Create customer + oResponse.setContent( + processorResponse.content + ); + + // Check for errors + if ( processorResponse.status >= 300 ) { + oResponse.setError( true ); + } + + if ( log.canDebug() ) { + log.debug( "Stripe customer creation response: #serializeJSON( oResponse.getContent() )#" ); + } + + return oResponse; + } + + /** + * Retrieves a list of customers in the stripe system + * + * @limit The max rows to return + * @offset The offset to start the list + * @metadata + * + * @return a struct containing the error and, if no error, the content of the [customers list](https://docs.stripe.com/api/customers/list) + */ + ProcessorResponse function listCustomers( + numeric limit = 10, + numeric offset = 0, + struct metadata = {} + ){ + var oResponse = newResponse(); + + if ( log.canDebug() ) { + log.debug( "Stripe list customers starting: #serializeJSON( arguments )#" ); + } + + var processorResponse = variables.stripe.customers.list( limit = arguments.limit, offset = arguments.offset ); + + // List customers + oResponse.setContent( processorResponse.content ); + + // Check for errors + if ( processorResponse.status >= 300 ) { + oResponse.setError( true ); + } + + if ( log.canDebug() ) { + log.debug( "Stripe list customers response: #serializeJSON( oResponse.getContent() )#" ); + } + + return oResponse; + } + + /** + * Get the customer struct from the provider + * + * @providerCustomerId The provider customer Id + * @metadata A struct of metadata to send to the processor (Optional)* @return a struct containing the error and, if no error, the content of the [customer object](https://docs.stripe.com/api/customers/object) + */ + ProcessorResponse function getCustomer( required providerCustomerId, struct metadata ){ + var customer = {}; + var oResponse = newResponse(); + + if ( log.canDebug() ) { + log.debug( "Stripe get customer starting: #serializeJSON( arguments )#" ); + } + + oResponse.setContent( {} ); + + // Find customer + var customer = variables.stripe.customers.retrieve( arguments.providerCustomerId ); + if ( structKeyExists( customer, "content" ) && !structKeyExists( customer.content, "error" ) ) { + oResponse.setContent( customer.content ); + } else { + oResponse.setError( true ); + return oResponse; + } + + // Check for additional errors + if ( customer.status >= 300 ) { + oResponse.setError( true ); + } + + if ( log.canDebug() ) { + log.debug( "Stripe get customer response: #serializeJSON( oResponse.getContent() )#" ); + } + + return oResponse; + } + + /** + * Create a subscription, combine a plan with a customer + * + * @plan Plan to associate to the subscription + * @customer Customer to associate the subscription plan with + * @metadata A struct of metadata to send to the processor (Optional) + * + * @return a struct containing the error and, if no error, the content of the [subscription object](https://stripe.com/docs/api/subscriptions/object) + */ + ProcessorResponse function createSubscription( + required providerCustomerId, + required planId, + numeric quantity = 1, + struct metadata = {} + ){ + var oResponse = newResponse(); + + if ( log.canDebug() ) { + log.debug( "Stripe subscription creation starting: #serializeJSON( arguments )#" ); + } + + var processorResponse = variables.stripe.subscriptions.create( + customer = arguments.providerCustomerId, + items = [ + { + "plan" : arguments.planId, + "quantity" : arguments.quantity + } + ], + expand = [ "latest_invoice.payment_intent" ] + ); + // Create customer + oResponse.setContent( processorResponse.content ); + + // Check for errors + if ( processorResponse.status >= 300 ) { + oResponse.setError( true ); + } + + if ( log.canDebug() ) { + log.debug( "Stripe subscription creation response: #serializeJSON( oResponse.getContent() )#" ); + } + + return oResponse; + } + + /** + * Cancel a subscription in the provider. + * Cancelling a subscription retains access through the end of the billing period. + * + * @subscriptionId Subscription Id + * @metadata A struct of metadata to send to the processor (Optional) + * + * @return a struct containing the error and, if no error, the content of the cancelled [subscription object](https://stripe.com/docs/api/subscriptions/object) + */ + ProcessorResponse function cancelSubscription( required subscriptionId, struct metadata = {} ){ + var oResponse = newResponse(); + + var processorResponse = variables.stripe.subscriptions.update( arguments.subscriptionId, { cancel_at_period_end : true } ); + // Associate payment method as default + oResponse.setContent( + processorResponse.content + ); + + // Check for errors + if ( processorResponse.status >= 300 ) { + oResponse.setError( true ); + } + + return oResponse; + } + + /** + * Resume a subscription in the provider. + * + * @subscriptionId Subscription Id + * @metadata A struct of metadata to send to the processor (Optional) + * + * @return a struct containing the error and, if no error, the content of the resumed [subscription object](https://stripe.com/docs/api/subscriptions/object) + */ + ProcessorResponse function resumeSubscription( required subscriptionId, struct metadata = {} ){ + var oResponse = newResponse(); + + var processorResponse = variables.stripe.subscriptions.update( arguments.subscriptionId, { cancel_at_period_end : false } ); + + // Associate payment method as default + oResponse.setContent( processorResponse.content ); + + // Check for errors + if ( processorResponse.status >= 300 ) { + oResponse.setError( true ); + } + + return oResponse; + } + + /** + * Update the subscription quantity in the provider. + * Cancelling a subscription retains access through the end of the billing period. + * + * @subscriptionId Subscription Id + * @quantity The new quantity, the subscription cost will be calculated based on this number + * @metadata A struct of metadata to send to the processor (Optional) + * + * @return a struct containing the error and, if no error, the content of the updated [subscription object](https://stripe.com/docs/api/subscriptions/object) + */ + ProcessorResponse function updateSubscriptionQuantity( + required subscriptionId, + required numeric quantity, + struct metadata = {} + ){ + var oResponse = newResponse(); + + var processorResponse = variables.stripe.subscriptions.update( arguments.subscriptionId, { quantity : arguments.quantity } ); + + // Associate payment method as default + oResponse.setContent( + processorResponse.content + ); + + // Check for errors + if ( processorResponse.status >= 300 ) { + oResponse.setError( true ); + } + + return oResponse; + } + + /** + * Get the payment method struct from the provider + * + * @subscriptionId The subscription Id + * @metadata A struct of metadata to send to the processor (Optional) + * + * @return returns a struct containing error information and, if no error, the content of the [subscription object](https://stripe.com/docs/api/subscriptions/object) + */ + ProcessorResponse function getSubscription( required subscriptionId, struct metadata ){ + var subscription = {}; + var oResponse = newResponse(); + + if ( log.canDebug() ) { + log.debug( "Stripe get provider subscription method starting: #serializeJSON( arguments )#" ); + } + + oResponse.setContent( {} ); + + // Find subscription in stripe + var subscription = variables.stripe.subscriptions.retrieve( arguments.subscriptionId ) + if ( structKeyExists( subscription, "content" ) && !structKeyExists( subscription.content, "error" ) ) { + oResponse.setContent( subscription.content ); + } else { + oResponse.setError( true ); + return oResponse; + } + + if ( log.canDebug() ) { + log.debug( "Stripe get provider subscription response: #serializeJSON( oResponse.getContent() )#" ); + } + + return oResponse; + } + + /** + * Get the customer object from the provider + * + * @subscriptionId The subscription Id + * @metadata A struct of metadata to send to the processor (Optional) + * + * @return struct of: https://stripe.com/docs/api/customers + */ + ProcessorResponse function getSubscriptionCustomer( required subscriptionId, struct metadata ){ + var subscription = {}; + var oResponse = newResponse(); + + if ( log.canDebug() ) { + log.debug( "Stripe get provider customer by subscription id method starting: #serializeJSON( arguments )#" ); + } + + oResponse.setContent( {} ); + + // Find subscription + var subscription = getSubscription( arguments.subscriptionId ); + if ( subscription.getError() ) { + oResponse.setError( true ); + return oResponse; + } + + var customer = getCustomer( subscription.getContent().content?.customer ?: "" ); + + if ( customer.getError() ) { + oResponse.setError( true ); + return oResponse; + } + + // Set content + oResponse.setContent( customer.getContent().content ); + + if ( log.canDebug() ) { + log.debug( "Stripe get provider customer by subscription id method response: #serializeJSON( oResponse.getContent() )#" ); + } + + return oResponse; + } + + /** + * Get the payment method struct from the provider + * + * @providerCustomerId The provider customer Id to get the payment method from + * @metadata A struct of metadata to send to the processor (Optional) + * + * @return struct of: https://stripe.com/docs/api/payment_methods + */ + ProcessorResponse function getPaymentMethod( required providerCustomerId, struct metadata ){ + var customer = {}; + var oResponse = newResponse(); + + if ( log.canDebug() ) { + log.debug( "Stripe get payment method starting: #serializeJSON( arguments )#" ); + } + + oResponse.setContent( {} ); + + // Get customer struct payment method + var customer = getCustomer( providerCustomerId ); + if ( customer.getError() ) { + oResponse.setError( true ); + return oResponse; + } + var customerHasPaymentMethod = customer.getContent().content?.invoice_settings?.default_payment_method neq "" ? true : false; + + // Validate and retrieve the customer payment method + if ( customerHasPaymentMethod ) { + oResponse.setContent( + variables.stripe.paymentMethods.retrieve( + customer.getContent().content.invoice_settings.default_payment_method + ).content + ); + } else { + oResponse.setError( true ); + } + + if ( log.canDebug() ) { + log.debug( "Stripe get payment method response: #serializeJSON( oResponse.getContent() )#" ); + } + + return oResponse; + } + + /** + * Update payment method + * + * @customerId Customer provider ID + * @paymentMethodId Payment Method + */ + ProcessorResponse function updatePaymentMethod( + required providerCustomerId, + required paymentMethodId, + struct metadata = {} + ){ + var oResponse = newResponse(); + + if ( log.canDebug() ) { + log.debug( "Stripe Update Payment Method starting: #serializeJSON( arguments )#" ); + } + + // Create payment Method + var paymentMethod = variables.stripe.paymentMethods.attach( + arguments.paymentMethodId, + { customer : arguments.providerCustomerId } + ).content; + + var processorResponse = variables.stripe.customers.update( + arguments.providerCustomerId, + { invoice_settings : { default_payment_method : paymentMethod.id } } + ); + // Associate payment method as default + + oResponse.setContent( processorResponse.content ); + + // Check for errors + if ( processorResponse.status >= 300 ) { + oResponse.setError( true ); + } + + if ( log.canDebug() ) { + log.debug( "Stripe Update Payment Method response: #serializeJSON( oResponse.getContent() )#" ); + } + + return oResponse; + } + + /** + * Change from one subscription plan to another one + * + * @planId The provider plan Id to change to + * @subscriptionId The subscription Id + * @metadata A struct of metadata to send to the processor (Optional) + */ + ProcessorResponse function changeSubscriptionPlan( + required planId, + required subscriptionId, + struct metadata = {} + ){ + var oResponse = newResponse(); + + if ( log.canDebug() ) { + log.debug( "Stripe Change Subscription Plan starting: #serializeJSON( arguments )#" ); + } + + // Retrieve subscription + var subscription = variables.stripe.subscriptions.retrieve( arguments.subscriptionId ).content; + + var processorResponse = variables.stripe.subscriptions.update( + arguments.subscriptionId, + { + cancel_at_period_end : false, + proration_behavior : "create_prorations", + items : [ + { + id : subscription.items.data[ 1 ].id, + plan : arguments.planId + } + ] + } + ); + + // Associate payment method as default + oResponse.setContent( processorResponse.content ); + + // Check for errors + if ( processorResponse.status >= 300 ) { + oResponse.setError( true ); + } + + if ( log.canDebug() ) { + log.debug( "Stripe Change Subscription Plan response: #serializeJSON( oResponse.getContent() )#" ); + } + + return oResponse; + } + + /** + * Returns an ISO formatted date from unixSeconds + * + * @epochSeconds + */ + string function fromUnixSeconds( required numeric epochSeconds ){ + return getISOTime( dateAdd( "s", arguments.epochSeconds, "1970-01-01T00:00:00Z" ) ); + } + + /** + * Returns a struct with the formatted content of the charge response + * + * @content + */ + struct function formatChargeResponse( required struct content ){ + var remove = [ "calculated_statement_descriptor" ]; + content[ "processor" ] = content.calculated_statement_descriptor ?: "Stripe"; + content.created = content.keyExists( "created" ) ? fromUnixSeconds( content.created ) : javacast( + "null", + "" + ); + remove.each( ( key ) => { + structDelete( content, key ); + } ); + return content; + } + +} diff --git a/readme.md b/readme.md index 152ac4e..f0841cb 100644 --- a/readme.md +++ b/readme.md @@ -1,101 +1,3 @@ -

- -
- - - -

+`cbPayments` - The Coldbox Payments and Subscription Processing Module -

- Copyright Since 2005 ColdBox Platform by Luis Majano and Ortus Solutions, Corp -
- www.coldbox.org | - www.ortussolutions.com -

----- - -# Ortus ColdBox Module Template - -This template can be used to create Ortus based ColdBox Modules. To use, just click the `Use this Template` button in the github repository: https://github.com/coldbox-modules/module-template and run the setup task from where you cloned it. - -```bash -box task run taskFile=build/SetupTemplate -``` - -The `SetupTemplate` task will ask you for your module name, id and description and configure the template for you! Enjoy! - -## Directory Structure - -The root of the module is the root of the repository. Add all the necessary files your module will need. - -* `.github/workflows` - These are the github actions to test and build the module via CI -* `build` - This is the CommandBox task that builds the project. Only modify if needed. Most modules will never modify it. (Modify if needed) -* `test-harness` - This is a ColdBox testing application, where you will add your testing files, specs etc. -* `.cfformat.json` - A CFFormat using the Ortus Standards -* `.cflintrc` - A CFLint configuration file according to Ortus Standards -* `.editorconfig` - Smooth consistency between editors -* `.gitattributes` - Git attributes -* `.gitignore` - Basic ignores. Modify as needed. -* `.markdownlint.json` - A linting file for markdown docs -* `box.json` - The box.json for YOUR module. Modify as needed. -* `changelog.md` - A nice changelog tracking file -* `ModuleConfig.cfc` - Your module's configuration. Modify as needed. -* `readme.md` - Your module's readme. Modify as needed. -* `server-xx@x.json` - A set of json files to configure the major engines your modules supports. - -## Test Harness - -The test harness is created to bootstrap your working module into the application `afterAspectsLoad`. This is done in the `config/ColdBox.cfc`. It includes some key features: - -* `config` - Modify as needed -* `tests` - All your testing specs should go here. Please notice the commented out ORM fixtures. Enable them if your module requires ORM -* `.cfconfig.json` - A prepared cfconfig json file so your engine data is consistent. Modify as needed. -* `.env.sample` - An environment property file sample. Copy and create a `.env` if your app requires it. - - -## API Docs - -The build task will take care of building API Docs using DocBox for you but **ONLY** for the `models` folder in your module. If you want to document more then make sure you modify the `build/Build.cfc` task. - -## Github Actions Automation - -The github actions will clone, test, package, deploy your module to ForgeBox and the Ortus S3 accounts for API Docs and Artifacts. So please make sure the following environment variables are set in your repository. ** Please note that most of them are already defined at the org level ** - -* `FORGEBOX_TOKEN` - The Ortus ForgeBox API Token -* `AWS_ACCESS_KEY` - The travis user S3 account -* `AWS_ACCESS_SECRET` - The travis secret S3 - -> Please contact the admins in the `#infrastructure` channel for these credentials if needed - -## Welcome to ColdBox - -ColdBox *Hierarchical* MVC is the de-facto enterprise-level [HMVC](https://en.wikipedia.org/wiki/Hierarchical_model%E2%80%93view%E2%80%93controller) framework for ColdFusion (CFML) developers. It's professionally backed, conventions-based, modular, highly extensible, and productive. Getting started with ColdBox is quick and painless. ColdBox takes the pain out of development by giving you a standardized methodology for modern ColdFusion (CFML) development with features such as: - -* [Conventions instead of configuration](https://coldbox.ortusbooks.com/getting-started/conventions) -* [Modern URL routing](https://coldbox.ortusbooks.com/the-basics/routing) -* [RESTFul APIs](https://coldbox.ortusbooks.com/the-basics/event-handlers/rendering-data) -* [A hierarchical approach to MVC using ColdBox Modules](https://coldbox.ortusbooks.com/hmvc/modules) -* [Event-driven programming](https://coldbox.ortusbooks.com/digging-deeper/interceptors) -* [Async and Parallel programming constructs](https://coldbox.ortusbooks.com/digging-deeper/promises-async-programming) -* [Integration & Unit Testing](https://coldbox.ortusbooks.com/testing/testing-coldbox-applications) -* [Included dependency injection](https://wirebox.ortusbooks.com) -* [Caching engine and API](https://cachebox.ortusbooks.com) -* [Logging engine](https://logbox.ortusbooks.com) -* [An extensive eco-system](https://forgebox.io) -* Much More - -## Learning ColdBox - -ColdBox is the defacto standard for building modern ColdFusion (CFML) applications. It has the most extensive [documentation](https://coldbox.ortusbooks.com) of all modern web application frameworks. - - -If you don't like reading so much, then you can try our video learning platform: [CFCasts (www.cfcasts.com)](https://www.cfcasts.com) - -## Ortus Sponsors - -ColdBox is a professional open-source project and it is completely funded by the [community](https://patreon.com/ortussolutions) and [Ortus Solutions, Corp](https://www.ortussolutions.com). Ortus Patreons get many benefits like a cfcasts account, a FORGEBOX Pro account and so much more. If you are interested in becoming a sponsor, please visit our patronage page: [https://patreon.com/ortussolutions](https://patreon.com/ortussolutions) - -### THE DAILY BREAD - - > "I am the way, and the truth, and the life; no one comes to the Father, but by me (JESUS)" Jn 14:1-12 diff --git a/server-adobe@2018.json b/server-adobe@2018.json index 8c13686..f0e06d9 100644 --- a/server-adobe@2018.json +++ b/server-adobe@2018.json @@ -1,5 +1,5 @@ { - "name":"@MODULE_NAME@-adobe@2018", + "name":"cbPayments-adobe@2018", "app":{ "serverHomeDirectory":".engine/adobe2018", "cfengine":"adobe@2018" @@ -13,7 +13,7 @@ }, "webroot": "test-harness", "aliases":{ - "/moduleroot/@MODULE_NAME@":"../" + "/moduleroot/cbPayments":"../" } }, "openBrowser":"false", diff --git a/server-adobe@2021.json b/server-adobe@2021.json index d0630be..663ccb9 100644 --- a/server-adobe@2021.json +++ b/server-adobe@2021.json @@ -1,5 +1,5 @@ { - "name":"@MODULE_NAME@-adobe@2021", + "name":"cbPayments-adobe@2021", "app":{ "serverHomeDirectory":".engine/adobe2021", "cfengine":"adobe@2021" @@ -13,7 +13,7 @@ }, "webroot": "test-harness", "aliases":{ - "/moduleroot/@MODULE_NAME@":"../" + "/moduleroot/cbPayments":"../" } }, "jvm":{ diff --git a/server-adobe@2023.json b/server-adobe@2023.json index ef303ae..934d900 100644 --- a/server-adobe@2023.json +++ b/server-adobe@2023.json @@ -1,5 +1,5 @@ { - "name":"@MODULE_NAME@-adobe@2023", + "name":"cbPayments-adobe@2023", "app":{ "serverHomeDirectory":".engine/adobe2023", "cfengine":"adobe@2023" @@ -13,7 +13,7 @@ }, "webroot": "test-harness", "aliases":{ - "/moduleroot/@MODULE_NAME@":"../" + "/moduleroot/cbPayments":"../" } }, "jvm":{ diff --git a/server-lucee@5.json b/server-lucee@5.json index 6423ca7..158560d 100644 --- a/server-lucee@5.json +++ b/server-lucee@5.json @@ -1,5 +1,5 @@ { - "name":"@MODULE_NAME@-lucee@5", + "name":"cbPayments-lucee@5", "app":{ "serverHomeDirectory":".engine/lucee5", "cfengine":"lucee@5" @@ -11,13 +11,13 @@ "rewrites":{ "enable":"true" }, - "webroot": "test-harness", - "aliases":{ - "/moduleroot/@MODULE_NAME@":"../" + "webroot":"test-harness", + "aliases":{ + "/moduleroot/cbPayments":"../" } }, "openBrowser":"false", - "cfconfig": { - "file" : ".cfconfig.json" - } + "cfconfig":{ + "file":".cfconfig.json" + } } diff --git a/server-lucee@6.json b/server-lucee@6.json index c3b490e..7db71f5 100644 --- a/server-lucee@6.json +++ b/server-lucee@6.json @@ -1,5 +1,5 @@ { - "name":"@MODULE_NAME@-lucee@6", + "name":"cbPayments-lucee@6", "app":{ "serverHomeDirectory":".engine/lucee6", "cfengine":"lucee@6" @@ -13,7 +13,7 @@ }, "webroot": "test-harness", "aliases":{ - "/moduleroot/@MODULE_NAME@":"../" + "/moduleroot/cbPayments":"../" } }, "openBrowser":"false", diff --git a/test-harness/Application.cfc b/test-harness/Application.cfc index 0fcffde..5cae8c1 100644 --- a/test-harness/Application.cfc +++ b/test-harness/Application.cfc @@ -7,7 +7,7 @@ www.ortussolutions.com component{ // UPDATE THE NAME OF THE MODULE IN TESTING BELOW - request.MODULE_NAME = "@MODULE_NAME@"; + request.MODULE_NAME = "cbPayments"; // Application properties this.name = hash( getCurrentTemplatePath() ); diff --git a/test-harness/box.json b/test-harness/box.json index e8f47b1..f50a63a 100644 --- a/test-harness/box.json +++ b/test-harness/box.json @@ -5,7 +5,7 @@ "private":true, "description":"", "dependencies":{ - "coldbox":"^6.0.0" + "coldbox":"^7.0.0" }, "devDependencies":{ "testbox":"*" diff --git a/test-harness/config/Coldbox.cfc b/test-harness/config/Coldbox.cfc index d35b065..440b6b4 100644 --- a/test-harness/config/Coldbox.cfc +++ b/test-harness/config/Coldbox.cfc @@ -48,6 +48,12 @@ exclude = [] }; + moduleSettings = { + "stripecfml" : { + "apiKey" : getSystemSetting( "STRIPE_API_KEY", "" ) + } + } + //Register interceptors as an array, we need order interceptors = [ ]; diff --git a/test-harness/tests/Application.cfc b/test-harness/tests/Application.cfc index 9da19bf..a567036 100644 --- a/test-harness/tests/Application.cfc +++ b/test-harness/tests/Application.cfc @@ -1,36 +1,40 @@ /** -* Copyright 2005-2007 ColdBox Framework by Luis Majano and Ortus Solutions, Corp -* www.ortussolutions.com -* --- -*/ -component{ + * Copyright 2005-2007 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- + */ +component { // The name of the module used in cfmappings ,etc - request.MODULE_NAME = "@MODULE_NAME@"; + request.MODULE_NAME = "cbPayments"; // The directory name of the module on disk. Usually, it's the same as the module name - request.MODULE_PATH = "@MODULE_NAME@"; + request.MODULE_PATH = "cbPayments"; // APPLICATION CFC PROPERTIES - this.name = "#request.MODULE_NAME# Testing Suite"; - this.sessionManagement = true; - this.sessionTimeout = createTimeSpan( 0, 0, 15, 0 ); - this.applicationTimeout = createTimeSpan( 0, 0, 15, 0 ); - this.setClientCookies = true; + this.name = "#request.MODULE_NAME# Testing Suite"; + this.sessionManagement = true; + this.sessionTimeout = createTimespan( 0, 0, 15, 0 ); + this.applicationTimeout = createTimespan( 0, 0, 15, 0 ); + this.setClientCookies = true; // Turn on/off white space management this.whiteSpaceManagement = "smart"; - this.enableNullSupport = shouldEnableFullNullSupport(); + this.enableNullSupport = shouldEnableFullNullSupport(); // Create testing mapping this.mappings[ "/tests" ] = getDirectoryFromPath( getCurrentTemplatePath() ); // The application root - rootPath = REReplaceNoCase( this.mappings[ "/tests" ], "tests(\\|/)", "" ); - this.mappings[ "/root" ] = rootPath; + rootPath = reReplaceNoCase( this.mappings[ "/tests" ], "tests(\\|/)", "" ); + this.mappings[ "/root" ] = rootPath; // The module root path - moduleRootPath = REReplaceNoCase( rootPath, "#request.MODULE_PATH#(\\|/)test-harness(\\|/)", "" ); - this.mappings[ "/moduleroot" ] = moduleRootPath; - this.mappings[ "/#request.MODULE_NAME#" ] = moduleRootPath & "#request.MODULE_PATH#"; + moduleRootPath = reReplaceNoCase( + rootPath, + "#request.MODULE_PATH#(\\|/)test-harness(\\|/)", + "" + ); + this.mappings[ "/moduleroot" ] = moduleRootPath; + this.mappings[ "/#request.MODULE_NAME#" ] = moduleRootPath & "#request.MODULE_PATH#"; // ORM Definitions /** @@ -50,11 +54,10 @@ component{ **/ function onRequestStart( required targetPage ){ - // Set a high timeout for long running tests - setting requestTimeout="9999"; + setting requestTimeout ="9999"; // New ColdBox Virtual Application Starter - request.coldBoxVirtualApp = new coldbox.system.testing.VirtualApp( appMapping = "/root" ); + request.coldBoxVirtualApp= new coldbox.system.testing.VirtualApp( appMapping = "/root" ); // If hitting the runner or specs, prep our virtual app if ( getBaseTemplatePath().replace( expandPath( "/tests" ), "" ).reFindNoCase( "(runner|specs)" ) ) { @@ -62,8 +65,8 @@ component{ } // ORM Reload for fresh results - if( structKeyExists( url, "fwreinit" ) ){ - if( structKeyExists( server, "lucee" ) ){ + if ( structKeyExists( url, "fwreinit" ) ) { + if ( structKeyExists( server, "lucee" ) ) { pagePoolClear(); } // ormReload(); @@ -73,13 +76,14 @@ component{ return true; } - public void function onRequestEnd( required targetPage ) { + public void function onRequestEnd( required targetPage ){ request.coldBoxVirtualApp.shutdown(); } - private boolean function shouldEnableFullNullSupport() { - var system = createObject( "java", "java.lang.System" ); - var value = system.getEnv( "FULL_NULL" ); - return isNull( value ) ? false : !!value; - } + private boolean function shouldEnableFullNullSupport(){ + var system = createObject( "java", "java.lang.System" ); + var value = system.getEnv( "FULL_NULL" ); + return isNull( value ) ? false : !!value; + } + } diff --git a/test-harness/tests/resources/BaseProcessorTest.cfc b/test-harness/tests/resources/BaseProcessorTest.cfc new file mode 100644 index 0000000..dacd59d --- /dev/null +++ b/test-harness/tests/resources/BaseProcessorTest.cfc @@ -0,0 +1,164 @@ +component extends="coldbox.system.testing.BaseTestCase" { + + property name="processor"; + + /*********************************** LIFE CYCLE Methods ***********************************/ + + function beforeAll(){ + super.beforeAll(); + variables.model = getInstance( variables.processor ); + } + + function afterAll(){ + super.afterAll(); + } + + /*********************************** BDD SUITES ***********************************/ + + function run(){ + describe( "Stripe Processor", function(){ + it( "can be created", function(){ + expect( variables.model ).toBeComponent(); + } ); + + + it( "can give me its name and version", function(){ + var name = variables.model.getName(); + var version = variables.model.getVersion(); + + expect( name.len() ).toBeTrue(); + expect( version.len() ).toBeTrue(); + } ); + + it( "can get the processor", function(){ + var processor = variables.model.getProcessor(); + expect( processor ).toBeComponent(); + } ); + + it( "Will throw an error on a fake charge", function(){ + var response = variables.model.charge( + amount = 1, + source = "bogus", + description = "Unit test charge" + ); + + expect( response.getError() ).toBeTrue(); + debug( response.getContent() ); + } ); + + it( "can make a test valid charge", function(){ + var response = variables.model.charge( + amount = 100, + source = "tok_visa", + description = "Unit test charge" + ); + + expect( response.getError() ).toBeFalse(); + expect( response.getContent().paid ).toBeTrue(); + expect( response.getContent() ).toBeStruct() + .toHaveKey( "id" ) + .toHaveKey( "object" ) + .toHaveKey( "amount" ) + .toHaveKey( "amount_captured" ) + .toHaveKey( "amount_refunded" ) + .toHaveKey( "balance_transaction" ) + .toHaveKey( "billing_details" ) + .toHaveKey( "processor" ) + .toHaveKey( "captured" ) + .toHaveKey( "created" ) + .toHaveKey( "currency" ) + .toHaveKey( "disputed" ) + .toHaveKey( "fraud_details" ) + .toHaveKey( "livemode" ) + .toHaveKey( "metadata" ) + .toHaveKey( "outcome" ) + .toHaveKey( "paid" ) + .toHaveKey( "payment_method" ) + .toHaveKey( "payment_method_details" ) + .toHaveKey( "receipt_url" ) + .toHaveKey( "refunded" ) + .toHaveKey( "status" ); + variables.testCharge = response.getContent().id; + } ); + + it( "can make a test preAuth", function(){ + var response = variables.model.preAuthorize( + amount = 100, + source = "tok_visa", + description = "Unit test charge" + ); + + expect( response.getError() ).toBeFalse(); + expect( response.getContent().paid ).toBeTrue(); + expect( response.getContent() ).toBeStruct() + .toHaveKey( "id" ) + .toHaveKey( "object" ) + .toHaveKey( "amount" ) + .toHaveKey( "amount_captured" ) + .toHaveKey( "amount_refunded" ) + .toHaveKey( "billing_details" ) + .toHaveKey( "processor" ) + .toHaveKey( "captured" ) + .toHaveKey( "created" ) + .toHaveKey( "currency" ) + .toHaveKey( "disputed" ) + .toHaveKey( "fraud_details" ) + .toHaveKey( "livemode" ) + .toHaveKey( "metadata" ) + .toHaveKey( "outcome" ) + .toHaveKey( "paid" ) + .toHaveKey( "payment_method" ) + .toHaveKey( "payment_method_details" ) + .toHaveKey( "receipt_url" ) + .toHaveKey( "refunded" ) + .toHaveKey( "status" ); + expect( response.getContent().captured ).toBeFalse(); + expect( structKeyExists( response.getContent(), "balance_transaction" ) ).toBeFalse(); + variables.testPreAuth = response.getContent().id; + } ); + + it( "Can capture a pre-authorization", function(){ + if( !variables.keyExists( "testPreAuth" ) ){ + variables.testPreAuth = variables.model.preAuthorize( + amount = 100, + source = "tok_visa", + description = "Unit test charge" + ).getContent().id; + } + var response = variables.model.capture( variables.testPreAuth ); + + expect( response.getContent() ).toBeStruct() + .toHaveKey( "id" ) + .toHaveKey( "object" ) + .toHaveKey( "amount" ) + .toHaveKey( "amount_captured" ) + .toHaveKey( "amount_refunded" ) + .toHaveKey( "balance_transaction" ) + .toHaveKey( "billing_details" ) + .toHaveKey( "processor" ) + .toHaveKey( "captured" ) + .toHaveKey( "created" ) + .toHaveKey( "currency" ) + .toHaveKey( "disputed" ) + .toHaveKey( "fraud_details" ) + .toHaveKey( "livemode" ) + .toHaveKey( "metadata" ) + .toHaveKey( "outcome" ) + .toHaveKey( "paid" ) + .toHaveKey( "payment_method" ) + .toHaveKey( "payment_method_details" ) + .toHaveKey( "receipt_url" ) + .toHaveKey( "refunded" ) + .toHaveKey( "status" ); + + } ); + + it( "Will error on a fake refund", function(){ + var response = variables.model.refund( charge = 123, reason = "Unit test refund" ); + expect( response.getError() ).toBeTrue(); + } ); + } ); + } + +} + diff --git a/test-harness/tests/specs/ModuleSpec.cfc b/test-harness/tests/specs/ModuleSpec.cfc index 4a2dd25..1bb8aac 100644 --- a/test-harness/tests/specs/ModuleSpec.cfc +++ b/test-harness/tests/specs/ModuleSpec.cfc @@ -17,7 +17,6 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="root" { describe( "MockData CFC", function(){ beforeEach( function( currentSpec ){ } ); - } ); } diff --git a/test-harness/tests/specs/unit/processor/ProcessorResponseTest.cfc b/test-harness/tests/specs/unit/processor/ProcessorResponseTest.cfc new file mode 100644 index 0000000..8e70b9e --- /dev/null +++ b/test-harness/tests/specs/unit/processor/ProcessorResponseTest.cfc @@ -0,0 +1,37 @@ +/** + * The base orm entity test case will use the 'model' annotation as the instantiation path + * and then create it, prepare it for mocking and then place it in the variables scope as 'model'. It is your + * responsibility to update the model annotation instantiation path and init your model. + */ +component extends="coldbox.system.testing.BaseTestCase" { + + /*********************************** LIFE CYCLE Methods ***********************************/ + + function beforeAll(){ + super.beforeAll(); + variables.model = getWirebox().getInstance( "ProcessorResponse@cbpayments" ); + } + + function afterAll(){ + super.afterAll(); + } + + /*********************************** BDD SUITES ***********************************/ + + function run(){ + describe( "Processor response", function(){ + it( "can be created", function(){ + expect( model ).toBeComponent(); + } ); + + it( "Has consistent repsponse format", function(){ + model.setError( true ); + model.setContent( "This is a test response" ); + var response = model.getMemento(); + expect( response ).toHaveKey( "error" ).toHaveKey( "content" ); + } ); + } ); + } + +} + diff --git a/test-harness/tests/specs/unit/processor/StripeProcessorTest.cfc b/test-harness/tests/specs/unit/processor/StripeProcessorTest.cfc new file mode 100644 index 0000000..75cfb75 --- /dev/null +++ b/test-harness/tests/specs/unit/processor/StripeProcessorTest.cfc @@ -0,0 +1,13 @@ +component extends="tests.resources.BaseProcessorTest" { + + variables.processor = "StripeProcessor@cbpayments"; + + /*********************************** BDD SUITES ***********************************/ + + function run(){ + super.run(); + // Customizations below + } + +} +