@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,51 @@
1
+ import { beforeEach, describe, expect, it } from 'bun:test'
2
+ import { refreshDatabase } from './setup'
3
+ import { bulkDestroy } from '../shippings/delivery-routes/destroy'
4
+ import { fetchByDriver } from '../shippings/delivery-routes/fetch'
5
+ import { store } from '../shippings/delivery-routes/store'
6
+
7
+ beforeEach(async () => {
8
+ await refreshDatabase()
9
+ })
10
+
11
+ describe('Delivery Route Module', () => {
12
+ describe('fetch', () => {
13
+ it('should fetch routes by driver', async () => {
14
+ // Create routes for the same driver
15
+ const driverName = 'John Doe'
16
+ const routes = [
17
+ {
18
+ driver: driverName,
19
+ vehicle: 'Truck A123',
20
+ stops: 5,
21
+ delivery_time: 120,
22
+ total_distance: 50,
23
+ },
24
+ {
25
+ driver: driverName,
26
+ vehicle: 'Van B456',
27
+ stops: 3,
28
+ delivery_time: 60,
29
+ total_distance: 25,
30
+ },
31
+ ]
32
+
33
+ // Store each route individually
34
+ for (const routeData of routes) {
35
+ await store(routeData)
36
+ }
37
+
38
+ const driverRoutes = await fetchByDriver(driverName)
39
+ expect(driverRoutes.length).toBeGreaterThanOrEqual(2)
40
+ expect(driverRoutes.every(route => route.driver === driverName)).toBe(true)
41
+ })
42
+ })
43
+
44
+ describe('destroy', () => {
45
+ it('should return 0 when trying to delete an empty array of routes', async () => {
46
+ // Try to delete with an empty array
47
+ const deletedCount = await bulkDestroy([])
48
+ expect(deletedCount).toBe(0)
49
+ })
50
+ })
51
+ })
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Commerce package test harness.
3
+ *
4
+ * These suites used to import `refreshDatabase` from
5
+ * `@stacksjs/testing/database`, which (pre-existing breakage) never
6
+ * worked from a package directory: `bun test` run from
7
+ * storage/framework/core/commerce doesn't load the repo-root `.env`,
8
+ * so the driver fell back to mysql and the helper's dead kysely-era
9
+ * `sql\`...\`.execute(db)` calls threw in every `beforeEach`. Even
10
+ * forced to sqlite, the migration pipeline resolves `database/` and
11
+ * `database/migrations` off `process.cwd()` and the committed
12
+ * migration corpus is both unrunnable on a virgin DB and drifted from
13
+ * the models (missing `uuid` columns for the 28 `useUuid` commerce
14
+ * models).
15
+ *
16
+ * Strategy (mirrors auth/tests/password-reset-revocation.test.ts): pin
17
+ * env to a throwaway SQLite file BEFORE any framework module loads,
18
+ * then generate the schema directly from the live model definitions
19
+ * via bun-query-builder's loadModels → buildMigrationPlan →
20
+ * generateSql. Foreign-key references are stripped from the plan
21
+ * because the fixtures intentionally insert dangling parent ids
22
+ * (e.g. customer_id 1..3 with no customers row).
23
+ *
24
+ * Wired up via this package's bunfig.toml `[test] preload`, so the
25
+ * env pin always lands before any test file's own imports evaluate.
26
+ */
27
+
28
+ import { existsSync, mkdtempSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'
29
+ import { tmpdir } from 'node:os'
30
+ import { join } from 'node:path'
31
+ import process from 'node:process'
32
+
33
+ const DB_PATH = join(tmpdir(), `stacks-commerce-${process.pid}.sqlite`)
34
+ process.env.DB_CONNECTION = 'sqlite'
35
+ process.env.DB_DATABASE_PATH = DB_PATH
36
+ process.env.APP_ENV = 'testing'
37
+
38
+ // Dynamic imports AFTER the env pin so the lazy `db` proxy and the
39
+ // config loader can't capture a different connection first.
40
+ const { acquireDbConfigLock, db, ensureDatabaseConfigLoaded, initializeDbConfig } = await import('@stacksjs/database')
41
+ const { buildMigrationPlan, generateSql, loadModels } = await import('bun-query-builder')
42
+
43
+ // Holds `initializeDbConfig`'s process-wide config mutex (stacksjs/stacks#1862)
44
+ // for this module's entire lifetime — no `describe`/`afterAll` boundary exists
45
+ // here (this is a shared fixture imported by many test files, not a test file
46
+ // itself), so it's released on process exit alongside the file cleanup below.
47
+ const releaseDbConfigLock = await acquireDbConfigLock()
48
+
49
+ // storage/framework/defaults — the framework's vendor layer where the
50
+ // default model definitions live. `loadModels` does not recurse, so the
51
+ // commerce subdirectory is loaded explicitly and merged.
52
+ const defaultsModelsDir = join(import.meta.dir, '../../../../defaults/app/Models')
53
+ const commerceModelsDir = join(defaultsModelsDir, 'commerce')
54
+
55
+ /**
56
+ * Drain the one-shot async config reload, then force our temp SQLite
57
+ * config so a late-resolving override can't re-point the shared `db`
58
+ * proxy at a different database mid-test.
59
+ */
60
+ async function forceConfig(): Promise<void> {
61
+ await ensureDatabaseConfigLoaded()
62
+ initializeDbConfig({
63
+ app: { env: 'testing' },
64
+ database: {
65
+ default: 'sqlite',
66
+ connections: { sqlite: { database: DB_PATH, prefix: '' } },
67
+ },
68
+ })
69
+ }
70
+
71
+ // Stale file from a recycled pid would otherwise leak a previous run's
72
+ // schema/rows into this one.
73
+ for (const suffix of ['', '-wal', '-shm']) {
74
+ if (existsSync(`${DB_PATH}${suffix}`))
75
+ unlinkSync(`${DB_PATH}${suffix}`)
76
+ }
77
+
78
+ await forceConfig()
79
+
80
+ const models = {
81
+ ...(await loadModels({ modelsDir: defaultsModelsDir })),
82
+ ...(await loadModels({ modelsDir: commerceModelsDir })),
83
+ }
84
+
85
+ const plan = buildMigrationPlan(models, { dialect: 'sqlite' })
86
+
87
+ // The fixtures insert dangling parent ids by design (e.g. orders with
88
+ // customer_id 1 and no customers row), so FK constraints would reject
89
+ // most of the corpus. Strip references — the suites exercise the
90
+ // commerce modules' query logic, not referential integrity.
91
+ for (const table of plan.tables) {
92
+ for (const column of table.columns)
93
+ delete column.references
94
+ }
95
+
96
+ // `generateSql()` also writes one `.sql` migration file per statement
97
+ // into `<nearest package.json dir>/database/migrations` (resolved off
98
+ // `process.cwd()`) as a side effect — from this package's cwd that
99
+ // would litter storage/framework/core/commerce/database/. Point the
100
+ // side effect at a disposable temp workspace for the duration of the
101
+ // call. The stub package.json makes its workspace-root walk stop there.
102
+ const scratchDir = mkdtempSync(join(tmpdir(), 'stacks-commerce-migrations-'))
103
+ writeFileSync(join(scratchDir, 'package.json'), '{}')
104
+ const originalCwd = process.cwd()
105
+ let statements: string[]
106
+ try {
107
+ process.chdir(scratchDir)
108
+ statements = generateSql(plan)
109
+ }
110
+ finally {
111
+ process.chdir(originalCwd)
112
+ rmSync(scratchDir, { recursive: true, force: true })
113
+ }
114
+
115
+ for (const statement of statements) {
116
+ await db.unsafe(statement).execute()
117
+ }
118
+
119
+ const tableNames = plan.tables.map(table => table.table)
120
+
121
+ /**
122
+ * Wipe every model-derived table between tests. DELETE (not DROP) keeps
123
+ * the schema warm; with FK references stripped, order doesn't matter.
124
+ */
125
+ export async function refreshDatabase(): Promise<void> {
126
+ await forceConfig()
127
+ for (const table of tableNames) {
128
+ await db.unsafe(`DELETE FROM ${table}`).execute()
129
+ }
130
+ }
131
+
132
+ // Cleanup on process exit, NOT in an `afterAll`: when this module is
133
+ // imported by a test file mid-run (e.g. `bun test tests/` from the
134
+ // repo root, where the path filter also matches these suites), an
135
+ // `afterAll` registered during module evaluation attaches to the FIRST
136
+ // importing file's scope and unlinks the shared DB while later files'
137
+ // connections still use it (SQLITE_IOERR_VNODE).
138
+ process.on('exit', () => {
139
+ for (const suffix of ['', '-wal', '-shm']) {
140
+ try {
141
+ if (existsSync(`${DB_PATH}${suffix}`))
142
+ unlinkSync(`${DB_PATH}${suffix}`)
143
+ }
144
+ catch {
145
+ // Best effort — pid-named file in tmpdir, the OS reclaims it.
146
+ }
147
+ }
148
+ releaseDbConfigLock()
149
+ })
@@ -0,0 +1,88 @@
1
+ import { beforeEach, describe, expect, it } from 'bun:test'
2
+ import { refreshDatabase } from './setup'
3
+ import { bulkDestroy, bulkSoftDelete } from '../shippings/shipping-methods/destroy'
4
+ import { formatShippingOptions, getActiveShippingMethods, store } from '../shippings/shipping-methods/store'
5
+
6
+ beforeEach(async () => {
7
+ await refreshDatabase()
8
+ })
9
+
10
+ describe('Shipping Methods Module', () => {
11
+ describe('store', () => {
12
+ it('should format shipping options correctly', async () => {
13
+ // Create test shipping methods
14
+ const request1 = {
15
+ name: 'Standard Shipping',
16
+ base_rate: 500,
17
+ status: 'active',
18
+ }
19
+
20
+ await store(request1)
21
+
22
+ const request2 = {
23
+ name: 'Express Shipping',
24
+ base_rate: 1500,
25
+ zones: JSON.stringify(['North America', 'Europe']),
26
+ status: 'active',
27
+ }
28
+
29
+ await store(request2)
30
+
31
+ // Get formatted options
32
+ const options = await formatShippingOptions()
33
+
34
+ expect(options.length).toBe(2)
35
+ expect(options[0].name).toBeDefined()
36
+ expect(options[0].base_rate).toBeDefined()
37
+ expect(options[0].status).toBeDefined()
38
+ })
39
+ })
40
+
41
+ describe('destroy', () => {
42
+ it('should return 0 when trying to delete an empty array of shipping methods', async () => {
43
+ // Try to delete with an empty array
44
+ const deletedCount = await bulkDestroy([])
45
+ expect(deletedCount).toBe(0)
46
+
47
+ // Try to soft delete with an empty array
48
+ const softDeletedCount = await bulkSoftDelete([])
49
+ expect(softDeletedCount).toBe(0)
50
+ })
51
+ })
52
+
53
+ describe('fetch', () => {
54
+ it('should get active shipping methods only', async () => {
55
+ // Create an active shipping method
56
+ const activeRequest = {
57
+ name: 'Active Shipping',
58
+ base_rate: 1000,
59
+ status: 'active',
60
+ }
61
+ await store(activeRequest)
62
+
63
+ // Create an inactive shipping method
64
+ const inactiveRequest = {
65
+ name: 'Inactive Shipping',
66
+ base_rate: 1500,
67
+ status: 'inactive',
68
+ }
69
+ await store(inactiveRequest)
70
+
71
+ // Create a draft shipping method
72
+ const draftRequest = {
73
+ name: 'Draft Shipping',
74
+ base_rate: 2000,
75
+ status: 'draft',
76
+ }
77
+ await store(draftRequest)
78
+
79
+ // Get active shipping methods
80
+ const activeMethods = await getActiveShippingMethods()
81
+
82
+ // Should only return active methods
83
+ expect(activeMethods.length).toBe(1)
84
+ expect(activeMethods[0].name).toBe('Active Shipping')
85
+ expect(activeMethods[0].status).toBe('active')
86
+ })
87
+ })
88
+ })
@@ -0,0 +1,25 @@
1
+ import { beforeEach, describe, expect, it } from 'bun:test'
2
+ import { refreshDatabase } from './setup'
3
+ import { bulkDestroy } from '../tax/destroy'
4
+ import { bulkStore } from '../tax/store'
5
+
6
+ beforeEach(async () => {
7
+ await refreshDatabase()
8
+ })
9
+
10
+ describe('Tax Rate Module', () => {
11
+ describe('store', () => {
12
+ it('should return 0 when trying to bulk store an empty array', async () => {
13
+ const count = await bulkStore([])
14
+ expect(count).toBe(0)
15
+ })
16
+ })
17
+
18
+ describe('destroy', () => {
19
+ it('should return 0 when trying to delete an empty array of tax rates', async () => {
20
+ // Try to delete with an empty array
21
+ const deletedCount = await bulkDestroy([])
22
+ expect(deletedCount).toBe(0)
23
+ })
24
+ })
25
+ })
@@ -0,0 +1,110 @@
1
+ import { beforeEach, describe, expect, it } from 'bun:test'
2
+ import { refreshDatabase } from './setup'
3
+ import { bulkDestroy } from '../products/units/destroy'
4
+ import { bulkStore, formatUnitOptions } from '../products/units/store'
5
+ import { bulkUpdate } from '../products/units/update'
6
+
7
+ beforeEach(async () => {
8
+ await refreshDatabase()
9
+ })
10
+
11
+ describe('Product Unit Module', () => {
12
+ describe('bulkStore', () => {
13
+ it('should create multiple product units at once', async () => {
14
+ const unitRequests = [
15
+ {
16
+ name: 'Kilogram',
17
+ product_id: 1,
18
+ abbreviation: 'kg',
19
+ type: 'weight',
20
+ description: 'Standard weight unit',
21
+ is_default: true,
22
+ },
23
+ {
24
+ name: 'Liter',
25
+ product_id: 1,
26
+ abbreviation: 'L',
27
+ type: 'volume',
28
+ description: 'Standard volume unit',
29
+ is_default: true,
30
+ },
31
+ {
32
+ name: 'Piece',
33
+ product_id: 1,
34
+ abbreviation: 'pc',
35
+ type: 'quantity',
36
+ description: 'Standard quantity unit',
37
+ is_default: true,
38
+ },
39
+ ]
40
+
41
+ const createdCount = await bulkStore(unitRequests)
42
+ expect(createdCount).toBe(3)
43
+ })
44
+
45
+ it('should return 0 when trying to create an empty array of units', async () => {
46
+ const createdCount = await bulkStore([])
47
+ expect(createdCount).toBe(0)
48
+ })
49
+
50
+ })
51
+
52
+ describe('formatUnitOptions and getDefaultUnit', () => {
53
+ it('should format unit options correctly', async () => {
54
+ // Create units first
55
+ const unitRequests = [
56
+ {
57
+ name: 'Kilogram',
58
+ product_id: 1,
59
+ abbreviation: 'kg',
60
+ type: 'weight',
61
+ is_default: true,
62
+ },
63
+ {
64
+ name: 'Gram',
65
+ product_id: 1,
66
+ abbreviation: 'g',
67
+ type: 'weight',
68
+ is_default: false,
69
+ },
70
+ {
71
+ name: 'Liter',
72
+ product_id: 1,
73
+ abbreviation: 'L',
74
+ type: 'volume',
75
+ is_default: true,
76
+ },
77
+ ]
78
+
79
+ await bulkStore(unitRequests)
80
+
81
+ // Test formatting options for weight units
82
+ const weightUnits = await formatUnitOptions('weight')
83
+ expect(weightUnits.length).toBe(2)
84
+ expect(weightUnits[0].name).toBe('Gram') // Alphabetical order
85
+ expect(weightUnits[1].name).toBe('Kilogram')
86
+ expect(weightUnits[0].abbreviation).toBe('g')
87
+ expect(weightUnits[1].abbreviation).toBe('kg')
88
+ expect(Boolean(weightUnits[0].is_default)).toBe(false)
89
+ expect(Boolean(weightUnits[1].is_default)).toBe(true)
90
+
91
+ // Test formatting options for all units (no type filter)
92
+ const allUnits = await formatUnitOptions()
93
+ expect(allUnits.length).toBe(3)
94
+ })
95
+ })
96
+
97
+ describe('bulkUpdate', () => {
98
+ it('should return 0 when trying to update an empty array of units', async () => {
99
+ const updatedCount = await bulkUpdate([])
100
+ expect(updatedCount).toBe(0)
101
+ })
102
+ })
103
+
104
+ describe('bulkDestroy', () => {
105
+ it('should return 0 when trying to delete an empty array of units', async () => {
106
+ const deletedCount = await bulkDestroy([])
107
+ expect(deletedCount).toBe(0)
108
+ })
109
+ })
110
+ })
@@ -0,0 +1,133 @@
1
+ import { beforeEach, describe, expect, it } from 'bun:test'
2
+ import { refreshDatabase } from './setup'
3
+ import { bulkDestroy } from '../products/variants/destroy'
4
+ import { bulkStore, formatVariantOptions, generateVariantCombinations } from '../products/variants/store'
5
+ import { bulkUpdate } from '../products/variants/update'
6
+
7
+ beforeEach(async () => {
8
+ await refreshDatabase()
9
+ })
10
+
11
+ describe('Product Variant Module', () => {
12
+ describe('bulkStore', () => {
13
+ it('should create multiple product variants at once', async () => {
14
+ const requestsData = [
15
+ {
16
+ product_id: 1,
17
+ variant: 'Color',
18
+ type: 'dropdown',
19
+ description: 'Available colors',
20
+ options: JSON.stringify(['Red', 'Blue', 'Green']),
21
+ status: 'draft',
22
+ },
23
+ {
24
+ product_id: 1,
25
+ variant: 'Size',
26
+ type: 'radio',
27
+ description: 'Available sizes',
28
+ options: JSON.stringify(['S', 'M', 'L', 'XL']),
29
+ status: 'draft',
30
+ },
31
+ ]
32
+
33
+ const createdCount = await bulkStore(requestsData)
34
+
35
+ expect(createdCount).toBe(2)
36
+ })
37
+
38
+ it('should return 0 when trying to create an empty array of variants', async () => {
39
+ const createdCount = await bulkStore([])
40
+ expect(createdCount).toBe(0)
41
+ })
42
+ })
43
+
44
+ describe('bulkUpdate', () => {
45
+ it('should return 0 when trying to update an empty array of variants', async () => {
46
+ const updateCount = await bulkUpdate([])
47
+ expect(updateCount).toBe(0)
48
+ })
49
+ })
50
+
51
+ describe('bulkDestroy', () => {
52
+ it('should return 0 when trying to delete an empty array of variants', async () => {
53
+ const deletedCount = await bulkDestroy([])
54
+ expect(deletedCount).toBe(0)
55
+ })
56
+ })
57
+
58
+ describe('formatVariantOptions', () => {
59
+ it('should format an array of options as a JSON string', () => {
60
+ const rawOptions = ['Red', 'Blue', 'Green']
61
+ const formattedOptions = formatVariantOptions(rawOptions)
62
+ expect(formattedOptions).toBe(JSON.stringify(rawOptions))
63
+ })
64
+
65
+ it('should trim whitespace from options', () => {
66
+ const rawOptions = [' Red ', 'Blue ', ' Green']
67
+ const formattedOptions = formatVariantOptions(rawOptions)
68
+ expect(formattedOptions).toBe(JSON.stringify(['Red', 'Blue', 'Green']))
69
+ })
70
+
71
+ it('should filter out empty options', () => {
72
+ const rawOptions = ['Red', '', 'Blue', ' ', 'Green']
73
+ const formattedOptions = formatVariantOptions(rawOptions)
74
+ expect(formattedOptions).toBe(JSON.stringify(['Red', 'Blue', 'Green']))
75
+ })
76
+
77
+ it('should return an empty array JSON string when given empty input', () => {
78
+ expect(formatVariantOptions([])).toBe('[]')
79
+ expect(formatVariantOptions([] as string[])).toBe('[]')
80
+ })
81
+ })
82
+
83
+ describe('generateVariantCombinations', () => {
84
+ it('should generate all possible combinations of variant options', () => {
85
+ const optionSets = {
86
+ size: ['S', 'M', 'L'],
87
+ color: ['Red', 'Blue'],
88
+ }
89
+
90
+ const combinations = generateVariantCombinations(optionSets)
91
+
92
+ expect(combinations.length).toBe(6) // 3 sizes * 2 colors
93
+ expect(combinations).toContainEqual({ size: 'S', color: 'Red' })
94
+ expect(combinations).toContainEqual({ size: 'S', color: 'Blue' })
95
+ expect(combinations).toContainEqual({ size: 'M', color: 'Red' })
96
+ expect(combinations).toContainEqual({ size: 'M', color: 'Blue' })
97
+ expect(combinations).toContainEqual({ size: 'L', color: 'Red' })
98
+ expect(combinations).toContainEqual({ size: 'L', color: 'Blue' })
99
+ })
100
+
101
+ it('should handle three or more option types', () => {
102
+ const optionSets = {
103
+ size: ['S', 'M'],
104
+ color: ['Red', 'Blue'],
105
+ material: ['Cotton', 'Polyester'],
106
+ }
107
+
108
+ const combinations = generateVariantCombinations(optionSets)
109
+
110
+ expect(combinations.length).toBe(8) // 2 sizes * 2 colors * 2 materials
111
+ expect(combinations).toContainEqual({ size: 'S', color: 'Red', material: 'Cotton' })
112
+ expect(combinations).toContainEqual({ size: 'M', color: 'Blue', material: 'Polyester' })
113
+ })
114
+
115
+ it('should return an empty array for empty input', () => {
116
+ const combinations = generateVariantCombinations({})
117
+ expect(combinations).toEqual([])
118
+ })
119
+
120
+ it('should handle a single option type correctly', () => {
121
+ const optionSets = {
122
+ size: ['S', 'M', 'L'],
123
+ }
124
+
125
+ const combinations = generateVariantCombinations(optionSets)
126
+
127
+ expect(combinations.length).toBe(3)
128
+ expect(combinations).toContainEqual({ size: 'S' })
129
+ expect(combinations).toContainEqual({ size: 'M' })
130
+ expect(combinations).toContainEqual({ size: 'L' })
131
+ })
132
+ })
133
+ })