/** * 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 { Observable } from 'rxjs'; import { ProductsService } from 'app/products/products.service'; import { ClientsService } from 'app/clients/clients.service'; import { SavingsService } from 'app/savings/savings.service'; import { FixedDepositsService } from 'app/deposits/fixed-deposits/fixed-deposits.service'; import { RecurringDepositsService } from 'app/deposits/recurring-deposits/recurring-deposits.service'; import { SystemService } from 'app/system/system.service'; import { AccountingService } from 'app/accounting/accounting.service'; import { UsersService } from 'app/users/users.service'; import { OrganizationService } from 'app/organization/organization.service'; /** * The services a maker-checker review screen needs in order to read an entity's current state. * Passed in rather than injected here so this stays a plain lookup table with no Angular wiring. */ export interface EntityFetchServices { products: ProductsService; clients: ClientsService; savings: SavingsService; fixedDeposits: FixedDepositsService; recurringDeposits: RecurringDepositsService; system: SystemService; accounting: AccountingService; users: UsersService; organization: OrganizationService; } /** * Entities with exactly one row in the whole tenant (a global settings screen, not a record with an * id) - Working Days being the first. These commands carry no resourceId at all, so the normal * "no id, no lookup" guard below is relaxed for exactly this set; every other entity's behavior is * unchanged. */ const SINGLETON_ENTITIES: ReadonlySet = new Set(['WORKINGDAYS']); /** * Maps a maker-checker `entityName` to the existing API that returns that entity as it stands today, * so an UPDATE can be shown as OLD -> NEW and a DELETE can show what is about to be removed. * * Every entry reuses an API the application already calls elsewhere - nothing new was added on the * backend. Products are fetched with `template=true` because that response also carries the option * lists (`accountingRuleOptions`, `interestCompoundingPeriodTypeOptions`, ...) the payload's bare ids * refer to. * * An entity absent from this table simply has no current-state lookup; callers fall back to rendering * the submitted payload, which is what every entity did before this table existed. */ const ENTITY_FETCHERS: Record Observable> = { // ---- Products ---------------------------------------------------------------- SAVINGSPRODUCT: (id, s) => s.products.getSavingProduct(id, true), CURRENTPRODUCT: (id, s) => s.products.getCurrentProduct(id, true), SHAREPRODUCT: (id, s) => s.products.getShareProduct(id, true), LOANPRODUCT: (id, s) => s.products.getLoanProduct(id, true), FIXEDDEPOSITPRODUCT: (id, s) => s.products.getFixedDepositProductAndTemplate(id), RECURRINGDEPOSITPRODUCT: (id, s) => s.products.getRecurringDepositProduct(id, true), CHARGE: (id, s) => s.products.getCharge(id, true), // ---- Accounts ---------------------------------------------------------------- SAVINGSACCOUNT: (id, s) => s.savings.getSavingsAccountData(id), // Deposit accounts must be read through their own endpoints: /savingsaccounts omits the deposit // terms (depositAmount, depositPeriod, charts, ...), so comparing against it reports every one of // those as newly added. FIXEDDEPOSITACCOUNT: (id, s) => s.fixedDeposits.getFixedDepositsAccountData(id), RECURRINGDEPOSITACCOUNT: (id, s) => s.recurringDeposits.getRecurringDepositsAccountAndTemplate(id), // ---- People and administration ---------------------------------------------- CLIENT: (id, s) => s.clients.getClientData(id, true), ROLE: (id, s) => s.system.getRole(id), USER: (id, s) => s.users.getUser(id), GLACCOUNT: (id, s) => s.accounting.getGlAccount(id, true), // ---- Singleton settings screens ---------------------------------------------- // No resourceId - the command applies to the one Working Days row the tenant has. WORKINGDAYS: (id, s) => s.organization.getWorkingDays() }; /** Whether this screen can read the entity's current state, and therefore produce a real diff. */ export function canFetchEntity(entityName: string | undefined | null): boolean { return !!ENTITY_FETCHERS[(entityName ?? '').toUpperCase()]; } /** * Returns the entity as it stands today, or null when there is no mapping for it or no resource to * read - in which case the caller keeps showing the submitted payload rather than failing. */ export function fetchCurrentEntity( entityName: string | undefined | null, resourceId: string | number | undefined | null, services: EntityFetchServices ): Observable | null { const key = (entityName ?? '').toUpperCase(); const fetcher = ENTITY_FETCHERS[key]; if (!fetcher) { return null; } const hasResourceId = resourceId !== null && resourceId !== undefined && resourceId !== ''; if (!hasResourceId && !SINGLETON_ENTITIES.has(key)) { return null; } return fetcher(String(resourceId ?? ''), services); } /** * The handful of fields worth showing for a DELETE, whose payload is empty - the checker needs to * recognise *which* record is about to go, not read its full configuration. Keys are tried in order * and the first present one wins; anything not listed falls back to a generic set. */ const DELETE_SUMMARY_KEYS: Record = { SAVINGSPRODUCT: [ 'name', 'shortName', 'description', 'currency' ], CURRENTPRODUCT: [ 'name', 'shortName', 'description', 'currency' ], SHAREPRODUCT: [ 'name', 'shortName', 'description', 'currency' ], LOANPRODUCT: [ 'name', 'shortName', 'description', 'currency' ], FIXEDDEPOSITPRODUCT: [ 'name', 'shortName', 'description', 'currency' ], RECURRINGDEPOSITPRODUCT: [ 'name', 'shortName', 'description', 'currency' ], CHARGE: [ 'name', 'currency', 'amount', 'chargeAppliesTo', 'chargeTimeType' ], CLIENT: [ 'displayName', 'accountNo', 'officeName', 'status', 'mobileNo', 'emailAddress' ], SAVINGSACCOUNT: [ 'accountNo', 'clientName', 'savingsProductName', 'status', 'currency' ], FIXEDDEPOSITACCOUNT: [ 'accountNo', 'clientName', 'savingsProductName', 'status', 'currency' ], RECURRINGDEPOSITACCOUNT: [ 'accountNo', 'clientName', 'savingsProductName', 'status', 'currency' ], ROLE: [ 'name', 'description', 'disabled' ], USER: [ 'username', 'firstname', 'lastname', 'email', 'officeName' ], GLACCOUNT: [ 'name', 'glCode', 'type', 'usage', 'description' ] }; const GENERIC_SUMMARY_KEYS = [ 'name', 'displayName', 'accountNo', 'shortName', 'username', 'description', 'status' ]; /** Field names worth surfacing for the entity about to be deleted. */ export function deleteSummaryKeys(entityName: string | undefined | null): string[] { return DELETE_SUMMARY_KEYS[(entityName ?? '').toUpperCase()] ?? GENERIC_SUMMARY_KEYS; }