cabloy 5.1.125 → 5.1.127

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/skills/cabloy-backend-scaffold/SKILL.md +1 -1
  3. package/.claude/skills/cabloy-backend-scaffold/evals/evals.json +6 -0
  4. package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +13 -4
  5. package/.claude/skills/cabloy-workflow/SKILL.md +4 -2
  6. package/.claude/skills/cabloy-workflow/evals/evals.json +14 -2
  7. package/.claude/skills/cabloy-worktree-environment/SKILL.md +206 -0
  8. package/.claude/skills/cabloy-worktree-environment/evals/evals.json +77 -0
  9. package/.github/workflows/vona-test-pg.yml +3 -0
  10. package/.github/workflows/vona-test-sqlite3.yml +3 -0
  11. package/CHANGELOG.md +36 -0
  12. package/CLAUDE.md +1 -1
  13. package/cabloy-docs/.vitepress/config.mjs +1 -0
  14. package/cabloy-docs/backend/backend-contract-emission-specimen.md +2 -2
  15. package/cabloy-docs/backend/dto-guide.md +10 -7
  16. package/cabloy-docs/backend/dto-infer-generation.md +127 -7
  17. package/cabloy-docs/backend/error-guide.md +17 -0
  18. package/cabloy-docs/backend/foundation.md +1 -1
  19. package/cabloy-docs/backend/introduction.md +1 -0
  20. package/cabloy-docs/backend/module-dependencies.md +104 -0
  21. package/cabloy-docs/backend/serialization-guide.md +6 -0
  22. package/cabloy-docs/fullstack/parallel-worktree-environment.md +72 -34
  23. package/cabloy-docs/fullstack/suites-and-modules.md +1 -0
  24. package/cabloy-docs/reference/package-map.md +1 -0
  25. package/e2e/specs/a-commerce/commerce.spec.ts +224 -4
  26. package/package.json +2 -1
  27. package/scripts/init.ts +7 -12
  28. package/scripts/initAppName.test.ts +72 -0
  29. package/scripts/initAppName.ts +50 -0
  30. package/vona/packages-cli/cli/package.json +1 -1
  31. package/vona/packages-cli/cli-set-api/package.json +1 -1
  32. package/vona/packages-vona/vona/package.json +1 -1
  33. package/vona/packages-vona/vona-core/package.json +1 -1
  34. package/vona/packages-vona/vona-core/src/lib/bean/resource/error/errorClass.ts +21 -4
  35. package/vona/packages-vona/vona-core/src/lib/bean/resource/error/errorObject.ts +1 -0
  36. package/vona/packages-vona/vona-core/src/lib/bean/type.ts +3 -1
  37. package/vona/packages-vona/vona-core/src/types/interface/module.ts +6 -1
  38. package/vona/packages-vona/vona-mock/package.json +1 -1
  39. package/vona/pnpm-lock.yaml +27 -147
  40. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +36 -1
  41. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +1 -0
  42. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +1 -0
  43. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/categorySelectResItem.tsx +3 -1
  44. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/categoryView.tsx +3 -1
  45. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/category.tsx +21 -2
  46. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/model/category.ts +11 -1
  47. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/category.ts +7 -2
  48. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/categoryPresentation.test.ts +218 -0
  49. package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineCreate.tsx +6 -27
  50. package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineItem.tsx +6 -31
  51. package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineUpdate.tsx +6 -2
  52. package/vona/src/suite/a-commerce/modules/commerce-member/src/lib/addressMine.ts +16 -0
  53. package/vona/src/suite/a-commerce/modules/commerce-member/src/service/address.ts +4 -17
  54. package/vona/src/suite/a-commerce/modules/commerce-member/test/addressOwnership.test.ts +63 -19
  55. package/vona/src/suite/a-commerce/modules/commerce-payment/package.json +2 -1
  56. package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/payScene.commerceOrder.ts +11 -2
  57. package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/commercePayScene.ts +26 -0
  58. package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentAttempt.test.ts +19 -2
  59. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/en-us.ts +5 -2
  60. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/zh-cn.ts +5 -2
  61. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateCreate.tsx +26 -35
  62. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateUpdate.tsx +6 -1
  63. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/entity/couponTemplate.tsx +40 -4
  64. package/vona/src/suite/a-commerce/modules/commerce-promotion/test/couponTemplateFormLayout.test.ts +85 -0
  65. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +1 -1
  66. package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/order.ts +3 -5
  67. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderSummary.tsx +7 -40
  68. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/refundResult.tsx +3 -0
  69. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/shipmentView.tsx +6 -15
  70. package/vona/src/suite/a-commerce/modules/commerce-trade/src/lib/order.ts +9 -0
  71. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +123 -12
  72. package/vona/src/suite/a-commerce/modules/commerce-trade/test/order.test.ts +25 -0
  73. package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +19 -2
  74. package/vona/src/suite/a-commerce/modules/commerce-trade/test/refundLifecycle.test.ts +111 -3
  75. package/vona/src/suite/a-training/modules/training-record/src/dto/detailRecordSubjectResItem.tsx +6 -1
  76. package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectResItem.tsx +3 -1
  77. package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +4 -1
  78. package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +45 -0
  79. package/vona/src/suite/a-training/modules/training-student/src/dto/detailRecordResItem.tsx +6 -1
  80. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSummary.tsx +5 -23
  81. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +37 -5
  82. package/vona/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
  83. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/.metadata/index.ts +29 -0
  84. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.index.ts +3 -0
  85. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.redlock.ts +3 -1
  86. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.version.ts +7 -0
  87. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/queue.outboxDispatch.ts +9 -4
  88. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/schedule.providerOperationDispatch.ts +11 -0
  89. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/outboxEvent.tsx +11 -4
  90. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/providerOperation.tsx +12 -0
  91. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/webhookInbox.tsx +6 -0
  92. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/outbox.ts +12 -5
  93. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/paymentSession.ts +1 -37
  94. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/providerOperation.ts +355 -1
  95. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/refundOperation.ts +226 -0
  96. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/webhook.ts +148 -50
  97. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payScene.ts +2 -1
  98. package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
  99. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/.metadata/index.ts +39 -22
  100. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/bean/payProvider.mock.ts +42 -13
  101. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/controller/mockPayment.ts +11 -0
  102. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/dto/mockRefundComplete.tsx +13 -0
  103. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/dto/mockRefundReceipt.tsx +16 -0
  104. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/service/payMock.ts +56 -0
  105. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/package.json +2 -1
  106. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/bean/payProvider.paypal.ts +117 -8
  107. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +1 -1
  108. package/vona/src/suite-vendor/a-pay/package.json +5 -5
  109. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  110. package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
  111. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoGet.ts +5 -5
  112. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  113. package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/schemas.ts +58 -12
  114. package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/types.ts +673 -66
  115. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/types.ts +55 -15
  116. package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +1 -1
  117. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeOrder.ts +15 -17
  118. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +12 -8
  119. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +116 -30
  120. package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeOrder.ts +3 -3
  121. package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionRefund/controller.tsx +1 -8
  122. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +5 -9
  123. package/zova/src/suite/a-training/modules/training-student/src/.metadata/index.ts +1 -1
  124. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +301 -17
  125. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +5461 -1788
  126. package/zova/src/suite/cabloy-basic/modules/basic-select/src/bean/tableCell.select.tsx +12 -6
  127. package/zova/src/suite/cabloy-basic/modules/basic-select/src/component/formFieldSelect/controller.tsx +6 -3
