diff --git a/apps/api/domain/whatsapp_number.go b/apps/api/domain/whatsapp_number.go index 7047cebf..ec310f32 100644 --- a/apps/api/domain/whatsapp_number.go +++ b/apps/api/domain/whatsapp_number.go @@ -10,11 +10,13 @@ func NormalizeWhatsappNumber(raw string) (string, *Error) { stripped := whatsappNumberSeparators.Replace(raw) var normalized string + hasExplicitCountryCode := false switch { case strings.HasPrefix(stripped, "0"): normalized = "62" + stripped[1:] case strings.HasPrefix(stripped, "+"): normalized = strings.TrimPrefix(stripped, "+") + hasExplicitCountryCode = true default: normalized = stripped } @@ -33,6 +35,10 @@ func NormalizeWhatsappNumber(raw string) (string, *Error) { return normalized, nil } + if !hasExplicitCountryCode { + return "", invalidWhatsappNumberError() + } + if len(normalized) < 8 || len(normalized) > 15 { return "", invalidWhatsappNumberError() } diff --git a/apps/api/domain/whatsapp_number_test.go b/apps/api/domain/whatsapp_number_test.go index 70a7fa12..44d1d21b 100644 --- a/apps/api/domain/whatsapp_number_test.go +++ b/apps/api/domain/whatsapp_number_test.go @@ -16,6 +16,8 @@ func TestNormalizeWhatsappNumber(t *testing.T) { }{ {name: "leading 0 becomes 62", raw: "0812-3456-7890", expected: "6281234567890"}, {name: "628 prefix is unchanged", raw: "6281234567890", expected: "6281234567890"}, + {name: "62 prefix without a plus is accepted", raw: "6285335473895", expected: "6285335473895"}, + {name: "0 prefix converts to the same number", raw: "085335473895", expected: "6285335473895"}, {name: "+628 drops the plus", raw: "+62 812 3456 7890", expected: "6281234567890"}, {name: "other country plus is dropped, kept if 8-15 digits", raw: "+6591234567", expected: "6591234567"}, {name: "dots and parentheses are stripped", raw: "(0812).3456.7890", expected: "6281234567890"}, @@ -33,6 +35,9 @@ func TestNormalizeWhatsappNumber(t *testing.T) { {name: "non-62 number at the 15 digit ceiling is accepted", raw: "+123456789012345", expected: "123456789012345"}, {name: "non-62 number over 15 digits is rejected", raw: "+1234567890123456", expectsErr: true}, {name: "bare plus sign is rejected", raw: "+", expectsErr: true}, + {name: "number without 0 or 62 prefix is rejected", raw: "85335473895", expectsErr: true}, + {name: "number without prefix and with separators is rejected", raw: "853-3547-3895", expectsErr: true}, + {name: "non-62 number without a plus is rejected", raw: "6591234567", expectsErr: true}, } for _, tt := range tests { diff --git a/libs/ui/src/domain/entities/Customer.test.ts b/libs/ui/src/domain/entities/Customer.test.ts index 01cf4a11..d04156c7 100644 --- a/libs/ui/src/domain/entities/Customer.test.ts +++ b/libs/ui/src/domain/entities/Customer.test.ts @@ -11,6 +11,8 @@ describe('normalizeWhatsappNumber', () => { }[] = [ { name: 'leading 0 becomes 62', raw: '0812-3456-7890', expected: '6281234567890' }, { name: '628 prefix is unchanged', raw: '6281234567890', expected: '6281234567890' }, + { name: '62 prefix without a plus is accepted', raw: '6285335473895', expected: '6285335473895' }, + { name: '0 prefix converts to the same number', raw: '085335473895', expected: '6285335473895' }, { name: '+628 drops the plus', raw: '+62 812 3456 7890', expected: '6281234567890' }, { name: 'other country plus is dropped, kept if 8-15 digits', raw: '+6591234567', expected: '6591234567' }, { name: 'dots and parentheses are stripped', raw: '(0812).3456.7890', expected: '6281234567890' }, @@ -28,6 +30,9 @@ describe('normalizeWhatsappNumber', () => { { name: 'non-62 number at the 15 digit ceiling is accepted', raw: '+123456789012345', expected: '123456789012345' }, { name: 'non-62 number over 15 digits is rejected', raw: '+1234567890123456', expected: null }, { name: 'bare plus sign is rejected', raw: '+', expected: null }, + { name: 'number without 0 or 62 prefix is rejected', raw: '85335473895', expected: null }, + { name: 'number without prefix and with separators is rejected', raw: '853-3547-3895', expected: null }, + { name: 'non-62 number without a plus is rejected', raw: '6591234567', expected: null }, ]; it.each(cases)('$name', ({ raw, expected }) => { @@ -40,8 +45,8 @@ describe('formatWhatsappNumberForInput', () => { expect(formatWhatsappNumberForInput('6281234567890')).toBe('081234567890'); }); - it('leaves a non-Indonesian number unchanged', () => { - expect(formatWhatsappNumberForInput('6591234567')).toBe('6591234567'); + it('shows a non-Indonesian number with a plus so it stays valid', () => { + expect(formatWhatsappNumberForInput('6591234567')).toBe('+6591234567'); }); it('leaves an empty string unchanged', () => { diff --git a/libs/ui/src/domain/entities/Customer.ts b/libs/ui/src/domain/entities/Customer.ts index 452c1acb..57f3c37f 100644 --- a/libs/ui/src/domain/entities/Customer.ts +++ b/libs/ui/src/domain/entities/Customer.ts @@ -9,10 +9,12 @@ export function normalizeWhatsappNumber(raw: string): string | null { const stripped = raw.replace(WHATSAPP_NUMBER_SEPARATORS, ''); let normalized: string; + let hasExplicitCountryCode = false; if (stripped.startsWith('0')) { normalized = '62' + stripped.slice(1); } else if (stripped.startsWith('+')) { normalized = stripped.slice(1); + hasExplicitCountryCode = true; } else { normalized = stripped; } @@ -25,12 +27,15 @@ export function normalizeWhatsappNumber(raw: string): string | null { return normalized; } + if (!hasExplicitCountryCode) return null; + if (normalized.length < 8 || normalized.length > 15) return null; return normalized; } export function formatWhatsappNumberForInput(normalized: string): string { + if (normalized === '') return normalized; if (normalized.startsWith('62')) return '0' + normalized.slice(2); - return normalized; + return '+' + normalized; }