diff --git a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts index dba14f5e8..78d6a0728 100644 --- a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts +++ b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts @@ -548,8 +548,16 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges this.maxDate = this.settingsService.businessDate; this.setOptions(); - this.clientService.getClientRiskRules().subscribe((rules) => { - this.riskRules = rules || []; + // Risk rules are an enhancement on top of the default low/medium-risk fallback in + // calculatedRisk — if the endpoint isn't available (e.g. not yet deployed on this + // backend), fail silently rather than surfacing a raw HTTP-error toast to the user. + this.clientService.getClientRiskRules().subscribe({ + next: (rules) => { + this.riskRules = rules || []; + }, + error: () => { + this.riskRules = []; + } }); // Fetch marital status options from code values this.clientService.getCodeValuesByName('MaritalStatus').subscribe((options) => { diff --git a/src/app/clients/clients.component.ts b/src/app/clients/clients.component.ts index fd99d30f7..98f9df007 100644 --- a/src/app/clients/clients.component.ts +++ b/src/app/clients/clients.component.ts @@ -137,7 +137,16 @@ export class ClientsComponent implements OnInit { private getClients() { this.isLoading = true; this.clientService - .searchByText(this.filterText, this.currentPage, this.pageSize, this.sortAttribute, this.sortDirection) + .searchByText( + this.filterText, + this.currentPage, + this.pageSize, + this.sortAttribute, + this.sortDirection, + false, + '', + true + ) .subscribe( (data: any) => { this.allRows = data.content || []; @@ -166,7 +175,7 @@ export class ClientsComponent implements OnInit { // Use individual subscriptions instead of forkJoin to avoid EMPTY observable issues clients.forEach((client, i) => { this.clientService - .getClientData(client.id?.toString()) + .getClientData(client.id?.toString(), true) .pipe(catchError(() => of(null))) .subscribe((data: any) => { if (data?.legalForm) { @@ -176,7 +185,7 @@ export class ClientsComponent implements OnInit { }); this.clientService - .getClientKyc(client.id?.toString()) + .getClientKyc(client.id?.toString(), true) .pipe(catchError(() => of(null))) .subscribe((kyc: any) => { if (kyc) { @@ -188,7 +197,7 @@ export class ClientsComponent implements OnInit { }); this.clientService - .getClientAddressData(client.id?.toString()) + .getClientAddressData(client.id?.toString(), true) .pipe(catchError(() => of(null))) .subscribe((addr: any) => { if (addr && Array.isArray(addr) && addr.length > 0) { diff --git a/src/app/clients/clients.service.ts b/src/app/clients/clients.service.ts index b412ccc45..cbb893260 100644 --- a/src/app/clients/clients.service.ts +++ b/src/app/clients/clients.service.ts @@ -8,7 +8,8 @@ /** Angular Imports */ import { Injectable, inject } from '@angular/core'; -import { HttpClient, HttpParams, HttpBackend, HttpHeaders } from '@angular/common/http'; +import { HttpClient, HttpParams, HttpBackend, HttpHeaders, HttpContext } from '@angular/common/http'; +import { SILENT_HTTP_ERROR } from 'app/core/http/silent-error.token'; /** rxjs Imports */ import { Observable, of, throwError } from 'rxjs'; @@ -66,8 +67,11 @@ export class ClientsService { return this.http.get(`/clients/template?officeId=${officeId}&staffInSelectedOfficeOnly=true`); } - getClientData(clientId: string) { - return this.http.get(`/clients/${clientId}`); + getClientData(clientId: string, silent = false) { + return this.http.get( + `/clients/${clientId}`, + silent ? { context: new HttpContext().set(SILENT_HTTP_ERROR, true) } : {} + ); } createClient(client: any) { @@ -153,8 +157,11 @@ export class ClientsService { return this.http.get(`/clients/${clientId}/accounts`); } - getClientAddressData(clientId: string) { - return this.http.get(`/client/${clientId}/addresses`); + getClientAddressData(clientId: string, silent = false) { + return this.http.get( + `/client/${clientId}/addresses`, + silent ? { context: new HttpContext().set(SILENT_HTTP_ERROR, true) } : {} + ); } getClientChargesData(clientId: string) { @@ -698,7 +705,8 @@ export class ClientsService { sortAttribute: string = '', sortDirection: string = '', activeOnly: boolean = false, - legalFormType: string = '' + legalFormType: string = '', + silent: boolean = false ) { const innerRequest: any = { text }; if (activeOnly) { @@ -727,7 +735,11 @@ export class ClientsService { ] }; } - return this.http.post(`/v2/clients/search`, request); + return this.http.post( + `/v2/clients/search`, + request, + silent ? { context: new HttpContext().set(SILENT_HTTP_ERROR, true) } : {} + ); } /** @@ -764,8 +776,11 @@ export class ClientsService { * Gets KYC details for a client. * @param clientId Client ID */ - getClientKyc(clientId: string): Observable { - return this.http.get(`/clients/${clientId}/kyc`); + getClientKyc(clientId: string, silent = false): Observable { + return this.http.get( + `/clients/${clientId}/kyc`, + silent ? { context: new HttpContext().set(SILENT_HTTP_ERROR, true) } : {} + ); } /** diff --git a/src/app/core/http/error-handler.interceptor.ts b/src/app/core/http/error-handler.interceptor.ts index d83994a33..0d9413845 100644 --- a/src/app/core/http/error-handler.interceptor.ts +++ b/src/app/core/http/error-handler.interceptor.ts @@ -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'; // Added import for TranslateService +import { SILENT_HTTP_ERROR } from './silent-error.token'; /** Initialize Logger */ const log = new Logger('ErrorHandlerInterceptor'); @@ -57,13 +58,30 @@ export class ErrorHandlerInterceptor implements HttpInterceptor { // "No pending edit for this client" is the normal/expected state — checked on every edit-page // load, not a real error the user needs to see. const isClientPendingEditRequest = request.url.match(/\/clients\/\d+\/edit\/pending$/); + // Risk rules are an enhancement on top of a default risk-category fallback — if this + // endpoint isn't available on a given backend deployment, the client-general-step + // component already falls back to riskRules = [] silently; no toast needed here either. + const isKycRiskRulesRequest = request.url.includes('/kyc/risk-rules'); const isSilentRequest = - isClientImageRequest || isClientKycRequest || isDirectorRequest || isClientPendingEditRequest; + isClientImageRequest || + isClientKycRequest || + isDirectorRequest || + isClientPendingEditRequest || + isKycRiskRulesRequest; - if (!environment.production && !isSilentRequest) { + // Explicit per-request opt-out (e.g. per-row enrichment fetches on a list page whose + // component already recovers via its own catchError) — silenced for every status, since + // the caller took responsibility for handling failure when it set this. + const isSilentOverride = request.context.get(SILENT_HTTP_ERROR); + + if (!environment.production && !isSilentRequest && !isSilentOverride) { log.error(`Request Error: ${errorMessage}`); } + if (isSilentOverride) { + throw response; + } + // Silently handle expected 403/404 errors (no image, no KYC record) // Throw instead of EMPTY so component-level catchError can handle it if (isSilentRequest && (status === 403 || status === 404)) { diff --git a/src/app/core/http/silent-error.token.ts b/src/app/core/http/silent-error.token.ts new file mode 100644 index 000000000..9623b800e --- /dev/null +++ b/src/app/core/http/silent-error.token.ts @@ -0,0 +1,17 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { HttpContextToken } from '@angular/common/http'; + +/** + * Per-request opt-out of ErrorHandlerInterceptor's toast. Set via + * `{ context: new HttpContext().set(SILENT_HTTP_ERROR, true) }` on calls whose caller already + * handles failures itself (e.g. per-row enrichment fetches on a list page) — the interceptor + * still throws so the caller's own catchError/error callback runs, it just skips the alert. + */ +export const SILENT_HTTP_ERROR = new HttpContextToken(() => false);