@proxima-io/storefront-core 0.3.0 → 0.8.1

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 (170) hide show
  1. package/README.md +23 -17
  2. package/dist/addresses/address-book.d.ts +36 -0
  3. package/dist/addresses/address-book.d.ts.map +1 -0
  4. package/dist/addresses/address-book.js +62 -0
  5. package/dist/addresses/address-book.js.map +1 -0
  6. package/dist/analytics/analytics.d.ts +28 -0
  7. package/dist/analytics/analytics.d.ts.map +1 -0
  8. package/dist/analytics/analytics.js +124 -0
  9. package/dist/analytics/analytics.js.map +1 -0
  10. package/dist/analytics/attribution.d.ts +28 -0
  11. package/dist/analytics/attribution.d.ts.map +1 -0
  12. package/dist/analytics/attribution.js +116 -0
  13. package/dist/analytics/attribution.js.map +1 -0
  14. package/dist/analytics/session.d.ts +12 -0
  15. package/dist/analytics/session.d.ts.map +1 -0
  16. package/dist/analytics/session.js +62 -0
  17. package/dist/analytics/session.js.map +1 -0
  18. package/dist/analytics/trackers.d.ts +29 -0
  19. package/dist/analytics/trackers.d.ts.map +1 -0
  20. package/dist/analytics/trackers.js +30 -0
  21. package/dist/analytics/trackers.js.map +1 -0
  22. package/dist/api/endpoints.d.ts +70 -0
  23. package/dist/api/endpoints.d.ts.map +1 -0
  24. package/dist/api/endpoints.js +70 -0
  25. package/dist/api/endpoints.js.map +1 -0
  26. package/dist/api/index.d.ts +3 -0
  27. package/dist/api/index.d.ts.map +1 -0
  28. package/dist/api/index.js +3 -0
  29. package/dist/api/index.js.map +1 -0
  30. package/dist/api/storefront-client.d.ts +50 -0
  31. package/dist/api/storefront-client.d.ts.map +1 -0
  32. package/dist/api/storefront-client.js +123 -0
  33. package/dist/api/storefront-client.js.map +1 -0
  34. package/dist/buyer/auth.d.ts +105 -0
  35. package/dist/buyer/auth.d.ts.map +1 -0
  36. package/dist/buyer/auth.js +215 -0
  37. package/dist/buyer/auth.js.map +1 -0
  38. package/dist/cache/cache.d.ts +31 -0
  39. package/dist/cache/cache.d.ts.map +1 -0
  40. package/dist/cache/cache.js +71 -0
  41. package/dist/cache/cache.js.map +1 -0
  42. package/dist/campaign/countdown.d.ts +40 -0
  43. package/dist/campaign/countdown.d.ts.map +1 -0
  44. package/dist/campaign/countdown.js +71 -0
  45. package/dist/campaign/countdown.js.map +1 -0
  46. package/dist/cart/cart.d.ts +57 -0
  47. package/dist/cart/cart.d.ts.map +1 -0
  48. package/dist/cart/cart.js +64 -0
  49. package/dist/cart/cart.js.map +1 -0
  50. package/dist/catalog/listings.d.ts +87 -0
  51. package/dist/catalog/listings.d.ts.map +1 -0
  52. package/dist/catalog/listings.js +140 -0
  53. package/dist/catalog/listings.js.map +1 -0
  54. package/dist/cms/payment-methods.d.ts +13 -0
  55. package/dist/cms/payment-methods.d.ts.map +1 -0
  56. package/dist/cms/payment-methods.js +41 -0
  57. package/dist/cms/payment-methods.js.map +1 -0
  58. package/dist/cms/website.d.ts +74 -0
  59. package/dist/cms/website.d.ts.map +1 -0
  60. package/dist/cms/website.js +144 -0
  61. package/dist/cms/website.js.map +1 -0
  62. package/dist/cookie-consent/consent.d.ts +22 -0
  63. package/dist/cookie-consent/consent.d.ts.map +1 -0
  64. package/dist/cookie-consent/consent.js +93 -0
  65. package/dist/cookie-consent/consent.js.map +1 -0
  66. package/dist/index.d.ts +43 -1310
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +40 -1576
  69. package/dist/index.js.map +1 -1
  70. package/dist/internal/http.d.ts +5 -0
  71. package/dist/internal/http.d.ts.map +1 -0
  72. package/dist/internal/http.js +27 -0
  73. package/dist/internal/http.js.map +1 -0
  74. package/dist/orders/guest.d.ts +9 -0
  75. package/dist/orders/guest.d.ts.map +1 -0
  76. package/dist/orders/guest.js +30 -0
  77. package/dist/orders/guest.js.map +1 -0
  78. package/dist/orders/orders.d.ts +23 -0
  79. package/dist/orders/orders.d.ts.map +1 -0
  80. package/dist/orders/orders.js +33 -0
  81. package/dist/orders/orders.js.map +1 -0
  82. package/dist/seo/indexnow.d.ts +24 -0
  83. package/dist/seo/indexnow.d.ts.map +1 -0
  84. package/dist/seo/indexnow.js +50 -0
  85. package/dist/seo/indexnow.js.map +1 -0
  86. package/dist/seo/json-ld.d.ts +52 -0
  87. package/dist/seo/json-ld.d.ts.map +1 -0
  88. package/dist/seo/json-ld.js +175 -0
  89. package/dist/seo/json-ld.js.map +1 -0
  90. package/dist/seo/page-seo.d.ts +21 -0
  91. package/dist/seo/page-seo.d.ts.map +1 -0
  92. package/dist/seo/page-seo.js +68 -0
  93. package/dist/seo/page-seo.js.map +1 -0
  94. package/dist/seo/robots.d.ts +23 -0
  95. package/dist/seo/robots.d.ts.map +1 -0
  96. package/dist/seo/robots.js +35 -0
  97. package/dist/seo/robots.js.map +1 -0
  98. package/dist/seo/sitemap.d.ts +35 -0
  99. package/dist/seo/sitemap.d.ts.map +1 -0
  100. package/dist/seo/sitemap.js +131 -0
  101. package/dist/seo/sitemap.js.map +1 -0
  102. package/dist/server/process.d.ts +136 -0
  103. package/dist/server/process.d.ts.map +1 -0
  104. package/dist/server/process.js +143 -0
  105. package/dist/server/process.js.map +1 -0
  106. package/dist/types/address.d.ts +40 -0
  107. package/dist/types/address.d.ts.map +1 -0
  108. package/dist/types/address.js +2 -0
  109. package/dist/types/address.js.map +1 -0
  110. package/dist/types/analytics.d.ts +79 -0
  111. package/dist/types/analytics.d.ts.map +1 -0
  112. package/dist/types/analytics.js +2 -0
  113. package/dist/types/analytics.js.map +1 -0
  114. package/dist/types/business.d.ts +95 -0
  115. package/dist/types/business.d.ts.map +1 -0
  116. package/dist/types/business.js +2 -0
  117. package/dist/types/business.js.map +1 -0
  118. package/dist/types/buyer.d.ts +144 -0
  119. package/dist/types/buyer.d.ts.map +1 -0
  120. package/dist/types/buyer.js +45 -0
  121. package/dist/types/buyer.js.map +1 -0
  122. package/dist/types/campaign.d.ts +51 -0
  123. package/dist/types/campaign.d.ts.map +1 -0
  124. package/dist/types/campaign.js +2 -0
  125. package/dist/types/campaign.js.map +1 -0
  126. package/dist/types/cart.d.ts +40 -0
  127. package/dist/types/cart.d.ts.map +1 -0
  128. package/dist/types/cart.js +2 -0
  129. package/dist/types/cart.js.map +1 -0
  130. package/dist/types/catalog.d.ts +164 -0
  131. package/dist/types/catalog.d.ts.map +1 -0
  132. package/dist/types/catalog.js +2 -0
  133. package/dist/types/catalog.js.map +1 -0
  134. package/dist/types/cms.d.ts +196 -0
  135. package/dist/types/cms.d.ts.map +1 -0
  136. package/dist/types/cms.js +2 -0
  137. package/dist/types/cms.js.map +1 -0
  138. package/dist/types/cookie-consent.d.ts +18 -0
  139. package/dist/types/cookie-consent.d.ts.map +1 -0
  140. package/dist/types/cookie-consent.js +7 -0
  141. package/dist/types/cookie-consent.js.map +1 -0
  142. package/dist/types/guest-order.d.ts +16 -0
  143. package/dist/types/guest-order.d.ts.map +1 -0
  144. package/dist/types/guest-order.js +9 -0
  145. package/dist/types/guest-order.js.map +1 -0
  146. package/dist/types/listing.d.ts +14 -0
  147. package/dist/types/listing.d.ts.map +1 -0
  148. package/dist/types/listing.js +2 -0
  149. package/dist/types/listing.js.map +1 -0
  150. package/dist/types/order.d.ts +40 -0
  151. package/dist/types/order.d.ts.map +1 -0
  152. package/dist/types/order.js +2 -0
  153. package/dist/types/order.js.map +1 -0
  154. package/dist/types/seo.d.ts +93 -0
  155. package/dist/types/seo.d.ts.map +1 -0
  156. package/dist/types/seo.js +2 -0
  157. package/dist/types/seo.js.map +1 -0
  158. package/dist/types/server-env.d.ts +19 -0
  159. package/dist/types/server-env.d.ts.map +1 -0
  160. package/dist/types/server-env.js +10 -0
  161. package/dist/types/server-env.js.map +1 -0
  162. package/dist/types/wishlist.d.ts +10 -0
  163. package/dist/types/wishlist.d.ts.map +1 -0
  164. package/dist/types/wishlist.js +2 -0
  165. package/dist/types/wishlist.js.map +1 -0
  166. package/dist/wishlist/wishlist.d.ts +28 -0
  167. package/dist/wishlist/wishlist.d.ts.map +1 -0
  168. package/dist/wishlist/wishlist.js +42 -0
  169. package/dist/wishlist/wishlist.js.map +1 -0
  170. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,1577 +1,41 @@
