diff --git a/apps/delivery-options/src/composables/useLanguage.ts b/apps/delivery-options/src/composables/useLanguage.ts index ecdc8ef2..331deae8 100644 --- a/apps/delivery-options/src/composables/useLanguage.ts +++ b/apps/delivery-options/src/composables/useLanguage.ts @@ -45,7 +45,9 @@ const translate = useMemoize((translatable: AnyTranslatable): string => { const replacers = translation.match(/\{(.+?)}/g); if (replacers?.length && isOfType(translatable, 'args')) { - return replacers.toReversed().reduce((string, match) => { + replacers.reverse(); + + return replacers.reduce((string, match) => { const argKey = match.slice(1, -1); const matchingArg = translatable?.args?.[argKey]; diff --git a/libs/shared/src/composables/useApiExceptions.spec.ts b/libs/shared/src/composables/useApiExceptions.spec.ts new file mode 100644 index 00000000..dffc3d74 --- /dev/null +++ b/libs/shared/src/composables/useApiExceptions.spec.ts @@ -0,0 +1,101 @@ +import {describe, it, expect, beforeEach} from 'vitest'; +import {ApiException} from '@myparcel/sdk'; +import {type TranslatableWithArgs} from '../types'; +import {ERROR_INVALID_POSTAL_CODE, IGNORED_ERRORS, ERROR_MISSING_REQUIRED_PARAMETER, ERROR_REPLACE_MAP} from '../data'; +import {useApiExceptions} from './useApiExceptions'; + +const createException = (code: string | number, message = 'error'): ApiException => { + return new ApiException({ + message: 'error', + request_id: '...', + errors: [{code: Number(code), message}], + }); +}; + +describe('useApiExceptions', () => { + beforeEach(() => { + useApiExceptions().clear(); + }); + + it('adds exceptions', () => { + const {addException, exceptions, hasExceptions} = useApiExceptions(); + const exception = createException(ERROR_INVALID_POSTAL_CODE); + + addException(['test'], exception); + + expect(hasExceptions.value).toBe(true); + expect(exceptions.value).toEqual([ + { + code: ERROR_INVALID_POSTAL_CODE, + label: `error${ERROR_INVALID_POSTAL_CODE}`, + }, + ]); + + addException(['test'], createException(ERROR_INVALID_POSTAL_CODE)); + + // Expect no duplicates + expect(exceptions.value).toHaveLength(1); + }); + + it.each(IGNORED_ERRORS)('ignores exceptions with code %s', (code) => { + const {addException, exceptions, hasExceptions} = useApiExceptions(); + const exception = createException(code); + + addException(['someRequest'], exception); + + expect(hasExceptions.value).toBe(false); + expect(exceptions.value).toEqual([]); + }); + + it.each(Object.entries(ERROR_REPLACE_MAP))(`replaces error code %d with %d`, (code, replacement) => { + const {addException, exceptions, hasExceptions} = useApiExceptions(); + const exception = createException(code); + + addException(['test'], exception); + + expect(hasExceptions.value).toBe(true); + expect(exceptions.value).toEqual([ + { + code: replacement, + label: `error${replacement}`, + }, + ]); + }); + + it('can clear exceptions', () => { + const {addException, clear, hasExceptions, exceptions} = useApiExceptions(); + const exception = createException(ERROR_INVALID_POSTAL_CODE); + + addException(['test'], exception); + + expect(hasExceptions.value).toBe(true); + + clear(); + + expect(hasExceptions.value).toBe(false); + expect(exceptions.value).toEqual([]); + }); + + it(`adds arguments to exception with code ${ERROR_MISSING_REQUIRED_PARAMETER}`, () => { + const {addException, exceptions, hasExceptions} = useApiExceptions(); + const exception = createException(ERROR_MISSING_REQUIRED_PARAMETER, 'city is required'); + + addException(['test'], exception); + + expect(hasExceptions.value).toBe(true); + expect(exceptions.value).toEqual([ + { + code: ERROR_MISSING_REQUIRED_PARAMETER, + label: { + key: `error${ERROR_MISSING_REQUIRED_PARAMETER}`, + args: { + field: { + key: 'city', + plain: true, + }, + }, + } satisfies TranslatableWithArgs, + }, + ]); + }); +}); diff --git a/libs/shared/src/composables/useApiExceptions.ts b/libs/shared/src/composables/useApiExceptions.ts index 151e90bc..4e2f5066 100644 --- a/libs/shared/src/composables/useApiExceptions.ts +++ b/libs/shared/src/composables/useApiExceptions.ts @@ -2,7 +2,7 @@ import {ref, type Ref, computed, type ComputedRef} from 'vue'; import {useMemoize} from '@vueuse/core'; import {type ApiException, type ErrorResponse} from '@myparcel/sdk'; import {type RequestKey, type AnyTranslatable} from '../types'; -import {IGNORED_ERRORS, ERROR_MISSING_REQUIRED_PARAMETER} from '../data'; +import {IGNORED_ERRORS, ERROR_MISSING_REQUIRED_PARAMETER, ERROR_REPLACE_MAP} from '../data'; const exceptions = ref([]); @@ -19,27 +19,28 @@ interface UseErrors { } const parseError = (error: ErrorResponse['errors'][number]): ParsedError => { - if (error.code === ERROR_MISSING_REQUIRED_PARAMETER) { - const strings = error.message.split(' '); + const resolvedCode = ERROR_REPLACE_MAP[error.code] ?? error.code; - return { - code: error.code, - label: { - key: `error${error.code}`, - args: { - field: { - key: strings?.[0], - plain: true, - }, + const resolvedError: ParsedError = { + code: resolvedCode, + label: `error${resolvedCode}`, + }; + + if (resolvedCode === ERROR_MISSING_REQUIRED_PARAMETER) { + const words = error.message.split(' '); + + resolvedError.label = { + key: `error${resolvedCode}`, + args: { + field: { + key: words[0], + plain: true, }, }, }; } - return { - code: error.code, - label: `error${error.code}`, - }; + return resolvedError; }; export const useApiExceptions = useMemoize((): UseErrors => { diff --git a/libs/shared/src/data/errors.ts b/libs/shared/src/data/errors.ts index cd431052..7c9786cf 100644 --- a/libs/shared/src/data/errors.ts +++ b/libs/shared/src/data/errors.ts @@ -18,6 +18,8 @@ export const ERROR_NO_DELIVERY_OPTIONS_FOUND = 3721; export const ERROR_UNSUPPORTED_CARRIER = 3728; +export const ERROR_ADDRESS_CAN_NOT_BE_SPLIT = 3731; + export const ERROR_WADDEN_ISLANDS = 3753; /** @@ -31,3 +33,7 @@ export const IGNORED_ERRORS = Object.freeze([ ERROR_INVALID_COUNTRY_CODE, ERROR_INVALID_CARRIER_PLATFORM_COMBINATION, ]); + +export const ERROR_REPLACE_MAP: Record = Object.freeze({ + [ERROR_ADDRESS_CAN_NOT_BE_SPLIT]: ERROR_ADDRESS_UNKNOWN, +});