@grupolapa/desarrollos-sdk 0.1.0
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/README.md +154 -0
- package/dist/bundle-payment-schedule.calc.d.ts +2 -0
- package/dist/bundle-payment-schedule.calc.js +160 -0
- package/dist/calculate-quote.d.ts +2 -0
- package/dist/calculate-quote.js +382 -0
- package/dist/corporate-actions.d.ts +95 -0
- package/dist/corporate-actions.js +87 -0
- package/dist/corporate-rent.d.ts +5 -0
- package/dist/corporate-rent.js +43 -0
- package/dist/delivery-date.d.ts +6 -0
- package/dist/delivery-date.js +22 -0
- package/dist/entrada.d.ts +111 -0
- package/dist/entrada.js +139 -0
- package/dist/format.d.ts +7 -0
- package/dist/format.js +37 -0
- package/dist/http.d.ts +29 -0
- package/dist/http.js +123 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +22 -0
- package/dist/installment-allocation.d.ts +30 -0
- package/dist/installment-allocation.js +69 -0
- package/dist/inventory-availability.d.ts +10 -0
- package/dist/inventory-availability.js +105 -0
- package/dist/live-inventory-availability.d.ts +16 -0
- package/dist/live-inventory-availability.js +88 -0
- package/dist/payment-schemes.d.ts +22 -0
- package/dist/payment-schemes.js +99 -0
- package/dist/post-delivery-value.d.ts +3 -0
- package/dist/post-delivery-value.js +61 -0
- package/dist/product-types.d.ts +16 -0
- package/dist/product-types.js +82 -0
- package/dist/query-state.d.ts +5 -0
- package/dist/query-state.js +157 -0
- package/dist/quote-outcome-graph.d.ts +31 -0
- package/dist/quote-outcome-graph.js +113 -0
- package/dist/quote.d.ts +19 -0
- package/dist/quote.js +18 -0
- package/dist/selection-state.d.ts +34 -0
- package/dist/selection-state.js +156 -0
- package/dist/server.d.ts +17 -0
- package/dist/server.js +18 -0
- package/dist/sitemap.d.ts +22 -0
- package/dist/sitemap.js +45 -0
- package/dist/social-quote.d.ts +8 -0
- package/dist/social-quote.js +51 -0
- package/dist/tax.d.ts +5 -0
- package/dist/tax.js +11 -0
- package/dist/types.d.ts +511 -0
- package/dist/types.js +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { getFinanceRulesRentalRateForUnit, getUnitEstimatedRentalAnnualMXN, getUnitEstimatedRentalMonthlyMXN, getUnitDeliveryMonths, getUnitListPriceMXN, isCashbackScheme, isInstallmentScheme, MAX_CASHBACK_MONTHS, } from "./payment-schemes.js";
|
|
2
|
+
import { getInstallmentAllocation, getInstallmentDiscountPct, INSTALLMENT_DEFAULT_DOWN_PAYMENT_PCT, INSTALLMENT_DEFAULT_MONTHLY_PCT, } from "./installment-allocation.js";
|
|
3
|
+
import { getEscalatedAnnualAmountMXN, isCorporateRentalUnit, } from "./corporate-rent.js";
|
|
4
|
+
import { applyIvaModeMXN } from "./tax.js";
|
|
5
|
+
function getVisibleTimelineRows(rows, deferredPaymentMonths, deliveryMonths, isCashback, cashbackMonths) {
|
|
6
|
+
const visibleMonths = new Set([
|
|
7
|
+
0,
|
|
8
|
+
1,
|
|
9
|
+
2,
|
|
10
|
+
3,
|
|
11
|
+
Math.round(deliveryMonths / 2),
|
|
12
|
+
deliveryMonths,
|
|
13
|
+
]);
|
|
14
|
+
for (let year = 1; year <= Math.ceil(deliveryMonths / 12); year += 1) {
|
|
15
|
+
visibleMonths.add(year * 12);
|
|
16
|
+
}
|
|
17
|
+
if (isCashback) {
|
|
18
|
+
visibleMonths.add(cashbackMonths);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
deferredPaymentMonths.forEach((month) => visibleMonths.add(month));
|
|
22
|
+
}
|
|
23
|
+
return [...visibleMonths]
|
|
24
|
+
.filter((month) => month >= 0 && month <= deliveryMonths)
|
|
25
|
+
.sort((left, right) => left - right)
|
|
26
|
+
.map((month) => rows[month]);
|
|
27
|
+
}
|
|
28
|
+
export function calculateQuote(unit, input, scheme, rules) {
|
|
29
|
+
const listPriceMXN = applyIvaModeMXN(getUnitListPriceMXN(unit), input.includeIva);
|
|
30
|
+
const plusvaliaRate = input.plusvaliaAnnualPct / 100;
|
|
31
|
+
const plusvaliaMonthlyRate = plusvaliaRate / 12;
|
|
32
|
+
const deliveryMonths = getUnitDeliveryMonths(unit);
|
|
33
|
+
const isCashback = isCashbackScheme(scheme);
|
|
34
|
+
const cashbackMonths = isCashback
|
|
35
|
+
? Math.min(deliveryMonths, MAX_CASHBACK_MONTHS)
|
|
36
|
+
: 0;
|
|
37
|
+
const isCashbackCapped = isCashback && cashbackMonths < deliveryMonths;
|
|
38
|
+
const installmentAllocation = isInstallmentScheme(scheme)
|
|
39
|
+
? getInstallmentAllocation(input.installmentDownPaymentPct ?? INSTALLMENT_DEFAULT_DOWN_PAYMENT_PCT, input.installmentMonthlyPct ?? INSTALLMENT_DEFAULT_MONTHLY_PCT)
|
|
40
|
+
: null;
|
|
41
|
+
const downPaymentPct = isCashback
|
|
42
|
+
? input.downPaymentPct
|
|
43
|
+
: (installmentAllocation?.downPaymentPct ?? 0);
|
|
44
|
+
const downPaymentAmountMXN = listPriceMXN * (downPaymentPct / 100);
|
|
45
|
+
const balanceAmountMXN = listPriceMXN - downPaymentAmountMXN;
|
|
46
|
+
const cashbackMonthlyMXN = isCashback
|
|
47
|
+
? (downPaymentAmountMXN * scheme.cashbackRate) / 12
|
|
48
|
+
: 0;
|
|
49
|
+
const cashbackTotalMXN = cashbackMonthlyMXN * cashbackMonths;
|
|
50
|
+
const rawMonthlyPaymentPct = installmentAllocation?.monthlyPct ?? 0;
|
|
51
|
+
const rawDeferredPaymentPct = installmentAllocation?.deferredPct ?? 0;
|
|
52
|
+
const preDeliveryPaymentMonths = Math.max(deliveryMonths - 1, 0);
|
|
53
|
+
const deferredPaymentsCount = installmentAllocation && rawDeferredPaymentPct > 0
|
|
54
|
+
? Math.floor(preDeliveryPaymentMonths / 12)
|
|
55
|
+
: 0;
|
|
56
|
+
const deferredPaymentMonths = Array.from({ length: deferredPaymentsCount }, (_, index) => (index + 1) * 12);
|
|
57
|
+
const deferredPaymentMonthSet = new Set(deferredPaymentMonths);
|
|
58
|
+
const monthlyPaymentMonths = installmentAllocation && rawMonthlyPaymentPct > 0
|
|
59
|
+
? Array.from({ length: preDeliveryPaymentMonths }, (_, index) => index + 1).filter((month) => !deferredPaymentMonthSet.has(month))
|
|
60
|
+
: [];
|
|
61
|
+
const monthlyPaymentCount = monthlyPaymentMonths.length;
|
|
62
|
+
const monthlyPaymentMonthSet = new Set(monthlyPaymentMonths);
|
|
63
|
+
const monthlyPaymentPct = monthlyPaymentCount > 0 ? rawMonthlyPaymentPct : 0;
|
|
64
|
+
const monthlyPctRolledToDelivery = rawMonthlyPaymentPct - monthlyPaymentPct;
|
|
65
|
+
const monthlyPaymentTotalMXN = listPriceMXN * (monthlyPaymentPct / 100);
|
|
66
|
+
const monthlyPaymentAmountMXN = monthlyPaymentCount > 0 ? monthlyPaymentTotalMXN / monthlyPaymentCount : 0;
|
|
67
|
+
const deferredPaymentPct = deferredPaymentsCount > 0 ? rawDeferredPaymentPct : 0;
|
|
68
|
+
const deferredPctRolledToDelivery = rawDeferredPaymentPct - deferredPaymentPct;
|
|
69
|
+
const deferredPaymentsTotalMXN = listPriceMXN * (deferredPaymentPct / 100);
|
|
70
|
+
const deferredPaymentAmountMXN = deferredPaymentsCount > 0
|
|
71
|
+
? deferredPaymentsTotalMXN / deferredPaymentsCount
|
|
72
|
+
: 0;
|
|
73
|
+
const deliveryPaymentPct = installmentAllocation
|
|
74
|
+
? installmentAllocation.deliveryPct +
|
|
75
|
+
monthlyPctRolledToDelivery +
|
|
76
|
+
deferredPctRolledToDelivery
|
|
77
|
+
: 100 - downPaymentPct;
|
|
78
|
+
const deliveryPaymentAmountMXN = installmentAllocation
|
|
79
|
+
? listPriceMXN * (deliveryPaymentPct / 100)
|
|
80
|
+
: balanceAmountMXN;
|
|
81
|
+
const installmentDiscountPct = installmentAllocation
|
|
82
|
+
? getInstallmentDiscountPct(installmentAllocation.downPaymentPct)
|
|
83
|
+
: 0;
|
|
84
|
+
const installmentDiscountMXN = listPriceMXN * (installmentDiscountPct / 100);
|
|
85
|
+
let installmentDiscountRemainingMXN = installmentDiscountMXN;
|
|
86
|
+
const deliveryDiscountMXN = Math.min(deliveryPaymentAmountMXN, installmentDiscountRemainingMXN);
|
|
87
|
+
installmentDiscountRemainingMXN -= deliveryDiscountMXN;
|
|
88
|
+
const adjustedDeliveryAmountMXN = deliveryPaymentAmountMXN - deliveryDiscountMXN;
|
|
89
|
+
const deferredDiscountMXN = Math.min(deferredPaymentsTotalMXN, installmentDiscountRemainingMXN);
|
|
90
|
+
installmentDiscountRemainingMXN -= deferredDiscountMXN;
|
|
91
|
+
const adjustedDeferredTotalMXN = deferredPaymentsTotalMXN - deferredDiscountMXN;
|
|
92
|
+
const adjustedDeferredAmountMXN = deferredPaymentsCount > 0
|
|
93
|
+
? adjustedDeferredTotalMXN / deferredPaymentsCount
|
|
94
|
+
: 0;
|
|
95
|
+
const monthlyTotalGrossMXN = monthlyPaymentAmountMXN * monthlyPaymentCount;
|
|
96
|
+
const monthlyDiscountMXN = Math.min(monthlyTotalGrossMXN, installmentDiscountRemainingMXN);
|
|
97
|
+
const adjustedMonthlyTotalMXN = monthlyTotalGrossMXN - monthlyDiscountMXN;
|
|
98
|
+
const adjustedMonthlyAmountMXN = monthlyPaymentCount > 0 ? adjustedMonthlyTotalMXN / monthlyPaymentCount : 0;
|
|
99
|
+
const milestones = {};
|
|
100
|
+
if (isCashback) {
|
|
101
|
+
milestones[1] = rules.milestones.cashbackStart;
|
|
102
|
+
milestones[Math.round(deliveryMonths / 2)] = rules.milestones.midpoint;
|
|
103
|
+
if (isCashbackCapped) {
|
|
104
|
+
milestones[cashbackMonths] = "Último cashback";
|
|
105
|
+
}
|
|
106
|
+
milestones[deliveryMonths] = rules.milestones.delivery;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
deferredPaymentMonths.forEach((month) => {
|
|
110
|
+
milestones[month] =
|
|
111
|
+
`${rules.milestones.annualityPrefix} ${deferredPaymentAmountMXN.toLocaleString("es-MX", {
|
|
112
|
+
style: "currency",
|
|
113
|
+
currency: "MXN",
|
|
114
|
+
maximumFractionDigits: 0,
|
|
115
|
+
})}`;
|
|
116
|
+
});
|
|
117
|
+
milestones[deliveryMonths] = rules.milestones.delivery;
|
|
118
|
+
}
|
|
119
|
+
const timelineRows = [
|
|
120
|
+
{
|
|
121
|
+
month: 0,
|
|
122
|
+
paymentMXN: downPaymentAmountMXN,
|
|
123
|
+
paymentLabel: "Enganche",
|
|
124
|
+
cashbackMXN: 0,
|
|
125
|
+
plusvaliaDeltaMXN: 0,
|
|
126
|
+
accumulatedCashbackMXN: 0,
|
|
127
|
+
accumulatedPlusvaliaMXN: 0,
|
|
128
|
+
assetValueMXN: listPriceMXN,
|
|
129
|
+
isMilestone: true,
|
|
130
|
+
milestoneLabel: "Enganche",
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
let previousAssetValueMXN = listPriceMXN;
|
|
134
|
+
let accumulatedCashbackMXN = 0;
|
|
135
|
+
let accumulatedPlusvaliaMXN = 0;
|
|
136
|
+
for (let month = 1; month <= deliveryMonths; month += 1) {
|
|
137
|
+
const assetValueMXN = listPriceMXN * Math.pow(1 + plusvaliaMonthlyRate, month);
|
|
138
|
+
const plusvaliaDeltaMXN = assetValueMXN - previousAssetValueMXN;
|
|
139
|
+
accumulatedPlusvaliaMXN += plusvaliaDeltaMXN;
|
|
140
|
+
const cashbackForMonthMXN = isCashback && month <= cashbackMonths ? cashbackMonthlyMXN : 0;
|
|
141
|
+
let paymentMXN = 0;
|
|
142
|
+
let paymentLabel = "—";
|
|
143
|
+
if (isCashback) {
|
|
144
|
+
if (month === deliveryMonths) {
|
|
145
|
+
paymentMXN = balanceAmountMXN;
|
|
146
|
+
paymentLabel = "Saldo contra entrega";
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (installmentAllocation) {
|
|
150
|
+
const hasMonthlyPaymentForMonth = monthlyPaymentPct > 0 && monthlyPaymentMonthSet.has(month);
|
|
151
|
+
const hasDeferredPaymentForMonth = deferredPaymentMonthSet.has(month);
|
|
152
|
+
if (hasMonthlyPaymentForMonth) {
|
|
153
|
+
paymentMXN += adjustedMonthlyAmountMXN;
|
|
154
|
+
paymentLabel = "Mensualidad";
|
|
155
|
+
}
|
|
156
|
+
if (hasDeferredPaymentForMonth) {
|
|
157
|
+
paymentMXN += adjustedDeferredAmountMXN;
|
|
158
|
+
paymentLabel = "Anualidad";
|
|
159
|
+
}
|
|
160
|
+
if (month === deliveryMonths && adjustedDeliveryAmountMXN > 0) {
|
|
161
|
+
paymentMXN += adjustedDeliveryAmountMXN;
|
|
162
|
+
paymentLabel = `Contra entrega ${deliveryPaymentPct}%`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
accumulatedCashbackMXN += cashbackForMonthMXN;
|
|
166
|
+
timelineRows.push({
|
|
167
|
+
month,
|
|
168
|
+
paymentMXN,
|
|
169
|
+
paymentLabel,
|
|
170
|
+
cashbackMXN: cashbackForMonthMXN,
|
|
171
|
+
plusvaliaDeltaMXN,
|
|
172
|
+
accumulatedCashbackMXN,
|
|
173
|
+
accumulatedPlusvaliaMXN,
|
|
174
|
+
assetValueMXN,
|
|
175
|
+
isMilestone: Boolean(milestones[month]),
|
|
176
|
+
milestoneLabel: milestones[month] ?? "",
|
|
177
|
+
});
|
|
178
|
+
previousAssetValueMXN = assetValueMXN;
|
|
179
|
+
}
|
|
180
|
+
const finalTimelineRow = timelineRows.at(-1);
|
|
181
|
+
if (!finalTimelineRow) {
|
|
182
|
+
throw new Error("Unable to build the quote timeline.");
|
|
183
|
+
}
|
|
184
|
+
const projectionPoints = [
|
|
185
|
+
{
|
|
186
|
+
yearOffset: 0,
|
|
187
|
+
label: "Entrega",
|
|
188
|
+
assetValueMXN: finalTimelineRow.assetValueMXN,
|
|
189
|
+
rentalAccumulatedMXN: 0,
|
|
190
|
+
valueWithRentMXN: finalTimelineRow.assetValueMXN,
|
|
191
|
+
},
|
|
192
|
+
];
|
|
193
|
+
let projectedAssetValueMXN = finalTimelineRow.assetValueMXN;
|
|
194
|
+
let rentalAccumulatedMXN = 0;
|
|
195
|
+
const rentalRate = getFinanceRulesRentalRateForUnit(unit, rules);
|
|
196
|
+
const baseRentalAnnualMXN = getUnitEstimatedRentalAnnualMXN(unit, rentalRate, input.includeIva);
|
|
197
|
+
const usesCorporateRentalInflation = isCorporateRentalUnit(unit);
|
|
198
|
+
for (let year = 1; year <= input.postDeliveryYears; year += 1) {
|
|
199
|
+
projectedAssetValueMXN *= 1 + plusvaliaRate;
|
|
200
|
+
rentalAccumulatedMXN += usesCorporateRentalInflation
|
|
201
|
+
? getEscalatedAnnualAmountMXN(baseRentalAnnualMXN, rules.corporateInflationRate, year)
|
|
202
|
+
: projectedAssetValueMXN * rentalRate;
|
|
203
|
+
projectionPoints.push({
|
|
204
|
+
yearOffset: year,
|
|
205
|
+
label: `Año +${year}`,
|
|
206
|
+
assetValueMXN: projectedAssetValueMXN,
|
|
207
|
+
rentalAccumulatedMXN,
|
|
208
|
+
valueWithRentMXN: projectedAssetValueMXN + rentalAccumulatedMXN,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
const netCostMXN = isCashback
|
|
212
|
+
? listPriceMXN - cashbackTotalMXN
|
|
213
|
+
: listPriceMXN - installmentDiscountMXN;
|
|
214
|
+
const plusvaliaAtDeliveryMXN = finalTimelineRow.accumulatedPlusvaliaMXN;
|
|
215
|
+
const plusvaliaPostDeliveryMXN = projectedAssetValueMXN - finalTimelineRow.assetValueMXN;
|
|
216
|
+
const totalGainMXN = plusvaliaAtDeliveryMXN +
|
|
217
|
+
plusvaliaPostDeliveryMXN +
|
|
218
|
+
cashbackTotalMXN +
|
|
219
|
+
installmentDiscountMXN +
|
|
220
|
+
rentalAccumulatedMXN;
|
|
221
|
+
const cashbackTotalLabel = isCashbackCapped
|
|
222
|
+
? `Cashback total recibido (${cashbackMonths} meses)`
|
|
223
|
+
: "Cashback total recibido";
|
|
224
|
+
const costBreakdown = isCashback
|
|
225
|
+
? [
|
|
226
|
+
{
|
|
227
|
+
label: `Enganche (${downPaymentPct}%)`,
|
|
228
|
+
valueMXN: -downPaymentAmountMXN,
|
|
229
|
+
tone: "negative",
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
label: `Saldo contra entrega (${100 - downPaymentPct}%)`,
|
|
233
|
+
valueMXN: -balanceAmountMXN,
|
|
234
|
+
tone: "negative",
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
label: cashbackTotalLabel,
|
|
238
|
+
valueMXN: cashbackTotalMXN,
|
|
239
|
+
tone: "auric",
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
label: "Costo neto",
|
|
243
|
+
valueMXN: -netCostMXN,
|
|
244
|
+
tone: "neutral",
|
|
245
|
+
},
|
|
246
|
+
]
|
|
247
|
+
: installmentAllocation
|
|
248
|
+
? [
|
|
249
|
+
{
|
|
250
|
+
label: `Enganche inicial (${installmentAllocation.downPaymentPct}%)`,
|
|
251
|
+
valueMXN: -downPaymentAmountMXN,
|
|
252
|
+
tone: "negative",
|
|
253
|
+
},
|
|
254
|
+
...(monthlyPaymentPct > 0
|
|
255
|
+
? [
|
|
256
|
+
{
|
|
257
|
+
label: `Mensualidades (${monthlyPaymentPct}%)`,
|
|
258
|
+
valueMXN: -monthlyPaymentTotalMXN,
|
|
259
|
+
tone: "negative",
|
|
260
|
+
},
|
|
261
|
+
]
|
|
262
|
+
: []),
|
|
263
|
+
...(deferredPaymentPct > 0
|
|
264
|
+
? [
|
|
265
|
+
{
|
|
266
|
+
label: `Anualidades diferidas (${deferredPaymentPct}%)`,
|
|
267
|
+
valueMXN: -deferredPaymentsTotalMXN,
|
|
268
|
+
tone: "negative",
|
|
269
|
+
},
|
|
270
|
+
]
|
|
271
|
+
: []),
|
|
272
|
+
...(deliveryPaymentPct > 0
|
|
273
|
+
? [
|
|
274
|
+
{
|
|
275
|
+
label: `Contra entrega (${deliveryPaymentPct}%)`,
|
|
276
|
+
valueMXN: -deliveryPaymentAmountMXN,
|
|
277
|
+
tone: "negative",
|
|
278
|
+
},
|
|
279
|
+
]
|
|
280
|
+
: []),
|
|
281
|
+
...(installmentDiscountMXN > 0
|
|
282
|
+
? [
|
|
283
|
+
{
|
|
284
|
+
label: `Descuento por enganche (${installmentDiscountPct}%)`,
|
|
285
|
+
valueMXN: installmentDiscountMXN,
|
|
286
|
+
tone: "auric",
|
|
287
|
+
},
|
|
288
|
+
]
|
|
289
|
+
: []),
|
|
290
|
+
{
|
|
291
|
+
label: "Total invertido",
|
|
292
|
+
valueMXN: -netCostMXN,
|
|
293
|
+
tone: "neutral",
|
|
294
|
+
},
|
|
295
|
+
]
|
|
296
|
+
: [];
|
|
297
|
+
const gainBreakdown = [
|
|
298
|
+
{
|
|
299
|
+
label: "Plusvalía acumulada al escriturar",
|
|
300
|
+
valueMXN: plusvaliaAtDeliveryMXN,
|
|
301
|
+
tone: "positive",
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
label: "Plusvalía posterior a la escritura",
|
|
305
|
+
valueMXN: plusvaliaPostDeliveryMXN,
|
|
306
|
+
tone: "positive",
|
|
307
|
+
},
|
|
308
|
+
...(isCashback
|
|
309
|
+
? [
|
|
310
|
+
{
|
|
311
|
+
label: cashbackTotalLabel,
|
|
312
|
+
valueMXN: cashbackTotalMXN,
|
|
313
|
+
tone: "auric",
|
|
314
|
+
},
|
|
315
|
+
]
|
|
316
|
+
: []),
|
|
317
|
+
...(!isCashback && installmentDiscountMXN > 0
|
|
318
|
+
? [
|
|
319
|
+
{
|
|
320
|
+
label: `Descuento por enganche (${installmentDiscountPct}%)`,
|
|
321
|
+
valueMXN: installmentDiscountMXN,
|
|
322
|
+
tone: "auric",
|
|
323
|
+
},
|
|
324
|
+
]
|
|
325
|
+
: []),
|
|
326
|
+
{
|
|
327
|
+
label: "Valor proyectado del activo",
|
|
328
|
+
valueMXN: projectedAssetValueMXN,
|
|
329
|
+
tone: "neutral",
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
label: "Renta acumulada proyectada",
|
|
333
|
+
valueMXN: rentalAccumulatedMXN,
|
|
334
|
+
tone: "positive",
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
label: "Ganancia total estimada",
|
|
338
|
+
valueMXN: totalGainMXN,
|
|
339
|
+
tone: "positive",
|
|
340
|
+
},
|
|
341
|
+
];
|
|
342
|
+
return {
|
|
343
|
+
unit,
|
|
344
|
+
scheme,
|
|
345
|
+
input,
|
|
346
|
+
listPriceMXN,
|
|
347
|
+
deliveryMonths,
|
|
348
|
+
isCashback,
|
|
349
|
+
cashbackMonths,
|
|
350
|
+
isCashbackCapped,
|
|
351
|
+
downPaymentPct,
|
|
352
|
+
downPaymentAmountMXN,
|
|
353
|
+
balanceAmountMXN,
|
|
354
|
+
cashbackMonthlyMXN,
|
|
355
|
+
cashbackTotalMXN,
|
|
356
|
+
installmentDiscountPct,
|
|
357
|
+
installmentDiscountMXN,
|
|
358
|
+
monthlyPaymentPct,
|
|
359
|
+
monthlyPaymentCount,
|
|
360
|
+
monthlyPaymentAmountMXN,
|
|
361
|
+
deferredPaymentPct,
|
|
362
|
+
deferredPaymentsTotalMXN,
|
|
363
|
+
deferredPaymentsCount,
|
|
364
|
+
deferredPaymentMonths,
|
|
365
|
+
deferredPaymentAmountMXN,
|
|
366
|
+
deliveryPaymentPct,
|
|
367
|
+
deliveryPaymentAmountMXN,
|
|
368
|
+
netCostMXN,
|
|
369
|
+
plusvaliaAtDeliveryMXN,
|
|
370
|
+
plusvaliaPostDeliveryMXN,
|
|
371
|
+
totalGainMXN,
|
|
372
|
+
finalAssetValueMXN: finalTimelineRow.assetValueMXN,
|
|
373
|
+
projectedAssetValueMXN,
|
|
374
|
+
rentalMonthlyMXN: getUnitEstimatedRentalMonthlyMXN(unit, rentalRate, input.includeIva),
|
|
375
|
+
rentalAccumulatedMXN,
|
|
376
|
+
projectionPoints,
|
|
377
|
+
timelineRows,
|
|
378
|
+
visibleTimelineRows: getVisibleTimelineRows(timelineRows, deferredPaymentMonths, deliveryMonths, isCashback, cashbackMonths),
|
|
379
|
+
costBreakdown,
|
|
380
|
+
gainBreakdown,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { buildCorporatePlan, type EntradaActionSchemeId } from "./entrada.js";
|
|
2
|
+
import type { AppContent, InventoryAvailabilityByUnitId, InventoryUnit } from "./types.js";
|
|
3
|
+
export declare const CORPORATE_ACTION_MIN_COUNT = 1;
|
|
4
|
+
export declare const CORPORATE_ACTION_MAX_COUNT = 5;
|
|
5
|
+
export declare const CORPORATE_ACTION_DEFAULT_SCHEME_ID: "contado";
|
|
6
|
+
export declare const CORPORATE_ACTION_MIN_ENGANCHE_SCHEME_ID = "amplio";
|
|
7
|
+
export type CorporateActionPlan = ReturnType<typeof buildCorporatePlan>;
|
|
8
|
+
export declare function isCorporateActionUnit(unit: InventoryUnit | null | undefined, content: AppContent): boolean;
|
|
9
|
+
export declare function getCorporateActionTotalShares({ availabilityByUnitId, content, unit, }: {
|
|
10
|
+
availabilityByUnitId: InventoryAvailabilityByUnitId;
|
|
11
|
+
content: AppContent;
|
|
12
|
+
unit: InventoryUnit;
|
|
13
|
+
}): number;
|
|
14
|
+
export declare function clampCorporateActionCount(value: number): number;
|
|
15
|
+
export declare function parseCorporateActionCount(value: string | undefined): number;
|
|
16
|
+
export declare function normalizeCorporateActionSchemeId(value: string | null | undefined): EntradaActionSchemeId;
|
|
17
|
+
export declare function buildCorporateActionPlan({ actionCount, availabilityByUnitId, content, schemeId, unit, }: {
|
|
18
|
+
actionCount: number;
|
|
19
|
+
availabilityByUnitId: InventoryAvailabilityByUnitId;
|
|
20
|
+
content: AppContent;
|
|
21
|
+
schemeId: EntradaActionSchemeId | string | null | undefined;
|
|
22
|
+
unit: InventoryUnit;
|
|
23
|
+
}): {
|
|
24
|
+
capitalMXN: number;
|
|
25
|
+
engancheMXN: number;
|
|
26
|
+
m2: number;
|
|
27
|
+
monthlyCount: 0 | 5 | 13;
|
|
28
|
+
monthlyPaymentMXN: number;
|
|
29
|
+
numActions: number;
|
|
30
|
+
operationMonthlyReturnMXN: number;
|
|
31
|
+
operationAnnualReturnNetMXN: number;
|
|
32
|
+
operationMonthlyReturnNetMXN: number;
|
|
33
|
+
operationReturnHorizonNetMXN: number;
|
|
34
|
+
obraReturnMonthlyMXN: number;
|
|
35
|
+
obraReturnMonthlyNetMXN: number;
|
|
36
|
+
obraReturnNetMXN: number;
|
|
37
|
+
patrimonioTotalMXN: number;
|
|
38
|
+
plusvaliaGainMXN: number;
|
|
39
|
+
pricePerActionMXN: number;
|
|
40
|
+
projectedAssetValueMXN: number;
|
|
41
|
+
scheme: {
|
|
42
|
+
readonly id: "contado";
|
|
43
|
+
readonly description: "Liquidas tu acción en una sola exhibición y accedes a tasa preferencial desde el mes 1.";
|
|
44
|
+
readonly engancheRatio: 1;
|
|
45
|
+
readonly label: "Contado";
|
|
46
|
+
readonly monthlyCount: 0;
|
|
47
|
+
readonly monthlyRatio: 0;
|
|
48
|
+
readonly tag: "Máximo rendimiento";
|
|
49
|
+
readonly tasaObra: 0.08;
|
|
50
|
+
readonly tasaObraLabel: "8%";
|
|
51
|
+
} | {
|
|
52
|
+
readonly id: "medio";
|
|
53
|
+
readonly description: "Enganche de $120K y 5 mensualidades de $40K. Rendimiento del 6% anual una vez liquidado el capital.";
|
|
54
|
+
readonly engancheRatio: number;
|
|
55
|
+
readonly label: "Financiado 5 meses";
|
|
56
|
+
readonly monthlyCount: 5;
|
|
57
|
+
readonly monthlyRatio: number;
|
|
58
|
+
readonly tag: "Balance";
|
|
59
|
+
readonly tasaObra: 0.06;
|
|
60
|
+
readonly tasaObraLabel: "6%";
|
|
61
|
+
} | {
|
|
62
|
+
readonly id: "amplio";
|
|
63
|
+
readonly description: "Enganche de $60K y 13 mensualidades de $20K. Rendimiento del 4% anual una vez liquidado el capital.";
|
|
64
|
+
readonly engancheRatio: number;
|
|
65
|
+
readonly label: "Financiado 13 meses";
|
|
66
|
+
readonly monthlyCount: 13;
|
|
67
|
+
readonly monthlyRatio: number;
|
|
68
|
+
readonly tag: "Más accesible";
|
|
69
|
+
readonly tasaObra: 0.04;
|
|
70
|
+
readonly tasaObraLabel: "4%";
|
|
71
|
+
};
|
|
72
|
+
totalMonths: 0 | 5 | 13;
|
|
73
|
+
totalPaidMXN: number;
|
|
74
|
+
};
|
|
75
|
+
export declare function getCorporateActionUnitDisplay({ availabilityByUnitId, content, unit, }: {
|
|
76
|
+
availabilityByUnitId: InventoryAvailabilityByUnitId;
|
|
77
|
+
content: AppContent;
|
|
78
|
+
unit: InventoryUnit;
|
|
79
|
+
}): {
|
|
80
|
+
m2PerAction: number;
|
|
81
|
+
monthlyRentPerActionMXN: number;
|
|
82
|
+
pricePerActionMXN: number;
|
|
83
|
+
totalShares: number;
|
|
84
|
+
};
|
|
85
|
+
export declare function buildCorporateActionProductLabel(plan: CorporateActionPlan): string;
|
|
86
|
+
export declare function buildCorporateActionCheckoutSelection(plan: CorporateActionPlan): {
|
|
87
|
+
capitalMXN: number;
|
|
88
|
+
engancheMXN: number;
|
|
89
|
+
m2: number;
|
|
90
|
+
monthlyPaymentMXN: number;
|
|
91
|
+
numActions: number;
|
|
92
|
+
operationMonthlyReturnNetMXN: number;
|
|
93
|
+
pricePerActionMXN: number;
|
|
94
|
+
schemeId: "contado" | "medio" | "amplio";
|
|
95
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { isCorporateRentalUnit } from "./corporate-rent.js";
|
|
2
|
+
import { buildCorporatePlan, ENTRADA_ACTION_SCHEMES, getCorporateM2PerAction, getCorporatePricePerActionMXN, } from "./entrada.js";
|
|
3
|
+
import { getInventoryAvailability } from "./inventory-availability.js";
|
|
4
|
+
export const CORPORATE_ACTION_MIN_COUNT = 1;
|
|
5
|
+
export const CORPORATE_ACTION_MAX_COUNT = 5;
|
|
6
|
+
export const CORPORATE_ACTION_DEFAULT_SCHEME_ID = ENTRADA_ACTION_SCHEMES[0].id;
|
|
7
|
+
export const CORPORATE_ACTION_MIN_ENGANCHE_SCHEME_ID = "amplio";
|
|
8
|
+
function getProductTypeForUnit(content, unit) {
|
|
9
|
+
return (content.productTypes.find((productType) => productType.id === unit.productTypeId) ?? null);
|
|
10
|
+
}
|
|
11
|
+
export function isCorporateActionUnit(unit, content) {
|
|
12
|
+
if (!unit) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
const productType = getProductTypeForUnit(content, unit);
|
|
16
|
+
const usesShareInventory = unit.inventoryMode === "shares" ||
|
|
17
|
+
productType?.inventoryMode === "shares" ||
|
|
18
|
+
Boolean(productType?.shareInventory);
|
|
19
|
+
if (!usesShareInventory) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return (unit.id === content.entryInventory.corporateUnitId ||
|
|
23
|
+
isCorporateRentalUnit(unit));
|
|
24
|
+
}
|
|
25
|
+
export function getCorporateActionTotalShares({ availabilityByUnitId, content, unit, }) {
|
|
26
|
+
const availability = getInventoryAvailability(unit, availabilityByUnitId);
|
|
27
|
+
const productType = getProductTypeForUnit(content, unit);
|
|
28
|
+
return Math.max(availability?.share?.total ?? productType?.shareInventory?.totalShares ?? 1, 1);
|
|
29
|
+
}
|
|
30
|
+
export function clampCorporateActionCount(value) {
|
|
31
|
+
if (!Number.isFinite(value)) {
|
|
32
|
+
return CORPORATE_ACTION_MIN_COUNT;
|
|
33
|
+
}
|
|
34
|
+
return Math.min(CORPORATE_ACTION_MAX_COUNT, Math.max(CORPORATE_ACTION_MIN_COUNT, Math.round(value)));
|
|
35
|
+
}
|
|
36
|
+
export function parseCorporateActionCount(value) {
|
|
37
|
+
return clampCorporateActionCount(Number(value));
|
|
38
|
+
}
|
|
39
|
+
export function normalizeCorporateActionSchemeId(value) {
|
|
40
|
+
return (ENTRADA_ACTION_SCHEMES.find((scheme) => scheme.id === value)?.id ??
|
|
41
|
+
CORPORATE_ACTION_DEFAULT_SCHEME_ID);
|
|
42
|
+
}
|
|
43
|
+
export function buildCorporateActionPlan({ actionCount, availabilityByUnitId, content, schemeId, unit, }) {
|
|
44
|
+
return buildCorporatePlan({
|
|
45
|
+
inflationRate: content.financeRules.corporateInflationRate,
|
|
46
|
+
numActions: clampCorporateActionCount(actionCount),
|
|
47
|
+
requestedSchemeId: schemeId,
|
|
48
|
+
rentalRate: content.financeRules.accionesRentalRate,
|
|
49
|
+
totalShares: getCorporateActionTotalShares({
|
|
50
|
+
availabilityByUnitId,
|
|
51
|
+
content,
|
|
52
|
+
unit,
|
|
53
|
+
}),
|
|
54
|
+
unit,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export function getCorporateActionUnitDisplay({ availabilityByUnitId, content, unit, }) {
|
|
58
|
+
const totalShares = getCorporateActionTotalShares({
|
|
59
|
+
availabilityByUnitId,
|
|
60
|
+
content,
|
|
61
|
+
unit,
|
|
62
|
+
});
|
|
63
|
+
const pricePerActionMXN = getCorporatePricePerActionMXN(unit, totalShares);
|
|
64
|
+
const m2PerAction = getCorporateM2PerAction(unit, totalShares);
|
|
65
|
+
return {
|
|
66
|
+
m2PerAction,
|
|
67
|
+
monthlyRentPerActionMXN: (pricePerActionMXN * content.financeRules.accionesRentalRate) / 12,
|
|
68
|
+
pricePerActionMXN,
|
|
69
|
+
totalShares,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export function buildCorporateActionProductLabel(plan) {
|
|
73
|
+
const actionLabel = `${plan.numActions} ${plan.numActions === 1 ? "acción Serie B" : "acciones Serie B"}`;
|
|
74
|
+
return `Acciones corporativo · ${actionLabel}`;
|
|
75
|
+
}
|
|
76
|
+
export function buildCorporateActionCheckoutSelection(plan) {
|
|
77
|
+
return {
|
|
78
|
+
capitalMXN: plan.totalPaidMXN,
|
|
79
|
+
engancheMXN: plan.engancheMXN,
|
|
80
|
+
m2: plan.m2,
|
|
81
|
+
monthlyPaymentMXN: plan.monthlyPaymentMXN,
|
|
82
|
+
numActions: plan.numActions,
|
|
83
|
+
operationMonthlyReturnNetMXN: plan.operationMonthlyReturnNetMXN,
|
|
84
|
+
pricePerActionMXN: plan.pricePerActionMXN,
|
|
85
|
+
schemeId: plan.scheme.id,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { InventoryUnit } from "./types.js";
|
|
2
|
+
export declare const DEFAULT_CORPORATE_INFLATION_RATE = 0.04;
|
|
3
|
+
export declare function isCorporateRentalUnit(unit: Pick<InventoryUnit, "category" | "name" | "productTypeName" | "unitNumber">): boolean;
|
|
4
|
+
export declare function getEscalatedAnnualAmountMXN(baseAnnualAmountMXN: number, annualEscalationRate: number, yearNumber: number): number;
|
|
5
|
+
export declare function getEscalatedAmountOverHorizonMXN(baseAnnualAmountMXN: number, annualEscalationRate: number, years: number): number;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const DEFAULT_CORPORATE_INFLATION_RATE = 0.04;
|
|
2
|
+
const CORPORATE_RENT_CATEGORY_TOKENS = new Set(["corporativo", "acciones"]);
|
|
3
|
+
function normalizeComparableLabel(value) {
|
|
4
|
+
return (value ?? "")
|
|
5
|
+
.normalize("NFD")
|
|
6
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
7
|
+
.toLowerCase()
|
|
8
|
+
.trim()
|
|
9
|
+
.replace(/\s+/g, " ");
|
|
10
|
+
}
|
|
11
|
+
function matchesCorporateToken(value) {
|
|
12
|
+
const normalizedValue = normalizeComparableLabel(value);
|
|
13
|
+
if (!normalizedValue) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (CORPORATE_RENT_CATEGORY_TOKENS.has(normalizedValue)) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
return [...CORPORATE_RENT_CATEGORY_TOKENS].some((token) => normalizedValue.includes(token));
|
|
20
|
+
}
|
|
21
|
+
export function isCorporateRentalUnit(unit) {
|
|
22
|
+
return (matchesCorporateToken(unit.category) ||
|
|
23
|
+
matchesCorporateToken(unit.productTypeName) ||
|
|
24
|
+
matchesCorporateToken(unit.name) ||
|
|
25
|
+
matchesCorporateToken(unit.unitNumber));
|
|
26
|
+
}
|
|
27
|
+
export function getEscalatedAnnualAmountMXN(baseAnnualAmountMXN, annualEscalationRate, yearNumber) {
|
|
28
|
+
return (baseAnnualAmountMXN *
|
|
29
|
+
Math.pow(1 + Math.max(annualEscalationRate, 0), Math.max(yearNumber - 1, 0)));
|
|
30
|
+
}
|
|
31
|
+
export function getEscalatedAmountOverHorizonMXN(baseAnnualAmountMXN, annualEscalationRate, years) {
|
|
32
|
+
let totalMXN = 0;
|
|
33
|
+
let remainingYears = Math.max(years, 0);
|
|
34
|
+
let yearNumber = 1;
|
|
35
|
+
while (remainingYears > 0) {
|
|
36
|
+
const yearFraction = Math.min(remainingYears, 1);
|
|
37
|
+
totalMXN +=
|
|
38
|
+
getEscalatedAnnualAmountMXN(baseAnnualAmountMXN, annualEscalationRate, yearNumber) * yearFraction;
|
|
39
|
+
remainingYears -= yearFraction;
|
|
40
|
+
yearNumber += 1;
|
|
41
|
+
}
|
|
42
|
+
return totalMXN;
|
|
43
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function normalizeDeliveryDateInput(value: string | null | undefined): string | undefined;
|
|
2
|
+
export declare function parseDeliveryDateParts(value: string): {
|
|
3
|
+
year: number;
|
|
4
|
+
month: number;
|
|
5
|
+
} | null;
|
|
6
|
+
export declare function isValidDeliveryDateString(value: string): boolean;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function normalizeDeliveryDateInput(value) {
|
|
2
|
+
const trimmed = value?.trim();
|
|
3
|
+
return trimmed ? trimmed : undefined;
|
|
4
|
+
}
|
|
5
|
+
export function parseDeliveryDateParts(value) {
|
|
6
|
+
const match = /^(\d{4})-(\d{2})$/.exec(value.trim());
|
|
7
|
+
if (!match) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const year = Number(match[1]);
|
|
11
|
+
const month = Number(match[2]);
|
|
12
|
+
if (!Number.isFinite(year) ||
|
|
13
|
+
!Number.isFinite(month) ||
|
|
14
|
+
month < 1 ||
|
|
15
|
+
month > 12) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return { year, month };
|
|
19
|
+
}
|
|
20
|
+
export function isValidDeliveryDateString(value) {
|
|
21
|
+
return parseDeliveryDateParts(value) !== null;
|
|
22
|
+
}
|