tb_checkout 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +162 -0
  4. data/Rakefile +24 -0
  5. data/app/assets/images/tb_checkout/admin/shopping_carts.png +0 -0
  6. data/app/assets/images/tb_checkout/admin/transactions.png +0 -0
  7. data/app/assets/javascripts/tb_checkout/admin/carts.js +2 -0
  8. data/app/assets/javascripts/tb_checkout/admin/transactions.js +2 -0
  9. data/app/assets/javascripts/tb_checkout/application.js +13 -0
  10. data/app/assets/javascripts/tb_checkout/cart_items.js +2 -0
  11. data/app/assets/javascripts/tb_checkout/carts.js +2 -0
  12. data/app/assets/javascripts/tb_checkout/transactions.js +2 -0
  13. data/app/assets/stylesheets/tb_checkout/admin/carts.css +4 -0
  14. data/app/assets/stylesheets/tb_checkout/admin/transactions.css +4 -0
  15. data/app/assets/stylesheets/tb_checkout/application.css +15 -0
  16. data/app/assets/stylesheets/tb_checkout/cart_items.css +4 -0
  17. data/app/assets/stylesheets/tb_checkout/carts.css +4 -0
  18. data/app/assets/stylesheets/tb_checkout/transactions.css +4 -0
  19. data/app/controllers/concerns/tb_checkout/controller_helpers.rb +16 -0
  20. data/app/controllers/tb_checkout/admin/carts_controller.rb +39 -0
  21. data/app/controllers/tb_checkout/admin/transactions_controller.rb +26 -0
  22. data/app/controllers/tb_checkout/application_controller.rb +6 -0
  23. data/app/controllers/tb_checkout/cart_items_controller.rb +63 -0
  24. data/app/controllers/tb_checkout/carts_controller.rb +8 -0
  25. data/app/controllers/tb_checkout/transactions_controller.rb +87 -0
  26. data/app/helpers/tb_checkout/admin/carts_helper.rb +13 -0
  27. data/app/helpers/tb_checkout/admin/transactions_helper.rb +16 -0
  28. data/app/helpers/tb_checkout/application_helper.rb +138 -0
  29. data/app/helpers/tb_checkout/transactions_helper.rb +2 -0
  30. data/app/models/concerns/tb_checkout/belongs_to_user_session.rb +15 -0
  31. data/app/models/concerns/tb_checkout/purchasable.rb +81 -0
  32. data/app/models/tb_checkout/basic_product.rb +6 -0
  33. data/app/models/tb_checkout/cart.rb +56 -0
  34. data/app/models/tb_checkout/cart_item.rb +48 -0
  35. data/app/models/tb_checkout/transaction.rb +253 -0
  36. data/app/views/tb_checkout/admin/carts/index.html.erb +54 -0
  37. data/app/views/tb_checkout/admin/carts/show.html.erb +45 -0
  38. data/app/views/tb_checkout/admin/transactions/_table.html.erb +24 -0
  39. data/app/views/tb_checkout/admin/transactions/index.html.erb +7 -0
  40. data/app/views/tb_checkout/admin/transactions/show.html.erb +70 -0
  41. data/app/views/tb_checkout/cart_items/create.html.erb +2 -0
  42. data/app/views/tb_checkout/cart_items/destroy.html.erb +2 -0
  43. data/app/views/tb_checkout/cart_items/update.html.erb +2 -0
  44. data/app/views/tb_checkout/carts/_cart.html.erb +65 -0
  45. data/app/views/tb_checkout/carts/show.html.erb +10 -0
  46. data/app/views/tb_checkout/transactions/_details.html.erb +17 -0
  47. data/app/views/tb_checkout/transactions/confirm.html.erb +11 -0
  48. data/app/views/tb_checkout/transactions/index.html.erb +29 -0
  49. data/app/views/tb_checkout/transactions/new.html.erb +90 -0
  50. data/app/views/tb_checkout/transactions/show.html.erb +14 -0
  51. data/config/locales/en.yml +23 -0
  52. data/config/routes.rb +20 -0
  53. data/db/migrate/20140703185256_create_tb_checkout_carts.rb +12 -0
  54. data/db/migrate/20140703185258_create_tb_checkout_cart_items.rb +15 -0
  55. data/db/migrate/20140703185259_create_tb_checkout_transactions.rb +23 -0
  56. data/db/migrate/20140707142350_create_tb_checkout_basic_products.rb +8 -0
  57. data/db/migrate/20140915202848_add_spud_user_and_session_id_to_transactions.rb +10 -0
  58. data/lib/tasks/tb_checkout_tasks.rake +19 -0
  59. data/lib/tb_checkout.rb +4 -0
  60. data/lib/tb_checkout/configuration.rb +8 -0
  61. data/lib/tb_checkout/engine.rb +36 -0
  62. data/lib/tb_checkout/schema.rb +51 -0
  63. data/lib/tb_checkout/version.rb +3 -0
  64. data/spec/controllers/tb_checkout/carts_controller_spec.rb +11 -0
  65. data/spec/controllers/tb_checkout/transactions_controller_spec.rb +110 -0
  66. data/spec/dummy/README.rdoc +28 -0
  67. data/spec/dummy/Rakefile +6 -0
  68. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  69. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  70. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  71. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  72. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  73. data/spec/dummy/bin/bundle +3 -0
  74. data/spec/dummy/bin/rails +4 -0
  75. data/spec/dummy/bin/rake +4 -0
  76. data/spec/dummy/config.ru +4 -0
  77. data/spec/dummy/config/application.rb +30 -0
  78. data/spec/dummy/config/boot.rb +5 -0
  79. data/spec/dummy/config/database.yml +13 -0
  80. data/spec/dummy/config/environment.rb +5 -0
  81. data/spec/dummy/config/environments/development.rb +37 -0
  82. data/spec/dummy/config/environments/production.rb +82 -0
  83. data/spec/dummy/config/environments/test.rb +39 -0
  84. data/spec/dummy/config/initializers/assets.rb +8 -0
  85. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  86. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  87. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  88. data/spec/dummy/config/initializers/inflections.rb +16 -0
  89. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  90. data/spec/dummy/config/initializers/session_store.rb +3 -0
  91. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  92. data/spec/dummy/config/locales/en.yml +23 -0
  93. data/spec/dummy/config/routes.rb +4 -0
  94. data/spec/dummy/config/secrets.yml +22 -0
  95. data/spec/dummy/db/migrate/20140915202259_create_spud_admin_permissions.tb_core.rb +12 -0
  96. data/spec/dummy/db/migrate/20140915202260_create_spud_users.tb_core.rb +30 -0
  97. data/spec/dummy/db/migrate/20140915202261_add_time_zone_to_spud_user.tb_core.rb +7 -0
  98. data/spec/dummy/db/migrate/20140915202262_add_scope_to_spud_admin_permissions.tb_core.rb +7 -0
  99. data/spec/dummy/db/migrate/20140915202263_create_spud_user_settings.tb_core.rb +12 -0
  100. data/spec/dummy/db/migrate/20140915202264_create_spud_roles.tb_core.rb +11 -0
  101. data/spec/dummy/db/migrate/20140915202265_create_spud_permissions.tb_core.rb +11 -0
  102. data/spec/dummy/db/migrate/20140915202266_create_spud_role_permissions.tb_core.rb +12 -0
  103. data/spec/dummy/db/migrate/20140915202267_drop_spud_admin_permissions.tb_core.rb +16 -0
  104. data/spec/dummy/db/migrate/20140915202268_create_tb_checkout_carts.tb_checkout_engine.rb +13 -0
  105. data/spec/dummy/db/migrate/20140915202269_create_tb_checkout_cart_items.tb_checkout_engine.rb +16 -0
  106. data/spec/dummy/db/migrate/20140915202270_create_tb_checkout_transactions.tb_checkout_engine.rb +24 -0
  107. data/spec/dummy/db/migrate/20140915202271_create_tb_checkout_basic_products.tb_checkout_engine.rb +9 -0
  108. data/spec/dummy/db/migrate/20140915234534_add_spud_user_and_session_id_to_transactions.tb_checkout_engine.rb +11 -0
  109. data/spec/dummy/db/schema.rb +135 -0
  110. data/spec/dummy/public/404.html +67 -0
  111. data/spec/dummy/public/422.html +67 -0
  112. data/spec/dummy/public/500.html +66 -0
  113. data/spec/dummy/public/favicon.ico +0 -0
  114. data/spec/factories/spud_users.rb +16 -0
  115. data/spec/factories/tb_checkout_basic_products.rb +8 -0
  116. data/spec/factories/tb_checkout_cart_items.rb +11 -0
  117. data/spec/factories/tb_checkout_carts.rb +14 -0
  118. data/spec/factories/tb_checkout_transactions.rb +37 -0
  119. data/spec/models/tb_checkout/transaction_spec.rb +5 -0
  120. data/spec/rails_helper.rb +73 -0
  121. data/spec/spec_helper.rb +85 -0
  122. metadata +335 -0
