feat: implement product approval workflow updates
This commit is contained in:
@@ -11,15 +11,27 @@ import { Resolve } from '@angular/router';
|
||||
import { Observable, forkJoin, of } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { ProductsService } from 'app/products/products.service';
|
||||
import { TasksService } from '../tasks.service';
|
||||
import { isProductEntity, isTempTableProductEdit, PRODUCT_ENTITY_TYPE_LABEL } from '../product-entities';
|
||||
|
||||
/**
|
||||
* Merges pending edits across all product types sharing the m_savings_product_temp backing table -
|
||||
* Savings, Current Account, Share, Fixed Deposit, and Recurring Deposit products - into one list for
|
||||
* the Pending Product Edits screen.
|
||||
* Builds the Approvals -> Product Approval list from the two pending-request sources the platform
|
||||
* already has. No new workflow or table is involved; both feeds are read as-is.
|
||||
*
|
||||
* 1. Pending product edits (m_saving_product_temp), via /{producttype}/pending. UPDATE only, for the
|
||||
* five product types whose PUT handler stages the change instead of logging a command.
|
||||
* 2. Native maker-checker entries (m_portfolio_command_source), via /makercheckers. This is where
|
||||
* product CREATE and DELETE land - and LOANPRODUCT/CHARGE requests of every action, since those
|
||||
* were never diverted to the staging table.
|
||||
*
|
||||
* Rows staged in (1) also have an audit-only command-source row that surfaces in (2), so the
|
||||
* maker-checker feed drops them - see isTempTableProductEdit. Each source keeps its own approve/reject
|
||||
* path in the component; the merge here is presentational only.
|
||||
*/
|
||||
@Injectable()
|
||||
export class GetPendingSavingsProducts implements Resolve<any> {
|
||||
private productsService = inject(ProductsService);
|
||||
private tasksService = inject(TasksService);
|
||||
|
||||
resolve(): Observable<any> {
|
||||
return forkJoin({
|
||||
@@ -27,15 +39,73 @@ export class GetPendingSavingsProducts implements Resolve<any> {
|
||||
current: this.productsService.getPendingCurrentProductEdits().pipe(catchError(() => of([]))),
|
||||
share: this.productsService.getPendingShareProductEdits().pipe(catchError(() => of([]))),
|
||||
fd: this.productsService.getPendingFixedDepositProductEdits().pipe(catchError(() => of([]))),
|
||||
rd: this.productsService.getPendingRecurringDepositProductEdits().pipe(catchError(() => of([])))
|
||||
rd: this.productsService.getPendingRecurringDepositProductEdits().pipe(catchError(() => of([]))),
|
||||
// includeJson so a CREATE row - which has no resourceId, and therefore no product to look up -
|
||||
// can still show the submitted product name.
|
||||
makerChecker: this.tasksService.getMakerCheckerData({ includeJson: true }).pipe(catchError(() => of([])))
|
||||
}).pipe(
|
||||
map((results) => [
|
||||
...(results.savings || []).map((e: any) => ({ ...e, productType: 'SAVINGS' })),
|
||||
...(results.current || []).map((e: any) => ({ ...e, productType: 'CURRENT' })),
|
||||
...(results.share || []).map((e: any) => ({ ...e, productType: 'SHARE' })),
|
||||
...(results.fd || []).map((e: any) => ({ ...e, productType: 'FD' })),
|
||||
...(results.rd || []).map((e: any) => ({ ...e, productType: 'RD' }))
|
||||
...this.asEditRows(results.savings, 'SAVINGS'),
|
||||
...this.asEditRows(results.current, 'CURRENT'),
|
||||
...this.asEditRows(results.share, 'SHARE'),
|
||||
...this.asEditRows(results.fd, 'FD'),
|
||||
...this.asEditRows(results.rd, 'RD'),
|
||||
...this.asMakerCheckerRows(results.makerChecker)
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/** Staged product edits, unchanged apart from the source/action tags the merged table needs. */
|
||||
private asEditRows(edits: any[], productType: string): any[] {
|
||||
return (edits || []).map((edit: any) => ({
|
||||
...edit,
|
||||
productType,
|
||||
source: 'PENDING_EDIT',
|
||||
actionName: 'UPDATE'
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalises maker-checker entries onto the same field names the pending-edit table already renders,
|
||||
* so the existing template keeps working for both sources.
|
||||
*/
|
||||
private asMakerCheckerRows(entries: any): any[] {
|
||||
const items = Array.isArray(entries) ? entries : (entries?.pageItems ?? []);
|
||||
return items
|
||||
.filter((entry: any) => isProductEntity(entry?.entityName))
|
||||
.filter((entry: any) => !isTempTableProductEdit(entry?.entityName, entry?.actionName))
|
||||
.map((entry: any) => ({
|
||||
id: entry.id,
|
||||
source: 'MAKER_CHECKER',
|
||||
entityName: entry.entityName,
|
||||
actionName: (entry.actionName ?? '').toUpperCase(),
|
||||
productType: PRODUCT_ENTITY_TYPE_LABEL[(entry.entityName ?? '').toUpperCase()] ?? entry.entityName,
|
||||
savingsProductId: entry.resourceId ?? null,
|
||||
savingsProductName: this.resolveName(entry),
|
||||
makerUsername: entry.maker,
|
||||
// /makercheckers returns madeOnDate as a Unix timestamp in seconds.
|
||||
// new Date(seconds) misreads it as milliseconds → January 1970.
|
||||
// Normalise to milliseconds here, matching DatetimeFormatPipe's unix() branch.
|
||||
createdOn:
|
||||
typeof entry.madeOnDate === 'number' && entry.madeOnDate < 1e12 ? entry.madeOnDate * 1000 : entry.madeOnDate,
|
||||
status: 'PENDING'
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* A CREATE has no resourceId - the product does not exist until approval - so the name comes from
|
||||
* the submitted command payload. Falls back to the entity/id when the payload is absent (a DELETE
|
||||
* carries none) or unparseable.
|
||||
*/
|
||||
private resolveName(entry: any): string {
|
||||
try {
|
||||
const payload = entry?.commandAsJson ? JSON.parse(entry.commandAsJson) : null;
|
||||
if (payload?.name) {
|
||||
return payload.name;
|
||||
}
|
||||
} catch {
|
||||
/* fall through to the identifier below */
|
||||
}
|
||||
return entry?.resourceId != null ? `${entry.entityName} #${entry.resourceId}` : entry.entityName;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user