Compare commits

..

5 Commits

Author SHA1 Message Date
Sathira 835f30b4fb Merge pull request 'Development' (#2) from development into main
CI / test (push) Successful in 10m26s
Reviewed-on: #2
2026-09-12 06:48:45 +00:00
Sathira Sri Sathara f920ca8920 feat: Implement Phase 10 support and recommendations features
CI / test (push) Successful in 10m22s
CI / test (pull_request) Successful in 10m53s
- Added models for support messages, SLA policies, tickets, ticket events, and ticket links.
- Created routes for help, help admin, newsletter, recommendations, and support for both customer and admin.
- Developed services for help, marketing (newsletter), and recommendations.
- Introduced support service for ticket management, including creation, replies, transitions, and attachments.
- Added validation schemas for support recommendations and ticket management.
- Implemented a cron job for support reconciliation and recommendation cleanup.
- Created migration for new support and recommendation database tables.
- Added unit tests for validation and policy checks related to Phase 10 features.
2026-09-09 16:07:31 +05:30
Sathira Sri Sathara abb760cc19 feat: Implement loyalty and wholesale services with models, routes, and validation
CI / test (push) Successful in 10m23s
- Added models for loyalty points allocation, redemption, rewards, tiers, tier history, and referrals.
- Created business credit ledger entries, settlements, and settlement items models.
- Developed services for loyalty operations including account management, point allocation, redemption, and referral handling.
- Implemented wholesale credit management services for transactions and settlements.
- Established routes for loyalty and wholesale admin and customer operations with appropriate middleware for authentication and permission checks.
- Introduced validation schemas for loyalty and wholesale operations.
- Set up cron jobs for loyalty reconciliation tasks such as birthday rewards and point expirations.
- Created migration scripts to set up the database schema for loyalty and wholesale features.
- Added unit tests for validation and policy enforcement in loyalty and wholesale services.
2026-09-09 13:56:37 +05:30
Sathira Sri Sathara 222483d194 feat: Implement Phase 8 Delivery and Rider Management
CI / test (push) Successful in 10m26s
- Introduced new Delivery and Rider API documentation.
- Added new models for RiderProfile, Shipment, ShipmentItem, ShipmentAssignment, ShipmentEvent, and ShipmentProof.
- Developed controllers for admin and rider logistics, including shipment management and rider actions.
- Created services for handling shipment creation, assignment, and state transitions.
- Implemented validation schemas for shipment and rider operations.
- Added new routes for admin and rider logistics, including tracking endpoints.
- Established a state machine for shipment status transitions.
- Created migration scripts for new database tables and relationships.
- Added unit tests for shipment state transitions and validation security.
2026-09-09 12:55:32 +05:30
Sathira Sri Sathara cd2c1c6d08 feat: Implement return and refund functionality in commerce module
CI / test (push) Successful in 10m27s
- Added return request handling in return.controller.js
- Implemented webhook handling for PayHere payments in webhook.controller.js
- Created models for coupon redemptions, invoices, orders, order items, payments, payment attempts, payment webhook events, refunds, return items, and return requests.
- Developed services for order management, payment processing, refunds, and returns.
- Introduced validation schemas for payment and return requests.
- Created migration scripts for new database tables related to orders, payments, refunds, and returns.
- Added unit tests for order state transitions and PayHere provider functionality.
2026-09-09 12:39:57 +05:30
135 changed files with 1087 additions and 58 deletions
+3
View File
@@ -75,3 +75,6 @@ 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
+41
View File
@@ -0,0 +1,41 @@
# 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.
+30
View File
@@ -0,0 +1,30 @@
# 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.
+9
View File
@@ -0,0 +1,9 @@
# 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.
+42
View File
@@ -0,0 +1,42 @@
# 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.
+14
View File
@@ -0,0 +1,14 @@
# 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.
+29
View File
@@ -0,0 +1,29 @@
# 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.
+25
View File
@@ -0,0 +1,25 @@
# 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.
+27
View File
@@ -417,3 +417,30 @@ 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.
@@ -0,0 +1,53 @@
# 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.
+84
View File
@@ -0,0 +1,84 @@
# 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.
+87
View File
@@ -0,0 +1,87 @@
# 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.
@@ -0,0 +1,90 @@
# 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.
+6
View File
@@ -47,6 +47,12 @@ 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(),
+4
View File
@@ -25,4 +25,8 @@ 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",
}; };
@@ -1,4 +1,4 @@
const crypto=require("crypto");const db=require("../../models");const {logActivity}=require("../../services/activity.service"); const crypto=require("crypto");const db=require("../../models");const loyalty=require("../../services/loyalty/loyalty.service");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 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.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.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()});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()});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);}};
@@ -0,0 +1 @@
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);}};
@@ -0,0 +1,2 @@
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);}};
@@ -0,0 +1 @@
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);}};
@@ -0,0 +1 @@
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
View File
@@ -0,0 +1 @@
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
View File
@@ -0,0 +1 @@
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);}};
@@ -0,0 +1 @@
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);}};
@@ -0,0 +1,4 @@
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);}};
@@ -0,0 +1 @@
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);}};
@@ -0,0 +1 @@
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);}};
@@ -0,0 +1 @@
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
View File
@@ -0,0 +1 @@
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);}};
@@ -0,0 +1 @@
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);}};
@@ -0,0 +1 @@
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);}};
@@ -0,0 +1 @@
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);}};
@@ -0,0 +1 @@
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);}};
@@ -0,0 +1 @@
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);}};
@@ -0,0 +1 @@
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);}};
@@ -0,0 +1 @@
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
View File
@@ -0,0 +1 @@
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
View File
@@ -0,0 +1 @@
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
View File
@@ -0,0 +1 @@
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
View File
@@ -0,0 +1 @@
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;};
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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
View File
@@ -0,0 +1 @@
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
View File
@@ -0,0 +1 @@
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
View File
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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
View File
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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
View File
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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"]}]});
+281 -45
View File
@@ -9,24 +9,19 @@
// 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 = process.env.NODE_ENV === "development" ? console.log : false; const loggingOption =
process.env.NODE_ENV === "development" ? console.log : false;
const sequelize = new Sequelize( const sequelize = new Sequelize(dbConfig.DB, dbConfig.USER, dbConfig.PASSWORD, {
dbConfig.DB,
dbConfig.USER,
dbConfig.PASSWORD,
{
host: dbConfig.HOST, host: dbConfig.HOST,
port: dbConfig.PORT, port: dbConfig.PORT,
dialect: dbConfig.DIALECT, dialect: dbConfig.DIALECT,
pool: dbConfig.pool, pool: dbConfig.pool,
logging: loggingOption logging: loggingOption,
} });
);
const db = {}; const db = {};
@@ -38,16 +33,34 @@ 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")(sequelize, DataTypes); db.BusinessCustomer = require("./user/businessCustomer.model")(
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")(sequelize, DataTypes); db.UserActivity = require("./activities/userActivities.model")(
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")(sequelize, DataTypes); db.BusinessApplication = require("./user/businessApplication.model")(
db.BusinessContact = require("./user/businessContact.model")(sequelize, DataTypes); sequelize,
db.BusinessCreditAccount = require("./user/businessCreditAccount.model")(sequelize, DataTypes); 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);
@@ -55,55 +68,278 @@ 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")(sequelize, DataTypes); db.rolePermission = require("./permission/rolePermission.model")(
db.userPermission = require("./permission/userPermission.model")(sequelize, DataTypes); sequelize,
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")(sequelize, DataTypes); db.DocumentType = require("./document/documentType.model")(
sequelize,
DataTypes,
);
// Reference Numbers // Reference Numbers
db.referenceNumber = require("./referenceNumbers/referenceNumber.model")(sequelize, DataTypes); db.referenceNumber = require("./referenceNumbers/referenceNumber.model")(
sequelize,
DataTypes,
);
// Notifications // Notifications
db.notification = require("./notification/notification.model")(sequelize, DataTypes); db.notification = require("./notification/notification.model")(
db.userNotification = require("./notification/userNotification.model")(sequelize, DataTypes); sequelize,
db.NotificationDelivery = require("./notification/notificationDelivery.model")(sequelize, DataTypes); 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")(sequelize, DataTypes); db.CategoryTranslation = require("./catalogue/categoryTranslation.model")(
sequelize,
DataTypes,
);
db.Product = require("./catalogue/product.model")(sequelize, DataTypes); db.Product = require("./catalogue/product.model")(sequelize, DataTypes);
db.ProductTranslation = require("./catalogue/productTranslation.model")(sequelize, DataTypes); db.ProductTranslation = require("./catalogue/productTranslation.model")(
db.ProductCategory = require("./catalogue/productCategory.model")(sequelize, DataTypes); sequelize,
db.ProductVariant = require("./catalogue/productVariant.model")(sequelize, DataTypes); DataTypes,
db.ProductOption = require("./catalogue/productOption.model")(sequelize, DataTypes); );
db.ProductOptionValue = require("./catalogue/productOptionValue.model")(sequelize, DataTypes); db.ProductCategory = require("./catalogue/productCategory.model")(
db.VariantOptionValue = require("./catalogue/variantOptionValue.model")(sequelize, DataTypes); sequelize,
db.ProductAttribute = require("./catalogue/productAttribute.model")(sequelize, DataTypes); 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")(sequelize, DataTypes); db.CollectionTranslation = require("./catalogue/collectionTranslation.model")(
db.CollectionProduct = require("./catalogue/collectionProduct.model")(sequelize, DataTypes); sequelize,
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")(sequelize, DataTypes); db.ProductRelation = require("./catalogue/productRelation.model")(
db.ProductReview = require("./catalogue/productReview.model")(sequelize, DataTypes); sequelize,
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); 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.ProductReview = require("./catalogue/productReview.model")(
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); sequelize,
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); 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;
@@ -0,0 +1 @@
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
View File
@@ -0,0 +1 @@
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;};
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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;};
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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"]}]});
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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"]}]});
@@ -0,0 +1 @@
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
View File
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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
View File
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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"]}]});
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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"]}]});
@@ -0,0 +1 @@
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});
@@ -0,0 +1 @@
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
View File
@@ -0,0 +1 @@
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
View File
@@ -0,0 +1 @@
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
View File
@@ -0,0 +1 @@
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
View File
@@ -0,0 +1 @@
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;
+30
View File
@@ -34,6 +34,21 @@ 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 router = express.Router(); const router = express.Router();
@@ -60,5 +75,20 @@ 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);
module.exports = router; module.exports = router;
+1
View File
@@ -0,0 +1 @@
const r=require("express").Router(),c=require("../../controllers/logistics/admin.controller"),riders=require("../../controllers/logistics/rider.controller"),{authenticate}=require("../../middleware/auth.middleware"),{checkPermission}=require("../../middleware/permission.middleware"),validate=require("../../middleware/validate.middleware"),s=require("../../validation/logistics.schemas");r.use(authenticate);r.get("/shipments",checkPermission("shipments.read",{custom:true}),c.shipments);r.get("/shipments/:id",checkPermission("shipments.read",{custom:true}),c.shipment);r.post("/shipments",checkPermission("shipments.create",{custom:true}),validate(s.shipment),c.create);r.post("/shipments/return-pickup",checkPermission("returns.logistics.manage",{custom:true}),validate(s.returnPickup),c.returnPickup);r.post("/shipments/:id/assign",checkPermission("shipments.assign",{custom:true}),validate(s.assign),c.assign);r.post("/shipments/:id/unassign",checkPermission("shipments.assign",{custom:true}),validate(s.note),c.unassign);r.post("/shipments/:id/reschedule",checkPermission("shipments.manage",{custom:true}),validate(s.reschedule),c.reschedule);r.post("/shipments/:id/cancel",checkPermission("shipments.manage",{custom:true}),c.cancel);r.get("/dispatch",checkPermission("dispatch.read",{custom:true}),c.dispatch);r.get("/riders",checkPermission("riders.read",{custom:true}),riders.adminList);r.get("/riders/:id",checkPermission("riders.read",{custom:true}),riders.adminGet);r.post("/riders",checkPermission("riders.manage",{custom:true}),validate(s.rider),riders.adminCreate);r.patch("/riders/:id",checkPermission("riders.manage",{custom:true}),validate(s.rider),riders.adminUpdate);r.get("/riders/:id/assignments",checkPermission("riders.read",{custom:true}),riders.assignments);module.exports=r;
+1
View File
@@ -0,0 +1 @@
const r=require("express").Router(),c=require("../../controllers/logistics/rider.controller"),{authenticate}=require("../../middleware/auth.middleware"),{authorizedAccountType}=require("../../middleware/permission.middleware"),validate=require("../../middleware/validate.middleware"),s=require("../../validation/logistics.schemas");r.use(authenticate,authorizedAccountType(["rider"]));r.get("/shipments",c.list);r.get("/shipments/:id",c.get);r.post("/shipments/:id/accept",validate(s.note),c.accept);r.post("/shipments/:id/reject",validate(s.note),c.reject);r.post("/shipments/:id/pickup",validate(s.note),c.pickup);r.post("/shipments/:id/in-transit",validate(s.note),c.inTransit);r.post("/shipments/:id/out-for-delivery",validate(s.note),c.outForDelivery);r.post("/shipments/:id/deliver",validate(s.proof),c.deliver);r.post("/shipments/:id/fail-delivery",validate(s.reason),c.fail);r.put("/location",validate(s.location),c.location);module.exports=r;
+1
View File
@@ -0,0 +1 @@
const r=require("express").Router(),c=require("../../controllers/logistics/tracking.controller"),{authenticate}=require("../../middleware/auth.middleware");r.use(authenticate);r.get("/orders/:orderId/tracking",c.order);r.get("/returns/:id/tracking",c.return);module.exports=r;
+1
View File
@@ -0,0 +1 @@
const r=require("express").Router(),c=require("../../controllers/loyalty/admin.controller"),{authenticate}=require("../../middleware/auth.middleware"),{checkPermission}=require("../../middleware/permission.middleware"),validate=require("../../middleware/validate.middleware"),s=require("../../validation/loyaltyWholesale.schemas"),idempotency=(req,res,next)=>req.get("Idempotency-Key")?next():res.status(400).json({success:false,error:{code:"IDEMPOTENCY_KEY_REQUIRED",message:"Idempotency-Key is required"}});r.use(authenticate);r.get("/loyalty/accounts",checkPermission("loyalty.accounts.read",{custom:true}),c.accounts);r.post("/loyalty/adjustments",checkPermission("loyalty.points.adjust",{custom:true}),idempotency,validate(s.adjust),c.adjust);r.get("/loyalty/tiers",checkPermission("loyalty.manage",{custom:true}),c.tiers);r.post("/loyalty/tiers",checkPermission("loyalty.manage",{custom:true}),validate(s.tier),c.createTier);r.get("/loyalty/rules",checkPermission("loyalty.manage",{custom:true}),c.rules);r.post("/loyalty/rules",checkPermission("loyalty.manage",{custom:true}),validate(s.rule),c.createRule);r.get("/loyalty/rewards",checkPermission("loyalty.manage",{custom:true}),c.rewards);r.post("/loyalty/rewards",checkPermission("loyalty.manage",{custom:true}),validate(s.reward),c.createReward);r.get("/loyalty/referrals",checkPermission("loyalty.referrals.read",{custom:true}),c.referrals);module.exports=r;
+1
View File
@@ -0,0 +1 @@
const r=require("express").Router(),c=require("../../controllers/loyalty/customer.controller"),{authenticate}=require("../../middleware/auth.middleware"),validate=require("../../middleware/validate.middleware"),s=require("../../validation/loyaltyWholesale.schemas");r.use(authenticate);r.get("/loyalty",c.summary);r.get("/loyalty/history",c.history);r.get("/loyalty/tiers",c.tiers);r.get("/loyalty/rewards",c.rewards);r.post("/loyalty/redeem",validate(s.redeem),c.redeem);r.get("/loyalty/vouchers",c.vouchers);r.get("/loyalty/referral",c.referral);r.post("/loyalty/referral/claim",validate(s.referral),c.claimReferral);module.exports=r;
+1
View File
@@ -0,0 +1 @@
const r=require("express").Router(),c=require("../controllers/newsletter.controller"),{authenticate}=require("../middleware/auth.middleware"),{checkPermission}=require("../middleware/permission.middleware"),validate=require("../middleware/validate.middleware"),s=require("../validation/supportRecommendation.schemas"),{sensitiveLimiter}=require("../middleware/rateLimit.middleware");r.post("/newsletter/subscribe",sensitiveLimiter,validate(s.newsletter),c.subscribe);r.post("/newsletter/unsubscribe/:token",sensitiveLimiter,c.unsubscribe);r.get("/newsletter/me",authenticate,c.mine);r.get("/admin/newsletter/subscribers",authenticate,checkPermission("newsletter.subscribers.read",{custom:true}),c.list);module.exports=r;
+1
View File
@@ -0,0 +1 @@
const r=require("express").Router(),c=require("../controllers/recommendation.controller"),{authenticate}=require("../middleware/auth.middleware"),validate=require("../middleware/validate.middleware"),s=require("../validation/supportRecommendation.schemas"),{sensitiveLimiter}=require("../middleware/rateLimit.middleware");r.post("/recommendations/events",authenticate,sensitiveLimiter,validate(s.viewEvent),c.event);r.get("/products/:productId/recommendations/related",c.related);r.get("/recommendations/trending",c.trending);r.get("/recommendations/popular",c.popular);r.get("/recommendations/recently-viewed",authenticate,c.recent);r.get("/recommendations/for-you",authenticate,c.forYou);module.exports=r;
+1
View File
@@ -0,0 +1 @@
const r=require("express").Router(),c=require("../../controllers/support/admin.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("/support/tickets",checkPermission("support.tickets.read",{custom:true}),c.list);r.get("/support/tickets/:id",checkPermission("support.tickets.read",{custom:true}),c.detail);r.post("/support/tickets/:id/assign",checkPermission("support.tickets.assign",{custom:true}),validate(s.assign),c.assign);r.post("/support/tickets/:id/claim",checkPermission("support.tickets.assign",{custom:true}),c.claim);r.post("/support/tickets/:id/messages",checkPermission("support.tickets.reply",{custom:true}),validate(s.message),c.reply);r.post("/support/tickets/:id/internal-notes",checkPermission("support.tickets.internal_notes",{custom:true}),validate(s.internalNote),c.note);r.post("/support/tickets/:id/resolve",checkPermission("support.tickets.status",{custom:true}),c.action("RESOLVED"));r.post("/support/tickets/:id/reopen",checkPermission("support.tickets.status",{custom:true}),c.action("WAITING_FOR_SUPPORT"));r.post("/support/tickets/:id/close",checkPermission("support.tickets.status",{custom:true}),c.action("CLOSED"));r.patch("/support/tickets/:id/priority",checkPermission("support.tickets.priority",{custom:true}),validate(s.priority),c.priority);r.get("/support/tickets/:id/attachments/:attachmentId",checkPermission("support.tickets.read",{custom:true}),c.attachment);module.exports=r;
+1
View File
@@ -0,0 +1 @@
const r=require("express").Router(),c=require("../../controllers/support/customer.controller"),{authenticate}=require("../../middleware/auth.middleware"),validate=require("../../middleware/validate.middleware"),s=require("../../validation/supportRecommendation.schemas"),{sensitiveLimiter}=require("../../middleware/rateLimit.middleware");r.use(authenticate);r.get("/support/tickets",c.list);r.post("/support/tickets",sensitiveLimiter,validate(s.ticketCreate),c.create);r.get("/support/tickets/:id",c.detail);r.post("/support/tickets/:id/messages",sensitiveLimiter,validate(s.message),c.reply);r.post("/support/tickets/:id/close",c.close);r.get("/support/tickets/:id/attachments/:attachmentId",c.attachment);module.exports=r;
+1
View File
@@ -0,0 +1 @@
const r=require("express").Router(),c=require("../../controllers/wholesale/admin.controller"),{authenticate}=require("../../middleware/auth.middleware"),{checkPermission}=require("../../middleware/permission.middleware"),validate=require("../../middleware/validate.middleware"),s=require("../../validation/loyaltyWholesale.schemas");r.use(authenticate);r.post("/wholesale/credit/transactions",checkPermission("wholesale.credit.manage",{custom:true}),validate(s.credit),c.credit);r.get("/wholesale/businesses/:businessId/credit",checkPermission("wholesale.credit.read",{custom:true}),c.creditHistory);r.get("/wholesale/settlements",checkPermission("wholesale.settlements.read",{custom:true}),c.settlements);r.post("/wholesale/settlements",checkPermission("wholesale.settlements.manage",{custom:true}),validate(s.settlement),c.generate);r.post("/wholesale/settlements/:id/issue",checkPermission("wholesale.settlements.manage",{custom:true}),c.action("ISSUED"));r.post("/wholesale/settlements/:id/mark-paid",checkPermission("wholesale.settlements.manage",{custom:true}),c.action("PAID"));module.exports=r;
+1
View File
@@ -0,0 +1 @@
const r=require("express").Router(),c=require("../../controllers/wholesale/customer.controller"),purchase=require("../../controllers/wholesale/creditPurchase.controller"),{authenticate}=require("../../middleware/auth.middleware"),{authorizedAccountType}=require("../../middleware/permission.middleware"),guard=[authenticate,authorizedAccountType(["business_customer"])];r.get("/business/dashboard",guard,c.dashboard);r.get("/business/credit",guard,c.credit);r.get("/business/settlements",guard,c.settlements);r.get("/business/settlements/:id",guard,c.settlement);r.get("/business/orders/recent",guard,c.orders);r.post("/business/orders/:id/use-credit",guard,purchase.purchase);module.exports=r;
+4
View File
@@ -0,0 +1,4 @@
const crypto=require("crypto"),db=require("../../models"),inventory=require("../inventory/inventory.service"),state=require("./orderState.service"),{nextSequence,pad}=require("../../utils/referenceNumber.util");const fault=(m,c,s=400)=>Object.assign(new Error(m),{code:c,status:s});const ref=async(prefix,key,t)=>`${prefix}-${new Date().getUTCFullYear()}-${pad(await nextSequence(key,t),6)}`;
async function createFromCheckout({checkoutId,userId}){return db.sequelize.transaction(async transaction=>{const checkout=await db.CheckoutSession.findOne({where:{id:checkoutId,user_id:userId},include:[{model:db.CheckoutItem,as:"items"}],transaction,lock:transaction.LOCK.UPDATE});if(!checkout)throw fault("Checkout not found","NOT_FOUND",404);let order=await db.Order.findOne({where:{checkout_session_id:checkout.id},transaction,lock:transaction.LOCK.UPDATE});if(order)return {order,idempotent:true};if(checkout.status!=="READY"||new Date(checkout.expires_at)<=new Date())throw fault("Checkout is not ready","CHECKOUT_NOT_READY",409);for(const item of checkout.items){const reservation=await db.InventoryReservation.findOne({where:{reservation_key:item.reservation_key,status:"ACTIVE"},transaction,lock:transaction.LOCK.UPDATE});if(!reservation)throw fault("Checkout reservation is unavailable","RESERVATION_UNAVAILABLE",409);}order=await db.Order.create({id:crypto.randomUUID(),order_number:await ref("ORD","orders",transaction),user_id:userId,business_customer_id:checkout.business_customer_id,checkout_session_id:checkout.id,status:"PENDING_PAYMENT",payment_status:"PENDING",currency:checkout.currency,subtotal:checkout.subtotal,discount_total:checkout.discount_total,shipping_amount:checkout.shipping_amount,tax_amount:checkout.tax_amount,duty_amount:checkout.duty_amount,grand_total:checkout.grand_total,shipping_address_snapshot:checkout.shipping_address_snapshot,billing_address_snapshot:checkout.billing_address_snapshot,shipping_method_snapshot:checkout.shipping_snapshot,coupon_code:checkout.coupon_code,placed_at:new Date()},{transaction});await db.OrderItem.bulkCreate(checkout.items.map(x=>({id:crypto.randomUUID(),order_id:order.id,product_id:x.product_id,variant_id:x.variant_id,reservation_key:x.reservation_key,sku:x.sku,product_name:x.product_name,variant_description:x.variant_description,quantity:x.quantity,unit_price:x.unit_price,discount_amount:x.discount_amount,line_total:x.line_total,currency:x.currency,metadata:x.metadata})),{transaction});await checkout.update({status:"CONVERTED"},{transaction});await db.Cart.update({status:"CONVERTED"},{where:{id:checkout.cart_id,status:"CHECKOUT_LOCKED"},transaction});return {order,idempotent:false};});}
async function cancel({orderId,userId,admin=false,requestId}){return db.sequelize.transaction(async transaction=>{const where={id:orderId,...(!admin&&{user_id:userId})},order=await db.Order.findOne({where,include:[{model:db.OrderItem,as:"items"},{model:db.Payment,as:"payments"}],transaction,lock:transaction.LOCK.UPDATE});if(!order)throw fault("Order not found","NOT_FOUND",404);if(order.status==="CANCELLED")return order;if(order.payment_status==="PAID")throw fault("Paid orders require a refund","REFUND_REQUIRED",409);state.assertTransition(order.status,"CANCELLED");for(const item of order.items)await inventory.releaseReservation({reservationKey:item.reservation_key,requestId,transaction});await db.Payment.update({status:"CANCELLED",cancelled_at:new Date()},{where:{order_id:order.id,status:["PENDING","REQUIRES_ACTION"]},transaction});await order.update({status:"CANCELLED",payment_status:"CANCELLED",cancelled_at:new Date()},{transaction});return order;});}
module.exports={createFromCheckout,cancel,ref};
@@ -0,0 +1 @@
const transitions={PENDING_PAYMENT:new Set(["PAID","CANCELLED"]),PAID:new Set(["PROCESSING","REFUND_PENDING"]),PROCESSING:new Set(["READY_FOR_FULFILLMENT","REFUND_PENDING"]),READY_FOR_FULFILLMENT:new Set(["PARTIALLY_FULFILLED","FULFILLED"]),PARTIALLY_FULFILLED:new Set(["FULFILLED"]),FULFILLED:new Set(["COMPLETED"]),COMPLETED:new Set(["REFUND_PENDING"]),REFUND_PENDING:new Set(["PARTIALLY_REFUNDED","REFUNDED"]),PARTIALLY_REFUNDED:new Set(["REFUND_PENDING","REFUNDED"]),CANCELLED:new Set(),REFUNDED:new Set()};const canTransition=(from,to)=>Boolean(transitions[from]?.has(to));const assertTransition=(from,to)=>{if(!canTransition(from,to))throw Object.assign(new Error(`Illegal order transition ${from} -> ${to}`),{code:"ILLEGAL_ORDER_TRANSITION",status:409});};module.exports={transitions,canTransition,assertTransition};
+6
View File
@@ -0,0 +1,6 @@
const crypto=require("crypto"),db=require("../../models"),money=require("../pricing/money"),inventory=require("../inventory/inventory.service"),loyalty=require("../loyalty/loyalty.service"),state=require("./orderState.service"),{nextSequence,pad}=require("../../utils/referenceNumber.util"),payhere=require("./providers/payhere.provider");const fault=(m,c,s=400)=>Object.assign(new Error(m),{code:c,status:s});const adapters={PAYHERE:payhere};
async function create({orderId,userId,provider="PAYHERE",method="ONLINE"}){if(!adapters[provider])throw fault("Payment provider is unavailable","PROVIDER_NOT_CONFIGURED",503);return db.sequelize.transaction(async transaction=>{const order=await db.Order.findOne({where:{id:orderId,user_id:userId,status:"PENDING_PAYMENT"},transaction,lock:transaction.LOCK.UPDATE});if(!order)throw fault("Payable order not found","NOT_FOUND",404);let payment=await db.Payment.findOne({where:{order_id:order.id,status:["PENDING","REQUIRES_ACTION"]},transaction,lock:transaction.LOCK.UPDATE});if(payment)return payment;payment=await db.Payment.create({id:crypto.randomUUID(),order_id:order.id,payment_reference:order.order_number,provider,method,status:"PENDING",currency:order.currency,amount:order.grand_total},{transaction});await db.PaymentAttempt.create({id:crypto.randomUUID(),payment_id:payment.id,attempt_number:1,provider_request_id:crypto.randomUUID(),status:"STARTED",amount:payment.amount,started_at:new Date(),metadata:{provider}},{transaction});return payment;});}
async function redeemCoupon(order,transaction){if(!order.coupon_code)return;const coupon=await db.Coupon.findOne({where:{code:order.coupon_code},transaction,lock:transaction.LOCK.UPDATE});if(!coupon)return;const used=await db.CouponRedemption.count({where:{coupon_id:coupon.id},transaction});if(coupon.max_uses&&used>=coupon.max_uses)throw fault("Coupon usage limit reached","COUPON_LIMIT_REACHED",409);await db.CouponRedemption.findOrCreate({where:{coupon_id:coupon.id,order_id:order.id},defaults:{id:crypto.randomUUID(),user_id:order.user_id,redeemed_at:new Date()},transaction});}
async function issueInvoice(order,transaction){let invoice=await db.Invoice.findOne({where:{order_id:order.id},transaction,lock:transaction.LOCK.UPDATE});if(!invoice){const n=await nextSequence("invoices",transaction);invoice=await db.Invoice.create({id:crypto.randomUUID(),invoice_number:`INV-${new Date().getUTCFullYear()}-${pad(n,6)}`,order_id:order.id,status:"ISSUED",currency:order.currency,subtotal:order.subtotal,discount_total:order.discount_total,shipping_amount:order.shipping_amount,tax_amount:order.tax_amount,duty_amount:order.duty_amount,grand_total:order.grand_total,issued_at:new Date(),paid_at:new Date()},{transaction});}await loyalty.earnPurchase(order,transaction);return invoice;}
async function handleWebhook(provider,payload){const adapter=adapters[provider];if(!adapter||!adapter.verifyWebhook(payload))throw fault("Invalid webhook signature","INVALID_WEBHOOK_SIGNATURE",401);const event=adapter.parseWebhook(payload),hash=crypto.createHash("sha256").update(JSON.stringify(payload)).digest("hex");return db.sequelize.transaction(async transaction=>{let record=await db.PaymentWebhookEvent.findOne({where:{provider,provider_event_id:event.eventId},transaction,lock:transaction.LOCK.UPDATE});if(record?.processing_status==="PROCESSED")return{idempotent:true,status:"PROCESSED"};if(!record)record=await db.PaymentWebhookEvent.create({id:crypto.randomUUID(),provider,provider_event_id:event.eventId,event_type:event.eventType,payload_hash:hash,processing_status:"RECEIVED",received_at:new Date()},{transaction});const payment=await db.Payment.findOne({where:{payment_reference:event.paymentReference,provider},transaction,lock:transaction.LOCK.UPDATE});if(!payment)throw fault("Payment not found","PAYMENT_NOT_FOUND",404);if(payment.currency!==event.currency||money.parse(payment.amount)!==money.parse(event.amount))throw fault("Payment amount or currency mismatch","PAYMENT_MISMATCH",409);const order=await db.Order.findByPk(payment.order_id,{include:[{model:db.OrderItem,as:"items"}],transaction,lock:transaction.LOCK.UPDATE});if(event.status==="PAID"&&payment.status!=="PAID"){state.assertTransition(order.status,"PAID");for(const item of [...order.items].sort((a,b)=>a.variant_id.localeCompare(b.variant_id)))await inventory.consumeReservation({reservationKey:item.reservation_key,requestId:`webhook:${record.id}`,transaction});await redeemCoupon(order,transaction);await payment.update({status:"PAID",provider_transaction_id:event.providerTransactionId,paid_at:new Date()},{transaction});await order.update({status:"PAID",payment_status:"PAID"},{transaction});await issueInvoice(order,transaction);}else if(event.status==="FAILED"&&payment.status!=="PAID"){await payment.update({status:"FAILED",failed_at:new Date(),failure_code:event.eventType},{transaction});await order.update({payment_status:"FAILED"},{transaction});}await record.update({processing_status:"PROCESSED",processed_at:new Date()},{transaction});return{idempotent:false,status:event.status,paymentId:payment.id};});}
module.exports={create,handleWebhook,redeemCoupon,issueInvoice,adapters};
@@ -0,0 +1,5 @@
const crypto=require("crypto"),md5=x=>crypto.createHash("md5").update(String(x)).digest("hex").toUpperCase();
function verifyWebhook(p){const secret=process.env.PAYHERE_MERCHANT_SECRET;if(!secret)return false;const expected=md5(`${p.merchant_id}${p.order_id}${p.payhere_amount}${p.payhere_currency}${p.status_code}${md5(secret)}`),actual=String(p.md5sig||"").toUpperCase().padEnd(expected.length,"0").slice(0,expected.length);return crypto.timingSafeEqual(Buffer.from(expected),Buffer.from(actual));}
function parseWebhook(p){return{eventId:p.payment_id||`${p.order_id}:${p.status_code}:${p.payhere_amount}`,paymentReference:p.order_id,providerTransactionId:p.payment_id,status:String(p.status_code)==="2"?"PAID":String(p.status_code)==="0"?"PENDING":"FAILED",amount:String(p.payhere_amount),currency:p.payhere_currency,eventType:`PAYHERE_${p.status_code}`};}
function createPayment({reference,amount,currency}){const merchantId=process.env.PAYHERE_MERCHANT_ID,secret=process.env.PAYHERE_MERCHANT_SECRET;if(!merchantId||!secret)throw Object.assign(new Error("PayHere is not configured"),{code:"PROVIDER_NOT_CONFIGURED",status:503});return{merchant_id:merchantId,order_id:reference,amount,currency,hash:md5(`${merchantId}${reference}${amount}${currency}${md5(secret)}`),notify_url:process.env.PAYHERE_NOTIFY_URL,return_url:process.env.PAYHERE_RETURN_URL,cancel_url:process.env.PAYHERE_CANCEL_URL};}
const unavailable=()=>{throw Object.assign(new Error("Provider operation is not configured"),{code:"PROVIDER_OPERATION_UNAVAILABLE",status:503});};module.exports={createPayment,verifyWebhook,parseWebhook,refund:unavailable,getPaymentStatus:unavailable};

Some files were not shown because too many files have changed in this diff Show More