cabloy 5.1.114 → 5.1.116

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 (34) hide show
  1. package/.cabloy-version +1 -1
  2. package/.github/workflows/vona-test-mysql.yml +6 -10
  3. package/.github/workflows/vona-test-pg.yml +6 -10
  4. package/CHANGELOG.md +20 -0
  5. package/cabloy-docs/backend/orm-mutation-guide.md +36 -10
  6. package/cabloy-docs/backend/orm-select-guide.md +15 -10
  7. package/package.json +1 -1
  8. package/vona/packages-vona/vona/package.json +1 -1
  9. package/vona/pnpm-lock.yaml +37 -37
  10. package/vona/pnpm-workspace.yaml +1 -1
  11. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/sku.test.ts +1 -1
  12. package/vona/src/suite/a-commerce/modules/commerce-member/test/addressOwnership.test.ts +4 -1
  13. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponIssue.tsx +1 -1
  14. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/service/coupon.ts +4 -1
  15. package/vona/src/suite/a-commerce/modules/commerce-promotion/test/couponReservation.test.ts +85 -2
  16. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/cart.ts +14 -0
  17. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +42 -2
  18. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/stockBalance.ts +4 -3
  19. package/vona/src/suite/a-commerce/modules/commerce-trade/test/cartOwnership.test.ts +160 -80
  20. package/vona/src/suite/a-commerce/modules/commerce-trade/test/checkoutReservation.test.ts +255 -1
  21. package/vona/src/suite/a-commerce/modules/commerce-trade/test/checkoutTransaction.test.ts +208 -0
  22. package/vona/src/suite/a-commerce/modules/commerce-trade/test/orderSnapshot.test.ts +28 -4
  23. package/vona/src/suite/a-commerce/modules/commerce-trade/test/reservationExpiry.test.ts +90 -8
  24. package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockBalance.test.ts +15 -1
  25. package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockReservation.test.ts +20 -2
  26. package/vona/src/suite/a-commerce/modules/commerce-trade/test/testLock.ts +11 -0
  27. package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
  28. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_crud_inner.ts +3 -0
  29. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_utils.ts +1 -1
  30. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  31. package/zova/pnpm-lock.yaml +31 -31
  32. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/schemas.ts +40 -32
  33. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/types.ts +638 -633
  34. /package/vona/patches/{zova-core@5.1.78.patch → zova-core@5.1.79.patch} +0 -0
package/.cabloy-version CHANGED
@@ -1 +1 @@
1
- 5.1.114
1
+ 5.1.116
@@ -35,16 +35,12 @@ jobs:
35
35
  - name: init
36
36
  run: npm run init
37
37
  - name: run Commerce lifecycle gate
38
- run: >-
39
- DATABASE_DEFAULT_CLIENT=mysql DATABASE_CLIENT_MYSQL_HOST=127.0.0.1 DATABASE_CLIENT_MYSQL_PASSWORD=root npm run vona :bin:test --
40
- commerce-trade/test/stockBalance.test.ts
41
- commerce-trade/test/stockReservation.test.ts
42
- commerce-trade/test/stockAudit.test.ts
43
- commerce-promotion/test/couponReservation.test.ts
44
- commerce-payment/test/paymentAttempt.test.ts
45
- commerce-trade/test/checkoutReservation.test.ts
46
- commerce-trade/test/reservationExpiry.test.ts
47
- --flavor=normal
38
+ run: |
39
+ DATABASE_DEFAULT_CLIENT=mysql DATABASE_CLIENT_MYSQL_HOST=127.0.0.1 DATABASE_CLIENT_MYSQL_PASSWORD=root npm run vona :bin:test -- commerce-trade/test/stockBalance.test.ts commerce-trade/test/stockReservation.test.ts commerce-trade/test/stockAudit.test.ts --flavor=normal
40
+ DATABASE_DEFAULT_CLIENT=mysql DATABASE_CLIENT_MYSQL_HOST=127.0.0.1 DATABASE_CLIENT_MYSQL_PASSWORD=root npm run vona :bin:test -- commerce-promotion/test/couponReservation.test.ts commerce-payment/test/paymentAttempt.test.ts --flavor=normal
41
+ DATABASE_DEFAULT_CLIENT=mysql DATABASE_CLIENT_MYSQL_HOST=127.0.0.1 DATABASE_CLIENT_MYSQL_PASSWORD=root npm run vona :bin:test -- commerce-trade/test/checkoutReservation.test.ts commerce-trade/test/checkoutTransaction.test.ts --flavor=normal
42
+ DATABASE_DEFAULT_CLIENT=mysql DATABASE_CLIENT_MYSQL_HOST=127.0.0.1 DATABASE_CLIENT_MYSQL_PASSWORD=root npm run vona :bin:test -- commerce-trade/test/orderSnapshot.test.ts --flavor=normal
43
+ DATABASE_DEFAULT_CLIENT=mysql DATABASE_CLIENT_MYSQL_HOST=127.0.0.1 DATABASE_CLIENT_MYSQL_PASSWORD=root npm run vona :bin:test -- commerce-trade/test/reservationExpiry.test.ts --flavor=normal
48
44
  working-directory: vona
