gimme_gimme 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
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