Support for SupporterAddress events and some Supporter events
This commit is contained in:
parent
4271334311
commit
cab6465e30
9 changed files with 339 additions and 37 deletions
|
@ -5,7 +5,8 @@
|
|||
module Nonprofits
|
||||
class SupportersController < ApplicationController
|
||||
include Controllers::Nonprofit::Current
|
||||
include Controllers::Nonprofit::Authorization
|
||||
include Controllers::Nonprofit::Authorization
|
||||
include Controllers::Supporter::Current
|
||||
|
||||
before_action :authenticate_nonprofit_user!, except: %i[new create]
|
||||
|
||||
|
@ -54,13 +55,12 @@ module Nonprofits
|
|||
end
|
||||
|
||||
def email_address
|
||||
render json: Supporter.find(params[:supporter_id]).email
|
||||
render json: current_supporter.email
|
||||
end
|
||||
|
||||
def full_contact
|
||||
begin
|
||||
s = Supporter.find params[:id]
|
||||
if s.method_defined? :full_contact_infos && (fc = s.full_contact_infos.first)
|
||||
begin
|
||||
if current_supporter.method_defined? :full_contact_infos && (fc = current_supporter.full_contact_infos.first)
|
||||
render json: { full_contact: QueryFullContactInfos.fetch_associated_tables(fc.id) }
|
||||
else
|
||||
render json: { full_contact: nil }
|
||||
|
@ -76,13 +76,12 @@ module Nonprofits
|
|||
|
||||
# post /nonprofits/:nonprofit_id/supporters
|
||||
def create
|
||||
render_json { InsertSupporter.create_or_update(params[:nonprofit_id], create_supporter_params.to_h) }
|
||||
render_json { InsertSupporter.create_or_update(nonprofit, create_supporter_params.to_h) }
|
||||
end
|
||||
|
||||
# put /nonprofits/:nonprofit_id/supporters/:id
|
||||
def update
|
||||
@supporter = current_nonprofit.supporters.find(params[:id])
|
||||
json_saved UpdateSupporter.from_info(@supporter, update_supporter_params[:supporter])
|
||||
json_saved UpdateSupporter.from_info(current_supporter, update_supporter_params[:supporter])
|
||||
end
|
||||
|
||||
def bulk_delete
|
||||
|
|
|
@ -55,6 +55,22 @@ class Supporter < ApplicationRecord
|
|||
validates :nonprofit, presence: true
|
||||
scope :not_deleted, -> { where(deleted: false) }
|
||||
|
||||
# TODO replace with Discard gem
|
||||
define_model_callbacks :discard
|
||||
|
||||
after_discard :publish_deleted
|
||||
|
||||
after_create_commit :publish_create
|
||||
after_update_commit :publish_updated
|
||||
|
||||
# TODO replace with discard gem
|
||||
def discard!
|
||||
run_callbacks(:discard) do
|
||||
self.deleted = true
|
||||
save!
|
||||
end
|
||||
end
|
||||
|
||||
geocoded_by :full_address
|
||||
reverse_geocoded_by :latitude, :longitude do |obj, results|
|
||||
geo = results.first
|
||||
|
@ -82,15 +98,96 @@ class Supporter < ApplicationRecord
|
|||
end
|
||||
|
||||
def to_builder(*expand)
|
||||
supporter_addresses = [self]
|
||||
Jbuilder.new do |json|
|
||||
json.object "supporter"
|
||||
json.id id
|
||||
json.(self, :id, :name, :organization, :phone, :anonymous, :deleted)
|
||||
if expand.include? :supporter_address
|
||||
json.supporter_addresses supporter_addresses do |i|
|
||||
json.merge! i.to_supporter_address_builder.attributes!
|
||||
end
|
||||
else
|
||||
json.supporter_addresses [id]
|
||||
end
|
||||
|
||||
if expand.include? :nonprofit
|
||||
json.nonprofit nonprofit.to_builder
|
||||
else
|
||||
json.nonprofit nonprofit.id
|
||||
end
|
||||
|
||||
unless merged_into.nil?
|
||||
if expand.include? :merged_into
|
||||
json.merged_into merged_into.to_builder
|
||||
else
|
||||
json.merged_into merged_into.id
|
||||
end
|
||||
else
|
||||
json.merged_into nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def to_supporter_address_builder(*expand)
|
||||
Jbuilder.new do |json|
|
||||
json.(self, :id, :address, :state_code, :city, :country, :zip_code, :deleted)
|
||||
json.object 'supporter_address'
|
||||
if expand.include? :supporter
|
||||
json.supporter to_builder
|
||||
else
|
||||
json.supporter id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def full_address
|
||||
Format::Address.full_address(address, city, state_code)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
ADDRESS_ATTRIBUTES = [:address, :city, :state_code, :zip_code, :country]
|
||||
|
||||
def supporter_address_updated?
|
||||
ADDRESS_ATTRIBUTES.any?{|attrib| saved_change_to_attribute?(attrib)}
|
||||
end
|
||||
|
||||
def nonsupporter_address_updated?
|
||||
(saved_changes.keys.map{|i| i.to_sym} - ADDRESS_ATTRIBUTES).any?
|
||||
end
|
||||
|
||||
def publish_create
|
||||
Houdini.event_publisher.announce(:supporter_created, to_event('supporter.created', :supporter_address, :nonprofit).attributes!)
|
||||
Houdini.event_publisher.announce(:supporter_address_created, to_event('supporter_address.created', :supporter, :nonprofit).attributes!)
|
||||
end
|
||||
|
||||
def publish_updated
|
||||
if !deleted
|
||||
if nonsupporter_address_updated?
|
||||
Houdini.event_publisher.announce(:supporter_updated, to_event('supporter.updated', :supporter_address, :nonprofit).attributes!)
|
||||
end
|
||||
if supporter_address_updated?
|
||||
Houdini.event_publisher.announce(:supporter_address_updated, to_event('supporter_address.updated', :supporter, :nonprofit).attributes!)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def publish_deleted
|
||||
Houdini.event_publisher.announce(:supporter_deleted, to_event('supporter.deleted', :supporter_address, :nonprofit, :merged_into).attributes!)
|
||||
Houdini.event_publisher.announce(:supporter_address_deleted, to_event('supporter_address.deleted', :supporter, :nonprofit).attributes!)
|
||||
end
|
||||
|
||||
# we do something custom here since Supporter and SupporterAddress are in the same model
|
||||
def to_event(event_type, *expand)
|
||||
Jbuilder.new do |event|
|
||||
event.id SecureRandom.uuid
|
||||
event.object 'object_event'
|
||||
event.type event_type
|
||||
event.data do
|
||||
event.object event_type.start_with?('supporter_address') ? to_supporter_address_builder(*expand) : to_builder(*expand)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ActiveSupport.run_load_hooks(:houdini_supporter, Supporter)
|
|
@ -7,9 +7,7 @@ require 'qexpr'
|
|||
require 'i18n'
|
||||
|
||||
module InsertSupporter
|
||||
def self.create_or_update(np_id, data, update = false)
|
||||
ParamValidation.new(data.merge(np_id: np_id),
|
||||
np_id: { required: true, is_integer: true })
|
||||
def self.create_or_update(nonprofit, data, update = false)
|
||||
address_keys = %w[name address city country state_code]
|
||||
custom_fields = data['customFields']
|
||||
data = HashWithIndifferentAccess.new(Format::RemoveDiacritics.from_hash(data, address_keys))
|
||||
|
@ -17,7 +15,7 @@ module InsertSupporter
|
|||
|
||||
supporter = Qx.select('*').from(:supporters)
|
||||
.where('name = $n AND email = $e', n: data[:name], e: data[:email])
|
||||
.and_where('nonprofit_id=$id', id: np_id)
|
||||
.and_where('nonprofit_id=$id', id: nonprofit.id)
|
||||
.and_where('coalesce(deleted, FALSE)=FALSE')
|
||||
.execute.last
|
||||
if supporter && update
|
||||
|
@ -28,20 +26,13 @@ module InsertSupporter
|
|||
.timestamps
|
||||
.execute.last
|
||||
else
|
||||
supporter = Qx.insert_into(:supporters)
|
||||
.values(defaults(data).merge(nonprofit_id: np_id))
|
||||
.returning('*')
|
||||
.timestamps
|
||||
.execute.last
|
||||
supporter = nonprofit.supporters.create(defaults(data))
|
||||
end
|
||||
|
||||
if custom_fields
|
||||
InsertCustomFieldJoins.find_or_create(np_id, [supporter['id']], custom_fields)
|
||||
InsertCustomFieldJoins.find_or_create(nonprofit.id, [supporter.id], custom_fields)
|
||||
end
|
||||
|
||||
# GeocodeModel.delay.supporter(supporter['id'])
|
||||
Houdini.event_publisher.announce(:supporter_create, Supporter.find(supporter['id']))
|
||||
|
||||
supporter
|
||||
end
|
||||
|
||||
|
|
|
@ -117,6 +117,8 @@ describe InsertDonation do
|
|||
end
|
||||
|
||||
it 'process campaign donation' do
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_created, anything)
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_created, anything)
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_create, any_args)
|
||||
process_campaign_donation(sepa: true) { InsertDonation.with_sepa(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, direct_debit_detail_id: direct_debit_detail.id, campaign_id: campaign.id, date: (Time.now + 1.day).to_s, dedication: 'dedication', designation: 'designation') }
|
||||
end
|
||||
|
|
|
@ -287,6 +287,8 @@ describe InsertTickets do
|
|||
expect(QueryRoles).to receive(:is_authorized_for_nonprofit?).with(user.id, nonprofit.id).and_return true
|
||||
result = nil
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:ticket_level_created, any_args).ordered
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_created, anything)
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_created, anything)
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:ticket_create, any_args).ordered
|
||||
result = InsertTickets.create(tickets: [{ quantity: 1, ticket_level_id: ticket_level.id }], nonprofit_id: nonprofit.id, supporter_id: supporter.id, token: source_token.token, event_id: event.id, kind: 'offsite', offsite_payment: { kind: 'check', check_number: 'fake_checknumber' }, current_user: user)
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ require 'rails_helper'
|
|||
|
||||
describe RetrieveActiveRecordItems do
|
||||
describe '.retrieve' do
|
||||
let(:item) { force_create(:supporter) }
|
||||
let(:item) { force_create(:supporter, nonprofit: item2) }
|
||||
let(:item2) { force_create(:nm_justice) }
|
||||
it 'raises if not a class for key' do
|
||||
expect { RetrieveActiveRecordItems.retrieve('item' => 1) }.to raise_error(ArgumentError)
|
||||
|
@ -41,7 +41,7 @@ describe RetrieveActiveRecordItems do
|
|||
end
|
||||
|
||||
describe '.retrieve_from_keys' do
|
||||
let(:item) { force_create(:supporter) }
|
||||
let(:item) { force_create(:supporter, nonprofit: item2) }
|
||||
let(:item2) { force_create(:nm_justice) }
|
||||
it 'raises if not a class for key' do
|
||||
expect { RetrieveActiveRecordItems.retrieve_from_keys({}, 'item' => 1) }.to raise_error(ArgumentError)
|
||||
|
|
|
@ -9,7 +9,7 @@ describe UpdateRecurringDonations do
|
|||
describe '.cancel' do
|
||||
|
||||
let(:np) { force_create(:nm_justice) }
|
||||
let(:s) { force_create(:supporter) }
|
||||
let(:s) { force_create(:supporter, nonprofit: np) }
|
||||
let(:donation) { force_create(:donation, nonprofit_id: np.id, supporter_id: s.id) }
|
||||
let(:email) { 'test@test.com' }
|
||||
let!(:rd) do
|
||||
|
|
|
@ -10,11 +10,29 @@ RSpec.describe SupporterNote, type: :model do
|
|||
let(:content2) {"CONTENT2"}
|
||||
|
||||
let(:supporter_note) { supporter.supporter_notes.create(content: content, user: user) }
|
||||
|
||||
let(:supporter_base) do
|
||||
{
|
||||
'anonymous' => false,
|
||||
'deleted' => false,
|
||||
'name' => name,
|
||||
'organization' => nil,
|
||||
'phone' => nil,
|
||||
'supporter_addresses' => [kind_of(Numeric)],
|
||||
'id'=> kind_of(Numeric),
|
||||
'merged_into' => nil,
|
||||
'nonprofit'=> nonprofit.id,
|
||||
'object' => 'supporter'
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates' do
|
||||
expect(supporter_note.errors).to be_empty
|
||||
end
|
||||
|
||||
it 'announces created' do
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_created, anything)
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_created, anything)
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_note_created, {
|
||||
'id' => kind_of(String),
|
||||
'object' => 'object_event',
|
||||
|
@ -34,10 +52,7 @@ RSpec.describe SupporterNote, type: :model do
|
|||
'id' => user.id,
|
||||
'object' => 'user'
|
||||
},
|
||||
'supporter' => {
|
||||
'id' => supporter.id,
|
||||
'object' => 'supporter'
|
||||
}
|
||||
'supporter' => supporter_base.merge({'name' => "Fake Supporter Name"})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -46,6 +61,8 @@ RSpec.describe SupporterNote, type: :model do
|
|||
end
|
||||
|
||||
it 'announces updated' do
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_created, anything)
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_created, anything)
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_note_created, anything).ordered
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_note_updated, {
|
||||
'id' => kind_of(String),
|
||||
|
@ -66,10 +83,7 @@ RSpec.describe SupporterNote, type: :model do
|
|||
'id' => user.id,
|
||||
'object' => 'user'
|
||||
},
|
||||
'supporter' => {
|
||||
'id' => supporter.id,
|
||||
'object' => 'supporter'
|
||||
}
|
||||
'supporter' => supporter_base.merge({'name' => "Fake Supporter Name"})
|
||||
}
|
||||
}
|
||||
}).ordered
|
||||
|
@ -80,6 +94,8 @@ RSpec.describe SupporterNote, type: :model do
|
|||
end
|
||||
|
||||
it 'announces deleted' do
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_created, anything)
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_created, anything)
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_note_created, anything).ordered
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_note_deleted, {
|
||||
'id' => kind_of(String),
|
||||
|
@ -100,10 +116,7 @@ RSpec.describe SupporterNote, type: :model do
|
|||
'id' => user.id,
|
||||
'object' => 'user'
|
||||
},
|
||||
'supporter' => {
|
||||
'id' => supporter.id,
|
||||
'object' => 'supporter'
|
||||
}
|
||||
'supporter' => supporter_base.merge({'name' => "Fake Supporter Name"})
|
||||
}
|
||||
}
|
||||
}).ordered
|
||||
|
|
198
spec/models/supporter_spec.rb
Normal file
198
spec/models/supporter_spec.rb
Normal file
|
@ -0,0 +1,198 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later
|
||||
# Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Supporter, type: :model do
|
||||
include_context :shared_donation_charge_context
|
||||
let(:name) {"CUSTOM SSUPPORTER"}
|
||||
let(:merged_into_supporter_name) {"I've been merged into!"}
|
||||
let(:address) { "address for supporter"}
|
||||
let(:supporter) { nonprofit.supporters.create(name: name, address: address)}
|
||||
let(:merged_supporter) {nonprofit.supporters.create(name: name, address: address, merged_into: merged_into_supporter, deleted: true) }
|
||||
let(:merged_into_supporter) {nonprofit.supporters.create(name: merged_into_supporter_name, address: address) }
|
||||
|
||||
let(:supporter_base) do
|
||||
{
|
||||
'anonymous' => false,
|
||||
'deleted' => false,
|
||||
'name' => name,
|
||||
'organization' => nil,
|
||||
'phone' => nil,
|
||||
'supporter_addresses' => [kind_of(Numeric)],
|
||||
'id'=> kind_of(Numeric),
|
||||
'merged_into' => nil,
|
||||
'nonprofit'=> nonprofit.id,
|
||||
'object' => 'supporter'
|
||||
}
|
||||
end
|
||||
|
||||
let(:supporter_address_base) do
|
||||
{
|
||||
'id' => kind_of(Numeric),
|
||||
'deleted' => false,
|
||||
'address' => address,
|
||||
'city' => nil,
|
||||
'state_code' => nil,
|
||||
'zip_code' => nil,
|
||||
'country' => 'United States',
|
||||
'object' => 'supporter_address',
|
||||
'supporter' => kind_of(Numeric)
|
||||
}
|
||||
end
|
||||
|
||||
let(:nonprofit_base) do
|
||||
{
|
||||
'id' => nonprofit.id,
|
||||
'name' => nonprofit.name,
|
||||
'object' => 'nonprofit'
|
||||
}
|
||||
end
|
||||
|
||||
describe 'supporter' do
|
||||
it 'created' do
|
||||
|
||||
supporter_result = supporter_base.merge({
|
||||
'supporter_addresses' => [
|
||||
supporter_address_base
|
||||
],
|
||||
'nonprofit' => nonprofit_base
|
||||
})
|
||||
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_created, {
|
||||
'id' => kind_of(String),
|
||||
'object' => 'object_event',
|
||||
'type' => 'supporter.created',
|
||||
'data' => {
|
||||
'object' => supporter_result
|
||||
}
|
||||
}).ordered
|
||||
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_created, anything).ordered
|
||||
|
||||
supporter
|
||||
end
|
||||
|
||||
it 'deletes' do
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_created,anything).ordered
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_created, anything).ordered
|
||||
expect(Houdini.event_publisher).to_not receive(:announce).with(:supporter_updated)
|
||||
expect(Houdini.event_publisher).to_not receive(:announce).with(:supporter_address_updated)
|
||||
|
||||
supporter_result = supporter_base.merge({
|
||||
'deleted' => true,
|
||||
'supporter_addresses' => [
|
||||
supporter_address_base.merge({'deleted' => true})
|
||||
],
|
||||
'nonprofit' => nonprofit_base
|
||||
})
|
||||
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_deleted, {
|
||||
'id' => kind_of(String),
|
||||
'object' => 'object_event',
|
||||
'type' => 'supporter.deleted',
|
||||
'data' => {
|
||||
'object' => supporter_result
|
||||
}
|
||||
}).ordered
|
||||
|
||||
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_deleted,anything).ordered
|
||||
|
||||
supporter.discard!
|
||||
end
|
||||
end
|
||||
|
||||
describe 'supporter_address events' do
|
||||
it 'creates' do
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_created, anything).ordered
|
||||
|
||||
|
||||
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_created, {
|
||||
'id' => kind_of(String),
|
||||
'object' => 'object_event',
|
||||
'type' => 'supporter_address.created',
|
||||
'data' => {
|
||||
'object' => supporter_address_base.merge({
|
||||
'supporter' => supporter_base
|
||||
})
|
||||
}
|
||||
}).ordered
|
||||
|
||||
supporter
|
||||
end
|
||||
|
||||
it 'deletes' do
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_created,anything).ordered
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_created, anything).ordered
|
||||
expect(Houdini.event_publisher).to_not receive(:announce).with(:supporter_updated)
|
||||
expect(Houdini.event_publisher).to_not receive(:announce).with(:supporter_address_updated)
|
||||
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_deleted, anything)
|
||||
|
||||
|
||||
supporter_address_result = supporter_address_base.merge({
|
||||
'deleted' => true,
|
||||
'supporter'=> supporter_base.merge({'deleted' => true})
|
||||
})
|
||||
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_deleted, {
|
||||
'id' => kind_of(String),
|
||||
'object' => 'object_event',
|
||||
'type' => 'supporter_address.deleted',
|
||||
'data' => {
|
||||
'object' => supporter_address_result
|
||||
}
|
||||
}).ordered
|
||||
|
||||
|
||||
supporter.discard!
|
||||
end
|
||||
end
|
||||
|
||||
describe 'supporter and supporter_address events update events are separate' do
|
||||
it 'only fires supporter on supporter only change' do
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_created, anything).ordered
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_created, anything).ordered
|
||||
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_updated, { 'id' => kind_of(String),
|
||||
'object' => 'object_event',
|
||||
'type' => 'supporter.updated',
|
||||
'data' => {
|
||||
'object' => supporter_base.merge({
|
||||
'name' => merged_into_supporter_name,
|
||||
'supporter_addresses' => [
|
||||
supporter_address_base
|
||||
],
|
||||
'nonprofit' => nonprofit_base
|
||||
})
|
||||
|
||||
}}).ordered
|
||||
expect(Houdini.event_publisher).to_not receive(:announce).with(:supporter_address_updated, anything)
|
||||
|
||||
supporter.update(name: merged_into_supporter_name)
|
||||
end
|
||||
|
||||
it 'only fires supporter_address on supporter_address only change' do
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_created, anything).ordered
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_created, anything).ordered
|
||||
|
||||
expect(Houdini.event_publisher).to receive(:announce).with(:supporter_address_updated, {
|
||||
'id' => kind_of(String),
|
||||
'object' => 'object_event',
|
||||
'type' => 'supporter_address.updated',
|
||||
'data' => {
|
||||
'object' => supporter_address_base.merge({
|
||||
'city' => 'new_city',
|
||||
'supporter'=> supporter_base
|
||||
})
|
||||
}}).ordered
|
||||
|
||||
#expect(Houdini.event_publisher).to_not receive(:announce).with(:supporter_updated, anything)
|
||||
|
||||
supporter.update(city: 'new_city')
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue