225 lines
7.2 KiB
TypeScript
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;
|
|
}
|