@@ -0,0 +1,218 @@
1
+ import assert from 'node:assert';
2
+ import { describe, it } from 'node:test';
3
+ import { app } from 'vona-mock';
4
+ import {
5
+ DtoCategoryCreate,
6
+ DtoCategorySelectResItem,
7
+ DtoCategoryUpdate,
8
+ DtoCategoryView,
9
+ } from 'vona-module-commerce-catalog';
10
+
11
+ function findComponent(apiJson: any, predicate: (properties: any) => boolean) {
12
+ return Object.values(apiJson.components!.schemas as any).find(item => {
13
+ return predicate((item as any).properties);
14
+ }) as any;
15
+ }
16
+
17
+ function getEntryForm(component: any) {
18
+ const pageEntry = component?.rest?.blocks?.[0];
19
+ const formBlock = pageEntry?.options?.blocks?.[0];
20
+ const toolbarBlock = pageEntry?.options?.blocks?.[1];
21
+ assert.equal(pageEntry?.render, 'basic-pageentry:blockPageEntry');
22
+ assert.equal(formBlock?.render, 'basic-pageentry:blockForm');
23
+ assert.equal(toolbarBlock?.render, 'basic-pageentry:blockToolbarRow');
24
+ return toolbarBlock;
25
+ }
26
+
27
+ describe('categoryPresentation.test.ts', () => {
28
+ it('action:category:presentationMetadata', async () => {
29
+ await app.bean.executor.mockCtx(async () => {
30
+ const createJson = await app.bean.openapi.generateJsonOfClass(DtoCategoryCreate);
31
+ const createComponent = findComponent(createJson, properties => {
32
+ return (
33
+ properties?.name &&
34
+ properties?.parentId &&
35
+ properties?.published &&
36
+ properties?.description
37
+ );
38
+ });
39
+ const createProperties = createComponent.properties;
40
+ assert.deepEqual(Object.keys(createProperties).sort(), [
41
+ 'description',
42
+ 'name',
43
+ 'parentId',
44
+ 'published',
45
+ ]);
46
+ assert.equal(
47
+ createProperties.parentId.rest.form.render,
48
+ 'basic-resource:formFieldResourcePicker',
49
+ );
50
+ assert.deepEqual(createProperties.parentId.rest.form.options, {
51
+ resource: 'commerce-catalog:category',
52
+ relationName: 'parent',
53
+ });
54
+ assert.equal(createProperties.published.rest.form.render, 'basic-select:formFieldSelect');
55
+ assert.deepEqual(
56
+ createProperties.published.rest.form.options.items.map((item: any) => item.value),
57
+ [false, true],
58
+ );
59
+ assert.ok(
60
+ createProperties.published.rest.form.options.items.every(
61
+ (item: any) => item.title?.toJSON && item.title?.toString,
62
+ ),
63
+ );
64
+ const createToolbar = getEntryForm(createComponent);
65
+ assert.deepEqual(
66
+ createToolbar.options.actions.map((action: any) => action.render),
67
+ ['basic-form:actionSubmit', 'basic-form:actionBack'],
68
+ );
69
+
70
+ const updateJson = await app.bean.openapi.generateJsonOfClass(DtoCategoryUpdate);
71
+ const updateComponent = findComponent(updateJson, properties => {
72
+ return (
73
+ properties?.name &&
74
+ properties?.parentId &&
75
+ properties?.published &&
76
+ properties?.description
77
+ );
78
+ });
79
+ assert.deepEqual(Object.keys(updateComponent.properties).sort(), [
80
+ 'description',
81
+ 'name',
82
+ 'parentId',
83
+ 'published',
84
+ ]);
85
+ assert.equal(
86
+ updateComponent.properties.parentId.rest.form.render,
87
+ 'basic-resource:formFieldResourcePicker',
88
+ );
89
+ assert.equal(
90
+ updateComponent.properties.published.rest.form.render,
91
+ 'basic-select:formFieldSelect',
92
+ );
93
+
94
+ const viewJson = await app.bean.openapi.generateJsonOfClass(DtoCategoryView);
95
+ const viewComponent = findComponent(viewJson, properties => {
96
+ return (
97
+ properties?.id && properties?.parentId && properties?.parent && properties?.published
98
+ );
99
+ });
100
+ assert.deepEqual(Object.keys(viewComponent.properties.parent.properties).sort(), [
101
+ 'id',
102
+ 'name',
103
+ ]);
104
+ assert.equal(viewComponent.properties.iid.rest.visible, false);
105
+ assert.equal(viewComponent.properties.deleted.rest.visible, false);
106
+ assert.equal(
107
+ viewComponent.properties.parentId.rest.form.render,
108
+ 'basic-resource:formFieldResourcePicker',
109
+ );
110
+ assert.equal(
111
+ viewComponent.properties.published.rest.form.render,
112
+ 'basic-select:formFieldSelect',
113
+ );
114
+ const viewToolbar = getEntryForm(viewComponent);
115
+ assert.deepEqual(
116
+ viewToolbar.options.actions.map((action: any) => action.render),
117
+ ['basic-form:actionBack'],
118
+ );
119
+
120
+ const listJson = await app.bean.openapi.generateJsonOfClass(DtoCategorySelectResItem);
121
+ const listComponent = findComponent(listJson, properties => properties?._operationsRow);
122
+ const listProperties = listComponent.properties;
123
+ assert.deepEqual(Object.keys(listProperties.parent.properties).sort(), ['id', 'name']);
124
+ assert.equal(listProperties.parentId.rest.table.render, 'basic-resource:resourcePicker');
125
+ assert.deepEqual(listProperties.parentId.rest.table.columnProps, {
126
+ resource: 'commerce-catalog:category',
127
+ relationName: 'parent',
128
+ });
129
+ assert.equal(listProperties.published.rest.table.render, 'basic-select:select');
130
+ assert.deepEqual(
131
+ listProperties.published.rest.table.columnProps.items.map((item: any) => item.value),
132
+ [false, true],
133
+ );
134
+ assert.deepEqual(
135
+ listProperties._operationsRow.rest.table.columnProps.actions.map(
136
+ (action: any) => action.render,
137
+ ),
138
+ ['basic-table:actionUpdate', 'basic-table:actionDelete'],
139
+ );
140
+
141
+ const filterBlock = listComponent.rest.blocks[0].options.blocks[0];
142
+ const filterLayout = filterBlock.options.blocks[0].options.formLayout;
143
+ const section = filterLayout.children[0];
144
+ assert.equal(section.type, 'section');
145
+ assert.equal(section.layout, 'flow');
146
+ assert.deepEqual(
147
+ section.children.slice(0, 2).map((field: any) => field.name),
148
+ ['name', 'createdAt'],
149
+ );
150
+ assert.equal(section.children[2].block.render, 'basic-page:blockFilterActions');
151
+ });
152
+ });
153
+
154
+ it('action:category:parentProjection', async () => {
155
+ await app.bean.executor.mockCtx(async () => {
156
+ const suffix = `${Date.now()}`;
157
+ let parentId: number | string | undefined;
158
+ let childId: number | string | undefined;
159
+ await app.bean.passport.signinMock();
160
+ try {
161
+ parentId = await app.bean.executor.performAction('post', '/commerce/catalog/category', {
162
+ body: { name: `presentation-parent-${suffix}`, published: false },
163
+ });
164
+ childId = await app.bean.executor.performAction('post', '/commerce/catalog/category', {
165
+ body: {
166
+ name: `presentation-child-${suffix}`,
167
+ parentId,
168
+ published: true,
169
+ },
170
+ });
171
+
172
+ const view: any = await app.bean.executor.performAction(
173
+ 'get',
174
+ '/commerce/catalog/category/:id',
175
+ { params: { id: childId } },
176
+ );
177
+ assert.equal(String(view.parentId), String(parentId));
178
+ assert.deepEqual(view.parent, { id: parentId, name: `presentation-parent-${suffix}` });
179
+ assert.equal(view.published, true);
180
+
181
+ const select: any = await app.bean.executor.performAction(
182
+ 'get',
183
+ '/commerce/catalog/category',
184
+ );
185
+ const root = select.list.find((item: any) => String(item.id) === String(parentId));
186
+ const child = select.list.find((item: any) => String(item.id) === String(childId));
187
+ assert.equal(root.parent, undefined);
188
+ assert.equal(root.published, false);
189
+ assert.equal(String(child.parentId), String(parentId));
190
+ assert.deepEqual(child.parent, { id: parentId, name: `presentation-parent-${suffix}` });
191
+ assert.equal(child.published, true);
192
+
193
+ await assert.rejects(
194
+ app.bean.executor.performAction('patch', '/commerce/catalog/category/:id', {
195
+ params: { id: childId },
196
+ body: {
197
+ name: `presentation-child-${suffix}`,
198
+ parentId: childId,
199
+ published: true,
200
+ },
201
+ }),
202
+ );
203
+ } finally {
204
+ if (childId) {
205
+ await app.bean.executor.performAction('delete', '/commerce/catalog/category/:id', {
206
+ params: { id: childId },
207
+ });
208
+ }
209
+ if (parentId) {
210
+ await app.bean.executor.performAction('delete', '/commerce/catalog/category/:id', {
211
+ params: { id: parentId },
212
+ });
213
+ }
214
+ await app.bean.passport.signout();
215
+ }
216
+ });
217
+ });
218
+ });
@@ -1,35 +1,14 @@
1
1
  import type { IDecoratorDtoOptions } from 'vona-module-a-web';
