@novha/calc-engines 1.3.1 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -14,12 +14,14 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.FranceIncomeTaxService = exports.CanadaMortgageService = exports.CanadaIncomeTaxService = void 0;
17
+ exports.SouthAfricaMortgageService = exports.FranceIncomeTaxService = exports.CanadaMortgageService = exports.CanadaIncomeTaxService = void 0;
18
18
  var CanadaIncomeTaxServiceImpl_1 = require("./income-tax/canada/CanadaIncomeTaxServiceImpl");
19
19
  Object.defineProperty(exports, "CanadaIncomeTaxService", { enumerable: true, get: function () { return CanadaIncomeTaxServiceImpl_1.CanadaIncomeTaxServiceImpl; } });
20
20
  var CanadaMortgageServiceImpl_1 = require("./mortgage/canada/CanadaMortgageServiceImpl");
21
21
  Object.defineProperty(exports, "CanadaMortgageService", { enumerable: true, get: function () { return CanadaMortgageServiceImpl_1.CanadaMortgageServiceImpl; } });
22
22
  var FranceIncomeTaxServiceImpl_1 = require("./income-tax/france/FranceIncomeTaxServiceImpl");
23
23
  Object.defineProperty(exports, "FranceIncomeTaxService", { enumerable: true, get: function () { return FranceIncomeTaxServiceImpl_1.FranceIncomeTaxServiceImpl; } });
24
+ var SouthAfricaMortgageServiceImpl_1 = require("./mortgage/south-africa/SouthAfricaMortgageServiceImpl");
25
+ Object.defineProperty(exports, "SouthAfricaMortgageService", { enumerable: true, get: function () { return SouthAfricaMortgageServiceImpl_1.SouthAfricaMortgageServiceImpl; } });
24
26
  __exportStar(require("./income-tax/domain/types"), exports);
