@stacksjs/commerce 0.70.53 → 0.70.55
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/package.json +3 -2
- package/src/carts/destroy.ts +54 -0
- package/src/carts/fetch.ts +20 -0
- package/src/carts/index.ts +13 -0
- package/src/carts/store.ts +75 -0
- package/src/carts/update.ts +78 -0
- package/src/coupons/destroy.ts +49 -0
- package/src/coupons/fetch.ts +510 -0
- package/src/coupons/index.ts +30 -0
- package/src/coupons/store.ts +46 -0
- package/src/coupons/update.ts +147 -0
- package/src/customers/destroy.ts +62 -0
- package/src/customers/fetch.ts +20 -0
- package/src/customers/index.ts +17 -0
- package/src/customers/store.ts +45 -0
- package/src/customers/update.ts +44 -0
- package/src/devices/destroy.ts +54 -0
- package/src/devices/export.ts +98 -0
- package/src/devices/fetch.ts +125 -0
- package/src/devices/index.ts +31 -0
- package/src/devices/store.ts +82 -0
- package/src/devices/update.ts +125 -0
- package/src/errors/destroy.ts +54 -0
- package/src/errors/fetch.ts +197 -0
- package/src/errors/index.ts +28 -0
- package/src/errors/update.ts +116 -0
- package/src/gift-cards/destroy.ts +87 -0
- package/src/gift-cards/fetch.ts +431 -0
- package/src/gift-cards/index.ts +26 -0
- package/src/gift-cards/store.ts +56 -0
- package/src/gift-cards/update.ts +102 -0
- package/src/index.ts +78 -0
- package/src/orders/destroy.ts +111 -0
- package/src/orders/events.ts +121 -0
- package/src/orders/export.ts +158 -0
- package/src/orders/fetch.ts +379 -0
- package/src/orders/guards.ts +261 -0
- package/src/orders/index.ts +91 -0
- package/src/orders/place-order.ts +274 -0
- package/src/orders/store.ts +51 -0
- package/src/orders/totals.ts +190 -0
- package/src/orders/update.ts +164 -0
- package/src/orders/webhook.ts +312 -0
- package/src/payments/destroy.ts +56 -0
- package/src/payments/fetch.ts +288 -0
- package/src/payments/index.ts +16 -0
- package/src/payments/store.ts +63 -0
- package/src/payments/update.ts +52 -0
- package/src/product.ts +0 -0
- package/src/products/categories/destroy.ts +103 -0
- package/src/products/categories/fetch.ts +285 -0
- package/src/products/categories/index.ts +36 -0
- package/src/products/categories/store.ts +87 -0
- package/src/products/categories/update.ts +251 -0
- package/src/products/index.ts +35 -0
- package/src/products/items/destroy.ts +54 -0
- package/src/products/items/fetch.ts +282 -0
- package/src/products/items/index.ts +26 -0
- package/src/products/items/store.ts +84 -0
- package/src/products/items/update.ts +324 -0
- package/src/products/manufacturers/destroy.ts +56 -0
- package/src/products/manufacturers/fetch.ts +117 -0
- package/src/products/manufacturers/index.ts +24 -0
- package/src/products/manufacturers/store.ts +82 -0
- package/src/products/manufacturers/update.ts +113 -0
- package/src/products/reviews/destroy.ts +56 -0
- package/src/products/reviews/fetch.ts +63 -0
- package/src/products/reviews/index.ts +25 -0
- package/src/products/reviews/store.ts +46 -0
- package/src/products/reviews/update.ts +82 -0
- package/src/products/units/destroy.ts +56 -0
- package/src/products/units/fetch.ts +20 -0
- package/src/products/units/index.ts +13 -0
- package/src/products/units/store.ts +162 -0
- package/src/products/units/update.ts +159 -0
- package/src/products/variants/destroy.ts +56 -0
- package/src/products/variants/fetch.ts +20 -0
- package/src/products/variants/index.ts +21 -0
- package/src/products/variants/store.ts +136 -0
- package/src/products/variants/update.ts +104 -0
- package/src/receipts/destroy.ts +54 -0
- package/src/receipts/export.ts +0 -0
- package/src/receipts/fetch.ts +222 -0
- package/src/receipts/index.ts +24 -0
- package/src/receipts/printer.ts +69 -0
- package/src/receipts/store.ts +69 -0
- package/src/receipts/update.ts +124 -0
- package/src/shippings/delivery-routes/destroy.ts +54 -0
- package/src/shippings/delivery-routes/fetch.ts +42 -0
- package/src/shippings/delivery-routes/index.ts +26 -0
- package/src/shippings/delivery-routes/store.ts +69 -0
- package/src/shippings/delivery-routes/update.ts +120 -0
- package/src/shippings/digital-deliveries/destroy.ts +111 -0
- package/src/shippings/digital-deliveries/fetch.ts +20 -0
- package/src/shippings/digital-deliveries/index.ts +26 -0
- package/src/shippings/digital-deliveries/store.ts +76 -0
- package/src/shippings/digital-deliveries/update.ts +129 -0
- package/src/shippings/drivers/destroy.ts +78 -0
- package/src/shippings/drivers/fetch.ts +20 -0
- package/src/shippings/drivers/index.ts +24 -0
- package/src/shippings/drivers/store.ts +69 -0
- package/src/shippings/drivers/update.ts +117 -0
- package/src/shippings/index.ts +39 -0
- package/src/shippings/license-keys/destroy.ts +111 -0
- package/src/shippings/license-keys/fetch.ts +20 -0
- package/src/shippings/license-keys/index.ts +26 -0
- package/src/shippings/license-keys/store.ts +77 -0
- package/src/shippings/license-keys/update.ts +117 -0
- package/src/shippings/shipping-methods/destroy.ts +111 -0
- package/src/shippings/shipping-methods/fetch.ts +62 -0
- package/src/shippings/shipping-methods/index.ts +28 -0
- package/src/shippings/shipping-methods/store.ts +123 -0
- package/src/shippings/shipping-methods/update.ts +120 -0
- package/src/shippings/shipping-rates/destroy.ts +102 -0
- package/src/shippings/shipping-rates/fetch.ts +320 -0
- package/src/shippings/shipping-rates/index.ts +36 -0
- package/src/shippings/shipping-rates/store.ts +102 -0
- package/src/shippings/shipping-rates/update.ts +139 -0
- package/src/shippings/shipping-zones/destroy.ts +111 -0
- package/src/shippings/shipping-zones/fetch.ts +96 -0
- package/src/shippings/shipping-zones/index.ts +26 -0
- package/src/shippings/shipping-zones/store.ts +74 -0
- package/src/shippings/shipping-zones/update.ts +156 -0
- package/src/tax/destroy.ts +54 -0
- package/src/tax/fetch.ts +20 -0
- package/src/tax/index.ts +24 -0
- package/src/tax/store.ts +69 -0
- package/src/tax/update.ts +119 -0
- package/src/tests/coupons.test.ts +90 -0
- package/src/tests/customers.test.ts +39 -0
- package/src/tests/device.test.ts +111 -0
- package/src/tests/digitals.test.ts +31 -0
- package/src/tests/drivers.test.ts +25 -0
- package/src/tests/gift-cards.test.ts +36 -0
- package/src/tests/license.test.ts +31 -0
- package/src/tests/manufacturers.test.ts +80 -0
- package/src/tests/orders.test.ts +25 -0
- package/src/tests/payments.test.ts +75 -0
- package/src/tests/rates.test.ts +367 -0
- package/src/tests/receipts.test.ts +287 -0
- package/src/tests/restaurant.test.ts +522 -0
- package/src/tests/reviews.test.ts +165 -0
- package/src/tests/routes.test.ts +51 -0
- package/src/tests/setup.ts +149 -0
- package/src/tests/shipping.test.ts +88 -0
- package/src/tests/tax.test.ts +25 -0
- package/src/tests/units.test.ts +110 -0
- package/src/tests/variants.test.ts +133 -0
- package/src/tests/waitlist.test.ts +303 -0
- package/src/tests/zones.test.ts +86 -0
- package/src/types.ts +312 -0
- package/src/utils/typed-stats.ts +67 -0
- package/src/waitlists/index.ts +19 -0
- package/src/waitlists/products/destroy.ts +65 -0
- package/src/waitlists/products/export.ts +113 -0
- package/src/waitlists/products/fetch.ts +362 -0
- package/src/waitlists/products/index.ts +33 -0
- package/src/waitlists/products/store.ts +112 -0
- package/src/waitlists/products/update.ts +125 -0
- package/src/waitlists/restaurant/destroy.ts +65 -0
- package/src/waitlists/restaurant/export.ts +107 -0
- package/src/waitlists/restaurant/fetch.ts +538 -0
- package/src/waitlists/restaurant/index.ts +43 -0
- package/src/waitlists/restaurant/store.ts +109 -0
- package/src/waitlists/restaurant/update.ts +198 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { db, sql } from '@stacksjs/database'
|
|
2
|
+
import { formatDate } from '@stacksjs/orm'
|
|
3
|
+
type ProductJsonResponse = ModelRow<typeof Product>
|
|
4
|
+
type ProductUpdate = UpdateModelData<typeof Product>
|
|
5
|
+
import { fetchById } from './fetch'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Update a product item
|
|
9
|
+
*
|
|
10
|
+
* @param id The ID of the product item to update
|
|
11
|
+
* @param data The product item data to update
|
|
12
|
+
* @returns The updated product item record
|
|
13
|
+
*/
|
|
14
|
+
export async function update(id: number, data: Omit<ProductUpdate, 'id'>): Promise<ProductJsonResponse> {
|
|
15
|
+
try {
|
|
16
|
+
if (!id)
|
|
17
|
+
throw new Error('Product item ID is required for update')
|
|
18
|
+
|
|
19
|
+
const result = await db
|
|
20
|
+
.updateTable('products')
|
|
21
|
+
.set({
|
|
22
|
+
...data,
|
|
23
|
+
updated_at: formatDate(new Date()),
|
|
24
|
+
})
|
|
25
|
+
.where('id', '=', id)
|
|
26
|
+
.returningAll()
|
|
27
|
+
.executeTakeFirst()
|
|
28
|
+
|
|
29
|
+
if (!result)
|
|
30
|
+
throw new Error('Failed to update product item')
|
|
31
|
+
|
|
32
|
+
return result as ProductJsonResponse
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error instanceof Error) {
|
|
36
|
+
throw new TypeError(`Failed to update product item: ${error.message}`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
throw error
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Update multiple product items at once
|
|
45
|
+
*
|
|
46
|
+
* @param data Array of product item updates
|
|
47
|
+
* @returns Number of product items updated
|
|
48
|
+
*/
|
|
49
|
+
export async function bulkUpdate(data: ProductUpdate[]): Promise<number> {
|
|
50
|
+
if (!data.length)
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
let updatedCount = 0
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
await (db as any).transaction().execute(async (trx: any) => {
|
|
57
|
+
for (const item of data) {
|
|
58
|
+
if (!(item as Record<string, unknown>).id)
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
const result = await trx
|
|
62
|
+
.updateTable('products')
|
|
63
|
+
.set({
|
|
64
|
+
...item,
|
|
65
|
+
updated_at: formatDate(new Date()),
|
|
66
|
+
})
|
|
67
|
+
.where('id', '=', (item as Record<string, unknown>).id)
|
|
68
|
+
.executeTakeFirst()
|
|
69
|
+
|
|
70
|
+
if (Number(result.numUpdatedRows) > 0)
|
|
71
|
+
updatedCount++
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
return updatedCount
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error instanceof Error) {
|
|
79
|
+
throw new TypeError(`Failed to update product items in bulk: ${error.message}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
throw error
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Update a product item's availability status
|
|
88
|
+
*
|
|
89
|
+
* @param id The ID of the product item
|
|
90
|
+
* @param isAvailable The new availability status
|
|
91
|
+
* @returns The updated product item with the new availability status
|
|
92
|
+
*/
|
|
93
|
+
export async function updateAvailability(
|
|
94
|
+
id: number,
|
|
95
|
+
isAvailable: boolean,
|
|
96
|
+
): Promise<ProductJsonResponse | undefined> {
|
|
97
|
+
// Check if product item exists
|
|
98
|
+
const productItem = await fetchById(id)
|
|
99
|
+
|
|
100
|
+
if (!productItem) {
|
|
101
|
+
throw new Error(`Product item with ID ${id} not found`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// Update the product item availability
|
|
106
|
+
await db
|
|
107
|
+
.updateTable('products')
|
|
108
|
+
.set({
|
|
109
|
+
is_available: isAvailable,
|
|
110
|
+
updated_at: formatDate(new Date()),
|
|
111
|
+
})
|
|
112
|
+
.where('id', '=', id)
|
|
113
|
+
.execute()
|
|
114
|
+
|
|
115
|
+
// Fetch the updated product item
|
|
116
|
+
return await fetchById(id)
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (error instanceof Error) {
|
|
120
|
+
throw new TypeError(`Failed to update product item availability: ${error.message}`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
throw error
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Atomically decrement (or increment) a product's inventory count by `delta`.
|
|
129
|
+
*
|
|
130
|
+
* Returns the updated row, or `null` if the requested decrement would push
|
|
131
|
+
* inventory below zero — that's how callers detect "we sold the last one"
|
|
132
|
+
* during checkout. The conditional UPDATE means two parallel checkout
|
|
133
|
+
* requests can't both observe stock=1 and both succeed; the second one
|
|
134
|
+
* matches zero rows and the function returns null. Existing
|
|
135
|
+
* `updateInventory(id, count)` keeps the previous "set absolute count"
|
|
136
|
+
* semantics for admin tooling.
|
|
137
|
+
*/
|
|
138
|
+
export async function adjustInventory(
|
|
139
|
+
id: number,
|
|
140
|
+
delta: number,
|
|
141
|
+
): Promise<ProductJsonResponse | null> {
|
|
142
|
+
if (!Number.isFinite(delta) || delta === 0) {
|
|
143
|
+
throw new Error('[commerce/inventory] adjustInventory delta must be a non-zero finite number')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const result: any = await db
|
|
147
|
+
.updateTable('products')
|
|
148
|
+
.set({
|
|
149
|
+
inventory_count: sql`inventory_count + ${delta}`,
|
|
150
|
+
updated_at: formatDate(new Date()),
|
|
151
|
+
})
|
|
152
|
+
.where('id', '=', id)
|
|
153
|
+
// Guard against negative stock when decrementing.
|
|
154
|
+
.where(sql`inventory_count + ${delta}`, '>=', 0)
|
|
155
|
+
.execute()
|
|
156
|
+
|
|
157
|
+
// Drivers report affected rows differently — handle the common shapes.
|
|
158
|
+
const affected = Number(
|
|
159
|
+
result?.numUpdatedRows
|
|
160
|
+
?? result?.[0]?.numUpdatedRows
|
|
161
|
+
?? result?.numAffectedRows
|
|
162
|
+
?? 0,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if (!affected) return null
|
|
166
|
+
return (await fetchById(id)) ?? null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Discriminated result for `adjustInventoryMany` — `ok: true` when
|
|
171
|
+
* every per-item adjustment succeeded; `ok: false` with `failedAt`
|
|
172
|
+
* (the index in the input array that triggered the rollback) when
|
|
173
|
+
* any single item couldn't be adjusted (out of stock, missing,
|
|
174
|
+
* etc.). All adjustments roll back as a unit on failure.
|
|
175
|
+
*/
|
|
176
|
+
export type InventoryBatchResult =
|
|
177
|
+
| { ok: true, products: ProductJsonResponse[] }
|
|
178
|
+
| { ok: false, failedAt: number, productId: number, reason: 'out-of-stock' | 'not-found' }
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Atomic batch inventory adjustment (stacksjs/stacks#1879 Co-2).
|
|
182
|
+
*
|
|
183
|
+
* Pre-fix: a multi-item cart called `adjustInventory(id, delta)` N
|
|
184
|
+
* times sequentially. If item 2 failed (out of stock), item 1's
|
|
185
|
+
* decrement was already committed with no rollback — the order
|
|
186
|
+
* persisted with partial stock reservation and the customer saw
|
|
187
|
+
* a refund-on-next-page-load surprise.
|
|
188
|
+
*
|
|
189
|
+
* Post-fix: wraps the per-item loop in `db.transaction`. Any item
|
|
190
|
+
* that can't be adjusted (the existing `adjustInventory` returns
|
|
191
|
+
* null on insufficient stock) throws inside the transaction; the
|
|
192
|
+
* driver rolls every prior decrement back. Returns a discriminated
|
|
193
|
+
* result so the cart handler can surface "item X was out of stock"
|
|
194
|
+
* specifically instead of a generic transaction-failed error.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```ts
|
|
198
|
+
* const result = await adjustInventoryMany([
|
|
199
|
+
* { id: cart.itemA, delta: -2 },
|
|
200
|
+
* { id: cart.itemB, delta: -1 },
|
|
201
|
+
* ])
|
|
202
|
+
* if (!result.ok) {
|
|
203
|
+
* throw new HttpError(409, `Item ${result.productId} is out of stock`)
|
|
204
|
+
* }
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
export async function adjustInventoryMany(
|
|
208
|
+
updates: ReadonlyArray<{ id: number, delta: number }>,
|
|
209
|
+
): Promise<InventoryBatchResult> {
|
|
210
|
+
if (updates.length === 0) return { ok: true, products: [] }
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const products = await db.transaction().execute(async (trx: any) => {
|
|
214
|
+
const out: ProductJsonResponse[] = []
|
|
215
|
+
for (let i = 0; i < updates.length; i++) {
|
|
216
|
+
const { id, delta } = updates[i]!
|
|
217
|
+
if (!Number.isFinite(delta) || delta === 0)
|
|
218
|
+
throw new Error('[commerce/inventory] adjustInventoryMany delta must be a non-zero finite number')
|
|
219
|
+
|
|
220
|
+
// Same atomic UPDATE as adjustInventory, but routed through
|
|
221
|
+
// the transaction handle so all decrements share a single
|
|
222
|
+
// commit boundary.
|
|
223
|
+
const result: any = await trx
|
|
224
|
+
.updateTable('products')
|
|
225
|
+
.set({
|
|
226
|
+
inventory_count: sql`inventory_count + ${delta}`,
|
|
227
|
+
updated_at: formatDate(new Date()),
|
|
228
|
+
})
|
|
229
|
+
.where('id', '=', id)
|
|
230
|
+
.where(sql`inventory_count + ${delta}`, '>=', 0)
|
|
231
|
+
.execute()
|
|
232
|
+
|
|
233
|
+
const affected = Number(
|
|
234
|
+
result?.numUpdatedRows
|
|
235
|
+
?? result?.[0]?.numUpdatedRows
|
|
236
|
+
?? result?.numAffectedRows
|
|
237
|
+
?? 0,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if (!affected) {
|
|
241
|
+
// Throw a structured error the outer try/catch can convert
|
|
242
|
+
// into a discriminated result. Driver rolls back automatically.
|
|
243
|
+
const exists = await trx.selectFrom('products').where('id', '=', id).select('id').executeTakeFirst()
|
|
244
|
+
const reason = exists ? 'out-of-stock' : 'not-found'
|
|
245
|
+
throw Object.assign(new Error(`adjustInventoryMany failed at index ${i}: ${reason}`), {
|
|
246
|
+
__batchFail: true,
|
|
247
|
+
failedAt: i,
|
|
248
|
+
productId: id,
|
|
249
|
+
reason,
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const refreshed = await trx.selectFrom('products').where('id', '=', id).selectAll().executeTakeFirst()
|
|
254
|
+
if (refreshed) out.push(refreshed as ProductJsonResponse)
|
|
255
|
+
}
|
|
256
|
+
return out
|
|
257
|
+
})
|
|
258
|
+
return { ok: true, products }
|
|
259
|
+
}
|
|
260
|
+
catch (err: any) {
|
|
261
|
+
if (err?.__batchFail) {
|
|
262
|
+
return {
|
|
263
|
+
ok: false,
|
|
264
|
+
failedAt: err.failedAt as number,
|
|
265
|
+
productId: err.productId as number,
|
|
266
|
+
reason: err.reason as 'out-of-stock' | 'not-found',
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Real error (network, schema, etc.) — surface to caller.
|
|
270
|
+
throw err
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Update inventory information for a product item
|
|
276
|
+
*
|
|
277
|
+
* @param id The ID of the product item
|
|
278
|
+
* @param inventoryCount The updated inventory count
|
|
279
|
+
* @returns The updated product item
|
|
280
|
+
*/
|
|
281
|
+
export async function updateInventory(
|
|
282
|
+
id: number,
|
|
283
|
+
inventoryCount?: number,
|
|
284
|
+
): Promise<ProductJsonResponse | undefined> {
|
|
285
|
+
// Check if product item exists
|
|
286
|
+
const productItem = await fetchById(id)
|
|
287
|
+
|
|
288
|
+
if (!productItem) {
|
|
289
|
+
throw new Error(`Product item with ID ${id} not found`)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Create update data with only provided fields
|
|
293
|
+
const updateData: Record<string, any> = {
|
|
294
|
+
updated_at: formatDate(new Date()),
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (inventoryCount !== undefined) {
|
|
298
|
+
updateData.inventory_count = inventoryCount
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// If no inventory fields to update, just return the existing product item
|
|
302
|
+
if (Object.keys(updateData).length === 1) { // Only updated_at was set
|
|
303
|
+
return productItem
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
// Update the product item
|
|
308
|
+
await db
|
|
309
|
+
.updateTable('products')
|
|
310
|
+
.set(updateData)
|
|
311
|
+
.where('id', '=', id)
|
|
312
|
+
.execute()
|
|
313
|
+
|
|
314
|
+
// Fetch the updated product item
|
|
315
|
+
return await fetchById(id)
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
if (error instanceof Error) {
|
|
319
|
+
throw new TypeError(`Failed to update inventory information: ${error.message}`)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
throw error
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { db } from '@stacksjs/database'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Delete an order by ID
|
|
5
|
+
*
|
|
6
|
+
* @param id The ID of the order to delete
|
|
7
|
+
* @returns True if the order was deleted, false otherwise
|
|
8
|
+
*/
|
|
9
|
+
export async function destroy(id: number): Promise<boolean> {
|
|
10
|
+
try {
|
|
11
|
+
// Perform the delete operation
|
|
12
|
+
const result = await db
|
|
13
|
+
.deleteFrom('manufacturers')
|
|
14
|
+
.where('id', '=', id)
|
|
15
|
+
.executeTakeFirst()
|
|
16
|
+
|
|
17
|
+
// Return true if any row was affected (deleted)
|
|
18
|
+
return Number(result.numDeletedRows) > 0
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error instanceof Error) {
|
|
22
|
+
throw new TypeError(`Failed to delete order: ${error.message}`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
throw error
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Delete multiple orders by ID
|
|
31
|
+
*
|
|
32
|
+
* @param ids Array of order IDs to delete
|
|
33
|
+
* @returns Number of orders deleted
|
|
34
|
+
*/
|
|
35
|
+
export async function bulkDestroy(ids: number[]): Promise<number> {
|
|
36
|
+
if (!ids.length)
|
|
37
|
+
return 0
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Perform the delete operation
|
|
41
|
+
const result = await db
|
|
42
|
+
.deleteFrom('manufacturers')
|
|
43
|
+
.where('id', 'in', ids)
|
|
44
|
+
.executeTakeFirst()
|
|
45
|
+
|
|
46
|
+
// Return the number of deleted rows
|
|
47
|
+
return Number(result.numDeletedRows)
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error instanceof Error) {
|
|
51
|
+
throw new TypeError(`Failed to delete orders: ${error.message}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
throw error
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
type ManufacturerJsonResponse = ModelRow<typeof Manufacturer>
|
|
2
|
+
import type { FetchManufacturersOptions, ManufacturerResponse } from '../../types'
|
|
3
|
+
import type { StacksExpressionBuilder } from '@stacksjs/database'
|
|
4
|
+
import { db } from '@stacksjs/database'
|
|
5
|
+
|
|
6
|
+
export function fetchAll(): Promise<ManufacturerJsonResponse[]> {
|
|
7
|
+
return db.selectFrom('manufacturers').selectAll().execute() as Promise<ManufacturerJsonResponse[]>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Fetch a product manufacturer by ID
|
|
12
|
+
*/
|
|
13
|
+
export async function fetchById(id: number): Promise<ManufacturerJsonResponse | undefined> {
|
|
14
|
+
return await db
|
|
15
|
+
.selectFrom('manufacturers')
|
|
16
|
+
.where('id', '=', id)
|
|
17
|
+
.selectAll()
|
|
18
|
+
.executeTakeFirst() as ManufacturerJsonResponse | undefined
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Fetch a product manufacturer by UUID
|
|
23
|
+
*/
|
|
24
|
+
export async function fetchByUuid(uuid: string): Promise<ManufacturerJsonResponse | undefined> {
|
|
25
|
+
return await db
|
|
26
|
+
.selectFrom('manufacturers')
|
|
27
|
+
.where('uuid', '=', uuid)
|
|
28
|
+
.selectAll()
|
|
29
|
+
.executeTakeFirst() as ManufacturerJsonResponse | undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Fetch featured manufacturers
|
|
34
|
+
*/
|
|
35
|
+
export async function fetchFeatured(limit: number = 10): Promise<ManufacturerJsonResponse[]> {
|
|
36
|
+
return await db
|
|
37
|
+
.selectFrom('manufacturers')
|
|
38
|
+
.where('featured', '=', true)
|
|
39
|
+
.selectAll()
|
|
40
|
+
.orderBy('manufacturer', 'asc')
|
|
41
|
+
.limit(limit)
|
|
42
|
+
.execute() as ManufacturerJsonResponse[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Fetch manufacturers by country
|
|
47
|
+
*/
|
|
48
|
+
export async function fetchByCountry(country: string, options: FetchManufacturersOptions = {}): Promise<ManufacturerResponse> {
|
|
49
|
+
// Set default values
|
|
50
|
+
const page = (options as any).page || 1
|
|
51
|
+
const limit = (options as any).limit || 2
|
|
52
|
+
|
|
53
|
+
// Get total count for pagination
|
|
54
|
+
const countResult = await db
|
|
55
|
+
.selectFrom('manufacturers')
|
|
56
|
+
.select(((eb: StacksExpressionBuilder) => eb.fn.count('id').as('total')) as any)
|
|
57
|
+
.where('country', '=', country)
|
|
58
|
+
.executeTakeFirst() as { total: number } | undefined
|
|
59
|
+
|
|
60
|
+
const total = Number(countResult?.total || 0)
|
|
61
|
+
|
|
62
|
+
// Apply pagination
|
|
63
|
+
const manufacturers = await db
|
|
64
|
+
.selectFrom('manufacturers')
|
|
65
|
+
.selectAll()
|
|
66
|
+
.where('country', '=', country)
|
|
67
|
+
.limit(limit)
|
|
68
|
+
.offset((page - 1) * limit)
|
|
69
|
+
.execute()
|
|
70
|
+
|
|
71
|
+
// Calculate pagination info
|
|
72
|
+
const totalPages = Math.ceil(total / limit)
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
data: manufacturers as ManufacturerJsonResponse[],
|
|
76
|
+
paging: {
|
|
77
|
+
total_records: total,
|
|
78
|
+
page,
|
|
79
|
+
total_pages: totalPages,
|
|
80
|
+
},
|
|
81
|
+
next_cursor: page < totalPages ? page + 1 : null,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Fetch manufacturers with their product count
|
|
87
|
+
*/
|
|
88
|
+
export async function fetchWithProductCount(options: FetchManufacturersOptions = {}): Promise<ManufacturerJsonResponse[]> {
|
|
89
|
+
// Start building the query with joins and filters before groupBy
|
|
90
|
+
let query = db.selectFrom('manufacturers as m')
|
|
91
|
+
.leftJoin('products as p', 'p.manufacturer_id', '=', 'm.id') as any
|
|
92
|
+
|
|
93
|
+
// Apply filters before groupBy (WHERE must come before GROUP BY in SQL)
|
|
94
|
+
if (options.country)
|
|
95
|
+
query = query.where('m.country', '=', options.country)
|
|
96
|
+
|
|
97
|
+
if (options.featured !== undefined)
|
|
98
|
+
query = query.where('m.featured', '=', options.featured)
|
|
99
|
+
|
|
100
|
+
// Apply select and groupBy after where clauses
|
|
101
|
+
query = query
|
|
102
|
+
.select([
|
|
103
|
+
'm.id',
|
|
104
|
+
'm.uuid',
|
|
105
|
+
'm.manufacturer',
|
|
106
|
+
'm.description',
|
|
107
|
+
'm.country',
|
|
108
|
+
'm.featured',
|
|
109
|
+
'm.created_at',
|
|
110
|
+
'm.updated_at',
|
|
111
|
+
(eb: StacksExpressionBuilder) => eb.fn.count('p.id').as('product_count'),
|
|
112
|
+
] as any)
|
|
113
|
+
.groupBy('m.id')
|
|
114
|
+
|
|
115
|
+
// Return all manufacturers
|
|
116
|
+
return query.execute() as Promise<ManufacturerJsonResponse[]>
|
|
117
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export {
|
|
2
|
+
bulkDestroy,
|
|
3
|
+
destroy,
|
|
4
|
+
} from './destroy'
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
fetchAll,
|
|
8
|
+
fetchByCountry,
|
|
9
|
+
fetchById,
|
|
10
|
+
fetchByUuid,
|
|
11
|
+
fetchFeatured,
|
|
12
|
+
fetchWithProductCount,
|
|
13
|
+
} from './fetch'
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
bulkStore,
|
|
17
|
+
store,
|
|
18
|
+
} from './store'
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
update,
|
|
22
|
+
updateByUuid,
|
|
23
|
+
updateFeaturedStatus,
|
|
24
|
+
} from './update'
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
type ManufacturerJsonResponse = ModelRow<typeof Manufacturer>
|
|
2
|
+
type NewManufacturer = NewModelData<typeof Manufacturer>
|
|
3
|
+
import { randomUUIDv7 } from 'bun'
|
|
4
|
+
import { db } from '@stacksjs/database'
|
|
5
|
+
import { HttpError } from '@stacksjs/error-handling'
|
|
6
|
+
import { isUniqueViolation } from '@stacksjs/orm'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a new product manufacturer
|
|
10
|
+
*
|
|
11
|
+
* @param data The manufacturer data to store
|
|
12
|
+
* @returns The newly created manufacturer record
|
|
13
|
+
*/
|
|
14
|
+
export async function store(data: NewManufacturer): Promise<ManufacturerJsonResponse> {
|
|
15
|
+
try {
|
|
16
|
+
const manufacturerData = {
|
|
17
|
+
...data,
|
|
18
|
+
uuid: randomUUIDv7(),
|
|
19
|
+
featured: data.featured ?? false,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const result = await db
|
|
23
|
+
.insertInto('manufacturers')
|
|
24
|
+
.values(manufacturerData)
|
|
25
|
+
.returningAll()
|
|
26
|
+
.executeTakeFirst()
|
|
27
|
+
|
|
28
|
+
if (!result)
|
|
29
|
+
throw new Error('Failed to create manufacturer')
|
|
30
|
+
|
|
31
|
+
return result as ManufacturerJsonResponse
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error instanceof HttpError)
|
|
35
|
+
throw error
|
|
36
|
+
// Cross-dialect duplicate detection (#1957).
|
|
37
|
+
if (isUniqueViolation(error))
|
|
38
|
+
throw new HttpError(409, 'A manufacturer with this name already exists')
|
|
39
|
+
if (error instanceof Error)
|
|
40
|
+
throw new Error(`Failed to create manufacturer: ${error.message}`)
|
|
41
|
+
throw error
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create multiple manufacturers at once
|
|
47
|
+
*
|
|
48
|
+
* @param data Array of manufacturer data to store
|
|
49
|
+
* @returns Number of manufacturers created
|
|
50
|
+
*/
|
|
51
|
+
export async function bulkStore(data: NewManufacturer[]): Promise<number> {
|
|
52
|
+
if (!data.length)
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
let createdCount = 0
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
for (const manufacturer of data) {
|
|
59
|
+
const manufacturerData = {
|
|
60
|
+
...manufacturer,
|
|
61
|
+
uuid: randomUUIDv7(),
|
|
62
|
+
featured: manufacturer.featured ?? false,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await db
|
|
66
|
+
.insertInto('manufacturers')
|
|
67
|
+
.values(manufacturerData)
|
|
68
|
+
.execute()
|
|
69
|
+
|
|
70
|
+
createdCount++
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return createdCount
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error instanceof Error) {
|
|
77
|
+
throw new TypeError(`Failed to create manufacturers in bulk: ${error.message}`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
throw error
|
|
81
|
+
}
|
|
82
|
+
}
|