@stacksjs/commerce 0.70.54 → 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,285 @@
1
+ import type { CategoryStats } from '../../types'
2
+ import type { StacksExpressionBuilder } from '@stacksjs/database'
3
+ import { db } from '@stacksjs/database'
4
+ import { formatDate } from '@stacksjs/orm'
5
+ type CategoryJsonResponse = ModelRow<typeof Category>
6
+
7
+ /**
8
+ * Fetch all categories from the database
9
+ */
10
+ export async function fetchAll(): Promise<CategoryJsonResponse[]> {
11
+ const categories = await db
12
+ .selectFrom('categories')
13
+ .selectAll()
14
+ .execute()
15
+
16
+ return categories as CategoryJsonResponse[]
17
+ }
18
+
19
+ /**
20
+ * Fetch a category by ID
21
+ */
22
+ export async function fetchById(id: number): Promise<CategoryJsonResponse | undefined> {
23
+ return await db
24
+ .selectFrom('categories')
25
+ .where('id', '=', id)
26
+ .selectAll()
27
+ .executeTakeFirst() as CategoryJsonResponse | undefined
28
+ }
29
+
30
+ /**
31
+ * Fetch a category by name
32
+ */
33
+ export async function fetchByName(name: string): Promise<CategoryJsonResponse | undefined> {
34
+ return await db
35
+ .selectFrom('categories')
36
+ .where('name', '=', name)
37
+ .selectAll()
38
+ .executeTakeFirst() as CategoryJsonResponse | undefined
39
+ }
40
+
41
+ /**
42
+ * Fetch active categories (is_active = true)
43
+ */
44
+ export async function fetchActive(): Promise<CategoryJsonResponse[]> {
45
+ const categories = await db
46
+ .selectFrom('categories')
47
+ .where('is_active', '=', true)
48
+ .selectAll()
49
+ .execute()
50
+
51
+ return categories as CategoryJsonResponse[]
52
+ }
53
+
54
+ /**
55
+ * Fetch root categories (parent_category_id is null)
56
+ */
57
+ export async function fetchRootCategories(): Promise<CategoryJsonResponse[]> {
58
+ const categories = await db
59
+ .selectFrom('categories')
60
+ .where('parent_category_id', 'is', null)
61
+ .where('is_active', '=', true)
62
+ .selectAll()
63
+ .execute()
64
+
65
+ return categories as CategoryJsonResponse[]
66
+ }
67
+
68
+ /**
69
+ * Fetch child categories for a given parent category ID
70
+ */
71
+ export async function fetchChildCategories(parentId: string): Promise<CategoryJsonResponse[]> {
72
+ const categories = await db
73
+ .selectFrom('categories')
74
+ .where('parent_category_id', '=', parentId)
75
+ .where('is_active', '=', true)
76
+ .selectAll()
77
+ .execute()
78
+
79
+ return categories as CategoryJsonResponse[]
80
+ }
81
+
82
+ /**
83
+ * Fetch categories sorted by display order
84
+ */
85
+ export async function fetchByDisplayOrder(ascending: boolean = true): Promise<CategoryJsonResponse[]> {
86
+ const query = db
87
+ .selectFrom('categories')
88
+ .where('is_active', '=', true)
89
+ .selectAll()
90
+
91
+ if (ascending) {
92
+ return await query.orderBy('display_order', 'asc').execute() as CategoryJsonResponse[]
93
+ }
94
+ else {
95
+ return await query.orderBy('display_order', 'desc').execute() as CategoryJsonResponse[]
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Get category statistics
101
+ */
102
+ export async function fetchStats(): Promise<CategoryStats> {
103
+ // Total categories
104
+ const totalCategories = await db
105
+ .selectFrom('categories')
106
+ .select(((eb: StacksExpressionBuilder) => eb.fn.count('id').as('count')) as any)
107
+ .executeTakeFirst() as { count: number } | undefined
108
+
109
+ // Active categories
110
+ const activeCategories = await db
111
+ .selectFrom('categories')
112
+ .where('is_active', '=', true)
113
+ .select(((eb: StacksExpressionBuilder) => eb.fn.count('id').as('count')) as any)
114
+ .executeTakeFirst() as { count: number } | undefined
115
+
116
+ // Root vs child categories
117
+ const rootCategories = await db
118
+ .selectFrom('categories')
119
+ .where('parent_category_id', 'is', null)
120
+ .select(((eb: StacksExpressionBuilder) => eb.fn.count('id').as('count')) as any)
121
+ .executeTakeFirst() as { count: number } | undefined
122
+
123
+ // Categories with images
124
+ const categoriesWithImages = await db
125
+ .selectFrom('categories')
126
+ .where('image_url', 'is not', null)
127
+ .select(((eb: StacksExpressionBuilder) => eb.fn.count('id').as('count')) as any)
128
+ .executeTakeFirst() as { count: number } | undefined
129
+
130
+ // Recently added categories (last 30 days)
131
+ const thirtyDaysAgo = new Date()
132
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
133
+ const thirtyDaysAgoStr = formatDate(thirtyDaysAgo)
134
+
135
+ const recentlyAddedCategories = await db
136
+ .selectFrom('categories')
137
+ .where('created_at', '>=', thirtyDaysAgoStr)
138
+ .selectAll()
139
+ .limit(5)
140
+ .execute()
141
+
142
+ // Categories by parent (top 5 parents with most children)
143
+ const categoriesByParent = await db
144
+ .selectFrom('categories as c')
145
+ .leftJoin('categories as parent', 'c.parent_category_id', '=', 'parent.id')
146
+ .where('c.parent_category_id', 'is not', null)
147
+ .select([
148
+ 'c.parent_category_id',
149
+ 'parent.name as parent_name',
150
+ (eb: StacksExpressionBuilder) => eb.fn.count('c.id').as('child_count'),
151
+ ] as any)
152
+ .groupBy(['c.parent_category_id', 'parent.name'] as any)
153
+ .orderBy('child_count', 'desc')
154
+ .limit(5)
155
+ .execute() as { parent_category_id: string, parent_name: string, child_count: number }[]
156
+
157
+ return {
158
+ total: Number(totalCategories?.count || 0),
159
+ active: Number(activeCategories?.count || 0),
160
+ root_categories: Number(rootCategories?.count || 0),
161
+ child_categories: Number(totalCategories?.count || 0) - Number(rootCategories?.count || 0),
162
+ with_images: Number(categoriesWithImages?.count || 0),
163
+ recently_added: recentlyAddedCategories,
164
+ top_parent_categories: categoriesByParent.map((item: any) => ({
165
+ id: String(item.parent_category_id || ''),
166
+ name: String(item.parent_name || ''),
167
+ child_count: Number(item.child_count),
168
+ })),
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Compare category growth between different time periods
174
+ * @param daysRange Number of days to look back (7, 30, 60, etc.)
175
+ */
176
+ export async function compareCategoryGrowth(_daysRange: number = 30): Promise<{
177
+ current_period: number
178
+ previous_period: number
179
+ difference: number
180
+ percentage_change: number
181
+ days_range: number
182
+ }> {
183
+ const today = new Date()
184
+ const todayStr = formatDate(today)
185
+
186
+ // Current period (last N days)
187
+ const currentPeriodStart = new Date()
188
+ currentPeriodStart.setDate(today.getDate() - _daysRange)
189
+ const currentPeriodStartStr = formatDate(currentPeriodStart)
190
+
191
+ // Previous period (N days before the current period)
192
+ const previousPeriodEnd = new Date(currentPeriodStart)
193
+ previousPeriodEnd.setDate(previousPeriodEnd.getDate() - 1)
194
+ const previousPeriodEndStr = formatDate(previousPeriodEnd)
195
+
196
+ const previousPeriodStart = new Date(previousPeriodEnd)
197
+ previousPeriodStart.setDate(previousPeriodEnd.getDate() - _daysRange)
198
+ const previousPeriodStartStr = formatDate(previousPeriodStart)
199
+
200
+ // Get categories for current period
201
+ const currentPeriodCategories = await db
202
+ .selectFrom('categories')
203
+ .select(((eb: StacksExpressionBuilder) => eb.fn.count('id').as('count')) as any)
204
+ .where('created_at', '>=', currentPeriodStartStr)
205
+ .where('created_at', '<=', todayStr)
206
+ .executeTakeFirst() as { count: number } | undefined
207
+
208
+ // Get categories for previous period
209
+ const previousPeriodCategories = await db
210
+ .selectFrom('categories')
211
+ .select(((eb: StacksExpressionBuilder) => eb.fn.count('id').as('count')) as any)
212
+ .where('created_at', '>=', previousPeriodStartStr)
213
+ .where('created_at', '<=', previousPeriodEndStr)
214
+ .executeTakeFirst() as { count: number } | undefined
215
+
216
+ const currentCount = Number(currentPeriodCategories?.count || 0)
217
+ const previousCount = Number(previousPeriodCategories?.count || 0)
218
+ const difference = currentCount - previousCount
219
+
220
+ // Calculate percentage change, handling division by zero
221
+ const percentageChange = previousCount !== 0
222
+ ? (difference / previousCount) * 100
223
+ : (currentCount > 0 ? 100 : 0)
224
+
225
+ return {
226
+ current_period: currentCount,
227
+ previous_period: previousCount,
228
+ difference,
229
+ percentage_change: percentageChange,
230
+ days_range: _daysRange,
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Build category tree structure
236
+ */
237
+ export async function fetchCategoryTree(): Promise<any[]> {
238
+ // First get all categories
239
+ const allCategories = await fetchAll()
240
+
241
+ // Create a map for quick access
242
+ const categoryMap = new Map<any, any>()
243
+ allCategories.forEach((category: any) => {
244
+ categoryMap.set(category.id, {
245
+ ...category,
246
+ children: [],
247
+ })
248
+ })
249
+
250
+ // Build the tree
251
+ const rootCategories: any[] = []
252
+
253
+ allCategories.forEach((category: any) => {
254
+ const categoryWithChildren = categoryMap.get(category.id)
255
+
256
+ if (category.parent_category_id) {
257
+ // This is a child category
258
+ const parent = categoryMap.get(category.parent_category_id)
259
+ if (parent) {
260
+ parent.children.push(categoryWithChildren)
261
+ }
262
+ }
263
+ else {
264
+ // This is a root category
265
+ rootCategories.push(categoryWithChildren)
266
+ }
267
+ })
268
+
269
+ // Sort root categories by display_order
270
+ rootCategories.sort((a, b) => a.display_order - b.display_order)
271
+
272
+ // Sort children by display_order
273
+ const sortChildrenByDisplayOrder = (categories: any[]) => {
274
+ categories.sort((a, b) => a.display_order - b.display_order)
275
+ categories.forEach((category: any) => {
276
+ if (category.children.length > 0) {
277
+ sortChildrenByDisplayOrder(category.children)
278
+ }
279
+ })
280
+ }
281
+
282
+ sortChildrenByDisplayOrder(rootCategories)
283
+
284
+ return rootCategories
285
+ }
@@ -0,0 +1,36 @@
1
+ // Export all functions from destroy.ts
2
+ export {
3
+ bulkRemove,
4
+ deactivate,
5
+ deactivateChildCategories,
6
+ remove,
7
+ removeChildCategories,
8
+ } from './destroy'
9
+
10
+ // Export all functions from fetch.ts
11
+ export {
12
+ compareCategoryGrowth,
13
+ fetchActive,
14
+ fetchAll,
15
+ fetchByDisplayOrder,
16
+ fetchById,
17
+ fetchByName,
18
+ fetchCategoryTree,
19
+ fetchChildCategories,
20
+ fetchRootCategories,
21
+ fetchStats,
22
+ } from './fetch'
23
+
24
+ // Export all functions from store.ts
25
+ export {
26
+ findOrCreateByName,
27
+ store,
28
+ } from './store'
29
+
30
+ // Export all functions from update.ts
31
+ export {
32
+ update,
33
+ updateActiveStatus,
34
+ updateDisplayOrder,
35
+ updateParent,
36
+ } from './update'
@@ -0,0 +1,87 @@
1
+ type CategoryJsonResponse = ModelRow<typeof Category>
2
+ type NewCategory = NewModelData<typeof Category>
3
+ import { db } from '@stacksjs/database'
4
+ import { slug } from '@stacksjs/strings'
5
+ import { fetchById } from './fetch'
6
+
7
+ export interface CategorizableTable {
8
+ id: number
9
+ name: string
10
+ slug: string
11
+ description?: string
12
+ is_active: boolean
13
+ categorizable_type: string
14
+ created_at?: string
15
+ updated_at?: string
16
+ }
17
+
18
+ /**
19
+ * Create a new category
20
+ *
21
+ * @param data The category data to store
22
+ * @returns The newly created category record
23
+ */
24
+ export async function store(data: NewCategory): Promise<CategoryJsonResponse> {
25
+ try {
26
+ // Add missing required properties
27
+ const categoryData = {
28
+ ...data,
29
+ slug: data.name?.toLowerCase().replace(/\s+/g, '-') || '',
30
+ categorizable_type: 'product',
31
+ display_order: 0,
32
+ }
33
+
34
+ const result = await db
35
+ .insertInto('categories')
36
+ .values(categoryData)
37
+ .executeTakeFirst()
38
+
39
+ if (!result)
40
+ throw new Error('Failed to create category')
41
+
42
+ const insertId = Number(result.insertId) || Number(result.numInsertedOrUpdatedRows)
43
+
44
+ const model = await fetchById(insertId)
45
+
46
+ if (!model)
47
+ throw new Error('Failed to create category')
48
+
49
+ return model
50
+ }
51
+ catch (error) {
52
+ if (error instanceof Error) {
53
+ if (error.message.includes('Duplicate entry') && error.message.includes('name')) {
54
+ throw new Error('A category with this name already exists')
55
+ }
56
+
57
+ throw new Error(`Failed to create category: ${error.message}`)
58
+ }
59
+
60
+ throw error
61
+ }
62
+ }
63
+
64
+ export async function findOrCreateByName(data: Partial<CategorizableTable>): Promise<CategoryJsonResponse> {
65
+ if (!data.name)
66
+ throw new Error('Name is required')
67
+
68
+ const existingCategory = await db
69
+ .selectFrom('categories')
70
+ .selectAll()
71
+ .where('name', '=', data.name)
72
+ .executeTakeFirst()
73
+
74
+ if (existingCategory)
75
+ return existingCategory as CategoryJsonResponse
76
+
77
+ const categoryData = {
78
+ name: data.name,
79
+ slug: slug(data.name),
80
+ is_active: data.is_active ?? true,
81
+ categorizable_type: data.categorizable_type ?? 'default',
82
+ display_order: 0,
83
+
84
+ }
85
+
86
+ return await store(categoryData)
87
+ }
@@ -0,0 +1,251 @@
1
+ import { db } from '@stacksjs/database'
2
+ import { formatDate } from '@stacksjs/orm'
3
+ import { fetchById } from './fetch'
4
+
5
+ type CategoryRow = ModelRow<typeof Category>
6
+
7
+ /**
8
+ * Update a category by ID
9
+ *
10
+ * @param id The ID of the category to update
11
+ * @param request The updated category data
12
+ * @returns The updated category record
13
+ */
14
+ export async function update(id: number, request: RequestInstance<typeof Category>): Promise<CategoryRow | undefined> {
15
+ // Validate the request data
16
+ await request.validate()
17
+
18
+ // Check if category exists
19
+ const existingCategory = await fetchById(id)
20
+ if (!existingCategory) {
21
+ throw new Error(`Category with ID ${id} not found`)
22
+ }
23
+
24
+ // Create update data object using request fields
25
+ const updateData = {
26
+ name: request.get('name'),
27
+ description: request.get('description'),
28
+ imageUrl: request.get('imageUrl'),
29
+ isActive: request.get('isActive'),
30
+ parentCategoryId: request.get('parentCategoryId'),
31
+ displayOrder: request.get('displayOrder'),
32
+ updated_at: formatDate(new Date()),
33
+ }
34
+
35
+ // If no fields to update, just return the existing category
36
+ if (Object.keys(updateData).length === 0) {
37
+ return existingCategory
38
+ }
39
+
40
+ try {
41
+ // Update the category
42
+ await db
43
+ .updateTable('categories')
44
+ .set(updateData)
45
+ .where('id', '=', id)
46
+ .execute()
47
+
48
+ // Fetch and return the updated category
49
+ return await fetchById(id)
50
+ }
51
+ catch (error) {
52
+ if (error instanceof Error) {
53
+ // Handle duplicate name error
54
+ if (error.message.includes('Duplicate entry') && error.message.includes('name')) {
55
+ throw new Error('A category with this name already exists')
56
+ }
57
+
58
+ throw new Error(`Failed to update category: ${error.message}`)
59
+ }
60
+
61
+ throw error
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Update category display order
67
+ *
68
+ * @param id The ID of the category
69
+ * @param newOrder The new display order value
70
+ * @returns The updated category
71
+ */
72
+ export async function updateDisplayOrder(id: number, newOrder: number): Promise<CategoryRow | undefined> {
73
+ // Check if category exists
74
+ const category = await fetchById(id)
75
+
76
+ if (!category) {
77
+ throw new Error(`Category with ID ${id} not found`)
78
+ }
79
+
80
+ try {
81
+ // Update the category's display order
82
+ await db
83
+ .updateTable('categories')
84
+ .set({
85
+ display_order: newOrder,
86
+ updated_at: formatDate(new Date()),
87
+ })
88
+ .where('id', '=', id)
89
+ .execute()
90
+
91
+ // Fetch the updated category
92
+ return await fetchById(id)
93
+ }
94
+ catch (error) {
95
+ if (error instanceof Error) {
96
+ throw new TypeError(`Failed to update category display order: ${error.message}`)
97
+ }
98
+
99
+ throw error
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Update category active status
105
+ *
106
+ * @param id The ID of the category
107
+ * @param isActive Whether the category should be active
108
+ * @returns The updated category
109
+ */
110
+ export async function updateActiveStatus(id: number, isActive: boolean): Promise<CategoryRow | undefined> {
111
+ // Check if category exists
112
+ const category = await fetchById(id)
113
+
114
+ if (!category) {
115
+ throw new Error(`Category with ID ${id} not found`)
116
+ }
117
+
118
+ try {
119
+ // Update the category's active status
120
+ await db
121
+ .updateTable('categories')
122
+ .set({
123
+ is_active: isActive,
124
+ updated_at: formatDate(new Date()),
125
+ })
126
+ .where('id', '=', id)
127
+ .execute()
128
+
129
+ // Fetch the updated category
130
+ return await fetchById(id)
131
+ }
132
+ catch (error) {
133
+ if (error instanceof Error) {
134
+ throw new TypeError(`Failed to update category active status: ${error.message}`)
135
+ }
136
+
137
+ throw error
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Move category to a different parent
143
+ *
144
+ * @param id The ID of the category to move
145
+ * @param newParentId The ID of the new parent category, or null to make it a root category
146
+ * @returns The updated category
147
+ */
148
+ export async function updateParent(id: number, newParentId: string | null): Promise<CategoryRow | undefined> {
149
+ // Check if category exists
150
+ const category = await fetchById(id)
151
+
152
+ if (!category) {
153
+ throw new Error(`Category with ID ${id} not found`)
154
+ }
155
+
156
+ // If moving to a parent, check that the parent exists and is not the same category
157
+ if (newParentId) {
158
+ // Convert to number for comparison since id is number
159
+ const newParentIdNum = Number(newParentId)
160
+
161
+ if (newParentIdNum === id) {
162
+ throw new Error('A category cannot be its own parent')
163
+ }
164
+
165
+ const parentCategory = await fetchById(newParentIdNum)
166
+ if (!parentCategory) {
167
+ throw new Error(`Parent category with ID ${newParentId} not found`)
168
+ }
169
+
170
+ // Check for circular reference
171
+ if (await wouldCreateCircularReference(id, newParentId)) {
172
+ throw new Error('This operation would create a circular reference in the category hierarchy')
173
+ }
174
+ }
175
+
176
+ try {
177
+ // Update the category's parent
178
+ const updateObject: { updated_at: string, parent_category_id?: string | undefined } = {
179
+ updated_at: formatDate(new Date()),
180
+ }
181
+
182
+ // Set parent_category_id explicitly based on whether newParentId is null
183
+ if (newParentId === null) {
184
+ await db
185
+ .updateTable('categories')
186
+ .set({
187
+ ...updateObject,
188
+ parent_category_id: undefined,
189
+ })
190
+ .where('id', '=', id)
191
+ .execute()
192
+ }
193
+ else {
194
+ await db
195
+ .updateTable('categories')
196
+ .set({
197
+ ...updateObject,
198
+ parent_category_id: newParentId,
199
+ })
200
+ .where('id', '=', id)
201
+ .execute()
202
+ }
203
+
204
+ // Fetch the updated category
205
+ return await fetchById(id)
206
+ }
207
+ catch (error) {
208
+ if (error instanceof Error) {
209
+ throw new TypeError(`Failed to update category parent: ${error.message}`)
210
+ }
211
+
212
+ throw error
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Helper function to check if changing a category's parent would create a circular reference
218
+ * @param categoryId ID of the category being moved
219
+ * @param newParentId ID of the new parent
220
+ * @returns boolean indicating if a circular reference would be created
221
+ */
222
+ async function wouldCreateCircularReference(categoryId: number, newParentId: string): Promise<boolean> {
223
+ // Convert to number for consistency in the check
224
+ let currentParentId = Number(newParentId)
225
+ const visited = new Set<number>()
226
+
227
+ while (currentParentId) {
228
+ // If we've seen this ID before, we have a cycle
229
+ if (visited.has(currentParentId)) {
230
+ return true
231
+ }
232
+
233
+ // If we've reached the original category, we have a cycle
234
+ if (currentParentId === categoryId) {
235
+ return true
236
+ }
237
+
238
+ visited.add(currentParentId)
239
+
240
+ // Get the parent's parent
241
+ const parent = await fetchById(currentParentId)
242
+ if (!parent || !parent.parentCategoryId) {
243
+ // We've reached a root category, no cycle
244
+ return false
245
+ }
246
+
247
+ currentParentId = Number(parent.parentCategoryId)
248
+ }
249
+
250
+ return false
251
+ }
@@ -0,0 +1,35 @@
1
+ import * as categories from './categories'
2
+ import * as items from './items'
3
+ import * as manufacturers from './manufacturers'
4
+ import * as reviews from './reviews'
5
+ import * as units from './units'
6
+ import * as variants from './variants'
7
+
8
+ export {
9
+ categories,
10
+ items,
11
+ manufacturers,
12
+ reviews,
13
+ units,
14
+ variants,
15
+ }
16
+
17
+ interface ProductsNamespace {
18
+ categories: typeof categories
19
+ items: typeof items
20
+ manufacturers: typeof manufacturers
21
+ reviews: typeof reviews
22
+ units: typeof units
23
+ variants: typeof variants
24
+ }
25
+
26
+ const products: ProductsNamespace = {
27
+ categories,
28
+ items,
29
+ manufacturers,
30
+ reviews,
31
+ units,
32
+ variants,
33
+ }
34
+
35
+ export default products