25
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFBQSw2RkFBc0g7QUFBN0csb0lBQUEsMEJBQTBCLE9BQTBCO0FBQzdELHlGQUFpSDtBQUF4RyxrSUFBQSx5QkFBeUIsT0FBeUI7QUFDM0QsNkZBQXNIO0FBQTdHLG9JQUFBLDBCQUEwQixPQUEwQjtBQWU3RCw0REFBMEMiLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgeyBDYW5hZGFJbmNvbWVUYXhTZXJ2aWNlSW1wbCBhcyBDYW5hZGFJbmNvbWVUYXhTZXJ2aWNlIH0gZnJvbSAnLi9pbmNvbWUtdGF4L2NhbmFkYS9DYW5hZGFJbmNvbWVUYXhTZXJ2aWNlSW1wbCc7XG5leHBvcnQgeyBDYW5hZGFNb3J0Z2FnZVNlcnZpY2VJbXBsIGFzIENhbmFkYU1vcnRnYWdlU2VydmljZSB9IGZyb20gJy4vbW9ydGdhZ2UvY2FuYWRhL0NhbmFkYU1vcnRnYWdlU2VydmljZUltcGwnO1xuZXhwb3J0IHsgRnJhbmNlSW5jb21lVGF4U2VydmljZUltcGwgYXMgRnJhbmNlSW5jb21lVGF4U2VydmljZSB9IGZyb20gJy4vaW5jb21lLXRheC9mcmFuY2UvRnJhbmNlSW5jb21lVGF4U2VydmljZUltcGwnO1xuZXhwb3J0IHtcbiAgICBDb21wdXRlZEluY29tZVRheFZhbHVlcyBhcyBDYW5hZGFDb21wdXRlZEluY29tZVRheFZhbHVlcyxcbiAgICBJbmNvbWVUYXhSdWxlcyBhcyBDYW5hZGFJbmNvbWVUYXhSdWxlcyxcbn0gZnJvbSAnLi9pbmNvbWUtdGF4L2NhbmFkYS9kb21haW4vdHlwZXMnO1xuZXhwb3J0IHtcbiAgICBNb3J0Z2FnZVJ1bGVzIGFzIENhbmFkYU1vcnRnYWdlUnVsZXMsXG4gICAgTW9ydGdhZ2VDYWxjdWxhdGlvbklucHV0IGFzIENhbmFkYU1vcnRnYWdlQ2FsY3VsYXRpb25JbnB1dCxcbiAgICBNb3J0Z2FnZUNhbGN1bGF0aW9uUmVzdWx0IGFzIENhbmFkYU1vcnRnYWdlQ2FsY3VsYXRpb25SZXN1bHQsXG59IGZyb20gJy4vbW9ydGdhZ2UvY2FuYWRhL2RvbWFpbi90eXBlcyc7XG5leHBvcnQgeyBcbiAgICBDb21wdXRlZEluY29tZVRheFZhbHVlcyBhcyBGcmFuY2VDb21wdXRlZEluY29tZVRheFZhbHVlcyxcbiAgICBJbmNvbWVUYXhSdWxlcyBhcyBGcmFuY2VJbmNvbWVUYXhSdWxlcyxcbn0gZnJvbSAnLi9pbmNvbWUtdGF4L2ZyYW5jZS9kb21haW4vdHlwZXMnO1xuZXhwb3J0IHsgSW5jb21lVGF4Q2FsY3VsYXRvclNjaGVtYSB9IGZyb20gJy4vaW5jb21lLXRheC9kb21haW4vdHlwZXMnO1xuZXhwb3J0ICogZnJvbSAnLi9pbmNvbWUtdGF4L2RvbWFpbi90eXBlcyc7Il19
27
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFBQSw2RkFBc0g7QUFBN0csb0lBQUEsMEJBQTBCLE9BQTBCO0FBQzdELHlGQUFpSDtBQUF4RyxrSUFBQSx5QkFBeUIsT0FBeUI7QUFDM0QsNkZBQXNIO0FBQTdHLG9JQUFBLDBCQUEwQixPQUEwQjtBQVU3RCx5R0FBc0k7QUFBN0gsNElBQUEsOEJBQThCLE9BQThCO0FBV3JFLDREQUEwQyIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCB7IENhbmFkYUluY29tZVRheFNlcnZpY2VJbXBsIGFzIENhbmFkYUluY29tZVRheFNlcnZpY2UgfSBmcm9tICcuL2luY29tZS10YXgvY2FuYWRhL0NhbmFkYUluY29tZVRheFNlcnZpY2VJbXBsJztcbmV4cG9ydCB7IENhbmFkYU1vcnRnYWdlU2VydmljZUltcGwgYXMgQ2FuYWRhTW9ydGdhZ2VTZXJ2aWNlIH0gZnJvbSAnLi9tb3J0Z2FnZS9jYW5hZGEvQ2FuYWRhTW9ydGdhZ2VTZXJ2aWNlSW1wbCc7XG5leHBvcnQgeyBGcmFuY2VJbmNvbWVUYXhTZXJ2aWNlSW1wbCBhcyBGcmFuY2VJbmNvbWVUYXhTZXJ2aWNlIH0gZnJvbSAnLi9pbmNvbWUtdGF4L2ZyYW5jZS9GcmFuY2VJbmNvbWVUYXhTZXJ2aWNlSW1wbCc7XG5leHBvcnQge1xuICAgIENvbXB1dGVkSW5jb21lVGF4VmFsdWVzIGFzIENhbmFkYUNvbXB1dGVkSW5jb21lVGF4VmFsdWVzLFxuICAgIEluY29tZVRheFJ1bGVzIGFzIENhbmFkYUluY29tZVRheFJ1bGVzLFxufSBmcm9tICcuL2luY29tZS10YXgvY2FuYWRhL2RvbWFpbi90eXBlcyc7XG5leHBvcnQge1xuICAgIE1vcnRnYWdlUnVsZXMgYXMgQ2FuYWRhTW9ydGdhZ2VSdWxlcyxcbiAgICBNb3J0Z2FnZUNhbGN1bGF0aW9uSW5wdXQgYXMgQ2FuYWRhTW9ydGdhZ2VDYWxjdWxhdGlvbklucHV0LFxuICAgIE1vcnRnYWdlQ2FsY3VsYXRpb25SZXN1bHQgYXMgQ2FuYWRhTW9ydGdhZ2VDYWxjdWxhdGlvblJlc3VsdCxcbn0gZnJvbSAnLi9tb3J0Z2FnZS9jYW5hZGEvZG9tYWluL3R5cGVzJztcbmV4cG9ydCB7IFNvdXRoQWZyaWNhTW9ydGdhZ2VTZXJ2aWNlSW1wbCBhcyBTb3V0aEFmcmljYU1vcnRnYWdlU2VydmljZSB9IGZyb20gJy4vbW9ydGdhZ2Uvc291dGgtYWZyaWNhL1NvdXRoQWZyaWNhTW9ydGdhZ2VTZXJ2aWNlSW1wbCc7XG5leHBvcnQge1xuICAgIE1vcnRnYWdlUnVsZXMgYXMgU291dGhBZnJpY2FNb3J0Z2FnZVJ1bGVzLFxuICAgIE1vcnRnYWdlSW5wdXQgYXMgU291dGhBZnJpY2FNb3J0Z2FnZUlucHV0LFxuICAgIE1vcnRnYWdlT3V0cHV0IGFzIFNvdXRoQWZyaWNhTW9ydGdhZ2VPdXRwdXQsXG59IGZyb20gJy4vbW9ydGdhZ2Uvc291dGgtYWZyaWNhL2RvbWFpbi90eXBlcyc7XG5leHBvcnQgeyBcbiAgICBDb21wdXRlZEluY29tZVRheFZhbHVlcyBhcyBGcmFuY2VDb21wdXRlZEluY29tZVRheFZhbHVlcyxcbiAgICBJbmNvbWVUYXhSdWxlcyBhcyBGcmFuY2VJbmNvbWVUYXhSdWxlcyxcbn0gZnJvbSAnLi9pbmNvbWUtdGF4L2ZyYW5jZS9kb21haW4vdHlwZXMnO1xuZXhwb3J0IHsgSW5jb21lVGF4Q2FsY3VsYXRvclNjaGVtYSB9IGZyb20gJy4vaW5jb21lLXRheC9kb21haW4vdHlwZXMnO1xuZXhwb3J0ICogZnJvbSAnLi9pbmNvbWUtdGF4L2RvbWFpbi90eXBlcyc7Il19
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiU291dGhBZnJpY2FNb3J0Z2FnZVNlcnZpY2UuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvbW9ydGdhZ2Uvc291dGgtYWZyaWNhL1NvdXRoQWZyaWNhTW9ydGdhZ2VTZXJ2aWNlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiIiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBNb3J0Z2FnZVJ1bGVzLCBNb3J0Z2FnZUlucHV0LCBNb3J0Z2FnZU91dHB1dCB9IGZyb20gJy4vZG9tYWluL3R5cGVzJztcblxuZXhwb3J0IGludGVyZmFjZSBTb3V0aEFmcmljYU1vcnRnYWdlU2VydmljZSB7XG4gICAgY2FsY3VsYXRlKFxuICAgICAgICBpbnB1dDogTW9ydGdhZ2VJbnB1dCxcbiAgICAgICAgcnVsZXM6IE1vcnRnYWdlUnVsZXNcbiAgICApOiBNb3J0Z2FnZU91dHB1dDtcbn0gIl19
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SouthAfricaMortgageServiceImpl = void 0;
4
+ class SouthAfricaMortgageServiceImpl {
5
+ constructor(input, rules) {
6
+ this.input = input;
7
+ this.rules = rules;
8
+ }
9
+ calculate() {
10
+ const loanAmount = this.input.propertyPrice - this.input.downPayment;
11
+ // Monthly interest rate
12
+ const monthlyRate = this.input.annualInterestRate / 100 / 12;
13
+ const totalPayments = this.input.amortizationYears * 12;
14
+ // Standard amortized monthly payment formula
15
+ const monthlyPayment = (loanAmount * monthlyRate) /
16
+ (1 - Math.pow(1 + monthlyRate, -totalPayments));
17
+ const totalPaid = monthlyPayment * totalPayments;
18
+ const totalInterestPaid = totalPaid - loanAmount;
19
+ // Debt-to-income ratio
20
+ const debtToIncomeRatio = (monthlyPayment / this.input.grossMonthlyIncome) * 100;
21
+ const isAffordable = debtToIncomeRatio <= this.rules.loanConstraints.maxDebtToIncomePercent;
22
+ // Fees
23
+ const bondRegistrationFee = loanAmount * (this.rules.fees.bondRegistrationPercent / 100);
24
+ const transferDuty = this.calculateTransferDuty(this.input.propertyPrice);
25
+ return {
26
+ loanAmount,
27
+ monthlyPayment,
28
+ totalInterestPaid,
29
+ totalPaid,
30
+ debtToIncomeRatio,
31
+ isAffordable,
32
+ transferDuty,
33
+ bondRegistrationFee
34
+ };
35
+ }
36
+ calculateTransferDuty(propertyPrice) {
37
+ let duty = 0;
38
+ let previousLimit = 0;
39
+ for (const bracket of this.rules.fees.transferDuty.brackets) {
40
+ if (bracket.upTo && propertyPrice > previousLimit) {
41
+ const taxable = Math.min(propertyPrice, bracket.upTo) - previousLimit;
42
+ duty += taxable * bracket.rate;
43
+ previousLimit = bracket.upTo;
44
+ }
45
+ if (bracket.above && propertyPrice > bracket.above) {
46
+ duty += (propertyPrice - bracket.above) * bracket.rate;
47
+ break;
48
+ }
49
+ }
50
+ return duty;
51
+ }
52
+ }
53
+ exports.SouthAfricaMortgageServiceImpl = SouthAfricaMortgageServiceImpl;
54
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiU291dGhBZnJpY2FNb3J0Z2FnZVNlcnZpY2VJbXBsLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL21vcnRnYWdlL3NvdXRoLWFmcmljYS9Tb3V0aEFmcmljYU1vcnRnYWdlU2VydmljZUltcGwudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBR0EsTUFBYSw4QkFBOEI7SUFDekMsWUFDbUIsS0FBb0IsRUFDcEIsS0FBb0I7UUFEcEIsVUFBSyxHQUFMLEtBQUssQ0FBZTtRQUNwQixVQUFLLEdBQUwsS0FBSyxDQUFlO0lBQ3RDLENBQUM7SUFFRixTQUFTO1FBQ1AsTUFBTSxVQUFVLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxhQUFhLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxXQUFXLENBQUM7UUFFckUsd0JBQXdCO1FBQ3hCLE1BQU0sV0FBVyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsa0JBQWtCLEdBQUcsR0FBRyxHQUFHLEVBQUUsQ0FBQztRQUM3RCxNQUFNLGFBQWEsR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLGlCQUFpQixHQUFHLEVBQUUsQ0FBQztRQUV4RCw2Q0FBNkM7UUFDN0MsTUFBTSxjQUFjLEdBQ2xCLENBQUMsVUFBVSxHQUFHLFdBQVcsQ0FBQztZQUMxQixDQUFDLENBQUMsR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsR0FBRyxXQUFXLEVBQUUsQ0FBQyxhQUFhLENBQUMsQ0FBQyxDQUFDO1FBRWxELE1BQU0sU0FBUyxHQUFHLGNBQWMsR0FBRyxhQUFhLENBQUM7UUFDakQsTUFBTSxpQkFBaUIsR0FBRyxTQUFTLEdBQUcsVUFBVSxDQUFDO1FBRWpELHVCQUF1QjtRQUN2QixNQUFNLGlCQUFpQixHQUNyQixDQUFDLGNBQWMsR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLGtCQUFrQixDQUFDLEdBQUcsR0FBRyxDQUFDO1FBRXpELE1BQU0sWUFBWSxHQUNoQixpQkFBaUIsSUFBSSxJQUFJLENBQUMsS0FBSyxDQUFDLGVBQWUsQ0FBQyxzQkFBc0IsQ0FBQztRQUV6RSxPQUFPO1FBQ1AsTUFBTSxtQkFBbUIsR0FDdkIsVUFBVSxHQUFHLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsdUJBQXVCLEdBQUcsR0FBRyxDQUFDLENBQUM7UUFFL0QsTUFBTSxZQUFZLEdBQUcsSUFBSSxDQUFDLHFCQUFxQixDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsYUFBYSxDQUFDLENBQUM7UUFDMUUsT0FBTztZQUNMLFVBQVU7WUFDVixjQUFjO1lBQ2QsaUJBQWlCO1lBQ2pCLFNBQVM7WUFDVCxpQkFBaUI7WUFDakIsWUFBWTtZQUNaLFlBQVk7WUFDWixtQkFBbUI7U0FDcEIsQ0FBQztJQUNKLENBQUM7SUFFTyxxQkFBcUIsQ0FBQyxhQUFxQjtRQUNqRCxJQUFJLElBQUksR0FBRyxDQUFDLENBQUM7UUFDYixJQUFJLGFBQWEsR0FBRyxDQUFDLENBQUM7UUFFdEIsS0FBSyxNQUFNLE9BQU8sSUFBSSxJQUFJLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxZQUFZLENBQUMsUUFBUSxFQUFFLENBQUM7WUFDNUQsSUFBSSxPQUFPLENBQUMsSUFBSSxJQUFJLGFBQWEsR0FBRyxhQUFhLEVBQUUsQ0FBQztnQkFDbEQsTUFBTSxPQUFPLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxhQUFhLEVBQUUsT0FBTyxDQUFDLElBQUksQ0FBQyxHQUFHLGFBQWEsQ0FBQztnQkFDdEUsSUFBSSxJQUFJLE9BQU8sR0FBRyxPQUFPLENBQUMsSUFBSSxDQUFDO2dCQUMvQixhQUFhLEdBQUcsT0FBTyxDQUFDLElBQUksQ0FBQztZQUMvQixDQUFDO1lBRUQsSUFBSSxPQUFPLENBQUMsS0FBSyxJQUFJLGFBQWEsR0FBRyxPQUFPLENBQUMsS0FBSyxFQUFFLENBQUM7Z0JBQ25ELElBQUksSUFBSSxDQUFDLGFBQWEsR0FBRyxPQUFPLENBQUMsS0FBSyxDQUFDLEdBQUcsT0FBTyxDQUFDLElBQUksQ0FBQztnQkFDdkQsTUFBTTtZQUNSLENBQUM7UUFDSCxDQUFDO1FBRUQsT0FBTyxJQUFJLENBQUM7SUFDZCxDQUFDO0NBQ0Y7QUFoRUQsd0VBZ0VDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgTW9ydGdhZ2VSdWxlcywgTW9ydGdhZ2VJbnB1dCwgTW9ydGdhZ2VPdXRwdXQgfSBmcm9tICcuL2RvbWFpbi90eXBlcyc7XG5pbXBvcnQgeyBTb3V0aEFmcmljYU1vcnRnYWdlU2VydmljZSB9IGZyb20gJy4vU291dGhBZnJpY2FNb3J0Z2FnZVNlcnZpY2UnO1xuXG5leHBvcnQgY2xhc3MgU291dGhBZnJpY2FNb3J0Z2FnZVNlcnZpY2VJbXBsIGltcGxlbWVudHMgU291dGhBZnJpY2FNb3J0Z2FnZVNlcnZpY2Uge1xuICBjb25zdHJ1Y3RvcihcbiAgICBwcml2YXRlIHJlYWRvbmx5IGlucHV0OiBNb3J0Z2FnZUlucHV0LFxuICAgIHByaXZhdGUgcmVhZG9ubHkgcnVsZXM6IE1vcnRnYWdlUnVsZXNcbikge31cblxuICBjYWxjdWxhdGUoKTogTW9ydGdhZ2VPdXRwdXQge1xuICAgIGNvbnN0IGxvYW5BbW91bnQgPSB0aGlzLmlucHV0LnByb3BlcnR5UHJpY2UgLSB0aGlzLmlucHV0LmRvd25QYXltZW50O1xuXG4gICAgLy8gTW9udGhseSBpbnRlcmVzdCByYXRlXG4gICAgY29uc3QgbW9udGhseVJhdGUgPSB0aGlzLmlucHV0LmFubnVhbEludGVyZXN0UmF0ZSAvIDEwMCAvIDEyO1xuICAgIGNvbnN0IHRvdGFsUGF5bWVudHMgPSB0aGlzLmlucHV0LmFtb3J0aXphdGlvblllYXJzICogMTI7XG5cbiAgICAvLyBTdGFuZGFyZCBhbW9ydGl6ZWQgbW9udGhseSBwYXltZW50IGZvcm11bGFcbiAgICBjb25zdCBtb250aGx5UGF5bWVudCA9XG4gICAgICAobG9hbkFtb3VudCAqIG1vbnRobHlSYXRlKSAvXG4gICAgICAoMSAtIE1hdGgucG93KDEgKyBtb250aGx5UmF0ZSwgLXRvdGFsUGF5bWVudHMpKTtcblxuICAgIGNvbnN0IHRvdGFsUGFpZCA9IG1vbnRobHlQYXltZW50ICogdG90YWxQYXltZW50cztcbiAgICBjb25zdCB0b3RhbEludGVyZXN0UGFpZCA9IHRvdGFsUGFpZCAtIGxvYW5BbW91bnQ7XG5cbiAgICAvLyBEZWJ0LXRvLWluY29tZSByYXRpb1xuICAgIGNvbnN0IGRlYnRUb0luY29tZVJhdGlvID1cbiAgICAgIChtb250aGx5UGF5bWVudCAvIHRoaXMuaW5wdXQuZ3Jvc3NNb250aGx5SW5jb21lKSAqIDEwMDtcblxuICAgIGNvbnN0IGlzQWZmb3JkYWJsZSA9XG4gICAgICBkZWJ0VG9JbmNvbWVSYXRpbyA8PSB0aGlzLnJ1bGVzLmxvYW5Db25zdHJhaW50cy5tYXhEZWJ0VG9JbmNvbWVQZXJjZW50O1xuXG4gICAgLy8gRmVlc1xuICAgIGNvbnN0IGJvbmRSZWdpc3RyYXRpb25GZWUgPVxuICAgICAgbG9hbkFtb3VudCAqICh0aGlzLnJ1bGVzLmZlZXMuYm9uZFJlZ2lzdHJhdGlvblBlcmNlbnQgLyAxMDApO1xuXG4gICAgY29uc3QgdHJhbnNmZXJEdXR5ID0gdGhpcy5jYWxjdWxhdGVUcmFuc2ZlckR1dHkodGhpcy5pbnB1dC5wcm9wZXJ0eVByaWNlKTtcbiAgICByZXR1cm4ge1xuICAgICAgbG9hbkFtb3VudCxcbiAgICAgIG1vbnRobHlQYXltZW50LFxuICAgICAgdG90YWxJbnRlcmVzdFBhaWQsXG4gICAgICB0b3RhbFBhaWQsXG4gICAgICBkZWJ0VG9JbmNvbWVSYXRpbyxcbiAgICAgIGlzQWZmb3JkYWJsZSxcbiAgICAgIHRyYW5zZmVyRHV0eSxcbiAgICAgIGJvbmRSZWdpc3RyYXRpb25GZWVcbiAgICB9O1xuICB9XG5cbiAgcHJpdmF0ZSBjYWxjdWxhdGVUcmFuc2ZlckR1dHkocHJvcGVydHlQcmljZTogbnVtYmVyKTogbnVtYmVyIHtcbiAgICBsZXQgZHV0eSA9IDA7XG4gICAgbGV0IHByZXZpb3VzTGltaXQgPSAwO1xuXG4gICAgZm9yIChjb25zdCBicmFja2V0IG9mIHRoaXMucnVsZXMuZmVlcy50cmFuc2ZlckR1dHkuYnJhY2tldHMpIHtcbiAgICAgIGlmIChicmFja2V0LnVwVG8gJiYgcHJvcGVydHlQcmljZSA+IHByZXZpb3VzTGltaXQpIHtcbiAgICAgICAgY29uc3QgdGF4YWJsZSA9IE1hdGgubWluKHByb3BlcnR5UHJpY2UsIGJyYWNrZXQudXBUbykgLSBwcmV2aW91c0xpbWl0O1xuICAgICAgICBkdXR5ICs9IHRheGFibGUgKiBicmFja2V0LnJhdGU7XG4gICAgICAgIHByZXZpb3VzTGltaXQgPSBicmFja2V0LnVwVG87XG4gICAgICB9XG5cbiAgICAgIGlmIChicmFja2V0LmFib3ZlICYmIHByb3BlcnR5UHJpY2UgPiBicmFja2V0LmFib3ZlKSB7XG4gICAgICAgIGR1dHkgKz0gKHByb3BlcnR5UHJpY2UgLSBicmFja2V0LmFib3ZlKSAqIGJyYWNrZXQucmF0ZTtcbiAgICAgICAgYnJlYWs7XG4gICAgICB9XG4gICAgfVxuXG4gICAgcmV0dXJuIGR1dHk7XG4gIH1cbn1cbiJdfQ==
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9zcmMvbW9ydGdhZ2Uvc291dGgtYWZyaWNhL2RvbWFpbi90eXBlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgUnVsZU1ldGEgfSBmcm9tIFwiLi4vLi4vLi4vc2hhcmVkL2RvbWFpbi90eXBlc1wiO1xuXG5leHBvcnQgaW50ZXJmYWNlIFJ1bGVJbnB1dERlZmluaXRpb24ge1xuICB0eXBlOiBcIm51bWJlclwiIHwgXCJzdHJpbmdcIjtcbiAgbWluPzogbnVtYmVyO1xuICBtYXg/OiBudW1iZXI7XG4gIGVudW0/OiBzdHJpbmdbXTtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBSdWxlSW5wdXRzIHtcbiAgcHJvcGVydHlQcmljZTogUnVsZUlucHV0RGVmaW5pdGlvbjtcbiAgZG93blBheW1lbnQ6IFJ1bGVJbnB1dERlZmluaXRpb247XG4gIGludGVyZXN0UmF0ZTogUnVsZUlucHV0RGVmaW5pdGlvbjtcbiAgYW1vcnRpemF0aW9uWWVhcnM6IFJ1bGVJbnB1dERlZmluaXRpb247XG4gIGdyb3NzTW9udGhseUluY29tZTogUnVsZUlucHV0RGVmaW5pdGlvbjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBMb2FuQ29uc3RyYWludHMge1xuICBtYXhMdHY6IG51bWJlcjtcbiAgbWluRG93blBheW1lbnRQZXJjZW50OiBudW1iZXI7XG4gIG1heEFtb3J0aXphdGlvblllYXJzOiBudW1iZXI7XG4gIG1heERlYnRUb0luY29tZVBlcmNlbnQ6IG51bWJlcjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBJbnRlcmVzdFJ1bGVzIHtcbiAgdHlwZTogXCJmaXhlZFwiIHwgXCJ2YXJpYWJsZVwiO1xuICByYXRlUmFuZ2VQZXJjZW50OiB7IG1pbjogbnVtYmVyOyBtYXg6IG51bWJlciB9O1xuICBzdHJlc3NUZXN0QnVmZmVyUGVyY2VudDogbnVtYmVyO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIEluc3VyYW5jZVJ1bGVzIHtcbiAgcmVxdWlyZWQ6IGJvb2xlYW47XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgVHJhbnNmZXJEdXR5QnJhY2tldCB7XG4gIHVwVG8/OiBudW1iZXI7XG4gIGFib3ZlPzogbnVtYmVyO1xuICByYXRlOiBudW1iZXI7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgRmVlc1J1bGVzIHtcbiAgYm9uZFJlZ2lzdHJhdGlvblBlcmNlbnQ6IG51bWJlcjtcbiAgdHJhbnNmZXJEdXR5OiB7XG4gICAgYnJhY2tldHM6IFRyYW5zZmVyRHV0eUJyYWNrZXRbXTtcbiAgfTtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBNb3J0Z2FnZVJ1bGVzIHtcbiAgbG9hbkNvbnN0cmFpbnRzOiBMb2FuQ29uc3RyYWludHM7XG4gIGludGVyZXN0OiBJbnRlcmVzdFJ1bGVzO1xuICBpbnN1cmFuY2U6IEluc3VyYW5jZVJ1bGVzO1xuICBmZWVzOiBGZWVzUnVsZXM7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgUnVsZU91dHB1dHMge1xuICBsb2FuQW1vdW50OiBcIm51bWJlclwiO1xuICBtb250aGx5UGF5bWVudDogXCJudW1iZXJcIjtcbiAgdG90YWxJbnRlcmVzdFBhaWQ6IFwibnVtYmVyXCI7XG4gIHRvdGFsUGFpZDogXCJudW1iZXJcIjtcbiAgZGVidFRvSW5jb21lUmF0aW86IFwibnVtYmVyXCI7XG4gIGlzQWZmb3JkYWJsZTogXCJib29sZWFuXCI7XG4gIHRyYW5zZmVyRHV0eTogXCJudW1iZXJcIjtcbiAgYm9uZFJlZ2lzdHJhdGlvbkZlZTogXCJudW1iZXJcIjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBNb3J0Z2FnZVJ1bGVTZXQge1xuICBtZXRhOiBSdWxlTWV0YTtcbiAgaW5wdXRzOiBSdWxlSW5wdXRzO1xuICBydWxlczogTW9ydGdhZ2VSdWxlcztcbiAgb3V0cHV0czogUnVsZU91dHB1dHM7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgTW9ydGdhZ2VJbnB1dCB7XG4gIHByb3BlcnR5UHJpY2U6IG51bWJlcjtcbiAgZG93blBheW1lbnQ6IG51bWJlcjtcbiAgYW5udWFsSW50ZXJlc3RSYXRlOiBudW1iZXI7IC8vIGUuZy4sIDExLjc1JVxuICBhbW9ydGl6YXRpb25ZZWFyczogbnVtYmVyO1xuICBncm9zc01vbnRobHlJbmNvbWU6IG51bWJlcjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBNb3J0Z2FnZU91dHB1dCB7XG4gIGxvYW5BbW91bnQ6IG51bWJlcjtcbiAgbW9udGhseVBheW1lbnQ6IG51bWJlcjtcbiAgdG90YWxJbnRlcmVzdFBhaWQ6IG51bWJlcjtcbiAgdG90YWxQYWlkOiBudW1iZXI7XG4gIGRlYnRUb0luY29tZVJhdGlvOiBudW1iZXI7XG4gIGlzQWZmb3JkYWJsZTogYm9vbGVhbjtcbiAgdHJhbnNmZXJEdXR5OiBudW1iZXI7XG4gIGJvbmRSZWdpc3RyYXRpb25GZWU6IG51bWJlcjtcbn0iXX0=
@@ -3,6 +3,8 @@ export { CanadaMortgageServiceImpl as CanadaMortgageService } from './mortgage/c
3
3
  export { FranceIncomeTaxServiceImpl as FranceIncomeTaxService } from './income-tax/france/FranceIncomeTaxServiceImpl';
