feat: implement business pricing service and money utilities

- Added `businessPricing.service.js` to handle business customer pricing logic.
- Introduced `money.js` for money parsing, formatting, and calculations.
- Created `pricing.service.js` to manage variant quoting and promotion application.
- Updated validation schemas in `catalogue.schemas.js` and `inventoryMerchandising.schemas.js` for new pricing and inventory features.
- Implemented cron job for inventory reservation expiry in `inventoryReservationExpiry.cron.js`.
- Created database migrations for catalogue and inventory structures.
- Added unit tests for catalogue localization, validation, and inventory merchandising functionalities.
This commit is contained in:
Sathira Sri Sathara
2026-09-03 19:25:13 +05:30
parent fd4e324571
commit 5643d89236
75 changed files with 663 additions and 1 deletions
+64
View File
@@ -0,0 +1,64 @@
# ZUMRI Catalogue API
Catalogue reads use `/api/v1`; `/api` remains compatible. Public endpoints require no authentication. Administrative endpoints require Phase 1 permissions. Examples use placeholders.
## Localization
Locale priority is `?locale=en|si|ta`, `X-Locale`, authenticated profile preference, `Accept-Language`, then English. Missing requested content falls back to English and then the first available translation.
## Public products
- `GET /api/v1/products`
- `GET /api/v1/products/:slug`
- `POST /api/v1/products/:productId/reviews` — authenticated customer
List query parameters: `page`, `limit` (maximum 100), `category`, `brand`, `search`, `minPrice`, `maxPrice`, `featured`, `newArrival`, `locale`, and `sort=newest|price_asc|price_desc|name|featured`. Unsupported sorts return 400. Stock and wholesale availability are intentionally absent.
```json
{"data":[{"id":"<product-id>","slug":"<slug>","name":"<localized-name>","primaryImage":{"url":"<short-lived-url>"},"minPrice":"1000.00","maxPrice":"1200.00","currency":"LKR","featured":false,"newArrival":true}]}
```
Detail returns localized content/SEO, brand, categories, active variants, options, descriptive attributes, signed gallery media, a structured size guide, rating summary, and approved review preview. DRAFT, INACTIVE, ARCHIVED, and HIDDEN products always return 404 publicly.
Review body is `{"rating":5,"title":"<title>","body":"<review>"}`. `status`, `verifiedPurchase`, moderator, and user IDs are rejected. One review per user/product is enforced. Purchase verification defaults false until order integration exists.
## Categories, brands, and collections
- `GET /api/v1/categories` — tree; add `flat=true` for a flat list
- `GET /api/v1/categories/:slug`
- `GET /api/v1/brands`
- `GET /api/v1/brands/:slug`
- `GET /api/v1/collections`
- `GET /api/v1/collections/:slug`
Only active categories/brands and currently scheduled active collections are returned.
## Administrative products
- `GET /api/v1/admin/products` — `catalogue.products.read`
- `POST /api/v1/admin/products` — `catalogue.products.create`
- `GET /api/v1/admin/products/:id` — read permission
- `PATCH /api/v1/admin/products/:id` — `catalogue.products.update`
- `DELETE /api/v1/admin/products/:id` — `catalogue.products.delete`; archives instead of deleting
- `POST /api/v1/admin/products/:productId/variants`
- `PATCH /api/v1/admin/products/:productId/variants/:variantId`
- `POST /api/v1/admin/products/:productId/options`
- `POST /api/v1/admin/products/:productId/media`
- `POST /api/v1/admin/products/:productId/relations`
Product creation atomically persists translations, category joins, variants, and owned Phase 2 uploads. Prices are decimal strings. Publishing requires English content, a brand, an active variant, and primary media. Published slugs are immutable.
## Administrative content
- `POST|PATCH /api/v1/admin/brands[/:id]` — `catalogue.brands.manage`
- `POST|PATCH /api/v1/admin/categories[/:id]` — `catalogue.categories.manage`
- `POST /api/v1/admin/collections` — `catalogue.collections.manage`
- `POST /api/v1/admin/size-guides` — `catalogue.size-guides.manage`
- `GET /api/v1/admin/reviews` — `catalogue.reviews.read`
- `PATCH /api/v1/admin/reviews/:id/status` — `catalogue.reviews.moderate`
Categories cannot parent themselves or form cycles. Referenced categories/brands have no hard-delete API. Collections use deterministic join ordering and publish windows. Size guides accept structured columns/rows, never HTML.
## Media and prices
Media must be an AVAILABLE upload owned by the acting administrator. Linking transfers metadata ownership to the catalogue Product. Public responses contain short-lived signed URLs but never object keys, bucket names, or credentials. `basePrice` and optional `compareAtPrice` are display catalogue prices only; promotions and authoritative shopping pricing are deferred.
@@ -0,0 +1,42 @@
# Inventory and Merchandising API
All routes use the `/api/v1` prefix. Admin routes require authentication and the named Phase 5 permission.
## Inventory and warehouses
- `GET /admin/inventory` (`inventory.read`)
- `GET /admin/inventory/:variantId` (`inventory.read`)
- `GET /admin/inventory/ledger` (`inventory.read`)
- `GET /admin/inventory/low-stock` (`inventory.read`)
- `POST /admin/inventory/adjustments` (`inventory.adjust`); send `Idempotency-Key`
- `POST /admin/inventory/transfers` (`inventory.transfer`); send `Idempotency-Key`
- `GET /admin/warehouses` (`inventory.read`)
- `POST /admin/warehouses`, `PATCH /admin/warehouses/:id` (`inventory.warehouses.manage`)
- `GET /availability/:variantId` returns only `IN_STOCK`, `LOW_STOCK`, or `OUT_OF_STOCK` and `availableForSale`; it never exposes warehouse quantities.
Inventory mutations are internal service operations: `reserveStock`, `releaseReservation`, and `consumeReservation`. Reservations default to `INVENTORY_RESERVATION_TTL_MINUTES=15`. The minute reconciliation job expires bounded batches of 100; the database remains authoritative.
## Business pricing
- `GET /admin/business-pricing` (`pricing.business.read`)
- `POST /admin/business-pricing` (`pricing.business.manage`)
Rules support exactly one tier or customer audience, effective dates, MOQ, and non-overlapping volume ranges. Precedence is customer override, business tier, then retail. Money is stored as DECIMAL and calculated using integer-scaled helpers.
## Promotions and coupons
- `GET|POST /admin/promotions` (`promotions.read` / `promotions.manage`)
- `GET|POST /admin/coupons` (`promotions.read` / `promotions.manage`)
The quote boundary resolves retail/business base price, then the highest-priority eligible automatic promotion, then a coupon only when stacking permits. Discounts floor at zero. Coupon codes are canonical uppercase. Usage redemption is intentionally deferred until orders exist.
## Banners
- `GET /banners?placement=&locale=` returns active, scheduled, audience-eligible localized banners.
- `GET|POST /admin/banners` requires `merchandising.banners.manage`.
Banner media reuses Upload records and only returns safe upload identifiers, never bucket/object keys.
## Not implemented
Cart, checkout, orders, coupon redemption, shipping, tax, payment, and delivery remain outside Phase 5.
+11
View File
@@ -1,5 +1,13 @@
# ZUMRI Current Backend Status
## Phase 4 Completion Update
Completion date: 2026-09-03. Module 03 (product catalogue) is approximately 84%; Module 04 (multi-language content) is approximately 82%. Phase 4 adds 18 catalogue models covering brands, hierarchical localized categories, products/translations/multi-category joins, DECIMAL-price variants, configurable options and attributes, owned media, scheduled localized collections, structured size guides, explicit relations, and moderated reviews.
Public APIs now provide active/public product lists and slug detail, safe search/filter/sort/pagination, locale fallback, categories, brands, current collections, signed media DTOs, price ranges, and approved review summaries. Permission-gated admin APIs cover product lifecycle, variants/options/media/relations, brands, cycle-safe categories, collections, size guides, and review moderation. No inventory quantity, wholesale pricing, promotion, or shopping behavior was introduced.
The mocked regression suite passes 15 suites/78 tests and syntax validation covers 204 JavaScript files. The Phase 4 migration is forward-only and was not executed. Restored-staging migration checks, permission seeding, real MySQL query/concurrency testing, and signed-media volume validation remain required before Phase 5. See `Documentation/PHASE_4_CATALOGUE_CONTENT.md` and `Documentation/API_CATALOGUE.md`.
## Phase 3 Completion Update
Completion date: 2026-09-03. Module 02 (customer profile/address management) is now approximately 88%; Module 13 (business accounts) is approximately 78%. Customer profile/preferences, structured owned addresses with transactional defaults, secure self-deactivation, business applications, permission-gated transactional approval, immutable partner identity, business profiles/contacts/addresses, domain status, DECIMAL credit configuration, and settlement-term primitives are implemented.
@@ -403,3 +411,6 @@ New `auth_sessions`, `user_identities`, and `user_roles` models/tables support d
Auth endpoints now cover customer registration, verification/resend, password-to-OTP challenge, OTP completion, refresh rotation, current/all-device logout, forgot/reset/change password, Google, Apple, `/me`, and shared admin/rider compatibility flows. Both `/api` and `/api/v1` remain. Security-focused unit/integration tests were added without real providers/email/database/Redis.
Remaining identity work is operational: validate/deduplicate deployed RBAC data before migration, run staging MySQL/Redis concurrency tests, seed initial privileged assignments securely, configure provider audiences/mail, and design manual privileged OAuth linking and email change if required. Module 01 is now approximately 91%; migration/staging validation prevents claiming 100% production completion.
## Phase 5 Completion Update
Date: 2026-09-03. Inventory/reservation foundations, business pricing, promotions/coupons, and localized scheduled banners are implemented behind new permissions and a forward-only migration. Phase 5 adds 13 models, secured administration routes, public availability/banner projections, audit calls, a bounded expiry reconciliation cron, and Phase 6 pricing/reservation service boundaries. Module 05 is 85%, Module 07 is 78%, and Module 13 is 75%. Inventory is 90%, reservations 90%, business pricing 82%, promotions/coupons 78%, and banners 82%. Automated verification: 18 suites and 89 tests passed; syntax passed for 232 JavaScript files. Migration was not executed. Staging must precheck legacy stock/pricing/promotion/banner data, apply the migration, seed permissions/default warehouse, and test genuine MySQL locking plus cron behavior before Phase 6 production use.
+103
View File
@@ -0,0 +1,103 @@
# ZUMRI Phase 4 Catalogue and Content
## Objective
Create the product/content foundation required before inventory and shopping, with no stock, reservation, wholesale-pricing, cart, order, or payment behavior.
## Architecture
Catalogue models live under `app/models/catalogue`, domain services under `app/services/catalogue`, controllers/routes under their catalogue folders, and one new forward migration owns the schema. Phase 1 RBAC/audit and Phase 2 Upload/storage are reused.
## Brand
Brand has immutable identity, unique normalized slug, editorial description, optional owned logo, website, ACTIVE/INACTIVE state, and deterministic order. Names remain language-neutral; no unnecessary BrandTranslation table was added.
## Category Hierarchy
Category separates structural code/slug/parent/status from localized content. Root and nested categories are supported. A bounded ancestor walk rejects invalid parents, self-parenting, and cycles. Public APIs offer tree or flat representations.
## Product
Product stores structural identity, unique slug/code, brand/default category, DRAFT/ACTIVE/INACTIVE/ARCHIVED state, PUBLIC/HIDDEN visibility, featured flag, publication/new-arrival dates, and audit actors. It stores no stock quantity.
## Product Variants
Variants carry globally unique SKU, optional barcode, state, `DECIMAL(15,2)` base/compare-at prices, ISO-style currency code, optional weight, and order. No inventory columns exist.
## Product Options
Normalized ProductOption and ProductOptionValue records define variant dimensions. VariantOptionValue links validated same-product values. ProductAttribute holds non-variant descriptive facts separately.
## Pricing Boundary
Phase 4 persists catalogue display price and optional comparison price only. Decimal values remain strings in validation/serialization. Promotions, coupons, business tiers, wholesale/MOQ/volume pricing, and final checkout pricing are excluded.
## Product Media
ProductMedia associates Phase 2 Upload records with products and optional same-product variants. Only supported image uploads may be linked. Primary selection is transactional. Upload metadata moves from administrator ownership to CATALOGUE/Product ownership. Public access uses short signed URLs and never exposes storage internals.
## Localization
ProductTranslation and CategoryTranslation enforce one row per `en|si|ta` locale. CollectionTranslation localizes editorial collections. Resolution uses explicit locale, profile/header language, and English fallback without hardcoded UI translations.
## SEO / Slugs
Unique lowercase safe slugs exist for brands, categories, products, and collections. Product/category translations carry meta title/description. Published product slugs are restricted from mutation, so a slug-history subsystem is not currently necessary.
## Collections
Collections support type, state, publish window, hero upload, translations, and deterministic CollectionProduct ordering. `featured` on Product is the canonical global featured flag; collection membership is contextual editorial merchandising.
## Size Guides
Reusable SizeGuide records use strictly validated structured column/row JSON and may link to a category or product. Raw arbitrary HTML is prohibited.
## Product Relationships
Explicit RELATED, SIMILAR, and COMPLETE_THE_LOOK relations are unique and reject self-relations. They are curated content, not AI recommendations.
## Reviews
Authenticated customer/business-customer accounts may create one pending review per public active product. Rating is 1–5. Public detail exposes APPROVED reviews only. Permission-gated moderation records actor/time and audit events.
## Verified Purchase Future Integration
Clients cannot submit `verifiedPurchase`; it always starts false. A future order subsystem may derive or update it from authoritative completed order lines. Phase 4 fabricates no purchase evidence.
## Public Catalogue
Public product/category/brand/collection routes filter state and visibility at query time, resolve localized content, use eager associations, return small DTOs, compute active-variant price ranges, and hide internal catalogue/storage fields.
## Admin Catalogue
Permission-scoped routes support product creation/update/archive, variants, options, media, relations, brands, hierarchical categories, collections, size guides, and review moderation. Product creation is transactional and never deletes pre-existing Upload objects on rollback.
## Search and Filtering
MySQL/Sequelize search covers localized name, product code/SKU foundation, and brand name. Filters include category, brand, decimal price bounds, featured, and new arrival. Sort modes are allowlisted. Inventory and wholesale filters are absent. The search boundary can later be replaced without changing public DTOs.
## Permissions
Added `catalogue.products.read/create/update/delete`, `catalogue.categories.manage`, `catalogue.brands.manage`, `catalogue.collections.manage`, `catalogue.size-guides.manage`, `catalogue.reviews.read`, and `catalogue.reviews.moderate`. SUPER_ADMIN retains the Phase 1 bypass.
## Audit
Stable events cover product/variant/brand/category/collection creation and updates, publishing/archive, and review submission/moderation. Events store identifiers and concise metadata rather than descriptions or media contents.
## Database Changes
`20260903040000-phase-4-catalogue-content.js` creates only the models used by Phase 4, with foreign keys, uniqueness, state/search indexes, rating/self-relation checks, and DECIMAL prices. It is forward-only and was not run.
Migration pre-checks: identify legacy product/category/brand tables, duplicate slugs/SKUs/barcodes, currency inconsistencies, legacy media ownership, missing/orphan Uploads, and conflicting table names. Back up and resolve explicitly; no data is silently removed. `catalogueExample.seeder.js` is optional and creates inactive editable examples only.
## Tests
Phase 4 tests cover strict schemas, slug/money/rating/locale validation, verified-purchase spoofing, structured size guides, fallback localization, exact decimal comparison, category cycles, and centralized publish requirements. The full Phase 0–3 regression suite remains required.
## Remaining Known Issues
The migration and real MySQL constraints/query plans are untested. Staging must validate multi-include pagination, concurrent slug/SKU/media-primary operations, signed-media volume, and actual migration ordering. Review approval/rejection notification was intentionally not enabled to avoid spam without a confirmed product requirement. Product relation projection and full admin update/reorder endpoints can expand when frontend workflows are finalized.
## Phase 5 Prerequisites
Complete legacy pre-checks, apply migrations to restored staging, seed permissions, load optional editable content if desired, run public-query EXPLAIN tests with realistic volume, validate signed media and concurrency, and freeze the ProductVariant identifier contract needed by inventory.
@@ -0,0 +1,85 @@
# ZUMRI Phase 5 Inventory and Merchandising
## Objective
Provide authoritative multi-warehouse stock, reservation primitives, wholesale pricing, price quotes, promotions, coupons, and scheduled localized banners for Phase 6 consumers.
## Existing Components Reused
ProductVariant, BusinessCustomer, Upload, authorization middleware, audit queue, cron bootstrap, Zod, Sequelize, and catalogue localization are reused.
## Inventory Architecture
Catalogue never stores stock. `InventoryBalance` is authoritative per warehouse/variant; availability is `onHand - reserved`. All writes pass through one service and append an immutable ledger event.
## Warehouses
Multiple active/inactive warehouses and a transactionally selected default are supported. Default warehouse selection is deterministic.
## Inventory Balance
Quantities are integers. Service invariants prevent negative stock and `reserved > onHand`.
## Inventory Ledger
Every adjustment, reservation lifecycle event, and transfer has a unique event ID. There is no ledger update API.
## Stock Adjustments
Adjustments lock balances, enforce invariants, append a ledger row, support HTTP idempotency keys, and emit audit events.
## Transfers
Synchronous transfers lock warehouse IDs in sorted order, then create paired OUT/IN ledger rows.
## Reservation Architecture
Unique reservation keys make reserve/release/consume idempotent. Generic references avoid premature cart/order coupling.
## Reservation Concurrency
MySQL transactions and row-level `FOR UPDATE` locks serialize competitors for final units. Real multi-connection InnoDB validation remains a staging prerequisite.
## Reservation Expiry
A no-overlap minute cron reconciles up to 100 expired ACTIVE records per run; each record is locked and the database state is authoritative.
## Availability Projection
Public responses expose status and boolean sale availability only. Out-of-stock catalogue items remain visible.
## Low Stock
Low stock is centrally defined as available quantity less than or equal to the configured balance threshold.
## Business Pricing
Wholesale rules remain separate from retail base price and are limited to approved ACTIVE business accounts.
## Business Tiers
Business customers may reference an active tier; customer-specific negotiated rules are also supported.
## MOQ
MOQ belongs to a wholesale rule and never applies to retail fallback.
## Volume Pricing
Integer, non-overlapping ranges select the greatest eligible minimum quantity; final maximum may be null.
## Pricing Precedence
Retail -> eligible customer override (preferred over tier) -> volume tier -> highest-priority automatic promotion -> permitted coupon. Effective price floors at zero.
## Promotions
Percentage, fixed amount, and fixed price campaigns support dates, priority, minimum quantity, audiences, stacking, and normalized targets.
## Coupons
Codes are uppercase and validated against coupon/promotion state and schedule. Redemption counters are not fabricated before orders.
## Banners
Placements are string-configurable. Status, schedule, sort order, audience, and optional business tier drive projection.
## Localization
Banner translations use `en`, `si`, and `ta`, with requested locale then English fallback.
## Permissions
Inventory read/adjust/transfer/reservation/warehouse, business pricing read/manage, promotion read/manage, and banner manage permissions were added.
## Audit Events
Warehouse, adjustment, transfer, business-price, promotion, coupon, and banner mutations enqueue sanitized Phase 2 audit activities.
## Database Changes
The forward-only `20260903050000` migration adds 13 domain tables and the BusinessCustomer tier reference. Earlier migrations are unchanged.
## Tests
Unit coverage verifies decimal precision, zero floor, availability states, strict input, coupon normalization, and permission boundaries. Existing regression tests remain green.
## Remaining Known Issues
Patch endpoints for promotion/coupon/banner/business pricing and real infrastructure integration tests remain follow-up hardening. No production migration was run.
## Phase 6 Prerequisites
Run legacy-data prechecks and migration on staging; validate constraints and concurrent reservations using two real InnoDB connections; verify cron in a multi-instance deployment; seed permissions and warehouse data.