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.
Files changed (109) 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 +21 -0
  12. package/CLAUDE.md +1 -1
  13. package/cabloy-docs/.vitepress/config.mjs +2 -0
  14. package/cabloy-docs/ai/playbook-technical-blog-authoring.md +158 -0
  15. package/cabloy-docs/backend/backend-contract-emission-specimen.md +2 -2
  16. package/cabloy-docs/backend/dto-guide.md +10 -7
  17. package/cabloy-docs/backend/dto-infer-generation.md +127 -7
  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 +89 -0
  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-vona/vona/package.json +1 -1
  31. package/vona/pnpm-lock.yaml +48 -146
  32. package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineCreate.tsx +6 -27
  33. package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineItem.tsx +6 -31
  34. package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineUpdate.tsx +6 -2
  35. package/vona/src/suite/a-commerce/modules/commerce-member/src/lib/addressMine.ts +16 -0
  36. package/vona/src/suite/a-commerce/modules/commerce-member/src/service/address.ts +4 -17
  37. package/vona/src/suite/a-commerce/modules/commerce-member/test/addressOwnership.test.ts +63 -19
  38. package/vona/src/suite/a-commerce/modules/commerce-payment/package.json +2 -1
  39. package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/payScene.commerceOrder.ts +11 -2
  40. package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/commercePayScene.ts +26 -0
  41. package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentAttempt.test.ts +19 -2
  42. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/en-us.ts +5 -2
  43. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/zh-cn.ts +5 -2
  44. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateCreate.tsx +26 -35
  45. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateUpdate.tsx +6 -1
  46. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/entity/couponTemplate.tsx +40 -4
  47. package/vona/src/suite/a-commerce/modules/commerce-promotion/test/couponTemplateFormLayout.test.ts +85 -0
  48. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +1 -1
  49. package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/order.ts +3 -5
  50. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderSummary.tsx +7 -40
  51. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/shipmentView.tsx +6 -15
  52. package/vona/src/suite/a-commerce/modules/commerce-trade/src/lib/order.ts +9 -0
  53. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +121 -11
  54. package/vona/src/suite/a-commerce/modules/commerce-trade/test/order.test.ts +25 -0
  55. package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +19 -2
  56. package/vona/src/suite/a-training/modules/training-record/src/dto/detailRecordSubjectResItem.tsx +6 -1
  57. package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectResItem.tsx +3 -1
  58. package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +4 -1
  59. package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +45 -0
  60. package/vona/src/suite/a-training/modules/training-student/src/dto/detailRecordResItem.tsx +6 -1
  61. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSummary.tsx +5 -23
  62. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +37 -5
  63. package/vona/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
  64. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/.metadata/index.ts +29 -0
  65. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.index.ts +3 -0
  66. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.redlock.ts +3 -1
  67. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.version.ts +7 -0
  68. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/queue.outboxDispatch.ts +9 -4
  69. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/schedule.providerOperationDispatch.ts +11 -0
  70. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/outboxEvent.tsx +11 -4
  71. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/providerOperation.tsx +12 -0
  72. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/webhookInbox.tsx +6 -0
  73. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/outbox.ts +12 -5
  74. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/paymentSession.ts +1 -37
  75. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/providerOperation.ts +355 -1
  76. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/refundOperation.ts +226 -0
  77. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/webhook.ts +148 -50
  78. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payScene.ts +2 -1
  79. package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
  80. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/.metadata/index.ts +39 -22
  81. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/bean/payProvider.mock.ts +42 -13
  82. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/controller/mockPayment.ts +11 -0
  83. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/dto/mockRefundComplete.tsx +13 -0
  84. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/dto/mockRefundReceipt.tsx +16 -0
  85. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/service/payMock.ts +56 -0
  86. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/package.json +2 -1
  87. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/bean/payProvider.paypal.ts +117 -8
  88. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +1 -1
  89. package/vona/src/suite-vendor/a-pay/package.json +5 -5
  90. package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
  91. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoGet.ts +5 -5
  92. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  93. package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/schemas.ts +54 -12
  94. package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/types.ts +625 -64
  95. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/types.ts +55 -15
  96. package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +1 -1
  97. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeOrder.ts +15 -17
  98. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +12 -8
  99. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +111 -26
  100. package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeOrder.ts +3 -3
  101. package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionRefund/controller.tsx +1 -8
  102. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +5 -9
  103. package/zova/src/suite/a-training/modules/training-student/src/.metadata/index.ts +1 -1
  104. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +301 -17
  105. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +5461 -1788
  106. package/vona/src/suite-vendor/a-pay/modules/a-pay/test/outbox.test.ts +0 -209
  107. package/vona/src/suite-vendor/a-pay/modules/a-pay/test/paymentSession.test.ts +0 -153
  108. package/vona/src/suite-vendor/a-pay/modules/a-pay/test/webhook.test.ts +0 -278
  109. package/vona/src/suite-vendor/a-pay/modules/pay-mock/test/payMock.test.ts +0 -141
