diff --git a/src/normalizer.ts b/src/normalizer.ts index d134e11..ac79391 100644 --- a/src/normalizer.ts +++ b/src/normalizer.ts @@ -285,7 +285,9 @@ export function hashCustomer(raw: RawCustomer): HashedCustomer | undefined { hashed.lastName = sha256Hex(lastNorm); } if (countryNorm !== undefined) { + // Meta wants the country hashed; Google wants it in plain text. Keep both. hashed.country = sha256Hex(countryNorm); + hashed.countryPlain = countryNorm; } if (zipNorm !== undefined) { hashed.zip = zipNorm; diff --git a/src/sync.ts b/src/sync.ts index e9a9942..b88644d 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -248,7 +248,7 @@ function toGoogleIdentifiers(customer: HashedCustomer): GoogleUserIdentifier[] { if ( customer.firstName !== undefined || customer.lastName !== undefined || - customer.country !== undefined || + customer.countryPlain !== undefined || customer.zip !== undefined ) { const addressInfo: NonNullable = {}; @@ -258,10 +258,10 @@ function toGoogleIdentifiers(customer: HashedCustomer): GoogleUserIdentifier[] { if (customer.lastName !== undefined) { Object.assign(addressInfo, { hashedLastName: customer.lastName }); } - if (customer.country !== undefined) { - // Google's addressInfo.countryCode is plain ISO; we don't have plain country here (it was - // hashed for Meta parity), so only emit when present as a hash is not valid. Skip to stay - // spec-compliant — country/zip address matching still works via zip below. + if (customer.countryPlain !== undefined) { + // Google's addressInfo.countryCode is plain-text ISO alpha-2 (NOT hashed). We carry the plain + // value in `countryPlain` precisely so address matching includes the country. + Object.assign(addressInfo, { countryCode: customer.countryPlain }); } if (customer.zip !== undefined) { Object.assign(addressInfo, { postalCode: customer.zip }); diff --git a/src/types.ts b/src/types.ts index 0f4bf94..8b1528d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -58,7 +58,14 @@ export interface HashedCustomer { readonly phone?: Sha256Hex; readonly firstName?: Sha256Hex; readonly lastName?: Sha256Hex; + /** Hashed ISO alpha-2 country, used by Meta (which hashes the COUNTRY column). */ readonly country?: Sha256Hex; + /** + * Plain (un-hashed) ISO alpha-2 country. Google's `addressInfo.countryCode` must be plain text, + * not hashed, so we keep both representations: `country` (hashed) for Meta, `countryPlain` for + * Google. Country is not PII on its own, so retaining it in the clear is safe. + */ + readonly countryPlain?: string; /** Google permits postal code in plain text (not hashed). Undefined when absent. */ readonly zip?: string; } diff --git a/test/normalizer.test.ts b/test/normalizer.test.ts index f2d05fe..2851fb6 100644 --- a/test/normalizer.test.ts +++ b/test/normalizer.test.ts @@ -144,6 +144,12 @@ describe('hashCustomer', () => { expect(hashed?.country).toBe(refHash('US')); }); + it('keeps country in both forms: hashed for Meta, plain ISO for Google', () => { + const hashed = hashCustomer({ email: 'a@b.com', country: ' us ' }); + expect(hashed?.country).toBe(refHash('US')); // Meta column (hashed) + expect(hashed?.countryPlain).toBe('US'); // Google addressInfo.countryCode (plain) + }); + it('drops records with neither email nor phone', () => { expect(hashCustomer({ firstName: 'Jane' })).toBeUndefined(); expect(hashCustomer({ email: 'not-an-email', phone: 'abc' })).toBeUndefined();