/** * 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 { Component, OnInit, inject } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { catchError, of } from 'rxjs'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { OrganizationService } from '../organization.service'; import { SettingsService } from 'app/settings/settings.service'; import { DISABLED_ACCOUNTING_FEATURES } from '../../accounting/disabled-features'; const BANK_TYPES = [ { value: 'COOPERATIVE', label: 'Co-operative Bank' }, { value: 'COMMERCIAL', label: 'Commercial Bank' }, { value: 'RRB', label: 'Regional Rural Bank' }, { value: 'NBFC', label: 'NBFC' }, { value: 'OTHER', label: 'Other' } ]; const OWNERSHIP_TYPES = [ { value: 'GOVERNMENT', label: 'Government' }, { value: 'PRIVATE', label: 'Private' }, { value: 'COOPERATIVE', label: 'Cooperative' }, { value: 'OTHER', label: 'Other' } ]; const LICENSE_STATUSES = [ { value: 'ACTIVE', label: 'Active' }, { value: 'EXPIRED', label: 'Expired' }, { value: 'SUSPENDED', label: 'Suspended' }, { value: 'NOT_APPLICABLE', label: 'Not Applicable' } ]; const STATUSES = [ { value: 'ACTIVE', label: 'Active' }, { value: 'INACTIVE', label: 'Inactive' } ]; /** * The bank's own "Bank Master" profile — identity/classification, regulatory details, address * (registered, with optional head-office/communication overrides), contact, the institution-level * banking identifier (SWIFT/BIC), document header names and terms, the authorised representative, * and audit fields. A singleton screen: there is only ever one institution running this tenant. * * IFSC/MICR are deliberately NOT here — those identify a specific branch, not the institution, and * live on Branch Profile instead. Business Date, Financial Year, Time Zone, Working Days and Holiday * Calendar are likewise not stored here — they already exist as their own configuration, and this * screen only links to them (see the Operational Configuration section) instead of holding a second, * driftable copy. */ @Component({ selector: 'mifosx-institution-profile', templateUrl: './institution-profile.component.html', styleUrls: ['./institution-profile.component.scss'], imports: [...STANDALONE_SHARED_IMPORTS] }) export class InstitutionProfileComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); private organizationService = inject(OrganizationService); private settingsService = inject(SettingsService); /** Accounting features switched off for this installation — see accounting/disabled-features.ts. */ disabledFeatures = DISABLED_ACCOUNTING_FEATURES; bankTypes = BANK_TYPES; ownershipTypes = OWNERSHIP_TYPES; licenseStatuses = LICENSE_STATUSES; statuses = STATUSES; form: UntypedFormGroup; loading = true; saving = false; errorMsg = ''; successMsg = ''; currencyCode = ''; audit: any = null; private static readonly DATE_FIELDS = [ 'establishedDate', 'registrationDate', 'licenseExpiryDate' ]; ngOnInit(): void { this.form = this.formBuilder.group({ bankName: [''], legalName: [''], shortName: [''], bankCode: [''], institutionCode: [''], bankType: [null], ownershipType: [null], parentInstitutionName: [''], establishedDate: [null], status: ['ACTIVE'], country: ['India'], registrationNumber: [''], licenseNumber: [''], regulatoryAuthority: [''], registrationDate: [null], licenseStatus: [null], licenseExpiryDate: [null], pan: [''], tan: [''], gstin: [''], addressLine1: [''], addressLine2: [''], city: [''], district: [''], state: [''], pincode: [''], headOfficeSameAsRegistered: [true], headOfficeAddress: [''], communicationSameAsRegistered: [true], communicationAddress: [''], contactPhone: [''], contactEmail: [''], customerCareNumber: [''], grievanceEmail: [''], website: [''], swiftBic: [''], statementHeaderName: [''], passbookHeaderName: [''], receiptHeaderName: [''], termsAndConditions: [''], authorizedPersonName: [''], authorizedPersonDesignation: [''], authorizedPersonContact: [''], authorizedPersonEmail: [''] }); this.load(); } load(): void { this.loading = true; this.errorMsg = ''; this.organizationService .getInstitutionProfile() .pipe( catchError((err: any) => { this.errorMsg = err?.error?.errors?.[0]?.defaultUserMessage ?? 'Could not load the bank profile.'; return of(null); }) ) .subscribe((res: any) => { this.loading = false; if (!res) return; this.applyResult(res); this.form.markAsPristine(); }); } submit(): void { if (this.saving) return; this.saving = true; this.errorMsg = ''; this.successMsg = ''; const raw = this.form.value; const payload: any = { ...raw, locale: this.settingsService.language.code }; for (const field of InstitutionProfileComponent.DATE_FIELDS) { payload[field] = raw[field] ? this.formatDate(raw[field]) : null; } this.organizationService .updateInstitutionProfile(payload) .pipe( catchError((err: any) => { this.saving = false; this.errorMsg = err?.error?.errors?.[0]?.defaultUserMessage ?? 'Could not save the bank profile.'; return of(null); }) ) .subscribe((res: any) => { this.saving = false; if (!res) return; if (res.rollbackTransaction) { // Parked for checker approval - the profile has not actually changed yet, so the form must // not be patched with the submitted (not-yet-applied) values as if it had. this.successMsg = 'Bank profile update has been submitted for approval.'; this.form.markAsPristine(); return; } this.successMsg = 'Bank profile saved.'; // The response is now a CommandProcessingResult, not the updated row (the write goes through // the command pipeline) - reload the saved row from the server instead of patching the form // from this response. this.load(); }); } private applyResult(res: any): void { const patch: any = { ...res }; for (const field of InstitutionProfileComponent.DATE_FIELDS) { patch[field] = res[field] ? new Date(res[field]) : null; } this.form.patchValue(patch); this.currencyCode = res.currencyCode ?? ''; this.audit = { createdBy: res.createdByUsername, createdAt: res.createdAt, updatedBy: res.updatedByUsername, updatedAt: res.updatedAt, activationDate: res.activationDate, deactivationDate: res.deactivationDate }; } private formatDate(d: Date): string { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; } }