cabloy 5.1.124 → 5.1.126
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/.claude/skills/cabloy-backend-scaffold/SKILL.md +1 -1
- package/.claude/skills/cabloy-backend-scaffold/evals/evals.json +6 -0
- package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +13 -4
- package/.claude/skills/cabloy-workflow/SKILL.md +4 -2
- package/.claude/skills/cabloy-workflow/evals/evals.json +14 -2
- package/.claude/skills/cabloy-worktree-environment/SKILL.md +206 -0
- package/.claude/skills/cabloy-worktree-environment/evals/evals.json +77 -0
- package/.github/workflows/vona-test-pg.yml +3 -0
- package/.github/workflows/vona-test-sqlite3.yml +3 -0
- package/CHANGELOG.md +21 -0
- package/CLAUDE.md +1 -1
- package/cabloy-docs/.vitepress/config.mjs +2 -0
- package/cabloy-docs/ai/playbook-technical-blog-authoring.md +158 -0
- package/cabloy-docs/backend/backend-contract-emission-specimen.md +2 -2
- package/cabloy-docs/backend/dto-guide.md +10 -7
- package/cabloy-docs/backend/dto-infer-generation.md +127 -7
- package/cabloy-docs/backend/foundation.md +1 -1
- package/cabloy-docs/backend/introduction.md +1 -0
- package/cabloy-docs/backend/module-dependencies.md +104 -0
- package/cabloy-docs/backend/serialization-guide.md +6 -0
- package/cabloy-docs/fullstack/parallel-worktree-environment.md +72 -34
- package/cabloy-docs/fullstack/suites-and-modules.md +1 -0
- package/cabloy-docs/reference/package-map.md +1 -0
- package/e2e/specs/a-commerce/commerce.spec.ts +89 -0
- package/package.json +2 -1
- package/scripts/init.ts +7 -12
- package/scripts/initAppName.test.ts +72 -0
- package/scripts/initAppName.ts +50 -0
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/pnpm-lock.yaml +48 -146
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineCreate.tsx +6 -27
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineItem.tsx +6 -31
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineUpdate.tsx +6 -2
- package/vona/src/suite/a-commerce/modules/commerce-member/src/lib/addressMine.ts +16 -0
- package/vona/src/suite/a-commerce/modules/commerce-member/src/service/address.ts +4 -17
- package/vona/src/suite/a-commerce/modules/commerce-member/test/addressOwnership.test.ts +63 -19
- package/vona/src/suite/a-commerce/modules/commerce-payment/package.json +2 -1
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/payScene.commerceOrder.ts +11 -2
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/commercePayScene.ts +26 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentAttempt.test.ts +19 -2
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/en-us.ts +5 -2
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/zh-cn.ts +5 -2
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateCreate.tsx +26 -35
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateUpdate.tsx +6 -1
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/entity/couponTemplate.tsx +40 -4
- package/vona/src/suite/a-commerce/modules/commerce-promotion/test/couponTemplateFormLayout.test.ts +85 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/order.ts +3 -5
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderSummary.tsx +7 -40
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/shipmentView.tsx +6 -15
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/lib/order.ts +9 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +121 -11
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/order.test.ts +25 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +19 -2
- package/vona/src/suite/a-training/modules/training-record/src/dto/detailRecordSubjectResItem.tsx +6 -1
- package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectResItem.tsx +3 -1
- package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +4 -1
- package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +45 -0
- package/vona/src/suite/a-training/modules/training-student/src/dto/detailRecordResItem.tsx +6 -1
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentSummary.tsx +5 -23
- package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +37 -5
- package/vona/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/.metadata/index.ts +29 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.index.ts +3 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.redlock.ts +3 -1
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.version.ts +7 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/queue.outboxDispatch.ts +9 -4
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/schedule.providerOperationDispatch.ts +11 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/outboxEvent.tsx +11 -4
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/providerOperation.tsx +12 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/webhookInbox.tsx +6 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/outbox.ts +12 -5
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/paymentSession.ts +1 -37
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/providerOperation.ts +355 -1
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/refundOperation.ts +226 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/webhook.ts +148 -50
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payScene.ts +2 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/.metadata/index.ts +39 -22
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/bean/payProvider.mock.ts +42 -13
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/controller/mockPayment.ts +11 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/dto/mockRefundComplete.tsx +13 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/dto/mockRefundReceipt.tsx +16 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/service/payMock.ts +56 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/package.json +2 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/bean/payProvider.paypal.ts +117 -8
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/package.json +5 -5
- 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/dto/dtoGet.ts +5 -5
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/schemas.ts +54 -12
- package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/types.ts +625 -64
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/types.ts +55 -15
- package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +1 -1
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeOrder.ts +15 -17
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +12 -8
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +111 -26
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeOrder.ts +3 -3
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionRefund/controller.tsx +1 -8
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +5 -9
- package/zova/src/suite/a-training/modules/training-student/src/.metadata/index.ts +1 -1
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +301 -17
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +5461 -1788
- package/vona/src/suite-vendor/a-pay/modules/a-pay/test/outbox.test.ts +0 -209
- package/vona/src/suite-vendor/a-pay/modules/a-pay/test/paymentSession.test.ts +0 -153
- package/vona/src/suite-vendor/a-pay/modules/a-pay/test/webhook.test.ts +0 -278
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/test/payMock.test.ts +0 -141
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TableIdentity } from 'table-identity';
|
|
2
|
-
import type { IQueryParams
|
|
2
|
+
import type { IQueryParams } from 'vona-module-a-orm';
|
|
3
3
|
|
|
4
4
|
import { BeanBase } from 'vona';
|
|
5
5
|
import { Service } from 'vona-module-a-bean';
|
|
@@ -10,22 +10,9 @@ import type { DtoAddressMineUpdate } from '../dto/addressMineUpdate.tsx';
|
|
|
10
10
|
import type { DtoAddressMineView } from '../dto/addressMineView.tsx';
|
|
11
11
|
import type { DtoAddressSelectRes } from '../dto/addressSelectRes.tsx';
|
|
12
12
|
import type { DtoAddressView } from '../dto/addressView.tsx';
|
|
13
|
-
import type { EntityAddress } from '../entity/address.tsx';
|
|
14
13
|
import type { ModelAddress } from '../model/address.ts';
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
'id',
|
|
18
|
-
'recipientName',
|
|
19
|
-
'phone',
|
|
20
|
-
'countryCode',
|
|
21
|
-
'region',
|
|
22
|
-
'city',
|
|
23
|
-
'postalCode',
|
|
24
|
-
'addressLine1',
|
|
25
|
-
'addressLine2',
|
|
26
|
-
'createdAt',
|
|
27
|
-
'updatedAt',
|
|
28
|
-
];
|
|
15
|
+
import { addressMineReadColumns } from '../lib/addressMine.ts';
|
|
29
16
|
|
|
30
17
|
@Service()
|
|
31
18
|
export class ServiceAddress extends BeanBase {
|
|
@@ -43,7 +30,7 @@ export class ServiceAddress extends BeanBase {
|
|
|
43
30
|
async mine(params?: IQueryParams<ModelAddress>): Promise<DtoAddressMineRes> {
|
|
44
31
|
return await this.scope.model.address.selectAndCount({
|
|
45
32
|
...params,
|
|
46
|
-
columns:
|
|
33
|
+
columns: addressMineReadColumns,
|
|
47
34
|
where: { ...params?.where, userId: this._getCurrentUserId() },
|
|
48
35
|
orders: params?.orders ?? [['id', 'asc']],
|
|
49
36
|
});
|
|
@@ -79,7 +66,7 @@ export class ServiceAddress extends BeanBase {
|
|
|
79
66
|
private async _getOwnedAddress(id: TableIdentity) {
|
|
80
67
|
return await this.scope.model.address.get(
|
|
81
68
|
{ id, userId: this._getCurrentUserId() },
|
|
82
|
-
{ columns:
|
|
69
|
+
{ columns: addressMineReadColumns },
|
|
83
70
|
);
|
|
84
71
|
}
|
|
85
72
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
DtoAddressMineCreate,
|
|
3
|
-
DtoAddressMineUpdate,
|
|
4
|
-
EntityAddress,
|
|
5
|
-
} from 'vona-module-commerce-member';
|
|
6
|
-
|
|
7
1
|
import { catchError } from '@cabloy/utils';
|
|
8
2
|
import assert from 'node:assert';
|
|
9
3
|
import { randomUUID } from 'node:crypto';
|
|
10
4
|
import { describe, it } from 'node:test';
|
|
11
5
|
import { app } from 'vona-mock';
|
|
6
|
+
import {
|
|
7
|
+
DtoAddressMineCreate,
|
|
8
|
+
DtoAddressMineItem,
|
|
9
|
+
DtoAddressMineUpdate,
|
|
10
|
+
DtoAddressMineView,
|
|
11
|
+
} from 'vona-module-commerce-member';
|
|
12
12
|
|
|
13
13
|
const actionPath = '/commerce/member/address';
|
|
14
14
|
const minePath = `${actionPath}/mine`;
|
|
@@ -16,6 +16,7 @@ const viewMinePath = `${actionPath}/viewMine/:id`;
|
|
|
16
16
|
const createMinePath = `${actionPath}/createMine`;
|
|
17
17
|
const updateMinePath = `${actionPath}/updateMine/:id`;
|
|
18
18
|
const deleteMinePath = `${actionPath}/deleteMine/:id`;
|
|
19
|
+
const addressMineExcludedFields = ['userId', 'iid', 'deleted', 'createdAt', 'updatedAt'] as const;
|
|
19
20
|
|
|
20
21
|
function createTestId() {
|
|
21
22
|
return randomUUID().slice(0, 12);
|
|
@@ -57,6 +58,38 @@ async function deleteOwnedAddresses(ids: Array<number | string | undefined>) {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
describe('addressOwnership.test.ts', () => {
|
|
61
|
+
it('emits aligned Mine DTO schemas', async () => {
|
|
62
|
+
await app.bean.executor.mockCtx(async () => {
|
|
63
|
+
const expectedWriteFields = [
|
|
64
|
+
'recipientName',
|
|
65
|
+
'phone',
|
|
66
|
+
'countryCode',
|
|
67
|
+
'region',
|
|
68
|
+
'city',
|
|
69
|
+
'postalCode',
|
|
70
|
+
'addressLine1',
|
|
71
|
+
'addressLine2',
|
|
72
|
+
].sort();
|
|
73
|
+
const expectedRequiredFields = expectedWriteFields.filter(name => name !== 'addressLine2');
|
|
74
|
+
for (const DtoClass of [DtoAddressMineCreate, DtoAddressMineUpdate]) {
|
|
75
|
+
const apiJson = await app.bean.openapi.generateJsonOfClass(DtoClass);
|
|
76
|
+
const component = Object.values(apiJson.components!.schemas as any).find(item => {
|
|
77
|
+
return (item as any).properties?.recipientName && (item as any).properties?.addressLine1;
|
|
78
|
+
}) as any;
|
|
79
|
+
assert.deepEqual(Object.keys(component.properties).sort(), expectedWriteFields);
|
|
80
|
+
assert.deepEqual(component.required?.sort(), expectedRequiredFields);
|
|
81
|
+
}
|
|
82
|
+
const expectedReadFields = ['id', ...expectedWriteFields].sort();
|
|
83
|
+
for (const DtoClass of [DtoAddressMineItem, DtoAddressMineView]) {
|
|
84
|
+
const apiJson = await app.bean.openapi.generateJsonOfClass(DtoClass);
|
|
85
|
+
const component = Object.values(apiJson.components!.schemas as any).find(item => {
|
|
86
|
+
return (item as any).properties?.id && (item as any).properties?.recipientName;
|
|
87
|
+
}) as any;
|
|
88
|
+
assert.deepEqual(Object.keys(component.properties).sort(), expectedReadFields);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
60
93
|
it('denies anonymous Admin and Web Address actions', async () => {
|
|
61
94
|
await app.bean.executor.mockCtx(async () => {
|
|
62
95
|
for (const [method, path, options] of [
|
|
@@ -97,14 +130,12 @@ describe('addressOwnership.test.ts', () => {
|
|
|
97
130
|
ownList.list.some(item => String(item.id) === String(addressId)),
|
|
98
131
|
true,
|
|
99
132
|
);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
true,
|
|
107
|
-
);
|
|
133
|
+
for (const field of addressMineExcludedFields) {
|
|
134
|
+
assert.equal(
|
|
135
|
+
ownList.list.every(item => !Object.hasOwn(item, field)),
|
|
136
|
+
true,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
108
139
|
|
|
109
140
|
const { addressLine2: _, ...update } = {
|
|
110
141
|
...addressData,
|
|
@@ -117,13 +148,26 @@ describe('addressOwnership.test.ts', () => {
|
|
|
117
148
|
}),
|
|
118
149
|
null,
|
|
119
150
|
);
|
|
120
|
-
const ownAddress:
|
|
121
|
-
|
|
122
|
-
|
|
151
|
+
const ownAddress: DtoAddressMineView = await performAs(
|
|
152
|
+
customerA.token,
|
|
153
|
+
'get',
|
|
154
|
+
viewMinePath,
|
|
155
|
+
{
|
|
156
|
+
params: { id: addressId },
|
|
157
|
+
},
|
|
158
|
+
);
|
|
123
159
|
assert.equal(ownAddress.city, update.city);
|
|
124
160
|
assert.equal(ownAddress.addressLine2, addressData.addressLine2);
|
|
125
|
-
|
|
126
|
-
|
|
161
|
+
for (const field of addressMineExcludedFields) {
|
|
162
|
+
assert.equal(Object.hasOwn(ownAddress, field), false);
|
|
163
|
+
}
|
|
164
|
+
const [, incompleteUpdateError] = await catchError(() =>
|
|
165
|
+
performAs(customerA.token, 'patch', updateMinePath, {
|
|
166
|
+
params: { id: addressId },
|
|
167
|
+
body: { city: 'Berkeley' },
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
assert.equal(incompleteUpdateError?.code, 422);
|
|
127
171
|
|
|
128
172
|
const foreignList = await performAs(customerB.token, 'get', minePath);
|
|
129
173
|
assert.equal(
|
package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/payScene.commerceOrder.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import type { IPaymentOutcomeEvent } from 'vona-module-a-pay';
|
|
1
|
+
import type { IPaymentOutcomeEvent, IRefundOutcomeEvent } from 'vona-module-a-pay';
|
|
2
2
|
|
|
3
3
|
import { BeanBase } from 'vona';
|
|
4
4
|
import { PayScene } from 'vona-module-a-pay';
|
|
5
5
|
|
|
6
6
|
@PayScene({
|
|
7
|
-
providers: [
|
|
7
|
+
providers: [
|
|
8
|
+
{ key: 'mock', providerName: 'pay-mock:mock', clientName: 'default' },
|
|
9
|
+
{ key: 'paypal', providerName: 'pay-paypal:paypal', clientName: 'default' },
|
|
10
|
+
],
|
|
11
|
+
resolveProvider: (_ctx, input) =>
|
|
12
|
+
input.providers.find(item => item.key === 'mock')?.key ?? input.providers[0]!.key,
|
|
8
13
|
currencies: ['USD'],
|
|
9
14
|
captureMode: 'automatic',
|
|
10
15
|
sessionExpiresIn: 30 * 60 * 1000,
|
|
@@ -17,4 +22,8 @@ export class PaySceneCommerceOrder extends BeanBase {
|
|
|
17
22
|
async onPaymentOutcome(event: IPaymentOutcomeEvent): Promise<void> {
|
|
18
23
|
await this.$scope.commerceTrade.service.order.settlePaymentFromProvider(event);
|
|
19
24
|
}
|
|
25
|
+
|
|
26
|
+
async onRefundOutcome(event: IRefundOutcomeEvent): Promise<void> {
|
|
27
|
+
await this.$scope.commerceTrade.service.order.settleRefundFromProvider(event);
|
|
28
|
+
}
|
|
20
29
|
}
|
|
@@ -29,10 +29,36 @@ export class ServiceCommercePayScene extends BeanBase {
|
|
|
29
29
|
async linkRefundOperation(refundAttemptId: TableIdentity, refundOperationId: TableIdentity) {
|
|
30
30
|
const refundAttempt = await this.scope.model.refundAttempt.getByIdForUpdate(refundAttemptId);
|
|
31
31
|
if (!refundAttempt) this.app.throw(404, 'commerce refund attempt not found');
|
|
32
|
+
if (
|
|
33
|
+
refundAttempt.refundOperationId &&
|
|
34
|
+
String(refundAttempt.refundOperationId) !== String(refundOperationId)
|
|
35
|
+
) {
|
|
36
|
+
this.app.throw(409, 'commerce refund attempt is linked to another refund operation');
|
|
37
|
+
}
|
|
32
38
|
await this.scope.model.refundAttempt.updateById(refundAttempt.id, { refundOperationId });
|
|
33
39
|
return { ...refundAttempt, refundOperationId };
|
|
34
40
|
}
|
|
35
41
|
|
|
42
|
+
async createRefundOperation(refundAttemptId: TableIdentity) {
|
|
43
|
+
const refundAttempt = await this.scope.model.refundAttempt.getById(refundAttemptId);
|
|
44
|
+
if (!refundAttempt) this.app.throw(404, 'commerce refund attempt not found');
|
|
45
|
+
const paymentAttempt = await this.scope.model.paymentAttempt.get({
|
|
46
|
+
orderId: refundAttempt.orderId,
|
|
47
|
+
});
|
|
48
|
+
if (!paymentAttempt?.paymentSessionId)
|
|
49
|
+
this.app.throw(409, 'commerce payment session is unavailable');
|
|
50
|
+
const refund = await this.$scope.pay.service.refundOperation.create({
|
|
51
|
+
paymentSessionId: paymentAttempt.paymentSessionId,
|
|
52
|
+
businessReference: String(refundAttempt.id),
|
|
53
|
+
amountMinor: refundAttempt.amountCents,
|
|
54
|
+
currency: refundAttempt.currency,
|
|
55
|
+
idempotencyKey: `${refundAttempt.correlationId}:refund`,
|
|
56
|
+
correlationId: refundAttempt.correlationId,
|
|
57
|
+
});
|
|
58
|
+
await this.linkRefundOperation(refundAttempt.id, refund.id);
|
|
59
|
+
return refund;
|
|
60
|
+
}
|
|
61
|
+
|
|
36
62
|
@Core.transaction({ isolationLevel: 'SERIALIZABLE' })
|
|
37
63
|
async acceptRefundOutcome(event: IRefundOutcomeEvent) {
|
|
38
64
|
const refundAttempt = await this.scope.model.refundAttempt.getForUpdate({
|
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { describe, it } from 'node:test';
|
|
4
|
-
import { app } from 'vona-mock';
|
|
3
|
+
import { after, before, describe, it } from 'node:test';
|
|
4
|
+
import { acquireTestLock, app } from 'vona-mock';
|
|
5
5
|
|
|
6
6
|
describe('paymentAttempt.test.ts', { concurrency: false }, () => {
|
|
7
|
+
const releaseTestLocks: Array<() => void> = [];
|
|
8
|
+
|
|
9
|
+
before(async () => {
|
|
10
|
+
try {
|
|
11
|
+
for (const scene of ['a-commerce', 'a-pay']) {
|
|
12
|
+
releaseTestLocks.push(await acquireTestLock(scene));
|
|
13
|
+
}
|
|
14
|
+
} catch (error) {
|
|
15
|
+
for (const release of releaseTestLocks.reverse()) release();
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
after(() => {
|
|
21
|
+
for (const release of releaseTestLocks.reverse()) release();
|
|
22
|
+
});
|
|
23
|
+
|
|
7
24
|
it('creates and internally cancels an attempt exactly once', async () => {
|
|
8
25
|
await app.bean.executor.mockCtx(async () => {
|
|
9
26
|
const scope = app.scope('commerce-payment');
|
|
@@ -10,14 +10,17 @@ export default {
|
|
|
10
10
|
CouponTemplate: 'Coupon Template',
|
|
11
11
|
CouponTemplateId: 'Coupon Template ID',
|
|
12
12
|
CouponTemplateState: 'Template State',
|
|
13
|
+
CouponTemplateStateActive: 'Active',
|
|
14
|
+
CouponTemplateStateDisabled: 'Disabled',
|
|
15
|
+
CouponTemplateStateDraft: 'Draft',
|
|
13
16
|
CorrelationId: 'Correlation ID',
|
|
14
17
|
Currency: 'Currency',
|
|
15
18
|
Description: 'Description',
|
|
16
|
-
DiscountCents: 'Fixed Discount
|
|
19
|
+
DiscountCents: 'Fixed Discount',
|
|
17
20
|
DiscountPolicy: 'Discount Policy',
|
|
18
21
|
FromState: 'Previous State',
|
|
19
22
|
IssuedCount: 'Issued Count',
|
|
20
|
-
MinSpendCents: 'Minimum Spend
|
|
23
|
+
MinSpendCents: 'Minimum Spend',
|
|
21
24
|
Name: 'Name',
|
|
22
25
|
Operations: 'Operations',
|
|
23
26
|
OrderId: 'Order ID',
|
|
@@ -10,14 +10,17 @@ export default {
|
|
|
10
10
|
CouponTemplate: '优惠券模板',
|
|
11
11
|
CouponTemplateId: '优惠券模板 ID',
|
|
12
12
|
CouponTemplateState: '模板状态',
|
|
13
|
+
CouponTemplateStateActive: '已启用',
|
|
14
|
+
CouponTemplateStateDisabled: '已停用',
|
|
15
|
+
CouponTemplateStateDraft: '草稿',
|
|
13
16
|
CorrelationId: '关联 ID',
|
|
14
17
|
Currency: '币种',
|
|
15
18
|
Description: '描述',
|
|
16
|
-
DiscountCents: '
|
|
19
|
+
DiscountCents: '固定优惠',
|
|
17
20
|
DiscountPolicy: '优惠策略',
|
|
18
21
|
FromState: '原状态',
|
|
19
22
|
IssuedCount: '已发放数',
|
|
20
|
-
MinSpendCents: '
|
|
23
|
+
MinSpendCents: '最低消费',
|
|
21
24
|
Name: '名称',
|
|
22
25
|
Operations: '操作',
|
|
23
26
|
OrderId: '订单 ID',
|
package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateCreate.tsx
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { IDecoratorDtoOptions } from 'vona-module-a-web';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { $makeSchema, v } from 'vona-module-a-openapiutils';
|
|
4
|
+
import { $Dto } from 'vona-module-a-orm';
|
|
4
5
|
import { Dto } from 'vona-module-a-web';
|
|
5
6
|
import { z } from 'zod';
|
|
6
7
|
import { ZovaRender } from 'zova-rest-cabloy-basic-admin';
|
|
7
8
|
|
|
8
9
|
import { $locale } from '../.metadata/locales.ts';
|
|
10
|
+
import { couponTemplateStateItemsCreate } from '../entity/couponTemplate.tsx';
|
|
11
|
+
import { ModelCouponTemplate } from '../model/couponTemplate.ts';
|
|
9
12
|
|
|
10
13
|
export interface IDtoOptionsCouponTemplateCreate extends IDecoratorDtoOptions {}
|
|
11
14
|
|
|
@@ -93,38 +96,26 @@ export interface IDtoOptionsCouponTemplateCreate extends IDecoratorDtoOptions {}
|
|
|
93
96
|
],
|
|
94
97
|
}),
|
|
95
98
|
],
|
|
99
|
+
fields: {
|
|
100
|
+
state: $makeSchema(
|
|
101
|
+
v.required(),
|
|
102
|
+
ZovaRender.field('basic-select:formFieldSelect', { items: couponTemplateStateItemsCreate }),
|
|
103
|
+
z.enum(['draft', 'active']),
|
|
104
|
+
),
|
|
105
|
+
},
|
|
96
106
|
})
|
|
97
|
-
export class DtoCouponTemplateCreate {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
@Api.field(v.required())
|
|
114
|
-
validFrom: Date;
|
|
115
|
-
|
|
116
|
-
@Api.field(v.required())
|
|
117
|
-
validUntil: Date;
|
|
118
|
-
|
|
119
|
-
@Api.field(v.optional(), z.number().int().positive())
|
|
120
|
-
totalIssueLimit?: number;
|
|
121
|
-
|
|
122
|
-
@Api.field(v.optional(), z.number().int().positive())
|
|
123
|
-
totalUsageLimit?: number;
|
|
124
|
-
|
|
125
|
-
@Api.field(v.optional(), z.number().int().positive())
|
|
126
|
-
perCustomerIssueLimit?: number;
|
|
127
|
-
|
|
128
|
-
@Api.field(v.optional(), v.max(255))
|
|
129
|
-
description?: string;
|
|
130
|
-
}
|
|
107
|
+
export class DtoCouponTemplateCreate extends $Dto.create(() => ModelCouponTemplate, {
|
|
108
|
+
columns: [
|
|
109
|
+
'name',
|
|
110
|
+
'state',
|
|
111
|
+
'currency',
|
|
112
|
+
'discountCents',
|
|
113
|
+
'minSpendCents',
|
|
114
|
+
'validFrom',
|
|
115
|
+
'validUntil',
|
|
116
|
+
'totalIssueLimit',
|
|
117
|
+
'totalUsageLimit',
|
|
118
|
+
'perCustomerIssueLimit',
|
|
119
|
+
'description',
|
|
120
|
+
],
|
|
121
|
+
}) {}
|
package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateUpdate.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { z } from 'zod';
|
|
|
6
6
|
import { ZovaRender } from 'zova-rest-cabloy-basic-admin';
|
|
7
7
|
|
|
8
8
|
import { $locale } from '../.metadata/locales.ts';
|
|
9
|
+
import { couponTemplateStateItems } from '../entity/couponTemplate.tsx';
|
|
9
10
|
|
|
10
11
|
export interface IDtoOptionsCouponTemplateUpdate extends IDecoratorDtoOptions {}
|
|
11
12
|
|
|
@@ -54,7 +55,11 @@ export class DtoCouponTemplateUpdate {
|
|
|
54
55
|
@Api.field(v.optional(), v.min(2), v.max(100))
|
|
55
56
|
name?: string;
|
|
56
57
|
|
|
57
|
-
@Api.field(
|
|
58
|
+
@Api.field(
|
|
59
|
+
v.optional(),
|
|
60
|
+
ZovaRender.field('basic-select:formFieldSelect', { items: couponTemplateStateItems }),
|
|
61
|
+
z.enum(['draft', 'active', 'disabled']),
|
|
62
|
+
)
|
|
58
63
|
state?: 'draft' | 'active' | 'disabled';
|
|
59
64
|
|
|
60
65
|
@Api.field(v.optional(), v.max(255))
|
|
@@ -9,6 +9,18 @@ import { $locale } from '../.metadata/locales.ts';
|
|
|
9
9
|
|
|
10
10
|
export type TypeCouponTemplateState = 'draft' | 'active' | 'disabled';
|
|
11
11
|
|
|
12
|
+
export const couponTemplateStateItems = [
|
|
13
|
+
{ value: 'draft', title: $locale('CouponTemplateStateDraft') },
|
|
14
|
+
{ value: 'active', title: $locale('CouponTemplateStateActive') },
|
|
15
|
+
{ value: 'disabled', title: $locale('CouponTemplateStateDisabled') },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export const couponTemplateStateItemsCreate = couponTemplateStateItems.slice(0, 2);
|
|
19
|
+
|
|
20
|
+
const currencyRendererOptions = { fixed: 2, exp: 2, zero: 2 };
|
|
21
|
+
const dateFieldRendererOptions = { inputType: 'date' as const, preset: 'DATE_SHORT' as const };
|
|
22
|
+
const dateCellRendererOptions = { preset: 'DATE_SHORT' as const };
|
|
23
|
+
|
|
12
24
|
export interface IEntityOptionsCouponTemplate extends IDecoratorEntityOptions {}
|
|
13
25
|
|
|
14
26
|
@Entity<IEntityOptionsCouponTemplate>('commercePromotionCouponTemplate', {
|
|
@@ -43,6 +55,8 @@ export class EntityCouponTemplate extends EntityBase {
|
|
|
43
55
|
@Api.field(
|
|
44
56
|
v.title($locale('CouponTemplateState')),
|
|
45
57
|
v.required(),
|
|
58
|
+
ZovaRender.field('basic-select:formFieldSelect', { items: couponTemplateStateItems }),
|
|
59
|
+
ZovaRender.cell('basic-select:select', { items: couponTemplateStateItems }),
|
|
46
60
|
z.enum(['draft', 'active', 'disabled']),
|
|
47
61
|
)
|
|
48
62
|
state: TypeCouponTemplateState;
|
|
@@ -50,16 +64,38 @@ export class EntityCouponTemplate extends EntityBase {
|
|
|
50
64
|
@Api.field(v.title($locale('Currency')), v.required(), z.literal('USD'))
|
|
51
65
|
currency: 'USD';
|
|
52
66
|
|
|
53
|
-
@Api.field(
|
|
67
|
+
@Api.field(
|
|
68
|
+
v.title($locale('DiscountCents')),
|
|
69
|
+
v.required(),
|
|
70
|
+
ZovaRender.field('basic-currency:formFieldCurrency', currencyRendererOptions),
|
|
71
|
+
ZovaRender.cell('basic-currency:currency', currencyRendererOptions),
|
|
72
|
+
z.number().int().positive(),
|
|
73
|
+
)
|
|
54
74
|
discountCents: number;
|
|
55
75
|
|
|
56
|
-
@Api.field(
|
|
76
|
+
@Api.field(
|
|
77
|
+
v.title($locale('MinSpendCents')),
|
|
78
|
+
v.required(),
|
|
79
|
+
ZovaRender.field('basic-currency:formFieldCurrency', currencyRendererOptions),
|
|
80
|
+
ZovaRender.cell('basic-currency:currency', currencyRendererOptions),
|
|
81
|
+
z.number().int().nonnegative(),
|
|
82
|
+
)
|
|
57
83
|
minSpendCents: number;
|
|
58
84
|
|
|
59
|
-
@Api.field(
|
|
85
|
+
@Api.field(
|
|
86
|
+
v.title($locale('ValidFrom')),
|
|
87
|
+
v.required(),
|
|
88
|
+
ZovaRender.field('basic-date:formFieldDate', dateFieldRendererOptions),
|
|
89
|
+
ZovaRender.cell('basic-date:date', dateCellRendererOptions),
|
|
90
|
+
)
|
|
60
91
|
validFrom: Date;
|
|
61
92
|
|
|
62
|
-
@Api.field(
|
|
93
|
+
@Api.field(
|
|
94
|
+
v.title($locale('ValidUntil')),
|
|
95
|
+
v.required(),
|
|
96
|
+
ZovaRender.field('basic-date:formFieldDate', dateFieldRendererOptions),
|
|
97
|
+
ZovaRender.cell('basic-date:date', dateCellRendererOptions),
|
|
98
|
+
)
|
|
63
99
|
validUntil: Date;
|
|
64
100
|
|
|
65
101
|
@Api.field(v.title($locale('TotalIssueLimit')), v.optional(), z.number().int().positive())
|
package/vona/src/suite/a-commerce/modules/commerce-promotion/test/couponTemplateFormLayout.test.ts
CHANGED
|
@@ -52,6 +52,23 @@ describe('couponTemplateFormLayout.test.ts', () => {
|
|
|
52
52
|
],
|
|
53
53
|
);
|
|
54
54
|
assert.deepEqual(getGroupFields(createLayout?.children[0])?.[2]?.span, { default: 1, md: 2 });
|
|
55
|
+
assert.deepEqual(
|
|
56
|
+
Object.keys((createComponent as any).properties).sort(),
|
|
57
|
+
[
|
|
58
|
+
'name',
|
|
59
|
+
'state',
|
|
60
|
+
'currency',
|
|
61
|
+
'discountCents',
|
|
62
|
+
'minSpendCents',
|
|
63
|
+
'validFrom',
|
|
64
|
+
'validUntil',
|
|
65
|
+
'totalIssueLimit',
|
|
66
|
+
'totalUsageLimit',
|
|
67
|
+
'perCustomerIssueLimit',
|
|
68
|
+
'description',
|
|
69
|
+
].sort(),
|
|
70
|
+
);
|
|
71
|
+
assert.deepEqual((createComponent as any).properties.state.enum, ['draft', 'active']);
|
|
55
72
|
|
|
56
73
|
const updateJson = await app.bean.openapi.generateJsonOfClass(DtoCouponTemplateUpdate);
|
|
57
74
|
const updateComponent = findComponent(updateJson, properties => {
|
|
@@ -66,6 +83,7 @@ describe('couponTemplateFormLayout.test.ts', () => {
|
|
|
66
83
|
['name', 'state', 'description'],
|
|
67
84
|
);
|
|
68
85
|
assert.deepEqual(getGroupFields(updateLayout?.children[0])?.[2]?.span, { default: 1, md: 2 });
|
|
86
|
+
assert.deepEqual((updateComponent as any).required ?? [], []);
|
|
69
87
|
|
|
70
88
|
const viewJson = await app.bean.openapi.generateJsonOfClass(DtoCouponTemplateView);
|
|
71
89
|
const viewComponent = findComponent(viewJson, properties => {
|
|
@@ -92,6 +110,73 @@ describe('couponTemplateFormLayout.test.ts', () => {
|
|
|
92
110
|
});
|
|
93
111
|
});
|
|
94
112
|
|
|
113
|
+
it('action:couponTemplate:fieldRenderMetadata', async () => {
|
|
114
|
+
await app.bean.executor.mockCtx(async () => {
|
|
115
|
+
const createJson = await app.bean.openapi.generateJsonOfClass(DtoCouponTemplateCreate);
|
|
116
|
+
const createComponent = findComponent(createJson, properties => {
|
|
117
|
+
return properties?.discountCents && properties?.totalIssueLimit;
|
|
118
|
+
}) as any;
|
|
119
|
+
const createProperties = createComponent.properties;
|
|
120
|
+
const expectedCurrencyOptions = { fixed: 2, exp: 2, zero: 2 };
|
|
121
|
+
const expectedCreateStateValues = ['draft', 'active'];
|
|
122
|
+
const expectedStateValues = ['draft', 'active', 'disabled'];
|
|
123
|
+
const stateItems = createProperties.state.rest.form.options.items;
|
|
124
|
+
|
|
125
|
+
assert.equal(createProperties.state.rest.form.render, 'basic-select:formFieldSelect');
|
|
126
|
+
assert.deepEqual(
|
|
127
|
+
stateItems.map((item: any) => item.value),
|
|
128
|
+
expectedCreateStateValues,
|
|
129
|
+
);
|
|
130
|
+
assert.ok(stateItems.every((item: any) => item.title?.toJSON && item.title?.toString));
|
|
131
|
+
assert.equal(createProperties.state.rest.table.render, 'basic-select:select');
|
|
132
|
+
assert.deepEqual(
|
|
133
|
+
createProperties.state.rest.table.columnProps.items.map((item: any) => item.value),
|
|
134
|
+
expectedStateValues,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
for (const fieldName of ['discountCents', 'minSpendCents']) {
|
|
138
|
+
const field = createProperties[fieldName];
|
|
139
|
+
assert.equal(field.rest.form.render, 'basic-currency:formFieldCurrency');
|
|
140
|
+
assert.deepEqual(field.rest.form.options, expectedCurrencyOptions);
|
|
141
|
+
assert.equal(field.rest.table.render, 'basic-currency:currency');
|
|
142
|
+
assert.deepEqual(field.rest.table.columnProps, expectedCurrencyOptions);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const fieldName of ['validFrom', 'validUntil']) {
|
|
146
|
+
const field = createProperties[fieldName];
|
|
147
|
+
assert.equal(field.rest.form.render, 'basic-date:formFieldDate');
|
|
148
|
+
assert.deepEqual(field.rest.form.options, { inputType: 'date', preset: 'DATE_SHORT' });
|
|
149
|
+
assert.equal(field.rest.table.render, 'basic-date:date');
|
|
150
|
+
assert.deepEqual(field.rest.table.columnProps, { preset: 'DATE_SHORT' });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const viewJson = await app.bean.openapi.generateJsonOfClass(DtoCouponTemplateView);
|
|
154
|
+
const viewComponent = findComponent(viewJson, properties => {
|
|
155
|
+
return properties?.issuedCount && properties?.redeemedCount;
|
|
156
|
+
}) as any;
|
|
157
|
+
assert.equal(
|
|
158
|
+
viewComponent.properties.discountCents.rest.form.render,
|
|
159
|
+
'basic-currency:formFieldCurrency',
|
|
160
|
+
);
|
|
161
|
+
assert.equal(
|
|
162
|
+
viewComponent.properties.validUntil.rest.form.render,
|
|
163
|
+
'basic-date:formFieldDate',
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const updateJson = await app.bean.openapi.generateJsonOfClass(DtoCouponTemplateUpdate);
|
|
167
|
+
const updateComponent = findComponent(updateJson, properties => {
|
|
168
|
+
return (
|
|
169
|
+
properties?.name && properties?.state && properties?.description && !properties?.currency
|
|
170
|
+
);
|
|
171
|
+
}) as any;
|
|
172
|
+
assert.equal(
|
|
173
|
+
updateComponent.properties.state.rest.form.render,
|
|
174
|
+
'basic-select:formFieldSelect',
|
|
175
|
+
);
|
|
176
|
+
assert.equal(updateComponent.properties.discountCents, undefined);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
95
180
|
it('action:couponTemplate:filterFormLayoutMetadata', async () => {
|
|
96
181
|
await app.bean.executor.mockCtx(async () => {
|
|
97
182
|
const apiJson = await app.bean.openapi.generateJsonOfClass(DtoCouponTemplateSelectResItem);
|
|
@@ -1282,7 +1282,7 @@ export interface IApiPathPostRecord{
|
|
|
1282
1282
|
'/commerce/trade/order/:id/requestRefund': undefined;
|
|
1283
1283
|
'/commerce/trade/order/:id/approveRefund': undefined;
|
|
1284
1284
|
'/commerce/trade/order/:id/rejectRefund': undefined;
|
|
1285
|
-
'/commerce/trade/order/:id/
|
|
1285
|
+
'/commerce/trade/order/:id/executeRefund': undefined;
|
|
1286
1286
|
'/commerce/trade/order/:id/ship': undefined;
|
|
1287
1287
|
'/commerce/trade/stockBalance/adjustStock': undefined;
|
|
1288
1288
|
}
|
|
@@ -16,7 +16,6 @@ import { DtoOrderSelectReq } from '../dto/orderSelectReq.tsx';
|
|
|
16
16
|
import { DtoOrderSelectRes } from '../dto/orderSelectRes.tsx';
|
|
17
17
|
import { DtoOrderShip } from '../dto/orderShip.tsx';
|
|
18
18
|
import { DtoOrderView } from '../dto/orderView.tsx';
|
|
19
|
-
import { DtoRefundOutcomeCreate } from '../dto/refundOutcomeCreate.tsx';
|
|
20
19
|
import { DtoRefundRequestCreate } from '../dto/refundRequestCreate.tsx';
|
|
21
20
|
import { DtoRefundResult } from '../dto/refundResult.tsx';
|
|
22
21
|
import { DtoRefundReview } from '../dto/refundReview.tsx';
|
|
@@ -72,14 +71,13 @@ export class ControllerOrder extends BeanBase {
|
|
|
72
71
|
return await this.scope.service.order.rejectRefund(id, command);
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
@Web.post(':id/
|
|
74
|
+
@Web.post(':id/executeRefund')
|
|
76
75
|
@Api.body(DtoRefundResult)
|
|
77
76
|
@Passport.systemAdmin()
|
|
78
|
-
async
|
|
77
|
+
async executeRefund(
|
|
79
78
|
@Arg.param('id', v.tableIdentity()) id: TableIdentity,
|
|
80
|
-
@Arg.body() command: DtoRefundOutcomeCreate,
|
|
81
79
|
): Promise<DtoRefundResult> {
|
|
82
|
-
return await this.scope.service.order.
|
|
80
|
+
return await this.scope.service.order.executeRefund(id);
|
|
83
81
|
}
|
|
84
82
|
|
|
85
83
|
@Web.post(':id/ship')
|