ashmont 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f63b6c7b821f3dae27701be2e0df7f3b8af6ded5
4
+ data.tar.gz: dd9f6f848a7bc533b7ce215bdfe38c4babe93c4e
5
+ SHA512:
6
+ metadata.gz: 4669eecb9e0d67700d3726a61a1da4519be58a83963251c0f4530d3ea6557d14dc6c47bacd97d03e571a1ce03de252b61bf71b1936bdec464df0ebe693cead00
7
+ data.tar.gz: 44e257f514158d23b876a352e086819c5571c72c274c2b27e651e617d5381e5dd74b169ad03ed183db10713f371ecdf6e6087a9ff26160f0cd0685ede8dea500
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ ~
6
+ *.swp
7
+ i*.swo
8
+ # ruby version and gemsets
9
+ .ruby-*
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.0
5
+ before_install: gem install bundler -v 1.14.6
data/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ ##v0.0.12
2
+
3
+ * bumped up version of braintree gem in gemspec
4
+
5
+ ## v0.0.11
6
+
7
+ * initial release
8
+ * modernized gem by fleshing out some missing files .travis.yml, CODE_OF_CONDUCT.md, LICENSE.txt, bin/console, bin/setup, spec/ashmont_spec.rb
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at rocketeer.captproton@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Carl Tanner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,119 @@
1
+ Ashmont
2
+ =======
3
+
4
+ Ashmont is a set of classes that make it easier to use
5
+ [Braintree payment processing](http://www.braintreepayments.com/) in a Rails application.
6
+
7
+ Ashmont attempts to make the following tasks easier:
8
+
9
+ * Processing error messages from Braintree and using them in the ActiveModel API
10
+ * Delegating and caching relevant information in a model backed by a Braintree
11
+ recurring subscription
12
+ * Delegating and caching relevant information in a model backed by a Braintree
13
+ customer with a credit card
14
+ * Determining what actions are necessary to handle a form update, such as
15
+ updating a credit card, switching subscription plans, or retrying failed
16
+ subscription transactions
17
+
18
+ Ashmont is still an early work in progress and the API may change dramatically with each release.
19
+
20
+ Installation
21
+ ------------
22
+
23
+ In your Gemfile:
24
+
25
+ gem "ashmont"
26
+
27
+ If you have an account with Braintree with multiple merchant accounts you'll
28
+ want to configure the merchant account for this application:
29
+
30
+ Ashmont.merchant_account_id = 'your merchant account id'
31
+
32
+ Ashmont converts billing dates from Braintree into TimeWithZone instances to
33
+ avoid time zone mishaps. You'll want to configure Ashmont with the correct
34
+ timezone so that billing dates end up on the correct day.
35
+
36
+ Ashmont.merchant_account_time_zone = 'Eastern Time (US & Canada)'
37
+
38
+ Usage
39
+ -----
40
+
41
+ In order to process payments with Braintree, you'll want to store customer and
42
+ subscription tokens locally. You'll also need to store the billing status next
43
+ billing date in order to synchronize account status with Braintree.
44
+
45
+ create_table "users" do |t|
46
+ t.string "customer_token"
47
+ t.string "subscription_token"
48
+ t.datetime "next_billing_date"
49
+ t.string "subscription_status"
50
+ end
51
+
52
+ Here's a simple example for creating and updating a subscribed customer:
53
+
54
+ class User < ActiveRecord::Base
55
+ before_create :create_customer
56
+ after_destroy :destroy_customer
57
+ memoize :customer
58
+
59
+ def past_due?
60
+ customer.past_due?
61
+ end
62
+
63
+ def save_customer(attributes)
64
+ customer.save(attributes)
65
+ end
66
+
67
+ private
68
+
69
+ def create_customer
70
+ save_customer(:email => email)
71
+ if customer.save(:email => email)
72
+ self.customer_token = customer.token
73
+ self.subscription_token = customer.subscription_token
74
+ self.next_billing_date = customer.next_billing_date
75
+ self.subscription_status = customer.status
76
+ true
77
+ else
78
+ copy_errors customer.errors
79
+ false
80
+ end
81
+ end
82
+
83
+ def destroy_customer
84
+ customer.delete
85
+ end
86
+
87
+ def copy_errors(source_errors)
88
+ source_errors.to_hash.each do |attribute, messages|
89
+ errors.set(attribute, messages)
90
+ end
91
+ end
92
+
93
+ def customer
94
+ Ashmont::SubscribedCustomer.new(
95
+ Ashmont::Customer.new(customer_token),
96
+ Ashmont::Subscription.new(subscription_token, :status => subscription_status)
97
+ )
98
+ end
99
+ end
100
+
101
+ user = User.new(params[:user])
102
+ user.save_customer(params[:customer])
103
+
104
+ The above `save_customer` method will accept attributes related to
105
+ subscriptions, customrers, and credit cards.
106
+
107
+ Testing
108
+ -------
109
+
110
+ We recommend testing your applications using the
111
+ [fake_braintree](https://github.com/thoughtbot/fake_braintree) library, which
112
+ allows applications to use the real Braintree API without actually hitting
113
+ Braintree's servers during automated tests.
114
+
115
+ License
116
+ -------
117
+
118
+ Ashmont is Copyright © 2011-2013 thoughtbot, inc. It is free software, and may be
119
+ redistributed under the terms specified in the LICENSE file.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new do |t|
5
+ t.pattern = 'spec/**/{*_spec.rb}'
6
+ end
7
+
8
+ task :default => [:spec]
data/ashmont.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "ashmont/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "ashmont"
7
+ s.version = Ashmont::VERSION.dup
8
+ s.authors = ["thoughtbot"]
9
+ s.email = ["jferris@thoughtbot.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{ActiveModel-like objects and helpers for interacting with Braintree.}
12
+ s.description = %q{ActiveModel-like objects and helpers for interacting with Braintree.}
13
+
14
+ s.rubyforge_project = "ashmont"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency('braintree', '>= 2.74.0')
22
+ s.add_dependency('activesupport', '>= 3.0.0')
23
+ s.add_dependency('i18n', '>= 0.6')
24
+ s.add_dependency('tzinfo', '>= 0.3')
25
+ s.add_development_dependency('bourne')
26
+ s.add_development_dependency('rake')
27
+ s.add_development_dependency('rspec')
28
+ s.add_development_dependency('timecop')
29
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "ashmont"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,146 @@
1
+ module Ashmont
2
+ class Customer
3
+ CREDIT_CARD_ATTRIBUTES = [:cardholder_name, :number, :cvv, :expiration_month, :expiration_year].freeze
4
+ ADDRESS_ATTRIBUTES = [:street_address, :extended_address, :locality, :region, :postal_code, :country_name].freeze
5
+ BILLING_ATTRIBUTES = (CREDIT_CARD_ATTRIBUTES + ADDRESS_ATTRIBUTES).freeze
6
+
7
+ attr_reader :token, :errors
8
+
9
+ def initialize(token = nil)
10
+ @token = token
11
+ @errors = {}
12
+ end
13
+
14
+ def credit_card
15
+ credit_cards[0]
16
+ end
17
+
18
+ def credit_cards
19
+ if persisted?
20
+ remote_customer.credit_cards
21
+ else
22
+ []
23
+ end
24
+ end
25
+
26
+ def has_billing_info?
27
+ credit_card.present?
28
+ end
29
+
30
+ def payment_method_token
31
+ credit_card.token if credit_card
32
+ end
33
+
34
+ def billing_email
35
+ remote_customer.email if persisted?
36
+ end
37
+
38
+ def save(attributes)
39
+ handle_result create_or_update(attributes)
40
+ end
41
+
42
+ def delete
43
+ Braintree::Customer.delete(@token)
44
+ end
45
+
46
+ def last_4
47
+ credit_card.last_4 if credit_card
48
+ end
49
+
50
+ def cardholder_name
51
+ credit_card.cardholder_name if credit_card
52
+ end
53
+
54
+ def expiration_month
55
+ credit_card.expiration_month if credit_card
56
+ end
57
+
58
+ def expiration_year
59
+ credit_card.expiration_year if credit_card
60
+ end
61
+
62
+ def street_address
63
+ credit_card.billing_address.street_address if credit_card
64
+ end
65
+
66
+ def extended_address
67
+ credit_card.billing_address.extended_address if credit_card
68
+ end
69
+
70
+ def locality
71
+ credit_card.billing_address.locality if credit_card
72
+ end
73
+
74
+ def region
75
+ credit_card.billing_address.region if credit_card
76
+ end
77
+
78
+ def postal_code
79
+ credit_card.billing_address.postal_code if credit_card
80
+ end
81
+
82
+ def country_name
83
+ credit_card.billing_address.country_name if credit_card
84
+ end
85
+
86
+ def confirm(query_string)
87
+ handle_result Braintree::TransparentRedirect.confirm(query_string)
88
+ end
89
+
90
+ private
91
+
92
+ def create_or_update(attributes)
93
+ remote_attributes = build_attribute_hash(attributes.symbolize_keys)
94
+ if persisted?
95
+ update(remote_attributes)
96
+ else
97
+ create(remote_attributes)
98
+ end
99
+ end
100
+
101
+ def build_attribute_hash(attributes)
102
+ result = { :email => attributes[:email] }
103
+ if BILLING_ATTRIBUTES.any? { |attribute| attributes[attribute].present? }
104
+ result[:credit_card] = CREDIT_CARD_ATTRIBUTES.inject({}) do |credit_card_attributes, attribute|
105
+ credit_card_attributes.update(attribute => attributes[attribute])
106
+ end
107
+ result[:credit_card][:billing_address] = ADDRESS_ATTRIBUTES.inject({}) do |address_attributes, attribute|
108
+ address_attributes.update(attribute => attributes[attribute])
109
+ end
110
+ if payment_method_token
111
+ result[:credit_card][:options] = { :update_existing_token => payment_method_token }
112
+ end
113
+ else
114
+ result[:credit_card] = {}
115
+ end
116
+ result
117
+ end
118
+
119
+ def create(attributes)
120
+ Braintree::Customer.create(attributes)
121
+ end
122
+
123
+ def update(attributes)
124
+ Braintree::Customer.update(@token, attributes)
125
+ end
126
+
127
+ def handle_result(result)
128
+ if result.success?
129
+ @token = result.customer.id
130
+ @remote_customer = result.customer
131
+ true
132
+ else
133
+ @errors = Ashmont::Errors.new(result.credit_card_verification, result.errors)
134
+ false
135
+ end
136
+ end
137
+
138
+ def persisted?
139
+ @token.present?
140
+ end
141
+
142
+ def remote_customer
143
+ @remote_customer ||= Braintree::Customer.find(@token)
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,47 @@
1
+ module Ashmont
2
+ class Errors
3
+ ERROR_MESSAGE_PREFIXES = {
4
+ "number" => "Credit card number ",
5
+ "CVV" => "CVV ",
6
+ "expiration_month" => "Expiration month ",
7
+ "expiration_year" => "Expiration year "
8
+ }
9
+
10
+ def initialize(result, remote_errors)
11
+ @errors = {}
12
+ parse_result(result)
13
+ parse_remote_errors(remote_errors)
14
+ end
15
+
16
+ def to_hash
17
+ @errors.dup
18
+ end
19
+
20
+ private
21
+
22
+ def parse_result(result)
23
+ if result.respond_to?(:status)
24
+ case result.status
25
+ when "processor_declined"
26
+ add_error :number, "was denied by the payment processor with the message: #{result.processor_response_text}"
27
+ when "gateway_rejected"
28
+ add_error :cvv, "did not match"
29
+ end
30
+ end
31
+ end
32
+
33
+ def parse_remote_errors(remote_errors)
34
+ remote_errors.each do |error|
35
+ if prefix = ERROR_MESSAGE_PREFIXES[error.attribute]
36
+ message = error.message.sub(prefix, "")
37
+ add_error error.attribute.downcase, message
38
+ end
39
+ end
40
+ end
41
+
42
+ def add_error(attribute, message)
43
+ @errors[attribute] ||= []
44
+ @errors[attribute] << message
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,80 @@
1
+ require 'delegate'
2
+ require 'forwardable'
3
+
4
+ module Ashmont
5
+ class SubscribedCustomer < DelegateClass(Customer)
6
+ CUSTOMER_ATTRIBUTES = [:cardholder_name, :email, :number,
7
+ :expiration_month, :expiration_year, :cvv,
8
+ :street_address, :extended_address, :locality,
9
+ :region, :postal_code, :country_name]
10
+
11
+ extend Forwardable
12
+
13
+ def initialize(customer, subscription)
14
+ super(customer)
15
+ @customer = customer
16
+ @subscription = subscription
17
+ end
18
+
19
+ def_delegators :@subscription, :reload, :status, :next_billing_date,
20
+ :transactions, :most_recent_transaction, :retry_charge, :past_due?
21
+
22
+ def subscription_token
23
+ @subscription.token
24
+ end
25
+
26
+ def save(attributes)
27
+ apply_customer_changes(attributes) && ensure_subscription_active && apply_subscription_changes(attributes)
28
+ end
29
+
30
+ def errors
31
+ super.to_hash.merge(@subscription.errors.to_hash)
32
+ end
33
+
34
+ private
35
+
36
+ def apply_customer_changes(attributes)
37
+ if new_customer? || changing_customer?(attributes)
38
+ @customer.save(attributes)
39
+ else
40
+ true
41
+ end
42
+ end
43
+
44
+ def new_customer?
45
+ token.nil?
46
+ end
47
+
48
+ def changing_customer?(attributes)
49
+ CUSTOMER_ATTRIBUTES.any? { |attribute| attributes[attribute].present? }
50
+ end
51
+
52
+ def ensure_subscription_active
53
+ if past_due?
54
+ retry_charge
55
+ else
56
+ true
57
+ end
58
+ end
59
+
60
+ def apply_subscription_changes(attributes)
61
+ if changing_subscription?(attributes)
62
+ save_subscription(attributes)
63
+ else
64
+ true
65
+ end
66
+ end
67
+
68
+ def changing_subscription?(attributes)
69
+ attributes[:plan_id].present?
70
+ end
71
+
72
+ def save_subscription(attributes)
73
+ @subscription.save(
74
+ :plan_id => attributes[:plan_id],
75
+ :price => attributes[:price].to_s,
76
+ :payment_method_token => payment_method_token
77
+ )
78
+ end
79
+ end
80
+ end