cabloy 5.1.115 → 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 +14 -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 +8 -124
- 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/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/.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,19 @@
|
|
|
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
|
+
|
|
3
17
|
## 5.1.115
|
|
4
18
|
|
|
5
19
|
### Improvements
|
|
@@ -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
|
@@ -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,15 +8236,9 @@ 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==}
|
|
8241
|
-
|
|
8242
8239
|
zova-core@5.1.79:
|
|
8243
8240
|
resolution: {integrity: sha512-q04JWAjwa5x0py9/FCFPX9I8cNA4zzZ/Z+8mm2mVsRBMqDFcafwVqNumOAkZ+DuA10b4YIoncyQKYz5PhhmBUw==}
|
|
8244
8241
|
|
|
8245
|
-
zova-jsx@1.1.78:
|
|
8246
|
-
resolution: {integrity: sha512-KToLi7h2uElT4ULd5QSXfLNqSA3PaeRKJ4IHNMEJU1xiH02j2u0KCd7iVYK54mQC1QYphan3Z/YXFuW0odPx8w==}
|
|
8247
|
-
|
|
8248
8242
|
zova-jsx@1.1.79:
|
|
8249
8243
|
resolution: {integrity: sha512-iFp9UpW/3Cbaq0H25xyR04LYyaqB2wlAGOL1dEL7OyuIzHI0IHcBwfZRbRGy7ewgDVHit2ULG03kO1sSjjPuWQ==}
|
|
8250
8244
|
|
|
@@ -8320,24 +8314,15 @@ packages:
|
|
|
8320
8314
|
zova-module-a-zod@5.1.36:
|
|
8321
8315
|
resolution: {integrity: sha512-4gEjxJBelVOA84aP2XWSNSmzQr+AYh14Ha5QCj9yZIpSnEajSgYLoyLw4dwiVcscuEw7xAAOOnWbZS9uXSpJzg==}
|
|
8322
8316
|
|
|
8323
|
-
zova-module-a-zova@5.1.93:
|
|
8324
|
-
resolution: {integrity: sha512-pZ7JGNvDoSLdgbQZLj/LtppDWinpd6W+u0WYQvXLuJH3Hnf0Satcpz5fQS+zseJLxm5jhXjmG3ngoYoOfv72RQ==}
|
|
8325
|
-
|
|
8326
8317
|
zova-module-a-zova@5.1.94:
|
|
8327
8318
|
resolution: {integrity: sha512-c2qbu3YiNYRSMpprPkBbZ9RaQ9KKF4Y/cEumDJaCpPoZEbJClI/WBZMrY/ZpG9Rsp/87DJ63Vkbu+iSTOh7csg==}
|
|
8328
8319
|
|
|
8329
8320
|
zova-module-rest-resource@5.1.42:
|
|
8330
8321
|
resolution: {integrity: sha512-eMWhc4428U5Hs4BHqFJoF7yaH+bK8t71scIrCnpLbQS7M80lNkoHxY9s/Kl4i8Uo3+lVWbSbuaXWtR+zVLNqpA==}
|
|
8331
8322
|
|
|
8332
|
-
zova-suite-a-zova@5.1.138:
|
|
8333
|
-
resolution: {integrity: sha512-KiX37VxKcZxEi+inDva3iTpeA90vNlEGxBvLqDMPgBYIpkiX8iAoGZmEHhV5iKtbvFoq3gcbGAki+iO6R8mtCw==}
|
|
8334
|
-
|
|
8335
8323
|
zova-suite-a-zova@5.1.139:
|
|
8336
8324
|
resolution: {integrity: sha512-LZzCJOXj6njLL91cBgyzThRo+1aOvkBag1XIq4QR6R28t2mrFEdhnvPXBIqcvPpalyZJYwwD+8H/eyE2V/biNg==}
|
|
8337
8325
|
|
|
8338
|
-
zova@5.1.139:
|
|
8339
|
-
resolution: {integrity: sha512-6gA4pfdrSv6ZLp/aeE2nZx8Tu6KwKpuHrIbmZ3zrdHazYRkz51l4y//fBLk45lIfiR7uCIlKb9ziFdvOQQ3F7A==}
|
|
8340
|
-
|
|
8341
8326
|
zova@5.1.140:
|
|
8342
8327
|
resolution: {integrity: sha512-T8x4YEvkX32lGeeP5rgw7/Y4au10gxqaIHl2nvZlD5Cn9EoLvHfEENpPov8a4cWeQtActoxSTNb3D3WYHE/zpg==}
|
|
8343
8328
|
|
|
@@ -14158,35 +14143,6 @@ snapshots:
|
|
|
14158
14143
|
dependencies:
|
|
14159
14144
|
zod: '@cabloy/zod@4.3.8'
|
|
14160
14145
|
|
|
14161
|
-
zova-core@5.1.78(typescript@5.9.3):
|
|
14162
|
-
dependencies:
|
|
14163
|
-
'@cabloy/compose': link:packages-utils/compose
|
|
14164
|
-
'@cabloy/extend': link:packages-utils/extend
|
|
14165
|
-
'@cabloy/json5': link:packages-utils/json5
|
|
14166
|
-
'@cabloy/localeutil': link:packages-utils/localeutil
|
|
14167
|
-
'@cabloy/logger': 1.1.24
|
|
14168
|
-
'@cabloy/module-info': 2.0.3
|
|
14169
|
-
'@cabloy/utils': link:packages-utils/utils
|
|
14170
|
-
'@cabloy/vue-compiler-sfc': 3.5.14
|
|
14171
|
-
'@cabloy/vue-reactivity': 3.5.16
|
|
14172
|
-
'@cabloy/vue-router': 4.4.16(vue@3.5.39(typescript@5.9.3))
|
|
14173
|
-
'@cabloy/vue-runtime-core': 3.5.57
|
|
14174
|
-
'@cabloy/vue-runtime-dom': 3.5.13
|
|
14175
|
-
'@cabloy/vue-server-renderer': 3.5.18(vue@3.5.39(typescript@5.9.3))
|
|
14176
|
-
'@cabloy/word-utils': 2.1.14
|
|
14177
|
-
'@cabloy/zod-errors-custom': link:packages-utils/zod-errors-custom
|
|
14178
|
-
'@cabloy/zod-openapi': link:packages-utils/zod-openapi
|
|
14179
|
-
'@cabloy/zod-query': link:packages-utils/zod-query
|
|
14180
|
-
'@types/js-cookie': 3.0.6
|
|
14181
|
-
'@vue/shared': 3.5.36
|
|
14182
|
-
deep-equal: 2.2.3
|
|
14183
|
-
reflect-metadata: 0.2.2
|
|
14184
|
-
vue: 3.5.39(typescript@5.9.3)
|
|
14185
|
-
vue-plugin-render-freeze: 2.0.4(vue@3.5.39(typescript@5.9.3))
|
|
14186
|
-
zod: '@cabloy/zod@4.3.8'
|
|
14187
|
-
transitivePeerDependencies:
|
|
14188
|
-
- typescript
|
|
14189
|
-
|
|
14190
14146
|
zova-core@5.1.79(patch_hash=ff659aa5f659be32501f56841b72f148c24acf1546f79cce551586e27fcde2ce)(typescript@5.9.3):
|
|
14191
14147
|
dependencies:
|
|
14192
14148
|
'@cabloy/compose': link:packages-utils/compose
|
|
@@ -14216,17 +14172,6 @@ snapshots:
|
|
|
14216
14172
|
transitivePeerDependencies:
|
|
14217
14173
|
- typescript
|
|
14218
14174
|
|
|
14219
|
-
zova-jsx@1.1.78(typescript@5.9.3):
|
|
14220
|
-
dependencies:
|
|
14221
|
-
'@cabloy/compose': link:packages-utils/compose
|
|
14222
|
-
'@cabloy/utils': link:packages-utils/utils
|
|
14223
|
-
'@cabloy/word-utils': 2.1.14
|
|
14224
|
-
typestyle: 2.4.0
|
|
14225
|
-
vue: 3.5.39(typescript@5.9.3)
|
|
14226
|
-
zova-core: 5.1.78(typescript@5.9.3)
|
|
14227
|
-
transitivePeerDependencies:
|
|
14228
|
-
- typescript
|
|
14229
|
-
|
|
14230
14175
|
zova-jsx@1.1.79(typescript@5.9.3):
|
|
14231
14176
|
dependencies:
|
|
14232
14177
|
'@cabloy/compose': link:packages-utils/compose
|
|
@@ -14336,22 +14281,6 @@ snapshots:
|
|
|
14336
14281
|
'@cabloy/zod-query': link:packages-utils/zod-query
|
|
14337
14282
|
zod: '@cabloy/zod@4.3.8'
|
|
14338
14283
|
|
|
14339
|
-
zova-module-a-zova@5.1.93(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3)):
|
|
14340
|
-
dependencies:
|
|
14341
|
-
'@cabloy/compose': link:packages-utils/compose
|
|
14342
|
-
'@cabloy/deps': link:packages-utils/deps
|
|
14343
|
-
'@cabloy/json5': link:packages-utils/json5
|
|
14344
|
-
'@cabloy/module-info': 2.0.3
|
|
14345
|
-
'@cabloy/utils': link:packages-utils/utils
|
|
14346
|
-
'@cabloy/vue-router': 4.4.16(vue@3.5.39(typescript@5.9.3))
|
|
14347
|
-
'@cabloy/word-utils': 2.1.14
|
|
14348
|
-
defu: 6.1.7
|
|
14349
|
-
luxon: 3.7.2
|
|
14350
|
-
zova-jsx: 1.1.78(typescript@5.9.3)
|
|
14351
|
-
transitivePeerDependencies:
|
|
14352
|
-
- typescript
|
|
14353
|
-
- vue
|
|
14354
|
-
|
|
14355
14284
|
zova-module-a-zova@5.1.94(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3)):
|
|
14356
14285
|
dependencies:
|
|
14357
14286
|
'@cabloy/compose': link:packages-utils/compose
|
|
@@ -14370,40 +14299,6 @@ snapshots:
|
|
|
14370
14299
|
|
|
14371
14300
|
zova-module-rest-resource@5.1.42: {}
|
|
14372
14301
|
|
|
14373
|
-
zova-suite-a-zova@5.1.138(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3)):
|
|
14374
|
-
dependencies:
|
|
14375
|
-
zova-module-a-api: 5.1.21
|
|
14376
|
-
zova-module-a-app: 5.1.25
|
|
14377
|
-
zova-module-a-bean: 5.1.32
|
|
14378
|
-
zova-module-a-behavior: 5.1.25
|
|
14379
|
-
zova-module-a-behaviors: 5.1.21
|
|
14380
|
-
zova-module-a-boundary: 5.1.21
|
|
14381
|
-
zova-module-a-command: 5.1.33
|
|
14382
|
-
zova-module-a-fetch: 5.1.24
|
|
14383
|
-
zova-module-a-form: 5.1.45(vue@3.5.39(typescript@5.9.3))
|
|
14384
|
-
zova-module-a-icon: 5.1.26
|
|
14385
|
-
zova-module-a-interceptor: 5.1.29
|
|
14386
|
-
zova-module-a-logger: 5.1.26
|
|
14387
|
-
zova-module-a-meta: 5.1.21
|
|
14388
|
-
zova-module-a-model: 5.1.33(vue@3.5.39(typescript@5.9.3))
|
|
14389
|
-
zova-module-a-openapi: 5.1.46
|
|
14390
|
-
zova-module-a-router: 5.1.31
|
|
14391
|
-
zova-module-a-routerstack: 5.1.26
|
|
14392
|
-
zova-module-a-routertabs: 5.1.33
|
|
14393
|
-
zova-module-a-ssr: 5.1.31
|
|
14394
|
-
zova-module-a-ssrhmr: 5.1.22
|
|
14395
|
-
zova-module-a-ssrserver: 5.1.23
|
|
14396
|
-
zova-module-a-style: 5.1.32
|
|
14397
|
-
zova-module-a-table: 5.1.37(vue@3.5.39(typescript@5.9.3))
|
|
14398
|
-
zova-module-a-zod: 5.1.36
|
|
14399
|
-
zova-module-a-zova: 5.1.93(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3))
|
|
14400
|
-
transitivePeerDependencies:
|
|
14401
|
-
- '@vue/composition-api'
|
|
14402
|
-
- debug
|
|
14403
|
-
- supports-color
|
|
14404
|
-
- typescript
|
|
14405
|
-
- vue
|
|
14406
|
-
|
|
14407
14302
|
zova-suite-a-zova@5.1.139(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3)):
|
|
14408
14303
|
dependencies:
|
|
14409
14304
|
zova-module-a-api: 5.1.21
|
|
@@ -14438,17 +14333,6 @@ snapshots:
|
|
|
14438
14333
|
- typescript
|
|
14439
14334
|
- vue
|
|
14440
14335
|
|
|
14441
|
-
zova@5.1.139(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3)):
|
|
14442
|
-
dependencies:
|
|
14443
|
-
zova-core: 5.1.78(typescript@5.9.3)
|
|
14444
|
-
zova-suite-a-zova: 5.1.138(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3))
|
|
14445
|
-
transitivePeerDependencies:
|
|
14446
|
-
- '@vue/composition-api'
|
|
14447
|
-
- debug
|
|
14448
|
-
- supports-color
|
|
14449
|
-
- typescript
|
|
14450
|
-
- vue
|
|
14451
|
-
|
|
14452
14336
|
zova@5.1.140(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3)):
|
|
14453
14337
|
dependencies:
|
|
14454
14338
|
zova-core: 5.1.79(patch_hash=ff659aa5f659be32501f56841b72f148c24acf1546f79cce551586e27fcde2ce)(typescript@5.9.3)
|
|
@@ -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: [] };
|