158 lines
6.8 KiB
JavaScript
158 lines
6.8 KiB
JavaScript
/*
|
|
* SERUBI cashback engine.
|
|
* Faithful port of sheet "COB" and "CALCULATOR CASHBACK SERUBI" from
|
|
* CALCULATOR CASHBACK SERUB KANTOR CABANG 22092026.xlsx. Cell references are
|
|
* noted next to each step so the numbers can be audited against the workbook.
|
|
* Works in the browser (window.SerubiEngine) and in Node (module.exports).
|
|
*/
|
|
(function (root, factory) {
|
|
if (typeof module === 'object' && module.exports) module.exports = factory();
|
|
else root.SerubiEngine = 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) works on 15 significant digits; mirror that so
|
|
// values like 203424.99999999997 do not lose a rupiah.
|
|
function rounddown(x) {
|
|
var v = Number(x.toPrecision(15));
|
|
return v < 0 ? Math.ceil(v) : Math.floor(v);
|
|
}
|
|
|
|
// Tiers are [{max: number|null, rate}], ascending; first tier with n <= max
|
|
// wins, a null max means "di atas" (everything above the previous tier).
|
|
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;
|
|
}
|
|
|
|
// Cashback sharing: [{min, nasabah}] — highest min that n reaches wins (COB!I34:I35).
|
|
function shareFor(params, n, tenor) {
|
|
if ((params.cashbackTenors || []).indexOf(tenor) === -1) return { share: null, reason: 'tenor' };
|
|
var tiers = (params.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 minCashbackNominal(params) {
|
|
var mins = (params.cashbackTiers || []).map(function (t) { return t.min; });
|
|
return mins.length ? Math.min.apply(null, mins) : 0;
|
|
}
|
|
|
|
function depositoRate(params, n, tenor) {
|
|
if (n < params.settings.deposito_min) return 0; // COB!K14: F4<1000000 -> 0
|
|
return tierRate((params.depositoRates || {})[tenor], n);
|
|
}
|
|
|
|
function calculate(params, input) {
|
|
var p = params.settings;
|
|
var n = Math.max(0, Number(input.nominal) || 0);
|
|
var t = Number(input.tenor) || 0;
|
|
var customerType = input.customerType === 'badan' ? 'badan' : 'perorangan';
|
|
var year = p.hari_setahun, month = p.hari_sebulan;
|
|
|
|
// Pengelolaan dana (COB!F23:F38)
|
|
var gwm = n * p.gwm; // F24
|
|
var kas = n * p.kas; // F25
|
|
var managed = n - (gwm + kas); // F26
|
|
var puabBase = managed * p.porsi_puab; // F28
|
|
var puabIncome = puabBase * p.puab * (month / year) * t; // F31 (A)
|
|
var creditBase = managed * p.porsi_kredit; // F34
|
|
var creditIncome = creditBase * p.kredit * (t / 12); // F36 (B)
|
|
var income = puabIncome + creditIncome; // F38
|
|
|
|
var sh = shareFor(params, n, t);
|
|
var eligible = sh.share !== null;
|
|
var share = eligible ? sh.share : 0;
|
|
|
|
// Biaya & spread per produk (COB!H22:R37)
|
|
var lps = Math.min(n, p.lps_batas) * p.premi_lps; // J27 (charged once, not prorated)
|
|
var ohc = puabIncome * p.ohc; // J26 / N25 / R25
|
|
var products = {};
|
|
PRODUCTS.forEach(function (prod) {
|
|
var rate = tierRate((params.productRates || {})[prod.key], n); // I24 / M24 / Q24
|
|
var bunga = rounddown(((n * rate * month) / year) * t); // J24 / N24 / R24
|
|
var program = prod.key === 'simanja' ? p.biaya_program : 0; // J25 (SIMANJA only)
|
|
var cost = bunga + program + ohc + lps; // J28 / N27 / R27
|
|
var spread = income - cost; // J29 / N28 / R28
|
|
var bungaTax = bunga * p.pajak_tabungan; // F7 / F10 / F13
|
|
products[prod.key] = {
|
|
key: prod.key, name: prod.name, rate: rate,
|
|
bungaGross: bunga, bungaTax: bungaTax, bungaNet: bunga - bungaTax,
|
|
program: program, ohc: ohc, lps: lps, cost: cost, spread: spread,
|
|
cashbackGross: spread * share, // J35 / N34 / R34
|
|
costOfFund: managed > 0 ? (cost + spread * share) / managed : 0 // J30 / N29 / R29
|
|
};
|
|
});
|
|
|
|
// Cashback = rata-rata 3 produk (COB!H7:H10)
|
|
var gross = eligible ? (products.giro.cashbackGross + products.simanja.cashbackGross + products.simpeda.cashbackGross) / 3 : 0;
|
|
var negative = gross < 0;
|
|
if (negative) gross = 0;
|
|
var taxRate = customerType === 'badan' ? p.pajak_cashback_badan : p.pajak_cashback_perorangan;
|
|
var tax = gross * taxRate; // H9
|
|
var net = gross - tax; // H10
|
|
var equiv = n > 0 && t > 0 ? (net / n) * (year / (month * t)) : 0; // CALC!D10
|
|
|
|
// Deposito pembanding (COB!K14:L17)
|
|
var dRate = depositoRate(params, n, t);
|
|
var dGross = rounddown(((n * dRate * month) / year) * t);
|
|
var dTax = dGross * p.pajak_deposito;
|
|
var dNet = dGross - dTax;
|
|
var deposito = { rate: dRate, gross: dGross, tax: dTax, net: dNet, monthly: t ? dNet / t : 0 };
|
|
|
|
// Perbandingan (CALC!A10:F13)
|
|
var comparison = PRODUCTS.map(function (prod) {
|
|
var pr = products[prod.key];
|
|
var total = pr.bungaNet + net;
|
|
return {
|
|
key: prod.key, name: prod.name, rate: pr.rate, bungaNet: pr.bungaNet,
|
|
cashbackNet: net, total: total, totalRate: pr.rate + equiv, diff: total - dNet
|
|
};
|
|
});
|
|
|
|
// Pinalti pencairan sebelum jatuh tempo (CALC!A17:B22)
|
|
var penalty = {
|
|
refund: net,
|
|
tax: gross * taxRate,
|
|
fee: net * p.pinalti,
|
|
total: 0
|
|
};
|
|
penalty.total = penalty.refund + penalty.tax + penalty.fee;
|
|
|
|
return {
|
|
input: { nominal: n, tenor: t, customerType: customerType },
|
|
eligible: eligible && !negative,
|
|
reason: negative ? 'spread' : sh.reason,
|
|
minNominal: minCashbackNominal(params),
|
|
share: share, shareBank: eligible ? 1 - share : 0,
|
|
fund: {
|
|
gwm: gwm, kas: kas, managed: managed, puabBase: puabBase, puabIncome: puabIncome,
|
|
creditBase: creditBase, creditIncome: creditIncome, income: income
|
|
},
|
|
products: products,
|
|
cashback: { gross: gross, taxRate: taxRate, tax: tax, net: net, monthly: t ? net / t : 0, equiv: equiv },
|
|
deposito: deposito,
|
|
comparison: comparison,
|
|
best: comparison.reduce(function (a, b) { return b.total > a.total ? b : a; }),
|
|
penalty: penalty
|
|
};
|
|
}
|
|
|
|
return {
|
|
calculate: calculate, tierRate: tierRate, depositoRate: depositoRate,
|
|
rounddown: rounddown, PRODUCTS: PRODUCTS
|
|
};
|
|
});
|