cabloy 5.1.125 → 5.1.127

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/skills/cabloy-backend-scaffold/SKILL.md +1 -1
  3. package/.claude/skills/cabloy-backend-scaffold/evals/evals.json +6 -0
  4. package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +13 -4
  5. package/.claude/skills/cabloy-workflow/SKILL.md +4 -2
  6. package/.claude/skills/cabloy-workflow/evals/evals.json +14 -2
  7. package/.claude/skills/cabloy-worktree-environment/SKILL.md +206 -0
  8. package/.claude/skills/cabloy-worktree-environment/evals/evals.json +77 -0
  9. package/.github/workflows/vona-test-pg.yml +3 -0
  10. package/.github/workflows/vona-test-sqlite3.yml +3 -0
  11. package/CHANGELOG.md +36 -0
  12. package/CLAUDE.md +1 -1
  13. package/cabloy-docs/.vitepress/config.mjs +1 -0
  14. package/cabloy-docs/backend/backend-contract-emission-specimen.md +2 -2
  15. package/cabloy-docs/backend/dto-guide.md +10 -7
  16. package/cabloy-docs/backend/dto-infer-generation.md +127 -7
  17. package/cabloy-docs/backend/error-guide.md +17 -0
  18. package/cabloy-docs/backend/foundation.md +1 -1
  19. package/cabloy-docs/backend/introduction.md +1 -0
  20. package/cabloy-docs/backend/module-dependencies.md +104 -0
  21. package/cabloy-docs/backend/serialization-guide.md +6 -0
  22. package/cabloy-docs/fullstack/parallel-worktree-environment.md +72 -34
  23. package/cabloy-docs/fullstack/suites-and-modules.md +1 -0
  24. package/cabloy-docs/reference/package-map.md +1 -0
  25. package/e2e/specs/a-commerce/commerce.spec.ts +224 -4
  26. package/package.json +2 -1
  27. package/scripts/init.ts +7 -12
  28. package/scripts/initAppName.test.ts +72 -0
  29. package/scripts/initAppName.ts +50 -0
  30. package/vona/packages-cli/cli/package.json +1 -1
  31. package/vona/packages-cli/cli-set-api/package.json +1 -1
  32. package/vona/packages-vona/vona/package.json +1 -1
  33. package/vona/packages-vona/vona-core/package.json +1 -1
  34. package/vona/packages-vona/vona-core/src/lib/bean/resource/error/errorClass.ts +21 -4
  35. package/vona/packages-vona/vona-core/src/lib/bean/resource/error/errorObject.ts +1 -0
  36. package/vona/packages-vona/vona-core/src/lib/bean/type.ts +3 -1
  37. package/vona/packages-vona/vona-core/src/types/interface/module.ts +6 -1
  38. package/vona/packages-vona/vona-mock/package.json +1 -1
  39. package/vona/pnpm-lock.yaml +27 -147
  40. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +36 -1
  41. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +1 -0
  42. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +1 -0
  43. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/categorySelectResItem.tsx +3 -1
  44. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/categoryView.tsx +3 -1
  45. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/category.tsx +21 -2
  46. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/model/category.ts +11 -1
  47. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/category.ts +7 -2
  48. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/categoryPresentation.test.ts +218 -0
  49. package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineCreate.tsx +6 -27
  50. package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineItem.tsx +6 -31
  51. package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineUpdate.tsx +6 -2
  52. package/vona/src/suite/a-commerce/modules/commerce-member/src/lib/addressMine.ts +16 -0
  53. package/vona/src/suite/a-commerce/modules/commerce-member/src/service/address.ts +4 -17
  54. package/vona/src/suite/a-commerce/modules/commerce-member/test/addressOwnership.test.ts +63 -19
  55. package/vona/src/suite/a-commerce/modules/commerce-payment/package.json +2 -1
  56. package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/payScene.commerceOrder.ts +11 -2
  57. package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/commercePayScene.ts +26 -0
  58. package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentAttempt.test.ts +19 -2
  59. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/en-us.ts +5 -2
  60. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/zh-cn.ts +5 -2
  61. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateCreate.tsx +26 -35
  62. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateUpdate.tsx +6 -1
  63. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/entity/couponTemplate.tsx +40 -4
  64. package/vona/src/suite/a-commerce/modules/commerce-promotion/test/couponTemplateFormLayout.test.ts +85 -0
  65. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +1 -1
  66. package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/order.ts +3 -5
  67. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderSummary.tsx +7 -40
  68. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/refundResult.tsx +3 -0
  69. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/shipmentView.tsx +6 -15
  70. package/vona/src/suite/a-commerce/modules/commerce-trade/src/lib/order.ts +9 -0
  71. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +123 -12
  72. package/vona/src/suite/a-commerce/modules/commerce-trade/test/order.test.ts +25 -0
  73. package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +19 -2
  74. package/vona/src/suite/a-commerce/modules/commerce-trade/test/refundLifecycle.test.ts +111 -3
  75. package/vona/src/suite/a-training/modules/training-record/src/dto/detailRecordSubjectResItem.tsx +6 -1
  76. package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectResItem.tsx +3 -1
  77. package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +4 -1
  78. package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +45 -0
  79. package/vona/src/suite/a-training/modules/training-student/src/dto/detailRecordResItem.tsx +6 -1
  80. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSummary.tsx +5 -23
  81. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +37 -5
  82. package/vona/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
  83. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/.metadata/index.ts +29 -0
  84. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.index.ts +3 -0
  85. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.redlock.ts +3 -1
  86. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.version.ts +7 -0
  87. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/queue.outboxDispatch.ts +9 -4
  88. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/schedule.providerOperationDispatch.ts +11 -0
  89. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/outboxEvent.tsx +11 -4
  90. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/providerOperation.tsx +12 -0
  91. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/webhookInbox.tsx +6 -0
  92. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/outbox.ts +12 -5
  93. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/paymentSession.ts +1 -37
  94. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/providerOperation.ts +355 -1
  95. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/refundOperation.ts +226 -0
  96. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/webhook.ts +148 -50
  97. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payScene.ts +2 -1
  98. package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
  99. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/.metadata/index.ts +39 -22
  100. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/bean/payProvider.mock.ts +42 -13
  101. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/controller/mockPayment.ts +11 -0
  102. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/dto/mockRefundComplete.tsx +13 -0
  103. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/dto/mockRefundReceipt.tsx +16 -0
  104. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/service/payMock.ts +56 -0
  105. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/package.json +2 -1
  106. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/bean/payProvider.paypal.ts +117 -8
  107. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +1 -1
  108. package/vona/src/suite-vendor/a-pay/package.json +5 -5
  109. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  110. package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
  111. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoGet.ts +5 -5
  112. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  113. package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/schemas.ts +58 -12
  114. package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/types.ts +673 -66
  115. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/types.ts +55 -15
  116. package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +1 -1
  117. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeOrder.ts +15 -17
  118. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +12 -8
  119. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +116 -30
  120. package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeOrder.ts +3 -3
  121. package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionRefund/controller.tsx +1 -8
  122. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +5 -9
  123. package/zova/src/suite/a-training/modules/training-student/src/.metadata/index.ts +1 -1
  124. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +301 -17
  125. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +5461 -1788
  126. package/zova/src/suite/cabloy-basic/modules/basic-select/src/bean/tableCell.select.tsx +12 -6
  127. package/zova/src/suite/cabloy-basic/modules/basic-select/src/component/formFieldSelect/controller.tsx +6 -3
