This commit is contained in:
DIGIDEV\corer committed 2026-10-20 13:16:38 +09:00
commit 3e4a056b14
67 files changed
+13926

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env node
/*
* Validates assets/js/engine.js against values recalculated by Microsoft Excel
* from the official workbook (tests/excel-fixtures.json, produced by
* tests/export-excel-fixtures.ps1), using the parameters stored in MySQL.
*
* node tests/validate.js engine (DB params) vs Excel
* node tests/validate.js --legacy also measure the original single-file HTML app
*/
'use strict';
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
const Engine = require('../assets/js/engine.js');
const root = path.join(__dirname, '..');
const fixtures = JSON.parse(fs.readFileSync(path.join(__dirname, 'excel-fixtures.json'), 'utf8').replace(/^\uFEFF/, ''));
const php = process.env.PHP_BIN || 'C:/laragon/bin/php/php-8.1.10-Win32-vs16-x64/php.exe';
const params = JSON.parse(execFileSync(php, ['-d', 'display_startup_errors=0', path.join(root, 'database/migrate.php'), '--dump-params'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }));
// Rupiah amounts must match to the rupiah after rounding; rates to 1e-9.
const close = (a, b, kind) => kind === 'rate' ? Math.abs(a - b) < 1e-9 : Math.round(a) === Math.round(b) && Math.abs(a - b) < 0.01;
function engineView(r) {
const by = Object.fromEntries(r.comparison.map(c => [c.key, c]));
const ok = r.eligible;
return {
cashbackGross: ok ? r.cashback.gross : null, cashbackNet: ok ? r.cashback.net : null,
giroNet: r.products.giro.bungaNet, simanjaNet: r.products.simanja.bungaNet, simpedaNet: r.products.simpeda.bungaNet,
depositoNet: r.deposito.net, depositoGross: r.deposito.gross, rateDeposito: r.deposito.rate,
totalGiro: ok ? by.giro.total : null, totalSimanja: ok ? by.simanja.total : null, totalSimpeda: ok ? by.simpeda.total : null,
totalDeposito: r.deposito.net,
rateGiro: r.products.giro.rate, rateSimanja: r.products.simanja.rate, rateSimpeda: r.products.simpeda.rate,
equiv: ok ? r.cashback.equiv : null,
totalRateGiro: ok ? by.giro.totalRate : null, totalRateSimanja: ok ? by.simanja.totalRate : null, totalRateSimpeda: ok ? by.simpeda.totalRate : null,
penaltyRefund: ok ? r.penalty.refund : null, penaltyTax: ok ? r.penalty.tax : null, penaltyFee: ok ? r.penalty.fee : null, penaltyTotal: ok ? r.penalty.total : null,
shareNasabah: ok ? r.share : null, shareBank: ok ? r.shareBank : null,
income: r.fund.income, puabIncome: r.fund.puabIncome, creditIncome: r.fund.creditIncome,
spreadSimanja: r.products.simanja.spread, spreadSimpeda: r.products.simpeda.spread, spreadGiro: r.products.giro.spread
};
}
const RATE_FIELDS = new Set(['equiv', 'rateGiro', 'rateSimanja', 'rateSimpeda', 'rateDeposito', 'totalRateGiro', 'totalRateSimanja', 'totalRateSimpeda', 'shareNasabah', 'shareBank']);
function compare(label, viewFn) {
let checks = 0; const failures = [];
for (const c of fixtures.cases) {
const got = viewFn(c);
for (const [field, expected] of Object.entries(c)) {
if (field === 'nominal' || field === 'tenor' || !(field in got)) continue;
checks++;
const actual = got[field];
const kind = RATE_FIELDS.has(field) ? 'rate' : 'idr';
const pass = expected === null ? actual === null : actual !== null && close(actual, expected, kind);
if (!pass) failures.push({ nominal: c.nominal, tenor: c.tenor, field, excel: expected, app: actual });
}
}
console.log(`\n${label}: ${checks - failures.length}/${checks} checks match Excel over ${fixtures.cases.length} cases`);
return failures;
}
const failures = compare('Engine (parameter dari MySQL)', c =>
engineView(Engine.calculate(params, { nominal: c.nominal, tenor: c.tenor, customerType: 'perorangan' })));
for (const f of failures.slice(0, 20)) console.log(' MISMATCH', JSON.stringify(f));
if (process.argv.includes('--legacy')) {
const legacy = require('./legacy-original.js');
const lf = compare('Aplikasi HTML lama', c => legacy(c.nominal, c.tenor));
const byField = {};
for (const f of lf) (byField[f.field] = byField[f.field] || []).push(f);
for (const [field, list] of Object.entries(byField)) {
const ex = list[0];
console.log(` ${field}: ${list.length} mismatch, contoh ${ex.nominal}/${ex.tenor} bln -> Excel ${ex.excel} vs lama ${ex.app}`);
}
}
process.exitCode = failures.length ? 1 : 0;