@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.
- package/README.md +18 -1
- package/dist/admin.d.ts +2 -0
- package/dist/admin.d.ts.map +1 -0
- package/dist/catalog.d.ts +1 -3
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +2 -2
- package/dist/chunk-3GERSY5F.js +526 -0
- package/dist/chunk-3GERSY5F.js.map +1 -0
- package/dist/chunk-FQPGE3YN.js +3 -0
- package/dist/{chunk-JR25TISO.js.map → chunk-FQPGE3YN.js.map} +1 -1
- package/dist/{chunk-CTKZ6O2J.js → chunk-IKNGCRUM.js} +41 -15
- package/dist/chunk-IKNGCRUM.js.map +1 -0
- package/dist/{chunk-IYQHKMLB.js → chunk-IL6GA4ER.js} +4 -4
- package/dist/chunk-IL6GA4ER.js.map +1 -0
- package/dist/chunk-KLDS7IPU.js +17 -0
- package/dist/chunk-KLDS7IPU.js.map +1 -0
- package/dist/chunk-RGTIJQJL.js +93 -0
- package/dist/chunk-RGTIJQJL.js.map +1 -0
- package/dist/{chunk-SVTCJ6HP.js → chunk-U3O2I3RM.js} +395 -27
- package/dist/chunk-U3O2I3RM.js.map +1 -0
- package/dist/chunk-XERLK5LD.js +1415 -0
- package/dist/chunk-XERLK5LD.js.map +1 -0
- package/dist/chunk-XOP6SHPB.js +142 -0
- package/dist/chunk-XOP6SHPB.js.map +1 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/{mock-catalog.d.ts → data/mock-catalog.d.ts} +21 -1
- package/dist/data/mock-catalog.d.ts.map +1 -0
- package/dist/data/mock-provider.d.ts +133 -0
- package/dist/data/mock-provider.d.ts.map +1 -0
- package/dist/data/mock.d.ts +3 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/provider.d.ts +142 -0
- package/dist/data/provider.d.ts.map +1 -0
- package/dist/data/supabase-provider.d.ts +100 -0
- package/dist/data/supabase-provider.d.ts.map +1 -0
- package/dist/data/tables.d.ts +35 -0
- package/dist/data/tables.d.ts.map +1 -0
- package/dist/data/tenant.d.ts.map +1 -0
- package/dist/entities/entity-reconcile.d.ts +32 -0
- package/dist/entities/entity-reconcile.d.ts.map +1 -0
- package/dist/entities/entity-sql.d.ts +50 -0
- package/dist/entities/entity-sql.d.ts.map +1 -0
- package/dist/entities/entity-store.d.ts +58 -0
- package/dist/entities/entity-store.d.ts.map +1 -0
- package/dist/entities/index.d.ts +11 -0
- package/dist/entities/index.d.ts.map +1 -0
- package/dist/entities/mock-entity-store.d.ts +17 -0
- package/dist/entities/mock-entity-store.d.ts.map +1 -0
- package/dist/entities/sidecar.d.ts +9 -0
- package/dist/entities/sidecar.d.ts.map +1 -0
- package/dist/entities/supabase-entity-store.d.ts +8 -0
- package/dist/entities/supabase-entity-store.d.ts.map +1 -0
- package/dist/entities.js +159 -0
- package/dist/entities.js.map +1 -0
- package/dist/index.d.ts +17 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +728 -41
- package/dist/index.js.map +1 -1
- package/dist/inventory/allocation.d.ts +98 -0
- package/dist/inventory/allocation.d.ts.map +1 -0
- package/dist/inventory/index.d.ts +13 -0
- package/dist/inventory/index.d.ts.map +1 -0
- package/dist/inventory/mock-stock-store.d.ts +89 -0
- package/dist/inventory/mock-stock-store.d.ts.map +1 -0
- package/dist/inventory/postgrest-double.d.ts +65 -0
- package/dist/inventory/postgrest-double.d.ts.map +1 -0
- package/dist/inventory/shopify-mapping.d.ts +67 -0
- package/dist/inventory/shopify-mapping.d.ts.map +1 -0
- package/dist/inventory/stock-contract.d.ts +46 -0
- package/dist/inventory/stock-contract.d.ts.map +1 -0
- package/dist/inventory/stock-store.d.ts +96 -0
- package/dist/inventory/stock-store.d.ts.map +1 -0
- package/dist/inventory/stock-types.d.ts +292 -0
- package/dist/inventory/stock-types.d.ts.map +1 -0
- package/dist/inventory/supabase-stock-store.d.ts +59 -0
- package/dist/inventory/supabase-stock-store.d.ts.map +1 -0
- package/dist/inventory/vocabulary.d.ts +86 -0
- package/dist/inventory/vocabulary.d.ts.map +1 -0
- package/dist/inventory.js +4 -0
- package/dist/inventory.js.map +1 -0
- package/dist/lib/catalog.d.ts +4 -0
- package/dist/lib/catalog.d.ts.map +1 -0
- package/dist/{commerce.d.ts → lib/commerce.d.ts} +2 -2
- package/dist/lib/commerce.d.ts.map +1 -0
- package/dist/lib/emails.d.ts +50 -0
- package/dist/lib/emails.d.ts.map +1 -0
- package/dist/lib/message-events.d.ts +4 -0
- package/dist/lib/message-events.d.ts.map +1 -0
- package/dist/lib/runtime.d.ts +5 -0
- package/dist/lib/runtime.d.ts.map +1 -0
- package/dist/mock.d.ts +1 -2
- package/dist/mock.d.ts.map +1 -1
- package/dist/mock.js +4 -2
- package/dist/provider.d.ts +1 -88
- package/dist/provider.d.ts.map +1 -1
- package/dist/rules/adapter.d.ts +36 -0
- package/dist/rules/adapter.d.ts.map +1 -0
- package/dist/rules/attributes.d.ts +64 -0
- package/dist/rules/attributes.d.ts.map +1 -0
- package/dist/rules/context.d.ts +43 -0
- package/dist/rules/context.d.ts.map +1 -0
- package/dist/rules/evaluator.d.ts +37 -0
- package/dist/rules/evaluator.d.ts.map +1 -0
- package/dist/rules/index.d.ts +13 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/prices.d.ts +25 -0
- package/dist/rules/prices.d.ts.map +1 -0
- package/dist/rules/rule-types.d.ts +160 -0
- package/dist/rules/rule-types.d.ts.map +1 -0
- package/dist/rules.js +22 -0
- package/dist/rules.js.map +1 -0
- package/dist/runtime.d.ts +1 -4
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +5 -3
- package/dist/types.d.ts +231 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +26 -20
- package/dist/chunk-CTKZ6O2J.js.map +0 -1
- package/dist/chunk-IYQHKMLB.js.map +0 -1
- package/dist/chunk-JR25TISO.js +0 -3
- package/dist/chunk-SVTCJ6HP.js.map +0 -1
- package/dist/commerce.d.ts.map +0 -1
- package/dist/mock-catalog.d.ts.map +0 -1
- package/dist/mock-provider.d.ts +0 -60
- package/dist/mock-provider.d.ts.map +0 -1
- package/dist/supabase-provider.d.ts +0 -56
- package/dist/supabase-provider.d.ts.map +0 -1
- package/dist/tables.d.ts +0 -12
- package/dist/tables.d.ts.map +0 -1
- package/dist/tenant.d.ts.map +0 -1
- package/src/catalog.ts +0 -3
- package/src/commerce.ts +0 -120
- package/src/index.ts +0 -40
- package/src/mock-catalog.ts +0 -266
- package/src/mock-provider.ts +0 -480
- package/src/mock.ts +0 -2
- package/src/provider.ts +0 -117
- package/src/runtime.ts +0 -20
- package/src/supabase-provider.ts +0 -754
- package/src/tables.ts +0 -17
- package/src/tenant.ts +0 -21
- package/src/types.ts +0 -623
- /package/dist/{tenant.d.ts → data/tenant.d.ts} +0 -0
package/src/supabase-provider.ts
DELETED
|
@@ -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
|
-
}
|