@@ -535,7 +535,9 @@ test(
535
535
  await orderCard.getByRole('button', { name: 'View', exact: true }).click();
536
536
  await expect(page).toHaveURL(new RegExp(`/commerce/order/${checkout.orderId}(?:/|$)`));
537
537
  await historyDetailResponse;
538
- await expect(page.getByRole('heading', { name: `Order #${checkout.orderId}` })).toBeVisible();
538
+ await expect(
539
+ page.getByRole('heading', { name: `Order #${checkout.orderId}`, level: 1 }),
540
+ ).toBeVisible();
539
541
  await expect(page.getByText('1 × $45.99 = $45.99')).toBeVisible();
540
542
  await expect(page.getByRole('alert')).toHaveCount(0);
541
543
  expect(pageErrors).toEqual([]);
@@ -705,11 +707,39 @@ test(
705
707
  const executeResponse = waitForApiResponse(
706
708
  adminPage,
707
709
  'POST',
708
- `/api/commerce/trade/order/${checkout.orderId}/refundOutcome`,
710
+ `/api/commerce/trade/order/${checkout.orderId}/executeRefund`,
709
711
  );
710
712
  await orderRow.getByRole('button', { name: 'Execute refund', exact: true }).click();
711
- expect((await executeResponse).ok()).toBeTruthy();
712
- await expect(orderRow).toContainText('refunded');
713
+ const executeResponseValue = await executeResponse;
714
+ expect(executeResponseValue.ok()).toBeTruthy();
715
+ const executeResult = (await executeResponseValue.json()).data;
716
+ const authorization = executeResponseValue.request().headers()['authorization'];
717
+ expect(authorization).toMatch(/^Bearer /);
718
+ const authHeaders = { Authorization: authorization };
719
+ expectTableIdentity(executeResult.refundAttemptId);
720
+ expectTableIdentity(executeResult.refundOperationId);
721
+ expect([
722
+ executeResult.orderState,
723
+ executeResult.refundState,
724
+ executeResult.refundAttemptState,
725
+ ]).toEqual(['refund_approved', 'approved', 'created']);
726
+ const completeRefundResponse = await adminPage.request.post(
727
+ `/api/pay/mock/payment-session/refund-operation/${executeResult.refundOperationId}/complete`,
728
+ { headers: authHeaders, data: { outcome: 'succeeded' } },
729
+ );
730
+ expect(completeRefundResponse.ok()).toBeTruthy();
731
+ await expect
732
+ .poll(
733
+ async () => {
734
+ await adminPage.reload({ waitUntil: 'load' });
735
+ const currentOrderRow = await getAdminOrderRow(adminPage, checkout.orderId);
736
+ return (await currentOrderRow.textContent()) ?? '';
737
+ },
738
+ { timeout: 40_000 },
739
+ )
740
+ .toContain('refunded');
741
+ const refundedOrderRow = await getAdminOrderRow(adminPage, checkout.orderId);
742
+ await expect(refundedOrderRow).toContainText('refunded');
713
743
  expect(adminPageErrors).toEqual([]);
714
744
  } finally {
715
745
  await adminContext.close().catch(() => {});
@@ -796,6 +826,196 @@ test(
796
826
  },
797
827
  );
798
828
 
829
+ test(
830
+ 'ATP-SPC-01: Coupon Template renders semantic Admin field controls',
831
+ { tag: ['@admin', '@flow', '@fia'] },
832
+ async ({ browser }) => {
833
+ test.setTimeout(60_000);
834
+ const adminContext = await browser.newContext();
835
+ const adminPage = await adminContext.newPage();
836
+ const adminPageErrors = collectPageErrors(adminPage);
837
+ try {
838
+ await adminPage.setViewportSize({ width: 1440, height: 900 });
839
+ await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin');
840
+ await adminPage.goto(
841
+ '/commerce-admin/rest/resource/commerce-promotion%3AcouponTemplate/create',
842
+ {
843
+ waitUntil: 'load',
844
+ },
845
+ );
846
+ await expect(adminPage).toHaveURL(
847
+ /\/commerce-admin\/rest\/resource\/commerce-promotion(?:%3A|:|%253A)couponTemplate\/create(?:[/?#]|$)/,
848
+ );
849
+
850
+ for (const groupName of [
851
+ 'Basic Information',
852
+ 'Discount Policy',
853
+ 'Validity Window',
854
+ 'Usage Limits',
855
+ ]) {
856
+ await expect(adminPage.getByRole('group', { name: groupName })).toBeVisible();
857
+ }
858
+
859
+ const state = adminPage
860
+ .getByRole('group', { name: 'Template State *' })
861
+ .getByRole('combobox');
862
+ await expect(state).toBeVisible();
863
+ await state.selectOption({ label: 'Active' });
864
+ await expect(state).toHaveValue('active');
865
+
866
+ const discountInput = adminPage
867
+ .getByRole('group', { name: 'Fixed Discount *' })
868
+ .getByRole('textbox');
869
+ const minSpendInput = adminPage
870
+ .getByRole('group', { name: 'Minimum Spend *' })
871
+ .getByRole('textbox');
872
+ await discountInput.fill('12.34');
873
+ await minSpendInput.fill('45.67');
874
+ await expect(discountInput).toHaveValue('12.34');
875
+ await expect(minSpendInput).toHaveValue('45.67');
876
+ await expect(
877
+ adminPage.getByRole('group', { name: 'Valid From *' }).locator('input[type="date"]'),
878
+ ).toBeVisible();
879
+ await expect(
880
+ adminPage.getByRole('group', { name: 'Valid Until *' }).locator('input[type="date"]'),
881
+ ).toBeVisible();
882
+
883
+ await adminPage
884
+ .getByRole('group', { name: 'Name *' })
885
+ .getByRole('textbox')
886
+ .fill('E2E Coupon Template');
887
+ await adminPage.getByRole('group', { name: 'Currency *' }).getByRole('textbox').fill('USD');
888
+ await adminPage
889
+ .getByRole('group', { name: 'Valid From *' })
890
+ .getByRole('textbox')
891
+ .fill('2026-08-04');
892
+ await adminPage
893
+ .getByRole('group', { name: 'Valid Until *' })
894
+ .getByRole('textbox')
895
+ .fill('2026-12-31');
896
+ await adminPage
897
+ .getByRole('group', { name: 'Total Issue Limit' })
898
+ .getByRole('textbox')
899
+ .fill('10');
900
+ await adminPage
901
+ .getByRole('group', { name: 'Total Usage Limit' })
902
+ .getByRole('textbox')
903
+ .fill('10');
904
+ await adminPage
905
+ .getByRole('group', { name: 'Per-customer Issue Limit' })
906
+ .getByRole('textbox')
907
+ .fill('1');
908
+
909
+ await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toBeVisible();
910
+ await expect(adminPage.getByRole('button', { name: 'Back', exact: true })).toBeVisible();
911
+ expect(adminPageErrors).toEqual([]);
912
+ } finally {
913
+ await adminContext.close().catch(() => {});
914
+ }
915
+ },
916
+ );
917
+
918
+ test(
919
+ 'ATP-SPC-02: Category renders semantic Admin relation and publication controls',
920
+ { tag: ['@admin', '@flow', '@category'] },
921
+ async ({ browser }, testInfo) => {
922
+ test.setTimeout(60_000);
923
+ const suffix = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`;
924
+ const parentName = `E2E Category Parent ${suffix}`;
925
+ const childName = `E2E Category Child ${suffix}`;
926
+ const adminContext = await browser.newContext();
927
+ const adminPage = await adminContext.newPage();
928
+ const adminPageErrors = collectPageErrors(adminPage);
929
+ const categoryCreatePath = '/commerce-admin/rest/resource/commerce-catalog%3Acategory/create';
930
+ const categoryListPath = '/commerce-admin/rest/resource/commerce-catalog%3Acategory';
931
+ const categoryActionPath = '/api/commerce/catalog/category';
932
+ let parentId: number | string | undefined;
933
+ let childId: number | string | undefined;
934
+ let headers: { Authorization: string } | undefined;
935
+ try {
936
+ await adminPage.setViewportSize({ width: 1440, height: 900 });
937
+ await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin');
938
+ const accessToken = (await adminContext.cookies()).find(
939
+ cookie => cookie.name === 'token',
940
+ )?.value;
941
+ expect(accessToken).toBeTruthy();
942
+ headers = { Authorization: `Bearer ${accessToken}` };
943
+
944
+ const parentCreateResponse = await adminPage.request.post(categoryActionPath, {
945
+ data: { name: parentName, published: false },
946
+ headers,
947
+ });
948
+ expect(parentCreateResponse.ok()).toBeTruthy();
949
+ parentId = (await parentCreateResponse.json()).data;
950
+ expectTableIdentity(parentId);
951
+
952
+ await adminPage.goto(categoryCreatePath, { waitUntil: 'load' });
953
+ const parentPublication = adminPage
954
+ .getByRole('group', { name: 'Published' })
955
+ .getByRole('combobox');
956
+ await expect(parentPublication).toBeVisible();
957
+ await expect(parentPublication.locator('option')).toHaveText([
958
+ '',
959
+ 'Unpublished',
960
+ 'Published',
961
+ ]);
962
+ await parentPublication.selectOption({ label: 'Unpublished' });
963
+ await expect(parentPublication).toHaveValue('false');
964
+
965
+ const parentPicker = adminPage
966
+ .getByRole('group', { name: 'Parent category' })
967
+ .getByRole('combobox');
968
+ await expect(parentPicker).toBeVisible();
969
+ await parentPicker.selectOption({ label: parentName });
970
+ const parentValue = await parentPicker.inputValue();
971
+ expect(parentValue).toBe(String(parentId));
972
+ const childPublication = adminPage
973
+ .getByRole('group', { name: 'Published' })
974
+ .getByRole('combobox');
975
+ await childPublication.selectOption({ label: 'Published' });
976
+ await expect(childPublication).toHaveValue('true');
977
+
978
+ const childCreateResponse = await adminPage.request.post(categoryActionPath, {
979
+ data: { name: childName, parentId: parentValue, published: true },
980
+ headers,
981
+ });
982
+ expect(childCreateResponse.ok()).toBeTruthy();
983
+ childId = (await childCreateResponse.json()).data;
984
+ expectTableIdentity(childId);
985
+
986
+ await adminPage.goto(categoryListPath, { waitUntil: 'load' });
987
+ const childRow = adminPage.getByRole('row', { name: new RegExp(childName) });
988
+ await expect(childRow).toBeVisible();
989
+ await expect(childRow.getByText(parentName, { exact: true })).toBeVisible();
990
+ await expect(childRow.getByText('Published', { exact: true })).toBeVisible();
991
+
992
+ await adminPage.goto(`${categoryListPath}/${childId}`, { waitUntil: 'load' });
993
+ await expect(
994
+ adminPage.getByRole('group', { name: 'Parent category' }).getByRole('textbox'),
995
+ ).toHaveValue(parentName);
996
+ await expect(
997
+ adminPage.getByRole('group', { name: 'Published' }).getByRole('textbox'),
998
+ ).toHaveValue('Published');
999
+ await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toHaveCount(0);
1000
+ expect(adminPageErrors).toEqual([]);
1001
+ } finally {
1002
+ if (childId && headers) {
1003
+ const response = await adminPage.request.delete(`${categoryActionPath}/${childId}`, {
1004
+ headers,
1005
+ });
1006
+ expect(response.ok()).toBeTruthy();
1007
+ }
1008
+ if (parentId && headers) {
1009
+ const response = await adminPage.request.delete(`${categoryActionPath}/${parentId}`, {
1010
+ headers,
1011
+ });
1012
+ expect(response.ok()).toBeTruthy();
1013
+ }
1014
+ await adminContext.close().catch(() => {});
1015
+ }
1016
+ },
1017
+ );
1018
+
799
1019
  test(
800
1020
  'Commerce Cart: anonymous browser is redirected to login',
801
1021
  { tag: ['@web', '@cart'] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cabloy",
3
- "version": "5.1.125",
3
+ "version": "5.1.127",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A Node.js fullstack framework",
6
6
  "keywords": [
@@ -23,6 +23,7 @@
23
23
  "scripts": {
24
24
  "init": "node scripts/init.ts",
25
25
  "init:test-data": "node scripts/initTestData.ts",
26
+ "test:scripts": "node --test scripts/initAppName.test.ts",
26
27
  "upgrade": "node scripts/upgrade.ts",
27
28
  "upgrade:dry-run": "node scripts/upgrade.ts --dry-run",
28
29
  "vona": "node ./vona/packages-cli/cli/src/bin/vona.ts --projectPath=vona",
package/scripts/init.ts CHANGED
@@ -9,10 +9,11 @@ import {
9
9
  rmSync,
10
10
  writeFileSync,
11
11
  } from 'node:fs';
12
- import { basename } from 'node:path';
13
12
  import { dirname, resolve } from 'node:path';
14
13
  import { fileURLToPath } from 'node:url';
15
14
 
15
+ import { updateBaseAppName } from './initAppName.ts';
16
+
16
17
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
18
  const ROOT_DIR = resolve(__dirname, '..');
18
19
  const VONA_DIR = resolve(ROOT_DIR, 'vona');
@@ -141,19 +142,13 @@ function seedVonaZovaRestWorkspaceDependencies(pkgPath: string): void {
141
142
  console.log('[init] Seeded Vona .zova-rest dependencies');
142
143
  }
143
144
 
144
- // --- Step 0: Set APP_NAME in .env files ---
145
+ // --- Step 0: Set APP_NAME in base .env files ---
145
146
 
146
147
  function setAppName(): void {
147
- const projectName = basename(ROOT_DIR);
148
- const envFiles = [resolve(ROOT_DIR, 'vona/env/.env'), resolve(ROOT_DIR, 'zova/env/.env')];
149
- for (const filePath of envFiles) {
150
- if (!existsSync(filePath)) continue;
151
- let content = readFileSync(filePath, 'utf-8');
152
- content = content.replace(/^APP_NAME.*/m, `APP_NAME = ${projectName}`);
153
- writeFileSync(filePath, content);
154
- // eslint-disable-next-line
155
- console.log(`[init] Set APP_NAME = ${projectName} in ${filePath}`);
156
- }
148
+ updateBaseAppName(ROOT_DIR);
149
+ // if (updateBaseAppName(ROOT_DIR)) return;
150
+ // eslint-disable-next-line
151
+ // console.log('[init] Linked Git worktree detected; preserving base APP_NAME defaults');
157
152
  }
158
153
 
159
154
  // --- Step A: Generate vona/env/.env.prod.local ---
@@ -0,0 +1,72 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { basename, resolve } from 'node:path';
6
+ import test from 'node:test';
7
+
8
+ import { isLinkedGitWorktree, updateBaseAppName } from './initAppName.ts';
9
+
10
+ function git(cwd: string, args: string[]): void {
11
+ execFileSync('git', args, { cwd, stdio: 'ignore' });
12
+ }
13
+
14
+ function writeBaseEnvFiles(rootDir: string, appName: string): void {
15
+ for (const relativePath of ['vona/env/.env', 'zova/env/.env']) {
16
+ const filePath = resolve(rootDir, relativePath);
17
+ mkdirSync(resolve(filePath, '..'), { recursive: true });
18
+ writeFileSync(filePath, `APP_NAME = ${appName}\nOTHER_VALUE = preserved\n`);
19
+ }
20
+ }
21
+
22
+ function readBaseEnvFiles(rootDir: string): string[] {
23
+ return ['vona/env/.env', 'zova/env/.env'].map(relativePath =>
24
+ readFileSync(resolve(rootDir, relativePath), 'utf-8'),
25
+ );
26
+ }
27
+
28
+ test('classifies project roots before updating base APP_NAME files', { concurrency: false }, () => {
29
+ const tempDir = mkdtempSync(resolve(tmpdir(), 'cabloy-init-app-name-'));
30
+ const primaryDir = resolve(tempDir, 'primary');
31
+ const linkedDir = resolve(tempDir, 'linked');
32
+ try {
33
+ mkdirSync(primaryDir);
34
+ git(primaryDir, ['init']);
35
+ git(primaryDir, ['config', 'user.email', 'test@example.com']);
36
+ git(primaryDir, ['config', 'user.name', 'Cabloy Test']);
37
+ writeBaseEnvFiles(primaryDir, 'original');
38
+ git(primaryDir, ['add', '.']);
39
+ git(primaryDir, ['commit', '-m', 'initial fixture']);
40
+
41
+ assert.equal(isLinkedGitWorktree(primaryDir), false);
42
+ assert.equal(updateBaseAppName(primaryDir), true);
43
+ for (const content of readBaseEnvFiles(primaryDir)) {
44
+ assert.equal(content, `APP_NAME = ${basename(primaryDir)}\nOTHER_VALUE = preserved\n`);
45
+ }
46
+
47
+ git(primaryDir, ['checkout', '--', '.']);
48
+ git(primaryDir, ['worktree', 'add', '--detach', linkedDir, 'HEAD']);
49
+ const linkedBefore = readBaseEnvFiles(linkedDir);
50
+ assert.equal(isLinkedGitWorktree(linkedDir), true);
51
+ assert.equal(updateBaseAppName(linkedDir), false);
52
+ assert.deepEqual(readBaseEnvFiles(linkedDir), linkedBefore);
53
+
54
+ const nestedProjectDir = resolve(linkedDir, 'created-project');
55
+ writeBaseEnvFiles(nestedProjectDir, 'original');
56
+ assert.equal(isLinkedGitWorktree(nestedProjectDir), false);
57
+ assert.equal(updateBaseAppName(nestedProjectDir), true);
58
+ for (const content of readBaseEnvFiles(nestedProjectDir)) {
59
+ assert.equal(content, `APP_NAME = ${basename(nestedProjectDir)}\nOTHER_VALUE = preserved\n`);
60
+ }
61
+
62
+ const nonGitProjectDir = resolve(tempDir, 'non-git-project');
63
+ writeBaseEnvFiles(nonGitProjectDir, 'original');
64
+ assert.equal(isLinkedGitWorktree(nonGitProjectDir), false);
65
+ assert.equal(updateBaseAppName(nonGitProjectDir), true);
66
+ for (const content of readBaseEnvFiles(nonGitProjectDir)) {
67
+ assert.equal(content, `APP_NAME = ${basename(nonGitProjectDir)}\nOTHER_VALUE = preserved\n`);
68
+ }
69
+ } finally {
70
+ rmSync(tempDir, { recursive: true, force: true });
71
+ }
72
+ });
@@ -0,0 +1,50 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
3
+ import { basename, resolve } from 'node:path';
4
+
5
+ function gitOutput(rootDir: string, args: string[]): string {
6
+ return execFileSync('git', args, {
7
+ cwd: rootDir,
8
+ encoding: 'utf-8',
9
+ stdio: ['ignore', 'pipe', 'ignore'],
10
+ }).trim();
11
+ }
12
+
13
+ export function isLinkedGitWorktree(rootDir: string): boolean {
14
+ const projectRoot = realpathSync(resolve(rootDir));
15
+ try {
16
+ const gitRoot = realpathSync(
17
+ resolve(gitOutput(projectRoot, ['rev-parse', '--path-format=absolute', '--show-toplevel'])),
18
+ );
19
+ if (gitRoot !== projectRoot) return false;
20
+
21
+ const gitDir = realpathSync(
22
+ resolve(gitOutput(projectRoot, ['rev-parse', '--path-format=absolute', '--git-dir'])),
23
+ );
24
+ const gitCommonDir = realpathSync(
25
+ resolve(gitOutput(projectRoot, ['rev-parse', '--path-format=absolute', '--git-common-dir'])),
26
+ );
27
+ return gitDir !== gitCommonDir;
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ export function updateBaseAppName(rootDir: string): boolean {
34
+ if (isLinkedGitWorktree(rootDir)) return false;
35
+
36
+ const projectRoot = resolve(rootDir);
37
+ const projectName = basename(projectRoot);
38
+ const envFiles = [resolve(projectRoot, 'vona/env/.env'), resolve(projectRoot, 'zova/env/.env')];
39
+ for (const filePath of envFiles) {
40
+ if (!existsSync(filePath)) continue;
41
+ const content = readFileSync(filePath, 'utf-8').replace(
42
+ /^APP_NAME.*/m,
43
+ `APP_NAME = ${projectName}`,
44
+ );
45
+ writeFileSync(filePath, content);
46
+ // eslint-disable-next-line
47
+ console.log(`[init] Set APP_NAME = ${projectName} in ${filePath}`);
48
+ }
49
+ return true;
50
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli",
3
- "version": "1.1.136",
3
+ "version": "1.1.137",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli-set-api",
3
- "version": "1.1.134",
3
+ "version": "1.1.135",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli-set-api",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona",
3
- "version": "5.1.82",
3
+ "version": "5.1.84",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "Vona is an intuitive, elegant and powerful Node.js framework for rapidly developing enterprise applications of any size",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-core",
3
- "version": "5.1.35",
3
+ "version": "5.1.36",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona",
6
6
  "keywords": [
@@ -21,7 +21,7 @@ export class ErrorClass extends BeanSimple {
21
21
  fail(module, code, ...args) {
22
22
  const body = this.parseFail(module, code, ...args);
23
23
 
24
- this.ctx.response.status = __calcStatus(body.code);
24
+ this.ctx.response.status = body.status;
25
25
  this.ctx.response.type = 'application/json';
26
26
  this.ctx.response.body = { code: body.code, message: body.message }; // body maybe Error
27
27
  }
@@ -32,7 +32,7 @@ export class ErrorClass extends BeanSimple {
32
32
  const err = new Error();
33
33
  err.code = body.code;
34
34
  err.message = body.message;
35
- err.status = __calcStatus(body.code);
35
+ err.status = body.status;
36
36
  throw err;
37
37
  }
38
38
 
@@ -58,9 +58,16 @@ export class ErrorClass extends BeanSimple {
58
58
 
59
59
  // convert from enum
60
60
  let text;
61
+ let status;
61
62
  if (ebError && code && typeof code === 'string') {
62
63
  text = code;
63
- code = ebError[code];
64
+ const declaration = ebError[code];
65
+ if (__isErrorDescriptor(declaration)) {
66
+ code = declaration.code;
67
+ status = declaration.status;
68
+ } else {
69
+ code = declaration;
70
+ }
64
71
  }
65
72
 
66
73
  if (code === undefined || code === null || code === '') {
@@ -75,10 +82,20 @@ export class ErrorClass extends BeanSimple {
75
82
  }
76
83
 
77
84
  code = __combineErrorCode(module, code);
78
- return { code, message };
85
+ status ??= __calcStatus(code);
86
+ return { code, status, message };
79
87
  }
80
88
  }
81
89
 
90
+ function __isErrorDescriptor(value) {
91
+ return (
92
+ value &&
93
+ typeof value === 'object' &&
94
+ typeof value.code === 'number' &&
95
+ typeof value.status === 'number'
96
+ );
97
+ }
98
+
82
99
  function __combineErrorCode(module, code) {
83
100
  if (typeof code !== 'number' || code <= 1000) return code;
84
101
  return module ? `${module}:${code}` : code;
@@ -1,4 +1,5 @@
1
1
  export interface IErrorObject {
2
2
  code: number | string;
3
+ status: number;
3
4
  message: string;
4
5
  }
@@ -29,12 +29,14 @@ export type TypeBeanScopeLocaleKeys = keyof IBeanScopeLocale;
29
29
  export interface IBeanScopeErrors {}
30
30
  export type TypeBeanScopeErrorsKeys = keyof IBeanScopeErrors;
31
31
 
32
+ type TypeErrorDeclarationCode<T> = T extends { code: infer Code } ? Code : T;
33
+
32
34
  export type TypeScopesErrorsHelper<
33
35
  ModuleName extends keyof IBeanScopeErrors,
34
36
  Errors extends IBeanScopeErrors[ModuleName],
35
37
  > = {
36
38
  // @ts-ignore: ignore
37
- [K in keyof Errors as `${ModuleName}:${Errors[K]}`]: K;
39
+ [K in keyof Errors as `${ModuleName}:${TypeErrorDeclarationCode<Errors[K]>}`]: K;
38
40
  };
39
41
  export type TypeScopesErrorCodes = TypeRecordValues<{
40
42
  [ModuleName in keyof IBeanScopeErrors]: keyof TypeScopesErrorsHelper<
@@ -4,7 +4,12 @@ import type { IModuleMain, IMonkeyModule, IMonkeySystem } from './monkey.ts';
4
4
 
5
5
  export type TypeModuleResourceLocales = Record<string, object>;
6
6
  export type TypeModuleResourceLocaleModules = Record<string, TypeModuleResourceLocales>;
7
- export type TypeModuleResourceErrors = Record<string, number>;
7
+ export interface IModuleResourceErrorDescriptor {
8
+ code: number;
9
+ status: number;
10
+ }
11
+ export type TypeModuleResourceError = number | IModuleResourceErrorDescriptor;
12
+ export type TypeModuleResourceErrors = Record<string, TypeModuleResourceError>;
8
13
  export type TypeModuleResourceErrorModules = Record<string, TypeModuleResourceErrors>;
9
14
  export type TypeModuleResourceConfig = (app: VonaApplication) => object | Promise<object>;
10
15
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-mock",
3
- "version": "6.1.35",
3
+ "version": "6.1.36",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona mock",
6
6
  "keywords": [