Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aeb3ab7fcf |
@@ -75,6 +75,3 @@ EMAIL_VERIFICATION_TTL_SECONDS=86400
|
|||||||
PUPPETEER_EXECUTABLE_PATH=
|
PUPPETEER_EXECUTABLE_PATH=
|
||||||
GOOGLE_CLIENT_ID=
|
GOOGLE_CLIENT_ID=
|
||||||
APPLE_CLIENT_ID=
|
APPLE_CLIENT_ID=
|
||||||
# Phase 10 bounded reconciliation and privacy retention
|
|
||||||
SUPPORT_SLA_RECONCILIATION_BATCH_SIZE=100
|
|
||||||
RECOMMENDATION_EVENT_RETENTION_DAYS=90
|
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
# Delivery and Rider API
|
|
||||||
|
|
||||||
All endpoints use `/api/v1`. There is no public tracking endpoint.
|
|
||||||
|
|
||||||
## Admin fulfillment
|
|
||||||
|
|
||||||
- `GET /admin/shipments`, `GET /admin/shipments/:id`
|
|
||||||
- `POST /admin/shipments` creates a delivery from immutable Order address/shipping snapshots and item quantities.
|
|
||||||
- `POST /admin/shipments/return-pickup` creates one pickup for an approved return.
|
|
||||||
- `POST /admin/shipments/:id/assign`, `/unassign`, `/reschedule`, `/cancel`
|
|
||||||
- `GET /admin/dispatch` projects unassigned, active, and attention-needed shipments.
|
|
||||||
|
|
||||||
Administration requires explicit shipment, dispatch, rider, or return-logistics permission. Shipment creation supports partial quantities, locks the order, checks existing non-cancelled allocations, and uses `Idempotency-Key` event identity.
|
|
||||||
|
|
||||||
## Rider administration
|
|
||||||
|
|
||||||
- `GET /admin/riders`, `GET /admin/riders/:id`
|
|
||||||
- `POST /admin/riders` attaches an operational profile to an existing RIDER User; it does not create another identity/password system.
|
|
||||||
- `PATCH /admin/riders/:id`
|
|
||||||
- `GET /admin/riders/:id/assignments`
|
|
||||||
|
|
||||||
Profile status is independent of User account status. Availability is AVAILABLE, BUSY, or OFFLINE and capacity is configurable.
|
|
||||||
|
|
||||||
## Rider workflow
|
|
||||||
|
|
||||||
- `GET /rider/shipments`, `GET /rider/shipments/:id`
|
|
||||||
- Explicit actions: accept, reject, pickup, in-transit, out-for-delivery, deliver, and fail-delivery.
|
|
||||||
- `PUT /rider/location` stores optional latest coordinates; location is not included in customer tracking.
|
|
||||||
|
|
||||||
Every operation derives the rider from authentication and constrains the current assignment. State changes use row locks and unique event IDs. Delivery requires structured proof; linked photo/signature uploads must be AVAILABLE images uploaded by that rider.
|
|
||||||
|
|
||||||
## Customer tracking
|
|
||||||
|
|
||||||
- `GET /orders/:orderId/tracking`
|
|
||||||
- `GET /returns/:id/tracking`
|
|
||||||
|
|
||||||
Responses contain shipment status and ordered event projections, not rider phone, location, capacity, assignment history, payment, or provider data.
|
|
||||||
|
|
||||||
## Behavior boundaries
|
|
||||||
|
|
||||||
Delivered item quantities recalculate Order fulfillment without changing payment status. Failed delivery never refunds or restocks. Return-pickup delivery marks the RMA received for Phase 7 inspection; it does not refund or restock. External couriers, maps, route optimization, delivery OTP, live sockets, and AI dispatch are not implemented.
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# Loyalty and Rewards API
|
|
||||||
|
|
||||||
All customer APIs are authenticated and derive the loyalty owner from the session.
|
|
||||||
|
|
||||||
## Customer
|
|
||||||
|
|
||||||
- `GET /api/v1/loyalty` — balance, debt, lifetime points, current tier, benefits, and next-tier progress.
|
|
||||||
- `GET /api/v1/loyalty/history?page=&limit=` — paginated immutable ledger.
|
|
||||||
- `GET /api/v1/loyalty/tiers`, `GET /api/v1/loyalty/rewards`.
|
|
||||||
- `POST /api/v1/loyalty/redeem` — requires `Idempotency-Key` and reward ID.
|
|
||||||
- `GET /api/v1/loyalty/vouchers` — owner-specific coupon entitlements.
|
|
||||||
- `GET /api/v1/loyalty/referral`, `POST /api/v1/loyalty/referral/claim`.
|
|
||||||
|
|
||||||
## Administration
|
|
||||||
|
|
||||||
- Loyalty accounts and referral listing.
|
|
||||||
- Tier, earn-rule, and reward listing/creation.
|
|
||||||
- `POST /admin/loyalty/adjustments` requires `loyalty.points.adjust`, a non-zero integer delta, reason, and idempotency key.
|
|
||||||
|
|
||||||
## Policy
|
|
||||||
|
|
||||||
Purchase points are awarded only after authoritative PAID processing. Eligible value is discounted merchandise (`subtotal - discountTotal`), excluding shipping, tax, and duties. Money is divided by configured `amountUnit`, floored to whole units, then multiplied by configured points.
|
|
||||||
|
|
||||||
Verified-review rewards require APPROVED and verified purchase. Referral rewards require the referred account's qualifying paid Order. Birthday events use `BIRTHDAY:user:year`; February 29 follows the actual calendar date.
|
|
||||||
|
|
||||||
Ledger entries are never edited. Refund reversals append negative entries. If already-spent points prevent a complete debit, available points floor at zero and the remainder becomes explicit `pointsDebt`; later earnings repay debt first.
|
|
||||||
|
|
||||||
Redemption locks the account/reward, validates limits, spends earliest-expiring allocations first, and creates at most one result per account/idempotency key. Coupon rewards reuse Phase 5 Coupon and issue an owner-specific entitlement.
|
|
||||||
|
|
||||||
Expiry is configured per earn rule and reconciled in bounded daily batches. No direct balance/tier mutation API exists.
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# Newsletter Consent API
|
|
||||||
|
|
||||||
`POST /api/v1/newsletter/subscribe` accepts `email`, `locale`, and an allowlisted source (`HOME_FOOTER`, `CHECKOUT`, or `ACCOUNT`). Email is trimmed, lowercased, and uniquely stored. Repeated subscription is idempotent and enumeration-safe.
|
|
||||||
|
|
||||||
The current product policy uses immediate single opt-in. Each subscription receives a cryptographically random unsubscribe token, while only its SHA-256 hash is stored. `POST /api/v1/newsletter/unsubscribe/:token` always returns a neutral success response. Resubscription records fresh consent and rotates the token.
|
|
||||||
|
|
||||||
Authenticated users may inspect their linked consent at `GET /api/v1/newsletter/me`. Admin listing is `GET /api/v1/admin/newsletter/subscribers` and requires `newsletter.subscribers.read`; token hashes are never returned. Export/manage permissions are reserved, and Phase 10 does not implement campaign sending.
|
|
||||||
|
|
||||||
Newsletter consent is legally distinct from Phase 3 profile marketing preferences. An email preference does not create newsletter consent, and an explicit newsletter unsubscribe takes precedence until a new explicit subscribe action occurs.
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# Orders and Payments API
|
|
||||||
|
|
||||||
All `/api/v1` customer endpoints derive ownership from authentication.
|
|
||||||
|
|
||||||
## Orders
|
|
||||||
|
|
||||||
- `POST /orders/from-checkout` converts an owned READY checkout. Conversion locks the checkout, verifies ACTIVE reservations, copies immutable snapshots, converts the cart, and is idempotent by unique checkout ID.
|
|
||||||
- `GET /orders` and `GET /orders/:id` provide owner-scoped history/detail.
|
|
||||||
- `POST /orders/:id/cancel` releases unpaid reservations. Paid orders require an explicit refund.
|
|
||||||
- Admin: `GET /admin/orders`, `GET /admin/orders/:id`, cancel, and mark-processing action endpoints.
|
|
||||||
|
|
||||||
Legal transitions are centralized. Payment, order, and fulfillment status are separate.
|
|
||||||
|
|
||||||
## Payments and webhooks
|
|
||||||
|
|
||||||
- `POST /orders/:id/payment`, `GET /orders/:id/payment`
|
|
||||||
- `POST /payments/webhooks/payhere` is unauthenticated at the session layer but requires the PayHere merchant hash.
|
|
||||||
|
|
||||||
Online browser redirects are never authoritative. A verified webhook must match local payment reference, currency, and DECIMAL amount. Unique provider event IDs make retries idempotent. A first successful event consumes each reservation once, records coupon redemption, marks payment/order paid, and issues invoice metadata. Failure does not consume inventory.
|
|
||||||
|
|
||||||
PayHere configuration uses `PAYHERE_MERCHANT_ID`, `PAYHERE_MERCHANT_SECRET`, and notify/return/cancel URLs. Stripe has an explicit disabled adapter until its official SDK and webhook secret are configured. No raw card or provider secret is accepted or returned.
|
|
||||||
|
|
||||||
## Invoices
|
|
||||||
|
|
||||||
- `GET /orders/:id/invoice` is owner-scoped.
|
|
||||||
|
|
||||||
Invoice numbers use the locked ReferenceNumber sequence. Invoice metadata is created once per paid order. PDF generation is reserved for the existing document worker integration; no second PDF/storage subsystem was introduced.
|
|
||||||
|
|
||||||
## Refunds
|
|
||||||
|
|
||||||
- `POST /admin/orders/:id/refunds` requires `payments.refund` and `Idempotency-Key`.
|
|
||||||
|
|
||||||
Requested item quantities and captured totals are locked and validated. A refund never restocks inventory automatically. Provider refund execution remains disabled until provider API credentials/workflows are validated.
|
|
||||||
|
|
||||||
## Returns
|
|
||||||
|
|
||||||
- Customer: `POST /orders/:orderId/returns`, `GET /returns`, `GET /returns/:id`.
|
|
||||||
- Admin: list, approve, reject, mark-received, and complete actions.
|
|
||||||
|
|
||||||
The return window uses `RETURN_WINDOW_DAYS` (default 30). Quantity cannot exceed the remaining purchased quantity. Only accepted RESTOCKABLE items are added through the inventory service; damaged/non-restockable items are not. EXCHANGE records intent only.
|
|
||||||
|
|
||||||
Business credit purchasing is intentionally disabled: no locked credit ledger was added without an approved accounting policy. Delivery, loyalty, support AI, and analytics are outside Phase 7.
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# Deterministic Recommendation API
|
|
||||||
|
|
||||||
Phase 10 recommendations are local, explainable rules—not AI or machine learning.
|
|
||||||
|
|
||||||
- `POST /api/v1/recommendations/events` accepts authenticated, idempotent `PRODUCT_VIEW` events only. Clients cannot claim purchases or other authoritative commerce events.
|
|
||||||
- `GET /api/v1/recommendations/recently-viewed` returns a customer's unique recent public products.
|
|
||||||
- `GET /api/v1/products/:productId/recommendations/related` prefers Phase 4 `ProductRelation`, then bounded same-category/brand fallback.
|
|
||||||
- `GET /api/v1/recommendations/trending` ranks a 30-day bounded aggregate with purchase/cart/wishlist/click/view weights.
|
|
||||||
- `GET /api/v1/recommendations/popular` uses eligible paid order item quantities net of refunded quantity, with a featured fallback.
|
|
||||||
- `GET /api/v1/recommendations/for-you` combines the authenticated user's recent product interests with aggregate candidates and falls back to trending/featured products.
|
|
||||||
|
|
||||||
Responses reuse the Phase 4 localized product summary and signed media projection. Only active/public products with an active variant are eligible. Exact stock is not exposed. Limits are capped at 24. Business-specific price quotation remains a known integration item; no customer-specific recommendation response is shared in cache.
|
|
||||||
|
|
||||||
Events store no email, IP, access token, cookie, raw session key, or full user agent. Session keys, if enabled later for anonymous ingestion, are hash-only. Retention defaults to 90 days and cleanup is bounded. The clean service boundary can later be replaced by a separately authenticated recommendation service; Phase 10 adds no URL, credential, bypass, LLM, embedding, vector store, or ML model.
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# Support and Help API
|
|
||||||
|
|
||||||
Phase 10 adds a permission-gated support case system and a localized help center. All routes are under `/api/v1`.
|
|
||||||
|
|
||||||
## Customer support
|
|
||||||
|
|
||||||
- `POST /support/tickets` creates a self-owned ticket and initial public message in one transaction. Identity, business context, priority and status are server-derived.
|
|
||||||
- `GET /support/tickets` and `GET /support/tickets/:id` are self-only; internal notes and internal attachments are excluded.
|
|
||||||
- `POST /support/tickets/:id/messages` appends a public reply. A resolved ticket is explicitly reopened; closed/cancelled tickets reject replies.
|
|
||||||
- `POST /support/tickets/:id/close` performs a validated state transition.
|
|
||||||
- `GET /support/tickets/:id/attachments/:attachmentId` authorizes the ticket and visibility before issuing a short-lived signed S3 URL.
|
|
||||||
|
|
||||||
Ticket states are `OPEN`, `ASSIGNED`, `WAITING_FOR_CUSTOMER`, `WAITING_FOR_SUPPORT`, `RESOLVED`, `CLOSED`, and `CANCELLED`. Customers cannot select priority; new tickets default to `NORMAL`. Subjects are limited to 200 characters, messages to 10,000 characters, and five attachments per message. Attachments reuse Phase 2 uploads and allow JPEG, PNG, WebP, or PDF only.
|
|
||||||
|
|
||||||
Related resources support orders, payments, refunds, returns, shipments, loyalty redemptions, and business settlements. Creation verifies the resource against the authenticated customer or business; a resource identifier alone never grants access.
|
|
||||||
|
|
||||||
## Staff support
|
|
||||||
|
|
||||||
Under `/admin/support/tickets`, staff can list/detail, assign or atomically claim, reply, add internal notes, change priority, resolve, reopen, close, and download attachments. Permissions are granular: `support.tickets.read`, `.assign`, `.reply`, `.status`, `.priority`, `.internal_notes`, and `.escalate`. Category and SLA controls use `support.categories.manage` and `support.sla.manage`.
|
|
||||||
|
|
||||||
State/assignment operations lock the ticket row. Events are append-only. SLA deadlines are snapshotted from the active priority policy at creation using clock time; a bounded five-minute reconciliation creates idempotent first-response or resolution escalations. Business-hour calendars and multi-instance cron validation remain Phase 11 work.
|
|
||||||
|
|
||||||
## Help center
|
|
||||||
|
|
||||||
Public endpoints are `GET /help/categories`, `/help/categories/:slug`, `/help/articles`, `/help/articles/:slug`, and `/help/search?q=`. Only active categories and published articles are returned. `en`, `si`, and `ta` use the Phase 4 locale resolver with English fallback. Search is bounded to 2–100 query characters and at most 50 results.
|
|
||||||
|
|
||||||
Admin endpoints under `/admin/help` list/create categories and articles and explicitly publish/archive articles. They require `help.read`, `help.manage`, or `help.publish`. Article bodies are stored as plain Markdown-like text with HTML tags removed; consumers must render text/Markdown safely and must not treat it as trusted HTML.
|
|
||||||
|
|
||||||
All collection APIs use bounded pagination or bounded results and the standard `{ success, data, pagination? }` envelope.
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# Wholesale Completion API
|
|
||||||
|
|
||||||
Business endpoints require an authenticated, ACTIVE `business_customer`; organization identity is never accepted from the request.
|
|
||||||
|
|
||||||
## Business account
|
|
||||||
|
|
||||||
- `GET /api/v1/business/dashboard`
|
|
||||||
- `GET /api/v1/business/credit`
|
|
||||||
- `POST /api/v1/business/orders/:id/use-credit` with `Idempotency-Key`
|
|
||||||
- `GET /api/v1/business/settlements`, `GET /api/v1/business/settlements/:id`
|
|
||||||
- `GET /api/v1/business/orders/recent`
|
|
||||||
|
|
||||||
The dashboard uses paid Order snapshots for monthly/lifetime volume and discounts, existing BusinessTier for classification, the immutable credit ledger for utilization, and settlement records for next payment information.
|
|
||||||
|
|
||||||
## Administration
|
|
||||||
|
|
||||||
- `POST /admin/wholesale/credit/transactions` (`wholesale.credit.manage`)
|
|
||||||
- `GET /admin/wholesale/businesses/:businessId/credit` (`wholesale.credit.read`)
|
|
||||||
- Settlement list/generation/issue/mark-paid endpoints with settlement permissions.
|
|
||||||
|
|
||||||
Credit formula: `available = creditLimit - ledger balance`. Captures/authorizations increase utilization; payment, release, and refund entries decrease it. Every operation locks the existing BusinessCreditAccount, validates ACTIVE business/account status and currency, and uses a unique event ID. Credit-backed Order placement atomically captures credit, consumes reservations, records an INTERNAL_CREDIT Payment, marks the Order paid, and reuses invoice issuance.
|
|
||||||
|
|
||||||
Settlements snapshot ledger activity for a unique business/period. Due dates come from the existing SettlementTerm. Numbers use ReferenceNumber, and only explicit lifecycle actions are accepted.
|
|
||||||
|
|
||||||
Payment allocation storage exists as a foundation. External bank matching, general ledger, ERP, tax-authority integration, and document-worker validation remain outside this phase.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -417,51 +417,3 @@ Date: 2026-09-03. Inventory/reservation foundations, business pricing, promotion
|
|||||||
## Phase 6 Completion Update
|
## Phase 6 Completion Update
|
||||||
|
|
||||||
Date: 2026-09-03. Module 06 is 91%, Module 08 is 84%, and Module 07 is revised to 86%. Authenticated cart is 92%, wishlist 92%, shipping 86%, checkout 88%, and reservation integration 90%. Nine models, a forward-only migration, owner-scoped shopping APIs, permission-protected shipping administration, server-authoritative totals, atomic inventory reservation composition, idempotent checkout creation, and bounded expiry/cancellation release are implemented. Verification passes 20 suites/95 tests and 258 JavaScript syntax checks. The migration was not executed. Before Phase 7, staging must precheck legacy shopping/shipping tables and address/currency compatibility, seed shipping configuration/permissions, apply migrations, and validate real multi-connection InnoDB concurrency plus multi-instance expiry behavior.
|
Date: 2026-09-03. Module 06 is 91%, Module 08 is 84%, and Module 07 is revised to 86%. Authenticated cart is 92%, wishlist 92%, shipping 86%, checkout 88%, and reservation integration 90%. Nine models, a forward-only migration, owner-scoped shopping APIs, permission-protected shipping administration, server-authoritative totals, atomic inventory reservation composition, idempotent checkout creation, and bounded expiry/cancellation release are implemented. Verification passes 20 suites/95 tests and 258 JavaScript syntax checks. The migration was not executed. Before Phase 7, staging must precheck legacy shopping/shipping tables and address/currency compatibility, seed shipping configuration/permissions, apply migrations, and validate real multi-connection InnoDB concurrency plus multi-instance expiry behavior.
|
||||||
## Phase 7 Completion Update
|
|
||||||
|
|
||||||
Date: 2026-09-09. Module 09 is 86%, Module 10 is 76%, and Module 13 is revised to 82%. Orders are 90%, payments 78%, invoices 72%, refunds 68%, returns 82%, coupon redemption 80%, and verified-purchase reviews 90%. Eleven commerce models, a forward-only migration, transactional checkout conversion, owner/admin APIs, explicit state machines, verified/idempotent webhook processing, payment-time inventory consumption, invoice sequencing, itemized refund validation, RMA handling, and return-only restocking were added. Migration and real provider/MySQL/document-worker validation remain staging requirements. Module 16 AI Customer Support Chatbot is intentionally excluded from this backend and planned as a separate service.
|
|
||||||
## Phase 8 Completion Update
|
|
||||||
|
|
||||||
Date: 2026-09-09. Module 11 is 88%; shipments are 90%, riders 86%, assignment/dispatch 86%, tracking 88%, proof of delivery 82%, and return logistics 84%. Module 09 is revised to 90% through physical fulfillment integration. Six logistics models, a forward-only migration, explicit shipment states/actions, partial fulfillment, locked rider capacity/assignment, append-only events, proof media validation, safe tracking, and approved-return pickup were added. Automated checks pass 25 suites/123 tests with 305 JavaScript files syntax-checked. The migration was not executed. Staging must validate real InnoDB concurrency, multi-instance idempotency, proof storage, notification fan-out, permission seeding, and return handoff before production or Phase 9 rollout. Module 16 AI Customer Support Chatbot remains excluded from this backend and will be developed separately.
|
|
||||||
## Phase 9 Completion Update
|
|
||||||
|
|
||||||
Date: 2026-09-09. Module 12 is 86% and Module 13 is revised to 88%. Loyalty accounts are 92%, points ledger 90%, earning rules 86%, membership 88%, rewards/redemption 86%, referrals 82%, birthday rewards 80%, and expiry 80%. Business tiers are 84%, business credit 84%, settlements 78%, wholesale dashboard 82%, and wholesale analytics 76%. Module 07 is revised to 82% through coupon-entitlement foundations; Modules 09/10 are unchanged except for paid-event loyalty and internal-credit integration. Fourteen models, a forward-only migration, bounded cron reconciliation, new owner/admin APIs, and immutable concurrency-safe ledgers were added. Migration and real MySQL/cron/document/notification validation remain staging requirements. Module 16 AI Customer Support Chatbot remains intentionally excluded and will be developed separately.
|
|
||||||
## Phase 10 Completion Update
|
|
||||||
|
|
||||||
Date: 2026-09-09
|
|
||||||
|
|
||||||
- Module 15 Support Ticket: **84%**
|
|
||||||
- Module 17 Product Recommendations: **78%**
|
|
||||||
- Support tickets 90%; messages 88%; assignment 86%; SLA/escalation 74%; attachments 82%; related-resource integration 78%.
|
|
||||||
- Help center 84%; help localization 86%; help search 76%; newsletter consent 84%.
|
|
||||||
- Recommendation events 80%; related 86%; recently viewed 84%; trending 78%; popular 74%; personalized deterministic 70%.
|
|
||||||
- Module 14 notifications is unchanged: event/template delivery fanout remains outstanding.
|
|
||||||
- Module 03 catalogue relationships are reused without a revised completion claim.
|
|
||||||
- Module 04 localization infrastructure is reused without a revised completion claim.
|
|
||||||
- Verification: **28 suites / 145 tests passing**; syntax passed for **373 JavaScript files**; `npm ls --depth=0` reports no dependency problems.
|
|
||||||
- Migration: `20260909100000-phase-10-support-recommendations.js` created but **not executed**. Phase 0–9 migrations were not changed.
|
|
||||||
- Staging requirements: legacy-data precheck; real MySQL migration/FK/query-plan and row-lock validation; Redis/BullMQ/cron multi-instance validation; S3 signed-download and file-content validation; SMTP notification wiring; business-price projection; authoritative shopping/commerce recommendation events; IDOR/E2E/concurrency tests.
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
## 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,53 +0,0 @@
|
|||||||
# ZUMRI Phase 10 Support, Help Center, Newsletter and Recommendation Foundations
|
|
||||||
|
|
||||||
## Objective
|
|
||||||
|
|
||||||
Provide practical customer support, localized self-service content, explicit newsletter consent, and deterministic recommendations while preserving Phase 0–9 domain ownership.
|
|
||||||
|
|
||||||
## Existing Components Reused
|
|
||||||
|
|
||||||
Phase 1 identity/RBAC; Phase 2 upload, signed S3 access, notification/audit infrastructure; Phase 4 locale, product relations and product DTO; Phase 5 inventory boundary; Phase 7 order/payment truth; Phase 8 shipment truth; Phase 9 loyalty/business ownership; atomic reference numbers and existing cron lifecycle.
|
|
||||||
|
|
||||||
## Support Architecture
|
|
||||||
|
|
||||||
`SupportTicket` owns case lifecycle, `SupportMessage` conversation, `SupportTicketEvent` append-only history, `SupportTicketLink` polymorphic links, and `SupportAttachment` upload references. Ticket creation is transactional. The status graph is explicit; generic status mutation is absent. Agent claim/assignment locks the row. Internal notes and attachments are filtered from customer reads. Resource links validate domain ownership, and signed downloads require ticket authorization.
|
|
||||||
|
|
||||||
## SLA and Escalation
|
|
||||||
|
|
||||||
An active per-priority `SupportSlaPolicy` snapshots first-response and resolution deadlines using `CLOCK_TIME`. The bounded cron detects overdue open work and uses deterministic event keys to create each `SupportEscalation` once. Full calendars, warning tiers, acknowledgement APIs, and verified multi-instance scheduling remain outstanding.
|
|
||||||
|
|
||||||
## Support Notifications, Permissions and Audit
|
|
||||||
|
|
||||||
Phase 2 notification infrastructure is retained as the delivery boundary; full template/fanout wiring is outstanding. New support/help/newsletter/recommendation permissions are explicit. Support lifecycle history is durable in ticket events, and privileged general audit calls are intentionally limited pending real queue integration testing.
|
|
||||||
|
|
||||||
## Help Center Architecture
|
|
||||||
|
|
||||||
Help categories and articles have `en`/`si`/`ta` translation tables and English fallback. Articles implement draft, publish, and archive lifecycle; public APIs query published content only. FAQ is an article type, avoiding a parallel engine. Search uses bounded MySQL `LIKE` queries. HTML tags are removed and bodies are treated as untrusted Markdown-like text.
|
|
||||||
|
|
||||||
## Newsletter Consent
|
|
||||||
|
|
||||||
Newsletter subscriptions are unique by normalized email, source/locale aware, idempotent, independently revocable, and linked to a user when known. Immediate opt-in is the documented policy. Unsubscribe authorization uses a random token with hash-only persistence. Profile marketing preferences never override explicit unsubscribe.
|
|
||||||
|
|
||||||
## Recommendation Architecture
|
|
||||||
|
|
||||||
Privacy-conscious interaction events retain meaningful signals only. Client ingestion is restricted to product views and protected by authentication, validation, rate limiting, and event-id uniqueness. Explicit product relations lead related results. Recently viewed is de-duplicated; trending uses weighted 30-day SQL aggregation; popular uses paid order items net of refunds; for-you deterministically prioritizes recent interests with a featured fallback. Queries and response sizes are bounded, inactive/hidden products are excluded, and the existing localized serializer is reused.
|
|
||||||
|
|
||||||
## Future External Recommendation Service Boundary
|
|
||||||
|
|
||||||
The current implementation is `LOCAL_DETERMINISTIC`. A future external service may implement the same recommendation input/output contract using scoped machine credentials. There is no insecure internal bypass or placeholder service URL.
|
|
||||||
|
|
||||||
## Explicit AI Exclusion
|
|
||||||
|
|
||||||
Module 16 AI Customer Support Chatbot is intentionally excluded. No OpenAI/LLM integration, prompts, embeddings, RAG, vector database, generated replies, agent, or ML training pipeline was implemented.
|
|
||||||
|
|
||||||
## Security and Database Changes
|
|
||||||
|
|
||||||
Customer ownership is server-derived; strict request schemas reject unknown fields; internal visibility is enforced; uploads and linked resources receive independent authorization; newsletter tokens are hash-only; recommendation telemetry cannot spoof purchases. Migration `20260909100000-phase-10-support-recommendations.js` is forward-only and creates 14 tables with uniqueness and query indexes. Precheck legacy support/FAQ/newsletter/event tables and duplicate normalized email addresses before staging. No backfill is fabricated.
|
|
||||||
|
|
||||||
## Tests and Known Limitations
|
|
||||||
|
|
||||||
Unit coverage exercises strict fields, priority/event spoofing, transition policy, token hashing, source allowlists, body safety, and permissions. Real MySQL FK/migration/concurrency, Redis/BullMQ, S3 signed downloads, SMTP notifications, business-price projection, authoritative event hooks, rich CMS update APIs, and multi-instance cron behavior require Phase 11 staging work.
|
|
||||||
|
|
||||||
## Phase 11 Prerequisites
|
|
||||||
|
|
||||||
Apply migrations only after schema/data prechecks and a verified backup. Seed categories/SLA policies and permissions, validate real infrastructure, add concurrency/IDOR/E2E coverage, wire support notifications and authoritative recommendation signals, and measure aggregate query plans before production readiness assessment.
|
|
||||||
@@ -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.
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
# ZUMRI Phase 7 Orders and Payments
|
|
||||||
|
|
||||||
## Objective
|
|
||||||
Convert checkout snapshots into durable orders and establish authoritative payment, invoice, refund, and return lifecycles without delivery or rewards.
|
|
||||||
|
|
||||||
## Existing Components Reused
|
|
||||||
Checkout snapshots, inventory reservations, pricing money helpers, coupons, ReferenceNumber, document infrastructure, audit queue, authentication, and permissions.
|
|
||||||
|
|
||||||
## Order Architecture
|
|
||||||
Order/payment/fulfillment states are independent. Commercial and address/shipping details are immutable snapshots.
|
|
||||||
|
|
||||||
## Checkout Conversion
|
|
||||||
The centralized transaction locks an owned READY checkout, verifies reservations, returns any existing order, copies items, and converts checkout/cart.
|
|
||||||
|
|
||||||
## Order State Machine
|
|
||||||
Explicit transition sets reject illegal terminal-state transitions and arbitrary status patches.
|
|
||||||
|
|
||||||
## Order Snapshots
|
|
||||||
Items retain product/variant IDs, reservation key, SKU, names, quantity, unit price, discount, total, currency, and limited metadata.
|
|
||||||
|
|
||||||
## Payment Architecture
|
|
||||||
Payments retain immutable attempts. Only server/provider reconciliation changes authoritative financial state.
|
|
||||||
|
|
||||||
## Provider Adapters
|
|
||||||
Generic payment logic delegates verification/parsing/initiation/refund/status behavior to provider modules.
|
|
||||||
|
|
||||||
## PayHere
|
|
||||||
Merchant secrets are environmental. Browser redirects are non-authoritative; the notify hash, reference, amount, and currency must validate.
|
|
||||||
|
|
||||||
## Stripe
|
|
||||||
An explicit disabled boundary is present. Integration awaits official SDK/configuration rather than accepting unverified callbacks.
|
|
||||||
|
|
||||||
## Webhook Verification
|
|
||||||
Invalid signatures, currencies, amounts, and references are rejected.
|
|
||||||
|
|
||||||
## Webhook Idempotency
|
|
||||||
Unique provider/event records and locked payment/order rows ensure duplicate success cannot repeat side effects.
|
|
||||||
|
|
||||||
## Inventory Consumption
|
|
||||||
Checkout reserves; confirmed online payment consumes. Failed payment does not consume. Unpaid cancellation releases.
|
|
||||||
|
|
||||||
## Payment Reconciliation
|
|
||||||
Webhook persistence supports reconciliation, but remote status polling is deferred until the enabled provider supplies a validated status API.
|
|
||||||
|
|
||||||
## Business Credit
|
|
||||||
Disabled pending an approved immutable credit-ledger/accounting policy; concurrent unsafe balance mutation was not introduced.
|
|
||||||
|
|
||||||
## Invoice Architecture
|
|
||||||
One invoice per order uses transaction-safe ReferenceNumber sequencing. The existing document worker remains the PDF integration point.
|
|
||||||
|
|
||||||
## Refund Architecture
|
|
||||||
Idempotent itemized requests validate remaining quantity and captured amount. Provider processing is separate from request approval.
|
|
||||||
|
|
||||||
## Return/RMA Architecture
|
|
||||||
Owner-scoped requests and explicit admin transitions track physical receipt/condition independently from refunds.
|
|
||||||
|
|
||||||
## Exchange Boundary
|
|
||||||
EXCHANGE is recorded as resolution intent; no replacement order or fulfillment is created.
|
|
||||||
|
|
||||||
## Coupon Redemption
|
|
||||||
Redemption occurs once on confirmed payment while the coupon row is locked. Full-refund restoration is deferred by policy.
|
|
||||||
|
|
||||||
## Verified Purchase Reviews
|
|
||||||
Review creation derives verification only from an actual paid order containing the product.
|
|
||||||
|
|
||||||
## Permissions
|
|
||||||
Orders read/manage/cancel, payments read/manage/refund, invoices read, and returns read/manage were added.
|
|
||||||
|
|
||||||
## Audit Events
|
|
||||||
Order, payment creation, cancellation, refund request, return lifecycle, and admin operations reuse sanitized Phase 2 activity logging.
|
|
||||||
|
|
||||||
## Database Changes
|
|
||||||
Forward-only migration `20260909070000` creates eleven commerce tables with unique references/idempotency and lifecycle indexes.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
Unit tests cover order transitions, PayHere signature validation, forged notifications, secure return/refund inputs, and configurable return eligibility.
|
|
||||||
|
|
||||||
## Remaining Known Issues
|
|
||||||
Real PayHere, refunds, document generation, migration, webhook delivery, and MySQL concurrency were not exercised. Refund approval/provider completion APIs and reconciliation polling require provider policy/configuration.
|
|
||||||
|
|
||||||
## Phase 8 Prerequisites
|
|
||||||
Apply all migrations on restored staging, seed permissions, configure/validate PayHere sandbox, test concurrent duplicate webhooks, final-stock payment consumption, coupon quotas, invoice jobs, refund callbacks, and return restocking.
|
|
||||||
|
|
||||||
Module 16 AI Customer Support Chatbot is intentionally excluded and planned as a separate service.
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
# ZUMRI Phase 8 Delivery and Rider Management
|
|
||||||
|
|
||||||
## Objective
|
|
||||||
Add physical delivery and approved-return transportation after the paid Order boundary.
|
|
||||||
|
|
||||||
## Existing Components Reused
|
|
||||||
User/RIDER authentication, Order/OrderItem snapshots, ReturnRequest, ReferenceNumber, Upload, audit, notification foundations, and permissions.
|
|
||||||
|
|
||||||
## Shipment Architecture
|
|
||||||
Orders can have many CUSTOMER_DELIVERY shipments; returns have one RETURN_PICKUP. Shipment owns immutable destination and operational timestamps, never prices or payments.
|
|
||||||
|
|
||||||
## Shipment Items
|
|
||||||
ShipmentItem references OrderItem and supports partial allocation. Locked allocation checks prevent non-cancelled totals exceeding purchases.
|
|
||||||
|
|
||||||
## Shipment State Machine
|
|
||||||
Central legal transitions cover readiness, assignment, pickup, transit, delivery, failure, rescheduling, and cancellation. No generic status patch exists.
|
|
||||||
|
|
||||||
## Order Fulfillment Integration
|
|
||||||
Delivered shipment quantities derive UNFULFILLED, PARTIALLY_FULFILLED, or FULFILLED. Delivery never changes payment or initiates a refund.
|
|
||||||
|
|
||||||
## Rider Profile
|
|
||||||
Operational data attaches one-to-one to an existing RIDER User; authentication and identity remain on User.
|
|
||||||
|
|
||||||
## Rider Availability
|
|
||||||
ACTIVE/INACTIVE/SUSPENDED is separate from AVAILABLE/BUSY/OFFLINE. Assignment locks the rider and checks active capacity.
|
|
||||||
|
|
||||||
## Assignment Architecture
|
|
||||||
Append-preserved assignment rows record assignment, acceptance/rejection, unassignment, and completion. Shipment caches only the current rider.
|
|
||||||
|
|
||||||
## Dispatch
|
|
||||||
The dispatch view queries ready, active, failed, and rescheduled shipments without adding redundant persistence.
|
|
||||||
|
|
||||||
## Rider Workflow
|
|
||||||
Riders see and act only on their own current assignments through explicit legal action endpoints.
|
|
||||||
|
|
||||||
## Delivery Events
|
|
||||||
ShipmentEvent is append-only. Unique event IDs provide retry idempotency and chronological customer timelines.
|
|
||||||
|
|
||||||
## Proof of Delivery
|
|
||||||
Structured proof supports photo, signature, recipient confirmation, and return-pickup photo. Existing private Upload records are ownership/status/MIME checked.
|
|
||||||
|
|
||||||
## Failed Deliveries
|
|
||||||
Failure requires a reason code and increments attempts. It does not refund, cancel the Order, or restock.
|
|
||||||
|
|
||||||
## Rescheduling
|
|
||||||
Admin may schedule a failed delivery with bounded window/reason fields; no calendar optimizer is introduced.
|
|
||||||
|
|
||||||
## Customer Tracking
|
|
||||||
Authenticated ownership is mandatory. Safe projections exclude private rider/location and financial data.
|
|
||||||
|
|
||||||
## Location Privacy
|
|
||||||
Only latest optional coordinates are retained. They are operational data, absent from customer responses and broad audit metadata.
|
|
||||||
|
|
||||||
## Return Logistics
|
|
||||||
Physical transport remains separate from Phase 7 inspection, refund, and inventory decisions.
|
|
||||||
|
|
||||||
## Return Pickup
|
|
||||||
Only approved returns create an idempotent pickup. Delivery marks the request RECEIVED without premature refund/restock.
|
|
||||||
|
|
||||||
## Notifications
|
|
||||||
Existing Phase 2 infrastructure remains the integration boundary. Automated delivery notification fan-out needs durable outbox/worker hardening before production.
|
|
||||||
|
|
||||||
## Permissions
|
|
||||||
Shipment read/create/manage/assign, rider read/manage, dispatch read/manage, proof read, and return-logistics read/manage permissions were added.
|
|
||||||
|
|
||||||
## Audit
|
|
||||||
Creation, assignment, rider actions, cancellation, reschedule, and return pickup use stable Phase 2 activity types without precise location.
|
|
||||||
|
|
||||||
## Idempotency
|
|
||||||
Creation, assignment, and rider transitions use client keys mapped to unique ShipmentEvent IDs. Repeated delivery cannot duplicate proof/event/fulfillment changes.
|
|
||||||
|
|
||||||
## Concurrency
|
|
||||||
Transactions and row locks protect creation allocations, assignment, capacity, transitions, completion, and return-pickup uniqueness. Real multi-connection InnoDB tests remain required.
|
|
||||||
|
|
||||||
## Database Changes
|
|
||||||
Forward-only migration `20260909080000` creates six logistics tables with reference, event, lookup, and uniqueness indexes.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
Unit tests cover all primary legal/illegal transitions, owner-derived rider authorization, address injection rejection, failure reason validation, and location ranges.
|
|
||||||
|
|
||||||
## Remaining Known Issues
|
|
||||||
Real migrations/concurrency, durable notification fan-out, upload storage, pagination tuning, and assignment race testing require staging. No external courier or real-time GPS system exists.
|
|
||||||
|
|
||||||
## Phase 9 Prerequisites
|
|
||||||
Apply migrations on restored staging, seed permissions/rider profiles, validate real concurrent allocation/assignment/delivery requests, proof uploads, notification idempotency, and return receipt handoff.
|
|
||||||
|
|
||||||
Module 16 AI Customer Support Chatbot remains intentionally outside this backend and will be developed separately.
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
# ZUMRI Phase 9 Loyalty and Wholesale Completion
|
|
||||||
|
|
||||||
## Objective
|
|
||||||
Connect immutable loyalty rewards and wholesale credit/settlement accounting to existing users, paid orders, reviews, coupons, payments, and invoices.
|
|
||||||
|
|
||||||
## Existing Components Reused
|
|
||||||
User/Profile DOB, BusinessCustomer/Tier/CreditAccount/SettlementTerm, Orders, Payments, Refunds, Invoices, Reviews, Coupons, pricing money helpers, ReferenceNumber, cron, notifications, audit, and RBAC.
|
|
||||||
|
|
||||||
## Loyalty Architecture
|
|
||||||
One account caches balances while an append-only ledger remains authoritative. All mutations lock the account and use unique events.
|
|
||||||
|
|
||||||
## Loyalty Account
|
|
||||||
ACTIVE/SUSPENDED/CLOSED state, spendable/pending/debt balances, lifetime totals, tier, and last activity are maintained transactionally.
|
|
||||||
|
|
||||||
## Points Ledger
|
|
||||||
Integer EARN, REDEEM, EXPIRE, ADJUSTMENT, and REVERSAL entries store source, resulting balance, expiry, and sanitized metadata.
|
|
||||||
|
|
||||||
## Earning Rules
|
|
||||||
Data-driven effective rules support amount-based purchases and fixed review/referral/birthday awards.
|
|
||||||
|
|
||||||
## Purchase Rewards
|
|
||||||
Confirmed PAID processing awards discounted merchandise value only, with integer-safe floor rounding and deterministic Order event identity.
|
|
||||||
|
|
||||||
## Refund Reversals
|
|
||||||
Reversals append history. Insufficient available points produce tracked debt rather than silently discarding value.
|
|
||||||
|
|
||||||
## Verified Review Rewards
|
|
||||||
Only APPROVED verified-purchase reviews earn, once per review.
|
|
||||||
|
|
||||||
## Referral Rewards
|
|
||||||
Opaque random codes prevent self/multiple referral claims; signup alone does not reward, while a qualifying paid Order does.
|
|
||||||
|
|
||||||
## Birthday Rewards
|
|
||||||
Daily bounded reconciliation uses a unique user/calendar-year event, preventing DOB edits or retries from duplicating rewards.
|
|
||||||
|
|
||||||
## Points Expiry
|
|
||||||
Earn rules may assign expiry. Allocation records support earliest-expiring-first redemption and idempotent expiry debits.
|
|
||||||
|
|
||||||
## Membership Tiers
|
|
||||||
Active data-driven LIFETIME_POINTS thresholds select the highest rank and append tier history.
|
|
||||||
|
|
||||||
## Reward Catalogue
|
|
||||||
Scheduled, limited COUPON or MANUAL rewards use integer point costs.
|
|
||||||
|
|
||||||
## Reward Redemption
|
|
||||||
Account/reward/allocation locks enforce balance, stock, per-user limits, FIFO spending, and idempotency.
|
|
||||||
|
|
||||||
## Voucher Integration
|
|
||||||
COUPON rewards reference Phase 5 Coupon and issue customer entitlements rather than creating another coupon engine.
|
|
||||||
|
|
||||||
## Loyalty Notifications and Permissions
|
|
||||||
Phase 2 notification/audit remain the delivery boundaries. Accounts read, points adjustment, management, and referral-read permissions were added.
|
|
||||||
|
|
||||||
## Wholesale Architecture
|
|
||||||
Existing business identity/pricing remain authoritative. Phase 9 adds financial ledgers and period snapshots.
|
|
||||||
|
|
||||||
## Business Credit Ledger
|
|
||||||
Unique immutable events store amount, balance delta/after, currency, Order/Payment references, and event time.
|
|
||||||
|
|
||||||
## Credit Utilization
|
|
||||||
Account row locks prevent limits being exceeded. Cached `usedCredit` was not added to BusinessCreditAccount; the latest ledger balance is authoritative.
|
|
||||||
|
|
||||||
## Credit Purchase Integration
|
|
||||||
An eligible pending wholesale Order is captured, inventory-consumed, paid, and invoiced in one transaction.
|
|
||||||
|
|
||||||
## Settlement Terms and Lifecycle
|
|
||||||
Existing term days derive due dates. DRAFT, ISSUED, PARTIALLY_PAID, PAID, OVERDUE, and CANCELLED use explicit transitions.
|
|
||||||
|
|
||||||
## Business Statements, Invoice Integration, Payment Allocation
|
|
||||||
Settlement items reference source Orders/Invoices/ledger entries. Existing Invoice and Payment are reused; allocation persistence prepares later bank matching.
|
|
||||||
|
|
||||||
## Wholesale Dashboard and Analytics
|
|
||||||
Owner-scoped SQL aggregates provide monthly/lifetime volume, discount totals, credit utilization, next settlement, and recent Orders without loading all history.
|
|
||||||
|
|
||||||
## Security, Audit, Idempotency, Concurrency
|
|
||||||
No IDs select another organization on business routes. Strict schemas reject balance/tier/client financial fields. Unique event keys plus account/settlement/reward locks protect retries and competing writes.
|
|
||||||
|
|
||||||
## Database Changes
|
|
||||||
Forward-only `20260909090000` creates fourteen focused loyalty/wholesale tables. Earlier migrations are unchanged.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
Policy tests cover integer money conversion, configured caps, referral entropy, strict inputs, terminal settlement state, and permission denial, alongside Phase 0–8 regression.
|
|
||||||
|
|
||||||
## Remaining Known Issues
|
|
||||||
Refund-completion reversal wiring, coupon-entitlement enforcement in the Phase 6 quote path, payment allocation APIs, settlement document generation, notification fan-out, and broader integration/concurrency tests require hardening.
|
|
||||||
|
|
||||||
## Phase 10 Prerequisites
|
|
||||||
Run legacy loyalty/credit/voucher/DOB/reference prechecks, migrate restored staging, seed rules/tiers/rewards/permissions, and test real concurrent redemptions, credit purchases, settlements, webhook rewards, refunds, birthday, and expiry jobs.
|
|
||||||
|
|
||||||
Module 16 AI Customer Support Chatbot is intentionally excluded and will be developed as a separate service.
|
|
||||||
@@ -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
|
|
||||||
@@ -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.
|
|
||||||
@@ -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 +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"}}}}}}
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -47,12 +47,6 @@ const envSchema = z.object({
|
|||||||
INVENTORY_RESERVATION_TTL_MINUTES: z.coerce.number().int().positive().default(15),
|
INVENTORY_RESERVATION_TTL_MINUTES: z.coerce.number().int().positive().default(15),
|
||||||
CHECKOUT_TTL_MINUTES: z.coerce.number().int().positive().default(15),
|
CHECKOUT_TTL_MINUTES: z.coerce.number().int().positive().default(15),
|
||||||
CART_MAX_ITEM_QUANTITY: z.coerce.number().int().positive().default(100),
|
CART_MAX_ITEM_QUANTITY: z.coerce.number().int().positive().default(100),
|
||||||
RETURN_WINDOW_DAYS: z.coerce.number().int().positive().default(30),
|
|
||||||
PAYHERE_MERCHANT_ID: z.string().optional(), PAYHERE_MERCHANT_SECRET: z.string().optional(),
|
|
||||||
PAYHERE_NOTIFY_URL: z.string().url().optional(), PAYHERE_RETURN_URL: z.string().url().optional(), PAYHERE_CANCEL_URL: z.string().url().optional(),
|
|
||||||
LOYALTY_RECONCILIATION_BATCH_SIZE: z.coerce.number().int().positive().max(1000).default(100),
|
|
||||||
SUPPORT_SLA_RECONCILIATION_BATCH_SIZE: z.coerce.number().int().positive().max(1000).default(100),
|
|
||||||
RECOMMENDATION_EVENT_RETENTION_DAYS: z.coerce.number().int().positive().default(90),
|
|
||||||
LOG_RETENTION_DAYS: z.coerce.number().int().positive().default(30),
|
LOG_RETENTION_DAYS: z.coerce.number().int().positive().default(30),
|
||||||
DOCS_USER: z.string().optional(), DOCS_PASS: z.string().optional(),
|
DOCS_USER: z.string().optional(), DOCS_PASS: z.string().optional(),
|
||||||
GOOGLE_CLIENT_ID: z.string().optional(), APPLE_CLIENT_ID: z.string().optional(),
|
GOOGLE_CLIENT_ID: z.string().optional(), APPLE_CLIENT_ID: z.string().optional(),
|
||||||
|
|||||||
@@ -25,10 +25,4 @@ module.exports = {
|
|||||||
INVENTORY_READ:"inventory.read",INVENTORY_ADJUST:"inventory.adjust",INVENTORY_TRANSFER:"inventory.transfer",INVENTORY_RESERVATIONS_READ:"inventory.reservations.read",INVENTORY_WAREHOUSES_MANAGE:"inventory.warehouses.manage",
|
INVENTORY_READ:"inventory.read",INVENTORY_ADJUST:"inventory.adjust",INVENTORY_TRANSFER:"inventory.transfer",INVENTORY_RESERVATIONS_READ:"inventory.reservations.read",INVENTORY_WAREHOUSES_MANAGE:"inventory.warehouses.manage",
|
||||||
BUSINESS_PRICING_READ:"pricing.business.read",BUSINESS_PRICING_MANAGE:"pricing.business.manage",PROMOTIONS_READ:"promotions.read",PROMOTIONS_MANAGE:"promotions.manage",BANNERS_MANAGE:"merchandising.banners.manage",
|
BUSINESS_PRICING_READ:"pricing.business.read",BUSINESS_PRICING_MANAGE:"pricing.business.manage",PROMOTIONS_READ:"promotions.read",PROMOTIONS_MANAGE:"promotions.manage",BANNERS_MANAGE:"merchandising.banners.manage",
|
||||||
SHIPPING_ZONES_READ:"shipping.zones.read",SHIPPING_ZONES_MANAGE:"shipping.zones.manage",SHIPPING_METHODS_READ:"shipping.methods.read",SHIPPING_METHODS_MANAGE:"shipping.methods.manage",SHIPPING_RATES_READ:"shipping.rates.read",SHIPPING_RATES_MANAGE:"shipping.rates.manage",
|
SHIPPING_ZONES_READ:"shipping.zones.read",SHIPPING_ZONES_MANAGE:"shipping.zones.manage",SHIPPING_METHODS_READ:"shipping.methods.read",SHIPPING_METHODS_MANAGE:"shipping.methods.manage",SHIPPING_RATES_READ:"shipping.rates.read",SHIPPING_RATES_MANAGE:"shipping.rates.manage",
|
||||||
ORDERS_READ:"orders.read",ORDERS_MANAGE:"orders.manage",ORDERS_CANCEL:"orders.cancel",PAYMENTS_READ:"payments.read",PAYMENTS_MANAGE:"payments.manage",PAYMENTS_REFUND:"payments.refund",INVOICES_READ:"invoices.read",RETURNS_READ:"returns.read",RETURNS_MANAGE:"returns.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",
|
|
||||||
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 +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,4 +1,4 @@
|
|||||||
const crypto=require("crypto");const db=require("../../models");const loyalty=require("../../services/loyalty/loyalty.service");const {logActivity}=require("../../services/activity.service");
|
const crypto=require("crypto");const db=require("../../models");const {logActivity}=require("../../services/activity.service");
|
||||||
exports.create=async(req,res,next)=>{try{if(!["customer","business_customer"].includes(req.user.accountType))return res.status(403).json({success:false,error:{code:"FORBIDDEN",message:"Customer account required"}});const product=await db.Product.findOne({where:{id:req.params.productId,status:"ACTIVE",visibility:"PUBLIC"}});if(!product)return res.status(404).json({success:false,error:{code:"NOT_FOUND",message:"Product not found"}});const purchased=await db.Order.count({where:{user_id:req.user.id,payment_status:"PAID"},include:[{model:db.OrderItem,as:"items",where:{product_id:product.id},attributes:[]}]});const review=await db.ProductReview.create({id:crypto.randomUUID(),product_id:product.id,user_id:req.user.id,rating:req.body.rating,title:req.body.title,body:req.body.body,status:"PENDING",verified_purchase:purchased>0});await logActivity({user:req.user,type:"REVIEW_SUBMITTED",module:"Catalogue",description:"Product review submitted",targetType:"PRODUCT_REVIEW",targetId:review.id,requestId:req.id});return res.status(201).json({success:true,data:{id:review.id,status:review.status,verifiedPurchase:review.verified_purchase}});}catch(e){return next(e);}};
|
exports.create=async(req,res,next)=>{try{if(!["customer","business_customer"].includes(req.user.accountType))return res.status(403).json({success:false,error:{code:"FORBIDDEN",message:"Customer account required"}});const product=await db.Product.findOne({where:{id:req.params.productId,status:"ACTIVE",visibility:"PUBLIC"}});if(!product)return res.status(404).json({success:false,error:{code:"NOT_FOUND",message:"Product not found"}});const review=await db.ProductReview.create({id:crypto.randomUUID(),product_id:product.id,user_id:req.user.id,rating:req.body.rating,title:req.body.title,body:req.body.body,status:"PENDING",verified_purchase:false});await logActivity({user:req.user,type:"REVIEW_SUBMITTED",module:"Catalogue",description:"Product review submitted",targetType:"PRODUCT_REVIEW",targetId:review.id,requestId:req.id});return res.status(201).json({success:true,data:{id:review.id,status:review.status}});}catch(e){return next(e);}};
|
||||||
exports.listAdmin=async(req,res,next)=>{try{const page=Math.max(Number(req.query.page)||1,1),limit=Math.min(Number(req.query.limit)||20,100),where={};if(req.query.status)where.status=req.query.status;const x=await db.ProductReview.findAndCountAll({where,limit,offset:(page-1)*limit,order:[["createdAt","DESC"]]});return res.json({success:true,data:x.rows,pagination:{page,limit,total:x.count}});}catch(e){return next(e);}};
|
exports.listAdmin=async(req,res,next)=>{try{const page=Math.max(Number(req.query.page)||1,1),limit=Math.min(Number(req.query.limit)||20,100),where={};if(req.query.status)where.status=req.query.status;const x=await db.ProductReview.findAndCountAll({where,limit,offset:(page-1)*limit,order:[["createdAt","DESC"]]});return res.json({success:true,data:x.rows,pagination:{page,limit,total:x.count}});}catch(e){return next(e);}};
|
||||||
exports.moderate=async(req,res,next)=>{try{const r=await db.ProductReview.findByPk(req.params.id);if(!r)return res.status(404).json({success:false,error:{code:"NOT_FOUND",message:"Review not found"}});await r.update({status:req.body.status,moderated_by:req.user.id,moderated_at:new Date()});if(r.status==="APPROVED")await loyalty.earnReview(r);await logActivity({user:req.user,type:req.body.status==="APPROVED"?"REVIEW_APPROVED":"REVIEW_REJECTED",module:"Catalogue",description:"Product review moderated",targetType:"PRODUCT_REVIEW",targetId:r.id,requestId:req.id});return res.json({success:true,data:{id:r.id,status:r.status}});}catch(e){return next(e);}};
|
exports.moderate=async(req,res,next)=>{try{const r=await db.ProductReview.findByPk(req.params.id);if(!r)return res.status(404).json({success:false,error:{code:"NOT_FOUND",message:"Review not found"}});await r.update({status:req.body.status,moderated_by:req.user.id,moderated_at:new Date()});await logActivity({user:req.user,type:req.body.status==="APPROVED"?"REVIEW_APPROVED":"REVIEW_REJECTED",module:"Catalogue",description:"Product review moderated",targetType:"PRODUCT_REVIEW",targetId:r.id,requestId:req.id});return res.json({success:true,data:{id:r.id,status:r.status}});}catch(e){return next(e);}};
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
const db=require("../../models"),orders=require("../../services/commerce/order.service"),returns=require("../../services/commerce/return.service"),refunds=require("../../services/commerce/refund.service"),state=require("../../services/commerce/orderState.service"),{logActivity}=require("../../services/activity.service");exports.orders=async(req,res,next)=>{try{res.json({success:true,data:await db.Order.findAll({include:[{model:db.OrderItem,as:"items"}],order:[["createdAt","DESC"]]})});}catch(e){next(e);}};exports.order=async(req,res,next)=>{try{const row=await db.Order.findByPk(req.params.id,{include:[{model:db.OrderItem,as:"items"},{model:db.Payment,as:"payments"}]});if(!row)throw Object.assign(new Error("Order not found"),{status:404,code:"NOT_FOUND"});res.json({success:true,data:row});}catch(e){next(e);}};exports.cancel=async(req,res,next)=>{try{const row=await orders.cancel({orderId:req.params.id,admin:true,requestId:req.id});await logActivity({user:req.user,type:"ORDER_CANCELLED",module:"Orders",targetId:row.id,requestId:req.id});res.json({success:true,data:row});}catch(e){next(e);}};exports.processing=async(req,res,next)=>{try{const row=await db.Order.findByPk(req.params.id);if(!row)throw Object.assign(new Error("Order not found"),{status:404,code:"NOT_FOUND"});state.assertTransition(row.status,"PROCESSING");await row.update({status:"PROCESSING"});res.json({success:true,data:row});}catch(e){next(e);}};exports.returns=async(req,res,next)=>{try{res.json({success:true,data:await db.ReturnRequest.findAll({order:[["createdAt","DESC"]]})});}catch(e){next(e);}};exports.returnAction=status=>async(req,res,next)=>{try{const row=await returns.transition({id:req.params.id,status,actorUserId:req.user.id,conditions:req.body.conditions||[]});await logActivity({user:req.user,type:`RETURN_${status}`,module:"Returns",targetId:row.id,requestId:req.id});res.json({success:true,data:row});}catch(e){next(e);}};exports.refund=async(req,res,next)=>{try{const result=await refunds.request({orderId:req.params.id,actorUserId:req.user.id,operationKey:req.get("Idempotency-Key"),...req.body});await logActivity({user:req.user,type:"REFUND_REQUESTED",module:"Refunds",targetId:result.refund.id,requestId:req.id});res.status(result.idempotent?200:201).json({success:true,data:result.refund});}catch(e){next(e);}};
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
const db=require("../../models"),orders=require("../../services/commerce/order.service"),payments=require("../../services/commerce/payment.service"),{logActivity}=require("../../services/activity.service");const audit=(req,type,id)=>logActivity({user:req.user,type,module:"Orders",targetType:"ORDER",targetId:id,requestId:req.id});exports.create=async(req,res,next)=>{try{const result=await orders.createFromCheckout({checkoutId:req.body.checkoutId,userId:req.user.id});if(!result.idempotent)await audit(req,"ORDER_CREATED",result.order.id);res.status(result.idempotent?200:201).json({success:true,data:result.order});}catch(e){next(e);}};
|
|
||||||
exports.list=async(req,res,next)=>{try{const page=Math.max(Number(req.query.page)||1,1),limit=Math.min(Number(req.query.limit)||20,100),where={user_id:req.user.id};if(req.query.status)where.status=req.query.status;const x=await db.Order.findAndCountAll({where,limit,offset:(page-1)*limit,order:[["createdAt","DESC"]]});res.json({success:true,data:x.rows,pagination:{page,limit,total:x.count}});}catch(e){next(e);}};exports.get=async(req,res,next)=>{try{const row=await db.Order.findOne({where:{id:req.params.id,user_id:req.user.id},include:[{model:db.OrderItem,as:"items"},{model:db.Payment,as:"payments",attributes:{exclude:["failure_message"]}},{model:db.Invoice,as:"invoice"}]});if(!row)throw Object.assign(new Error("Order not found"),{status:404,code:"NOT_FOUND"});res.json({success:true,data:row});}catch(e){next(e);}};exports.cancel=async(req,res,next)=>{try{const row=await orders.cancel({orderId:req.params.id,userId:req.user.id,requestId:req.id});await audit(req,"ORDER_CANCELLED",row.id);res.json({success:true,data:row});}catch(e){next(e);}};exports.payment=async(req,res,next)=>{try{const row=await db.Payment.findOne({include:[{model:db.Order,as:"order",where:{id:req.params.id,user_id:req.user.id},attributes:[]}],attributes:{exclude:["failure_message"]}});res.json({success:true,data:row});}catch(e){next(e);}};exports.invoice=async(req,res,next)=>{try{const row=await db.Invoice.findOne({include:[{model:db.Order,as:"order",where:{id:req.params.id,user_id:req.user.id},attributes:[]}]});if(!row)throw Object.assign(new Error("Invoice not found"),{status:404,code:"NOT_FOUND"});res.json({success:true,data:row});}catch(e){next(e);}};exports.startPayment=async(req,res,next)=>{try{const row=await payments.create({orderId:req.params.id,userId:req.user.id,...req.body});await audit(req,"PAYMENT_CREATED",row.id);res.status(201).json({success:true,data:{id:row.id,paymentReference:row.payment_reference,provider:row.provider,status:row.status,amount:String(row.amount),currency:row.currency}});}catch(e){next(e);}};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
const db=require("../../models"),service=require("../../services/commerce/return.service"),{logActivity}=require("../../services/activity.service");exports.request=async(req,res,next)=>{try{const row=await service.request({orderId:req.params.orderId,userId:req.user.id,...req.body});await logActivity({user:req.user,type:"RETURN_REQUESTED",module:"Returns",targetId:row.id,requestId:req.id});res.status(201).json({success:true,data:row});}catch(e){next(e);}};exports.list=async(req,res,next)=>{try{res.json({success:true,data:await db.ReturnRequest.findAll({where:{user_id:req.user.id},order:[["createdAt","DESC"]]})});}catch(e){next(e);}};exports.get=async(req,res,next)=>{try{const row=await db.ReturnRequest.findOne({where:{id:req.params.id,user_id:req.user.id}});if(!row)throw Object.assign(new Error("Return not found"),{status:404,code:"NOT_FOUND"});res.json({success:true,data:row});}catch(e){next(e);}};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
const service=require("../../services/commerce/payment.service");exports.payhere=async(req,res,next)=>{try{await service.handleWebhook("PAYHERE",req.body);res.status(200).send("OK");}catch(e){next(e);}};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
const db=require("../models"),help=require("../services/help/help.service"),{resolveLocale}=require("../services/catalogue/locale.service");const locale=req=>resolveLocale(req);exports.categories=async(req,res,next)=>{try{res.json({success:true,data:await help.categories(locale(req))});}catch(e){next(e);}};exports.category=async(req,res,next)=>{try{const row=await db.HelpCategory.findOne({where:{slug:req.params.slug,status:"ACTIVE"}});if(!row)return res.status(404).json({success:false,error:{code:"NOT_FOUND",message:"Help category not found"}});res.json({success:true,data:{category:(await help.categories(locale(req))).find(x=>x.id===row.id),articles:await help.articles({locale:locale(req),categoryId:row.id})}});}catch(e){next(e);}};exports.articles=async(req,res,next)=>{try{res.json({success:true,data:await help.articles({locale:locale(req),categoryId:req.query.category,featured:req.query.featured==null?undefined:req.query.featured==="true",limit:Math.min(+req.query.limit||20,50)})});}catch(e){next(e);}};exports.article=async(req,res,next)=>{try{const a=await db.HelpArticle.findOne({where:{slug:req.params.slug,status:"PUBLISHED"}});if(!a)return res.status(404).json({success:false,error:{code:"NOT_FOUND",message:"Help article not found"}});const tr=help.pick(await db.HelpArticleTranslation.findAll({where:{article_id:a.id}}),locale(req));res.json({success:true,data:{id:a.id,slug:a.slug,categoryId:a.category_id,type:a.article_type,title:tr?.title||null,summary:tr?.summary||null,body:tr?.body||null,locale:tr?.locale||locale(req),isFeatured:a.is_featured,publishedAt:a.published_at}});}catch(e){next(e);}};exports.search=async(req,res,next)=>{try{const q=String(req.query.q||"").trim();if(q.length<2||q.length>100)return res.status(400).json({success:false,error:{code:"INVALID_QUERY",message:"q must contain 2-100 characters"}});res.json({success:true,data:await help.articles({locale:locale(req),q,limit:Math.min(+req.query.limit||20,50)})});}catch(e){next(e);}};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
const db=require("../models"),help=require("../services/help/help.service");exports.categories=async(req,res,next)=>{try{res.json({success:true,data:await db.HelpCategory.findAll({order:[["sort_order","ASC"]]})});}catch(e){next(e);}};exports.createCategory=async(req,res,next)=>{try{res.status(201).json({success:true,data:await help.createCategory(req.body)});}catch(e){next(e);}};exports.articles=async(req,res,next)=>{try{res.json({success:true,data:await db.HelpArticle.findAll({order:[["createdAt","DESC"]]})});}catch(e){next(e);}};exports.createArticle=async(req,res,next)=>{try{res.status(201).json({success:true,data:await help.createArticle(req.user,req.body)});}catch(e){next(e);}};exports.publish=status=>async(req,res,next)=>{try{res.json({success:true,data:await help.publish(req.params.id,req.user,status)});}catch(e){next(e);}};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
const crypto=require("crypto"),{Op}=require("sequelize"),db=require("../../models"),service=require("../../services/logistics/shipment.service"),state=require("../../services/logistics/shipmentState.service"),{logActivity}=require("../../services/activity.service");const key=req=>req.get("Idempotency-Key")||crypto.randomUUID(),audit=(req,type,id)=>logActivity({user:req.user,type,module:"Logistics",targetType:"SHIPMENT",targetId:id,requestId:req.id});exports.shipments=async(req,res,next)=>{try{const where={};if(req.query.status)where.status=req.query.status;res.json({success:true,data:await db.Shipment.findAll({where,include:[{model:db.ShipmentItem,as:"items"},{model:db.ShipmentAssignment,as:"assignments"}],order:[["createdAt","DESC"]]})});}catch(e){next(e);}};exports.shipment=async(req,res,next)=>{try{const row=await db.Shipment.findByPk(req.params.id,{include:[{model:db.ShipmentItem,as:"items"},{model:db.ShipmentAssignment,as:"assignments"},{model:db.ShipmentEvent,as:"events"},{model:db.ShipmentProof,as:"proofs"}]});if(!row)throw Object.assign(new Error("Shipment not found"),{status:404,code:"NOT_FOUND"});res.json({success:true,data:row});}catch(e){next(e);}};exports.create=async(req,res,next)=>{try{const result=await service.createDelivery({...req.body,actorUserId:req.user.id,eventId:`create:${key(req)}`});await audit(req,"SHIPMENT_CREATED",result.shipment.id);res.status(result.idempotent?200:201).json({success:true,data:result.shipment});}catch(e){next(e);}};exports.returnPickup=async(req,res,next)=>{try{const result=await service.createReturnPickup({...req.body,actorUserId:req.user.id,eventId:`return-pickup:${key(req)}`});res.status(result.idempotent?200:201).json({success:true,data:result.shipment});}catch(e){next(e);}};exports.assign=async(req,res,next)=>{try{const result=await service.assign({shipmentId:req.params.id,riderId:req.body.riderId,actorUserId:req.user.id,eventId:`assign:${key(req)}`});await audit(req,"RIDER_ASSIGNED",result.shipment.id);res.json({success:true,data:result.shipment});}catch(e){next(e);}};exports.unassign=async(req,res,next)=>{try{const row=await service.unassign({shipmentId:req.params.id,actorUserId:req.user.id,eventId:`unassign:${key(req)}`,reason:req.body.note});await audit(req,"RIDER_UNASSIGNED",row.id);res.json({success:true,data:row});}catch(e){next(e);}};exports.reschedule=async(req,res,next)=>{try{let row;await db.sequelize.transaction(async t=>{row=await db.Shipment.findByPk(req.params.id,{transaction:t,lock:t.LOCK.UPDATE});if(!row)throw Object.assign(new Error("Shipment not found"),{status:404,code:"NOT_FOUND"});state.assertTransition(row.status,"RESCHEDULED");await row.update({status:"RESCHEDULED",scheduled_date:req.body.scheduledDate,time_window_start:req.body.timeWindowStart,time_window_end:req.body.timeWindowEnd,notes:req.body.reason},{transaction:t});await db.ShipmentEvent.create({id:crypto.randomUUID(),shipment_id:row.id,event_id:`reschedule:${key(req)}`,type:"SHIPMENT_RESCHEDULED",status:"RESCHEDULED",actor_user_id:req.user.id,note:req.body.reason,occurred_at:new Date()},{transaction:t});});await audit(req,"SHIPMENT_RESCHEDULED",row.id);res.json({success:true,data:row});}catch(e){next(e);}};exports.cancel=async(req,res,next)=>{try{let row;await db.sequelize.transaction(async t=>{row=await db.Shipment.findByPk(req.params.id,{transaction:t,lock:t.LOCK.UPDATE});state.assertTransition(row.status,"CANCELLED");await row.update({status:"CANCELLED",cancelled_at:new Date()},{transaction:t});});await audit(req,"SHIPMENT_CANCELLED",row.id);res.json({success:true,data:row});}catch(e){next(e);}};exports.dispatch=async(req,res,next)=>{try{res.json({success:true,data:{unassigned:await db.Shipment.findAll({where:{status:"READY_FOR_ASSIGNMENT"}}),active:await db.Shipment.findAll({where:{status:{[Op.in]:["ASSIGNED","PICKUP_PENDING","PICKED_UP","IN_TRANSIT","OUT_FOR_DELIVERY"]}}}),attention:await db.Shipment.findAll({where:{status:{[Op.in]:["DELIVERY_FAILED","RESCHEDULED"]}}})}});}catch(e){next(e);}};
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
const crypto=require("crypto"),{Op}=require("sequelize"),db=require("../../models"),service=require("../../services/logistics/shipment.service"),{logActivity}=require("../../services/activity.service");const key=req=>req.get("Idempotency-Key")||crypto.randomUUID();exports.list=async(req,res,next)=>{try{const rider=await db.RiderProfile.findOne({where:{user_id:req.user.id,status:"ACTIVE"}});if(!rider)throw Object.assign(new Error("Active rider required"),{status:403,code:"RIDER_UNAVAILABLE"});res.json({success:true,data:await db.Shipment.findAll({where:{assigned_rider_id:rider.id},include:[{model:db.ShipmentItem,as:"items"}],order:[["createdAt","DESC"]]})});}catch(e){next(e);}};exports.get=async(req,res,next)=>{try{const rider=await db.RiderProfile.findOne({where:{user_id:req.user.id,status:"ACTIVE"}}),row=rider&&await db.Shipment.findOne({where:{id:req.params.id,assigned_rider_id:rider.id},include:[{model:db.ShipmentItem,as:"items"},{model:db.ShipmentEvent,as:"events"}]});if(!row)throw Object.assign(new Error("Assigned shipment not found"),{status:404,code:"NOT_FOUND"});res.json({success:true,data:{id:row.id,shipmentNumber:row.shipment_number,recipientName:row.recipient_name,recipientPhone:row.recipient_phone,address:row.address_snapshot,status:row.status,items:row.items,events:row.events}});}catch(e){next(e);}};const action=to=>async(req,res,next)=>{try{const result=await service.riderAction({shipmentId:req.params.id,userId:req.user.id,to,eventId:`${to}:${key(req)}`,note:req.body.note,proof:req.body.type?req.body:undefined});await logActivity({user:req.user,type:`SHIPMENT_${to}`,module:"Logistics",targetId:result.shipment.id,requestId:req.id});res.json({success:true,data:result.shipment});}catch(e){next(e);}};exports.accept=action("PICKUP_PENDING");exports.pickup=action("PICKED_UP");exports.inTransit=action("IN_TRANSIT");exports.outForDelivery=action("OUT_FOR_DELIVERY");exports.deliver=action("DELIVERED");exports.fail=action("DELIVERY_FAILED");exports.location=async(req,res,next)=>{try{const rider=await db.RiderProfile.findOne({where:{user_id:req.user.id,status:"ACTIVE"}});if(!rider)throw Object.assign(new Error("Active rider required"),{status:403,code:"RIDER_UNAVAILABLE"});await rider.update({current_latitude:req.body.latitude,current_longitude:req.body.longitude,last_location_at:new Date()});res.status(204).end();}catch(e){next(e);}};
|
|
||||||
exports.reject=async(req,res,next)=>{try{const rider=await db.RiderProfile.findOne({where:{user_id:req.user.id,status:"ACTIVE"}}),shipment=rider&&await db.Shipment.findOne({where:{id:req.params.id,assigned_rider_id:rider.id}});if(!shipment)throw Object.assign(new Error("Assigned shipment not found"),{status:404,code:"NOT_FOUND"});const row=await service.unassign({shipmentId:shipment.id,actorUserId:req.user.id,eventId:`reject:${key(req)}`,reason:req.body.note});await db.ShipmentAssignment.update({status:"REJECTED",rejected_at:new Date()},{where:{shipment_id:shipment.id,rider_id:rider.id,status:"UNASSIGNED"}});res.json({success:true,data:row});}catch(e){next(e);}};
|
|
||||||
exports.adminUpdate=async(req,res,next)=>{try{const row=await db.RiderProfile.findByPk(req.params.id);if(!row)throw Object.assign(new Error("Rider not found"),{status:404,code:"NOT_FOUND"});const b=req.body;await row.update({employee_code:b.employeeCode,status:b.status,availability_status:b.availabilityStatus,phone_override:b.phoneOverride,vehicle_type:b.vehicleType,vehicle_registration:b.vehicleRegistration,max_active_assignments:b.maxActiveAssignments});await logActivity({user:req.user,type:"RIDER_UPDATED",module:"Logistics",targetId:row.id,requestId:req.id});res.json({success:true,data:row});}catch(e){next(e);}};
|
|
||||||
exports.adminList=async(req,res,next)=>{try{res.json({success:true,data:await db.RiderProfile.findAll({include:[{model:db.User,as:"user",attributes:["id","firstName","lastName","accountStatus"]}]})});}catch(e){next(e);}};exports.adminGet=async(req,res,next)=>{try{const row=await db.RiderProfile.findByPk(req.params.id,{include:[{model:db.User,as:"user",attributes:["id","firstName","lastName","accountStatus"]}]});if(!row)throw Object.assign(new Error("Rider not found"),{status:404,code:"NOT_FOUND"});res.json({success:true,data:row});}catch(e){next(e);}};exports.adminCreate=async(req,res,next)=>{try{const user=await db.User.findOne({where:{id:req.body.userId,accountType:"rider"}});if(!user)throw Object.assign(new Error("Existing RIDER user required"),{status:400,code:"RIDER_USER_REQUIRED"});const b=req.body,row=await db.RiderProfile.create({id:crypto.randomUUID(),user_id:user.id,employee_code:b.employeeCode,status:b.status,availability_status:b.availabilityStatus,phone_override:b.phoneOverride,vehicle_type:b.vehicleType,vehicle_registration:b.vehicleRegistration,max_active_assignments:b.maxActiveAssignments});await logActivity({user:req.user,type:"RIDER_CREATED",module:"Logistics",targetId:row.id,requestId:req.id});res.status(201).json({success:true,data:row});}catch(e){next(e);}};exports.assignments=async(req,res,next)=>{try{res.json({success:true,data:await db.ShipmentAssignment.findAll({where:{rider_id:req.params.id},order:[["assigned_at","DESC"]]})});}catch(e){next(e);}};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
const db=require("../../models");const project=s=>({shipmentNumber:s.shipment_number,type:s.type,status:s.status,shippingMethod:s.shipping_method_name,deliveredAt:s.delivered_at,events:s.events.map(e=>({type:e.type,status:e.status,note:e.note,occurredAt:e.occurred_at}))});exports.order=async(req,res,next)=>{try{const order=await db.Order.findOne({where:{id:req.params.orderId,user_id:req.user.id}});if(!order)throw Object.assign(new Error("Order not found"),{status:404,code:"NOT_FOUND"});const shipments=await db.Shipment.findAll({where:{order_id:order.id,type:"CUSTOMER_DELIVERY"},include:[{model:db.ShipmentEvent,as:"events",attributes:["type","status","note","occurred_at"]}],order:[["createdAt","ASC"]]});res.json({success:true,data:{orderNumber:order.order_number,shipments:shipments.map(project)}});}catch(e){next(e);}};exports.return=async(req,res,next)=>{try{const request=await db.ReturnRequest.findOne({where:{id:req.params.id,user_id:req.user.id}});if(!request)throw Object.assign(new Error("Return not found"),{status:404,code:"NOT_FOUND"});const shipment=await db.Shipment.findOne({where:{return_request_id:request.id,type:"RETURN_PICKUP"},include:[{model:db.ShipmentEvent,as:"events",attributes:["type","status","note","occurred_at"]}]});res.json({success:true,data:{returnNumber:request.return_number,shipment:shipment&&project(shipment)}});}catch(e){next(e);}};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
const crypto=require("crypto"),db=require("../../models"),loyalty=require("../../services/loyalty/loyalty.service"),{logActivity}=require("../../services/activity.service");const list=model=>async(req,res,next)=>{try{res.json({success:true,data:await model.findAll({order:[["createdAt","DESC"]]})});}catch(e){next(e);}};exports.accounts=list(db.LoyaltyAccount);exports.tiers=list(db.LoyaltyTier);exports.rules=list(db.LoyaltyEarnRule);exports.rewards=list(db.LoyaltyReward);exports.referrals=list(db.Referral);exports.adjust=async(req,res,next)=>{try{const result=await loyalty.post({userId:req.body.userId,eventId:`LOYALTY:ADMIN:${req.get("Idempotency-Key")}`,type:"ADJUSTMENT",sourceType:"ADMIN_ADJUSTMENT",points:req.body.pointsDelta,descriptionCode:req.body.reasonCode,metadata:{note:req.body.note,actorUserId:req.user.id}});await logActivity({user:req.user,type:"LOYALTY_POINTS_ADJUSTED",module:"Loyalty",targetId:result.entry.id,requestId:req.id});res.status(result.idempotent?200:201).json({success:true,data:result.entry});}catch(e){next(e);}};exports.createTier=async(req,res,next)=>{try{const b=req.body,row=await db.LoyaltyTier.create({id:crypto.randomUUID(),code:b.code.toUpperCase(),name:b.name,status:b.status,rank:b.rank,qualification_threshold:b.qualificationThreshold,qualification_metric:"LIFETIME_POINTS",benefits_json:b.benefits,sort_order:b.sortOrder});res.status(201).json({success:true,data:row});}catch(e){next(e);}};exports.createRule=async(req,res,next)=>{try{const b=req.body,row=await db.LoyaltyEarnRule.create({id:crypto.randomUUID(),source_type:b.sourceType,status:b.status,points_per_amount:b.pointsPerAmount,amount_unit:b.amountUnit,fixed_points:b.fixedPoints,minimum_amount:b.minimumAmount,maximum_points:b.maximumPoints,expiry_days:b.expiryDays,effective_from:b.effectiveFrom,effective_to:b.effectiveTo});res.status(201).json({success:true,data:row});}catch(e){next(e);}};exports.createReward=async(req,res,next)=>{try{const b=req.body,row=await db.LoyaltyReward.create({id:crypto.randomUUID(),code:b.code.toUpperCase(),name:b.name,description:b.description,type:b.type,points_cost:b.pointsCost,status:b.status,starts_at:b.startsAt,ends_at:b.endsAt,stock_limit:b.stockLimit,per_user_limit:b.perUserLimit,configuration:b.configuration});res.status(201).json({success:true,data:row});}catch(e){next(e);}};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
const{Op}=require("sequelize"),db=require("../../models"),loyalty=require("../../services/loyalty/loyalty.service"),referrals=require("../../services/loyalty/referral.service");exports.summary=async(req,res,next)=>{try{const account=await db.sequelize.transaction(t=>loyalty.accountFor(req.user.id,t)),tier=account.current_tier_id&&await db.LoyaltyTier.findByPk(account.current_tier_id),next=await db.LoyaltyTier.findOne({where:{status:"ACTIVE",qualification_threshold:{[Op.gt]:account.lifetime_points_earned}},order:[["qualification_threshold","ASC"]]});res.json({success:true,data:{status:account.status,availablePoints:account.available_points,pendingPoints:account.pending_points,pointsDebt:account.points_debt,lifetimePointsEarned:account.lifetime_points_earned,tier:tier&&{code:tier.code,name:tier.name,benefits:tier.benefits_json},nextTier:next&&{name:next.name,pointsRequired:next.qualification_threshold-account.lifetime_points_earned}}});}catch(e){next(e);}};exports.history=async(req,res,next)=>{try{const account=await db.LoyaltyAccount.findOne({where:{user_id:req.user.id}});if(!account)return res.json({success:true,data:[],pagination:{page:1,total:0}});const page=Math.max(Number(req.query.page)||1,1),limit=Math.min(Number(req.query.limit)||20,100),x=await db.LoyaltyLedgerEntry.findAndCountAll({where:{loyalty_account_id:account.id},limit,offset:(page-1)*limit,order:[["occurred_at","DESC"]]});res.json({success:true,data:x.rows,pagination:{page,limit,total:x.count}});}catch(e){next(e);}};exports.tiers=async(req,res,next)=>{try{res.json({success:true,data:await db.LoyaltyTier.findAll({where:{status:"ACTIVE"},order:[["rank","ASC"]]})});}catch(e){next(e);}};exports.rewards=async(req,res,next)=>{try{res.json({success:true,data:await db.LoyaltyReward.findAll({where:{status:"ACTIVE"},order:[["points_cost","ASC"]]})});}catch(e){next(e);}};exports.redeem=async(req,res,next)=>{try{const result=await loyalty.redeem({userId:req.user.id,rewardId:req.body.rewardId,idempotencyKey:req.get("Idempotency-Key")});res.status(result.idempotent?200:201).json({success:true,data:result.redemption});}catch(e){next(e);}};exports.vouchers=async(req,res,next)=>{try{res.json({success:true,data:await db.CustomerCouponEntitlement.findAll({where:{user_id:req.user.id,status:"ACTIVE"},attributes:{exclude:["user_id"]}})});}catch(e){next(e);}};exports.referral=async(req,res,next)=>{try{res.json({success:true,data:await referrals.getOrCreate(req.user.id)});}catch(e){next(e);}};exports.claimReferral=async(req,res,next)=>{try{res.status(201).json({success:true,data:await referrals.claim({code:req.body.code,userId:req.user.id})});}catch(e){next(e);}};
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
const db=require("../models"),service=require("../services/marketing/newsletter.service");exports.subscribe=async(req,res,next)=>{try{const x=await service.subscribe({...req.body,userId:req.user?.id});res.status(x.idempotent?200:201).json({success:true,message:"Subscription request processed",data:{status:x.subscription.status}});}catch(e){next(e);}};exports.unsubscribe=async(req,res,next)=>{try{await service.unsubscribe(req.params.token);res.json({success:true,message:"Unsubscribe request processed"});}catch(e){next(e);}};exports.mine=async(req,res,next)=>{try{res.json({success:true,data:await db.NewsletterSubscription.findOne({where:{user_id:req.user.id},attributes:{exclude:["unsubscribe_token_hash"]}})});}catch(e){next(e);}};exports.list=async(req,res,next)=>{try{const page=Math.max(+req.query.page||1,1),limit=Math.min(+req.query.limit||50,100),x=await db.NewsletterSubscription.findAndCountAll({attributes:{exclude:["unsubscribe_token_hash"]},limit,offset:(page-1)*limit,order:[["createdAt","DESC"]]});res.json({success:true,data:x.rows,pagination:{page,limit,total:x.count}});}catch(e){next(e);}};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
const service=require("../services/recommendation/recommendation.service"),{resolveLocale}=require("../services/catalogue/locale.service");const loc=req=>resolveLocale(req),limit=req=>Math.min(Math.max(+req.query.limit||12,1),24);exports.event=async(req,res,next)=>{try{const x=await service.recordView({...req.body,userId:req.user?.id});res.status(x.created?201:200).json({success:true,data:{accepted:true,idempotent:!x.created}});}catch(e){next(e);}};exports.related=async(req,res,next)=>{try{res.json({success:true,data:await service.related(req.params.productId,loc(req),limit(req))});}catch(e){next(e);}};exports.trending=async(req,res,next)=>{try{res.json({success:true,data:await service.ranked({type:"trending",locale:loc(req),limit:limit(req)})});}catch(e){next(e);}};exports.popular=async(req,res,next)=>{try{res.json({success:true,data:await service.ranked({type:"popular",locale:loc(req),limit:limit(req)})});}catch(e){next(e);}};exports.recent=async(req,res,next)=>{try{res.json({success:true,data:await service.recent(req.user.id,loc(req),limit(req))});}catch(e){next(e);}};exports.forYou=async(req,res,next)=>{try{res.json({success:true,data:await service.ranked({type:"for-you",userId:req.user.id,locale:loc(req),limit:limit(req)})});}catch(e){next(e);}};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
const{Op}=require("sequelize"),db=require("../../models"),support=require("../../services/support/support.service");exports.list=async(req,res,next)=>{try{const page=Math.max(+req.query.page||1,1),limit=Math.min(+req.query.limit||20,100),where={...(req.query.status&&{status:req.query.status}),...(req.query.priority&&{priority:req.query.priority}),...(req.query.category&&{category_id:req.query.category}),...(req.query.assignedAgentId&&{assigned_agent_id:req.query.assignedAgentId}),...(req.query.business&&{business_customer_id:req.query.business}),...(req.query.unassigned==="true"&&{assigned_agent_id:null}),...(req.query.overdue==="true"&&{resolution_due_at:{[Op.lt]:new Date()},status:{[Op.notIn]:["RESOLVED","CLOSED","CANCELLED"]}})},x=await db.SupportTicket.findAndCountAll({where,order:[["createdAt","DESC"]],limit,offset:(page-1)*limit});res.json({success:true,data:x.rows,pagination:{page,limit,total:x.count}});}catch(e){next(e);}};exports.detail=async(req,res,next)=>{try{const ticket=await db.SupportTicket.findByPk(req.params.id);if(!ticket)return res.status(404).json({success:false,error:{code:"TICKET_NOT_FOUND",message:"Ticket not found"}});const[messages,attachments,links,events,escalations]=await Promise.all([db.SupportMessage.findAll({where:{ticket_id:ticket.id},order:[["createdAt","ASC"]]}),db.SupportAttachment.findAll({where:{ticket_id:ticket.id}}),db.SupportTicketLink.findAll({where:{ticket_id:ticket.id}}),db.SupportTicketEvent.findAll({where:{ticket_id:ticket.id},order:[["occurred_at","ASC"]]}),db.SupportEscalation.findAll({where:{ticket_id:ticket.id}})]);res.json({success:true,data:{ticket,messages,attachments,links,events,escalations}});}catch(e){next(e);}};exports.assign=async(req,res,next)=>{try{res.json({success:true,data:await support.assign(req.params.id,req.body.agentId,req.user)});}catch(e){next(e);}};exports.claim=async(req,res,next)=>{try{res.json({success:true,data:await support.assign(req.params.id,req.user.id,req.user,true)});}catch(e){next(e);}};exports.reply=async(req,res,next)=>{try{res.status(201).json({success:true,data:await support.agentMessage(req.user,req.params.id,req.body)});}catch(e){next(e);}};exports.note=async(req,res,next)=>{try{res.status(201).json({success:true,data:await support.agentMessage(req.user,req.params.id,req.body,true)});}catch(e){next(e);}};exports.action=to=>async(req,res,next)=>{try{res.json({success:true,data:await support.transition(req.params.id,to,req.user)});}catch(e){next(e);}};exports.priority=async(req,res,next)=>{try{const row=await db.SupportTicket.findByPk(req.params.id);if(!row)return res.status(404).json({success:false,error:{code:"TICKET_NOT_FOUND",message:"Ticket not found"}});const from=row.priority;await row.update({priority:req.body.priority});await db.SupportTicketEvent.create({id:require("crypto").randomUUID(),event_id:require("crypto").randomUUID(),ticket_id:row.id,actor_user_id:req.user.id,type:"PRIORITY_CHANGED",from_value:from,to_value:row.priority,occurred_at:new Date()});res.json({success:true,data:row});}catch(e){next(e);}};exports.attachment=async(req,res,next)=>{try{res.json({success:true,data:{url:await support.attachmentUrl(req.params.id,req.params.attachmentId,req.user,true)}});}catch(e){next(e);}};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
const db=require("../../models"),support=require("../../services/support/support.service");const page=req=>({page:Math.max(Number(req.query.page)||1,1),limit:Math.min(Math.max(Number(req.query.limit)||20,1),100)});exports.create=async(req,res,next)=>{try{res.status(201).json({success:true,data:await support.createTicket(req.user,req.body)});}catch(e){next(e);}};exports.list=async(req,res,next)=>{try{const p=page(req),where={customer_user_id:req.user.id,...(req.query.status&&{status:req.query.status}),...(req.query.category&&{category_id:req.query.category})},x=await db.SupportTicket.findAndCountAll({where,order:[["createdAt","DESC"]],limit:p.limit,offset:(p.page-1)*p.limit});res.json({success:true,data:x.rows,pagination:{...p,total:x.count}});}catch(e){next(e);}};exports.detail=async(req,res,next)=>{try{const ticket=await support.ownTicket(req.params.id,req.user.id),[messages,attachments,links,events]=await Promise.all([db.SupportMessage.findAll({where:{ticket_id:ticket.id,visibility:"CUSTOMER_VISIBLE"},order:[["createdAt","ASC"]]}),db.SupportAttachment.findAll({where:{ticket_id:ticket.id,visibility:"CUSTOMER_VISIBLE"}}),db.SupportTicketLink.findAll({where:{ticket_id:ticket.id}}),db.SupportTicketEvent.findAll({where:{ticket_id:ticket.id,type:["TICKET_CREATED","TICKET_ASSIGNED","TICKET_RESOLVED","TICKET_CLOSED","TICKET_REOPENED"]},attributes:{exclude:["metadata"]},order:[["occurred_at","ASC"]]})]);res.json({success:true,data:{ticket,messages,attachments,links,events}});}catch(e){next(e);}};exports.reply=async(req,res,next)=>{try{res.status(201).json({success:true,data:await support.customerReply(req.user,req.params.id,req.body)});}catch(e){next(e);}};exports.close=async(req,res,next)=>{try{await support.ownTicket(req.params.id,req.user.id);res.json({success:true,data:await support.transition(req.params.id,"CLOSED",req.user)});}catch(e){next(e);}};exports.attachment=async(req,res,next)=>{try{res.json({success:true,data:{url:await support.attachmentUrl(req.params.id,req.params.attachmentId,req.user,false)}});}catch(e){next(e);}};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
const db=require("../../models"),credit=require("../../services/wholesale/credit.service"),settlements=require("../../services/wholesale/settlement.service"),{logActivity}=require("../../services/activity.service");exports.credit=async(req,res,next)=>{try{if(!req.get("Idempotency-Key"))throw Object.assign(new Error("Idempotency-Key is required"),{status:400,code:"IDEMPOTENCY_KEY_REQUIRED"});const result=await credit.post({...req.body,currency:req.body.currency.toUpperCase(),eventId:`CREDIT:${req.get("Idempotency-Key")}`,actorUserId:req.user.id});await logActivity({user:req.user,type:`CREDIT_${req.body.type}`,module:"Wholesale",targetId:result.entry.id,requestId:req.id});res.status(result.idempotent?200:201).json({success:true,data:result});}catch(e){next(e);}};exports.creditHistory=async(req,res,next)=>{try{res.json({success:true,data:await db.BusinessCreditLedgerEntry.findAll({where:{business_customer_id:req.params.businessId},order:[["occurred_at","DESC"]],limit:200})});}catch(e){next(e);}};exports.generate=async(req,res,next)=>{try{const result=await settlements.generate({...req.body,currency:req.body.currency.toUpperCase()});res.status(result.idempotent?200:201).json({success:true,data:result.settlement});}catch(e){next(e);}};exports.settlements=async(req,res,next)=>{try{res.json({success:true,data:await db.BusinessSettlement.findAll({order:[["period_end","DESC"]]})});}catch(e){next(e);}};exports.action=to=>async(req,res,next)=>{try{res.json({success:true,data:await settlements.transition(req.params.id,to,req.user.id)});}catch(e){next(e);}};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
const service=require("../../services/wholesale/creditPurchase.service"),{logActivity}=require("../../services/activity.service");exports.purchase=async(req,res,next)=>{try{const key=req.get("Idempotency-Key");if(!key)throw Object.assign(new Error("Idempotency-Key is required"),{status:400,code:"IDEMPOTENCY_KEY_REQUIRED"});const result=await service.purchase({orderId:req.params.id,userId:req.user.id,eventId:`CREDIT:ORDER:${key}`});if(!result.idempotent)await logActivity({user:req.user,type:"CREDIT_CAPTURED",module:"Wholesale",targetId:result.credit.entry.id,requestId:req.id});res.status(result.idempotent?200:201).json({success:true,data:{orderId:result.order.id,paymentStatus:result.order.payment_status,credit:result.credit}});}catch(e){next(e);}};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
const{QueryTypes}=require("sequelize"),db=require("../../models"),money=require("../../services/pricing/money");async function business(userId){const row=await db.BusinessCustomer.findOne({where:{user_id:userId,status:"ACTIVE"}});if(!row)throw Object.assign(new Error("Active business account required"),{status:403,code:"BUSINESS_REQUIRED"});return row;}exports.dashboard=async(req,res,next)=>{try{const b=await business(req.user.id),credit=await db.BusinessCreditAccount.findOne({where:{business_profile_id:b.business_customer_id}}),last=credit&&await db.BusinessCreditLedgerEntry.findOne({where:{business_credit_account_id:credit.id},order:[["occurred_at","DESC"]]}),used=last?String(last.balance_after):"0.00",tier=b.business_tier_id&&await db.BusinessTier.findByPk(b.business_tier_id),settlement=await db.BusinessSettlement.findOne({where:{business_customer_id:b.business_customer_id,status:["ISSUED","PARTIALLY_PAID","OVERDUE"]},order:[["due_date","ASC"]]}),stats=(await db.sequelize.query("SELECT COALESCE(SUM(grand_total),0) AS lifetimeVolume, COALESCE(SUM(CASE WHEN placed_at >= DATE_FORMAT(UTC_DATE(), '%Y-%m-01') THEN grand_total ELSE 0 END),0) AS monthlyVolume, COALESCE(SUM(discount_total),0) AS discountTotal, COALESCE(SUM(subtotal),0) AS subtotalTotal FROM orders WHERE business_customer_id = :id AND payment_status = 'PAID'",{replacements:{id:b.business_customer_id},type:QueryTypes.SELECT}))[0];res.json({success:true,data:{partnerId:b.partnerId,tier:tier&&{code:tier.code,name:tier.name},credit:credit&&{status:credit.status,currency:credit.currency,limit:String(credit.credit_limit),used,available:money.subtractFloor(credit.credit_limit,used)},monthlyVolume:String(stats.monthlyVolume),lifetimeVolume:String(stats.lifetimeVolume),discountTotal:String(stats.discountTotal),subtotalTotal:String(stats.subtotalTotal),nextSettlement:settlement&&{number:settlement.settlement_number,dueDate:settlement.due_date,balanceDue:String(settlement.balance_due)}}});}catch(e){next(e);}};exports.credit=async(req,res,next)=>{try{const b=await business(req.user.id),account=await db.BusinessCreditAccount.findOne({where:{business_profile_id:b.business_customer_id}}),rows=account?await db.BusinessCreditLedgerEntry.findAll({where:{business_credit_account_id:account.id},order:[["occurred_at","DESC"]],limit:100}):[];res.json({success:true,data:rows});}catch(e){next(e);}};exports.settlements=async(req,res,next)=>{try{const b=await business(req.user.id);res.json({success:true,data:await db.BusinessSettlement.findAll({where:{business_customer_id:b.business_customer_id},order:[["period_end","DESC"]]})});}catch(e){next(e);}};exports.settlement=async(req,res,next)=>{try{const b=await business(req.user.id),row=await db.BusinessSettlement.findOne({where:{id:req.params.id,business_customer_id:b.business_customer_id}});if(!row)throw Object.assign(new Error("Settlement not found"),{status:404,code:"NOT_FOUND"});res.json({success:true,data:row});}catch(e){next(e);}};exports.orders=async(req,res,next)=>{try{const b=await business(req.user.id);res.json({success:true,data:await db.Order.findAll({where:{business_customer_id:b.business_customer_id},order:[["createdAt","DESC"]],limit:20})});}catch(e){next(e);}};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("CouponRedemption",{id:{type:D.STRING,primaryKey:true},coupon_id:{type:D.STRING,allowNull:false},user_id:{type:D.STRING,allowNull:false},order_id:{type:D.STRING,allowNull:false},redeemed_at:{type:D.DATE,allowNull:false}},{tableName:"coupon_redemptions",timestamps:true,indexes:[{unique:true,fields:["coupon_id","order_id"]}]});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>{const M=s.define("Invoice",{id:{type:D.STRING,primaryKey:true},invoice_number:{type:D.STRING(80),allowNull:false,unique:true},order_id:{type:D.STRING,allowNull:false,unique:true},status:{type:D.ENUM("ISSUED","VOID"),allowNull:false},currency:{type:D.STRING(3),allowNull:false},subtotal:{type:D.DECIMAL(15,2),allowNull:false},discount_total:{type:D.DECIMAL(15,2),allowNull:false},shipping_amount:{type:D.DECIMAL(15,2),allowNull:false},tax_amount:{type:D.DECIMAL(15,2),allowNull:false},duty_amount:D.DECIMAL(15,2),grand_total:{type:D.DECIMAL(15,2),allowNull:false},issued_at:{type:D.DATE,allowNull:false},paid_at:D.DATE,document_upload_id:D.INTEGER},{tableName:"invoices",timestamps:true});M.associate=db=>M.belongsTo(db.Order,{foreignKey:"order_id",as:"order"});return M;};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>{const M=s.define("Order",{id:{type:D.STRING,primaryKey:true},order_number:{type:D.STRING(80),allowNull:false,unique:true},user_id:{type:D.STRING,allowNull:false},business_customer_id:D.STRING,checkout_session_id:{type:D.STRING,allowNull:false,unique:true},status:{type:D.ENUM("PENDING_PAYMENT","PAID","PROCESSING","READY_FOR_FULFILLMENT","PARTIALLY_FULFILLED","FULFILLED","COMPLETED","CANCELLED","REFUND_PENDING","PARTIALLY_REFUNDED","REFUNDED"),allowNull:false,defaultValue:"PENDING_PAYMENT"},payment_status:{type:D.ENUM("PENDING","AUTHORIZED","PAID","FAILED","CANCELLED","PARTIALLY_REFUNDED","REFUNDED"),allowNull:false,defaultValue:"PENDING"},fulfillment_status:{type:D.ENUM("UNFULFILLED","PARTIALLY_FULFILLED","FULFILLED"),allowNull:false,defaultValue:"UNFULFILLED"},currency:{type:D.STRING(3),allowNull:false},subtotal:{type:D.DECIMAL(15,2),allowNull:false},discount_total:{type:D.DECIMAL(15,2),allowNull:false},shipping_amount:{type:D.DECIMAL(15,2),allowNull:false},tax_amount:{type:D.DECIMAL(15,2),allowNull:false},duty_amount:D.DECIMAL(15,2),grand_total:{type:D.DECIMAL(15,2),allowNull:false},shipping_address_snapshot:{type:D.JSON,allowNull:false},billing_address_snapshot:{type:D.JSON,allowNull:false},shipping_method_snapshot:{type:D.JSON,allowNull:false},coupon_code:D.STRING(50),business_partner_snapshot:D.JSON,placed_at:{type:D.DATE,allowNull:false},cancelled_at:D.DATE,completed_at:D.DATE},{tableName:"orders",timestamps:true});M.associate=db=>{M.hasMany(db.OrderItem,{foreignKey:"order_id",as:"items"});M.hasMany(db.Payment,{foreignKey:"order_id",as:"payments"});M.hasOne(db.Invoice,{foreignKey:"order_id",as:"invoice"});};return M;};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>{const M=s.define("OrderItem",{id:{type:D.STRING,primaryKey:true},order_id:{type:D.STRING,allowNull:false},product_id:{type:D.STRING,allowNull:false},variant_id:{type:D.STRING,allowNull:false},reservation_key:{type:D.STRING(160),allowNull:false},sku:{type:D.STRING(100),allowNull:false},product_name:{type:D.STRING(255),allowNull:false},variant_description:D.STRING(255),quantity:{type:D.INTEGER,allowNull:false},unit_price:{type:D.DECIMAL(15,2),allowNull:false},discount_amount:{type:D.DECIMAL(15,2),allowNull:false},line_total:{type:D.DECIMAL(15,2),allowNull:false},currency:{type:D.STRING(3),allowNull:false},returned_quantity:{type:D.INTEGER,allowNull:false,defaultValue:0},refunded_quantity:{type:D.INTEGER,allowNull:false,defaultValue:0},metadata:D.JSON},{tableName:"order_items",timestamps:true,updatedAt:false});M.associate=db=>M.belongsTo(db.Order,{foreignKey:"order_id",as:"order"});return M;};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>{const M=s.define("Payment",{id:{type:D.STRING,primaryKey:true},order_id:{type:D.STRING,allowNull:false},payment_reference:{type:D.STRING(100),allowNull:false,unique:true},provider:{type:D.ENUM("PAYHERE","STRIPE","INTERNAL_CREDIT","CASH","MANUAL"),allowNull:false},method:{type:D.STRING(50),allowNull:false},status:{type:D.ENUM("PENDING","REQUIRES_ACTION","AUTHORIZED","PAID","FAILED","CANCELLED","PARTIALLY_REFUNDED","REFUNDED"),allowNull:false,defaultValue:"PENDING"},currency:{type:D.STRING(3),allowNull:false},amount:{type:D.DECIMAL(15,2),allowNull:false},provider_transaction_id:D.STRING,provider_customer_reference:D.STRING,failure_code:D.STRING,failure_message:D.STRING(500),authorized_at:D.DATE,paid_at:D.DATE,failed_at:D.DATE,cancelled_at:D.DATE},{tableName:"payments",timestamps:true});M.associate=db=>{M.hasMany(db.PaymentAttempt,{foreignKey:"payment_id",as:"attempts"});M.belongsTo(db.Order,{foreignKey:"order_id",as:"order"});};return M;};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("PaymentAttempt",{id:{type:D.STRING,primaryKey:true},payment_id:{type:D.STRING,allowNull:false},attempt_number:{type:D.INTEGER,allowNull:false},provider_request_id:{type:D.STRING,allowNull:false,unique:true},status:{type:D.STRING(40),allowNull:false},amount:{type:D.DECIMAL(15,2),allowNull:false},provider_response_code:D.STRING,provider_transaction_id:D.STRING,started_at:{type:D.DATE,allowNull:false},completed_at:D.DATE,metadata:D.JSON},{tableName:"payment_attempts",timestamps:true});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("PaymentWebhookEvent",{id:{type:D.STRING,primaryKey:true},provider:{type:D.STRING(30),allowNull:false},provider_event_id:{type:D.STRING(160),allowNull:false},event_type:{type:D.STRING(80),allowNull:false},payload_hash:{type:D.STRING(64),allowNull:false},processing_status:{type:D.ENUM("RECEIVED","PROCESSED","FAILED","IGNORED"),allowNull:false},received_at:{type:D.DATE,allowNull:false},processed_at:D.DATE,error_code:D.STRING},{tableName:"payment_webhook_events",timestamps:true,indexes:[{unique:true,fields:["provider","provider_event_id"]}]});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("Refund",{id:{type:D.STRING,primaryKey:true},refund_number:{type:D.STRING(80),allowNull:false,unique:true},operation_key:{type:D.STRING(160),allowNull:false,unique:true},order_id:{type:D.STRING,allowNull:false},payment_id:{type:D.STRING,allowNull:false},status:{type:D.ENUM("REQUESTED","APPROVED","PROCESSING","COMPLETED","FAILED","REJECTED","CANCELLED"),allowNull:false},amount:{type:D.DECIMAL(15,2),allowNull:false},currency:{type:D.STRING(3),allowNull:false},reason_code:{type:D.STRING(80),allowNull:false},reason_text:D.STRING(500),provider_refund_id:D.STRING,requested_by:{type:D.STRING,allowNull:false},approved_by:D.STRING,requested_at:{type:D.DATE,allowNull:false},processed_at:D.DATE},{tableName:"refunds",timestamps:true});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("RefundItem",{id:{type:D.STRING,primaryKey:true},refund_id:{type:D.STRING,allowNull:false},order_item_id:{type:D.STRING,allowNull:false},quantity:{type:D.INTEGER,allowNull:false},amount:{type:D.DECIMAL(15,2),allowNull:false}},{tableName:"refund_items",timestamps:true});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("ReturnItem",{id:{type:D.STRING,primaryKey:true},return_request_id:{type:D.STRING,allowNull:false},order_item_id:{type:D.STRING,allowNull:false},quantity:{type:D.INTEGER,allowNull:false},resolution:{type:D.ENUM("REFUND","EXCHANGE"),allowNull:false},condition:{type:D.ENUM("PENDING","RESTOCKABLE","DAMAGED","NON_RESTOCKABLE"),allowNull:false,defaultValue:"PENDING"},refund_amount:D.DECIMAL(15,2)},{tableName:"return_items",timestamps:true});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("ReturnRequest",{id:{type:D.STRING,primaryKey:true},return_number:{type:D.STRING(80),allowNull:false,unique:true},order_id:{type:D.STRING,allowNull:false},user_id:{type:D.STRING,allowNull:false},status:{type:D.ENUM("REQUESTED","APPROVED","REJECTED","AWAITING_RETURN","IN_TRANSIT","RECEIVED","INSPECTING","COMPLETED","CANCELLED"),allowNull:false},reason_code:{type:D.STRING(80),allowNull:false},reason_text:D.STRING(500),requested_at:{type:D.DATE,allowNull:false},approved_at:D.DATE,received_at:D.DATE,completed_at:D.DATE},{tableName:"return_requests",timestamps:true});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("HelpArticle",{id:{type:D.STRING,primaryKey:true},slug:{type:D.STRING(180),allowNull:false,unique:true},category_id:{type:D.STRING,allowNull:false},article_type:{type:D.ENUM("ARTICLE","FAQ"),allowNull:false,defaultValue:"ARTICLE"},status:{type:D.ENUM("DRAFT","PUBLISHED","ARCHIVED"),allowNull:false,defaultValue:"DRAFT"},sort_order:{type:D.INTEGER,allowNull:false,defaultValue:0},is_featured:{type:D.BOOLEAN,allowNull:false,defaultValue:false},published_at:D.DATE,created_by:{type:D.STRING,allowNull:false},updated_by:D.STRING},{tableName:"help_articles",timestamps:true});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("HelpArticleTranslation",{id:{type:D.STRING,primaryKey:true},article_id:{type:D.STRING,allowNull:false},locale:{type:D.ENUM("en","si","ta"),allowNull:false},title:{type:D.STRING(220),allowNull:false},summary:D.STRING(500),body:{type:D.TEXT("long"),allowNull:false},seo_title:D.STRING(220),seo_description:D.STRING(500)},{tableName:"help_article_translations",timestamps:true,indexes:[{unique:true,fields:["article_id","locale"]}]});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("HelpCategory",{id:{type:D.STRING,primaryKey:true},slug:{type:D.STRING(160),allowNull:false,unique:true},status:{type:D.ENUM("ACTIVE","INACTIVE"),allowNull:false,defaultValue:"ACTIVE"},sort_order:{type:D.INTEGER,allowNull:false,defaultValue:0},icon_key:D.STRING(80)},{tableName:"help_categories",timestamps:true});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("HelpCategoryTranslation",{id:{type:D.STRING,primaryKey:true},category_id:{type:D.STRING,allowNull:false},locale:{type:D.ENUM("en","si","ta"),allowNull:false},name:{type:D.STRING(160),allowNull:false},description:D.STRING(500)},{tableName:"help_category_translations",timestamps:true,indexes:[{unique:true,fields:["category_id","locale"]}]});
|
|
||||||
+49
-285
@@ -9,19 +9,24 @@
|
|||||||
|
|
||||||
// app/models/index.js
|
// app/models/index.js
|
||||||
|
|
||||||
|
|
||||||
const { Sequelize, DataTypes } = require("sequelize");
|
const { Sequelize, DataTypes } = require("sequelize");
|
||||||
const dbConfig = require("../config/db.config");
|
const dbConfig = require("../config/db.config");
|
||||||
|
|
||||||
const loggingOption =
|
const loggingOption = process.env.NODE_ENV === "development" ? console.log : false;
|
||||||
process.env.NODE_ENV === "development" ? console.log : false;
|
|
||||||
|
|
||||||
const sequelize = new Sequelize(dbConfig.DB, dbConfig.USER, dbConfig.PASSWORD, {
|
const sequelize = new Sequelize(
|
||||||
host: dbConfig.HOST,
|
dbConfig.DB,
|
||||||
port: dbConfig.PORT,
|
dbConfig.USER,
|
||||||
dialect: dbConfig.DIALECT,
|
dbConfig.PASSWORD,
|
||||||
pool: dbConfig.pool,
|
{
|
||||||
logging: loggingOption,
|
host: dbConfig.HOST,
|
||||||
});
|
port: dbConfig.PORT,
|
||||||
|
dialect: dbConfig.DIALECT,
|
||||||
|
pool: dbConfig.pool,
|
||||||
|
logging: loggingOption
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const db = {};
|
const db = {};
|
||||||
|
|
||||||
@@ -33,34 +38,16 @@ db.sequelize = sequelize;
|
|||||||
// User and Authentication
|
// User and Authentication
|
||||||
db.User = require("./user/user.model")(sequelize, DataTypes);
|
db.User = require("./user/user.model")(sequelize, DataTypes);
|
||||||
db.Customer = require("./user/customer.model")(sequelize, DataTypes);
|
db.Customer = require("./user/customer.model")(sequelize, DataTypes);
|
||||||
db.BusinessCustomer = require("./user/businessCustomer.model")(
|
db.BusinessCustomer = require("./user/businessCustomer.model")(sequelize, DataTypes);
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.AuthSession = require("./user/authSession.model")(sequelize, DataTypes);
|
db.AuthSession = require("./user/authSession.model")(sequelize, DataTypes);
|
||||||
db.UserIdentity = require("./user/userIdentity.model")(sequelize, DataTypes);
|
db.UserIdentity = require("./user/userIdentity.model")(sequelize, DataTypes);
|
||||||
db.UserActivity = require("./activities/userActivities.model")(
|
db.UserActivity = require("./activities/userActivities.model")(sequelize, DataTypes);
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.Profile = require("./user/profile.model")(sequelize, DataTypes);
|
db.Profile = require("./user/profile.model")(sequelize, DataTypes);
|
||||||
db.Address = require("./user/address.model")(sequelize, DataTypes);
|
db.Address = require("./user/address.model")(sequelize, DataTypes);
|
||||||
db.BusinessApplication = require("./user/businessApplication.model")(
|
db.BusinessApplication = require("./user/businessApplication.model")(sequelize, DataTypes);
|
||||||
sequelize,
|
db.BusinessContact = require("./user/businessContact.model")(sequelize, DataTypes);
|
||||||
DataTypes,
|
db.BusinessCreditAccount = require("./user/businessCreditAccount.model")(sequelize, DataTypes);
|
||||||
);
|
db.SettlementTerm = require("./user/settlementTerm.model")(sequelize, DataTypes);
|
||||||
db.BusinessContact = require("./user/businessContact.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.BusinessCreditAccount = require("./user/businessCreditAccount.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.SettlementTerm = require("./user/settlementTerm.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Uploads
|
// Uploads
|
||||||
db.Upload = require("./upload/upload.model")(sequelize, DataTypes);
|
db.Upload = require("./upload/upload.model")(sequelize, DataTypes);
|
||||||
@@ -68,278 +55,55 @@ db.Upload = require("./upload/upload.model")(sequelize, DataTypes);
|
|||||||
// Roles and Permissions
|
// Roles and Permissions
|
||||||
db.roles = require("./permission/role.model")(sequelize, DataTypes);
|
db.roles = require("./permission/role.model")(sequelize, DataTypes);
|
||||||
db.permission = require("./permission/permission.model")(sequelize, DataTypes);
|
db.permission = require("./permission/permission.model")(sequelize, DataTypes);
|
||||||
db.rolePermission = require("./permission/rolePermission.model")(
|
db.rolePermission = require("./permission/rolePermission.model")(sequelize, DataTypes);
|
||||||
sequelize,
|
db.userPermission = require("./permission/userPermission.model")(sequelize, DataTypes);
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.userPermission = require("./permission/userPermission.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.UserRole = require("./permission/userRole.model")(sequelize, DataTypes);
|
db.UserRole = require("./permission/userRole.model")(sequelize, DataTypes);
|
||||||
|
|
||||||
// Document Management
|
// Document Management
|
||||||
db.Document = require("./document/document.model")(sequelize, DataTypes);
|
db.Document = require("./document/document.model")(sequelize, DataTypes);
|
||||||
db.DocumentType = require("./document/documentType.model")(
|
db.DocumentType = require("./document/documentType.model")(sequelize, DataTypes);
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reference Numbers
|
// Reference Numbers
|
||||||
db.referenceNumber = require("./referenceNumbers/referenceNumber.model")(
|
db.referenceNumber = require("./referenceNumbers/referenceNumber.model")(sequelize, DataTypes);
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Notifications
|
// Notifications
|
||||||
db.notification = require("./notification/notification.model")(
|
db.notification = require("./notification/notification.model")(sequelize, DataTypes);
|
||||||
sequelize,
|
db.userNotification = require("./notification/userNotification.model")(sequelize, DataTypes);
|
||||||
DataTypes,
|
db.NotificationDelivery = require("./notification/notificationDelivery.model")(sequelize, DataTypes);
|
||||||
);
|
|
||||||
db.userNotification = require("./notification/userNotification.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.NotificationDelivery = require("./notification/notificationDelivery.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Catalogue and localized content
|
// Catalogue and localized content
|
||||||
db.Brand = require("./catalogue/brand.model")(sequelize, DataTypes);
|
db.Brand = require("./catalogue/brand.model")(sequelize, DataTypes);
|
||||||
db.Category = require("./catalogue/category.model")(sequelize, DataTypes);
|
db.Category = require("./catalogue/category.model")(sequelize, DataTypes);
|
||||||
db.CategoryTranslation = require("./catalogue/categoryTranslation.model")(
|
db.CategoryTranslation = require("./catalogue/categoryTranslation.model")(sequelize, DataTypes);
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.Product = require("./catalogue/product.model")(sequelize, DataTypes);
|
db.Product = require("./catalogue/product.model")(sequelize, DataTypes);
|
||||||
db.ProductTranslation = require("./catalogue/productTranslation.model")(
|
db.ProductTranslation = require("./catalogue/productTranslation.model")(sequelize, DataTypes);
|
||||||
sequelize,
|
db.ProductCategory = require("./catalogue/productCategory.model")(sequelize, DataTypes);
|
||||||
DataTypes,
|
db.ProductVariant = require("./catalogue/productVariant.model")(sequelize, DataTypes);
|
||||||
);
|
db.ProductOption = require("./catalogue/productOption.model")(sequelize, DataTypes);
|
||||||
db.ProductCategory = require("./catalogue/productCategory.model")(
|
db.ProductOptionValue = require("./catalogue/productOptionValue.model")(sequelize, DataTypes);
|
||||||
sequelize,
|
db.VariantOptionValue = require("./catalogue/variantOptionValue.model")(sequelize, DataTypes);
|
||||||
DataTypes,
|
db.ProductAttribute = require("./catalogue/productAttribute.model")(sequelize, DataTypes);
|
||||||
);
|
db.ProductMedia = require("./catalogue/productMedia.model")(sequelize, DataTypes);
|
||||||
db.ProductVariant = require("./catalogue/productVariant.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.ProductOption = require("./catalogue/productOption.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.ProductOptionValue = require("./catalogue/productOptionValue.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.VariantOptionValue = require("./catalogue/variantOptionValue.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.ProductAttribute = require("./catalogue/productAttribute.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.ProductMedia = require("./catalogue/productMedia.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.Collection = require("./catalogue/collection.model")(sequelize, DataTypes);
|
db.Collection = require("./catalogue/collection.model")(sequelize, DataTypes);
|
||||||
db.CollectionTranslation = require("./catalogue/collectionTranslation.model")(
|
db.CollectionTranslation = require("./catalogue/collectionTranslation.model")(sequelize, DataTypes);
|
||||||
sequelize,
|
db.CollectionProduct = require("./catalogue/collectionProduct.model")(sequelize, DataTypes);
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.CollectionProduct = require("./catalogue/collectionProduct.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.SizeGuide = require("./catalogue/sizeGuide.model")(sequelize, DataTypes);
|
db.SizeGuide = require("./catalogue/sizeGuide.model")(sequelize, DataTypes);
|
||||||
db.ProductRelation = require("./catalogue/productRelation.model")(
|
db.ProductRelation = require("./catalogue/productRelation.model")(sequelize, DataTypes);
|
||||||
sequelize,
|
db.ProductReview = require("./catalogue/productReview.model")(sequelize, DataTypes);
|
||||||
DataTypes,
|
db.Warehouse=require("./inventory/warehouse.model")(sequelize,DataTypes);db.InventoryBalance=require("./inventory/inventoryBalance.model")(sequelize,DataTypes);db.InventoryTransaction=require("./inventory/inventoryTransaction.model")(sequelize,DataTypes);db.InventoryReservation=require("./inventory/inventoryReservation.model")(sequelize,DataTypes);db.InventoryTransfer=require("./inventory/inventoryTransfer.model")(sequelize,DataTypes);
|
||||||
);
|
db.BusinessTier=require("./pricing/businessTier.model")(sequelize,DataTypes);db.VariantBusinessPrice=require("./pricing/variantBusinessPrice.model")(sequelize,DataTypes);db.BusinessPriceTier=require("./pricing/businessPriceTier.model")(sequelize,DataTypes);
|
||||||
db.ProductReview = require("./catalogue/productReview.model")(
|
db.Promotion=require("./merchandising/promotion.model")(sequelize,DataTypes);db.PromotionTarget=require("./merchandising/promotionTarget.model")(sequelize,DataTypes);db.Coupon=require("./merchandising/coupon.model")(sequelize,DataTypes);db.Banner=require("./merchandising/banner.model")(sequelize,DataTypes);db.BannerTranslation=require("./merchandising/bannerTranslation.model")(sequelize,DataTypes);
|
||||||
sequelize,
|
db.Cart=require("./shopping/cart.model")(sequelize,DataTypes);db.CartItem=require("./shopping/cartItem.model")(sequelize,DataTypes);db.WishlistItem=require("./shopping/wishlistItem.model")(sequelize,DataTypes);db.CheckoutSession=require("./shopping/checkoutSession.model")(sequelize,DataTypes);db.CheckoutItem=require("./shopping/checkoutItem.model")(sequelize,DataTypes);
|
||||||
DataTypes,
|
db.ShippingZone=require("./shipping/shippingZone.model")(sequelize,DataTypes);db.ShippingZoneRegion=require("./shipping/shippingZoneRegion.model")(sequelize,DataTypes);db.ShippingMethod=require("./shipping/shippingMethod.model")(sequelize,DataTypes);db.ShippingRate=require("./shipping/shippingRate.model")(sequelize,DataTypes);
|
||||||
);
|
|
||||||
db.Warehouse = require("./inventory/warehouse.model")(sequelize, DataTypes);
|
|
||||||
db.InventoryBalance = require("./inventory/inventoryBalance.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.InventoryTransaction = require("./inventory/inventoryTransaction.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.InventoryReservation = require("./inventory/inventoryReservation.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.InventoryTransfer = require("./inventory/inventoryTransfer.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.BusinessTier = require("./pricing/businessTier.model")(sequelize, DataTypes);
|
|
||||||
db.VariantBusinessPrice = require("./pricing/variantBusinessPrice.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.BusinessPriceTier = require("./pricing/businessPriceTier.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.Promotion = require("./merchandising/promotion.model")(sequelize, DataTypes);
|
|
||||||
db.PromotionTarget = require("./merchandising/promotionTarget.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.Coupon = require("./merchandising/coupon.model")(sequelize, DataTypes);
|
|
||||||
db.Banner = require("./merchandising/banner.model")(sequelize, DataTypes);
|
|
||||||
db.BannerTranslation = require("./merchandising/bannerTranslation.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.Cart = require("./shopping/cart.model")(sequelize, DataTypes);
|
|
||||||
db.CartItem = require("./shopping/cartItem.model")(sequelize, DataTypes);
|
|
||||||
db.WishlistItem = require("./shopping/wishlistItem.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.CheckoutSession = require("./shopping/checkoutSession.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.CheckoutItem = require("./shopping/checkoutItem.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.ShippingZone = require("./shipping/shippingZone.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.ShippingZoneRegion = require("./shipping/shippingZoneRegion.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.ShippingMethod = require("./shipping/shippingMethod.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.ShippingRate = require("./shipping/shippingRate.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.Order = require("./commerce/order.model")(sequelize, DataTypes);
|
|
||||||
db.OrderItem = require("./commerce/orderItem.model")(sequelize, DataTypes);
|
|
||||||
db.Payment = require("./commerce/payment.model")(sequelize, DataTypes);
|
|
||||||
db.PaymentAttempt = require("./commerce/paymentAttempt.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.PaymentWebhookEvent = require("./commerce/paymentWebhookEvent.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.Invoice = require("./commerce/invoice.model")(sequelize, DataTypes);
|
|
||||||
db.Refund = require("./commerce/refund.model")(sequelize, DataTypes);
|
|
||||||
db.RefundItem = require("./commerce/refundItem.model")(sequelize, DataTypes);
|
|
||||||
db.ReturnRequest = require("./commerce/returnRequest.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.ReturnItem = require("./commerce/returnItem.model")(sequelize, DataTypes);
|
|
||||||
db.CouponRedemption = require("./commerce/couponRedemption.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.RiderProfile = require("./logistics/riderProfile.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.Shipment = require("./logistics/shipment.model")(sequelize, DataTypes);
|
|
||||||
db.ShipmentItem = require("./logistics/shipmentItem.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.ShipmentAssignment = require("./logistics/shipmentAssignment.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.ShipmentEvent = require("./logistics/shipmentEvent.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.ShipmentProof = require("./logistics/shipmentProof.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.LoyaltyAccount = require("./loyalty/loyaltyAccount.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.LoyaltyTier = require("./loyalty/loyaltyTier.model")(sequelize, DataTypes);
|
|
||||||
db.LoyaltyTierHistory = require("./loyalty/loyaltyTierHistory.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.LoyaltyEarnRule = require("./loyalty/loyaltyEarnRule.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.LoyaltyLedgerEntry = require("./loyalty/loyaltyLedgerEntry.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.LoyaltyPointAllocation = require("./loyalty/loyaltyPointAllocation.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.LoyaltyReward = require("./loyalty/loyaltyReward.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.LoyaltyRedemption = require("./loyalty/loyaltyRedemption.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.Referral = require("./loyalty/referral.model")(sequelize, DataTypes);
|
|
||||||
db.CustomerCouponEntitlement =
|
|
||||||
require("./loyalty/customerCouponEntitlement.model")(sequelize, DataTypes);
|
|
||||||
db.BusinessCreditLedgerEntry =
|
|
||||||
require("./wholesale/businessCreditLedgerEntry.model")(sequelize, DataTypes);
|
|
||||||
db.BusinessSettlement = require("./wholesale/businessSettlement.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.BusinessSettlementItem = require("./wholesale/businessSettlementItem.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.PaymentAllocation = require("./wholesale/paymentAllocation.model")(
|
|
||||||
sequelize,
|
|
||||||
DataTypes,
|
|
||||||
);
|
|
||||||
db.SupportCategory = require("./support/supportCategory.model")(sequelize, DataTypes);
|
|
||||||
db.SupportTicket = require("./support/supportTicket.model")(sequelize, DataTypes);
|
|
||||||
db.SupportMessage = require("./support/supportMessage.model")(sequelize, DataTypes);
|
|
||||||
db.SupportTicketEvent = require("./support/supportTicketEvent.model")(sequelize, DataTypes);
|
|
||||||
db.SupportTicketLink = require("./support/supportTicketLink.model")(sequelize, DataTypes);
|
|
||||||
db.SupportAttachment = require("./support/supportAttachment.model")(sequelize, DataTypes);
|
|
||||||
db.SupportSlaPolicy = require("./support/supportSlaPolicy.model")(sequelize, DataTypes);
|
|
||||||
db.SupportEscalation = require("./support/supportEscalation.model")(sequelize, DataTypes);
|
|
||||||
db.HelpCategory = require("./help/helpCategory.model")(sequelize, DataTypes);
|
|
||||||
db.HelpCategoryTranslation = require("./help/helpCategoryTranslation.model")(sequelize, DataTypes);
|
|
||||||
db.HelpArticle = require("./help/helpArticle.model")(sequelize, DataTypes);
|
|
||||||
db.HelpArticleTranslation = require("./help/helpArticleTranslation.model")(sequelize, DataTypes);
|
|
||||||
db.NewsletterSubscription = require("./marketing/newsletterSubscription.model")(sequelize, DataTypes);
|
|
||||||
db.ProductInteractionEvent = require("./recommendation/productInteractionEvent.model")(sequelize, DataTypes);
|
|
||||||
|
|
||||||
/* Associations */
|
/* Associations */
|
||||||
Object.keys(db).forEach((model) => {
|
Object.keys(db).forEach(model => {
|
||||||
if (db[model].associate) {
|
if (db[model].associate) {
|
||||||
db[model].associate(db);
|
db[model].associate(db);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = db;
|
module.exports = db;
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>{const M=s.define("RiderProfile",{id:{type:D.STRING,primaryKey:true},user_id:{type:D.STRING,allowNull:false,unique:true},employee_code:{type:D.STRING(60),allowNull:false,unique:true},phone_override:D.STRING(30),vehicle_type:D.STRING(60),vehicle_registration:D.STRING(60),status:{type:D.ENUM("ACTIVE","INACTIVE","SUSPENDED"),allowNull:false,defaultValue:"ACTIVE"},availability_status:{type:D.ENUM("AVAILABLE","BUSY","OFFLINE"),allowNull:false,defaultValue:"OFFLINE"},current_latitude:D.DECIMAL(10,7),current_longitude:D.DECIMAL(10,7),last_location_at:D.DATE,max_active_assignments:{type:D.INTEGER,allowNull:false,defaultValue:1}},{tableName:"rider_profiles",timestamps:true});M.associate=db=>M.belongsTo(db.User,{foreignKey:"user_id",as:"user"});return M;};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>{const M=s.define("Shipment",{id:{type:D.STRING,primaryKey:true},shipment_number:{type:D.STRING(80),allowNull:false,unique:true},order_id:D.STRING,return_request_id:D.STRING,type:{type:D.ENUM("CUSTOMER_DELIVERY","RETURN_PICKUP"),allowNull:false},status:{type:D.ENUM("PENDING","READY_FOR_ASSIGNMENT","ASSIGNED","PICKUP_PENDING","PICKED_UP","IN_TRANSIT","OUT_FOR_DELIVERY","DELIVERED","DELIVERY_FAILED","RESCHEDULED","CANCELLED"),allowNull:false,defaultValue:"PENDING"},shipping_method_code:D.STRING(60),shipping_method_name:D.STRING(150),recipient_name:{type:D.STRING(150),allowNull:false},recipient_phone:{type:D.STRING(30),allowNull:false},address_snapshot:{type:D.JSON,allowNull:false},assigned_rider_id:D.STRING,assigned_at:D.DATE,picked_up_at:D.DATE,out_for_delivery_at:D.DATE,delivered_at:D.DATE,failed_at:D.DATE,cancelled_at:D.DATE,scheduled_date:D.DATE,time_window_start:D.STRING(20),time_window_end:D.STRING(20),delivery_attempt_count:{type:D.INTEGER,allowNull:false,defaultValue:0},notes:D.STRING(500)},{tableName:"shipments",timestamps:true});M.associate=db=>{M.hasMany(db.ShipmentItem,{foreignKey:"shipment_id",as:"items"});M.hasMany(db.ShipmentAssignment,{foreignKey:"shipment_id",as:"assignments"});M.hasMany(db.ShipmentEvent,{foreignKey:"shipment_id",as:"events"});M.hasMany(db.ShipmentProof,{foreignKey:"shipment_id",as:"proofs"});M.belongsTo(db.Order,{foreignKey:"order_id",as:"order"});};return M;};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("ShipmentAssignment",{id:{type:D.STRING,primaryKey:true},shipment_id:{type:D.STRING,allowNull:false},rider_id:{type:D.STRING,allowNull:false},status:{type:D.ENUM("ASSIGNED","ACCEPTED","REJECTED","UNASSIGNED","COMPLETED"),allowNull:false},assigned_by:{type:D.STRING,allowNull:false},assigned_at:{type:D.DATE,allowNull:false},accepted_at:D.DATE,rejected_at:D.DATE,unassigned_at:D.DATE,completed_at:D.DATE,reason:D.STRING(500)},{tableName:"shipment_assignments",timestamps:true});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("ShipmentEvent",{id:{type:D.STRING,primaryKey:true},shipment_id:{type:D.STRING,allowNull:false},event_id:{type:D.STRING(160),allowNull:false,unique:true},type:{type:D.STRING(80),allowNull:false},status:{type:D.STRING(40),allowNull:false},actor_user_id:D.STRING,latitude:D.DECIMAL(10,7),longitude:D.DECIMAL(10,7),note:D.STRING(500),occurred_at:{type:D.DATE,allowNull:false}},{tableName:"shipment_events",timestamps:true,updatedAt:false});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>{const M=s.define("ShipmentItem",{id:{type:D.STRING,primaryKey:true},shipment_id:{type:D.STRING,allowNull:false},order_item_id:{type:D.STRING,allowNull:false},quantity:{type:D.INTEGER,allowNull:false}},{tableName:"shipment_items",timestamps:true,indexes:[{unique:true,fields:["shipment_id","order_item_id"]}]});M.associate=db=>M.belongsTo(db.Shipment,{foreignKey:"shipment_id",as:"shipment"});return M;};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("ShipmentProof",{id:{type:D.STRING,primaryKey:true},shipment_id:{type:D.STRING,allowNull:false},event_id:{type:D.STRING(160),allowNull:false,unique:true},type:{type:D.ENUM("SIGNATURE","PHOTO","RECIPIENT_CONFIRMATION","RETURN_PICKUP_PHOTO"),allowNull:false},recipient_name:D.STRING(150),signature_upload_id:D.INTEGER,photo_upload_id:D.INTEGER,notes:D.STRING(500),captured_at:{type:D.DATE,allowNull:false},captured_by:{type:D.STRING,allowNull:false}},{tableName:"shipment_proofs",timestamps:true});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("CustomerCouponEntitlement",{id:{type:D.STRING,primaryKey:true},user_id:{type:D.STRING,allowNull:false},coupon_id:{type:D.STRING,allowNull:false},source_type:{type:D.STRING(40),allowNull:false},source_id:{type:D.STRING,allowNull:false},status:{type:D.ENUM("ACTIVE","USED","EXPIRED","REVOKED"),allowNull:false},issued_at:{type:D.DATE,allowNull:false},used_at:D.DATE,expires_at:D.DATE},{tableName:"customer_coupon_entitlements",timestamps:true,indexes:[{unique:true,fields:["user_id","coupon_id","source_type","source_id"]}]});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("LoyaltyAccount",{id:{type:D.STRING,primaryKey:true},user_id:{type:D.STRING,allowNull:false,unique:true},status:{type:D.ENUM("ACTIVE","SUSPENDED","CLOSED"),allowNull:false,defaultValue:"ACTIVE"},current_tier_id:D.STRING,lifetime_points_earned:{type:D.INTEGER,allowNull:false,defaultValue:0},lifetime_points_redeemed:{type:D.INTEGER,allowNull:false,defaultValue:0},available_points:{type:D.INTEGER,allowNull:false,defaultValue:0},pending_points:{type:D.INTEGER,allowNull:false,defaultValue:0},points_debt:{type:D.INTEGER,allowNull:false,defaultValue:0},last_activity_at:D.DATE},{tableName:"loyalty_accounts",timestamps:true});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("LoyaltyEarnRule",{id:{type:D.STRING,primaryKey:true},source_type:{type:D.ENUM("PURCHASE","VERIFIED_REVIEW","REFERRAL","BIRTHDAY"),allowNull:false},status:{type:D.ENUM("ACTIVE","INACTIVE"),allowNull:false},points_per_amount:{type:D.INTEGER},amount_unit:D.DECIMAL(15,2),fixed_points:D.INTEGER,minimum_amount:D.DECIMAL(15,2),maximum_points:D.INTEGER,expiry_days:D.INTEGER,effective_from:{type:D.DATE,allowNull:false},effective_to:D.DATE},{tableName:"loyalty_earn_rules",timestamps:true});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("LoyaltyLedgerEntry",{id:{type:D.STRING,primaryKey:true},event_id:{type:D.STRING(180),allowNull:false,unique:true},loyalty_account_id:{type:D.STRING,allowNull:false},type:{type:D.ENUM("EARN","REDEEM","EXPIRE","ADJUSTMENT","REVERSAL"),allowNull:false},source_type:{type:D.STRING(50),allowNull:false},source_id:D.STRING,points_delta:{type:D.INTEGER,allowNull:false},balance_after:{type:D.INTEGER,allowNull:false},status:{type:D.ENUM("POSTED"),allowNull:false,defaultValue:"POSTED"},expires_at:D.DATE,description_code:D.STRING(80),metadata:D.JSON,occurred_at:{type:D.DATE,allowNull:false}},{tableName:"loyalty_ledger_entries",timestamps:true,updatedAt:false});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("LoyaltyPointAllocation",{id:{type:D.STRING,primaryKey:true},earn_ledger_id:{type:D.STRING,allowNull:false},loyalty_account_id:{type:D.STRING,allowNull:false},original_points:{type:D.INTEGER,allowNull:false},remaining_points:{type:D.INTEGER,allowNull:false},expires_at:D.DATE},{tableName:"loyalty_point_allocations",timestamps:true});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("LoyaltyRedemption",{id:{type:D.STRING,primaryKey:true},redemption_number:{type:D.STRING(80),allowNull:false,unique:true},idempotency_key:{type:D.STRING(160),allowNull:false},loyalty_account_id:{type:D.STRING,allowNull:false},reward_id:{type:D.STRING,allowNull:false},points_cost:{type:D.INTEGER,allowNull:false},status:{type:D.ENUM("COMPLETED","CANCELLED"),allowNull:false},fulfilled_reference:D.STRING,redeemed_at:{type:D.DATE,allowNull:false}},{tableName:"loyalty_redemptions",timestamps:true,indexes:[{unique:true,fields:["loyalty_account_id","idempotency_key"]}]});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("LoyaltyReward",{id:{type:D.STRING,primaryKey:true},code:{type:D.STRING(60),allowNull:false,unique:true},name:{type:D.STRING(160),allowNull:false},description:D.TEXT,type:{type:D.ENUM("COUPON","MANUAL"),allowNull:false},points_cost:{type:D.INTEGER,allowNull:false},status:{type:D.ENUM("ACTIVE","INACTIVE","ARCHIVED"),allowNull:false},starts_at:D.DATE,ends_at:D.DATE,stock_limit:D.INTEGER,per_user_limit:D.INTEGER,configuration:D.JSON},{tableName:"loyalty_rewards",timestamps:true});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("LoyaltyTier",{id:{type:D.STRING,primaryKey:true},code:{type:D.STRING(60),allowNull:false,unique:true},name:{type:D.STRING(120),allowNull:false},status:{type:D.ENUM("ACTIVE","INACTIVE"),allowNull:false,defaultValue:"ACTIVE"},rank:{type:D.INTEGER,allowNull:false},qualification_threshold:{type:D.INTEGER,allowNull:false},qualification_metric:{type:D.ENUM("LIFETIME_POINTS"),allowNull:false,defaultValue:"LIFETIME_POINTS"},benefits_json:D.JSON,sort_order:{type:D.INTEGER,allowNull:false,defaultValue:0}},{tableName:"loyalty_tiers",timestamps:true});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("LoyaltyTierHistory",{id:{type:D.STRING,primaryKey:true},loyalty_account_id:{type:D.STRING,allowNull:false},from_tier_id:D.STRING,to_tier_id:{type:D.STRING,allowNull:false},reason:{type:D.STRING(120),allowNull:false},effective_at:{type:D.DATE,allowNull:false}},{tableName:"loyalty_tier_history",timestamps:true});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("Referral",{id:{type:D.STRING,primaryKey:true},referrer_user_id:{type:D.STRING,allowNull:false},referred_user_id:{type:D.STRING,unique:true},referral_code:{type:D.STRING(32),allowNull:false,unique:true},status:{type:D.ENUM("PENDING","REGISTERED","QUALIFIED","REWARDED","REJECTED","EXPIRED"),allowNull:false},qualified_order_id:D.STRING,qualified_at:D.DATE,rewarded_at:D.DATE},{tableName:"referrals",timestamps:true});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("NewsletterSubscription",{id:{type:D.STRING,primaryKey:true},email_normalized:{type:D.STRING(254),allowNull:false,unique:true},user_id:D.STRING,status:{type:D.ENUM("PENDING","SUBSCRIBED","UNSUBSCRIBED"),allowNull:false},locale:{type:D.ENUM("en","si","ta"),allowNull:false,defaultValue:"en"},source:{type:D.ENUM("HOME_FOOTER","CHECKOUT","ACCOUNT","ADMIN_IMPORT"),allowNull:false},consented_at:D.DATE,confirmed_at:D.DATE,unsubscribed_at:D.DATE,unsubscribe_token_hash:{type:D.STRING(64),allowNull:false}},{tableName:"newsletter_subscriptions",timestamps:true});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("ProductInteractionEvent",{id:{type:D.STRING,primaryKey:true},event_id:{type:D.STRING(180),allowNull:false,unique:true},user_id:D.STRING,session_key_hash:D.STRING(64),product_id:{type:D.STRING,allowNull:false},variant_id:D.STRING,event_type:{type:D.ENUM("PRODUCT_VIEW","PRODUCT_CLICK","WISHLIST_ADD","CART_ADD","CHECKOUT_START","PURCHASE"),allowNull:false},source:{type:D.STRING(40),allowNull:false},occurred_at:{type:D.DATE,allowNull:false}},{tableName:"recommendation_events",timestamps:true,updatedAt:false});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("SupportAttachment",{id:{type:D.STRING,primaryKey:true},ticket_id:{type:D.STRING,allowNull:false},message_id:D.STRING,upload_id:{type:D.INTEGER,allowNull:false},uploaded_by:{type:D.STRING,allowNull:false},visibility:{type:D.ENUM("CUSTOMER_VISIBLE","INTERNAL"),allowNull:false,defaultValue:"CUSTOMER_VISIBLE"}},{tableName:"support_attachments",timestamps:true,updatedAt:false});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("SupportCategory",{id:{type:D.STRING,primaryKey:true},code:{type:D.STRING(60),allowNull:false,unique:true},status:{type:D.ENUM("ACTIVE","INACTIVE"),allowNull:false,defaultValue:"ACTIVE"},translations:{type:D.JSON,allowNull:false,defaultValue:{}},sort_order:{type:D.INTEGER,allowNull:false,defaultValue:0}},{tableName:"support_categories",timestamps:true});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("SupportEscalation",{id:{type:D.STRING,primaryKey:true},event_id:{type:D.STRING(180),allowNull:false,unique:true},ticket_id:{type:D.STRING,allowNull:false},type:{type:D.ENUM("FIRST_RESPONSE_OVERDUE","RESOLUTION_OVERDUE","MANUAL"),allowNull:false},level:{type:D.INTEGER,allowNull:false,defaultValue:1},reason:{type:D.STRING(300),allowNull:false},status:{type:D.ENUM("OPEN","ACKNOWLEDGED"),allowNull:false,defaultValue:"OPEN"},acknowledged_at:D.DATE},{tableName:"support_escalations",timestamps:true});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("SupportMessage",{id:{type:D.STRING,primaryKey:true},ticket_id:{type:D.STRING,allowNull:false},sender_user_id:D.STRING,sender_type:{type:D.ENUM("CUSTOMER","AGENT","SYSTEM"),allowNull:false},message_type:{type:D.ENUM("MESSAGE","SYSTEM"),allowNull:false,defaultValue:"MESSAGE"},body:{type:D.TEXT,allowNull:false},visibility:{type:D.ENUM("CUSTOMER_VISIBLE","INTERNAL"),allowNull:false,defaultValue:"CUSTOMER_VISIBLE"}},{tableName:"support_messages",timestamps:true,updatedAt:false});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("SupportSlaPolicy",{id:{type:D.STRING,primaryKey:true},name:{type:D.STRING(120),allowNull:false},status:{type:D.ENUM("ACTIVE","INACTIVE"),allowNull:false},priority:{type:D.ENUM("LOW","NORMAL","HIGH","URGENT"),allowNull:false,unique:true},first_response_minutes:{type:D.INTEGER,allowNull:false},resolution_minutes:{type:D.INTEGER,allowNull:false},business_hours_mode:{type:D.ENUM("CLOCK_TIME"),allowNull:false,defaultValue:"CLOCK_TIME"}},{tableName:"support_sla_policies",timestamps:true});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("SupportTicket",{id:{type:D.STRING,primaryKey:true},ticket_number:{type:D.STRING(80),allowNull:false,unique:true},customer_user_id:{type:D.STRING,allowNull:false},business_customer_id:D.STRING,subject:{type:D.STRING(200),allowNull:false},category_id:D.STRING,priority:{type:D.ENUM("LOW","NORMAL","HIGH","URGENT"),allowNull:false,defaultValue:"NORMAL"},status:{type:D.ENUM("OPEN","ASSIGNED","WAITING_FOR_CUSTOMER","WAITING_FOR_SUPPORT","RESOLVED","CLOSED","CANCELLED"),allowNull:false,defaultValue:"OPEN"},assigned_agent_id:D.STRING,source:{type:D.ENUM("WEB","MOBILE","ADMIN"),allowNull:false,defaultValue:"WEB"},sla_policy_id:D.STRING,first_response_due_at:D.DATE,resolution_due_at:D.DATE,first_response_at:D.DATE,resolved_at:D.DATE,closed_at:D.DATE,last_customer_message_at:D.DATE,last_agent_message_at:D.DATE},{tableName:"support_tickets",timestamps:true});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("SupportTicketEvent",{id:{type:D.STRING,primaryKey:true},event_id:{type:D.STRING(180),allowNull:false,unique:true},ticket_id:{type:D.STRING,allowNull:false},actor_user_id:D.STRING,type:{type:D.STRING(60),allowNull:false},from_value:D.STRING,to_value:D.STRING,metadata:D.JSON,occurred_at:{type:D.DATE,allowNull:false}},{tableName:"support_ticket_events",timestamps:true,updatedAt:false});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("SupportTicketLink",{id:{type:D.STRING,primaryKey:true},ticket_id:{type:D.STRING,allowNull:false},resource_type:{type:D.ENUM("ORDER","PAYMENT","REFUND","RETURN","SHIPMENT","LOYALTY_REDEMPTION","BUSINESS_SETTLEMENT"),allowNull:false},resource_id:{type:D.STRING,allowNull:false}},{tableName:"support_ticket_links",timestamps:true,updatedAt:false,indexes:[{unique:true,fields:["ticket_id","resource_type","resource_id"]}]});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("BusinessCreditLedgerEntry",{id:{type:D.STRING,primaryKey:true},event_id:{type:D.STRING(180),allowNull:false,unique:true},business_credit_account_id:{type:D.STRING,allowNull:false},business_customer_id:{type:D.STRING,allowNull:false},order_id:D.STRING,payment_id:D.STRING,type:{type:D.ENUM("AUTHORIZATION","CAPTURE","RELEASE","PAYMENT","REFUND","ADJUSTMENT"),allowNull:false},amount:{type:D.DECIMAL(15,2),allowNull:false},currency:{type:D.STRING(3),allowNull:false},balance_delta:{type:D.DECIMAL(15,2),allowNull:false},balance_after:{type:D.DECIMAL(15,2),allowNull:false},reference:D.STRING(160),occurred_at:{type:D.DATE,allowNull:false},metadata:D.JSON},{tableName:"business_credit_ledger_entries",timestamps:true,updatedAt:false});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("BusinessSettlement",{id:{type:D.STRING,primaryKey:true},settlement_number:{type:D.STRING(80),allowNull:false,unique:true},business_customer_id:{type:D.STRING,allowNull:false},period_start:{type:D.DATEONLY,allowNull:false},period_end:{type:D.DATEONLY,allowNull:false},due_date:{type:D.DATEONLY,allowNull:false},status:{type:D.ENUM("DRAFT","ISSUED","PARTIALLY_PAID","PAID","OVERDUE","CANCELLED"),allowNull:false},currency:{type:D.STRING(3),allowNull:false},charge_total:{type:D.DECIMAL(15,2),allowNull:false},payment_total:{type:D.DECIMAL(15,2),allowNull:false},balance_due:{type:D.DECIMAL(15,2),allowNull:false},issued_at:D.DATE,paid_at:D.DATE},{tableName:"business_settlements",timestamps:true,indexes:[{unique:true,fields:["business_customer_id","period_start","period_end"]}]});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("BusinessSettlementItem",{id:{type:D.STRING,primaryKey:true},business_settlement_id:{type:D.STRING,allowNull:false},order_id:D.STRING,invoice_id:D.STRING,credit_ledger_entry_id:D.STRING,description:{type:D.STRING(255),allowNull:false},amount:{type:D.DECIMAL(15,2),allowNull:false},currency:{type:D.STRING(3),allowNull:false}},{tableName:"business_settlement_items",timestamps:true});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module.exports=(s,D)=>s.define("PaymentAllocation",{id:{type:D.STRING,primaryKey:true},event_id:{type:D.STRING(180),allowNull:false,unique:true},payment_id:{type:D.STRING,allowNull:false},business_settlement_id:{type:D.STRING,allowNull:false},amount:{type:D.DECIMAL(15,2),allowNull:false},currency:{type:D.STRING(3),allowNull:false},allocated_at:{type:D.DATE,allowNull:false},allocated_by:{type:D.STRING,allowNull:false}},{tableName:"payment_allocations",timestamps:true});
|
|
||||||
@@ -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 +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;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
const r=require("express").Router(),orders=require("../controllers/commerce/order.controller"),returns=require("../controllers/commerce/return.controller"),webhook=require("../controllers/commerce/webhook.controller"),{authenticate}=require("../middleware/auth.middleware"),validate=require("../middleware/validate.middleware"),s=require("../validation/commerce.schemas");r.post("/payments/webhooks/payhere",webhook.payhere);r.use(authenticate);r.get("/orders",orders.list);r.post("/orders/from-checkout",validate(s.fromCheckout),orders.create);r.get("/orders/:id",orders.get);r.post("/orders/:id/cancel",orders.cancel);r.post("/orders/:id/payment",validate(s.payment),orders.startPayment);r.get("/orders/:id/payment",orders.payment);r.get("/orders/:id/invoice",orders.invoice);r.post("/orders/:orderId/returns",validate(s.returnRequest),returns.request);r.get("/returns",returns.list);r.get("/returns/:id",returns.get);module.exports=r;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
const r=require("express").Router(),c=require("../../controllers/commerce/admin.controller"),{authenticate}=require("../../middleware/auth.middleware"),{checkPermission}=require("../../middleware/permission.middleware"),validate=require("../../middleware/validate.middleware"),s=require("../../validation/commerce.schemas");r.use(authenticate);r.get("/orders",checkPermission("orders.read",{custom:true}),c.orders);r.get("/orders/:id",checkPermission("orders.read",{custom:true}),c.order);r.post("/orders/:id/cancel",checkPermission("orders.cancel",{custom:true}),c.cancel);r.post("/orders/:id/mark-processing",checkPermission("orders.manage",{custom:true}),c.processing);r.post("/orders/:id/refunds",checkPermission("payments.refund",{custom:true}),validate(s.refund),c.refund);r.get("/returns",checkPermission("returns.read",{custom:true}),c.returns);r.post("/returns/:id/approve",checkPermission("returns.manage",{custom:true}),c.returnAction("APPROVED"));r.post("/returns/:id/reject",checkPermission("returns.manage",{custom:true}),c.returnAction("REJECTED"));r.post("/returns/:id/mark-received",checkPermission("returns.manage",{custom:true}),c.returnAction("RECEIVED"));r.post("/returns/:id/complete",checkPermission("returns.manage",{custom:true}),validate(s.returnComplete),c.returnAction("COMPLETED"));module.exports=r;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
const r=require("express").Router(),c=require("../controllers/help.controller"),{sensitiveLimiter}=require("../middleware/rateLimit.middleware");r.get("/help/categories",c.categories);r.get("/help/categories/:slug",c.category);r.get("/help/articles",c.articles);r.get("/help/articles/:slug",c.article);r.get("/help/search",sensitiveLimiter,c.search);module.exports=r;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
const r=require("express").Router(),c=require("../controllers/helpAdmin.controller"),{authenticate}=require("../middleware/auth.middleware"),{checkPermission}=require("../middleware/permission.middleware"),validate=require("../middleware/validate.middleware"),s=require("../validation/supportRecommendation.schemas");r.use(authenticate);r.get("/help/categories",checkPermission("help.read",{custom:true}),c.categories);r.post("/help/categories",checkPermission("help.manage",{custom:true}),validate(s.helpCategory),c.createCategory);r.get("/help/articles",checkPermission("help.read",{custom:true}),c.articles);r.post("/help/articles",checkPermission("help.manage",{custom:true}),validate(s.helpArticle),c.createArticle);r.post("/help/articles/:id/publish",checkPermission("help.publish",{custom:true}),c.publish("PUBLISHED"));r.post("/help/articles/:id/archive",checkPermission("help.publish",{custom:true}),c.publish("ARCHIVED"));module.exports=r;
|
|
||||||
@@ -34,24 +34,6 @@ const pricingAdminRoutes = require("./pricing/admin.routes");
|
|||||||
const merchandisingPublicRoutes = require("./merchandising/public.routes");
|
const merchandisingPublicRoutes = require("./merchandising/public.routes");
|
||||||
const shoppingRoutes = require("./shopping.routes");
|
const shoppingRoutes = require("./shopping.routes");
|
||||||
const shippingAdminRoutes = require("./shipping/admin.routes");
|
const shippingAdminRoutes = require("./shipping/admin.routes");
|
||||||
const commerceRoutes = require("./commerce.routes");
|
|
||||||
const commerceAdminRoutes = require("./commerce/admin.routes");
|
|
||||||
const logisticsAdminRoutes = require("./logistics/admin.routes");
|
|
||||||
const logisticsRiderRoutes = require("./logistics/rider.routes");
|
|
||||||
const logisticsTrackingRoutes = require("./logistics/tracking.routes");
|
|
||||||
const loyaltyCustomerRoutes = require("./loyalty/customer.routes");
|
|
||||||
const loyaltyAdminRoutes = require("./loyalty/admin.routes");
|
|
||||||
const wholesaleCustomerRoutes = require("./wholesale/customer.routes");
|
|
||||||
const wholesaleAdminRoutes = require("./wholesale/admin.routes");
|
|
||||||
const supportCustomerRoutes = require("./support/customer.routes");
|
|
||||||
const supportAdminRoutes = require("./support/admin.routes");
|
|
||||||
const helpRoutes = require("./help.routes");
|
|
||||||
const helpAdminRoutes = require("./helpAdmin.routes");
|
|
||||||
const newsletterRoutes = require("./newsletter.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();
|
||||||
|
|
||||||
@@ -78,23 +60,5 @@ router.use("/admin", pricingAdminRoutes);
|
|||||||
router.use("/", merchandisingPublicRoutes);
|
router.use("/", merchandisingPublicRoutes);
|
||||||
router.use("/", shoppingRoutes);
|
router.use("/", shoppingRoutes);
|
||||||
router.use("/admin", shippingAdminRoutes);
|
router.use("/admin", shippingAdminRoutes);
|
||||||
router.use("/", commerceRoutes);
|
|
||||||
router.use("/admin", commerceAdminRoutes);
|
|
||||||
router.use("/admin", logisticsAdminRoutes);
|
|
||||||
router.use("/rider", logisticsRiderRoutes);
|
|
||||||
router.use("/", logisticsTrackingRoutes);
|
|
||||||
router.use("/", loyaltyCustomerRoutes);
|
|
||||||
router.use("/admin", loyaltyAdminRoutes);
|
|
||||||
router.use("/", wholesaleCustomerRoutes);
|
|
||||||
router.use("/admin", wholesaleAdminRoutes);
|
|
||||||
router.use("/", supportCustomerRoutes);
|
|
||||||
router.use("/admin", supportAdminRoutes);
|
|
||||||
router.use("/", helpRoutes);
|
|
||||||
router.use("/admin", helpAdminRoutes);
|
|
||||||
router.use("/", newsletterRoutes);
|
|
||||||
router.use("/", recommendationRoutes);
|
|
||||||
router.use("/admin", analyticsRoutes);
|
|
||||||
router.use("/", accountOverviewRoutes);
|
|
||||||
router.use("/admin", metricsRoutes);
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user