Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/normalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 5 additions & 5 deletions src/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<GoogleUserIdentifier['addressInfo']> = {};
Expand All @@ -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 });
Expand Down
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
6 changes: 6 additions & 0 deletions test/normalizer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading