flowcommerce_spree 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 679a3d268ba2e06caff2460bcfd86b671630de034567017f5a9b8f51a6c20d4b
4
- data.tar.gz: 6c95ee74d3783dbb01f1aa1ae9107b386b75de5de9f298d9ea65270ed5a43ffa
3
+ metadata.gz: 51a668ee3cc70d0bc9bd8c7662a972f086e06dcdcf4c68df2177e94818dfb820
4
+ data.tar.gz: aa9ff17e57cd19452736af11d51bd1905c4dd7e66bb461b5dff017fc5dea0b0d
5
5
  SHA512:
6
- metadata.gz: af6dba3c9673dbedf2b3d3672c9dc7a09375a7bc12581393ec4173df0cefe2157d2d4022bcf3b0ac0ceeedf87b91fd6e71558d296d1d848d60abac18de9de47a
7
- data.tar.gz: 9ad313a9ab18cb88bb9fe68dff643933bf2b940613ca1a11fd5014c51cfa26bf911443074254c719d393cb1671ef36819b3e292730d677622135bf94a464c83e
6
+ metadata.gz: 024a29a6fc47f932e432f2477f9a1f3b520af7cc10b7a85eb35613328276d5390a682187151217c0527b20435431f78e8cc55255d95298620bfb5077c7ed5011
7
+ data.tar.gz: b6d9485612e360ed50caabfab537baf9583feec384607b4a890c1ab1fd9fc48cd9ea3954562aac828f4a858682ad1515ae7f2b9bf05a8a501ff8abf96c28f972
data/README.md CHANGED
@@ -11,32 +11,36 @@ All flowcommerce_spree code is located in the ./app and ./lib folders.
11
11
  gem 'flowcommerce_spree', git: 'https://github.com/mejuri-inc/flowcommerce_spree'
12
12
  ```
13
13
 
14
- - If the main application's Rails version is less than 4.2, add also to main application's Gemfile the `activerecord
15
- -postgres-json` gem (the mejuri-inc fork allows using a recent Rake version:
14
+ - If the main application's Rails version is less than 4.2, add also to main application's Gemfile the `activerecord
15
+ -postgres-json` gem (at least version 0.2.3):
16
16
 
17
17
  ```
18
- gem 'activerecord-postgres-json', git: 'https://github.com/mejuri-inc/activerecord-postgres-json'
18
+ gem 'activerecord-postgres-json', '>= 0.2.3'
19
19
  ```
20
-
21
20
 
22
21
  - Run `bundle install`.
23
22
 
24
- - Define this additional ENV variables. You will find them in
23
+ - Define this additional ENV variables. You will find all of them, except FLOW_MOUNT_PATH in
25
24
  [Flow console](https://console.flow.io/org_account_name/organization/integrations):
26
25
 
27
26
  ```
28
27
  FLOW_TOKEN='SUPERsecretTOKEN' # API_KEY
29
28
  FLOW_ORGANIZATION='spree-app-sandbox'
30
29
  FLOW_BASE_COUNTRY='usa'
30
+ # The path to which the FlowcommerceSpree engine will be mounted (default, if this variable is missing, will be the
31
+ # '/flow' path)
32
+ FLOW_MOUNT_PATH='/flow'
31
33
  ```
32
34
 
33
- - The only piece of code that is needed to enable payments with the FlowCommerce engine
35
+ - To enable payments with the FlowCommerce engine, the payment method `flow.io` with `Spree::Gateway::FlowIo` should be
36
+ added in the Spree Admin. This payment method is automatically registered in the gem in an after_initialize Rails
37
+ engine callback:
34
38
 
35
39
  ```
36
- # config/application.rb
40
+ # lib/flowcommerce_spree/engine.rb
37
41
  config.after_initialize do |app|
38
42
  # init Flow payments as an option
39
- app.config.spree.payment_methods << Spree::Gateway::Flow
43
+ app.config.spree.payment_methods << Spree::Gateway::FlowIo
40
44
  end
41
45
  ```
42
46
 
@@ -80,7 +84,7 @@ Decorators are used extensively across the gem to modify or add behaviour of sev
80
84
  properly deal with the precedence in the Ruby ancestor chain, the `class_eval`, `include` and `prepend` methods are
81
85
  being used, depending on the level of modification.
82
86
 
83
- ### Spree::Flow::Gateway
87
+ ### Spree::Gateway::FlowIo
84
88
 
85
89
  Adapter for Spree, that allows using [Flow.io](https://www.flow.io) as payment gateway. Flow is PCI compliant payment processor.
86
90
 
data/SPREE_FLOW.md CHANGED
@@ -2,40 +2,19 @@
2
2
 
3
3
  Integration of Spree with Flow, how it is done.
4
4
 
5
- I plan to be concise as possible, but cover all important topics.
5
+ ## Installation
6
6
 
7
- ## Instalation
8
-
9
- Add the following lines to `./config/application.rb` :
10
-
11
- ```
12
- config.to_prepare do
13
- # add all flow libs
14
- overload = Dir.glob('./app/flow/**/*.rb')
15
- overload.reverse.each { |c| require(c) }
16
- end
17
-
18
- config.after_initialize do |app|
19
- # init Flow payments as an option
20
- app.config.spree.payment_methods << Spree::Gateway::Flow
21
- end
22
- ```
23
-
24
- Additional configuration could be adjusted in the gem's initializer. For example, the following file could be created in the main application:
7
+ Additional configuration could be adjusted in the gem's initializer. For example, the following file could be created
8
+ in the main application, which would add additional attributes of spree_variants to be imported/exported to flow.io:
25
9
 
26
10
  ```
27
11
  # ./config/initializers/flowcommerce_spree.rb
28
12
 