49
45
  - run: DATABASE_DEFAULT_CLIENT=mysql DATABASE_CLIENT_MYSQL_HOST=127.0.0.1 DATABASE_CLIENT_MYSQL_PASSWORD=root npm run test
50
46
  working-directory: vona
@@ -35,16 +35,12 @@ jobs:
35
35
  - name: init
36
36
  run: npm run init
37
37
  - name: run Commerce stock contention gate
38
- run: >-
39
- DATABASE_DEFAULT_CLIENT=pg npm run vona :bin:test --
40
- commerce-trade/test/stockBalance.test.ts
41
- commerce-trade/test/stockReservation.test.ts
42
- commerce-trade/test/stockAudit.test.ts
43
- commerce-promotion/test/couponReservation.test.ts
44
- commerce-payment/test/paymentAttempt.test.ts
45
- commerce-trade/test/checkoutReservation.test.ts
46
- commerce-trade/test/reservationExpiry.test.ts
47
- --flavor=normal
38
+ run: |
39
+ DATABASE_DEFAULT_CLIENT=pg npm run vona :bin:test -- commerce-trade/test/stockBalance.test.ts commerce-trade/test/stockReservation.test.ts commerce-trade/test/stockAudit.test.ts --flavor=normal
40
+ DATABASE_DEFAULT_CLIENT=pg npm run vona :bin:test -- commerce-promotion/test/couponReservation.test.ts commerce-payment/test/paymentAttempt.test.ts --flavor=normal
41
+ DATABASE_DEFAULT_CLIENT=pg npm run vona :bin:test -- commerce-trade/test/checkoutReservation.test.ts commerce-trade/test/checkoutTransaction.test.ts --flavor=normal
42
+ DATABASE_DEFAULT_CLIENT=pg npm run vona :bin:test -- commerce-trade/test/orderSnapshot.test.ts --flavor=normal
43
+ DATABASE_DEFAULT_CLIENT=pg npm run vona :bin:test -- commerce-trade/test/reservationExpiry.test.ts --flavor=normal
48
44
  working-directory: vona
49
45
  - run: DATABASE_DEFAULT_CLIENT=pg npm run test
50
46
  working-directory: vona
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.116
4
+
5
+ ### Features
6
+
7
+ - Update platform capabilities and integrations.
8
+
9
+ ### Bug Fixes
10
+
11
+ - Stabilize Commerce trade transactions and associated tests.
12
+
13
+ ### Improvements
14
+
15
+ - Update address ownership test coverage.
16
+
17
+ ## 5.1.115
18
+
19
+ ### Improvements
20
+
21
+ - Refresh the Zova lockfile and core patch dependencies.
22
+
3
23
  ## 5.1.114
4
24
 
5
25
  ### Features
@@ -29,6 +29,32 @@ await this.scope.model.post.delete({ id: 1 });
29
29
 
30
30
  These operations are the clearest fit when the caller already knows the exact write intent.
31
31
 
32
+ ## Field presence in insert and update
33
+
34
+ For normal writable fields, insert and update intentionally interpret field presence differently:
35
+
36
+ | Field state | `insert` | `update` |
37
+ | -------------------------- | -------------------------------------------------- | -------------------------------- |
38
+ | Field is absent | Leave the column to normal insert/default behavior | Leave the stored value unchanged |
39
+ | Own field with `undefined` | Leave the column to normal insert/default behavior | Write SQL `NULL` |
40
+ | `null` | Write SQL `NULL` | Write SQL `NULL` |
41
+
42
+ An own `undefined` means the property exists on the JavaScript object but its value is `undefined`, such as `{ title: undefined }`. This lets an update distinguish an omitted field from an intentional clear:
43
+
44
+ ```typescript
45
+ await this.scope.model.post.update({
46
+ id,
47
+ title: undefined, // write SQL NULL
48
+ // stars is absent, so its stored value is unchanged
49
+ });
50
+ ```
51
+
52
+ ### JSON columns
53
+
54
+ JSON columns follow the same field-presence rules. Non-null values are persisted as JSON. An explicit `null` writes SQL `NULL`, not a JSON literal `null`. On update, an own `undefined` also clears the column to SQL `NULL`.
55
+
56
+ A JSON request body normally omits object properties whose value is `undefined`. This distinction therefore most often applies to in-process TypeScript model calls or server-side payload construction.
57
+
32
58
  ## Conditional update and delete paths
33
59
 
34
60
  Write operations do not have to target one row only by primary key.
@@ -56,7 +82,7 @@ await this.scope.model.post.delete({
56
82
  });
