cabloy 5.1.117 → 5.1.119

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 (119) hide show
  1. package/.cabloy-version +1 -1
  2. package/.github/workflows/vona-test-pg.yml +2 -2
  3. package/.github/workflows/vona-test-sqlite3.yml +2 -2
  4. package/CHANGELOG.md +19 -0
  5. package/cabloy-docs/backend/metrics-guide.md +75 -0
  6. package/e2e/specs/a-commerce/commerce.spec.ts +111 -0
  7. package/observability/README.md +18 -0
  8. package/observability/collector/otel-collector.yaml +34 -0
  9. package/observability/docker-compose.metrics.yml +25 -0
  10. package/observability/grafana/provisioning/datasources/prometheus.yaml +8 -0
  11. package/observability/prometheus/prometheus.yaml +7 -0
  12. package/package.json +1 -1
  13. package/scripts/upgrade.ts +1 -0
  14. package/vona/env/.env +6 -0
  15. package/vona/packages-cli/cli/package.json +1 -1
  16. package/vona/packages-cli/cli-set-api/package.json +1 -1
  17. package/vona/packages-vona/vona/package.json +1 -1
  18. package/vona/packages-vona/vona-core/package.json +1 -1
  19. package/vona/packages-vona/vona-core/src/lib/framework/processWorker.ts +1 -1
  20. package/vona/packages-vona/vona-mock/package.json +1 -1
  21. package/vona/packages-vona/vona-mock/src/index.ts +2 -0
  22. package/vona/packages-vona/vona-mock/src/lib/testLock.ts +21 -0
  23. package/vona/pnpm-lock.yaml +167 -64
  24. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/catalog.test.ts +13 -3
  25. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/product.test.ts +124 -70
  26. package/vona/src/suite/a-commerce/modules/commerce-payment/src/.metadata/index.ts +221 -0
  27. package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/meta.index.ts +12 -0
  28. package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/meta.version.ts +91 -0
  29. package/vona/src/suite/a-commerce/modules/commerce-payment/src/entity/refundAttempt.tsx +37 -0
  30. package/vona/src/suite/a-commerce/modules/commerce-payment/src/entity/refundAudit.tsx +47 -0
  31. package/vona/src/suite/a-commerce/modules/commerce-payment/src/entity/refundRequest.tsx +43 -0
  32. package/vona/src/suite/a-commerce/modules/commerce-payment/src/model/refundAttempt.ts +10 -0
  33. package/vona/src/suite/a-commerce/modules/commerce-payment/src/model/refundAudit.ts +10 -0
  34. package/vona/src/suite/a-commerce/modules/commerce-payment/src/model/refundRequest.ts +10 -0
  35. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +36 -0
  36. package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/order.ts +43 -0
  37. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderDetail.tsx +23 -2
  38. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderSelectResItem.tsx +3 -0
  39. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderSummary.tsx +23 -2
  40. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/refundOutcomeCreate.tsx +16 -0
  41. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/refundRequestCreate.tsx +15 -0
  42. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/refundResult.tsx +35 -0
  43. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/refundReview.tsx +15 -0
  44. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/orderAudit.tsx +55 -13
  45. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +459 -10
  46. package/vona/src/suite/a-commerce/modules/commerce-trade/test/cartOwnership.test.ts +2 -4
  47. package/vona/src/suite/a-commerce/modules/commerce-trade/test/checkoutReservation.test.ts +2 -4
  48. package/vona/src/suite/a-commerce/modules/commerce-trade/test/checkoutTransaction.test.ts +2 -4
  49. package/vona/src/suite/a-commerce/modules/commerce-trade/test/orderSnapshot.test.ts +2 -4
  50. package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +2 -4
  51. package/vona/src/suite/a-commerce/modules/commerce-trade/test/refundLifecycle.test.ts +337 -0
  52. package/vona/src/suite/a-commerce/modules/commerce-trade/test/reservationExpiry.test.ts +2 -3
  53. package/vona/src/suite/a-commerce/modules/commerce-trade/test/shipment.test.ts +2 -4
  54. package/vona/src/suite/a-commerce/modules/commerce-trade/test/shipmentRefundRace.test.ts +143 -0
  55. package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockBalance.test.ts +2 -4
  56. package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockReservation.test.ts +2 -4
  57. package/vona/src/suite/cabloy-basic/modules/basic-metrics/package.json +54 -0
  58. package/vona/src/suite/cabloy-basic/modules/basic-metrics/src/.metadata/index.ts +189 -0
  59. package/vona/src/suite/cabloy-basic/modules/basic-metrics/src/.metadata/this.ts +1 -0
  60. package/vona/src/suite/cabloy-basic/modules/basic-metrics/src/bean/ssrMenu.metrics.ts +23 -0
  61. package/vona/src/suite/cabloy-basic/modules/basic-metrics/src/controller/metrics.ts +21 -0
  62. package/vona/src/suite/cabloy-basic/modules/basic-metrics/src/dto/metricsQueue.ts +25 -0
  63. package/vona/src/suite/cabloy-basic/modules/basic-metrics/src/dto/metricsRuntime.ts +36 -0
  64. package/vona/src/suite/cabloy-basic/modules/basic-metrics/src/dto/metricsSnapshot.ts +21 -0
  65. package/vona/src/suite/cabloy-basic/modules/basic-metrics/src/index.ts +1 -0
  66. package/vona/src/suite/cabloy-basic/modules/basic-metrics/src/service/metrics.ts +11 -0
  67. package/vona/src/suite/cabloy-basic/modules/basic-metrics/tsconfig.build.json +11 -0
  68. package/vona/src/suite/cabloy-basic/modules/basic-metrics/tsconfig.json +5 -0
  69. package/vona/src/suite/cabloy-basic/package.json +1 -0
  70. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  71. package/vona/src/suite-vendor/a-vona/modules/a-instance/package.json +1 -1
  72. package/vona/src/suite-vendor/a-vona/modules/a-instance/src/bean/middlewareSystem.app.ts +2 -0
  73. package/vona/src/suite-vendor/a-vona/modules/a-instance/src/bean/middlewareSystem.instance.ts +2 -0
  74. package/vona/src/suite-vendor/a-vona/modules/a-metrics/package.json +62 -0
  75. package/vona/src/suite-vendor/a-vona/modules/a-metrics/src/.metadata/index.ts +178 -0
  76. package/vona/src/suite-vendor/a-vona/modules/a-metrics/src/.metadata/this.ts +1 -0
  77. package/vona/src/suite-vendor/a-vona/modules/a-metrics/src/bean/bean.metrics.ts +15 -0
  78. package/vona/src/suite-vendor/a-vona/modules/a-metrics/src/bean/middlewareSystem.health.ts +24 -0
  79. package/vona/src/suite-vendor/a-vona/modules/a-metrics/src/bean/middlewareSystem.metrics.ts +51 -0
  80. package/vona/src/suite-vendor/a-vona/modules/a-metrics/src/config/config.ts +75 -0
  81. package/vona/src/suite-vendor/a-vona/modules/a-metrics/src/index.ts +2 -0
  82. package/vona/src/suite-vendor/a-vona/modules/a-metrics/src/monkey.ts +27 -0
  83. package/vona/src/suite-vendor/a-vona/modules/a-metrics/src/service/health.ts +53 -0
  84. package/vona/src/suite-vendor/a-vona/modules/a-metrics/src/service/metrics.ts +222 -0
  85. package/vona/src/suite-vendor/a-vona/modules/a-metrics/src/service/metricsRuntime.ts +69 -0
  86. package/vona/src/suite-vendor/a-vona/modules/a-metrics/src/types/context.ts +9 -0
  87. package/vona/src/suite-vendor/a-vona/modules/a-metrics/src/types/index.ts +2 -0
  88. package/vona/src/suite-vendor/a-vona/modules/a-metrics/src/types/metrics.ts +50 -0
  89. package/vona/src/suite-vendor/a-vona/modules/a-metrics/tsconfig.build.json +11 -0
  90. package/vona/src/suite-vendor/a-vona/modules/a-metrics/tsconfig.json +7 -0
  91. package/vona/src/suite-vendor/a-vona/package.json +2 -1
  92. package/zova/pnpm-lock.yaml +104 -89
  93. package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +4 -0
  94. package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/component/tableCellActionRefund.ts +31 -0
  95. package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +35 -6
  96. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeOrder.ts +104 -0
  97. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +16 -0
  98. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +231 -2
  99. package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeOrder.ts +20 -0
  100. package/zova/src/suite/a-commerce/modules/commerce-trade/src/bean/tableCell.actionRefund.tsx +40 -0
  101. package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionRefund/controller.tsx +106 -0
  102. package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +7 -0
  103. package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +7 -0
  104. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +45 -1
  105. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/orderMine.ts +15 -0
  106. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/order/controller.tsx +41 -0
  107. package/zova/src/suite/cabloy-basic/modules/basic-metrics/package.json +53 -0
  108. package/zova/src/suite/cabloy-basic/modules/basic-metrics/src/.metadata/index.ts +151 -0
  109. package/zova/src/suite/cabloy-basic/modules/basic-metrics/src/.metadata/page/dashboard.ts +9 -0
  110. package/zova/src/suite/cabloy-basic/modules/basic-metrics/src/.metadata/this.ts +1 -0
  111. package/zova/src/suite/cabloy-basic/modules/basic-metrics/src/api/basicMetricsMetrics.ts +41 -0
  112. package/zova/src/suite/cabloy-basic/modules/basic-metrics/src/index.ts +2 -0
  113. package/zova/src/suite/cabloy-basic/modules/basic-metrics/src/model/metrics.ts +16 -0
  114. package/zova/src/suite/cabloy-basic/modules/basic-metrics/src/page/dashboard/controller.tsx +95 -0
  115. package/zova/src/suite/cabloy-basic/modules/basic-metrics/src/routes.ts +5 -0
  116. package/zova/src/suite/cabloy-basic/modules/basic-metrics/tsconfig.build.json +13 -0
  117. package/zova/src/suite/cabloy-basic/modules/basic-metrics/tsconfig.json +5 -0
  118. package/zova/src/suite/cabloy-basic/package.json +1 -0
  119. package/vona/src/suite/a-commerce/modules/commerce-trade/test/testLock.ts +0 -11
