Integration with the Authorize.NET api, providing ActiveJob backed support for creating transactions.
Add this line to your application's Gemfile:
gem 'accounting', '~> 0.1.5.4'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install accounting
Then run the installer to create the initializer config, accounting.rb:
$ rails g accounting:install
Within the initializer, input your Authorize.NET api login id, key, and signature, as well as configure the gateway and any other options that seem applicable for your intended usage.
To start, simply add the accountable
method to the top of any model you'd like to associate with transactions:
class User < ApplicationRecord
accountable
# Or with optional config that will be associated with the Authorize.NET profile
# Each config option can be a symbol referring to a method to be called, a proc/lambda, or a static value
# By default, email will attempt to fetch the value of the `email` method, if it exists.
# id and description are both nil if not specified
accountable email: :email_method, id: proc { |user| rand() }, description: 'Static description'
def email_method
# Do something that returns the users email address
end
end
While the config is not required, it is beneficial to generate unique information, as it may help you locate your profiles within Authorize.NET later on.
Once a model is considered 'accountable' it exposes all of the available transaction methods, which consist of:
- hold
- capture
- void
- charge
- refund
- subscribe
See the details of each transaction method below for details on arguments and options.
In addition to the transaction methods, each created accountable model gets an associated profile
which is the entry point for all Authorize.NET related data (transactions, subscriptions, payment methods, etc.) The profile will also hold the Authorize.NET profile id, encrypted via the use of the attr_encrypted gem. Note that many of the associations that exist on the profile itself exist as delegates on the accountable model, so @accountable.profile.payments
is the same as @accountable.payments
. The idea is to prevent ever needing to actually interact with the profile model directly, since it largely just serves as a middle man between the subject model and the actions to be taken.
Once you've created either a sandbox or live Authorize.net account and logged in, take the following steps to configure the app and Authorize.net to work together:
Note: Where relevant, remember the default secret answer for the security question is "Simon" unless you have changed it already.
- From the account settings page, one must deal with 5 specific sections, highlighted here
- First, grab the API login id, key, and signature from the credentials section. You'll plug all three into your app's configuration.
- Second, create a public client key from the 'Manage Public Client key' section. You'll plug this into your app's configuration too.
- Third, enable the transaction details api.
- Also, ensure the authorize.net account is in
Live
mode, otherwise transactions will not be processed.
- Last, enable all WebHooks. The requested url should be your app's domain, followed by
/accounting/hooks/:uid
, which is the app endpoint that will handle requests from authorize.net. WebHooks are what essentially "sync" events in authorize.net with your application, such as ensuring a subscription related transaction is generated when the billing anniversary date is reached.
To create a stored payment method, simply create a new record of @accountable.payments
, where @accountable
is any model you've called the accountable
method on:
Note: An address must be assigned to each payment method, as in most cases it serves as the billing address associated with the transaction.
# Card Payment Method
@accountable.payments.create!(
profile_type: 'card',
number: '4012888888881881',
ccv: '123',
month: 8,
year: 2022,
address_attributes: { first_name: 'John', last_name: 'Doe', street_address: '123 Fake St', city: 'Gary', state: 'ID', zip: '11111', country: 'US' }
)
# ACH/Check Payment Method
@accountable.payments.create!(
profile_type: 'ach',
routing: '102003154',
account: '1234567890',
bank_name: 'Large Conglomerate Bank, Inc',
account_holder: 'Frank Boyd',
account_type: 'checking', # Possible options are: [checking, savings, businessChecking]
check_number: '1111', # Optional, the number that appears on the check itself, i.e. - 1014
echeck_type: 'WEB', # Optional, possible options are here: https://github.com/AuthorizeNet/sdk-ruby/blob/master/lib/authorize_net/payment_methods/echeck.rb#L20-L27
address_attributes: { first_name: 'John', last_name: 'Doe', street_address: '123 Fake St', city: 'Gary', state: 'ID', zip: '11111', country: 'US' }
)
Any errors during the creation of the payment method will appear as errors on the payment instance itself. Due to possible errors that could come from Authorize.NET during the creation of a payment method, creating payments is not done via ActiveJob, since errors must be present in real time.
So long as one or more payment methods exist for an accountable model, one will always be flagged as the default. By default the first payment method to be created will become the default. From then on, the default payment method can be changed by calling default!
on the payment method instance. Any other payment method flagged as default will have it's flag removed so the new default can take precedence. You can fetch the default payment method at any point by using the association helper method, @accountable.payments.default
or check if a payment method is the default by calling default?
on the payment instance. If the default payment method is ever deleted, the first available payment method (if any left) will be flagged as the default, to ensure that there's always a default payment method.
All created transactions will have an associated status that can be used to filter by. Possible statuses for transactions and their meaning are:
Status | Comment |
---|---|
pending | The default status for all transactions. Any transaction that exists in the ActiveJob queue and hasn't been processed yet will be pending. |
duplicate | Transactions which Authorize.NET deems to be a duplicate of an existing transaction will receive this status, and will no longer attempt to be processed as it's assumed based on Authorize.NET's response that the actual transaction has already been run. You may use this status to reference, but once flagged as a duplicate it can not be re-processed. |
held | Transactions that are currently "holds" on a fixed amount. Holds still need to be captured by way of the capture method in order to receive the amount specified. |
captured | Transactions that are finalized and captured, meaning the money was transferred successfully. Captured applies to both holds that were later captured as well as "charges" that were captured immediately. |
voided | Any transaction that has been successfully voided. |
refunded | Any transaction that was of the type refund. Note that refunds don't affect the original, captured transaction. They are seen as two completely separate events, not a separate step in the life of a single transaction. |
expired | Applies to "holds" only. If a hold is not captured within 30 days, it becomes an expired transaction. At which point the transaction cannot be re-processed or captured. See: AUTH_ONLY at https://support.authorize.net/authkb/index?page=content&id=A510&pmv=print&impressions=false |
All transaction methods will simply build
the transaction. Nothing is actually enqueued until the record itself is saved. This is for validation purposes since, for example, charging a negative amount would fail anyways. If you prefer however, each transaction method has a corresponding 'bang' method which will attempt to save and enqueue immediately, but will raise validation errors if anything is not okay. So instead of
@accountable.charge(1.00, @accountable.payments.default)
@accountable.save
You could just write:
@accountable.charge!(1.00, @accountable.payments.default) rescue 'Something went wrong'
A hold will place an authorization only request for the specified amount. The hold is only valid for 30 days at which point it will expire and can no longer be captured.
@accountable.hold(amount, payment, **options) # Requires @accountable.save
@accountable.hold!(amount, payment, **options) # Raise on validation error
Argument | Comment |
---|---|
amount | Decimal value, the amount to be held. |
payment | An instance of Accounting::Payment , the payment method you want to hold the amount on. Any valid instance from @accountable.payments should work. |
options |
|
Given a held
transaction, will capture the amount defined or the amount associated with the provided transaction.
@accountable.capture(transaction, amount=nil, **options) # Requires @accountable.save
@accountable.capture!(transaction, amount=nil, **options) # Raise on validation error
Argument | Comment |
---|---|
transaction | An instance of Accounting::Transaction with a status of held that you want to capture. Held transactions can be found via @accountable.transactions.held |
amount | The specific amount to capture from the held transaction. If omitted, will just use the amount from the transaction argument's instance |
options |
|
Given a transaction that has been captured but not yet settled (occurs every 24 hours), will void the transaction. Any transaction that does not meet the requirements necessary to be voided, will either fail to save or raise an error, depending upon which method is used.
@accountable.void(transaction, **options) # Requires @accountable.save
@accountable.void!(transaction, **options) # Raise on validation error
Argument | Comment |
---|---|
transaction | An instance of Accounting::Transaction with a status of captured that you want to void. |
options |
|
Immediately authorizes and charges the given payment method with the provided amount.
@accountable.charge(amount, payment, **options) # Requires @accountable.save
@accountable.charge!(amount, payment, **options) # Raise on validation error
Argument | Comment |
---|---|
amount | Decimal value, the amount to charge the payment method |
payment | An instance of Accounting::Payment , the payment method you want to charge the payment method. Any valid instance from @accountable.payments should work. |
options |
|
Refunds the transaction amount of a captured, settled transaction to the payment method defined. If the transaction has not yet settled, void
should be used instead. Refunds can only be applied to transactions that have settled within the last 120 days.
@accountable.refund(amount, transaction, payment=nil, **options) # Requires @accountable.save
@accountable.refund!(amount, transaction, payment=nil, **options) # Raise on validation error
Argument | Comment |
---|---|
amount | Decimal value, the amount to refund. Must be less than or equal to the original transaction amount. |
transaction | An instance of Accounting::Transaction you want to refund. |
payment | An instance of Account::Payment that the refunded amount should be credited to. If omitted, the original payment associated with the transaction will be used. It is up to the developer to ensure the presence of the payment method originally associated with the transaction on Authorize.NET before omitting this argument. If at all possible, this should always be explicitly defined. |
options |
|
Subscriptions can be thought of as recurring transactions, and functionally are created in a nearly identical way.
@accountable.subscribe(name, amount, payment, **options) # Requires @accountable.save
@accountable.subscribe!(name, amount, payment, **options) # Raise on validation error
Argument | Comment |
---|---|
name | A name that describes what the subscription is for. |
amount | Decimal value, the amount to be charged each 'interval' during the subscription. If a trial amount is required it should be set within the options using trial_amount and trial_occurrences |
payment | Any instance of Accounting::Payment associated with the accountable profile. |
options |
|
Provided you enter the Authorize.net signature key in the configuration and have enabled webhooks within the Authorize.net dashboard (see: Setup, step 5), Authorize.net will utilize it's own WebHooks to notify the application of changes/updates to four resources: Customer Profiles, Payment Profiles, Transactions, and Subscriptions.
Note that the use of WebHooks is all but required if utilizing subscription functionality due to the fact that webhooks are the only way subscription related transactions get created and the "next transaction date" gets moved forward. If webhooks are not enabled it is entirely up to the developer to calculate when a subscription transaction occurs and update the subscription instance's next_transaction_at
column accordingly.
Several callback methods exist to allow hooking into webhook related events, the details of which are listed below:
Note: after_*
callbacks are only triggered if the event itself was successful, so it is possible to see a before_*
callback fired, but not see it's corresponding after_*
callback if the action undertaken was not successful.
Callback Method | Arguments | Comment |
---|---|---|
before_subscription_tick | Subscription, Transaction | Triggered via a WebHook, fired just before saving the transaction associated with the subscription. |
after_subscription_tick | Subscription, Transaction | Fired just after saving a subscription transaction. |
before_transaction_submit | Transaction | Fired just prior to making the api request to Authorize.net to create the transaction. This is the last opportunity to update the transaction instance itself (i.e. - The amount to be charged, payment method, etc.) before the request is made. |
after_transaction_submit | Transaction | Fired just after making the request to Authorize.net, if successful. The callback may not be fired if the request failed or if the transaction is seen as a duplicate. |
before_transaction_sync | Transaction | Triggered via a WebHook, fired just before saving/updating a transaction. The transaction may or may not be associated with a subscription, if you need to observe subscription specific transaction syncs, use *_subscription_tick callbacks |
after_transaction_sync | Transaction | Fired just after saving/updating a transaction |
before_subscription_submit | Subscription | Fired just before making a "create subscription" api request to Authorize.net. Last opportunity to make changes to a subscription. |
after_subscription_submit | Subscription | Fired just after completing a successful "create subscription" request. |
before_subscription_sync | Subscription | Triggered via a WebHook, fired just before saving the subscription in question. Can use assign_attributes or other attribute setter on the subscription argument to manipulate the subscription just prior to saving. |
after_subscription_sync | Subscription | Fired just after syncing subscription data from Authorize.net |
before_subscription_cancel | Subscription | Fired just prior to canceling the subscription in question. Note that if Accounting.config.cancel_subscription_on_destroy is set to true this callback will be fired when destroy is called on the subscription instance. |
after_subscription_cancel | Subscription | Fired just after canceling the subscription, if the cancellation was successful. Note that if Accounting.config.cancel_subscription_on_destroy is set to true it's possible to prevent subscription destruction by including a raise in the callback method, and should be used with caution for that reason. |
Each callback should be defined as a class method within your accountable model, like so:
class User < ApplicationRecord
accountable
after_subscription_tick :subscription_ticked
def subscription_ticked(subscription, transaction)
# Do something with the subscription or transaction objects, i.e. - send a notification
end
end
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Web hook service specs don't pass unless you update the webhook json before testing.
For webhook testing, run railsapp and ngrok to receive webhook. Use these commands in rails console to create profile and payment:
u = User.create
address = Accounting::Address.new(first_name: 'Ming', last_name: 'Jin', street_address: 'street address', city: 'Calgary', state:'Illionis', zip: '345677')
Accounting::Payment.new(profile: u.profile, profile_type: 'ach', routing: '011401533', account: '021000021', bank_name: 'Test bank', account_holder: 'Ming Jin', account_type: 'checking', address: address)
Accounting::Test::CreateCard.new(u.profile, '370000000000002',1234,nil,nil,address).create_payment
Bug reports and pull requests are welcome on GitHub at https://github.com/ehainer/accounting. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
The gem is available as open source under the terms of the GNU General Public License.