@@ -1,47 +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
- import { z } from 'zod';
5
+
6
+ import { orderSummaryColumns } from '../lib/order.ts';
7
+ import { ModelOrder } from '../model/order.ts';
7
8
 
8
9
  export interface IDtoOptionsOrderSummary extends IDecoratorDtoOptions {}
9
10
 
10
11
  @Dto<IDtoOptionsOrderSummary>()
11
- export class DtoOrderSummary {
12
- @Api.field(v.tableIdentity())
13
- id: TableIdentity;
14
-
15
- @Api.field(
16
- z.enum([
17
- 'awaiting_payment',
18
- 'paid',
19
- 'refund_requested',
20
- 'refund_approved',
21
- 'refund_rejected',
22
- 'shipped',
23
- 'refunded',
24
- 'cancelled',
25
- 'expired',
26
- ]),
27
- )
28
- state:
29
- | 'awaiting_payment'
30
- | 'paid'
31
- | 'refund_requested'
32
- | 'refund_approved'
33
- | 'refund_rejected'
34
- | 'shipped'
35
- | 'refunded'
36
- | 'cancelled'
37
- | 'expired';
38
-
39
- @Api.field(z.literal('USD'))
40
- currency: 'USD';
41
-
42
- @Api.field(z.number().int().nonnegative())
43
- payableTotalCents: number;
44
-
45
- @Api.field(v.required())
46
- createdAt: Date;
47
- }
12
+ export class DtoOrderSummary extends $Dto.get(() => ModelOrder, {
13
+ columns: orderSummaryColumns,
14
+ }) {}
@@ -1,22 +1,13 @@
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
 
6
+ import { ModelShipment } from '../model/shipment.ts';
7
+
7
8
  export interface IDtoOptionsShipmentView extends IDecoratorDtoOptions {}
8
9
 
9
10
  @Dto<IDtoOptionsShipmentView>()
10
- export class DtoShipmentView {
11
- @Api.field(v.tableIdentity())
12
- id: TableIdentity;
13
-
14
- @Api.field(v.required())
15
- carrier: string;
16
-
17
- @Api.field(v.required())
18
- trackingNumber: string;
19
-
20
- @Api.field(v.required())
21
- shippedAt: Date;
22
- }
11
+ export class DtoShipmentView extends $Dto.get(() => ModelShipment, {
12
+ columns: ['id', 'carrier', 'trackingNumber', 'shippedAt'],
13
+ }) {}
@@ -0,0 +1,9 @@
1
+ import type { EntityOrder } from '../entity/order.tsx';
2
+
3
+ export const orderSummaryColumns = [
4
+ 'id',
5
+ 'state',
6
+ 'currency',
7
+ 'payableTotalCents',
8
+ 'createdAt',
9
+ ] satisfies Array<keyof EntityOrder>;
@@ -1,6 +1,6 @@
1
1
  import type { TableIdentity } from 'table-identity';
2
2
  import type { IQueryParams } from 'vona-module-a-orm';
3
- import type { IPaymentOutcomeEvent } from 'vona-module-a-pay';
3
+ import type { IPaymentOutcomeEvent, IRefundOutcomeEvent } from 'vona-module-a-pay';
4
4
  import type { EntityPaymentAttempt } from 'vona-module-commerce-payment';
