@fayz-ai/shop 0.9.0 → 0.10.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 (144) 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-3GERSY5F.js +526 -0
  8. package/dist/chunk-3GERSY5F.js.map +1 -0
  9. package/dist/chunk-FQPGE3YN.js +3 -0
  10. package/dist/{chunk-JR25TISO.js.map → chunk-FQPGE3YN.js.map} +1 -1
  11. package/dist/{chunk-CTKZ6O2J.js → chunk-IKNGCRUM.js} +41 -15
  12. package/dist/chunk-IKNGCRUM.js.map +1 -0
  13. package/dist/{chunk-IYQHKMLB.js → chunk-IL6GA4ER.js} +4 -4
  14. package/dist/chunk-IL6GA4ER.js.map +1 -0
  15. package/dist/chunk-KLDS7IPU.js +17 -0
  16. package/dist/chunk-KLDS7IPU.js.map +1 -0
  17. package/dist/chunk-RGTIJQJL.js +93 -0
  18. package/dist/chunk-RGTIJQJL.js.map +1 -0
  19. package/dist/{chunk-SVTCJ6HP.js → chunk-U3O2I3RM.js} +395 -27
  20. package/dist/chunk-U3O2I3RM.js.map +1 -0
  21. package/dist/chunk-XERLK5LD.js +1415 -0
  22. package/dist/chunk-XERLK5LD.js.map +1 -0
  23. package/dist/chunk-XOP6SHPB.js +142 -0
  24. package/dist/chunk-XOP6SHPB.js.map +1 -0
  25. package/dist/client.d.ts +2 -0
  26. package/dist/client.d.ts.map +1 -0
  27. package/dist/{mock-catalog.d.ts → data/mock-catalog.d.ts} +21 -1
  28. package/dist/data/mock-catalog.d.ts.map +1 -0
  29. package/dist/data/mock-provider.d.ts +133 -0
  30. package/dist/data/mock-provider.d.ts.map +1 -0
  31. package/dist/data/mock.d.ts +3 -0
  32. package/dist/data/mock.d.ts.map +1 -0
  33. package/dist/data/provider.d.ts +142 -0
  34. package/dist/data/provider.d.ts.map +1 -0
  35. package/dist/data/supabase-provider.d.ts +100 -0
  36. package/dist/data/supabase-provider.d.ts.map +1 -0
  37. package/dist/data/tables.d.ts +35 -0
  38. package/dist/data/tables.d.ts.map +1 -0
  39. package/dist/data/tenant.d.ts.map +1 -0
  40. package/dist/entities/entity-reconcile.d.ts +32 -0
  41. package/dist/entities/entity-reconcile.d.ts.map +1 -0
  42. package/dist/entities/entity-sql.d.ts +50 -0
  43. package/dist/entities/entity-sql.d.ts.map +1 -0
  44. package/dist/entities/entity-store.d.ts +58 -0
  45. package/dist/entities/entity-store.d.ts.map +1 -0
  46. package/dist/entities/index.d.ts +11 -0
  47. package/dist/entities/index.d.ts.map +1 -0
  48. package/dist/entities/mock-entity-store.d.ts +17 -0
  49. package/dist/entities/mock-entity-store.d.ts.map +1 -0
  50. package/dist/entities/sidecar.d.ts +9 -0
  51. package/dist/entities/sidecar.d.ts.map +1 -0
  52. package/dist/entities/supabase-entity-store.d.ts +8 -0
  53. package/dist/entities/supabase-entity-store.d.ts.map +1 -0
  54. package/dist/entities.js +159 -0
  55. package/dist/entities.js.map +1 -0
  56. package/dist/index.d.ts +17 -10
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +728 -41
  59. package/dist/index.js.map +1 -1
  60. package/dist/inventory/allocation.d.ts +98 -0
  61. package/dist/inventory/allocation.d.ts.map +1 -0
  62. package/dist/inventory/index.d.ts +13 -0
  63. package/dist/inventory/index.d.ts.map +1 -0
  64. package/dist/inventory/mock-stock-store.d.ts +89 -0
  65. package/dist/inventory/mock-stock-store.d.ts.map +1 -0
  66. package/dist/inventory/postgrest-double.d.ts +65 -0
  67. package/dist/inventory/postgrest-double.d.ts.map +1 -0
  68. package/dist/inventory/shopify-mapping.d.ts +67 -0
  69. package/dist/inventory/shopify-mapping.d.ts.map +1 -0
  70. package/dist/inventory/stock-contract.d.ts +46 -0
  71. package/dist/inventory/stock-contract.d.ts.map +1 -0
  72. package/dist/inventory/stock-store.d.ts +96 -0
  73. package/dist/inventory/stock-store.d.ts.map +1 -0
  74. package/dist/inventory/stock-types.d.ts +292 -0
  75. package/dist/inventory/stock-types.d.ts.map +1 -0
  76. package/dist/inventory/supabase-stock-store.d.ts +59 -0
  77. package/dist/inventory/supabase-stock-store.d.ts.map +1 -0
  78. package/dist/inventory/vocabulary.d.ts +86 -0
  79. package/dist/inventory/vocabulary.d.ts.map +1 -0
  80. package/dist/inventory.js +4 -0
  81. package/dist/inventory.js.map +1 -0
  82. package/dist/lib/catalog.d.ts +4 -0
  83. package/dist/lib/catalog.d.ts.map +1 -0
  84. package/dist/{commerce.d.ts → lib/commerce.d.ts} +2 -2
  85. package/dist/lib/commerce.d.ts.map +1 -0
  86. package/dist/lib/emails.d.ts +50 -0
  87. package/dist/lib/emails.d.ts.map +1 -0
  88. package/dist/lib/message-events.d.ts +4 -0
  89. package/dist/lib/message-events.d.ts.map +1 -0
  90. package/dist/lib/runtime.d.ts +5 -0
  91. package/dist/lib/runtime.d.ts.map +1 -0
  92. package/dist/mock.d.ts +1 -2
  93. package/dist/mock.d.ts.map +1 -1
  94. package/dist/mock.js +4 -2
  95. package/dist/provider.d.ts +1 -88
  96. package/dist/provider.d.ts.map +1 -1
  97. package/dist/rules/adapter.d.ts +36 -0
  98. package/dist/rules/adapter.d.ts.map +1 -0
  99. package/dist/rules/attributes.d.ts +64 -0
  100. package/dist/rules/attributes.d.ts.map +1 -0
  101. package/dist/rules/context.d.ts +43 -0
  102. package/dist/rules/context.d.ts.map +1 -0
  103. package/dist/rules/evaluator.d.ts +37 -0
  104. package/dist/rules/evaluator.d.ts.map +1 -0
  105. package/dist/rules/index.d.ts +13 -0
  106. package/dist/rules/index.d.ts.map +1 -0
  107. package/dist/rules/prices.d.ts +25 -0
  108. package/dist/rules/prices.d.ts.map +1 -0
  109. package/dist/rules/rule-types.d.ts +160 -0
  110. package/dist/rules/rule-types.d.ts.map +1 -0
  111. package/dist/rules.js +22 -0
  112. package/dist/rules.js.map +1 -0
  113. package/dist/runtime.d.ts +1 -4
  114. package/dist/runtime.d.ts.map +1 -1
  115. package/dist/runtime.js +5 -3
  116. package/dist/types.d.ts +231 -0
  117. package/dist/types.d.ts.map +1 -1
  118. package/package.json +26 -20
  119. package/dist/chunk-CTKZ6O2J.js.map +0 -1
  120. package/dist/chunk-IYQHKMLB.js.map +0 -1
  121. package/dist/chunk-JR25TISO.js +0 -3
  122. package/dist/chunk-SVTCJ6HP.js.map +0 -1
  123. package/dist/commerce.d.ts.map +0 -1
  124. package/dist/mock-catalog.d.ts.map +0 -1
  125. package/dist/mock-provider.d.ts +0 -60
  126. package/dist/mock-provider.d.ts.map +0 -1
  127. package/dist/supabase-provider.d.ts +0 -56
  128. package/dist/supabase-provider.d.ts.map +0 -1
  129. package/dist/tables.d.ts +0 -12
  130. package/dist/tables.d.ts.map +0 -1
  131. package/dist/tenant.d.ts.map +0 -1
  132. package/src/catalog.ts +0 -3
  133. package/src/commerce.ts +0 -120
  134. package/src/index.ts +0 -40
  135. package/src/mock-catalog.ts +0 -266
  136. package/src/mock-provider.ts +0 -480
  137. package/src/mock.ts +0 -2
  138. package/src/provider.ts +0 -117
  139. package/src/runtime.ts +0 -20
  140. package/src/supabase-provider.ts +0 -754
  141. package/src/tables.ts +0 -17
  142. package/src/tenant.ts +0 -21
  143. package/src/types.ts +0 -623
  144. /package/dist/{tenant.d.ts → data/tenant.d.ts} +0 -0
