[Shopify] Fix customer matching when phone numbers contain spaces - #10256
[Shopify] Fix customer matching when phone numbers contain spaces#10256Onat Buyukakkus (onbuyuka) wants to merge 2 commits into
Conversation
Fixes AB#646794 When exporting a Business Central customer to Shopify, the connector searched existing Shopify customers by email and then by phone. Shopify's search syntax treats whitespace as a term separator (implicit AND), so a formatted phone number such as "+45 4545 4545" was parsed as `phone:+45` plus two loose "4545" terms and could match an unrelated customer. The connector then accepted that customer id and skipped the export with the misleading message "Customer already exists with the same e-mail or phone.", even though both the email and phone were unique. The result also depended on export order. Fix: - Normalize the phone number to '+' and digits before sending it to Shopify so the whole number is scoped to the phone filter as a single, whitespace-free term (Shpfy Customer API: FindIdByPhone + new FormatPhoneNo helper). - Only accept a search result when the returned customer's phone/email matches the requested value exactly, so a tokenized or partial Shopify search result is no longer treated as an existing customer (FindIdByPhone / FindIdByEmail). - Return defaultPhoneNumber / defaultEmailAddress from the find-by-phone and find-by-email GraphQL queries so the match can be verified. Tests: - Added Shpfy Customer API tests covering spaced international phone numbers, tokenized non-matches, exact matches, and case-insensitive email matching. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 89e1180c-4ba0-4612-9f7c-d7686ccecd2b
| @@ -1,2 +1,2 @@ | |||
| # cost: 3 | |||
| {"query":"{customers(first: 1, query:\"email:{{EMail}}\") {edges {node {id}}}}"} No newline at end of file | |||
| {"query":"{customers(first: 1, query:\"email:{{EMail}}\") {edges {node {id defaultEmailAddress { emailAddress }}}}}"} No newline at end of file | |||
There was a problem hiding this comment.
FindIdByEmail now rejects a returned customer unless its defaultEmailAddress.emailAddress matches the requested e-mail exactly, but the backing GraphQL resource still requests only customers(first: 1, ...). If Shopify's tokenized search ranks a non-matching customer first, the exact-match check now correctly rejects it, but the code never looks at any further candidates, so an existing customer with the exact e-mail (ranked lower) is missed and reported as not found. Consider requesting more than one candidate (or paging) before giving up, so a real match ranked behind a partial match is still found.
Agent judgement — not directly backed by a BCQuality knowledge article.
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.33.4
| @@ -1,2 +1,2 @@ | |||
| # cost: 3 | |||
| {"query":"{customers(first: 1, query:\"phone:{{Phone}}\") {edges {node {id}}}}"} No newline at end of file | |||
| {"query":"{customers(first: 1, query:\"phone:{{Phone}}\") {edges {node {id defaultPhoneNumber { phoneNumber }}}}}"} No newline at end of file | |||
There was a problem hiding this comment.
FindIdByPhone now normalizes the requested phone number and verifies the returned defaultPhoneNumber.phoneNumber matches exactly, but the backing GraphQL resource still requests only customers(first: 1, ...). If Shopify's tokenized search (the same tokenization behavior the accompanying code comment describes) ranks a non-matching customer first, the real matching customer further down the result set is never inspected and the lookup incorrectly reports 'not found'. Consider requesting more than one candidate (or paging) before concluding no match exists.
Agent judgement — not directly backed by a BCQuality knowledge article.
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.33.4
The four HTTP-mocked FindIdByPhone/FindIdByEmail tests call
ShpfyInitializeTest.CreateShop(), which creates GL accounts via
LibraryERM and therefore needs the demo General Posting Setup that is
only available in the IntegrationTest environment. Without a TestType
the codeunit ran in the Default bucket, so CreateShop() failed with
"There is no General Posting Setup within the filter" and the remaining
tests then saw a blank Shop ("Shopify Admin URL must have a value").
Set TestType = IntegrationTest to match the sibling Shpfy Create
Customer Test / Shpfy Customer Mapping Test codeunits.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 89e1180c-4ba0-4612-9f7c-d7686ccecd2b
| SearchEMail: Text; | ||
| begin | ||
| if EMail <> '' then begin | ||
| Parameters.Add('EMail', EMail.ToLower()); |
There was a problem hiding this comment.
FindIdByEmail and FindIdByPhone keep the same signatures, but they now reject a Shopify search result unless the returned e-mail/phone matches exactly, whereas before any tokenized/partial search hit was accepted as a match. For existing Shopify Connector installs this is a semantic behavior change: flows that previously auto-matched an existing customer via a loose match can now return 0 and treat that customer as new, changing synchronization behavior and risking duplicate customer creation. This is a legitimate correctness fix, but consider documenting the new exact-match requirement (e.g. release/upgrade note) so integrators are aware existing loosely-matched customers may no longer be recognized.
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.33.4
| if EMail <> '' then begin | ||
| Parameters.Add('EMail', EMail.ToLower()); | ||
| SearchEMail := EMail.ToLower(); | ||
| Parameters.Add('EMail', SearchEMail); |
There was a problem hiding this comment.
The new exact-match guards treat a missing or null Shopify match field as "not found" instead of as an API/response anomaly. JsonHelper.GetValueAsText returns empty text for an absent path, so if defaultEmailAddress.emailAddress or defaultPhoneNumber.phoneNumber is missing from the response, the lookup silently falls through and returns 0, making a malformed/unexpected Shopify response indistinguishable from a genuine no-match. Consider validating that the comparison field is actually present before concluding there is no match, so a shape change in the Shopify response doesn't silently mask itself as "customer not found".
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.33.4
| end; | ||
|
|
||
| [HttpClientHandler] | ||
| internal procedure HttpClientHandler(Request: TestHttpRequestMessage; var Response: TestHttpResponseMessage): Boolean |
There was a problem hiding this comment.
The new FindIdByPhone/FindIdByEmail integration tests only assert the returned customer id against a mocked response; the HttpClientHandler never verifies the outbound GraphQL search term that was actually sent. A regression in the new e-mail lowercasing or phone-normalization logic (SearchEMail/SearchPhone/FormatPhoneNo) would still pass these tests as long as the canned response contains a matching customer, since the request payload itself is unchecked. Extend the handler or the tests to assert the normalized search value sent to Shopify for both the e-mail and phone lookups.
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.33.4
Fixes AB#646794
Problem
When exporting a Business Central customer to Shopify, the connector searches existing Shopify customers by email and then by phone. Shopify's search syntax treats whitespace as a term separator (implicit AND), so a formatted phone number such as
+45 4545 4545is parsed asphone:+45plus two loose4545terms and can match an unrelated customer. The connector then accepts that customer id and skips the export with the misleading message "Customer already exists with the same e-mail or phone." — even though both the email and phone are unique. The outcome also depended on export order.Fix
+and digits before sending it to Shopify, so the whole number is scoped to thephone:filter as a single, whitespace-free term (Shpfy Customer API—FindIdByPhone+ newFormatPhoneNohelper).FindIdByPhone/FindIdByEmail).FindCustomerIdByPhone.graphqlandFindCustomerIdByEMail.graphqlnow returndefaultPhoneNumber/defaultEmailAddressso the match can be verified.Tests
Added
Shpfy Customer APItests (HTTP-mocked) covering: