@stacksjs/commerce 0.70.55 → 0.70.57
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 +10 -7
- package/src/carts/destroy.ts +0 -54
- package/src/carts/fetch.ts +0 -20
- package/src/carts/index.ts +0 -13
- package/src/carts/store.ts +0 -75
- package/src/carts/update.ts +0 -78
- package/src/coupons/destroy.ts +0 -49
- package/src/coupons/fetch.ts +0 -510
- package/src/coupons/index.ts +0 -30
- package/src/coupons/store.ts +0 -46
- package/src/coupons/update.ts +0 -147
- package/src/customers/destroy.ts +0 -62
- package/src/customers/fetch.ts +0 -20
- package/src/customers/index.ts +0 -17
- package/src/customers/store.ts +0 -45
- package/src/customers/update.ts +0 -44
- package/src/devices/destroy.ts +0 -54
- package/src/devices/export.ts +0 -98
- package/src/devices/fetch.ts +0 -125
- package/src/devices/index.ts +0 -31
- package/src/devices/store.ts +0 -82
- package/src/devices/update.ts +0 -125
- package/src/errors/destroy.ts +0 -54
- package/src/errors/fetch.ts +0 -197
- package/src/errors/index.ts +0 -28
- package/src/errors/update.ts +0 -116
- package/src/gift-cards/destroy.ts +0 -87
- package/src/gift-cards/fetch.ts +0 -431
- package/src/gift-cards/index.ts +0 -26
- package/src/gift-cards/store.ts +0 -56
- package/src/gift-cards/update.ts +0 -102
- package/src/index.ts +0 -78
- package/src/orders/destroy.ts +0 -111
- package/src/orders/events.ts +0 -121
- package/src/orders/export.ts +0 -158
- package/src/orders/fetch.ts +0 -379
- package/src/orders/guards.ts +0 -261
- package/src/orders/index.ts +0 -91
- package/src/orders/place-order.ts +0 -274
- package/src/orders/store.ts +0 -51
- package/src/orders/totals.ts +0 -190
- package/src/orders/update.ts +0 -164
- package/src/orders/webhook.ts +0 -312
- package/src/payments/destroy.ts +0 -56
- package/src/payments/fetch.ts +0 -288
- package/src/payments/index.ts +0 -16
- package/src/payments/store.ts +0 -63
- package/src/payments/update.ts +0 -52
- package/src/product.ts +0 -0
- package/src/products/categories/destroy.ts +0 -103
- package/src/products/categories/fetch.ts +0 -285
- package/src/products/categories/index.ts +0 -36
- package/src/products/categories/store.ts +0 -87
- package/src/products/categories/update.ts +0 -251
- package/src/products/index.ts +0 -35
- package/src/products/items/destroy.ts +0 -54
- package/src/products/items/fetch.ts +0 -282
- package/src/products/items/index.ts +0 -26
- package/src/products/items/store.ts +0 -84
- package/src/products/items/update.ts +0 -324
- package/src/products/manufacturers/destroy.ts +0 -56
- package/src/products/manufacturers/fetch.ts +0 -117
- package/src/products/manufacturers/index.ts +0 -24
- package/src/products/manufacturers/store.ts +0 -82
- package/src/products/manufacturers/update.ts +0 -113
- package/src/products/reviews/destroy.ts +0 -56
- package/src/products/reviews/fetch.ts +0 -63
- package/src/products/reviews/index.ts +0 -25
- package/src/products/reviews/store.ts +0 -46
- package/src/products/reviews/update.ts +0 -82
- package/src/products/units/destroy.ts +0 -56
- package/src/products/units/fetch.ts +0 -20
- package/src/products/units/index.ts +0 -13
- package/src/products/units/store.ts +0 -162
- package/src/products/units/update.ts +0 -159
- package/src/products/variants/destroy.ts +0 -56
- package/src/products/variants/fetch.ts +0 -20
- package/src/products/variants/index.ts +0 -21
- package/src/products/variants/store.ts +0 -136
- package/src/products/variants/update.ts +0 -104
- package/src/receipts/destroy.ts +0 -54
- package/src/receipts/export.ts +0 -0
- package/src/receipts/fetch.ts +0 -222
- package/src/receipts/index.ts +0 -24
- package/src/receipts/printer.ts +0 -69
- package/src/receipts/store.ts +0 -69
- package/src/receipts/update.ts +0 -124
- package/src/shippings/delivery-routes/destroy.ts +0 -54
- package/src/shippings/delivery-routes/fetch.ts +0 -42
- package/src/shippings/delivery-routes/index.ts +0 -26
- package/src/shippings/delivery-routes/store.ts +0 -69
- package/src/shippings/delivery-routes/update.ts +0 -120
- package/src/shippings/digital-deliveries/destroy.ts +0 -111
- package/src/shippings/digital-deliveries/fetch.ts +0 -20
- package/src/shippings/digital-deliveries/index.ts +0 -26
- package/src/shippings/digital-deliveries/store.ts +0 -76
- package/src/shippings/digital-deliveries/update.ts +0 -129
- package/src/shippings/drivers/destroy.ts +0 -78
- package/src/shippings/drivers/fetch.ts +0 -20
- package/src/shippings/drivers/index.ts +0 -24
- package/src/shippings/drivers/store.ts +0 -69
- package/src/shippings/drivers/update.ts +0 -117
- package/src/shippings/index.ts +0 -39
- package/src/shippings/license-keys/destroy.ts +0 -111
- package/src/shippings/license-keys/fetch.ts +0 -20
- package/src/shippings/license-keys/index.ts +0 -26
- package/src/shippings/license-keys/store.ts +0 -77
- package/src/shippings/license-keys/update.ts +0 -117
- package/src/shippings/shipping-methods/destroy.ts +0 -111
- package/src/shippings/shipping-methods/fetch.ts +0 -62
- package/src/shippings/shipping-methods/index.ts +0 -28
- package/src/shippings/shipping-methods/store.ts +0 -123
- package/src/shippings/shipping-methods/update.ts +0 -120
- package/src/shippings/shipping-rates/destroy.ts +0 -102
- package/src/shippings/shipping-rates/fetch.ts +0 -320
- package/src/shippings/shipping-rates/index.ts +0 -36
- package/src/shippings/shipping-rates/store.ts +0 -102
- package/src/shippings/shipping-rates/update.ts +0 -139
- package/src/shippings/shipping-zones/destroy.ts +0 -111
- package/src/shippings/shipping-zones/fetch.ts +0 -96
- package/src/shippings/shipping-zones/index.ts +0 -26
- package/src/shippings/shipping-zones/store.ts +0 -74
- package/src/shippings/shipping-zones/update.ts +0 -156
- package/src/tax/destroy.ts +0 -54
- package/src/tax/fetch.ts +0 -20
- package/src/tax/index.ts +0 -24
- package/src/tax/store.ts +0 -69
- package/src/tax/update.ts +0 -119
- package/src/tests/coupons.test.ts +0 -90
- package/src/tests/customers.test.ts +0 -39
- package/src/tests/device.test.ts +0 -111
- package/src/tests/digitals.test.ts +0 -31
- package/src/tests/drivers.test.ts +0 -25
- package/src/tests/gift-cards.test.ts +0 -36
- package/src/tests/license.test.ts +0 -31
- package/src/tests/manufacturers.test.ts +0 -80
- package/src/tests/orders.test.ts +0 -25
- package/src/tests/payments.test.ts +0 -75
- package/src/tests/rates.test.ts +0 -367
- package/src/tests/receipts.test.ts +0 -287
- package/src/tests/restaurant.test.ts +0 -522
- package/src/tests/reviews.test.ts +0 -165
- package/src/tests/routes.test.ts +0 -51
- package/src/tests/setup.ts +0 -149
- package/src/tests/shipping.test.ts +0 -88
- package/src/tests/tax.test.ts +0 -25
- package/src/tests/units.test.ts +0 -110
- package/src/tests/variants.test.ts +0 -133
- package/src/tests/waitlist.test.ts +0 -303
- package/src/tests/zones.test.ts +0 -86
- package/src/types.ts +0 -312
- package/src/utils/typed-stats.ts +0 -67
- package/src/waitlists/index.ts +0 -19
- package/src/waitlists/products/destroy.ts +0 -65
- package/src/waitlists/products/export.ts +0 -113
- package/src/waitlists/products/fetch.ts +0 -362
- package/src/waitlists/products/index.ts +0 -33
- package/src/waitlists/products/store.ts +0 -112
- package/src/waitlists/products/update.ts +0 -125
- package/src/waitlists/restaurant/destroy.ts +0 -65
- package/src/waitlists/restaurant/export.ts +0 -107
- package/src/waitlists/restaurant/fetch.ts +0 -538
- package/src/waitlists/restaurant/index.ts +0 -43
- package/src/waitlists/restaurant/store.ts +0 -109
- package/src/waitlists/restaurant/update.ts +0 -198
- /package/dist/{src/coupons → coupons}/destroy.d.ts +0 -0
- /package/dist/{src/coupons → coupons}/fetch.d.ts +0 -0
- /package/dist/{src/coupons → coupons}/index.d.ts +0 -0
- /package/dist/{src/coupons → coupons}/store.d.ts +0 -0
- /package/dist/{src/coupons → coupons}/update.d.ts +0 -0
- /package/dist/{src/customers → customers}/destroy.d.ts +0 -0
- /package/dist/{src/customers → customers}/fetch.d.ts +0 -0
- /package/dist/{src/customers → customers}/index.d.ts +0 -0
- /package/dist/{src/customers → customers}/store.d.ts +0 -0
- /package/dist/{src/customers → customers}/update.d.ts +0 -0
- /package/dist/{src/devices → devices}/destroy.d.ts +0 -0
- /package/dist/{src/devices → devices}/export.d.ts +0 -0
- /package/dist/{src/devices → devices}/fetch.d.ts +0 -0
- /package/dist/{src/devices → devices}/index.d.ts +0 -0
- /package/dist/{src/devices → devices}/store.d.ts +0 -0
- /package/dist/{src/devices → devices}/update.d.ts +0 -0
- /package/dist/{src/errors → errors}/destroy.d.ts +0 -0
- /package/dist/{src/errors → errors}/fetch.d.ts +0 -0
- /package/dist/{src/errors → errors}/index.d.ts +0 -0
- /package/dist/{src/errors → errors}/update.d.ts +0 -0
- /package/dist/{src/gift-cards → gift-cards}/destroy.d.ts +0 -0
- /package/dist/{src/gift-cards → gift-cards}/fetch.d.ts +0 -0
- /package/dist/{src/gift-cards → gift-cards}/index.d.ts +0 -0
- /package/dist/{src/gift-cards → gift-cards}/store.d.ts +0 -0
- /package/dist/{src/gift-cards → gift-cards}/update.d.ts +0 -0
- /package/dist/{src/index.d.ts → index.d.ts} +0 -0
- /package/dist/{src/orders → orders}/destroy.d.ts +0 -0
- /package/dist/{src/orders → orders}/events.d.ts +0 -0
- /package/dist/{src/orders → orders}/export.d.ts +0 -0
- /package/dist/{src/orders → orders}/fetch.d.ts +0 -0
- /package/dist/{src/orders → orders}/guards.d.ts +0 -0
- /package/dist/{src/orders → orders}/index.d.ts +0 -0
- /package/dist/{src/orders → orders}/place-order.d.ts +0 -0
- /package/dist/{src/orders → orders}/store.d.ts +0 -0
- /package/dist/{src/orders → orders}/totals.d.ts +0 -0
- /package/dist/{src/orders → orders}/update.d.ts +0 -0
- /package/dist/{src/orders → orders}/webhook.d.ts +0 -0
- /package/dist/{src/payments → payments}/destroy.d.ts +0 -0
- /package/dist/{src/payments → payments}/fetch.d.ts +0 -0
- /package/dist/{src/payments → payments}/index.d.ts +0 -0
- /package/dist/{src/payments → payments}/store.d.ts +0 -0
- /package/dist/{src/payments → payments}/update.d.ts +0 -0
- /package/dist/{src/products → products}/categories/destroy.d.ts +0 -0
- /package/dist/{src/products → products}/categories/fetch.d.ts +0 -0
- /package/dist/{src/products → products}/categories/index.d.ts +0 -0
- /package/dist/{src/products → products}/categories/store.d.ts +0 -0
- /package/dist/{src/products → products}/categories/update.d.ts +0 -0
- /package/dist/{src/products → products}/index.d.ts +0 -0
- /package/dist/{src/products → products}/items/destroy.d.ts +0 -0
- /package/dist/{src/products → products}/items/fetch.d.ts +0 -0
- /package/dist/{src/products → products}/items/index.d.ts +0 -0
- /package/dist/{src/products → products}/items/store.d.ts +0 -0
- /package/dist/{src/products → products}/items/update.d.ts +0 -0
- /package/dist/{src/products → products}/manufacturers/destroy.d.ts +0 -0
- /package/dist/{src/products → products}/manufacturers/fetch.d.ts +0 -0
- /package/dist/{src/products → products}/manufacturers/index.d.ts +0 -0
- /package/dist/{src/products → products}/manufacturers/store.d.ts +0 -0
- /package/dist/{src/products → products}/manufacturers/update.d.ts +0 -0
- /package/dist/{src/products → products}/reviews/destroy.d.ts +0 -0
- /package/dist/{src/products → products}/reviews/fetch.d.ts +0 -0
- /package/dist/{src/products → products}/reviews/index.d.ts +0 -0
- /package/dist/{src/products → products}/reviews/store.d.ts +0 -0
- /package/dist/{src/products → products}/reviews/update.d.ts +0 -0
- /package/dist/{src/products → products}/units/destroy.d.ts +0 -0
- /package/dist/{src/products → products}/units/fetch.d.ts +0 -0
- /package/dist/{src/products → products}/units/index.d.ts +0 -0
- /package/dist/{src/products → products}/units/store.d.ts +0 -0
- /package/dist/{src/products → products}/units/update.d.ts +0 -0
- /package/dist/{src/products → products}/variants/destroy.d.ts +0 -0
- /package/dist/{src/products → products}/variants/fetch.d.ts +0 -0
- /package/dist/{src/products → products}/variants/index.d.ts +0 -0
- /package/dist/{src/products → products}/variants/store.d.ts +0 -0
- /package/dist/{src/products → products}/variants/update.d.ts +0 -0
- /package/dist/{src/receipts → receipts}/destroy.d.ts +0 -0
- /package/dist/{src/receipts → receipts}/fetch.d.ts +0 -0
- /package/dist/{src/receipts → receipts}/index.d.ts +0 -0
- /package/dist/{src/receipts → receipts}/store.d.ts +0 -0
- /package/dist/{src/receipts → receipts}/update.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/delivery-routes/destroy.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/delivery-routes/fetch.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/delivery-routes/index.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/delivery-routes/store.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/delivery-routes/update.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/digital-deliveries/destroy.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/digital-deliveries/fetch.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/digital-deliveries/index.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/digital-deliveries/store.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/digital-deliveries/update.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/drivers/destroy.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/drivers/fetch.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/drivers/index.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/drivers/store.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/drivers/update.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/index.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/license-keys/destroy.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/license-keys/fetch.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/license-keys/index.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/license-keys/store.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/license-keys/update.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/shipping-methods/destroy.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/shipping-methods/fetch.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/shipping-methods/index.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/shipping-methods/store.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/shipping-methods/update.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/shipping-rates/destroy.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/shipping-rates/fetch.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/shipping-rates/index.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/shipping-rates/store.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/shipping-rates/update.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/shipping-zones/destroy.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/shipping-zones/fetch.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/shipping-zones/index.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/shipping-zones/store.d.ts +0 -0
- /package/dist/{src/shippings → shippings}/shipping-zones/update.d.ts +0 -0
- /package/dist/{src/tax → tax}/destroy.d.ts +0 -0
- /package/dist/{src/tax → tax}/fetch.d.ts +0 -0
- /package/dist/{src/tax → tax}/index.d.ts +0 -0
- /package/dist/{src/tax → tax}/store.d.ts +0 -0
- /package/dist/{src/tax → tax}/update.d.ts +0 -0
- /package/dist/{src/types.d.ts → types.d.ts} +0 -0
- /package/dist/{src/utils → utils}/typed-stats.d.ts +0 -0
- /package/dist/{src/waitlists → waitlists}/index.d.ts +0 -0
- /package/dist/{src/waitlists → waitlists}/products/destroy.d.ts +0 -0
- /package/dist/{src/waitlists → waitlists}/products/fetch.d.ts +0 -0
- /package/dist/{src/waitlists → waitlists}/products/index.d.ts +0 -0
- /package/dist/{src/waitlists → waitlists}/products/store.d.ts +0 -0
- /package/dist/{src/waitlists → waitlists}/products/update.d.ts +0 -0
- /package/dist/{src/waitlists → waitlists}/restaurant/destroy.d.ts +0 -0
- /package/dist/{src/waitlists → waitlists}/restaurant/fetch.d.ts +0 -0
- /package/dist/{src/waitlists → waitlists}/restaurant/index.d.ts +0 -0
- /package/dist/{src/waitlists → waitlists}/restaurant/store.d.ts +0 -0
- /package/dist/{src/waitlists → waitlists}/restaurant/update.d.ts +0 -0
package/src/orders/fetch.ts
DELETED
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
type OrderJsonResponse = ModelRow<typeof Order>
|
|
2
|
-
import type {
|
|
3
|
-
OrderStats,
|
|
4
|
-
OrderTypeCount,
|
|
5
|
-
StatusCount,
|
|
6
|
-
} from '../types'
|
|
7
|
-
import type { StacksExpressionBuilder } from '@stacksjs/database'
|
|
8
|
-
import { db } from '@stacksjs/database'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Fetch all orders from the database with their items
|
|
12
|
-
* @param limit Optional limit on number of orders to fetch
|
|
13
|
-
*/
|
|
14
|
-
export async function fetchAll(limit?: number): Promise<OrderJsonResponse[]> {
|
|
15
|
-
let query = db
|
|
16
|
-
.selectFrom('orders')
|
|
17
|
-
.selectAll()
|
|
18
|
-
.orderBy('created_at', 'desc')
|
|
19
|
-
|
|
20
|
-
if (limit) {
|
|
21
|
-
query = query.limit(limit)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const orders = await query.execute()
|
|
25
|
-
|
|
26
|
-
// Fetch items for each order
|
|
27
|
-
return await Promise.all(orders.map(async (order: OrderJsonResponse) => {
|
|
28
|
-
const items = await db
|
|
29
|
-
.selectFrom('order_items')
|
|
30
|
-
.where('order_id', '=', order.id)
|
|
31
|
-
.selectAll()
|
|
32
|
-
.execute()
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
...order,
|
|
36
|
-
items,
|
|
37
|
-
}
|
|
38
|
-
}))
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Fetch the most recent orders
|
|
43
|
-
* @param limit Number of recent orders to fetch (default: 10)
|
|
44
|
-
*/
|
|
45
|
-
export async function fetchRecent(limit: number = 10): Promise<OrderJsonResponse[]> {
|
|
46
|
-
return fetchAll(limit)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Fetch an order by ID
|
|
51
|
-
*/
|
|
52
|
-
export async function fetchById(id: number): Promise<OrderJsonResponse | undefined> {
|
|
53
|
-
const order = await db
|
|
54
|
-
.selectFrom('orders')
|
|
55
|
-
.where('id', '=', id)
|
|
56
|
-
.selectAll()
|
|
57
|
-
.executeTakeFirst()
|
|
58
|
-
|
|
59
|
-
return order as OrderJsonResponse | undefined
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Get order statistics
|
|
64
|
-
*/
|
|
65
|
-
export async function fetchStats(): Promise<OrderStats> {
|
|
66
|
-
// Total orders
|
|
67
|
-
const totalOrders = await db
|
|
68
|
-
.selectFrom('orders')
|
|
69
|
-
.select(((eb: StacksExpressionBuilder) => eb.fn.count('id').as('count')) as any)
|
|
70
|
-
.executeTakeFirst() as { count: number } | undefined
|
|
71
|
-
|
|
72
|
-
// Orders by status
|
|
73
|
-
const ordersByStatus = await db
|
|
74
|
-
.selectFrom('orders')
|
|
75
|
-
.select(['status', (eb: StacksExpressionBuilder) => eb.fn.count('id').as('count')] as any)
|
|
76
|
-
.groupBy('status')
|
|
77
|
-
.execute() as unknown as StatusCount[]
|
|
78
|
-
|
|
79
|
-
// Orders by type
|
|
80
|
-
const ordersByType = await db
|
|
81
|
-
.selectFrom('orders')
|
|
82
|
-
.select(['order_type', (eb: StacksExpressionBuilder) => eb.fn.count('id').as('count')] as any)
|
|
83
|
-
.groupBy('order_type')
|
|
84
|
-
.execute() as unknown as OrderTypeCount[]
|
|
85
|
-
|
|
86
|
-
// Recent orders with their items
|
|
87
|
-
const recentOrdersRaw = await db
|
|
88
|
-
.selectFrom('orders')
|
|
89
|
-
.selectAll()
|
|
90
|
-
.orderBy('created_at', 'desc')
|
|
91
|
-
.limit(5)
|
|
92
|
-
.execute()
|
|
93
|
-
|
|
94
|
-
// Fetch items for each recent order
|
|
95
|
-
const recentOrders = await Promise.all(recentOrdersRaw.map(async (order: OrderJsonResponse) => {
|
|
96
|
-
const items = await db
|
|
97
|
-
.selectFrom('order_items')
|
|
98
|
-
.where('order_id', '=', order.id)
|
|
99
|
-
.selectAll()
|
|
100
|
-
.execute()
|
|
101
|
-
|
|
102
|
-
return {
|
|
103
|
-
...order,
|
|
104
|
-
items,
|
|
105
|
-
}
|
|
106
|
-
}))
|
|
107
|
-
|
|
108
|
-
// Total revenue
|
|
109
|
-
const revenue = await db
|
|
110
|
-
.selectFrom('orders')
|
|
111
|
-
.select(((eb: StacksExpressionBuilder) => eb.fn.sum('total_amount').as('total')) as any)
|
|
112
|
-
.executeTakeFirst() as { total: number } | undefined
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
total: Number(totalOrders?.count || 0),
|
|
116
|
-
by_status: ordersByStatus,
|
|
117
|
-
by_type: ordersByType,
|
|
118
|
-
recent: recentOrders,
|
|
119
|
-
revenue: Number(revenue?.total || 0),
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Compare orders between different time periods
|
|
125
|
-
* @param daysRange Number of days to look back (7, 30, 60, etc.)
|
|
126
|
-
*/
|
|
127
|
-
export async function compareOrdersByPeriod(_daysRange: number = 30): Promise<{
|
|
128
|
-
current_period: number
|
|
129
|
-
previous_period: number
|
|
130
|
-
difference: number
|
|
131
|
-
percentage_change: number
|
|
132
|
-
days_range: number
|
|
133
|
-
}> {
|
|
134
|
-
const today = new Date()
|
|
135
|
-
|
|
136
|
-
// Current period (last N days)
|
|
137
|
-
const currentPeriodStart = new Date(today)
|
|
138
|
-
currentPeriodStart.setDate(today.getDate() - _daysRange)
|
|
139
|
-
|
|
140
|
-
// Previous period (N days before the current period)
|
|
141
|
-
const previousPeriodEnd = new Date(currentPeriodStart)
|
|
142
|
-
previousPeriodEnd.setDate(previousPeriodEnd.getDate() - 1)
|
|
143
|
-
|
|
144
|
-
const previousPeriodStart = new Date(previousPeriodEnd)
|
|
145
|
-
previousPeriodStart.setDate(previousPeriodEnd.getDate() - _daysRange)
|
|
146
|
-
|
|
147
|
-
// Get orders for current period
|
|
148
|
-
const currentPeriodOrders = await db
|
|
149
|
-
.selectFrom('orders')
|
|
150
|
-
.select(((eb: StacksExpressionBuilder) => eb.fn.count('id').as('count')) as any)
|
|
151
|
-
.where('created_at', '>=', currentPeriodStart.toISOString())
|
|
152
|
-
.where('created_at', '<=', today.toISOString())
|
|
153
|
-
.executeTakeFirst() as { count: number } | undefined
|
|
154
|
-
|
|
155
|
-
// Get orders for previous period
|
|
156
|
-
const previousPeriodOrders = await db
|
|
157
|
-
.selectFrom('orders')
|
|
158
|
-
.select(((eb: StacksExpressionBuilder) => eb.fn.count('id').as('count')) as any)
|
|
159
|
-
.where('created_at', '>=', previousPeriodStart.toISOString())
|
|
160
|
-
.where('created_at', '<=', previousPeriodEnd.toISOString())
|
|
161
|
-
.executeTakeFirst() as { count: number } | undefined
|
|
162
|
-
|
|
163
|
-
const currentCount = Number(currentPeriodOrders?.count || 0)
|
|
164
|
-
const previousCount = Number(previousPeriodOrders?.count || 0)
|
|
165
|
-
const difference = currentCount - previousCount
|
|
166
|
-
|
|
167
|
-
// Calculate percentage change, handling division by zero
|
|
168
|
-
const percentageChange = previousCount !== 0
|
|
169
|
-
? (difference / previousCount) * 100
|
|
170
|
-
: (currentCount > 0 ? 100 : 0)
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
current_period: currentCount,
|
|
174
|
-
previous_period: previousCount,
|
|
175
|
-
difference,
|
|
176
|
-
percentage_change: percentageChange,
|
|
177
|
-
days_range: _daysRange,
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Calculate order values and metrics for different time periods
|
|
183
|
-
* @param daysRange Number of days to look back (7, 30, 60, etc.)
|
|
184
|
-
*/
|
|
185
|
-
export async function calculateOrderMetrics(_daysRange: number = 30): Promise<{
|
|
186
|
-
current_period: {
|
|
187
|
-
total_orders: number
|
|
188
|
-
total_revenue: number
|
|
189
|
-
average_order_value: number
|
|
190
|
-
orders_by_status: { status: string, count: number }[]
|
|
191
|
-
orders_by_type: { order_type: string, count: number }[]
|
|
192
|
-
}
|
|
193
|
-
previous_period: {
|
|
194
|
-
total_orders: number
|
|
195
|
-
total_revenue: number
|
|
196
|
-
average_order_value: number
|
|
197
|
-
}
|
|
198
|
-
comparison: {
|
|
199
|
-
orders: {
|
|
200
|
-
difference: number
|
|
201
|
-
percentage: number
|
|
202
|
-
is_increase: boolean
|
|
203
|
-
}
|
|
204
|
-
revenue: {
|
|
205
|
-
difference: number
|
|
206
|
-
percentage: number
|
|
207
|
-
is_increase: boolean
|
|
208
|
-
}
|
|
209
|
-
average_order_value: {
|
|
210
|
-
difference: number
|
|
211
|
-
percentage: number
|
|
212
|
-
is_increase: boolean
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
days_range: number
|
|
216
|
-
}> {
|
|
217
|
-
const today = new Date()
|
|
218
|
-
|
|
219
|
-
// Current period (last N days)
|
|
220
|
-
const currentPeriodStart = new Date(today)
|
|
221
|
-
currentPeriodStart.setDate(today.getDate() - _daysRange)
|
|
222
|
-
|
|
223
|
-
// Previous period (N days before the current period)
|
|
224
|
-
const previousPeriodEnd = new Date(currentPeriodStart)
|
|
225
|
-
previousPeriodEnd.setDate(previousPeriodEnd.getDate() - 1)
|
|
226
|
-
|
|
227
|
-
const previousPeriodStart = new Date(previousPeriodEnd)
|
|
228
|
-
previousPeriodStart.setDate(previousPeriodEnd.getDate() - _daysRange)
|
|
229
|
-
|
|
230
|
-
// Get values for current period
|
|
231
|
-
const currentPeriodValues = await db
|
|
232
|
-
.selectFrom('orders')
|
|
233
|
-
.select(((eb: StacksExpressionBuilder) => [
|
|
234
|
-
eb.fn.count('id').as('total_orders'),
|
|
235
|
-
eb.fn.sum('total_amount').as('total_revenue'),
|
|
236
|
-
]) as any)
|
|
237
|
-
.where('created_at', '>=', currentPeriodStart.toISOString())
|
|
238
|
-
.where('created_at', '<=', today.toISOString())
|
|
239
|
-
.executeTakeFirst() as { total_orders: number, total_revenue: number } | undefined
|
|
240
|
-
|
|
241
|
-
// Get values for previous period
|
|
242
|
-
const previousPeriodValues = await db
|
|
243
|
-
.selectFrom('orders')
|
|
244
|
-
.select(((eb: StacksExpressionBuilder) => [
|
|
245
|
-
eb.fn.count('id').as('total_orders'),
|
|
246
|
-
eb.fn.sum('total_amount').as('total_revenue'),
|
|
247
|
-
]) as any)
|
|
248
|
-
.where('created_at', '>=', previousPeriodStart.toISOString())
|
|
249
|
-
.where('created_at', '<=', previousPeriodEnd.toISOString())
|
|
250
|
-
.executeTakeFirst() as { total_orders: number, total_revenue: number } | undefined
|
|
251
|
-
|
|
252
|
-
// Get orders by status for current period
|
|
253
|
-
const ordersByStatus = await db
|
|
254
|
-
.selectFrom('orders')
|
|
255
|
-
.select(['status', (eb: StacksExpressionBuilder) => eb.fn.count('id').as('count')] as any)
|
|
256
|
-
.where('created_at', '>=', currentPeriodStart.toISOString())
|
|
257
|
-
.where('created_at', '<=', today.toISOString())
|
|
258
|
-
.groupBy('status')
|
|
259
|
-
.execute() as { status: string, count: number }[]
|
|
260
|
-
|
|
261
|
-
// Get orders by type for current period
|
|
262
|
-
const ordersByType = await db
|
|
263
|
-
.selectFrom('orders')
|
|
264
|
-
.select(['order_type', (eb: StacksExpressionBuilder) => eb.fn.count('id').as('count')] as any)
|
|
265
|
-
.where('created_at', '>=', currentPeriodStart.toISOString())
|
|
266
|
-
.where('created_at', '<=', today.toISOString())
|
|
267
|
-
.groupBy('order_type')
|
|
268
|
-
.execute() as { order_type: string, count: number }[]
|
|
269
|
-
|
|
270
|
-
// Calculate values for current period
|
|
271
|
-
const currentTotalOrders = Number(currentPeriodValues?.total_orders || 0)
|
|
272
|
-
const currentTotalRevenue = Number(currentPeriodValues?.total_revenue || 0)
|
|
273
|
-
const currentAverageOrderValue = currentTotalOrders > 0
|
|
274
|
-
? currentTotalRevenue / currentTotalOrders
|
|
275
|
-
: 0
|
|
276
|
-
|
|
277
|
-
// Calculate values for previous period
|
|
278
|
-
const previousTotalOrders = Number(previousPeriodValues?.total_orders || 0)
|
|
279
|
-
const previousTotalRevenue = Number(previousPeriodValues?.total_revenue || 0)
|
|
280
|
-
const previousAverageOrderValue = previousTotalOrders > 0
|
|
281
|
-
? previousTotalRevenue / previousTotalOrders
|
|
282
|
-
: 0
|
|
283
|
-
|
|
284
|
-
// Calculate differences
|
|
285
|
-
const ordersDifference = currentTotalOrders - previousTotalOrders
|
|
286
|
-
const revenueDifference = currentTotalRevenue - previousTotalRevenue
|
|
287
|
-
const aovDifference = currentAverageOrderValue - previousAverageOrderValue
|
|
288
|
-
|
|
289
|
-
// Calculate percentage changes
|
|
290
|
-
const ordersPercentageChange = previousTotalOrders !== 0
|
|
291
|
-
? (ordersDifference / previousTotalOrders) * 100
|
|
292
|
-
: (currentTotalOrders > 0 ? 100 : 0)
|
|
293
|
-
|
|
294
|
-
const revenuePercentageChange = previousTotalRevenue !== 0
|
|
295
|
-
? (revenueDifference / previousTotalRevenue) * 100
|
|
296
|
-
: (currentTotalRevenue > 0 ? 100 : 0)
|
|
297
|
-
|
|
298
|
-
const aovPercentageChange = previousAverageOrderValue !== 0
|
|
299
|
-
? (aovDifference / previousAverageOrderValue) * 100
|
|
300
|
-
: (currentAverageOrderValue > 0 ? 100 : 0)
|
|
301
|
-
|
|
302
|
-
return {
|
|
303
|
-
current_period: {
|
|
304
|
-
total_orders: currentTotalOrders,
|
|
305
|
-
total_revenue: currentTotalRevenue,
|
|
306
|
-
average_order_value: currentAverageOrderValue,
|
|
307
|
-
orders_by_status: ordersByStatus.map((item: any) => ({
|
|
308
|
-
status: item.status,
|
|
309
|
-
count: Number(item.count),
|
|
310
|
-
})),
|
|
311
|
-
orders_by_type: ordersByType.map((item: any) => ({
|
|
312
|
-
order_type: item.order_type,
|
|
313
|
-
count: Number(item.count),
|
|
314
|
-
})),
|
|
315
|
-
},
|
|
316
|
-
previous_period: {
|
|
317
|
-
total_orders: previousTotalOrders,
|
|
318
|
-
total_revenue: previousTotalRevenue,
|
|
319
|
-
average_order_value: previousAverageOrderValue,
|
|
320
|
-
},
|
|
321
|
-
comparison: {
|
|
322
|
-
orders: {
|
|
323
|
-
difference: ordersDifference,
|
|
324
|
-
percentage: Math.abs(ordersPercentageChange),
|
|
325
|
-
is_increase: ordersDifference >= 0,
|
|
326
|
-
},
|
|
327
|
-
revenue: {
|
|
328
|
-
difference: revenueDifference,
|
|
329
|
-
percentage: Math.abs(revenuePercentageChange),
|
|
330
|
-
is_increase: revenueDifference >= 0,
|
|
331
|
-
},
|
|
332
|
-
average_order_value: {
|
|
333
|
-
difference: aovDifference,
|
|
334
|
-
percentage: Math.abs(aovPercentageChange),
|
|
335
|
-
is_increase: aovDifference >= 0,
|
|
336
|
-
},
|
|
337
|
-
},
|
|
338
|
-
days_range: _daysRange,
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Get daily order counts for a time period
|
|
344
|
-
* @param daysRange Number of days to look back
|
|
345
|
-
*/
|
|
346
|
-
export async function fetchDailyOrderTrends(_daysRange: number = 30): Promise<{
|
|
347
|
-
date: string
|
|
348
|
-
order_count: number
|
|
349
|
-
revenue: number
|
|
350
|
-
}[]> {
|
|
351
|
-
const today = new Date()
|
|
352
|
-
const startDate = new Date(today)
|
|
353
|
-
startDate.setDate(today.getDate() - _daysRange)
|
|
354
|
-
|
|
355
|
-
// Query all orders in the range, then aggregate in JS by date
|
|
356
|
-
const orders = await db
|
|
357
|
-
.selectFrom('orders')
|
|
358
|
-
.select(['created_at', 'total_amount'] as any)
|
|
359
|
-
.where('created_at', '>=', startDate.toISOString())
|
|
360
|
-
.where('created_at', '<=', today.toISOString())
|
|
361
|
-
.orderBy('created_at', 'asc')
|
|
362
|
-
.execute() as { created_at: string, total_amount: number }[]
|
|
363
|
-
|
|
364
|
-
// Group by date (YYYY-MM-DD)
|
|
365
|
-
const dailyMap = new Map<string, { order_count: number, revenue: number }>()
|
|
366
|
-
for (const order of orders) {
|
|
367
|
-
const date = (order.created_at || '').split('T')[0] || 'unknown'
|
|
368
|
-
const entry = dailyMap.get(date) || { order_count: 0, revenue: 0 }
|
|
369
|
-
entry.order_count++
|
|
370
|
-
entry.revenue += Number(order.total_amount || 0)
|
|
371
|
-
dailyMap.set(date, entry)
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
return Array.from(dailyMap.entries()).map(([date, stats]) => ({
|
|
375
|
-
date,
|
|
376
|
-
order_count: stats.order_count,
|
|
377
|
-
revenue: stats.revenue,
|
|
378
|
-
}))
|
|
379
|
-
}
|
package/src/orders/guards.ts
DELETED
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Order / cart policy guards (stacksjs/stacks#1879 Co-7, Co-14, Co-15).
|
|
3
|
-
*
|
|
4
|
-
* Three small helpers that callers pre-flight against the cart
|
|
5
|
-
* before handing off to `placeOrder`. Kept together because they
|
|
6
|
-
* all answer the same question: "should this checkout be
|
|
7
|
-
* allowed to proceed?"
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { StacksExpressionBuilder } from '@stacksjs/database'
|
|
11
|
-
import { db } from '@stacksjs/database'
|
|
12
|
-
|
|
13
|
-
// ============================================================================
|
|
14
|
-
// Co-7: coupon stacking guard
|
|
15
|
-
// ============================================================================
|
|
16
|
-
|
|
17
|
-
export interface CouponStackingPolicy {
|
|
18
|
-
/**
|
|
19
|
-
* Allow multiple coupons on the same order. Default is `false`
|
|
20
|
-
* (single-coupon-only). Apps that intentionally stack coupons
|
|
21
|
-
* (e.g. one fixed-amount + one percentage) opt in explicitly.
|
|
22
|
-
*/
|
|
23
|
-
allowMultiple?: boolean
|
|
24
|
-
/**
|
|
25
|
-
* When `allowMultiple: true`, cap the number of stacked coupons.
|
|
26
|
-
* Default unlimited (no cap). Useful for "up to 2 coupons" or
|
|
27
|
-
* "BOGO + free-shipping" style rules.
|
|
28
|
-
*/
|
|
29
|
-
maxStacked?: number
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface CouponStackingResult {
|
|
33
|
-
ok: boolean
|
|
34
|
-
/** Number of coupon ids the caller submitted. */
|
|
35
|
-
count: number
|
|
36
|
-
/** Set when the policy rejected the combination. */
|
|
37
|
-
reason?: 'multiple-not-allowed' | 'exceeds-max-stacked'
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Validate that a set of coupon ids satisfies the stacking policy
|
|
42
|
-
* (stacksjs/stacks#1879 Co-7). Pre-fix there was no enforcement
|
|
43
|
-
* anywhere — frontend could submit two coupons and double-discount
|
|
44
|
-
* the order with no signal. Default policy is single-coupon-only;
|
|
45
|
-
* apps that intentionally support stacking opt in.
|
|
46
|
-
*
|
|
47
|
-
* @example
|
|
48
|
-
* ```ts
|
|
49
|
-
* const stack = validateCouponStacking(cart.couponIds)
|
|
50
|
-
* if (!stack.ok)
|
|
51
|
-
* throw new HttpError(400, `Coupon stacking violation: ${stack.reason}`)
|
|
52
|
-
* ```
|
|
53
|
-
*/
|
|
54
|
-
export function validateCouponStacking(
|
|
55
|
-
couponIds: ReadonlyArray<number>,
|
|
56
|
-
policy: CouponStackingPolicy = {},
|
|
57
|
-
): CouponStackingResult {
|
|
58
|
-
const count = couponIds.length
|
|
59
|
-
if (count <= 1) return { ok: true, count }
|
|
60
|
-
if (!policy.allowMultiple) {
|
|
61
|
-
return { ok: false, count, reason: 'multiple-not-allowed' }
|
|
62
|
-
}
|
|
63
|
-
const cap = policy.maxStacked ?? Number.POSITIVE_INFINITY
|
|
64
|
-
if (count > cap) {
|
|
65
|
-
return { ok: false, count, reason: 'exceeds-max-stacked' }
|
|
66
|
-
}
|
|
67
|
-
return { ok: true, count }
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ============================================================================
|
|
71
|
-
// Co-14: min/max order quantity bounds
|
|
72
|
-
// ============================================================================
|
|
73
|
-
|
|
74
|
-
export interface QuantityBoundsResult {
|
|
75
|
-
ok: boolean
|
|
76
|
-
productId: number
|
|
77
|
-
quantity: number
|
|
78
|
-
/** Set when the quantity violated a bound. */
|
|
79
|
-
reason?: 'below-min' | 'above-max' | 'non-positive' | 'product-missing'
|
|
80
|
-
/** The bound that was violated (when applicable). */
|
|
81
|
-
bound?: number
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Validate that a cart line-item's quantity respects the product's
|
|
86
|
-
* `min_order_qty` / `max_order_qty` fields
|
|
87
|
-
* (stacksjs/stacks#1879 Co-14). Looks up the product server-side
|
|
88
|
-
* so the client can't bypass by lying about the bounds.
|
|
89
|
-
*
|
|
90
|
-
* Products without explicit bounds default to "any positive int."
|
|
91
|
-
* A quantity of 0 or negative always fails — the cart's UI is
|
|
92
|
-
* responsible for letting users remove items, not for passing
|
|
93
|
-
* sentinel zeros through the API.
|
|
94
|
-
*/
|
|
95
|
-
export async function validateQuantityBounds(
|
|
96
|
-
productId: number,
|
|
97
|
-
quantity: number,
|
|
98
|
-
): Promise<QuantityBoundsResult> {
|
|
99
|
-
if (!Number.isFinite(quantity) || quantity <= 0)
|
|
100
|
-
return { ok: false, productId, quantity, reason: 'non-positive' }
|
|
101
|
-
|
|
102
|
-
let product: { min_order_qty?: number | null, max_order_qty?: number | null } | undefined
|
|
103
|
-
try {
|
|
104
|
-
product = await (db as any)
|
|
105
|
-
.selectFrom('products')
|
|
106
|
-
.where('id', '=', productId)
|
|
107
|
-
.select(['min_order_qty', 'max_order_qty'])
|
|
108
|
-
.executeTakeFirst()
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
// Schema doesn't have the columns yet — degrade to "any positive
|
|
112
|
-
// quantity is fine," matching the pre-fix behavior. Apps that
|
|
113
|
-
// need bounds enforcement add the columns first.
|
|
114
|
-
return { ok: true, productId, quantity }
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (!product)
|
|
118
|
-
return { ok: false, productId, quantity, reason: 'product-missing' }
|
|
119
|
-
|
|
120
|
-
const min = product.min_order_qty ?? null
|
|
121
|
-
const max = product.max_order_qty ?? null
|
|
122
|
-
|
|
123
|
-
if (min != null && quantity < min)
|
|
124
|
-
return { ok: false, productId, quantity, reason: 'below-min', bound: min }
|
|
125
|
-
if (max != null && quantity > max)
|
|
126
|
-
return { ok: false, productId, quantity, reason: 'above-max', bound: max }
|
|
127
|
-
|
|
128
|
-
return { ok: true, productId, quantity }
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Batch variant — short-circuits on the first violation so the
|
|
133
|
-
* caller can surface a single useful "item X has min qty Y"
|
|
134
|
-
* error rather than a list of every failed line.
|
|
135
|
-
*/
|
|
136
|
-
export async function validateCartQuantities(
|
|
137
|
-
items: ReadonlyArray<{ productId: number, quantity: number }>,
|
|
138
|
-
): Promise<QuantityBoundsResult> {
|
|
139
|
-
for (const item of items) {
|
|
140
|
-
const r = await validateQuantityBounds(item.productId, item.quantity)
|
|
141
|
-
if (!r.ok) return r
|
|
142
|
-
}
|
|
143
|
-
// ok-path uses the last item's id as a placeholder; callers
|
|
144
|
-
// only branch on `.ok`.
|
|
145
|
-
const last = items[items.length - 1] ?? { productId: 0, quantity: 0 }
|
|
146
|
-
return { ok: true, productId: last.productId, quantity: last.quantity }
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// ============================================================================
|
|
150
|
-
// Co-15: abandoned cart cleanup
|
|
151
|
-
// ============================================================================
|
|
152
|
-
|
|
153
|
-
export interface CleanupAbandonedCartsOptions {
|
|
154
|
-
/**
|
|
155
|
-
* Delete carts whose `updated_at` is older than this many days.
|
|
156
|
-
* Default 30. Apps with rolling-window cart-recovery emails set
|
|
157
|
-
* higher (e.g. 90).
|
|
158
|
-
*/
|
|
159
|
-
olderThanDays?: number
|
|
160
|
-
/**
|
|
161
|
-
* Maximum rows to delete in a single call. Default 1000 — keeps
|
|
162
|
-
* the delete from holding a long write lock on busy databases.
|
|
163
|
-
* Apps call repeatedly until `deleted < limit`.
|
|
164
|
-
*/
|
|
165
|
-
limit?: number
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export interface CleanupAbandonedCartsResult {
|
|
169
|
-
/** Number of cart rows deleted in this call. */
|
|
170
|
-
deleted: number
|
|
171
|
-
/** Cutoff timestamp used for the delete (ISO 8601). */
|
|
172
|
-
cutoffAt: string
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Delete carts that have been untouched for more than
|
|
177
|
-
* `olderThanDays` days (stacksjs/stacks#1879 Co-15). Default 30.
|
|
178
|
-
* Capped at `limit` rows per call (default 1000) so the delete
|
|
179
|
-
* doesn't hold a long write lock on a busy database. Returns the
|
|
180
|
-
* count + cutoff so callers can log or loop.
|
|
181
|
-
*
|
|
182
|
-
* Schedule via `@stacksjs/scheduler` (now fully wired since
|
|
183
|
-
* #1877's scheduler audit closed) to run nightly:
|
|
184
|
-
*
|
|
185
|
-
* @example
|
|
186
|
-
* ```ts
|
|
187
|
-
* // app/Scheduler.ts
|
|
188
|
-
* import { cleanupAbandonedCarts } from '@stacksjs/commerce'
|
|
189
|
-
*
|
|
190
|
-
* schedule.job(async () => {
|
|
191
|
-
* let total = 0
|
|
192
|
-
* while (true) {
|
|
193
|
-
* const r = await cleanupAbandonedCarts({ olderThanDays: 30, limit: 1000 })
|
|
194
|
-
* total += r.deleted
|
|
195
|
-
* if (r.deleted < 1000) break
|
|
196
|
-
* }
|
|
197
|
-
* log.info(`Cleaned up ${total} abandoned carts`)
|
|
198
|
-
* }).daily().setTimeZone('UTC').withName('CleanupAbandonedCarts')
|
|
199
|
-
* ```
|
|
200
|
-
*/
|
|
201
|
-
export async function cleanupAbandonedCarts(
|
|
202
|
-
options: CleanupAbandonedCartsOptions = {},
|
|
203
|
-
): Promise<CleanupAbandonedCartsResult> {
|
|
204
|
-
const olderThanDays = options.olderThanDays ?? 30
|
|
205
|
-
const limit = options.limit ?? 1000
|
|
206
|
-
|
|
207
|
-
const cutoff = new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000)
|
|
208
|
-
const cutoffAt = cutoff.toISOString().slice(0, 19).replace('T', ' ')
|
|
209
|
-
|
|
210
|
-
// The pattern below uses a sub-select to apply the LIMIT because
|
|
211
|
-
// most SQL drivers don't accept LIMIT on DELETE directly. SQLite
|
|
212
|
-
// and MySQL both accept the DELETE-from-subselect form; PG needs
|
|
213
|
-
// a CTE-style alternative. Fall back via try/catch so the helper
|
|
214
|
-
// works across drivers without driver-detect plumbing.
|
|
215
|
-
try {
|
|
216
|
-
const result: any = await (db as any)
|
|
217
|
-
.deleteFrom('carts')
|
|
218
|
-
.where('updated_at', '<', cutoffAt)
|
|
219
|
-
.where('id', 'in', (eb: StacksExpressionBuilder) =>
|
|
220
|
-
eb.selectFrom('carts')
|
|
221
|
-
.select('id')
|
|
222
|
-
.where('updated_at', '<', cutoffAt)
|
|
223
|
-
.limit(limit),
|
|
224
|
-
)
|
|
225
|
-
.execute()
|
|
226
|
-
const deleted = Number(
|
|
227
|
-
result?.numDeletedRows
|
|
228
|
-
?? result?.[0]?.numDeletedRows
|
|
229
|
-
?? result?.affectedRows
|
|
230
|
-
?? 0,
|
|
231
|
-
)
|
|
232
|
-
return { deleted, cutoffAt }
|
|
233
|
-
}
|
|
234
|
-
catch {
|
|
235
|
-
// Fallback: select-then-delete by id. Slower but driver-portable.
|
|
236
|
-
try {
|
|
237
|
-
const rows = await (db as any)
|
|
238
|
-
.selectFrom('carts')
|
|
239
|
-
.where('updated_at', '<', cutoffAt)
|
|
240
|
-
.select(['id'])
|
|
241
|
-
.limit(limit)
|
|
242
|
-
.execute() as Array<{ id: number }>
|
|
243
|
-
if (rows.length === 0) return { deleted: 0, cutoffAt }
|
|
244
|
-
const ids = rows.map(r => r.id)
|
|
245
|
-
const result: any = await (db as any)
|
|
246
|
-
.deleteFrom('carts')
|
|
247
|
-
.where('id', 'in', ids)
|
|
248
|
-
.execute()
|
|
249
|
-
const deleted = Number(
|
|
250
|
-
result?.numDeletedRows
|
|
251
|
-
?? result?.[0]?.numDeletedRows
|
|
252
|
-
?? result?.affectedRows
|
|
253
|
-
?? rows.length,
|
|
254
|
-
)
|
|
255
|
-
return { deleted, cutoffAt }
|
|
256
|
-
}
|
|
257
|
-
catch {
|
|
258
|
-
return { deleted: 0, cutoffAt }
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|