stall 0.2.0 → 0.3.1

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 (140) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +60 -23
  3. data/app/assets/javascripts/para/stall.coffee +1 -0
  4. data/app/assets/javascripts/para/stall/inputs/variant-select.coffee +62 -0
  5. data/app/assets/javascripts/para/stall/inputs/variants-matrix.coffee +12 -0
  6. data/app/assets/javascripts/para/stall/inputs/variants-matrix/helpers.coffee +40 -0
  7. data/app/assets/javascripts/para/stall/inputs/variants-matrix/input.coffee +133 -0
  8. data/app/assets/javascripts/para/stall/inputs/variants-matrix/nested-fields.coffee +38 -0
  9. data/app/assets/javascripts/para/stall/inputs/variants-matrix/properties_select.coffee +45 -0
  10. data/app/assets/javascripts/para/stall/inputs/variants-matrix/variant.coffee +59 -0
  11. data/app/assets/javascripts/stall.coffee +1 -0
  12. data/app/assets/javascripts/stall/add-to-cart-form.coffee +53 -28
  13. data/app/assets/javascripts/stall/cart-form.coffee +7 -2
  14. data/app/assets/stylesheets/para/stall.sass +28 -0
  15. data/app/controllers/para/stall/admin/carts_controller.rb +27 -0
  16. data/app/controllers/stall/cart_credits_controller.rb +27 -0
  17. data/app/controllers/stall/carts_controller.rb +1 -1
  18. data/app/controllers/stall/checkout/steps_controller.rb +22 -12
  19. data/app/controllers/stall/checkouts_controller.rb +1 -0
  20. data/app/controllers/stall/line_items_controller.rb +1 -0
  21. data/app/controllers/stall/payments_controller.rb +16 -1
  22. data/app/helpers/stall/credit_notes_helper.rb +25 -0
  23. data/app/helpers/stall/customers_helper.rb +3 -1
  24. data/app/models/billing_address.rb +2 -0
  25. data/app/models/cart_credit_note_adjustment.rb +3 -0
  26. data/app/models/credit_note.rb +3 -0
  27. data/app/models/credit_note_adjustment.rb +3 -0
  28. data/app/models/credit_note_usage.rb +3 -0
  29. data/app/models/product.rb +3 -0
  30. data/app/models/product_category.rb +3 -0
  31. data/app/models/product_detail.rb +3 -0
  32. data/app/models/property.rb +3 -0
  33. data/app/models/property_value.rb +3 -0
  34. data/app/models/shipping_address.rb +2 -0
  35. data/app/models/stall/models/address.rb +2 -2
  36. data/app/models/stall/models/cart.rb +2 -15
  37. data/app/models/stall/models/cart_credit_note_adjustment.rb +11 -0
  38. data/app/models/stall/models/credit_note.rb +50 -0
  39. data/app/models/stall/models/credit_note_adjustment.rb +13 -0
  40. data/app/models/stall/models/credit_note_usage.rb +16 -0
  41. data/app/models/stall/models/customer.rb +20 -8
  42. data/app/models/stall/models/line_item.rb +9 -0
  43. data/app/models/stall/models/product.rb +45 -0
  44. data/app/models/stall/models/product_category.rb +31 -0
  45. data/app/models/stall/models/product_detail.rb +17 -0
  46. data/app/models/stall/models/product_list.rb +9 -45
  47. data/app/models/stall/models/property.rb +20 -0
  48. data/app/models/stall/models/property_value.rb +23 -0
  49. data/app/models/stall/models/variant.rb +34 -0
  50. data/app/models/stall/models/variant_property_value.rb +18 -0
  51. data/app/models/variant.rb +3 -0
  52. data/app/models/variant_property_value.rb +3 -0
  53. data/app/services/stall/cart_credit_note_creation_service.rb +40 -0
  54. data/app/services/stall/cart_payment_validation_service.rb +28 -0
  55. data/app/services/stall/cart_update_service.rb +17 -6
  56. data/app/services/stall/credit_usage_service.rb +102 -0
  57. data/app/services/stall/payment_notification_service.rb +4 -8
  58. data/app/services/stall/product_list_staleness_handling_service.rb +33 -0
  59. data/app/views/admin/addresses/_fields.html.haml +9 -0
  60. data/app/views/admin/carts/_filters.html.haml +17 -0
  61. data/app/views/admin/carts/_form.html.haml +43 -0
  62. data/app/views/admin/carts/_table.html.haml +17 -0
  63. data/app/views/admin/customers/_fields.html.haml +1 -0
  64. data/app/views/admin/line_items/_fields.html.haml +15 -0
  65. data/app/views/admin/products/_table.html.haml +12 -0
  66. data/app/views/admin/properties/_form.html.haml +6 -0
  67. data/app/views/admin/properties/_table.html.haml +8 -0
  68. data/app/views/admin/property_values/_fields.html.haml +1 -0
  69. data/app/views/admin/shipments/_fields.html.haml +7 -0
  70. data/app/views/checkout/steps/_informations.html.haml +3 -1
  71. data/app/views/checkout/steps/_payment.html.haml +2 -0
  72. data/app/views/checkout/steps/_payment_return.html.haml +0 -1
  73. data/app/views/para/admin/resources/_variant_row.html.haml +24 -0
  74. data/app/views/para/stall/inputs/_variant_select.html.haml +14 -0
  75. data/app/views/para/stall/inputs/_variants_matrix.html.haml +41 -0
  76. data/app/views/stall/addresses/_fields.html.haml +6 -12
  77. data/app/views/stall/carts/_cart.html.haml +45 -37
  78. data/app/views/stall/carts/_widget.html.haml +28 -0
  79. data/app/views/stall/carts/show.html.haml +2 -0
  80. data/app/views/stall/checkout/steps/_navigation.html.haml +13 -0
  81. data/app/views/stall/credit_note_adjustments/_form.html.haml +28 -0
  82. data/app/views/stall/line_items/_added.html.haml +2 -2
  83. data/app/views/stall/line_items/_form.html.haml +1 -1
  84. data/app/views/stall/payments/manual_payment_gateway/_form.html.haml +10 -0
  85. data/app/views/stall/shared/mailers/_cart.html.haml +1 -1
  86. data/config/locales/stall.fr.yml +82 -2
  87. data/db/migrate/20161129101956_add_type_to_stall_address_ownerships.rb +52 -0
  88. data/db/migrate/20161202080218_add_reference_to_product_lists.rb +17 -0
  89. data/db/migrate/20170118103916_create_credit_notes.rb +17 -0
  90. data/db/migrate/20170118144047_create_credit_note_adjustments.rb +13 -0
  91. data/db/migrate/20170123123115_create_stall_product_categories.rb +12 -0
  92. data/db/migrate/20170123123326_create_stall_products.rb +17 -0
  93. data/db/migrate/20170123125030_create_stall_variants.rb +13 -0
  94. data/db/migrate/20170123131748_create_stall_product_category_hierarchies.rb +16 -0
  95. data/db/migrate/20170123143704_create_stall_product_details.rb +14 -0
  96. data/db/migrate/20170125152622_convert_all_money_fields_to_decimal_to_use_infinite_precision.rb +27 -0
  97. data/db/migrate/20170131162537_add_data_to_stall_adjustments.rb +5 -0
  98. data/db/migrate/20170202165514_create_stall_properties.rb +9 -0
  99. data/db/migrate/20170202165516_create_stall_property_values.rb +13 -0
  100. data/db/migrate/20170202165518_create_stall_variant_property_values.rb +13 -0
  101. data/lib/generators/stall/install/templates/initializer.rb +21 -0
  102. data/lib/generators/stall/view/view_generator.rb +41 -19
  103. data/lib/para/stall.rb +32 -0
  104. data/lib/para/stall/inputs.rb +13 -0
  105. data/lib/para/stall/inputs/variant_input_helper.rb +34 -0
  106. data/lib/para/stall/inputs/variant_select_input.rb +79 -0
  107. data/lib/para/stall/inputs/variants_matrix_input.rb +72 -0
  108. data/lib/para/stall/routes.rb +11 -0
  109. data/lib/para/stall/variants_property_config.rb +78 -0
  110. data/lib/stall.rb +10 -0
  111. data/lib/stall/addressable.rb +11 -59
  112. data/lib/stall/addresses.rb +1 -0
  113. data/lib/stall/addresses/copier_base.rb +3 -1
  114. data/lib/stall/addresses/copy.rb +10 -0
  115. data/lib/stall/addresses/copy_source_to_target.rb +10 -24
  116. data/lib/stall/addresses/prefill_target_from_source.rb +8 -16
  117. data/lib/stall/adjustable.rb +20 -0
  118. data/lib/stall/archived_paid_cart_helper.rb +36 -0
  119. data/lib/stall/cart_helper.rb +15 -7
  120. data/lib/stall/checkout/informations_checkout_step.rb +47 -50
  121. data/lib/stall/checkout/payment_return_checkout_step.rb +4 -1
  122. data/lib/stall/checkout/step.rb +24 -3
  123. data/lib/stall/checkout/step_form.rb +11 -5
  124. data/lib/stall/checkout/wizard.rb +7 -6
  125. data/lib/stall/config.rb +9 -0
  126. data/lib/stall/default_currency_manager.rb +27 -0
  127. data/lib/stall/engine.rb +14 -3
  128. data/lib/stall/payments.rb +2 -0
  129. data/lib/stall/payments/gateway_request.rb +15 -0
  130. data/lib/stall/payments/gateway_response.rb +33 -0
  131. data/lib/stall/payments/manual_payment_gateway.rb +86 -0
  132. data/lib/stall/priceable.rb +4 -0
  133. data/lib/stall/reference_manager.rb +17 -0
  134. data/lib/stall/routes.rb +1 -0
  135. data/lib/stall/shippable.rb +18 -0
  136. data/lib/stall/total_prices_manager.rb +40 -0
  137. data/lib/stall/version.rb +1 -1
  138. metadata +120 -5
  139. data/app/models/address_ownership.rb +0 -3
  140. data/app/models/stall/models/address_ownership.rb +0 -26
