cabloy 5.1.126 → 5.1.128

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 (102) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +37 -0
  3. package/cabloy-docs/backend/error-guide.md +17 -0
  4. package/cabloy-docs/backend/i18n-guide.md +86 -7
  5. package/e2e/specs/a-commerce/commerce.spec.ts +222 -6
  6. package/package.json +1 -1
  7. package/vona/env/.env +2 -0
  8. package/vona/env/.env.prod +6 -2
  9. package/vona/packages-cli/cabloy-cli/package.json +1 -1
  10. package/vona/packages-cli/cli/package.json +1 -1
  11. package/vona/packages-cli/cli-set-api/package.json +1 -1
  12. package/vona/packages-utils/module-glob/package.json +1 -1
  13. package/vona/packages-utils/utils/package.json +1 -1
  14. package/vona/packages-utils/utils/src/vona.ts +8 -1
  15. package/vona/packages-vona/vona/package.json +1 -1
  16. package/vona/packages-vona/vona-core/package.json +1 -1
  17. package/vona/packages-vona/vona-core/src/lib/bean/resource/error/errorClass.ts +21 -4
  18. package/vona/packages-vona/vona-core/src/lib/bean/resource/error/errorObject.ts +1 -0
  19. package/vona/packages-vona/vona-core/src/lib/bean/type.ts +3 -1
  20. package/vona/packages-vona/vona-core/src/types/interface/module.ts +6 -1
  21. package/vona/packages-vona/vona-mock/package.json +1 -1
  22. package/vona/pnpm-lock.yaml +15 -44
  23. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +37 -2
  24. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +1 -0
  25. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +1 -0
  26. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/categorySelectResItem.tsx +3 -1
  27. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/categoryView.tsx +3 -1
  28. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productSelectResItem.tsx +3 -1
  29. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productView.tsx +3 -1
  30. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/category.tsx +21 -2
  31. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/product.tsx +23 -3
  32. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/model/category.ts +11 -1
  33. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/model/product.ts +1 -0
  34. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/category.ts +7 -2
  35. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/product.ts +7 -2
  36. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/categoryPresentation.test.ts +218 -0
  37. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/productPresentation.test.ts +214 -0
  38. package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/payScene.commerceOrder.ts +39 -3
  39. package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/commercePayScene.ts +14 -1
  40. package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/paymentAttempt.ts +5 -1
  41. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +17 -0
  42. package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/checkout.ts +18 -0
  43. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/checkoutCreate.tsx +3 -0
  44. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/checkoutPaymentMethod.tsx +19 -0
  45. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/checkoutPaymentMethods.tsx +17 -0
  46. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/refundResult.tsx +3 -0
  47. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +53 -1
  48. package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +2 -1
  49. package/vona/src/suite/a-commerce/modules/commerce-trade/test/refundLifecycle.test.ts +111 -3
  50. package/vona/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
  51. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/.metadata/index.ts +94 -55
  52. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/bean.payScene.ts +65 -1
  53. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.redlock.ts +2 -0
  54. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/controller/paymentCallback.ts +32 -0
  55. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/controller/paymentSession.ts +26 -8
  56. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/paymentCallback.ts +99 -0
  57. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/paymentSession.ts +26 -3
  58. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/providerOperation.ts +214 -82
  59. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/refundOperation.ts +27 -2
  60. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payScene.ts +9 -0
  61. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payment.ts +5 -0
  62. package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
  63. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/bean/payProvider.mock.ts +7 -0
  64. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/package.json +1 -1
  65. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/bean/payProvider.paypal.ts +395 -84
  66. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/lib/index.ts +1 -1
  67. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/lib/paypalGateway.ts +129 -0
  68. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/types/index.ts +2 -0
  69. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +1 -1
  70. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/bean/payProvider.stripe.ts +7 -0
  71. package/vona/src/suite-vendor/a-pay/package.json +5 -5
  72. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  73. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  74. package/zova/packages-cli/cli/package.json +3 -3
  75. package/zova/packages-cli/cli-set-front/package.json +4 -4
  76. package/zova/packages-utils/zova-jsx/package.json +3 -3
  77. package/zova/packages-utils/zova-vite/package.json +2 -2
  78. package/zova/packages-zova/zova/package.json +3 -3
  79. package/zova/packages-zova/zova-core/package.json +2 -2
  80. package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/schemas.ts +16 -4
  81. package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/types.ts +240 -19
  82. package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +1 -0
  83. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeCheckout.ts +16 -0
  84. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +8 -0
  85. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +184 -8
  86. package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeCheckout.ts +12 -1
  87. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/cart.ts +11 -0
  88. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/checkout/controller.tsx +25 -1
  89. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +27 -4
  90. package/zova/src/suite/cabloy-basic/modules/basic-select/src/bean/tableCell.select.tsx +12 -6
  91. package/zova/src/suite/cabloy-basic/modules/basic-select/src/component/formFieldSelect/controller.tsx +6 -3
  92. package/zova/src/suite-vendor/a-pay/modules/a-pay/cli/openapi.config.ts +1 -1
  93. package/zova/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
  94. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/api/openapi/schemas.ts +20 -8
  95. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/api/openapi/types.ts +292 -32
  96. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/api/paymentSession.ts +26 -4
  97. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/apiSchema/paymentSession.ts +5 -0
  98. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/component/paymentNextAction/controller.tsx +2 -1
  99. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/model/paymentSession.ts +12 -0
  100. package/zova/src/suite-vendor/a-pay/package.json +2 -2
  101. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +4 -4
  102. package/zova/src/suite-vendor/a-zova/package.json +2 -2
