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.
- package/.cabloy-version +1 -1
- package/.github/workflows/vona-test-mysql.yml +6 -10
- package/.github/workflows/vona-test-pg.yml +6 -10
- package/CHANGELOG.md +20 -0
- package/cabloy-docs/backend/orm-mutation-guide.md +36 -10
- package/cabloy-docs/backend/orm-select-guide.md +15 -10
- package/package.json +1 -1
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/pnpm-lock.yaml +37 -37
- package/vona/pnpm-workspace.yaml +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/test/sku.test.ts +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-member/test/addressOwnership.test.ts +4 -1
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponIssue.tsx +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/service/coupon.ts +4 -1
- package/vona/src/suite/a-commerce/modules/commerce-promotion/test/couponReservation.test.ts +85 -2
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/cart.ts +14 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +42 -2
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/stockBalance.ts +4 -3
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/cartOwnership.test.ts +160 -80
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/checkoutReservation.test.ts +255 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/checkoutTransaction.test.ts +208 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/orderSnapshot.test.ts +28 -4
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/reservationExpiry.test.ts +90 -8
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockBalance.test.ts +15 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockReservation.test.ts +20 -2
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/testLock.ts +11 -0
- package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_crud_inner.ts +3 -0
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_utils.ts +1 -1
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/pnpm-lock.yaml +31 -31
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/schemas.ts +40 -32
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/types.ts +638 -633
- /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.
|
|
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-
|
|
41
|
-
commerce-trade/test/
|
|
42
|
-
commerce-trade/test/
|
|
43
|
-
commerce-
|
|
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-
|
|
41
|
-
commerce-trade/test/
|
|
42
|
-
commerce-trade/test/
|
|
43
|
-
commerce-
|
|
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
|
-
-
|
|
91
|
-
- `id`
|
|
92
|
-
- `id`
|
|
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
|
|
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
|
-
|
|
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
|
-
###
|
|
125
|
+
### Absent, `undefined`, and `null` in `where`
|
|
126
126
|
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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": "vona",
|
|
3
|
-
"version": "5.1.
|
|
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": [
|
package/vona/pnpm-lock.yaml
CHANGED
|
@@ -16,7 +16,7 @@ overrides:
|
|
|
16
16
|
zod: npm:@cabloy/zod@4.3.8
|
|
17
17
|
|
|
18
18
|
patchedDependencies:
|
|
19
|
-
zova-core@5.1.
|
|
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.
|
|
538
|
-
version: 5.1.
|
|
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.
|
|
541
|
-
version: 1.1.
|
|
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.
|
|
610
|
-
version: 5.1.
|
|
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.
|
|
613
|
-
version: 1.1.
|
|
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.
|
|
682
|
-
version: 5.1.
|
|
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.
|
|
685
|
-
version: 1.1.
|
|
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.
|
|
754
|
-
version: 5.1.
|
|
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.
|
|
757
|
-
version: 1.1.
|
|
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.
|
|
8240
|
-
resolution: {integrity: sha512-
|
|
8239
|
+
zova-core@5.1.79:
|
|
8240
|
+
resolution: {integrity: sha512-q04JWAjwa5x0py9/FCFPX9I8cNA4zzZ/Z+8mm2mVsRBMqDFcafwVqNumOAkZ+DuA10b4YIoncyQKYz5PhhmBUw==}
|
|
8241
8241
|
|
|
8242
|
-
zova-jsx@1.1.
|
|
8243
|
-
resolution: {integrity: sha512-
|
|
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.
|
|
8318
|
-
resolution: {integrity: sha512-
|
|
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.
|
|
8324
|
-
resolution: {integrity: sha512-
|
|
8323
|
+
zova-suite-a-zova@5.1.139:
|
|
8324
|
+
resolution: {integrity: sha512-LZzCJOXj6njLL91cBgyzThRo+1aOvkBag1XIq4QR6R28t2mrFEdhnvPXBIqcvPpalyZJYwwD+8H/eyE2V/biNg==}
|
|
8325
8325
|
|
|
8326
|
-
zova@5.1.
|
|
8327
|
-
resolution: {integrity: sha512-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
14339
|
-
zova-suite-a-zova: 5.1.
|
|
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
|
package/vona/pnpm-workspace.yaml
CHANGED
|
@@ -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
|
|
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(
|
|
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:
|
|
42
|
+
totalIssueLimit: 2,
|
|
43
43
|
totalUsageLimit: 1,
|
|
44
|
-
perCustomerIssueLimit:
|
|
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: [] };
|