@fayz-ai/shop 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +18 -1
  2. package/dist/admin.d.ts +2 -0
  3. package/dist/admin.d.ts.map +1 -0
  4. package/dist/catalog.d.ts +1 -3
  5. package/dist/catalog.d.ts.map +1 -1
  6. package/dist/catalog.js +2 -2
  7. package/dist/{chunk-IYQHKMLB.js → chunk-2WACO2TG.js} +4 -4
  8. package/dist/chunk-2WACO2TG.js.map +1 -0
  9. package/dist/chunk-3GERSY5F.js +526 -0
  10. package/dist/chunk-3GERSY5F.js.map +1 -0
  11. package/dist/chunk-6BO2TMZ5.js +93 -0
  12. package/dist/chunk-6BO2TMZ5.js.map +1 -0
  13. package/dist/chunk-FQPGE3YN.js +3 -0
  14. package/dist/{chunk-JR25TISO.js.map → chunk-FQPGE3YN.js.map} +1 -1
  15. package/dist/{chunk-CTKZ6O2J.js → chunk-IKNGCRUM.js} +41 -15
  16. package/dist/chunk-IKNGCRUM.js.map +1 -0
  17. package/dist/chunk-KLDS7IPU.js +17 -0
  18. package/dist/chunk-KLDS7IPU.js.map +1 -0
  19. package/dist/chunk-NWW3WJ7Y.js +136 -0
  20. package/dist/chunk-NWW3WJ7Y.js.map +1 -0
  21. package/dist/chunk-STRNMFC4.js +1438 -0
  22. package/dist/chunk-STRNMFC4.js.map +1 -0
  23. package/dist/chunk-U6HV3LVP.js +1170 -0
  24. package/dist/chunk-U6HV3LVP.js.map +1 -0
  25. package/dist/client.d.ts +2 -0
  26. package/dist/client.d.ts.map +1 -0
  27. package/dist/data/customer-search.d.ts +3 -0
  28. package/dist/data/customer-search.d.ts.map +1 -0
  29. package/dist/data/functions.d.ts +9 -0
  30. package/dist/data/functions.d.ts.map +1 -0
  31. package/dist/data/media.d.ts +37 -0
  32. package/dist/data/media.d.ts.map +1 -0
  33. package/dist/{mock-catalog.d.ts → data/mock-catalog.d.ts} +21 -1
  34. package/dist/data/mock-catalog.d.ts.map +1 -0
  35. package/dist/data/mock-provider.d.ts +165 -0
  36. package/dist/data/mock-provider.d.ts.map +1 -0
  37. package/dist/data/mock.d.ts +3 -0
  38. package/dist/data/mock.d.ts.map +1 -0
  39. package/dist/data/provider.d.ts +335 -0
  40. package/dist/data/provider.d.ts.map +1 -0
  41. package/dist/data/supabase-provider.d.ts +208 -0
  42. package/dist/data/supabase-provider.d.ts.map +1 -0
  43. package/dist/data/tables.d.ts +38 -0
  44. package/dist/data/tables.d.ts.map +1 -0
  45. package/dist/data/tenant.d.ts.map +1 -0
  46. package/dist/entities/entity-reconcile.d.ts +32 -0
  47. package/dist/entities/entity-reconcile.d.ts.map +1 -0
  48. package/dist/entities/entity-sql.d.ts +50 -0
  49. package/dist/entities/entity-sql.d.ts.map +1 -0
  50. package/dist/entities/entity-store.d.ts +58 -0
  51. package/dist/entities/entity-store.d.ts.map +1 -0
  52. package/dist/entities/index.d.ts +10 -0
  53. package/dist/entities/index.d.ts.map +1 -0
  54. package/dist/entities/mock-entity-store.d.ts +17 -0
  55. package/dist/entities/mock-entity-store.d.ts.map +1 -0
  56. package/dist/entities/supabase-entity-store.d.ts +8 -0
  57. package/dist/entities/supabase-entity-store.d.ts.map +1 -0
  58. package/dist/entities.js +159 -0
  59. package/dist/entities.js.map +1 -0
  60. package/dist/index.d.ts +28 -12
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +1558 -40
  63. package/dist/index.js.map +1 -1
  64. package/dist/inventory/allocation.d.ts +98 -0
  65. package/dist/inventory/allocation.d.ts.map +1 -0
  66. package/dist/inventory/index.d.ts +13 -0
  67. package/dist/inventory/index.d.ts.map +1 -0
  68. package/dist/inventory/mock-stock-store.d.ts +89 -0
  69. package/dist/inventory/mock-stock-store.d.ts.map +1 -0
  70. package/dist/inventory/postgrest-double.d.ts +65 -0
  71. package/dist/inventory/postgrest-double.d.ts.map +1 -0
  72. package/dist/inventory/shopify-mapping.d.ts +67 -0
  73. package/dist/inventory/shopify-mapping.d.ts.map +1 -0
  74. package/dist/inventory/stock-contract.d.ts +46 -0
  75. package/dist/inventory/stock-contract.d.ts.map +1 -0
  76. package/dist/inventory/stock-store.d.ts +96 -0
  77. package/dist/inventory/stock-store.d.ts.map +1 -0
  78. package/dist/inventory/stock-types.d.ts +351 -0
  79. package/dist/inventory/stock-types.d.ts.map +1 -0
  80. package/dist/inventory/supabase-stock-store.d.ts +60 -0
  81. package/dist/inventory/supabase-stock-store.d.ts.map +1 -0
  82. package/dist/inventory/vocabulary.d.ts +86 -0
  83. package/dist/inventory/vocabulary.d.ts.map +1 -0
  84. package/dist/inventory.js +4 -0
  85. package/dist/inventory.js.map +1 -0
  86. package/dist/lib/catalog.d.ts +4 -0
  87. package/dist/lib/catalog.d.ts.map +1 -0
  88. package/dist/{commerce.d.ts → lib/commerce.d.ts} +2 -2
  89. package/dist/lib/commerce.d.ts.map +1 -0
  90. package/dist/lib/document.d.ts +13 -0
  91. package/dist/lib/document.d.ts.map +1 -0
  92. package/dist/lib/emails.d.ts +50 -0
  93. package/dist/lib/emails.d.ts.map +1 -0
  94. package/dist/lib/message-events.d.ts +4 -0
  95. package/dist/lib/message-events.d.ts.map +1 -0
  96. package/dist/lib/runtime.d.ts +5 -0
  97. package/dist/lib/runtime.d.ts.map +1 -0
  98. package/dist/lib/variant-matrix.d.ts +120 -0
  99. package/dist/lib/variant-matrix.d.ts.map +1 -0
  100. package/dist/mock.d.ts +1 -2
  101. package/dist/mock.d.ts.map +1 -1
  102. package/dist/mock.js +4 -2
  103. package/dist/provider.d.ts +1 -88
  104. package/dist/provider.d.ts.map +1 -1
  105. package/dist/providers/checkout.d.ts +129 -0
  106. package/dist/providers/checkout.d.ts.map +1 -0
  107. package/dist/providers/index.d.ts +5 -0
  108. package/dist/providers/index.d.ts.map +1 -0
  109. package/dist/providers/shipping.d.ts +43 -0
  110. package/dist/providers/shipping.d.ts.map +1 -0
  111. package/dist/providers.js +70 -0
  112. package/dist/providers.js.map +1 -0
  113. package/dist/rules/adapter.d.ts +36 -0
  114. package/dist/rules/adapter.d.ts.map +1 -0
  115. package/dist/rules/attributes.d.ts +64 -0
  116. package/dist/rules/attributes.d.ts.map +1 -0
  117. package/dist/rules/context.d.ts +43 -0
  118. package/dist/rules/context.d.ts.map +1 -0
  119. package/dist/rules/evaluator.d.ts +37 -0
  120. package/dist/rules/evaluator.d.ts.map +1 -0
  121. package/dist/rules/index.d.ts +13 -0
  122. package/dist/rules/index.d.ts.map +1 -0
  123. package/dist/rules/prices.d.ts +25 -0
  124. package/dist/rules/prices.d.ts.map +1 -0
  125. package/dist/rules/rule-types.d.ts +160 -0
  126. package/dist/rules/rule-types.d.ts.map +1 -0
  127. package/dist/rules.js +22 -0
  128. package/dist/rules.js.map +1 -0
  129. package/dist/runtime.d.ts +1 -4
  130. package/dist/runtime.d.ts.map +1 -1
  131. package/dist/runtime.js +5 -3
  132. package/dist/shipping/label-provider.d.ts +43 -0
  133. package/dist/shipping/label-provider.d.ts.map +1 -0
  134. package/dist/types.d.ts +688 -0
  135. package/dist/types.d.ts.map +1 -1
  136. package/package.json +30 -20
  137. package/dist/chunk-CTKZ6O2J.js.map +0 -1
  138. package/dist/chunk-IYQHKMLB.js.map +0 -1
  139. package/dist/chunk-JR25TISO.js +0 -3
  140. package/dist/chunk-SVTCJ6HP.js +0 -440
  141. package/dist/chunk-SVTCJ6HP.js.map +0 -1
  142. package/dist/commerce.d.ts.map +0 -1
  143. package/dist/mock-catalog.d.ts.map +0 -1
  144. package/dist/mock-provider.d.ts +0 -60
  145. package/dist/mock-provider.d.ts.map +0 -1
  146. package/dist/supabase-provider.d.ts +0 -56
  147. package/dist/supabase-provider.d.ts.map +0 -1
  148. package/dist/tables.d.ts +0 -12
  149. package/dist/tables.d.ts.map +0 -1
  150. package/dist/tenant.d.ts.map +0 -1
  151. package/src/catalog.ts +0 -3
  152. package/src/commerce.ts +0 -120
  153. package/src/index.ts +0 -40
  154. package/src/mock-catalog.ts +0 -266
  155. package/src/mock-provider.ts +0 -480
  156. package/src/mock.ts +0 -2
  157. package/src/provider.ts +0 -117
  158. package/src/runtime.ts +0 -20
  159. package/src/supabase-provider.ts +0 -754
  160. package/src/tables.ts +0 -17
  161. package/src/tenant.ts +0 -21
  162. package/src/types.ts +0 -623
  163. /package/dist/{tenant.d.ts → data/tenant.d.ts} +0 -0
