@proxima-io/storefront-core 0.3.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -17
- package/dist/addresses/address-book.d.ts +36 -0
- package/dist/addresses/address-book.d.ts.map +1 -0
- package/dist/addresses/address-book.js +62 -0
- package/dist/addresses/address-book.js.map +1 -0
- package/dist/analytics/analytics.d.ts +28 -0
- package/dist/analytics/analytics.d.ts.map +1 -0
- package/dist/analytics/analytics.js +124 -0
- package/dist/analytics/analytics.js.map +1 -0
- package/dist/analytics/attribution.d.ts +28 -0
- package/dist/analytics/attribution.d.ts.map +1 -0
- package/dist/analytics/attribution.js +116 -0
- package/dist/analytics/attribution.js.map +1 -0
- package/dist/analytics/session.d.ts +12 -0
- package/dist/analytics/session.d.ts.map +1 -0
- package/dist/analytics/session.js +62 -0
- package/dist/analytics/session.js.map +1 -0
- package/dist/analytics/trackers.d.ts +29 -0
- package/dist/analytics/trackers.d.ts.map +1 -0
- package/dist/analytics/trackers.js +30 -0
- package/dist/analytics/trackers.js.map +1 -0
- package/dist/api/endpoints.d.ts +70 -0
- package/dist/api/endpoints.d.ts.map +1 -0
- package/dist/api/endpoints.js +70 -0
- package/dist/api/endpoints.js.map +1 -0
- package/dist/api/index.d.ts +3 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +3 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/storefront-client.d.ts +50 -0
- package/dist/api/storefront-client.d.ts.map +1 -0
- package/dist/api/storefront-client.js +123 -0
- package/dist/api/storefront-client.js.map +1 -0
- package/dist/buyer/auth.d.ts +105 -0
- package/dist/buyer/auth.d.ts.map +1 -0
- package/dist/buyer/auth.js +215 -0
- package/dist/buyer/auth.js.map +1 -0
- package/dist/cache/cache.d.ts +31 -0
- package/dist/cache/cache.d.ts.map +1 -0
- package/dist/cache/cache.js +71 -0
- package/dist/cache/cache.js.map +1 -0
- package/dist/campaign/countdown.d.ts +40 -0
- package/dist/campaign/countdown.d.ts.map +1 -0
- package/dist/campaign/countdown.js +71 -0
- package/dist/campaign/countdown.js.map +1 -0
- package/dist/cart/cart.d.ts +57 -0
- package/dist/cart/cart.d.ts.map +1 -0
- package/dist/cart/cart.js +64 -0
- package/dist/cart/cart.js.map +1 -0
- package/dist/catalog/listings.d.ts +87 -0
- package/dist/catalog/listings.d.ts.map +1 -0
- package/dist/catalog/listings.js +140 -0
- package/dist/catalog/listings.js.map +1 -0
- package/dist/cms/payment-methods.d.ts +13 -0
- package/dist/cms/payment-methods.d.ts.map +1 -0
- package/dist/cms/payment-methods.js +41 -0
- package/dist/cms/payment-methods.js.map +1 -0
- package/dist/cms/website.d.ts +76 -0
- package/dist/cms/website.d.ts.map +1 -0
- package/dist/cms/website.js +146 -0
- package/dist/cms/website.js.map +1 -0
- package/dist/cookie-consent/consent.d.ts +22 -0
- package/dist/cookie-consent/consent.d.ts.map +1 -0
- package/dist/cookie-consent/consent.js +93 -0
- package/dist/cookie-consent/consent.js.map +1 -0
- package/dist/index.d.ts +45 -1310
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +42 -1576
- package/dist/index.js.map +1 -1
- package/dist/internal/http.d.ts +5 -0
- package/dist/internal/http.d.ts.map +1 -0
- package/dist/internal/http.js +27 -0
- package/dist/internal/http.js.map +1 -0
- package/dist/orders/guest.d.ts +9 -0
- package/dist/orders/guest.d.ts.map +1 -0
- package/dist/orders/guest.js +30 -0
- package/dist/orders/guest.js.map +1 -0
- package/dist/orders/orders.d.ts +23 -0
- package/dist/orders/orders.d.ts.map +1 -0
- package/dist/orders/orders.js +33 -0
- package/dist/orders/orders.js.map +1 -0
- package/dist/seo/engine-url.d.ts +26 -0
- package/dist/seo/engine-url.d.ts.map +1 -0
- package/dist/seo/engine-url.js +111 -0
- package/dist/seo/engine-url.js.map +1 -0
- package/dist/seo/hreflang.d.ts +19 -0
- package/dist/seo/hreflang.d.ts.map +1 -0
- package/dist/seo/hreflang.js +52 -0
- package/dist/seo/hreflang.js.map +1 -0
- package/dist/seo/indexnow.d.ts +24 -0
- package/dist/seo/indexnow.d.ts.map +1 -0
- package/dist/seo/indexnow.js +50 -0
- package/dist/seo/indexnow.js.map +1 -0
- package/dist/seo/json-ld.d.ts +57 -0
- package/dist/seo/json-ld.d.ts.map +1 -0
- package/dist/seo/json-ld.js +180 -0
- package/dist/seo/json-ld.js.map +1 -0
- package/dist/seo/page-seo.d.ts +21 -0
- package/dist/seo/page-seo.d.ts.map +1 -0
- package/dist/seo/page-seo.js +68 -0
- package/dist/seo/page-seo.js.map +1 -0
- package/dist/seo/robots.d.ts +23 -0
- package/dist/seo/robots.d.ts.map +1 -0
- package/dist/seo/robots.js +35 -0
- package/dist/seo/robots.js.map +1 -0
- package/dist/seo/sitemap.d.ts +35 -0
- package/dist/seo/sitemap.d.ts.map +1 -0
- package/dist/seo/sitemap.js +186 -0
- package/dist/seo/sitemap.js.map +1 -0
- package/dist/server/process.d.ts +136 -0
- package/dist/server/process.d.ts.map +1 -0
- package/dist/server/process.js +143 -0
- package/dist/server/process.js.map +1 -0
- package/dist/types/address.d.ts +40 -0
- package/dist/types/address.d.ts.map +1 -0
- package/dist/types/address.js +2 -0
- package/dist/types/address.js.map +1 -0
- package/dist/types/analytics.d.ts +79 -0
- package/dist/types/analytics.d.ts.map +1 -0
- package/dist/types/analytics.js +2 -0
- package/dist/types/analytics.js.map +1 -0
- package/dist/types/business.d.ts +95 -0
- package/dist/types/business.d.ts.map +1 -0
- package/dist/types/business.js +2 -0
- package/dist/types/business.js.map +1 -0
- package/dist/types/buyer.d.ts +144 -0
- package/dist/types/buyer.d.ts.map +1 -0
- package/dist/types/buyer.js +45 -0
- package/dist/types/buyer.js.map +1 -0
- package/dist/types/campaign.d.ts +51 -0
- package/dist/types/campaign.d.ts.map +1 -0
- package/dist/types/campaign.js +2 -0
- package/dist/types/campaign.js.map +1 -0
- package/dist/types/cart.d.ts +40 -0
- package/dist/types/cart.d.ts.map +1 -0
- package/dist/types/cart.js +2 -0
- package/dist/types/cart.js.map +1 -0
- package/dist/types/catalog.d.ts +164 -0
- package/dist/types/catalog.d.ts.map +1 -0
- package/dist/types/catalog.js +2 -0
- package/dist/types/catalog.js.map +1 -0
- package/dist/types/cms.d.ts +200 -0
- package/dist/types/cms.d.ts.map +1 -0
- package/dist/types/cms.js +2 -0
- package/dist/types/cms.js.map +1 -0
- package/dist/types/cookie-consent.d.ts +18 -0
- package/dist/types/cookie-consent.d.ts.map +1 -0
- package/dist/types/cookie-consent.js +7 -0
- package/dist/types/cookie-consent.js.map +1 -0
- package/dist/types/guest-order.d.ts +16 -0
- package/dist/types/guest-order.d.ts.map +1 -0
- package/dist/types/guest-order.js +9 -0
- package/dist/types/guest-order.js.map +1 -0
- package/dist/types/listing.d.ts +14 -0
- package/dist/types/listing.d.ts.map +1 -0
- package/dist/types/listing.js +2 -0
- package/dist/types/listing.js.map +1 -0
- package/dist/types/order.d.ts +40 -0
- package/dist/types/order.d.ts.map +1 -0
- package/dist/types/order.js +2 -0
- package/dist/types/order.js.map +1 -0
- package/dist/types/seo.d.ts +96 -0
- package/dist/types/seo.d.ts.map +1 -0
- package/dist/types/seo.js +2 -0
- package/dist/types/seo.js.map +1 -0
- package/dist/types/server-env.d.ts +19 -0
- package/dist/types/server-env.d.ts.map +1 -0
- package/dist/types/server-env.js +10 -0
- package/dist/types/server-env.js.map +1 -0
- package/dist/types/wishlist.d.ts +10 -0
- package/dist/types/wishlist.d.ts.map +1 -0
- package/dist/types/wishlist.js +2 -0
- package/dist/types/wishlist.js.map +1 -0
- package/dist/wishlist/wishlist.d.ts +28 -0
- package/dist/wishlist/wishlist.d.ts.map +1 -0
- package/dist/wishlist/wishlist.js +42 -0
- package/dist/wishlist/wishlist.js.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,1577 +1,43 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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, "&")
|
|
263
|
-
.replace(/</g, "<")
|
|
264
|
-
.replace(/>/g, ">")
|
|
265
|
-
.replace(/"/g, """)
|
|
266
|
-
.replace(/'/g, "'");
|
|
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/hreflang.js';
|
|
19
|
+
export * from './seo/engine-url.js';
|
|
20
|
+
export * from './seo/page-seo.js';
|
|
21
|
+
export * from './seo/json-ld.js';
|
|
22
|
+
export * from './seo/sitemap.js';
|
|
23
|
+
export * from './seo/robots.js';
|
|
24
|
+
export * from './seo/indexnow.js';
|
|
25
|
+
export * from './cms/website.js';
|
|
26
|
+
export * from './cms/payment-methods.js';
|
|
27
|
+
export * from './buyer/auth.js';
|
|
28
|
+
export * from './catalog/listings.js';
|
|
29
|
+
export * from './cart/cart.js';
|
|
30
|
+
export * from './orders/orders.js';
|
|
31
|
+
export * from './orders/guest.js';
|
|
32
|
+
export * from './addresses/address-book.js';
|
|
33
|
+
export * from './wishlist/wishlist.js';
|
|
34
|
+
export * from './server/process.js';
|
|
35
|
+
export * from './campaign/countdown.js';
|
|
36
|
+
export { analytics } from './analytics/analytics.js';
|
|
37
|
+
export { resolveAnalyticsSessionId, ANALYTICS_SESSION_STORAGE_KEY } from './analytics/session.js';
|
|
38
|
+
export { captureSessionAttribution, getSessionAttribution, clearSessionAttribution, inferAttributionFromReferrer, ATTRIBUTION_STORAGE_KEY, } from './analytics/attribution.js';
|
|
39
|
+
export { createStorefrontAnalyticsTrackers } from './analytics/trackers.js';
|
|
40
|
+
export * from './cookie-consent/consent.js';
|
|
41
|
+
export * from './cache/cache.js';
|
|
42
|
+
export { createFixtureBundle, createStorefrontDataSource, validateFixtureBundle, resolveStorefrontDataSourceForRequest, FixtureGuestOrderError, } from './fixtures-commerce.js';
|
|
1577
43
|
//# sourceMappingURL=index.js.map
|