@fayz-ai/shop 0.2.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 (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +36 -0
  3. package/dist/catalog.cjs +176 -0
  4. package/dist/catalog.cjs.map +1 -0
  5. package/dist/catalog.d.ts +4 -0
  6. package/dist/catalog.d.ts.map +1 -0
  7. package/dist/catalog.js +170 -0
  8. package/dist/catalog.js.map +1 -0
  9. package/dist/commerce.d.ts +28 -0
  10. package/dist/commerce.d.ts.map +1 -0
  11. package/dist/index.cjs +1047 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.d.ts +12 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +1027 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/mock-catalog.d.ts +42 -0
  18. package/dist/mock-catalog.d.ts.map +1 -0
  19. package/dist/mock-provider.d.ts +48 -0
  20. package/dist/mock-provider.d.ts.map +1 -0
  21. package/dist/mock.cjs +430 -0
  22. package/dist/mock.cjs.map +1 -0
  23. package/dist/mock.d.ts +3 -0
  24. package/dist/mock.d.ts.map +1 -0
  25. package/dist/mock.js +427 -0
  26. package/dist/mock.js.map +1 -0
  27. package/dist/provider.cjs +4 -0
  28. package/dist/provider.cjs.map +1 -0
  29. package/dist/provider.d.ts +42 -0
  30. package/dist/provider.d.ts.map +1 -0
  31. package/dist/provider.js +3 -0
  32. package/dist/provider.js.map +1 -0
  33. package/dist/runtime.cjs +447 -0
  34. package/dist/runtime.cjs.map +1 -0
  35. package/dist/runtime.d.ts +5 -0
  36. package/dist/runtime.d.ts.map +1 -0
  37. package/dist/runtime.js +443 -0
  38. package/dist/runtime.js.map +1 -0
  39. package/dist/supabase-provider.d.ts +33 -0
  40. package/dist/supabase-provider.d.ts.map +1 -0
  41. package/dist/tenant.d.ts +4 -0
  42. package/dist/tenant.d.ts.map +1 -0
  43. package/dist/types.cjs +4 -0
  44. package/dist/types.cjs.map +1 -0
  45. package/dist/types.d.ts +345 -0
  46. package/dist/types.d.ts.map +1 -0
  47. package/dist/types.js +3 -0
  48. package/dist/types.js.map +1 -0
  49. package/package.json +99 -0
  50. package/src/catalog.ts +3 -0
  51. package/src/commerce.ts +120 -0
  52. package/src/index.ts +34 -0
  53. package/src/mock-catalog.ts +258 -0
  54. package/src/mock-provider.ts +372 -0
  55. package/src/mock.ts +2 -0
  56. package/src/provider.ts +59 -0
  57. package/src/runtime.ts +20 -0
  58. package/src/supabase-provider.ts +499 -0
  59. package/src/tenant.ts +21 -0
  60. package/src/types.ts +401 -0
@@ -0,0 +1,59 @@
1
+ import type {
2
+ Product, ProductImage, Category,
3
+ Order, ShopCustomer, Discount,
4
+ CreateProductInput, UpdateProductInput, ListProductsOptions,
5
+ CreateCategoryInput, UpdateCategoryInput,
6
+ CreateOrderInput, UpdateOrderInput, ListOrdersOptions, PlaceOrderInput,
7
+ CreateCustomerInput, UpdateCustomerInput, ListCustomersOptions, ResolveCustomerInput,
8
+ CreateDiscountInput, UpdateDiscountInput, ListDiscountsOptions,
9
+ } from './types'
10
+
11
+ export interface ShopProvider {
12
+ // Products
13
+ listProducts(options?: ListProductsOptions): Promise<Product[]>
14
+ getProduct(id: string): Promise<Product | null>
15
+ createProduct(input: CreateProductInput): Promise<Product>
16
+ updateProduct(id: string, input: UpdateProductInput): Promise<Product>
17
+ deleteProduct(id: string): Promise<void>
18
+ uploadProductImage(productId: string, file: File): Promise<ProductImage>
19
+ deleteProductImage(imageId: string): Promise<void>
20
+
21
+ // Categories
22
+ listCategories(): Promise<Category[]>
23
+ createCategory(input: CreateCategoryInput): Promise<Category>
24
+ updateCategory(id: string, input: UpdateCategoryInput): Promise<Category>
25
+ deleteCategory(id: string): Promise<void>
26
+
27
+ // Orders
28
+ listOrders(options?: ListOrdersOptions): Promise<Order[]>
29
+ getOrder(id: string): Promise<Order | null>
30
+ createOrder(input: CreateOrderInput): Promise<Order>
31
+ updateOrder(id: string, input: UpdateOrderInput): Promise<Order>
32
+ /**
33
+ * Trusted order placement: the server re-reads product prices + stock,
34
+ * validates the discount, computes totals, and decrements inventory
35
+ * atomically. Storefront checkout MUST use this instead of createOrder so the
36
+ * browser cannot tamper with prices. createOrder remains for trusted admin entry.
37
+ */
38
+ placeOrder(input: PlaceOrderInput): Promise<Order>
39
+
40
+ // Customers
41
+ listCustomers(options?: ListCustomersOptions): Promise<ShopCustomer[]>
42
+ getCustomer(id: string): Promise<ShopCustomer | null>
43
+ createCustomer(input: CreateCustomerInput): Promise<ShopCustomer>
44
+ updateCustomer(id: string, input: UpdateCustomerInput): Promise<ShopCustomer>
45
+ deleteCustomer(id: string): Promise<void>
46
+ /**
47
+ * Find-or-create the customer for a storefront shopper and (on Supabase) link
48
+ * it to auth.uid server-side, so order reads can be scoped to the owner by RLS.
49
+ * Optional: providers without it fall back to listCustomers+createCustomer.
50
+ */
51
+ resolveCustomer?(input: ResolveCustomerInput): Promise<ShopCustomer>
52
+
53
+ // Discounts
54
+ listDiscounts(options?: ListDiscountsOptions): Promise<Discount[]>
55
+ getDiscount(id: string): Promise<Discount | null>
56
+ createDiscount(input: CreateDiscountInput): Promise<Discount>
57
+ updateDiscount(id: string, input: UpdateDiscountInput): Promise<Discount>
58
+ deleteDiscount(id: string): Promise<void>
59
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,20 @@
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
+ }
@@ -0,0 +1,499 @@
1
+ import { getSupabaseClientOptional } from '@fayz-ai/core'
2
+ import type { ShopProvider } from './provider'
3
+ import { getShopTenantId } from './tenant'
4
+ import type {
5
+ Product, ProductImage, Category,
6
+ Order, ShopCustomer, Discount,
7
+ CreateProductInput, UpdateProductInput, ListProductsOptions,
8
+ CreateCategoryInput, UpdateCategoryInput,
9
+ CreateOrderInput, UpdateOrderInput, ListOrdersOptions, PlaceOrderInput,
10
+ CreateCustomerInput, UpdateCustomerInput, ListCustomersOptions, ResolveCustomerInput,
11
+ CreateDiscountInput, UpdateDiscountInput, ListDiscountsOptions,
12
+ } from './types'
13
+
14
+ function getDb(): any {
15
+ const supabase = getSupabaseClientOptional()
16
+ if (!supabase) throw new Error('@fayz-ai/shop: Supabase client not initialized. Call setGlobalSupabaseClient() first.')
17
+ return supabase
18
+ }
19
+
20
+ const getTenantId = getShopTenantId
21
+
22
+ function slugify(name: string): string {
23
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
24
+ }
25
+
26
+ function asUuid(value: string | null | undefined): string | null {
27
+ if (!value) return null
28
+ 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)
29
+ ? value
30
+ : null
31
+ }
32
+
33
+ function rowToProduct(row: any): Product {
34
+ return {
35
+ id: row.id,
36
+ tenantId: row.tenant_id,
37
+ name: row.name,
38
+ slug: row.slug ?? slugify(row.name),
39
+ description: row.description,
40
+ price: row.price ?? 0,
41
+ compareAtPrice: row.compare_at_price,
42
+ currency: row.currency ?? 'BRL',
43
+ status: row.status ?? 'active',
44
+ inventoryCount: row.inventory_count ?? 0,
45
+ sku: row.sku,
46
+ sortOrder: row.sort_order ?? 0,
47
+ metadata: row.metadata ?? {},
48
+ images: Array.isArray(row.images) ? row.images.map(rowToImage) : [],
49
+ categoryId: row.category_id,
50
+ categoryName: row.category_name ?? null,
51
+ isPhysical: row.is_physical ?? true,
52
+ weight: row.weight,
53
+ weightUnit: row.weight_unit ?? 'kg',
54
+ createdAt: row.created_at,
55
+ updatedAt: row.updated_at,
56
+ }
57
+ }
58
+
59
+ function rowToImage(row: any): ProductImage {
60
+ return {
61
+ id: row.id,
62
+ productId: row.product_id,
63
+ url: row.url,
64
+ altText: row.alt_text,
65
+ sortOrder: row.sort_order ?? 0,
66
+ isPrimary: row.is_primary ?? false,
67
+ createdAt: row.created_at,
68
+ }
69
+ }
70
+
71
+ function rowToCategory(row: any): Category {
72
+ return {
73
+ id: row.id,
74
+ tenantId: row.tenant_id,
75
+ name: row.name,
76
+ slug: row.slug ?? slugify(row.name),
77
+ description: row.description,
78
+ parentId: row.parent_id,
79
+ sortOrder: row.sort_order ?? 0,
80
+ createdAt: row.created_at,
81
+ }
82
+ }
83
+
84
+ function rowToOrder(row: any): Order {
85
+ return {
86
+ id: row.id,
87
+ tenantId: row.tenant_id,
88
+ orderNumber: row.order_number,
89
+ status: row.status ?? 'open',
90
+ financialStatus: row.financial_status ?? 'pending',
91
+ fulfillmentStatus: row.fulfillment_status ?? 'unfulfilled',
92
+ currency: row.currency ?? 'BRL',
93
+ subtotal: row.subtotal ?? 0,
94
+ taxTotal: row.tax_total ?? 0,
95
+ discountTotal: row.discount_total ?? 0,
96
+ shippingTotal: row.shipping_total ?? 0,
97
+ total: row.total ?? 0,
98
+ customerId: row.customer_id,
99
+ customerName: row.customer_name,
100
+ customerEmail: row.customer_email,
101
+ discountCode: row.discount_code,
102
+ notes: row.notes,
103
+ items: Array.isArray(row.items) ? row.items : [],
104
+ createdAt: row.created_at,
105
+ updatedAt: row.updated_at,
106
+ }
107
+ }
108
+
109
+ function rowToCustomer(row: any): ShopCustomer {
110
+ return {
111
+ id: row.id,
112
+ tenantId: row.tenant_id,
113
+ firstName: row.first_name,
114
+ lastName: row.last_name ?? '',
115
+ email: row.email,
116
+ phone: row.phone,
117
+ notes: row.notes,
118
+ ordersCount: row.orders_count ?? 0,
119
+ totalSpent: row.total_spent ?? 0,
120
+ createdAt: row.created_at,
121
+ updatedAt: row.updated_at,
122
+ }
123
+ }
124
+
125
+ function rowToDiscount(row: any): Discount {
126
+ return {
127
+ id: row.id,
128
+ tenantId: row.tenant_id,
129
+ title: row.title,
130
+ code: row.code,
131
+ type: row.type,
132
+ method: row.method ?? 'code',
133
+ value: row.value ?? 0,
134
+ usageLimit: row.usage_limit,
135
+ oncePerCustomer: row.once_per_customer ?? false,
136
+ startsAt: row.starts_at,
137
+ endsAt: row.ends_at,
138
+ status: row.status ?? 'active',
139
+ timesUsed: row.times_used ?? 0,
140
+ createdAt: row.created_at,
141
+ updatedAt: row.updated_at,
142
+ }
143
+ }
144
+
145
+ export class SupabaseShopProvider implements ShopProvider {
146
+ // ---------------------------------------------------------------------------
147
+ // Products
148
+ // ---------------------------------------------------------------------------
149
+
150
+ async listProducts(options?: ListProductsOptions): Promise<Product[]> {
151
+ const db = getDb()
152
+ const tenantId = getTenantId()
153
+ let q = db.from('shop_products').select('*, images:shop_product_images(*)')
154
+ if (tenantId) q = q.eq('tenant_id', tenantId)
155
+ if (options?.status) q = q.eq('status', options.status)
156
+ if (options?.categoryId) q = q.eq('category_id', options.categoryId)
157
+ if (options?.slug) q = q.eq('slug', options.slug)
158
+ if (options?.search) q = q.ilike('name', `%${options.search}%`)
159
+ q = q.order(options?.orderBy ?? 'sort_order', { ascending: options?.order !== 'desc' })
160
+ if (options?.limit) q = q.limit(options.limit)
161
+ if (options?.offset) q = q.range(options.offset, (options.offset ?? 0) + (options.limit ?? 50) - 1)
162
+ const { data, error } = await q
163
+ if (error) throw error
164
+ return (data ?? []).map(rowToProduct)
165
+ }
166
+
167
+ async getProduct(id: string): Promise<Product | null> {
168
+ const db = getDb()
169
+ const { data, error } = await db
170
+ .from('shop_products')
171
+ .select('*, images:shop_product_images(*)')
172
+ .eq('id', id)
173
+ .maybeSingle()
174
+ if (error) throw error
175
+ return data ? rowToProduct(data) : null
176
+ }
177
+
178
+ async createProduct(input: CreateProductInput): Promise<Product> {
179
+ const db = getDb()
180
+ const tenantId = getTenantId()
181
+ const { data, error } = await db
182
+ .from('shop_products')
183
+ .insert({
184
+ tenant_id: tenantId,
185
+ name: input.name,
186
+ slug: input.slug ?? slugify(input.name),
187
+ description: input.description ?? null,
188
+ price: input.price,
189
+ compare_at_price: input.compareAtPrice ?? null,
190
+ currency: input.currency ?? 'BRL',
191
+ status: input.status ?? 'active',
192
+ inventory_count: input.inventoryCount ?? 0,
193
+ sku: input.sku ?? null,
194
+ sort_order: input.sortOrder ?? 0,
195
+ metadata: input.metadata ?? {},
196
+ category_id: input.categoryId ?? null,
197
+ is_physical: input.isPhysical ?? true,
198
+ weight: input.weight ?? null,
199
+ weight_unit: input.weightUnit ?? 'kg',
200
+ })
201
+ .select('*, images:shop_product_images(*)')
202
+ .single()
203
+ if (error) throw error
204
+ return rowToProduct(data)
205
+ }
206
+
207
+ async updateProduct(id: string, input: UpdateProductInput): Promise<Product> {
208
+ const db = getDb()
209
+ const updates: Record<string, unknown> = {}
210
+ if (input.name !== undefined) { updates.name = input.name; updates.slug = input.slug ?? slugify(input.name) }
211
+ if (input.slug !== undefined) updates.slug = input.slug
212
+ if (input.description !== undefined) updates.description = input.description
213
+ if (input.price !== undefined) updates.price = input.price
214
+ if (input.compareAtPrice !== undefined) updates.compare_at_price = input.compareAtPrice
215
+ if (input.currency !== undefined) updates.currency = input.currency
216
+ if (input.status !== undefined) updates.status = input.status
217
+ if (input.inventoryCount !== undefined) updates.inventory_count = input.inventoryCount
218
+ if (input.sku !== undefined) updates.sku = input.sku
219
+ if (input.categoryId !== undefined) updates.category_id = input.categoryId
220
+ if (input.isPhysical !== undefined) updates.is_physical = input.isPhysical
221
+ if (input.weight !== undefined) updates.weight = input.weight
222
+ const { data, error } = await db
223
+ .from('shop_products').update(updates).eq('id', id)
224
+ .select('*, images:shop_product_images(*)')
225
+ .single()
226
+ if (error) throw error
227
+ return rowToProduct(data)
228
+ }
229
+
230
+ async deleteProduct(id: string): Promise<void> {
231
+ const { error } = await getDb().from('shop_products').delete().eq('id', id)
232
+ if (error) throw error
233
+ }
234
+
235
+ async uploadProductImage(productId: string, file: File): Promise<ProductImage> {
236
+ const db = getDb()
237
+ const tenantId = getTenantId()
238
+ const path = `${tenantId}/products/${productId}/${Date.now()}-${file.name}`
239
+ const { error: uploadErr } = await db.storage.from('shop-images').upload(path, file)
240
+ if (uploadErr) throw uploadErr
241
+ const { data: { publicUrl } } = db.storage.from('shop-images').getPublicUrl(path)
242
+ const { data, error } = await db
243
+ .from('shop_product_images')
244
+ .insert({ product_id: productId, tenant_id: tenantId, url: publicUrl, sort_order: 0, is_primary: false })
245
+ .select().single()
246
+ if (error) throw error
247
+ return rowToImage(data)
248
+ }
249
+
250
+ async deleteProductImage(imageId: string): Promise<void> {
251
+ const { error } = await getDb().from('shop_product_images').delete().eq('id', imageId)
252
+ if (error) throw error
253
+ }
254
+
255
+ // ---------------------------------------------------------------------------
256
+ // Categories
257
+ // ---------------------------------------------------------------------------
258
+
259
+ async listCategories(): Promise<Category[]> {
260
+ const db = getDb()
261
+ const tenantId = getTenantId()
262
+ let q = db.from('shop_categories').select('*').order('sort_order', { ascending: true })
263
+ if (tenantId) q = q.eq('tenant_id', tenantId)
264
+ const { data, error } = await q
265
+ if (error) throw error
266
+ return (data ?? []).map(rowToCategory)
267
+ }
268
+
269
+ async createCategory(input: CreateCategoryInput): Promise<Category> {
270
+ const db = getDb()
271
+ const { data, error } = await db
272
+ .from('shop_categories')
273
+ .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 })
274
+ .select().single()
275
+ if (error) throw error
276
+ return rowToCategory(data)
277
+ }
278
+
279
+ async updateCategory(id: string, input: UpdateCategoryInput): Promise<Category> {
280
+ const db = getDb()
281
+ const updates: Record<string, unknown> = {}
282
+ if (input.name !== undefined) updates.name = input.name
283
+ if (input.slug !== undefined) updates.slug = input.slug
284
+ if (input.description !== undefined) updates.description = input.description
285
+ if (input.parentId !== undefined) updates.parent_id = input.parentId
286
+ if (input.sortOrder !== undefined) updates.sort_order = input.sortOrder
287
+ const { data, error } = await db.from('shop_categories').update(updates).eq('id', id).select().single()
288
+ if (error) throw error
289
+ return rowToCategory(data)
290
+ }
291
+
292
+ async deleteCategory(id: string): Promise<void> {
293
+ const { error } = await getDb().from('shop_categories').delete().eq('id', id)
294
+ if (error) throw error
295
+ }
296
+
297
+ // ---------------------------------------------------------------------------
298
+ // Orders
299
+ // ---------------------------------------------------------------------------
300
+
301
+ async listOrders(options?: ListOrdersOptions): Promise<Order[]> {
302
+ const db = getDb()
303
+ const tenantId = getTenantId()
304
+ let q = db.from('shop_orders').select('*, items:shop_order_items(*)')
305
+ if (tenantId) q = q.eq('tenant_id', tenantId)
306
+ if (options?.status) q = q.eq('status', options.status)
307
+ if (options?.financialStatus) q = q.eq('financial_status', options.financialStatus)
308
+ if (options?.fulfillmentStatus) q = q.eq('fulfillment_status', options.fulfillmentStatus)
309
+ if (options?.customerId) q = q.eq('customer_id', options.customerId)
310
+ if (options?.customerEmail) q = q.eq('customer_email', options.customerEmail)
311
+ if (options?.search) q = q.or(`customer_name.ilike.%${options.search}%,customer_email.ilike.%${options.search}%`)
312
+ q = q.order('created_at', { ascending: false })
313
+ if (options?.limit) q = q.limit(options.limit)
314
+ const { data, error } = await q
315
+ if (error) throw error
316
+ return (data ?? []).map(rowToOrder)
317
+ }
318
+
319
+ async getOrder(id: string): Promise<Order | null> {
320
+ const { data, error } = await getDb()
321
+ .from('shop_orders').select('*, items:shop_order_items(*)')
322
+ .eq('id', id).maybeSingle()
323
+ if (error) throw error
324
+ return data ? rowToOrder(data) : null
325
+ }
326
+
327
+ async createOrder(input: CreateOrderInput): Promise<Order> {
328
+ const db = getDb()
329
+ const tenantId = getTenantId()
330
+ const subtotal = input.items.reduce((s, i) => s + i.quantity * i.unitPrice, 0)
331
+ const discountTotal = input.discountTotal ?? 0
332
+ const shippingTotal = input.shippingTotal ?? 0
333
+ const total = Math.round((subtotal - discountTotal + shippingTotal) * 100) / 100
334
+ const { data: order, error: orderErr } = await db
335
+ .from('shop_orders')
336
+ .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' })
337
+ .select().single()
338
+ if (orderErr) throw orderErr
339
+ 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 }))
340
+ await db.from('shop_order_items').insert(items)
341
+ return this.getOrder(order.id) as Promise<Order>
342
+ }
343
+
344
+ async updateOrder(id: string, input: UpdateOrderInput): Promise<Order> {
345
+ const updates: Record<string, unknown> = {}
346
+ if (input.status !== undefined) updates.status = input.status
347
+ if (input.financialStatus !== undefined) updates.financial_status = input.financialStatus
348
+ if (input.fulfillmentStatus !== undefined) updates.fulfillment_status = input.fulfillmentStatus
349
+ if (input.notes !== undefined) updates.notes = input.notes
350
+ const { error } = await getDb().from('shop_orders').update(updates).eq('id', id)
351
+ if (error) throw error
352
+ return this.getOrder(id) as Promise<Order>
353
+ }
354
+
355
+ // Trusted placement — delegates all price/discount/inventory authority to the
356
+ // shop_place_order RPC (SECURITY DEFINER). The browser never sets a price.
357
+ async placeOrder(input: PlaceOrderInput): Promise<Order> {
358
+ const db = getDb()
359
+ const tenantId = input.tenantId ?? getTenantId()
360
+ if (!tenantId) throw new Error('@fayz-ai/shop: placeOrder requires a tenant id (configure the shop tenant resolver).')
361
+ const { data, error } = await db.rpc('shop_place_order', {
362
+ p_tenant_id: tenantId,
363
+ p_items: input.items.map(i => ({ product_id: i.productId, quantity: i.quantity, options_label: i.optionsLabel ?? null })),
364
+ p_customer_id: asUuid(input.customerId),
365
+ p_customer_name: input.customer?.name ?? null,
366
+ p_customer_email: input.customer?.email ?? null,
367
+ p_currency: input.currency ?? 'BRL',
368
+ p_discount_code: input.discountCode ?? null,
369
+ p_shipping_total: input.shippingTotal ?? 0,
370
+ p_notes: input.notes ?? null,
371
+ })
372
+ if (error) throw error
373
+ const orderId = typeof data === 'string' ? data : (data?.id ?? data?.order_id ?? data)
374
+ const order = await this.getOrder(orderId)
375
+ if (!order) throw new Error('@fayz-ai/shop: placeOrder did not return a persisted order.')
376
+ return order
377
+ }
378
+
379
+ // ---------------------------------------------------------------------------
380
+ // Customers
381
+ // ---------------------------------------------------------------------------
382
+
383
+ async listCustomers(options?: ListCustomersOptions): Promise<ShopCustomer[]> {
384
+ const db = getDb()
385
+ const tenantId = getTenantId()
386
+ let q = db.from('shop_customers').select('*').order('first_name', { ascending: true })
387
+ if (tenantId) q = q.eq('tenant_id', tenantId)
388
+ if (options?.search) q = q.or(`first_name.ilike.%${options.search}%,last_name.ilike.%${options.search}%,email.ilike.%${options.search}%`)
389
+ if (options?.limit) q = q.limit(options.limit)
390
+ const { data, error } = await q
391
+ if (error) throw error
392
+ return (data ?? []).map(rowToCustomer)
393
+ }
394
+
395
+ async getCustomer(id: string): Promise<ShopCustomer | null> {
396
+ const { data, error } = await getDb().from('shop_customers').select('*').eq('id', id).maybeSingle()
397
+ if (error) throw error
398
+ return data ? rowToCustomer(data) : null
399
+ }
400
+
401
+ async createCustomer(input: CreateCustomerInput): Promise<ShopCustomer> {
402
+ const { data, error } = await getDb()
403
+ .from('shop_customers')
404
+ .insert({ tenant_id: getTenantId(), first_name: input.firstName, last_name: input.lastName ?? '', email: input.email ?? null, phone: input.phone ?? null, notes: input.notes ?? null })
405
+ .select().single()
406
+ if (error) throw error
407
+ return rowToCustomer(data)
408
+ }
409
+
410
+ async updateCustomer(id: string, input: UpdateCustomerInput): Promise<ShopCustomer> {
411
+ const updates: Record<string, unknown> = {}
412
+ if (input.firstName !== undefined) updates.first_name = input.firstName
413
+ if (input.lastName !== undefined) updates.last_name = input.lastName
414
+ if (input.email !== undefined) updates.email = input.email
415
+ if (input.phone !== undefined) updates.phone = input.phone
416
+ if (input.notes !== undefined) updates.notes = input.notes
417
+ const { data, error } = await getDb().from('shop_customers').update(updates).eq('id', id).select().single()
418
+ if (error) throw error
419
+ return rowToCustomer(data)
420
+ }
421
+
422
+ async deleteCustomer(id: string): Promise<void> {
423
+ const { error } = await getDb().from('shop_customers').delete().eq('id', id)
424
+ if (error) throw error
425
+ }
426
+
427
+ // Find-or-create + link to auth.uid server-side (SECURITY DEFINER RPC), so the
428
+ // browser never supplies the auth id and "my orders" is RLS-scoped to the owner.
429
+ async resolveCustomer(input: ResolveCustomerInput): Promise<ShopCustomer> {
430
+ const db = getDb()
431
+ const tenantId = input.tenantId ?? getTenantId()
432
+ if (!tenantId) throw new Error('@fayz-ai/shop: resolveCustomer requires a tenant id.')
433
+ const { data, error } = await db.rpc('shop_resolve_customer', {
434
+ p_tenant_id: tenantId,
435
+ p_email: input.email,
436
+ p_name: input.name ?? null,
437
+ })
438
+ if (error) throw error
439
+ const row = Array.isArray(data) ? data[0] : data
440
+ if (!row) throw new Error('@fayz-ai/shop: resolveCustomer returned no row.')
441
+ return rowToCustomer(row)
442
+ }
443
+
444
+ // ---------------------------------------------------------------------------
445
+ // Discounts
446
+ // ---------------------------------------------------------------------------
447
+
448
+ async listDiscounts(options?: ListDiscountsOptions): Promise<Discount[]> {
449
+ const db = getDb()
450
+ const tenantId = getTenantId()
451
+ let q = db.from('shop_discounts').select('*').order('created_at', { ascending: false })
452
+ if (tenantId) q = q.eq('tenant_id', tenantId)
453
+ if (options?.status) q = q.eq('status', options.status)
454
+ if (options?.type) q = q.eq('type', options.type)
455
+ if (options?.search) q = q.ilike('title', `%${options.search}%`)
456
+ if (options?.limit) q = q.limit(options.limit)
457
+ const { data, error } = await q
458
+ if (error) throw error
459
+ return (data ?? []).map(rowToDiscount)
460
+ }
461
+
462
+ async getDiscount(id: string): Promise<Discount | null> {
463
+ const { data, error } = await getDb().from('shop_discounts').select('*').eq('id', id).maybeSingle()
464
+ if (error) throw error
465
+ return data ? rowToDiscount(data) : null
466
+ }
467
+
468
+ async createDiscount(input: CreateDiscountInput): Promise<Discount> {
469
+ const { data, error } = await getDb()
470
+ .from('shop_discounts')
471
+ .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, starts_at: input.startsAt ?? new Date().toISOString(), ends_at: input.endsAt ?? null, status: input.status ?? 'active', times_used: 0 })
472
+ .select().single()
473
+ if (error) throw error
474
+ return rowToDiscount(data)
475
+ }
476
+
477
+ async updateDiscount(id: string, input: UpdateDiscountInput): Promise<Discount> {
478
+ const updates: Record<string, unknown> = {}
479
+ if (input.title !== undefined) updates.title = input.title
480
+ if (input.code !== undefined) updates.code = input.code
481
+ if (input.type !== undefined) updates.type = input.type
482
+ if (input.value !== undefined) updates.value = input.value
483
+ if (input.usageLimit !== undefined) updates.usage_limit = input.usageLimit
484
+ if (input.status !== undefined) updates.status = input.status
485
+ if (input.endsAt !== undefined) updates.ends_at = input.endsAt
486
+ const { data, error } = await getDb().from('shop_discounts').update(updates).eq('id', id).select().single()
487
+ if (error) throw error
488
+ return rowToDiscount(data)
489
+ }
490
+
491
+ async deleteDiscount(id: string): Promise<void> {
492
+ const { error } = await getDb().from('shop_discounts').delete().eq('id', id)
493
+ if (error) throw error
494
+ }
495
+ }
496
+
497
+ export function createSupabaseShopProvider(): ShopProvider {
498
+ return new SupabaseShopProvider()
499
+ }
package/src/tenant.ts ADDED
@@ -0,0 +1,21 @@
1
+ // Tenant scoping for shop queries. @fayz-ai/shop has no dependency on @fayz-ai/saas —
2
+ // the host app (or @fayz-ai/plugin-shop) registers a resolver that returns the
3
+ // current tenant id. When no resolver is set, queries are unscoped (RLS still
4
+ // applies server-side).
5
+
6
+ export type TenantResolver = () => string | undefined
7
+
8
+ let _resolver: TenantResolver | null = null
9
+
10
+ export function setShopTenantResolver(resolver: TenantResolver): void {
11
+ _resolver = resolver
12
+ }
13
+
14
+ export function getShopTenantId(): string | undefined {
15
+ if (!_resolver) return undefined
16
+ try {
17
+ return _resolver()
18
+ } catch {
19
+ return undefined
20
+ }
21
+ }