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
|
@@ -20,7 +20,12 @@ const serializationRetryOptions = {
|
|
|
20
20
|
minTimeout: 0,
|
|
21
21
|
maxTimeout: 0,
|
|
22
22
|
randomize: false,
|
|
23
|
-
errorCodes: ['40001'],
|
|
23
|
+
errorCodes: ['40001', 'ER_LOCK_DEADLOCK', 'ER_LOCK_WAIT_TIMEOUT'],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const checkoutSerializationRetryOptions = {
|
|
27
|
+
...serializationRetryOptions,
|
|
28
|
+
retries: 3,
|
|
24
29
|
};
|
|
25
30
|
|
|
26
31
|
export interface IOrderSnapshotLineCommand {
|
|
@@ -40,6 +45,16 @@ export interface IOrderSnapshotCreateResult {
|
|
|
40
45
|
lines: EntityOrderLine[];
|
|
41
46
|
}
|
|
42
47
|
|
|
48
|
+
export type CheckoutFailureStage =
|
|
49
|
+
| 'afterOrderSnapshot'
|
|
50
|
+
| 'afterCouponReservation'
|
|
51
|
+
| 'afterStockReservation'
|
|
52
|
+
| 'afterPaymentAttempt'
|
|
53
|
+
| 'afterOrderAudit'
|
|
54
|
+
| 'afterCartMutation';
|
|
55
|
+
|
|
56
|
+
type CheckoutStageCallback = (stage: CheckoutFailureStage) => void | Promise<void>;
|
|
57
|
+
|
|
43
58
|
interface IPreparedOrderLine {
|
|
44
59
|
index: number;
|
|
45
60
|
skuId: TableIdentity;
|
|
@@ -157,8 +172,24 @@ export class ServiceOrder extends BeanBase {
|
|
|
157
172
|
}
|
|
158
173
|
|
|
159
174
|
@Core.transaction({ isolationLevel: 'SERIALIZABLE' })
|
|
160
|
-
@Core.retryable(
|
|
175
|
+
@Core.retryable(checkoutSerializationRetryOptions)
|
|
161
176
|
async checkout(command: DtoCheckoutCreate): Promise<DtoCheckoutResult> {
|
|
177
|
+
return await this._checkout(command);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@Core.transaction({ isolationLevel: 'SERIALIZABLE' })
|
|
181
|
+
@Core.retryable(serializationRetryOptions)
|
|
182
|
+
async checkoutForTest(
|
|
183
|
+
command: DtoCheckoutCreate,
|
|
184
|
+
onStage: CheckoutStageCallback,
|
|
185
|
+
): Promise<DtoCheckoutResult> {
|
|
186
|
+
return await this._checkout(command, onStage);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private async _checkout(
|
|
190
|
+
command: DtoCheckoutCreate,
|
|
191
|
+
onStage?: CheckoutStageCallback,
|
|
192
|
+
): Promise<DtoCheckoutResult> {
|
|
162
193
|
this._assertCheckoutCommand(command);
|
|
163
194
|
const userId = this.bean.passport.currentUser!.id;
|
|
164
195
|
const existingOrder = await this.scope.model.order.getForUpdate({
|
|
@@ -211,6 +242,7 @@ export class ServiceOrder extends BeanBase {
|
|
|
211
242
|
couponGrantId: command.couponGrantId,
|
|
212
243
|
preparedLines,
|
|
213
244
|
reason: 'checkout created',
|
|
245
|
+
onStage,
|
|
214
246
|
});
|
|
215
247
|
const paymentAttempt = await this.$scope.commercePayment.service.paymentAttempt.create({
|
|
216
248
|
orderId: order.id,
|
|
@@ -219,6 +251,7 @@ export class ServiceOrder extends BeanBase {
|
|
|
219
251
|
amountCents: order.payableTotalCents,
|
|
220
252
|
correlationId: `${command.correlationId}:payment`,
|
|
221
253
|
});
|
|
254
|
+
await onStage?.('afterPaymentAttempt');
|
|
222
255
|
await this._appendAudit({
|
|
223
256
|
orderId: order.id,
|
|
224
257
|
operation: 'created',
|
|
@@ -226,9 +259,11 @@ export class ServiceOrder extends BeanBase {
|
|
|
226
259
|
correlationId: command.correlationId,
|
|
227
260
|
reason: 'checkout created',
|
|
228
261
|
});
|
|
262
|
+
await onStage?.('afterOrderAudit');
|
|
229
263
|
for (const item of lockedCartItems) {
|
|
230
264
|
await this.scope.model.cartItem.deleteById(item.id);
|
|
231
265
|
}
|
|
266
|
+
await onStage?.('afterCartMutation');
|
|
232
267
|
return {
|
|
233
268
|
orderId: order.id,
|
|
234
269
|
paymentAttemptId: paymentAttempt.id,
|
|
@@ -315,6 +350,7 @@ export class ServiceOrder extends BeanBase {
|
|
|
315
350
|
couponGrantId,
|
|
316
351
|
preparedLines,
|
|
317
352
|
reason,
|
|
353
|
+
onStage,
|
|
318
354
|
}: {
|
|
319
355
|
userId: TableIdentity;
|
|
320
356
|
address: {
|
|
@@ -332,6 +368,7 @@ export class ServiceOrder extends BeanBase {
|
|
|
332
368
|
couponGrantId?: TableIdentity;
|
|
333
369
|
preparedLines: IPreparedOrderLine[];
|
|
334
370
|
reason: string;
|
|
371
|
+
onStage?: CheckoutStageCallback;
|
|
335
372
|
}): Promise<IOrderSnapshotCreateResult> {
|
|
336
373
|
const eligibleSubtotalCents = preparedLines.reduce((total, line) => {
|
|
337
374
|
const nextTotal = total + line.eligibleSubtotalCents;
|
|
@@ -352,6 +389,7 @@ export class ServiceOrder extends BeanBase {
|
|
|
352
389
|
payableTotalCents: eligibleSubtotalCents,
|
|
353
390
|
reservationExpiresAt: new Date(Date.now() + 30 * 60 * 1000),
|
|
354
391
|
});
|
|
392
|
+
await onStage?.('afterOrderSnapshot');
|
|
355
393
|
if (couponGrantId) {
|
|
356
394
|
const reservation = await this.$scope.commercePromotion.service.coupon.reserve({
|
|
357
395
|
couponGrantId,
|
|
@@ -381,6 +419,7 @@ export class ServiceOrder extends BeanBase {
|
|
|
381
419
|
order.couponSnapshot = couponSnapshot;
|
|
382
420
|
order.discountCents = reservation.discountCents;
|
|
383
421
|
order.payableTotalCents = eligibleSubtotalCents - reservation.discountCents;
|
|
422
|
+
await onStage?.('afterCouponReservation');
|
|
384
423
|
}
|
|
385
424
|
const lines: EntityOrderLine[] = [];
|
|
386
425
|
for (const line of preparedLines) {
|
|
@@ -403,6 +442,7 @@ export class ServiceOrder extends BeanBase {
|
|
|
403
442
|
correlationId: `${correlationId}:line:${line.index}`,
|
|
404
443
|
reason,
|
|
405
444
|
});
|
|
445
|
+
await onStage?.('afterStockReservation');
|
|
406
446
|
lines.push(orderLine);
|
|
407
447
|
}
|
|
408
448
|
return { order, lines };
|
|
@@ -34,7 +34,7 @@ const serializationRetryOptions = {
|
|
|
34
34
|
minTimeout: 0,
|
|
35
35
|
maxTimeout: 0,
|
|
36
36
|
randomize: false,
|
|
37
|
-
errorCodes: ['40001'],
|
|
37
|
+
errorCodes: ['40001', 'ER_LOCK_DEADLOCK', 'ER_LOCK_WAIT_TIMEOUT'],
|
|
38
38
|
ownerOnly: true,
|
|
39
39
|
};
|
|
40
40
|
|
|
@@ -49,6 +49,7 @@ export class ServiceStockBalance extends BeanBase {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
@Core.transaction({ isolationLevel: 'SERIALIZABLE' })
|
|
52
|
+
@Core.retryable(serializationRetryOptions)
|
|
52
53
|
async adjustStock(stockAdjust: DtoStockAdjust): Promise<EntityStockBalance> {
|
|
53
54
|
const sku = await this.$scope.commerceCatalog.model.sku.getById(stockAdjust.skuId);
|
|
54
55
|
if (!sku) {
|
|
@@ -65,7 +66,7 @@ export class ServiceStockBalance extends BeanBase {
|
|
|
65
66
|
};
|
|
66
67
|
if (!stockBalance) {
|
|
67
68
|
if (stockAdjust.delta < 0) {
|
|
68
|
-
throw
|
|
69
|
+
this.app.throw(409, 'stock adjustment would make balance negative');
|
|
69
70
|
}
|
|
70
71
|
stockBalance = await this.scope.model.stockBalance.insert({
|
|
71
72
|
skuId: stockAdjust.skuId,
|
|
@@ -78,7 +79,7 @@ export class ServiceStockBalance extends BeanBase {
|
|
|
78
79
|
const reserved = stockBalance.reserved;
|
|
79
80
|
const available = onHand - reserved;
|
|
80
81
|
if (onHand < 0 || reserved < 0 || available < 0) {
|
|
81
|
-
throw
|
|
82
|
+
this.app.throw(409, 'stock adjustment would make balance negative');
|
|
82
83
|
}
|
|
83
84
|
await this.scope.model.stockBalance.updateById(stockBalance.id, {
|
|
84
85
|
onHand,
|
|
@@ -3,11 +3,19 @@ import type { DtoCartAddItem, DtoCartUpdateItem } from 'vona-module-commerce-tra
|
|
|
3
3
|
import { catchError } from '@cabloy/utils';
|
|
4
4
|
import assert from 'node:assert';
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
|
-
import { describe, it } from 'node:test';
|
|
6
|
+
import { after, before, describe, it } from 'node:test';
|
|
7
7
|
import { app } from 'vona-mock';
|
|
8
8
|
|
|
9
|
+
import { acquireTestLock } from './testLock.ts';
|
|
10
|
+
|
|
9
11
|
const actionPath = '/commerce/trade/cart';
|
|
10
12
|
|
|
13
|
+
interface ISellableSkuFixture {
|
|
14
|
+
categoryId: number;
|
|
15
|
+
productId: number;
|
|
16
|
+
skuId: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
function createTestId() {
|
|
12
20
|
return randomUUID().slice(0, 12);
|
|
13
21
|
}
|
|
@@ -27,12 +35,12 @@ async function performAs(accessToken: string, method: string, path: string, opti
|
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
async function registerAndSignin(name: string) {
|
|
30
|
-
await app.bean.user.register({ name }, true);
|
|
38
|
+
const user = await app.bean.user.register({ name }, true);
|
|
31
39
|
const token = await app.bean.passport.signinMock(name as any);
|
|
32
|
-
return { token: token.accessToken, user
|
|
40
|
+
return { token: token.accessToken, user };
|
|
33
41
|
}
|
|
34
42
|
|
|
35
|
-
async function createSellableSku(suffix: string) {
|
|
43
|
+
async function createSellableSku(suffix: string): Promise<ISellableSkuFixture> {
|
|
36
44
|
await app.bean.passport.signinMock();
|
|
37
45
|
try {
|
|
38
46
|
const categoryId = await app.bean.executor.performAction('post', '/commerce/catalog/category', {
|
|
@@ -57,13 +65,47 @@ async function createSellableSku(suffix: string) {
|
|
|
57
65
|
correlationId: `cart-stock-${suffix}`,
|
|
58
66
|
},
|
|
59
67
|
});
|
|
60
|
-
return skuId;
|
|
68
|
+
return { categoryId, productId, skuId };
|
|
61
69
|
} finally {
|
|
62
70
|
await app.bean.passport.signout();
|
|
63
71
|
}
|
|
64
72
|
}
|
|
65
73
|
|
|
74
|
+
async function dropSellableSku(fixture: ISellableSkuFixture | undefined) {
|
|
75
|
+
if (!fixture) return;
|
|
76
|
+
const trade = app.scope('commerce-trade');
|
|
77
|
+
const catalog = app.scope('commerce-catalog');
|
|
78
|
+
await trade.model.stockAudit.delete({ skuId: fixture.skuId });
|
|
79
|
+
await trade.model.stockReservation.delete({ skuId: fixture.skuId });
|
|
80
|
+
await trade.model.stockBalance.delete({ skuId: fixture.skuId });
|
|
81
|
+
await catalog.model.sku.delete({ id: fixture.skuId });
|
|
82
|
+
await catalog.model.product.delete({ id: fixture.productId });
|
|
83
|
+
await catalog.model.category.delete({ id: fixture.categoryId });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function dropUser(userId: number | undefined) {
|
|
87
|
+
if (userId === undefined) return;
|
|
88
|
+
const trade = app.scope('commerce-trade');
|
|
89
|
+
const carts = await trade.model.cart.select({ where: { userId } });
|
|
90
|
+
for (const cart of carts) {
|
|
91
|
+
await trade.model.cartItem.delete({ cartId: cart.id });
|
|
92
|
+
await trade.model.cart.delete({ id: cart.id });
|
|
93
|
+
}
|
|
94
|
+
await app.scope('home-user').model.roleUser.delete({ userId });
|
|
95
|
+
await app.bean.user.removeById(userId);
|
|
96
|
+
}
|
|
97
|
+
|
|
66
98
|
describe('cartOwnership.test.ts', { concurrency: false }, () => {
|
|
99
|
+
let releaseTestLock: (() => void) | undefined;
|
|
100
|
+
|
|
101
|
+
before(async () => {
|
|
102
|
+
releaseTestLock = await acquireTestLock();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
after(() => {
|
|
106
|
+
releaseTestLock?.();
|
|
107
|
+
});
|
|
108
|
+
|
|
67
109
|
it('denies anonymous cart actions', async () => {
|
|
68
110
|
await app.bean.executor.mockCtx(async () => {
|
|
69
111
|
for (const [method, path, options] of [
|
|
@@ -84,96 +126,134 @@ describe('cartOwnership.test.ts', { concurrency: false }, () => {
|
|
|
84
126
|
it('derives cart ownership from the authenticated customer and scopes every item action', async () => {
|
|
85
127
|
await app.bean.executor.mockCtx(async () => {
|
|
86
128
|
const suffix = createTestId();
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
assert.equal(cart.items.length, 1);
|
|
95
|
-
assert.equal(cart.items[0].quantity, 2);
|
|
96
|
-
assert.equal(Object.hasOwn(cart, 'userId'), false);
|
|
97
|
-
assert.equal(Object.hasOwn(cart, 'iid'), false);
|
|
98
|
-
const itemId = cart.items[0].id;
|
|
99
|
-
|
|
100
|
-
const persistedCart = await app.scope('commerce-trade').model.cart.getById(cart.id);
|
|
101
|
-
assert.equal(String(persistedCart?.userId), String(customerA.user.id));
|
|
102
|
-
assert.equal(persistedCart?.iid, app.ctx.instance.id);
|
|
103
|
-
const persistedItem = await app.scope('commerce-trade').model.cartItem.getById(itemId);
|
|
104
|
-
assert.equal(String(persistedItem?.cartId), String(cart.id));
|
|
105
|
-
|
|
106
|
-
const updated = await performAs(customerA.token, 'patch', `${actionPath}/items/:id`, {
|
|
107
|
-
params: { id: itemId },
|
|
108
|
-
body: { quantity: 3 } satisfies DtoCartUpdateItem,
|
|
109
|
-
});
|
|
110
|
-
assert.equal(updated.items[0].quantity, 3);
|
|
111
|
-
|
|
112
|
-
assert.deepEqual(await performAs(customerB.token, 'get', actionPath), { items: [] });
|
|
113
|
-
assert.deepEqual(
|
|
114
|
-
await performAs(customerB.token, 'patch', `${actionPath}/items/:id`, {
|
|
115
|
-
params: { id: itemId },
|
|
116
|
-
body: { quantity: 1 },
|
|
117
|
-
}),
|
|
118
|
-
{ items: [] },
|
|
119
|
-
);
|
|
120
|
-
assert.deepEqual(
|
|
121
|
-
await performAs(customerB.token, 'delete', `${actionPath}/items/:id`, {
|
|
122
|
-
params: { id: itemId },
|
|
123
|
-
}),
|
|
124
|
-
{ items: [] },
|
|
125
|
-
);
|
|
126
|
-
assert.equal((await app.scope('commerce-trade').model.cartItem.getById(itemId))?.quantity, 3);
|
|
129
|
+
let sku: ISellableSkuFixture | undefined;
|
|
130
|
+
let customerA: Awaited<ReturnType<typeof registerAndSignin>> | undefined;
|
|
131
|
+
let customerB: Awaited<ReturnType<typeof registerAndSignin>> | undefined;
|
|
132
|
+
try {
|
|
133
|
+
sku = await createSellableSku(suffix);
|
|
134
|
+
customerA = await registerAndSignin(`cart-a-${suffix}`);
|
|
135
|
+
customerB = await registerAndSignin(`cart-b-${suffix}`);
|
|
127
136
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
const cart = await performAs(customerA.token, 'post', `${actionPath}/items`, {
|
|
138
|
+
body: { ...cartItem(sku.skuId, 2), userId: customerB.user.id, iid: 999999 } as any,
|
|
139
|
+
});
|
|
140
|
+
assert.equal(cart.items.length, 1);
|
|
141
|
+
assert.equal(cart.items[0].quantity, 2);
|
|
142
|
+
assert.equal(Object.hasOwn(cart, 'userId'), false);
|
|
143
|
+
assert.equal(Object.hasOwn(cart, 'iid'), false);
|
|
144
|
+
const itemId = cart.items[0].id;
|
|
133
145
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
146
|
+
const persistedCart = await app.scope('commerce-trade').model.cart.getById(cart.id);
|
|
147
|
+
assert.equal(String(persistedCart?.userId), String(customerA.user.id));
|
|
148
|
+
assert.equal(persistedCart?.iid, app.ctx.instance.id);
|
|
149
|
+
const persistedItem = await app.scope('commerce-trade').model.cartItem.getById(itemId);
|
|
150
|
+
assert.equal(String(persistedItem?.cartId), String(cart.id));
|
|
138
151
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const skuId = await createSellableSku(suffix);
|
|
145
|
-
customerDefault = await registerAndSignin(`cart-default-${suffix}`);
|
|
146
|
-
const cart = await performAs(customerDefault.token, 'post', `${actionPath}/items`, {
|
|
147
|
-
body: cartItem(skuId),
|
|
148
|
-
});
|
|
149
|
-
itemId = cart.items[0].id;
|
|
150
|
-
});
|
|
152
|
+
const updated = await performAs(customerA.token, 'patch', `${actionPath}/items/:id`, {
|
|
153
|
+
params: { id: itemId },
|
|
154
|
+
body: { quantity: 3 } satisfies DtoCartUpdateItem,
|
|
155
|
+
});
|
|
156
|
+
assert.equal(updated.items[0].quantity, 3);
|
|
151
157
|
|
|
152
|
-
|
|
153
|
-
async () => {
|
|
154
|
-
const customer = await registerAndSignin(`cart-share-${Date.now()}`);
|
|
155
|
-
assert.deepEqual(await performAs(customer.token, 'get', actionPath), { items: [] });
|
|
158
|
+
assert.deepEqual(await performAs(customerB.token, 'get', actionPath), { items: [] });
|
|
156
159
|
assert.deepEqual(
|
|
157
|
-
await performAs(
|
|
160
|
+
await performAs(customerB.token, 'patch', `${actionPath}/items/:id`, {
|
|
158
161
|
params: { id: itemId },
|
|
159
|
-
body: { quantity:
|
|
162
|
+
body: { quantity: 1 },
|
|
160
163
|
}),
|
|
161
164
|
{ items: [] },
|
|
162
165
|
);
|
|
163
166
|
assert.deepEqual(
|
|
164
|
-
await performAs(
|
|
167
|
+
await performAs(customerB.token, 'delete', `${actionPath}/items/:id`, {
|
|
165
168
|
params: { id: itemId },
|
|
166
169
|
}),
|
|
167
170
|
{ items: [] },
|
|
168
171
|
);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
+
assert.equal(
|
|
173
|
+
(await app.scope('commerce-trade').model.cartItem.getById(itemId))?.quantity,
|
|
174
|
+
3,
|
|
175
|
+
);
|
|
172
176
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
+
const merged = await performAs(customerA.token, 'post', `${actionPath}/items`, {
|
|
178
|
+
body: cartItem(sku.skuId, 2),
|
|
179
|
+
});
|
|
180
|
+
assert.equal(merged.items.length, 1);
|
|
181
|
+
assert.equal(merged.items[0].quantity, 5);
|
|
182
|
+
|
|
183
|
+
const cleared = await performAs(customerA.token, 'delete', `${actionPath}/items`);
|
|
184
|
+
assert.deepEqual(cleared.items, []);
|
|
185
|
+
} finally {
|
|
186
|
+
await app.bean.passport.signout();
|
|
187
|
+
await dropUser(customerB?.user.id as number | undefined);
|
|
188
|
+
await dropUser(customerA?.user.id as number | undefined);
|
|
189
|
+
await dropSellableSku(sku);
|
|
190
|
+
}
|
|
177
191
|
});
|
|
178
192
|
});
|
|
193
|
+
|
|
194
|
+
it('treats another instance cart as absent', async () => {
|
|
195
|
+
const suffix = createTestId();
|
|
196
|
+
let itemId!: number;
|
|
197
|
+
let customerDefault: Awaited<ReturnType<typeof registerAndSignin>> | undefined;
|
|
198
|
+
let customerShare: Awaited<ReturnType<typeof registerAndSignin>> | undefined;
|
|
199
|
+
let sku: ISellableSkuFixture | undefined;
|
|
200
|
+
try {
|
|
201
|
+
await app.bean.executor.mockCtx(async () => {
|
|
202
|
+
sku = await createSellableSku(suffix);
|
|
203
|
+
customerDefault = await registerAndSignin(`cart-default-${suffix}`);
|
|
204
|
+
const cart = await performAs(customerDefault.token, 'post', `${actionPath}/items`, {
|
|
205
|
+
body: cartItem(sku.skuId),
|
|
206
|
+
});
|
|
207
|
+
itemId = cart.items[0].id;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await app.bean.executor.mockCtx(
|
|
211
|
+
async () => {
|
|
212
|
+
try {
|
|
213
|
+
customerShare = await registerAndSignin(`cart-share-${suffix}`);
|
|
214
|
+
assert.deepEqual(await performAs(customerShare.token, 'get', actionPath), {
|
|
215
|
+
items: [],
|
|
216
|
+
});
|
|
217
|
+
assert.deepEqual(
|
|
218
|
+
await performAs(customerShare.token, 'patch', `${actionPath}/items/:id`, {
|
|
219
|
+
params: { id: itemId },
|
|
220
|
+
body: { quantity: 2 },
|
|
221
|
+
}),
|
|
222
|
+
{ items: [] },
|
|
223
|
+
);
|
|
224
|
+
assert.deepEqual(
|
|
225
|
+
await performAs(customerShare.token, 'delete', `${actionPath}/items/:id`, {
|
|
226
|
+
params: { id: itemId },
|
|
227
|
+
}),
|
|
228
|
+
{ items: [] },
|
|
229
|
+
);
|
|
230
|
+
} finally {
|
|
231
|
+
await app.bean.passport.signout();
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
{ instanceName: 'shareTest' as any },
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
await app.bean.executor.mockCtx(async () => {
|
|
238
|
+
try {
|
|
239
|
+
const cart = await performAs(customerDefault!.token, 'get', actionPath);
|
|
240
|
+
assert.equal(cart.items.length, 1);
|
|
241
|
+
assert.equal(cart.items[0].id, itemId);
|
|
242
|
+
} finally {
|
|
243
|
+
await app.bean.passport.signout();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
} finally {
|
|
247
|
+
await app.bean.executor.mockCtx(
|
|
248
|
+
async () => {
|
|
249
|
+
await dropUser(customerShare?.user.id as number | undefined);
|
|
250
|
+
},
|
|
251
|
+
{ instanceName: 'shareTest' as any },
|
|
252
|
+
);
|
|
253
|
+
await app.bean.executor.mockCtx(async () => {
|
|
254
|
+
await dropUser(customerDefault?.user.id as number | undefined);
|
|
255
|
+
await dropSellableSku(sku);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
});
|
|
179
259
|
});
|