@@ -1,480 +0,0 @@
1
- import type { ShopProvider } from './provider'
2
- import type {
3
- Product, ProductImage, Category, Order, ShopCustomer, Discount, ProductEnquiry,
4
- CreateProductInput, UpdateProductInput, ListProductsOptions,
5
- CreateProductEnquiryInput, ListProductEnquiriesOptions,
6
- CreateCategoryInput, UpdateCategoryInput,
7
- CreateOrderInput, UpdateOrderInput, ListOrdersOptions, PlaceOrderInput,
8
- CreateCustomerInput, UpdateCustomerInput, ListCustomersOptions, ResolveCustomerInput,
9
- CreateDiscountInput, UpdateDiscountInput, ListDiscountsOptions,
10
- SalesChannel, CreateSalesChannelInput, UpdateSalesChannelInput,
11
- } from './types'
12
- import { MOCK_PRODUCTS, MOCK_CATEGORIES, MOCK_DISCOUNTS } from './mock-catalog'
13
-
14
- function uid() { return Math.random().toString(36).slice(2) }
15
- function now() { return new Date().toISOString() }
16
-
17
- // Orders/customers persist to localStorage so "my purchases" survives a page
18
- // reload. Products/categories/discounts always reseed from the catalog —
19
- // the storefront sees a deterministic catalog on every load.
20
- const ORDERS_KEY = 'fayz.shop.mock.orders.v1'
21
- const CUSTOMERS_KEY = 'fayz.shop.mock.customers.v1'
22
- const ENQUIRIES_KEY = 'fayz.shop.mock.enquiries.v1'
23
- const CHANNELS_KEY = 'fayz.shop.mock.channels.v1'
24
-
25
- const DEFAULT_CHANNELS: SalesChannel[] = [
26
- { id: 'online-store', tenantId: 'mock', kind: 'online_store', label: 'Loja Online', url: null, enabled: true, position: 0, createdAt: now(), updatedAt: now() },
27
- ]
28
-
29
- function loadStored<T>(key: string): T[] {
30
- try {
31
- if (typeof localStorage === 'undefined') return []
32
- const raw = localStorage.getItem(key)
33
- return raw ? (JSON.parse(raw) as T[]) : []
34
- } catch {
35
- return []
36
- }
37
- }
38
-
39
- function store<T>(key: string, value: T[]): void {
40
- try {
41
- if (typeof localStorage === 'undefined') return
42
- localStorage.setItem(key, JSON.stringify(value))
43
- } catch {
44
- /* quota/serialization failures are non-fatal in mock mode */
45
- }
46
- }
47
-
48
- export interface MockShopSeed {
49
- products?: Product[]
50
- categories?: Category[]
51
- discounts?: Discount[]
52
- }
53
-
54
- export class MockShopProvider implements ShopProvider {
55
- private products: Product[]
56
- private categories: Category[]
57
- private discounts: Discount[]
58
- private orders: Order[] = loadStored<Order>(ORDERS_KEY)
59
- private customers: ShopCustomer[] = loadStored<ShopCustomer>(CUSTOMERS_KEY)
60
- private enquiries: ProductEnquiry[] = loadStored<ProductEnquiry>(ENQUIRIES_KEY)
61
- private channels: SalesChannel[] = (() => {
62
- const stored = loadStored<SalesChannel>(CHANNELS_KEY)
63
- return stored.length ? stored : DEFAULT_CHANNELS.map(c => ({ ...c }))
64
- })()
65
-
66
- constructor(seed?: MockShopSeed) {
67
- this.products = (seed?.products ?? MOCK_PRODUCTS).map(p => ({ ...p }))
68
- this.categories = (seed?.categories ?? MOCK_CATEGORIES).map(c => ({ ...c }))
69
- this.discounts = (seed?.discounts ?? MOCK_DISCOUNTS).map(d => ({ ...d }))
70
- }
71
-
72
- // Re-read before mutating: HMR can re-instantiate the provider mid-session
73
- // and a stale in-memory copy must not clobber persisted records.
74
- private syncOrders() { this.orders = loadStored<Order>(ORDERS_KEY) }
75
- private syncCustomers() { this.customers = loadStored<ShopCustomer>(CUSTOMERS_KEY) }
76
- private syncEnquiries() { this.enquiries = loadStored<ProductEnquiry>(ENQUIRIES_KEY) }
77
- private syncChannels() {
78
- const stored = loadStored<SalesChannel>(CHANNELS_KEY)
79
- if (stored.length) this.channels = stored
80
- }
81
- private persistOrders() { store(ORDERS_KEY, this.orders) }
82
- private persistCustomers() { store(CUSTOMERS_KEY, this.customers) }
83
- private persistEnquiries() { store(ENQUIRIES_KEY, this.enquiries) }
84
- private persistChannels() { store(CHANNELS_KEY, this.channels) }
85
-
86
- // ---------------------------------------------------------------------------
87
- // Products
88
- // ---------------------------------------------------------------------------
89
-
90
- async listProducts(opts?: ListProductsOptions): Promise<Product[]> {
91
- let list = [...this.products]
92
- if (opts?.status) list = list.filter(p => p.status === opts.status)
93
- if (opts?.categoryId) list = list.filter(p => p.categoryId === opts.categoryId)
94
- if (opts?.slug) list = list.filter(p => p.slug === opts.slug)
95
- if (opts?.search) {
96
- const q = opts.search.toLowerCase()
97
- list = list.filter(p =>
98
- p.name.toLowerCase().includes(q) ||
99
- (p.description ?? '').toLowerCase().includes(q) ||
100
- (p.sku ?? '').toLowerCase().includes(q),
101
- )
102
- }
103
- const orderBy = opts?.orderBy ?? 'sort_order'
104
- const dir = opts?.order === 'desc' ? -1 : 1
105
- list.sort((a, b) => {
106
- switch (orderBy) {
107
- case 'name': return a.name.localeCompare(b.name) * dir
108
- case 'price': return (a.price - b.price) * dir
109
- case 'created_at': return a.createdAt.localeCompare(b.createdAt) * dir
110
- default: return (a.sortOrder - b.sortOrder) * dir
111
- }
112
- })
113
- const offset = opts?.offset ?? 0
114
- if (offset || opts?.limit) list = list.slice(offset, opts?.limit ? offset + opts.limit : undefined)
115
- return list
116
- }
117
-
118
- async getProduct(id: string) { return this.products.find(p => p.id === id) ?? null }
119
-
120
- async createProduct(input: CreateProductInput): Promise<Product> {
121
- const p: Product = { id: uid(), tenantId: 'mock', name: input.name, slug: input.slug ?? input.name.toLowerCase().replace(/\s+/g, '-'), description: input.description ?? null, price: input.price, compareAtPrice: input.compareAtPrice ?? null, currency: input.currency ?? 'BRL', status: input.status ?? 'active', inventoryCount: input.inventoryCount ?? 0, sku: input.sku ?? null, sortOrder: input.sortOrder ?? 0, metadata: input.metadata ?? {}, images: [], categoryId: input.categoryId ?? null, categoryName: null, isPhysical: input.isPhysical ?? true, weight: input.weight ?? null, weightUnit: input.weightUnit ?? 'kg', createdAt: now(), updatedAt: now() }
122
- this.products.push(p); return p
123
- }
124
-
125
- async updateProduct(id: string, input: UpdateProductInput): Promise<Product> {
126
- const i = this.products.findIndex(p => p.id === id)
127
- if (i < 0) throw new Error('Product not found')
128
- this.products[i] = { ...this.products[i], ...input, updatedAt: now() }
129
- return this.products[i]
130
- }
131
-
132
- async deleteProduct(id: string) { this.products = this.products.filter(p => p.id !== id) }
133
- async uploadProductImage(_productId: string, _file: File): Promise<ProductImage> { throw new Error('Image upload not available in mock mode') }
134
- async deleteProductImage(_imageId: string) {}
135
-
136
- async createProductEnquiry(input: CreateProductEnquiryInput): Promise<ProductEnquiry> {
137
- this.syncEnquiries()
138
- const ts = now()
139
- const enquiry: ProductEnquiry = {
140
- id: uid(),
141
- tenantId: 'mock',
142
- productId: input.productId ?? null,
143
- productName: input.productName,
144
- productSlug: input.productSlug ?? null,
145
- customerName: input.customerName,
146
- customerEmail: input.customerEmail,
147
- customerPhone: input.customerPhone ?? null,
148
- subject: input.subject ?? null,
149
- message: input.message,
150
- sourceUrl: input.sourceUrl ?? null,
151
- status: 'new',
152
- metadata: input.metadata ?? {},
153
- createdAt: ts,
154
- updatedAt: ts,
155
- }
156
- this.enquiries.unshift(enquiry)
157
- this.persistEnquiries()
158
- return enquiry
159
- }
160
-
161
- async listProductEnquiries(opts?: ListProductEnquiriesOptions): Promise<ProductEnquiry[]> {
162
- this.syncEnquiries()
163
- let list = [...this.enquiries]
164
- if (opts?.productId) list = list.filter((e) => e.productId === opts.productId)
165
- if (opts?.customerEmail) {
166
- const email = opts.customerEmail.toLowerCase()
167
- list = list.filter((e) => e.customerEmail.toLowerCase() === email)
168
- }
169
- if (opts?.status) list = list.filter((e) => e.status === opts.status)
170
- list.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
171
- const offset = opts?.offset ?? 0
172
- if (offset || opts?.limit) list = list.slice(offset, opts?.limit ? offset + opts.limit : undefined)
173
- return list
174
- }
175
-
176
- // ---------------------------------------------------------------------------
177
- // Categories
178
- // ---------------------------------------------------------------------------
179
-
180
- async listCategories() { return [...this.categories].sort((a, b) => a.sortOrder - b.sortOrder) }
181
-
182
- async createCategory(input: CreateCategoryInput): Promise<Category> {
183
- const c: Category = { id: uid(), tenantId: 'mock', name: input.name, slug: input.slug ?? input.name.toLowerCase().replace(/\s+/g, '-'), description: input.description ?? null, parentId: input.parentId ?? null, sortOrder: input.sortOrder ?? 0, createdAt: now() }
184
- this.categories.push(c); return c
185
- }
186
-
187
- async updateCategory(id: string, input: UpdateCategoryInput): Promise<Category> {
188
- const i = this.categories.findIndex(c => c.id === id)
189
- if (i < 0) throw new Error('Category not found')
190
- this.categories[i] = { ...this.categories[i], ...input }
191
- return this.categories[i]
192
- }
193
-
194
- async deleteCategory(id: string) { this.categories = this.categories.filter(c => c.id !== id) }
195
-
196
- // ---------------------------------------------------------------------------
197
- // Orders
198
- // ---------------------------------------------------------------------------
199
-
200
- async listOrders(opts?: ListOrdersOptions): Promise<Order[]> {
201
- this.syncOrders()
202
- let list = [...this.orders]
203
- if (opts?.status) list = list.filter(o => o.status === opts.status)
204
- if (opts?.financialStatus) list = list.filter(o => o.financialStatus === opts.financialStatus)
205
- if (opts?.fulfillmentStatus) list = list.filter(o => o.fulfillmentStatus === opts.fulfillmentStatus)
206
- if (opts?.customerId) list = list.filter(o => o.customerId === opts.customerId)
207
- if (opts?.customerEmail) {
208
- const email = opts.customerEmail.toLowerCase()
209
- list = list.filter(o => (o.customerEmail ?? '').toLowerCase() === email)
210
- }
211
- if (opts?.search) {
212
- const q = opts.search.toLowerCase()
213
- list = list.filter(o =>
214
- (o.customerName ?? '').toLowerCase().includes(q) ||
215
- (o.customerEmail ?? '').toLowerCase().includes(q) ||
216
- String(o.orderNumber).includes(q),
217
- )
218
- }
219
- list.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
220
- if (opts?.limit) list = list.slice(0, opts.limit)
221
- return list
222
- }
223
-
224
- async getOrder(id: string) {
225
- this.syncOrders()
226
- return this.orders.find(o => o.id === id) ?? null
227
- }
228
-
229
- async createOrder(input: CreateOrderInput): Promise<Order> {
230
- this.syncOrders()
231
- const subtotal = input.items.reduce((s, i) => s + i.quantity * i.unitPrice, 0)
232
- const discountTotal = input.discountTotal ?? 0
233
- const shippingTotal = input.shippingTotal ?? 0
234
- const total = Math.round((subtotal - discountTotal + shippingTotal) * 100) / 100
235
- const orderId = uid()
236
- const maxNumber = this.orders.reduce((m, o) => Math.max(m, o.orderNumber), 1000)
237
- const o: Order = {
238
- id: orderId,
239
- tenantId: 'mock',
240
- orderNumber: maxNumber + 1,
241
- status: 'open',
242
- financialStatus: 'pending',
243
- fulfillmentStatus: 'unfulfilled',
244
- currency: input.currency ?? 'BRL',
245
- subtotal: Math.round(subtotal * 100) / 100,
246
- taxTotal: 0,
247
- discountTotal,
248
- shippingTotal,
249
- total,
250
- customerId: input.customerId ?? null,
251
- customerName: input.customerName ?? null,
252
- customerEmail: input.customerEmail ?? null,
253
- discountCode: input.discountCode ?? null,
254
- notes: input.notes ?? null,
255
- shippingAddress: null,
256
- // CreateOrderInput has no payment method — that is a PlaceOrderInput
257
- // concern; the mock records none rather than inventing one.
258
- paymentMethodKind: null,
259
- paidAt: null,
260
- items: input.items.map(i => ({ id: uid(), orderId, productId: i.productId ?? null, name: i.name, sku: i.sku ?? null, quantity: i.quantity, unitPrice: i.unitPrice, total: Math.round(i.quantity * i.unitPrice * 100) / 100, imageUrl: i.imageUrl ?? null })),
261
- createdAt: now(),
262
- updatedAt: now(),
263
- }
264
- this.orders.push(o)
265
- this.persistOrders()
266
- return o
267
- }
268
-
269
- async updateOrder(id: string, input: UpdateOrderInput): Promise<Order> {
270
- this.syncOrders()
271
- const i = this.orders.findIndex(o => o.id === id)
272
- if (i < 0) throw new Error('Order not found')
273
- this.orders[i] = { ...this.orders[i], ...input, updatedAt: now() }
274
- this.persistOrders()
275
- return this.orders[i]
276
- }
277
-
278
- // Trusted placement (mock parity with the shop_place_order RPC): re-reads
279
- // prices + stock from the catalog, validates the discount, decrements
280
- // inventory. The browser-supplied prices are never used.
281
- async placeOrder(input: PlaceOrderInput): Promise<Order> {
282
- this.syncOrders()
283
- this.syncCustomers()
284
-
285
- // Resolve / create the customer (find-or-create by email).
286
- let customerId = input.customerId ?? null
287
- let customerName = input.customer?.name?.trim() || null
288
- const email = input.customer?.email?.trim().toLowerCase() || null
289
- if (!customerId && email) {
290
- const existing = this.customers.find(c => (c.email ?? '').toLowerCase() === email)
291
- if (existing) {
292
- customerId = existing.id
293
- customerName = customerName ?? `${existing.firstName} ${existing.lastName}`.trim()
294
- } else {
295
- const [first, ...rest] = (customerName ?? email).split(' ')
296
- const created = await this.createCustomer({ firstName: first || email, lastName: rest.join(' '), email })
297
- customerId = created.id
298
- }
299
- }
300
-
301
- // Re-read price + guard stock from the catalog (never trust the client).
302
- const items: CreateOrderInput['items'] = []
303
- // Per-line facts for coupon scoping (productId, categoryId, line total).
304
- const lineDetails: Array<{ productId: string; categoryId: string | null; lineTotal: number }> = []
305
- let subtotal = 0
306
- for (const line of input.items) {
307
- const product = this.products.find(p => p.id === line.productId)
308
- if (!product || product.status !== 'active') throw new Error(`Product unavailable: ${line.productId}`)
309
- if (product.inventoryCount < line.quantity) throw new Error(`Insufficient stock for ${product.name}`)
310
- const name = line.optionsLabel ? `${product.name} (${line.optionsLabel})` : product.name
311
- const lineTotal = product.price * line.quantity
312
- subtotal += lineTotal
313
- lineDetails.push({ productId: product.id, categoryId: product.categoryId, lineTotal })
314
- items.push({ productId: product.id, name, sku: product.sku ?? undefined, quantity: line.quantity, unitPrice: product.price, imageUrl: product.images[0]?.url })
315
- }
316
- subtotal = Math.round(subtotal * 100) / 100
317
-
318
- // Validate + apply the discount server-side.
319
- let shippingTotal = Math.max(input.shippingTotal ?? 0, 0)
320
- let discountTotal = 0
321
- let appliedCode: string | null = null
322
- if (input.discountCode) {
323
- const code = input.discountCode.trim().toLowerCase()
324
- const d = this.discounts.find(x => (x.code ?? '').toLowerCase() === code)
325
- const ts = now()
326
- const meetsMin = !d?.minSubtotal || subtotal >= d.minSubtotal
327
- const valid = !!d && d.status === 'active' && d.startsAt <= ts && (!d.endsAt || d.endsAt >= ts) && (d.usageLimit == null || d.timesUsed < d.usageLimit) && meetsMin
328
- if (valid && d) {
329
- appliedCode = d.code
330
- // Eligible subtotal = the part of the cart the coupon is scoped to.
331
- // applies_to='all' → whole subtotal (unchanged for legacy coupons).
332
- let eligible = subtotal
333
- if (d.appliesTo === 'products') {
334
- eligible = lineDetails.filter(l => d.productIds.includes(l.productId)).reduce((s, l) => s + l.lineTotal, 0)
335
- } else if (d.appliesTo === 'categories') {
336
- eligible = lineDetails.filter(l => l.categoryId && d.categoryIds.includes(l.categoryId)).reduce((s, l) => s + l.lineTotal, 0)
337
- }
338
- eligible = Math.round(eligible * 100) / 100
339
- if (d.type === 'percentage') discountTotal = Math.round(eligible * d.value) / 100
340
- else if (d.type === 'fixed_amount') discountTotal = Math.min(d.value, eligible)
341
- else if (d.type === 'free_shipping') shippingTotal = 0
342
- // buy_x_get_y unsupported in v1: code recorded, no monetary effect
343
- d.timesUsed += 1
344
- }
345
- }
346
- discountTotal = Math.round(Math.min(Math.max(discountTotal, 0), subtotal) * 100) / 100
347
-
348
- // Persist via createOrder (recomputes total from these trusted values).
349
- const order = await this.createOrder({
350
- customerId: customerId ?? undefined,
351
- customerName: customerName ?? undefined,
352
- customerEmail: input.customer?.email ?? undefined,
353
- currency: input.currency,
354
- notes: input.notes,
355
- discountCode: appliedCode ?? undefined,
356
- discountTotal,
357
- shippingTotal,
358
- items,
359
- })
360
-
361
- // Decrement inventory (in-memory; the mock catalog reseeds on reload).
362
- for (const line of input.items) {
363
- const product = this.products.find(p => p.id === line.productId)
364
- if (product) product.inventoryCount = Math.max(0, product.inventoryCount - line.quantity)
365
- }
366
-
367
- return order
368
- }
369
-
370
- // ---------------------------------------------------------------------------
371
- // Customers
372
- // ---------------------------------------------------------------------------
373
-
374
- async listCustomers(opts?: ListCustomersOptions) {
375
- this.syncCustomers()
376
- let list = [...this.customers]
377
- if (opts?.search) list = list.filter(c => `${c.firstName} ${c.lastName} ${c.email}`.toLowerCase().includes(opts.search!.toLowerCase()))
378
- if (opts?.limit) list = list.slice(0, opts.limit)
379
- return list
380
- }
381
-
382
- async getCustomer(id: string) {
383
- this.syncCustomers()
384
- return this.customers.find(c => c.id === id) ?? null
385
- }
386
-
387
- async createCustomer(input: CreateCustomerInput): Promise<ShopCustomer> {
388
- this.syncCustomers()
389
- const c: ShopCustomer = { id: uid(), tenantId: 'mock', firstName: input.firstName, lastName: input.lastName ?? '', email: input.email ?? null, phone: input.phone ?? null, notes: input.notes ?? null, ordersCount: 0, totalSpent: 0, createdAt: now(), updatedAt: now() }
390
- this.customers.push(c)
391
- this.persistCustomers()
392
- return c
393
- }
394
-
395
- async updateCustomer(id: string, input: UpdateCustomerInput): Promise<ShopCustomer> {
396
- this.syncCustomers()
397
- const i = this.customers.findIndex(c => c.id === id)
398
- if (i < 0) throw new Error('Customer not found')
399
- this.customers[i] = { ...this.customers[i], ...input, updatedAt: now() }
400
- this.persistCustomers()
401
- return this.customers[i]
402
- }
403
-
404
- async deleteCustomer(id: string) {
405
- this.syncCustomers()
406
- this.customers = this.customers.filter(c => c.id !== id)
407
- this.persistCustomers()
408
- }
409
-
410
- async resolveCustomer(input: ResolveCustomerInput): Promise<ShopCustomer> {
411
- this.syncCustomers()
412
- const email = input.email.trim().toLowerCase()
413
- const existing = this.customers.find(c => (c.email ?? '').toLowerCase() === email)
414
- if (existing) return existing
415
- const [first, ...rest] = (input.name?.trim() || email).split(/\s+/)
416
- return this.createCustomer({ firstName: first || email, lastName: rest.join(' '), email })
417
- }
418
-
419
- // ---------------------------------------------------------------------------
420
- // Discounts
421
- // ---------------------------------------------------------------------------
422
-
423
- async listDiscounts(opts?: ListDiscountsOptions) {
424
- let list = [...this.discounts]
425
- if (opts?.status) list = list.filter(d => d.status === opts.status)
426
- if (opts?.type) list = list.filter(d => d.type === opts.type)
427
- if (opts?.search) list = list.filter(d => d.title.toLowerCase().includes(opts.search!.toLowerCase()))
428
- if (opts?.limit) list = list.slice(0, opts.limit)
429
- return list
430
- }
431
-
432
- async getDiscount(id: string) { return this.discounts.find(d => d.id === id) ?? null }
433
-
434
- async createDiscount(input: CreateDiscountInput): Promise<Discount> {
435
- const d: Discount = { id: uid(), tenantId: 'mock', title: input.title, code: input.code ?? null, type: input.type, method: input.method ?? 'code', value: input.value, usageLimit: input.usageLimit ?? null, oncePerCustomer: input.oncePerCustomer ?? false, minSubtotal: input.minSubtotal ?? null, appliesTo: input.appliesTo ?? 'all', productIds: input.productIds ?? [], categoryIds: input.categoryIds ?? [], startsAt: input.startsAt ?? now(), endsAt: input.endsAt ?? null, status: input.status ?? 'active', timesUsed: 0, createdAt: now(), updatedAt: now() }
436
- this.discounts.push(d); return d
437
- }
438
-
439
- async updateDiscount(id: string, input: UpdateDiscountInput): Promise<Discount> {
440
- const i = this.discounts.findIndex(d => d.id === id)
441
- if (i < 0) throw new Error('Discount not found')
442
- this.discounts[i] = { ...this.discounts[i], ...input, updatedAt: now() }
443
- return this.discounts[i]
444
- }
445
-
446
- async deleteDiscount(id: string) { this.discounts = this.discounts.filter(d => d.id !== id) }
447
-
448
- // ---------------------------------------------------------------------------
449
- // Sales channels
450
- // ---------------------------------------------------------------------------
451
-
452
- async listSalesChannels(): Promise<SalesChannel[]> {
453
- this.syncChannels()
454
- return [...this.channels].sort((a, b) => a.position - b.position)
455
- }
456
-
457
- async createSalesChannel(input: CreateSalesChannelInput): Promise<SalesChannel> {
458
- this.syncChannels()
459
- const c: SalesChannel = { id: uid(), tenantId: 'mock', kind: input.kind, label: input.label, url: input.url ?? null, enabled: input.enabled ?? true, position: input.position ?? this.channels.length, createdAt: now(), updatedAt: now() }
460
- this.channels.push(c); this.persistChannels(); return c
461
- }
462
-
463
- async updateSalesChannel(id: string, input: UpdateSalesChannelInput): Promise<SalesChannel> {
464
- this.syncChannels()
465
- const i = this.channels.findIndex(c => c.id === id)
466
- if (i < 0) throw new Error('Sales channel not found')
467
- this.channels[i] = { ...this.channels[i], ...input, updatedAt: now() }
468
- this.persistChannels(); return this.channels[i]
469
- }
470
-
471
- async deleteSalesChannel(id: string): Promise<void> {
472
- this.syncChannels()
473
- this.channels = this.channels.filter(c => c.id !== id)
474
- this.persistChannels()
475
- }
476
- }
477
-
478
- export function createMockShopProvider(seed?: MockShopSeed): ShopProvider {
479
- return new MockShopProvider(seed)
480
- }
package/src/mock.ts DELETED
@@ -1,2 +0,0 @@
1
- export { MockShopProvider, createMockShopProvider } from './mock-provider'
2
- export type { MockShopSeed } from './mock-provider'
package/src/provider.ts DELETED
@@ -1,117 +0,0 @@
1
- import type {
2
- Product, ProductImage, Category,
3
- Order, ShopCustomer, Discount, ProductEnquiry,
4
- CreateProductInput, UpdateProductInput, ListProductsOptions,
5
- CreateProductEnquiryInput, ListProductEnquiriesOptions,
6
- CreateCategoryInput, UpdateCategoryInput,
7
- CreateOrderInput, UpdateOrderInput, ListOrdersOptions, PlaceOrderInput,
8
- CreateCustomerInput, UpdateCustomerInput, ListCustomersOptions, ResolveCustomerInput,
9
- CustomerAddress, ShippingQuoteOption,
10
- ShippingZone, CreateShippingZoneInput, UpdateShippingZoneInput,
11
- CreateDiscountInput, UpdateDiscountInput, ListDiscountsOptions,
12
- SalesChannel, CreateSalesChannelInput, UpdateSalesChannelInput,
13
- } from './types'
14
-
15
- export interface ShopProvider {
16
- // Products
17
- listProducts(options?: ListProductsOptions): Promise<Product[]>
18
- getProduct(id: string): Promise<Product | null>
19
- createProduct(input: CreateProductInput): Promise<Product>
20
- updateProduct(id: string, input: UpdateProductInput): Promise<Product>
21
- deleteProduct(id: string): Promise<void>
22
- uploadProductImage(productId: string, file: File): Promise<ProductImage>
23
- deleteProductImage(imageId: string): Promise<void>
24
- /**
25
- * Reorder a gallery or promote a photo to primary. Optional so a provider
26
- * that only ever holds one image per product need not implement it.
27
- *
28
- * Setting `isPrimary` is exclusive: the previous primary is demoted in the
29
- * same call, because two primaries make "the product photo" ambiguous
30
- * everywhere it is read (card, cart line, order item, admin table).
31
- */
32
- updateProductImage?(
33
- imageId: string,
34
- input: { isPrimary?: boolean; sortOrder?: number; altText?: string | null },
35
- ): Promise<ProductImage>
36
-
37
- // Product enquiries
38
- createProductEnquiry?(input: CreateProductEnquiryInput): Promise<ProductEnquiry>
39
- listProductEnquiries?(options?: ListProductEnquiriesOptions): Promise<ProductEnquiry[]>
40
-
41
- // Categories
42
- listCategories(): Promise<Category[]>
43
- createCategory(input: CreateCategoryInput): Promise<Category>
44
- updateCategory(id: string, input: UpdateCategoryInput): Promise<Category>
45
- deleteCategory(id: string): Promise<void>
46
-
47
- // Orders
48
- listOrders(options?: ListOrdersOptions): Promise<Order[]>
49
- getOrder(id: string): Promise<Order | null>
50
- createOrder(input: CreateOrderInput): Promise<Order>
51
- updateOrder(id: string, input: UpdateOrderInput): Promise<Order>
52
- /**
53
- * Mock/dev payment seam: pending->paid transition via a whitelisted RPC
54
- * (anon storefronts have no UPDATE grant on the orders table). Real PSPs
55
- * confirm via webhook with service_role instead.
56
- */
57
- confirmPayment?(id: string, reference?: string): Promise<Order | null>
58
- /**
59
- * Trusted order placement: the server re-reads product prices + stock,
60
- * validates the discount, computes totals, and decrements inventory
61
- * atomically. Storefront checkout MUST use this instead of createOrder so the
62
- * browser cannot tamper with prices. createOrder remains for trusted admin entry.
63
- */
64
- placeOrder(input: PlaceOrderInput): Promise<Order>
65
-
66
- // Customers
67
- listCustomers(options?: ListCustomersOptions): Promise<ShopCustomer[]>
68
- getCustomer(id: string): Promise<ShopCustomer | null>
69
- createCustomer(input: CreateCustomerInput): Promise<ShopCustomer>
70
- updateCustomer(id: string, input: UpdateCustomerInput): Promise<ShopCustomer>
71
- deleteCustomer(id: string): Promise<void>
72
- /**
73
- * Find-or-create the customer for a storefront shopper and (on Supabase) link
74
- * it to auth.uid server-side, so order reads can be scoped to the owner by RLS.
75
- * Optional: providers without it fall back to listCustomers+createCustomer.
76
- */
77
- resolveCustomer?(input: ResolveCustomerInput): Promise<ShopCustomer>
78
- /**
79
- * The signed-in shopper's saved delivery addresses, so checkout can offer
80
- * them instead of inventing a placeholder. Optional: a provider without it
81
- * (or an anonymous request) yields an empty book and a blank form.
82
- */
83
- listCustomerAddresses?(customerId: string): Promise<CustomerAddress[]>
84
-
85
- // Delivery
86
- /**
87
- * Delivery options for a postal code at a given subtotal. An empty array
88
- * means the store does not deliver there — shown before the buyer commits to
89
- * a cart, which is the whole point of quoting on the product page.
90
- *
91
- * This is for DISPLAY. shop_place_order recomputes the freight from the same
92
- * zones and ignores whatever the client sends, so a tampered quote changes
93
- * what the shopper sees and nothing about what they are charged.
94
- */
95
- quoteShipping?(postalCode: string, subtotal: number): Promise<ShippingQuoteOption[]>
96
-
97
- /** Delivery zones the merchant maintains. Admin-only: the storefront reads
98
- * quotes, never the ranges themselves. */
99
- listShippingZones?(): Promise<ShippingZone[]>
100
- createShippingZone?(input: CreateShippingZoneInput): Promise<ShippingZone>
101
- updateShippingZone?(id: string, input: UpdateShippingZoneInput): Promise<ShippingZone>
102
- deleteShippingZone?(id: string): Promise<void>
103
-
104
- // Discounts
105
- listDiscounts(options?: ListDiscountsOptions): Promise<Discount[]>
106
- getDiscount(id: string): Promise<Discount | null>
107
- createDiscount(input: CreateDiscountInput): Promise<Discount>
108
- updateDiscount(id: string, input: UpdateDiscountInput): Promise<Discount>
109
- deleteDiscount(id: string): Promise<void>
110
-
111
- // Sales channels — the store's points of sale (Loja Online, WhatsApp, …).
112
- // Optional: a provider without them yields no "Canais de venda" section.
113
- listSalesChannels?(): Promise<SalesChannel[]>
114
- createSalesChannel?(input: CreateSalesChannelInput): Promise<SalesChannel>
115
- updateSalesChannel?(id: string, input: UpdateSalesChannelInput): Promise<SalesChannel>
116
- deleteSalesChannel?(id: string): Promise<void>
117
- }
package/src/runtime.ts DELETED
@@ -1,20 +0,0 @@
1
- import type { ShopProvider } from './provider'
2
- import { createMockShopProvider } from './mock-provider'
3
-
4
- let provider: ShopProvider | null = null
5
- let mockFallback: ShopProvider | null = null
6
-
7
- export function getShopProvider(): ShopProvider {
8
- if (provider) return provider
9
- if (!mockFallback) mockFallback = createMockShopProvider()
10
- return mockFallback
11
- }
12
-
13
- export function setShopProvider(nextProvider: ShopProvider): void {
14
- provider = nextProvider
15
- }
16
-
17
- export function resetShopProvider(): void {
18
- provider = null
19
- mockFallback = null
20
- }