gimme_gimme 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. data/Gemfile +3 -0
  2. data/Gemfile.lock +143 -0
  3. data/README.md +37 -0
  4. data/Rakefile +10 -0
  5. data/features/run_features.feature +40 -0
  6. data/features/step_definitions/rails_steps.rb +62 -0
  7. data/features/support/env.rb +2 -0
  8. data/features/support/file_helpers.rb +11 -0
  9. data/lib/generators/gimme_gimme/base.rb +17 -0
  10. data/lib/generators/gimme_gimme/install/install_generator.rb +34 -0
  11. data/lib/generators/gimme_gimme/install/templates/controllers/credit_cards_controller.rb +3 -0
  12. data/lib/generators/gimme_gimme/install/templates/create_gimme_gimme_tables.rb +29 -0
  13. data/lib/generators/gimme_gimme/install/templates/models/credit_card.rb +3 -0
  14. data/lib/generators/gimme_gimme/test_support/templates/factories/factories.rb +10 -0
  15. data/lib/generators/gimme_gimme/test_support/templates/support/fake_cim.rb +9 -0
  16. data/lib/generators/gimme_gimme/test_support/templates/testing/fake_cim_gateway.rb +15 -0
  17. data/lib/generators/gimme_gimme/test_support/templates/testing/fake_cim_server.rb +329 -0
  18. data/lib/generators/gimme_gimme/test_support/templates/testing/fake_cim_spec_helpers.rb +21 -0
  19. data/lib/generators/gimme_gimme/test_support/test_support_generator.rb +23 -0
  20. data/lib/gimme_gimme.rb +7 -0
  21. data/lib/gimme_gimme/cim_gateway.rb +16 -0
  22. data/lib/gimme_gimme/credit_card.rb +125 -0
  23. data/lib/gimme_gimme/credit_cards_controller.rb +18 -0
  24. data/lib/gimme_gimme/engine.rb +7 -0
  25. data/lib/gimme_gimme/user.rb +43 -0
  26. data/lib/gimme_gimme/version.rb +3 -0
  27. data/spec/environment.rb +94 -0
  28. data/spec/factories/clearance.rb +8 -0
  29. data/spec/models/credit_card_spec.rb +219 -0
  30. data/spec/models/user_spec.rb +20 -0
  31. data/spec/spec_helper.rb +30 -0
  32. data/spec/testapp/config/routes.rb +4 -0
  33. metadata +226 -0
