spree_unified_payment 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/.gitignore +9 -0
  2. data/.travis.yml +6 -0
  3. data/Gemfile +20 -0
  4. data/LICENSE +26 -0
  5. data/README.md +61 -0
  6. data/Versionfile +12 -0
  7. data/app/assets/javascripts/admin/spree_unified_payment.js +39 -0
  8. data/app/assets/stylesheets/store/spree_unified_payment.css +7 -0
  9. data/app/controllers/application_controller.rb +2 -0
  10. data/app/controllers/spree/admin/unified_payments_controller.rb +34 -0
  11. data/app/controllers/spree/checkout_controller_decorator.rb +19 -0
  12. data/app/controllers/spree/unified_payments_controller.rb +151 -0
  13. data/app/helpers/transaction_notification_mail_helper.rb +12 -0
  14. data/app/helpers/unified_transaction_helper.rb +18 -0
  15. data/app/mailers/spree/transaction_notification_mailer.rb +16 -0
  16. data/app/models/spree/order_decorator.rb +67 -0
  17. data/app/models/spree/payment_method/unified_payment_method.rb +34 -0
  18. data/app/models/spree/store_credit_decorator.rb +4 -0
  19. data/app/models/spree/user_decorator.rb +7 -0
  20. data/app/models/unified_payment/transaction_decorator.rb +102 -0
  21. data/app/overrides/add_unified_tabs_to_admin_menu.rb +6 -0
  22. data/app/views/spree/admin/unified_payments/index.html.erb +98 -0
  23. data/app/views/spree/admin/unified_payments/query_gateway.js.erb +10 -0
  24. data/app/views/spree/admin/unified_payments/receipt.html.erb +20 -0
  25. data/app/views/spree/checkout/payment/_unifiedpaymentmethod.html.erb +24 -0
  26. data/app/views/spree/transaction_notification_mailer/send_mail.html.erb +20 -0
  27. data/app/views/spree/unified_payments/approved.html.erb +25 -0
  28. data/app/views/spree/unified_payments/canceled.html.erb +1 -0
  29. data/app/views/spree/unified_payments/create.html.erb +8 -0
  30. data/app/views/spree/unified_payments/create.js.erb +3 -0
  31. data/app/views/spree/unified_payments/declined.html.erb +4 -0
  32. data/app/views/spree/unified_payments/index.html.erb +28 -0
  33. data/app/views/spree/unified_payments/new.html.erb +35 -0
  34. data/app/views/spree/unified_payments/new.js.erb +3 -0
  35. data/config/initializers/constants.rb +8 -0
  36. data/config/routes.rb +15 -0
  37. data/db/migrate/20140120075553_add_transaction_fields_to_unified_payment_transactions.rb +14 -0
  38. data/db/migrate/20140120081453_add_unified_transaction_id_to_spree_store_credits.rb +6 -0
  39. data/lib/generators/spree_unified_payment/install/install_generator.rb +27 -0
  40. data/lib/spree_unified_payment/engine.rb +26 -0
  41. data/lib/spree_unified_payment.rb +2 -0
  42. data/lib/transaction_expiration.rb +9 -0
  43. data/spec/constants_spec.rb +11 -0
  44. data/spec/controllers/spree/admin/unified_payments_controller_spec.rb +155 -0
  45. data/spec/controllers/spree/checkout_controller_decorator_spec.rb +114 -0
  46. data/spec/controllers/spree/unified_payments_controller_spec.rb +509 -0
  47. data/spec/mailers/transaction_notification_mailer_spec.rb +48 -0
  48. data/spec/models/spree/order_decorator_spec.rb +206 -0
  49. data/spec/models/spree/payment_method/unified_payment_spec.rb +25 -0
  50. data/spec/models/spree/store_credit_decorator_spec.rb +11 -0
  51. data/spec/models/spree/user_decorator_spec.rb +12 -0
  52. data/spec/models/unified_payment/transaction_decorator_spec.rb +483 -0
  53. data/spec/spec_helper.rb +66 -0
  54. data/spree_unified_payment.gemspec +23 -0
  55. metadata +184 -0
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ *.DS_Store
2
+ tmp/
3
+ config/initializers/spree.rb
4
+ config/application.rb
5
+ config/boot.rb
6
+ config/database.yml
7
+ config/environment.rb
8
+ log/
9
+ Gemfile.lock
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - ruby-1.9.3
4
+ script:
5
+ - bundle exec rake test_app
6
+ - bundle exec rspec spec
data/Gemfile ADDED
@@ -0,0 +1,20 @@
1
+ source "https://rubygems.org"
2
+ gem 'rails', '3.2.16'
3
+ gem 'mysql2'
4
+
5
+
6
+ gem 'spree', :git => 'git://github.com/spree/spree.git', :tag => 'v2.0.3'
7
+ gem 'spree_wallet', :git => 'git://github.com/vinsol/spree_wallet.git'
8
+ gem 'spree_auth_devise', github: 'spree/spree_auth_devise', branch: '2-0-stable'
9
+
10
+ gem 'unified_payment', :git => "git@github.com:vinsol/Unified-Payments.git", :branch => 'master', :ref => '4ced91335fec2cf186c30b593d9a1f6083028748'
11
+ gem 'delayed_job_active_record', :tag => 'v4.0.0'
12
+
13
+ group :test do
14
+ gem 'rspec-rails', '~> 2.10'
15
+ gem 'shoulda-matchers', '2.2.0'
16
+ gem 'simplecov', :require => false
17
+ gem 'database_cleaner'
18
+ gem 'rspec-html-matchers'
19
+ end
20
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ Copyright (c) 2014 vinsol.com
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification,
5
+ are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+ * Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+ * Neither the name Spree nor the names of its contributors may be used to
13
+ endorse or promote products derived from this software without specific
14
+ prior written permission.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
20
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
21
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
22
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
23
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
24
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
25
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,61 @@
1
+ Spree-Unified-Payments [![Code Climate](https://codeclimate.com/github/vinsol/Spree-Unified-Payments.png)](https://codeclimate.com/github/vinsol/Spree-Unified-Payments)
2
+ ================
3
+ Enable spree store to allow payment via UnifiedPayment
4
+
5
+ Dependencies
6
+ ================
7
+
8
+ 1) gem unified_payment
9
+ ```ruby
10
+ gem 'unified_payment'
11
+ ```
12
+ 2) delayed_job
13
+ ```ruby
14
+ gem 'delayed_job_active_record'
15
+ ```
16
+ 3) spree_wallet
17
+ ```ruby
18
+ gem 'spree_wallet'
19
+ ```
20
+
21
+ Set Up
22
+ ================
23
+
24
+ Add To Gemfile:
25
+ ```ruby
26
+ gem 'spree_unified_payment'
27
+ ```
28
+
29
+ And run below command
30
+ ```ruby
31
+ bundle exec rails g spree_unified_payment:install
32
+ ```
33
+ Usage
34
+ ---------
35
+ Customer :
36
+
37
+ Customer can pay via Unified Payment payment method at Checkout and can also see the list of Unified Payment Transactions initiated by them.
38
+
39
+ If a transaction is completed but the order fails to complete, the amount paid by the customer is added to the customer's account which he can use in future so that the user does not get stuck while making the payment.
40
+
41
+ Admin :
42
+
43
+ Admin can see the list of Unified Payment Transactions initiated by customers under admin section.
44
+
45
+ Admin can also ping Unified Payment gateway for an updated status of a transaction and the transaction is then updated accordingly.
46
+
47
+ Testing
48
+ ---------
49
+ Be sure to bundle your dependencies and then create a dummy test app for the specs to run against.
50
+ ```ruby
51
+ bundle
52
+ bundle exec rake test_app
53
+ bundle exec rspec spec
54
+ ```
55
+
56
+ Credits
57
+ -------
58
+
59
+ [![vinsol.com: Ruby on Rails, iOS and Android developers](http://vinsol.com/vin_logo.png "Ruby on Rails, iOS and Android developers")](http://vinsol.com)
60
+
61
+ Copyright (c) 2014 [vinsol.com](http://vinsol.com "Ruby on Rails, iOS and Android developers"), released under the New MIT License
data/Versionfile ADDED
@@ -0,0 +1,12 @@
1
+ # This file is used to designate compatibilty with different versions of Spree
2
+ # Please see http://spreecommerce.com/documentation/extensions.html#versionfile for details
3
+
4
+ # Examples
5
+ #
6
+ # '1.2.x' => { :branch => 'master' }
7
+ # '1.1.x' => { :branch => '1-1-stable' }
8
+ # '1.0.x' => { :branch => '1-0-stable' }
9
+ # '0.70.x' => { :branch => '0-70-stable' }
10
+ # '0.40.x' => { :tag => 'v1.0.0', :version => '1.0.0' }
11
+
12
+ "1.0.x" => { :branch => '2-0-stable' }
@@ -0,0 +1,39 @@
1
+ $(document).ready(function() {
2
+ click_on_overlay = function(pop_up_div) {
3
+ $('div.ui-widget-overlay').on('click', function(){
4
+ $('div.ui-widget-overlay').remove();
5
+ pop_up_div.dialog("close");
6
+ });
7
+ }
8
+
9
+ ModalPopUp = function(pop_up_div) {
10
+ var self = {
11
+ init: function() {
12
+ pop_up_div.dialog({
13
+ autoOpen: false,
14
+ modal: true,
15
+ width:700,
16
+ closeText: "X",
17
+ dialogClass:"quick_view_container",
18
+ close: function(event, ui) {
19
+ pop_up_div.remove();
20
+ $('#easy_zoom').remove();
21
+ }
22
+ });
23
+ },
24
+ show: function() {
25
+ pop_up_div.dialog("open");
26
+ click_on_overlay(pop_up_div);
27
+ }
28
+ }
29
+ self.init();
30
+ return self;
31
+ }
32
+
33
+ $('#reveal_xml').click(function() {
34
+ ct_id = $(this).attr('data-ct-id');
35
+ var quick_view = new ModalPopUp($("<div></div>").attr('id', 'quick_view_popup').html($("#xml_response_" + ct_id).clone()));
36
+ $('#quick_view_popup .xml_response').removeClass('hidden');
37
+ quick_view.show();
38
+ })
39
+ });
@@ -0,0 +1,7 @@
1
+ #approved_notice { font-family:Georgia, 'Times New Roman', Times, serif; letter-spacing:2px; font-size:12px; text-transform:uppercase; padding:20px; border:solid 1px #3333ff; background:#dedeff; color:#3333ff }
2
+ #unified_payments_approved_info { width:100%; border:0px; cellspacing: 15; cellpadding: 0; }
3
+ #unified_payments_approved_info td.info_attr { color:#000; font-family:Georgia, 'Times New Roman', Times, serif; letter-spacing:2px; font-size:12px; text-transform:uppercase; }
4
+ #unified_payments_approved_info td.info_val { font-family:Arial, Helvetica, sans-serif; font-size:12px; color:#666; }
5
+ #unified_payment_detail_link_container { display:block; padding:10px 0; font-family:Arial, Helvetica, sans-serif; font-size:14px; font-weight:bold; }
6
+ #unified_payment_detail_link_container a { text-decoration:underline; color:#09F; }
7
+ #approved_fail_notice { color:#EC1B23; border:solid 1px #EC1B23; font-family:Georgia, 'Times New Roman', Times, serif; letter-spacing:2px; font-size:12px; text-transform:uppercase; text-align:center; padding:20px 0; background:#ffe0e0; }
@@ -0,0 +1,2 @@
1
+ class ApplicationController < ActionController::Base
2
+ end
@@ -0,0 +1,34 @@
1
+ module Spree
2
+ module Admin
3
+ class UnifiedPaymentsController < Spree::Admin::BaseController
4
+ helper 'transaction_notification_mail'
5
+
6
+ before_filter :load_transactions, :only => [:query_gateway, :receipt]
7
+
8
+ def index
9
+ params[:q] ||= {}
10
+ @search = UnifiedPayment::Transaction.order('updated_at desc').ransack(params[:q])
11
+ @card_transactions = @search.result.page(params[:page]).per(20)
12
+ end
13
+
14
+ def receipt
15
+ @order = @card_transaction.order
16
+ doc = Nokogiri::XML(@card_transaction.xml_response)
17
+ @message = Hash.from_xml(doc.to_xml)['Message']
18
+ render :layout => false
19
+ end
20
+
21
+ def query_gateway
22
+ response = UnifiedPayment::Client.get_order_status(@card_transaction.gateway_order_id, @card_transaction.gateway_session_id)
23
+ @card_transaction.update_transaction_on_query(response["orderStatus"])
24
+ end
25
+
26
+ private
27
+
28
+ def load_transactions
29
+ @card_transaction = UnifiedPayment::Transaction.where(:payment_transaction_id => params[:transaction_id]).first
30
+ render js: "alert('Could not find transaction')" unless @card_transaction
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,19 @@
1
+ Spree::CheckoutController.class_eval do
2
+ before_filter :redirect_for_card_payment, :only => :update, :if => :payment_state?
3
+
4
+ private
5
+
6
+ def payment_state?
7
+ params[:state] == 'payment'
8
+ end
9
+
10
+ def redirect_for_card_payment
11
+ payment_method_id = params[:order][:payments_attributes][0][:payment_method_id] if params[:order] && params[:order][:payments_attributes]
12
+ payment_method = Spree::PaymentMethod.where(:id => payment_method_id).first
13
+
14
+ if payment_method.is_a?(Spree::PaymentMethod::UnifiedPaymentMethod)
15
+ @order.update_attributes(object_params)
16
+ redirect_to new_unified_transaction_path
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,151 @@
1
+ module Spree
2
+ #
3
+ # UnifiedPaymentsController controls payment via UnifiedPayment
4
+ # and has routes via methods : create, declined, canceled, approved
5
+ #
6
+
7
+ class UnifiedPaymentsController < StoreController
8
+ include UnifiedTransactionHelper
9
+
10
+ before_filter :ensure_valid_order, :only => [:new, :create]
11
+ skip_before_filter :verify_authenticity_token, :only => [:approved, :declined, :canceled]
12
+
13
+ before_filter :load_info_on_return, :only => [:declined, :canceled, :approved]
14
+ before_filter :ensure_session_transaction_id, :abort_pending_transactions, :only => :create
15
+
16
+ def index
17
+ @card_transactions = spree_current_user.unified_payments.order('updated_at desc').page(params[:page]).per(20)
18
+ end
19
+
20
+ def new
21
+ session[:transaction_id] = generate_transaction_id
22
+ end
23
+
24
+ def create
25
+ # We also can extract these options is a method.
26
+ #[MK] it was decided not giving any options since we want the requested to be redirected defined methods only
27
+ response = UnifiedPayment::Transaction.create_order_at_unified(@order.total, { :approve_url => approved_unified_payments_url, :cancel_url => canceled_unified_payments_url, :decline_url => declined_unified_payments_url, :description => "Purchasing items from #{Spree::Config[:site_name]}" })
28
+ if response
29
+ @payment_url = UnifiedPayment::Transaction.extract_url_for_unified_payment(response)
30
+ tasks_on_gateway_create_response(response, session[:transaction_id])
31
+ else
32
+ @error_message = "Could not create payment at unified, please pay by other methods or try again later."
33
+ end
34
+
35
+ render js: "$('#confirm_payment').hide();top.location.href = '#{@payment_url}'" if @payment_url
36
+ end
37
+
38
+ def declined
39
+ transaction_unsuccesful_with_message("Payment declined")
40
+ end
41
+
42
+ def canceled
43
+ transaction_unsuccesful_with_message("Successfully Canceled payment")
44
+ end
45
+
46
+ def approved
47
+ @transaction_expired = @card_transaction.expired_at?
48
+ @card_transaction.xml_response = params[:xmlmsg]
49
+
50
+ @payment_made = @gateway_message_hash['PurchaseAmountScr'].to_f
51
+ if @card_transaction.approved_at_gateway?
52
+ if @card_transaction.amount != @payment_made
53
+ process_unsuccessful_transaction
54
+ else
55
+ process_successful_transaction
56
+ end
57
+ else
58
+ add_error("Not Approved At Gateway")
59
+ end
60
+
61
+ @card_transaction.save(:validate => false)
62
+ end
63
+
64
+ private
65
+
66
+ def process_unsuccessful_transaction
67
+ add_error("Payment made was not same as requested to gateway. Please contact administrator for queries.")
68
+ @card_transaction.status = 'unsuccessful'
69
+ end
70
+
71
+ def process_successful_transaction
72
+ @card_transaction.status = 'successful'
73
+
74
+ if @transaction_expired
75
+ add_error("Payment was successful but transaction has expired. The payment made has been walleted in your account. Please contact administrator to help you further.")
76
+ elsif @order.paid? || @order.completed?
77
+ add_error("Order Already Paid Or Completed")
78
+ elsif @order.total != @payment_made
79
+ add_error("Payment made is different from order total. Payment made has been walleted to your account.")
80
+ end
81
+ end
82
+
83
+ def abort_pending_transactions
84
+ pending_card_transaction = @order.pending_card_transaction
85
+ pending_card_transaction.abort! if pending_card_transaction
86
+ end
87
+
88
+ def transaction_unsuccesful_with_message(message)
89
+ @card_transaction.assign_attributes(:status => 'unsuccessful', :xml_response => params[:xmlmsg])
90
+ @card_transaction.save(:validate => false)
91
+ flash[:error] = message
92
+ end
93
+
94
+ def add_error(message)
95
+ flash[:error] = flash[:error] ? [flash[:error], message].join('. ') : message
96
+ end
97
+
98
+ def order_invalid_with_message
99
+ if current_order
100
+ current_order.reason_if_cant_pay_by_card
101
+ else
102
+ 'Order not found'
103
+ end
104
+ end
105
+
106
+ def ensure_valid_order
107
+ if @invalid_order_message = order_invalid_with_message
108
+ flash[:error] = @invalid_order_message
109
+
110
+ redirect_to cart_path
111
+ else
112
+ load_order
113
+ end
114
+ end
115
+
116
+ def load_order
117
+ @order = current_order
118
+ end
119
+
120
+ def load_info_on_return
121
+ @gateway_message_hash = Hash.from_xml(params[:xmlmsg])['Message']
122
+ if @card_transaction = UnifiedPayment::Transaction.where(:gateway_order_id => @gateway_message_hash['OrderID']).first
123
+ @order = @card_transaction.order
124
+ else
125
+ flash[:error] = 'No transaction. Please contact our support team.'
126
+ redirect_to root_path
127
+ end
128
+ end
129
+
130
+ def tasks_on_gateway_create_response(response, transaction_id)
131
+ response_order = response['Order']
132
+
133
+ #[TODO_CR] Make required attributes protected and save them with without protection in the place where protection is not necessary.
134
+ #[MK] Please look into this change. Could not find a good way to implement it without much hastle.
135
+ gateway_transaction = UnifiedPayment::Transaction.where(:gateway_session_id => response_order['SessionID'], :gateway_order_id => response_order['OrderID'], :url => response_order['URL']).first
136
+ gateway_transaction.assign_attributes({:user_id => @order.user.try(:id), :payment_transaction_id => transaction_id, :order_id => @order.id, :gateway_order_status => 'CREATED', :amount => @order.total, :currency => Spree::Config[:currency], :response_status => response["Status"], :status => 'pending'}, :without_protection => true)
137
+ gateway_transaction.save!
138
+
139
+ @order.reserve_stock
140
+ @order.next if @order.state == 'payment'
141
+ session[:transaction_id] = nil
142
+ end
143
+
144
+ def ensure_session_transaction_id
145
+ unless session[:transaction_id]
146
+ flash[:error] = "No transaction id found, please try again"
147
+ render js: "top.location.href = '#{checkout_state_url('payment')}'"
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,12 @@
1
+ module TransactionNotificationMailHelper
2
+ def mail_content_hash_for_unified(info_hash, card_transaction)
3
+ send_info = {}
4
+
5
+ UNIFIED_XML_CONTENT_MAPPING.each_pair { |key, value| send_info[key] = info_hash[value] }
6
+
7
+ send_info[:transaction_reference] = card_transaction.payment_transaction_id
8
+ send_info[:merchants_name] = MERCHANT_NAME
9
+ send_info[:merchants_url] = MERCHANT_URL
10
+ send_info
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ module UnifiedTransactionHelper
2
+ def naira_to_kobo(amount)
3
+ (amount.to_f)*100
4
+ end
5
+
6
+ def generate_transaction_id
7
+ begin
8
+ payment_transaction_id = generate_id_using_timestamp
9
+ end while UnifiedPayment::Transaction.exists?(payment_transaction_id: payment_transaction_id)
10
+ payment_transaction_id
11
+ end
12
+
13
+ private
14
+
15
+ def generate_id_using_timestamp(length = 14)
16
+ (Time.current.to_i.to_s + [*(0..9)].sample(4).join.to_s)[0,length]
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ module Spree
2
+ class TransactionNotificationMailer < ActionMailer::Base
3
+ helper 'transaction_notification_mail'
4
+ # helper 'application'
5
+ default :from => ADMIN_EMAIL
6
+
7
+ def send_mail(card_transaction)
8
+ @card_transaction = card_transaction
9
+ @message = @card_transaction.xml_response.include?('<Message') ? Hash.from_xml(@card_transaction.xml_response)['Message'] : {}
10
+ mail(
11
+ :to => @card_transaction.user.email,
12
+ :subject => "#{Spree::Config[:site_name]} - Unified Payment Transaction #{@card_transaction.status} notification"
13
+ )
14
+ end
15
+ end
16
+ end