4
4
  export { ComputedIncomeTaxValues as CanadaComputedIncomeTaxValues, IncomeTaxRules as CanadaIncomeTaxRules, } from './income-tax/canada/domain/types';
5
5
  export { MortgageRules as CanadaMortgageRules, MortgageCalculationInput as CanadaMortgageCalculationInput, MortgageCalculationResult as CanadaMortgageCalculationResult, } from './mortgage/canada/domain/types';
6
+ export { SouthAfricaMortgageServiceImpl as SouthAfricaMortgageService } from './mortgage/south-africa/SouthAfricaMortgageServiceImpl';
7
+ export { MortgageRules as SouthAfricaMortgageRules, MortgageInput as SouthAfricaMortgageInput, MortgageOutput as SouthAfricaMortgageOutput, } from './mortgage/south-africa/domain/types';
6
8
  export { ComputedIncomeTaxValues as FranceComputedIncomeTaxValues, IncomeTaxRules as FranceIncomeTaxRules, } from './income-tax/france/domain/types';
7
9
  export { IncomeTaxCalculatorSchema } from './income-tax/domain/types';
8
10
  export * from './income-tax/domain/types';
@@ -0,0 +1,4 @@
1
+ import { MortgageRules, MortgageInput, MortgageOutput } from './domain/types';
2
+ export interface SouthAfricaMortgageService {
3
+ calculate(input: MortgageInput, rules: MortgageRules): MortgageOutput;
4
+ }
@@ -0,0 +1,9 @@
1
+ import { MortgageRules, MortgageInput, MortgageOutput } from './domain/types';
2
+ import { SouthAfricaMortgageService } from './SouthAfricaMortgageService';
3
+ export declare class SouthAfricaMortgageServiceImpl implements SouthAfricaMortgageService {
4
+ private readonly input;
5
+ private readonly rules;
6
+ constructor(input: MortgageInput, rules: MortgageRules);
7
+ calculate(): MortgageOutput;
8
+ private calculateTransferDuty;
9
+ }
@@ -0,0 +1,81 @@
1
+ import { RuleMeta } from "../../../shared/domain/types";
2
+ export interface RuleInputDefinition {
3
+ type: "number" | "string";
4
+ min?: number;
5
+ max?: number;
6
+ enum?: string[];
7
+ }
8
+ export interface RuleInputs {
9
+ propertyPrice: RuleInputDefinition;
10
+ downPayment: RuleInputDefinition;
11
+ interestRate: RuleInputDefinition;
12
+ amortizationYears: RuleInputDefinition;
13
+ grossMonthlyIncome: RuleInputDefinition;
14
+ }
15
+ export interface LoanConstraints {
16
+ maxLtv: number;
17
+ minDownPaymentPercent: number;
18
+ maxAmortizationYears: number;
19
+ maxDebtToIncomePercent: number;
20
+ }
21
+ export interface InterestRules {
22
+ type: "fixed" | "variable";
23
+ rateRangePercent: {
24
+ min: number;
25
+ max: number;
26
+ };
27
+ stressTestBufferPercent: number;
28
+ }
29
+ export interface InsuranceRules {
30
+ required: boolean;
31
+ }
32
+ export interface TransferDutyBracket {
33
+ upTo?: number;
34
+ above?: number;
35
+ rate: number;
36
+ }
37
+ export interface FeesRules {
38
+ bondRegistrationPercent: number;
39
+ transferDuty: {
40
+ brackets: TransferDutyBracket[];
41
+ };
42
+ }
43
+ export interface MortgageRules {
44
+ loanConstraints: LoanConstraints;
45
+ interest: InterestRules;
46
+ insurance: InsuranceRules;
47
+ fees: FeesRules;
48
+ }
49
+ export interface RuleOutputs {
50
+ loanAmount: "number";
51
+ monthlyPayment: "number";
52
+ totalInterestPaid: "number";
53
+ totalPaid: "number";
54
+ debtToIncomeRatio: "number";
55
+ isAffordable: "boolean";
56
+ transferDuty: "number";
57
+ bondRegistrationFee: "number";
58
+ }
59
+ export interface MortgageRuleSet {
60
+ meta: RuleMeta;
61
+ inputs: RuleInputs;
62
+ rules: MortgageRules;
63
+ outputs: RuleOutputs;
64
+ }
65
+ export interface MortgageInput {
66
+ propertyPrice: number;
67
+ downPayment: number;
68
+ annualInterestRate: number;
69
+ amortizationYears: number;
70
+ grossMonthlyIncome: number;
71
+ }
72
+ export interface MortgageOutput {
73
+ loanAmount: number;
74
+ monthlyPayment: number;
75
+ totalInterestPaid: number;
76
+ totalPaid: number;
77
+ debtToIncomeRatio: number;
78
+ isAffordable: boolean;
79
+ transferDuty: number;
80
+ bondRegistrationFee: number;
81
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@novha/calc-engines",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/types/index.d.ts",
6
6
  "scripts": {
package/src/index.ts CHANGED
@@ -10,6 +10,12 @@ export {
10
10
  MortgageCalculationInput as CanadaMortgageCalculationInput,
11
11
  MortgageCalculationResult as CanadaMortgageCalculationResult,
12
12
  } from './mortgage/canada/domain/types';
13
+ export { SouthAfricaMortgageServiceImpl as SouthAfricaMortgageService } from './mortgage/south-africa/SouthAfricaMortgageServiceImpl';
14
+ export {
15
+ MortgageRules as SouthAfricaMortgageRules,
16
+ MortgageInput as SouthAfricaMortgageInput,
17
+ MortgageOutput as SouthAfricaMortgageOutput,
18
+ } from './mortgage/south-africa/domain/types';
13
19
  export {
14
20
  ComputedIncomeTaxValues as FranceComputedIncomeTaxValues,
15
21
  IncomeTaxRules as FranceIncomeTaxRules,
@@ -0,0 +1,8 @@
1
+ import { MortgageRules, MortgageInput, MortgageOutput } from './domain/types';
2
+
3
+ export interface SouthAfricaMortgageService {
4
+ calculate(
5
+ input: MortgageInput,
6
+ rules: MortgageRules
7
+ ): MortgageOutput;
8
+ }
@@ -0,0 +1,68 @@
1
+ import { MortgageRules, MortgageInput, MortgageOutput } from './domain/types';
2
+ import { SouthAfricaMortgageService } from './SouthAfricaMortgageService';
3
+
4
+ export class SouthAfricaMortgageServiceImpl implements SouthAfricaMortgageService {
5
+ constructor(
6
+ private readonly input: MortgageInput,
7
+ private readonly rules: MortgageRules
8
+ ) {}
9
+
10
+ calculate(): MortgageOutput {
11
+ const loanAmount = this.input.propertyPrice - this.input.downPayment;
12
+
13
+ // Monthly interest rate
14
+ const monthlyRate = this.input.annualInterestRate / 100 / 12;
15
+ const totalPayments = this.input.amortizationYears * 12;
16
+
17
+ // Standard amortized monthly payment formula
18
+ const monthlyPayment =
19
+ (loanAmount * monthlyRate) /
20
+ (1 - Math.pow(1 + monthlyRate, -totalPayments));
21
+
22
+ const totalPaid = monthlyPayment * totalPayments;
23
+ const totalInterestPaid = totalPaid - loanAmount;
24
+
25
+ // Debt-to-income ratio
26
+ const debtToIncomeRatio =
27
+ (monthlyPayment / this.input.grossMonthlyIncome) * 100;
28
+
29
+ const isAffordable =
30
+ debtToIncomeRatio <= this.rules.loanConstraints.maxDebtToIncomePercent;
31
+
32
+ // Fees
33
+ const bondRegistrationFee =
34
+ loanAmount * (this.rules.fees.bondRegistrationPercent / 100);
35
+
36
+ const transferDuty = this.calculateTransferDuty(this.input.propertyPrice);
37
+ return {
38
+ loanAmount,
39
+ monthlyPayment,
40
+ totalInterestPaid,
41
+ totalPaid,
42
+ debtToIncomeRatio,
43
+ isAffordable,
44
+ transferDuty,
45
+ bondRegistrationFee
46
+ };
47
+ }
48
+
49
+ private calculateTransferDuty(propertyPrice: number): number {
50
+ let duty = 0;
51
+ let previousLimit = 0;
52
+
53
+ for (const bracket of this.rules.fees.transferDuty.brackets) {
54
+ if (bracket.upTo && propertyPrice > previousLimit) {
55
+ const taxable = Math.min(propertyPrice, bracket.upTo) - previousLimit;
56
+ duty += taxable * bracket.rate;
57
+ previousLimit = bracket.upTo;
58
+ }
59
+
60
+ if (bracket.above && propertyPrice > bracket.above) {
61
+ duty += (propertyPrice - bracket.above) * bracket.rate;
62
+ break;
63
+ }
64
+ }
65
+
66
+ return duty;
67
+ }
68
+ }
@@ -0,0 +1,90 @@
1
+ import { RuleMeta } from "../../../shared/domain/types";
2
+
3
+ export interface RuleInputDefinition {
4
+ type: "number" | "string";
5
+ min?: number;
6
+ max?: number;
7
+ enum?: string[];
8
+ }
9
+
10
+ export interface RuleInputs {
11
+ propertyPrice: RuleInputDefinition;
12
+ downPayment: RuleInputDefinition;
13
+ interestRate: RuleInputDefinition;
14
+ amortizationYears: RuleInputDefinition;
15
+ grossMonthlyIncome: RuleInputDefinition;
16
+ }
17
+
18
+ export interface LoanConstraints {
19
+ maxLtv: number;
20
+ minDownPaymentPercent: number;
21
+ maxAmortizationYears: number;
22
+ maxDebtToIncomePercent: number;
23
+ }
24
+
25
+ export interface InterestRules {
26
+ type: "fixed" | "variable";
27
+ rateRangePercent: { min: number; max: number };
28
+ stressTestBufferPercent: number;
29
+ }
30
+
31
+ export interface InsuranceRules {
32
+ required: boolean;
33
+ }
34
+
35
+ export interface TransferDutyBracket {
36
+ upTo?: number;
37
+ above?: number;
38
+ rate: number;
39
+ }
40
+
41
+ export interface FeesRules {
42
+ bondRegistrationPercent: number;
43
+ transferDuty: {
44
+ brackets: TransferDutyBracket[];
45
+ };
46
+ }
47
+
48
+ export interface MortgageRules {
49
+ loanConstraints: LoanConstraints;
50
+ interest: InterestRules;
51
+ insurance: InsuranceRules;
52
+ fees: FeesRules;
53
+ }
54
+
55
+ export interface RuleOutputs {
56
+ loanAmount: "number";
57
+ monthlyPayment: "number";
58
+ totalInterestPaid: "number";
59
+ totalPaid: "number";
60
+ debtToIncomeRatio: "number";
61
+ isAffordable: "boolean";
62
+ transferDuty: "number";
63
+ bondRegistrationFee: "number";
64
+ }
65
+
66
+ export interface MortgageRuleSet {
67
+ meta: RuleMeta;
68
+ inputs: RuleInputs;
69
+ rules: MortgageRules;
70
+ outputs: RuleOutputs;
71
+ }
72
+
73
+ export interface MortgageInput {
74
+ propertyPrice: number;
75
+ downPayment: number;
76
+ annualInterestRate: number; // e.g., 11.75%
77
+ amortizationYears: number;
78
+ grossMonthlyIncome: number;
79
+ }
80
+
81
+ export interface MortgageOutput {
82
+ loanAmount: number;
83
+ monthlyPayment: number;
84
+ totalInterestPaid: number;
85
+ totalPaid: number;
86
+ debtToIncomeRatio: number;
87
+ isAffordable: boolean;
88
+ transferDuty: number;
89
+ bondRegistrationFee: number;
90
+ }