100 lines
3.9 KiB
JavaScript
100 lines
3.9 KiB
JavaScript
/*
|
||
* 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 };
|
||
});
|