69 lines
5.6 KiB
TypeScript
69 lines
5.6 KiB
TypeScript
import { restoreBusinessPlanSnapshot } from './business-financial-plan.ts';
|
|
const empty = (value: unknown) => value == null || value === '' || (Array.isArray(value) && value.length === 0);
|
|
function canonical(value: unknown): unknown {
|
|
if (Array.isArray(value)) return value.map(canonical);
|
|
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).sort(([a],[b])=>a.localeCompare(b)).map(([key,item])=>[key,canonical(item)]));
|
|
return value;
|
|
}
|
|
// Change tracking only, never an authorization, integrity or legal-acceptance token. No personal values in metadata.
|
|
function signature(value: unknown): string {
|
|
const text=JSON.stringify(canonical(value)) ?? 'undefined';let hash=14695981039346656037n;
|
|
for(let index=0;index<text.length;index++) hash=BigInt.asUintN(64,(hash^BigInt(text.charCodeAt(index)))*1099511628211n);
|
|
return `${text.length}:${hash.toString(16)}`;
|
|
}
|
|
export function inheritCompatibleFields(scope:string, values:Record<string,unknown>, source:Record<string,unknown>, raw:unknown, allowed?:string[]): {patch:Record<string,unknown>; metadata:string; inherited:string[]; conflicts:string[]} {
|
|
let previous:Record<string,string>={};
|
|
try { const saved=JSON.parse(String(raw ?? ''));if(saved.version===1 && saved.scope===scope && saved.inherited && typeof saved.inherited==='object') previous=saved.inherited; } catch { /* Legacy inputs are retained, not overwritten. */ }
|
|
const next={...previous};const patch:Record<string,unknown>={};const inherited:string[]=[];const conflicts:string[]=[];
|
|
const geo=['province_id','district_id','admin_post_id','locality_id'];
|
|
const geoConflict=geo.some(key=>(!empty(values[key]) || key in previous) && signature(values[key])!==signature(source[key]) && previous[key]!==signature(values[key]));
|
|
for(const [key,value] of Object.entries(source)) {
|
|
if ((allowed && !allowed.includes(key)) || (empty(value) && !(key in previous))) continue;
|
|
const current=values[key];
|
|
const track = (item:unknown) => key==='parcels' ? Object.fromEntries(geo.map(field=>[field,(item as Array<Record<string,unknown>>)?.[0]?.[field] ?? ''])) : item;
|
|
const owned=previous[key]===signature(track(current));
|
|
const vacant=key==='parcels' ? geo.every(field=>empty((current as Array<Record<string,unknown>>)?.[0]?.[field])) : empty(current);
|
|
if ((geo.includes(key) && geoConflict) || (!owned && (!vacant || key in previous))) {
|
|
if(signature(current)!==signature(value)) conflicts.push(key);
|
|
continue;
|
|
}
|
|
if(key==='parcels') {
|
|
const parcels=current as Array<Record<string,unknown>>|undefined;
|
|
if(!owned && parcels?.length && geo.some(field=>!empty(parcels[0]?.[field]))) {conflicts.push(key);continue;}
|
|
}
|
|
if(signature(track(current))!==signature(track(value))) {
|
|
if(key==='parcels') {
|
|
const parcels=(current as Array<Record<string,unknown>>) ?? [];
|
|
patch[key]=[{...parcels[0],...Object.fromEntries(geo.map(field=>[field,(value as Array<Record<string,unknown>>)[0]?.[field] ?? '']))},...parcels.slice(1)];
|
|
} else patch[key]=value;
|
|
}
|
|
next[key]=signature(track(value));inherited.push(key);
|
|
}
|
|
return {patch,metadata:JSON.stringify({version:1,scope,inherited:next}),inherited,conflicts};
|
|
}
|
|
|
|
export function inheritSimulatorFields<T extends Record<string,unknown>>(draft:T): T {
|
|
const source:Record<string,unknown>={};
|
|
const plan=draft.business_plan_snapshot ? restoreBusinessPlanSnapshot(String(draft.business_plan_snapshot)) : null;
|
|
if(plan?.currency) source.currency=plan.currency;
|
|
const currency=draft.currency || source.currency;
|
|
if(currency) {source.fiscal_currency=currency;source.opportunity_currency=currency;source.published_iva_currency=currency;}
|
|
if(draft.tax_residency_country) source.fiscal_residency_country=draft.tax_residency_country;
|
|
if(draft.fiscal_reference_date) source.published_iva_date=draft.fiscal_reference_date;
|
|
let settings:Record<string,unknown>={start:'',basis:'unknown',declared:false,auto:false};
|
|
let settingsValid=!draft.monthly_iva_settings;
|
|
try {const saved=JSON.parse(String(draft.monthly_iva_settings ?? ''));if(saved && !Array.isArray(saved) && typeof saved.start==='string' && ['net','included','unknown'].includes(saved.basis) && typeof saved.declared==='boolean' && typeof saved.auto==='boolean') {settings=saved;settingsValid=true;}} catch { /* Invalid stored inputs remain intact. */ }
|
|
const start=String(draft.expected_start_date ?? '');
|
|
if(/^\d{4}-\d{2}-\d{2}$/.test(start)) source.monthly_iva_start=start.slice(0,7);
|
|
else if(/^\d{4}$/.test(String(draft.fiscal_period)) && draft.fiscal_periodicity==='monthly' && /^(0?[1-9]|1[0-2])$/.test(String(draft.fiscal_month))) source.monthly_iva_start=`${draft.fiscal_period}-${String(draft.fiscal_month).padStart(2,'0')}`;
|
|
if(!settingsValid) delete source.monthly_iva_start;
|
|
// Legacy drafts without a compatible source need no inheritance marker or autosave churn.
|
|
if (!Object.keys(source).length && !draft.compatible_fields_snapshot) return draft;
|
|
const values={...draft,monthly_iva_start:settings.start ?? ''};
|
|
const patch=inheritCompatibleFields('simulator-compatible-fields',values,source,draft.compatible_fields_snapshot);
|
|
const next:Record<string,unknown>={...draft,...patch.patch,compatible_fields_snapshot:patch.metadata};
|
|
if('monthly_iva_start' in patch.patch) next.monthly_iva_settings=JSON.stringify({...settings,start:patch.patch.monthly_iva_start});
|
|
delete next.monthly_iva_start;
|
|
return JSON.stringify(next)===JSON.stringify(draft) ? draft : next as T;
|
|
}
|