Files
cbsnexora/src/app/tasks/maker-checker-entity-fetch.ts
T

225 lines
7.2 KiB
TypeScript

/**
* 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<string> = 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<string, (id: string, s: EntityFetchServices) => Observable<any>> = {
// ---- 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<any> | 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<string, string[]> = {
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;
}