@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.
Files changed (50) hide show
  1. package/README.md +154 -0
  2. package/dist/bundle-payment-schedule.calc.d.ts +2 -0
  3. package/dist/bundle-payment-schedule.calc.js +160 -0
  4. package/dist/calculate-quote.d.ts +2 -0
  5. package/dist/calculate-quote.js +382 -0
  6. package/dist/corporate-actions.d.ts +95 -0
  7. package/dist/corporate-actions.js +87 -0
  8. package/dist/corporate-rent.d.ts +5 -0
  9. package/dist/corporate-rent.js +43 -0
  10. package/dist/delivery-date.d.ts +6 -0
  11. package/dist/delivery-date.js +22 -0
  12. package/dist/entrada.d.ts +111 -0
  13. package/dist/entrada.js +139 -0
  14. package/dist/format.d.ts +7 -0
  15. package/dist/format.js +37 -0
  16. package/dist/http.d.ts +29 -0
  17. package/dist/http.js +123 -0
  18. package/dist/index.d.ts +15 -0
  19. package/dist/index.js +22 -0
  20. package/dist/installment-allocation.d.ts +30 -0
  21. package/dist/installment-allocation.js +69 -0
  22. package/dist/inventory-availability.d.ts +10 -0
  23. package/dist/inventory-availability.js +105 -0
  24. package/dist/live-inventory-availability.d.ts +16 -0
  25. package/dist/live-inventory-availability.js +88 -0
  26. package/dist/payment-schemes.d.ts +22 -0
  27. package/dist/payment-schemes.js +99 -0
  28. package/dist/post-delivery-value.d.ts +3 -0
  29. package/dist/post-delivery-value.js +61 -0
  30. package/dist/product-types.d.ts +16 -0
  31. package/dist/product-types.js +82 -0
  32. package/dist/query-state.d.ts +5 -0
  33. package/dist/query-state.js +157 -0
  34. package/dist/quote-outcome-graph.d.ts +31 -0
  35. package/dist/quote-outcome-graph.js +113 -0
  36. package/dist/quote.d.ts +19 -0
  37. package/dist/quote.js +18 -0
  38. package/dist/selection-state.d.ts +34 -0
  39. package/dist/selection-state.js +156 -0
  40. package/dist/server.d.ts +17 -0
  41. package/dist/server.js +18 -0
  42. package/dist/sitemap.d.ts +22 -0
  43. package/dist/sitemap.js +45 -0
  44. package/dist/social-quote.d.ts +8 -0
  45. package/dist/social-quote.js +51 -0
  46. package/dist/tax.d.ts +5 -0
  47. package/dist/tax.js +11 -0
  48. package/dist/types.d.ts +511 -0
  49. package/dist/types.js +1 -0
  50. 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
+ }