package/.cabloy-version CHANGED
@@ -1 +1 @@
1
- 5.1.117
1
+ 5.1.119
@@ -42,8 +42,8 @@ jobs:
42
42
  DATABASE_DEFAULT_CLIENT=pg npm run vona :bin:test -- commerce-trade/test/orderSnapshot.test.ts --flavor=normal
43
43
  DATABASE_DEFAULT_CLIENT=pg npm run vona :bin:test -- commerce-trade/test/reservationExpiry.test.ts --flavor=normal
44
44
  working-directory: vona
45
- - name: run Commerce payment and shipment lifecycle gate
46
- run: DATABASE_DEFAULT_CLIENT=pg npm run vona :bin:test -- commerce-trade/test/paymentOutcome.test.ts commerce-trade/test/reservationExpiry.test.ts commerce-trade/test/shipment.test.ts --flavor=normal
45
+ - name: run Commerce payment, shipment, and refund lifecycle gate
46
+ run: DATABASE_DEFAULT_CLIENT=pg npm run vona :bin:test -- commerce-trade/test/paymentOutcome.test.ts commerce-trade/test/reservationExpiry.test.ts commerce-trade/test/shipment.test.ts commerce-trade/test/refundLifecycle.test.ts commerce-trade/test/shipmentRefundRace.test.ts --flavor=normal
47
47
  working-directory: vona
