Files
calcserubi/assets/js/engine-public.js
T
2026-10-20 13:16:38 +09:00

100 lines
3.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* SERUBI public engine: the customer-facing result, calculated in the browser
* from the folded model of engine_client_model() (includes/engine.php). The
* page never receives the bank's internal parameters, only one margin per
* tenor. Same numbers as assets/js/engine.js and includes/engine.php, checked
* against the reference workbook by tests/validate_public.js.
* Works in the browser (window.SerubiPublicEngine) and in Node (module.exports).
*/
(function (root, factory) {
if (typeof module === 'object' && module.exports) module.exports = factory();
else root.SerubiPublicEngine = factory();
})(typeof self !== 'undefined' ? self : this, function () {
'use strict';
var PRODUCTS = [
{ key: 'giro', name: 'Giro Umum' },
{ key: 'simanja', name: 'Tabungan SIMANJA' },
{ key: 'simpeda', name: 'Tabungan SIMPEDA' }
];
// Excel ROUNDDOWN(x, 0) on 15 significant digits (see engine.js).
function rounddown(x) {
var v = Number(x.toPrecision(15));
return v < 0 ? Math.ceil(v) : Math.floor(v);
}
function tierRate(tiers, n) {
if (!tiers || !tiers.length) return 0;
for (var i = 0; i < tiers.length; i++) {
var t = tiers[i];
if (t.max === null || t.max === undefined || n <= t.max) return Number(t.rate) || 0;
}
return 0;
}
function shareFor(m, n, tenor) {
if ((m.cashbackTenors || []).indexOf(tenor) === -1) return { share: null, reason: 'tenor' };
var tiers = (m.cashbackTiers || []).slice().sort(function (a, b) { return b.min - a.min; });
for (var i = 0; i < tiers.length; i++) {
if (n >= tiers[i].min) return { share: Number(tiers[i].nasabah), reason: null };
}
return { share: null, reason: 'nominal' };
}
function calculate(m, input) {
var n = Math.max(0, Number(input.nominal) || 0);
var t = Number(input.tenor) || 0;
var customerType = input.customerType === 'badan' ? 'badan' : 'perorangan';
var year = m.days.year, month = m.days.month;
var sh = shareFor(m, n, t);
var eligible = sh.share !== null;
var share = eligible ? sh.share : 0;
// Bunga per produk (COB!J24/N24/R24), identical to the full engine
var bunga = {}, bungaSum = 0;
PRODUCTS.forEach(function (prod) {
var rate = tierRate((m.productRates || {})[prod.key], n);
var b = rounddown(((n * rate * month) / year) * t);
bunga[prod.key] = { rate: rate, gross: b };
bungaSum += b;
});
// Rata-rata spread 3 produk = margin × nominal − rata-rata bunga − biaya program/3 − premi LPS
var lps = Math.min(n, m.lps.cap) * m.lps.rate;
var avgSpread = n * (m.margin[t] || 0) - bungaSum / 3 - m.programAvg - lps;
var gross = eligible ? avgSpread * share : 0;
var negative = gross < 0;
if (negative) gross = 0;
var taxRate = m.tax[customerType];
var tax = gross * taxRate;
var net = gross - tax;
var equiv = n > 0 && t > 0 ? (net / n) * (year / (month * t)) : 0;
var comparison = PRODUCTS.map(function (prod) {
var b = bunga[prod.key];
var bungaNet = b.gross - b.gross * m.tax.bunga;
var total = bungaNet + net;
return { key: prod.key, name: prod.name, rate: b.rate, bungaNet: bungaNet, cashbackNet: net, total: total, totalRate: b.rate + equiv };
});
var penalty = { refund: net, tax: gross * taxRate, fee: net * m.pinalti, rate: m.pinalti };
penalty.total = penalty.refund + penalty.tax + penalty.fee;
var mins = (m.cashbackTiers || []).map(function (x) { return x.min; });
return {
input: { nominal: n, tenor: t, customerType: customerType },
eligible: eligible && !negative,
reason: negative ? 'spread' : sh.reason,
minNominal: mins.length ? Math.min.apply(null, mins) : 0,
share: share,
cashback: { gross: gross, taxRate: taxRate, tax: tax, net: net, monthly: t ? net / t : 0, equiv: equiv },
comparison: comparison,
penalty: penalty
};
}
return { calculate: calculate, PRODUCTS: PRODUCTS };
});