@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.
Files changed (165) hide show
  1. package/package.json +3 -2
  2. package/src/carts/destroy.ts +54 -0
  3. package/src/carts/fetch.ts +20 -0
  4. package/src/carts/index.ts +13 -0
  5. package/src/carts/store.ts +75 -0
  6. package/src/carts/update.ts +78 -0
  7. package/src/coupons/destroy.ts +49 -0
  8. package/src/coupons/fetch.ts +510 -0
  9. package/src/coupons/index.ts +30 -0
  10. package/src/coupons/store.ts +46 -0
  11. package/src/coupons/update.ts +147 -0
  12. package/src/customers/destroy.ts +62 -0
  13. package/src/customers/fetch.ts +20 -0
  14. package/src/customers/index.ts +17 -0
  15. package/src/customers/store.ts +45 -0
  16. package/src/customers/update.ts +44 -0
  17. package/src/devices/destroy.ts +54 -0
  18. package/src/devices/export.ts +98 -0
  19. package/src/devices/fetch.ts +125 -0
  20. package/src/devices/index.ts +31 -0
  21. package/src/devices/store.ts +82 -0
  22. package/src/devices/update.ts +125 -0
  23. package/src/errors/destroy.ts +54 -0
  24. package/src/errors/fetch.ts +197 -0
  25. package/src/errors/index.ts +28 -0
  26. package/src/errors/update.ts +116 -0
  27. package/src/gift-cards/destroy.ts +87 -0
  28. package/src/gift-cards/fetch.ts +431 -0
  29. package/src/gift-cards/index.ts +26 -0
  30. package/src/gift-cards/store.ts +56 -0
  31. package/src/gift-cards/update.ts +102 -0
  32. package/src/index.ts +78 -0
  33. package/src/orders/destroy.ts +111 -0
  34. package/src/orders/events.ts +121 -0
  35. package/src/orders/export.ts +158 -0
  36. package/src/orders/fetch.ts +379 -0
  37. package/src/orders/guards.ts +261 -0
  38. package/src/orders/index.ts +91 -0
  39. package/src/orders/place-order.ts +274 -0
  40. package/src/orders/store.ts +51 -0
  41. package/src/orders/totals.ts +190 -0
  42. package/src/orders/update.ts +164 -0
  43. package/src/orders/webhook.ts +312 -0
  44. package/src/payments/destroy.ts +56 -0
  45. package/src/payments/fetch.ts +288 -0
  46. package/src/payments/index.ts +16 -0
  47. package/src/payments/store.ts +63 -0
  48. package/src/payments/update.ts +52 -0
  49. package/src/product.ts +0 -0
  50. package/src/products/categories/destroy.ts +103 -0
  51. package/src/products/categories/fetch.ts +285 -0
  52. package/src/products/categories/index.ts +36 -0
  53. package/src/products/categories/store.ts +87 -0
  54. package/src/products/categories/update.ts +251 -0
  55. package/src/products/index.ts +35 -0
  56. package/src/products/items/destroy.ts +54 -0
  57. package/src/products/items/fetch.ts +282 -0
  58. package/src/products/items/index.ts +26 -0
  59. package/src/products/items/store.ts +84 -0
  60. package/src/products/items/update.ts +324 -0
  61. package/src/products/manufacturers/destroy.ts +56 -0
  62. package/src/products/manufacturers/fetch.ts +117 -0
  63. package/src/products/manufacturers/index.ts +24 -0
  64. package/src/products/manufacturers/store.ts +82 -0
  65. package/src/products/manufacturers/update.ts +113 -0
  66. package/src/products/reviews/destroy.ts +56 -0
  67. package/src/products/reviews/fetch.ts +63 -0
  68. package/src/products/reviews/index.ts +25 -0
  69. package/src/products/reviews/store.ts +46 -0
  70. package/src/products/reviews/update.ts +82 -0
  71. package/src/products/units/destroy.ts +56 -0
  72. package/src/products/units/fetch.ts +20 -0
  73. package/src/products/units/index.ts +13 -0
  74. package/src/products/units/store.ts +162 -0
  75. package/src/products/units/update.ts +159 -0
  76. package/src/products/variants/destroy.ts +56 -0
  77. package/src/products/variants/fetch.ts +20 -0
  78. package/src/products/variants/index.ts +21 -0
  79. package/src/products/variants/store.ts +136 -0
  80. package/src/products/variants/update.ts +104 -0
  81. package/src/receipts/destroy.ts +54 -0
  82. package/src/receipts/export.ts +0 -0
  83. package/src/receipts/fetch.ts +222 -0
  84. package/src/receipts/index.ts +24 -0
  85. package/src/receipts/printer.ts +69 -0
  86. package/src/receipts/store.ts +69 -0
  87. package/src/receipts/update.ts +124 -0
  88. package/src/shippings/delivery-routes/destroy.ts +54 -0
  89. package/src/shippings/delivery-routes/fetch.ts +42 -0
  90. package/src/shippings/delivery-routes/index.ts +26 -0
  91. package/src/shippings/delivery-routes/store.ts +69 -0
  92. package/src/shippings/delivery-routes/update.ts +120 -0
  93. package/src/shippings/digital-deliveries/destroy.ts +111 -0
  94. package/src/shippings/digital-deliveries/fetch.ts +20 -0
  95. package/src/shippings/digital-deliveries/index.ts +26 -0
  96. package/src/shippings/digital-deliveries/store.ts +76 -0
  97. package/src/shippings/digital-deliveries/update.ts +129 -0
  98. package/src/shippings/drivers/destroy.ts +78 -0
  99. package/src/shippings/drivers/fetch.ts +20 -0
  100. package/src/shippings/drivers/index.ts +24 -0
  101. package/src/shippings/drivers/store.ts +69 -0
  102. package/src/shippings/drivers/update.ts +117 -0
  103. package/src/shippings/index.ts +39 -0
  104. package/src/shippings/license-keys/destroy.ts +111 -0
  105. package/src/shippings/license-keys/fetch.ts +20 -0
  106. package/src/shippings/license-keys/index.ts +26 -0
  107. package/src/shippings/license-keys/store.ts +77 -0
  108. package/src/shippings/license-keys/update.ts +117 -0
  109. package/src/shippings/shipping-methods/destroy.ts +111 -0
  110. package/src/shippings/shipping-methods/fetch.ts +62 -0
  111. package/src/shippings/shipping-methods/index.ts +28 -0
  112. package/src/shippings/shipping-methods/store.ts +123 -0
  113. package/src/shippings/shipping-methods/update.ts +120 -0
  114. package/src/shippings/shipping-rates/destroy.ts +102 -0
  115. package/src/shippings/shipping-rates/fetch.ts +320 -0
  116. package/src/shippings/shipping-rates/index.ts +36 -0
  117. package/src/shippings/shipping-rates/store.ts +102 -0
  118. package/src/shippings/shipping-rates/update.ts +139 -0
  119. package/src/shippings/shipping-zones/destroy.ts +111 -0
  120. package/src/shippings/shipping-zones/fetch.ts +96 -0
  121. package/src/shippings/shipping-zones/index.ts +26 -0
  122. package/src/shippings/shipping-zones/store.ts +74 -0
  123. package/src/shippings/shipping-zones/update.ts +156 -0
  124. package/src/tax/destroy.ts +54 -0
  125. package/src/tax/fetch.ts +20 -0
  126. package/src/tax/index.ts +24 -0
  127. package/src/tax/store.ts +69 -0
  128. package/src/tax/update.ts +119 -0
  129. package/src/tests/coupons.test.ts +90 -0
  130. package/src/tests/customers.test.ts +39 -0
  131. package/src/tests/device.test.ts +111 -0
  132. package/src/tests/digitals.test.ts +31 -0
  133. package/src/tests/drivers.test.ts +25 -0
  134. package/src/tests/gift-cards.test.ts +36 -0
  135. package/src/tests/license.test.ts +31 -0
  136. package/src/tests/manufacturers.test.ts +80 -0
  137. package/src/tests/orders.test.ts +25 -0
  138. package/src/tests/payments.test.ts +75 -0
  139. package/src/tests/rates.test.ts +367 -0
  140. package/src/tests/receipts.test.ts +287 -0
  141. package/src/tests/restaurant.test.ts +522 -0
  142. package/src/tests/reviews.test.ts +165 -0
  143. package/src/tests/routes.test.ts +51 -0
  144. package/src/tests/setup.ts +149 -0
  145. package/src/tests/shipping.test.ts +88 -0
  146. package/src/tests/tax.test.ts +25 -0
  147. package/src/tests/units.test.ts +110 -0
  148. package/src/tests/variants.test.ts +133 -0
  149. package/src/tests/waitlist.test.ts +303 -0
  150. package/src/tests/zones.test.ts +86 -0
  151. package/src/types.ts +312 -0
  152. package/src/utils/typed-stats.ts +67 -0
  153. package/src/waitlists/index.ts +19 -0
  154. package/src/waitlists/products/destroy.ts +65 -0
  155. package/src/waitlists/products/export.ts +113 -0
  156. package/src/waitlists/products/fetch.ts +362 -0
  157. package/src/waitlists/products/index.ts +33 -0
  158. package/src/waitlists/products/store.ts +112 -0
  159. package/src/waitlists/products/update.ts +125 -0
  160. package/src/waitlists/restaurant/destroy.ts +65 -0
  161. package/src/waitlists/restaurant/export.ts +107 -0
  162. package/src/waitlists/restaurant/fetch.ts +538 -0
  163. package/src/waitlists/restaurant/index.ts +43 -0
  164. package/src/waitlists/restaurant/store.ts +109 -0
  165. 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
+ }