Files
Investidor-V2/resources/js/lib/context-inheritance.ts
T

326 lines
12 KiB
TypeScript

import { inputText } from './input-text.ts';
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(inputText(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;
if (key === 'context_applicability_snapshot') {
// Read-only assessment of the current dossier, not a deliberate form answer.
if (signature(values[key]) !== signature(value)) patch[key] = value;
next[key] = signature(value);
inherited.push(key);
continue;
}
if (key.endsWith('_label')) {
const base = key.slice(0, -6);
const baseOwned = previous[base] === signature(values[base]);
const baseVacant = empty(values[base]) && !(base in previous);
if (
!(base in source) ||
(geo.includes(base) && geoConflict) ||
(!baseOwned &&
!baseVacant &&
signature(values[base]) !== signature(source[base]))
)
continue;
// A label describes its identifier; it must never lag behind an adopted identifier.
if (signature(values[key]) !== signature(value)) patch[key] = value;
next[key] = signature(value);
inherited.push(key);
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,
};
}
/** Presentation only: a saved, unedited inherited value can be shown without asking again. */
export function reusedInvestmentValue(
scope: string,
key: string,
values: Record<string, unknown>,
): string | null {
if (
![
'journey_type',
'purpose',
'project_title',
'project_summary',
'sector_id',
'province_id',
'district_id',
'admin_post_id',
'locality_id',
].includes(key)
)
return null;
try {
const metadata = JSON.parse(inputText(values.context_reuse ?? ''));
if (
metadata.version !== 1 ||
metadata.scope !== scope ||
empty(values[key]) ||
metadata.inherited?.[key] !== signature(values[key])
)
return null;
if (key.endsWith('_id') || ['journey_type', 'purpose'].includes(key)) {
const label = values[`${key}_label`];
return typeof label === 'string' &&
label.trim() &&
metadata.inherited?.[`${key}_label`] === signature(label)
? label
: null;
}
return typeof values[key] === 'string' ? values[key] : null;
} catch {
return null;
}
}
/** Report the lineage without changing a stored answer or disclosing the tracking hash. */
export function investmentFieldOrigin(
values: Record<string, unknown>,
key: string,
fallback: string,
): string {
try {
const reuse = JSON.parse(inputText(values.context_reuse ?? ''));
if (
reuse.version !== 1 ||
!/^dossier:.+:simulator$/.test(reuse.scope) ||
!reuse.inherited?.[key]
)
return fallback;
const snapshot = JSON.parse(
inputText(values.context_applicability_snapshot ?? ''),
);
const origin = snapshot.field_provenance?.[key];
const source =
origin?.kind === 'opportunity'
? `oportunidade «${origin.title ?? 'referência guardada'}»`
: origin?.kind === 'process_declaration' ||
origin?.kind === 'dossier'
? 'dados introduzidos pelo utilizador no dossier'
: origin?.kind === 'sector'
? `catálogo sectorial «${origin.title ?? 'referência guardada'}»`
: origin?.kind === 'territory'
? `catálogo territorial «${origin.title ?? 'referência guardada'}»`
: origin?.kind === 'linked_simulation'
? 'simulador ligado ao mesmo dossier'
: 'dossier de investimento';
const version = origin?.version ? `, versão ${origin.version}` : '';
return reuse.inherited[key] === signature(values[key])
? `Herdado de ${source}${version}`
: `Alterado posteriormente pelo utilizador; origem anterior: ${source}${version}`;
} catch {
return fallback;
}
}
export function inheritSimulatorFields<T extends Record<string, unknown>>(
draft: T,
): T {
const source: Record<string, unknown> = {};
const plan = draft.business_plan_snapshot
? restoreBusinessPlanSnapshot(inputText(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(inputText(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 = inputText(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 = `${inputText(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);
}