@fayz-ai/storefront 0.84.0 → 0.84.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/dist/CheckoutPage-DDY4WLFJ.js +12 -0
- package/dist/CheckoutPage-DDY4WLFJ.js.map +1 -0
- package/dist/MyPurchasesPage-IHCB5QRR.js +11 -0
- package/dist/MyPurchasesPage-IHCB5QRR.js.map +1 -0
- package/dist/OrderConfirmationPage-SAKAGOCC.js +10 -0
- package/dist/OrderConfirmationPage-SAKAGOCC.js.map +1 -0
- package/dist/analytics/meta-pixel.d.ts +13 -0
- package/dist/analytics/meta-pixel.d.ts.map +1 -1
- package/dist/app/createStorefrontApp.d.ts.map +1 -1
- package/dist/chunk-45R7CIEA.js +234 -0
- package/dist/chunk-45R7CIEA.js.map +1 -0
- package/dist/{chunk-QU5O4CKY.js → chunk-6NUBVJJY.js} +5 -4
- package/dist/chunk-6NUBVJJY.js.map +1 -0
- package/dist/chunk-BKO2AQ3Q.js +329 -0
- package/dist/chunk-BKO2AQ3Q.js.map +1 -0
- package/dist/chunk-CV6RYJIC.js +454 -0
- package/dist/chunk-CV6RYJIC.js.map +1 -0
- package/dist/chunk-D5C5U2SG.js +267 -0
- package/dist/chunk-D5C5U2SG.js.map +1 -0
- package/dist/chunk-DRMDBXMT.js +2123 -0
- package/dist/chunk-DRMDBXMT.js.map +1 -0
- package/dist/chunk-DVQHKNNX.js +29 -0
- package/dist/chunk-DVQHKNNX.js.map +1 -0
- package/dist/chunk-FCPOPWG3.js +31 -0
- package/dist/chunk-FCPOPWG3.js.map +1 -0
- package/dist/chunk-GQ3TCJVX.js +216 -0
- package/dist/chunk-GQ3TCJVX.js.map +1 -0
- package/dist/{chunk-7N5HKQBM.js → chunk-GVZ2SAOI.js} +5 -552
- package/dist/chunk-GVZ2SAOI.js.map +1 -0
- package/dist/{chunk-7FR6KOW2.js → chunk-JLCF7CPP.js} +1256 -6590
- package/dist/chunk-JLCF7CPP.js.map +1 -0
- package/dist/chunk-K27LYMSF.js +435 -0
- package/dist/chunk-K27LYMSF.js.map +1 -0
- package/dist/chunk-MFAU3AWK.js +817 -0
- package/dist/chunk-MFAU3AWK.js.map +1 -0
- package/dist/chunk-OHWX6BEY.js +436 -0
- package/dist/chunk-OHWX6BEY.js.map +1 -0
- package/dist/chunk-PATS4DIF.js +484 -0
- package/dist/chunk-PATS4DIF.js.map +1 -0
- package/dist/chunk-WDQFNHYO.js +41 -0
- package/dist/chunk-WDQFNHYO.js.map +1 -0
- package/dist/chunk-WPWNYBNG.js +68 -0
- package/dist/chunk-WPWNYBNG.js.map +1 -0
- package/dist/chunk-WXXYJZE6.js +99 -0
- package/dist/chunk-WXXYJZE6.js.map +1 -0
- package/dist/host.js +4 -2
- package/dist/host.js.map +1 -1
- package/dist/index.js +26 -6
- package/dist/index.js.map +1 -1
- package/dist/render.js +11 -3
- package/dist/render.js.map +1 -1
- package/dist/tanstack.js +10 -2
- package/dist/tanstack.js.map +1 -1
- package/package.json +4 -4
- package/dist/chunk-7FR6KOW2.js.map +0 -1
- package/dist/chunk-7N5HKQBM.js.map +0 -1
- package/dist/chunk-QU5O4CKY.js.map +0 -1
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { roundCents } from './chunk-PATS4DIF.js';
|
|
2
|
+
import { create } from 'zustand';
|
|
3
|
+
import { persist } from 'zustand/middleware';
|
|
4
|
+
import { normalizePostalCode, getCountrySchema, lookupPostalCode } from '@fayz-ai/core';
|
|
5
|
+
import { getShopProvider } from '@fayz-ai/shop/runtime';
|
|
6
|
+
|
|
7
|
+
var useMarketStore = create()((set) => ({
|
|
8
|
+
markets: [],
|
|
9
|
+
active: null,
|
|
10
|
+
prices: {},
|
|
11
|
+
setMarkets: (markets) => set({ markets }),
|
|
12
|
+
setActive: (active) => set({ active }),
|
|
13
|
+
setPrices: (code, list) => set((state) => ({
|
|
14
|
+
prices: {
|
|
15
|
+
...state.prices,
|
|
16
|
+
[code]: Object.fromEntries(list.map((p) => [`${p.productId}:${p.variantId ?? ""}`, p.price]))
|
|
17
|
+
}
|
|
18
|
+
}))
|
|
19
|
+
}));
|
|
20
|
+
function marketForCountry(markets, country) {
|
|
21
|
+
const iso2 = country.toUpperCase();
|
|
22
|
+
return markets.find((m) => m.countries.includes(iso2)) ?? markets.find((m) => m.isDefault) ?? null;
|
|
23
|
+
}
|
|
24
|
+
function marketUnitPrice(state, productId, variantId, fallback) {
|
|
25
|
+
const market = state.active;
|
|
26
|
+
if (!market) return fallback;
|
|
27
|
+
const list = state.prices[market.code];
|
|
28
|
+
const hit = list?.[`${productId}:${variantId ?? ""}`] ?? list?.[`${productId}:`];
|
|
29
|
+
if (hit != null) return hit;
|
|
30
|
+
return market.isDefault ? fallback : null;
|
|
31
|
+
}
|
|
32
|
+
function withMarketPrice(product, state = useMarketStore.getState()) {
|
|
33
|
+
const market = state.active;
|
|
34
|
+
if (!market || market.isDefault) return product;
|
|
35
|
+
const price = marketUnitPrice(state, product.id, null, product.price);
|
|
36
|
+
if (price == null) {
|
|
37
|
+
return { ...product, compareAtPrice: null, currency: market.currency, inventoryCount: 0, metadata: { ...product.metadata, unavailableInMarket: market.code } };
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
...product,
|
|
41
|
+
price,
|
|
42
|
+
compareAtPrice: null,
|
|
43
|
+
currency: market.currency,
|
|
44
|
+
variants: product.variants?.map((v) => ({ ...v, price: marketUnitPrice(state, product.id, v.id, v.price ?? product.price), compareAtPrice: null }))
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
var HINT_TIMEOUT_MS = 1200;
|
|
48
|
+
var MAX_SAVED_ADDRESSES = 8;
|
|
49
|
+
function newAddressId() {
|
|
50
|
+
const c = globalThis.crypto;
|
|
51
|
+
if (c && typeof c.randomUUID === "function") return c.randomUUID();
|
|
52
|
+
return `addr-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
53
|
+
}
|
|
54
|
+
var EMPTY = {
|
|
55
|
+
postalCode: "",
|
|
56
|
+
country: "BR",
|
|
57
|
+
address: null,
|
|
58
|
+
options: [],
|
|
59
|
+
selectedZoneId: null,
|
|
60
|
+
status: "idle",
|
|
61
|
+
error: null,
|
|
62
|
+
errorKey: null,
|
|
63
|
+
errorVars: null,
|
|
64
|
+
refusedZone: null,
|
|
65
|
+
reviewZone: null
|
|
66
|
+
};
|
|
67
|
+
var useDeliveryStore = create()(
|
|
68
|
+
persist(
|
|
69
|
+
(set, get) => ({
|
|
70
|
+
...EMPTY,
|
|
71
|
+
// 'delivery' é o default porque é o que toda loja anterior à 0094 é, e o
|
|
72
|
+
// que uma loja sem retirada continua sendo.
|
|
73
|
+
fulfillmentType: "delivery",
|
|
74
|
+
scheduledFor: null,
|
|
75
|
+
setFulfillmentType(type) {
|
|
76
|
+
if (get().fulfillmentType === type) return;
|
|
77
|
+
set({ fulfillmentType: type, scheduledFor: null });
|
|
78
|
+
},
|
|
79
|
+
setScheduledFor(startsAt) {
|
|
80
|
+
set({ scheduledFor: startsAt });
|
|
81
|
+
},
|
|
82
|
+
async resolve(postalCode, subtotal, country = "BR") {
|
|
83
|
+
const iso2 = country.toUpperCase();
|
|
84
|
+
if (iso2 !== "BR") {
|
|
85
|
+
const schema = getCountrySchema(iso2);
|
|
86
|
+
const code2 = schema.postal.normalize(postalCode);
|
|
87
|
+
if (!schema.postal.pattern.test(code2)) {
|
|
88
|
+
set({
|
|
89
|
+
...EMPTY,
|
|
90
|
+
postalCode,
|
|
91
|
+
country: iso2,
|
|
92
|
+
status: "error",
|
|
93
|
+
error: `Confira o c\xF3digo postal (ex.: ${schema.postal.example}).`,
|
|
94
|
+
errorKey: "delivery.error.postal",
|
|
95
|
+
errorVars: { example: schema.postal.example }
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const address2 = await lookupPostalCode(code2, iso2).catch(() => null);
|
|
100
|
+
set({ ...EMPTY, postalCode: code2, country: iso2, address: address2, status: "idle" });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const code = normalizePostalCode(postalCode);
|
|
104
|
+
if (code.length !== 8) {
|
|
105
|
+
set({ ...EMPTY, postalCode, status: "error", error: "Digite os 8 d\xEDgitos do CEP.", errorKey: "delivery.error.digits" });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
set({ postalCode: code, country: "BR", status: "loading", error: null, errorKey: null, errorVars: null });
|
|
109
|
+
const shop = getShopProvider();
|
|
110
|
+
const addressPromise = lookupPostalCode(code).catch(() => null);
|
|
111
|
+
const hint = await Promise.race([
|
|
112
|
+
addressPromise.then((a) => a ? { district: a.district, city: a.city, state: a.state, cityCode: a.cityCode } : void 0),
|
|
113
|
+
new Promise((resolve) => {
|
|
114
|
+
setTimeout(() => resolve(void 0), HINT_TIMEOUT_MS);
|
|
115
|
+
})
|
|
116
|
+
]);
|
|
117
|
+
const checkPromise = shop.checkDelivery ? shop.checkDelivery(code, hint).catch(() => null) : Promise.resolve(null);
|
|
118
|
+
let options = [];
|
|
119
|
+
try {
|
|
120
|
+
options = await shop.quoteShipping?.(code, subtotal, hint) ?? [];
|
|
121
|
+
} catch {
|
|
122
|
+
set({
|
|
123
|
+
...EMPTY,
|
|
124
|
+
postalCode: code,
|
|
125
|
+
status: "error",
|
|
126
|
+
error: "N\xE3o foi poss\xEDvel calcular o frete agora. Tente de novo.",
|
|
127
|
+
errorKey: "delivery.error.quote"
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const check = await checkPromise;
|
|
132
|
+
const refusedZone = check?.refused ? check.refusedZone || "Regi\xE3o n\xE3o atendida" : null;
|
|
133
|
+
const reviewZone = !refusedZone && check?.outcome === "review" ? check.zoneName || "Regi\xE3o a confirmar" : null;
|
|
134
|
+
if (refusedZone) options = [];
|
|
135
|
+
const address = await addressPromise;
|
|
136
|
+
const notFound = address === null;
|
|
137
|
+
set({
|
|
138
|
+
postalCode: code,
|
|
139
|
+
address,
|
|
140
|
+
options,
|
|
141
|
+
selectedZoneId: options[0]?.zoneId ?? null,
|
|
142
|
+
refusedZone,
|
|
143
|
+
reviewZone,
|
|
144
|
+
// A store with no zones configured quotes nothing, and that must NOT
|
|
145
|
+
// read as "we don't deliver here" — it falls back to the store-wide
|
|
146
|
+
// flat rate, which the cart already knows how to show.
|
|
147
|
+
status: options.length > 0 ? "served" : "unserved",
|
|
148
|
+
// Reported alongside the price, not instead of it: the CEP may be
|
|
149
|
+
// mistyped even though its range is covered.
|
|
150
|
+
error: notFound ? "CEP n\xE3o encontrado. Confira os n\xFAmeros." : null,
|
|
151
|
+
errorKey: notFound ? "delivery.error.notFound" : null,
|
|
152
|
+
errorVars: null
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
selectZone(zoneId) {
|
|
156
|
+
if (!get().options.some((option) => option.zoneId === zoneId)) return;
|
|
157
|
+
set({ selectedZoneId: zoneId });
|
|
158
|
+
void getShopProvider().selectShippingOption?.(zoneId)?.catch(() => {
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
clear() {
|
|
162
|
+
set({ ...EMPTY });
|
|
163
|
+
},
|
|
164
|
+
savedAddresses: [],
|
|
165
|
+
selectedAddressId: null,
|
|
166
|
+
saveAddress(input) {
|
|
167
|
+
const id = newAddressId();
|
|
168
|
+
const entry = { ...input, id, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
169
|
+
set((state) => ({
|
|
170
|
+
// Mesmo CEP + mesmo número é o MESMO endereço com o complemento
|
|
171
|
+
// corrigido, não um segundo. Sem isto, três tentativas de acertar o
|
|
172
|
+
// apartamento viram três linhas idênticas na lista.
|
|
173
|
+
savedAddresses: [
|
|
174
|
+
entry,
|
|
175
|
+
...state.savedAddresses.filter(
|
|
176
|
+
(a) => normalizePostalCode(a.postalCode) !== normalizePostalCode(entry.postalCode) || a.number.trim().toLowerCase() !== entry.number.trim().toLowerCase()
|
|
177
|
+
)
|
|
178
|
+
].slice(0, MAX_SAVED_ADDRESSES),
|
|
179
|
+
selectedAddressId: id
|
|
180
|
+
}));
|
|
181
|
+
return id;
|
|
182
|
+
},
|
|
183
|
+
async selectAddress(id, subtotal) {
|
|
184
|
+
const entry = get().savedAddresses.find((a) => a.id === id);
|
|
185
|
+
if (!entry) return;
|
|
186
|
+
set({ selectedAddressId: id });
|
|
187
|
+
await get().resolve(entry.postalCode, subtotal);
|
|
188
|
+
},
|
|
189
|
+
removeAddress(id) {
|
|
190
|
+
set((state) => ({
|
|
191
|
+
savedAddresses: state.savedAddresses.filter((a) => a.id !== id),
|
|
192
|
+
selectedAddressId: state.selectedAddressId === id ? null : state.selectedAddressId
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
}),
|
|
196
|
+
{
|
|
197
|
+
name: "fayz.storefront.delivery.v1",
|
|
198
|
+
// Deliberately NOT persisting `options`: a rehydrated
|
|
199
|
+
// price would be shown as current without anything having quoted it. The
|
|
200
|
+
// address survives, the money is asked for again.
|
|
201
|
+
// `fulfillmentType` acompanha o endereço e pela mesma razão: é uma
|
|
202
|
+
// preferência ("eu sempre busco na loja"), e ela vale amanhã.
|
|
203
|
+
// `scheduledFor` NÃO acompanha, pela mesma razão de `options`: um horário
|
|
204
|
+
// escolhido ontem é um horário que já passou, e restaurá-lo mandaria o
|
|
205
|
+
// comprador ao checkout com uma janela que o servidor vai recusar.
|
|
206
|
+
partialize: (state) => ({
|
|
207
|
+
postalCode: state.postalCode,
|
|
208
|
+
country: state.country,
|
|
209
|
+
address: state.address,
|
|
210
|
+
fulfillmentType: state.fulfillmentType,
|
|
211
|
+
// O caderno é o ponto: número e complemento são o que o comprador
|
|
212
|
+
// digita de novo a cada visita, e o que nenhuma consulta de CEP
|
|
213
|
+
// devolve. Sem persistir, salvar endereço não salva nada.
|
|
214
|
+
savedAddresses: state.savedAddresses,
|
|
215
|
+
selectedAddressId: state.selectedAddressId
|
|
216
|
+
}),
|
|
217
|
+
onRehydrateStorage: () => (state) => {
|
|
218
|
+
if (state?.address) state.status = "idle";
|
|
219
|
+
if (state && !state.country) state.country = "BR";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
);
|
|
224
|
+
function resolveOptionRate(option, subtotal) {
|
|
225
|
+
if (option.freeAbove != null && subtotal >= option.freeAbove) return 0;
|
|
226
|
+
return option.baseRate;
|
|
227
|
+
}
|
|
228
|
+
var selectQuotedShipping = (state, subtotal) => {
|
|
229
|
+
if (state.status !== "served") return null;
|
|
230
|
+
const option = state.options.find((o) => o.zoneId === state.selectedZoneId) ?? state.options[0];
|
|
231
|
+
return option ? resolveOptionRate(option, subtotal) : null;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// src/data/product-options.ts
|
|
235
|
+
function cleanValues(value) {
|
|
236
|
+
if (!Array.isArray(value)) return [];
|
|
237
|
+
return Array.from(new Set(value.filter((item) => typeof item === "string" && item.trim().length > 0)));
|
|
238
|
+
}
|
|
239
|
+
function cleanOptionGroup(value) {
|
|
240
|
+
if (!value || typeof value !== "object") return null;
|
|
241
|
+
const record = value;
|
|
242
|
+
const id = typeof record.id === "string" ? record.id : typeof record.name === "string" ? record.name : null;
|
|
243
|
+
const label = typeof record.label === "string" ? record.label : typeof record.name === "string" ? record.name : id;
|
|
244
|
+
const values = cleanValues(record.values);
|
|
245
|
+
if (!id || !label || values.length === 0) return null;
|
|
246
|
+
return { id, label, values };
|
|
247
|
+
}
|
|
248
|
+
var warnedOverlayProducts = /* @__PURE__ */ new Set();
|
|
249
|
+
function resetProductOptionsDeprecationWarnings() {
|
|
250
|
+
warnedOverlayProducts.clear();
|
|
251
|
+
}
|
|
252
|
+
function getProductOptionGroups(product) {
|
|
253
|
+
if (product.options && product.options.length > 0) {
|
|
254
|
+
return [...product.options].sort((a, b) => a.position - b.position).map((option) => ({ id: option.name, label: option.name, values: [...option.values] }));
|
|
255
|
+
}
|
|
256
|
+
const options = product.metadata.options;
|
|
257
|
+
const explicit = Array.isArray(options) ? options.map(cleanOptionGroup).filter((group) => Boolean(group)) : [];
|
|
258
|
+
const groups = [...explicit];
|
|
259
|
+
if (groups.length === 0) {
|
|
260
|
+
const sizes = cleanValues(product.metadata.sizes);
|
|
261
|
+
const colors = cleanValues(product.metadata.colors);
|
|
262
|
+
if (sizes.length > 0) groups.push({ id: "Tamanho", label: "Tamanho", values: sizes });
|
|
263
|
+
if (colors.length > 0) groups.push({ id: "Cor", label: "Cor", values: colors });
|
|
264
|
+
}
|
|
265
|
+
if (groups.length > 0 && !warnedOverlayProducts.has(product.id)) {
|
|
266
|
+
warnedOverlayProducts.add(product.id);
|
|
267
|
+
console.warn(
|
|
268
|
+
`[storefront] product "${product.slug}" renders options from the DEPRECATED metadata overlay (metadata.options/sizes/colors). These labels are display-only \u2014 checkout cannot price or reserve them. Define real variants (0028) instead; the overlay path is removed at M9.`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
return groups;
|
|
272
|
+
}
|
|
273
|
+
function resolveVariantForSelection(product, selection) {
|
|
274
|
+
const variants = product.variants;
|
|
275
|
+
if (!variants || variants.length === 0) return null;
|
|
276
|
+
const normalized = normalizeProductOptionSelection(selection);
|
|
277
|
+
const wanted = Object.entries(normalized);
|
|
278
|
+
return variants.find((variant) => {
|
|
279
|
+
const options = variant.options;
|
|
280
|
+
return Object.keys(options).length === wanted.length && wanted.every(([key, value]) => options[key] === value);
|
|
281
|
+
}) ?? null;
|
|
282
|
+
}
|
|
283
|
+
function selectionPrice(product, selection) {
|
|
284
|
+
const variant = resolveVariantForSelection(product, selection);
|
|
285
|
+
return variant?.price ?? product.price;
|
|
286
|
+
}
|
|
287
|
+
function normalizeProductOptionSelection(selection) {
|
|
288
|
+
if (!selection) return {};
|
|
289
|
+
return Object.fromEntries(
|
|
290
|
+
Object.entries(selection).filter((entry) => entry[0].trim().length > 0 && entry[1].trim().length > 0).sort(([a], [b]) => a.localeCompare(b))
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
function formatProductOptionSelection(selection) {
|
|
294
|
+
return Object.entries(normalizeProductOptionSelection(selection)).map(([label, value]) => `${label}: ${value}`).join(" \xB7 ");
|
|
295
|
+
}
|
|
296
|
+
function productOptionSelectionKey(selection) {
|
|
297
|
+
const normalized = normalizeProductOptionSelection(selection);
|
|
298
|
+
const entries = Object.entries(normalized);
|
|
299
|
+
return entries.length > 0 ? JSON.stringify(entries) : "";
|
|
300
|
+
}
|
|
301
|
+
var useCartStore = create()(
|
|
302
|
+
persist(
|
|
303
|
+
(set, get) => ({
|
|
304
|
+
lines: [],
|
|
305
|
+
discountCode: null,
|
|
306
|
+
discountPercent: 0,
|
|
307
|
+
discountFreeShipping: false,
|
|
308
|
+
isOpen: false,
|
|
309
|
+
justAddedLineId: null,
|
|
310
|
+
addItem: (product, qty = 1, options, notes, opts) => set((state) => {
|
|
311
|
+
const normalizedOptions = normalizeProductOptionSelection(options);
|
|
312
|
+
const optionKey = productOptionSelectionKey(normalizedOptions);
|
|
313
|
+
const cleanNotes = notes?.trim().slice(0, 250) || void 0;
|
|
314
|
+
const baseId = optionKey ? `${product.id}:${optionKey}` : product.id;
|
|
315
|
+
const lineId = cleanNotes ? `${baseId}#${cleanNotes}` : baseId;
|
|
316
|
+
const variant = resolveVariantForSelection(product, normalizedOptions);
|
|
317
|
+
const basePrice = variant?.price ?? product.price;
|
|
318
|
+
const unitPrice = marketUnitPrice(useMarketStore.getState(), product.id, variant?.id, basePrice);
|
|
319
|
+
if (unitPrice == null) return state;
|
|
320
|
+
const stock = variant ? variant.inventoryCount : product.inventoryCount;
|
|
321
|
+
const existing = state.lines.find((l) => (l.lineId ?? l.productId) === lineId);
|
|
322
|
+
if (existing) {
|
|
323
|
+
const nextQty = Math.min(existing.quantity + qty, existing.maxQuantity || Infinity);
|
|
324
|
+
return {
|
|
325
|
+
// Auto-open the cart on add — the standard premium "slide-in cart"
|
|
326
|
+
// signal — and flag the line so the drawer can highlight it.
|
|
327
|
+
isOpen: opts?.openDrawer ?? true,
|
|
328
|
+
justAddedLineId: lineId,
|
|
329
|
+
lines: state.lines.map(
|
|
330
|
+
(l) => (l.lineId ?? l.productId) === lineId ? { ...l, lineId, quantity: nextQty } : l
|
|
331
|
+
)
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const image = product.images.find((i) => i.isPrimary) ?? product.images[0];
|
|
335
|
+
const line = {
|
|
336
|
+
lineId,
|
|
337
|
+
productId: product.id,
|
|
338
|
+
variantId: variant?.id,
|
|
339
|
+
slug: product.slug,
|
|
340
|
+
name: product.name,
|
|
341
|
+
sku: variant?.sku ?? product.sku,
|
|
342
|
+
options: normalizedOptions,
|
|
343
|
+
optionsLabel: formatProductOptionSelection(normalizedOptions),
|
|
344
|
+
unitPrice,
|
|
345
|
+
basePrice,
|
|
346
|
+
quantity: Math.min(qty, stock || Infinity),
|
|
347
|
+
imageUrl: image?.url ?? null,
|
|
348
|
+
maxQuantity: stock,
|
|
349
|
+
requiresShipping: product.isPhysical !== false,
|
|
350
|
+
weight: variant?.weight ?? product.weight ?? null,
|
|
351
|
+
weightUnit: variant?.weightUnit ?? product.weightUnit ?? null,
|
|
352
|
+
heightCm: variant?.heightCm ?? product.heightCm ?? null,
|
|
353
|
+
widthCm: variant?.widthCm ?? product.widthCm ?? null,
|
|
354
|
+
lengthCm: variant?.lengthCm ?? product.lengthCm ?? null,
|
|
355
|
+
packageId: product.packageId ?? null,
|
|
356
|
+
notes: cleanNotes
|
|
357
|
+
};
|
|
358
|
+
return {
|
|
359
|
+
isOpen: opts?.openDrawer ?? true,
|
|
360
|
+
justAddedLineId: lineId,
|
|
361
|
+
lines: [...state.lines, line]
|
|
362
|
+
};
|
|
363
|
+
}),
|
|
364
|
+
setQuantity: (lineId, qty) => set((state) => {
|
|
365
|
+
if (qty <= 0) return { lines: state.lines.filter((l) => (l.lineId ?? l.productId) !== lineId) };
|
|
366
|
+
return {
|
|
367
|
+
lines: state.lines.map(
|
|
368
|
+
(l) => (l.lineId ?? l.productId) === lineId ? { ...l, quantity: Math.min(qty, l.maxQuantity || Infinity) } : l
|
|
369
|
+
)
|
|
370
|
+
};
|
|
371
|
+
}),
|
|
372
|
+
removeItem: (lineId) => set((state) => ({ lines: state.lines.filter((l) => (l.lineId ?? l.productId) !== lineId) })),
|
|
373
|
+
applyDiscount: (code, percent, options) => set({
|
|
374
|
+
discountCode: code,
|
|
375
|
+
discountPercent: percent,
|
|
376
|
+
discountFreeShipping: options?.freeShipping === true
|
|
377
|
+
}),
|
|
378
|
+
clearDiscount: () => set({ discountCode: null, discountPercent: 0, discountFreeShipping: false }),
|
|
379
|
+
clear: () => set({ lines: [], discountCode: null, discountPercent: 0, discountFreeShipping: false }),
|
|
380
|
+
repriceForMarket: () => set((state) => {
|
|
381
|
+
const market = useMarketStore.getState();
|
|
382
|
+
let changed = false;
|
|
383
|
+
const lines = state.lines.flatMap((line) => {
|
|
384
|
+
const price = marketUnitPrice(market, line.productId, line.variantId, line.basePrice ?? line.unitPrice);
|
|
385
|
+
if (price == null) {
|
|
386
|
+
changed = true;
|
|
387
|
+
return [];
|
|
388
|
+
}
|
|
389
|
+
if (price !== line.unitPrice) changed = true;
|
|
390
|
+
return [{ ...line, unitPrice: price }];
|
|
391
|
+
});
|
|
392
|
+
return changed ? { lines } : state;
|
|
393
|
+
}),
|
|
394
|
+
openDrawer: () => set({ isOpen: true }),
|
|
395
|
+
closeDrawer: () => set({ isOpen: false, justAddedLineId: null }),
|
|
396
|
+
reconcile: async (resolve) => {
|
|
397
|
+
const lines = get().lines;
|
|
398
|
+
if (lines.length === 0) return;
|
|
399
|
+
const keep = await Promise.all(
|
|
400
|
+
lines.map((line) => resolve(line).catch(() => true))
|
|
401
|
+
);
|
|
402
|
+
const kept = lines.filter((_, i) => keep[i]);
|
|
403
|
+
if (kept.length !== lines.length) set({ lines: kept });
|
|
404
|
+
}
|
|
405
|
+
}),
|
|
406
|
+
{
|
|
407
|
+
name: "fayz.storefront.cart.v1",
|
|
408
|
+
partialize: (state) => ({
|
|
409
|
+
lines: state.lines,
|
|
410
|
+
discountCode: state.discountCode,
|
|
411
|
+
discountPercent: state.discountPercent,
|
|
412
|
+
discountFreeShipping: state.discountFreeShipping
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
)
|
|
416
|
+
);
|
|
417
|
+
var selectCount = (s) => s.lines.reduce((n, l) => n + l.quantity, 0);
|
|
418
|
+
var selectSubtotal = (s) => roundCents(s.lines.reduce((sum, l) => sum + l.unitPrice * l.quantity, 0));
|
|
419
|
+
var selectDiscountTotal = (s) => roundCents(selectSubtotal(s) * (s.discountPercent / 100));
|
|
420
|
+
var selectRequiresShipping = (s) => s.lines.some((l) => l.requiresShipping !== false);
|
|
421
|
+
var selectShipping = (s, cfg) => {
|
|
422
|
+
if (s.lines.length === 0) return 0;
|
|
423
|
+
if (!selectRequiresShipping(s)) return 0;
|
|
424
|
+
if (s.discountFreeShipping) return 0;
|
|
425
|
+
const subtotal = selectSubtotal(s);
|
|
426
|
+
const quoted = selectQuotedShipping(useDeliveryStore.getState(), subtotal);
|
|
427
|
+
if (quoted != null) return quoted;
|
|
428
|
+
if (cfg.shipping.freeAbove != null && subtotal >= cfg.shipping.freeAbove) return 0;
|
|
429
|
+
return cfg.shipping.flatRate;
|
|
430
|
+
};
|
|
431
|
+
var selectTotal = (s, cfg) => roundCents(selectSubtotal(s) - selectDiscountTotal(s) + selectShipping(s, cfg));
|
|
432
|
+
|
|
433
|
+
export { formatProductOptionSelection, getProductOptionGroups, marketForCountry, marketUnitPrice, normalizeProductOptionSelection, productOptionSelectionKey, resetProductOptionsDeprecationWarnings, resolveOptionRate, resolveVariantForSelection, selectCount, selectDiscountTotal, selectRequiresShipping, selectShipping, selectSubtotal, selectTotal, selectionPrice, useCartStore, useDeliveryStore, useMarketStore, withMarketPrice };
|
|
434
|
+
//# sourceMappingURL=chunk-K27LYMSF.js.map
|
|
435
|
+
//# sourceMappingURL=chunk-K27LYMSF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stores/market.store.ts","../src/stores/delivery.store.ts","../src/data/product-options.ts","../src/stores/cart.store.ts"],"names":["create","code","address","persist"],"mappings":"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","file":"chunk-K27LYMSF.js","sourcesContent":["import { create } from 'zustand'\nimport type { MarketPrice, Product, ShopMarket } from '@fayz-ai/shop/types'\n\n// The store's markets (0116) and the active one's price list.\n//\n// Module-level on purpose, like the cart: the cart prices a line when it is\n// added, far from any render, and must read the same numbers the page shows.\n// Not persisted: prices are asked for again on every visit.\n\nexport interface MarketState {\n markets: ShopMarket[]\n /** The market the buyer is in, null when the store has none. */\n active: ShopMarket | null\n /** code → `${productId}:${variantId ?? ''}` → price. */\n prices: Record<string, Record<string, number>>\n setMarkets(markets: ShopMarket[]): void\n setActive(market: ShopMarket | null): void\n setPrices(code: string, list: MarketPrice[]): void\n}\n\nexport const useMarketStore = create<MarketState>()((set) => ({\n markets: [],\n active: null,\n prices: {},\n setMarkets: (markets) => set({ markets }),\n setActive: (active) => set({ active }),\n setPrices: (code, list) =>\n set((state) => ({\n prices: {\n ...state.prices,\n [code]: Object.fromEntries(list.map((p) => [`${p.productId}:${p.variantId ?? ''}`, p.price])),\n },\n })),\n}))\n\n/** The market for a country: the one that ships there, else the default. */\nexport function marketForCountry(markets: readonly ShopMarket[], country: string): ShopMarket | null {\n const iso2 = country.toUpperCase()\n return markets.find((m) => m.countries.includes(iso2)) ?? markets.find((m) => m.isDefault) ?? null\n}\n\n/**\n * A unit price in the active market, by shop_market_unit_price's rule: the\n * market's price (variant over product), the catalogue's own in the default\n * market, and `null` — not for sale here — anywhere else.\n */\nexport function marketUnitPrice(\n state: Pick<MarketState, 'active' | 'prices'>,\n productId: string,\n variantId: string | null | undefined,\n fallback: number,\n): number | null {\n const market = state.active\n if (!market) return fallback\n const list = state.prices[market.code]\n const hit = list?.[`${productId}:${variantId ?? ''}`] ?? list?.[`${productId}:`]\n if (hit != null) return hit\n return market.isDefault ? fallback : null\n}\n\n/**\n * The product as the buyer's market sells it: its price, no compare-at (that\n * number is in the default market's money), and out of stock where the market\n * has no price — the page must not offer a sale the checkout will refuse.\n */\nexport function withMarketPrice(product: Product, state: Pick<MarketState, 'active' | 'prices'> = useMarketStore.getState()): Product {\n const market = state.active\n if (!market || market.isDefault) return product\n const price = marketUnitPrice(state, product.id, null, product.price)\n if (price == null) {\n return { ...product, compareAtPrice: null, currency: market.currency, inventoryCount: 0, metadata: { ...product.metadata, unavailableInMarket: market.code } }\n }\n return {\n ...product,\n price,\n compareAtPrice: null,\n currency: market.currency,\n variants: product.variants?.map((v) => ({ ...v, price: marketUnitPrice(state, product.id, v.id, v.price ?? product.price), compareAtPrice: null })),\n }\n}\n","import { create } from 'zustand'\nimport { persist } from 'zustand/middleware'\nimport { getCountrySchema, lookupPostalCode, normalizePostalCode, type PostalAddress } from '@fayz-ai/core'\nimport { getShopProvider } from '@fayz-ai/shop/runtime'\nimport type { FulfillmentType, ShippingQuoteOption } from '@fayz-ai/shop/types'\nimport type { MessageKey } from '../locales/messages'\n\n/**\n * Where the shopper wants it delivered, decided before the cart exists.\n *\n * Two different things live here and they are persisted differently on purpose:\n *\n * the ADDRESS is durable — a CEP maps to the same street for years, and\n * carrying it to checkout is the whole point (the buyer types the number and\n * nothing else);\n *\n * the QUOTE is not persisted at all. It is a price, and a price restored from\n * localStorage is a price the shopper can edit with devtools. It is re-asked\n * on demand, and resolved for the current cart through resolveOptionRate —\n * the same rule shop_shipping_for applies — so it can never go stale. The\n * order is charged from shop_place_order, which recomputes from the same\n * zones and ignores anything the client sends.\n */\n\nexport type DeliveryStatus = 'idle' | 'loading' | 'served' | 'unserved' | 'error'\n\n/**\n * Um endereço que o comprador salvou NESTE navegador.\n *\n * Existe porque o caderno de endereços do `plugin-shop` é de quem tem conta, e\n * num delivery a pessoa tem endereço antes de ter login — é a primeira coisa\n * que ela informa e a última que quer digitar de novo. O CEP resolve rua,\n * bairro e cidade; o que NENHUMA consulta de CEP sabe é o número, o\n * complemento e \"o portão azul ao lado da padaria\". É exatamente isso que se\n * perde a cada visita quando não se guarda nada.\n *\n * Fica no navegador e só nele. Não é conta, não sincroniza entre aparelhos e\n * não vai para o servidor por conta própria — quem grava endereço no pedido é\n * o checkout, com o que a pessoa confirmou na hora.\n */\nexport interface SavedDeliveryAddress {\n id: string\n /** \"Casa\", \"Trabalho\"… Opcional: a maioria salva um só e não nomeia nada. */\n label: string | null\n postalCode: string\n street: string\n number: string\n complement: string | null\n /** O ponto de referência — \"portão azul\", \"prédio ao lado do mercado\". */\n reference: string | null\n district: string\n city: string\n state: string\n /** ISO2. Absent on everything saved before countries: those are Brazilian. */\n country?: string\n createdAt: string\n}\n\n/** How long the quote waits for the postcode lookup before going without it.\n * Short on purpose: the hint improves the answer, it must never delay it. */\nconst HINT_TIMEOUT_MS = 1200\n\nexport interface DeliveryState {\n postalCode: string\n /** ISO2 the postal code belongs to. 'BR' for everything stored before countries. */\n country: string\n address: PostalAddress | null\n options: ShippingQuoteOption[]\n selectedZoneId: string | null\n status: DeliveryStatus\n error: string | null\n /** `error` as a message key, so a screen can say it in the buyer's language. */\n errorKey: MessageKey | null\n errorVars: Record<string, string> | null\n /**\n * The name of the exclusion zone covering this CEP, when the store REFUSES the\n * region (0048). Null when it does not.\n *\n * Carried as a separate field rather than a fourth status because every\n * consumer already treats `unserved` as \"no delivery here\" and that is exactly\n * what this is — the field only lets the message say WHY.\n */\n refusedZone: string | null\n\n /**\n * The name of the rule that says this address is delivered but has to be\n * CONFIRMED first (0081) — the community that shares a postcode with the\n * neighbourhood next door.\n *\n * Not a status either, and for the opposite reason to `refusedZone`: the\n * order goes through and the price is real. Every consumer that shows a price\n * keeps showing it; this only adds a line saying the store will confirm.\n */\n reviewZone: string | null\n\n /** Look the postal code up and quote it. `subtotal` decides free-above\n * thresholds; `country` (ISO2, default 'BR') decides what a code looks like. */\n resolve: (postalCode: string, subtotal: number, country?: string) => Promise<void>\n selectZone: (zoneId: string) => void\n clear: () => void\n\n // -------------------------------------------------------------------------\n // O caderno de endereços do navegador\n // -------------------------------------------------------------------------\n savedAddresses: SavedDeliveryAddress[]\n selectedAddressId: string | null\n /** Grava e passa a ser o endereço corrente. Devolve o id gravado. */\n saveAddress: (input: Omit<SavedDeliveryAddress, 'id' | 'createdAt'>) => string\n /** Escolhe um já gravado e recota o frete para ele. */\n selectAddress: (id: string, subtotal: number) => Promise<void>\n removeAddress: (id: string) => void\n\n // -------------------------------------------------------------------------\n // 0094 — entrega × retirada, e quando\n // -------------------------------------------------------------------------\n /**\n * Mora AQUI e não numa store nova de propósito: esta já é a store do \"para\n * onde e como isso chega\", já persiste o endereço e já é lida pelo checkout.\n * Uma segunda store com a metade da resposta acabaria discordando desta na\n * primeira tela que lesse só uma das duas.\n */\n fulfillmentType: FulfillmentType\n /** Início da janela escolhida (ISO). null = o quanto antes. */\n scheduledFor: string | null\n setFulfillmentType: (type: FulfillmentType) => void\n setScheduledFor: (startsAt: string | null) => void\n}\n\n/** Quantos endereços o caderno guarda. Uma lista longa deixa de ser um atalho. */\nconst MAX_SAVED_ADDRESSES = 8\n\n/** `crypto.randomUUID` quando existe; um id local quando não (Safari antigo,\n * contexto sem crypto). O id só precisa ser único NESTE navegador. */\nfunction newAddressId(): string {\n const c = globalThis.crypto\n if (c && typeof c.randomUUID === 'function') return c.randomUUID()\n return `addr-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`\n}\n\nconst EMPTY = {\n postalCode: '',\n country: 'BR',\n address: null,\n options: [] as ShippingQuoteOption[],\n selectedZoneId: null,\n status: 'idle' as DeliveryStatus,\n error: null,\n errorKey: null,\n errorVars: null,\n refusedZone: null,\n reviewZone: null,\n}\n\nexport const useDeliveryStore = create<DeliveryState>()(\n persist(\n (set, get) => ({\n ...EMPTY,\n\n // 'delivery' é o default porque é o que toda loja anterior à 0094 é, e o\n // que uma loja sem retirada continua sendo.\n fulfillmentType: 'delivery' as FulfillmentType,\n scheduledFor: null,\n\n setFulfillmentType(type: FulfillmentType) {\n // Trocar de modo derruba o agendamento: as janelas de entrega e de\n // retirada são cadastradas separadamente (0094 aceita fulfillment_type\n // por linha de horário), então um horário escolhido para entregar pode\n // não existir para retirar. Guardá-lo seria guardar uma escolha que o\n // servidor vai recusar no fim do checkout.\n if (get().fulfillmentType === type) return\n set({ fulfillmentType: type, scheduledFor: null })\n },\n\n setScheduledFor(startsAt: string | null) {\n set({ scheduledFor: startsAt })\n },\n\n async resolve(postalCode: string, subtotal: number, country = 'BR') {\n const iso2 = country.toUpperCase()\n if (iso2 !== 'BR') {\n // Outside Brazil there are no zones or carrier to ask yet: the store's\n // flat rate applies, in the cart and in shop_shipping_for alike. What\n // the code CAN tell is its region (Spain: the province), offline.\n const schema = getCountrySchema(iso2)\n const code = schema.postal.normalize(postalCode)\n if (!schema.postal.pattern.test(code)) {\n set({\n ...EMPTY, postalCode, country: iso2, status: 'error',\n error: `Confira o código postal (ex.: ${schema.postal.example}).`,\n errorKey: 'delivery.error.postal', errorVars: { example: schema.postal.example },\n })\n return\n }\n const address = await lookupPostalCode(code, iso2).catch(() => null)\n set({ ...EMPTY, postalCode: code, country: iso2, address, status: 'idle' })\n return\n }\n\n const code = normalizePostalCode(postalCode)\n if (code.length !== 8) {\n set({ ...EMPTY, postalCode, status: 'error', error: 'Digite os 8 dígitos do CEP.', errorKey: 'delivery.error.digits' })\n return\n }\n\n set({ postalCode: code, country: 'BR', status: 'loading', error: null, errorKey: null, errorVars: null })\n\n // 0081 changed what the quote needs to know. A rule can now name a\n // MUNICIPALITY or a NEIGHBOURHOOD, and eight digits alone do not carry\n // either — so the lookup has to happen before the quote, not after it.\n //\n // The original rule still stands, though, and it is why this is a race\n // and not a plain await: with ViaCEP slow or rate-limited, a perfectly\n // serviceable address used to report an error instead of a price. So the\n // lookup gets a short window to answer and the quote goes out with or\n // without it. The server is the authority either way — it prefers the\n // address IT resolved (public.postal_areas) and treats this as a hint.\n const shop = getShopProvider()\n\n const addressPromise = lookupPostalCode(code).catch(() => null)\n const hint = await Promise.race([\n addressPromise.then((a) => (a\n ? { district: a.district, city: a.city, state: a.state, cityCode: a.cityCode }\n : undefined)),\n new Promise<undefined>((resolve) => { setTimeout(() => resolve(undefined), HINT_TIMEOUT_MS) }),\n ])\n\n // The store's own refusal (0048), asked CONCURRENTLY with the quote —\n // both are one RPC against the same eight digits, and the shopper is on\n // the page waiting for a number.\n //\n // It is a different question from \"does a zone cover this\": an exclusion\n // beats every zone AND a carrier's quote, because the refusal is about\n // the store and not about who carries the parcel. So it empties the\n // options even when a carrier just answered with a price.\n //\n // Swallowing the failure is deliberate: the order itself is refused\n // server-side either way, so a check that cannot run costs the shopper a\n // clearer message, never a sale that should not have happened.\n const checkPromise = shop.checkDelivery\n ? shop.checkDelivery(code, hint).catch(() => null)\n : Promise.resolve(null)\n\n let options: ShippingQuoteOption[] = []\n try {\n options = (await shop.quoteShipping?.(code, subtotal, hint)) ?? []\n } catch {\n set({ ...EMPTY, postalCode: code, status: 'error',\n error: 'Não foi possível calcular o frete agora. Tente de novo.', errorKey: 'delivery.error.quote' })\n return\n }\n\n const check = await checkPromise\n const refusedZone = check?.refused ? (check.refusedZone || 'Região não atendida') : null\n const reviewZone = !refusedZone && check?.outcome === 'review'\n ? (check.zoneName || 'Região a confirmar')\n : null\n if (refusedZone) options = []\n\n // The ADDRESS is the convenience layer: it saves the buyer typing, and\n // losing it costs typing, not the sale. Failures here never change the\n // delivery verdict already decided above.\n // Already in flight since before the quote — awaited here only to fill in\n // the street the buyer would otherwise type.\n const address = await addressPromise\n const notFound = address === null\n\n set({\n postalCode: code,\n address,\n options,\n selectedZoneId: options[0]?.zoneId ?? null,\n refusedZone,\n reviewZone,\n // A store with no zones configured quotes nothing, and that must NOT\n // read as \"we don't deliver here\" — it falls back to the store-wide\n // flat rate, which the cart already knows how to show.\n status: options.length > 0 ? 'served' : 'unserved',\n // Reported alongside the price, not instead of it: the CEP may be\n // mistyped even though its range is covered.\n error: notFound ? 'CEP não encontrado. Confira os números.' : null,\n errorKey: notFound ? 'delivery.error.notFound' : null,\n errorVars: null,\n })\n },\n\n selectZone(zoneId: string) {\n if (!get().options.some((option) => option.zoneId === zoneId)) return\n set({ selectedZoneId: zoneId })\n // Told to the server as well, because a CARRIER price is not something\n // the order can re-derive on its own the way it re-derives a zone: it\n // has to read back the option the shopper chose. Deliberately not\n // awaited — the radio button must not wait on a round trip, and the\n // server defaults to the cheapest option, so a lost call can only ever\n // charge less than what is on screen. Stores quoting from their own\n // zones do not implement this at all.\n void getShopProvider().selectShippingOption?.(zoneId)?.catch(() => {})\n },\n\n clear() {\n set({ ...EMPTY })\n },\n\n savedAddresses: [],\n selectedAddressId: null,\n\n saveAddress(input) {\n const id = newAddressId()\n const entry: SavedDeliveryAddress = { ...input, id, createdAt: new Date().toISOString() }\n set((state) => ({\n // Mesmo CEP + mesmo número é o MESMO endereço com o complemento\n // corrigido, não um segundo. Sem isto, três tentativas de acertar o\n // apartamento viram três linhas idênticas na lista.\n savedAddresses: [\n entry,\n ...state.savedAddresses.filter(\n (a) =>\n normalizePostalCode(a.postalCode) !== normalizePostalCode(entry.postalCode) ||\n a.number.trim().toLowerCase() !== entry.number.trim().toLowerCase(),\n ),\n ].slice(0, MAX_SAVED_ADDRESSES),\n selectedAddressId: id,\n }))\n return id\n },\n\n async selectAddress(id: string, subtotal: number) {\n const entry = get().savedAddresses.find((a) => a.id === id)\n if (!entry) return\n set({ selectedAddressId: id })\n // Recota SEMPRE, e não restaura um preço guardado: a zona pode ter\n // mudado desde que a pessoa salvou, e um frete vindo do localStorage é\n // um frete que devtools edita.\n await get().resolve(entry.postalCode, subtotal)\n },\n\n removeAddress(id: string) {\n set((state) => ({\n savedAddresses: state.savedAddresses.filter((a) => a.id !== id),\n selectedAddressId: state.selectedAddressId === id ? null : state.selectedAddressId,\n }))\n },\n }),\n {\n name: 'fayz.storefront.delivery.v1',\n // Deliberately NOT persisting `options`: a rehydrated\n // price would be shown as current without anything having quoted it. The\n // address survives, the money is asked for again.\n // `fulfillmentType` acompanha o endereço e pela mesma razão: é uma\n // preferência (\"eu sempre busco na loja\"), e ela vale amanhã.\n // `scheduledFor` NÃO acompanha, pela mesma razão de `options`: um horário\n // escolhido ontem é um horário que já passou, e restaurá-lo mandaria o\n // comprador ao checkout com uma janela que o servidor vai recusar.\n partialize: (state) => ({\n postalCode: state.postalCode,\n country: state.country,\n address: state.address,\n fulfillmentType: state.fulfillmentType,\n // O caderno é o ponto: número e complemento são o que o comprador\n // digita de novo a cada visita, e o que nenhuma consulta de CEP\n // devolve. Sem persistir, salvar endereço não salva nada.\n savedAddresses: state.savedAddresses,\n selectedAddressId: state.selectedAddressId,\n }),\n onRehydrateStorage: () => (state) => {\n if (state?.address) state.status = 'idle'\n // Stored before countries existed: every one of those was a CEP.\n if (state && !state.country) state.country = 'BR'\n },\n },\n ),\n)\n\n/**\n * The freight for the chosen zone at THIS subtotal, or null when there is no\n * zone in play (no CEP yet, or a store that configured none) and the caller\n * should use the store-wide rate. Null never means free.\n *\n * The free-above rule is applied here rather than trusting the `rate` the\n * server resolved, because that one was resolved for whatever the cart held at\n * quote time. Re-deriving from `baseRate`/`freeAbove` — the same CASE\n * shop_shipping_for runs — means adding an item can never leave a stale price\n * on screen. An earlier version fell back to the config flat rate when the\n * quote went stale, which showed R$ 8 (the flat rate) for an address in an\n * R$ 18 zone: shown one number, charged another.\n */\nexport function resolveOptionRate(option: ShippingQuoteOption, subtotal: number): number {\n if (option.freeAbove != null && subtotal >= option.freeAbove) return 0\n return option.baseRate\n}\n\nexport const selectQuotedShipping = (\n state: Pick<DeliveryState, 'options' | 'selectedZoneId' | 'status'>,\n subtotal: number,\n): number | null => {\n if (state.status !== 'served') return null\n const option = state.options.find((o) => o.zoneId === state.selectedZoneId) ?? state.options[0]\n return option ? resolveOptionRate(option, subtotal) : null\n}\n","import type { Product, ProductVariant } from '@fayz-ai/shop/types'\n\nexport interface ProductOptionGroup {\n id: string\n label: string\n values: string[]\n}\n\nexport type ProductOptionSelection = Record<string, string>\n\nfunction cleanValues(value: unknown): string[] {\n if (!Array.isArray(value)) return []\n return Array.from(new Set(value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0)))\n}\n\nfunction cleanOptionGroup(value: unknown): ProductOptionGroup | null {\n if (!value || typeof value !== 'object') return null\n const record = value as Record<string, unknown>\n const id = typeof record.id === 'string' ? record.id : typeof record.name === 'string' ? record.name : null\n const label = typeof record.label === 'string' ? record.label : typeof record.name === 'string' ? record.name : id\n const values = cleanValues(record.values)\n if (!id || !label || values.length === 0) return null\n return { id, label, values }\n}\n\n// I10: the overlay shim warns once per product, not per render — the console\n// is a signal channel, not a firehose.\nconst warnedOverlayProducts = new Set<string>()\n\n/** Test seam — re-arms the once-per-product overlay deprecation warning. */\nexport function resetProductOptionsDeprecationWarnings(): void {\n warnedOverlayProducts.clear()\n}\n\n/**\n * The option axes the PDP picker renders, in order of authority:\n *\n * 1. `product.options` — the variant tables (0028), attached by the\n * products.bySlug loader. THE source since spine M5: these axes are the\n * ones the server prices and decrements against.\n * 2. metadata options / sizes / colors — the retired productMetadata overlay,\n * kept rendering as an I10 deprecation shim (logged, documented). Display\n * only: nothing server-side has ever heard of these labels, which is\n * exactly why the overlay lost the job. Full deletion from the stores is\n * M9 (`catalog.ts` dies everywhere).\n */\nexport function getProductOptionGroups(product: Product): ProductOptionGroup[] {\n if (product.options && product.options.length > 0) {\n return [...product.options]\n .sort((a, b) => a.position - b.position)\n .map((option) => ({ id: option.name, label: option.name, values: [...option.values] }))\n }\n\n const options = product.metadata.options\n const explicit = Array.isArray(options)\n ? options.map(cleanOptionGroup).filter((group): group is ProductOptionGroup => Boolean(group))\n : []\n\n const groups: ProductOptionGroup[] = [...explicit]\n if (groups.length === 0) {\n const sizes = cleanValues(product.metadata.sizes)\n const colors = cleanValues(product.metadata.colors)\n if (sizes.length > 0) groups.push({ id: 'Tamanho', label: 'Tamanho', values: sizes })\n if (colors.length > 0) groups.push({ id: 'Cor', label: 'Cor', values: colors })\n }\n\n if (groups.length > 0 && !warnedOverlayProducts.has(product.id)) {\n warnedOverlayProducts.add(product.id)\n console.warn(\n `[storefront] product \"${product.slug}\" renders options from the DEPRECATED metadata overlay ` +\n '(metadata.options/sizes/colors). These labels are display-only — checkout cannot price or reserve them. ' +\n 'Define real variants (0028) instead; the overlay path is removed at M9.',\n )\n }\n return groups\n}\n\n/**\n * The variant a selection denotes, or null when the product has no variants\n * or the combination does not exist (a partial selection, or axes/values the\n * variant table never had). Matching is exact over the variant's own options\n * object — the same jsonb equality 0029 relies on server-side.\n */\nexport function resolveVariantForSelection(\n product: Product,\n selection?: ProductOptionSelection | null,\n): ProductVariant | null {\n const variants = product.variants\n if (!variants || variants.length === 0) return null\n const normalized = normalizeProductOptionSelection(selection)\n const wanted = Object.entries(normalized)\n return (\n variants.find((variant) => {\n const options = variant.options\n return (\n Object.keys(options).length === wanted.length &&\n wanted.every(([key, value]) => options[key] === value)\n )\n }) ?? null\n )\n}\n\n/** The price a selection sells for: the variant's own, or the product's when\n * the variant inherits (price null) or nothing matched. */\nexport function selectionPrice(product: Product, selection?: ProductOptionSelection | null): number {\n const variant = resolveVariantForSelection(product, selection)\n return variant?.price ?? product.price\n}\n\nexport function normalizeProductOptionSelection(selection?: ProductOptionSelection | null): ProductOptionSelection {\n if (!selection) return {}\n return Object.fromEntries(\n Object.entries(selection)\n .filter((entry): entry is [string, string] => entry[0].trim().length > 0 && entry[1].trim().length > 0)\n .sort(([a], [b]) => a.localeCompare(b)),\n )\n}\n\nexport function formatProductOptionSelection(selection?: ProductOptionSelection | null): string {\n return Object.entries(normalizeProductOptionSelection(selection))\n .map(([label, value]) => `${label}: ${value}`)\n .join(' · ')\n}\n\nexport function productOptionSelectionKey(selection?: ProductOptionSelection | null): string {\n const normalized = normalizeProductOptionSelection(selection)\n const entries = Object.entries(normalized)\n return entries.length > 0 ? JSON.stringify(entries) : ''\n}\n","import { create } from 'zustand'\nimport { marketUnitPrice, useMarketStore } from './market.store'\nimport { persist } from 'zustand/middleware'\nimport type { Product } from '@fayz-ai/shop/types'\nimport { roundCents } from '../lib/format'\nimport type { ResolvedStorefrontConfig } from '../document/config'\nimport { useDeliveryStore, selectQuotedShipping } from './delivery.store'\nimport {\n formatProductOptionSelection,\n normalizeProductOptionSelection,\n productOptionSelectionKey,\n resolveVariantForSelection,\n type ProductOptionSelection,\n} from '../data/product-options'\n\nexport interface CartLine {\n lineId: string\n productId: string\n /** Variant the selection resolved to (spine M5) — what checkout sends so\n * the server prices and decrements the right counter. Absent for\n * variantless products and overlay-only (display) options. */\n variantId?: string\n slug: string\n name: string\n sku: string | null\n options: ProductOptionSelection\n optionsLabel: string\n /** In the market the line was added in (0116). */\n unitPrice: number\n /** The catalogue's own price, kept so a change of market can reprice the\n * line. Absent on lines persisted before markets. */\n basePrice?: number\n quantity: number\n imageUrl: string | null\n /** inventoryCount at add time — clamps quantity */\n maxQuantity: number\n /**\n * What the carrier needs to price a box, copied at add time from the VARIANT\n * when there is one and the product otherwise (0046: a camiseta P and a GG do\n * not occupy the same box).\n *\n * Copied rather than re-fetched because the quote runs on every postcode\n * keystroke and the cart is the only thing that knows what is in it. Lines\n * persisted before FAY-1407 carry none of these and read as unmeasured, which\n * is exactly what they are — the quote degrades to the store's default\n * package instead of failing.\n */\n requiresShipping?: boolean\n weight?: number | null\n weightUnit?: string | null\n heightCm?: number | null\n widthCm?: number | null\n lengthCm?: number | null\n /** Em que embalagem da loja o lojista despacha este produto (0047). Do\n * PRODUTO, nunca da variante — a variante carrega medida, não caixa. */\n packageId?: string | null\n /**\n * O que o comprador pediu NESTA linha — \"sem cebola\" (0094). Até 250\n * caracteres, que é o que o banco aceita e o que a tela conta.\n *\n * Participa do `lineId`: ver addItem. Duas unidades do mesmo prato com\n * pedidos diferentes são duas linhas, não uma de quantidade 2.\n */\n notes?: string\n}\n\nexport interface CartState {\n lines: CartLine[]\n discountCode: string | null\n discountPercent: number\n /** The applied coupon zeroes freight (a `free_shipping` discount) rather than\n * discounting the subtotal. Kept apart from `discountPercent` because the\n * two answer different questions and a coupon can only be one of them. */\n discountFreeShipping: boolean\n isOpen: boolean\n /** Line most recently added — lets the drawer briefly highlight it. */\n justAddedLineId: string | null\n /** `notes` (0094) é a observação da linha — participa da identidade dela. */\n /**\n * `openDrawer` decide se a gaveta abre sozinha. Default `true`, que é o\n * comportamento de sempre: numa loja de catálogo você compra uma ou duas\n * coisas e a gaveta deslizando é a confirmação.\n *\n * Num CARDÁPIO isso está errado. A pessoa monta uma cesta de oito itens, e\n * abrir a gaveta a cada \"+\" interrompe a montagem oito vezes — cada uma\n * exigindo um toque para voltar ao ponto da lista onde ela estava. Lá o\n * cardápio passa `false` e avisa com um toast que oferece as duas saídas.\n */\n addItem(\n product: Product,\n qty?: number,\n options?: ProductOptionSelection,\n notes?: string,\n opts?: { openDrawer?: boolean },\n ): void\n setQuantity(lineId: string, qty: number): void\n removeItem(lineId: string): void\n applyDiscount(code: string, percent: number, options?: { freeShipping?: boolean }): void\n clearDiscount(): void\n clear(): void\n /** Reprice every line in the active market; drop the ones it does not sell. */\n repriceForMarket(): void\n openDrawer(): void\n closeDrawer(): void\n /** Drop persisted lines the active backend can no longer resolve (e.g. a cart\n * built in an earlier mock-mode session, whose ids don't exist live). The\n * resolver returns false only for a definitive \"not found\"; on error it must\n * return true so a transient failure never discards a legit line. */\n reconcile(resolve: (line: CartLine) => Promise<boolean>): Promise<void>\n}\n\nexport const useCartStore = create<CartState>()(\n persist(\n (set, get) => ({\n lines: [],\n discountCode: null,\n discountPercent: 0,\n discountFreeShipping: false,\n isOpen: false,\n justAddedLineId: null,\n\n addItem: (product, qty = 1, options, notes, opts) =>\n set((state) => {\n const normalizedOptions = normalizeProductOptionSelection(options)\n const optionKey = productOptionSelectionKey(normalizedOptions)\n // A observação ENTRA na identidade da linha (0094), e isto não é\n // detalhe: sem ela, \"1x costela sem cebola\" e \"1x costela bem passada\"\n // colidiriam no mesmo lineId e virariam uma linha de quantidade 2 com\n // UM dos dois pedidos — o outro sumiria em silêncio, e a cozinha\n // nunca saberia que existiu.\n const cleanNotes = notes?.trim().slice(0, 250) || undefined\n const baseId = optionKey ? `${product.id}:${optionKey}` : product.id\n const lineId = cleanNotes ? `${baseId}#${cleanNotes}` : baseId\n // The selection's variant (spine M5): when the product carries real\n // variants, the VARIANT is the sellable unit — its price and its own\n // stock counter, never the product's. Display-only overlay options\n // resolve to null and keep the product-level numbers.\n const variant = resolveVariantForSelection(product, normalizedOptions)\n const basePrice = variant?.price ?? product.price\n // In the buyer's market (0116). A product the market has no price for\n // is not for sale there: adding it would end in a checkout refusal.\n const unitPrice = marketUnitPrice(useMarketStore.getState(), product.id, variant?.id, basePrice)\n if (unitPrice == null) return state\n const stock = variant ? variant.inventoryCount : product.inventoryCount\n const existing = state.lines.find((l) => (l.lineId ?? l.productId) === lineId)\n if (existing) {\n const nextQty = Math.min(existing.quantity + qty, existing.maxQuantity || Infinity)\n return {\n // Auto-open the cart on add — the standard premium \"slide-in cart\"\n // signal — and flag the line so the drawer can highlight it.\n isOpen: opts?.openDrawer ?? true,\n justAddedLineId: lineId,\n lines: state.lines.map((l) =>\n (l.lineId ?? l.productId) === lineId ? { ...l, lineId, quantity: nextQty } : l,\n ),\n }\n }\n const image = product.images.find((i) => i.isPrimary) ?? product.images[0]\n const line: CartLine = {\n lineId,\n productId: product.id,\n variantId: variant?.id,\n slug: product.slug,\n name: product.name,\n sku: variant?.sku ?? product.sku,\n options: normalizedOptions,\n optionsLabel: formatProductOptionSelection(normalizedOptions),\n unitPrice,\n basePrice,\n quantity: Math.min(qty, stock || Infinity),\n imageUrl: image?.url ?? null,\n maxQuantity: stock,\n requiresShipping: product.isPhysical !== false,\n weight: variant?.weight ?? product.weight ?? null,\n weightUnit: variant?.weightUnit ?? product.weightUnit ?? null,\n heightCm: variant?.heightCm ?? product.heightCm ?? null,\n widthCm: variant?.widthCm ?? product.widthCm ?? null,\n lengthCm: variant?.lengthCm ?? product.lengthCm ?? null,\n packageId: product.packageId ?? null,\n notes: cleanNotes,\n }\n return {\n isOpen: opts?.openDrawer ?? true,\n justAddedLineId: lineId,\n lines: [...state.lines, line],\n }\n }),\n\n setQuantity: (lineId, qty) =>\n set((state) => {\n if (qty <= 0) return { lines: state.lines.filter((l) => (l.lineId ?? l.productId) !== lineId) }\n return {\n lines: state.lines.map((l) =>\n (l.lineId ?? l.productId) === lineId\n ? { ...l, quantity: Math.min(qty, l.maxQuantity || Infinity) }\n : l,\n ),\n }\n }),\n\n removeItem: (lineId) =>\n set((state) => ({ lines: state.lines.filter((l) => (l.lineId ?? l.productId) !== lineId) })),\n\n applyDiscount: (code, percent, options) => set({\n discountCode: code,\n discountPercent: percent,\n discountFreeShipping: options?.freeShipping === true,\n }),\n clearDiscount: () => set({ discountCode: null, discountPercent: 0, discountFreeShipping: false }),\n clear: () => set({ lines: [], discountCode: null, discountPercent: 0, discountFreeShipping: false }),\n\n repriceForMarket: () =>\n set((state) => {\n const market = useMarketStore.getState()\n let changed = false\n const lines = state.lines.flatMap((line) => {\n const price = marketUnitPrice(market, line.productId, line.variantId, line.basePrice ?? line.unitPrice)\n if (price == null) {\n changed = true\n return []\n }\n if (price !== line.unitPrice) changed = true\n return [{ ...line, unitPrice: price }]\n })\n return changed ? { lines } : state\n }),\n openDrawer: () => set({ isOpen: true }),\n closeDrawer: () => set({ isOpen: false, justAddedLineId: null }),\n\n reconcile: async (resolve) => {\n const lines = get().lines\n if (lines.length === 0) return\n const keep = await Promise.all(\n lines.map((line) => resolve(line).catch(() => true)),\n )\n const kept = lines.filter((_, i) => keep[i])\n if (kept.length !== lines.length) set({ lines: kept })\n },\n }),\n {\n name: 'fayz.storefront.cart.v1',\n partialize: (state) => ({\n lines: state.lines,\n discountCode: state.discountCode,\n discountPercent: state.discountPercent,\n discountFreeShipping: state.discountFreeShipping,\n }),\n },\n ),\n)\n\n// ---------------------------------------------------------------------------\n// Money selectors — the single place cart math happens. Checkout passes these\n// exact values to createOrder so drawer totals always match order totals.\n// ---------------------------------------------------------------------------\n\nexport const selectCount = (s: Pick<CartState, 'lines'>): number =>\n s.lines.reduce((n, l) => n + l.quantity, 0)\n\nexport const selectSubtotal = (s: Pick<CartState, 'lines'>): number =>\n roundCents(s.lines.reduce((sum, l) => sum + l.unitPrice * l.quantity, 0))\n\nexport const selectDiscountTotal = (s: Pick<CartState, 'lines' | 'discountPercent'>): number =>\n roundCents(selectSubtotal(s) * (s.discountPercent / 100))\n\n/**\n * Freight to display.\n *\n * When the shopper has given a CEP and the store quoted it, that quote wins —\n * it came from the same shipping_zones rows shop_place_order will charge from.\n * Otherwise this falls back to the store-wide rate in config, which is what\n * every storefront did before zones existed.\n *\n * The subtotal is PRE-discount on both sides of this. That is 0017's rule, and\n * the quote is requested against the same number, so a coupon can never make\n * the cart and the order disagree.\n *\n * Read through getState() rather than a hook because this is a plain selector\n * called from several screens; components that render the value subscribe to\n * useDeliveryStore themselves so a fresh quote re-renders them.\n */\n/**\n * Whether anything in this cart travels.\n *\n * A line added before FAY-1407 has no flag and counts as physical: a cart of\n * real goods that suddenly ships for free is a worse failure than a download\n * that shows a delivery box.\n */\nexport const selectRequiresShipping = (s: Pick<CartState, 'lines'>): boolean =>\n s.lines.some((l) => l.requiresShipping !== false)\n\nexport type ShippingCart = Pick<CartState, 'lines'> & Partial<Pick<CartState, 'discountFreeShipping'>>\n\nexport const selectShipping = (\n s: ShippingCart,\n cfg: ResolvedStorefrontConfig,\n): number => {\n if (s.lines.length === 0) return 0\n // Nothing here is delivered. Not \"free delivery\" — no delivery, so neither\n // the quote nor the store-wide flat rate applies. Without this a cart of\n // e-books was charged the config's flatRate for a parcel that never exists.\n if (!selectRequiresShipping(s)) return 0\n // A free_shipping coupon. shop_place_order zeroes freight the same way and\n // does not even consult the zones (0017:178), so the quote below would show\n // a number the order will never charge.\n if (s.discountFreeShipping) return 0\n const subtotal = selectSubtotal(s)\n\n const quoted = selectQuotedShipping(useDeliveryStore.getState(), subtotal)\n if (quoted != null) return quoted\n\n if (cfg.shipping.freeAbove != null && subtotal >= cfg.shipping.freeAbove) return 0\n return cfg.shipping.flatRate\n}\n\nexport const selectTotal = (\n s: Pick<CartState, 'lines' | 'discountPercent'> & Partial<Pick<CartState, 'discountFreeShipping'>>,\n cfg: ResolvedStorefrontConfig,\n): number =>\n roundCents(selectSubtotal(s) - selectDiscountTotal(s) + selectShipping(s, cfg))\n"]}
|