@@ -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
+ });
@@ -0,0 +1,214 @@
1
+ import assert from 'node:assert';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { describe, it } from 'node:test';
4
+ import { app } from 'vona-mock';
5
+ import {
6
+ DtoProductCreate,
7
+ DtoProductSelectResItem,
8
+ DtoProductUpdate,
9
+ DtoProductView,
10
+ } from 'vona-module-commerce-catalog';
11
+
12
+ function findComponent(apiJson: any, predicate: (properties: any) => boolean) {
13
+ return Object.values(apiJson.components!.schemas as any).find(item => {
14
+ return predicate((item as any).properties);
15
+ }) as any;
16
+ }
17
+
18
+ function getEntryForm(component: any) {
19
+ const pageEntry = component?.rest?.blocks?.[0];
20
+ const formBlock = pageEntry?.options?.blocks?.[0];
21
+ const toolbarBlock = pageEntry?.options?.blocks?.[1];
22
+ assert.equal(pageEntry?.render, 'basic-pageentry:blockPageEntry');
23
+ assert.equal(formBlock?.render, 'basic-pageentry:blockForm');
24
+ assert.equal(toolbarBlock?.render, 'basic-pageentry:blockToolbarRow');
25
+ return toolbarBlock;
26
+ }
27
+
28
+ describe('productPresentation.test.ts', () => {
29
+ it('action:product:presentationMetadata', async () => {
30
+ await app.bean.executor.mockCtx(async () => {
31
+ const createJson = await app.bean.openapi.generateJsonOfClass(DtoProductCreate);
32
+ const createComponent = findComponent(createJson, properties => {
33
+ return properties?.title && properties?.categoryId && properties?.published;
34
+ });
35
+ assert.deepEqual(Object.keys(createComponent.properties).sort(), [
36
+ 'categoryId',
37
+ 'description',
38
+ 'published',
39
+ 'title',
40
+ ]);
41
+ assert.equal(
42
+ createComponent.properties.categoryId.rest.form.render,
43
+ 'basic-resource:formFieldResourcePicker',
44
+ );
45
+ assert.deepEqual(createComponent.properties.categoryId.rest.form.options, {
46
+ resource: 'commerce-catalog:category',
47
+ relationName: 'category',
48
+ });
49
+ assert.equal(
50
+ createComponent.properties.published.rest.form.render,
51
+ 'basic-select:formFieldSelect',
52
+ );
53
+ assert.deepEqual(
54
+ createComponent.properties.published.rest.form.options.items.map((item: any) => item.value),
55
+ [false, true],
56
+ );
57
+ const createToolbar = getEntryForm(createComponent);
58
+ assert.deepEqual(
59
+ createToolbar.options.actions.map((action: any) => action.render),
60
+ ['basic-form:actionSubmit', 'basic-form:actionBack'],
61
+ );
62
+
63
+ const updateJson = await app.bean.openapi.generateJsonOfClass(DtoProductUpdate);
64
+ const updateComponent = findComponent(updateJson, properties => {
65
+ return properties?.title && properties?.categoryId && properties?.published;
66
+ });
67
+ assert.deepEqual(Object.keys(updateComponent.properties).sort(), [
68
+ 'categoryId',
69
+ 'description',
70
+ 'published',
71
+ 'title',
72
+ ]);
73
+ assert.equal(
74
+ updateComponent.properties.categoryId.rest.form.render,
75
+ 'basic-resource:formFieldResourcePicker',
76
+ );
77
+ assert.equal(
78
+ updateComponent.properties.published.rest.form.render,
79
+ 'basic-select:formFieldSelect',
80
+ );
81
+
82
+ const viewJson = await app.bean.openapi.generateJsonOfClass(DtoProductView);
83
+ const viewComponent = findComponent(viewJson, properties => {
84
+ return (
85
+ properties?.id && properties?.categoryId && properties?.category && properties?.published
86
+ );
87
+ });
88
+ assert.deepEqual(Object.keys(viewComponent.properties.category.properties).sort(), [
89
+ 'id',
90
+ 'name',
91
+ 'published',
92
+ ]);
93
+ assert.equal(viewComponent.properties.iid.rest.visible, false);
94
+ assert.equal(viewComponent.properties.deleted.rest.visible, false);
95
+ assert.equal(
96
+ viewComponent.properties.categoryId.rest.form.render,
97
+ 'basic-resource:formFieldResourcePicker',
98
+ );
99
+ assert.deepEqual(viewComponent.properties.categoryId.rest.form.options, {
100
+ resource: 'commerce-catalog:category',
101
+ relationName: 'category',
102
+ });
103
+ assert.equal(
104
+ viewComponent.properties.published.rest.form.render,
105
+ 'basic-select:formFieldSelect',
106
+ );
107
+ const viewToolbar = getEntryForm(viewComponent);
108
+ assert.deepEqual(
109
+ viewToolbar.options.actions.map((action: any) => action.render),
110
+ ['basic-form:actionBack'],
111
+ );
112
+
113
+ const listJson = await app.bean.openapi.generateJsonOfClass(DtoProductSelectResItem);
114
+ const listComponent = findComponent(listJson, properties => properties?._operationsRow);
115
+ assert.deepEqual(Object.keys(listComponent.properties.category.properties).sort(), [
116
+ 'id',
117
+ 'name',
118
+ 'published',
119
+ ]);
120
+ assert.equal(
121
+ listComponent.properties.categoryId.rest.table.render,
122
+ 'basic-resource:resourcePicker',
123
+ );
124
+ assert.deepEqual(listComponent.properties.categoryId.rest.table.columnProps, {
125
+ resource: 'commerce-catalog:category',
126
+ relationName: 'category',
127
+ });
128
+ assert.equal(listComponent.properties.published.rest.table.render, 'basic-select:select');
129
+ assert.deepEqual(
130
+ listComponent.properties.published.rest.table.columnProps.items.map(
131
+ (item: any) => item.value,
132
+ ),
133
+ [false, true],
134
+ );
135
+ assert.deepEqual(
136
+ listComponent.properties._operationsRow.rest.table.columnProps.actions.map(
137
+ (action: any) => action.render,
138
+ ),
139
+ ['basic-table:actionUpdate', 'basic-table:actionDelete'],
140
+ );
141
+
142
+ const filterBlock = listComponent.rest.blocks[0].options.blocks[0];
143
+ const filterLayout = filterBlock.options.blocks[0].options.formLayout;
144
+ const section = filterLayout.children[0];
145
+ assert.equal(section.type, 'section');
146
+ assert.equal(section.layout, 'flow');
147
+ assert.deepEqual(
148
+ section.children.slice(0, 2).map((field: any) => field.name),
149
+ ['title', 'createdAt'],
150
+ );
151
+ assert.equal(section.children[2].block.render, 'basic-page:blockFilterActions');
152
+ });
153
+ });
154
+
155
+ it('action:product:categoryProjection', async () => {
156
+ await app.bean.executor.mockCtx(async () => {
157
+ const suffix = randomUUID().slice(0, 12);
158
+ let categoryId: number | string | undefined;
159
+ let productId: number | string | undefined;
160
+ await app.bean.passport.signinMock();
161
+ try {
162
+ categoryId = await app.bean.executor.performAction('post', '/commerce/catalog/category', {
163
+ body: { name: `presentation-product-category-${suffix}`, published: true },
164
+ });
165
+ productId = await app.bean.executor.performAction('post', '/commerce/catalog/product', {
166
+ body: {
167
+ categoryId,
168
+ title: `presentation-product-${suffix}`,
169
+ published: false,
170
+ },
171
+ });
172
+
173
+ const view: any = await app.bean.executor.performAction(
174
+ 'get',
175
+ '/commerce/catalog/product/:id',
176
+ {
177
+ params: { id: productId },
178
+ },
179
+ );
180
+ assert.equal(String(view.categoryId), String(categoryId));
181
+ assert.deepEqual(view.category, {
182
+ id: categoryId,
183
+ name: `presentation-product-category-${suffix}`,
184
+ published: true,
185
+ });
186
+
187
+ const select: any = await app.bean.executor.performAction(
188
+ 'get',
189
+ '/commerce/catalog/product',
190
+ );
191
+ const item = select.list.find((entry: any) => String(entry.id) === String(productId));
192
+ assert.ok(item);
193
+ assert.deepEqual(item.category, {
194
+ id: categoryId,
195
+ name: `presentation-product-category-${suffix}`,
196
+ published: true,
197
+ });
198
+ assert.equal(item.published, false);
199
+ } finally {
200
+ if (productId) {
201
+ await app.bean.executor.performAction('delete', '/commerce/catalog/product/:id', {
202
+ params: { id: productId },
203
+ });
204
+ }
205
+ if (categoryId) {
206
+ await app.bean.executor.performAction('delete', '/commerce/catalog/category/:id', {
207
+ params: { id: categoryId },
208
+ });
209
+ }
210
+ await app.bean.passport.signout();
211
+ }
212
+ });
213
+ });
214
+ });
@@ -1,4 +1,8 @@
1
- import type { IPaymentOutcomeEvent, IRefundOutcomeEvent } from 'vona-module-a-pay';
1
+ import type {
2
+ EntityPaymentSession,
3
+ IPaymentOutcomeEvent,
4
+ IRefundOutcomeEvent,
5
+ } from 'vona-module-a-pay';
2
6
 