2
2
 
3
- import { Api, v } from 'vona-module-a-openapiutils';
3
+ import { $Dto } from 'vona-module-a-orm';
4
4
  import { Dto } from 'vona-module-a-web';
5
5
 
6
- import { $locale } from '../.metadata/locales.ts';
6
+ import { addressMineWriteColumns } from '../lib/addressMine.ts';
7
+ import { ModelAddress } from '../model/address.ts';
7
8
 
8
9
  export interface IDtoOptionsAddressMineCreate extends IDecoratorDtoOptions {}
9
10
 
10
11
  @Dto<IDtoOptionsAddressMineCreate>()
11
- export class DtoAddressMineCreate {
12
- @Api.field(v.title($locale('RecipientName')), v.required(), v.min(2), v.max(100))
13
- recipientName: string;
14
-
15
- @Api.field(v.title($locale('Phone')), v.required(), v.min(1), v.max(50))
16
- phone: string;
17
-
18
- @Api.field(v.title($locale('CountryCode')), v.required(), v.min(2), v.max(10))
19
- countryCode: string;
20
-
21
- @Api.field(v.title($locale('Region')), v.required(), v.min(1), v.max(100))
22
- region: string;
23
-
24
- @Api.field(v.title($locale('City')), v.required(), v.min(1), v.max(100))
25
- city: string;
26
-
27
- @Api.field(v.title($locale('PostalCode')), v.required(), v.min(1), v.max(30))
28
- postalCode: string;
29
-
30
- @Api.field(v.title($locale('AddressLine1')), v.required(), v.min(1), v.max(255))
31
- addressLine1: string;
32
-
33
- @Api.field(v.title($locale('AddressLine2')), v.optional(), v.max(255))
34
- addressLine2?: string;
35
- }
12
+ export class DtoAddressMineCreate extends $Dto.create(() => ModelAddress, {
13
+ columns: addressMineWriteColumns,
14
+ }) {}
@@ -1,39 +1,14 @@
1
- import type { TableIdentity } from 'table-identity';
2
1
  import type { IDecoratorDtoOptions } from 'vona-module-a-web';
