/** * 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/. */ /** * Shared between the maker side (edit-client, which captures a point-in-time snapshot of the * client's current values right before staging an edit) and the checker side (pending-client-edits, * which diffs the staged payload against that snapshot). Keeping both in one place means the two * sides can never drift out of sync on which fields are captured/compared/labelled. * * Extracts a payload key's CURRENT value from a client record (as returned by GET /clients/{id}) — * CodeValue fields come back as nested {id, name} objects, so most extractors pull the nested .id * to compare against the payload's raw id. */ export const PENDING_EDIT_FIELD_EXTRACTORS: Record any> = { officeId: (c) => c.officeId, legalFormId: (c) => c.legalForm?.id, nationalityId: (c) => c.nationality, isStaff: (c) => c.isStaff, active: (c) => c.active, accountNo: (c) => c.accountNo, salutationId: (c) => c.salutation?.id, firstname: (c) => c.firstname, middlename: (c) => c.middlename, lastname: (c) => c.lastname, fullname: (c) => c.fullname, genderId: (c) => c.gender?.id, maritalStatusId: (c) => c.maritalStatus?.id, educationLevelId: (c) => c.educationLevel?.id, degreeQualificationId: (c) => c.degreeQualification?.id, religionId: (c) => c.religion?.id, socialCategoryId: (c) => c.socialCategory?.id, mobileNo: (c) => c.mobileNo, alternateMobileNo: (c) => c.alternateMobileNo, emailAddress: (c) => c.emailAddress, dateOfBirth: (c) => c.dateOfBirth, occupationCategoryId: (c) => c.occupationCategory?.id, occupationId: (c) => c.occupation?.id, monthlyIncome: (c) => c.monthlyIncome, annualIncome: (c) => c.annualIncome, annualTurnover: (c) => c.annualTurnover, monthlyTurnover: (c) => c.monthlyTurnover, isPwd: (c) => c.isPwd, pwdCategoryId: (c) => c.pwdCategory?.id, isSeniorCitizen: (c) => c.isSeniorCitizen, agricultureIncome: (c) => c.agricultureIncome, nonAgricultureIncome: (c) => c.nonAgricultureIncome, savingsProductId: (c) => c.savingsProductId, staffId: (c) => c.staffId, guardianClientId: (c) => c.guardianClientId, guardianRelation: (c) => c.guardianRelation }; /** Payload keys that are computed, internal, or otherwise never meaningfully "changed" from a * checker's point of view — always sent by the form (age breakdown, OTP tracking ids, the * business-date default on submittedOnDate) but with no comparable "current" value. */ export const NON_COMPARABLE_KEYS = new Set([ 'dateFormat', 'locale', 'ageYears', 'ageMonths', 'ageDays', 'isMinor', 'addSavings', 'submittedOnDate', 'activationDate', // Flattened onto top-level guardianClientId/guardianRelation right after this in the payload — // that flattened pair already has real comparators above, so the nested group is redundant. 'guardianDetails', // Mobile/email DIP verification + name-match + sanctions-screening results — carried along on // every save to re-affirm the client's EXISTING verification state, not something the maker // edited on this pass. dipTimestamp especially would almost never string-match exactly even // when functionally unchanged, so these were never real "proposed changes" to review. 'dipVerified', 'dipLookupLogId', 'dipTimestamp', 'alternateDipVerified', 'alternateDipLookupLogId', 'alternateDipTimestamp', 'nameVerified', 'screeningLogId', 'mobileNewOtpId', 'mobileOldOtpId', 'emailNewOtpId', 'emailOldOtpId' ]); export const PENDING_EDIT_FIELD_LABELS: Record = { officeId: 'Office', legalFormId: 'Legal Form', nationalityId: 'Nationality', isStaff: 'Is Staff', active: 'Active', accountNo: 'Account Number', salutationId: 'Salutation', firstname: 'First Name', middlename: 'Middle Name', lastname: 'Last Name', fullname: 'Full Name', genderId: 'Gender', maritalStatusId: 'Marital Status', educationLevelId: 'Education Level', degreeQualificationId: 'Degree / Qualification', religionId: 'Religion', socialCategoryId: 'Social Category', mobileNo: 'Mobile Number', alternateMobileNo: 'Alternate Mobile Number', emailAddress: 'Email Address', dateOfBirth: 'Date of Birth', occupationCategoryId: 'Occupation Category', occupationId: 'Occupation', monthlyIncome: 'Monthly Income', annualIncome: 'Annual Income', annualTurnover: 'Yearly Turnover', monthlyTurnover: 'Monthly Turnover', isPwd: 'Person with Disability', pwdCategoryId: 'Disability Type', isSeniorCitizen: 'Senior Citizen', agricultureIncome: 'Agriculture Income', nonAgricultureIncome: 'Non-Agriculture Income', submittedOnDate: 'Submitted On', savingsProductId: 'Linked Savings Product', staffId: 'Staff' }; /** Key under which the maker's point-in-time "before" snapshot rides along in the submit payload — * stripped out by the backend before replaying the rest through updateClient(), and stored * separately as original_json for the checker's diff. Never a real Client field name, so it can't * collide with anything ClientDataValidator actually expects. */ export const ORIGINAL_VALUES_PAYLOAD_KEY = '_originalValues'; /** Builds the maker-side "before" snapshot from the client record loaded when the edit page * opened — flat key names matching the payload's own, so the checker side can diff two plain * objects directly without needing to know Client's nested response shape at all. */ export function buildOriginalValuesSnapshot(clientData: any): Record { const snapshot: Record = {}; if (!clientData) return snapshot; for (const key of Object.keys(PENDING_EDIT_FIELD_EXTRACTORS)) { const value = PENDING_EDIT_FIELD_EXTRACTORS[key](clientData); if (value !== undefined) { snapshot[key] = value; } } return snapshot; }