3
7
  import { BeanBase } from 'vona';
4
8
  import { PayScene } from 'vona-module-a-pay';
@@ -8,8 +12,33 @@ import { PayScene } from 'vona-module-a-pay';
8
12
  { key: 'mock', providerName: 'pay-mock:mock', clientName: 'default' },
9
13
  { key: 'paypal', providerName: 'pay-paypal:paypal', clientName: 'default' },
10
14
  ],
11
- resolveProvider: (_ctx, input) =>
12
- input.providers.find(item => item.key === 'mock')?.key ?? input.providers[0]!.key,
15
+ isProviderAvailable: (_ctx, input) => {
16
+ if (input.candidate.key === 'mock') return true;
17
+ if (input.candidate.key !== 'paypal') return false;
18
+ const { clientOptions } = _ctx.bean.payProvider.resolveByName(
19
+ input.candidate.providerName,
20
+ input.candidate.clientName,
21
+ );
22
+ const options = clientOptions as {
23
+ secretCredential?: { clientId?: string; clientSecret?: string };
24
+ webhookId?: string;
25
+ merchantReference?: string;
26
+ };
27
+ const server = _ctx.app.config.server.serve;
28
+ return !!(
29
+ options.secretCredential?.clientId &&
30
+ options.secretCredential.clientSecret &&
31
+ options.webhookId &&
32
+ options.merchantReference &&
33
+ server.protocol &&
34
+ server.host
35
+ );
36
+ },
37
+ resolveProvider: (_ctx, input) => {
38
+ const mock = input.providers.find(item => item.key === 'mock');
39
+ if (!mock) return input.providers[0]!.key;
40
+ return mock.key;
41
+ },
13
42
  currencies: ['USD'],
