grantforge-api / frontend-react /src /components /company /strategyCascadeModel.node-test.mjs
GrantForge Bot
Deploy sha-565ad85979610064f6d1c18ab3b6404357d61073 — source build (no GHCR)
ce8f04a
Raw
History Blame Contribute Delete
11.2 kB
/**
* Node built-in test runner for pure strategy cascade model.
* Mirrors strategyCascadeModel.ts logic (kept small + isolated).
* Run: node --test src/components/company/strategyCascadeModel.node-test.mjs
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
const CANONICAL_CASCADE_ORDER = [
'tax_psi_ulgi',
'regional_fst',
'national_feng',
'debt_loan_guarantee',
'direct_eu_prep',
];
function normalizeStrategyResult(raw) {
const empty = {
paths: [],
primaryPathId: null,
recommendedPathIds: [],
deMinimisExhausted: false,
mspStatus: null,
deMinimisUsedEur: null,
notes: [],
cascadeOrder: [...CANONICAL_CASCADE_ORDER],
};
if (!raw || typeof raw !== 'object') return empty;
const pathsIn = Array.isArray(raw.paths) ? raw.paths : [];
const paths = pathsIn
.filter((p) => p && typeof p === 'object')
.map((p) => {
const checklistIds = Array.isArray(p.checklist_ids)
? p.checklist_ids.map(String).filter(Boolean)
: undefined;
const view = {
id: String(p.id || ''),
label: String(p.label || p.id || 'Ścieżka'),
score: Number(p.score) || 0,
reasons: Array.isArray(p.reasons) ? p.reasons.map(String).filter(Boolean) : [],
primary: Boolean(p.primary),
de_minimis_blocked: Boolean(p.de_minimis_blocked),
};
if (checklistIds && checklistIds.length > 0) {
view.checklist_ids = checklistIds;
}
if (p.checklist_id != null && p.checklist_id !== '') {
view.checklist_id = String(p.checklist_id);
} else if (checklistIds && checklistIds.length > 0) {
view.checklist_id = checklistIds[0];
}
if (p.never_say_submit_to_parp != null) {
view.never_say_submit_to_parp = Boolean(p.never_say_submit_to_parp);
}
return view;
})
.filter((p) => p.id);
paths.sort((a, b) => b.score - a.score);
const primaryFromFlag = paths.find((p) => p.primary)?.id ?? null;
const primaryPathId =
(typeof raw.primary_path_id === 'string' && raw.primary_path_id) || primaryFromFlag;
if (primaryPathId) {
for (const p of paths) {
p.primary = p.id === primaryPathId && !p.de_minimis_blocked;
}
if (!paths.some((p) => p.primary)) {
const fallback = paths.find((p) => !p.de_minimis_blocked);
if (fallback) fallback.primary = true;
}
}
const rec = Array.isArray(raw.recommended_paths) ? raw.recommended_paths : [];
const recommendedPathIds = rec
.map((p) => (p && typeof p === 'object' ? String(p.id || '') : ''))
.filter(Boolean);
const elig =
raw.eligibility_summary && typeof raw.eligibility_summary === 'object'
? raw.eligibility_summary
: {};
return {
paths,
primaryPathId: paths.find((p) => p.primary)?.id ?? primaryPathId,
recommendedPathIds:
recommendedPathIds.length > 0
? recommendedPathIds
: paths.filter((p) => !p.de_minimis_blocked).slice(0, 4).map((p) => p.id),
deMinimisExhausted: Boolean(elig.de_minimis_exhausted),
mspStatus: elig.msp != null ? String(elig.msp) : null,
deMinimisUsedEur:
typeof elig.de_minimis_used_eur === 'number' ? elig.de_minimis_used_eur : null,
notes: Array.isArray(raw.notes) ? raw.notes.map(String) : [],
cascadeOrder: Array.isArray(raw.cascade_order)
? raw.cascade_order.map(String)
: [...CANONICAL_CASCADE_ORDER],
};
}
function formatScore(score) {
if (!Number.isFinite(score)) return '—';
return Math.round(score).toString();
}
function isBlockedPath(path) {
return Boolean(path.de_minimis_blocked);
}
const TAX_CHECKLIST_LABELS = {
psi: 'PSI',
ulga_br: 'Ulga B+R',
ip_box: 'IP Box',
};
function formatTaxChecklistId(id) {
return TAX_CHECKLIST_LABELS[id] || id;
}
function hasTaxChecklists(path) {
return Array.isArray(path.checklist_ids) && path.checklist_ids.length > 0;
}
/**
* Mirror of strategyCascadeModel.resolveAutoSelectedPath
*/
function resolveAutoSelectedPath(view, currentSelected, userHasPicked) {
if (userHasPicked && currentSelected) {
const still = view.paths.find(
(p) => p.id === currentSelected && !isBlockedPath(p),
);
if (still) return null;
}
if (currentSelected) {
const still = view.paths.find(
(p) => p.id === currentSelected && !isBlockedPath(p),
);
if (still && !userHasPicked) {
return null;
}
}
const primary =
view.paths.find((p) => p.primary && !isBlockedPath(p)) ||
view.paths.find((p) => p.id === view.primaryPathId && !isBlockedPath(p)) ||
view.paths.find((p) => !isBlockedPath(p));
if (!primary) return null;
if (primary.id === currentSelected) return null;
return primary;
}
describe('normalizeStrategyResult', () => {
it('returns empty for null', () => {
const v = normalizeStrategyResult(null);
assert.equal(v.paths.length, 0);
assert.equal(v.primaryPathId, null);
});
it('keeps ≥4 paths and marks primary non-blocked', () => {
const v = normalizeStrategyResult({
paths: [
{ id: 'tax_psi_ulgi', label: 'Tax', score: 55, primary: false },
{ id: 'regional_fst', label: 'Region', score: 70, primary: true },
{ id: 'national_feng', label: 'FENG', score: 60, primary: false },
{
id: 'debt_loan_guarantee',
label: 'Debt',
score: 80,
primary: false,
de_minimis_blocked: true,
},
{ id: 'direct_eu_prep', label: 'EU', score: 40, primary: false },
],
primary_path_id: 'regional_fst',
recommended_paths: [
{ id: 'regional_fst' },
{ id: 'national_feng' },
{ id: 'tax_psi_ulgi' },
{ id: 'direct_eu_prep' },
],
eligibility_summary: { de_minimis_exhausted: true, msp: 'srednia' },
});
assert.ok(v.paths.length >= 4);
assert.equal(v.primaryPathId, 'regional_fst');
assert.equal(v.paths.find((p) => p.id === 'regional_fst')?.primary, true);
assert.equal(v.paths.find((p) => p.id === 'debt_loan_guarantee')?.de_minimis_blocked, true);
assert.equal(v.deMinimisExhausted, true);
// blocked path still visible (sorted by score first)
assert.equal(v.paths[0].id, 'debt_loan_guarantee');
assert.ok(v.recommendedPathIds.includes('regional_fst'));
});
it('does not mark blocked path as primary even if API says so', () => {
const v = normalizeStrategyResult({
paths: [
{ id: 'debt_loan_guarantee', label: 'Debt', score: 90, primary: true, de_minimis_blocked: true },
{ id: 'tax_psi_ulgi', label: 'Tax', score: 50, primary: false },
{ id: 'regional_fst', label: 'R', score: 48 },
{ id: 'national_feng', label: 'F', score: 47 },
],
primary_path_id: 'debt_loan_guarantee',
});
assert.notEqual(v.primaryPathId, 'debt_loan_guarantee');
assert.equal(v.paths.find((p) => p.id === 'debt_loan_guarantee')?.primary, false);
assert.ok(v.paths.some((p) => p.primary && !p.de_minimis_blocked));
});
it('passes through tax checklist_ids and never_say_submit_to_parp', () => {
const v = normalizeStrategyResult({
paths: [
{
id: 'tax_psi_ulgi',
label: 'Podatki / PSI / ulgi',
score: 75,
primary: true,
checklist_ids: ['psi', 'ulga_br', 'ip_box'],
checklist_id: 'psi',
never_say_submit_to_parp: true,
},
{ id: 'regional_fst', label: 'Region', score: 50 },
{ id: 'national_feng', label: 'FENG', score: 48 },
{ id: 'debt_loan_guarantee', label: 'Debt', score: 40 },
],
primary_path_id: 'tax_psi_ulgi',
});
const tax = v.paths.find((p) => p.id === 'tax_psi_ulgi');
assert.ok(tax);
assert.deepEqual(tax.checklist_ids, ['psi', 'ulga_br', 'ip_box']);
assert.equal(tax.checklist_id, 'psi');
assert.equal(tax.never_say_submit_to_parp, true);
assert.equal(hasTaxChecklists(tax), true);
assert.equal(hasTaxChecklists(v.paths.find((p) => p.id === 'regional_fst')), false);
assert.equal(formatTaxChecklistId('psi'), 'PSI');
assert.equal(formatTaxChecklistId('ulga_br'), 'Ulga B+R');
assert.equal(formatTaxChecklistId('ip_box'), 'IP Box');
});
it('defaults checklist_id from first checklist_ids entry', () => {
const v = normalizeStrategyResult({
paths: [
{
id: 'tax_psi_ulgi',
label: 'Tax',
score: 60,
checklist_ids: ['psi', 'ulga_br', 'ip_box'],
},
{ id: 'regional_fst', label: 'R', score: 50 },
{ id: 'national_feng', label: 'F', score: 48 },
{ id: 'debt_loan_guarantee', label: 'D', score: 40 },
],
});
const tax = v.paths.find((p) => p.id === 'tax_psi_ulgi');
assert.equal(tax.checklist_id, 'psi');
});
});
describe('formatScore', () => {
it('rounds finite scores', () => {
assert.equal(formatScore(55.4), '55');
assert.equal(formatScore(55.6), '56');
});
it('handles non-finite', () => {
assert.equal(formatScore(NaN), '—');
});
});
describe('resolveAutoSelectedPath', () => {
const baseView = () =>
normalizeStrategyResult({
paths: [
{ id: 'regional_fst', label: 'Region', score: 70, primary: true },
{ id: 'national_feng', label: 'FENG', score: 60, primary: false },
{ id: 'tax_psi_ulgi', label: 'Tax', score: 55, primary: false },
{
id: 'debt_loan_guarantee',
label: 'Debt',
score: 80,
de_minimis_blocked: true,
},
{ id: 'direct_eu_prep', label: 'EU', score: 40, primary: false },
],
primary_path_id: 'regional_fst',
});
it('auto-picks primary when nothing selected', () => {
const v = baseView();
const auto = resolveAutoSelectedPath(v, null, false);
assert.ok(auto);
assert.equal(auto.id, 'regional_fst');
assert.equal(auto.primary, true);
});
it('does not re-notify when parent already holds primary', () => {
const v = baseView();
const auto = resolveAutoSelectedPath(v, 'regional_fst', false);
assert.equal(auto, null);
});
it('keeps user pick when still valid', () => {
const v = baseView();
const auto = resolveAutoSelectedPath(v, 'national_feng', true);
assert.equal(auto, null);
});
it('falls back when user pick is blocked', () => {
const v = baseView();
const auto = resolveAutoSelectedPath(v, 'debt_loan_guarantee', true);
assert.ok(auto);
assert.equal(auto.id, 'regional_fst');
});
it('never auto-selects de_minimis_blocked path', () => {
const v = normalizeStrategyResult({
paths: [
{
id: 'debt_loan_guarantee',
label: 'Debt',
score: 99,
primary: true,
de_minimis_blocked: true,
},
{ id: 'tax_psi_ulgi', label: 'Tax', score: 50 },
{ id: 'regional_fst', label: 'R', score: 48 },
{ id: 'national_feng', label: 'F', score: 47 },
],
primary_path_id: 'debt_loan_guarantee',
});
const auto = resolveAutoSelectedPath(v, null, false);
assert.ok(auto);
assert.notEqual(auto.id, 'debt_loan_guarantee');
assert.equal(auto.de_minimis_blocked, false);
});
});