@@ -0,0 +1,253 @@
1
+ module TbCheckout
2
+ class Transaction < ActiveRecord::Base
3
+ self.table_name = 'tb_checkout_transactions'
4
+ belongs_to :cart, :inverse_of => :transactions
5
+
6
+ attr_accessor :card_number
7
+ attr_accessor :card_ccv
8
+ attr_reader :card_expiration
9
+ self.columns_hash['card_expiration'] = OpenStruct.new(type: :date, klass: Date)
10
+
11
+ attr_reader :response
12
+
13
+ validates_presence_of :cart, :billing_first_name, :billing_last_name, :billing_address_1,
14
+ :billing_city, :billing_state, :billing_postal, :card_type
15
+ validates_numericality_of :amount_charged, :greater_than => 0
16
+ validate :credit_card_is_valid, :on => :create
17
+ before_validation :set_amount_charged
18
+ before_create :set_invoice_num, :set_spud_user_and_session_id
19
+
20
+ include TbCheckout::BelongsToUserSession
21
+
22
+ # Transaction status
23
+ #
24
+ # Status describes the current state of a transaction
25
+ #
26
+ # * Pending: The transaction is newly created
27
+ # * Authorized: The transaction has been submitted to the payment gateway and successfully authorized
28
+ # * Captured: The transaction has been submitted to the payment gateway and the purchase is finalized
29
+ # * Fail: A failure code was received from the payment gateway
30
+ # * Voided: The transaction is voided in the gateway
31
+ # * Refunded: The transaction is refunded in the gateway
32
+ module Status
33
+ PENDING = 'pending'
34
+ AUTHORIZED = 'authorized'
35
+ CAPTURED = 'captured'
36
+ FAIL = 'fail'
37
+ VOIDED = 'voided'
38
+ REFUNDED = 'refunded'
39
+ end
40
+
41
+ # Scopes
42
+ default_scope { order('created_at desc') }
43
+ scope :pending, ->{ where(:status => Status::PENDING) }
44
+ scope :authorized, ->{ where(:status => Status::AUTHORIZED) }
45
+ scope :captured, ->{ where(:status => Status::CAPTURED) }
46
+ scope :failed, ->{ where(:status => Status::FAIL) }
47
+
48
+ # Authorize the transaction against the payment gateway
49
+ #
50
+ # * The transaction must be in PENDING state in order ot take this action
51
+ def authorize!
52
+ self.with_lock(true) do
53
+ if self.status != Status::PENDING
54
+ raise StandardError, 'Payment must be in Pending state before authorization'
55
+ end
56
+ @response = TbCheckout.gateway.authorize(amount_in_cents, build_credit_card(), build_purchase_options())
57
+ if @response.success?
58
+ self.update_columns(:status => Status::AUTHORIZED, :gateway_transaction_id => @response.params['transaction_id'], :response_text => @response.to_json)
59
+ return true
60
+ else
61
+ self.update_columns(:status => Status::FAIL, :response_text => @response.to_json)
62
+ logger.fatal @response.inspect
63
+ return false
64
+ end
65
+ end
66
+ end
67
+
68
+ # Capture the funds from an authorized transaction through the payment gateway
69
+ #
70
+ # * The transaction must be in AUTHORIZED state in order ot take this action
71
+ def capture!
72
+ self.with_lock(true) do
73
+ if self.status != Status::AUTHORIZED
74
+ raise StandardError, 'Payment must be in Authorized state before capture'
75
+ end
76
+ @response = TbCheckout.gateway.capture(amount_in_cents, self.gateway_transaction_id)
77
+ if @response.success? || @response.params['action'] == 'PRIOR_AUTH_CAPTURE'
78
+ self.update_columns(:status => Status::CAPTURED, :response_text => @response.to_json)
79
+ else
80
+ self.update_columns(:status => Status::FAIL, :response_text => @response.to_json)
81
+ logger.fatal @response.inspect
82
+ end
83
+ end
84
+ if self.status == Status::CAPTURED
85
+ self.cart.update_attributes(:is_completed => true)
86
+ self.cart.cart_items.each do |cart_item|
87
+ cart_item.item.run_callbacks(:capture)
88
+ end
89
+ return true
90
+ else
91
+ return false
92
+ end
93
+ end
94
+
95
+ # Voids or Refunds the transaction
96
+ #
97
+ # * An unsettled trasanction cannot be refunded, and a settled transaction cannot be voided
98
+ # * Transactions are settled nightly
99
+ # * If the transaction is less than 24 hours old, attempt to void it and if that fails attempt a refund
100
+ def void_or_refund!
101
+ if (1.days.ago < self.created_at && void!) || refund!
102
+ return true
103
+ else
104
+ return false
105
+ end
106
+ end
107
+
108
+ def amount_in_cents
109
+ return amount_charged * 100
110
+ end
111
+
112
+ def billing_full_name
113
+ return "#{billing_first_name} #{billing_last_name}"
114
+ end
115
+
116
+ def billing_full_address
117
+ street = [billing_address_1, billing_address_2].reject(&:blank?).join(', ')
118
+ return "#{street}, #{billing_city} #{billing_state}, #{billing_postal}"
119
+ end
120
+
121
+ # Check for card_expiration value in String format and convert to Date format
122
+ #
123
+ def card_expiration=(date)
124
+ if date.is_a? String
125
+ begin
126
+ @card_expiration = Date.parse(date)
127
+ rescue ArgumentError => e
128
+ @card_expiration = nil
129
+ end
130
+ else
131
+ @card_expiration = date
132
+ end
133
+ end
134
+
135
+ private
136
+
137
+ def void!
138
+ self.with_lock(true) do
139
+ if self.status != Status::CAPTURED
140
+ raise StandardError, 'Payment must be in captured state before it can be voided'
141
+ end
142
+ @response = TbCheckout.gateway.void(gateway_transaction_id)
143
+ if @response.success?
144
+ self.update_columns(:status => Status::VOIDED, :response_text => @response.to_json)
145
+ return true
146
+ else
147
+ self.update_columns(:response_text => @response.to_json)
148
+ logger.fatal @response.inspect
149
+ return false
150
+ end
151
+ end
152
+ end
153
+
154
+ def refund!
155
+ self.with_lock(true) do
156
+ if self.status != Status::CAPTURED
157
+ raise StandardError, 'Payment must be in captured state before it can be voided'
158
+ end
159
+ cc_last_four_digits = self.card_display.chars.last(4).join()
160
+ @response = TbCheckout.gateway.refund(amount_in_cents, self.gateway_transaction_id, {
161
+ :order_id => invoice_num,
162
+ :description => "REFUND: #{self.cart.description}",
163
+ :card_number => cc_last_four_digits,
164
+ :first_name => billing_first_name,
165
+ :last_name => billing_last_name,
166
+ :zip => billing_postal
167
+ })
168
+ if @response.success? || @response.params['response_reason_code'] == '55' # already refunded
169
+ self.update_columns(:status => Status::REFUNDED, :response_text => @response.to_json, :refund_gateway_transaction_id => @response.params['gateway_transaction_id'])
170
+ return true
171
+ else
172
+ self.update_columns(:response_text => @response.to_json)
173
+ logger.fatal @response.inspect
174
+ return false
175
+ end
176
+ end
177
+ end
178
+
179
+ def credit_card_is_valid
180
+ if self.card_number.blank?
181
+ errors.add(:card_number, 'cannot be blank')
182
+ end
183
+ if self.card_ccv.blank?
184
+ errors.add(:card_ccv, 'cannot be blank')
185
+ end
186
+ if errors.size == 0
187
+ credit_card = build_credit_card()
188
+ if credit_card.valid?()
189
+ self.card_display = credit_card.display_number()
190
+ else
191
+ credit_card.errors.full_messages.each do |msg|
192
+ errors.add(:base, msg)
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ def build_credit_card
199
+ return ActiveMerchant::Billing::CreditCard.new({
200
+ :brand => card_type,
201
+ :number => card_number,
202
+ :verification_value => card_ccv,
203
+ :month => card_expiration.month,
204
+ :year => card_expiration.year,
205
+ :first_name => billing_first_name,
206
+ :last_name => billing_last_name
207
+ })
208
+ end
209
+
210
+ def build_purchase_options
211
+ return {
212
+ :email => self.cart.spud_user.try(:email) || nil,
213
+ :order_id => self.invoice_num,
214
+ :description => self.cart.description,
215
+ :billing_address => {
216
+ :name => billing_full_name,
217
+ :address1 => billing_address_1,
218
+ :address2 => billing_address_2,
219
+ :city => billing_city,
220
+ :state => billing_state,
221
+ :country => country_for_billing_state(),
222
+ :zip => billing_postal,
223
+ :phone => self.cart.spud_user.try(:phone) || nil
224
+ }
225
+ }
226
+ end
227
+
228
+ def set_amount_charged
229
+ if self.cart
230
+ self.amount_charged = self.cart.total_price
231
+ end
232
+ end
233
+
234
+ def set_invoice_num
235
+ user_id = self.cart.spud_user_id || 0
236
+ self.invoice_num = [user_id, Time.now.to_i].join('-')
237
+ end
238
+
239
+ def set_spud_user_and_session_id
240
+ self.spud_user_id = self.cart.spud_user_id || 0
241
+ self.session_id = self.cart.session_id || 0
242
+ end
243
+
244
+ def country_for_billing_state
245
+ if ['AB', 'BC', 'MB', 'NB', 'NL', 'NS', 'NT', 'NU', 'ON', 'PE', 'QC', 'SK', 'YT'].include?(billing_state)
246
+ return 'Canada'
247
+ else
248
+ return 'USA'
249
+ end
250
+ end
251
+
252
+ end
253
+ end
@@ -0,0 +1,54 @@
1
+ <% content_for :data_controls do %>
2
+ <%= form_tag tb_checkout_admin_carts_path, :method => :get, :class => 'tb-checkout-cart-filter-form' do %>
3
+ <%= select_tag :status, options_for_select([
4
+ ['All', 'all'],
5
+ ['Complete', 'completed'],
6
+ ['In Progress', 'in-progress'],
7
+ ['Abandoned', 'abandoned']
8
+ ], params[:status]) %>
9
+ <% end %>
10
+ <% end %>
11
+
12
+ <% content_for :detail do %>
13
+ <table class="table table-striped">
14
+ <thead>
15
+ <th>User</th>
16
+ <th>Description</th>
17
+ <th>Total Price</th>
18
+ <th>Status</th>
19
+ <th>Last Modified</th>
20
+ <th></th>
21
+ </thead>
22
+ <tbody>
23
+ <% @carts.each do |cart| %>
24
+ <tr>
25
+ <td class="no-wrap"><%= cart.user_full_name %></td>
26
+ <td><%= cart.description %></td>
27
+ <td>
28
+ <% if cart.is_empty? %>
29
+ Empty
30
+ <% else %>
31
+ <%= number_to_currency cart.total_price %>
32
+ <% end %>
33
+ </td>
34
+ <td>
35
+ <%= tb_checkout_status_label_for_cart(cart) %>
36
+ </td>
37
+ <td class="no-wrap"><%= cart.updated_at.strftime('%I:%M %p, %D') %></td>
38
+ <td>
39
+ <%= link_to 'Detail', tb_checkout_admin_cart_path(cart, :status => params[:status]), :class => 'btn btn-mini' %>
40
+ </td>
41
+ </tr>
42
+ <% end %>
43
+ </tbody>
44
+ </table>
45
+ <%= will_paginate @carts %>
46
+
47
+ <script>
48
+ $(document).ready(function(){
49
+ $('.tb-checkout-cart-filter-form').on('change', 'select', function(e){
50
+ $(this).parents('form').submit();
51
+ });
52
+ });
53
+ </script>
54
+ <% end %>
@@ -0,0 +1,45 @@
1
+ <h3>General Info</h3>
2
+
3
+ <ul>
4
+ <li><strong>User</strong> <%= @cart.user_full_name %></li>
5
+ <li><strong>Created At</strong> <%= @cart.created_at.strftime('%I:%M %p, %D') %></li>
6
+ <li><strong>Last Modified</strong> <%= @cart.updated_at.strftime('%I:%M %p, %D') %></li>
7
+ <li><strong>Status</strong> <%= tb_checkout_status_label_for_cart(@cart) %></li>
8
+ </ul>
9
+
10
+ <h3>Products</h3>
11
+
12
+ <% if @cart.is_empty? %>
13
+ <p>This cart does not contain any items.</p>
14
+ <% else %>
15
+ <table class="table table-striped">
16
+ <thead>
17
+ <th>Product</th>
18
+ <th>Quantity</th>
19
+ <th>Price</th>
20
+ </thead>
21
+ <tbody>
22
+ <% @cart.cart_items.each do |cart_item| %>
23
+ <tr>
24
+ <td><%= cart_item.item_description %></td>
25
+ <td><%= cart_item.quantity %></td>
26
+ <td><%= number_to_currency cart_item.total_price %></td>
27
+ </tr>
28
+ <% end %>
29
+ </tbody>
30
+ <tfoot>
31
+ <tr>
32
+ <td colspan="2">Total</td>
33
+ <td><%= number_to_currency @cart.total_price %></td>
34
+ </tr>
35
+ </tfoot>
36
+ </table>
37
+ <% end %>
38
+
39
+ <h3>Transactions</h3>
40
+
41
+ <% if @cart.transactions.blank? %>
42
+ <p>There are no transactions associated with this shopping cart.</p>
43
+ <% else %>
44
+ <%= render :partial => '/tb_checkout/admin/transactions/table', :locals => {:transactions => @cart.transactions} %>
45
+ <% end %>
@@ -0,0 +1,24 @@
1
+ <table class="table table-striped">
2
+ <thead>
3
+ <th>Invoice Number</th>
4
+ <th>Transaction ID</th>
5
+ <th>Billing Name</th>
6
+ <th>Card</th>
7
+ <th>Amount Charged</th>
8
+ <th>Status</th>
9
+ <th></th>
10
+ </thead>
11
+ <tbody>
12
+ <% transactions.each do |transaction| %>
13
+ <tr>
14
+ <td><%= transaction.invoice_num %></td>
15
+ <td><%= transaction.gateway_transaction_id %></td>
16
+ <td class="no-wrap"><%= transaction.billing_full_name %></td>
17
+ <td class="no-wrap"><%= transaction.card_display %> (<%= transaction.card_type %>)</td>
18
+ <td><%= number_to_currency transaction.amount_charged %></td>
19
+ <td><%= tb_checkout_status_label_for_transaction(transaction.status) %></td>
20
+ <td><%= link_to 'Detail', tb_checkout_admin_transaction_path(transaction), :class => 'btn btn-mini' %></td>
21
+ </tr>
22
+ <% end %>
23
+ </tbody>
24
+ </table>
@@ -0,0 +1,7 @@
1
+ <% content_for :data_controls do %>
2
+
3
+ <% end %>
4
+
5
+ <% content_for :detail do %>
6
+ <%= render :partial => 'table', :locals => {:transactions => @transactions} %>
7
+ <% end %>
@@ -0,0 +1,70 @@
1
+ <h3>Summary</h3>
2
+ <table class="table table-bordered">
3
+ <thead>
4
+ <tr>
5
+ <th>User</th>
6
+ <th>Invoice Number</th>
7
+ <th>Transaction ID</th>
8
+ <th>Card</th>
9
+ <th>Amount Charged</th>
10
+ <th>Status</th>
11
+ </tr>
12
+ </thead>
13
+ <tbody>
14
+ <td><%= @transaction.cart.user_full_name %></td>
15
+ <td><%= @transaction.invoice_num %></td>
16
+ <td><%= @transaction.gateway_transaction_id %></td>
17
+ <td><%= @transaction.card_display %> (<%= @transaction.card_type %>)</td>
18
+ <td><%= number_to_currency @transaction.amount_charged %></td>
19
+ <td><%= tb_checkout_status_label_for_transaction(@transaction.status) %></td>
20
+ </tbody>
21
+ </table>
22
+
23
+ <h3>Billing Address</h3>
24
+ <table class="table table-bordered">
25
+ <thead>
26
+ <tr>
27
+ <th>Name</th>
28
+ <th>Address</th>
29
+ <th>Address 2</th>
30
+ <th>City</th>
31
+ <th>State</th>
32
+ <th>Postal</th>
33
+ </tr>
34
+ </thead>
35
+ <tbody>
36
+ <tr>
37
+ <td><%= @transaction.billing_full_name %></td>
38
+ <td><%= @transaction.billing_address_1 %></td>
39
+ <td><%= @transaction.billing_address_2 || "--" %></td>
40
+ <td><%= @transaction.billing_city %></td>
41
+ <td><%= @transaction.billing_state %></td>
42
+ <td><%= @transaction.billing_postal %></td>
43
+ </tr>
44
+ </table>
45
+
46
+ <h3>Line Items</h3>
47
+ <table class="table table-bordered">
48
+ <thead>
49
+ <tr>
50
+ <th>Product</th>
51
+ <th>Quantity</th>
52
+ <th>Price</th>
53
+ </tr>
54
+ </thead>
55
+ <tbody>
56
+ <% @transaction.cart.cart_items.each do |cart_item| %>
57
+ <tr>
58
+ <td><%= cart_item.item_description %></td>
59
+ <td><%= cart_item.quantity %></td>
60
+ <td><%= number_to_currency cart_item.total_price %></td>
61
+ </tr>
62
+ <% end %>
63
+ </tbody>
64
+ <tfoot>
65
+ <tr>
66
+ <td colspan="2">Total</td>
67
+ <td><%= number_to_currency @transaction.cart.total_price %></td>
68
+ </tr>
69
+ </tfoot>
70
+ </table>