diff --git a/apps/api/src/jobs/__tests__/import-processor.test.ts b/apps/api/src/jobs/__tests__/import-processor.test.ts index e52b140..2e9998f 100644 --- a/apps/api/src/jobs/__tests__/import-processor.test.ts +++ b/apps/api/src/jobs/__tests__/import-processor.test.ts @@ -283,11 +283,11 @@ describe('Contact Import - Subscription Status Preservation', () => { describe('coerceCustomValue', () => { describe('boolean coercion', () => { - it.each(['true', 'TRUE', 'True', ' true ', '1', 'yes', 'YES', 'Yes'])('coerces %j to true', value => { + it.each(['true', 'TRUE', 'True', ' true ', 'yes', 'YES', 'Yes'])('coerces %j to true', value => { expect(coerceCustomValue(value)).toBe(true); }); - it.each(['false', 'FALSE', 'False', ' false ', '0', 'no', 'NO', 'No'])('coerces %j to false', value => { + it.each(['false', 'FALSE', 'False', ' false ', 'no', 'NO', 'No'])('coerces %j to false', value => { expect(coerceCustomValue(value)).toBe(false); }); }); @@ -300,6 +300,8 @@ describe('coerceCustomValue', () => { [' 42 ', 42], ['0.5', 0.5], ['-0.25', -0.25], + ['0', 0], + ['1', 1], ])('coerces %j to %j', (value, expected) => { expect(coerceCustomValue(value)).toBe(expected); }); @@ -311,14 +313,6 @@ describe('coerceCustomValue', () => { }, ); - it('keeps boolean precedence: "1" stays boolean true, not number 1', () => { - expect(coerceCustomValue('1')).toBe(true); - }); - - it('keeps boolean precedence: "0" stays boolean false, not number 0', () => { - expect(coerceCustomValue('0')).toBe(false); - }); - it('"1.0" is a number (does not match the boolean truthy set)', () => { expect(coerceCustomValue('1.0')).toBe(1); }); diff --git a/apps/api/src/jobs/import-processor.ts b/apps/api/src/jobs/import-processor.ts index d66f540..94ff3f9 100644 --- a/apps/api/src/jobs/import-processor.ts +++ b/apps/api/src/jobs/import-processor.ts @@ -221,8 +221,12 @@ function isValidEmail(email: string): boolean { return emailRegex.test(email); } -const BOOLEAN_TRUE = new Set(['true', '1', 'yes']); -const BOOLEAN_FALSE = new Set(['false', '0', 'no']); +// `0` and `1` are intentionally absent: in custom columns they are far more +// often counts/ids/quantities than boolean flags, so they fall through to +// numeric coercion below. The reserved `subscribed` column has its own +// parser above that still accepts `1`/`0` as booleans. +const BOOLEAN_TRUE = new Set(['true', 'yes']); +const BOOLEAN_FALSE = new Set(['false', 'no']); // Strict integer-or-decimal pattern. Rejects leading zeros (preserves IDs, // zips, phone numbers), scientific notation, `+` prefix, and `.5` / `42.`. const NUMERIC_RE = /^-?(0|[1-9]\d*)(\.\d+)?$/; @@ -230,9 +234,8 @@ const NUMERIC_RE = /^-?(0|[1-9]\d*)(\.\d+)?$/; /** * Coerce a raw CSV cell to its natural JSON primitive so post-import type * inference (ContactService.getAvailableFields) can detect booleans and - * numbers on custom fields the same way it already does for the reserved - * `subscribed` column. Values that match neither recogniser are returned - * unchanged. + * numbers on custom fields. Values that match neither recogniser are + * returned unchanged. */ export function coerceCustomValue(value: string): string | boolean | number { const trimmed = value.trim();