@@ -0,0 +1,20 @@
1
+ module Stall
2
+ module Models
3
+ module Property
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ self.table_name = 'stall_properties'
8
+
9
+ has_many :property_values, dependent: :destroy,
10
+ inverse_of: :property
11
+ accepts_nested_attributes_for :property_values, allow_destroy: true
12
+
13
+ has_many :variant_property_values, through: :property_values
14
+ has_many :variants, through: :variant_property_values
15
+
16
+ validates :name, presence: true
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ module Stall
2
+ module Models
3
+ module PropertyValue
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ self.table_name = 'stall_property_values'
8
+
9
+ acts_as_orderable
10
+
11
+ belongs_to :property
12
+
13
+ has_many :variant_property_values, dependent: :destroy,
14
+ inverse_of: :property_value
15
+ has_many :variants, through: :variant_property_values
16
+
17
+ validates :property, :value, presence: true
18
+
19
+ alias_attribute :name, :value
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,34 @@
1
+ module Stall
2
+ module Models
3
+ module Variant
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ self.table_name = 'stall_variants'
8
+
9
+ include Stall::Sellable
10
+
11
+ belongs_to :product
12
+
13
+ has_many :variant_property_values, dependent: :destroy,
14
+ inverse_of: :variant
15
+ accepts_nested_attributes_for :variant_property_values,
16
+ allow_destroy: true,
17
+ reject_if: :all_blank
18
+
19
+ has_many :property_values, through: :variant_property_values
20
+ has_many :properties, through: :property_values
21
+
22
+ monetize :price_cents, with_model_currency: :currency, allow_nil: false
23
+
24
+ delegate :name, :image, :image?, :vat_rate, to: :product, allow_nil: true
25
+
26
+ scope :published, -> { where(published: true) }
27
+
28
+ def currency
29
+ @currency ||= Money::Currency.new(Stall.config.default_currency)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,18 @@
1
+ module Stall
2
+ module Models
3
+ module VariantPropertyValue
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ self.table_name = 'stall_variant_property_values'
8
+
9
+ belongs_to :property_value
10
+ has_one :property, through: :property_value
11
+
12
+ belongs_to :variant
13
+
14
+ validates :property_value, :variant, presence: true
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ class Variant < ActiveRecord::Base
2
+ include Stall::Models::Variant
3
+ end
@@ -0,0 +1,3 @@
1
+ class VariantPropertyValue < ActiveRecord::Base
2
+ include Stall::Models::VariantPropertyValue
3
+ end
@@ -0,0 +1,40 @@
1
+ module Stall
2
+ class CartCreditNoteCreationService < Stall::BaseService
3
+ attr_reader :cart
4
+
5
+ def initialize(cart)
6
+ @cart = cart
7
+ end
8
+
9
+ def call
10
+ if cart.remainder? && !adjustment_exists?
11
+ credit_note = create_credit_note!
12
+ create_adjustment_for!(credit_note)
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def adjustment_exists?
19
+ cart.adjustments.any? { |adj| CartCreditNoteAdjustment === adj }
20
+ end
21
+
22
+ def create_credit_note!
23
+ cart.customer.credit_notes.create!(amount: cart.remainder)
24
+ end
25
+
26
+ def create_adjustment_for!(credit_note)
27
+ name = I18n.t(
28
+ 'stall.credit_notes.source_cart_adjustment_label',
29
+ ref: credit_note.reference
30
+ )
31
+
32
+ cart.adjustments.create!(
33
+ type: 'CartCreditNoteAdjustment',
34
+ name: name,
35
+ price: credit_note.amount,
36
+ credit_note: credit_note
37
+ )
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,28 @@
1
+ module Stall
2
+ class CartPaymentValidationService < Stall::BaseService
3
+ attr_reader :cart
4
+
5
+ def initialize(cart)
6
+ @cart = cart
7
+ end
8
+
9
+ def call
10
+ cart.payment.pay!
11
+ send_payment_notification_emails!
12
+ create_credit_notes!
13
+ end
14
+
15
+ private
16
+
17
+ def send_payment_notification_emails!
18
+ Stall::CustomerMailer.order_paid_email(cart).deliver
19
+ Stall::AdminMailer.order_paid_email(cart).deliver
20
+ end
21
+
22
+ def create_credit_notes!
23
+ if cart.remainder?
24
+ Stall.config.service_for(:cart_credit_note_creation).new(cart).call
25
+ end
26
+ end
27
+ end
28
+ end
@@ -2,7 +2,7 @@ module Stall
2
2
  class CartUpdateService < Stall::BaseService
