@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,123 @@
|
|
|
1
|
+
type ShippingMethodJsonResponse = ModelRow<typeof ShippingMethod>
|
|
2
|
+
type NewShippingMethod = NewModelData<typeof ShippingMethod>
|
|
3
|
+
import { randomUUIDv7 } from 'bun'
|
|
4
|
+
import { db } from '@stacksjs/database'
|
|
5
|
+
import { fetchById } from './fetch'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a new shipping method
|
|
9
|
+
*
|
|
10
|
+
* @param data The shipping method data to store
|
|
11
|
+
* @returns The newly created shipping method record
|
|
12
|
+
*/
|
|
13
|
+
export async function store(data: NewShippingMethod): Promise<ShippingMethodJsonResponse> {
|
|
14
|
+
try {
|
|
15
|
+
const d = data as Record<string, unknown>
|
|
16
|
+
const shippingData = {
|
|
17
|
+
name: data.name,
|
|
18
|
+
description: data.description,
|
|
19
|
+
base_rate: d.base_rate,
|
|
20
|
+
free_shipping: d.free_shipping,
|
|
21
|
+
status: data.status,
|
|
22
|
+
uuid: randomUUIDv7(),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const result = await db
|
|
26
|
+
.insertInto('shipping_methods')
|
|
27
|
+
.values(shippingData)
|
|
28
|
+
.executeTakeFirst()
|
|
29
|
+
|
|
30
|
+
if (!result)
|
|
31
|
+
throw new Error('Failed to create shipping method')
|
|
32
|
+
|
|
33
|
+
const insertId = Number(result.insertId) || Number(result.numInsertedOrUpdatedRows)
|
|
34
|
+
|
|
35
|
+
const model = await fetchById(insertId)
|
|
36
|
+
|
|
37
|
+
return model as ShippingMethodJsonResponse
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof Error) {
|
|
41
|
+
throw new TypeError(`Failed to create shipping method: ${error.message}`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
throw error
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create multiple shipping methods at once
|
|
50
|
+
*
|
|
51
|
+
* @param data Array of shipping method data to store
|
|
52
|
+
* @returns Number of shipping methods created
|
|
53
|
+
*/
|
|
54
|
+
export async function bulkStore(data: NewShippingMethod[]): Promise<number> {
|
|
55
|
+
if (!data.length)
|
|
56
|
+
return 0
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const shippingDataArray = data.map(item => ({
|
|
60
|
+
...item,
|
|
61
|
+
uuid: randomUUIDv7(),
|
|
62
|
+
}))
|
|
63
|
+
|
|
64
|
+
const result = await db
|
|
65
|
+
.insertInto('shipping_methods')
|
|
66
|
+
.values(shippingDataArray)
|
|
67
|
+
.executeTakeFirst()
|
|
68
|
+
|
|
69
|
+
return Number(result.numInsertedOrUpdatedRows)
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error instanceof Error) {
|
|
73
|
+
throw new TypeError(`Failed to create shipping methods in bulk: ${error.message}`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
throw error
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Format shipping method options for dropdown menus or selectors
|
|
82
|
+
*
|
|
83
|
+
* @returns Array of formatted shipping method options with id, name, and status
|
|
84
|
+
*/
|
|
85
|
+
export function formatShippingOptions(): Promise<{ id: number, name: string, status: string | string[], base_rate: number }[]> {
|
|
86
|
+
try {
|
|
87
|
+
return db
|
|
88
|
+
.selectFrom('shipping_methods')
|
|
89
|
+
.select(['id', 'name', 'status', 'base_rate'])
|
|
90
|
+
.orderBy('name')
|
|
91
|
+
.execute() as any
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error instanceof Error) {
|
|
95
|
+
throw new TypeError(`Failed to format shipping options: ${error.message}`)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
throw error
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get active shipping methods
|
|
104
|
+
*
|
|
105
|
+
* @returns List of active shipping methods
|
|
106
|
+
*/
|
|
107
|
+
export async function getActiveShippingMethods(): Promise<ShippingMethodJsonResponse[]> {
|
|
108
|
+
try {
|
|
109
|
+
return await db
|
|
110
|
+
.selectFrom('shipping_methods')
|
|
111
|
+
.selectAll()
|
|
112
|
+
.where('status', '=', 'active')
|
|
113
|
+
.orderBy('name')
|
|
114
|
+
.execute() as ShippingMethodJsonResponse[]
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (error instanceof Error) {
|
|
118
|
+
throw new TypeError(`Failed to get active shipping methods: ${error.message}`)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
throw error
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { db } from '@stacksjs/database'
|
|
2
|
+
import { formatDate } from '@stacksjs/orm'
|
|
3
|
+
type ShippingMethodJsonResponse = ModelRow<typeof ShippingMethod>
|
|
4
|
+
type ShippingMethodUpdate = UpdateModelData<typeof ShippingMethod>
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Update a shipping method
|
|
8
|
+
*
|
|
9
|
+
* @param id The ID of the shipping method
|
|
10
|
+
* @param data The shipping method data to update
|
|
11
|
+
* @returns The updated shipping method record
|
|
12
|
+
*/
|
|
13
|
+
export async function update(id: number, data: ShippingMethodUpdate): Promise<ShippingMethodJsonResponse> {
|
|
14
|
+
try {
|
|
15
|
+
if (!id)
|
|
16
|
+
throw new Error('Shipping method ID is required for update')
|
|
17
|
+
|
|
18
|
+
const result = await db
|
|
19
|
+
.updateTable('shipping_methods')
|
|
20
|
+
.set({
|
|
21
|
+
...data,
|
|
22
|
+
updated_at: formatDate(new Date()),
|
|
23
|
+
})
|
|
24
|
+
.where('id', '=', id)
|
|
25
|
+
.returningAll()
|
|
26
|
+
.executeTakeFirst()
|
|
27
|
+
|
|
28
|
+
if (!result)
|
|
29
|
+
throw new Error('Failed to update shipping method')
|
|
30
|
+
|
|
31
|
+
return result as ShippingMethodJsonResponse
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error instanceof Error) {
|
|
35
|
+
throw new TypeError(`Failed to update shipping method: ${error.message}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
throw error
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Update a shipping method's status
|
|
44
|
+
*
|
|
45
|
+
* @param id The ID of the shipping method
|
|
46
|
+
* @param status The new status
|
|
47
|
+
* @returns The updated shipping method with the new status
|
|
48
|
+
*/
|
|
49
|
+
export async function updateStatus(
|
|
50
|
+
id: number,
|
|
51
|
+
status: string | string[],
|
|
52
|
+
): Promise<ShippingMethodJsonResponse> {
|
|
53
|
+
try {
|
|
54
|
+
const result = await db
|
|
55
|
+
.updateTable('shipping_methods')
|
|
56
|
+
.set({
|
|
57
|
+
status,
|
|
58
|
+
updated_at: formatDate(new Date()),
|
|
59
|
+
})
|
|
60
|
+
.where('id', '=', id)
|
|
61
|
+
.returningAll()
|
|
62
|
+
.executeTakeFirst()
|
|
63
|
+
|
|
64
|
+
if (!result)
|
|
65
|
+
throw new Error('Failed to update shipping method status')
|
|
66
|
+
|
|
67
|
+
return result as ShippingMethodJsonResponse
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (error instanceof Error) {
|
|
71
|
+
throw new TypeError(`Failed to update shipping method status: ${error.message}`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
throw error
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Update pricing information for a shipping method
|
|
80
|
+
*
|
|
81
|
+
* @param id The ID of the shipping method
|
|
82
|
+
* @param base_rate Optional updated base rate
|
|
83
|
+
* @param free_shipping Optional updated free shipping threshold
|
|
84
|
+
* @returns The updated shipping method
|
|
85
|
+
*/
|
|
86
|
+
export async function updatePricing(
|
|
87
|
+
id: number,
|
|
88
|
+
base_rate?: number,
|
|
89
|
+
free_shipping?: number,
|
|
90
|
+
): Promise<ShippingMethodJsonResponse> {
|
|
91
|
+
try {
|
|
92
|
+
const updateData: Record<string, any> = {
|
|
93
|
+
updated_at: formatDate(new Date()),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (base_rate !== undefined)
|
|
97
|
+
updateData.base_rate = base_rate
|
|
98
|
+
if (free_shipping !== undefined)
|
|
99
|
+
updateData.free_shipping = free_shipping
|
|
100
|
+
|
|
101
|
+
const result = await db
|
|
102
|
+
.updateTable('shipping_methods')
|
|
103
|
+
.set(updateData)
|
|
104
|
+
.where('id', '=', id)
|
|
105
|
+
.returningAll()
|
|
106
|
+
.executeTakeFirst()
|
|
107
|
+
|
|
108
|
+
if (!result)
|
|
109
|
+
throw new Error('Failed to update pricing information')
|
|
110
|
+
|
|
111
|
+
return result as ShippingMethodJsonResponse
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error instanceof Error) {
|
|
115
|
+
throw new TypeError(`Failed to update pricing information: ${error.message}`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
throw error
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { db } from '@stacksjs/database'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Delete a shipping rate by ID
|
|
5
|
+
*
|
|
6
|
+
* @param id The ID of the shipping rate to delete
|
|
7
|
+
* @returns True if the shipping rate was deleted, false otherwise
|
|
8
|
+
*/
|
|
9
|
+
export async function destroy(id: number): Promise<boolean> {
|
|
10
|
+
try {
|
|
11
|
+
const result = await db
|
|
12
|
+
.deleteFrom('shipping_rates')
|
|
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 shipping rate: ${error.message}`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
throw error
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Delete multiple shipping rates by ID
|
|
29
|
+
*
|
|
30
|
+
* @param ids Array of shipping rate IDs to delete
|
|
31
|
+
* @returns Number of shipping rates 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('shipping_rates')
|
|
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 shipping rates: ${error.message}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
throw error
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Delete shipping rates by zone
|
|
58
|
+
*
|
|
59
|
+
* @param zone The zone to delete shipping rates for
|
|
60
|
+
* @returns Number of shipping rates deleted
|
|
61
|
+
*/
|
|
62
|
+
export async function destroyByZone(zone: string): Promise<number> {
|
|
63
|
+
try {
|
|
64
|
+
const result = await db
|
|
65
|
+
.deleteFrom('shipping_rates')
|
|
66
|
+
.where('zone', '=', zone)
|
|
67
|
+
.executeTakeFirst()
|
|
68
|
+
|
|
69
|
+
return Number(result.numDeletedRows)
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error instanceof Error) {
|
|
73
|
+
throw new TypeError(`Failed to delete shipping rates by zone: ${error.message}`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
throw error
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Delete shipping rates by method
|
|
82
|
+
*
|
|
83
|
+
* @param method The shipping method to delete rates for
|
|
84
|
+
* @returns Number of shipping rates deleted
|
|
85
|
+
*/
|
|
86
|
+
export async function destroyByMethod(method: string): Promise<number> {
|
|
87
|
+
try {
|
|
88
|
+
const result = await db
|
|
89
|
+
.deleteFrom('shipping_rates')
|
|
90
|
+
.where('method', '=', method)
|
|
91
|
+
.executeTakeFirst()
|
|
92
|
+
|
|
93
|
+
return Number(result.numDeletedRows)
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error instanceof Error) {
|
|
97
|
+
throw new TypeError(`Failed to delete shipping rates by method: ${error.message}`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
throw error
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
type ShippingRateJsonResponse = ModelRow<typeof ShippingRate>
|
|
2
|
+
import { db } from '@stacksjs/database'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fetch a shipping rate by ID
|
|
6
|
+
*/
|
|
7
|
+
export async function fetchById(id: number): Promise<ShippingRateJsonResponse | undefined> {
|
|
8
|
+
const model = await db
|
|
9
|
+
.selectFrom('shipping_rates')
|
|
10
|
+
.where('id', '=', id)
|
|
11
|
+
.selectAll()
|
|
12
|
+
.executeTakeFirst()
|
|
13
|
+
|
|
14
|
+
if (model) {
|
|
15
|
+
let shippingZone = null
|
|
16
|
+
let shippingMethod = null
|
|
17
|
+
|
|
18
|
+
if (model.shipping_zone_id) {
|
|
19
|
+
shippingZone = await db
|
|
20
|
+
.selectFrom('shipping_zones')
|
|
21
|
+
.where('id', '=', model.shipping_zone_id)
|
|
22
|
+
.selectAll()
|
|
23
|
+
.executeTakeFirst()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (model.shipping_method_id) {
|
|
27
|
+
shippingMethod = await db
|
|
28
|
+
.selectFrom('shipping_methods')
|
|
29
|
+
.where('id', '=', model.shipping_method_id)
|
|
30
|
+
.selectAll()
|
|
31
|
+
.executeTakeFirst()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
...model,
|
|
36
|
+
shipping_zone: shippingZone,
|
|
37
|
+
shipping_method: shippingMethod,
|
|
38
|
+
} as unknown as ShippingRateJsonResponse
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Fetch all shipping rates with their shipping zones and methods
|
|
46
|
+
*/
|
|
47
|
+
export async function fetchAll(): Promise<ShippingRateJsonResponse[]> {
|
|
48
|
+
// Fetch all shipping rates
|
|
49
|
+
const models = await db.selectFrom('shipping_rates').selectAll().execute()
|
|
50
|
+
|
|
51
|
+
// Get the IDs of all shipping zones and methods
|
|
52
|
+
const shippingZoneIds = models.map((model: any) => model.shipping_zone_id).filter((id: any) => id !== null && id !== undefined)
|
|
53
|
+
const shippingMethodIds = models.map((model: any) => model.shipping_method_id).filter((id: any) => id !== null && id !== undefined)
|
|
54
|
+
|
|
55
|
+
let shippingZonesQuery = db.selectFrom('shipping_zones') as any
|
|
56
|
+
let shippingMethodsQuery = db.selectFrom('shipping_methods') as any
|
|
57
|
+
|
|
58
|
+
if (shippingZoneIds.length > 0) {
|
|
59
|
+
shippingZonesQuery = shippingZonesQuery.where('id', 'in', shippingZoneIds)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (shippingMethodIds.length > 0) {
|
|
63
|
+
shippingMethodsQuery = shippingMethodsQuery.where('id', 'in', shippingMethodIds)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Fetch shipping zones and methods for these specific IDs using WHERE IN
|
|
67
|
+
const allShippingZones = await shippingZonesQuery.selectAll().execute()
|
|
68
|
+
const allShippingMethods = await shippingMethodsQuery.selectAll().execute()
|
|
69
|
+
|
|
70
|
+
// Group shipping zones and methods by ID
|
|
71
|
+
const shippingZonesById = allShippingZones.reduce((acc: any, zone: any) => {
|
|
72
|
+
acc[zone.id] = zone
|
|
73
|
+
return acc
|
|
74
|
+
}, {} as Record<number, typeof allShippingZones[0]>)
|
|
75
|
+
|
|
76
|
+
const shippingMethodsById = allShippingMethods.reduce((acc: any, method: any) => {
|
|
77
|
+
acc[method.id] = method
|
|
78
|
+
return acc
|
|
79
|
+
}, {} as Record<number, typeof allShippingMethods[0]>)
|
|
80
|
+
|
|
81
|
+
// Attach shipping zones and methods to each shipping rate
|
|
82
|
+
return models.map((model: any) => ({
|
|
83
|
+
...model,
|
|
84
|
+
shipping_zone: model.shipping_zone_id ? shippingZonesById[model.shipping_zone_id] : [],
|
|
85
|
+
shipping_method: model.shipping_method_id ? shippingMethodsById[model.shipping_method_id] : [],
|
|
86
|
+
}))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get shipping rates by zone
|
|
91
|
+
*
|
|
92
|
+
* @param zoneId Shipping zone identifier
|
|
93
|
+
* @returns List of shipping rates for the specified zone
|
|
94
|
+
*/
|
|
95
|
+
export async function getRatesByZone(zoneId: number): Promise<ShippingRateJsonResponse[]> {
|
|
96
|
+
try {
|
|
97
|
+
const rates = await db
|
|
98
|
+
.selectFrom('shipping_rates')
|
|
99
|
+
.selectAll()
|
|
100
|
+
.where('shipping_zone_id', '=', zoneId)
|
|
101
|
+
.orderBy('weight_from')
|
|
102
|
+
.execute()
|
|
103
|
+
|
|
104
|
+
return rates as ShippingRateJsonResponse[]
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (error instanceof Error) {
|
|
108
|
+
throw new TypeError(`Failed to get shipping rates by zone: ${error.message}`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
throw error
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get shipping rate based on weight and zone.
|
|
117
|
+
*
|
|
118
|
+
* **⚠️ Caller-trusted zone (stacksjs/stacks#1879 Co-11).** This
|
|
119
|
+
* function does NOT verify that the `zoneId` actually serves the
|
|
120
|
+
* customer's declared address. A user in NYC can pass a Texas
|
|
121
|
+
* `zoneId` and get cheaper shipping. Use `getRateByWeightAndAddress`
|
|
122
|
+
* below when the request boundary involves untrusted input — it
|
|
123
|
+
* resolves the zone server-side from the address.
|
|
124
|
+
*
|
|
125
|
+
* Kept for back-compat with internal callers that have already
|
|
126
|
+
* resolved the zone (admin tooling, scheduled fulfillment jobs).
|
|
127
|
+
*
|
|
128
|
+
* @param weight Weight in the appropriate unit
|
|
129
|
+
* @param zoneId Shipping zone identifier (caller-trusted)
|
|
130
|
+
* @returns Matching shipping rate or undefined
|
|
131
|
+
*/
|
|
132
|
+
export async function getRateByWeightAndZone(weight: number, zoneId: number): Promise<ShippingRateJsonResponse | undefined> {
|
|
133
|
+
try {
|
|
134
|
+
const rate = await db
|
|
135
|
+
.selectFrom('shipping_rates')
|
|
136
|
+
.selectAll()
|
|
137
|
+
.where('shipping_zone_id', '=', zoneId)
|
|
138
|
+
.where('weight_from', '<=', weight)
|
|
139
|
+
.where('weight_to', '>=', weight)
|
|
140
|
+
.executeTakeFirst()
|
|
141
|
+
|
|
142
|
+
return rate as ShippingRateJsonResponse | undefined
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (error instanceof Error) {
|
|
146
|
+
throw new TypeError(`Failed to get shipping rate by weight and zone: ${error.message}`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
throw error
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Shipping address shape used by the safe-by-default lookup helpers
|
|
155
|
+
* (stacksjs/stacks#1879 Co-11). All fields except `countryCode` are
|
|
156
|
+
* optional — zone matching tries them in order of specificity
|
|
157
|
+
* (postal → region → country) and returns the most specific match.
|
|
158
|
+
*/
|
|
159
|
+
export interface ShippingAddress {
|
|
160
|
+
/** ISO 3166-1 alpha-2 country code (e.g. `'US'`, `'GB'`, `'JP'`). */
|
|
161
|
+
countryCode: string
|
|
162
|
+
/** ISO 3166-2 region/state code (e.g. `'NY'`, `'CA'`, `'TX'`). */
|
|
163
|
+
region?: string
|
|
164
|
+
/** Postal code / ZIP / postcode in the country's convention. */
|
|
165
|
+
postalCode?: string
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Resolve the canonical shipping zone for a delivery address
|
|
170
|
+
* (stacksjs/stacks#1879 Co-11). Walks the zones table looking for
|
|
171
|
+
* the most specific match: postal-code → region → country-only.
|
|
172
|
+
* Returns null when no zone covers the address.
|
|
173
|
+
*
|
|
174
|
+
* The current zones schema stores `countries` as a comma-separated
|
|
175
|
+
* string (per `getZonesByCountry`'s LIKE-query); we use the same
|
|
176
|
+
* convention here. Apps with a normalized zone-country join table
|
|
177
|
+
* can override by replacing this function via the package facade.
|
|
178
|
+
*/
|
|
179
|
+
export async function resolveZoneForAddress(address: ShippingAddress): Promise<number | null> {
|
|
180
|
+
if (!address?.countryCode) return null
|
|
181
|
+
try {
|
|
182
|
+
// Pull every active zone that mentions the country and pick the
|
|
183
|
+
// most-specific match in memory. The fan-out is bounded (most
|
|
184
|
+
// stores have <100 zones) so the in-memory pass is cheap.
|
|
185
|
+
const zones = await db
|
|
186
|
+
.selectFrom('shipping_zones')
|
|
187
|
+
.selectAll()
|
|
188
|
+
.where('countries', 'like', `%${address.countryCode}%`)
|
|
189
|
+
.where('status', '=', 'active')
|
|
190
|
+
.execute() as Array<{ id: number, regions?: string | null, postal_codes?: string | null }>
|
|
191
|
+
|
|
192
|
+
if (zones.length === 0) return null
|
|
193
|
+
|
|
194
|
+
// Specificity scoring: postal match > region match > country-only.
|
|
195
|
+
let best: { id: number, score: number } | null = null
|
|
196
|
+
for (const zone of zones) {
|
|
197
|
+
let score = 1 // country-only baseline
|
|
198
|
+
if (address.region && zone.regions && includesToken(zone.regions, address.region))
|
|
199
|
+
score = 2
|
|
200
|
+
if (address.postalCode && zone.postal_codes && includesToken(zone.postal_codes, address.postalCode))
|
|
201
|
+
score = 3
|
|
202
|
+
if (!best || score > best.score)
|
|
203
|
+
best = { id: zone.id, score }
|
|
204
|
+
}
|
|
205
|
+
return best?.id ?? null
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return null
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Safer counterpart to `getRateByWeightAndZone`. Resolves the zone
|
|
214
|
+
* server-side from the delivery address, then fetches the rate
|
|
215
|
+
* for that zone. Throws when the address doesn't match any active
|
|
216
|
+
* zone — caller surfaces "we don't ship to your area" to the user.
|
|
217
|
+
*
|
|
218
|
+
* Use this from any HTTP/API boundary where the address is
|
|
219
|
+
* caller-controlled. Internal callers that already have a verified
|
|
220
|
+
* zone can stay on `getRateByWeightAndZone`.
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```ts
|
|
224
|
+
* const rate = await getRateByWeightAndAddress(2.5, {
|
|
225
|
+
* countryCode: 'US',
|
|
226
|
+
* region: 'NY',
|
|
227
|
+
* postalCode: '10001',
|
|
228
|
+
* })
|
|
229
|
+
* if (!rate) throw new HttpError(404, 'No shipping rate for this address')
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
export async function getRateByWeightAndAddress(
|
|
233
|
+
weight: number,
|
|
234
|
+
address: ShippingAddress,
|
|
235
|
+
): Promise<ShippingRateJsonResponse | undefined> {
|
|
236
|
+
const zoneId = await resolveZoneForAddress(address)
|
|
237
|
+
if (zoneId == null) return undefined
|
|
238
|
+
return getRateByWeightAndZone(weight, zoneId)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Verify that a caller-supplied `zoneId` actually serves the given
|
|
243
|
+
* address. Returns true when the address resolves to the same zone.
|
|
244
|
+
* Use when a flow already has a `zoneId` from earlier and just
|
|
245
|
+
* wants to confirm it wasn't tampered with mid-checkout.
|
|
246
|
+
*/
|
|
247
|
+
export async function validateZoneMatchesAddress(zoneId: number, address: ShippingAddress): Promise<boolean> {
|
|
248
|
+
const resolved = await resolveZoneForAddress(address)
|
|
249
|
+
return resolved === zoneId
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Match a single value inside a comma-or-pipe-separated string,
|
|
254
|
+
* tolerating whitespace and case. Used because the zones schema
|
|
255
|
+
* stores region/postal lists as plain strings.
|
|
256
|
+
*/
|
|
257
|
+
function includesToken(haystack: string, needle: string): boolean {
|
|
258
|
+
if (!haystack || !needle) return false
|
|
259
|
+
const target = needle.trim().toUpperCase()
|
|
260
|
+
for (const token of haystack.split(/[,|;]/)) {
|
|
261
|
+
if (token.trim().toUpperCase() === target) return true
|
|
262
|
+
}
|
|
263
|
+
return false
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Format shipping rate options for dropdown menus or selectors
|
|
268
|
+
*
|
|
269
|
+
* @returns Array of formatted shipping rate options with id, method, zone, and rate
|
|
270
|
+
*/
|
|
271
|
+
export async function formatShippingRateOptions(): Promise<{ id: number, shipping_method_id: number, shipping_zone_id: number, rate: number }[]> {
|
|
272
|
+
try {
|
|
273
|
+
const results = await db
|
|
274
|
+
.selectFrom('shipping_rates')
|
|
275
|
+
.select(['id', 'shipping_method_id', 'shipping_zone_id', 'rate'])
|
|
276
|
+
.orderBy('shipping_method_id')
|
|
277
|
+
.execute()
|
|
278
|
+
|
|
279
|
+
// Filter out any results with undefined/null values to match the return type
|
|
280
|
+
return results.filter((result: any) =>
|
|
281
|
+
result.shipping_method_id !== null
|
|
282
|
+
&& result.shipping_method_id !== undefined
|
|
283
|
+
&& result.shipping_zone_id !== null
|
|
284
|
+
&& result.shipping_zone_id !== undefined,
|
|
285
|
+
) as { id: number, shipping_method_id: number, shipping_zone_id: number, rate: number }[]
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
if (error instanceof Error) {
|
|
289
|
+
throw new TypeError(`Failed to format shipping rate options: ${error.message}`)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
throw error
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get shipping rates for a specific method
|
|
298
|
+
*
|
|
299
|
+
* @param methodId The shipping method to filter by
|
|
300
|
+
* @returns List of shipping rates for the specified method
|
|
301
|
+
*/
|
|
302
|
+
export async function getShippingRatesByMethod(methodId: number): Promise<ShippingRateJsonResponse[]> {
|
|
303
|
+
try {
|
|
304
|
+
const rates = await db
|
|
305
|
+
.selectFrom('shipping_rates')
|
|
306
|
+
.selectAll()
|
|
307
|
+
.where('shipping_method_id', '=', methodId)
|
|
308
|
+
.orderBy('weight_from')
|
|
309
|
+
.execute()
|
|
310
|
+
|
|
311
|
+
return rates as ShippingRateJsonResponse[]
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
if (error instanceof Error) {
|
|
315
|
+
throw new TypeError(`Failed to get shipping rates by method: ${error.message}`)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
throw error
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export {
|
|
2
|
+
bulkDestroy,
|
|
3
|
+
destroy,
|
|
4
|
+
destroyByMethod,
|
|
5
|
+
destroyByZone,
|
|
6
|
+
} from './destroy'
|
|
7
|
+
|
|
8
|
+
// Functions from fetch.ts
|
|
9
|
+
export {
|
|
10
|
+
fetchAll,
|
|
11
|
+
fetchById,
|
|
12
|
+
formatShippingRateOptions,
|
|
13
|
+
// Safe-by-default address-based lookups (stacksjs/stacks#1879 Co-11).
|
|
14
|
+
// Use these from request handlers where the address is caller-controlled.
|
|
15
|
+
getRateByWeightAndAddress,
|
|
16
|
+
getRateByWeightAndZone,
|
|
17
|
+
getRatesByZone,
|
|
18
|
+
getShippingRatesByMethod,
|
|
19
|
+
resolveZoneForAddress,
|
|
20
|
+
validateZoneMatchesAddress,
|
|
21
|
+
} from './fetch'
|
|
22
|
+
export type { ShippingAddress } from './fetch'
|
|
23
|
+
|
|
24
|
+
// Functions from store.ts
|
|
25
|
+
export {
|
|
26
|
+
bulkStore,
|
|
27
|
+
store,
|
|
28
|
+
} from './store'
|
|
29
|
+
|
|
30
|
+
// Functions from update.ts
|
|
31
|
+
export {
|
|
32
|
+
bulkUpdate,
|
|
33
|
+
update,
|
|
34
|
+
updateByMethod,
|
|
35
|
+
updateByZone,
|
|
36
|
+
} from './update'
|