asd
This commit is contained in:
commit
3e4a056b14
67 files changed
+13926
No files matched your search
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* 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
|
||||
};
|
||||
});
|
||||
Reference in new issue
Block a user