Compare commits

..

2 Commits

Author SHA1 Message Date
Sathira 835f30b4fb Merge pull request 'Development' (#2) from development into main
CI / test (push) Successful in 10m26s
Reviewed-on: #2
2026-09-12 06:48:45 +00:00
Sathira aeb3ab7fcf Merge pull request 'Auth' (#1) from auth into main
CI / test (push) Has been cancelled
Reviewed-on: #1
2026-09-07 06:53:04 +00:00
25 changed files with 2265 additions and 1676 deletions
-31
View File
@@ -1,31 +0,0 @@
# Authorization Matrix
This inventory is derived from the mounted route source. Both `/api` (legacy) and `/api/v1` mount the same router; new clients use `/api/v1`. `SUPER_ADMIN` bypasses permission checks through the central middleware.
| Route group | Methods/path | Auth | Permission/account constraint | Ownership/rate limit |
|---|---|---|---|---|
| Auth | `/auth/*` | Mixed | Public login/registration; authenticated session actions | Sensitive limiter on credential flows |
| User/profile/address | `/user/*`, `/profile/*`, `/addresses/*`, `/account/overview` | Yes | Self or explicit admin permission | User ID derived from token/self query |
| Upload/document | `/upload/*`, `/document/*` | Yes | Owner or media/document permissions | Signed URL after owner/permission check |
| Permissions/admin users | `/permissions/*`, `/admin/users/*` | Yes | Permission/admin-gated | No public mutations |
| Business | `/business/*`, `/admin/business/*` | Yes | Self business or business permissions | Business context resolved from user |
| Catalogue/help | `/products`, `/categories`, `/brands`, `/collections`, `/help/*` | Public GET | Published/active projection | General limiter; help search sensitive limiter |
| Catalogue admin | `/admin/products/*`, categories/brands/collections/reviews | Yes | Catalogue permissions | Strict schemas/action endpoints |
| Inventory/pricing/merchandising/shipping admin | `/admin/inventory/*`, `/admin/pricing/*`, `/admin/promotions/*`, `/admin/shipping/*` | Yes | Domain permissions | Strict schemas; bounded pages |
| Shopping | `/cart/*`, `/wishlist/*`, `/checkout/*` | Yes | Authenticated self | Identity derived from token; idempotency/reservations |
| Orders/payments/returns | `/orders/*`, `/returns/*` | Yes | Self ownership | Order/user join checks; strict payment actions |
| Commerce admin | `/admin/orders/*`, payments/refunds/returns | Yes | Orders/payments/returns permissions | Explicit actions; no generic status patch |
| Logistics public | `/tracking/*` | Public token/reference | Limited safe projection | Sensitive limiter where configured |
| Logistics rider/admin | `/rider/*`, `/admin/shipments/*` | Yes | Rider ownership or logistics permissions | Assignment/state checks |
| Loyalty/wholesale | `/loyalty/*`, `/wholesale/*`, corresponding `/admin/*` | Yes | Self or loyalty/wholesale permissions | Ledger identity server-derived |
| Support customer | `/support/tickets/*` | Yes | Self-owned ticket | Creation/replies rate-limited; internal visibility excluded |
| Support admin | `/admin/support/tickets/*` | Yes | Granular support permission per action | Row-lock assignment; staff-only internal notes |
| Newsletter | subscribe/unsubscribe | Public | Token authorizes unsubscribe | Sensitive limiter; enumeration-neutral response |
| Newsletter admin | `/admin/newsletter/subscribers` | Yes | `newsletter.subscribers.read` | Token hashes excluded |
| Recommendations | public reads; authenticated event/recent/for-you | Mixed | Self for behavioral data | Event allowlist/idempotency; sensitive limiter |
| Analytics | `/admin/dashboard/overview`, `/admin/analytics/*` | Yes | Matching `analytics.*.read` | UTC range ≤366 days; bounded SQL aggregates |
| Metrics | `/admin/metrics` | Yes | `system.metrics.read` | No user/resource labels |
| Queue board | `/admin/queues` | Yes | Admin/superadmin account type | Internal operational UI |
| Health | `/health`, `/health/live`, `/health/ready` | Public | None | No sensitive payload |
Detailed route definitions remain authoritative in `app/routes`. Security audit found no newly unprotected admin route. Remaining staging work includes an automated route-to-matrix drift check and full authenticated IDOR E2E execution.
-11
View File
@@ -1,11 +0,0 @@
# Backup and Restore
Use encrypted, access-controlled backups with retention tiers and regular restore drills. Do not place credentials on command lines; use a protected client option file or secret injection.
1. Quiesce high-risk writes or capture a transactionally consistent MySQL backup (`mysqldump --single-transaction --routines --triggers`) and record schema migration state.
2. Enable S3 versioning, lifecycle rules, encryption and cross-account/region recovery appropriate to policy. Database-only recovery does not restore uploads/documents.
3. Treat Redis as ephemeral/reconstructable for queues/cache but security-sensitive for OTP/session challenges. Redis loss may invalidate challenges and lose queued work; MySQL remains business truth.
4. Restore MySQL into a temporary isolated database, run consistency queries, compare row counts/totals, verify migrations, and test application reads before promotion.
5. Recover in order: MySQL, object storage, Redis, API, workers, single cron scheduler, reverse proxy, then smoke/E2E checks.
Rollback after live commerce writes is restore/forward-fix based; do not assume destructive migration `down` functions are safe. Define RPO/RTO, backup owners, retention, restore approval and audit evidence before go-live.
-11
View File
@@ -1,11 +0,0 @@
# Cron Operations
| Job | Schedule | Authority / boundedness | Idempotency |
|---|---|---|---|
| Notification cleanup | configured source schedule | DB records; retention bounded | repeated delete safe |
| Inventory reservation expiry | frequent | batch-limited DB scan | reservation state/event key |
| Checkout expiry | frequent | batch-limited DB scan | checkout/reservation state |
| Loyalty reconciliation | scheduled | configured batch | ledger event IDs |
| Support SLA + recommendation retention | every 5 minutes | 100 SLA / 1,000 event max | deterministic escalation key / old-event deletion |
Cron starts only when the deployment enables `RUN_CRON`; use one dedicated scheduler replica. Current jobs use local `noOverlap` where available but do not all have a distributed lock, so multi-instance cron is prohibited until staging validates/implements Redis locking. All times are server/UTC clock time unless a domain snapshot says otherwise.
-21
View File
@@ -444,24 +444,3 @@ Date: 2026-09-09
- Phase 11 readiness: safe to begin hardening after the Phase 10 migration and infrastructure checks are scheduled; Phase 10 is not a production-readiness claim. - Phase 11 readiness: safe to begin hardening after the Phase 10 migration and infrastructure checks are scheduled; Phase 10 is not a production-readiness claim.
Module 16 AI Customer Support Chatbot is intentionally excluded from this backend. It will be developed as a separate service. Module 16 AI Customer Support Chatbot is intentionally excluded from this backend. It will be developed as a separate service.
## Phase 11 Completion Update
Date: 2026-09-15
Overall backend code completion is estimated at **90%**; production readiness is **64%**. Module 16 is excluded from both calculations.
- 01 Authentication/Authorization 92%; 02 Customer/Profile/Address 90%; 03 Catalogue 88%; 04 Localization 86%; 05 Inventory 88%; 06 Cart/Wishlist 92%; 07 Merchandising 84%; 08 Checkout 90%.
- 09 Orders 91%; 10 Payments 82%; 11 Delivery/Rider 88%; 12 Loyalty 86%; 13 Wholesale 88%; 14 Notifications 74%; 15 Support 84%; 17 Recommendations 79%.
- 18 Admin Dashboard/Analytics 82%; 19 File/Media 84%; 20 Audit/System Configuration 74%; 21 Jobs/Queues 80%; 22 Security 82%; 23 Testing/QA 76%; 24 API Documentation 82%; 25 Deployment/Infrastructure 78%.
- Added permission-separated dashboard/sales/order/customer/product/inventory/payment/delivery/loyalty/support/recommendation analytics, bounded UTC date ranges, self-only account overview, protected low-cardinality metrics, OpenAPI 3.1 baseline, smoke test, and operational/recovery documentation.
- Final automated verification: **29 suites / 150 tests passing**, **383 JavaScript files** syntax checked, OpenAPI JSON valid, and `npm ls --depth=0` clean.
- Security upgrades reduced `npm audit` from 30 findings (20 high/9 moderate/1 low) to **19 findings (13 high/5 moderate/1 low)**. Remaining Puppeteer/transitive and major-version remediations require controlled upgrade/risk review.
- Security findings fixed: vulnerable current-major versions of AWS SDK, Axios, BullMQ, Morgan, Multer, MySQL2, Sequelize and related packages were upgraded; analytics/metrics permissions and bounded query inputs were added.
- Security findings remaining: full live IDOR/CSRF/proxy verification, antivirus defense, dependency majors/transitives, full route-matrix drift automation, and provider/infrastructure threat validation.
- Migrations: Phase 0–10 migrations remain unchanged; no Phase 11 migration was necessary and **no migration was executed**.
- Real infrastructure: MySQL migration/concurrency, Redis, BullMQ, multi-instance cron, S3, SMTP, Stripe, PayHere, Nginx/TLS, backup restore and load behavior are **IMPLEMENTED_UNVERIFIED**.
- E2E: mocked/unit regression is VERIFIED; real retail, failure, cancellation, delivery, return/refund, loyalty, wholesale, support, authorization and concurrency flows are UNVERIFIED.
- Deployment decision: **STAGING-READY**, not production-candidate. Complete the production checklist and generate real staging evidence before go-live.
Module 16 AI Customer Support Chatbot remains intentionally excluded. No AI/LLM, prompt, RAG, embedding, vector database, generated reply, or agent implementation was added.
@@ -1,15 +0,0 @@
# Notification Event Matrix
| Domain | Event | In-App | Email | Push | Mandatory | Recipient | Implemented | Tested |
|---|---|---:|---:|---:|---:|---|---|---|
| Identity | Email verification/password reset/security | Yes | Yes | No | Yes | User | Yes | Unit/mocked |
| Business | Application received/approved/rejected/status | Yes | Yes | No | Mixed | Applicant | Yes | Mocked |
| Orders | ORDER_PLACED / ORDER_CANCELLED | Partial | No | No | Yes | Customer | Partial | Audit tests only |
| Payments | PAYMENT_CONFIRMED / PAYMENT_FAILED | Partial | No | No | Yes | Customer | Partial | Provider unit tests |
| Refunds/returns | completion/status | No | No | No | Yes | Customer | No | No |
| Shipment | dispatched/delivered/failed | No | No | No | Yes | Customer | No | No |
| Loyalty | reward redeemed | No | No | No | Optional | Customer | No | Policy tests |
| Support | created/replied/resolved | No | No | No | Mixed | Customer/agent | No | State tests |
| Wholesale | credit/settlement due | No | No | No | Mixed | Business contact | No | Ledger tests |
Push is **NOT_IMPLEMENTED** because no device-token/FCM infrastructure exists. Phase 2 provides persisted in-app/email delivery primitives, preferences, BullMQ retry and failure storage. Domain fanout above remains staging work; it was not faked in Phase 11.
@@ -1,57 +0,0 @@
# ZUMRI Phase 11 Analytics and Production Readiness
## Objective and Baseline
The final main backend phase adds bounded operational analytics, account overview, security/dependency hardening, observability, API/deployment operations, and an evidence-based readiness assessment. Baseline: Node 22.12.0, npm 10.9.0, 28 suites/145 tests, 373 syntax files, and 30 audit advisories before upgrades.
## Architecture Reviewed
Current source comprises identity/RBAC, customer/business, catalogue/localization, inventory/pricing/merchandising, shopping/checkout, commerce, logistics, loyalty/wholesale, support/help/newsletter/recommendations, shared uploads/notifications/audit/documents, four queues/workers, five cron families, eleven forward-only phase migrations, Docker/Compose and Nginx samples. Mounted routes retain legacy `/api` and preferred `/api/v1` prefixes.
## Admin Analytics and Account Overview
Permission-separated endpoints cover dashboard, sales, orders, customers, products, inventory, payments, delivery, loyalty, support, and recommendation signals. All use SQL aggregation and bounded results; date-based reports default to 30 days UTC and reject ranges over 366 days. Revenue means paid/captured payment amount; refunds mean completed refunds; net is their difference. Profit/valuation and recommendation conversion are explicitly unavailable. `/account/overview` is self-only and combines recent orders, wishlist, default address, loyalty, vouchers, and open support count. Existing wholesale dashboard remains authoritative.
## Notification Completion
The matrix documents actual Phase 2 delivery and domain gaps. Push is not implemented. High-value commerce/logistics/support fanout remains implemented-unverified or absent and is not falsely claimed.
## Security, Authorization, IDOR and Validation Audit
Central authentication checks issuer/audience-signed JWT claims, active user/session, rotation/token version and permissions. Admin analytics/metrics are permission gated. Customer domains derive identity or constrain queries by owner. Phase 10 support attachments, messages, internal visibility and resource links have independent authorization. Strict Zod schemas protect new mutations. Existing route source remains authoritative; full live authenticated IDOR and concurrency execution is outstanding.
Cookies plus Bearer tokens serve browser/mobile clients. CORS is restricted to `FRONTEND_URL` with credentials; cookie flags remain part of the Phase 1 implementation. Helmet defaults are retained except CSP (disabled for compatibility) and non-production HSTS. Production must validate same-site/origin behavior behind the exact proxy topology. Logs use request IDs and avoid request bodies; error middleware suppresses production stack/internal details. No secret values were copied into this report. `.env` is ignored; rotate any credential ever committed outside the reviewed history.
## Authentication, Payment and File Security
Shared password policy/session invalidation and provider verification/idempotency remain covered by prior tests. Stripe/PayHere cryptographic and refund behavior is IMPLEMENTED_UNVERIFIED without sandbox credentials. Uploads enforce size, allowed type, content sniffing/checksum, ownership, private storage and signed expiry; antivirus is not implemented and is a recommended defense-in-depth control.
## Dependency Audit
Current-major upgrades were applied for AWS SDK, Bull Board, Axios, BullMQ, dotenv, ioredis, Jest, Morgan, Multer, MySQL2, node-cron, Nodemailer 8, pdfmake, Sequelize and Zod. The audit fell from 30 to 19 advisories. Remaining findings are primarily transitive and include Puppeteer/extract-zip plus packages requiring major/breaking remediation; they must be triaged or accepted before production.
## E2E and Concurrency Testing
Mocked/unit regression verifies domain state, authorization/validation policies and deterministic calculations. Real retail, failed-payment, cancellation, delivery, return/refund, loyalty/referral, business, support, authorization, InnoDB contention and duplicate webhook/claim flows remain UNVERIFIED. Use isolated staging identities and provider sandboxes; never run destructive commerce smoke against production.
## Migration and Infrastructure Validation
Phase 0–10 migrations were left unchanged. No migration was executed. Fresh and restored-upgrade MySQL procedures, FK/index/query-plan validation, Redis, BullMQ, single-scheduler cron, S3, SMTP, Stripe and PayHere are IMPLEMENTED_UNVERIFIED pending credentials/services. Forward correction or verified restore is the rollback model after live writes.
## Queue/Cron Operations and Observability
Operational inventories are in `QUEUE_OPERATIONS.md` and `CRON_OPERATIONS.md`. `/health/live` tests process life; `/health/ready` tests required MySQL/Redis. Protected `/api/v1/admin/metrics` exports process memory, uptime, HTTP counts and duration sums using only method/status-class labels. Recommended alerts cover readiness, 5xx, DB/Redis, queues, webhooks, cron, latency and resource pressure.
## OpenAPI, Docker, Nginx and CI/CD
`openapi.json` provides a valid OpenAPI 3.1 security/error and critical-route baseline, not a claim of complete route coverage. Docker uses Node 22, Chromium, production installs, non-root execution and liveness healthcheck. Compose separates API/worker and health-gates MySQL/Redis; production secrets must not use `.env` files. Nginx forwards standard proxy headers and bounds upload/timeouts; production TLS/trusted-proxy validation is required. Existing Gitea CI plus syntax/tests/dependency/OpenAPI validation should gate releases.
## Backup/Restore and Production Runbook
See `BACKUP_RESTORE.md`, `PRODUCTION_RUNBOOK.md`, and `PRODUCTION_CHECKLIST.md`. Database and object storage must be recoverable together; Redis loss semantics, RPO/RTO, restore drills and rollback authority require approval.
## Remaining Unverified Items and Assessment
Code implementation is approximately 91%; security 82%; automated tests 76%; real integration validation 25%; observability 72%; deployment 78%; documentation 88%; backup/recovery 68%. Overall code completion is **90%**; production readiness is **64%**. Decision: **STAGING-READY**, not production-candidate, because migrations, real dependencies, provider sandboxes, concurrency/E2E, notification fanout, remaining dependency advisories, and restore drills lack evidence.
Module 16 AI Customer Support Chatbot is excluded and was not implemented. No LLM, prompt, RAG, embedding, vector database, generated reply, or AI agent code was added.
-23
View File
@@ -1,23 +0,0 @@
# Production Checklist
- [ ] Infrastructure sized, isolated, patched, TLS-enabled
- [ ] Secrets generated, injected, access-limited, rotation tested
- [ ] MySQL fresh/upgrade migrations verified on restored data
- [ ] Backup and restore drill meets approved RPO/RTO
- [ ] Redis auth/persistence/loss behavior validated
- [ ] Private S3 upload, signed download, expiry, delete, versioning tested
- [ ] SMTP security and non-customer test recipients verified
- [ ] Stripe and PayHere sandbox flows/webhooks/refunds validated
- [ ] Roles and all Phase 1–11 permissions seeded/reviewed
- [ ] Authorization matrix and IDOR E2E suite approved
- [ ] Dependency/image scan risk accepted; remaining majors planned
- [ ] API, workers, and exactly one cron scheduler deployed
- [ ] Queue retry/backlog/failure alerts tested
- [ ] Health and protected metrics scraped/alerted
- [ ] Nginx proxy trust, size, timeout, webhook, HTTPS behavior tested
- [ ] Frontend/mobile uses `/api/v1` and handles standard errors
- [ ] Full retail/business/payment/delivery/return/loyalty/support E2E passes
- [ ] Concurrency/idempotency tests pass on real InnoDB/Redis
- [ ] Non-destructive production smoke passes
- [ ] Go-live owner, rollback authority and incident contacts assigned
- [ ] Post-go-live reconciliation and monitoring window scheduled
-18
View File
@@ -1,18 +0,0 @@
# Production Runbook
## Deployment
Provision MySQL 8.4, authenticated Redis, private encrypted S3-compatible storage, SMTP, payment-provider credentials, TLS reverse proxy, API replicas, independent worker replicas, and exactly one cron scheduler. Inject secrets; never bake them into images.
1. Validate `.env` with `node server.js` in a sealed staging environment.
2. Take database/object-storage backups and run legacy prechecks.
3. Run `npx sequelize-cli db:migrate` against staging first; verify `SequelizeMeta`, constraints, indexes and counts.
4. Seed required roles/permissions, document types, shipping/configuration, SLA/help data using approved idempotent procedures.
5. Build the Node 22 image, scan it, deploy workers, API, and one `RUN_CRON=true` scheduler.
6. Configure Nginx/TLS/proxy trust; verify `/health/live` and `/health/ready`.
7. Run `npm run smoke`; run the staging-only commerce sequence with designated test identities/provider sandbox.
8. Monitor 5xx rate, readiness, DB/Redis, queue failures/backlog, webhook failures, cron failures, latency, memory and disk/log pressure.
Commands: API `npm start`; worker `node app/workers/index.js`; syntax `npm run check:syntax`; tests `npm test -- --runInBand`; dependencies `npm ls --depth=0`; audit `npm audit`; smoke `npm run smoke`; OpenAPI `npm run validate:openapi`.
Incident basics: stop hazardous writers, preserve logs/request IDs/provider event IDs, assess customer impact, rotate exposed credentials, prefer forward fixes, reconcile payments/inventory/ledgers, and communicate from verified database/provider truth. Roll back application images only when schema compatibility is proven; restore data only through the approved recovery procedure.
-10
View File
@@ -1,10 +0,0 @@
# Queue Operations
| Queue | Producer | Consumer | Idempotency/failure |
|---|---|---|---|
| activity | audit service | activity worker | event-based job ID; database activity record |
| email | email service | email worker | event job ID; retry/backoff; delivery status |
| document | document service | document worker | persisted document state; bounded concurrency |
| log | structured log utility | log worker | redacted payload; retained operationally |
Workers run independently with `node app/workers/index.js`; the API does not consume jobs. Validate Redis authentication, queue prefixes, retries, retention, dead/final failures, and Bull Board access in staging. Alert on failed jobs and sustained backlog. Redis/BullMQ were **IMPLEMENTED_UNVERIFIED** in this environment.
-1
View File
@@ -1 +0,0 @@
{"openapi":"3.1.0","info":{"title":"ZUMRI Backend API","version":"1.0.0","description":"Phase 11 maintained baseline. Module 16 AI support is intentionally excluded."},"servers":[{"url":"/api/v1"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"},"cookieAuth":{"type":"apiKey","in":"cookie","name":"access_token"}},"schemas":{"Error":{"type":"object","required":["success","error"],"properties":{"success":{"const":false},"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"requestId":{"type":"string"}}}}}}},"paths":{"/products":{"get":{"summary":"List public products","responses":{"200":{"description":"Product list"}}}},"/help/categories":{"get":{"summary":"List published help categories","responses":{"200":{"description":"Help categories"}}}},"/auth/me":{"get":{"security":[{"bearerAuth":[]},{"cookieAuth":[]}],"responses":{"200":{"description":"Current identity"},"401":{"description":"Unauthenticated"}}}},"/account/overview":{"get":{"security":[{"bearerAuth":[]},{"cookieAuth":[]}],"responses":{"200":{"description":"Self-owned account overview"}}}},"/support/tickets":{"get":{"security":[{"bearerAuth":[]},{"cookieAuth":[]}],"responses":{"200":{"description":"Self-owned tickets"}}},"post":{"security":[{"bearerAuth":[]},{"cookieAuth":[]}],"responses":{"201":{"description":"Ticket created"}}}},"/admin/dashboard/overview":{"get":{"security":[{"bearerAuth":[]},{"cookieAuth":[]}],"responses":{"200":{"description":"Requires analytics.dashboard.read"},"403":{"description":"Permission denied"}}}},"/admin/analytics/sales":{"get":{"security":[{"bearerAuth":[]},{"cookieAuth":[]}],"parameters":[{"name":"from","in":"query","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"Requires analytics.sales.read"}}}},"/recommendations/trending":{"get":{"responses":{"200":{"description":"Deterministic trending products"}}}},"/admin/metrics":{"get":{"security":[{"bearerAuth":[]},{"cookieAuth":[]}],"responses":{"200":{"description":"Prometheus text; requires system.metrics.read"}}}}}}
-2
View File
@@ -18,7 +18,6 @@ const { authorizedAccountType } = require("./app/middleware/permission.middlewar
const requestId = require("./app/middleware/requestId.middleware"); const requestId = require("./app/middleware/requestId.middleware");
const { generalApiLimiter } = require("./app/middleware/rateLimit.middleware"); const { generalApiLimiter } = require("./app/middleware/rateLimit.middleware");
const { notFound, errorHandler } = require("./app/middleware/error.middleware"); const { notFound, errorHandler } = require("./app/middleware/error.middleware");
const metrics = require("./app/services/metrics.service");
const app = express(); const app = express();
const trustProxy = Number(process.env.TRUST_PROXY || 0); const trustProxy = Number(process.env.TRUST_PROXY || 0);
@@ -26,7 +25,6 @@ if (trustProxy > 0) app.set("trust proxy", trustProxy);
app.disable("x-powered-by"); app.disable("x-powered-by");
app.use(requestId); app.use(requestId);
app.use(metrics.middleware);
app.use(helmet({ app.use(helmet({
contentSecurityPolicy: false, contentSecurityPolicy: false,
hsts: process.env.NODE_ENV === "production" ? undefined : false, hsts: process.env.NODE_ENV === "production" ? undefined : false,
-2
View File
@@ -29,6 +29,4 @@ module.exports = {
SHIPMENTS_READ:"shipments.read",SHIPMENTS_CREATE:"shipments.create",SHIPMENTS_MANAGE:"shipments.manage",SHIPMENTS_ASSIGN:"shipments.assign",RIDERS_READ:"riders.read",RIDERS_MANAGE:"riders.manage",DISPATCH_READ:"dispatch.read",DISPATCH_MANAGE:"dispatch.manage",DELIVERY_PROOF_READ:"delivery.proof.read",RETURNS_LOGISTICS_READ:"returns.logistics.read",RETURNS_LOGISTICS_MANAGE:"returns.logistics.manage", SHIPMENTS_READ:"shipments.read",SHIPMENTS_CREATE:"shipments.create",SHIPMENTS_MANAGE:"shipments.manage",SHIPMENTS_ASSIGN:"shipments.assign",RIDERS_READ:"riders.read",RIDERS_MANAGE:"riders.manage",DISPATCH_READ:"dispatch.read",DISPATCH_MANAGE:"dispatch.manage",DELIVERY_PROOF_READ:"delivery.proof.read",RETURNS_LOGISTICS_READ:"returns.logistics.read",RETURNS_LOGISTICS_MANAGE:"returns.logistics.manage",
LOYALTY_ACCOUNTS_READ:"loyalty.accounts.read",LOYALTY_POINTS_ADJUST:"loyalty.points.adjust",LOYALTY_MANAGE:"loyalty.manage",LOYALTY_REFERRALS_READ:"loyalty.referrals.read",WHOLESALE_CREDIT_READ:"wholesale.credit.read",WHOLESALE_CREDIT_MANAGE:"wholesale.credit.manage",WHOLESALE_SETTLEMENTS_READ:"wholesale.settlements.read",WHOLESALE_SETTLEMENTS_MANAGE:"wholesale.settlements.manage",WHOLESALE_ANALYTICS_READ:"wholesale.analytics.read", LOYALTY_ACCOUNTS_READ:"loyalty.accounts.read",LOYALTY_POINTS_ADJUST:"loyalty.points.adjust",LOYALTY_MANAGE:"loyalty.manage",LOYALTY_REFERRALS_READ:"loyalty.referrals.read",WHOLESALE_CREDIT_READ:"wholesale.credit.read",WHOLESALE_CREDIT_MANAGE:"wholesale.credit.manage",WHOLESALE_SETTLEMENTS_READ:"wholesale.settlements.read",WHOLESALE_SETTLEMENTS_MANAGE:"wholesale.settlements.manage",WHOLESALE_ANALYTICS_READ:"wholesale.analytics.read",
SUPPORT_TICKETS_READ:"support.tickets.read",SUPPORT_TICKETS_ASSIGN:"support.tickets.assign",SUPPORT_TICKETS_REPLY:"support.tickets.reply",SUPPORT_TICKETS_STATUS:"support.tickets.status",SUPPORT_TICKETS_PRIORITY:"support.tickets.priority",SUPPORT_TICKETS_INTERNAL_NOTES:"support.tickets.internal_notes",SUPPORT_TICKETS_ESCALATE:"support.tickets.escalate",SUPPORT_CATEGORIES_MANAGE:"support.categories.manage",SUPPORT_SLA_MANAGE:"support.sla.manage",HELP_READ:"help.read",HELP_MANAGE:"help.manage",HELP_PUBLISH:"help.publish",NEWSLETTER_SUBSCRIBERS_READ:"newsletter.subscribers.read",NEWSLETTER_SUBSCRIBERS_EXPORT:"newsletter.subscribers.export",NEWSLETTER_SUBSCRIBERS_MANAGE:"newsletter.subscribers.manage",RECOMMENDATIONS_READ:"recommendations.read",RECOMMENDATIONS_MANAGE:"recommendations.manage", SUPPORT_TICKETS_READ:"support.tickets.read",SUPPORT_TICKETS_ASSIGN:"support.tickets.assign",SUPPORT_TICKETS_REPLY:"support.tickets.reply",SUPPORT_TICKETS_STATUS:"support.tickets.status",SUPPORT_TICKETS_PRIORITY:"support.tickets.priority",SUPPORT_TICKETS_INTERNAL_NOTES:"support.tickets.internal_notes",SUPPORT_TICKETS_ESCALATE:"support.tickets.escalate",SUPPORT_CATEGORIES_MANAGE:"support.categories.manage",SUPPORT_SLA_MANAGE:"support.sla.manage",HELP_READ:"help.read",HELP_MANAGE:"help.manage",HELP_PUBLISH:"help.publish",NEWSLETTER_SUBSCRIBERS_READ:"newsletter.subscribers.read",NEWSLETTER_SUBSCRIBERS_EXPORT:"newsletter.subscribers.export",NEWSLETTER_SUBSCRIBERS_MANAGE:"newsletter.subscribers.manage",RECOMMENDATIONS_READ:"recommendations.read",RECOMMENDATIONS_MANAGE:"recommendations.manage",
ANALYTICS_DASHBOARD_READ:"analytics.dashboard.read",ANALYTICS_SALES_READ:"analytics.sales.read",ANALYTICS_ORDERS_READ:"analytics.orders.read",ANALYTICS_CUSTOMERS_READ:"analytics.customers.read",ANALYTICS_PRODUCTS_READ:"analytics.products.read",ANALYTICS_INVENTORY_READ:"analytics.inventory.read",ANALYTICS_PAYMENTS_READ:"analytics.payments.read",ANALYTICS_DELIVERY_READ:"analytics.delivery.read",ANALYTICS_LOYALTY_READ:"analytics.loyalty.read",ANALYTICS_WHOLESALE_READ:"analytics.wholesale.read",ANALYTICS_SUPPORT_READ:"analytics.support.read",ANALYTICS_RECOMMENDATIONS_READ:"analytics.recommendations.read",
SYSTEM_METRICS_READ:"system.metrics.read",
}; };
@@ -1 +0,0 @@
const db=require("../models");exports.overview=async(req,res,next)=>{try{const userId=req.user.id,[recentOrders,orderCount,wishlistCount,defaultAddress,loyalty,openTickets,vouchers]=await Promise.all([db.Order.findAll({where:{user_id:userId},attributes:["id","order_number","status","payment_status","grand_total","currency","placed_at"],order:[["placed_at","DESC"]],limit:5}),db.Order.count({where:{user_id:userId}}),db.WishlistItem.count({where:{user_id:userId}}),db.Address.findOne({where:{user_id:userId,is_default:true},attributes:{exclude:["user_id"]}}),db.LoyaltyAccount.findOne({where:{user_id:userId},attributes:["available_points","current_tier_id","lifetime_points_earned"]}),db.SupportTicket.count({where:{customer_user_id:userId,status:["OPEN","ASSIGNED","WAITING_FOR_CUSTOMER","WAITING_FOR_SUPPORT"]}}),db.CustomerCouponEntitlement.count({where:{user_id:userId,status:"ACTIVE"}})]);res.json({success:true,data:{recentOrders,orderCount,wishlistCount,defaultAddress,loyalty:loyalty&&{availablePoints:loyalty.available_points,lifetimePointsEarned:loyalty.lifetime_points_earned,tierId:loyalty.current_tier_id},availableVoucherCount:vouchers,openSupportTicketCount:openTickets}});}catch(e){next(e);}};
-1
View File
@@ -1 +0,0 @@
const a=require("../services/analytics/analytics.service");const run=fn=>async(req,res,next)=>{try{res.json({success:true,data:await fn(req.query)});}catch(e){next(e);}};exports.overview=run(a.overview);exports.sales=run(a.sales);exports.orders=run(a.orders);exports.customers=run(a.customers);exports.products=run(a.products);exports.inventory=run(a.inventory);exports.payments=run(a.payments);exports.delivery=run(a.delivery);exports.loyalty=run(a.loyalty);exports.support=run(a.support);exports.recommendations=run(a.recommendations);
-1
View File
@@ -1 +0,0 @@
const metrics=require("../services/metrics.service");exports.read=(_req,res)=>res.type("text/plain; version=0.0.4").send(metrics.render());
-1
View File
@@ -1 +0,0 @@
const r=require("express").Router(),c=require("../controllers/accountOverview.controller"),{authenticate}=require("../middleware/auth.middleware");r.get("/account/overview",authenticate,c.overview);module.exports=r;
-1
View File
@@ -1 +0,0 @@
const r=require("express").Router(),c=require("../controllers/analytics.controller"),{authenticate}=require("../middleware/auth.middleware"),{checkPermission}=require("../middleware/permission.middleware");r.use(authenticate);const p=(name,handler)=>r.get(name==="dashboard"?"/dashboard/overview":`/analytics/${name}`,checkPermission(`analytics.${name}.read`,{custom:true}),handler);p("dashboard",c.overview);p("sales",c.sales);p("orders",c.orders);p("customers",c.customers);p("products",c.products);p("inventory",c.inventory);p("payments",c.payments);p("delivery",c.delivery);p("loyalty",c.loyalty);p("support",c.support);p("recommendations",c.recommendations);module.exports=r;
-6
View File
@@ -49,9 +49,6 @@ const helpRoutes = require("./help.routes");
const helpAdminRoutes = require("./helpAdmin.routes"); const helpAdminRoutes = require("./helpAdmin.routes");
const newsletterRoutes = require("./newsletter.routes"); const newsletterRoutes = require("./newsletter.routes");
const recommendationRoutes = require("./recommendation.routes"); const recommendationRoutes = require("./recommendation.routes");
const analyticsRoutes = require("./analytics.routes");
const accountOverviewRoutes = require("./accountOverview.routes");
const metricsRoutes = require("./metrics.routes");
const router = express.Router(); const router = express.Router();
@@ -93,8 +90,5 @@ router.use("/", helpRoutes);
router.use("/admin", helpAdminRoutes); router.use("/admin", helpAdminRoutes);
router.use("/", newsletterRoutes); router.use("/", newsletterRoutes);
router.use("/", recommendationRoutes); router.use("/", recommendationRoutes);
router.use("/admin", analyticsRoutes);
router.use("/", accountOverviewRoutes);
router.use("/admin", metricsRoutes);
module.exports = router; module.exports = router;
-1
View File
@@ -1 +0,0 @@
const r=require("express").Router(),c=require("../controllers/metrics.controller"),{authenticate}=require("../middleware/auth.middleware"),{checkPermission}=require("../middleware/permission.middleware");r.get("/metrics",authenticate,checkPermission("system.metrics.read",{custom:true}),c.read);module.exports=r;
@@ -1,12 +0,0 @@
const{Op,fn,col,literal}=require("sequelize"),db=require("../../models");const PAID=["PAID","PARTIALLY_REFUNDED","REFUNDED"],range=(query={},maxDays=366)=>{const to=query.to?new Date(`${query.to}T23:59:59.999Z`):new Date(),from=query.from?new Date(`${query.from}T00:00:00.000Z`):new Date(to.getTime()-29*86400000);if(Number.isNaN(+from)||Number.isNaN(+to)||from>to||to-from>maxDays*86400000)throw Object.assign(new Error(`Date range must be valid and at most ${maxDays} days`),{status:400,code:"INVALID_DATE_RANGE"});return{from,to,where:{[Op.between]:[from,to]}};},sum=x=>String(x||"0.00"),countBy=async(Model,column,where)=>Model.findAll({where,attributes:[column,[fn("COUNT",col(column)),"count"]],group:[column],raw:true});
async function overview(){const[orders,customers,businesses,products,openTickets,deliveries,lowStock]=await Promise.all([db.Order.count({where:{status:{[Op.ne]:"CANCELLED"}}}),db.User.count({where:{accountType:"customer",accountStatus:"ACTIVE"}}),db.BusinessCustomer.count({where:{status:"ACTIVE"}}),db.Product.count({where:{status:"ACTIVE",visibility:"PUBLIC"}}),db.SupportTicket.count({where:{status:{[Op.notIn]:["RESOLVED","CLOSED","CANCELLED"]}}}),db.Shipment.count({where:{status:{[Op.in]:["ASSIGNED","PICKUP_PENDING","PICKED_UP","IN_TRANSIT","OUT_FOR_DELIVERY"]}}}),db.InventoryBalance.count({where:{[Op.and]:[literal("on_hand - reserved > 0"),literal("on_hand - reserved <= low_stock_threshold")]}})]);const paid=await db.Payment.sum("amount",{where:{status:{[Op.in]:PAID}}});return{orders,paidRevenue:sum(paid),activeCustomers:customers,activeBusinessCustomers:businesses,activeProducts:products,lowStockProducts:lowStock,openSupportTickets:openTickets,activeDeliveries:deliveries};}
async function sales(q){const r=range(q),where={status:{[Op.in]:PAID},paid_at:r.where},[gross,refunds,orderCount,units]=await Promise.all([db.Payment.sum("amount",{where}),db.Refund.sum("amount",{where:{status:"COMPLETED",processed_at:r.where}}),db.Payment.count({where}),db.OrderItem.sum("quantity",{where:{createdAt:r.where}})]),g=Number(gross||0),f=Number(refunds||0);return{range:{from:r.from,to:r.to,timezone:"UTC"},grossPaidSales:g.toFixed(2),refunds:f.toFixed(2),netSales:(g-f).toFixed(2),paidPaymentCount:orderCount,averageOrderValue:(orderCount?g/orderCount:0).toFixed(2),unitsSold:Number(units||0),formula:"paid payment amount - completed refund amount"};}
async function orders(q){const r=range(q),where={createdAt:r.where},[byStatus,total,cancelled,returns,refunds]=await Promise.all([countBy(db.Order,"status",where),db.Order.count({where}),db.Order.count({where:{...where,status:"CANCELLED"}}),db.ReturnRequest.count({where:{requested_at:r.where}}),db.Refund.count({where:{requested_at:r.where}})]);return{range:r,byStatus,total,cancelled,cancellationRate:total?cancelled/total:0,returns,returnRate:total?returns/total:0,refunds,refundRate:total?refunds/total:0};}
async function customers(q){const r=range(q),[newCustomers,activeCustomers,retail,business]=await Promise.all([db.User.count({where:{createdAt:r.where,accountType:"customer"}}),db.Order.count({where:{createdAt:r.where,status:{[Op.ne]:"CANCELLED"},user_id:{[Op.ne]:null}},distinct:true,col:"user_id"}),db.User.count({where:{accountType:"customer",accountStatus:"ACTIVE"}}),db.BusinessCustomer.count({where:{status:"ACTIVE"}})]);return{range:r,newCustomers,activeCustomers,activeDefinition:"unique customers with a non-cancelled order in range",activeRetailAccounts:retail,activeBusinessAccounts:business};}
async function products(q){const r=range(q);return{range:r,topSelling:await db.OrderItem.findAll({where:{createdAt:r.where},attributes:["product_id",[fn("SUM",col("quantity")),"units"],[fn("SUM",col("line_total")),"revenue"]],group:["product_id"],order:[[literal("units"),"DESC"]],limit:20,raw:true}),lowStock:await db.InventoryBalance.findAll({where:{[Op.and]:[literal("on_hand - reserved > 0"),literal("on_hand - reserved <= low_stock_threshold")]},attributes:["variant_id","warehouse_id","on_hand","reserved","low_stock_threshold"],limit:100,raw:true})};}
async function inventory(){return{byWarehouse:await db.InventoryBalance.findAll({attributes:["warehouse_id",[fn("SUM",col("on_hand")),"onHand"],[fn("SUM",col("reserved")),"reserved"]],group:["warehouse_id"],limit:100,raw:true}),outOfStock:await db.InventoryBalance.count({where:literal("on_hand - reserved <= 0")}),valuation:null,valuationReason:"Cost of goods is not modeled"};}
async function payments(q){const r=range(q),[byStatus,byProvider,successfulAmount,refundAmount]=await Promise.all([countBy(db.Payment,"status",{createdAt:r.where}),countBy(db.Payment,"provider",{createdAt:r.where}),db.Payment.sum("amount",{where:{paid_at:r.where,status:{[Op.in]:PAID}}}),db.Refund.sum("amount",{where:{processed_at:r.where,status:"COMPLETED"}})]);return{range:r,byStatus,byProvider,successfulAmount:sum(successfulAmount),refundAmount:sum(refundAmount)};}
async function delivery(q){const r=range(q);return{range:r,byStatus:await countBy(db.Shipment,"status",{createdAt:r.where}),riderWorkload:await db.Shipment.findAll({where:{assigned_rider_id:{[Op.ne]:null},status:{[Op.notIn]:["DELIVERED","CANCELLED"]}},attributes:["assigned_rider_id",[fn("COUNT",col("id")),"active"]],group:["assigned_rider_id"],limit:100,raw:true})};}
async function loyalty(q){const r=range(q),[issued,redeemed,expired,accounts,tiers,rewards]=await Promise.all([db.LoyaltyLedgerEntry.sum("points_delta",{where:{occurred_at:r.where,type:"EARN"}}),db.LoyaltyLedgerEntry.sum("points_delta",{where:{occurred_at:r.where,type:"REDEEM"}}),db.LoyaltyLedgerEntry.sum("points_delta",{where:{occurred_at:r.where,type:"EXPIRE"}}),db.LoyaltyAccount.count({where:{status:"ACTIVE"}}),countBy(db.LoyaltyAccount,"current_tier_id",{}),db.LoyaltyRedemption.count({where:{redeemed_at:r.where,status:"COMPLETED"}})]);return{range:r,pointsIssued:Number(issued||0),pointsRedeemed:Math.abs(Number(redeemed||0)),pointsExpired:Math.abs(Number(expired||0)),activeAccounts:accounts,membershipDistribution:tiers,rewardRedemptions:rewards};}
async function support(q){const r=range(q),[byStatus,byPriority,breaches,agents]=await Promise.all([countBy(db.SupportTicket,"status",{createdAt:r.where}),countBy(db.SupportTicket,"priority",{createdAt:r.where}),db.SupportEscalation.count({where:{createdAt:r.where}}),db.SupportTicket.findAll({where:{assigned_agent_id:{[Op.ne]:null},status:{[Op.notIn]:["CLOSED","CANCELLED"]}},attributes:["assigned_agent_id",[fn("COUNT",col("id")),"tickets"]],group:["assigned_agent_id"],limit:100,raw:true})]);return{range:r,byStatus,byPriority,slaBreaches:breaches,agentWorkload:agents};}
async function recommendations(q){const r=range(q);return{range:r,byEventType:await countBy(db.ProductInteractionEvent,"event_type",{occurred_at:r.where}),topViewed:await db.ProductInteractionEvent.findAll({where:{occurred_at:r.where,event_type:"PRODUCT_VIEW"},attributes:["product_id",[fn("COUNT",col("id")),"views"]],group:["product_id"],order:[[literal("views"),"DESC"]],limit:20,raw:true}),conversionAttribution:null};}module.exports={range,overview,sales,orders,customers,products,inventory,payments,delivery,loyalty,support,recommendations};
-1
View File
@@ -1 +0,0 @@
const started=Date.now(),counts=new Map(),durations=new Map();const observe=(method,status,duration)=>{const key=`${method}:${Math.floor(status/100)}xx`;counts.set(key,(counts.get(key)||0)+1);durations.set(key,(durations.get(key)||0)+duration);};const middleware=(req,res,next)=>{const begin=process.hrtime.bigint();res.on("finish",()=>observe(req.method,res.statusCode,Number(process.hrtime.bigint()-begin)/1e9));next();};const render=()=>{const lines=["# HELP zumri_process_uptime_seconds Process uptime","# TYPE zumri_process_uptime_seconds gauge",`zumri_process_uptime_seconds ${Math.floor((Date.now()-started)/1000)}`,"# HELP zumri_http_requests_total HTTP requests by method and status class","# TYPE zumri_http_requests_total counter"];for(const[k,v]of counts){const[method,status_class]=k.split(":");lines.push(`zumri_http_requests_total{method="${method}",status_class="${status_class}"} ${v}`);}lines.push("# HELP zumri_http_request_duration_seconds_sum HTTP request duration","# TYPE zumri_http_request_duration_seconds_sum counter");for(const[k,v]of durations){const[method,status_class]=k.split(":");lines.push(`zumri_http_request_duration_seconds_sum{method="${method}",status_class="${status_class}"} ${v}`);}lines.push(`# TYPE zumri_process_resident_memory_bytes gauge`,`zumri_process_resident_memory_bytes ${process.memoryUsage().rss}`);return `${lines.join("\n")}\n`;};module.exports={observe,middleware,render};
+2247 -1427
View File
File diff suppressed because it is too large Load Diff
+17 -19
View File
@@ -11,8 +11,6 @@
"test:unit": "jest tests/unit --runInBand", "test:unit": "jest tests/unit --runInBand",
"test:integration": "jest tests/integration --runInBand", "test:integration": "jest tests/integration --runInBand",
"check:syntax": "node scripts/check-syntax.js", "check:syntax": "node scripts/check-syntax.js",
"smoke": "node scripts/smoke-test.js",
"validate:openapi": "node -e \"JSON.parse(require('fs').readFileSync('Documentation/openapi.json','utf8')); console.log('OpenAPI JSON valid')\"",
"db:migrate": "sequelize-cli db:migrate", "db:migrate": "sequelize-cli db:migrate",
"db:migrate:status": "sequelize-cli db:migrate:status", "db:migrate:status": "sequelize-cli db:migrate:status",
"db:migrate:undo": "sequelize-cli db:migrate:undo" "db:migrate:undo": "sequelize-cli db:migrate:undo"
@@ -34,38 +32,38 @@
] ]
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.1132.0", "@aws-sdk/client-s3": "^3.1021.0",
"@aws-sdk/s3-request-presigner": "^3.1132.0", "@aws-sdk/s3-request-presigner": "^3.1021.0",
"@bull-board/api": "^6.21.3", "@bull-board/api": "^6.20.6",
"@bull-board/express": "^6.21.3", "@bull-board/express": "^6.20.6",
"axios": "^1.20.0", "axios": "^1.17.0",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"bullmq": "^5.81.5", "bullmq": "^5.71.1",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"cors": "^2.8.6", "cors": "^2.8.6",
"dotenv": "^17.4.2", "dotenv": "^17.2.4",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
"express": "^5.2.1", "express": "^5.2.1",
"express-rate-limit": "^8.7.0", "express-rate-limit": "^8.7.0",
"google-auth-library": "^11.0.2", "google-auth-library": "^11.0.2",
"helmet": "^8.3.0", "helmet": "^8.3.0",
"ioredis": "^5.11.1", "ioredis": "^5.10.1",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"morgan": "^1.12.1", "morgan": "^1.10.1",
"multer": "^2.4.0", "multer": "^2.1.1",
"mysql2": "^3.24.4", "mysql2": "^3.17.0",
"node-cron": "^4.6.0", "node-cron": "^4.5.0",
"nodemailer": "^8.0.11", "nodemailer": "^8.0.1",
"nodeman": "^1.1.2", "nodeman": "^1.1.2",
"pdfmake": "^0.2.23", "pdfmake": "^0.2.7",
"puppeteer": "^24.43.1", "puppeteer": "^24.43.1",
"sequelize": "^6.37.8", "sequelize": "^6.37.7",
"uuid": "^11.1.1", "uuid": "^11.1.1",
"zod": "^4.6.5" "zod": "^4.5.4"
}, },
"devDependencies": { "devDependencies": {
"jest": "^30.5.1", "jest": "^30.4.2",
"nodemon": "^3.1.14", "nodemon": "^3.1.14",
"sequelize-cli": "^6.6.5", "sequelize-cli": "^6.6.5",
"supertest": "^7.2.2" "supertest": "^7.2.2"
-1
View File
@@ -1 +0,0 @@
const base=(process.env.SMOKE_BASE_URL||"http://127.0.0.1:3070").replace(/\/$/,"");const checks=["/health/live","/health/ready","/api/v1/products?limit=1","/api/v1/help/categories","/api/v1/banners/current"];(async()=>{let failed=0;for(const path of checks){try{const r=await fetch(base+path,{signal:AbortSignal.timeout(10000)});const ok=r.ok||(path==="/health/ready"&&r.status===503);console.log(`${ok?"PASS":"FAIL"} ${path} ${r.status}`);if(!ok)failed++;}catch(e){console.log(`FAIL ${path} ${e.message}`);failed++;}}process.exitCode=failed?1:0;})();
@@ -1 +0,0 @@
const analytics=require("../../app/services/analytics/analytics.service"),metrics=require("../../app/services/metrics.service"),permissions=require("../../app/constants/permissions");describe("Phase 11 bounded analytics and observability",()=>{test("defaults analytics to a bounded 30 day UTC range",()=>{const r=analytics.range({},366);expect(r.to-r.from).toBe(29*86400000);});test("rejects reversed dates",()=>expect(()=>analytics.range({from:"2026-02-02",to:"2026-01-01"})).toThrow("Date range"));test("rejects ranges over one year",()=>expect(()=>analytics.range({from:"2024-01-01",to:"2026-01-01"})).toThrow("Date range"));test("metrics use bounded method and status-class labels",()=>{metrics.observe("GET",200,0.01);metrics.observe("POST",503,0.02);const body=metrics.render();expect(body).toContain('method="GET",status_class="2xx"');expect(body).toContain('method="POST",status_class="5xx"');expect(body).not.toMatch(/userId|orderId|email|ticketId|productId/);});test("analytics permissions are separated by domain",()=>expect([permissions.ANALYTICS_DASHBOARD_READ,permissions.ANALYTICS_PAYMENTS_READ,permissions.SYSTEM_METRICS_READ]).toEqual(["analytics.dashboard.read","analytics.payments.read","system.metrics.read"]));});