14
43
  captureMode: 'automatic',
15
44
  sessionExpiresIn: 30 * 60 * 1000,
@@ -19,11 +48,18 @@ import { PayScene } from 'vona-module-a-pay';
19
48
  },
20
49
  })
21
50
  export class PaySceneCommerceOrder extends BeanBase {
51
+ async getPaymentCallbackPath(session: EntityPaymentSession): Promise<string> {
52
+ const attempt = await this.scope.model.paymentAttempt.get({ paymentSessionId: session.id });
53
+ if (!attempt) this.app.throw(404, 'commerce payment attempt not found');
54
+ return `/commerce/payment/${session.id}/${attempt.orderId}`;
55
+ }
56
+
22
57
  async onPaymentOutcome(event: IPaymentOutcomeEvent): Promise<void> {
23
58
  await this.$scope.commerceTrade.service.order.settlePaymentFromProvider(event);
24
59
  }
25
60
 
26
61
  async onRefundOutcome(event: IRefundOutcomeEvent): Promise<void> {
62
+ if (event.businessReference.startsWith('late-capture:')) return;
27
63
  await this.$scope.commerceTrade.service.order.settleRefundFromProvider(event);
28
64
  }
29
65
  }
@@ -9,7 +9,19 @@ import type { EntityPaymentAttempt } from '../entity/paymentAttempt.tsx';
9
9
 