5
5
  import type {
6
6
  EntityRefundAttempt,
@@ -21,7 +21,6 @@ import type { DtoOrderLineSkuAttributeSnapshot } from '../dto/orderLineSkuAttrib
21
21
  import type { DtoOrderMineRes } from '../dto/orderMineRes.tsx';
22
22
  import type { DtoOrderSelectRes } from '../dto/orderSelectRes.tsx';
23
23
  import type { DtoOrderShip } from '../dto/orderShip.tsx';
24
- import type { DtoOrderSummary } from '../dto/orderSummary.tsx';
25
24
  import type { DtoOrderView } from '../dto/orderView.tsx';
26
25
  import type { DtoPaymentOutcomeCreate } from '../dto/paymentOutcomeCreate.tsx';
27
26
  import type { DtoPaymentOutcomeResult } from '../dto/paymentOutcomeResult.tsx';
@@ -35,6 +34,8 @@ import type { EntityOrderLine } from '../entity/orderLine.tsx';
35
34
  import type { EntityShipment } from '../entity/shipment.tsx';
36
35
  import type { ModelOrder } from '../model/order.ts';
37
36
 
37
+ import { orderSummaryColumns } from '../lib/order.ts';
38
+
38
39
  const maxOrderCents = 2_147_483_647;
39
40
  const customerVisibleOrderStates: EntityOrder['state'][] = [
40
41
  'awaiting_payment',
@@ -421,6 +422,87 @@ export class ServiceOrder extends BeanBase {
421
422
  return this._paymentOutcomeResult({ ...order, state: orderState }, finalizedAttempt);
422
423
  }
423
424
 
425
+ @Core.transaction({ isolationLevel: 'SERIALIZABLE' })
426
+ @Core.retryable(serializationRetryOptions)
427
+ async settleRefundFromProvider(event: IRefundOutcomeEvent): Promise<DtoRefundResult> {
428
+ const refundAttempt = await this.$scope.commercePayment.model.refundAttempt.get({
429
+ refundOperationId: event.refundOperationId,
430
+ });
431
+ if (!refundAttempt || String(refundAttempt.id) !== event.businessReference) {
432
+ this.app.throw(404, 'commerce refund attempt not found');
433
+ }
434
+ const order = await this.scope.model.order.getByIdForUpdate(refundAttempt.orderId);
435
+ if (!order) this.app.throw(404, 'order not found');
436
+ const request = await this.$scope.commercePayment.model.refundRequest.getByIdForUpdate(
437
+ refundAttempt.refundRequestId,
438
+ );
439
+ const lockedAttempt = await this.$scope.commercePayment.model.refundAttempt.getByIdForUpdate(
440
+ refundAttempt.id,
441
+ );
442
+ if (!request || !lockedAttempt) this.app.throw(404, 'commerce refund request not found');
443
+ if (
444
+ lockedAttempt.amountCents !== event.amountMinor ||
445
+ lockedAttempt.currency !== event.currency ||
446
+ String(lockedAttempt.refundOperationId) !== String(event.refundOperationId)
447
+ ) {
448
+ this.app.throw(409, 'provider event conflicts with the commerce refund attempt');
449
+ }
450
+ const existingAudit = await this.$scope.commercePayment.model.refundAudit.get({
451
+ refundAttemptId: lockedAttempt.id,
452
+ idempotencyKey: event.eventId.slice(0, 100),
453
+ });
454
+ if (existingAudit) return this._refundResult(order, request, lockedAttempt);
455
+ if (
456
+ order.state !== 'refund_approved' ||
457
+ request.state !== 'approved' ||
458
+ lockedAttempt.state !== 'created'
459
+ ) {
460
+ this.app.throw(409, 'refund attempt is no longer available');
461
+ }
462
+
463
+ const now = new Date();
464
+ const isSucceeded = event.state === 'succeeded';
465
+ const attemptState = isSucceeded ? 'succeeded' : 'failed';
466
+ const refundState = isSucceeded ? 'refunded' : 'failed';
467
+ const orderState = isSucceeded ? 'refunded' : 'paid';
468
+ const correlationId = `${order.correlationId}:provider-refund:${event.eventId}`;
469
+ const reason = `provider refund ${event.state}`;
470
+ await this.$scope.commercePayment.model.refundAttempt.updateById(lockedAttempt.id, {
471
+ state: attemptState,
472
+ providerRefundId: event.providerRefundId,
473
+ finalizedAt: now,
474
+ });
475
+ await this.$scope.commercePayment.model.refundRequest.updateById(request.id, {
476
+ state: refundState,
477
+ finalizedAt: now,
478
+ });
479
+ await this.scope.model.order.updateById(order.id, { state: orderState });
480
+ if (isSucceeded) await this._restoreConsumedOrderStock(order, reason);
481
+ await this._appendAudit({
482
+ orderId: order.id,
483
+ operation: isSucceeded ? 'refunded' : 'refund_failed',
484
+ fromState: 'refund_approved',
485
+ toState: orderState,
486
+ correlationId,
487
+ reason,
488
+ });
489
+ await this._appendRefundAudit({
490
+ refundRequest: request,
491
+ refundAttempt: lockedAttempt,
492
+ order,
493
+ toRefundState: refundState,
494
+ attemptState,
495
+ idempotencyKey: event.eventId.slice(0, 100),
496
+ correlationId,
497
+ reason,
498
+ });
499
+ return this._refundResult(
500
+ { ...order, state: orderState },
501
+ { ...request, state: refundState },
502
+ { ...lockedAttempt, state: attemptState },
503
+ );
504
+ }
505
+
424
506
  @Core.transaction({ isolationLevel: 'SERIALIZABLE' })
425
507
  @Core.retryable(serializationRetryOptions)
426
508
  async applyPaymentOutcome(
@@ -636,6 +718,41 @@ export class ServiceOrder extends BeanBase {
636
718
  return await this._reviewRefund(orderId, command, decision, onStage);
637
719
  }
638
720
 
721
+ async executeRefund(orderId: TableIdentity): Promise<DtoRefundResult> {
722
+ const prepared = await this._prepareRefundExecution(orderId);
723
+ await this.$scope.commercePayment.service.commercePayScene.createRefundOperation(prepared.id);
724
+ const linked = await this.$scope.commercePayment.model.refundAttempt.getById(prepared.id);
725
+ if (!linked?.refundOperationId) this.app.throw(500, 'commerce refund operation was not linked');
726
+ await this.$scope.pay.service.refundOperation.submit(linked.refundOperationId);
727
+ const order = await this.scope.model.order.getById(orderId);
728
+ const request = await this.$scope.commercePayment.model.refundRequest.getById(
729
+ prepared.refundRequestId,
730
+ );
731
+ const attempt = await this.$scope.commercePayment.model.refundAttempt.getById(prepared.id);
732
+ if (!order || !request || !attempt) this.app.throw(404, 'commerce refund attempt not found');
733
+ return this._refundResult(order, request, attempt);
734
+ }
735
+
736
+ @Core.transaction({ isolationLevel: 'SERIALIZABLE' })
737
+ @Core.retryable(serializationRetryOptions)
738
+ private async _prepareRefundExecution(orderId: TableIdentity) {
739
+ const order = await this.scope.model.order.getByIdForUpdate(orderId);
740
+ if (!order) this.app.throw(404, 'order not found');
741
+ const request = await this.$scope.commercePayment.model.refundRequest.getForUpdate({
742
+ orderId: order.id,
743
+ state: 'approved',
744
+ });
745
+ if (!request) this.app.throw(404, 'approved refund request not found');
746
+ const attempt = await this.$scope.commercePayment.model.refundAttempt.getForUpdate({
747
+ refundRequestId: request.id,
748
+ });
749
+ if (!attempt) this.app.throw(404, 'refund attempt not found');
750
+ if (order.state !== 'refund_approved' || attempt.state !== 'created') {
751
+ this.app.throw(409, 'refund is not available for execution');
752
+ }
753
+ return attempt;
754
+ }
755
+
639
756
  @Core.transaction({ isolationLevel: 'SERIALIZABLE' })
640
757
  @Core.retryable(serializationRetryOptions)
641
758
  async applyRefundOutcome(
@@ -825,9 +942,9 @@ export class ServiceOrder extends BeanBase {
825
942
  }
826
943
 
827
944
  async mine(params?: IQueryParams<ModelOrder>): Promise<DtoOrderMineRes> {
828
- const result = await this.scope.model.order.selectAndCount({
945
+ return await this.scope.model.order.selectAndCount({
829
946
  ...params,
830
- columns: ['id', 'state', 'currency', 'payableTotalCents', 'createdAt'],
947
+ columns: orderSummaryColumns,
831
948
  where: {
832
949
  ...params?.where,
833
950
  userId: this.bean.passport.currentUser!.id,
@@ -835,13 +952,6 @@ export class ServiceOrder extends BeanBase {
835
952
  },
836
953
  orders: params?.orders ?? [['id', 'desc']],
837
954
  });
838
- return {
839
- ...result,
840
- list: result.list.map(order => ({
841
- ...order,
842
- state: order.state as DtoOrderSummary['state'],
843
- })),
844
- };
845
955
  }
846
956
 
847
957
  async viewMine(id: TableIdentity): Promise<DtoOrderDetail | undefined> {
@@ -1,8 +1,33 @@
1
1
  import assert from 'node:assert';
2
2
  import { describe, it } from 'node:test';
3
3
  import { app } from 'vona-mock';
4
+ import { DtoOrderSummary, DtoShipmentView } from 'vona-module-commerce-trade';
4
5
 
5
6
  describe('order.test.ts', () => {
7
+ it('action:order:emittedDtoSchemas', async () => {
8
+ await app.bean.executor.mockCtx(async () => {
9
+ const summaryJson = await app.bean.openapi.generateJsonOfClass(DtoOrderSummary);
10
+ const summaryComponent = Object.values(summaryJson.components!.schemas as any).find(item => {
11
+ return (item as any).properties?.payableTotalCents && (item as any).properties?.createdAt;
12
+ }) as any;
13
+ assert.deepEqual(
14
+ Object.keys(summaryComponent.properties).sort(),
15
+ ['id', 'state', 'currency', 'payableTotalCents', 'createdAt'].sort(),
16
+ );
17
+
18
+ const shipmentJson = await app.bean.openapi.generateJsonOfClass(DtoShipmentView);
19
+ const shipmentComponent = Object.values(shipmentJson.components!.schemas as any).find(
20
+ item => {
21
+ return (item as any).properties?.carrier && (item as any).properties?.trackingNumber;
22
+ },
23
+ ) as any;
24
+ assert.deepEqual(
25
+ Object.keys(shipmentComponent.properties).sort(),
26
+ ['id', 'carrier', 'trackingNumber', 'shippedAt'].sort(),
27
+ );
28
+ });
29
+ });
30
+
6
31
  it('action:order permits system-admin read and shipment actions only', async () => {
7
32
  await app.bean.executor.mockCtx(async () => {
8
33
  await app.bean.passport.signinMock();
@@ -1,7 +1,7 @@
1
1
  import assert from 'node:assert';
2
2
  import { createHmac, 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
  interface IFixture {
7
7
  categoryId?: number;
@@ -166,6 +166,23 @@ async function createCheckoutFixture(suffix: string, withCoupon = true): Promise
166
166
  }
167
167
 
168
168
  describe('paymentOutcome.test.ts', { concurrency: false, sequential: true }, () => {
169
+ const releaseTestLocks: Array<() => void> = [];
170
+
171
+ before(async () => {
172
+ try {
173
+ for (const scene of ['a-commerce', 'a-pay']) {
174
+ releaseTestLocks.push(await acquireTestLock(scene));
175
+ }
176
+ } catch (error) {
177
+ for (const release of releaseTestLocks.reverse()) release();
178
+ throw error;
179
+ }
180
+ });
181
+
182
+ after(() => {
183
+ for (const release of releaseTestLocks.reverse()) release();
184
+ });
185
+
169
186
  it('delivers a signed webhook through the durable outbox to Commerce exactly once', async () => {
170
187
  await app.bean.executor.mockCtx(async () => {
171
188
  const fixture: IFixture = {};
@@ -38,7 +38,12 @@ export interface IDtoOptionsDetailRecordSubjectResItem extends IDecoratorDtoOpti
38
38
  export class DtoDetailRecordSubjectResItem extends $Dto.get(() => ModelSubject, {
39
39
  dtoClass: DtoDetailRecordSubjectBase,
40
40
  }) {
41
- @Api.field(v.title('#'), ZovaRender.order(1, 'core'), ZovaRender.cell('basic-details:lineNumber'))
41
+ @Api.field(
42
+ v.title('#'),
43
+ ZovaRender.order(1, 'core'),
44
+ ZovaRender.cell('basic-details:lineNumber'),
45
+ v.optional(),
46
+ )
42
47
  _lineNumber?: number;
43
48
 
44
49
  @Api.field(
@@ -49,7 +49,9 @@ export interface IDtoOptionsRecordSelectResItem extends IDecoratorDtoOptions {}
49
49
  }),
50
50
  ],
51
51
  })
52
- export class DtoRecordSelectResItem extends $Dto.get(() => ModelRecord) {
52
+ export class DtoRecordSelectResItem extends $Dto.get(() => ModelRecord, {
53
+ include: { student: true },
54
+ }) {
53
55
  @Api.field(
54
56
  ZovaRender.visible(false),
55
57
  v.optional(),
@@ -37,7 +37,10 @@ export interface IDtoOptionsRecordView extends IDecoratorDtoOptions {}
37
37
  },
38
38
  })
39
39
  export class DtoRecordView extends $Dto.get(() => ModelRecord, {
40
- include: { trainingRecordSubjects: { dtoClass: DtoDetailRecordSubjectView } },
40
+ include: {
41
+ student: true,
42
+ trainingRecordSubjects: { dtoClass: DtoDetailRecordSubjectView },
43
+ },
41
44
  }) {
42
45
  @Api.field(
43
46
  ZovaRender.visible(false),
@@ -8,10 +8,51 @@ import path from 'node:path';
8
8
  import { describe, it } from 'node:test';
9
9
  import { app } from 'vona-mock';
10
10
 
11
+ import { DtoDetailRecordSubjectResItem } from '../src/dto/detailRecordSubjectResItem.tsx';
12
+ import { DtoRecordSelectResItem } from '../src/dto/recordSelectResItem.tsx';
13
+ import { DtoRecordView } from '../src/dto/recordView.tsx';
14
+
11
15
  const dossierTextAttendance = Buffer.from('attendance dossier file');
12
16
  const dossierTextAssessment = Buffer.from('assessment dossier file');
13
17
 
14
18
  describe('record.test.ts', () => {
19
+ it('action:record:emittedDtoSchemas', async () => {
20
+ await app.bean.executor.mockCtx(async () => {
21
+ const subjectApiJson = await app.bean.openapi.generateJsonOfClass(
22
+ DtoDetailRecordSubjectResItem,
23
+ );
24
+ const subjectComponent = Object.values(subjectApiJson.components!.schemas as any).find(
25
+ item => (item as any).properties?._lineNumber,
26
+ ) as any;
27
+ assert.ok(subjectComponent?.properties?._lineNumber);
28
+ assert.equal(subjectComponent.required?.includes('_lineNumber'), false);
29
+
30
+ for (const [DtoClass, hasTrainingRecordSubjects] of [
31
+ [DtoRecordSelectResItem, false],
32
+ [DtoRecordView, true],
33
+ ] as const) {
34
+ const apiJson = await app.bean.openapi.generateJsonOfClass(DtoClass);
35
+ const component = Object.values(apiJson.components!.schemas as any).find(item => {
36
+ const properties = (item as any).properties;
37
+ return (
38
+ properties?.student &&
39
+ Boolean(properties?.trainingRecordSubjects) === hasTrainingRecordSubjects
40
+ );
41
+ }) as any;
42
+ assert.ok(component);
43
+ assert.ok(component.properties.student);
44
+ assert.equal(
45
+ Boolean(component.properties.trainingRecordSubjects),
46
+ hasTrainingRecordSubjects,
47
+ );
48
+ assert.deepEqual(Object.keys(component.properties.student.properties).sort(), [
49
+ 'id',
50
+ 'name',
51
+ ]);
52
+ }
53
+ });
54
+ });
55
+
15
56
  it('action:record', async () => {
16
57
  await app.bean.executor.mockCtx(async () => {
17
58
  const studentData: DtoStudentCreate = {
@@ -80,6 +121,8 @@ describe('record.test.ts', () => {
80
121
  assert.equal(!!recordItem, true);
81
122
  assert.equal(recordItem!.name, recordData.name);
82
123
  assert.equal(String(recordItem!.studentId), String(studentId));
124
+ assert.equal(String(recordItem!.student?.id), String(studentId));
125
+ assert.equal(recordItem!.student?.name, studentData.name);
83
126
  assert.equal(recordItem!.dossierFiles?.length, 1);
84
127
  assert.equal(recordItem!.dossierFiles?.[0]?.filename, 'attendance.txt');
85
128
 
@@ -90,6 +133,8 @@ describe('record.test.ts', () => {
90
133
  const recordDossierFile = record.dossierFiles?.[0];
91
134
  assert.equal(record.name, recordData.name);
92
135
  assert.equal(String(record.studentId), String(studentId));
136
+ assert.equal(String(record.student?.id), String(studentId));
137
+ assert.equal(record.student?.name, studentData.name);
93
138
  assert.equal(record.subjectCount, recordData.subjectCount);
94
139
  assert.equal(record.totalScore, recordData.totalScore);
95
140
  assert.equal(Number(record.averageScore), recordData.averageScore);
@@ -39,7 +39,12 @@ export class DtoDetailRecordResItem extends $Dto.get(() => ModelRecord, {
39
39
  dtoClass: DtoDetailRecordBase,
40
40
  include: { trainingRecordSubjects: false },
41
41
  }) {
42
- @Api.field(v.title('#'), ZovaRender.order(1, 'core'), ZovaRender.cell('basic-details:lineNumber'))
42
+ @Api.field(
43
+ v.title('#'),
44
+ ZovaRender.order(1, 'core'),
45
+ ZovaRender.cell('basic-details:lineNumber'),
46
+ v.optional(),
47
+ )
43
48
  _lineNumber?: number;
44
49
 
45
50
  @Api.field(
@@ -1,39 +1,21 @@
1
- import type { TableIdentity } from 'table-identity';
2
1
  import type { IDecoratorDtoOptions } from 'vona-module-a-web';
3
2
 
4
3
  import { Api, v } from 'vona-module-a-openapiutils';
4
+ import { $Dto } from 'vona-module-a-orm';
5
5
  import { Dto } from 'vona-module-a-web';
6
6
 
7
7
  import { $locale } from '../.metadata/locales.ts';
8
+ import { ModelStudent } from '../model/student.ts';
8
9
 
9
10
  export interface IDtoOptionsStudentSummary extends IDecoratorDtoOptions {}
10
11
 
11
12
  @Dto<IDtoOptionsStudentSummary>()
12
- export class DtoStudentSummary {
13
- @Api.field(v.tableIdentity())
14
- id: TableIdentity;
15
-
16
- @Api.field(v.title($locale('StudentName')))
17
- name: string;
18
-
19
- @Api.field(
20
- v.title($locale('Mobile')),
21
- v.serializerReplace({
22
- patternFrom: /^(\d{3})\d{4}(\d+)$/,
23
- patternTo: '$1****$2',
24
- }),
25
- )
26
- mobile: string;
27
-
28
- @Api.field(v.title($locale('Level')))
29
- level: number;
30
-
13
+ export class DtoStudentSummary extends $Dto.get(() => ModelStudent, {
14
+ columns: ['id', 'name', 'mobile', 'level', 'description'],
15
+ }) {
31
16
  @Api.field(v.title($locale('LevelTitle')))
32
17
  levelTitle: string;
33
18
 
34
- @Api.field(v.title($locale('Description')), v.optional())
35
- description?: string;
36
-
37
19
  @Api.field(v.title($locale('DescriptionLength')))
38
20
  descriptionLength: number;
39
21
 
@@ -1,8 +1,4 @@
1
- import type {
2
- DtoStudentSelectRes,
3
- DtoStudentSummary,
4
- EntityStudent,
5
- } from 'vona-module-training-student';
1
+ import type { DtoStudentSelectRes, EntityStudent } from 'vona-module-training-student';
6
2
 
7
3
  import assert from 'node:assert';
8
4
  import { describe, it } from 'node:test';
@@ -14,6 +10,9 @@ import {
14
10
  DtoStudentView,
15
11
  } from 'vona-module-training-student';
16
12
 
13
+ import { DtoDetailRecordResItem } from '../src/dto/detailRecordResItem.tsx';
14
+ import { DtoStudentSummary } from '../src/dto/studentSummary.tsx';
15
+
17
16
  describe('student.test.ts', () => {
18
17
  it('action:student:formLayoutMetadata', async () => {
19
18
  await app.bean.executor.mockCtx(async () => {
@@ -77,6 +76,39 @@ describe('student.test.ts', () => {
77
76
  });
78
77
  });
79
78
 
79
+ it('action:student:emittedDtoSchemas', async () => {
80
+ await app.bean.executor.mockCtx(async () => {
81
+ const summaryApiJson = await app.bean.openapi.generateJsonOfClass(DtoStudentSummary);
82
+ const summaryComponent = Object.values(summaryApiJson.components!.schemas as any).find(
83
+ item => {
84
+ return (item as any).properties?.summaryText;
85
+ },
86
+ ) as any;
87
+ assert.ok(summaryComponent);
88
+ assert.deepEqual(
89
+ Object.keys(summaryComponent.properties).sort(),
90
+ [
91
+ 'id',
92
+ 'name',
93
+ 'mobile',
94
+ 'level',
95
+ 'description',
96
+ 'levelTitle',
97
+ 'descriptionLength',
98
+ 'summaryText',
99
+ ].sort(),
100
+ );
101
+
102
+ const detailRecordApiJson =
103
+ await app.bean.openapi.generateJsonOfClass(DtoDetailRecordResItem);
104
+ const detailRecordComponent = Object.values(
105
+ detailRecordApiJson.components!.schemas as any,
106
+ ).find(item => (item as any).properties?._lineNumber) as any;
107
+ assert.ok(detailRecordComponent?.properties?._lineNumber);
108
+ assert.equal(detailRecordComponent.required?.includes('_lineNumber'), false);
109
+ });
110
+ });
111
+
80
112
  it('action:student', async () => {
81
113
  await app.bean.executor.mockCtx(async () => {
82
114
  // data
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-pay",
3
- "version": "5.0.1",
3
+ "version": "5.0.3",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "Vona Module"
@@ -501,6 +501,7 @@ declare module 'vona' {
501
501
  export * from '../service/outbox.ts';
502
502
  export * from '../service/paymentSession.ts';
503
503
  export * from '../service/providerOperation.ts';
504
+ export * from '../service/refundOperation.ts';
504
505
  export * from '../service/webhook.ts';
505
506
 
506
507
  import 'vona-module-a-bean';
@@ -510,6 +511,7 @@ declare module 'vona-module-a-bean' {
510
511
  'a-pay:outbox': never;
511
512
  'a-pay:paymentSession': never;
512
513
  'a-pay:providerOperation': never;
514
+ 'a-pay:refundOperation': never;
513
515
  'a-pay:webhook': never;
514
516
  }
515
517
 
@@ -550,6 +552,17 @@ declare module 'vona-module-a-pay' {
550
552
 
551
553
  }
552
554
 
555
+ export interface ServiceRefundOperation {
556
+ /** @internal */
557
+ get scope(): ScopeModuleAPay;
558
+ }
559
+
560
+ export interface ServiceRefundOperation {
561
+ get $beanFullName(): 'a-pay.service.refundOperation';
562
+ get $onionName(): 'a-pay:refundOperation';
563
+
564
+ }
565
+
553
566
  export interface ServiceWebhook {
554
567
  /** @internal */
555
568
  get scope(): ScopeModuleAPay;
@@ -566,11 +579,13 @@ declare module 'vona-module-a-pay' {
566
579
  import type { ServiceOutbox } from '../service/outbox.ts';
567
580
  import type { ServicePaymentSession } from '../service/paymentSession.ts';
568
581
  import type { ServiceProviderOperation } from '../service/providerOperation.ts';
582
+ import type { ServiceRefundOperation } from '../service/refundOperation.ts';
569
583
  import type { ServiceWebhook } from '../service/webhook.ts';
570
584
  export interface IModuleService {
571
585
  'outbox': ServiceOutbox;
572
586
  'paymentSession': ServicePaymentSession;
573
587
  'providerOperation': ServiceProviderOperation;
588
+ 'refundOperation': ServiceRefundOperation;
574
589
  'webhook': ServiceWebhook;
575
590
  }
576
591
  /** service: end */
@@ -582,6 +597,7 @@ declare module 'vona' {
582
597
  'a-pay.service.outbox': ServiceOutbox;
583
598
  'a-pay.service.paymentSession': ServicePaymentSession;
584
599
  'a-pay.service.providerOperation': ServiceProviderOperation;
600
+ 'a-pay.service.refundOperation': ServiceRefundOperation;
585
601
  'a-pay.service.webhook': ServiceWebhook;
586
602
  }
587
603
  }
@@ -675,12 +691,14 @@ export interface IModuleQueue {
675
691
  /** queue: end */
676
692
  /** schedule: begin */
677
693
  export * from '../bean/schedule.outboxDispatch.ts';
694
+ export * from '../bean/schedule.providerOperationDispatch.ts';
678
695
 
679
696
  import { type IDecoratorScheduleOptions } from 'vona-module-a-schedule';
680
697
  declare module 'vona-module-a-schedule' {
681
698
 
682
699
  export interface IScheduleRecord {
683
700
  'a-pay:outboxDispatch': IDecoratorScheduleOptions;
701
+ 'a-pay:providerOperationDispatch': IDecoratorScheduleOptions;
684
702
  }
685
703
 
686
704
 
@@ -696,6 +714,17 @@ declare module 'vona-module-a-pay' {
696
714
  get $beanFullName(): 'a-pay.schedule.outboxDispatch';
697
715
  get $onionName(): 'a-pay:outboxDispatch';
698
716
  get $onionOptions(): IDecoratorScheduleOptions;
717
+ }
718
+
719
+ export interface ScheduleProviderOperationDispatch {
720
+ /** @internal */
721
+ get scope(): ScopeModuleAPay;
722
+ }
723
+
724
+ export interface ScheduleProviderOperationDispatch {
725
+ get $beanFullName(): 'a-pay.schedule.providerOperationDispatch';
726
+ get $onionName(): 'a-pay:providerOperationDispatch';
727
+ get $onionOptions(): IDecoratorScheduleOptions;
699
728
  }
700
729
  }
701
730
  /** schedule: end */