3
3
  attr_reader :cart, :params
4
4
 
5
- def initialize(cart, params)
5
+ def initialize(cart, params = {})
6
6
  @cart = cart
7
7
  @params = params
8
8
  end
@@ -10,17 +10,28 @@ module Stall
10
10
  def call
11
11
  cart.update(params).tap do |saved|
12
12
  return false unless saved
13
-
14
- # Recalculate shipping fee if available for calculation to ensure
15
- # that the fee us always up to date when displayed to the customer
16
- shipping_fee_service.call if shipping_fee_service.available?
13
+ refresh_associated_services!
17
14
  end
18
15
  end
19
16
 
17
+ def refresh_associated_services!
18
+ # Recalculate shipping fee if available for calculation to ensure
19
+ # that the fee us always up to date when displayed to the customer
20
+ shipping_fee_service.call if shipping_fee_service.available?
21
+
22
+ # Recalculate the credit usage amount if already used to avoid negative
23
+ # cart totals
24
+ credit_usage_service.ensure_valid_or_remove! if credit_usage_service.available? && credit_usage_service.credit_used?
25
+ end
26
+
20
27
  private
21
28
 
22
29
  def shipping_fee_service
23
- @shipping_fee_service ||= Stall::ShippingFeeCalculatorService.new(cart)
30
+ @shipping_fee_service ||= Stall.config.service_for(:shipping_fee_calculator).new(cart)
31
+ end
32
+
33
+ def credit_usage_service
34
+ @credit_usage_service ||= Stall.config.service_for(:credit_usage).new(cart)
24
35
  end