@@ -1,754 +0,0 @@
1
- import { getSupabaseClientOptional } from '@fayz-ai/core'
2
- import type { ShopProvider } from './provider'
3
- import { getShopTenantId } from './tenant'
4
- import { T } from './tables'
5
- import type {
6
- Product, ProductImage, Category,
7
- Order, ShopCustomer, Discount,
8
- CreateProductInput, UpdateProductInput, ListProductsOptions,
9
- CreateCategoryInput, UpdateCategoryInput,
10
- CreateOrderInput, UpdateOrderInput, ListOrdersOptions, PlaceOrderInput,
11
- CreateCustomerInput, UpdateCustomerInput, ListCustomersOptions, ResolveCustomerInput,
12
- CreateDiscountInput, UpdateDiscountInput, ListDiscountsOptions, OrderItem,
13
- SalesChannel, CreateSalesChannelInput, UpdateSalesChannelInput,
14
- ShippingZone, CreateShippingZoneInput, UpdateShippingZoneInput } from './types'
15
-
16
- function getDb(): any {
17
- const supabase = getSupabaseClientOptional()
18
- if (!supabase) throw new Error('@fayz-ai/shop: Supabase client not initialized. Call setGlobalSupabaseClient() first.')
19
- return supabase
20
- }
21
-
22
- const getTenantId = getShopTenantId
23
-
24
- function slugify(name: string): string {
25
- return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
26
- }
27
-
28
- function asUuid(value: string | null | undefined): string | null {
29
- if (!value) return null
30
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)
31
- ? value
32
- : null
33
- }
34
-
35
- /** 8 bare digits, zero-padded — the shape shipping_zones stores and compares. */
36
- function toPostalDigits(value: string | undefined | null): string | undefined {
37
- const digits = String(value ?? '').replace(/\D/g, '')
38
- return digits ? digits.padStart(8, '0').slice(0, 8) : undefined
39
- }
40
-
41
- function rowToShippingZone(row: any): ShippingZone {
42
- return {
43
- id: row.id,
44
- tenantId: row.tenant_id,
45
- name: row.name,
46
- carrier: row.carrier ?? null,
47
- postalFrom: String(row.postal_from ?? '').trim(),
48
- postalTo: String(row.postal_to ?? '').trim(),
49
- rate: Number(row.rate ?? 0),
50
- freeAbove: row.free_above == null ? null : Number(row.free_above),
51
- etaMinDays: row.eta_min_days ?? null,
52
- etaMaxDays: row.eta_max_days ?? null,
53
- active: row.active ?? true,
54
- sortOrder: row.sort_order ?? 0,
55
- }
56
- }
57
-
58
- function shippingZonePayload(input: Partial<CreateShippingZoneInput>): Record<string, unknown> {
59
- const payload: Record<string, unknown> = {}
60
- if (input.name !== undefined) payload.name = input.name
61
- if (input.carrier !== undefined) payload.carrier = input.carrier || null
62
- if (input.postalFrom !== undefined) payload.postal_from = toPostalDigits(input.postalFrom)
63
- if (input.postalTo !== undefined) payload.postal_to = toPostalDigits(input.postalTo)
64
- if (input.rate !== undefined) payload.rate = input.rate
65
- if (input.freeAbove !== undefined) payload.free_above = input.freeAbove ?? null
66
- if (input.etaMinDays !== undefined) payload.eta_min_days = input.etaMinDays ?? null
67
- if (input.etaMaxDays !== undefined) payload.eta_max_days = input.etaMaxDays ?? null
68
- if (input.active !== undefined) payload.active = input.active
69
- if (input.sortOrder !== undefined) payload.sort_order = input.sortOrder
70
- return payload
71
- }
72
-
73
- function rowToProduct(row: any): Product {
74
- return {
75
- id: row.id,
76
- tenantId: row.tenant_id,
77
- name: row.name,
78
- slug: row.slug ?? slugify(row.name),
79
- description: row.description,
80
- price: row.price ?? 0,
81
- compareAtPrice: row.compare_at_price,
82
- currency: row.currency ?? 'BRL',
83
- status: row.status ?? 'active',
84
- inventoryCount: row.inventory_count ?? 0,
85
- sku: row.sku,
86
- sortOrder: row.sort_order ?? 0,
87
- metadata: row.metadata ?? {},
88
- images: Array.isArray(row.images) ? row.images.map(rowToImage) : [],
89
- categoryId: row.category_id,
90
- categoryName: row.category_name ?? null,
91
- isPhysical: row.is_physical ?? true,
92
- weight: row.weight,
93
- weightUnit: row.weight_unit ?? 'kg',
94
- createdAt: row.created_at,
95
- updatedAt: row.updated_at,
96
- }
97
- }
98
-
99
- function rowToImage(row: any): ProductImage {
100
- return {
101
- id: row.id,
102
- productId: row.product_id,
103
- url: row.url,
104
- altText: row.alt_text,
105
- sortOrder: row.sort_order ?? 0,
106
- isPrimary: row.is_primary ?? false,
107
- createdAt: row.created_at,
108
- }
109
- }
110
-
111
- function rowToCategory(row: any): Category {
112
- return {
113
- id: row.id,
114
- tenantId: row.tenant_id,
115
- name: row.name,
116
- slug: row.slug ?? slugify(row.name),
117
- description: row.description,
118
- parentId: row.parent_id,
119
- sortOrder: row.sort_order ?? 0,
120
- createdAt: row.created_at,
121
- }
122
- }
123
-
124
- /**
125
- * Order items were being passed through raw, so every snake_case column arrived
126
- * undefined on the camelCase type: unit_price, image_url and product_id were all
127
- * lost. `total` survived only because the name matches in both, which is exactly
128
- * why the bug looked like "price is zero" instead of "nothing is mapped".
129
- */
130
- function rowToOrderItem(row: any): OrderItem {
131
- return {
132
- id: row.id,
133
- orderId: row.order_id,
134
- productId: row.product_id ?? null,
135
- name: row.name,
136
- sku: row.sku ?? null,
137
- quantity: row.quantity ?? 0,
138
- unitPrice: row.unit_price ?? 0,
139
- total: row.total ?? 0,
140
- imageUrl: row.image_url ?? null,
141
- }
142
- }
143
-
144
- function rowToOrder(row: any): Order {
145
- return {
146
- id: row.id,
147
- tenantId: row.tenant_id,
148
- orderNumber: row.order_number,
149
- status: row.status ?? 'open',
150
- financialStatus: row.financial_status ?? 'pending',
151
- fulfillmentStatus: row.fulfillment_status ?? 'unfulfilled',
152
- currency: row.currency ?? 'BRL',
153
- subtotal: row.subtotal ?? 0,
154
- taxTotal: row.tax_total ?? 0,
155
- discountTotal: row.discount_total ?? 0,
156
- shippingTotal: row.shipping_total ?? 0,
157
- total: row.total ?? 0,
158
- customerId: row.customer_id,
159
- customerName: row.customer_name,
160
- customerEmail: row.customer_email,
161
- discountCode: row.discount_code,
162
- notes: row.notes,
163
- shippingAddress: row.shipping_address ?? null,
164
- paymentMethodKind: row.payment_method_kind ?? null,
165
- paidAt: row.paid_at ?? null,
166
- items: Array.isArray(row.items) ? row.items.map(rowToOrderItem) : [],
167
- createdAt: row.created_at,
168
- updatedAt: row.updated_at,
169
- }
170
- }
171
-
172
- function rowToCustomer(row: any): ShopCustomer {
173
- return {
174
- id: row.id,
175
- tenantId: row.tenant_id,
176
- firstName: row.first_name,
177
- lastName: row.last_name ?? '',
178
- email: row.email,
179
- phone: row.phone,
180
- notes: row.notes,
181
- ordersCount: row.orders_count ?? 0,
182
- totalSpent: row.total_spent ?? 0,
183
- createdAt: row.created_at,
184
- updatedAt: row.updated_at,
185
- }
186
- }
187
-
188
- function rowToDiscount(row: any): Discount {
189
- return {
190
- id: row.id,
191
- tenantId: row.tenant_id,
192
- title: row.title,
193
- code: row.code,
194
- type: row.type,
195
- method: row.method ?? 'code',
196
- value: row.value ?? 0,
197
- usageLimit: row.usage_limit,
198
- oncePerCustomer: row.once_per_customer ?? false,
199
- minSubtotal: row.min_subtotal ?? null,
200
- appliesTo: row.applies_to ?? 'all',
201
- productIds: row.product_ids ?? [],
202
- categoryIds: row.category_ids ?? [],
203
- startsAt: row.starts_at,
204
- endsAt: row.ends_at,
205
- status: row.status ?? 'active',
206
- timesUsed: row.times_used ?? 0,
207
- createdAt: row.created_at,
208
- updatedAt: row.updated_at,
209
- }
210
- }
211
-
212
- function rowToSalesChannel(row: any): SalesChannel {
213
- return {
214
- id: row.id,
215
- tenantId: row.tenant_id,
216
- kind: row.kind,
217
- label: row.label,
218
- url: row.url,
219
- enabled: row.enabled ?? true,
220
- position: row.position ?? 0,
221
- createdAt: row.created_at,
222
- updatedAt: row.updated_at,
223
- }
224
- }
225
-
226
- export class SupabaseShopProvider implements ShopProvider {
227
- // ---------------------------------------------------------------------------
228
- // Products
229
- // ---------------------------------------------------------------------------
230
-
231
- async listProducts(options?: ListProductsOptions): Promise<Product[]> {
232
- const db = getDb()
233
- const tenantId = getTenantId()
234
- let q = db.from(T.products).select(`*, images:${T.productImages}(*)`)
235
- if (tenantId) q = q.eq('tenant_id', tenantId)
236
- if (options?.status) q = q.eq('status', options.status)
237
- if (options?.categoryId) q = q.eq('category_id', options.categoryId)
238
- if (options?.slug) q = q.eq('slug', options.slug)
239
- if (options?.search) q = q.ilike('name', `%${options.search}%`)
240
- q = q.order(options?.orderBy ?? 'sort_order', { ascending: options?.order !== 'desc' })
241
- if (options?.limit) q = q.limit(options.limit)
242
- if (options?.offset) q = q.range(options.offset, (options.offset ?? 0) + (options.limit ?? 50) - 1)
243
- const { data, error } = await q
244
- if (error) throw error
245
- return (data ?? []).map(rowToProduct)
246
- }
247
-
248
- async getProduct(id: string): Promise<Product | null> {
249
- const db = getDb()
250
- const { data, error } = await db
251
- .from(T.products)
252
- .select(`*, images:${T.productImages}(*)`)
253
- .eq('id', id)
254
- .maybeSingle()
255
- if (error) throw error
256
- return data ? rowToProduct(data) : null
257
- }
258
-
259
- async createProduct(input: CreateProductInput): Promise<Product> {
260
- const db = getDb()
261
- const tenantId = getTenantId()
262
- const { data, error } = await db
263
- .from(T.products)
264
- .insert({
265
- tenant_id: tenantId,
266
- name: input.name,
267
- slug: input.slug ?? slugify(input.name),
268
- description: input.description ?? null,
269
- price: input.price,
270
- compare_at_price: input.compareAtPrice ?? null,
271
- currency: input.currency ?? 'BRL',
272
- status: input.status ?? 'active',
273
- inventory_count: input.inventoryCount ?? 0,
274
- sku: input.sku ?? null,
275
- sort_order: input.sortOrder ?? 0,
276
- metadata: input.metadata ?? {},
277
- category_id: input.categoryId ?? null,
278
- is_physical: input.isPhysical ?? true,
279
- weight: input.weight ?? null,
280
- weight_unit: input.weightUnit ?? 'kg',
281
- })
282
- .select(`*, images:${T.productImages}(*)`)
283
- .single()
284
- if (error) throw error
285
- return rowToProduct(data)
286
- }
287
-
288
- async updateProduct(id: string, input: UpdateProductInput): Promise<Product> {
289
- const db = getDb()
290
- const updates: Record<string, unknown> = {}
291
- if (input.name !== undefined) { updates.name = input.name; updates.slug = input.slug ?? slugify(input.name) }
292
- if (input.slug !== undefined) updates.slug = input.slug
293
- if (input.description !== undefined) updates.description = input.description
294
- if (input.price !== undefined) updates.price = input.price
295
- if (input.compareAtPrice !== undefined) updates.compare_at_price = input.compareAtPrice
296
- if (input.currency !== undefined) updates.currency = input.currency
297
- if (input.status !== undefined) updates.status = input.status
298
- if (input.inventoryCount !== undefined) updates.inventory_count = input.inventoryCount
299
- if (input.sku !== undefined) updates.sku = input.sku
300
- if (input.categoryId !== undefined) updates.category_id = input.categoryId
301
- if (input.isPhysical !== undefined) updates.is_physical = input.isPhysical
302
- if (input.weight !== undefined) updates.weight = input.weight
303
- const { data, error } = await db
304
- .from(T.products).update(updates).eq('id', id)
305
- .select(`*, images:${T.productImages}(*)`)
306
- .single()
307
- if (error) throw error
308
- return rowToProduct(data)
309
- }
310
-
311
- async deleteProduct(id: string): Promise<void> {
312
- const { error } = await getDb().from(T.products).delete().eq('id', id)
313
- if (error) throw error
314
- }
315
-
316
- async uploadProductImage(productId: string, file: File): Promise<ProductImage> {
317
- const db = getDb()
318
- const tenantId = getTenantId()
319
- const path = `${tenantId}/products/${productId}/${Date.now()}-${file.name}`
320
- const { error: uploadErr } = await db.storage.from('shop-images').upload(path, file)
321
- if (uploadErr) throw uploadErr
322
- const { data: { publicUrl } } = db.storage.from('shop-images').getPublicUrl(path)
323
-
324
- // Every upload used to land as sort_order 0 / is_primary false, so a product
325
- // whose only photo was uploaded here had NO primary at all, and a second one
326
- // tied with the first for position. New photos now go to the end, and the
327
- // first photo a product ever gets becomes its primary.
328
- const { data: existing } = await db
329
- .from(T.productImages)
330
- .select('id,sort_order')
331
- .eq('product_id', productId)
332
- .order('sort_order', { ascending: false })
333
- .limit(1)
334
- const first = !existing || existing.length === 0
335
- const nextOrder = first ? 0 : (existing[0].sort_order ?? 0) + 1
336
-
337
- const { data, error } = await db
338
- .from(T.productImages)
339
- .insert({
340
- product_id: productId, tenant_id: tenantId, url: publicUrl,
341
- sort_order: nextOrder, is_primary: first,
342
- })
343
- .select().single()
344
- if (error) throw error
345
- return rowToImage(data)
346
- }
347
-
348
- async updateProductImage(
349
- imageId: string,
350
- input: { isPrimary?: boolean; sortOrder?: number; altText?: string | null },
351
- ): Promise<ProductImage> {
352
- const db = getDb()
353
- const updates: Record<string, unknown> = {}
354
- if (input.sortOrder !== undefined) updates.sort_order = input.sortOrder
355
- if (input.altText !== undefined) updates.alt_text = input.altText
356
- if (input.isPrimary !== undefined) updates.is_primary = input.isPrimary
357
-
358
- // Demote the incumbent first. Doing it after would leave a window with two
359
- // primaries, and if the second statement failed the product would keep two
360
- // for good.
361
- if (input.isPrimary) {
362
- const { data: target } = await db
363
- .from(T.productImages).select('product_id').eq('id', imageId).single()
364
- if (target?.product_id) {
365
- await db.from(T.productImages)
366
- .update({ is_primary: false })
367
- .eq('product_id', target.product_id)
368
- .neq('id', imageId)
369
- }
370
- }
371
-
372
- const { data, error } = await db
373
- .from(T.productImages).update(updates).eq('id', imageId).select().single()
374
- if (error) throw error
375
- return rowToImage(data)
376
- }
377
-
378
- async deleteProductImage(imageId: string): Promise<void> {
379
- const { error } = await getDb().from(T.productImages).delete().eq('id', imageId)
380
- if (error) throw error
381
- }
382
-
383
- // ---------------------------------------------------------------------------
384
- // Categories
385
- // ---------------------------------------------------------------------------
386
-
387
- async listCategories(): Promise<Category[]> {
388
- const db = getDb()
389
- const tenantId = getTenantId()
390
- let q = db.from(T.categories).select('*').order('sort_order', { ascending: true })
391
- if (tenantId) q = q.eq('tenant_id', tenantId)
392
- const { data, error } = await q
393
- if (error) throw error
394
- return (data ?? []).map(rowToCategory)
395
- }
396
-
397
- async createCategory(input: CreateCategoryInput): Promise<Category> {
398
- const db = getDb()
399
- const { data, error } = await db
400
- .from(T.categories)
401
- .insert({ tenant_id: getTenantId(), name: input.name, slug: input.slug ?? slugify(input.name), description: input.description ?? null, parent_id: input.parentId ?? null, sort_order: input.sortOrder ?? 0 })
402
- .select().single()
403
- if (error) throw error
404
- return rowToCategory(data)
405
- }
406
-
407
- async updateCategory(id: string, input: UpdateCategoryInput): Promise<Category> {
408
- const db = getDb()
409
- const updates: Record<string, unknown> = {}
410
- if (input.name !== undefined) updates.name = input.name
411
- if (input.slug !== undefined) updates.slug = input.slug
412
- if (input.description !== undefined) updates.description = input.description
413
- if (input.parentId !== undefined) updates.parent_id = input.parentId
414
- if (input.sortOrder !== undefined) updates.sort_order = input.sortOrder
415
- const { data, error } = await db.from(T.categories).update(updates).eq('id', id).select().single()
416
- if (error) throw error
417
- return rowToCategory(data)
418
- }
419
-
420
- async deleteCategory(id: string): Promise<void> {
421
- const { error } = await getDb().from(T.categories).delete().eq('id', id)
422
- if (error) throw error
423
- }
424
-
425
- // ---------------------------------------------------------------------------
426
- // Orders
427
- // ---------------------------------------------------------------------------
428
-
429
- async listOrders(options?: ListOrdersOptions): Promise<Order[]> {
430
- const db = getDb()
431
- const tenantId = getTenantId()
432
- let q = db.from(T.orders).select(`*, items:${T.orderItems}(*)`)
433
- if (tenantId) q = q.eq('tenant_id', tenantId)
434
- if (options?.status) q = q.eq('status', options.status)
435
- if (options?.financialStatus) q = q.eq('financial_status', options.financialStatus)
436
- if (options?.fulfillmentStatus) q = q.eq('fulfillment_status', options.fulfillmentStatus)
437
- if (options?.customerId) q = q.eq('customer_id', options.customerId)
438
- if (options?.customerEmail) q = q.eq('customer_email', options.customerEmail)
439
- if (options?.search) q = q.or(`customer_name.ilike.%${options.search}%,customer_email.ilike.%${options.search}%`)
440
- q = q.order('created_at', { ascending: false })
441
- if (options?.limit) q = q.limit(options.limit)
442
- const { data, error } = await q
443
- if (error) throw error
444
- return (data ?? []).map(rowToOrder)
445
- }
446
-
447
- async getOrder(id: string): Promise<Order | null> {
448
- const { data, error } = await getDb()
449
- .from(T.orders).select(`*, items:${T.orderItems}(*)`)
450
- .eq('id', id).maybeSingle()
451
- if (error) throw error
452
- return data ? rowToOrder(data) : null
453
- }
454
-
455
- async createOrder(input: CreateOrderInput): Promise<Order> {
456
- const db = getDb()
457
- const tenantId = getTenantId()
458
- const subtotal = input.items.reduce((s, i) => s + i.quantity * i.unitPrice, 0)
459
- const discountTotal = input.discountTotal ?? 0
460
- const shippingTotal = input.shippingTotal ?? 0
461
- const total = Math.round((subtotal - discountTotal + shippingTotal) * 100) / 100
462
- const { data: order, error: orderErr } = await db
463
- .from(T.orders)
464
- .insert({ tenant_id: tenantId, customer_id: input.customerId ?? null, customer_name: input.customerName ?? null, customer_email: input.customerEmail ?? null, currency: input.currency ?? 'BRL', notes: input.notes ?? null, subtotal, discount_total: discountTotal, shipping_total: shippingTotal, discount_code: input.discountCode ?? null, total, status: 'open', financial_status: 'pending', fulfillment_status: 'unfulfilled' })
465
- .select().single()
466
- if (orderErr) throw orderErr
467
- const items = input.items.map(i => ({ order_id: order.id, product_id: asUuid(i.productId), name: i.name, sku: i.sku ?? null, quantity: i.quantity, unit_price: i.unitPrice, total: i.quantity * i.unitPrice, image_url: i.imageUrl ?? null }))
468
- await db.from(T.orderItems).insert(items)
469
- return this.getOrder(order.id) as Promise<Order>
470
- }
471
-
472
- async updateOrder(id: string, input: UpdateOrderInput): Promise<Order> {
473
- const updates: Record<string, unknown> = {}
474
- if (input.status !== undefined) updates.status = input.status
475
- if (input.financialStatus !== undefined) updates.financial_status = input.financialStatus
476
- if (input.fulfillmentStatus !== undefined) updates.fulfillment_status = input.fulfillmentStatus
477
- if (input.notes !== undefined) updates.notes = input.notes
478
- const { error } = await getDb().from(T.orders).update(updates).eq('id', id)
479
- if (error) throw error
480
- return this.getOrder(id) as Promise<Order>
481
- }
482
-
483
- // -------------------------------------------------------------------------
484
- // Delivery zones
485
- //
486
- // Postal codes are stored as 8 bare digits so the range test in
487
- // shop_quote_shipping is a plain BETWEEN. Accepting '22.041-001' from the
488
- // form and normalising here means the merchant never has to think about it.
489
- // -------------------------------------------------------------------------
490
- async listShippingZones(): Promise<ShippingZone[]> {
491
- const tenantId = getTenantId()
492
- const { data, error } = await getDb()
493
- .from(T.shippingZones)
494
- .select('*')
495
- .eq('tenant_id', tenantId)
496
- .order('sort_order', { ascending: true })
497
- .order('postal_from', { ascending: true })
498
- if (error) throw error
499
- return (data ?? []).map(rowToShippingZone)
500
- }
501
-
502
- async createShippingZone(input: CreateShippingZoneInput): Promise<ShippingZone> {
503
- const tenantId = getTenantId()
504
- if (!tenantId) throw new Error('@fayz-ai/shop: createShippingZone requires a tenant id.')
505
- const { data, error } = await getDb()
506
- .from(T.shippingZones)
507
- .insert({ tenant_id: tenantId, ...shippingZonePayload(input) })
508
- .select()
509
- .single()
510
- if (error) throw error
511
- return rowToShippingZone(data)
512
- }
513
-
514
- async updateShippingZone(id: string, input: UpdateShippingZoneInput): Promise<ShippingZone> {
515
- const { data, error } = await getDb()
516
- .from(T.shippingZones)
517
- .update(shippingZonePayload(input))
518
- .eq('id', id)
519
- .select()
520
- .single()
521
- if (error) throw error
522
- return rowToShippingZone(data)
523
- }
524
-
525
- async deleteShippingZone(id: string): Promise<void> {
526
- const { error } = await getDb().from(T.shippingZones).delete().eq('id', id)
527
- if (error) throw error
528
- }
529
-
530
- /**
531
- * Settle a pending order: the merchant confirming that the money arrived.
532
- *
533
- * Goes through the RPC rather than an UPDATE so the payment ledger row in
534
- * public.transactions closes in the same transaction as the order — set
535
- * financial_status by hand and the order says paid while the ledger still
536
- * says pending. Since 0019 the RPC also enforces who may call it: a member of
537
- * the owning tenant, or a PSP webhook with the service role.
538
- */
539
- async confirmPayment(id: string, reference?: string): Promise<Order | null> {
540
- const { error } = await getDb().rpc('shop_confirm_payment', {
541
- p_order_id: id,
542
- p_reference: reference ?? null,
543
- })
544
- if (error) throw error
545
- return this.getOrder(id)
546
- }
547
-
548
- // Trusted placement — delegates all price/discount/inventory authority to the
549
- // shop_place_order RPC (SECURITY DEFINER). The browser never sets a price.
550
- async placeOrder(input: PlaceOrderInput): Promise<Order> {
551
- const db = getDb()
552
- const tenantId = input.tenantId ?? getTenantId()
553
- if (!tenantId) throw new Error('@fayz-ai/shop: placeOrder requires a tenant id (configure the shop tenant resolver).')
554
- const { data, error } = await db.rpc('shop_place_order', {
555
- p_tenant_id: tenantId,
556
- p_items: input.items.map(i => ({ product_id: i.productId, quantity: i.quantity, options_label: i.optionsLabel ?? null })),
557
- p_customer_id: asUuid(input.customerId),
558
- p_customer_name: input.customer?.name ?? null,
559
- p_customer_email: input.customer?.email ?? null,
560
- p_currency: input.currency ?? 'BRL',
561
- p_discount_code: input.discountCode ?? null,
562
- p_shipping_total: input.shippingTotal ?? 0,
563
- // snake_case keys on purpose: the RPC freezes this jsonb onto the
564
- // order and public.addresses uses these exact column names.
565
- p_shipping_address: input.shippingAddress
566
- ? {
567
- postal_code: input.shippingAddress.postalCode,
568
- street: input.shippingAddress.street,
569
- number: input.shippingAddress.number ?? null,
570
- complement: input.shippingAddress.complement ?? null,
571
- district: input.shippingAddress.district ?? null,
572
- city: input.shippingAddress.city,
573
- state: input.shippingAddress.state,
574
- country: input.shippingAddress.country ?? 'BR',
575
- recipient: input.shippingAddress.recipient ?? null,
576
- phone: input.shippingAddress.phone ?? null,
577
- label: input.shippingAddress.label ?? null,
578
- }
579
- : null,
580
- p_payment_method: input.paymentMethod ?? null,
581
- p_notes: input.notes ?? null,
582
- })
583
- if (error) throw error
584
- const orderId = typeof data === 'string' ? data : (data?.id ?? data?.order_id ?? data)
585
- const order = await this.getOrder(orderId)
586
- if (!order) throw new Error('@fayz-ai/shop: placeOrder did not return a persisted order.')
587
- return order
588
- }
589
-
590
- // ---------------------------------------------------------------------------
591
- // Customers
592
- // ---------------------------------------------------------------------------
593
-
594
- async listCustomers(options?: ListCustomersOptions): Promise<ShopCustomer[]> {
595
- const db = getDb()
596
- const tenantId = getTenantId()
597
- let q = db.from(T.customers).select('*').order('first_name', { ascending: true })
598
- if (tenantId) q = q.eq('tenant_id', tenantId)
599
- if (options?.search) q = q.or(`first_name.ilike.%${options.search}%,last_name.ilike.%${options.search}%,email.ilike.%${options.search}%`)
600
- if (options?.limit) q = q.limit(options.limit)
601
- const { data, error } = await q
602
- if (error) throw error
603
- return (data ?? []).map(rowToCustomer)
604
- }
605
-
606
- async getCustomer(id: string): Promise<ShopCustomer | null> {
607
- const { data, error } = await getDb().from(T.customers).select('*').eq('id', id).maybeSingle()
608
- if (error) throw error
609
- return data ? rowToCustomer(data) : null
610
- }
611
-
612
- async createCustomer(input: CreateCustomerInput): Promise<ShopCustomer> {
613
- const { data, error } = await getDb()
614
- .from(T.customers)
615
- .insert({ tenant_id: getTenantId(), first_name: input.firstName, last_name: input.lastName ?? '', email: input.email ?? null, phone: input.phone ?? null, notes: input.notes ?? null })
616
- .select().single()
617
- if (error) throw error
618
- return rowToCustomer(data)
619
- }
620
-
621
- async updateCustomer(id: string, input: UpdateCustomerInput): Promise<ShopCustomer> {
622
- const updates: Record<string, unknown> = {}
623
- if (input.firstName !== undefined) updates.first_name = input.firstName
624
- if (input.lastName !== undefined) updates.last_name = input.lastName
625
- if (input.email !== undefined) updates.email = input.email
626
- if (input.phone !== undefined) updates.phone = input.phone
627
- if (input.notes !== undefined) updates.notes = input.notes
628
- const { data, error } = await getDb().from(T.customers).update(updates).eq('id', id).select().single()
629
- if (error) throw error
630
- return rowToCustomer(data)
631
- }
632
-
633
- async deleteCustomer(id: string): Promise<void> {
634
- const { error } = await getDb().from(T.customers).delete().eq('id', id)
635
- if (error) throw error
636
- }
637
-
638
- // Find-or-create + link to auth.uid server-side (SECURITY DEFINER RPC), so the
639
- // browser never supplies the auth id and "my orders" is RLS-scoped to the owner.
640
- async resolveCustomer(input: ResolveCustomerInput): Promise<ShopCustomer> {
641
- const db = getDb()
642
- const tenantId = input.tenantId ?? getTenantId()
643
- if (!tenantId) throw new Error('@fayz-ai/shop: resolveCustomer requires a tenant id.')
644
- const { data, error } = await db.rpc('shop_resolve_customer', {
645
- p_tenant_id: tenantId,
646
- p_email: input.email,
647
- p_name: input.name ?? null,
648
- })
649
- if (error) throw error
650
- const row = Array.isArray(data) ? data[0] : data
651
- if (!row) throw new Error('@fayz-ai/shop: resolveCustomer returned no row.')
652
- return rowToCustomer(row)
653
- }
654
-
655
- // ---------------------------------------------------------------------------
656
- // Discounts
657
- // ---------------------------------------------------------------------------
658
-
659
- async listDiscounts(options?: ListDiscountsOptions): Promise<Discount[]> {
660
- const db = getDb()
661
- const tenantId = getTenantId()
662
- let q = db.from(T.discounts).select('*').order('created_at', { ascending: false })
663
- if (tenantId) q = q.eq('tenant_id', tenantId)
664
- if (options?.status) q = q.eq('status', options.status)
665
- if (options?.type) q = q.eq('type', options.type)
666
- if (options?.search) q = q.ilike('title', `%${options.search}%`)
667
- if (options?.limit) q = q.limit(options.limit)
668
- const { data, error } = await q
669
- if (error) throw error
670
- return (data ?? []).map(rowToDiscount)
671
- }
672
-
673
- async getDiscount(id: string): Promise<Discount | null> {
674
- const { data, error } = await getDb().from(T.discounts).select('*').eq('id', id).maybeSingle()
675
- if (error) throw error
676
- return data ? rowToDiscount(data) : null
677
- }
678
-
679
- async createDiscount(input: CreateDiscountInput): Promise<Discount> {
680
- const { data, error } = await getDb()
681
- .from(T.discounts)
682
- .insert({ tenant_id: getTenantId(), title: input.title, code: input.code ?? null, type: input.type, method: input.method ?? 'code', value: input.value, usage_limit: input.usageLimit ?? null, once_per_customer: input.oncePerCustomer ?? false, min_subtotal: input.minSubtotal ?? null, applies_to: input.appliesTo ?? 'all', product_ids: input.productIds ?? [], category_ids: input.categoryIds ?? [], starts_at: input.startsAt ?? new Date().toISOString(), ends_at: input.endsAt ?? null, status: input.status ?? 'active', times_used: 0 })
683
- .select().single()
684
- if (error) throw error
685
- return rowToDiscount(data)
686
- }
687
-
688
- async updateDiscount(id: string, input: UpdateDiscountInput): Promise<Discount> {
689
- const updates: Record<string, unknown> = {}
690
- if (input.title !== undefined) updates.title = input.title
691
- if (input.code !== undefined) updates.code = input.code
692
- if (input.type !== undefined) updates.type = input.type
693
- if (input.value !== undefined) updates.value = input.value
694
- if (input.usageLimit !== undefined) updates.usage_limit = input.usageLimit
695
- if (input.minSubtotal !== undefined) updates.min_subtotal = input.minSubtotal
696
- if (input.appliesTo !== undefined) updates.applies_to = input.appliesTo
697
- if (input.productIds !== undefined) updates.product_ids = input.productIds
698
- if (input.categoryIds !== undefined) updates.category_ids = input.categoryIds
699
- if (input.status !== undefined) updates.status = input.status
700
- if (input.endsAt !== undefined) updates.ends_at = input.endsAt
701
- const { data, error } = await getDb().from(T.discounts).update(updates).eq('id', id).select().single()
702
- if (error) throw error
703
- return rowToDiscount(data)
704
- }
705
-
706
- async deleteDiscount(id: string): Promise<void> {
707
- const { error } = await getDb().from(T.discounts).delete().eq('id', id)
708
- if (error) throw error
709
- }
710
-
711
- // ---------------------------------------------------------------------------
712
- // Sales channels
713
- // ---------------------------------------------------------------------------
714
-
715
- async listSalesChannels(): Promise<SalesChannel[]> {
716
- const db = getDb()
717
- const tenantId = getTenantId()
718
- let q = db.from(T.salesChannels).select('*').order('position', { ascending: true })
719
- if (tenantId) q = q.eq('tenant_id', tenantId)
720
- const { data, error } = await q
721
- if (error) throw error
722
- return (data ?? []).map(rowToSalesChannel)
723
- }
724
-
725
- async createSalesChannel(input: CreateSalesChannelInput): Promise<SalesChannel> {
726
- const { data, error } = await getDb()
727
- .from(T.salesChannels)
728
- .insert({ tenant_id: getTenantId(), kind: input.kind, label: input.label, url: input.url ?? null, enabled: input.enabled ?? true, position: input.position ?? 0 })
729
- .select().single()
730
- if (error) throw error
731
- return rowToSalesChannel(data)
732
- }
733
-
734
- async updateSalesChannel(id: string, input: UpdateSalesChannelInput): Promise<SalesChannel> {
735
- const updates: Record<string, unknown> = {}
736
- if (input.kind !== undefined) updates.kind = input.kind
737
- if (input.label !== undefined) updates.label = input.label
738
- if (input.url !== undefined) updates.url = input.url
739
- if (input.enabled !== undefined) updates.enabled = input.enabled
740
- if (input.position !== undefined) updates.position = input.position
741
- const { data, error } = await getDb().from(T.salesChannels).update(updates).eq('id', id).select().single()
742
- if (error) throw error
743
- return rowToSalesChannel(data)
744
- }
745
-
746
- async deleteSalesChannel(id: string): Promise<void> {
747
- const { error } = await getDb().from(T.salesChannels).delete().eq('id', id)
748
- if (error) throw error
749
- }
750
- }
751
-
752
- export function createSupabaseShopProvider(): ShopProvider {
753
- return new SupabaseShopProvider()
754
- }