spree_unified_payment 1.0.0

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 (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
@@ -0,0 +1,67 @@
1
+ Spree::Order.class_eval do
2
+ has_many :unified_transactions, :class_name => 'UnifiedPayment::Transaction'
3
+ def pending_card_transaction
4
+ unified_transactions.pending.first
5
+ end
6
+
7
+ def release_inventory
8
+ shipments.each do |shipment|
9
+ shipment.cancel if shipment.inventory_units.any? { |iu| iu.pending == false }
10
+ end
11
+ end
12
+
13
+ #over writing this method to release inventory units before being deleted in case reserved
14
+ def create_proposed_shipments
15
+ release_inventory
16
+ shipments.destroy_all
17
+
18
+ packages = Spree::Stock::Coordinator.new(self).packages
19
+ packages.each do |package|
20
+ shipments << package.to_shipment
21
+ end
22
+
23
+ shipments
24
+ end
25
+
26
+ def reason_if_cant_pay_by_card
27
+ if total.zero? then 'Order Total is invalid'
28
+ elsif completed? then 'Order already completed'
29
+ elsif insufficient_stock_lines.present? then 'An item in your cart has become unavailable.'
30
+ end
31
+ end
32
+
33
+ #overwriting this method under class Spree::Order to avoid reserve_stock twice in case order is from confirm state
34
+ def finalize!
35
+ touch :completed_at
36
+
37
+ # lock all adjustments (coupon promotions, etc.)
38
+ adjustments.each { |adjustment| adjustment.update_column('state', "closed") }
39
+
40
+ # update payment states, and save
41
+ updater.update_payment_state
42
+ reserve_stock unless previous_states.last == :confirm
43
+
44
+ updater.update_shipment_state
45
+ save
46
+ updater.run_hooks
47
+
48
+ deliver_order_confirmation_email
49
+
50
+ self.state_changes.create({
51
+ previous_state: previous_states.last.to_s,
52
+ next_state: 'complete',
53
+ name: 'order' ,
54
+ user_id: self.user_id
55
+ }, without_protection: true)
56
+ end
57
+
58
+ def reserve_stock
59
+ shipments.each do |shipment|
60
+ #to reserve stock only if it has not been reserved already.
61
+ if shipment.inventory_units.any? { |inventory_unit| inventory_unit.pending == true }
62
+ shipment.update!(self)
63
+ shipment.finalize!
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,34 @@
1
+ module Spree
2
+ class PaymentMethod::UnifiedPaymentMethod < PaymentMethod
3
+
4
+ def actions
5
+ %w{capture void}
6
+ end
7
+
8
+ # Indicates whether its possible to capture the payment
9
+ def can_capture?(payment)
10
+ ['checkout', 'pending'].include?(payment.state)
11
+ end
12
+
13
+ # Indicates whether its possible to void the payment.
14
+ def can_void?(payment)
15
+ payment.state != 'void'
16
+ end
17
+
18
+ def capture(*args)
19
+ ActiveMerchant::Billing::Response.new(true, "", {}, {})
20
+ end
21
+
22
+ def void(*args)
23
+ ActiveMerchant::Billing::Response.new(true, "", {}, {})
24
+ end
25
+
26
+ def source_required?
27
+ false
28
+ end
29
+
30
+ def payment_profiles_supported?
31
+ true
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,4 @@
1
+ Spree::StoreCredit.class_eval do
2
+ attr_accessible :balance, :user, :transactioner, :type
3
+ belongs_to :unified_transaction, :class_name => 'UnifiedPayment::Transaction', :foreign_key => 'unified_transaction_id'
4
+ end
@@ -0,0 +1,7 @@
1
+ Spree::User.class_eval do
2
+ has_many :unified_payments, :class_name => 'UnifiedPayment::Transaction'
3
+
4
+ def self.create_unified_transaction_user(email)
5
+ create(:email => email, :password => SecureRandom.hex(5))
6
+ end
7
+ end
@@ -0,0 +1,102 @@
1
+ UnifiedPayment::Transaction.class_eval do
2
+ attr_accessible :status
3
+
4
+ belongs_to :order, :class_name => "Spree::Order"
5
+ belongs_to :user, :class_name => "Spree::User"
6
+ has_one :store_credit, :class_name => "Spree::StoreCredit"
7
+ scope :pending, lambda { where :status => 'pending' }
8
+ after_create :enqueue_expiration_task, :if => [:payment_transaction_id?]
9
+
10
+ after_save :notify_user_on_transaction_status, :if => [:status_changed?, "status_was == 'pending'"]
11
+ before_save :assign_attributes_using_xml, :if => [:status_changed?, "!pending?"]
12
+ after_save :complete_order, :if => [:status_changed?, :payment_valid_for_order?, :successful? ,"!order_inventory_released?"]
13
+ after_save :wallet_transaction, :if => [:status_changed? ,"(!payment_valid_for_order? || order_inventory_released?)", :successful?]
14
+ after_save :cancel_order, :if => [:status_changed?, :unsuccessful?]
15
+ after_save :release_order_inventory, :if => [:expired_at?, "expired_at_was == nil"]
16
+
17
+ def payment_valid_for_order?
18
+ !order.completed? && order.total == amount
19
+ end
20
+
21
+ def order_inventory_released?
22
+ expired_at?
23
+ end
24
+
25
+ def abort!
26
+ update_attribute(:expired_at, Time.current)
27
+ end
28
+
29
+ def successful?
30
+ status == 'successful'
31
+ end
32
+
33
+ def unsuccessful?
34
+ status == 'unsuccessful'
35
+ end
36
+
37
+ def wallet_transaction(transactioner = nil)
38
+ associate_user if user.nil?
39
+ store_credit_balance = user.store_credits_total + amount.to_f
40
+ store_credit = build_store_credit(:balance => store_credit_balance, :user => user, :transactioner => (transactioner || user), :amount => amount.to_f, :reason => "transferred from transaction:#{payment_transaction_id}", :payment_mode => Spree::Credit::PAYMENT_MODE['Payment Refund'], :type => "Spree::Credit")
41
+ store_credit.save!
42
+ end
43
+
44
+ def pending?
45
+ status == 'pending'
46
+ end
47
+
48
+ def update_transaction_on_query(gateway_order_status)
49
+ update_hash = gateway_order_status == "APPROVED" ? {:status => 'successful'} : {}
50
+ assign_attributes({:gateway_order_status => gateway_order_status}.merge(update_hash))
51
+ save(:validate => false)
52
+ end
53
+
54
+ private
55
+
56
+ def associate_user
57
+ associate_with_user = Spree::User.where(:email => order.email).first
58
+ unless associate_with_user
59
+ associate_with_user = Spree::User.create_unified_transaction_user(order.email)
60
+ end
61
+ self.user = associate_with_user
62
+ save!
63
+ end
64
+
65
+ def assign_attributes_using_xml
66
+ if xml_response.include?('<Message')
67
+ info_hash = Hash.from_xml(xml_response)['Message']
68
+
69
+ UNIFIED_XML_CONTENT_MAPPING
70
+ {:pan= => 'PAN', :response_description= => 'ResponseDescription', :gateway_order_status= => 'OrderStatus', :order_description= => 'OrderDescription', :response_status= => 'Status', :merchant_id= => 'MerchantTranID', :approval_code= => 'ApprovalCode'}.each_pair do |attribute, xml_mapping|
71
+ self.send(attribute, info_hash[xml_mapping])
72
+ end
73
+ end
74
+ end
75
+
76
+ def notify_user_on_transaction_status
77
+ Spree::TransactionNotificationMailer.delay.send_mail(self)
78
+ end
79
+
80
+ def release_order_inventory
81
+ #unless needed to not release inventory when transaction expires after order completed via some other payment method before expire or abort
82
+ order.release_inventory unless order.completed?
83
+ end
84
+
85
+ def complete_order
86
+ if order.total == amount
87
+ order.next!
88
+ order.pending_payments.first.complete
89
+ end
90
+ end
91
+
92
+ def cancel_order
93
+ unless order.completed?
94
+ order.pending_payments.first.update_attribute(:state, 'failed')
95
+ order.release_inventory unless order_inventory_released?
96
+ end
97
+ end
98
+
99
+ def enqueue_expiration_task
100
+ Delayed::Job.enqueue(TransactionExpiration.new(id), { :run_at => TRANSACTION_LIFETIME.minutes.from_now })
101
+ end
102
+ end
@@ -0,0 +1,6 @@
1
+ Deface::Override.new(:virtual_path => "spree/layouts/admin",
2
+ :name => "Add Unified tab to menu",
3
+ :insert_bottom => "[data-hook='admin_tabs'], #admin_tabs[data-hook]",
4
+ :text => " <%= tab( :Unified , :url => admin_unified_payments_path) %>",
5
+ :sequence => {:after => "promo_admin_tabs"},
6
+ :disabled => false)
@@ -0,0 +1,98 @@
1
+ <h1>Card Transactions</h1>
2
+
3
+ <% content_for :table_filter do %>
4
+ <div data-hook="admin_orders_index_search">
5
+ <%= form_for [:admin, @search], :url => unified_payments_path, :method => :get do |f| %>
6
+ <div class="field-block alpha four columns">
7
+ <div class="date-range-filter field">
8
+ <%= label_tag nil, Spree.t(:date_range) %>
9
+ <div class="date-range-fields">
10
+ <%= f.text_field :created_at_gt, :class => 'datepicker datepicker-from', :value => params[:q][:created_at_gt], :placeholder => 'From' %>
11
+
12
+ <span class="range-divider">
13
+ <i class="icon-arrow-right"></i>
14
+ </span>
15
+
16
+ <%= f.text_field :created_at_lt, :class => 'datepicker datepicker-to', :value => params[:q][:created_at_lt], :placeholder => 'Till' %>
17
+ </div>
18
+ </div>
19
+
20
+ <div class="field">
21
+ <%= label_tag nil, Spree.t(:status) %>
22
+ <%= f.select :status_eq, ['pending', 'successful', 'unsuccessful'].each {|s| [s, s]}, {:include_blank => true}, :class => 'select2' %>
23
+ </div>
24
+ </div>
25
+
26
+ <div class="four columns">
27
+ <div class="field">
28
+ <%= label_tag nil, 'TransactionId' %>
29
+ <%= f.text_field :payment_transaction_id_eq %>
30
+ </div>
31
+ </div>
32
+
33
+ <div class="clearfix"></div>
34
+
35
+ <div class="actions filter-actions">
36
+ <div data-hook="admin_orders_index_search_buttons">
37
+ <%= button Spree.t(:filter_results), 'icon-search' %>
38
+ </div>
39
+ </div>
40
+ <% end %>
41
+ </div>
42
+ <% end %>
43
+
44
+ <% if @card_transactions.present? %>
45
+ <table class="order-summary" cellspacing="0" cellpadding="0" border="0">
46
+ <thead>
47
+ <tr>
48
+ <th>Name</th>
49
+ <th>Transaction Id</th>
50
+ <th>Order Status</td>
51
+ <th><%= Spree.t(:amount) %></th>
52
+ <th>PAN</th>
53
+ <th>Gateway Order Status</th>
54
+ <th>Response</th>
55
+ <th>transaction Status</th>
56
+ <th>Date</th>
57
+ <th>XmlResponse/<br>Receipt/<br>Query/Update<br>Wallet</th>
58
+ </tr>
59
+ </thead>
60
+ <tbody>
61
+ <% @card_transactions.each do |card_transaction| %>
62
+ <tr class="<%= cycle('even', 'odd') %>" id = "<%= card_transaction.payment_transaction_id %>" >
63
+ <% #using || for checkout guest users as there is no user for it%>
64
+ <td><%= card_transaction.user.try(:email) || card_transaction.order.email %></td>
65
+ <td><%= card_transaction.payment_transaction_id %></td>
66
+ <td><%= card_transaction.order.try(:state) %></td>
67
+ <td><%= number_to_currency(card_transaction.amount) %></td>
68
+ <td><%= card_transaction.pan %></td>
69
+ <td class="gateway_order_status"><%= card_transaction.gateway_order_status %></td>
70
+ <td>
71
+ <span class="card_transaction_label">ResponseCode: &darr;</span>
72
+ <%= card_transaction.response_status %>
73
+ <span class="card_transaction_label">Description: &darr;</span>
74
+ <%= card_transaction.response_description%>
75
+ <span class="card_transaction_label">AprrovalCode: &darr;</span>
76
+ <%= card_transaction.approval_code %>
77
+ </td>
78
+ <td><%= card_transaction.status %></td>
79
+ <td><%= card_transaction.created_at.strftime("%d %b %Y, %I:%M%p") %></td>
80
+ <td class="action">
81
+ <% unless card_transaction.status == 'pending' %>
82
+ <a href="javascript:void(0)" id="reveal_xml" data-ct-id=<%= card_transaction.id%>>View XML</a>
83
+ <div class="hidden xml_response" id="xml_response_<%= card_transaction.id %>"><%= card_transaction.xml_response %></div>
84
+ <% unless card_transaction.store_credit.present? %>
85
+ |<%= link_to "Receipt", unified_payments_receipt_path(:transaction_id => card_transaction.payment_transaction_id) %>
86
+ <% end %>
87
+ <% else %>
88
+ <%= link_to("QueryAndWallet", admin_unified_payments_query_gateway_path(:transaction_id => card_transaction.payment_transaction_id), :remote => true, confirm: "#{card_transaction.amount} will be walleted to #{card_transaction.user.try(:email) }. Are You Sure?", :method => 'post', :class => 'wallet_link') unless card_transaction.store_credit.present? %>
89
+ <% end %>
90
+ </td>
91
+ </tr>
92
+ <% end %>
93
+ </tbody>
94
+ </table>
95
+ <%= paginate @card_transactions %>
96
+ <% else %>
97
+ No Transactions
98
+ <% end %>
@@ -0,0 +1,10 @@
1
+ // Are we crediting user in wallet all the time?
2
+ //[MK] It wont happen all the time. refer to model/callback
3
+ <% if @card_transaction.successful? %>
4
+ alert('Successfully walleted');
5
+ $('#<%= @card_transaction.payment_transaction_id %> .action .wallet_link').hide();
6
+ $('#<%= @card_transaction.payment_transaction_id %> .gateway_order_status').text('<%= @card_transaction.gateway_order_status %>')
7
+ <% else %>
8
+ alert('Could not wallet the money. Got order status <%= @card_transaction.gateway_order_status %>');
9
+ $('#<%= @card_transaction.payment_transaction_id %> .gateway_order_status').text('<%= @@card_transaction.gateway_order_status %>')
10
+ <% end %>
@@ -0,0 +1,20 @@
1
+ <div class="logo-container">
2
+ <%= image_tag(Spree::Config[:admin_interface_logo], :id => 'logo') %>
3
+ </div>
4
+ <h1>Receipt:</h1>
5
+ <table border="0" cellpadding="5" cellspacing="0">
6
+ <% mail_content_hash_for_unified(@message, @card_transaction).each_pair do |key, value| %>
7
+ <tr>
8
+ <td><%= key.to_s.split('_').inject([]) { |r,s| r += [s.capitalize] }.join(' ') %></td>
9
+ <td><%= value.to_s %></td>
10
+ </tr>
11
+ <% end %>
12
+ <tr>
13
+ <td>Order Description</td>
14
+ <% if @order.line_items.count == 1 %>
15
+ <td>Payment for <%= @order.line_items.first.product.name + " at #{Spree::Config[:site_name]}"%></td>
16
+ <% else %>
17
+ <td>Payment for <%= @order.line_items.count.to_s + " products at #{Spree::Config[:site_name]}"%></td>
18
+ <% end %>
19
+ </tr>
20
+ </table>
@@ -0,0 +1,24 @@
1
+
2
+
3
+ <div class="unified-visa-footnote">
4
+ <%= Spree::Config[:site_name] %> never stores or collects your card details. We securely transfer you to Unified Payments Ltd to complete your purchase. <%= Spree::Config[:site_name] %> is certified by Unified Payments Ltd for security and safety so feel comfortable purchasing on our site.
5
+ Refer <%= Spree::Config[:site_name] %>'s Delivery, Return, Refund and Cancellation Policy
6
+ </div>
7
+ <div class="visa_learn_more"><span class="visa_content">Service Provided by <br>Unified Payments Services Limited</span></div>
8
+
9
+ <div name="unified-visa-instructions" >
10
+ <div class="instructions_list">
11
+ <h6 class="subtaxon-title">About Verified by Visa</h6>
12
+ <p class="rows">Enrolling your card for Verified by Visa. <%= Spree::Config[:site_name] %> is protected with VbV and requires that the card is enrolled to participate in the VbV program. To enrol Visa cards issued by Nigerian Banks, you would need to follow the steps outlined below:</p>
13
+ Locate a nearest VISA/VPAY enabled ATM<br/>
14
+ Insert your card and punch in your PIN <br/>
15
+ Select the PIN change option <br/>
16
+ Select the Internet PIN (i-PIN) change option <br/>
17
+ Insert any four - six digits of your choice as your iPIN<br/>
18
+ Re-enter the digits entered in Step 5 above<br/>
19
+ If you have performed the above steps correctly, a message is displayed informing you you’re your PIN was changed successfully. This means that your card is now enrolled in the VbV (Verified by Visa) program and may be used for any internet related transactions. <br/>
20
+ Note that the word ‘iPIN’ , ‘Password’ and VbV code are the same<br/>
21
+ You can now pay/buy on <%= Spree::Config[:site_name] %>.com, VbV enabled site to shop securely <br/>
22
+ </ol>
23
+ </div>
24
+ </div>
@@ -0,0 +1,20 @@
1
+
2
+ <table border="0" cellpadding="5" cellspacing="0">
3
+ <tr>
4
+ <th colspan="2" align="left">Your Unified Payment Transaction was <%= @card_transaction.status %>. Below mentioned are the details</th>
5
+ </tr>
6
+ <% mail_content_hash_for_unified(@message, @card_transaction).each_pair do |key, value| %>
7
+ <tr>
8
+ <td><%= key.to_s.split('_').inject([]) { |r,s| r += [s.capitalize] }.join(' ') %></td>
9
+ <td>: <%= value.to_s %></td>
10
+ </tr>
11
+ <% end %>
12
+ <tr>
13
+ <td>Order Description</td>
14
+ <% if @card_transaction.order.line_items.count == 1 %>
15
+ <td>: Payment for <%= @card_transaction.order.line_items.first.product.name + " at #{Spree::Config[:site_name]}"%></td>
16
+ <% else %>
17
+ <td>: Payment for <%= @card_transaction.order.line_items.count.to_s + " products at #{Spree::Config[:site_name]}"%></td>
18
+ <% end %>
19
+ </tr>
20
+ </table>
@@ -0,0 +1,25 @@
1
+ <% unless flash[:error] %>
2
+ <div id="approved_notice"> Transaction was completed successfully and your order has been processed. <br>
3
+ <%= @notice %> </div>
4
+ <table id="unified_payments_approved_info">
5
+ <tr>
6
+ <td class='info_attr' width="280" valign="top">Response :</td>
7
+ <td class='info_val' align="left" valign="top"><%= @card_transaction.response_description %></td>
8
+ </tr>
9
+ <tr>
10
+ <td class='info_attr' valign="top">Order Description:</td>
11
+ <td class='info_val' align="left" valign="top"<%= @card_transaction.order_description %></td>
12
+ </tr>
13
+ <tr>
14
+ <td class='info_attr' valign="top">Amount:</td>
15
+ <td class='info_val' align="left" valign="top"><%= number_to_currency(@card_transaction.amount) %></td>
16
+ </tr>
17
+ <tr>
18
+ <td class='info_attr' valign="top">Merchant Reference Number:</td>
19
+ <td class='info_val' align="left" valign="top"><%= @card_transaction.payment_transaction_id %></td>
20
+ </tr>
21
+ </table>
22
+ <div id="unified_payment_detail_link_container">View <a href="/orders/<%= @card_transaction.order.number %>" target="_parent">Order Details</a></div>
23
+ <% else %>
24
+ <div id="approved_fail_notice"> Order was not processed. Please contact support team for any queries.</div>
25
+ <% end %>
@@ -0,0 +1 @@
1
+ <%= link_to 'Back To Cart', cart_path, :target => "_parent", :class => 'button' %>
@@ -0,0 +1,8 @@
1
+ <% unless @payment_url %>
2
+ <div id="card_error">
3
+ <%= flash[:error] %>
4
+ </div>
5
+ <% else %>
6
+ <p>This transaction will expire in <%= TRANSACTION_LIFETIME %> minutes. Please finish payment within <%= TRANSACTION_LIFETIME %> minutes.</p>
7
+ <a href=<%= @payment_url %> target='_parent' class="button">Proceed to Payment</a>
8
+ <% end %>