57
83
  ```
58
84
 
59
- That matters because the mutation layer still participates in the same structured query language used by select operations.
85
+ That matters because the mutation layer still participates in the same structured query language used by select operations. In `options.where`, an absent field, an own `undefined`, and `Op.omit` all omit that condition; `null` requests SQL `IS NULL`. See [ORM Select Guide](/backend/orm-select-guide#absent-undefined-and-null-in-where) for the detailed query contract.
60
86
 
61
87
  ## Bulk mutation operations
62
88
 
@@ -87,9 +113,12 @@ Instead of forcing callers to choose insert/update/delete up front, Vona can inf
87
113
 
88
114
  Representative logic:
89
115
 
90
- - no `id` → insert
91
- - `id` present → update
92
- - `id` present and `deleted: true` → delete
116
+ - `id` absent, `undefined`, or `null` → insert
117
+ - non-nullish `id` → update
118
+ - non-nullish `id` and `deleted: true` → delete
119
+ - `deleted: true` without a non-nullish `id` → ignore the item because no deletion target exists
120
+
121
+ A non-nullish identity is neither `null` nor `undefined`. `mutate` chooses by that usable identity value, not merely by whether the `id` key exists.
93
122
 
94
123
  Representative pattern:
95
124
 
@@ -182,13 +211,10 @@ The important rule is:
182
211
 
183
212
  There is also a useful argument-handling rule to remember:
184
213
 
185
- - default `eq` magic methods such as `getByName()` or `selectByName()` treat an omitted argument as `null`
186
- - non-`eq` magic methods such as `getByNameEqI()` require an explicit value and throw if the argument is omitted
187
-
188
- This aligns magic-method behavior with the current ORM `where` semantics:
214
+ - default `eq` magic methods such as `getByName()` or `selectByName()` treat an omitted or `undefined` argument as `null`
215
+ - non-`eq` magic methods such as `getByNameEqI()` require a concrete value and throw when the argument is omitted or `undefined`
189
216
 
190
- - `Op.omit` means omit a condition explicitly
191
- - `null` means SQL `IS NULL`
217
+ This is argument adaptation performed by the convenience method. It differs from a direct structured `where` object, where `{ name: undefined }` omits the condition. Use `Op.omit` to make direct condition omission explicit, and use `null` to request SQL `IS NULL`.
192
218
 
193
219
  That means mutation should stay conceptually grounded in the standard model methods even when convenience wrappers are present.
194
220
 
@@ -122,29 +122,34 @@ A practical operator-family reading is:
122
122
  - composition/subquery: `_and_`, `_or_`, `_not_`, `_exists_`, `_notExists_`
123
123
  - identifier helper: `_ref_`
124
124
 
125
- ### Omitting a condition with `Op.omit`
125
+ ### Absent, `undefined`, and `null` in `where`
126
126
 
127
- Use `Op.omit` when you want to express omission explicitly at the call site. `Op.omit` is defined as `undefined`, but using the named constant makes the business meaning of the query object clearer.
127
+ A direct `where` object distinguishes omitted conditions from SQL `NULL` checks:
128
+
129
+ | Field value | Meaning |
130
+ | -------------------------- | --------------------------------- |
131
+ | Field is absent | Do not add a condition |
132
+ | Own field with `undefined` | Do not add a condition |
133
+ | `Op.omit` | Explicitly do not add a condition |
134
+ | `null` | Match SQL `NULL` with `IS NULL` |
135
+
136
+ An own `undefined` means the property exists on the JavaScript object but its value is `undefined`, such as `{ title: undefined }`. It has the same query result as an absent field, while `Op.omit` is the readable way to make that omission intentional in a dynamically composed query.
128
137
 
129
138
  Representative pattern:
130
139
 
131
140
  ```typescript
132
141
  import { Op } from 'vona-module-a-orm';
133
142
 
134
- const where = {
135
- title: { _includes_: 'ai' },
136
- stars: { _gt_: 20 },
137
- };
138
-
139
143
  await this.scope.model.post.select({
140
144
  where: {
141
- ...where,
142
- stars: Op.omit,
145
+ title: undefined, // omitted
146
+ stars: Op.omit, // explicitly omitted
147
+ publishedAt: null, // SQL IS NULL
143
148
  },
144
149
  });
