@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
package/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # Desarrollos SDK
2
+
3
+ Framework-agnostic TypeScript helpers for third-party websites that integrate
4
+ with Grupo LAPA developments through the public API.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ npm install @grupolapa/desarrollos-sdk
10
+ ```
11
+
12
+ ```bash
13
+ pnpm add @grupolapa/desarrollos-sdk
14
+ ```
15
+
16
+ The public API base URL is:
17
+
18
+ ```txt
19
+ https://grupolapa.com/api/public/v1/desarrollos
20
+ ```
21
+
22
+ Agent-readable integration instructions are available at:
23
+
24
+ ```txt
25
+ https://grupolapa.com/api/public/v1/desarrollos/agent.json
26
+ ```
27
+
28
+ ## Browser Reads and Quotes
29
+
30
+ ```ts
31
+ import { createDesarrollosClient } from "@grupolapa/desarrollos-sdk";
32
+ import {
33
+ calculateQuote,
34
+ getResolvedSchemeForUnit,
35
+ } from "@grupolapa/desarrollos-sdk/quote";
36
+ import {
37
+ getSelectableUnitHotspots,
38
+ resolveInitialSitemapMap,
39
+ } from "@grupolapa/desarrollos-sdk/sitemap";
40
+
41
+ const client = createDesarrollosClient({
42
+ baseUrl: "https://grupolapa.com/api/public/v1/desarrollos",
43
+ });
44
+
45
+ const desarrolloKey = "viamare";
46
+ const [{ snapshot }, inventory, sitemap, paymentMethods] = await Promise.all([
47
+ client.getSnapshot({ desarrolloKey }),
48
+ client.getInventory({ desarrolloKey }),
49
+ client.getSitemap({ desarrolloKey }),
50
+ client.getPaymentMethods({ desarrolloKey }),
51
+ ]);
52
+
53
+ const map = resolveInitialSitemapMap(sitemap);
54
+ const hotspots = map ? getSelectableUnitHotspots(map) : [];
55
+ const unit = inventory.inventory.find(
56
+ (item) => item.id === hotspots[0]?.unitId,
57
+ );
58
+ const scheme = unit
59
+ ? getResolvedSchemeForUnit(unit, snapshot.content.paymentSchemes, null)
60
+ : null;
61
+
62
+ const quote =
63
+ unit && scheme
64
+ ? calculateQuote(
65
+ unit,
66
+ {
67
+ unitId: unit.id,
68
+ scheme: scheme.id,
69
+ plusvaliaAnnualPct:
70
+ snapshot.content.financeRules.defaults.plusvaliaAnnualPct,
71
+ postDeliveryYears:
72
+ snapshot.content.financeRules.defaults.postDeliveryYears,
73
+ downPaymentPct: snapshot.content.financeRules.defaults.downPaymentPct,
74
+ includeIva: false,
75
+ },
76
+ scheme,
77
+ snapshot.content.financeRules,
78
+ )
79
+ : null;
80
+ ```
81
+
82
+ ## Trusted Server Flow
83
+
84
+ Keep `lapa_dev_<prefix>_<secret>` API keys on a server. Use the trusted server
85
+ client to list allowed developments and mint short-lived browser session tokens.
86
+ Browser reservation writes should use only those session tokens.
87
+
88
+ ```ts
89
+ import { createDesarrollosServerClient } from "@grupolapa/desarrollos-sdk/server";
90
+
91
+ const serverClient = createDesarrollosServerClient({
92
+ baseUrl: "https://grupolapa.com/api/public/v1/desarrollos",
93
+ apiKey: process.env.LAPA_DESARROLLOS_API_KEY!,
94
+ });
95
+
96
+ export async function createBrowserSession(origin: string) {
97
+ return await serverClient.createBrowserSession({
98
+ desarrolloKey: "viamare",
99
+ origin,
100
+ });
101
+ }
102
+ ```
103
+
104
+ ```ts
105
+ import { createDesarrollosClient } from "@grupolapa/desarrollos-sdk";
106
+
107
+ const browserClient = createDesarrollosClient({
108
+ baseUrl: "https://grupolapa.com/api/public/v1/desarrollos",
109
+ });
110
+
111
+ await browserClient.createReservation({
112
+ desarrolloKey: "viamare",
113
+ sessionToken,
114
+ request: {
115
+ items: [{ unitId: "unit_1" }],
116
+ payment: {
117
+ provider: "stripe",
118
+ returnUrl: "https://partner.example.com/return",
119
+ },
120
+ },
121
+ });
122
+ ```
123
+
124
+ ## Sitemap Rendering
125
+
126
+ The API returns image and overlay asset URLs plus SVG-native shape metadata.
127
+ The SDK does not mutate the DOM. Use the sitemap helpers to choose maps and
128
+ resolve unit hotspots, then render the overlay with the frontend framework of
129
+ your choice.
130
+
131
+ ```ts
132
+ import {
133
+ getShapeForUnit,
134
+ getSitemapMapAssetPlan,
135
+ resolveInitialSitemapMap,
136
+ } from "@grupolapa/desarrollos-sdk/sitemap";
137
+
138
+ const map = resolveInitialSitemapMap(sitemap);
139
+ if (map) {
140
+ const assets = getSitemapMapAssetPlan(map);
141
+ const shape = getShapeForUnit(map, selectedUnitId);
142
+ }
143
+ ```
144
+
145
+ ## Exports
146
+
147
+ - `@grupolapa/desarrollos-sdk`: browser-safe client, public types, and
148
+ `DesarrollosApiError`.
149
+ - `@grupolapa/desarrollos-sdk/server`: trusted server API-key client.
150
+ - `@grupolapa/desarrollos-sdk/quote`: quote, selection, payment, inventory,
151
+ formatting, Entrada/action, and product helpers.
152
+ - `@grupolapa/desarrollos-sdk/sitemap`: sitemap asset, hotspot, and shape
153
+ helpers.
154
+ - `@grupolapa/desarrollos-sdk/types`: domain and public REST TypeScript types.
@@ -0,0 +1,2 @@
1
+ import type { QuoteResult } from "./types.js";
2
+ export declare function aggregateQuoteResults(quotes: QuoteResult[]): QuoteResult;
@@ -0,0 +1,160 @@
1
+ function sumBy(items, getValue) {
2
+ return items.reduce((total, item) => total + getValue(item), 0);
3
+ }
4
+ function roundPct(value) {
5
+ return Math.round(value * 100) / 100;
6
+ }
7
+ function getTimelineRowAtOrAfterDelivery(quote, month) {
8
+ return (quote.timelineRows[month] ??
9
+ quote.timelineRows.at(-1) ??
10
+ quote.timelineRows[0]);
11
+ }
12
+ function joinLabels(labels) {
13
+ const uniqueLabels = [...new Set(labels.filter(Boolean))];
14
+ if (uniqueLabels.length === 0) {
15
+ return "—";
16
+ }
17
+ if (uniqueLabels.length === 1) {
18
+ return uniqueLabels[0];
19
+ }
20
+ return uniqueLabels.join(" + ");
21
+ }
22
+ function aggregateTimelineRows(quotes) {
23
+ const maxDeliveryMonths = Math.max(...quotes.map((quote) => quote.deliveryMonths));
24
+ return Array.from({ length: maxDeliveryMonths + 1 }, (_, month) => {
25
+ const exactRows = quotes
26
+ .map((quote) => quote.timelineRows[month] ?? null)
27
+ .filter((row) => Boolean(row));
28
+ const accumulatedRows = quotes.map((quote) => getTimelineRowAtOrAfterDelivery(quote, month));
29
+ const paymentLabels = exactRows
30
+ .filter((row) => row.paymentMXN > 0.005)
31
+ .map((row) => row.paymentLabel);
32
+ const milestoneLabels = exactRows
33
+ .filter((row) => row.isMilestone)
34
+ .map((row) => row.milestoneLabel);
35
+ return {
36
+ month,
37
+ paymentMXN: sumBy(exactRows, (row) => row.paymentMXN),
38
+ paymentLabel: joinLabels(paymentLabels),
39
+ cashbackMXN: sumBy(exactRows, (row) => row.cashbackMXN),
40
+ plusvaliaDeltaMXN: sumBy(exactRows, (row) => row.plusvaliaDeltaMXN),
41
+ accumulatedCashbackMXN: sumBy(accumulatedRows, (row) => row.accumulatedCashbackMXN),
42
+ accumulatedPlusvaliaMXN: sumBy(accumulatedRows, (row) => row.accumulatedPlusvaliaMXN),
43
+ assetValueMXN: sumBy(accumulatedRows, (row) => row.assetValueMXN),
44
+ isMilestone: milestoneLabels.length > 0,
45
+ milestoneLabel: joinLabels(milestoneLabels),
46
+ };
47
+ });
48
+ }
49
+ function aggregateProjectionPoints(quotes) {
50
+ const maxYearOffset = Math.max(...quotes.map((quote) => quote.projectionPoints.at(-1)?.yearOffset ?? 0));
51
+ return Array.from({ length: maxYearOffset + 1 }, (_, yearOffset) => {
52
+ const points = quotes.map((quote) => quote.projectionPoints.find((point) => point.yearOffset === yearOffset) ??
53
+ quote.projectionPoints.at(-1) ??
54
+ quote.projectionPoints[0]);
55
+ return {
56
+ yearOffset,
57
+ label: yearOffset === 0 ? "Entrega" : `Año +${yearOffset}`,
58
+ assetValueMXN: sumBy(points, (point) => point.assetValueMXN),
59
+ rentalAccumulatedMXN: sumBy(points, (point) => point.rentalAccumulatedMXN),
60
+ valueWithRentMXN: sumBy(points, (point) => point.valueWithRentMXN),
61
+ };
62
+ });
63
+ }
64
+ function aggregateBreakdownItems(items) {
65
+ const itemMap = new Map();
66
+ for (const item of items) {
67
+ const existing = itemMap.get(item.label);
68
+ if (existing) {
69
+ existing.valueMXN += item.valueMXN;
70
+ }
71
+ else {
72
+ itemMap.set(item.label, { ...item });
73
+ }
74
+ }
75
+ return [...itemMap.values()];
76
+ }
77
+ function countTimelineRows(rows, matchesLabel) {
78
+ return rows.filter((row) => row.paymentMXN > 0.005 && matchesLabel(row.paymentLabel)).length;
79
+ }
80
+ function sumTimelinePayments(rows, matchesLabel) {
81
+ return sumBy(rows.filter((row) => row.paymentMXN > 0.005 && matchesLabel(row.paymentLabel)), (row) => row.paymentMXN);
82
+ }
83
+ function labelIncludes(label, search) {
84
+ return label.toLocaleLowerCase("es-MX").includes(search);
85
+ }
86
+ export function aggregateQuoteResults(quotes) {
87
+ if (quotes.length === 0) {
88
+ throw new Error("At least one quote is required to aggregate a schedule.");
89
+ }
90
+ const [representativeQuote] = quotes;
91
+ const timelineRows = aggregateTimelineRows(quotes);
92
+ const projectionPoints = aggregateProjectionPoints(quotes);
93
+ const listPriceMXN = sumBy(quotes, (quote) => quote.listPriceMXN);
94
+ const downPaymentAmountMXN = sumBy(quotes, (quote) => quote.downPaymentAmountMXN);
95
+ const balanceAmountMXN = sumBy(quotes, (quote) => quote.balanceAmountMXN);
96
+ const cashbackTotalMXN = sumBy(quotes, (quote) => quote.cashbackTotalMXN);
97
+ const installmentDiscountMXN = sumBy(quotes, (quote) => quote.installmentDiscountMXN);
98
+ const netCostMXN = sumBy(quotes, (quote) => quote.netCostMXN);
99
+ const monthlyPaymentCount = countTimelineRows(timelineRows, (label) => labelIncludes(label, "mensualidad"));
100
+ const monthlyPaymentTotalMXN = sumTimelinePayments(timelineRows, (label) => labelIncludes(label, "mensualidad"));
101
+ const deferredPaymentsCount = countTimelineRows(timelineRows, (label) => labelIncludes(label, "anualidad"));
102
+ const deferredPaymentsTotalMXN = sumTimelinePayments(timelineRows, (label) => labelIncludes(label, "anualidad"));
103
+ const deliveryPaymentAmountMXN = sumTimelinePayments(timelineRows, (label) => labelIncludes(label, "contra entrega"));
104
+ const finalTimelineRow = timelineRows.at(-1);
105
+ const finalProjectionPoint = projectionPoints.at(-1);
106
+ return {
107
+ ...representativeQuote,
108
+ listPriceMXN,
109
+ deliveryMonths: finalTimelineRow.month,
110
+ cashbackMonths: Math.max(...quotes.map((quote) => quote.cashbackMonths)),
111
+ isCashbackCapped: quotes.some((quote) => quote.isCashbackCapped),
112
+ downPaymentPct: listPriceMXN > 0
113
+ ? roundPct((downPaymentAmountMXN / listPriceMXN) * 100)
114
+ : 0,
115
+ downPaymentAmountMXN,
116
+ balanceAmountMXN,
117
+ cashbackMonthlyMXN: sumBy(quotes, (quote) => quote.cashbackMonthlyMXN),
118
+ cashbackTotalMXN,
119
+ installmentDiscountPct: listPriceMXN > 0
120
+ ? roundPct((installmentDiscountMXN / listPriceMXN) * 100)
121
+ : 0,
122
+ installmentDiscountMXN,
123
+ monthlyPaymentPct: listPriceMXN > 0
124
+ ? roundPct((monthlyPaymentTotalMXN / listPriceMXN) * 100)
125
+ : 0,
126
+ monthlyPaymentCount,
127
+ monthlyPaymentAmountMXN: monthlyPaymentCount > 0
128
+ ? monthlyPaymentTotalMXN / monthlyPaymentCount
129
+ : 0,
130
+ deferredPaymentPct: listPriceMXN > 0
131
+ ? roundPct((deferredPaymentsTotalMXN / listPriceMXN) * 100)
132
+ : 0,
133
+ deferredPaymentsTotalMXN,
134
+ deferredPaymentsCount,
135
+ deferredPaymentMonths: timelineRows
136
+ .filter((row) => row.paymentMXN > 0.005 &&
137
+ labelIncludes(row.paymentLabel, "anualidad"))
138
+ .map((row) => row.month),
139
+ deferredPaymentAmountMXN: deferredPaymentsCount > 0
140
+ ? deferredPaymentsTotalMXN / deferredPaymentsCount
141
+ : 0,
142
+ deliveryPaymentPct: listPriceMXN > 0
143
+ ? roundPct((deliveryPaymentAmountMXN / listPriceMXN) * 100)
144
+ : 0,
145
+ deliveryPaymentAmountMXN,
146
+ netCostMXN,
147
+ plusvaliaAtDeliveryMXN: finalTimelineRow.accumulatedPlusvaliaMXN,
148
+ plusvaliaPostDeliveryMXN: sumBy(quotes, (quote) => quote.plusvaliaPostDeliveryMXN),
149
+ totalGainMXN: sumBy(quotes, (quote) => quote.totalGainMXN),
150
+ finalAssetValueMXN: finalTimelineRow.assetValueMXN,
151
+ projectedAssetValueMXN: finalProjectionPoint.assetValueMXN,
152
+ rentalMonthlyMXN: sumBy(quotes, (quote) => quote.rentalMonthlyMXN),
153
+ rentalAccumulatedMXN: finalProjectionPoint.rentalAccumulatedMXN,
154
+ projectionPoints,
155
+ timelineRows,
156
+ visibleTimelineRows: timelineRows.filter((row) => row.paymentMXN > 0.005 || row.isMilestone),
157
+ costBreakdown: aggregateBreakdownItems(quotes.flatMap((quote) => quote.costBreakdown)),
158
+ gainBreakdown: aggregateBreakdownItems(quotes.flatMap((quote) => quote.gainBreakdown)),
159
+ };
160
+ }
@@ -0,0 +1,2 @@
1
+ import type { FinanceRules, InventoryUnit, PaymentSchemeDefinition, QuoteInput, QuoteResult } from "./types.js";
2
+ export declare function calculateQuote(unit: InventoryUnit, input: QuoteInput, scheme: PaymentSchemeDefinition, rules: FinanceRules): QuoteResult;