@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,147 @@
1
+ import type { StacksExpressionBuilder } from '@stacksjs/database'
2
+ import { db } from '@stacksjs/database'
3
+ import { HttpError } from '@stacksjs/error-handling'
4
+ import { formatDate, isUniqueViolation } from '@stacksjs/orm'
5
+ type CouponJsonResponse = ModelRow<typeof Coupon>
6
+ type CouponUpdate = UpdateModelData<typeof Coupon>
7
+ import { fetchById } from './fetch'
8
+
9
+ /**
10
+ * Discriminated result for `redeem` — `ok: true` on a successful
11
+ * atomic redemption (the row's `usage_count` was bumped and the
12
+ * limit wasn't already hit); `ok: false` with a `reason` when the
13
+ * redemption failed (out of uses, expired, inactive, missing).
14
+ * Callers branch on `reason` to surface a useful error to the user.
15
+ */
16
+ export type CouponRedemptionResult =
17
+ | { ok: true, coupon: CouponJsonResponse }
18
+ | { ok: false, reason: 'not-found' | 'inactive' | 'expired' | 'limit-reached' }
19
+
20
+ /**
21
+ * Update a coupon by ID
22
+ *
23
+ * @param id The ID of the coupon to update
24
+ * @param data The updated coupon data
25
+ * @returns The updated coupon record
26
+ */
27
+ export async function update(id: number, data: Omit<CouponUpdate, 'id'>): Promise<CouponJsonResponse | undefined> {
28
+ // Check if coupon exists
29
+ const existingCoupon = await fetchById(id)
30
+ if (!existingCoupon) {
31
+ throw new Error(`Coupon with ID ${id} not found`)
32
+ }
33
+
34
+ try {
35
+ // Update the coupon
36
+ await db
37
+ .updateTable('coupons')
38
+ .set({
39
+ ...data,
40
+ updated_at: formatDate(new Date()),
41
+ })
42
+ .where('id', '=', id)
43
+ .execute()
44
+
45
+ // Fetch and return the updated coupon
46
+ const updatedCoupon = await fetchById(id)
47
+
48
+ if (updatedCoupon) {
49
+ // Convert SQLite integer boolean to actual boolean
50
+ const c = updatedCoupon as Record<string, unknown>
51
+ c.is_active = Boolean(c.is_active)
52
+ }
53
+
54
+ return updatedCoupon
55
+ }
56
+ catch (error) {
57
+ if (error instanceof HttpError)
58
+ throw error
59
+ // Cross-dialect duplicate detection (#1957).
60
+ if (isUniqueViolation(error))
61
+ throw new HttpError(409, 'A coupon with this code already exists')
62
+ if (error instanceof Error)
63
+ throw new Error(`Failed to update coupon: ${error.message}`)
64
+ throw error
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Atomically redeem a coupon (stacksjs/stacks#1879 Co-5).
70
+ *
71
+ * Pre-fix: callers fetched the coupon, checked `usage_count <
72
+ * max_uses`, then bumped `usage_count` via `update()`. Two concurrent
73
+ * redemption requests against a coupon with `max_uses=1` both saw
74
+ * `usage_count=0`, both incremented, both succeeded. The coupon
75
+ * was redeemed twice — direct revenue loss equal to `discount_value`
76
+ * times the over-redemption count.
77
+ *
78
+ * Post-fix: single conditional UPDATE that bumps `usage_count` and
79
+ * enforces `max_uses` / `is_active` / expiry in the WHERE clause.
80
+ * The database guarantees only one writer wins the race. Returns a
81
+ * discriminated result so callers can surface a useful reason
82
+ * ("limit reached" vs "expired" vs "inactive") instead of a generic
83
+ * failure.
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * const result = await redeem(couponId)
88
+ * if (!result.ok) {
89
+ * throw new HttpError(400, `Coupon cannot be redeemed: ${result.reason}`)
90
+ * }
91
+ * // Use result.coupon — it reflects the post-redemption state.
92
+ * ```
93
+ */
94
+ export async function redeem(id: number): Promise<CouponRedemptionResult> {
95
+ // Single atomic UPDATE — increment usage_count and enforce every
96
+ // redemption precondition in the WHERE clause. `max_uses IS NULL`
97
+ // means "unlimited" so coupons without an explicit cap aren't
98
+ // accidentally rejected.
99
+ const now = formatDate(new Date())
100
+ const result = await db
101
+ .updateTable('coupons')
102
+ .set({
103
+ usage_count: (db as any).raw(`COALESCE(usage_count, 0) + 1`),
104
+ updated_at: now,
105
+ })
106
+ .where('id', '=', id)
107
+ .where((eb: StacksExpressionBuilder) => eb.or([
108
+ eb('max_uses', 'is', null),
109
+ eb.cmpr('usage_count', '<', eb.ref('max_uses')),
110
+ ]))
111
+ .where('is_active', '=', 1)
112
+ .where((eb: StacksExpressionBuilder) => eb.or([
113
+ eb('start_date', 'is', null),
114
+ eb.cmpr('start_date', '<=', now),
115
+ ]))
116
+ .where((eb: StacksExpressionBuilder) => eb.or([
117
+ eb('end_date', 'is', null),
118
+ eb.cmpr('end_date', '>=', now),
119
+ ]))
120
+ .executeTakeFirst()
121
+
122
+ // bun-query-builder's executeTakeFirst returns metadata describing
123
+ // affected rows. If the UPDATE matched zero rows, the redemption
124
+ // failed (precondition mismatch); diagnose which one to give the
125
+ // caller a useful reason.
126
+ const affected = Number((result as any)?.numUpdatedRows ?? (result as any)?.affectedRows ?? 0)
127
+ if (affected > 0) {
128
+ const refreshed = await fetchById(id)
129
+ if (refreshed) {
130
+ // Convert SQLite integer boolean to actual boolean.
131
+ const c = refreshed as Record<string, unknown>
132
+ c.is_active = Boolean(c.is_active)
133
+ return { ok: true, coupon: refreshed }
134
+ }
135
+ return { ok: false, reason: 'not-found' }
136
+ }
137
+
138
+ // UPDATE matched zero rows — figure out which precondition failed.
139
+ const existing = await fetchById(id)
140
+ if (!existing) return { ok: false, reason: 'not-found' }
141
+ if (!existing.is_active) return { ok: false, reason: 'inactive' }
142
+ const startOk = !existing.start_date || existing.start_date <= now
143
+ const endOk = !existing.end_date || existing.end_date >= now
144
+ if (!startOk || !endOk) return { ok: false, reason: 'expired' }
145
+ // Fell through every other check → limit must have been reached.
146
+ return { ok: false, reason: 'limit-reached' }
147
+ }
@@ -0,0 +1,62 @@
1
+ import { db } from '@stacksjs/database'
2
+ import { fetchById } from './fetch'
3
+
4
+ /**
5
+ * Delete a customer by ID
6
+ *
7
+ * @param id The ID of the customer to delete
8
+ * @returns True if the deletion was successful, false otherwise
9
+ */
10
+ export async function destroy(id: number): Promise<boolean> {
11
+ try {
12
+ // First check if the customer exists
13
+ const customer = await fetchById(id)
14
+
15
+ if (!customer) {
16
+ throw new Error(`Customer with ID ${id} not found`)
17
+ }
18
+
19
+ // Delete the customer
20
+ const result = await db
21
+ .deleteFrom('customers')
22
+ .where('id', '=', id)
23
+ .executeTakeFirst()
24
+
25
+ return result.numDeletedRows > 0
26
+ }
27
+ catch (error) {
28
+ if (error instanceof Error) {
29
+ throw new TypeError(`Failed to delete customer: ${error.message}`)
30
+ }
31
+
32
+ throw error
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Bulk delete multiple customers
38
+ *
39
+ * @param ids Array of customer IDs to delete
40
+ * @returns Number of customers successfully deleted
41
+ */
42
+ export async function bulkDestroy(ids: number[]): Promise<number> {
43
+ if (!ids.length)
44
+ return 0
45
+
46
+ try {
47
+ // Delete all customers in the array
48
+ const result = await db
49
+ .deleteFrom('customers')
50
+ .where('id', 'in', ids)
51
+ .executeTakeFirst()
52
+
53
+ return Number(result.numDeletedRows) || 0
54
+ }
55
+ catch (error) {
56
+ if (error instanceof Error) {
57
+ throw new TypeError(`Failed to delete customers in bulk: ${error.message}`)
58
+ }
59
+
60
+ throw error
61
+ }
62
+ }
@@ -0,0 +1,20 @@
1
+ type CustomerJsonResponse = ModelRow<typeof Customer>
2
+ import { db } from '@stacksjs/database'
3
+
4
+ /**
5
+ * Fetch a customer by ID
6
+ */
7
+ export async function fetchById(id: number): Promise<CustomerJsonResponse | undefined> {
8
+ return await db
9
+ .selectFrom('customers')
10
+ .where('id', '=', id)
11
+ .selectAll()
12
+ .executeTakeFirst() as CustomerJsonResponse | undefined
13
+ }
14
+
15
+ /**
16
+ * Fetch all customers
17
+ */
18
+ export async function fetchAll(): Promise<CustomerJsonResponse[]> {
19
+ return await db.selectFrom('customers').selectAll().execute() as CustomerJsonResponse[]
20
+ }
@@ -0,0 +1,17 @@
1
+ export {
2
+ bulkDestroy,
3
+ destroy,
4
+ } from './destroy'
5
+
6
+ export {
7
+ fetchAll,
8
+ fetchById,
9
+ } from './fetch'
10
+
11
+ export {
12
+ store,
13
+ } from './store'
14
+
15
+ export {
16
+ update,
17
+ } from './update'
@@ -0,0 +1,45 @@
1
+ type CustomerJsonResponse = ModelRow<typeof Customer>
2
+ type NewCustomer = NewModelData<typeof Customer>
3
+ import { db } from '@stacksjs/database'
4
+ import { HttpError } from '@stacksjs/error-handling'
5
+ import { isUniqueViolation } from '@stacksjs/orm'
6
+ import { fetchById } from './fetch'
7
+
8
+ /**
9
+ * Create a new customer
10
+ *
11
+ * @param data The customer data to store
12
+ * @returns The newly created customer record
13
+ */
14
+ export async function store(data: NewCustomer): Promise<CustomerJsonResponse> {
15
+ try {
16
+ const customerData = {
17
+ ...data,
18
+ avatar: data.avatar ?? 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400&fit=crop&q=60',
19
+ }
20
+
21
+ const result = await db
22
+ .insertInto('customers')
23
+ .values(customerData)
24
+ .executeTakeFirst()
25
+
26
+ if (!result)
27
+ throw new Error('Failed to create customer')
28
+
29
+ const insertId = result.insertId || Number(result.numInsertedOrUpdatedRows)
30
+
31
+ const customerResult = await fetchById(Number(insertId)) as CustomerJsonResponse
32
+
33
+ return customerResult
34
+ }
35
+ catch (error) {
36
+ if (error instanceof HttpError)
37
+ throw error
38
+ // Cross-dialect duplicate detection (#1957).
39
+ if (isUniqueViolation(error))
40
+ throw new HttpError(409, 'A customer with this email already exists')
41
+ if (error instanceof Error)
42
+ throw new Error(`Failed to create customer: ${error.message}`)
43
+ throw error
44
+ }
45
+ }
@@ -0,0 +1,44 @@
1
+ import { db } from '@stacksjs/database'
2
+ import { HttpError } from '@stacksjs/error-handling'
3
+ import { formatDate, isUniqueViolation } from '@stacksjs/orm'
4
+ type CustomerJsonResponse = ModelRow<typeof Customer>
5
+ type CustomerUpdate = UpdateModelData<typeof Customer>
6
+
7
+ /**
8
+ * Update a customer
9
+ *
10
+ * @param id The ID of the customer to update
11
+ * @param data The customer data to update
12
+ * @returns The updated customer record
13
+ */
14
+ export async function update(id: number, data: Omit<CustomerUpdate, 'id'>): Promise<CustomerJsonResponse> {
15
+ try {
16
+ if (!id)
17
+ throw new Error('Customer ID is required for update')
18
+
19
+ const result = await db
20
+ .updateTable('customers')
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 customer')
31
+
32
+ return result as CustomerJsonResponse
33
+ }
34
+ catch (error) {
35
+ if (error instanceof HttpError)
36
+ throw error
37
+ // Cross-dialect duplicate detection (#1957).
38
+ if (isUniqueViolation(error))
39
+ throw new HttpError(409, 'A customer with this email already exists')
40
+ if (error instanceof Error)
41
+ throw new Error(`Failed to update customer: ${error.message}`)
42
+ throw error
43
+ }
44
+ }
@@ -0,0 +1,54 @@
1
+ import { db } from '@stacksjs/database'
2
+
3
+ /**
4
+ * Delete a print device by ID
5
+ *
6
+ * @param id The ID of the print device to delete
7
+ * @returns True if the print device was deleted, false otherwise
8
+ */
9
+ export async function destroy(id: number): Promise<boolean> {
10
+ try {
11
+ const result = await db
12
+ .deleteFrom('print_devices')
13
+ .where('id', '=', id)
14
+ .executeTakeFirst()
15
+
16
+ return Number(result.numDeletedRows) > 0
17
+ }
18
+ catch (error) {
19
+ if (error instanceof Error) {
20
+ throw new TypeError(`Failed to delete print device: ${error.message}`)
21
+ }
22
+
23
+ throw error
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Delete multiple print devices by ID
29
+ *
30
+ * @param ids Array of print device IDs to delete
31
+ * @returns Number of print devices deleted
32
+ */
33
+ export async function bulkDestroy(ids: number[]): Promise<number> {
34
+ if (!ids.length)
35
+ return 0
36
+
37
+ try {
38
+ // Perform the delete operation
39
+ const result = await db
40
+ .deleteFrom('print_devices')
41
+ .where('id', 'in', ids)
42
+ .executeTakeFirst()
43
+
44
+ // Return the number of deleted rows
45
+ return Number(result.numDeletedRows)
46
+ }
47
+ catch (error) {
48
+ if (error instanceof Error) {
49
+ throw new TypeError(`Failed to delete print devices: ${error.message}`)
50
+ }
51
+
52
+ throw error
53
+ }
54
+ }
@@ -0,0 +1,98 @@
1
+ type PrintDeviceJsonResponse = ModelRow<typeof PrintDevice>
2
+ import type { SpreadsheetWrapper } from 'ts-spreadsheets'
3
+ import { createSpreadsheet } from 'ts-spreadsheets'
4
+ import { fetchAll } from './fetch'
5
+
6
+ /**
7
+ * Represents the structure of an exported print device
8
+ */
9
+ export interface ExportedPrintDevice {
10
+ 'Device ID': number
11
+ 'Name': string
12
+ 'MAC Address': string
13
+ 'Location': string
14
+ 'Terminal': string
15
+ 'Status': string
16
+ 'Last Ping': string
17
+ 'Print Count': number
18
+ }
19
+
20
+ /**
21
+ * Export print devices to a spreadsheet
22
+ * @param format The format of the spreadsheet (default is CSV)
23
+ * @returns Spreadsheet object ready for download or storage
24
+ */
25
+ export async function exportPrintDevices(format: 'csv' | 'excel' = 'csv'): Promise<SpreadsheetWrapper> {
26
+ // Fetch all print devices
27
+ const devices = await fetchAll()
28
+
29
+ // Prepare data for spreadsheet
30
+ const spreadsheetData = prepareDevicesForExport(devices)
31
+
32
+ // Create and return spreadsheet
33
+ return createSpreadsheet(spreadsheetData, { type: format })
34
+ }
35
+
36
+ /**
37
+ * Prepare print devices data for spreadsheet export
38
+ * @param devices Array of print device objects
39
+ * @returns Spreadsheet data structure
40
+ */
41
+ function prepareDevicesForExport(devices: PrintDeviceJsonResponse[]) {
42
+ // Define headings
43
+ const headings: (keyof ExportedPrintDevice)[] = [
44
+ 'Device ID',
45
+ 'Name',
46
+ 'MAC Address',
47
+ 'Location',
48
+ 'Terminal',
49
+ 'Status',
50
+ 'Last Ping',
51
+ 'Print Count',
52
+ ]
53
+
54
+ // Transform devices into export format
55
+ const data = devices.map((device) => {
56
+ return [
57
+ device.id,
58
+ device.name,
59
+ device.macAddress,
60
+ device.location,
61
+ device.terminal,
62
+ device.status,
63
+ new Date(device.lastPing).toLocaleString(),
64
+ device.printCount,
65
+ ]
66
+ })
67
+
68
+ return { headings, data }
69
+ }
70
+
71
+ /**
72
+ * Export print devices and automatically download
73
+ * @param format The format of the spreadsheet (default is CSV)
74
+ * @param filename Optional filename for the download
75
+ * @returns Download response
76
+ */
77
+ export async function downloadPrintDevices(format: 'csv' | 'excel' = 'csv', filename?: string): Promise<Response> {
78
+ const spreadsheet = await exportPrintDevices(format)
79
+
80
+ // Use default filename if not provided
81
+ const defaultFilename = `print_devices_export_${new Date().toISOString().split('T')[0]}.${format}`
82
+ return spreadsheet.download(filename || defaultFilename)
83
+ }
84
+
85
+ /**
86
+ * Store print devices export to disk
87
+ * @param format The format of the spreadsheet (default is CSV)
88
+ * @param path Optional path to store the file
89
+ * @returns Path where the file is stored
90
+ */
91
+ export async function storePrintDevicesExport(format: 'csv' | 'excel' = 'csv', path?: string): Promise<void> {
92
+ const spreadsheet = await exportPrintDevices(format)
93
+
94
+ // Use default path and filename if not provided
95
+ const defaultPath = `print_devices_export_${new Date().toISOString().split('T')[0]}.${format}`
96
+
97
+ spreadsheet.store(path || defaultPath)
98
+ }
@@ -0,0 +1,125 @@
1
+ type PrintDeviceJsonResponse = ModelRow<typeof PrintDevice>
2
+ type ReceiptJsonResponse = ModelRow<typeof Receipt>
3
+ import type { StacksExpressionBuilder } from '@stacksjs/database'
4
+ import { db } from '@stacksjs/database'
5
+
6
+ /**
7
+ * Fetch a print device by ID
8
+ */
9
+ export async function fetchById(id: number): Promise<PrintDeviceJsonResponse | undefined> {
10
+ return await db
11
+ .selectFrom('print_devices')
12
+ .where('id', '=', id)
13
+ .selectAll()
14
+ .executeTakeFirst() as PrintDeviceJsonResponse | undefined
15
+ }
16
+
17
+ /**
18
+ * Fetch all print devices
19
+ */
20
+ export async function fetchAll(): Promise<PrintDeviceJsonResponse[]> {
21
+ return await db.selectFrom('print_devices').selectAll().execute() as PrintDeviceJsonResponse[]
22
+ }
23
+
24
+ /**
25
+ * Count all print devices
26
+ */
27
+ export async function countAll(): Promise<number> {
28
+ const result = await db
29
+ .selectFrom('print_devices')
30
+ .select(((eb: StacksExpressionBuilder) => eb.fn.count('id').as('count')) as any)
31
+ .executeTakeFirst() as { count: number } | undefined
32
+ return result?.count ?? 0
33
+ }
34
+
35
+ /**
36
+ * Count total prints across all print devices from receipts
37
+ */
38
+ export async function countTotalPrints(): Promise<number> {
39
+ const result = await db
40
+ .selectFrom('print_devices')
41
+ .select('print_count' as any)
42
+ .executeTakeFirst()
43
+
44
+ return (result as any)?.print_count ?? 0
45
+ }
46
+
47
+ /**
48
+ * Count total prints for a specific print device from receipts
49
+ */
50
+ export async function countPrintsByDeviceId(printDeviceId: number): Promise<number> {
51
+ const result = await db
52
+ .selectFrom('print_devices')
53
+ .select('print_count' as any)
54
+ .where('id', '=', printDeviceId)
55
+ .executeTakeFirst()
56
+
57
+ return (result as any)?.print_count ?? 0
58
+ }
59
+
60
+ /**
61
+ * Calculate error rate percentage based on receipts
62
+ */
63
+ export async function calculateErrorRate(): Promise<number> {
64
+ const result = await db
65
+ .selectFrom('receipts')
66
+ .select(((eb: StacksExpressionBuilder) => [
67
+ eb.fn.count('id').as('total'),
68
+ eb.fn.count('id').filterWhere('status', '=', 'error').as('error_count'),
69
+ ]) as any)
70
+ .executeTakeFirst()
71
+
72
+ if (!(result as any)?.total || (result as any).total === 0) {
73
+ return 0
74
+ }
75
+
76
+ return Number((((result as any).error_count ?? 0) / (result as any).total) * 100)
77
+ }
78
+
79
+ /**
80
+ * Fetch all errors from receipts for a specific print device
81
+ */
82
+ export async function fetchErrorsByDeviceId(printDeviceId: number): Promise<ReceiptJsonResponse[]> {
83
+ return await db
84
+ .selectFrom('receipts')
85
+ .where('print_device_id', '=', printDeviceId)
86
+ .where('status', '=', 'error')
87
+ .selectAll()
88
+ .execute() as ReceiptJsonResponse[]
89
+ }
90
+
91
+ /**
92
+ * Calculate printer health percentage based on online status
93
+ */
94
+ export async function calculatePrinterHealth(): Promise<number> {
95
+ const result = await db
96
+ .selectFrom('print_devices')
97
+ .select(((eb: StacksExpressionBuilder) => [
98
+ eb.fn.count('id').as('total'),
99
+ eb.fn.count('id').filterWhere('status', '=', 'online').as('online_count'),
100
+ ]) as any)
101
+ .executeTakeFirst()
102
+
103
+ if (!(result as any)?.total || (result as any).total === 0) {
104
+ return 0
105
+ }
106
+
107
+ return Number((((result as any).online_count ?? 0) / (result as any).total) * 100)
108
+ }
109
+
110
+ /**
111
+ * Get printer counts by status for visualization
112
+ */
113
+ export async function getPrinterStatusCounts(): Promise<Record<string, number>> {
114
+ const result = await db
115
+ .selectFrom('print_devices')
116
+ .select(['status', (eb: StacksExpressionBuilder) => eb.fn.count('id').as('count')] as any)
117
+ .groupBy('status')
118
+ .execute() as { status: string, count: number }[]
119
+
120
+ // Convert array to object with status as key and count as value
121
+ return result.reduce((acc: any, curr: any) => {
122
+ acc[curr.status as string] = curr.count
123
+ return acc
124
+ }, {} as Record<string, number>)
125
+ }
@@ -0,0 +1,31 @@
1
+ // Functions from destroy.ts
2
+ export {
3
+ bulkDestroy,
4
+ destroy,
5
+ } from './destroy'
6
+
7
+ // Functions from export.ts
8
+ export {
9
+ downloadPrintDevices,
10
+ exportPrintDevices,
11
+ storePrintDevicesExport,
12
+ } from './export'
13
+
14
+ // Functions from fetch.ts
15
+ export {
16
+ fetchAll,
17
+ fetchById,
18
+ } from './fetch'
19
+
20
+ // Functions from store.ts
21
+ export {
22
+ bulkStore,
23
+ store,
24
+ } from './store'
25
+
26
+ // Functions from update.ts
27
+ export {
28
+ update,
29
+ updatePrintCount,
30
+ updateStatus,
31
+ } from './update'