145
150
  ```
146
151
 
147
- This lets a query builder remove one condition cleanly without rebuilding the whole `where` object by hand, while `null` remains available for SQL `IS NULL` semantics.
152
+ The same direct-`where` rule applies to filters used by mutation, aggregate, and group operations.
148
153
 
149
154
  That distinction also matters at the request-contract layer: omitting a nullable filter and passing a real `null` are different operations. When a query DTO needs `IS NULL` behavior from frontend query params, the parsing contract must explicitly preserve `null` instead of collapsing it to omission.
150
155
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cabloy",
3
- "version": "5.1.114",
3
+ "version": "5.1.116",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A Node.js fullstack framework",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona",
3
- "version": "5.1.79",
3
+ "version": "5.1.80",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "Vona is an intuitive, elegant and powerful Node.js framework for rapidly developing enterprise applications of any size",
6
6
  "keywords": [
@@ -16,7 +16,7 @@ overrides:
16
16
  zod: npm:@cabloy/zod@4.3.8
17
17
 
18
18
  patchedDependencies:
19
- zova-core@5.1.78: ff659aa5f659be32501f56841b72f148c24acf1546f79cce551586e27fcde2ce
19
+ zova-core@5.1.79: ff659aa5f659be32501f56841b72f148c24acf1546f79cce551586e27fcde2ce
20
20
 
21
21
  importers:
22
22
 
@@ -534,11 +534,11 @@ importers:
534
534
  specifier: npm:@cabloy/zod@4.3.8
535
535
  version: '@cabloy/zod@4.3.8'
536
536
  zova:
537
- specifier: ^5.1.139
538
- version: 5.1.139(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3))
537
+ specifier: ^5.1.140
538
+ version: 5.1.140(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3))
539
539
  zova-jsx:
540
- specifier: ^1.1.78
541
- version: 1.1.78(typescript@5.9.3)
540
+ specifier: ^1.1.79
541
+ version: 1.1.79(typescript@5.9.3)
542
542
  zova-module-a-api:
543
543
  specifier: ^5.1.21
544
544
  version: 5.1.21
@@ -606,11 +606,11 @@ importers:
606
606
  specifier: npm:@cabloy/zod@4.3.8
607
607
  version: '@cabloy/zod@4.3.8'
608
608
  zova:
609
- specifier: ^5.1.139
610
- version: 5.1.139(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3))
609
+ specifier: ^5.1.140
610
+ version: 5.1.140(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3))
611
611
  zova-jsx:
612
- specifier: ^1.1.78
613
- version: 1.1.78(typescript@5.9.3)
612
+ specifier: ^1.1.79
613
+ version: 1.1.79(typescript@5.9.3)
614
614
  zova-module-a-api:
615
615
  specifier: ^5.1.21
616
616
  version: 5.1.21
@@ -678,11 +678,11 @@ importers:
678
678
  specifier: npm:@cabloy/zod@4.3.8
679
679
  version: '@cabloy/zod@4.3.8'
680
680
  zova:
681
- specifier: ^5.1.139
682
- version: 5.1.139(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3))
681
+ specifier: ^5.1.140
682
+ version: 5.1.140(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3))
683
683
  zova-jsx:
684
- specifier: ^1.1.78
685
- version: 1.1.78(typescript@5.9.3)
684
+ specifier: ^1.1.79
685
+ version: 1.1.79(typescript@5.9.3)
686
686
  zova-module-a-api:
687
687
  specifier: ^5.1.21
688
688
  version: 5.1.21
@@ -750,11 +750,11 @@ importers:
750
750
  specifier: npm:@cabloy/zod@4.3.8
751
751
  version: '@cabloy/zod@4.3.8'
752
752
  zova:
753
- specifier: ^5.1.139
754
- version: 5.1.139(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3))
753
+ specifier: ^5.1.140
754
+ version: 5.1.140(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3))
755
755
  zova-jsx:
756
- specifier: ^1.1.78
757
- version: 1.1.78(typescript@5.9.3)
756
+ specifier: ^1.1.79
757
+ version: 1.1.79(typescript@5.9.3)
758
758
  zova-module-a-api:
759
759
  specifier: ^5.1.21
760
760
  version: 5.1.21
@@ -8236,11 +8236,11 @@ packages:
8236
8236
  peerDependencies:
8237
8237
  zod: ^3.25.28 || ^4
8238
8238
 
8239
- zova-core@5.1.78:
8240
- resolution: {integrity: sha512-PWvgOujRI1k7TpsJy1G8/Wf7W+/Rm6QbGHPNwcRjgDz4PhTg9z8UN1hfIO1Gg37IAI2hIrK8kqHhFRayROQHgQ==}
8239
+ zova-core@5.1.79:
8240
+ resolution: {integrity: sha512-q04JWAjwa5x0py9/FCFPX9I8cNA4zzZ/Z+8mm2mVsRBMqDFcafwVqNumOAkZ+DuA10b4YIoncyQKYz5PhhmBUw==}
8241
8241
 
8242
- zova-jsx@1.1.78:
8243
- resolution: {integrity: sha512-KToLi7h2uElT4ULd5QSXfLNqSA3PaeRKJ4IHNMEJU1xiH02j2u0KCd7iVYK54mQC1QYphan3Z/YXFuW0odPx8w==}
8242
+ zova-jsx@1.1.79:
8243
+ resolution: {integrity: sha512-iFp9UpW/3Cbaq0H25xyR04LYyaqB2wlAGOL1dEL7OyuIzHI0IHcBwfZRbRGy7ewgDVHit2ULG03kO1sSjjPuWQ==}
8244
8244
 
8245
8245
  zova-module-a-api@5.1.21:
8246
8246
  resolution: {integrity: sha512-qjm/hfjC4/+7Ap/uzJfBzV0PwRlp2idEmEKwCZYxquaXq6QtJBbTWTznw1iukT4lkJZip2KFkjOvXTzdvZA2UA==}
@@ -8314,17 +8314,17 @@ packages:
8314
8314
  zova-module-a-zod@5.1.36:
8315
8315
  resolution: {integrity: sha512-4gEjxJBelVOA84aP2XWSNSmzQr+AYh14Ha5QCj9yZIpSnEajSgYLoyLw4dwiVcscuEw7xAAOOnWbZS9uXSpJzg==}
8316
8316
 
8317
- zova-module-a-zova@5.1.93:
8318
- resolution: {integrity: sha512-pZ7JGNvDoSLdgbQZLj/LtppDWinpd6W+u0WYQvXLuJH3Hnf0Satcpz5fQS+zseJLxm5jhXjmG3ngoYoOfv72RQ==}
8317
+ zova-module-a-zova@5.1.94:
8318
+ resolution: {integrity: sha512-c2qbu3YiNYRSMpprPkBbZ9RaQ9KKF4Y/cEumDJaCpPoZEbJClI/WBZMrY/ZpG9Rsp/87DJ63Vkbu+iSTOh7csg==}
8319
8319
 
8320
8320
  zova-module-rest-resource@5.1.42:
8321
8321
  resolution: {integrity: sha512-eMWhc4428U5Hs4BHqFJoF7yaH+bK8t71scIrCnpLbQS7M80lNkoHxY9s/Kl4i8Uo3+lVWbSbuaXWtR+zVLNqpA==}
8322
8322
 
8323
- zova-suite-a-zova@5.1.138:
8324
- resolution: {integrity: sha512-KiX37VxKcZxEi+inDva3iTpeA90vNlEGxBvLqDMPgBYIpkiX8iAoGZmEHhV5iKtbvFoq3gcbGAki+iO6R8mtCw==}
8323
+ zova-suite-a-zova@5.1.139:
8324
+ resolution: {integrity: sha512-LZzCJOXj6njLL91cBgyzThRo+1aOvkBag1XIq4QR6R28t2mrFEdhnvPXBIqcvPpalyZJYwwD+8H/eyE2V/biNg==}
8325
8325
 
8326
- zova@5.1.139:
8327
- resolution: {integrity: sha512-6gA4pfdrSv6ZLp/aeE2nZx8Tu6KwKpuHrIbmZ3zrdHazYRkz51l4y//fBLk45lIfiR7uCIlKb9ziFdvOQQ3F7A==}
8326
+ zova@5.1.140:
8327
+ resolution: {integrity: sha512-T8x4YEvkX32lGeeP5rgw7/Y4au10gxqaIHl2nvZlD5Cn9EoLvHfEENpPov8a4cWeQtActoxSTNb3D3WYHE/zpg==}
8328
8328
 
8329
8329
  zwitch@1.0.5:
8330
8330
  resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==}
@@ -14143,7 +14143,7 @@ snapshots:
14143
14143
  dependencies:
14144
14144
  zod: '@cabloy/zod@4.3.8'
14145
14145
 
14146
- zova-core@5.1.78(patch_hash=ff659aa5f659be32501f56841b72f148c24acf1546f79cce551586e27fcde2ce)(typescript@5.9.3):
14146
+ zova-core@5.1.79(patch_hash=ff659aa5f659be32501f56841b72f148c24acf1546f79cce551586e27fcde2ce)(typescript@5.9.3):
14147
14147
  dependencies:
14148
14148
  '@cabloy/compose': link:packages-utils/compose
14149
14149
  '@cabloy/extend': link:packages-utils/extend
@@ -14172,14 +14172,14 @@ snapshots:
14172
14172
  transitivePeerDependencies:
14173
14173
  - typescript
14174
14174
 
14175
- zova-jsx@1.1.78(typescript@5.9.3):
14175
+ zova-jsx@1.1.79(typescript@5.9.3):
14176
14176
  dependencies:
14177
14177
  '@cabloy/compose': link:packages-utils/compose
14178
14178
  '@cabloy/utils': link:packages-utils/utils
14179
14179
  '@cabloy/word-utils': 2.1.14
14180
14180
  typestyle: 2.4.0
14181
14181
  vue: 3.5.39(typescript@5.9.3)
14182
- zova-core: 5.1.78(patch_hash=ff659aa5f659be32501f56841b72f148c24acf1546f79cce551586e27fcde2ce)(typescript@5.9.3)
14182
+ zova-core: 5.1.79(patch_hash=ff659aa5f659be32501f56841b72f148c24acf1546f79cce551586e27fcde2ce)(typescript@5.9.3)
14183
14183
  transitivePeerDependencies:
14184
14184
  - typescript
14185
14185
 
@@ -14281,7 +14281,7 @@ snapshots:
14281
14281
  '@cabloy/zod-query': link:packages-utils/zod-query
14282
14282
  zod: '@cabloy/zod@4.3.8'
14283
14283
 
14284
- zova-module-a-zova@5.1.93(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3)):
14284
+ zova-module-a-zova@5.1.94(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3)):
14285
14285
  dependencies:
14286
14286
  '@cabloy/compose': link:packages-utils/compose
14287
14287
  '@cabloy/deps': link:packages-utils/deps
@@ -14292,14 +14292,14 @@ snapshots:
14292
14292
  '@cabloy/word-utils': 2.1.14
14293
14293
  defu: 6.1.7
14294
14294
  luxon: 3.7.2
14295
- zova-jsx: 1.1.78(typescript@5.9.3)
14295
+ zova-jsx: 1.1.79(typescript@5.9.3)
14296
14296
  transitivePeerDependencies:
14297
14297
  - typescript
14298
14298
  - vue
14299
14299
 
14300
14300
  zova-module-rest-resource@5.1.42: {}
14301
14301
 
14302
- zova-suite-a-zova@5.1.138(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3)):
14302
+ zova-suite-a-zova@5.1.139(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3)):
14303
14303
  dependencies:
14304
14304
  zova-module-a-api: 5.1.21
14305
14305
  zova-module-a-app: 5.1.25
@@ -14325,7 +14325,7 @@ snapshots:
14325
14325
  zova-module-a-style: 5.1.32
14326
14326
  zova-module-a-table: 5.1.37(vue@3.5.39(typescript@5.9.3))
14327
14327
  zova-module-a-zod: 5.1.36
14328
- zova-module-a-zova: 5.1.93(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3))
14328
+ zova-module-a-zova: 5.1.94(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3))
14329
14329
  transitivePeerDependencies:
14330
14330
  - '@vue/composition-api'
14331
14331
  - debug
@@ -14333,10 +14333,10 @@ snapshots:
14333
14333
  - typescript
14334
14334
  - vue
14335
14335
 
14336
- zova@5.1.139(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3)):
14336
+ zova@5.1.140(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3)):
14337
14337
  dependencies:
14338
- zova-core: 5.1.78(patch_hash=ff659aa5f659be32501f56841b72f148c24acf1546f79cce551586e27fcde2ce)(typescript@5.9.3)
14339
- zova-suite-a-zova: 5.1.138(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3))
14338
+ zova-core: 5.1.79(patch_hash=ff659aa5f659be32501f56841b72f148c24acf1546f79cce551586e27fcde2ce)(typescript@5.9.3)
14339
+ zova-suite-a-zova: 5.1.139(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3))
14340
14340
  transitivePeerDependencies:
14341
14341
  - '@vue/composition-api'
14342
14342
  - debug
@@ -53,4 +53,4 @@ allowBuilds:
53
53
  vue-demi: true
54
54
 
55
55
  patchedDependencies:
56
- zova-core@5.1.78: patches/zova-core@5.1.78.patch
56
+ zova-core@5.1.79: patches/zova-core@5.1.79.patch
@@ -26,7 +26,7 @@ async function viewSku(id: TableIdentity): Promise<EntitySku> {
26
26
  });
27
27
  }
28
28
 
29
- describe('sku.test.ts', () => {
29
+ describe('sku.test.ts', { concurrency: false }, () => {
30
30
  it('action:sku', async () => {
31
31
  await app.bean.executor.mockCtx(async () => {
32
32
  await app.bean.passport.signinMock();
@@ -91,7 +91,10 @@ describe('address.test.ts', () => {
91
91
  true,
92
92
  );
93
93
 
94
- const update: DtoAddressUpdate = { ...addressData, city: 'Oakland', addressLine2: undefined };
94
+ const { addressLine2: _, ...update } = {
95
+ ...addressData,
96
+ city: 'Oakland',
97
+ } satisfies DtoAddressUpdate;
95
98
  assert.equal(
96
99
  await performAs(customerA.token, 'patch', `${actionPath}/:id`, {
97
100
  params: { id: addressId },
@@ -14,7 +14,7 @@ export class DtoCouponIssue {
14
14
  @Api.field(v.required(), v.tableIdentity())
15
15
  userId: TableIdentity;
16
16
 
17
- @Api.field(v.required(), v.min(1), v.max(100))
17
+ @Api.field(v.required(), v.min(1), v.max(80))
18
18
  correlationId: string;
19
19
 
20
20
  @Api.field(v.required(), v.min(1), v.max(255))
@@ -42,7 +42,7 @@ const serializationRetryOptions = {
42
42
  minTimeout: 0,
43
43
  maxTimeout: 0,
44
44
  randomize: false,
45
- errorCodes: ['40001'],
45
+ errorCodes: ['40001', 'ER_LOCK_DEADLOCK', 'ER_LOCK_WAIT_TIMEOUT'],
46
46
  ownerOnly: true,
47
47
  };
48
48
 
@@ -52,6 +52,9 @@ export class ServiceCoupon extends BeanBase {
52
52
  @Core.retryable(serializationRetryOptions)
53
53
  async issue(command: ICouponIssueCommand): Promise<EntityCouponGrant> {
54
54
  this._assertCommand(command);
55
+ if (command.correlationId.length > 80) {
56
+ this.app.throw(400, 'coupon issuance correlationId is too long');
57
+ }
55
58
  const template = await this.scope.model.couponTemplate.getByIdForUpdate(command.templateId);
56
59
  if (!template) this.app.throw(404, 'coupon template not found');
57
60
  this._assertTemplateIssuable(template);
@@ -39,14 +39,29 @@ describe('couponReservation.test.ts', { concurrency: false }, () => {
39
39
  minSpendCents: 1000,
40
40
  validFrom: new Date(Date.now() - 1_000),
41
41
  validUntil: new Date(Date.now() + 60_000),
42
- totalIssueLimit: 1,
42
+ totalIssueLimit: 2,
43
43
  totalUsageLimit: 1,
44
- perCustomerIssueLimit: 1,
44
+ perCustomerIssueLimit: 3,
45
45
  issuedCount: 0,
46
46
  redeemedCount: 0,
47
47
  })
48
48
  ).id as number;
49
49
  const coupon = app.scope('commerce-promotion').service.coupon;
50
+ const [, issuanceLengthError] = await catchError(() =>
51
+ coupon.issue({
52
+ templateId: fixture.templateId!,
53
+ userId: customer.id,
54
+ correlationId: 'x'.repeat(81),
55
+ reason: 'test overlong issue correlation',
56
+ }),
57
+ );
58
+ assert.equal(issuanceLengthError?.code, 400);
59
+ assert.equal(
60
+ await app
61
+ .scope('commerce-promotion')
62
+ .model.couponGrant.get({ couponCode: 'x'.repeat(81) }),
63
+ undefined,
64
+ );
50
65
  const grant = await coupon.issue({
51
66
  templateId: fixture.templateId,
52
67
  userId: customer.id,
@@ -56,6 +71,20 @@ describe('couponReservation.test.ts', { concurrency: false }, () => {
56
71
  fixture.grantId = grant.id as number;
57
72
  assert.equal(grant.state, 'available');
58
73
  assert.equal(grant.discountCentsSnapshot, 500);
74
+ const exactLimitGrant = await coupon.issue({
75
+ templateId: fixture.templateId,
76
+ userId: customer.id,
77
+ correlationId: 'a'.repeat(80),
78
+ reason: 'test exact issue correlation limit',
79
+ });
80
+ assert.equal(exactLimitGrant.couponCode.length, 80);
81
+ await app.scope('commerce-promotion').model.couponAudit.delete({
82
+ couponGrantId: exactLimitGrant.id,
83
+ });
84
+ await app.scope('commerce-promotion').model.couponGrant.delete({ id: exactLimitGrant.id });
85
+ await app.scope('commerce-promotion').model.couponTemplate.updateById(fixture.templateId, {
86
+ issuedCount: 1,
87
+ });
59
88
 
60
89
  const [_, belowMinimumError] = await catchError(() =>
61
90
  coupon.reserve({
@@ -95,6 +124,25 @@ describe('couponReservation.test.ts', { concurrency: false }, () => {
95
124
  reason: 'test reserve',
96
125
  });
97
126
  assert.equal(replay.couponGrant.id, grant.id);
127
+ const [, wrongCustomerError] = await catchError(() =>
128
+ coupon.reserve({
129
+ couponGrantId: grant.id,
130
+ userId: customer.id + 10_000,
131
+ orderId: 100_001,
132
+ eligibleSubtotalCents: 1_200,
133
+ currency: 'USD',
134
+ correlationId: `reserve-wrong-customer-${suffix}`,
135
+ reason: 'test wrong customer',
136
+ }),
137
+ );
138
+ assert.equal(wrongCustomerError?.code, 404);
139
+ const reserveAuditsBeforeRelease = await app
140
+ .scope('commerce-promotion')
141
+ .model.couponAudit.select({ where: { couponGrantId: grant.id } });
142
+ assert.deepEqual(
143
+ reserveAuditsBeforeRelease.map(item => item.operation),
144
+ ['issue', 'reserve'],
145
+ );
98
146
 
99
147
  const released = await coupon.release({
100
148
  couponGrantId: grant.id,
@@ -103,6 +151,12 @@ describe('couponReservation.test.ts', { concurrency: false }, () => {
103
151
  reason: 'test release',
104
152
  });
105
153
  assert.equal(released.state, 'available');
154
+ const releasedPersisted = await app
155
+ .scope('commerce-promotion')
156
+ .model.couponGrant.getById(grant.id);
157
+ assert.equal(releasedPersisted?.reservationOrderId, null);
158
+ assert.equal(releasedPersisted?.reservationCorrelationId, null);
159
+ assert.equal(releasedPersisted?.reservedAt, null);
106
160
  const releaseReplay = await coupon.release({
107
161
  couponGrantId: grant.id,
108
162
  orderId: 100_001,
@@ -124,6 +178,35 @@ describe('couponReservation.test.ts', { concurrency: false }, () => {
124
178
  reason: 'test reserve again',
125
179
  });
126
180
  assert.equal(reservedAgain.discountCents, 500);
181
+ const anotherGrant = await coupon.issue({
182
+ templateId: fixture.templateId,
183
+ userId: customer.id,
184
+ correlationId: `issue-another-${suffix}`,
185
+ reason: 'test second grant',
186
+ });
187
+ const [, secondCouponError] = await catchError(() =>
188
+ coupon.reserve({
189
+ couponGrantId: anotherGrant.id,
190
+ userId: customer.id,
191
+ orderId: 100_002,
192
+ eligibleSubtotalCents: 1_200,
193
+ currency: 'USD',
194
+ correlationId: `reserve-another-${suffix}`,
195
+ reason: 'test second coupon',
196
+ }),
197
+ );
198
+ assert.equal(secondCouponError?.code, 409);
199
+ assert.equal(
200
+ (await app.scope('commerce-promotion').model.couponGrant.getById(anotherGrant.id))?.state,
201
+ 'available',
202
+ );
203
+ await app.scope('commerce-promotion').model.couponAudit.delete({
204
+ couponGrantId: anotherGrant.id,
205
+ });
206
+ await app.scope('commerce-promotion').model.couponGrant.delete({ id: anotherGrant.id });
207
+ await app.scope('commerce-promotion').model.couponTemplate.updateById(fixture.templateId, {
208
+ issuedCount: 1,
209
+ });
127
210
  const redeemed = await coupon.redeem({
128
211
  couponGrantId: grant.id,
129
212
  orderId: 100_002,
@@ -10,6 +10,16 @@ import type { DtoCartUpdateItem } from '../dto/cartUpdateItem.tsx';
10
10
  import type { DtoCartView } from '../dto/cartView.tsx';
11
11
  import type { EntityCart } from '../entity/cart.tsx';
12
12
 
13
+ const serializationRetryOptions = {
14
+ retries: 1,
15
+ factor: 1,
16
+ minTimeout: 0,
17
+ maxTimeout: 0,
18
+ randomize: false,
19
+ errorCodes: ['40001', 'ER_LOCK_DEADLOCK', 'ER_LOCK_WAIT_TIMEOUT'],
20
+ ownerOnly: true,
21
+ };
22
+
13
23
  @Service()
14
24
  export class ServiceCart extends BeanBase {
15
25
  async current(): Promise<DtoCartView> {
@@ -18,6 +28,7 @@ export class ServiceCart extends BeanBase {
18
28
  }
19
29
 
20
30
  @Core.transaction({ isolationLevel: 'SERIALIZABLE' })
31
+ @Core.retryable(serializationRetryOptions)
21
32
  async addItem(command: DtoCartAddItem): Promise<DtoCartView> {
22
33
  await this._assertSellableSku(command.skuId, command.quantity);
23
34
  const cart = await this._getOrCreateCurrentCart();
@@ -40,6 +51,7 @@ export class ServiceCart extends BeanBase {
40
51
  }
41
52
 
42
53
  @Core.transaction({ isolationLevel: 'SERIALIZABLE' })
54
+ @Core.retryable(serializationRetryOptions)
43
55
  async updateItem(id: TableIdentity, command: DtoCartUpdateItem): Promise<DtoCartView> {
44
56
  const cart = await this._getCurrentCartForUpdate();
45
57
  if (!cart) return { items: [] };
@@ -51,6 +63,7 @@ export class ServiceCart extends BeanBase {
51
63
  }
52
64
 
53
65
  @Core.transaction({ isolationLevel: 'SERIALIZABLE' })
66
+ @Core.retryable(serializationRetryOptions)
54
67
  async deleteItem(id: TableIdentity): Promise<DtoCartView> {
55
68
  const cart = await this._getCurrentCartForUpdate();
56
69
  if (!cart) return { items: [] };
@@ -60,6 +73,7 @@ export class ServiceCart extends BeanBase {
60
73
  }
61
74
 
62
75
  @Core.transaction({ isolationLevel: 'SERIALIZABLE' })
76
+ @Core.retryable(serializationRetryOptions)
63
77
  async clear(): Promise<DtoCartView> {
64
78
  const cart = await this._getCurrentCartForUpdate();
65
79
  if (!cart) return { items: [] };