diff --git a/app/controllers/concerns/epp/common.rb b/app/controllers/concerns/epp/common.rb index d7f25c0e1..7b4b9abf5 100644 --- a/app/controllers/concerns/epp/common.rb +++ b/app/controllers/concerns/epp/common.rb @@ -60,6 +60,15 @@ module Epp::Common epp_errors.empty? end + def xml_nested_attrs_present?(array_ph, attributes ) + [array_ph].flatten.each do |ph| + attributes.each do |x| + epp_errors << {code: '2003', msg: I18n.t('errors.messages.required_parameter_missing', key: x.last)} unless has_attribute(ph, x) + end + end + epp_errors.empty? + end + def has_attribute(ph, path) path.reduce(ph) do |location, key| location.respond_to?(:keys) ? location[key] : nil diff --git a/app/helpers/epp/contacts_helper.rb b/app/helpers/epp/contacts_helper.rb index 465c2ccbc..fd840558e 100644 --- a/app/helpers/epp/contacts_helper.rb +++ b/app/helpers/epp/contacts_helper.rb @@ -49,9 +49,12 @@ module Epp::ContactsHelper @ph = params_hash['epp']['command']['create']['create'] xml_attrs_present?(@ph, [['id'], %w(authInfo pw), - %w(postalInfo name), - %w(postalInfo addr city), - %w(postalInfo addr cc)]) + %w(postalInfo)]) + if @ph['postalInfo'].is_a?(Hash) || @ph['postalInfo'].is_a?(Array) + xml_nested_attrs_present?( @ph['postalInfo'], [ %w(name), + %w(addr city), + %w(addr cc)]) + end end ## UPDATE @@ -102,12 +105,14 @@ module Epp::ContactsHelper case type when :update contact_hash = Contact.extract_attributes(@ph[:chg], type) - contact_hash[:address_attributes] = - Address.extract_attributes(( @ph.try(:[], :chg).try(:[], :postalInfo).try(:[], :addr) || []), type) + contact_hash = contact_hash.merge( + Address.extract_attributes(( @ph.try(:[], :chg).try(:[], :postalInfo) || [] ), type) + ) else contact_hash = Contact.extract_attributes(@ph, type) - contact_hash[:address_attributes] = - Address.extract_attributes(( @ph.try(:[], :postalInfo).try(:[], :addr) || []), type) + contact_hash = contact_hash.merge( + Address.extract_attributes(( @ph.try(:[], :postalInfo) || [] ), type) + ) end contact_hash[:ident_type] = ident_type unless ident_type.nil? contact_hash diff --git a/app/models/address.rb b/app/models/address.rb index 459acd23a..c7fc0ba36 100644 --- a/app/models/address.rb +++ b/app/models/address.rb @@ -1,20 +1,46 @@ class Address < ActiveRecord::Base + LOCAL_TYPE_SHORT = 'loc' + INTERNATIONAL_TYPE_SHORT = 'int' + LOCAL_TYPE = 'LocalAddress' + TYPES = [ + LOCAL_TYPE_SHORT, + INTERNATIONAL_TYPE_SHORT + ] + belongs_to :contact belongs_to :country + #validates_inclusion_of :type, in: TYPES + class << self def extract_attributes(ah, _type = :create) address_hash = {} - address_hash = ({ - country_id: Country.find_by(iso: ah[:cc]).try(:id), - city: ah[:city], - street: ah[:street][0], - street2: ah[:street][1], - street3: ah[:street][2], - zip: ah[:pc] - }) if ah.is_a?(Hash) + [ah].flatten.each do |pi| + address_hash[local?(pi)] = addr_hash_from_params(pi) + end - address_hash.delete_if { |_k, v| v.nil? } + address_hash + end + + private + + def local?(postal_info) + return :local_address_attributes if postal_info[:type] == LOCAL_TYPE_SHORT + :international_address_attributes + end + + def addr_hash_from_params(addr) + return {} unless addr[:addr].is_a?(Hash) + { + name: addr[:name], + org_name: addr[:org], + country_id: Country.find_by(iso: addr[:addr][:cc]).try(:id), + city: addr[:addr][:city], + street: addr[:addr][:street][0], + street2: addr[:addr][:street][1], + street3: addr[:addr][:street][2], + zip: addr[:addr][:pc] + }.delete_if { |k, v| v.nil? } end end end diff --git a/app/models/contact.rb b/app/models/contact.rb index f86ad2ff3..6b7496e9f 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -6,16 +6,18 @@ class Contact < ActiveRecord::Base EPP_ATTR_MAP = {} - has_one :address + has_one :local_address#, class_name: 'Address'#, foreign_key: 'local_address_id' + has_one :international_address + has_many :domain_contacts has_many :domains, through: :domain_contacts belongs_to :created_by, class_name: 'EppUser', foreign_key: :created_by_id belongs_to :updated_by, class_name: 'EppUser', foreign_key: :updated_by_id - accepts_nested_attributes_for :address + accepts_nested_attributes_for :local_address, :international_address - validates_presence_of :code, :name, :phone, :email, :ident + validates_presence_of :code, :phone, :email, :ident validate :ident_must_be_valid @@ -32,6 +34,20 @@ class Contact < ActiveRecord::Base 'birthday' # Birthday date ] + CONTACT_TYPE_TECH = 'tech' + CONTACT_TYPE_ADMIN = 'admin' + CONTACT_TYPES = [CONTACT_TYPE_TECH, CONTACT_TYPE_ADMIN] + + #TEMP METHODS for transaction to STI + def name + international_address.name + end + + def address + international_address + end + ## + def ident_must_be_valid # TODO Ident can also be passport number or company registry code. # so have to make changes to validations (and doc/schema) accordingly @@ -107,11 +123,6 @@ class Contact < ActiveRecord::Base email: ph[:email] } - contact_hash = contact_hash.merge({ - name: ph[:postalInfo][:name], - org_name: ph[:postalInfo][:org] - }) if ph[:postalInfo].is_a? Hash - contact_hash[:code] = ph[:id] if type == :create contact_hash.delete_if { |_k, v| v.nil? } diff --git a/app/models/international_address.rb b/app/models/international_address.rb new file mode 100644 index 000000000..20a3cb27f --- /dev/null +++ b/app/models/international_address.rb @@ -0,0 +1,3 @@ +class InternationalAddress < Address + +end diff --git a/app/models/local_address.rb b/app/models/local_address.rb new file mode 100644 index 000000000..1f6fa93bb --- /dev/null +++ b/app/models/local_address.rb @@ -0,0 +1,3 @@ +class LocalAddress < Address + +end diff --git a/app/views/epp/contacts/_postal_info.xml.builder b/app/views/epp/contacts/_postal_info.xml.builder new file mode 100644 index 000000000..ffa165bb7 --- /dev/null +++ b/app/views/epp/contacts/_postal_info.xml.builder @@ -0,0 +1,27 @@ +if @contact.international_address + address = @contact.international_address + xml.tag!('contact:postalInfo', type: 'int') do # TODO instance method of defining type + xml.tag!('contact:name', address.name) + xml.tag!('contact:org', address.org_name) + xml.tag!('contact:addr') do + xml.tag!('contact:street', address.street) if address.street + xml.tag!('contact:street', address.street2) if address.street2 + xml.tag!('contact:street', address.street3) if address.street3 + xml.tag!('contact:cc', address.try(:country).try(:iso)) unless address.try(:country).nil? + end + end +end +if @contact.local_address + address = @contact.local_address + xml.tag!('contact:postalInfo', type: 'loc') do + xml.tag!('contact:name', address.name) + xml.tag!('contact:org', address.org_name) + xml.tag!('contact:addr') do + xml.tag!('contact:street', address.street) if address.street + xml.tag!('contact:street', address.street2) if address.street2 + xml.tag!('contact:street', address.street3) if address.street3 + xml.tag!('contact:cc', address.try(:country).try(:iso)) unless address.try(:country).nil? + end + end +end + diff --git a/app/views/epp/contacts/info.xml.builder b/app/views/epp/contacts/info.xml.builder index e5a4a0fe8..576712ccc 100644 --- a/app/views/epp/contacts/info.xml.builder +++ b/app/views/epp/contacts/info.xml.builder @@ -6,15 +6,7 @@ xml.epp_head do xml.resData do xml.tag!('contact:chkData', 'xmlns:contact' => 'urn:ietf:params:xml:ns:contact-1.0') do - xml.tag!('contact:name', @contact.name) - xml.tag!('contact:org', @contact.org_name) - xml.tag!('contact:addr') do - address = @contact.address - xml.tag!('contact:street', address.street) if address.street - xml.tag!('contact:street', address.street2) if address.street2 - xml.tag!('contact:street', address.street3) if address.street3 - xml.tag!('contact:cc', address.try(:country).try(:iso)) unless address.try(:country).nil? - end + xml << render('/epp/contacts/postal_info') xml.tag!('contact:voice', @contact.phone) xml.tag!('contact:fax', @contact.fax) xml.tag!('contact:email', @contact.email) diff --git a/db/migrate/20140822122938_add_postal_info_to_address.rb b/db/migrate/20140822122938_add_postal_info_to_address.rb new file mode 100644 index 000000000..427eab48a --- /dev/null +++ b/db/migrate/20140822122938_add_postal_info_to_address.rb @@ -0,0 +1,10 @@ +class AddPostalInfoToAddress < ActiveRecord::Migration + def change + add_column :addresses, :name, :string + add_column :addresses, :org_name, :string + add_column :addresses, :type, :string + + remove_column :contacts, :name, :string + remove_column :contacts, :org_name, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 280bc11d5..b7d4a4464 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,181 +11,182 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20_140_819_103_517) do +ActiveRecord::Schema.define(version: 20140822122938) do # These are extensions that must be enabled in order to support this database - enable_extension 'plpgsql' + enable_extension "plpgsql" - create_table 'addresses', force: true do |t| - t.integer 'contact_id' - t.integer 'country_id' - t.string 'city' - t.string 'street' - t.string 'zip' - t.datetime 'created_at' - t.datetime 'updated_at' - t.string 'street2' - t.string 'street3' + create_table "addresses", force: true do |t| + t.integer "contact_id" + t.integer "country_id" + t.string "city" + t.string "street" + t.string "zip" + t.datetime "created_at" + t.datetime "updated_at" + t.string "street2" + t.string "street3" + t.string "name" + t.string "org_name" + t.string "type" end - create_table 'contacts', force: true do |t| - t.string 'code' - t.string 'name' - t.string 'type' - t.string 'reg_no' - t.string 'phone' - t.string 'email' - t.string 'fax' - t.datetime 'created_at' - t.datetime 'updated_at' - t.string 'ident' - t.string 'ident_type' - t.string 'org_name' - t.integer 'created_by_id' - t.integer 'updated_by_id' - t.string 'auth_info' + create_table "contacts", force: true do |t| + t.string "code" + t.string "type" + t.string "reg_no" + t.string "phone" + t.string "email" + t.string "fax" + t.datetime "created_at" + t.datetime "updated_at" + t.string "ident" + t.string "ident_type" + t.integer "created_by_id" + t.integer "updated_by_id" + t.string "auth_info" end - create_table 'countries', force: true do |t| - t.string 'iso' - t.string 'name' - t.datetime 'created_at' - t.datetime 'updated_at' + create_table "countries", force: true do |t| + t.string "iso" + t.string "name" + t.datetime "created_at" + t.datetime "updated_at" end - create_table 'domain_contacts', force: true do |t| - t.integer 'contact_id' - t.integer 'domain_id' - t.string 'contact_type' - t.datetime 'created_at' - t.datetime 'updated_at' + create_table "domain_contacts", force: true do |t| + t.integer "contact_id" + t.integer "domain_id" + t.string "contact_type" + t.datetime "created_at" + t.datetime "updated_at" end - create_table 'domain_statuses', force: true do |t| - t.integer 'domain_id' - t.integer 'setting_id' - t.string 'description' + create_table "domain_statuses", force: true do |t| + t.integer "domain_id" + t.integer "setting_id" + t.string "description" end - create_table 'domains', force: true do |t| - t.string 'name' - t.integer 'registrar_id' - t.datetime 'registered_at' - t.string 'status' - t.datetime 'valid_from' - t.datetime 'valid_to' - t.integer 'owner_contact_id' - t.integer 'admin_contact_id' - t.integer 'technical_contact_id' - t.integer 'ns_set_id' - t.string 'auth_info' - t.datetime 'created_at' - t.datetime 'updated_at' - t.string 'name_dirty' - t.string 'name_puny' - t.integer 'period' - t.string 'period_unit', limit: 1 + create_table "domains", force: true do |t| + t.string "name" + t.integer "registrar_id" + t.datetime "registered_at" + t.string "status" + t.datetime "valid_from" + t.datetime "valid_to" + t.integer "owner_contact_id" + t.integer "admin_contact_id" + t.integer "technical_contact_id" + t.integer "ns_set_id" + t.string "auth_info" + t.datetime "created_at" + t.datetime "updated_at" + t.string "name_dirty" + t.string "name_puny" + t.integer "period" + t.string "period_unit", limit: 1 end - create_table 'domains_nameservers', force: true do |t| - t.integer 'domain_id' - t.integer 'nameserver_id' + create_table "domains_nameservers", force: true do |t| + t.integer "domain_id" + t.integer "nameserver_id" end - create_table 'epp_sessions', force: true do |t| - t.string 'session_id' - t.text 'data' - t.datetime 'created_at' - t.datetime 'updated_at' + create_table "epp_sessions", force: true do |t| + t.string "session_id" + t.text "data" + t.datetime "created_at" + t.datetime "updated_at" end - add_index 'epp_sessions', ['session_id'], name: 'index_epp_sessions_on_session_id', unique: true, using: :btree - add_index 'epp_sessions', ['updated_at'], name: 'index_epp_sessions_on_updated_at', using: :btree + add_index "epp_sessions", ["session_id"], name: "index_epp_sessions_on_session_id", unique: true, using: :btree + add_index "epp_sessions", ["updated_at"], name: "index_epp_sessions_on_updated_at", using: :btree - create_table 'epp_users', force: true do |t| - t.integer 'registrar_id' - t.string 'username' - t.string 'password' - t.boolean 'active', default: false - t.text 'csr' - t.text 'crt' - t.datetime 'created_at' - t.datetime 'updated_at' + create_table "epp_users", force: true do |t| + t.integer "registrar_id" + t.string "username" + t.string "password" + t.boolean "active", default: false + t.text "csr" + t.text "crt" + t.datetime "created_at" + t.datetime "updated_at" end - create_table 'nameservers', force: true do |t| - t.string 'hostname' - t.string 'ipv4' - t.integer 'ns_set_id' - t.datetime 'created_at' - t.datetime 'updated_at' - t.string 'ipv6' + create_table "nameservers", force: true do |t| + t.string "hostname" + t.string "ipv4" + t.integer "ns_set_id" + t.datetime "created_at" + t.datetime "updated_at" + t.string "ipv6" end - create_table 'nameservers_ns_sets', force: true do |t| - t.integer 'nameserver_id' - t.integer 'ns_set_id' + create_table "nameservers_ns_sets", force: true do |t| + t.integer "nameserver_id" + t.integer "ns_set_id" end - create_table 'ns_sets', force: true do |t| - t.string 'code' - t.integer 'registrar_id' - t.string 'auth_info' - t.string 'report_level' - t.datetime 'created_at' - t.datetime 'updated_at' + create_table "ns_sets", force: true do |t| + t.string "code" + t.integer "registrar_id" + t.string "auth_info" + t.string "report_level" + t.datetime "created_at" + t.datetime "updated_at" end - create_table 'registrars', force: true do |t| - t.string 'name' - t.string 'reg_no' - t.string 'vat_no' - t.string 'address' - t.integer 'country_id' - t.string 'billing_address' - t.datetime 'created_at' - t.datetime 'updated_at' + create_table "registrars", force: true do |t| + t.string "name" + t.string "reg_no" + t.string "vat_no" + t.string "address" + t.integer "country_id" + t.string "billing_address" + t.datetime "created_at" + t.datetime "updated_at" end - create_table 'reserved_domains', force: true do |t| - t.string 'name' - t.datetime 'created_at' - t.datetime 'updated_at' + create_table "reserved_domains", force: true do |t| + t.string "name" + t.datetime "created_at" + t.datetime "updated_at" end - create_table 'rights', force: true do |t| - t.string 'code' - t.datetime 'created_at' - t.datetime 'updated_at' + create_table "rights", force: true do |t| + t.string "code" + t.datetime "created_at" + t.datetime "updated_at" end - create_table 'rights_roles', force: true do |t| - t.integer 'right_id' - t.integer 'role_id' + create_table "rights_roles", force: true do |t| + t.integer "right_id" + t.integer "role_id" end - create_table 'roles', force: true do |t| - t.string 'name' - t.datetime 'created_at' - t.datetime 'updated_at' + create_table "roles", force: true do |t| + t.string "name" + t.datetime "created_at" + t.datetime "updated_at" end - create_table 'setting_groups', force: true do |t| - t.string 'code' + create_table "setting_groups", force: true do |t| + t.string "code" end - create_table 'settings', force: true do |t| - t.integer 'setting_group_id' - t.string 'code' - t.string 'value' + create_table "settings", force: true do |t| + t.integer "setting_group_id" + t.string "code" + t.string "value" end - create_table 'users', force: true do |t| - t.string 'username' - t.string 'password' - t.integer 'role_id' - t.datetime 'created_at' - t.datetime 'updated_at' + create_table "users", force: true do |t| + t.string "username" + t.string "password" + t.integer "role_id" + t.datetime "created_at" + t.datetime "updated_at" end end diff --git a/spec/epp/contact_spec.rb b/spec/epp/contact_spec.rb index 536e0e9a7..64fef905a 100644 --- a/spec/epp/contact_spec.rb +++ b/spec/epp/contact_spec.rb @@ -29,17 +29,30 @@ describe 'EPP Contact', epp: true do expect(response[:result_code]).to eq('1000') expect(response[:msg]).to eq('Command completed successfully') expect(response[:clTRID]).to eq('ABC-12345') - expect(Contact.first.created_by_id).to be 1 - expect(Contact.first.updated_by_id).to be nil + expect(Contact.first.created_by_id).to eq 1 + expect(Contact.first.updated_by_id).to eq nil expect(Contact.count).to eq(1) - expect(Contact.first.org_name).to eq('Example Inc.') + + + expect(Contact.first.international_address.org_name).to eq('Example Inc.') expect(Contact.first.ident).to eq '37605030299' expect(Contact.first.ident_type).to eq 'op' - expect(Contact.first.address.street).to eq('123 Example Dr.') - expect(Contact.first.address.street2).to eq('Suite 100') - expect(Contact.first.address.street3).to eq nil + expect(Contact.first.international_address.street).to eq('123 Example Dr.') + expect(Contact.first.international_address.street2).to eq('Suite 100') + expect(Contact.first.international_address.street3).to eq nil + end + + it 'successfully creates contact with 2 addresses' do + response = epp_request('contacts/create_with_two_addresses.xml') + + expect(response[:result_code]).to eq('1000') + + expect(Contact.count).to eq(1) + expect(Contact.first.address).to_not eq Contact.first.local_address + expect(Address.count).to eq(2) + expect(LocalAddress.count).to eq(1) end it 'returns result data upon success' do @@ -62,6 +75,7 @@ describe 'EPP Contact', epp: true do response = epp_request(contact_create_xml, :xml) + expect(response[:result_code]).to eq('2302') expect(response[:msg]).to eq('Contact id already exists') @@ -204,8 +218,8 @@ describe 'EPP Contact', epp: true do end it 'returns info about contact' do - Fabricate(:contact, name: 'Johnny Awesome', created_by_id: '1', code: 'info-4444', auth_info: '2fooBAR') - Fabricate(:address) + Fabricate(:contact, created_by_id: '1', code: 'info-4444', auth_info: '2fooBAR', + international_address: Fabricate(:international_address, name: 'Johnny Awesome')) response = epp_request('contacts/info.xml') contact = response[:parsed].css('resData chkData') @@ -217,7 +231,7 @@ describe 'EPP Contact', epp: true do end it 'doesn\'t display unassociated object' do - Fabricate(:contact, name: 'Johnny Awesome', code: 'info-4444') + Fabricate(:contact, code: 'info-4444') response = epp_request('contacts/info.xml') expect(response[:result_code]).to eq('2201') diff --git a/spec/epp/requests/contacts/create_with_two_addresses.xml b/spec/epp/requests/contacts/create_with_two_addresses.xml new file mode 100644 index 000000000..f21d620d6 --- /dev/null +++ b/spec/epp/requests/contacts/create_with_two_addresses.xml @@ -0,0 +1,45 @@ + + + + + + loc_int + + John Doe Int + Example Int Inc. + + International street 1 + Dulles + VA + 20166-6503 + EE + + + + John Doe Loc + Example Loc Inc. + + Local street 1 + Vancouver + BC + 27123 + CA + + + +123.7035555555 + +1.7035555556 + jdoe@example.com + 37605030299 + + 2fooBAR + + + + + + + + ABC-12345 + + diff --git a/spec/fabricators/address_fabricator.rb b/spec/fabricators/address_fabricator.rb deleted file mode 100644 index 70aa7f867..000000000 --- a/spec/fabricators/address_fabricator.rb +++ /dev/null @@ -1,6 +0,0 @@ -Fabricator(:address) do - city Faker::Address.city - street Faker::Address.street_name - street2 Faker::Address.street_name - zip Faker::Address.zip -end diff --git a/spec/fabricators/contact_fabricator.rb b/spec/fabricators/contact_fabricator.rb index e6c6fec0e..bcba61a4d 100644 --- a/spec/fabricators/contact_fabricator.rb +++ b/spec/fabricators/contact_fabricator.rb @@ -1,10 +1,9 @@ Fabricator(:contact) do - name Faker::Name.name phone '+372.12345678' email Faker::Internet.email ident '37605030299' code { "sh#{Faker::Number.number(4)}" } ident_type 'op' auth_info 'ccds4324pok' - address + international_address end diff --git a/spec/fabricators/international_address_fabricator.rb b/spec/fabricators/international_address_fabricator.rb new file mode 100644 index 000000000..4b249f641 --- /dev/null +++ b/spec/fabricators/international_address_fabricator.rb @@ -0,0 +1,7 @@ +Fabricator(:international_address) do + name Faker::Name.name + city Faker::Address.city + street Faker::Address.street_name + street2 Faker::Address.street_name + zip Faker::Address.zip +end diff --git a/spec/models/address_spec.rb b/spec/models/address_spec.rb index 96fa20adb..95a685016 100644 --- a/spec/models/address_spec.rb +++ b/spec/models/address_spec.rb @@ -7,13 +7,16 @@ end describe Address, '.extract_params' do it 'returns params hash'do - Fabricate(:country, iso: 'EE') - ph = { postalInfo: { name: 'fred', addr: { cc: 'EE', city: 'Village', street: %w(street1 street2) } } } - expect(Address.extract_attributes(ph[:postalInfo][:addr])).to eq({ - city: 'Village', - country_id: 1, - street: 'street1', - street2: 'street2' + Fabricate(:country, iso:'EE') + ph = { postalInfo: { name: "fred", addr: { cc: 'EE', city: 'Village', street: [ 'street1', 'street2' ] } } } + expect(Address.extract_attributes(ph[:postalInfo])).to eq( { + international_address_attributes: { + name: 'fred', + city: 'Village', + country_id: 1, + street: 'street1', + street2: 'street2', + } }) end end diff --git a/spec/models/contact_spec.rb b/spec/models/contact_spec.rb index 286d87391..9ddd865f9 100644 --- a/spec/models/contact_spec.rb +++ b/spec/models/contact_spec.rb @@ -1,7 +1,8 @@ require 'rails_helper' describe Contact do - it { should have_one(:address) } + it { should have_one(:local_address) } + it { should have_one(:international_address) } context 'with invalid attribute' do before(:each) { @contact = Fabricate(:contact) } @@ -22,7 +23,6 @@ describe Contact do expect(@contact.errors.messages).to match_array({ code: ['Required parameter missing - code'], - name: ['Required parameter missing - name'], phone: ['Required parameter missing - phone', 'Phone nr is invalid'], email: ['Required parameter missing - email', 'Email is invalid'], ident: ['Required parameter missing - ident'] @@ -88,9 +88,8 @@ describe Contact, '.extract_params' do ph = { id: '123123', email: 'jdoe@example.com', postalInfo: { name: 'fred', addr: { cc: 'EE' } } } expect(Contact.extract_attributes(ph)).to eq({ code: '123123', - email: 'jdoe@example.com', - name: 'fred' - }) + email: 'jdoe@example.com' + } ) end end