25
36
  end
26
37
  end
@@ -0,0 +1,102 @@
1
+ module Stall
2
+ class CreditUsageService < Stall::BaseService
3
+ attr_reader :cart, :params
4
+
5
+ def initialize(cart, params = {})
6
+ @cart = cart
7
+ @params = params
8
+ end
9
+
10
+ def call
11
+ return false unless enough_credit?
12
+
13
+ clean_credit_note_adjustments!
14
+
15
+ available_credit_notes.reduce(amount) do |missing_amount, credit_note|
16
+ break true if missing_amount.to_d == 0
17
+
18
+ used_amount = [credit_note.remaining_amount, missing_amount].min
19
+ add_adjustment(used_amount, credit_note)
20
+
21
+ missing_amount - used_amount
22
+ end
23
+ end
24
+
25
+ def amount
26
+ @amount ||= begin
27
+ amount = if params[:amount]
28
+ cents = BigDecimal.new(params[:amount]) * 100
29
+ Money.new(cents, cart.currency)
30
+ else
31
+ credit
32
+ end
33
+
34
+ [amount, cart.total_price].min
35
+ end
36
+ end
37
+
38
+ def credit
39
+ @credit ||= begin
40
+ credit = cart.customer.try(:credit, cart.currency) || Money.new(0, cart.currency)
41
+ Money.new(credit + credit_note_adjustments.map(&:price).sum.abs, cart.currency)
42
+ end
43
+ end
44
+
45
+ def enough_credit?
46
+ amount <= credit
47
+ end
48
+
49
+ def clean_credit_note_adjustments!
50
+ credit_note_adjustments.each do |adjustment|
51
+ cart.adjustments.destroy(adjustment)
52
+ end
53
+ end
54
+
55
+ # FIXME: Ducktyping ShippingFeeCalculatorService and use this method in
56
+ # CartUpdateService#refresh_associated_services! to test if credit notes exists
57
+ #
58
+ def available?
59
+ cart.respond_to?(:adjustments)
60
+ end
61
+
62
+ def credit_used?
63
+ credit_note_adjustments.any?
64
+ end
65
+
66
+ def credit_used
67
+ credit_note_adjustments.map(&:price).sum
68
+ end
69
+
70
+ def ensure_valid_or_remove!
71
+ if !enough_credit?
72
+ clean_credit_note_adjustments!
73
+ elsif cart.total_price.to_d < 0
74
+ @amount = credit_used
75
+ call
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def available_credit_notes
82
+ @available_credit_notes ||= cart.customer.credit_notes
83
+ .for_currency(cart.currency)
84
+ .select(&:with_remaining_money?)
85
+ end
86
+
87
+ def add_adjustment(amount, credit_note)
88
+ cart.adjustments.create!(
89
+ type: 'CreditNoteAdjustment',
90
+ name: I18n.t('stall.credit_notes.adjustment_label', ref: credit_note.reference),
91
+ price: -amount,
92
+ credit_note: credit_note
93
+ )
94
+ end
95
+
96
+ def credit_note_adjustments
97
+ @credit_note_adjustments ||= cart.adjustments.select do |adjustment|
98
+ CreditNoteAdjustment === adjustment
99
+ end
100
+ end
101
+ end
102
+ end
@@ -11,7 +11,7 @@ module Stall
11
11
 
12
12
  def call
13
13
  if gateway_response.valid?
14
- send_payment_notification_emails! if gateway_response.notify
14
+ validate_cart! if gateway_response.process
15
15
  else
16
16
  raise UnknownNotificationError,
17
17
  "The payment notification request does not seem to come from " +
@@ -25,8 +25,9 @@ module Stall
25
25
 
26
26
  private
27
27
 
28
- def cart
29
- gateway_response.cart
28
+ def validate_cart!
29
+ service = Stall.config.service_for(:cart_payment_validation)
30
+ service.new(gateway_response.cart).call
30
31
  end
31
32
 
32
33
  def gateway_class
@@ -36,10 +37,5 @@ module Stall
36
37
  def gateway_response
37
38
  @gateway_response ||= gateway_class.response(request)
38
39
  end
39
-
40
- def send_payment_notification_emails!
41
- Stall::CustomerMailer.order_paid_email(gateway_response.cart).deliver
42
- Stall::AdminMailer.order_paid_email(gateway_response.cart).deliver
43
- end
44
40
  end
45
41
  end
@@ -0,0 +1,33 @@
1
+ module Stall
2
+ class ProductListStalenessHandlingService < Stall::BaseService
3
+ attr_reader :cart
4
+
5
+ def initialize(cart)
6
+ @cart = cart
7
+ end
8
+
9
+ def call
10
+ handle_line_items
11
+ end
12
+
13
+ private
14
+
15
+ def handle_line_items
16
+ cart.line_items.each do |line_item|
17
+ if stale?(line_item)
18
+ cart.line_items.delete(line_item)
19
+ else
20
+ handle_valid_line_item(line_item)
21
+ end
22
+ end
23
+ end
24
+
25
+ def handle_valid_line_item line_item
26
+ # Override this method
27
+ end
28
+
29
+ def stale? line_item
30
+ line_item.sellable.nil? || !line_item.sellable.published?
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,9 @@
1
+ = form.input :civility
2
+ = form.input :first_name
3
+ = form.input :last_name
4
+ = form.input :address
5
+ = form.input :address_details
6
+ = form.input :zip
7
+ = form.input :city
8
+ = form.input :country
9
+ = form.input :phone
@@ -0,0 +1,17 @@
1
+ = search_form_for @q, builder: SimpleForm::FormBuilder, url: @component.path, html: { data: { :'filters-form' => true } } do |form|
2
+ .row
3
+ .col-md-3
4
+ = form.input_field :payment_paid_at_null, as: :selectize, collection: [{ text: t('activerecord.enums.payment.state.paid'), value: '0' }, { text: t('activerecord.enums.payment.state.pending'), value: '1' }], placeholder: @component.model.human_attribute_name(:payment)
5
+
6
+ .col-md-3
7
+ = form.input_field :state_eq, as: :selectize, collection: DefaultCheckoutWizard.steps.map { |step| { text: t("activerecord.enums.cart.state.#{ step }"), value: step } }, placeholder: @component.model.human_attribute_name(:state)
8
+
9
+ .col-md-4
10
+ .input-group
11
+ %span.input-group-addon
12
+ %i.fa.fa-search
13
+
14
+ = form.input_field :reference_or_customer_email_cont, as: :string, placeholder: t('para.shared.search'), class: 'form-control'
15
+
16
+ .col-md-2
17
+ = form.submit t('para.shared.search'), class: 'btn btn-default btn-block'
@@ -0,0 +1,43 @@
1
+ = para_form_for(resource) do |form|
2
+ = form.tabs do |tabs|
3
+ = tabs.tab :informations do
4
+ = form.input :reference
5
+ = form.input :total_price do
6
+ .input-group
7
+ = form.input_field :total_price, class: 'form-control', disabled: 'disabled'
8
+ %span.input-group-addon
9
+ = form.object.total_price.symbol
10
+
11
+ = form.actions
12
+
13
+ = tabs.tab :billing do
14
+ = form.input :customer, :as => :nested_one
15
+
16
+ = form.input :billing_address do
17
+ .nested-one-field
18
+ = form.simple_fields_for :billing_address, (form.object.billing_address || form.object.build_billing_address), nested_attribute_name: :billing_address do |nested_form|
19
+ = render partial: find_partial_for(Address, :fields), locals: { form: nested_form, parent: form.object }
20
+
21
+ = form.actions
22
+
23
+ = tabs.tab :shipping do
24
+ = form.input :shipment, :as => :nested_one
25
+
26
+ = form.input :shipping_address do
27
+ .nested-one-field
28
+ = form.simple_fields_for :shipping_address, (form.object.shipping_address || form.object.build_shipping_address), nested_attribute_name: :shipping_address do |nested_form|
29
+ = render partial: find_partial_for(Address, :fields), locals: { form: nested_form, parent: form.object }
30
+
31
+ = form.actions
32
+
33
+ = tabs.tab :payment do
34
+ = form.input :payment, :as => :nested_one
35
+
36
+ = form.actions
37
+
38
+ = tabs.tab :line_items do
39
+ = form.input :line_items, :as => :nested_many
40
+
41
+ = form.actions
42
+
43
+