cabloy 5.1.125 → 5.1.127
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +36 -0
- package/CLAUDE.md +1 -1
- package/cabloy-docs/.vitepress/config.mjs +1 -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/error-guide.md +17 -0
- 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 +224 -4
- 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-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/packages-vona/vona-core/package.json +1 -1
- package/vona/packages-vona/vona-core/src/lib/bean/resource/error/errorClass.ts +21 -4
- package/vona/packages-vona/vona-core/src/lib/bean/resource/error/errorObject.ts +1 -0
- package/vona/packages-vona/vona-core/src/lib/bean/type.ts +3 -1
- package/vona/packages-vona/vona-core/src/types/interface/module.ts +6 -1
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/pnpm-lock.yaml +27 -147
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +36 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/categorySelectResItem.tsx +3 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/categoryView.tsx +3 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/category.tsx +21 -2
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/model/category.ts +11 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/category.ts +7 -2
- package/vona/src/suite/a-commerce/modules/commerce-catalog/test/categoryPresentation.test.ts +218 -0
- 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/refundResult.tsx +3 -0
- 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 +123 -12
- 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-commerce/modules/commerce-trade/test/refundLifecycle.test.ts +111 -3
- 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-core/package.json +1 -1
- 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 +58 -12
- package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/types.ts +673 -66
- 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 +116 -30
- 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/zova/src/suite/cabloy-basic/modules/basic-select/src/bean/tableCell.select.tsx +12 -6
- package/zova/src/suite/cabloy-basic/modules/basic-select/src/component/formFieldSelect/controller.tsx +6 -3
|
@@ -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 = {};
|
|
@@ -16,6 +16,7 @@ interface IFixture {
|
|
|
16
16
|
customerName?: string;
|
|
17
17
|
orderId?: number;
|
|
18
18
|
paymentAttemptId?: number;
|
|
19
|
+
paymentSessionId?: number;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
async function cleanup(fixture: IFixture) {
|
|
@@ -24,6 +25,21 @@ async function cleanup(fixture: IFixture) {
|
|
|
24
25
|
const promotion = app.scope('commerce-promotion');
|
|
25
26
|
const catalog = app.scope('commerce-catalog');
|
|
26
27
|
const member = app.scope('commerce-member');
|
|
28
|
+
const pay = app.scope('a-pay');
|
|
29
|
+
if (fixture.paymentSessionId !== undefined) {
|
|
30
|
+
const refundOperations = await pay.model.refundOperation.select({
|
|
31
|
+
where: { paymentSessionId: fixture.paymentSessionId },
|
|
32
|
+
});
|
|
33
|
+
for (const refundOperation of refundOperations) {
|
|
34
|
+
await pay.model.providerOperation.delete({ refundOperationId: refundOperation.id });
|
|
35
|
+
await pay.model.outboxEvent.delete({ refundOperationId: refundOperation.id });
|
|
36
|
+
await pay.model.webhookInbox.delete({ refundOperationId: refundOperation.id });
|
|
37
|
+
await pay.model.refundOperation.delete({ id: refundOperation.id });
|
|
38
|
+
}
|
|
39
|
+
await pay.model.outboxEvent.delete({ paymentSessionId: fixture.paymentSessionId });
|
|
40
|
+
await pay.model.webhookInbox.delete({ paymentSessionId: fixture.paymentSessionId });
|
|
41
|
+
await pay.model.providerOperation.delete({ paymentSessionId: fixture.paymentSessionId });
|
|
42
|
+
}
|
|
27
43
|
if (fixture.orderId !== undefined) {
|
|
28
44
|
const requests = await payment.model.refundRequest.select({
|
|
29
45
|
where: { orderId: fixture.orderId },
|
|
@@ -39,6 +55,8 @@ async function cleanup(fixture: IFixture) {
|
|
|
39
55
|
await payment.model.paymentAudit.delete({ paymentAttemptId: fixture.paymentAttemptId });
|
|
40
56
|
await payment.model.paymentAttempt.delete({ id: fixture.paymentAttemptId });
|
|
41
57
|
}
|
|
58
|
+
if (fixture.paymentSessionId !== undefined)
|
|
59
|
+
await pay.model.paymentSession.delete({ id: fixture.paymentSessionId });
|
|
42
60
|
if (fixture.orderId !== undefined)
|
|
43
61
|
await trade.model.orderAudit.delete({ orderId: fixture.orderId });
|
|
44
62
|
if (fixture.skuId !== undefined) await trade.model.stockAudit.delete({ skuId: fixture.skuId });
|
|
@@ -152,10 +170,17 @@ async function createPaidOrder(suffix: string): Promise<IFixture> {
|
|
|
152
170
|
});
|
|
153
171
|
fixture.orderId = checkout.orderId as number;
|
|
154
172
|
fixture.paymentAttemptId = checkout.paymentAttemptId as number;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
173
|
+
fixture.paymentSessionId = checkout.paymentSessionId as number;
|
|
174
|
+
const pay = app.scope('a-pay');
|
|
175
|
+
await pay.service.paymentSession.start(fixture.paymentSessionId);
|
|
176
|
+
await app
|
|
177
|
+
.scope('pay-mock')
|
|
178
|
+
.service.payMock.completePaymentSession(fixture.paymentSessionId, 'succeeded');
|
|
179
|
+
const outbox = await pay.model.outboxEvent.get({
|
|
180
|
+
paymentSessionId: fixture.paymentSessionId,
|
|
181
|
+
eventType: 'payment.outcome.v1',
|
|
158
182
|
});
|
|
183
|
+
await pay.queue.outboxDispatch.pushAsync({ outboxEventId: outbox!.id });
|
|
159
184
|
return fixture;
|
|
160
185
|
}
|
|
161
186
|
|
|
@@ -310,6 +335,89 @@ describe('refundLifecycle.test.ts', { concurrency: false, sequential: true }, ()
|
|
|
310
335
|
});
|
|
311
336
|
});
|
|
312
337
|
|
|
338
|
+
it('settles an executed mock-provider refund through the durable outbox', async () => {
|
|
339
|
+
await app.bean.executor.mockCtx(async () => {
|
|
340
|
+
const fixture: IFixture = {};
|
|
341
|
+
try {
|
|
342
|
+
Object.assign(fixture, await createPaidOrder(randomUUID().slice(0, 12)));
|
|
343
|
+
const order = app.scope('commerce-trade').service.order;
|
|
344
|
+
await order.requestRefund(fixture.orderId!, {
|
|
345
|
+
reason: 'provider-backed refund',
|
|
346
|
+
idempotencyKey: 'provider-backed-request-1',
|
|
347
|
+
});
|
|
348
|
+
await app.bean.passport.signout();
|
|
349
|
+
await app.bean.passport.signinMock();
|
|
350
|
+
await order.approveRefund(fixture.orderId!, {
|
|
351
|
+
reason: 'provider-backed approval',
|
|
352
|
+
idempotencyKey: 'provider-backed-approve-1',
|
|
353
|
+
});
|
|
354
|
+
const executed = await order.executeRefund(fixture.orderId!);
|
|
355
|
+
assert.ok(executed.refundAttemptId);
|
|
356
|
+
assert.ok(executed.refundOperationId);
|
|
357
|
+
assert.deepEqual(
|
|
358
|
+
[executed.orderState, executed.refundState, executed.refundAttemptState],
|
|
359
|
+
['refund_approved', 'approved', 'created'],
|
|
360
|
+
);
|
|
361
|
+
const pay = app.scope('a-pay');
|
|
362
|
+
const refundOperation = await pay.model.refundOperation.getById(executed.refundOperationId);
|
|
363
|
+
const refundAttempt = await app
|
|
364
|
+
.scope('commerce-payment')
|
|
365
|
+
.model.refundAttempt.getById(executed.refundAttemptId);
|
|
366
|
+
assert.equal(String(refundAttempt?.refundOperationId), String(executed.refundOperationId));
|
|
367
|
+
assert.equal(refundOperation?.state, 'pending');
|
|
368
|
+
const receipt = await app
|
|
369
|
+
.scope('pay-mock')
|
|
370
|
+
.service.payMock.completeRefundOperation(executed.refundOperationId, 'succeeded');
|
|
371
|
+
assert.deepEqual(receipt, {
|
|
372
|
+
refundOperationId: executed.refundOperationId,
|
|
373
|
+
accepted: true,
|
|
374
|
+
});
|
|
375
|
+
const outbox = await pay.model.outboxEvent.get({
|
|
376
|
+
refundOperationId: executed.refundOperationId,
|
|
377
|
+
eventType: 'refund.outcome.v1',
|
|
378
|
+
});
|
|
379
|
+
assert.ok(outbox);
|
|
380
|
+
await pay.queue.outboxDispatch.pushAsync({ outboxEventId: outbox.id });
|
|
381
|
+
await pay.queue.outboxDispatch.pushAsync({ outboxEventId: outbox.id });
|
|
382
|
+
const [
|
|
383
|
+
dispatched,
|
|
384
|
+
settledOrder,
|
|
385
|
+
settledRequest,
|
|
386
|
+
settledAttempt,
|
|
387
|
+
line,
|
|
388
|
+
balance,
|
|
389
|
+
grant,
|
|
390
|
+
audits,
|
|
391
|
+
] = await Promise.all([
|
|
392
|
+
pay.model.outboxEvent.getById(outbox.id),
|
|
393
|
+
app.scope('commerce-trade').model.order.getById(fixture.orderId!),
|
|
394
|
+
app.scope('commerce-payment').model.refundRequest.getById(executed.refundRequestId),
|
|
395
|
+
app.scope('commerce-payment').model.refundAttempt.getById(executed.refundAttemptId),
|
|
396
|
+
app.scope('commerce-trade').model.orderLine.get({ orderId: fixture.orderId }),
|
|
397
|
+
app.scope('commerce-trade').model.stockBalance.getById(fixture.balanceId!),
|
|
398
|
+
app.scope('commerce-promotion').model.couponGrant.getById(fixture.grantId!),
|
|
399
|
+
app
|
|
400
|
+
.scope('commerce-payment')
|
|
401
|
+
.model.refundAudit.select({ where: { orderId: fixture.orderId } }),
|
|
402
|
+
]);
|
|
403
|
+
const reservation = await app
|
|
404
|
+
.scope('commerce-trade')
|
|
405
|
+
.model.stockReservation.get({ orderLineId: line?.id });
|
|
406
|
+
assert.deepEqual([dispatched?.state, dispatched?.attemptCount], ['dispatched', 1]);
|
|
407
|
+
assert.deepEqual(
|
|
408
|
+
[settledOrder?.state, settledRequest?.state, settledAttempt?.state, reservation?.state],
|
|
409
|
+
['refunded', 'refunded', 'succeeded', 'restored'],
|
|
410
|
+
);
|
|
411
|
+
assert.deepEqual([balance?.onHand, balance?.reserved, balance?.available], [1, 0, 1]);
|
|
412
|
+
assert.equal(grant?.state, 'redeemed');
|
|
413
|
+
assert.equal(audits.filter(audit => audit.toRefundState === 'refunded').length, 1);
|
|
414
|
+
} finally {
|
|
415
|
+
await app.bean.passport.signout();
|
|
416
|
+
await cleanup(fixture);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
313
421
|
it('preserves rejected and failed cycles while permitting a fresh request', async () => {
|
|
314
422
|
await app.bean.executor.mockCtx(async () => {
|
|
315
423
|
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 */
|
|
@@ -12,7 +12,9 @@ import { $tableColumns } from 'vona-module-a-ormutils';
|
|
|
12
12
|
...$tableColumns('payPaymentSession', 'providerOrderId'),
|
|
13
13
|
...$tableColumns('payPaymentSession', 'providerCaptureId'),
|
|
14
14
|
...$tableColumns('payProviderOperation', 'paymentSessionId+kind'),
|
|
15
|
+
...$tableColumns('payProviderOperation', 'refundOperationId'),
|
|
15
16
|
...$tableColumns('payProviderOperation', 'state+nextAttemptAt'),
|
|
17
|
+
...$tableColumns('payProviderOperation', 'state+claimExpiresAt'),
|
|
16
18
|
...$tableColumns('payProviderOperation', 'idempotencyKey'),
|
|
17
19
|
...$tableColumns('payRefundOperation', 'paymentSessionId'),
|
|
18
20
|
...$tableColumns('payRefundOperation', 'state'),
|
|
@@ -21,6 +23,7 @@ import { $tableColumns } from 'vona-module-a-ormutils';
|
|
|
21
23
|
...$tableColumns('payWebhookInbox', 'paymentSessionId'),
|
|
22
24
|
...$tableColumns('payWebhookInbox', 'state'),
|
|
23
25
|
...$tableColumns('payPaymentAudit', 'paymentSessionId'),
|
|
26
|
+
...$tableColumns('payOutboxEvent', 'refundOperationId'),
|
|
24
27
|
...$tableColumns('payOutboxEvent', 'state+nextAttemptAt'),
|
|
25
28
|
...$tableColumns('payOutboxEvent', 'state+claimExpiresAt'),
|
|
26
29
|
},
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Meta } from 'vona-module-a-meta';
|
|
2
2
|
import { BeanRedlockBase } from 'vona-module-a-redlock';
|
|
3
3
|
|
|
4
|
-
export type TypeRedlockLockResource =
|
|
4
|
+
export type TypeRedlockLockResource =
|
|
5
|
+
| `pay.providerOperation.start.${string}`
|
|
6
|
+
| `pay.webhook.${string}`;
|
|
5
7
|
export type TypeRedlockLockIsolateResource = never;
|
|
6
8
|
|
|
7
9
|
@Meta()
|
|
@@ -33,6 +33,7 @@ export class MetaVersion extends BeanBase implements IMetaVersionUpdate {
|
|
|
33
33
|
await this.bean.model.createTable(providerOperation.$table, table => {
|
|
34
34
|
table.basicFields();
|
|
35
35
|
table.tableIdentity(providerOperation.paymentSessionId);
|
|
36
|
+
table.tableIdentity(providerOperation.refundOperationId);
|
|
36
37
|
table.string(providerOperation.kind, 32);
|
|
37
38
|
table.string(providerOperation.state, 32);
|
|
38
39
|
table.string(providerOperation.idempotencyKey, 100);
|
|
@@ -41,6 +42,9 @@ export class MetaVersion extends BeanBase implements IMetaVersionUpdate {
|
|
|
41
42
|
table.string(providerOperation.providerResourceId, 255);
|
|
42
43
|
table.integer(providerOperation.attemptCount);
|
|
43
44
|
table.dateTime(providerOperation.claimedAt);
|
|
45
|
+
table.string(providerOperation.claimToken, 100);
|
|
46
|
+
table.dateTime(providerOperation.claimExpiresAt);
|
|
47
|
+
table.dateTime(providerOperation.submittedAt);
|
|
44
48
|
table.dateTime(providerOperation.nextAttemptAt);
|
|
45
49
|
table.string(providerOperation.errorCode, 100);
|
|
46
50
|
table.string(providerOperation.errorSummary, 255);
|
|
@@ -71,9 +75,11 @@ export class MetaVersion extends BeanBase implements IMetaVersionUpdate {
|
|
|
71
75
|
table.tableIdentity(webhookInbox.paymentSessionId);
|
|
72
76
|
table.tableIdentity(webhookInbox.refundOperationId);
|
|
73
77
|
table.string(webhookInbox.paymentState, 32);
|
|
78
|
+
table.string(webhookInbox.refundState, 32);
|
|
74
79
|
table.integer(webhookInbox.amountMinor);
|
|
75
80
|
table.string(webhookInbox.currency, 3);
|
|
76
81
|
table.string(webhookInbox.providerCaptureId, 255);
|
|
82
|
+
table.string(webhookInbox.providerRefundId, 255);
|
|
77
83
|
table.string(webhookInbox.payloadHash, 64);
|
|
78
84
|
table.string(webhookInbox.state, 32);
|
|
79
85
|
table.integer(webhookInbox.retryCount);
|
|
@@ -99,6 +105,7 @@ export class MetaVersion extends BeanBase implements IMetaVersionUpdate {
|
|
|
99
105
|
table.basicFields();
|
|
100
106
|
table.string(outboxEvent.eventType, 100);
|
|
101
107
|
table.tableIdentity(outboxEvent.paymentSessionId);
|
|
108
|
+
table.tableIdentity(outboxEvent.refundOperationId);
|
|
102
109
|
table.json(outboxEvent.payload);
|
|
103
110
|
table.string(outboxEvent.state, 32);
|
|
104
111
|
table.integer(outboxEvent.attemptCount);
|
|
@@ -3,6 +3,8 @@ import type { IQueueExecute, IQueuePushOptions } from 'vona-module-a-queue';
|
|
|
3
3
|
|
|
4
4
|
import { BeanQueueBase, Queue } from 'vona-module-a-queue';
|
|
5
5
|
|
|
6
|
+
import type { IPaymentOutcomeEvent, IRefundOutcomeEvent } from '../types/payment.ts';
|
|
7
|
+
|
|
6
8
|
export interface IOutboxDispatchJobData {
|
|
7
9
|
outboxEventId: TableIdentity;
|
|
8
10
|
}
|
|
@@ -16,13 +18,16 @@ export class QueueOutboxDispatch
|
|
|
16
18
|
const event = await this.scope.service.outbox.claim(data.outboxEventId);
|
|
17
19
|
if (!event) return;
|
|
18
20
|
try {
|
|
19
|
-
if (event.eventType !== 'payment.outcome.v1') {
|
|
20
|
-
this.app.throw(409, `unsupported payment outbox event: ${event.eventType}`);
|
|
21
|
-
}
|
|
22
21
|
const session = await this.scope.model.paymentSession.getById(event.paymentSessionId);
|
|
23
22
|
if (!session) this.app.throw(404, 'payment session not found');
|
|
24
23
|
const payScene = this.bean.payScene.get(session.payScene as never);
|
|
25
|
-
|
|
24
|
+
if (event.eventType === 'payment.outcome.v1') {
|
|
25
|
+
await payScene.onPaymentOutcome(event.payload as IPaymentOutcomeEvent);
|
|
26
|
+
} else if (event.eventType === 'refund.outcome.v1') {
|
|
27
|
+
await payScene.onRefundOutcome(event.payload as IRefundOutcomeEvent);
|
|
28
|
+
} else {
|
|
29
|
+
this.app.throw(409, `unsupported payment outbox event: ${event.eventType}`);
|
|
30
|
+
}
|
|
26
31
|
await this.scope.service.outbox.markDispatched(event.id, event.claimToken!);
|
|
27
32
|
} catch (error) {
|
|
28
33
|
await this.scope.service.outbox.release(event.id, event.claimToken!, error);
|
package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/schedule.providerOperationDispatch.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { IScheduleExecute } from 'vona-module-a-schedule';
|
|
2
|
+
|
|
3
|
+
import { BeanBase } from 'vona';
|
|
4
|
+
import { Schedule } from 'vona-module-a-schedule';
|
|
5
|
+
|
|
6
|
+
@Schedule({ repeat: { every: 60 * 1000 } })
|
|
7
|
+
export class ScheduleProviderOperationDispatch extends BeanBase implements IScheduleExecute {
|
|
8
|
+
async execute() {
|
|
9
|
+
await this.scope.service.providerOperation.queueDue();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -5,20 +5,27 @@ import { Api, v } from 'vona-module-a-openapiutils';
|
|
|
5
5
|
import { Entity, EntityBase } from 'vona-module-a-orm';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
IPaymentOutcomeEvent,
|
|
10
|
+
IRefundOutcomeEvent,
|
|
11
|
+
TypeOutboxEventState,
|
|
12
|
+
} from '../types/payment.ts';
|
|
9
13
|
|
|
10
14
|
export interface IEntityOptionsOutboxEvent extends IDecoratorEntityOptions {}
|
|
11
15
|
|
|
12
16
|
@Entity<IEntityOptionsOutboxEvent>('payOutboxEvent')
|
|
13
17
|
export class EntityOutboxEvent extends EntityBase {
|
|
14
|
-
@Api.field(z.
|
|
15
|
-
eventType: 'payment.outcome.v1';
|
|
18
|
+
@Api.field(z.enum(['payment.outcome.v1', 'refund.outcome.v1']))
|
|
19
|
+
eventType: 'payment.outcome.v1' | 'refund.outcome.v1';
|
|
16
20
|
|
|
17
21
|
@Api.field(v.tableIdentity())
|
|
18
22
|
paymentSessionId: TableIdentity;
|
|
19
23
|
|
|
24
|
+
@Api.field(v.optional(), v.tableIdentity())
|
|
25
|
+
refundOperationId?: TableIdentity;
|
|
26
|
+
|
|
20
27
|
@Api.field(v.required())
|
|
21
|
-
payload: IPaymentOutcomeEvent;
|
|
28
|
+
payload: IPaymentOutcomeEvent | IRefundOutcomeEvent;
|
|
22
29
|
|
|
23
30
|
@Api.field(z.enum(['pending', 'claimed', 'dispatched', 'failed']))
|
|
24
31
|
state: TypeOutboxEventState;
|