@@ -0,0 +1,329 @@
1
+ require 'nokogiri'
2
+
3
+ module GimmeGimme
4
+ class FakeCimServer
5
+
6
+ attr_accessor :customer_profiles, :customer_payment_profiles, :transactions
7
+
8
+ def initialize
9
+ @customer_profiles = ActiveSupport::OrderedHash.new
10
+ @customer_payment_profiles = ActiveSupport::OrderedHash.new
11
+ @transactions = []
12
+ @errors = {}
13
+ end
14
+
15
+ def reset
16
+ @customer_payment_profiles.clear
17
+ @customer_profiles.clear
18
+ @transactions.clear
19
+ @errors.clear
20
+ end
21
+
22
+ def last_customer_profile
23
+ @customer_profiles.values.last
24
+ end
25
+
26
+ def last_transaction
27
+ @transactions.last
28
+ end
29
+
30
+ def last_customer_payment_profile
31
+ @customer_payment_profiles.values.last
32
+ end
33
+
34
+ def register_error_for(request, message, type = nil)
35
+ @errors[request] = {
36
+ :type => type,
37
+ :message => message
38
+ }
39
+ end
40
+
41
+ def request(method, url, data, headers)
42
+ Rails.logger.debug "[CIM Request] #{method} #{url}\n#{data}"
43
+ document = Nokogiri::XML.parse(data)
44
+ request_type = document.root.name
45
+ response = process_request(request_type, document)
46
+ Rails.logger.debug "[CIM Response]\n#{response}"
47
+ response
48
+ end
49
+
50
+ def customer_profile_exists?(customer_profile_id)
51
+ customer_profiles.key?(customer_profile_id)
52
+ end
53
+
54
+ def customer_payment_profile_exists?(payment_profile_id)
55
+ customer_payment_profiles.key?(payment_profile_id)
56
+ end
57
+
58
+ private
59
+
60
+ def process_request(request_type, document)
61
+ error = @errors[request_type]
62
+ auth_type = document.at('profileTransAuthOnly').present? ? :auth_only : :capture_only
63
+
64
+ if error && (error[:type].nil? || error[:type] == auth_type)
65
+ response_type = request_type.sub('Request', 'Response')
66
+ response(response_type, :message_text => error, :result_code => 'Failure')
67
+ else
68
+ case request_type
69
+ when 'createCustomerProfileRequest'
70
+ create_customer_profile(document)
71
+ when 'createCustomerProfileTransactionRequest'
72
+ create_transaction(document)
73
+ when 'getCustomerProfileRequest'
74
+ get_customer_profile(document)
75
+ when 'deleteCustomerProfileRequest'
76
+ delete_customer_profile(document)
77
+ when 'createCustomerPaymentProfileRequest'
78
+ create_customer_payment_profile(document)
79
+ when 'deleteCustomerPaymentProfileRequest'
80
+ delete_customer_payment_profile(document)
81
+ when 'updateCustomerPaymentProfileRequest'
82
+ update_customer_payment_profile(document)
83
+ else
84
+ raise "No such request type: #{request_type}"
85
+ end
86
+ end
87
+ end
88
+
89
+ def create_customer_profile(document)
90
+ customer_profile_id = next_id
91
+ profile = CustomerProfile.new(customer_profile_id, document)
92
+ @customer_profiles[customer_profile_id] = profile
93
+ response('createCustomerProfileResponse', :customerProfileId => profile.customer_profile_id)
94
+ end
95
+
96
+ def create_customer_payment_profile(document)
97
+ customer_payment_profile_id = next_id
98
+ profile = CustomerPaymentProfile.new(customer_payment_profile_id, document)
99
+ @customer_profiles[profile.customer_profile_id].customer_payment_profile_id = profile.customer_payment_profile_id
100
+ @customer_payment_profiles[customer_payment_profile_id] = profile
101
+ response('createCustomerPaymentProfileResponse', :customerPaymentProfileId => profile.customer_payment_profile_id)
102
+ end
103
+
104
+ def create_transaction(document)
105
+ xml_root = 'createCustomerProfileTransactionResponse'
106
+ transaction = Transaction.create(customer_profiles, customer_payment_profiles, document, transactions)
107
+
108
+ if transaction.success?
109
+ @transactions << transaction
110
+ response(xml_root, transaction.extra_response_opts)
111
+ else
112
+ response(xml_root, :result_code => 'Failure')
113
+ end
114
+ end
115
+
116
+ def get_customer_profile(document)
117
+ id = document.at("customerProfileId").text.to_i
118
+ profile = customer_profiles[id]
119
+ response('getCustomerProfileResponse', profile.to_hash)
120
+ end
121
+
122
+ def delete_customer_profile(document)
123
+ id = document.at("customerProfileId").text.to_i
124
+ customer_profiles.delete(id)
125
+ response('deleteCustomerProfileResponse', {})
126
+ end
127
+
128
+ def delete_customer_payment_profile(document)
129
+ customer_profile_id = document.at("customerProfileId").text.to_i
130
+ customer_payment_profile_id = document.at("customerPaymentProfileId").text.to_i
131
+ customer_payment_profiles.delete(customer_payment_profile_id)
132
+ response('deleteCustomerPaymentProfileResponse', {})
133
+ end
134
+
135
+ def update_customer_payment_profile(document)
136
+ customer_payment_profile_id = document.at("customerPaymentProfileId").text.to_i
137
+ profile = CustomerPaymentProfile.new(customer_payment_profile_id, document)
138
+ @customer_profiles[profile.customer_profile_id].customer_payment_profile_id = profile.customer_payment_profile_id
139
+ @customer_payment_profiles[customer_payment_profile_id] = profile
140
+ response('updateCustomerPaymentProfileResponse', :customerPaymentProfileId => profile.customer_payment_profile_id)
141
+ end
142
+
143
+ def response(root, options)
144
+ options = {
145
+ :refId => options.delete(:ref_id) || next_id,
146
+ :messages => {
147
+ :resultCode => options.delete(:result_code) || 'Ok',
148
+ :message => {
149
+ :code => options.delete(:message_code) || 'I00001',
150
+ :text => options.delete(:message_text) || 'Successful.'
151
+ }
152
+ }
153
+ }.update(options)
154
+ options.to_xml(:root => root)
155
+ end
156
+
157
+ def next_id
158
+ Sequencer.next_id
159
+ end
160
+
161
+ module RequestParser
162
+ def at(xpath)
163
+ @document.at(xpath).try(:text)
164
+ end
165
+ end
166
+
167
+ class CustomerProfile
168
+ attr_reader :customer_profile_id,
169
+ :customer_type, :first_name, :last_name, :expiration_date, :card_number,
170
+ :merchant_customer_id, :city, :zip, :phone_number, :address, :state
171
+ attr_accessor :customer_payment_profile_id
172
+
173
+ def initialize(customer_profile_id, document)
174
+ @document = document
175
+
176
+ @customer_profile_id = customer_profile_id
177
+
178
+ @customer_type = at("customerType")
179
+ @merchant_customer_id = at("merchantCustomerId")
180
+ end
181
+
182
+ include RequestParser
183
+
184
+ def to_hash
185
+ {
186
+ :profile => {
187
+ :customerProfileId => customer_profile_id,
188
+ :paymentProfiles => { :customerPaymentProfileId => customer_payment_profile_id }
189
+ }
190
+ }
191
+ end
192
+ end
193
+
194
+ class CustomerPaymentProfile
195
+ include RequestParser
196
+
197
+ attr_reader :customer_profile_id, :customer_payment_profile_id, :expiration_date,
198
+ :card_number, :city, :zip, :address, :state, :first_name, :last_name
199
+
200
+ def initialize(customer_payment_profile_id, document)
201
+ @document = document
202
+ @customer_payment_profile_id = customer_payment_profile_id
203
+
204
+ @customer_profile_id = at("customerProfileId").to_i
205
+ @card_number = at("cardNumber")
206
+ @expiration_date = at("expirationDate")
207
+ @address = at("address")
208
+ @city = at("city")
209
+ @state = at("state")
210
+ @zip = at("zip")
211
+ @first_name = at("firstName")
212
+ @last_name = at("lastName")
213
+ end
214
+
215
+ def expiration_month
216
+ expiration.month
217
+ end
218
+
219
+ def expiration_year
220
+ expiration.year
221
+ end
222
+
223
+ def expiration
224
+ year, month = expiration_date.split('-')
225
+ ActiveMerchant::Billing::CreditCard::ExpiryDate.new(month, year).expiration
226
+ end
227
+ private :expiration
228
+
229
+ def to_hash
230
+ end
231
+ end
232
+
233
+ class Transaction
234
+ class NotImplementedError < StandardError; end
235
+ include RequestParser
236
+
237
+ attr_reader :amount, :customer_profile_id, :customer_profile, :customer_payment_profile, :transactions
238
+
239
+ def self.create(customer_profiles, customer_payment_profiles, document, transactions)
240
+ if document.at("profileTransAuthOnly").present?
241
+ AuthOnlyTransaction.new(customer_profiles, customer_payment_profiles, document, transactions)
242
+ elsif document.at("profileTransCaptureOnly").present?
243
+ CaptureOnlyTransaction.new(customer_profiles, customer_payment_profiles, document, transactions)
244
+ end
245
+ end
246
+
247
+ def initialize(customer_profiles, customer_payment_profiles, document, transactions)
248
+ @document = document
249
+ @amount = at("amount").to_f
250
+ @customer_profile_id = at("customerProfileId")
251
+ @customer_profile_payment_id = at("customerPaymentProfileId")
252
+ @customer_profile = customer_profiles[@customer_profile_id.to_i]
253
+ @customer_payment_profile = customer_payment_profiles[@customer_profile_payment_id.to_i]
254
+ end
255
+
256
+ def card_number
257
+ customer_payment_profile.card_number
258
+ end
259
+
260
+ def success?
261
+ !customer_profile.nil? &&
262
+ customer_profile.customer_payment_profile_id == @customer_profile_payment_id.to_i
263
+ end
264
+
265
+ def extra_response_opts
266
+ raise NotImplementedError
267
+ end
268
+ end
269
+
270
+ class AuthOnlyTransaction < Transaction
271
+ attr_accessor :authorization_code
272
+
273
+ def initialize(customer_profiles, customer_payment_profiles, document, transactions)
274
+ super
275
+ @authorization_code = Sequencer.next_id
276
+ end
277
+
278
+ def extra_response_opts
279
+ success = success? ? 1 : 0
280
+ {
281
+ :directResponse => "#{success},,,,#{@authorization_code},,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"
282
+ }
283
+ end
284
+ end
285
+
286
+ class CaptureOnlyTransaction < Transaction
287
+ attr_accessor :transaction_id
288
+
289
+ def initialize(customer_profiles, customer_payment_profiles, document, transactions)
290
+ super
291
+ @transaction_id = Sequencer.next_id
292
+ @document = document
293
+ @authorize_transaction =
294
+ transactions.detect do |transaction|
295
+ transaction.customer_payment_profile == customer_payment_profile &&
296
+ transaction.amount == amount &&
297
+ transaction.authorization_code.to_s == authorization_code.to_s
298
+ end
299
+ end
300
+
301
+ def extra_response_opts
302
+ success = success? ? 1 : 0
303
+ {
304
+ :directResponse => "#{success},,,,,,#{@transaction_id},,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"
305
+ }
306
+ #{:direct_response => { :transaction_id => @transaction_id} }
307
+ end
308
+
309
+ def success?
310
+ super && !@authorize_transaction.nil?
311
+ end
312
+
313
+ def authorization_code
314
+ at("approvalCode")
315
+ end
316
+
317
+ end
318
+
319
+ end
320
+
321
+ module Sequencer
322
+ extend self
323
+ @@next_id = 0
324
+
325
+ def self.next_id
326
+ @@next_id += 1
327
+ end
328
+ end
329
+ end
@@ -0,0 +1,21 @@
1
+ module GimmeGimme
2
+ module FakeCimHelpers
3
+ def with_gateway(new_gateway)
4
+ old_gateway = CimGateway.instance.gateway
5
+ CimGateway.instance.gateway = new_gateway
6
+ begin
7
+ yield
8
+ ensure
9
+ CimGateway.instance.gateway = old_gateway
10
+ end
11
+ end
12
+
13
+ def fake_cim(&block)
14
+ FakeCimServer.new.tap do |server|
15
+ with_gateway(FakeCimGateway.new(:server => server)) do
16
+ yield(server)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ require 'generators/gimme_gimme/base'
2
+
3
+ module GimmeGimme
4
+ module Generators
5
+ class TestSupportGenerator < Base
6
+
7
+ desc <<DESC
8
+ Description:
9
+ Copy gimme_gimme fake server and spec helper files to your application.
10
+ DESC
11
+
12
+ def copy_support_files
13
+ empty_directory 'lib/gimme_gimme/testing'
14
+ empty_directory 'features/support'
15
+ empty_directory 'spec/factories'
16
+ directory 'testing', 'lib/gimme_gimme/testing'
17
+ directory 'support', 'features/support'
18
+ directory 'factories', 'spec/factories'
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ require 'activemerchant'
2
+ require 'gimme_gimme/version'
3
+ require 'gimme_gimme/engine'
4
+ require 'gimme_gimme/credit_cards_controller'
5
+ require 'gimme_gimme/cim_gateway'
6
+ require 'gimme_gimme/credit_card'
7
+ require 'gimme_gimme/user'
@@ -0,0 +1,16 @@
1
+ require 'singleton'
2
+
3
+ module GimmeGimme
4
+ class CimGateway
5
+ include Singleton
6
+
7
+ attr_accessor :gateway
8
+
9
+ def initialize
10
+ @gateway = ActiveMerchant::Billing::AuthorizeNetCimGateway.new(
11
+ :login => AUTHORIZE_NET_API_LOGIN_ID,
12
+ :password => AUTHORIZE_NET_TRANSACTION_KEY
13
+ )
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,125 @@
1
+ module GimmeGimme
2
+ module CreditCard
3
+ extend ActiveSupport::Concern
4
+ class ChargeError < StandardError; end
5
+ class AuthorizationError < StandardError; end
6
+ class InvalidCreditCard < StandardError; end
7
+
8
+ included do
9
+ CARD_TYPES = ['Visa', 'Master Card', 'American Express', 'Discover'].freeze
10
+ attr_accessor :card_number, :security_code, :store_card
11
+
12
+ belongs_to :user
13
+
14
+ validates_presence_of :card_number, :security_code
15
+ validates_presence_of :expiration_month, :expiration_year, :card_type
16
+ validates_inclusion_of :card_type, :in => CARD_TYPES
17
+ validates_inclusion_of :expiration_month, :in => (1..12)
18
+ before_save :store_last_four_digits
19
+ before_create :create_customer_payment_profile
20
+ before_update :update_customer_payment_profile
21
+ after_destroy :delete_customer_payment_profile
22
+ end
23
+
24
+ module InstanceMethods
25
+ def authorize(opts)
26
+ amount = opts[:amount]
27
+ amount = amount.round(2) unless amount.kind_of?(Integer)
28
+ response = self.class.gateway.create_customer_profile_transaction(
29
+ :transaction => {
30
+ :customer_profile_id => self.user.remote_customer_profile_id,
31
+ :customer_payment_profile_id => self.remote_customer_payment_profile_id,
32
+ :type => :auth_only,
33
+ :amount => amount
34
+ }
35
+ )
36
+
37
+ if response.success?
38
+ response.params['direct_response']['approval_code']
39
+ else
40
+ raise AuthorizationError
41
+ end
42
+ end
43
+
44
+ def charge(opts)
45
+ amount = opts[:amount]
46
+ amount = amount.round(2) unless amount.kind_of?(Integer)
47
+ opts[:amount] = amount
48
+ response = self.class.gateway.create_customer_profile_transaction(
49
+ :transaction => {
50
+ :customer_profile_id => self.user.remote_customer_profile_id,
51
+ :customer_payment_profile_id => self.remote_customer_payment_profile_id,
52
+ :type => :capture_only
53
+ }.merge(opts)
54
+ )
55
+
56
+ if response.success?
57
+ response.params['direct_response']['transaction_id']
58
+ else
59
+ raise ChargeError
60
+ end
61
+ end
62
+
63
+ def store_card?
64
+ !!store_card
65
+ end
66
+
67
+ def create_customer_payment_profile
68
+ response = self.class.gateway.create_customer_payment_profile(:customer_profile_id => user.remote_customer_profile_id,
69
+ :payment_profile => {
70
+ :payment => { :credit_card => credit_card_object },
71
+ :bill_to => { :first_name => first_name, :last_name => last_name }
72
+ })
73
+ if response.success?
74
+ self.remote_customer_payment_profile_id = response.params['customer_payment_profile_id']
75
+ else
76
+ raise InvalidCreditCard
77
+ end
78
+ end
79
+ private :create_customer_payment_profile
80
+
81
+ def update_customer_payment_profile
82
+ response = self.class.gateway.update_customer_payment_profile(:customer_profile_id => user.remote_customer_profile_id,
83
+ :payment_profile => {
84
+ :payment => { :credit_card => credit_card_object },
85
+ :bill_to => { :first_name => first_name, :last_name => last_name },
86
+ :customer_payment_profile_id => remote_customer_payment_profile_id
87
+ })
88
+ unless response.success?
89
+ raise InvalidCreditCard
90
+ end
91
+ end
92
+ private :update_customer_payment_profile
93
+
94
+ def credit_card_object
95
+ ActiveMerchant::Billing::CreditCard.new(
96
+ :first_name => self.first_name.try(:dup),
97
+ :last_name => self.last_name.try(:dup),
98
+ :number => self.card_number.try(:dup),
99
+ :month => self.expiration_month.to_s.try(:dup),
100
+ :year => self.expiration_year.to_s.try(:dup),
101
+ :type => self.card_type.try(:dup),
102
+ :verification_value => self.security_code.try(:dup)
103
+ )
104
+ end
105
+ private :credit_card_object
106
+
107
+ def store_last_four_digits
108
+ self.last_four_digits = self.card_number.strip.last(4)
109
+ end
110
+ private :store_last_four_digits
111
+
112
+ def delete_customer_payment_profile
113
+ self.class.gateway.delete_customer_payment_profile(:customer_profile_id => self.user.remote_customer_profile_id,
114
+ :customer_payment_profile_id => self.remote_customer_payment_profile_id)
115
+ end
116
+ private :delete_customer_payment_profile
117
+ end
118
+
119
+ module ClassMethods
120
+ def gateway
121
+ ::GimmeGimme::CimGateway.instance.gateway
122
+ end
123
+ end
124
+ end
125
+ end