48
48
  - run: DATABASE_DEFAULT_CLIENT=pg npm run test
49
49
  working-directory: vona
@@ -27,8 +27,8 @@ jobs:
27
27
  version: 11.5.2
28
28
  - name: init
29
29
  run: npm run init
30
- - name: run Commerce payment and shipment lifecycle gate
31
- run: DATABASE_DEFAULT_CLIENT=sqlite3 npm run vona :bin:test -- commerce-trade/test/paymentOutcome.test.ts commerce-trade/test/reservationExpiry.test.ts commerce-trade/test/shipment.test.ts --flavor=normal
30
+ - name: run Commerce payment, shipment, and refund lifecycle gate
31
+ run: DATABASE_DEFAULT_CLIENT=sqlite3 npm run vona :bin:test -- commerce-trade/test/paymentOutcome.test.ts commerce-trade/test/reservationExpiry.test.ts commerce-trade/test/shipment.test.ts commerce-trade/test/refundLifecycle.test.ts --flavor=normal
32
32
  working-directory: vona
33
33
  - run: DATABASE_DEFAULT_CLIENT=sqlite3 npm run test
34
34
  working-directory: vona
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.119
4
+
5
+ ### Improvements
6
+
7
+ - Update the upgrade script.
8
+
9
+ ## 5.1.118
10
+
11
+ ### Features
12
+
13
+ - Complete refund lifecycle coverage for commerce workflows.
14
+ - Add metrics support.
15
+ - Update application functionality and dependencies.
16
+
17
+ ### Improvements
18
+
19
+ - Extract the scene test lock into `vona-mock`.
20
+ - Update the pnPM lockfile and environment configuration.
21
+
3
22
  ## 5.1.117
