Shopify / shopify-api-ruby

ShopifyAPI is a lightweight gem for accessing the Shopify admin REST and GraphQL web services.
MIT License
1.06k stars 473 forks source link

Cannot update email marketing consent state if opt-in level doesn't change #1287

Closed ClaytonPassmore closed 3 months ago

ClaytonPassmore commented 8 months ago

Issue summary

Before opening this issue, I have:

When updating a customer's email marketing consent state, the Shopify API seems to require both the state and opt_in_level. However, if the opt_in_level does not change, the gem does not send up the opt_in_level part of the hash, which results in a 422.

Aside: The process of updating a customer's email marketing consent is not well documented. The official REST docs have an example titled "update a customer's marketing opt-in state" but it uses the old, deprecated parameters.

Expected behavior

The gem should send up all required parameters, regardless of whether or not they change.

Actual behavior

The gem only sends up fields that change, this makes it impossible to update a customer's email marketing consent state when the opt-in level doesn't change.

Steps to reproduce the problem

  1. Fetch a customer from Shopify
  2. Create a new hash for the email_marketing_consent field that contains all the same attributes, but a different state (e.g. use "subscribed" if the customer was not subscribed)
  3. Try to save the customer

Alternatively, try adding these tests to the test/rest/2024_01/customer_test.rb file.

The first test is a "control" to ensure the test works as expected when the opt-in level changes. This test should pass.

  sig do
    void
  end
  def test_x_control
    session = ShopifyAPI::Auth::Session.new(
      shop: "test-shop.myshopify.io",
      access_token: "this_is_a_test_token"
    )

    # Simulate fetching an existing customer via the API:
    customer = ShopifyAPI::Customer.create_instance(
      session: session,
      data: {
        "id" => 207119551,
        "email_marketing_consent" => {
          "state" => "not_subscribed",
          "opt_in_level" => "confirmed_opt_in",
          "consent_updated_at" => "2023-01-01T00:00:00.000Z"
        }
      }
    )

    # Stub update with full `email_marketing_consent` hash included:
    stub_request(:put, "https://test-shop.myshopify.io/admin/api/2024-01/customers/207119551.json")
      .with(
        # This stub matches because `opt_in_level` changes.
        body: { "customer" => hash_including({"email_marketing_consent" => {"state" => "subscribed", "opt_in_level" => "single_opt_in", "consent_updated_at" => "2024-01-01T00:00:00.000Z"}}) }
      )
      .to_return(status: 200, body: JSON.generate({"customer" => {"email" => "bob.norman@mail.example.com", "first_name" => "Bob", "last_name" => "Norman", "id" => 207119551, "accepts_marketing" => false, "created_at" => "2024-01-02T09:24:29-05:00", "updated_at" => "2024-01-02T09:24:29-05:00", "orders_count" => 1, "state" => "disabled", "total_spent" => "199.65", "last_order_id" => 450789469, "note" => nil, "verified_email" => true, "multipass_identifier" => nil, "tax_exempt" => false, "tags" => "L\u00E9on, No\u00EBl", "last_order_name" => "#1001", "currency" => "USD", "phone" => "+16136120707", "addresses" => [{"id" => 207119551, "customer_id" => 207119551, "first_name" => nil, "last_name" => nil, "company" => nil, "address1" => "Chestnut Street 92", "address2" => "", "city" => "Louisville", "province" => "Kentucky", "country" => "United States", "zip" => "40202", "phone" => "555-625-1199", "name" => "", "province_code" => "KY", "country_code" => "US", "country_name" => "United States", "default" => true}], "accepts_marketing_updated_at" => "2005-06-12T11:57:11-04:00", "marketing_opt_in_level" => nil, "tax_exemptions" => [], "email_marketing_consent" => {"state" => "not_subscribed", "opt_in_level" => nil, "consent_updated_at" => "2004-06-13T11:57:11-04:00"}, "sms_marketing_consent" => {"state" => "not_subscribed", "opt_in_level" => "single_opt_in", "consent_updated_at" => "2024-01-02T09:24:29-05:00", "consent_collected_from" => "OTHER"}, "admin_graphql_api_id" => "gid://shopify/Customer/207119551", "default_address" => {"id" => 207119551, "customer_id" => 207119551, "first_name" => nil, "last_name" => nil, "company" => nil, "address1" => "Chestnut Street 92", "address2" => "", "city" => "Louisville", "province" => "Kentucky", "country" => "United States", "zip" => "40202", "phone" => "555-625-1199", "name" => "", "province_code" => "KY", "country_code" => "US", "country_name" => "United States", "default" => true}}}), headers: {})

    customer.email_marketing_consent = {
      "state" => "subscribed",
      "opt_in_level" => "single_opt_in",
      "consent_updated_at" => "2024-01-01T00:00:00.000Z"
    }

    # Trigger put request which will match the stubbed request.
    customer.save

    assert_requested(:put, "https://test-shop.myshopify.io/admin/api/2024-01/customers/207119551.json")
  end

