Skip to content
Open
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
62 changes: 62 additions & 0 deletions src/app/core/error-handler/error-handler.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ export interface ErrorMessage {
providedIn: 'root'
})
export class ErrorHandlerService {
constructor(
// eslint-disable-next-line @angular-eslint/prefer-inject
private snackBar: MatSnackBar,
// eslint-disable-next-line @angular-eslint/prefer-inject
private router: Router,
// eslint-disable-next-line @angular-eslint/prefer-inject
private translate: TranslateService
) {}
private snackBar = inject(MatSnackBar);
private router = inject(Router);
private translateService = inject(TranslateService);
Expand Down Expand Up @@ -186,4 +194,58 @@ export class ErrorHandlerService {
panelClass: ['info-snackbar']
});
}

/**
* Translates a Fineract error response into a single, translated message.
* Falls back to defaultUserMessage when translation keys are missing.
*/
translateFineractError(errorResponse: FineractErrorResponse | null | undefined): string {
if (!errorResponse || typeof errorResponse !== 'object') {
return '';
}

const messages: string[] = [];

if (errorResponse.userMessageGlobalisationCode) {
const mainMsg = this.getMessageForCode(
errorResponse.userMessageGlobalisationCode,
errorResponse.defaultUserMessage
);
if (mainMsg) {
messages.push(mainMsg);
}
} else if (errorResponse.defaultUserMessage) {
messages.push(errorResponse.defaultUserMessage);
}
if (Array.isArray(errorResponse.errors)) {
errorResponse.errors.forEach((error: FineractErrorDetail) => {
if (!error || typeof error !== 'object') {
return;
}
if (error.userMessageGlobalisationCode) {
const nestedMsg = this.getMessageForCode(error.userMessageGlobalisationCode, error.defaultUserMessage);
if (nestedMsg) {
messages.push(nestedMsg);
}
} else if (error.defaultUserMessage) {
messages.push(error.defaultUserMessage);
}
});
}
const uniqueMessages = Array.from(new Set(messages.filter((m) => !!m && typeof m === 'string')));
return uniqueMessages.join(' ');
}

private getMessageForCode(code: string, defaultMessage?: string): string {
if (!code) {
return defaultMessage || '';
}

const translated = this.translate.instant(code);
if (translated && translated !== code) {
return translated;
}

return defaultMessage || '';
}
}
112 changes: 38 additions & 74 deletions src/app/core/http/error-handler.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { environment } from '../../../environments/environment';
import { Logger } from '../logger/logger.service';
import { AlertService } from '../alert/alert.service';
import { TranslateService } from '@ngx-translate/core';
import { ErrorHandlerService } from '../error-handler/error-handler.service';