3
2
 
4
- import { Api, v } from 'vona-module-a-openapiutils';
3
+ import { $Dto } from 'vona-module-a-orm';
5
4
  import { Dto } from 'vona-module-a-web';
6
5
 
7
- import { $locale } from '../.metadata/locales.ts';
6
+ import { addressMineReadColumns } from '../lib/addressMine.ts';
7
+ import { ModelAddress } from '../model/address.ts';
8
8
 
9
9
  export interface IDtoOptionsAddressMineItem extends IDecoratorDtoOptions {}
10
10
 
11
11
  @Dto<IDtoOptionsAddressMineItem>()
12
- export class DtoAddressMineItem {
13
- @Api.field(v.tableIdentity())
14
- id: TableIdentity;
15
-
16
- @Api.field(v.title($locale('RecipientName')), v.required(), v.min(2), v.max(100))
17
- recipientName: string;
18
-
19
- @Api.field(v.title($locale('Phone')), v.required(), v.min(1), v.max(50))
20
- phone: string;
21
-
22
- @Api.field(v.title($locale('CountryCode')), v.required(), v.min(2), v.max(10))
23
- countryCode: string;
24
-
25
- @Api.field(v.title($locale('Region')), v.required(), v.min(1), v.max(100))
26
- region: string;
27
-
28
- @Api.field(v.title($locale('City')), v.required(), v.min(1), v.max(100))
29
- city: string;
30
-
31
- @Api.field(v.title($locale('PostalCode')), v.required(), v.min(1), v.max(30))
32
- postalCode: string;
33
-
34
- @Api.field(v.title($locale('AddressLine1')), v.required(), v.min(1), v.max(255))
35
- addressLine1: string;
36
-
37
- @Api.field(v.title($locale('AddressLine2')), v.optional(), v.max(255))
38
- addressLine2?: string;
39
- }
12
+ export class DtoAddressMineItem extends $Dto.get(() => ModelAddress, {
13
+ columns: addressMineReadColumns,
14
+ }) {}
@@ -1,10 +1,14 @@
1
1
  import type { IDecoratorDtoOptions } from 'vona-module-a-web';
