@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,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,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'
|