/** Initialize Logger */
const log = new Logger('ErrorHandlerInterceptor');
Expand All @@ -32,10 +33,7 @@ const log = new Logger('ErrorHandlerInterceptor');
export class ErrorHandlerInterceptor implements HttpInterceptor {
private alertService = inject(AlertService);
private translate = inject(TranslateService);
private databaseErrorCodes: string[] = [
'error.msg.data.integrity.issue.entity.duplicated',
'error.msg.data.integrity.issue'
];
private errorHandlerService = inject(ErrorHandlerService);

intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(catchError((error) => this.handleError(error, request)));
Expand All @@ -61,98 +59,64 @@ export class ErrorHandlerInterceptor implements HttpInterceptor {

private handleError(response: HttpErrorResponse, request: HttpRequest<any>): Observable<HttpEvent<any>> {
const status = response.status;
const errorBody = this.parseErrorBody(response.error);
const errorBody: any = response.error;
const translatedErrorMessage = this.errorHandlerService.translateFineractError(errorBody);
const developerMessage: string | undefined = errorBody?.developerMessage;
const errorMessage = translatedErrorMessage || errorBody?.defaultUserMessage || response.message;

// Translate top-level globalisation code if present
const rawTopLevelMessage = errorBody?.defaultUserMessage || errorBody?.developerMessage;
let topLevelMessage = rawTopLevelMessage || response.message;
if (errorBody?.userMessageGlobalisationCode) {
const topCode = errorBody.userMessageGlobalisationCode;
const translated = this.translate.instant(topCode, errorBody || {});
if (translated !== topCode) {
topLevelMessage = translated;
}
}

// Translate nested globalisation code if present
let nestedMessage: string | null = null;
if (errorBody?.errors?.[0]?.userMessageGlobalisationCode) {
const nestedCode = errorBody.errors[0].userMessageGlobalisationCode;
const translated = this.translate.instant(nestedCode, errorBody.errors[0] || {});
nestedMessage = translated !== nestedCode ? translated : errorBody.errors[0].defaultUserMessage || null;
}

// Combine both messages if both exist and are distinct.
// Prefer translated messages; only fall back to raw defaultUserMessage when no translation was resolved.
const hasTopLevelPayload = Boolean(rawTopLevelMessage || errorBody?.userMessageGlobalisationCode);

let errorMessage = nestedMessage
? hasTopLevelPayload && nestedMessage !== topLevelMessage
? `${topLevelMessage} ${nestedMessage}`
: nestedMessage
: topLevelMessage;
let parameterName: string | null = null;
if (response.error.errors) {
if (response.error.errors[0]) {
if (
response.error.errors[0].userMessageGlobalisationCode &&
this.databaseErrorCodes.indexOf(response.error.errors[0].userMessageGlobalisationCode) > -1
) {
errorMessage = this.translate.instant('errors.error.msg.data.integrity.issue');
} else {
errorMessage =
response.error.errors[0].defaultUserMessage.replace(/\\./g, ' ') ||
response.error.errors[0].developerMessage.replace(/\\./g, ' ');
}
}
if ('parameterName' in errorBody.errors[0]) {
parameterName = errorBody.errors[0].parameterName;
}
}
const isClientImage404 = status === 404 && request.url.includes('/clients/') && request.url.includes('/images');

if (!environment.production && !isClientImage404) {
if (developerMessage) {
log.error(`Request Error (developerMessage): ${developerMessage}`);
}
log.error(`Request Error: ${errorMessage}`);
}

if (status === 401 || (environment.oauth.enabled && status === 400)) {
// OAuth2 errors for invalid grants are returned as 400, so we need to check the URL.
if (status === 401 || (environment.oauth.enabled && status === 400 && request.url.includes('/oauth/token'))) {
this.alertService.alert({
type: this.translate.instant('errors.error.auth.type'),
message: this.translate.instant('errors.error.auth.message')
});
} else if (
status === 403 &&
errorBody?.errors?.[0]?.defaultUserMessage === 'The provided one time token is invalid'
) {
this.alertService.alert({
type: this.translate.instant('errors.error.token.invalid.type'),
message: this.translate.instant('errors.error.token.invalid.message')
type: this.translate.instant('error.resource.authenticationError.type'),
message: this.translate.instant('error.resource.authenticationError.message')
});
} else if (status === 400) {
const fallback = this.translate.instant('errors.interceptor.invalidParams');
const message = parameterName ? `[${parameterName}] ${errorMessage || fallback}` : `${errorMessage || fallback}`;
this.alertService.alert({
type: this.translate.instant('errors.error.bad.request.type'),
message: message || this.translate.instant('errors.error.bad.request.message')
type: this.translate.instant('error.resource.badRequest.type'),
message: errorMessage || this.translate.instant('error.resource.badRequest.message')
});
} else if (status === 403) {
this.alertService.alert({
type: this.translate.instant('errors.error.unauthorized.type'),
message: errorMessage || this.translate.instant('errors.error.unauthorized.message')
});
// The token check must use a stable identifier, not the translated message.
const isInvalidToken =
errorBody?.userMessageGlobalisationCode === 'error.msg.invalid.onetime.token' ||
errorBody?.defaultUserMessage === 'The provided one time token is invalid' ||
errorMessage === 'The provided one time token is invalid';

if (isInvalidToken) {
this.alertService.alert({
type: this.translate.instant('error.resource.invalidToken.type'),
message: this.translate.instant('error.resource.invalidToken.message')
});
} else {
this.alertService.alert({
type: this.translate.instant('error.resource.unauthorizedRequest.type'),
message: errorMessage || this.translate.instant('error.resource.unauthorizedRequest.message')
});
}
} else if (status === 404) {
if (isClientImage404) {
return throwError(() => response);
} else {
this.alertService.alert({
type: this.translate.instant('errors.error.resource.not.found.type'),
message: errorMessage || this.translate.instant('errors.error.resource.not.found.message')
type: this.translate.instant('error.resource.not.found'),
message: errorMessage || this.translate.instant('error.resource.notFound.message')
});
}
} else if (status === 500) {
this.alertService.alert({
type: this.translate.instant('errors.error.server.internal.type'),
message: errorMessage || this.translate.instant('errors.error.server.internal.message')
type: this.translate.instant('error.resource.internalServerError.type'),
message: this.translate.instant('error.resource.internalServerError.message')
});
} else if (status === 501) {
this.alertService.alert({
Expand All @@ -161,8 +125,8 @@ export class ErrorHandlerInterceptor implements HttpInterceptor {
});
} else {
this.alertService.alert({
type: this.translate.instant('errors.error.unknown.type'),
message: errorMessage || this.translate.instant('errors.error.unknown.message')
type: this.translate.instant('error.resource.unknownError.type'),
message: errorMessage || this.translate.instant('error.resource.unknownError.message')
});
}

Expand Down
2 changes: 2 additions & 0 deletions src/assets/translations/cs-CS.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"Remember me": "Zapamatuj si mě",
"error.resource.notImplemented.type": "Nenaimplementovaná chyba",
"error.resource.notImplemented.message": "Funkce není implementována!",
"error.msg.platform.service.unavailable": "Server je v současné chvíli neschopen zpracovat požadavek. Zkuste to prosím později.",
"error.msg.database.type.not.supported": "Datový typ není podporován",
"errors": {
"accountingRule": {
"duplicateName": "Omlouváme se, ale účetní pravidlo s tímto názvem již existuje."
Expand Down
2 changes: 2 additions & 0 deletions src/assets/translations/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"Remember me": "Erinnere dich an mich",
"error.resource.notImplemented.type": "Nicht implementierter Fehler",
"error.resource.notImplemented.message": "Nicht implementierte Funktion!",
"error.msg.platform.service.unavailable": "Der Server kann die Anfrage derzeit nicht bearbeiten. Bitte versuchen Sie es später erneut.",
"error.msg.database.type.not.supported": "Der Datentyp wird nicht unterstützt",
"errors": {
"accountingRule": {
"duplicateName": "Entschuldigung, aber eine Buchungsregel mit diesem Namen existiert bereits."
Expand Down
2 changes: 2 additions & 0 deletions src/assets/translations/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"Remember me": "Remember me",
"error.resource.notImplemented.type": "Not Implemented Error",
"error.resource.notImplemented.message": "Not implemented functionality!",
"error.msg.platform.service.unavailable": "The server is currently unable to handle the request, please try again after some time.",
"error.msg.database.type.not.supported": "Data type is not supported",
"errors": {
"accountingRule": {
"duplicateName": "Sorry, but an Accounting Rule with this Name exists already."
Expand Down
2 changes: 2 additions & 0 deletions src/assets/translations/es-CL.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"APP_NAME": "Mifos® X WebApp",
"Logged in as": "Conectado como",
"Remember me": "Recordar me",
"error.msg.platform.service.unavailable": "El servidor no puede procesar la solicitud en este momento. Por favor, inténtelo de nuevo más tarde.",
"error.msg.database.type.not.supported": "El tipo de dato no es compatible.",
"errors": {
"accountingRule": {
"duplicateName": "Lo sentimos, pero ya existe una regla contable con este nombre."
Expand Down
2 changes: 2 additions & 0 deletions src/assets/translations/es-MX.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"APP_NAME": "Mifos® X WebApp",
"Logged in as": "Conectado como",
"Remember me": "Recordar me",
"error.msg.platform.service.unavailable": "El servidor no puede procesar la solicitud en este momento. Por favor, intente más tarde.",
"error.msg.database.type.not.supported": "El tipo de datos no es compatible",
"errors": {
"accountingRule": {
"duplicateName": "Lo sentimos, pero ya existe una regla contable con este nombre."
Expand Down
6 changes: 3 additions & 3 deletions src/assets/translations/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"APP_NAME": "Mifos® X WebApp",
"Logged in as": "connecté en tant que",
"Remember me": "Souviens-toi de moi",
"error.msg.platform.service.unavailable": "Le serveur est actuellement incapable de traiter la requête, veuillez réessayer ultérieurement.",
"error.msg.database.type.not.supported": "Le type de données n'est pas pris en charge",
"errors": {
"accountingRule": {
"duplicateName": "Désolé, mais une règle comptable portant ce nom existe déjà."
Expand Down Expand Up @@ -3690,9 +3692,7 @@
"No checker inbox data available for this account": "Aucune donnée de boîte de réception du vérificateur trouvé pour ce compte.",
"No checker inbox data available for this search": "Aucune donnée de boîte de réception du vérificateur trouvé pour cette recherche.",
"No client was found": "Aucun client n'a été trouvé",
"No data found": "Aucune donnée trouvé",
"No permissions found": "Aucune autorisation trouvé",
"NonTriggered": "Non déclenché",
"No data found": "Aucune donnée disponible",
"No penalties found": "Aucune pénalité trouvée",
"NoDocuments": "Aucun document trouvé.",
"NoFileSelected": "Aucun fichier sélectionné",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/translations/it-IT.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"APP_NAME": "Mifos® X WebApp",
"Logged in as": "Collegato come",
"Remember me": "Ricordati di me",
"error.msg.platform.service.unavailable": "Il server non può elaborare la richiesta in questo momento. Si prega di riprovare più tardi.",
"error.msg.database.type.not.supported": "Il tipo di dati non è supportato",
"errors": {
"accountingRule": {
"duplicateName": "Spiacenti, ma esiste già una regola contabile con questo nome."
Expand Down
2 changes: 2 additions & 0 deletions src/assets/translations/ko-KO.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"APP_NAME": "Mifos® X WebApp",
"Logged in as": "다음 계정으로 로그인됨",
"Remember me": "날 기억해",
"error.msg.platform.service.unavailable": "서버가 현재 요청을 처리할 수 없습니다. 나중에 다시 시도해주세요.",
"error.msg.database.type.not.supported": "지원하지 않는 데이터 유형입니다",
"errors": {
"accountingRule": {
"duplicateName": "죄송합니다. 이미 동일한 이름의 회계 규칙이 존재합니다."
Expand Down
2 changes: 2 additions & 0 deletions src/assets/translations/lt-LT.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"APP_NAME": "Mifos® X WebApp",
"Logged in as": "prisijungęs kaip",
"Remember me": "Prisimink mane",
"error.msg.platform.service.unavailable": "Serveris šiuo metu negali apdoroti užklausą. Prašome pabandyti vėliau.",
"error.msg.database.type.not.supported": "Duomenų tipas nepalaikomas",
"errors": {
"accountingRule": {
"duplicateName": "Atsiprašome, bet apskaitos taisyklė su šiuo pavadinimu jau egzistuoja."
Expand Down
2 changes: 2 additions & 0 deletions src/assets/translations/lv-LV.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"APP_NAME": "Mifos® X WebApp",
"Logged in as": "ielogojies Kā",
"Remember me": "Atceries mani",
"error.msg.platform.service.unavailable": "Serveris pašlaik nespēj apstrādāt pieprasījumu. Lūdzu, mēģiniet vēlāk.",
"error.msg.database.type.not.supported": "Datu tips nav atbalstīts",
"errors": {
"accountingRule": {
"duplicateName": "Atvainojiet, bet grāmatvedības noteikums ar šo nosaukumu jau pastāv."
Expand Down
2 changes: 2 additions & 0 deletions src/assets/translations/ne-NE.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"APP_NAME": "Mifos® X WebApp",
"Logged in as": "को रूपमा लग इन गरियो",
"Remember me": "मलाई सम्झनुहोस्",
"error.msg.platform.service.unavailable": "सर्भरले अहिले अनुरोध प्रस्तुत गर्न सक्षम छैन। कृपया केही समयपछि पुन: प्रयास गर्नुहोस्।",
"error.msg.database.type.not.supported": "डेटाबेस प्रकार समर्थन गरिंदैन।",
"errors": {
"accountingRule": {
"duplicateName": "माफ गर्नुहोस्, यो नामको लेखा नियम पहिले नै अवस्थित छ।"
Expand Down
2 changes: 2 additions & 0 deletions src/assets/translations/pt-PT.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"APP_NAME": "Mifos® X WebApp",
"Logged in as": "logado como",
"Remember me": "Lembre de mim",
"error.msg.platform.service.unavailable": "O servidor não consegue processar o pedido neste momento. Por favor, tente mais tarde.",
"error.msg.database.type.not.supported": "O tipo de dados não é suportado",
"errors": {
"accountingRule": {
"duplicateName": "Desculpe, mas já existe uma regra contabilística com este nome."
Expand Down
2 changes: 2 additions & 0 deletions src/assets/translations/sw-SW.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"APP_NAME": "Mifos® X WebApp",
"Logged in as": "Imeingia kama",
"Remember me": "Nikumbuke",
"error.msg.platform.service.unavailable": "Seva kwa sasa haiwezi kushughulikia ombi, tafadhali jaribu tena baada ya muda.",
"error.msg.database.type.not.supported": "Aina ya data haiungwa mkono",
"errors": {
"accountingRule": {
"duplicateName": "Samahani, lakini Kanuni ya Uhasibu yenye jina hili tayari ipo."
Expand Down
Loading