4
23
 
5
24
  ### Features
@@ -0,0 +1,75 @@
1
+ # Metrics Guide
2
+
3
+ Cabloy Metrics uses OpenTelemetry Metrics (OTLP/HTTP) and is disabled by default. It does not publish an application `/metrics` endpoint. A Vona application normally has several independent workers, so an in-memory Prometheus registry on the shared application port would expose only one worker per scrape.
4
+
5
+ ## Architecture
6
+
7
+ ```text
8
+ Vona workers -> private OpenTelemetry Collector -> Prometheus exporter -> Prometheus
9
+ ```
10
+
11
+ Each worker exports its own metric stream. Preserve `service.instance.id` during ingestion and aggregate workers in PromQL with `sum`, `rate`, and `histogram_quantile` as appropriate.
12
+
13
+ Prometheus must scrape the Collector exporter on a private monitoring network. Do not route Collector OTLP or Prometheus exporter ports through Cloudflare or a public Nginx listener.
14
+
15
+ ## Enablement
16
+
17
+ Inject configuration through the deployment platform. Do not commit credentials to environment files.
18
+
19
+ ```text
20
+ METRICS_ENABLED=true
21
+ METRICS_SERVICE_NAME=cabloy
22
+ METRICS_OTLP_HTTP_URL=https://collector.internal/v1/metrics
23
+ METRICS_OTLP_HTTP_HEADERS=Authorization=Bearer <secret>
24
+ ```
25
+
26
+ Additional bounded settings are available:
27
+
28
+ - `METRICS_EXPORT_INTERVAL_MS` — export cadence, default `15000`
29
+ - `METRICS_EXPORT_TIMEOUT_MS` — one export timeout, default `10000`
30
+ - `METRICS_RUNTIME_INTERVAL_MS` — process/runtime snapshot cadence, default `15000`
31
+ - `METRICS_QUEUE_INTERVAL_MS` — reserved queue collection cadence, default `30000`
32
+ - `METRICS_SNAPSHOT_CACHE_MS` — Admin snapshot cache lifetime, default `5000`
33
+ - `METRICS_SHUTDOWN_TIMEOUT_MS` — bounded final flush time, default `4000`
34
+
35
+ Invalid numeric values are clamped to safe ranges. Disabling Metrics creates no exporter or background sampler.
36
+
37
+ ## Initial metric families
38
+
39
+ - HTTP request count, duration histogram, and active-request count
40
+ - process uptime, RSS, heap, external memory, event-loop delay, and active Vona contexts
41
+ - metrics observation failure counters
42
+
43
+ HTTP labels are deliberately restricted to normalized method, route template, final status, and a bounded aborted marker. Literal URLs, query strings, hosts, tenant/instance names, users, request IDs, trace IDs, cookies, authorization values, Redis keys, queue job IDs, and job payloads are prohibited.
44
+
45
+ HTTP duration buckets in seconds are fixed as:
46
+
47
+ ```text
48
+ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30
49
+ ```
50
+
51
+ ## Health probes
52
+
53
+ Use these private infrastructure endpoints:
54
+
55
+ | Endpoint | Meaning |
56
+ | --------------------- | --------------------------------------------------------- |
57
+ | `GET /health/startup` | Vona startup completed |
58
+ | `GET /health/live` | process is serving and not closing |
59
+ | `GET /health/ready` | startup complete plus bounded PostgreSQL and Redis checks |
60
+
61
+ They return only a small status response. Collector and queue-observation failures never make the readiness probe fail.
62
+
63
+ ## Admin dashboard
64
+
65
+ The Basic Admin **Metrics** page is available only to `systemAdmin`. It shows a redacted, short-lived runtime snapshot and clear `disabled`, `fresh`, `stale`, or `unavailable` states. It is not a Prometheus query UI and never sends browser traffic to OTLP, Collector, or probe endpoints.
66
+
67
+ ## Local example
68
+
69
+ A loopback-only local stack is available under the repository's `observability/README.md`:
70
+
71
+ ```bash
72
+ docker compose -f observability/docker-compose.metrics.yml up
73
+ ```
74
+
75
+ Use it for local validation only. Production networking, TLS/mTLS, secret injection, retention, dashboards, and alert routing are operator-owned.
@@ -525,6 +525,117 @@ test(
525
525
  },