The second test is the same as the first, the only difference is that the opt-in level stays the same. This test currently fails (but should pass when the bug gets fixed).

  sig do
    void
  end
  def test_x_broken
    session = ShopifyAPI::Auth::Session.new(
      shop: "test-shop.myshopify.io",
      access_token: "this_is_a_test_token"
    )

    # Simulate fetching an existing customer via the API:
    customer = ShopifyAPI::Customer.create_instance(
      session: session,
      data: {
        "id" => 207119551,
        "email_marketing_consent" => {
          "state" => "not_subscribed",
          "opt_in_level" => "single_opt_in",
          "consent_updated_at" => "2023-01-01T00:00:00.000Z"
        }
      }
    )

    # Stub update with full `email_marketing_consent` hash included:
    stub_request(:put, "https://test-shop.myshopify.io/admin/api/2024-01/customers/207119551.json")
      .with(
        # This stub isn't going to match because the `opt_in_level` doesn't change and therefore isn't sent up.
        body: { "customer" => hash_including({"email_marketing_consent" => {"state" => "subscribed", "opt_in_level" => "single_opt_in", "consent_updated_at" => "2024-01-01T00:00:00.000Z"}}) }
      )
      .to_return(status: 200, body: JSON.generate({"customer" => {"email" => "bob.norman@mail.example.com", "first_name" => "Bob", "last_name" => "Norman", "id" => 207119551, "accepts_marketing" => false, "created_at" => "2024-01-02T09:24:29-05:00", "updated_at" => "2024-01-02T09:24:29-05:00", "orders_count" => 1, "state" => "disabled", "total_spent" => "199.65", "last_order_id" => 450789469, "note" => nil, "verified_email" => true, "multipass_identifier" => nil, "tax_exempt" => false, "tags" => "L\u00E9on, No\u00EBl", "last_order_name" => "#1001", "currency" => "USD", "phone" => "+16136120707", "addresses" => [{"id" => 207119551, "customer_id" => 207119551, "first_name" => nil, "last_name" => nil, "company" => nil, "address1" => "Chestnut Street 92", "address2" => "", "city" => "Louisville", "province" => "Kentucky", "country" => "United States", "zip" => "40202", "phone" => "555-625-1199", "name" => "", "province_code" => "KY", "country_code" => "US", "country_name" => "United States", "default" => true}], "accepts_marketing_updated_at" => "2005-06-12T11:57:11-04:00", "marketing_opt_in_level" => nil, "tax_exemptions" => [], "email_marketing_consent" => {"state" => "not_subscribed", "opt_in_level" => nil, "consent_updated_at" => "2004-06-13T11:57:11-04:00"}, "sms_marketing_consent" => {"state" => "not_subscribed", "opt_in_level" => "single_opt_in", "consent_updated_at" => "2024-01-02T09:24:29-05:00", "consent_collected_from" => "OTHER"}, "admin_graphql_api_id" => "gid://shopify/Customer/207119551", "default_address" => {"id" => 207119551, "customer_id" => 207119551, "first_name" => nil, "last_name" => nil, "company" => nil, "address1" => "Chestnut Street 92", "address2" => "", "city" => "Louisville", "province" => "Kentucky", "country" => "United States", "zip" => "40202", "phone" => "555-625-1199", "name" => "", "province_code" => "KY", "country_code" => "US", "country_name" => "United States", "default" => true}}}), headers: {})

    customer.email_marketing_consent = {
      "state" => "subscribed",
      "opt_in_level" => "single_opt_in",
      "consent_updated_at" => "2024-01-01T00:00:00.000Z"
    }

    # Trigger put request which won't match the stubbed request.
    customer.save

    assert_requested(:put, "https://test-shop.myshopify.io/admin/api/2024-01/customers/207119551.json")
  end

Debug logs

// Paste any relevant logs here
matteodepalo commented 8 months ago

Hi @ClaytonPassmore, thank you for opening an issue with this level of detail! I'm going to add it to our internal workflow.

ClaytonPassmore commented 8 months ago

Awesome, thank you @matteodepalo 🙏

paulomarg commented 3 months ago

Thanks for providing the failing test, that was super helpful in fixing this!

ClaytonPassmore commented 3 months ago

Happy to help @paulomarg! Looking forward to seeing the fix merged 🥳