From bbf3c25a2aa00b3713e61fef5ddbcc8ce6ae6f05 Mon Sep 17 00:00:00 2001 From: Keith Dawson Date: Thu, 12 Mar 2026 20:37:10 +0900 Subject: [PATCH 1/3] [del-1611] Added support for 'external_id' field in Contact model requests --- lib/chartmogul/contact.rb | 5 +- spec/chartmogul/contact_spec.rb | 51 +++++++++++++++---- .../creates_the_contact_correctly.yml | 4 +- ...ontact_with_null_external_id_correctly.yml | 36 +++++++++++++ ..._contact_without_external_id_correctly.yml | 36 +++++++++++++ ...ontact_correctly_with_the_class_method.yml | 4 +- ...ontact_with_null_external_id_correctly.yml | 36 +++++++++++++ 7 files changed, 156 insertions(+), 16 deletions(-) create mode 100644 spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/creates_the_contact_with_null_external_id_correctly.yml create mode 100644 spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/creates_the_contact_without_external_id_correctly.yml create mode 100644 spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/updates_the_contact_with_null_external_id_correctly.yml diff --git a/lib/chartmogul/contact.rb b/lib/chartmogul/contact.rb index 25a5b11..d17e5cd 100644 --- a/lib/chartmogul/contact.rb +++ b/lib/chartmogul/contact.rb @@ -20,6 +20,7 @@ class Contact < APIResource writeable_attr :linked_in writeable_attr :twitter writeable_attr :notes + writeable_attr :external_id writeable_attr :custom include API::Actions::Create @@ -43,13 +44,15 @@ def serialize_for_write if attribute_name == :custom && attribute_value.is_a?(Hash) payload = attribute_value.each_with_object([]) do |custom_value, arr| key, value = custom_value - arr << { key: key, value: value } + arr << ({ key:, value: }) end attributes[:custom] = payload else attributes[attribute_name] = attribute_value end end + # Include external_id attribute even when nil so callers can explicitly clear it + attributes[:external_id] = nil if instance_variable_defined?(:@external_id) && external_id.nil? end end end diff --git a/spec/chartmogul/contact_spec.rb b/spec/chartmogul/contact_spec.rb index 47a98d9..61f59e1 100644 --- a/spec/chartmogul/contact_spec.rb +++ b/spec/chartmogul/contact_spec.rb @@ -6,9 +6,10 @@ let(:attrs) do { uuid: contact_uuid, - customer_uuid: customer_uuid, - data_source_uuid: data_source_uuid, + customer_uuid:, + data_source_uuid:, customer_external_id: 'cus_004', + external_id: 'contact_external_id_001', first_name: 'First name', last_name: 'Last name', position: 9, @@ -20,7 +21,7 @@ notes: 'Heading\nBody\nFooter', custom: { MyStringAttribute: 'Test', - MyIntegerAttribute: 123, + MyIntegerAttribute: 123 } } end @@ -29,6 +30,7 @@ let(:data_source_uuid) { 'ds_03cfd2c4-2c7e-11ee-ab23-cb0f008cff46' } let(:updated_attributes) do { + external_id: 'contact_external_id_002', first_name: 'Foo', last_name: 'Bar', email: 'contact2@example.com', @@ -71,22 +73,44 @@ expect(contact).to have_attributes( uuid: contact_uuid, - customer_uuid: customer_uuid, - data_source_uuid: data_source_uuid, + customer_uuid:, + data_source_uuid:, email: 'contact@example.com' ) end it 'creates the contact correctly' do attributes = { - customer_uuid: customer_uuid, - data_source_uuid: data_source_uuid, - email: 'contact@example.com' + customer_uuid:, + data_source_uuid:, + email: 'contact@example.com', + external_id: 'contact_external_id_001' } contact = described_class.create!(**attributes) expect(contact).to have_attributes(uuid: contact_uuid, **attributes) end + it 'creates the contact with null external_id correctly' do + attributes = { + customer_uuid:, + data_source_uuid:, + email: 'contact@example.com', + external_id: nil + } + contact = described_class.create!(**attributes) + expect(contact).to have_attributes(uuid: contact_uuid, external_id: nil) + end + + it 'creates the contact without external_id correctly' do + attributes = { + customer_uuid:, + data_source_uuid:, + email: 'contact@example.com' + } + contact = described_class.create!(**attributes) + expect(contact).to have_attributes(uuid: contact_uuid, **attributes, external_id: nil) + end + it 'updates the contact correctly with the class method' do updated_contact = described_class.update!( contact_uuid, **updated_attributes @@ -94,12 +118,17 @@ expect(updated_contact).to have_attributes( uuid: contact_uuid, - data_source_uuid: data_source_uuid, - customer_uuid: customer_uuid, + data_source_uuid:, + customer_uuid:, **updated_attributes ) end + it 'updates the contact with null external_id correctly' do + updated_contact = described_class.update!(contact_uuid, external_id: nil) + expect(updated_contact).to have_attributes(uuid: contact_uuid, external_id: nil) + end + it 'destroys the contact correctly' do uuid_to_delete = 'con_ab1e60d4-7690-11ee-84d7-f7e55168a5df' deleted_contact = described_class.destroy!(uuid: uuid_to_delete) @@ -111,7 +140,7 @@ from_uuid = 'con_6f0b7208-7690-11ee-8857-9f75f1321afd' contact_result = described_class.merge!( - into_uuid: into_uuid, from_uuid: from_uuid + into_uuid:, from_uuid: ) expect(contact_result).to eq(true) end diff --git a/spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/creates_the_contact_correctly.yml b/spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/creates_the_contact_correctly.yml index 3f959e6..0d61e9b 100644 --- a/spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/creates_the_contact_correctly.yml +++ b/spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/creates_the_contact_correctly.yml @@ -5,7 +5,7 @@ http_interactions: uri: https://api.chartmogul.com/v1/contacts body: encoding: UTF-8 - string: '{"customer_uuid":"cus_23e01538-2c7e-11ee-b2ce-fb986e96e21b","data_source_uuid":"ds_03cfd2c4-2c7e-11ee-ab23-cb0f008cff46","email":"contact@example.com"}' + string: '{"customer_uuid":"cus_23e01538-2c7e-11ee-b2ce-fb986e96e21b","data_source_uuid":"ds_03cfd2c4-2c7e-11ee-ab23-cb0f008cff46","email":"contact@example.com","external_id":"contact_external_id_001"}' headers: User-Agent: - chartmogul-ruby/3.3.1 @@ -30,7 +30,7 @@ http_interactions: - keep-alive body: encoding: UTF-8 - string: '{"uuid":"con_36399f04-7686-11ee-86f6-8727560009c2","customer_uuid":"cus_23e01538-2c7e-11ee-b2ce-fb986e96e21b","customer_external_id":"cus_004","data_source_uuid":"ds_03cfd2c4-2c7e-11ee-ab23-cb0f008cff46","position":1,"first_name":null,"last_name":null,"title":null,"email":"contact@example.com","phone":null,"linked_in":null,"twitter":null,"notes":null,"custom":{}}' + string: '{"uuid":"con_36399f04-7686-11ee-86f6-8727560009c2","customer_uuid":"cus_23e01538-2c7e-11ee-b2ce-fb986e96e21b","customer_external_id":"cus_004","data_source_uuid":"ds_03cfd2c4-2c7e-11ee-ab23-cb0f008cff46","external_id":"contact_external_id_001","position":1,"first_name":null,"last_name":null,"title":null,"email":"contact@example.com","phone":null,"linked_in":null,"twitter":null,"notes":null,"custom":{}}' http_version: recorded_at: Sun, 29 Oct 2023 18:08:50 GMT recorded_with: VCR 5.1.0 diff --git a/spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/creates_the_contact_with_null_external_id_correctly.yml b/spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/creates_the_contact_with_null_external_id_correctly.yml new file mode 100644 index 0000000..64cbfb2 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/creates_the_contact_with_null_external_id_correctly.yml @@ -0,0 +1,36 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.chartmogul.com/v1/contacts + body: + encoding: UTF-8 + string: '{"customer_uuid":"cus_23e01538-2c7e-11ee-b2ce-fb986e96e21b","data_source_uuid":"ds_03cfd2c4-2c7e-11ee-ab23-cb0f008cff46","email":"contact@example.com","external_id":null}' + headers: + User-Agent: + - chartmogul-ruby/3.3.1 + Content-Type: + - application/json + Authorization: + - Basic hidden + response: + status: + code: 201 + message: Created + headers: + access-control-allow-credentials: + - 'true' + content-type: + - application/json + date: + - Sun, 29 Oct 2023 18:08:50 GMT + content-length: + - '379' + connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"uuid":"con_36399f04-7686-11ee-86f6-8727560009c2","customer_uuid":"cus_23e01538-2c7e-11ee-b2ce-fb986e96e21b","customer_external_id":"cus_004","data_source_uuid":"ds_03cfd2c4-2c7e-11ee-ab23-cb0f008cff46","position":1,"first_name":null,"last_name":null,"title":null,"email":"contact@example.com","phone":null,"linked_in":null,"twitter":null,"notes":null,"external_id":null,"custom":{}}' + http_version: + recorded_at: Sun, 29 Oct 2023 18:08:50 GMT +recorded_with: VCR 5.1.0 diff --git a/spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/creates_the_contact_without_external_id_correctly.yml b/spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/creates_the_contact_without_external_id_correctly.yml new file mode 100644 index 0000000..9784f1b --- /dev/null +++ b/spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/creates_the_contact_without_external_id_correctly.yml @@ -0,0 +1,36 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.chartmogul.com/v1/contacts + body: + encoding: UTF-8 + string: '{"customer_uuid":"cus_23e01538-2c7e-11ee-b2ce-fb986e96e21b","data_source_uuid":"ds_03cfd2c4-2c7e-11ee-ab23-cb0f008cff46","email":"contact@example.com"}' + headers: + User-Agent: + - chartmogul-ruby/3.3.1 + Content-Type: + - application/json + Authorization: + - Basic hidden + response: + status: + code: 201 + message: Created + headers: + access-control-allow-credentials: + - 'true' + content-type: + - application/json + date: + - Sun, 29 Oct 2023 18:08:50 GMT + content-length: + - '379' + connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"uuid":"con_36399f04-7686-11ee-86f6-8727560009c2","customer_uuid":"cus_23e01538-2c7e-11ee-b2ce-fb986e96e21b","customer_external_id":"cus_004","data_source_uuid":"ds_03cfd2c4-2c7e-11ee-ab23-cb0f008cff46","position":1,"first_name":null,"last_name":null,"title":null,"email":"contact@example.com","phone":null,"linked_in":null,"twitter":null,"notes":null,"external_id":null,"custom":{}}' + http_version: + recorded_at: Sun, 29 Oct 2023 18:08:50 GMT +recorded_with: VCR 5.1.0 diff --git a/spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/updates_the_contact_correctly_with_the_class_method.yml b/spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/updates_the_contact_correctly_with_the_class_method.yml index b1da988..c9b6b0c 100644 --- a/spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/updates_the_contact_correctly_with_the_class_method.yml +++ b/spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/updates_the_contact_correctly_with_the_class_method.yml @@ -5,7 +5,7 @@ http_interactions: uri: https://api.chartmogul.com/v1/contacts/con_36399f04-7686-11ee-86f6-8727560009c2 body: encoding: UTF-8 - string: '{"position":9,"first_name":"Foo","last_name":"Bar","title":"CTO","email":"contact2@example.com","phone":"+9876543210","linked_in":"https://linkedin.com/about","twitter":"https://twitter.com/about","custom":[{"key":"Toggle","value":false}]}' + string: '{"external_id":"contact_external_id_002","position":9,"first_name":"Foo","last_name":"Bar","title":"CTO","email":"contact2@example.com","phone":"+9876543210","linked_in":"https://linkedin.com/about","twitter":"https://twitter.com/about","custom":[{"key":"Toggle","value":false}]}' headers: User-Agent: - chartmogul-ruby/3.3.1 @@ -30,7 +30,7 @@ http_interactions: - keep-alive body: encoding: UTF-8 - string: '{"uuid":"con_36399f04-7686-11ee-86f6-8727560009c2","customer_uuid":"cus_23e01538-2c7e-11ee-b2ce-fb986e96e21b","customer_external_id":"cus_004","data_source_uuid":"ds_03cfd2c4-2c7e-11ee-ab23-cb0f008cff46","position":9,"first_name":"Foo","last_name":"Bar","title":"CTO","email":"contact2@example.com","phone":"+9876543210","linked_in":"https://linkedin.com/about","twitter":"https://twitter.com/about","notes":"Heading\\nBody\\nFooter","custom":{"Toggle":false}}' + string: '{"uuid":"con_36399f04-7686-11ee-86f6-8727560009c2","customer_uuid":"cus_23e01538-2c7e-11ee-b2ce-fb986e96e21b","customer_external_id":"cus_004","data_source_uuid":"ds_03cfd2c4-2c7e-11ee-ab23-cb0f008cff46","external_id":"contact_external_id_002","position":9,"first_name":"Foo","last_name":"Bar","title":"CTO","email":"contact2@example.com","phone":"+9876543210","linked_in":"https://linkedin.com/about","twitter":"https://twitter.com/about","notes":"Heading\\nBody\\nFooter","custom":{"Toggle":false}}' http_version: recorded_at: Sun, 29 Oct 2023 19:09:59 GMT recorded_with: VCR 5.1.0 diff --git a/spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/updates_the_contact_with_null_external_id_correctly.yml b/spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/updates_the_contact_with_null_external_id_correctly.yml new file mode 100644 index 0000000..dcab3b4 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/updates_the_contact_with_null_external_id_correctly.yml @@ -0,0 +1,36 @@ +--- +http_interactions: +- request: + method: patch + uri: https://api.chartmogul.com/v1/contacts/con_36399f04-7686-11ee-86f6-8727560009c2 + body: + encoding: UTF-8 + string: '{"external_id":null}' + headers: + User-Agent: + - chartmogul-ruby/3.3.1 + Content-Type: + - application/json + Authorization: + - Basic hidden + response: + status: + code: 200 + message: OK + headers: + access-control-allow-credentials: + - 'true' + content-type: + - application/json + date: + - Sun, 29 Oct 2023 19:09:59 GMT + content-length: + - '393' + connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"uuid":"con_36399f04-7686-11ee-86f6-8727560009c2","customer_uuid":"cus_23e01538-2c7e-11ee-b2ce-fb986e96e21b","customer_external_id":"cus_004","data_source_uuid":"ds_03cfd2c4-2c7e-11ee-ab23-cb0f008cff46","position":1,"first_name":null,"last_name":null,"title":null,"email":"contact@example.com","phone":null,"linked_in":null,"twitter":null,"notes":null,"external_id":null,"custom":{}}' + http_version: + recorded_at: Sun, 29 Oct 2023 19:09:59 GMT +recorded_with: VCR 5.1.0 From 87e8367dac2578b175c230fbfbe5206e700e943b Mon Sep 17 00:00:00 2001 From: Keith Dawson Date: Fri, 13 Mar 2026 17:32:01 +0900 Subject: [PATCH 2/3] Responded to PR feedback --- spec/chartmogul/contact_spec.rb | 34 +++++++++---------- .../creates_the_contact_correctly.yml} | 0 .../creates_the_contact_correctly.yml} | 0 3 files changed, 17 insertions(+), 17 deletions(-) rename spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/{creates_the_contact_with_null_external_id_correctly.yml => with_null_external_id/creates_the_contact_correctly.yml} (100%) rename spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/{creates_the_contact_without_external_id_correctly.yml => without_external_id/creates_the_contact_correctly.yml} (100%) diff --git a/spec/chartmogul/contact_spec.rb b/spec/chartmogul/contact_spec.rb index 61f59e1..844a427 100644 --- a/spec/chartmogul/contact_spec.rb +++ b/spec/chartmogul/contact_spec.rb @@ -3,6 +3,13 @@ require 'spec_helper' describe ChartMogul::Contact do + shared_examples 'creates contact with nil external_id' do + it 'creates the contact correctly' do + contact = described_class.create!(**create_attributes) + expect(contact).to have_attributes(uuid: contact_uuid, external_id: nil) + end + end + let(:attrs) do { uuid: contact_uuid, @@ -90,25 +97,18 @@ expect(contact).to have_attributes(uuid: contact_uuid, **attributes) end - it 'creates the contact with null external_id correctly' do - attributes = { - customer_uuid:, - data_source_uuid:, - email: 'contact@example.com', - external_id: nil - } - contact = described_class.create!(**attributes) - expect(contact).to have_attributes(uuid: contact_uuid, external_id: nil) + context 'with null external_id' do + let(:create_attributes) do + { customer_uuid:, data_source_uuid:, email: 'contact@example.com', external_id: nil } + end + include_examples 'creates contact with nil external_id' end - it 'creates the contact without external_id correctly' do - attributes = { - customer_uuid:, - data_source_uuid:, - email: 'contact@example.com' - } - contact = described_class.create!(**attributes) - expect(contact).to have_attributes(uuid: contact_uuid, **attributes, external_id: nil) + context 'without external_id' do + let(:create_attributes) do + { customer_uuid:, data_source_uuid:, email: 'contact@example.com' } + end + include_examples 'creates contact with nil external_id' end it 'updates the contact correctly with the class method' do diff --git a/spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/creates_the_contact_with_null_external_id_correctly.yml b/spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/with_null_external_id/creates_the_contact_correctly.yml similarity index 100% rename from spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/creates_the_contact_with_null_external_id_correctly.yml rename to spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/with_null_external_id/creates_the_contact_correctly.yml diff --git a/spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/creates_the_contact_without_external_id_correctly.yml b/spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/without_external_id/creates_the_contact_correctly.yml similarity index 100% rename from spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/creates_the_contact_without_external_id_correctly.yml rename to spec/fixtures/vcr_cassettes/ChartMogul_Contact/API_Actions/without_external_id/creates_the_contact_correctly.yml From 13f440120d1f2e5d103cf4d504c25c324f2dd6d6 Mon Sep 17 00:00:00 2001 From: Keith Dawson Date: Fri, 13 Mar 2026 21:35:20 +0900 Subject: [PATCH 3/3] Incremented library version to 4.12.0 --- changelog.md | 3 +++ lib/chartmogul/version.rb | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index bc39287..b9eede6 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,8 @@ # chartmogul-ruby Change Log +## Version 4.12.0 - Mar 16, 2026 +- Add `external_id` key to Contact model + ## Version 4.11.0 - Jan 6, 2026 - Add new API usage for Customer Subscriptions connect and disconnect - Update retrieve to accept query params diff --git a/lib/chartmogul/version.rb b/lib/chartmogul/version.rb index 796a105..c7db46c 100644 --- a/lib/chartmogul/version.rb +++ b/lib/chartmogul/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module ChartMogul - VERSION = '4.11.0' + VERSION = '4.12.0' end