29
- FlowcommerceSpree.configure do |c|
30
- c.experience_associator = FlowcommerceSpree::ExperienceAssociator
31
- end
13
+ FlowcommerceSpree::Config.additional_attributes =
14
+ { spree_variants: { country_of_origin: { import: true, export: :optional },
15
+ customs_description: { import: true, export: :optional, export_name: 'materials' } } }
32
16
  ```
33
17
 
34
- ### Configurable settings
35
-
36
- 1. experience_associator - this attribute could be assigned, if necessary, a service object to perform some
37
- additional association actions when upserting a FlowcommerceSpree::Experience model
38
-
39
18
  ## Things to take into account
40
19
 
41
20
  ActiveMerchant is not supporting sessions and orders, natively. If one wants
@@ -131,4 +110,3 @@ By default Flow Admin (on /admin/flow) is anybody that is Spree admin.
131
110
 
132
111
  This way we provide good frontend info, some integration notes in realtime as opposed to running
133
112
  rake tests to check for integrity of Flow integration.
134
-
@@ -3,29 +3,52 @@
3
3
  CurrentZoneLoader.module_eval do
4
4
  extend ActiveSupport::Concern
5
5
 
6
- def flow_zone # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
6
+ def current_zone
7
+ return @current_zone if defined?(@current_zone)
8
+
9
+ @current_zone = if (session_region_name = session['region']&.[]('name'))
10
+ Spree::Zones::Product.find_by(name: session_region_name)
11
+ elsif request_iso_code.present?
12
+ @current_zone = flow_zone
13
+ @current_zone ||= Spree::Country.find_by(iso: request_iso_code)&.product_zones&.active&.first
14
+ end
15
+
16
+ @current_zone ||= Spree::Zones::Product.find_by(name: 'International') ||
17
+ Spree::Zones::Product.new(name: 'International', taxon_ids: [], currencies: %w[USD CAD])
18
+
19
+ current_zone_name = @current_zone.name
20
+ session['region'] = { name: current_zone_name, available_currencies: @current_zone.available_currencies,
21
+ request_iso_code: request_iso_code }
22
+
23
+ RequestStore.store[:session] = session
24
+ Rails.logger.debug("Using product zone: #{current_zone_name}")
25
+ @current_zone
26
+ end
27
+
28
+ def flow_zone # rubocop:disable Metrics/AbcSize
7
29
  return unless Spree::Zones::Product.active
8
30
  .where("meta -> 'flow_data' ->> 'country' = ?",
9
31
  ISO3166::Country[request_iso_code]&.alpha3).exists?
10
32
 
11
- request_ip =
12
- if Rails.env.production?
13
- request.ip
14
- else
15
- Spree::Config[:debug_request_ip_address] || request.ip
16
- # Germany ip: 85.214.132.117, Sweden ip: 62.20.0.196, Moldova ip: 89.41.76.29
17
- end
18
- flow_io_session = FlowcommerceSpree::Session
19
- .new(ip: request_ip, visitor: visitor_id_for_flow_io)
33
+ request_ip = if Rails.env.production?
34
+ request.ip
35
+ else
36
+ Spree::Config[:debug_request_ip_address] || request.ip
37
+ # Germany ip: 85.214.132.117, Sweden ip: 62.20.0.196, Moldova ip: 89.41.76.29
38
+ end
39
+ flow_io_session = FlowcommerceSpree::Session.create(ip: request_ip, visitor: visitor_id_for_flow_io)
20
40
  # :create method will issue a request to flow.io. The experience, contained in the
21
41
  # response, will be available in the session object - flow_io_session.experience
22
- flow_io_session.create
23
- zone = Spree::Zones::Product.active.find_by(name: flow_io_session.experience&.key&.titleize)
24
- session['_f60_session'] = flow_io_session.id if zone
42
+
43
+ if (zone = Spree::Zones::Product.active.find_by(name: flow_io_session.experience&.key&.titleize))
44
+ session['_f60_session'] = flow_io_session.id
45
+ session['_f60_expires_at'] = flow_io_session.expires_at.to_s
46
+ end
47
+
25
48
  zone
26
49
  end
27
50
 
28
- # composes an unique vistor id for FlowcommerceSpree::Session model
51
+ # composes an unique visitor id for FlowcommerceSpree::Session model
29
52
  def visitor_id_for_flow_io
30
53
  guest_token = cookies.signed[:guest_token]
31
54
  uid = if guest_token
@@ -37,13 +60,4 @@ CurrentZoneLoader.module_eval do
37
60
 
38
61
  "session-#{uid}"
39
62
  end
40
-
41
- def fetch_product_for_zone(product)
42
- Rails.cache.fetch(
43
- "product_zone_#{current_zone.name}_#{product.sku}", expires_in: 1.day,
44
- race_condition_ttl: 10.seconds, compress: true
45
- ) do
46
- Spree::Zones::Product.find_product_for_zone(product, current_zone)
47
- end
48
- end
49
63
  end
@@ -2,14 +2,17 @@
2
2
 
3
3
  module FlowcommerceSpree
4
4
  class WebhooksController < ActionController::Base
5
+ wrap_parameters false
5
6
  respond_to :json
6
7
 
7
8
  # forward all incoming requests to Flow WebhookService object
8
9
  # /flow/event-target endpoint
9
10
  def handle_flow_web_hook_event
10
- webhook_result = WebhookService.process(params[:webhook])
11
- result = {}
12
- result[:error] = webhook_result.full_messages.join("\n") if webhook_result.errors.any?
11
+ result = check_organization
12
+ if result.blank?
13
+ webhook_result = WebhookService.process(params)
14
+ result[:error] = webhook_result.full_messages.join("\n") if webhook_result.errors.any?
15
+ end
13
16
  rescue StandardError => e
14
17
  result = { error: e.class.to_s, message: e.message, backtrace: e.backtrace }
15
18
  ensure
@@ -21,5 +24,14 @@ module FlowcommerceSpree
21
24
  end
22
25
  render json: result.except(:backtrace), status: response_status
23
26
  end
27
+
28
+ private
29
+
30
+ def check_organization
31
+ org = params[:organization]
32
+ return {} if org == FlowcommerceSpree::ORGANIZATION
33
+
34
+ { error: 'InvalidParam', message: "Organization '#{org}' is invalid!" }
35
+ end
24
36
  end
25
37
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Users
4
+ SessionsController.class_eval do
5
+ private
6
+
7
+ def external_checkout?
8
+ current_zone.flow_io_active_experience? ? 'true' : 'false'
9
+ end
10
+ end
11
+ end
@@ -12,16 +12,27 @@ module Spree
12
12
  update_meta = @current_order.zone_id ? nil : true
13
13
  @current_order.zone = current_zone
14
14
 
15
- if @current_order.zone&.flow_io_active_experience? && @current_order.flow_io_experience_key.nil?
16
- @current_order.flow_io_experience_from_zone
17
- order_flow_io_session_id = @current_order.flow_data['session_id']
15
+ if @current_order.zone&.flow_io_active_experience?
16
+ if @current_order.flow_io_experience_key.nil?
17
+ @current_order.flow_io_experience_from_zone
18
+ update_meta ||= true
19
+ end
20
+ order_flow_session_id = @current_order.flow_data['session_id']
21
+ order_session_expired = @current_order.flow_data['session_expires_at']
18
22
  flow_io_session_id = session['_f60_session']
19
- if order_flow_io_session_id.present? && flow_io_session_id.blank?
20
- session['_f60_session'] = order_flow_io_session_id
21
- elsif flow_io_session_id.present?
22
- @current_order.flow_data['session_id'] = flow_io_session_id
23
+ flow_io_session_expires = session['_f60_expires_at']
24
+ if flow_io_session_id.present?
25
+ if order_flow_session_id != flow_io_session_id &&
26
+ order_session_expired&.to_datetime.to_i < flow_io_session_expires&.to_datetime.to_i
27
+ @current_order.flow_data['session_id'] = flow_io_session_id
28
+ @current_order.flow_data['session_expires_at'] = flow_io_session_expires
29
+ @current_order.flow_data['checkout_token'] = nil
30
+ update_meta ||= true
31
+ end
32
+ elsif order_flow_session_id.present?
33
+ session['_f60_session'] = order_flow_session_id
34
+ session['_f60_expires_at'] = order_session_expired
23
35
  end
24
- update_meta = true
25
36
  end
26
37
 
27
38
  if @current_order.new_record?
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ Address.class_eval do
5
+ def prepare_from_flow_attributes(address_data)
6
+ self.attributes = {
7
+ first_name: address_data['first'],
8
+ last_name: address_data['last'],
9
+ phone: address_data['phone'],
10
+ address1: address_data['streets'][0],
11
+ address2: address_data['streets'][1],
12
+ zipcode: address_data['postal'],
13
+ city: address_data['city'],
14
+ state_name: address_data['province'] || 'something',
15
+ country: Spree::Country.find_by(iso3: address_data['country'])
16
+ }
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ class Calculator
5
+ class FlowIo < Calculator::DefaultTax
6
+ def self.description
7
+ 'FlowIO Calculator'
8
+ end
9
+
10
+ def compute_shipment_or_line_item(item)
11
+ order = item.order
12
+
13
+ if can_calculate_tax?(order)
14
+ flow_response = get_flow_tax_data(order)
15
+ tax_for_item(item, flow_response)
16
+ else
17
+ prev_tax_amount(item)
18
+ end
19
+ end
20
+ alias compute_shipment compute_shipment_or_line_item
21
+ alias compute_line_item compute_shipment_or_line_item
22
+
23
+ private
24
+
25
+ def prev_tax_amount(item)
26
+ if rate.included_in_price
27
+ item.included_tax_total
28
+ else
29
+ item.additional_tax_total
30
+ end
31
+ end
32
+
33
+ def can_calculate_tax?(order)
34
+ return false if order.flow_data.blank?
35
+ return false if %w[cart address delivery].include?(order.state)
36
+
37
+ true
38
+ end
39
+
40
+ def get_flow_tax_data(order)
41
+ flow_io_tax_response = Rails.cache.fetch(order.flow_tax_cache_key, time_to_idle: 5.minutes) do
42
+ FlowcommerceSpree.client.orders.get_allocations_by_number(FlowcommerceSpree::ORGANIZATION, order.number)
43
+ end
44
+ flow_io_tax_response
45
+ end
46
+
47
+ def tax_for_item(item, flow_response)
48
+ prev_tax_amount = prev_tax_amount(item)
49
+ return prev_tax_amount if flow_response.nil?
50
+
51
+ item_details = flow_response.details&.find do |el|
52
+ item.is_a?(Spree::LineItem) ? el.number == item.variant.sku : el.key.value == 'shipping'
53
+ end
54
+ price_components = rate.included_in_price ? item_details.included : item_details.not_included
55
+
56
+ amount = price_components&.find { |el| el.key.value == 'vat_item_price' }&.total&.amount
57
+ amount.present? && amount > 0 ? amount : prev_tax_amount
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ class Calculator
5
+ module Shipping
6
+ class FlowIo < ShippingCalculator
7
+ def self.description
8
+ 'FlowIO Calculator'
9
+ end
10
+
11
+ def compute_package(package)
12
+ flow_order = flow_order(package)
13
+ return unless flow_order
14
+
15
+ flow_order['prices'].find { |x| x['key'] == 'shipping' }['amount'] || 0
16
+ end
17
+
18
+ def default_charge(_country)
19
+ 0
20
+ end
21
+
22
+ def threshold
23
+ 0
24
+ end
25
+
26
+ private
27
+
28
+ def flow_order(package)
29
+ return @flow_order if defined?(@flow_order)
30
+
31
+ @flow_order = package.order.flow_data&.[]('order')
32
+ @flow_order
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -22,8 +22,8 @@ module Spree
22
22
  raise ArgumentError, 'Value should be a hash' unless value.is_a?(Hash)
23
23
 
24
24
  self.flow_data = flow_data || {}
25
- self.flow_data['exp'] ||= {}
26
- self.flow_data['exp'][exp] = value
25
+ self.flow_data['exp'] ||= {} # rubocop:disable Style/RedundantSelf
26
+ self.flow_data['exp'][exp] = value # rubocop:disable Style/RedundantSelf
27
27
  end
28
28
 
29
29
  # clears flow_data from the records
@@ -4,7 +4,7 @@
4
4
  # adapter for Spree that talks to activemerchant_flow
5
5
  module Spree
6
6
  class Gateway
7
- class Flow < Gateway
7
+ class FlowIo < Gateway
8
8
  def provider_class
9
9
  self.class
10
10
  end
@@ -81,7 +81,7 @@ module Spree
81
81
  def profile_ensure_payment_method_is_present!
82
82
  return if @credit_card.payment_method_id
83
83
 
84
- flow_payment = Spree::PaymentMethod.where(active: true, type: 'Spree::Gateway::Flow').first
84
+ flow_payment = Spree::PaymentMethod.where(active: true, type: 'Spree::Gateway::FlowIo').first
85
85
  @credit_card.payment_method_id = flow_payment.id if flow_payment
86
86
  end
87
87
 
@@ -11,6 +11,10 @@ module Spree # rubocop:disable Metrics/ModuleLength
11
11
  before_save :sync_to_flow_io
12
12
  after_touch :sync_to_flow_io
13
13
 
14
+ def flow_tax_cache_key
15
+ [number, 'flowcommerce', 'allocation', line_items.sum(:quantity)].join('-')
16
+ end
17
+
14
18
  def sync_to_flow_io
15
19
  return unless zone&.flow_io_active_experience? && state == 'cart' && line_items.size > 0
16
20
 
@@ -34,10 +38,7 @@ module Spree # rubocop:disable Metrics/ModuleLength
34
38
  # accepts line item, usually called from views
35
39
  def flow_line_item_price(line_item, total = false)
36
40
  result = if flow_order
37
- id = line_item.variant.sku
38
-
39
- lines = flow_order.lines || []
40
- item = lines.find { |el| el['item_number'] == id }
41
+ item = flow_order.lines&.find { |el| el['item_number'] == line_item.variant.sku }
41
42
 
42
43
  return 'n/a' unless item
43
44
 
@@ -107,6 +108,10 @@ module Spree # rubocop:disable Metrics/ModuleLength
107
108
  model.new ENV.fetch('FLOW_BASE_COUNTRY')
108
109
  end
109
110
 
111
+ def flow_io_checkout_token
112
+ flow_data&.[]('checkout_token')
113
+ end
114
+
110
115
  def flow_io_experience_key
111
116
  flow_data&.[]('exp')
112
117
  end
@@ -119,29 +124,40 @@ module Spree # rubocop:disable Metrics/ModuleLength
119
124
  flow_data&.dig('order', 'id')
120
125
  end
121
126
 
127
+ def flow_io_session_expires_at
128
+ flow_data&.[]('session_expires_at')&.to_datetime
129
+ end
130
+
122
131
  def flow_io_attributes
123
132
  flow_data&.dig('order', 'attributes') || {}
124
133
  end
125
134
 
126
- def add_user_consent_to_flow_data(consent, value)
135
+ def add_flow_checkout_token(token)
136
+ self.flow_data ||= {}
137
+ self.flow_data['checkout_token'] = token
138
+ end
139
+
140
+ def flow_io_attribute_add(attr_key, value)
127
141
  self.flow_data['order'] ||= {}
128
142
  self.flow_data['order']['attributes'] ||= {}
129
- self.flow_data['order']['attributes'][consent] = value
143
+ self.flow_data['order']['attributes'][attr_key] = value
130
144
  end
131
145
 
132
146
  def add_user_uuid_to_flow_data
133
147
  self.flow_data['order'] ||= {}
134
148
  self.flow_data['order']['attributes'] ||= {}
135
- self.flow_data['order']['attributes']['user_uuid'] = user&.uuid
149
+ self.flow_data['order']['attributes']['user_uuid'] = user&.uuid || ''
136
150
  end
137
151
 
138
- def flow_io_user_uuid
152
+ def flow_io_attr_user_uuid
139
153
  flow_data&.dig('order', 'attributes', 'user_uuid')
140
154
  end
141
155
 
142
156
  def checkout_url
143
- "https://checkout.flow.io/#{FlowcommerceSpree::ORGANIZATION}/checkout/#{number}/" \
144
- "contact-info?flow_session_id=#{flow_data['session_id']}"
157
+ FlowcommerceSpree::OrderSync.new(order: self).synchronize!
158
+
159
+ checkout_token = flow_io_checkout_token
160
+ return "https://checkout.flow.io/tokens/#{checkout_token}" if checkout_token
145
161
  end
146
162
 
147
163
  # clear invalid zero amount payments. Solidus bug?
@@ -175,5 +191,54 @@ module Spree # rubocop:disable Metrics/ModuleLength
175
191
  'cc' # creait card is default
176
192
  end
177
193
  end
194
+
195
+ def flow_customer_email
196
+ flow_data.dig('order', 'customer', 'email')
197
+ end
198
+
199
+ def flow_ship_address
200
+ flow_destination = flow_data.dig('order', 'destination')
201
+ return unless flow_destination.present?
202
+
203
+ flow_destination['first'] = flow_destination.dig('contact', 'name', 'first')
204
+ flow_destination['last'] = flow_destination.dig('contact', 'name', 'last')
205
+ flow_destination['phone'] = flow_destination.dig('contact', 'phone')
206
+
207
+ s_address = ship_address || build_ship_address
208
+ s_address.prepare_from_flow_attributes(flow_destination)
209
+ s_address
210
+ end
211
+
212
+ def flow_bill_address
213
+ flow_payment_address = flow_data.dig('order', 'payments')&.last&.[]('address')
214
+ return unless flow_payment_address
215
+
216
+ flow_payment_address['first'] = flow_payment_address.dig('name', 'first')
217
+ flow_payment_address['last'] = flow_payment_address.dig('name', 'last')
218
+ flow_payment_address['phone'] = ship_address['phone']
219
+
220
+ b_address = bill_address || build_bill_address
221
+ b_address.prepare_from_flow_attributes(flow_payment_address)
222
+ b_address
223
+ end
224
+
225
+ def prepare_flow_addresses
226
+ address_attributes = {}
227
+
228
+ s_address = flow_ship_address
229
+
230
+ if s_address&.changes&.any?
231
+ s_address.save
232
+ address_attributes[:ship_address_id] = s_address.id unless ship_address_id
233
+ end
234
+
235
+ b_address = flow_bill_address
236
+ if b_address&.changes&.any?
237
+ b_address.save
238
+ address_attributes[:bill_address_id] = b_address.id unless bill_address_id
239
+ end
240
+
241
+ address_attributes
242
+ end
178
243
  end
179
244
  end