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: 1 addition & 1 deletion docs/USER_PROFILE_PREFERENCES_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions src/controllers/__tests__/preferences.controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion src/utils/profileValidators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
10 changes: 10 additions & 0 deletions src/utils/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? [];
Expand All @@ -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;
Expand Down
Loading