1
- // ---------------------------------------------------------------------------
2
- // Proxima Storefront Core SDK
3
- // ---------------------------------------------------------------------------
4
- // Single-file client for the Proxima API. Import what you need:
5
- // import { loginBuyer, fetchRegistrationForm, addToWishlist } from '@proxima-io/storefront-core';
6
- // ---------------------------------------------------------------------------
7
- /**
8
- * Build fully resolved SEO metadata for a page.
9
- *
10
- * Data priority:
11
- * 1. Admin-set `PageSEO` fields in `composition.seo` (explicit overrides)
12
- * 2. Entity-derived data in `composition.seo.entity_name` / `entity_image` (auto-populated by API)
13
- * 3. Website-level defaults (`website.og_image_url`, etc.)
14
- * 4. Hard fallbacks (empty strings)
15
- *
16
- * @param seoData The `seo` object from `ProximaCompositionResponse` (may be null)
17
- * @param website Website-level SEO fields
18
- * @param locale Locale code for resolving localized strings (e.g. "es")
19
- * @param currentUrl Absolute URL of the current page — used as canonical fallback
20
- *
21
- * @example
22
- * const seo = buildPageSeo(composition.seo, website, website.locale, canonicalUrl);
23
- * // → pass to <SiteLayout seo={seo} />
24
- */
25
- export function buildPageSeo(seoData, website, locale, currentUrl) {
26
- /** Resolve a value that may be a localized dict `{ es: "...", en: "..." }` or a plain string */
27
- function resolveLocalized(value) {
28
- if (!value)
29
- return null;
30
- if (typeof value === "string")
31
- return value || null;
32
- if (typeof value === "object") {
33
- const map = value;
34
- return map[locale] ?? map["es"] ?? Object.values(map).find(Boolean) ?? null;
35
- }
36
- return null;
37
- }
38
- const entityName = seoData?.entity_name ?? null;
39
- const entityImage = seoData?.entity_image ?? null;
40
- // Title: admin-set > entity name + site name > site name
41
- const adminTitle = resolveLocalized(seoData?.meta_title);
42
- const title = adminTitle ?? (entityName ? `${entityName} | ${website.name}` : website.name);
43
- // Description: admin-set > entity-based fallback > site name
44
- const adminDescription = resolveLocalized(seoData?.meta_description);
45
- const description = adminDescription ??
46
- (entityName ? `${entityName} en ${website.name}` : website.name);
47
- // OG image: admin-set > entity image > website og_image_url
48
- const ogImage = seoData?.og_image ??
49
- entityImage ??
50
- (website.og_image_url ?? null);
51
- const ogType = seoData?.og_type ?? "website";
52
- const canonicalUrl = seoData?.canonical_url ?? currentUrl;
53
- const robots = seoData?.robots === "noindex"
54
- ? "noindex, nofollow"
55
- : "index, follow";
56
- const rawHandle = website.twitter_handle ?? null;
57
- const twitterSite = rawHandle ? `@${rawHandle.replace(/^@/, "")}` : null;
58
- return {
59
- title,
60
- description,
61
- ogTitle: title,
62
- ogDescription: description,
63
- ogImage,
64
- ogType,
65
- ogSiteName: website.name,
66
- canonicalUrl,
67
- robots,
68
- twitterCard: "summary_large_image",
69
- twitterSite,
70
- twitterImage: ogImage,
71
- faviconUrl: website.favicon_url ?? null,
72
- };
73
- }
74
- /**
75
- * Build a `WebSite` JSON-LD object.
76
- * Enables Google's Search Action box in SERPs.
77
- *
78
- * @example
79
- * <script type="application/ld+json" set:html={JSON.stringify(buildWebSiteJsonLd(website))} />
80
- */
81
- export function buildWebSiteJsonLd(website) {
82
- const siteUrl = `https://${website.domain}`;
83
- return {
84
- "@context": "https://schema.org",
85
- "@type": "WebSite",
86
- "name": website.name,
87
- "url": `${siteUrl}/`,
88
- "potentialAction": {
89
- "@type": "SearchAction",
90
- "target": {
91
- "@type": "EntryPoint",
92
- "urlTemplate": `${siteUrl}/buscar?q={search_term_string}`,
93
- },
94
- "query-input": "required name=search_term_string",
95
- },
96
- };
97
- }
98
- /**
99
- * Build an `Organization` JSON-LD object.
100
- * Returns `null` when `website.logo_url` is absent (Google ignores logo-less org markup).
101
- *
102
- * @example
103
- * {orgJsonLd && <script type="application/ld+json" set:html={JSON.stringify(orgJsonLd)} />}
104
- */
105
- export function buildOrganizationJsonLd(website) {
106
- if (!website.logo_url)
107
- return null;
108
- const siteUrl = `https://${website.domain}`;
109
- return {
110
- "@context": "https://schema.org",
111
- "@type": "Organization",
112
- "name": website.name,
113
- "url": `${siteUrl}/`,
114
- "logo": { "@type": "ImageObject", "url": website.logo_url },
115
- };
116
- }
117
- /**
118
- * Build a `Product` JSON-LD object for a product detail page.
119
- * Includes `Offer` with pricing, currency, and availability.
120
- *
121
- * @example
122
- * <script type="application/ld+json" set:html={JSON.stringify(buildProductJsonLd(product, website))} />
123
- */
124
- export function buildProductJsonLd(product, website) {
125
- const siteUrl = `https://${website.domain}`;
126
- const productUrl = `${siteUrl}/producto/${product.slug}`;
127
- // Deduplicated image list: primary first, then extras
128
- const images = [
129
- product.image,
130
- ...(product.images?.filter((img) => img && img !== product.image) ?? []),
131
- ].filter(Boolean);
132
- const result = {
133
- "@context": "https://schema.org",
134
- "@type": "Product",
135
- "name": product.name,
136
- "image": images.length === 1 ? images[0] : images,
137
- "offers": {
138
- "@type": "Offer",
139
- "url": productUrl,
140
- "price": product.priceRaw,
141
- "priceCurrency": website.currency,
142
- "availability": product.inStock === false
143
- ? "https://schema.org/OutOfStock"
144
- : "https://schema.org/InStock",
145
- },
146
- };
147
- if (product.description)
148
- result["description"] = product.description;
149
- if (product.sku)
150
- result["sku"] = product.sku;
151
- if (product.productId != null)
152
- result["identifier"] = String(product.productId);
153
- if (product.brand)
154
- result["brand"] = { "@type": "Brand", "name": product.brand };
155
- // Strikethrough price — only when compare-at is higher than current
156
- if (product.compareAtPrice && product.compareAtPrice > product.priceRaw) {
157
- result["offers"]["priceSpecification"] = {
158
- "@type": "PriceSpecification",
159
- "price": product.compareAtPrice,
160
- "priceCurrency": website.currency,
161
- };
162
- }
163
- return result;
164
- }
165
- /**
166
- * Build a `BreadcrumbList` JSON-LD object.
167
- *
168
- * @param items Array of breadcrumb steps. The last item typically has no `href`.
169
- * @param siteUrl Absolute site URL, e.g. `https://example.com`
170
- *
171
- * @example
172
- * const crumbs = buildBreadcrumbJsonLd(
173
- * [{ label: "Inicio", href: "/" }, { label: "Zapatos", href: "/categoria/zapatos" }, { label: "Nike Air Max" }],
174
- * `https://${website.domain}`
175
- * );
176
- * <script type="application/ld+json" set:html={JSON.stringify(crumbs)} />
177
- */
178
- export function buildBreadcrumbJsonLd(items, siteUrl) {
179
- return {
180
- "@context": "https://schema.org",
181
- "@type": "BreadcrumbList",
182
- "itemListElement": items.map((item, i) => ({
183
- "@type": "ListItem",
184
- "position": i + 1,
185
- "name": item.label,
186
- ...(item.href
187
- ? { "item": item.href.startsWith("http") ? item.href : `${siteUrl}${item.href}` }
188
- : {}),
189
- })),
190
- };
191
- }
192
- /**
193
- * Build a `LocalBusiness` JSON-LD object for brick-and-mortar stores.
194
- * Returns `null` when `seo` is falsy so callers can gate rendering easily.
195
- *
196
- * @example
197
- * const schema = buildLocalBusinessJsonLd(seo);
198
- * {schema && <script type="application/ld+json" set:html={JSON.stringify(schema)} />}
199
- */
200
- export function buildLocalBusinessJsonLd(seo) {
201
- if (!seo)
202
- return null;
203
- const result = {
204
- "@context": "https://schema.org",
205
- "@type": "LocalBusiness",
206
- "name": seo.name,
207
- "url": seo.url,
208
- "@id": `${seo.url}#localbusiness`,
209
- };
210
- if (seo.image)
211
- result["image"] = seo.image;
212
- if (seo.telephone)
213
- result["telephone"] = seo.telephone;
214
- const hasAddress = seo.street_address || seo.address_locality || seo.address_country;
215
- if (hasAddress) {
216
- const address = { "@type": "PostalAddress" };
217
- if (seo.street_address)
218
- address["streetAddress"] = seo.street_address;
219
- if (seo.address_locality)
220
- address["addressLocality"] = seo.address_locality;
221
- if (seo.address_region)
222
- address["addressRegion"] = seo.address_region;
223
- if (seo.postal_code)
224
- address["postalCode"] = seo.postal_code;
225
- if (seo.address_country)
226
- address["addressCountry"] = seo.address_country;
227
- result["address"] = address;
228
- }
229
- if (seo.latitude != null && seo.longitude != null) {
230
- result["geo"] = {
231
- "@type": "GeoCoordinates",
232
- "latitude": seo.latitude,
233
- "longitude": seo.longitude,
234
- };
235
- }
236
- if (seo.opening_hours?.length || seo.opens || seo.closes) {
237
- result["openingHoursSpecification"] = [{
238
- "@type": "OpeningHoursSpecification",
239
- ...(seo.opening_hours?.length ? { "dayOfWeek": seo.opening_hours } : {}),
240
- ...(seo.opens ? { "opens": seo.opens } : {}),
241
- ...(seo.closes ? { "closes": seo.closes } : {}),
242
- }];
243
- }
244
- if (seo.social_links?.length)
245
- result["sameAs"] = seo.social_links;
246
- return result;
247
- }
248
- /** Private resolver kinds that must never appear in a sitemap */
249
- const SITEMAP_PRIVATE_KINDS = new Set([
250
- "cart",
251
- "checkout",
252
- "buyer_login",
253
- "buyer_account",
254
- "buyer_register",
255
- "buyer_password_reset",
256
- "order_list",
257
- "order_detail",
258
- "product_compare",
259
- ]);
260
- function _xmlEscape(str) {
261
- return str
262
- .replace(/&/g, "&amp;")
263
- .replace(/</g, "&lt;")
264
- .replace(/>/g, "&gt;")
265
- .replace(/"/g, "&quot;")
266
- .replace(/'/g, "&apos;");
267
- }
268
- function _urlEntry(loc, priority = "0.7", changefreq = "weekly", lastmod) {
269
- const lines = [
270
- ` <url>`,
271
- ` <loc>${_xmlEscape(loc)}</loc>`,
272
- ` <changefreq>${changefreq}</changefreq>`,
273
- ` <priority>${priority}</priority>`,
274
- ];
275
- if (lastmod)
276
- lines.push(` <lastmod>${lastmod}</lastmod>`);
277
- lines.push(` </url>`);
278
- return lines.join("\n");
279
- }
280
- /**
281
- * Generate a complete `sitemap.xml` for a storefront.
282
- *
283
- * Includes:
284
- * 1. Content pages from the website manifest (priority 1.0 for home, 0.8 for others)
285
- * 2. Category pages from the recursive nav tree (priority 0.8)
286
- * 3. Brand pages from the brands directory (priority 0.7)
287
- * 4. Product pages — paginated up to `maxProducts` (priority 0.9)
288
- *
289
- * All entries use today's date as `lastmod`.
290
- *
291
- * @param website Resolved website object (domain + pages array)
292
- * @param apiUrl Base URL of the Proxima API (e.g. `http://localhost:8000`)
293
- * @param options Optional overrides: pageSize (default 60), maxProducts (default 3000)
294
- *
295
- * @example
296
- * // apps/{slug}/src/pages/sitemap.xml.ts
297
- * import type { APIRoute } from "astro";
298
- * import { resolveWebsiteOnly } from "@/lib/resolver";
299
- * import { generateSitemapXml } from "@proxima-io/storefront-core";
300
- *
301
- * export const GET: APIRoute = async () => {
302
- * const website = await resolveWebsiteOnly();
303
- * const xml = await generateSitemapXml(website, import.meta.env.PROXIMA_API_URL ?? "http://localhost:8000");
304
- * return new Response(xml, {
305
- * headers: { "Content-Type": "application/xml; charset=utf-8", "Cache-Control": "public, s-maxage=3600, stale-while-revalidate=86400" },
306
- * });
307
- * };
308
- */
309
- export async function generateSitemapXml(website, apiUrl, options = {}) {
310
- const PAGE_SIZE = Math.min(60, options.pageSize ?? 60);
311
- const MAX_PRODUCTS = options.maxProducts ?? 3000;
312
- const MAX_PAGES = Math.ceil(MAX_PRODUCTS / PAGE_SIZE);
313
- const TODAY = new Date().toISOString().split("T")[0];
314
- const siteUrl = `https://${website.domain}`;
315
- const entries = [];
316
- // 1. Content pages from the website manifest
317
- for (const page of website.pages ?? []) {
318
- if (SITEMAP_PRIVATE_KINDS.has(page.resolver_kind))
319
- continue;
320
- if (page.resolver_kind === "content_page" && page.path) {
321
- const priority = page.path === "/" ? "1.0" : "0.8";
322
- const changefreq = page.path === "/" ? "daily" : "weekly";
323
- entries.push(_urlEntry(`${siteUrl}${page.path}`, priority, changefreq, TODAY));
324
- }
325
- }
326
- // 2. Category pages (recursive nav tree)
327
- try {
328
- // Lazy import to avoid circular dependency — fetchCategoryNavTree is defined below
329
- const tree = await fetchCategoryNavTree({ baseUrl: apiUrl }, website);
330
- function collectHrefs(nodes) {
331
- for (const node of nodes) {
332
- entries.push(_urlEntry(`${siteUrl}${node.href}`, "0.8", "daily", TODAY));
333
- if (node.children.length > 0)
334
- collectHrefs(node.children);
335
- }
336
- }
337
- collectHrefs(tree.nodes);
338
- }
339
- catch {
340
- /* API offline — skip category URLs */
341
- }
342
- // 3. Brand pages
343
- try {
344
- const brandsResult = await fetchBrandsDirectory({ baseUrl: apiUrl }, website);
345
- for (const brand of brandsResult.items) {
346
- entries.push(_urlEntry(`${siteUrl}${brand.href}`, "0.7", "weekly", TODAY));
347
- }
348
- }
349
- catch {
350
- /* API offline — skip brand URLs */
351
- }
352
- // 4. Product pages (paginated)
353
- try {
354
- let currentPage = 1;
355
- let totalPages = 1;
356
- while (currentPage <= totalPages && currentPage <= MAX_PAGES) {
357
- const result = await fetchStorefrontProducts({ baseUrl: apiUrl }, website, {
358
- page: currentPage,
359
- pageSize: PAGE_SIZE,
360
- });
361
- for (const product of result.items) {
362
- entries.push(_urlEntry(`${siteUrl}/producto/${product.slug}`, "0.9", "weekly", TODAY));
363
- }
364
- totalPages = result.pagination.total_pages;
365
- currentPage++;
366
- }
367
- }
368
- catch {
369
- /* API offline — skip product URLs */
370
- }
371
- return [
372
- '<?xml version="1.0" encoding="UTF-8"?>',
373
- '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
374
- ...entries,
375
- "</urlset>",
376
- ].join("\n");
377
- }
378
- /**
379
- * Generate a `robots.txt` for a storefront.
380
- *
381
- * Blocks all private buyer routes and API paths.
382
- * Adds a `Sitemap:` directive pointing to `{siteUrl}/sitemap.xml`.
383
- *
384
- * @example
385
- * // apps/{slug}/src/pages/robots.txt.ts
386
- * import type { APIRoute } from "astro";
387
- * import { resolveWebsiteOnly } from "@/lib/resolver";
388
- * import { generateRobotsTxt } from "@proxima-io/storefront-core";
389
- *
390
- * export const GET: APIRoute = async () => {
391
- * const website = await resolveWebsiteOnly();
392
- * return new Response(generateRobotsTxt(website), {
393
- * headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "public, max-age=86400" },
394
- * });
395
- * };
396
- */
397
- export function generateRobotsTxt(website) {
398
- const siteUrl = `https://${website.domain}`;
399
- return [
400
- "User-agent: *",
401
- "Allow: /",
402
- "",
403
- "Disallow: /cuenta",
404
- "Disallow: /carrito",
405
- "Disallow: /checkout",
406
- "Disallow: /api/",
407
- "Disallow: /dev/",
408
- "",
409
- `Sitemap: ${siteUrl}/sitemap.xml`,
410
- ].join("\n");
411
- }
412
- // ---------------------------------------------------------------------------
413
- // IndexNow — real-time URL change notifications
414
- // ---------------------------------------------------------------------------
415
- /**
416
- * Submit a list of changed URLs to the IndexNow API.
417
- *
418
- * IndexNow notifies Bing, Yandex, and (increasingly) Google about new or
419
- * updated pages so they are re-crawled within seconds instead of waiting for
420
- * the next sitemap crawl.
421
- *
422
- * **Setup (one-time per storefront):**
423
- * 1. Generate or reuse the key from `PROXIMA_INDEXNOW_KEY` env var.
424
- * 2. Serve `GET /{key}.txt` returning the key as plain text — see the scaffold
425
- * template at `src/pages/[indexnow_key].txt.ts`.
426
- * 3. That's it — IndexNow verifies the key automatically on first submission.
427
- *
428
- * @param apiKey Your IndexNow key (platform-level; served at `/{apiKey}.txt`)
429
- * @param siteUrl Absolute origin of the site, e.g. `https://example.com`
430
- * @param urls Absolute URLs that changed (max 10 000 per call)
431
- *
432
- * @example
433
- * await notifyIndexNow("my-secret-key", "https://214store.com", [
434
- * "https://214store.com/producto/laptop-gamer",
435
- * ]);
436
- */
437
- export async function notifyIndexNow(apiKey, siteUrl, urls) {
438
- if (!apiKey || urls.length === 0)
439
- return;
440
- const host = new URL(siteUrl).hostname;
441
- const payload = {
442
- host,
443
- key: apiKey,
444
- keyLocation: `${siteUrl}/${apiKey}.txt`,
445
- urlList: urls,
446
- };
447
- try {
448
- const resp = await fetch("https://api.indexnow.org/indexnow", {
449
- method: "POST",
450
- headers: { "Content-Type": "application/json; charset=utf-8" },
451
- body: JSON.stringify(payload),
452
- });
453
- if (!resp.ok && resp.status !== 202) {
454
- console.warn(`[IndexNow] Submission returned ${resp.status} for ${host}`);
455
- }
456
- }
457
- catch (err) {
458
- console.warn(`[IndexNow] Submission failed for ${host}:`, err);
459
- }
460
- }
461
- /** List all websites for a service-key authenticated caller. Useful for build-time scripts. */
462
- export async function fetchProximaWebsiteList(config) {
463
- const url = new URL("/api/v1/storefront/cms/websites", config.baseUrl);
464
- const headers = {};
465
- if (config.serviceKey)
466
- headers["Authorization"] = `Bearer ${config.serviceKey}`;
467
- const response = await fetch(url, { headers });
468
- if (!response.ok)
469
- throw new Error(`Website list failed: ${response.status}`);
470
- return response.json();
471
- }
472
- /**
473
- * Resolve a website by domain/host. Call this once per request and cache the result.
474
- * Pass `host` if the incoming request host differs from `config.domain` (e.g. in middleware).
475
- *
476
- * @example
477
- * const website = await fetchProximaWebsite({ baseUrl: env.apiUrl, domain: Astro.url.hostname });
478
- */
479
- export async function fetchProximaWebsite(config) {
480
- const url = new URL("/api/v1/storefront/cms/websites/resolve", config.baseUrl);
481
- url.searchParams.set("host", config.host || config.domain);
482
- const headers = {};
483
- if (config.serviceKey)
484
- headers["Authorization"] = `Bearer ${config.serviceKey}`;
485
- const response = await fetch(url, { headers });
486
- if (!response.ok)
487
- throw new Error(`Website resolve failed: ${response.status}`);
488
- return response.json();
489
- }
490
- /**
491
- * Build a synthetic `ProximaWebsiteResponse` for the visual builder preview.
492
- * Use this when the builder passes `websiteId` and `businessId` via query params
493
- * instead of resolving by domain.
494
- */
495
- export function makeBuilderPreviewWebsite(config) {
496
- if (!config.websiteId || !config.businessId) {
497
- throw new Error("Builder preview requires websiteId and businessId");
498
- }
499
- return {
500
- id: config.websiteId,
501
- business_id: config.businessId,
502
- name: "Builder preview",
503
- domain: config.domain,
504
- delivery_mode: "custom_managed",
505
- website_kind: "ecommerce",
506
- template_key: null,
507
- code_profile: null,
508
- locale: "es",
509
- currency: "PEN",
510
- capabilities: {},
511
- theme_tokens: {},
512
- animation_config: {},
513
- pages: [],
514
- };
515
- }
516
- /**
517
- * Fetch the fully resolved page composition for the given path.
518
- * This is the main data-fetching call for every SSR page render.
519
- * The response embeds all section data (via smart collections) and, for detail pages,
520
- * the primary entity in `resolved_data` (product, category, brand, blog post).
521
- * No additional catalog API calls are needed for the initial render.
522
- *
523
- * @example
524
- * // src/pages/[...path].astro
525
- * const composition = await fetchProximaComposition(
526
- * { ...config, path: Astro.url.pathname },
527
- * website
528
- * );
529
- */
530
- export async function fetchProximaComposition(config, website) {
531
- const locale = website.locale ?? "es";
532
- const currency = website.currency ?? "PEN";
533
- const url = new URL(`/api/v1/storefront/cms/websites/${website.id}/pages/composition`, config.baseUrl);
534
- url.searchParams.set("path", config.path);
535
- url.searchParams.set("locale", locale);
536
- url.searchParams.set("business_id", website.business_id);
537
- if (config.variantId)
538
- url.searchParams.set("variant_id", config.variantId);
539
- if (config.previewToken)
540
- url.searchParams.set("preview_token", config.previewToken);
541
- const headers = {
542
- "X-Business-ID": website.business_id,
543
- "Accept-Language": locale,
544
- "X-Currency": currency,
545
- };
546
- if (config.serviceKey)
547
- headers["Authorization"] = `Bearer ${config.serviceKey}`;
548
- const response = await fetch(url, { headers });
549
- if (!response.ok)
550
- throw new Error(`Composition failed: ${response.status}`);
551
- return response.json();
552
- }
553
- /**
554
- * @deprecated Use `fetchStorefrontProducts` instead. This function calls the raw
555
- * catalog endpoint (`/api/v1/products`) which is not enriched for storefront use
556
- * (no price_formatted, no badge, no default_variant_id). It will be removed in a future version.
557
- */
558
- export async function fetchProximaProducts(config, website) {
559
- const url = new URL("/api/v1/products", config.baseUrl);
560
- url.searchParams.set("size", "12");
561
- const headers = {
562
- "X-Business-ID": website.business_id,
563
- "Accept-Language": website.locale ?? "es",
564
- "X-Currency": website.currency ?? "PEN",
565
- };
566
- if (config.serviceKey)
567
- headers["Authorization"] = `Bearer ${config.serviceKey}`;
568
- const response = await fetch(url, { headers });
569
- if (!response.ok)
570
- throw new Error(`Products failed: ${response.status}`);
571
- return response.json();
572
- }
573
- // ---------------------------------------------------------------------------
574
- // Error constants
575
- // ---------------------------------------------------------------------------
576
- /** Well-known error detail strings returned by the API. Use these for comparison
577
- * instead of hardcoding strings in your storefront.
578
- *
579
- * @example
580
- * try { await resetPassword(...) } catch (e: any) {
581
- * if (e.data?.detail === BUYER_AUTH_ERRORS.RESET_TOKEN_INVALID) { ... }
582
- * }
583
- */
584
- export const BUYER_AUTH_ERRORS = {
585
- RESET_TOKEN_INVALID: "RESET_TOKEN_INVALID",
586
- VERIFY_TOKEN_INVALID: "VERIFY_TOKEN_INVALID",
587
- EMAIL_ALREADY_VERIFIED: "EMAIL_ALREADY_VERIFIED",
588
- EMAIL_TAKEN: "Email already registered in this store",
589
- MISSING_REQUIRED_FIELDS: "MISSING_REQUIRED_FIELDS",
590
- };
591
- /**
592
- * Thrown when the API returns 422 MISSING_REQUIRED_FIELDS.
593
- * Use `error.missingFields` to mark exactly which fields are invalid in the UI.
594
- *
595
- * @example
596
- * try {
597
- * await registerBuyer(config, website, params);
598
- * } catch (e) {
599
- * if (e instanceof MissingFieldsError) {
600
- * for (const { field } of e.missingFields) markFieldError(field);
601
- * }
602
- * }
603
- */
604
- export class MissingFieldsError extends Error {
605
- status = 422;
606
- missingFields;
607
- constructor(missingFields) {
608
- super("MISSING_REQUIRED_FIELDS");
609
- this.name = "MissingFieldsError";
610
- this.missingFields = missingFields;
611
- }
612
- }
613
- /** Recommended options for the buyer session cookie. Apply to both `buyer_token` and `buyer_refresh_token`. */
614
- export const BUYER_COOKIE_OPTIONS = {
615
- path: "/",
616
- httpOnly: true,
617
- sameSite: "lax",
618
- maxAge: 60 * 60 * 24 * 7,
619
- };
620
- export const BUYER_COOKIE_NAME = "buyer_token";
621
- export const BUYER_REFRESH_COOKIE_NAME = "buyer_refresh_token";
622
- // ---------------------------------------------------------------------------
623
- // Internal helpers
624
- // ---------------------------------------------------------------------------
625
- function apiError(status, data) {
626
- return Object.assign(new Error(`Request failed: ${status}`), { status, data });
627
- }
628
- function authHeaders(businessId, token) {
629
- const h = { "X-Business-ID": businessId };
630
- if (token)
631
- h["Authorization"] = `Bearer ${token}`;
632
- return h;
633
- }
634
- // ---------------------------------------------------------------------------
635
- // Registration Form
636
- // ---------------------------------------------------------------------------
637
- /**
638
- * Fetch the merchant-configured registration form schema.
639
- * Call this server-side in your /register page to know which fields to render
640
- * and which are required. `email` and `password` are always prepended by the API.
641
- *
642
- * @example
643
- * // src/pages/register.astro
644
- * const form = await fetchRegistrationForm({ baseUrl: env.apiUrl }, website);
645
- * // Pass `form` as a prop to a client component that renders the dynamic form
646
- */
647
- export async function fetchRegistrationForm(config, website) {
648
- const url = new URL("/api/v1/store/auth/registration-form", config.baseUrl);
649
- const res = await fetch(url, {
650
- headers: { "X-Business-ID": website.business_id },
651
- });
652
- if (!res.ok) {
653
- throw apiError(res.status, await res.json().catch(() => ({})));
654
- }
655
- return res.json();
656
- }
657
- // ---------------------------------------------------------------------------
658
- // Buyer Auth — client-side functions
659
- // ---------------------------------------------------------------------------
660
- /**
661
- * Register a new customer. The merchant decides which fields are required —
662
- * call fetchRegistrationForm() first to know what to collect.
663
- *
664
- * Throws MissingFieldsError when the merchant marked fields as required but
665
- * they were omitted, so you can mark exactly which inputs are invalid.
666
- *
667
- * @example
668
- * try {
669
- * const session = await registerBuyer(config, website, { email, password, fullName });
670
- * } catch (e) {
671
- * if (e instanceof MissingFieldsError) {
672
- * e.missingFields.forEach(({ field }) => markError(field));
673
- * }
674
- * }
675
- */
676
- export async function registerBuyer(config, website, params) {
677
- const url = new URL("/api/v1/store/auth/register", config.baseUrl);
678
- const body = {
679
- email: params.email,
680
- password: params.password,
681
- };
682
- if (params.fullName !== undefined)
683
- body.full_name = params.fullName;
684
- if (params.phone !== undefined)
685
- body.phone = params.phone;
686
- if (params.docType !== undefined)
687
- body.doc_type = params.docType;
688
- if (params.docNumber !== undefined)
689
- body.doc_number = params.docNumber;
690
- if (params.birthDate !== undefined)
691
- body.birth_date = params.birthDate;
692
- if (params.newsletterSubscribed !== undefined)
693
- body.newsletter_subscribed = params.newsletterSubscribed;
694
- if (params.registrationSource !== undefined)
695
- body.registration_source = params.registrationSource;
696
- if (params.metadata !== undefined)
697
- body.metadata = params.metadata;
698
- if (params.address !== undefined)
699
- body.address = params.address;
700
- if (params.captchaToken)
701
- body.captcha_token = params.captchaToken;
702
- const res = await fetch(url, {
703
- method: "POST",
704
- headers: { "Content-Type": "application/json", "X-Business-ID": website.business_id },
705
- body: JSON.stringify(body),
706
- });
707
- if (!res.ok) {
708
- const data = await res.json().catch(() => ({}));
709
- // Parse MISSING_REQUIRED_FIELDS into structured MissingFieldsError
710
- if (res.status === 422 && typeof data.detail === "string" && data.detail.startsWith("MISSING_REQUIRED_FIELDS:")) {
711
- try {
712
- const raw = data.detail.replace("MISSING_REQUIRED_FIELDS:", "").trim();
713
- // API returns Python repr: [{'field': 'phone', 'msg': 'FIELD_REQUIRED'}, ...]
714
- // Safe to JSON.parse after normalizing Python single-quotes
715
- const normalized = raw.replace(/'/g, '"');
716
- const missingFields = JSON.parse(normalized);
717
- throw new MissingFieldsError(missingFields);
718
- }
719
- catch (e) {
720
- if (e instanceof MissingFieldsError)
721
- throw e;
722
- // If parsing failed, fall through to generic error
723
- }
724
- }
725
- throw apiError(res.status, data);
726
- }
727
- return res.json();
728
- }
729
- /**
730
- * Authenticate a customer with email and password.
731
- * Returns a BuyerSession with access_token and refresh_token.
732
- * Throws { status: 401 } on wrong credentials.
733
- */
734
- export async function loginBuyer(config, website, params) {
735
- const url = new URL("/api/v1/store/auth/login", config.baseUrl);
736
- const body = { email: params.email, password: params.password };
737
- if (params.captchaToken)
738
- body.captcha_token = params.captchaToken;
739
- const res = await fetch(url, {
740
- method: "POST",
741
- headers: { "Content-Type": "application/json", "X-Business-ID": website.business_id },
742
- body: JSON.stringify(body),
743
- });
744
- if (!res.ok)
745
- throw apiError(res.status, await res.json().catch(() => ({})));
746
- return res.json();
747
- }
748
- /** Invalidate the current session server-side. Best-effort — always clear the cookie too. */
749
- export async function logoutBuyer(config, website, params) {
750
- const url = new URL("/api/v1/store/auth/logout", config.baseUrl);
751
- await fetch(url, {
752
- method: "POST",
753
- headers: {
754
- "Authorization": `Bearer ${params.token}`,
755
- "X-Business-ID": website.business_id,
756
- },
757
- });
758
- }
759
- /**
760
- * Exchange a refresh token for a new access token.
761
- * Call this when you get a 401 on any authenticated request.
762
- * Throws { status: 401 } if the refresh token is expired or revoked.
763
- *
764
- * @example
765
- * // In Astro middleware:
766
- * try {
767
- * const session = await refreshBuyerToken(config, website, { refreshToken });
768
- * // Set new access_token cookie
769
- * } catch {
770
- * // Clear both cookies, redirect to login
771
- * }
772
- */
773
- export async function refreshBuyerToken(config, website, params) {
774
- const url = new URL("/api/v1/store/auth/refresh", config.baseUrl);
775
- url.searchParams.set("refresh_token", params.refreshToken);
776
- const res = await fetch(url, {
777
- method: "POST",
778
- headers: { "X-Business-ID": website.business_id },
779
- });
780
- if (!res.ok)
781
- throw apiError(res.status, await res.json().catch(() => ({})));
782
- return res.json();
783
- }
784
- /** Fetch the authenticated customer's full profile. */
785
- export async function fetchBuyerProfile(config, website, params) {
786
- const url = new URL("/api/v1/store/me", config.baseUrl);
787
- const res = await fetch(url, {
788
- headers: authHeaders(website.business_id, params.token),
789
- });
790
- if (!res.ok)
791
- throw apiError(res.status, await res.json().catch(() => ({})));
792
- return res.json();
793
- }
794
- /**
795
- * Update the authenticated customer's profile. Only the fields you include
796
- * in `params` are changed — it's a partial update.
797
- */
798
- export async function updateBuyerProfile(config, website, params) {
799
- const url = new URL("/api/v1/store/me/profile", config.baseUrl);
800
- const body = {};
801
- if (params.fullName !== undefined)
802
- body.full_name = params.fullName;
803
- if (params.phone !== undefined)
804
- body.phone = params.phone;
805
- if (params.docType !== undefined)
806
- body.doc_type = params.docType;
807
- if (params.docNumber !== undefined)
808
- body.doc_number = params.docNumber;
809
- if (params.birthDate !== undefined)
810
- body.birth_date = params.birthDate;
811
- if (params.newsletterSubscribed !== undefined)
812
- body.newsletter_subscribed = params.newsletterSubscribed;
813
- if (params.avatarUrl !== undefined)
814
- body.avatar_url = params.avatarUrl;
815
- if (params.password !== undefined)
816
- body.password = params.password;
817
- const res = await fetch(url, {
818
- method: "PATCH",
819
- headers: { "Content-Type": "application/json", ...authHeaders(website.business_id, params.token) },
820
- body: JSON.stringify(body),
821
- });
822
- if (!res.ok)
823
- throw apiError(res.status, await res.json().catch(() => ({})));
824
- return res.json();
825
- }
826
- // ---------------------------------------------------------------------------
827
- // Password recovery & email verification
828
- // ---------------------------------------------------------------------------
829
- /**
830
- * Send a password reset email. Always resolves — even if the email doesn't exist.
831
- * Always show: "Si el email existe, recibirás un enlace para restablecer tu contraseña."
832
- */
833
- export async function forgotPassword(config, website, params) {
834
- const url = new URL("/api/v1/store/auth/forgot-password", config.baseUrl);
835
- const body = { email: params.email };
836
- if (params.captchaToken)
837
- body.captcha_token = params.captchaToken;
838
- await fetch(url, {
839
- method: "POST",
840
- headers: { "Content-Type": "application/json", "X-Business-ID": website.business_id },
841
- body: JSON.stringify(body),
842
- });
843
- }
844
- /**
845
- * Reset the customer's password using the token from the reset email link.
846
- * The token is the `?token=` query param in the reset URL.
847
- * On success, all active sessions are revoked — redirect to login.
848
- * Throws { status: 400, data.detail: BUYER_AUTH_ERRORS.RESET_TOKEN_INVALID } on bad token.
849
- */
850
- export async function resetPassword(config, params) {
851
- const url = new URL("/api/v1/store/auth/reset-password", config.baseUrl);
852
- const res = await fetch(url, {
853
- method: "POST",
854
- headers: { "Content-Type": "application/json" },
855
- body: JSON.stringify({ token: params.token, new_password: params.newPassword }),
856
- });
857
- if (!res.ok)
858
- throw apiError(res.status, await res.json().catch(() => ({})));
859
- }
860
- /**
861
- * Verify the customer's email using the token from the verification email link.
862
- * Throws { status: 400, data.detail: BUYER_AUTH_ERRORS.VERIFY_TOKEN_INVALID } on bad token.
863
- */
864
- export async function verifyEmail(config, params) {
865
- const url = new URL("/api/v1/store/auth/verify-email", config.baseUrl);
866
- const res = await fetch(url, {
867
- method: "POST",
868
- headers: { "Content-Type": "application/json" },
869
- body: JSON.stringify({ token: params.token }),
870
- });
871
- if (!res.ok)
872
- throw apiError(res.status, await res.json().catch(() => ({})));
873
- }
874
- /**
875
- * Re-send the email verification link. Requires the customer to be authenticated.
876
- * Throws { status: 400, data.detail: BUYER_AUTH_ERRORS.EMAIL_ALREADY_VERIFIED } if already verified.
877
- */
878
- export async function resendVerification(config, website, params) {
879
- const url = new URL("/api/v1/store/auth/resend-verification", config.baseUrl);
880
- const res = await fetch(url, {
881
- method: "POST",
882
- headers: authHeaders(website.business_id, params.token),
883
- });
884
- if (!res.ok)
885
- throw apiError(res.status, await res.json().catch(() => ({})));
886
- }
887
- // ---------------------------------------------------------------------------
888
- // Storefront catalog — search & listings (client-side interactions)
889
- // ---------------------------------------------------------------------------
890
- // Internal helper: builds the Accept-Language + X-Currency + X-Business-ID headers
891
- function storefrontHeaders(businessId, locale, currency) {
892
- const h = { "X-Business-ID": businessId };
893
- if (locale)
894
- h["Accept-Language"] = locale;
895
- if (currency)
896
- h["X-Currency"] = currency;
897
- return h;
898
- }
899
- /**
900
- * Search products by query string. Use this for the search bar, autocomplete,
901
- * and search results pages — `resolved_data` for a `search` page is intentionally
902
- * null; query results must be fetched directly.
903
- *
904
- * @example
905
- * const results = await searchStorefront(config, website, { q: 'zapatillas', limit: 10 });
906
- */
907
- export async function searchStorefront(config, website, params) {
908
- const url = new URL("/api/v1/storefront/search", config.baseUrl);
909
- url.searchParams.set("q", params.q);
910
- if (params.limit !== undefined)
911
- url.searchParams.set("limit", String(params.limit));
912
- const res = await fetch(url, {
913
- headers: storefrontHeaders(website.business_id, params.locale ?? website.locale, params.currency ?? website.currency),
914
- });
915
- if (!res.ok)
916
- throw apiError(res.status, await res.json().catch(() => ({})));
917
- return res.json();
918
- }
919
- /**
920
- * Fetch the general product listing with optional filters and pagination.
921
- * Use this for client-side filter/sort/paginate interactions on the all-products page.
922
- * The initial page render is handled by the composition — call this for subsequent
923
- * filter changes and page turns.
924
- */
925
- export async function fetchStorefrontProducts(config, website, params = {}) {
926
- const url = new URL("/api/v1/storefront/products", config.baseUrl);
927
- if (params.page)
928
- url.searchParams.set("page", String(params.page));
929
- if (params.pageSize)
930
- url.searchParams.set("page_size", String(params.pageSize));
931
- if (params.sort)
932
- url.searchParams.set("sort", params.sort);
933
- if (params.brand)
934
- url.searchParams.set("brand", params.brand);
935
- if (params.category)
936
- url.searchParams.set("category", params.category);
937
- if (params.q)
938
- url.searchParams.set("q", params.q);
939
- const res = await fetch(url, {
940
- headers: storefrontHeaders(website.business_id, params.locale ?? website.locale, params.currency ?? website.currency),
941
- });
942
- if (!res.ok)
943
- throw apiError(res.status, await res.json().catch(() => ({})));
944
- return res.json();
945
- }
946
- /**
947
- * Fetch paginated + filtered products for a category page (CLP).
948
- * Use this for client-side filter/sort/paginate after the initial SSR render
949
- * (which arrives in `resolved_data` from `fetchProximaComposition`).
950
- */
951
- export async function fetchCategoryProducts(config, website, params) {
952
- const url = new URL(`/api/v1/storefront/categories/${encodeURIComponent(params.slug)}/products`, config.baseUrl);
953
- if (params.page)
954
- url.searchParams.set("page", String(params.page));
955
- if (params.pageSize)
956
- url.searchParams.set("page_size", String(params.pageSize));
957
- if (params.sort)
958
- url.searchParams.set("sort", params.sort);
959
- if (params.brand)
960
- url.searchParams.set("brand", params.brand);
961
- if (params.q)
962
- url.searchParams.set("q", params.q);
963
- const res = await fetch(url, {
964
- headers: storefrontHeaders(website.business_id, params.locale ?? website.locale, params.currency ?? website.currency),
965
- });
966
- if (!res.ok)
967
- throw apiError(res.status, await res.json().catch(() => ({})));
968
- return res.json();
969
- }
970
- /**
971
- * Fetch paginated + filtered products for a brand page (BLP).
972
- * Use this for client-side filter/sort/paginate after the initial SSR render
973
- * (which arrives in `resolved_data` from `fetchProximaComposition`).
974
- */
975
- export async function fetchBrandProducts(config, website, params) {
976
- const url = new URL(`/api/v1/storefront/brands/${encodeURIComponent(params.slug)}/products`, config.baseUrl);
977
- if (params.page)
978
- url.searchParams.set("page", String(params.page));
979
- if (params.pageSize)
980
- url.searchParams.set("page_size", String(params.pageSize));
981
- if (params.sort)
982
- url.searchParams.set("sort", params.sort);
983
- if (params.category)
984
- url.searchParams.set("category", params.category);
985
- if (params.q)
986
- url.searchParams.set("q", params.q);
987
- const res = await fetch(url, {
988
- headers: storefrontHeaders(website.business_id, params.locale ?? website.locale, params.currency ?? website.currency),
989
- });
990
- if (!res.ok)
991
- throw apiError(res.status, await res.json().catch(() => ({})));
992
- return res.json();
993
- }
994
- /**
995
- * Fetch the full category directory (all categories with product counts).
996
- * Useful for navigation menus and sitemap generation.
997
- * For section-level category carousels, use smart collections via composition instead.
998
- */
999
- export async function fetchCategoriesDirectory(config, website, params = {}) {
1000
- const url = new URL("/api/v1/storefront/categories", config.baseUrl);
1001
- const res = await fetch(url, {
1002
- headers: storefrontHeaders(website.business_id, params.locale ?? website.locale),
1003
- });
1004
- if (!res.ok)
1005
- throw apiError(res.status, await res.json().catch(() => ({})));
1006
- return res.json();
1007
- }
1008
- /**
1009
- * Fetch the full category hierarchy as a recursive tree.
1010
- * Use this for data-driven mega menus — it returns nested `children[]`
1011
- * with `/categoria/{slug}` hrefs ready to render.
1012
- *
1013
- * @param maxDepth - Maximum tree depth (1–5, default 3)
1014
- */
1015
- export async function fetchCategoryNavTree(config, website, params = {}) {
1016
- const url = new URL("/api/v1/storefront/categories/tree", config.baseUrl);
1017
- if (params.maxDepth !== undefined)
1018
- url.searchParams.set("max_depth", String(params.maxDepth));
1019
- const res = await fetch(url, {
1020
- headers: storefrontHeaders(website.business_id, params.locale ?? website.locale),
1021
- });
1022
- if (!res.ok)
1023
- throw apiError(res.status, await res.json().catch(() => ({})));
1024
- return res.json();
1025
- }
1026
- /**
1027
- * Fetch the full brand directory (all brands with product counts).
1028
- * Useful for navigation menus and sitemap generation.
1029
- */
1030
- export async function fetchBrandsDirectory(config, website, params = {}) {
1031
- const url = new URL("/api/v1/storefront/brands", config.baseUrl);
1032
- const res = await fetch(url, {
1033
- headers: storefrontHeaders(website.business_id, params.locale ?? website.locale),
1034
- });
1035
- if (!res.ok)
1036
- throw apiError(res.status, await res.json().catch(() => ({})));
1037
- return res.json();
1038
- }
1039
- // ---------------------------------------------------------------------------
1040
- // Cart
1041
- // ---------------------------------------------------------------------------
1042
- function cartHeaders(businessId, token, sessionId) {
1043
- const h = { "X-Business-ID": businessId };
1044
- if (token)
1045
- h["Authorization"] = `Bearer ${token}`;
1046
- if (sessionId && !token)
1047
- h["X-Session-ID"] = sessionId;
1048
- return h;
1049
- }
1050
- /** Fetch the current cart. Works for both guest sessions and authenticated customers. */
1051
- export async function fetchCart(config, website, params) {
1052
- const url = new URL("/api/v1/cart", config.baseUrl);
1053
- const res = await fetch(url, { headers: cartHeaders(website.business_id, params.token, params.sessionId) });
1054
- if (!res.ok)
1055
- throw apiError(res.status, await res.json().catch(() => ({})));
1056
- return res.json();
1057
- }
1058
- /**
1059
- * Add a product variant to the cart. Use `StorefrontProductSummary.default_variant_id`
1060
- * as `variantId` when adding from a listing card.
1061
- */
1062
- export async function addToCart(config, website, params) {
1063
- const url = new URL("/api/v1/cart/items", config.baseUrl);
1064
- const res = await fetch(url, {
1065
- method: "POST",
1066
- headers: { "Content-Type": "application/json", ...cartHeaders(website.business_id, params.token, params.sessionId) },
1067
- body: JSON.stringify({ product_variant_id: params.variantId, quantity: params.quantity }),
1068
- });
1069
- if (!res.ok)
1070
- throw apiError(res.status, await res.json().catch(() => ({})));
1071
- return res.json();
1072
- }
1073
- /** Update the quantity of an existing cart item. Set `quantity` to 0 to remove it. */
1074
- export async function updateCartItem(config, website, params) {
1075
- const url = new URL(`/api/v1/cart/items/${params.variantId}`, config.baseUrl);
1076
- const res = await fetch(url, {
1077
- method: "PATCH",
1078
- headers: { "Content-Type": "application/json", ...cartHeaders(website.business_id, params.token, params.sessionId) },
1079
- body: JSON.stringify({ quantity: params.quantity }),
1080
- });
1081
- if (!res.ok)
1082
- throw apiError(res.status, await res.json().catch(() => ({})));
1083
- return res.json();
1084
- }
1085
- /** Remove a product variant from the cart entirely. */
1086
- export async function removeCartItem(config, website, params) {
1087
- const url = new URL(`/api/v1/cart/items/${params.variantId}`, config.baseUrl);
1088
- const res = await fetch(url, {
1089
- method: "DELETE",
1090
- headers: cartHeaders(website.business_id, params.token, params.sessionId),
1091
- });
1092
- if (!res.ok)
1093
- throw apiError(res.status, await res.json().catch(() => ({})));
1094
- return res.json();
1095
- }
1096
- /**
1097
- * Merge a guest cart (identified by session ID) into the authenticated customer's cart.
1098
- * Call this immediately after a successful login if there is an active guest session.
1099
- *
1100
- * @example
1101
- * const sessionId = localStorage.getItem('proxima_session_id');
1102
- * if (sessionId) await mergeGuestCart(config, website, { token, sessionId });
1103
- */
1104
- export async function mergeGuestCart(config, website, params) {
1105
- const url = new URL("/api/v1/cart/merge", config.baseUrl);
1106
- const res = await fetch(url, {
1107
- method: "POST",
1108
- headers: {
1109
- "X-Business-ID": website.business_id,
1110
- "X-Session-ID": params.sessionId,
1111
- "Authorization": `Bearer ${params.token}`,
1112
- },
1113
- });
1114
- if (!res.ok)
1115
- throw apiError(res.status, await res.json().catch(() => ({})));
1116
- return res.json();
1117
- }
1118
- /**
1119
- * Validate a coupon code before checkout. Always resolves — check `result.valid`.
1120
- * Use the `discount_amount` from the result to preview the discount in the UI.
1121
- *
1122
- * @example
1123
- * const result = await validateCoupon(config, website, { code: 'PROMO10', amount: 150.00 });
1124
- * if (result.valid) showDiscount(result.discount_amount);
1125
- * else showError(result.error);
1126
- */
1127
- export async function validateCoupon(config, website, params) {
1128
- const url = new URL("/api/v1/commerce/coupons/validate", config.baseUrl);
1129
- url.searchParams.set("code", params.code);
1130
- url.searchParams.set("amount", String(params.amount));
1131
- const res = await fetch(url, {
1132
- headers: { "X-Business-ID": website.business_id },
1133
- });
1134
- if (!res.ok)
1135
- throw apiError(res.status, await res.json().catch(() => ({})));
1136
- return res.json();
1137
- }
1138
- // ---------------------------------------------------------------------------
1139
- // Orders
1140
- // ---------------------------------------------------------------------------
1141
- /**
1142
- * Submit the current cart as an order (checkout). The cart must be non-empty.
1143
- * On success the cart is cleared. Throws 400 on validation errors (e.g. out of stock).
1144
- */
1145
- export async function createOrder(config, website, params) {
1146
- const url = new URL("/api/v1/checkout", config.baseUrl);
1147
- const res = await fetch(url, {
1148
- method: "POST",
1149
- headers: { "Content-Type": "application/json", ...cartHeaders(website.business_id, params.token) },
1150
- body: JSON.stringify(params.checkout),
1151
- });
1152
- if (!res.ok)
1153
- throw apiError(res.status, await res.json().catch(() => ({})));
1154
- return res.json();
1155
- }
1156
- /** Fetch the authenticated customer's order history, paginated. */
1157
- export async function fetchOrders(config, website, params) {
1158
- const url = new URL("/api/v1/store/me/orders", config.baseUrl);
1159
- if (params.page)
1160
- url.searchParams.set("page", String(params.page));
1161
- if (params.size)
1162
- url.searchParams.set("size", String(params.size));
1163
- const res = await fetch(url, {
1164
- headers: authHeaders(website.business_id, params.token),
1165
- });
1166
- if (!res.ok)
1167
- throw apiError(res.status, await res.json().catch(() => ({})));
1168
- return res.json();
1169
- }
1170
- /** Fetch a single order by ID. Works for authenticated customers and guests with the receipt token. */
1171
- export async function fetchOrder(config, website, params) {
1172
- const url = new URL(`/api/v1/orders/${params.orderId}`, config.baseUrl);
1173
- const res = await fetch(url, {
1174
- headers: authHeaders(website.business_id, params.token),
1175
- });
1176
- if (!res.ok)
1177
- throw apiError(res.status, await res.json().catch(() => ({})));
1178
- return res.json();
1179
- }
1180
- // ---------------------------------------------------------------------------
1181
- // Address Book
1182
- // ---------------------------------------------------------------------------
1183
- /** Fetch all saved addresses for the authenticated customer. */
1184
- export async function fetchCustomerAddresses(config, website, params) {
1185
- const res = await fetch(`${config.baseUrl}/api/v1/store/me/addresses/`, {
1186
- headers: authHeaders(website.business_id, params.token),
1187
- });
1188
- if (!res.ok)
1189
- throw new Error(`fetchCustomerAddresses failed: ${res.status}`);
1190
- return res.json();
1191
- }
1192
- /** Save a new address to the customer's address book. */
1193
- export async function createCustomerAddress(config, website, params) {
1194
- const res = await fetch(`${config.baseUrl}/api/v1/store/me/addresses/`, {
1195
- method: "POST",
1196
- headers: { "Content-Type": "application/json", ...authHeaders(website.business_id, params.token) },
1197
- body: JSON.stringify(params.address),
1198
- });
1199
- if (!res.ok) {
1200
- const err = await res.json().catch(() => ({}));
1201
- throw Object.assign(new Error("createCustomerAddress failed"), { status: res.status, detail: err.detail });
1202
- }
1203
- return res.json();
1204
- }
1205
- /** Partially update a saved address. Only the fields included in `address` are changed. */
1206
- export async function updateCustomerAddress(config, website, params) {
1207
- const res = await fetch(`${config.baseUrl}/api/v1/store/me/addresses/${params.addressId}`, {
1208
- method: "PATCH",
1209
- headers: { "Content-Type": "application/json", ...authHeaders(website.business_id, params.token) },
1210
- body: JSON.stringify(params.address),
1211
- });
1212
- if (!res.ok) {
1213
- const err = await res.json().catch(() => ({}));
1214
- throw Object.assign(new Error("updateCustomerAddress failed"), { status: res.status, detail: err.detail });
1215
- }
1216
- return res.json();
1217
- }
1218
- /** Delete a saved address. Throws if the address does not exist. */
1219
- export async function deleteCustomerAddress(config, website, params) {
1220
- const res = await fetch(`${config.baseUrl}/api/v1/store/me/addresses/${params.addressId}`, {
1221
- method: "DELETE",
1222
- headers: authHeaders(website.business_id, params.token),
1223
- });
1224
- if (!res.ok && res.status !== 204)
1225
- throw new Error(`deleteCustomerAddress failed: ${res.status}`);
1226
- }
1227
- /** Mark an address as the customer's default. The previous default is unset automatically. */
1228
- export async function setDefaultAddress(config, website, params) {
1229
- const res = await fetch(`${config.baseUrl}/api/v1/store/me/addresses/${params.addressId}/default`, {
1230
- method: "POST",
1231
- headers: authHeaders(website.business_id, params.token),
1232
- });
1233
- if (!res.ok)
1234
- throw new Error(`setDefaultAddress failed: ${res.status}`);
1235
- return res.json();
1236
- }
1237
- /**
1238
- * Search Peruvian ubigeo codes (department/province/district) by name.
1239
- * Use this to power the address form's location selector.
1240
- * Returns an empty array on error instead of throwing.
1241
- */
1242
- export async function searchUbigeo(config, params) {
1243
- const res = await fetch(`${config.baseUrl}/api/v1/catalog/locations/ubigeos?q=${encodeURIComponent(params.q)}`);
1244
- if (!res.ok)
1245
- return [];
1246
- return res.json();
1247
- }
1248
- // ---------------------------------------------------------------------------
1249
- // Wishlist
1250
- // ---------------------------------------------------------------------------
1251
- /**
1252
- * Fetch all wishlist items for the authenticated customer.
1253
- * Returns an empty array if the wishlist is empty.
1254
- */
1255
- export async function fetchWishlist(config, website, params) {
1256
- const url = new URL("/api/v1/store/me/wishlist", config.baseUrl);
1257
- const res = await fetch(url, {
1258
- headers: authHeaders(website.business_id, params.token),
1259
- });
1260
- if (!res.ok)
1261
- throw apiError(res.status, await res.json().catch(() => ({})));
1262
- return res.json();
1263
- }
1264
- /**
1265
- * Add a product to the wishlist. Idempotent — if the product is already
1266
- * in the wishlist, returns the existing item without creating a duplicate.
1267
- */
1268
- export async function addToWishlist(config, website, params) {
1269
- const url = new URL("/api/v1/store/me/wishlist", config.baseUrl);
1270
- const res = await fetch(url, {
1271
- method: "POST",
1272
- headers: { "Content-Type": "application/json", ...authHeaders(website.business_id, params.token) },
1273
- body: JSON.stringify({
1274
- product_id: params.productId,
1275
- variant_id: params.variantId ?? null,
1276
- notes: params.notes ?? null,
1277
- }),
1278
- });
1279
- if (!res.ok)
1280
- throw apiError(res.status, await res.json().catch(() => ({})));
1281
- return res.json();
1282
- }
1283
- /**
1284
- * Remove a product from the wishlist.
1285
- * Throws { status: 404 } if the product was not in the wishlist.
1286
- */
1287
- export async function removeFromWishlist(config, website, params) {
1288
- const url = new URL(`/api/v1/store/me/wishlist/${params.productId}`, config.baseUrl);
1289
- const res = await fetch(url, {
1290
- method: "DELETE",
1291
- headers: authHeaders(website.business_id, params.token),
1292
- });
1293
- if (!res.ok)
1294
- throw apiError(res.status, await res.json().catch(() => ({})));
1295
- }
1296
- // ---------------------------------------------------------------------------
1297
- // Server-side Handler Helpers (for Astro API routes)
1298
- //
1299
- // These orchestrators combine fetchProximaWebsite + SDK calls so that Astro
1300
- // API routes become thin wrappers (~10 lines) that only deal with cookies
1301
- // and redirects. Use them in `src/pages/api/buyer/**` files.
1302
- // ---------------------------------------------------------------------------
1303
- /**
1304
- * Resolve the website then call loginBuyer.
1305
- * Returns { access_token, refresh_token, next } on success, throws on failure.
1306
- */
1307
- export async function processBuyerLogin(env, params) {
1308
- const website = await fetchProximaWebsite({ baseUrl: env.apiUrl, domain: env.domain, serviceKey: env.serviceKey });
1309
- const session = await loginBuyer({ baseUrl: env.apiUrl }, website, { email: params.email, password: params.password, captchaToken: params.captchaToken });
1310
- return { access_token: session.access_token, refresh_token: session.refresh_token ?? null, next: params.next || "/" };
1311
- }
1312
- /**
1313
- * Resolve the website then call registerBuyer.
1314
- * Returns { access_token, refresh_token, next } on success, throws on failure.
1315
- * Propagates MissingFieldsError so the API route can return structured 422 errors.
1316
- */
1317
- export async function processBuyerRegister(env, params) {
1318
- const website = await fetchProximaWebsite({ baseUrl: env.apiUrl, domain: env.domain, serviceKey: env.serviceKey });
1319
- const { next, ...registerParams } = params;
1320
- const session = await registerBuyer({ baseUrl: env.apiUrl }, website, registerParams);
1321
- return { access_token: session.access_token, refresh_token: session.refresh_token ?? null, next: next || "/" };
1322
- }
1323
- /**
1324
- * Call logoutBuyer (best-effort — never throws).
1325
- * Always clear the session cookie regardless of the result.
1326
- */
1327
- export async function processBuyerLogout(env, params) {
1328
- try {
1329
- const website = await fetchProximaWebsite({ baseUrl: env.apiUrl, domain: env.domain, serviceKey: env.serviceKey });
1330
- await logoutBuyer({ baseUrl: env.apiUrl }, website, { token: params.token });
1331
- }
1332
- catch {
1333
- // Best-effort — caller must always clear the cookie regardless
1334
- }
1335
- }
1336
- /**
1337
- * Resolve the website then exchange a refresh token for a new access token.
1338
- * Use this in Astro middleware to silently refresh expired sessions.
1339
- * Throws { status: 401 } if the refresh token is expired — clear cookies and redirect to login.
1340
- */
1341
- export async function processRefreshToken(env, params) {
1342
- const website = await fetchProximaWebsite({ baseUrl: env.apiUrl, domain: env.domain, serviceKey: env.serviceKey });
1343
- const session = await refreshBuyerToken({ baseUrl: env.apiUrl }, website, { refreshToken: params.refreshToken });
1344
- return { access_token: session.access_token, refresh_token: session.refresh_token ?? null };
1345
- }
1346
- /**
1347
- * Resolve the website then send a password reset email.
1348
- * Never throws — always show a generic confirmation message.
1349
- */
1350
- export async function processForgotPassword(env, params) {
1351
- try {
1352
- const website = await fetchProximaWebsite({ baseUrl: env.apiUrl, domain: env.domain, serviceKey: env.serviceKey });
1353
- await forgotPassword({ baseUrl: env.apiUrl }, website, { email: params.email, captchaToken: params.captchaToken });
1354
- }
1355
- catch {
1356
- // Never expose whether the email exists
1357
- }
1358
- }
1359
- /**
1360
- * Reset the customer's password with the token from the email link.
1361
- * Throws { status: 400, data.detail: BUYER_AUTH_ERRORS.RESET_TOKEN_INVALID } on bad token.
1362
- */
1363
- export async function processResetPassword(env, params) {
1364
- await resetPassword({ baseUrl: env.apiUrl }, params);
1365
- }
1366
- /**
1367
- * Verify the customer's email with the token from the email link.
1368
- * Throws { status: 400, data.detail: BUYER_AUTH_ERRORS.VERIFY_TOKEN_INVALID } on bad token.
1369
- */
1370
- export async function processVerifyEmail(env, params) {
1371
- await verifyEmail({ baseUrl: env.apiUrl }, params);
1372
- }
1373
- /**
1374
- * Resolve the website then add a variant to the cart.
1375
- * Token is optional (guest cart supported).
1376
- */
1377
- export async function processAddToCart(env, params) {
1378
- const website = await fetchProximaWebsite({ baseUrl: env.apiUrl, domain: env.domain, serviceKey: env.serviceKey });
1379
- return addToCart({ baseUrl: env.apiUrl }, website, { token: params.token, sessionId: params.sessionId, variantId: params.variantId, quantity: params.quantity });
1380
- }
1381
- /**
1382
- * Resolve the website then remove a variant from the cart.
1383
- * Token is optional (guest cart supported).
1384
- */
1385
- export async function processRemoveCartItem(env, params) {
1386
- const website = await fetchProximaWebsite({ baseUrl: env.apiUrl, domain: env.domain, serviceKey: env.serviceKey });
1387
- return removeCartItem({ baseUrl: env.apiUrl }, website, { token: params.token, sessionId: params.sessionId, variantId: params.variantId });
1388
- }
1389
- /**
1390
- * Resolve the website then update the quantity of a variant in the cart.
1391
- * Token is optional (guest cart supported).
1392
- */
1393
- export async function processUpdateCartItem(env, params) {
1394
- const website = await fetchProximaWebsite({ baseUrl: env.apiUrl, domain: env.domain, serviceKey: env.serviceKey });
1395
- return updateCartItem({ baseUrl: env.apiUrl }, website, { token: params.token, sessionId: params.sessionId, variantId: params.variantId, quantity: params.quantity });
1396
- }
1397
- /**
1398
- * Resolve the website then fetch the current cart.
1399
- * Token is optional (guest cart supported).
1400
- */
1401
- export async function processGetCart(env, params) {
1402
- const website = await fetchProximaWebsite({ baseUrl: env.apiUrl, domain: env.domain, serviceKey: env.serviceKey });
1403
- return fetchCart({ baseUrl: env.apiUrl }, website, { token: params.token, sessionId: params.sessionId });
1404
- }
1405
- /**
1406
- * Resolve the website then call POST /checkout.
1407
- * Returns { orderId } on success, throws on failure.
1408
- */
1409
- export async function processBuyerCheckout(env, params) {
1410
- const website = await fetchProximaWebsite({ baseUrl: env.apiUrl, domain: env.domain, serviceKey: env.serviceKey });
1411
- const order = await createOrder({ baseUrl: env.apiUrl }, website, { token: params.token, checkout: params.checkout });
1412
- return { orderId: order.id };
1413
- }
1414
- /** Resolve website then set the customer's default address. */
1415
- export async function processSetDefaultAddress(env, params) {
1416
- const website = await fetchProximaWebsite({ baseUrl: env.apiUrl, domain: env.domain, serviceKey: env.serviceKey });
1417
- return setDefaultAddress({ baseUrl: env.apiUrl }, website, params);
1418
- }
1419
- /** Resolve website then delete a saved address. */
1420
- export async function processDeleteAddress(env, params) {
1421
- const website = await fetchProximaWebsite({ baseUrl: env.apiUrl, domain: env.domain, serviceKey: env.serviceKey });
1422
- return deleteCustomerAddress({ baseUrl: env.apiUrl }, website, params);
1423
- }
1424
- /**
1425
- * Fetch a filtered, sorted, paginated product listing.
1426
- * Extends fetchStorefrontProducts with price range and stock filters.
1427
- */
1428
- export async function fetchProductListing(config, website, params = {}) {
1429
- const url = new URL("/api/v1/storefront/products", config.baseUrl);
1430
- if (params.filters?.brand)
1431
- url.searchParams.set("brand", params.filters.brand);
1432
- if (params.filters?.category)
1433
- url.searchParams.set("category", params.filters.category);
1434
- if (params.filters?.price_min != null)
1435
- url.searchParams.set("price_min", String(params.filters.price_min));
1436
- if (params.filters?.price_max != null)
1437
- url.searchParams.set("price_max", String(params.filters.price_max));
1438
- if (params.filters?.in_stock)
1439
- url.searchParams.set("in_stock", "true");
1440
- if (params.sort)
1441
- url.searchParams.set("sort", params.sort);
1442
- if (params.page)
1443
- url.searchParams.set("page", String(params.page));
1444
- if (params.page_size)
1445
- url.searchParams.set("page_size", String(params.page_size));
1446
- const res = await fetch(url, {
1447
- headers: storefrontHeaders(website.business_id, website.locale, website.currency),
1448
- });
1449
- if (!res.ok)
1450
- throw apiError(res.status, await res.json().catch(() => ({})));
1451
- return res.json();
1452
- }
1453
- export class GuestOrderError extends Error {
1454
- code;
1455
- constructor(code, message) {
1456
- super(message);
1457
- this.code = code;
1458
- this.name = "GuestOrderError";
1459
- }
1460
- }
1461
- /**
1462
- * Create an order without buyer authentication.
1463
- * The cart is identified by `session_id` (from the storefront session cookie).
1464
- * Throws `GuestOrderError` for typed error cases.
1465
- */
1466
- export async function initiateGuestOrder(config, website, payload) {
1467
- const { session_id, ...checkout } = payload;
1468
- const url = new URL("/api/v1/checkout", config.baseUrl);
1469
- const res = await fetch(url, {
1470
- method: "POST",
1471
- headers: {
1472
- "Content-Type": "application/json",
1473
- "X-Business-ID": website.business_id,
1474
- "X-Session-ID": session_id,
1475
- },
1476
- body: JSON.stringify(checkout),
1477
- });
1478
- if (!res.ok) {
1479
- const body = await res.json().catch(() => ({}));
1480
- const detail = body?.detail ?? "";
1481
- if (detail === "Cart is empty" || detail === "Cart not found") {
1482
- throw new GuestOrderError("CART_NOT_FOUND", detail);
1483
- }
1484
- if (typeof detail === "object" && detail?.code === "OUT_OF_STOCK") {
1485
- throw new GuestOrderError("OUT_OF_STOCK", "Some items are out of stock");
1486
- }
1487
- throw new GuestOrderError("SERVER_ERROR", String(detail || res.status));
1488
- }
1489
- const order = await res.json();
1490
- return { orderId: order.id };
1491
- }
1492
- /**
1493
- * Resolve website then call initiateGuestOrder.
1494
- * Server-side helper for Astro API routes.
1495
- */
1496
- export async function processGuestCheckout(env, payload) {
1497
- const website = await fetchProximaWebsite({ baseUrl: env.apiUrl, domain: env.domain, serviceKey: env.serviceKey });
1498
- return initiateGuestOrder({ baseUrl: env.apiUrl }, website, payload);
1499
- }
1500
- class ProximaAnalytics {
1501
- config = null;
1502
- queue = [];
1503
- preInitQueue = [];
1504
- timer = null;
1505
- initialized = false;
1506
- init(config) {
1507
- if (typeof window === 'undefined')
1508
- return;
1509
- this.config = config;
1510
- if (this.initialized)
1511
- return;
1512
- this.initialized = true;
1513
- for (const [type, payload] of this.preInitQueue.splice(0)) {
1514
- this.track(type, payload);
1515
- }
1516
- const firePageView = () => this.track('page_view');
1517
- firePageView();
1518
- document.addEventListener('astro:page-load', firePageView);
1519
- const interval = config.flushInterval ?? 3000;
1520
- this.timer = setInterval(() => this.flush(), interval);
1521
- document.addEventListener('visibilitychange', () => {
1522
- if (document.visibilityState === 'hidden')
1523
- this.flush(true);
1524
- });
1525
- }
1526
- track(type, payload = {}) {
1527
- if (typeof window === 'undefined')
1528
- return;
1529
- if (!this.config) {
1530
- this.preInitQueue.push([type, payload]);
1531
- return;
1532
- }
1533
- const event = {
1534
- event_type: type,
1535
- occurred_at: new Date().toISOString(),
1536
- website_id: this.config.websiteId,
1537
- path: window.location.pathname,
1538
- referrer: document.referrer || undefined,
1539
- locale: this.config.locale,
1540
- payload,
1541
- };
1542
- this.queue.push(event);
1543
- if (this.config.debug)
1544
- console.debug('[proxima:analytics] queued', event);
1545
- }
1546
- flush(beacon = false) {
1547
- if (!this.config || this.queue.length === 0)
1548
- return;
1549
- const batch = this.queue.splice(0);
1550
- const url = `${this.config.apiUrl}/api/v1/store/events`;
1551
- const body = JSON.stringify({ events: batch });
1552
- const headers = { 'Content-Type': 'application/json', 'X-Business-ID': this.config.businessId };
1553
- if (this.config.debug)
1554
- console.debug(`[proxima:analytics] flushing ${batch.length} event(s)`);
1555
- if (beacon && typeof navigator !== 'undefined' && navigator.sendBeacon) {
1556
- const blob = new Blob([body], { type: 'application/json' });
1557
- const sent = navigator.sendBeacon(url, blob);
1558
- if (!sent)
1559
- this.queue.unshift(...batch);
1560
- return;
1561
- }
1562
- fetch(url, { method: 'POST', headers, body, keepalive: true }).catch(() => { });
1563
- }
1564
- destroy() {
1565
- if (this.timer !== null)
1566
- clearInterval(this.timer);
1567
- this.timer = null;
1568
- this.initialized = false;
1569
- this.config = null;
1570
- this.queue = [];
1571
- this.preInitQueue = [];
1572
- }
1573
- }
1574
- /** Singleton analytics client. Call `analytics.init()` once from SiteLayout. */
1575
- export const analytics = new ProximaAnalytics();
1576
- export { createFixtureBundle, createStorefrontDataSource, validateFixtureBundle, resolveStorefrontDataSourceForRequest, FixtureGuestOrderError, } from "./fixtures-commerce.js";
1
+ // Proxima Storefront Core SDK — barrel re-exports
2
+ export * from './types/cms.js';
3
+ export * from './types/seo.js';
4
+ export * from './types/business.js';
5
+ export * from './types/buyer.js';
6
+ export * from './types/catalog.js';
7
+ export * from './types/cart.js';
8
+ export * from './types/order.js';
9
+ export * from './types/address.js';
10
+ export * from './types/wishlist.js';
11
+ export * from './types/server-env.js';
12
+ export * from './types/listing.js';
13
+ export * from './types/guest-order.js';
14
+ export * from './types/campaign.js';
15
+ export * from './types/analytics.js';
16
+ export * from './types/cookie-consent.js';
17
+ export * from './api/index.js';
18
+ export * from './seo/page-seo.js';
19
+ export * from './seo/json-ld.js';
20
+ export * from './seo/sitemap.js';
21
+ export * from './seo/robots.js';
22
+ export * from './seo/indexnow.js';
23
+ export * from './cms/website.js';
24
+ export * from './cms/payment-methods.js';
25
+ export * from './buyer/auth.js';
26
+ export * from './catalog/listings.js';
27
+ export * from './cart/cart.js';
28
+ export * from './orders/orders.js';
29
+ export * from './orders/guest.js';
30
+ export * from './addresses/address-book.js';
31
+ export * from './wishlist/wishlist.js';
32
+ export * from './server/process.js';
33
+ export * from './campaign/countdown.js';
34
+ export { analytics } from './analytics/analytics.js';
35
+ export { resolveAnalyticsSessionId, ANALYTICS_SESSION_STORAGE_KEY } from './analytics/session.js';
36
+ export { captureSessionAttribution, getSessionAttribution, clearSessionAttribution, inferAttributionFromReferrer, ATTRIBUTION_STORAGE_KEY, } from './analytics/attribution.js';
37
+ export { createStorefrontAnalyticsTrackers } from './analytics/trackers.js';
38
+ export * from './cookie-consent/consent.js';
39
+ export * from './cache/cache.js';
40
+ export { createFixtureBundle, createStorefrontDataSource, validateFixtureBundle, resolveStorefrontDataSourceForRequest, FixtureGuestOrderError, } from './fixtures-commerce.js';
1577
41
  //# sourceMappingURL=index.js.map