@stacksjs/commerce 0.70.53 → 0.70.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/package.json +3 -2
  2. package/src/carts/destroy.ts +54 -0
  3. package/src/carts/fetch.ts +20 -0
  4. package/src/carts/index.ts +13 -0
  5. package/src/carts/store.ts +75 -0
  6. package/src/carts/update.ts +78 -0
  7. package/src/coupons/destroy.ts +49 -0
  8. package/src/coupons/fetch.ts +510 -0
  9. package/src/coupons/index.ts +30 -0
  10. package/src/coupons/store.ts +46 -0
  11. package/src/coupons/update.ts +147 -0
  12. package/src/customers/destroy.ts +62 -0
  13. package/src/customers/fetch.ts +20 -0
  14. package/src/customers/index.ts +17 -0
  15. package/src/customers/store.ts +45 -0
  16. package/src/customers/update.ts +44 -0
  17. package/src/devices/destroy.ts +54 -0
  18. package/src/devices/export.ts +98 -0
  19. package/src/devices/fetch.ts +125 -0
  20. package/src/devices/index.ts +31 -0
  21. package/src/devices/store.ts +82 -0
  22. package/src/devices/update.ts +125 -0
  23. package/src/errors/destroy.ts +54 -0
  24. package/src/errors/fetch.ts +197 -0
  25. package/src/errors/index.ts +28 -0
  26. package/src/errors/update.ts +116 -0
  27. package/src/gift-cards/destroy.ts +87 -0
  28. package/src/gift-cards/fetch.ts +431 -0
  29. package/src/gift-cards/index.ts +26 -0
  30. package/src/gift-cards/store.ts +56 -0
  31. package/src/gift-cards/update.ts +102 -0
  32. package/src/index.ts +78 -0
  33. package/src/orders/destroy.ts +111 -0
  34. package/src/orders/events.ts +121 -0
  35. package/src/orders/export.ts +158 -0
  36. package/src/orders/fetch.ts +379 -0
  37. package/src/orders/guards.ts +261 -0
  38. package/src/orders/index.ts +91 -0
  39. package/src/orders/place-order.ts +274 -0
  40. package/src/orders/store.ts +51 -0
  41. package/src/orders/totals.ts +190 -0
  42. package/src/orders/update.ts +164 -0
  43. package/src/orders/webhook.ts +312 -0
  44. package/src/payments/destroy.ts +56 -0
  45. package/src/payments/fetch.ts +288 -0
  46. package/src/payments/index.ts +16 -0
  47. package/src/payments/store.ts +63 -0
  48. package/src/payments/update.ts +52 -0
  49. package/src/product.ts +0 -0
  50. package/src/products/categories/destroy.ts +103 -0
  51. package/src/products/categories/fetch.ts +285 -0
  52. package/src/products/categories/index.ts +36 -0
  53. package/src/products/categories/store.ts +87 -0
  54. package/src/products/categories/update.ts +251 -0
  55. package/src/products/index.ts +35 -0
  56. package/src/products/items/destroy.ts +54 -0
  57. package/src/products/items/fetch.ts +282 -0
  58. package/src/products/items/index.ts +26 -0
  59. package/src/products/items/store.ts +84 -0
  60. package/src/products/items/update.ts +324 -0
  61. package/src/products/manufacturers/destroy.ts +56 -0
  62. package/src/products/manufacturers/fetch.ts +117 -0
  63. package/src/products/manufacturers/index.ts +24 -0
  64. package/src/products/manufacturers/store.ts +82 -0
  65. package/src/products/manufacturers/update.ts +113 -0
  66. package/src/products/reviews/destroy.ts +56 -0
  67. package/src/products/reviews/fetch.ts +63 -0
  68. package/src/products/reviews/index.ts +25 -0
  69. package/src/products/reviews/store.ts +46 -0
  70. package/src/products/reviews/update.ts +82 -0
  71. package/src/products/units/destroy.ts +56 -0
  72. package/src/products/units/fetch.ts +20 -0
  73. package/src/products/units/index.ts +13 -0
  74. package/src/products/units/store.ts +162 -0
  75. package/src/products/units/update.ts +159 -0
  76. package/src/products/variants/destroy.ts +56 -0
  77. package/src/products/variants/fetch.ts +20 -0
  78. package/src/products/variants/index.ts +21 -0
  79. package/src/products/variants/store.ts +136 -0
  80. package/src/products/variants/update.ts +104 -0
  81. package/src/receipts/destroy.ts +54 -0
  82. package/src/receipts/export.ts +0 -0
  83. package/src/receipts/fetch.ts +222 -0
  84. package/src/receipts/index.ts +24 -0
  85. package/src/receipts/printer.ts +69 -0
  86. package/src/receipts/store.ts +69 -0
  87. package/src/receipts/update.ts +124 -0
  88. package/src/shippings/delivery-routes/destroy.ts +54 -0
  89. package/src/shippings/delivery-routes/fetch.ts +42 -0
  90. package/src/shippings/delivery-routes/index.ts +26 -0
  91. package/src/shippings/delivery-routes/store.ts +69 -0
  92. package/src/shippings/delivery-routes/update.ts +120 -0
  93. package/src/shippings/digital-deliveries/destroy.ts +111 -0
  94. package/src/shippings/digital-deliveries/fetch.ts +20 -0
  95. package/src/shippings/digital-deliveries/index.ts +26 -0
  96. package/src/shippings/digital-deliveries/store.ts +76 -0
  97. package/src/shippings/digital-deliveries/update.ts +129 -0
  98. package/src/shippings/drivers/destroy.ts +78 -0
  99. package/src/shippings/drivers/fetch.ts +20 -0
  100. package/src/shippings/drivers/index.ts +24 -0
  101. package/src/shippings/drivers/store.ts +69 -0
  102. package/src/shippings/drivers/update.ts +117 -0
  103. package/src/shippings/index.ts +39 -0
  104. package/src/shippings/license-keys/destroy.ts +111 -0
  105. package/src/shippings/license-keys/fetch.ts +20 -0
  106. package/src/shippings/license-keys/index.ts +26 -0
  107. package/src/shippings/license-keys/store.ts +77 -0
  108. package/src/shippings/license-keys/update.ts +117 -0
  109. package/src/shippings/shipping-methods/destroy.ts +111 -0
  110. package/src/shippings/shipping-methods/fetch.ts +62 -0
  111. package/src/shippings/shipping-methods/index.ts +28 -0
  112. package/src/shippings/shipping-methods/store.ts +123 -0
  113. package/src/shippings/shipping-methods/update.ts +120 -0
  114. package/src/shippings/shipping-rates/destroy.ts +102 -0
  115. package/src/shippings/shipping-rates/fetch.ts +320 -0
  116. package/src/shippings/shipping-rates/index.ts +36 -0
  117. package/src/shippings/shipping-rates/store.ts +102 -0
  118. package/src/shippings/shipping-rates/update.ts +139 -0
  119. package/src/shippings/shipping-zones/destroy.ts +111 -0
  120. package/src/shippings/shipping-zones/fetch.ts +96 -0
  121. package/src/shippings/shipping-zones/index.ts +26 -0
  122. package/src/shippings/shipping-zones/store.ts +74 -0
  123. package/src/shippings/shipping-zones/update.ts +156 -0
  124. package/src/tax/destroy.ts +54 -0
  125. package/src/tax/fetch.ts +20 -0
  126. package/src/tax/index.ts +24 -0
  127. package/src/tax/store.ts +69 -0
  128. package/src/tax/update.ts +119 -0
  129. package/src/tests/coupons.test.ts +90 -0
  130. package/src/tests/customers.test.ts +39 -0
  131. package/src/tests/device.test.ts +111 -0
  132. package/src/tests/digitals.test.ts +31 -0
  133. package/src/tests/drivers.test.ts +25 -0
  134. package/src/tests/gift-cards.test.ts +36 -0
  135. package/src/tests/license.test.ts +31 -0
  136. package/src/tests/manufacturers.test.ts +80 -0
  137. package/src/tests/orders.test.ts +25 -0
  138. package/src/tests/payments.test.ts +75 -0
  139. package/src/tests/rates.test.ts +367 -0
  140. package/src/tests/receipts.test.ts +287 -0
  141. package/src/tests/restaurant.test.ts +522 -0
  142. package/src/tests/reviews.test.ts +165 -0
  143. package/src/tests/routes.test.ts +51 -0
  144. package/src/tests/setup.ts +149 -0
  145. package/src/tests/shipping.test.ts +88 -0
  146. package/src/tests/tax.test.ts +25 -0
  147. package/src/tests/units.test.ts +110 -0
  148. package/src/tests/variants.test.ts +133 -0
  149. package/src/tests/waitlist.test.ts +303 -0
  150. package/src/tests/zones.test.ts +86 -0
  151. package/src/types.ts +312 -0
  152. package/src/utils/typed-stats.ts +67 -0
  153. package/src/waitlists/index.ts +19 -0
  154. package/src/waitlists/products/destroy.ts +65 -0
  155. package/src/waitlists/products/export.ts +113 -0
  156. package/src/waitlists/products/fetch.ts +362 -0
  157. package/src/waitlists/products/index.ts +33 -0
  158. package/src/waitlists/products/store.ts +112 -0
  159. package/src/waitlists/products/update.ts +125 -0
  160. package/src/waitlists/restaurant/destroy.ts +65 -0
  161. package/src/waitlists/restaurant/export.ts +107 -0
  162. package/src/waitlists/restaurant/fetch.ts +538 -0
  163. package/src/waitlists/restaurant/index.ts +43 -0
  164. package/src/waitlists/restaurant/store.ts +109 -0
  165. package/src/waitlists/restaurant/update.ts +198 -0
@@ -0,0 +1,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'