526
526
  );
527
527
 
528
+ test(
529
+ 'Phase 60: customer requests and operator executes a whole-order refund',
530
+ { tag: ['@web', '@admin', '@flow', '@payment', '@refund'] },
531
+ async ({ browser, request }, testInfo) => {
532
+ test.setTimeout(120_000);
533
+ const { context, fixture, page, pageErrors } = await createAddressThroughCustomerPage(
534
+ browser,
535
+ request,
536
+ testInfo,
537
+ );
538
+ try {
539
+ await page.goto('/commerce', { waitUntil: 'load' });
540
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
541
+ await expect(page.getByRole('heading', { name: 'Commerce catalogue' })).toBeVisible();
542
+ const productLink = page.getByRole('link', { name: 'Pour-Over Coffee Set', exact: true });
543
+ await expect(productLink).toBeVisible();
544
+ await productLink.click();
545
+ await expect(page).toHaveURL(/\/commerce\/product\/\d+(?:\/|$)/);
546
+ await expect(page.getByRole('heading', { name: 'Pour-Over Coffee Set' })).toBeVisible();
547
+ await expect(page.getByText('COF-SET-01')).toBeVisible();
548
+ await expect(page.getByText('$45.99')).toBeVisible();
549
+ const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items');
550
+ await page.getByRole('button', { name: 'Add to cart', exact: true }).click();
551
+ await addResponse;
552
+ await page.getByRole('link', { name: /^Cart/ }).click();
553
+ await expect(page).toHaveURL(/\/commerce\/cart(?:\/|$)/);
554
+ await page.getByRole('link', { name: 'Checkout', exact: true }).click();
555
+ await expect(page).toHaveURL(/\/commerce\/checkout(?:\/|$)/);
556
+ const addressChoice = page.getByRole('radio', { name: new RegExp(fixture.recipientName) });
557
+ await addressChoice.check();
558
+ await page.getByRole('radio', { name: 'No coupon', exact: true }).check();
559
+ const checkoutResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/checkout');
560
+ await page.getByRole('button', { name: 'Create order', exact: true }).click();
561
+ const checkout = (await (await checkoutResponse).json()).data;
562
+ const paymentResponse = waitForApiResponse(
563
+ page,
564
+ 'POST',
565
+ new RegExp(`/api/commerce/trade/payment/${checkout.paymentAttemptId}/outcome$`),
566
+ );
567
+ await page.getByRole('button', { name: 'Payment succeeded', exact: true }).click();
568
+ expect((await paymentResponse).ok()).toBeTruthy();
569
+ await expect(page.getByText('paid · $45.99')).toBeVisible();
570
+
571
+ const requestResponse = waitForApiResponse(
572
+ page,
573
+ 'POST',
574
+ `/api/commerce/trade/order/${checkout.orderId}/requestRefund`,
575
+ );
576
+ await page.getByPlaceholder('Reason for refund').fill('E2E refund request');
577
+ await page.getByRole('button', { name: 'Request refund', exact: true }).click();
578
+ expect((await requestResponse).ok()).toBeTruthy();
579
+ await expect(page.getByText('refund_requested · $45.99')).toBeVisible();
580
+
581
+ const adminContext = await browser.newContext();
582
+ const adminPage = await adminContext.newPage();
583
+ const adminPageErrors = collectPageErrors(adminPage);
584
+ try {
585
+ await adminPage.setViewportSize({ width: 1440, height: 900 });
586
+ await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin');
587
+ await adminPage.goto('/commerce-admin/rest/resource/commerce-trade%3Aorder', {
588
+ waitUntil: 'load',
589
+ });
590
+ await expect(adminPage.locator('html')).toHaveAttribute(
591
+ 'data-zova-hydrated',
592
+ 'commerceAdmin',
593
+ );
594
+ const orderRow = adminPage
595
+ .getByRole('cell', { name: String(checkout.orderId), exact: true })
596
+ .locator('..');
597
+ await expect(orderRow).toBeVisible();
598
+ await orderRow.getByPlaceholder('Decision reason').fill('E2E approval');
599
+ await orderRow.getByRole('checkbox').check();
600
+ const approveResponse = waitForApiResponse(
601
+ adminPage,
602
+ 'POST',
603
+ `/api/commerce/trade/order/${checkout.orderId}/approveRefund`,
604
+ );
605
+ await orderRow.getByRole('button', { name: 'Approve refund', exact: true }).click();
606
+ expect((await approveResponse).ok()).toBeTruthy();
607
+ await expect(orderRow).toContainText('refund_approved');
608
+ const executeResponse = waitForApiResponse(
609
+ adminPage,
610
+ 'POST',
611
+ `/api/commerce/trade/order/${checkout.orderId}/refundOutcome`,
612
+ );
613
+ await orderRow.getByRole('button', { name: 'Execute refund', exact: true }).click();
614
+ expect((await executeResponse).ok()).toBeTruthy();
615
+ await expect(orderRow).toContainText('refunded');
616
+ expect(adminPageErrors).toEqual([]);
617
+ } finally {
618
+ await adminContext.close().catch(() => {});
619
+ }
620
+
621
+ const refundedDetailResponse = waitForApiResponse(
622
+ page,
623
+ 'GET',
624
+ new RegExp(`/api/commerce/trade/order/viewMine/${checkout.orderId}$`),
625
+ );
626
+ await page.reload({ waitUntil: 'load' });
627
+ await refundedDetailResponse;
628
+ await expect(page.getByText('refunded · $45.99')).toBeVisible();
629
+ await expect(page.getByText('1 × $45.99 = $45.99')).toBeVisible();
630
+ await expect(page.getByRole('heading', { name: 'Shipment' })).toHaveCount(0);
631
+ await expect(page.getByRole('alert')).toHaveCount(0);
632
+ expect(pageErrors).toEqual([]);
633
+ } finally {
634
+ await context.close().catch(() => {});
635
+ }
636
+ },
637
+ );
638
+
528
639
  test(
529
640
  'ATP-ADDR-01: systemAdmin inspects Address Resource without mutation controls',
530
641
  { tag: ['@admin', '@flow', '@address'] },
@@ -0,0 +1,18 @@
1
+ # Local Metrics Stack
2
+
3
+ Start the local OpenTelemetry Collector, Prometheus, and Grafana stack:
4
+
5
+ ```bash
6
+ docker compose -f observability/docker-compose.metrics.yml up
7
+ ```
8
+
9
+ Enable the application metric exporter only for local development:
10
+
11
+ ```text
12
+ METRICS_ENABLED=true
13
+ METRICS_OTLP_HTTP_URL=http://127.0.0.1:4318/v1/metrics
14
+ ```
15
+
16
+ Prometheus scrapes the Collector at its private exporter endpoint. Cabloy deliberately does not expose an application `/metrics` endpoint: Vona workers export independently, and the Collector preserves each worker stream before PromQL aggregates them.
17
+
18
+ The published ports bind to loopback for development. Production must keep the Collector OTLP receiver and Prometheus exporter on a private workload/monitoring network, inject any OTLP credentials from the deployment secret store, and never proxy those paths through Cloudflare or the public Nginx listener.
@@ -0,0 +1,34 @@
1
+ receivers:
2
+ otlp:
3
+ protocols:
4
+ http:
5
+ endpoint: 0.0.0.0:4318
6
+
7
+ processors:
8
+ memory_limiter:
9
+ check_interval: 1s
10
+ limit_mib: 256
11
+ batch:
12
+ timeout: 5s
13
+ transform/metrics_allowlist:
14
+ error_mode: ignore
15
+ metric_statements:
16
+ - context: datapoint
17
+ statements:
18
+ - delete_key(attributes, "http.target") where attributes["http.target"] != nil
19
+ - delete_key(attributes, "url.full") where attributes["url.full"] != nil
20
+ - delete_key(attributes, "user.id") where attributes["user.id"] != nil
21
+ - delete_key(attributes, "enduser.id") where attributes["enduser.id"] != nil
22
+
23
+ exporters:
24
+ prometheus:
25
+ endpoint: 0.0.0.0:9464
26
+ resource_to_telemetry_conversion:
27
+ enabled: true
28
+
29
+ service:
30
+ pipelines:
31
+ metrics:
32
+ receivers: [otlp]
33
+ processors: [memory_limiter, transform/metrics_allowlist, batch]
34
+ exporters: [prometheus]
@@ -0,0 +1,25 @@
1
+ services:
2
+ collector:
3
+ image: otel/opentelemetry-collector-contrib:0.134.1
4
+ command: [--config=/etc/otelcol-contrib/config.yaml]
5
+ ports:
6
+ - '127.0.0.1:4318:4318'
7
+ - '127.0.0.1:9464:9464'
8
+ volumes:
9
+ - ./collector/otel-collector.yaml:/etc/otelcol-contrib/config.yaml:ro
10
+
11
+ prometheus:
12
+ image: prom/prometheus:v3.5.0
13
+ depends_on: [collector]
14
+ ports:
15
+ - '127.0.0.1:9090:9090'
16
+ volumes:
17
+ - ./prometheus/prometheus.yaml:/etc/prometheus/prometheus.yml:ro
18
+
19
+ grafana:
20
+ image: grafana/grafana:12.1.0
21
+ depends_on: [prometheus]
22
+ ports:
23
+ - '127.0.0.1:3000:3000'
24
+ volumes:
25
+ - ./grafana/provisioning:/etc/grafana/provisioning:ro
@@ -0,0 +1,8 @@
1
+ apiVersion: 1
2
+
3
+ datasources:
4
+ - name: Prometheus
5
+ type: prometheus
6
+ access: proxy
7
+ url: http://prometheus:9090
8
+ isDefault: true
@@ -0,0 +1,7 @@
1
+ global:
2
+ scrape_interval: 15s
3
+
4
+ scrape_configs:
5
+ - job_name: cabloy-collector
6
+ static_configs:
7
+ - targets: [collector:9464]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cabloy",
3
- "version": "5.1.117",
3
+ "version": "5.1.119",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A Node.js fullstack framework",
6
6
  "keywords": [
@@ -29,6 +29,7 @@ const OVERWRITE_DIRS: string[] = [
29
29
  'scripts',
30
30
  '.husky',
31
31
  'cabloy-docs',
32
+ 'observability',
32
33
  // vona
33
34
  'vona/packages-vona',
34
35
  'vona/packages-cli',
package/vona/env/.env CHANGED
@@ -34,6 +34,12 @@ LOGGER_ROTATE_DATEPATTERN = YYYY-MM-DD
34
34
  LOGGER_ROTATE_MAXSIZE = 20m
35
35
  LOGGER_ROTATE_MAXFILES = 7d
36
36
 
37
+ # telemetry
38
+ TELEMETRY_ENABLED = false
39
+
40
+ # metrics
41
+ METRICS_ENABLED = true
42
+
37
43
  # build
38
44
 
39
45
  BUILD_OUTDIR =
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli",
3
- "version": "1.1.133",
3
+ "version": "1.1.134",
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.131",
3
+ "version": "1.1.132",
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.81",
3
+ "version": "5.1.82",
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.34",
3
+ "version": "5.1.35",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona",
6
6
  "keywords": [
@@ -21,7 +21,7 @@ async function _closeInner() {
21
21
  }
22
22
 
23
23
  export function handleProcessWork() {
24
- ['SIGINT', 'SIGUSR2'].forEach(signal => {
24
+ ['SIGINT', 'SIGTERM', 'SIGUSR2'].forEach(signal => {
25
25
  process.on(signal, async () => {
26
26
  if (__closing) return;
27
27
  __closing = true;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-mock",
3
- "version": "6.1.34",
3
+ "version": "6.1.35",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona mock",
6
6
  "keywords": [
@@ -4,6 +4,8 @@ import type { VonaApplication } from 'vona-core';
4
4
  import { parseModuleInfo, ParseModuleNameLevelInit } from '@cabloy/module-info-pro';
5
5
  import { useApp } from 'vona-core';
6
6
 
7
+ export { acquireTestLock } from './lib/testLock.ts';
8
+
7
9
  const ParseModuleNameLevel = ParseModuleNameLevelInit + 2;
8
10
 
9
11
  export const app: VonaApplication = useApp();
@@ -0,0 +1,21 @@
1
+ const pendingScenes = new Map<string, Promise<void>>();
2
+
3
+ export async function acquireTestLock(scene: string): Promise<() => void> {
4
+ const previous = pendingScenes.get(scene) ?? Promise.resolve();
5
+ let resolve: () => void;
6
+ const pending = new Promise<void>(pendingResolve => {
7
+ resolve = pendingResolve;
8
+ });
9
+ pendingScenes.set(scene, pending);
10
+ await previous;
11
+
12
+ let released = false;
13
+ return () => {
14
+ if (released) return;
15
+ released = true;
16
+ resolve!();
17
+ if (pendingScenes.get(scene) === pending) {
18
+ pendingScenes.delete(scene);
19
+ }
20
+ };
21
+ }