diff --git a/docs/USER_PROFILE_PREFERENCES_API.md b/docs/USER_PROFILE_PREFERENCES_API.md index d5f42ad..af571d6 100644 --- a/docs/USER_PROFILE_PREFERENCES_API.md +++ b/docs/USER_PROFILE_PREFERENCES_API.md @@ -247,7 +247,7 @@ Updates all user preferences. - `digestFrequency`: must be one of `daily`, `weekly`, `monthly`, `never` - `theme`: must be one of `light`, `dark`, `system` - `language`: must be a valid BCP 47 language code -- `currencyPreference`: must be 3-character currency code +- `currencyPreference`: must be a valid ISO 4217 currency code (exactly 3 uppercase letters, e.g. `USD`, `EUR`, `GBP`) **Response (200):** ```json diff --git a/src/controllers/__tests__/preferences.controller.test.ts b/src/controllers/__tests__/preferences.controller.test.ts index b6ad067..6b57b0d 100644 --- a/src/controllers/__tests__/preferences.controller.test.ts +++ b/src/controllers/__tests__/preferences.controller.test.ts @@ -303,6 +303,32 @@ describe('Preferences Controller', () => { ); }); + it('should accept valid ISO 4217 currency codes', async () => { + for (const code of ['USD', 'EUR', 'GBP', 'JPY', 'NGN']) { + const res = await request(app) + .post('/api/preferences') + .set('Authorization', `Bearer ${userToken}`) + .send({ currencyPreference: code }) + .expect(202); + + expect(res.body.data.currencyPreference).toBe(code); + + await prisma.userPreferences.deleteMany({ where: { userId: testUserId } }); + } + }); + + it('should reject invalid currency preference values', async () => { + for (const bad of ['usd', 'US', 'USDD', '123', 'abc', 'u s']) { + const res = await request(app) + .post('/api/preferences') + .set('Authorization', `Bearer ${userToken}`) + .send({ currencyPreference: bad }) + .expect(422); + + expect(res.body.errors).toHaveProperty('currencyPreference'); + } + }); + it('should validate preference values', async () => { const res = await request(app) .post('/api/preferences') diff --git a/src/utils/profileValidators.ts b/src/utils/profileValidators.ts index 378031c..be455b2 100644 --- a/src/utils/profileValidators.ts +++ b/src/utils/profileValidators.ts @@ -49,7 +49,7 @@ export const preferencesValidationRules: InitialRules = { digestFrequency: ['string', 'in:daily,weekly,monthly,never'], theme: ['string', 'in:light,dark,system'], language: ['string', 'bcp47'], - currencyPreference: ['string', 'min:3,max:3'], + currencyPreference: ['string', 'iso4217'], twoFactorEnabled: ['boolean'], dataCollectionConsent: ['boolean'], analyticsTracking: ['boolean'], diff --git a/src/utils/validator.ts b/src/utils/validator.ts index 76e3b87..8b10a4f 100644 --- a/src/utils/validator.ts +++ b/src/utils/validator.ts @@ -45,11 +45,13 @@ setTranslationObject({ exists: 'The selected :attribute does not exist.', bcp47: 'The :attribute must be a valid BCP 47 language code.', ianaTimezone: 'The :attribute must be a valid IANA time zone identifier.', + iso4217: 'The :attribute must be a valid ISO 4217 currency code (3 uppercase letters, e.g. USD).', } }); export const BCP47_REGEX = /^[a-z]{2,3}(?:-[a-z]{3})?(?:-(?:[a-z]{4}|[a-z]{2}|[0-9]{3}))?(?:-(?:[a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*$/i; export const IANA_TIMEZONE_REGEX = /^[A-Za-z][A-Za-z0-9_\/-]*$/; +export const ISO4217_REGEX = /^[A-Z]{3}$/; register('unique', async function (value, parameters, attribute) { const [modelName, field, except, exceptField] = parameters ?? []; @@ -69,6 +71,14 @@ register('bcp47', function (value) { return BCP47_REGEX.test(value); }); +register('iso4217', function (value) { + if (typeof value !== 'string' || !value.trim()) { + return false; + } + + return ISO4217_REGEX.test(value); +}); + register('ianaTimezone', function (value) { if (typeof value !== 'string' || !value.trim()) { return false;