2
2
 
3
+ import { $Dto } from 'vona-module-a-orm';
3
4
  import { Dto } from 'vona-module-a-web';
4
5
 
5
- import { DtoAddressMineCreate } from './addressMineCreate.tsx';
6
+ import { addressMineWriteColumns } from '../lib/addressMine.ts';
7
+ import { ModelAddress } from '../model/address.ts';
6
8
 
7
9
  export interface IDtoOptionsAddressMineUpdate extends IDecoratorDtoOptions {}
8
10
 
9
11
  @Dto<IDtoOptionsAddressMineUpdate>()
10
- export class DtoAddressMineUpdate extends DtoAddressMineCreate {}
12
+ export class DtoAddressMineUpdate extends $Dto.update(() => ModelAddress, {
13
+ columns: addressMineWriteColumns,
14
+ }) {}
@@ -0,0 +1,16 @@
1
+ import type { EntityAddress } from '../entity/address.tsx';
2
+
3
+ export const addressMineWriteColumns = [
4
+ 'recipientName',
5
+ 'phone',
6
+ 'countryCode',
7
+ 'region',
8
+ 'city',
9
+ 'postalCode',
10
+ 'addressLine1',
11
+ 'addressLine2',
12
+ ] satisfies Array<keyof EntityAddress>;
13
+
14
+ export const addressMineReadColumns = ['id', ...addressMineWriteColumns] satisfies Array<
15
+ keyof EntityAddress
16
+ >;
@@ -1,5 +1,5 @@
1
1
  import type { TableIdentity } from 'table-identity';