10
10
  @Service()
11
11
  export class ServiceCommercePayScene extends BeanBase {
12
- async createSession(attempt: EntityPaymentAttempt) {
12
+ async availableProviderCandidates(userId: TableIdentity) {
13
+ return await this.bean.payScene.getAvailableProviderCandidates(
14
+ 'commerce-payment:commerceOrder',
15
+ {
16
+ userId,
17
+ businessReference: 'checkout-preview',
18
+ amountMinor: 0,
19
+ currency: 'USD',
20
+ },
21
+ );
22
+ }
23
+
24
+ async createSession(attempt: EntityPaymentAttempt, providerCandidateKey?: string) {
13
25
  const session = await this.$scope.pay.service.paymentSession.create({
14
26
  userId: attempt.userId,
15
27
  payScene: 'commerce-payment:commerceOrder',
@@ -17,6 +29,7 @@ export class ServiceCommercePayScene extends BeanBase {
17
29
  amountMinor: attempt.amountCents,
18
30
  currency: attempt.currency,
19
31
  correlationId: `${attempt.correlationId}:session`,
32
+ providerCandidateKey,
20
33
  });
21
34
  await this.scope.model.paymentAttempt.updateById(attempt.id, {
22
35
  paymentSessionId: session.id,
@@ -12,6 +12,7 @@ export interface IPaymentAttemptCreateCommand {
12
12
  currency: 'USD';
13
13
  amountCents: number;
14
14
  correlationId: string;
15
+ providerCandidateKey?: string;
15
16
  }
16
17
 
17
18
  export type TypePaymentAttemptFinalState = Extract<
@@ -30,7 +31,10 @@ export class ServicePaymentAttempt extends BeanBase {
30
31
  amountCents: command.amountCents,
31
32
  correlationId: command.correlationId,
32
33
  });
33
- const session = await this.scope.service.commercePayScene.createSession(attempt);
34
+ const session = await this.scope.service.commercePayScene.createSession(
35
+ attempt,
36
+ command.providerCandidateKey,
37
+ );
34
38
  return {
35
39
  ...attempt,
36
40
  paymentSessionId: session.id,
@@ -834,6 +834,8 @@ export * from '../dto/cartItem.tsx';
834
834
  export * from '../dto/cartUpdateItem.tsx';
835
835
  export * from '../dto/cartView.tsx';
836
836
  export * from '../dto/checkoutCreate.tsx';
837
+ export * from '../dto/checkoutPaymentMethod.tsx';
838
+ export * from '../dto/checkoutPaymentMethods.tsx';
837
839
  export * from '../dto/checkoutResult.tsx';
838
840
  export * from '../dto/orderAddressSnapshot.tsx';
839
841
  export * from '../dto/orderCouponSnapshot.tsx';
@@ -872,6 +874,8 @@ import type { IDtoOptionsCartItem } from '../dto/cartItem.tsx';
872
874
  import type { IDtoOptionsCartUpdateItem } from '../dto/cartUpdateItem.tsx';
873
875
  import type { IDtoOptionsCartView } from '../dto/cartView.tsx';
874
876
  import type { IDtoOptionsCheckoutCreate } from '../dto/checkoutCreate.tsx';
877
+ import type { IDtoOptionsCheckoutPaymentMethod } from '../dto/checkoutPaymentMethod.tsx';
878
+ import type { IDtoOptionsCheckoutPaymentMethods } from '../dto/checkoutPaymentMethods.tsx';
875
879
  import type { IDtoOptionsCheckoutResult } from '../dto/checkoutResult.tsx';
876
880
  import type { IDtoOptionsOrderAddressSnapshot } from '../dto/orderAddressSnapshot.tsx';
877
881
  import type { IDtoOptionsOrderCouponSnapshot } from '../dto/orderCouponSnapshot.tsx';
@@ -914,6 +918,8 @@ declare module 'vona-module-a-web' {
914
918
  'commerce-trade:cartUpdateItem': IDtoOptionsCartUpdateItem;
915
919
  'commerce-trade:cartView': IDtoOptionsCartView;
916
920
  'commerce-trade:checkoutCreate': IDtoOptionsCheckoutCreate;
921
+ 'commerce-trade:checkoutPaymentMethod': IDtoOptionsCheckoutPaymentMethod;
922
+ 'commerce-trade:checkoutPaymentMethods': IDtoOptionsCheckoutPaymentMethods;
917
923
  'commerce-trade:checkoutResult': IDtoOptionsCheckoutResult;
918
924
  'commerce-trade:orderAddressSnapshot': IDtoOptionsOrderAddressSnapshot;
919
925
  'commerce-trade:orderCouponSnapshot': IDtoOptionsOrderCouponSnapshot;
@@ -961,6 +967,8 @@ import type { DtoCartItem } from '../dto/cartItem.tsx';
961
967
  import type { DtoCartUpdateItem } from '../dto/cartUpdateItem.tsx';
962
968
  import type { DtoCartView } from '../dto/cartView.tsx';
963
969
  import type { DtoCheckoutCreate } from '../dto/checkoutCreate.tsx';
970
+ import type { DtoCheckoutPaymentMethod } from '../dto/checkoutPaymentMethod.tsx';
971
+ import type { DtoCheckoutPaymentMethods } from '../dto/checkoutPaymentMethods.tsx';
964
972
  import type { DtoCheckoutResult } from '../dto/checkoutResult.tsx';
965
973
  import type { DtoOrderAddressSnapshot } from '../dto/orderAddressSnapshot.tsx';
966
974
  import type { DtoOrderCouponSnapshot } from '../dto/orderCouponSnapshot.tsx';
@@ -1016,6 +1024,14 @@ declare module 'vona-module-commerce-trade' {
1016
1024
  fields?: TypeEntityOptionsFields<DtoCheckoutCreate, IDtoOptionsCheckoutCreate[TypeSymbolKeyFieldsMore]>;
1017
1025
  }
1018
1026
 
1027
+ export interface IDtoOptionsCheckoutPaymentMethod {
1028
+ fields?: TypeEntityOptionsFields<DtoCheckoutPaymentMethod, IDtoOptionsCheckoutPaymentMethod[TypeSymbolKeyFieldsMore]>;
1029
+ }
1030
+
1031
+ export interface IDtoOptionsCheckoutPaymentMethods {
1032
+ fields?: TypeEntityOptionsFields<DtoCheckoutPaymentMethods, IDtoOptionsCheckoutPaymentMethods[TypeSymbolKeyFieldsMore]>;
1033
+ }
1034
+
1019
1035
  export interface IDtoOptionsCheckoutResult {
1020
1036
  fields?: TypeEntityOptionsFields<DtoCheckoutResult, IDtoOptionsCheckoutResult[TypeSymbolKeyFieldsMore]>;
1021
1037
  }
@@ -1267,6 +1283,7 @@ declare module 'vona-module-commerce-trade' {
1267
1283
  declare module 'vona-module-a-web' {
1268
1284
  export interface IApiPathGetRecord{
1269
1285
  '/commerce/trade/cart': undefined;
1286
+ '/commerce/trade/checkout/payment-methods': undefined;
1270
1287
  '/commerce/trade/order/mine': undefined;
1271
1288
  '/commerce/trade/order/viewMine/:id': undefined;
1272
1289
  '/commerce/trade/order': undefined;
@@ -1,3 +1,4 @@
1
+ import type { IUser } from 'vona-module-a-user';
1
2
  import type { IDecoratorControllerOptions } from 'vona-module-a-web';
2
3
 
3
4
  import { BeanBase } from 'vona';
@@ -5,12 +6,29 @@ import { Api } from 'vona-module-a-openapiutils';
5
6
  import { Arg, Controller, Web } from 'vona-module-a-web';
6
7
 
7
8
  import { DtoCheckoutCreate } from '../dto/checkoutCreate.tsx';
9
+ import { DtoCheckoutPaymentMethods } from '../dto/checkoutPaymentMethods.tsx';
8
10
  import { DtoCheckoutResult } from '../dto/checkoutResult.tsx';
9
11
 
10
12
  export interface IControllerOptionsCheckout extends IDecoratorControllerOptions {}
11
13
 
12
14
  @Controller<IControllerOptionsCheckout>('checkout')
13
15
  export class ControllerCheckout extends BeanBase {
16
+ @Web.get('payment-methods')
17
+ @Api.body(DtoCheckoutPaymentMethods)
18
+ async paymentMethods(@Arg.user() user: IUser): Promise<DtoCheckoutPaymentMethods> {
19
+ const candidates =
20
+ await this.$scope.commercePayment.service.commercePayScene.availableProviderCandidates(
21
+ user.id,
22
+ );
23
+ const items = candidates.map(candidate => ({
24
+ key: candidate.key,
25
+ label: candidate.key === 'paypal' ? 'PayPal' : 'Mock payment',
26
+ interaction: candidate.key === 'paypal' ? ('redirect' as const) : ('embedded' as const),
27
+ }));
28
+ const defaultKey = items.find(item => item.key === 'mock')?.key ?? items[0]!.key;
29
+ return { items, defaultKey };
30
+ }
31
+
14
32
  @Web.post()
15
33
  @Api.body(DtoCheckoutResult)
16
34
  async create(@Arg.body() command: DtoCheckoutCreate): Promise<DtoCheckoutResult> {
@@ -16,4 +16,7 @@ export class DtoCheckoutCreate {
16
16
 
17
17
  @Api.field(v.required(), v.min(1), v.max(80))
18
18
  correlationId: string;
19
+
20
+ @Api.field(v.optional(), v.min(1), v.max(100))
21
+ providerCandidateKey?: string;
19
22
  }
@@ -0,0 +1,19 @@
1
+ import type { IDecoratorDtoOptions } from 'vona-module-a-web';
2
+
3
+ import { Api, v } from 'vona-module-a-openapiutils';
4
+ import { Dto } from 'vona-module-a-web';
5
+ import { z } from 'zod';
6
+
7
+ export interface IDtoOptionsCheckoutPaymentMethod extends IDecoratorDtoOptions {}
8
+
9
+ @Dto<IDtoOptionsCheckoutPaymentMethod>()
10
+ export class DtoCheckoutPaymentMethod {
11
+ @Api.field(v.required(), v.min(1), v.max(100))
12
+ key: string;
13
+
14
+ @Api.field(v.required(), v.min(1), v.max(100))
15
+ label: string;
16
+
17
+ @Api.field(z.enum(['redirect', 'embedded']))
18
+ interaction: 'redirect' | 'embedded';
19
+ }