cabloy 5.1.124 → 5.1.126
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cabloy-version +1 -1
- package/.claude/skills/cabloy-backend-scaffold/SKILL.md +1 -1
- package/.claude/skills/cabloy-backend-scaffold/evals/evals.json +6 -0
- package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +13 -4
- package/.claude/skills/cabloy-workflow/SKILL.md +4 -2
- package/.claude/skills/cabloy-workflow/evals/evals.json +14 -2
- package/.claude/skills/cabloy-worktree-environment/SKILL.md +206 -0
- package/.claude/skills/cabloy-worktree-environment/evals/evals.json +77 -0
- package/.github/workflows/vona-test-pg.yml +3 -0
- package/.github/workflows/vona-test-sqlite3.yml +3 -0
- package/CHANGELOG.md +21 -0
- package/CLAUDE.md +1 -1
- package/cabloy-docs/.vitepress/config.mjs +2 -0
- package/cabloy-docs/ai/playbook-technical-blog-authoring.md +158 -0
- package/cabloy-docs/backend/backend-contract-emission-specimen.md +2 -2
- package/cabloy-docs/backend/dto-guide.md +10 -7
- package/cabloy-docs/backend/dto-infer-generation.md +127 -7
- package/cabloy-docs/backend/foundation.md +1 -1
- package/cabloy-docs/backend/introduction.md +1 -0
- package/cabloy-docs/backend/module-dependencies.md +104 -0
- package/cabloy-docs/backend/serialization-guide.md +6 -0
- package/cabloy-docs/fullstack/parallel-worktree-environment.md +72 -34
- package/cabloy-docs/fullstack/suites-and-modules.md +1 -0
- package/cabloy-docs/reference/package-map.md +1 -0
- package/e2e/specs/a-commerce/commerce.spec.ts +89 -0
- package/package.json +2 -1
- package/scripts/init.ts +7 -12
- package/scripts/initAppName.test.ts +72 -0
- package/scripts/initAppName.ts +50 -0
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/pnpm-lock.yaml +48 -146
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineCreate.tsx +6 -27
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineItem.tsx +6 -31
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineUpdate.tsx +6 -2
- package/vona/src/suite/a-commerce/modules/commerce-member/src/lib/addressMine.ts +16 -0
- package/vona/src/suite/a-commerce/modules/commerce-member/src/service/address.ts +4 -17
- package/vona/src/suite/a-commerce/modules/commerce-member/test/addressOwnership.test.ts +63 -19
- package/vona/src/suite/a-commerce/modules/commerce-payment/package.json +2 -1
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/payScene.commerceOrder.ts +11 -2
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/commercePayScene.ts +26 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentAttempt.test.ts +19 -2
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/en-us.ts +5 -2
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/zh-cn.ts +5 -2
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateCreate.tsx +26 -35
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateUpdate.tsx +6 -1
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/entity/couponTemplate.tsx +40 -4
- package/vona/src/suite/a-commerce/modules/commerce-promotion/test/couponTemplateFormLayout.test.ts +85 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/order.ts +3 -5
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderSummary.tsx +7 -40
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/shipmentView.tsx +6 -15
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/lib/order.ts +9 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +121 -11
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/order.test.ts +25 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +19 -2
- package/vona/src/suite/a-training/modules/training-record/src/dto/detailRecordSubjectResItem.tsx +6 -1
- package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectResItem.tsx +3 -1
- package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +4 -1
- package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +45 -0
- package/vona/src/suite/a-training/modules/training-student/src/dto/detailRecordResItem.tsx +6 -1
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentSummary.tsx +5 -23
- package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +37 -5
- package/vona/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/.metadata/index.ts +29 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.index.ts +3 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.redlock.ts +3 -1
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.version.ts +7 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/queue.outboxDispatch.ts +9 -4
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/schedule.providerOperationDispatch.ts +11 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/outboxEvent.tsx +11 -4
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/providerOperation.tsx +12 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/webhookInbox.tsx +6 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/outbox.ts +12 -5
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/paymentSession.ts +1 -37
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/providerOperation.ts +355 -1
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/refundOperation.ts +226 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/webhook.ts +148 -50
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payScene.ts +2 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/.metadata/index.ts +39 -22
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/bean/payProvider.mock.ts +42 -13
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/controller/mockPayment.ts +11 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/dto/mockRefundComplete.tsx +13 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/dto/mockRefundReceipt.tsx +16 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/service/payMock.ts +56 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/package.json +2 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/bean/payProvider.paypal.ts +117 -8
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/package.json +5 -5
- package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoGet.ts +5 -5
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/schemas.ts +54 -12
- package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/types.ts +625 -64
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/types.ts +55 -15
- package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +1 -1
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeOrder.ts +15 -17
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +12 -8
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +111 -26
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeOrder.ts +3 -3
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionRefund/controller.tsx +1 -8
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +5 -9
- package/zova/src/suite/a-training/modules/training-student/src/.metadata/index.ts +1 -1
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +301 -17
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +5461 -1788
- package/vona/src/suite-vendor/a-pay/modules/a-pay/test/outbox.test.ts +0 -209
- package/vona/src/suite-vendor/a-pay/modules/a-pay/test/paymentSession.test.ts +0 -153
- package/vona/src/suite-vendor/a-pay/modules/a-pay/test/webhook.test.ts +0 -278
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/test/payMock.test.ts +0 -141
|
@@ -1,47 +1,14 @@
|
|
|
1
|
-
import type { TableIdentity } from 'table-identity';
|
|
2
1
|
import type { IDecoratorDtoOptions } from 'vona-module-a-web';
|
|
3
2
|
|
|
4
|
-
import {
|
|
3
|
+
import { $Dto } from 'vona-module-a-orm';
|
|
5
4
|
import { Dto } from 'vona-module-a-web';
|
|
6
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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 {
|
|
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
|
-
|
|
12
|
-
|
|
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
|
+
}) {}
|
|
@@ -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
|
-
|
|
945
|
+
return await this.scope.model.order.selectAndCount({
|
|
829
946
|
...params,
|
|
830
|
-
columns:
|
|
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 = {};
|
package/vona/src/suite/a-training/modules/training-record/src/dto/detailRecordSubjectResItem.tsx
CHANGED
|
@@ -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(
|
|
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: {
|
|
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(
|
|
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
|
-
|
|
14
|
-
|
|
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
|
|
@@ -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 */
|