2
- import type { IQueryParams, TypeModelColumns } from 'vona-module-a-orm';
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
- const addressColumns: TypeModelColumns<EntityAddress> = [
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: addressColumns,
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: addressColumns },
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
- assert.equal(
101
- ownList.list.every(item => !Object.hasOwn(item, 'userId')),
102
- true,
103
- );
104
- assert.equal(
105
- ownList.list.every(item => !Object.hasOwn(item, 'iid')),
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: EntityAddress = await performAs(customerA.token, 'get', viewMinePath, {
121
- params: { id: addressId },
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
- assert.equal(Object.hasOwn(ownAddress, 'userId'), false);
126
- assert.equal(Object.hasOwn(ownAddress, 'iid'), false);
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(
@@ -31,7 +31,8 @@
31
31
  "postpack": "clean-package restore"
32
32
  },
33
33
  "dependencies": {
34
- "vona-module-a-pay": "workspace:*"
34
+ "vona-module-a-pay": "workspace:*",
35
+ "vona-module-pay-paypal": "workspace:*"
35
36
  },
36
37
  "devDependencies": {
37
38
  "clean-package": "^2.2.0",
@@ -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: [{ key: 'mock', providerName: 'pay-mock:mock', clientName: 'default' }],
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 (Cents)',
19
+ DiscountCents: 'Fixed Discount',
17
20
  DiscountPolicy: 'Discount Policy',
18
21
  FromState: 'Previous State',
19
22
  IssuedCount: 'Issued Count',
20
- MinSpendCents: 'Minimum Spend (Cents)',
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',