|
const mongoose = require("mongoose");
|
|
const { v4: uuidv4 } = require("uuid");
|
|
const {
|
|
actionHistorySchema
|
|
} = require("../../models/Client/Client-model");
|
|
|
|
/*
|
|
* Generic name/value specification.
|
|
*
|
|
* Examples:
|
|
* Screen Size = 55 inch
|
|
* Package Size = 500 g
|
|
* Container Size = 10 inch
|
|
*/
|
|
const specificationSchema = new mongoose.Schema(
|
|
{
|
|
name: {
|
|
type: String,
|
|
required: true,
|
|
trim: true
|
|
},
|
|
|
|
value: {
|
|
type: mongoose.Schema.Types.Mixed,
|
|
required: true
|
|
},
|
|
|
|
unit: {
|
|
type: String,
|
|
trim: true,
|
|
default: undefined
|
|
},
|
|
|
|
filterable: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
|
|
displayOrder: {
|
|
type: Number,
|
|
default: 0
|
|
}
|
|
},
|
|
{
|
|
_id: false
|
|
}
|
|
);
|
|
|
|
/*
|
|
* Additional product images.
|
|
* Existing imageUrl remains the primary image.
|
|
*/
|
|
const ecommerceImageSchema = new mongoose.Schema(
|
|
{
|
|
url: {
|
|
type: String,
|
|
required: true,
|
|
trim: true
|
|
},
|
|
|
|
altText: {
|
|
type: String,
|
|
trim: true,
|
|
default: undefined
|
|
},
|
|
|
|
displayOrder: {
|
|
type: Number,
|
|
default: 0
|
|
}
|
|
},
|
|
{
|
|
_id: false
|
|
}
|
|
);
|
|
|
|
/*
|
|
* E-commerce SEO fields.
|
|
*/
|
|
const seoSchema = new mongoose.Schema(
|
|
{
|
|
title: {
|
|
type: String,
|
|
trim: true,
|
|
maxlength: 70,
|
|
default: undefined
|
|
},
|
|
|
|
description: {
|
|
type: String,
|
|
trim: true,
|
|
maxlength: 170,
|
|
default: undefined
|
|
},
|
|
|
|
keywords: {
|
|
type: [String],
|
|
default: undefined
|
|
}
|
|
},
|
|
{
|
|
_id: false,
|
|
minimize: true
|
|
}
|
|
);
|
|
|
|
/*
|
|
* Additional e-commerce configuration.
|
|
*
|
|
* Existing fields such as showInEcomm, isDelivery and featureProduct
|
|
* remain authoritative during the compatibility period.
|
|
*/
|
|
const ecommerceMetadataSchema = new mongoose.Schema(
|
|
{
|
|
/*
|
|
* Additional customer search words.
|
|
* Do not duplicate product name/category/brand unnecessarily.
|
|
*/
|
|
searchKeywords: {
|
|
type: [String],
|
|
default: undefined
|
|
},
|
|
|
|
/*
|
|
* Examples: NEW, POPULAR, BEST_SELLER, SEASONAL.
|
|
*/
|
|
badges: {
|
|
type: [String],
|
|
default: undefined
|
|
},
|
|
|
|
/*
|
|
* Dynamic filter values not already represented by:
|
|
* category, subcategory, brand, colors, price or tags.
|
|
*
|
|
* Examples:
|
|
* {
|
|
* size: ["M", "L"],
|
|
* material: ["cotton"],
|
|
* storage: ["128GB"],
|
|
* energyRating: ["5-star"]
|
|
* }
|
|
*/
|
|
filterAttributes: {
|
|
type: Map,
|
|
of: mongoose.Schema.Types.Mixed,
|
|
default: undefined
|
|
},
|
|
|
|
additionalImages: {
|
|
type: [ecommerceImageSchema],
|
|
default: undefined
|
|
},
|
|
|
|
seo: {
|
|
type: seoSchema,
|
|
default: undefined
|
|
},
|
|
|
|
displayOrder: {
|
|
type: Number,
|
|
default: undefined
|
|
},
|
|
|
|
newArrivalUntil: {
|
|
type: Date,
|
|
default: undefined
|
|
},
|
|
|
|
allowPickup: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
|
|
minimumOrderQuantity: {
|
|
type: Number,
|
|
min: 1,
|
|
default: 1
|
|
},
|
|
|
|
maximumOrderQuantity: {
|
|
type: Number,
|
|
min: 1,
|
|
default: undefined
|
|
},
|
|
|
|
orderIncrement: {
|
|
type: Number,
|
|
min: 0.001,
|
|
default: 1
|
|
},
|
|
|
|
allowBackorder: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
|
|
availabilityMessage: {
|
|
type: String,
|
|
trim: true,
|
|
maxlength: 250,
|
|
default: undefined
|
|
}
|
|
},
|
|
{
|
|
_id: false,
|
|
minimize: true
|
|
}
|
|
);
|
|
|
|
/*
|
|
* Optional Commerce Intelligence configuration.
|
|
*/
|
|
const commerceIntelligenceSchema = new mongoose.Schema(
|
|
{
|
|
enabled: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
|
|
occasionTags: {
|
|
type: [String],
|
|
default: undefined
|
|
},
|
|
|
|
seasonTags: {
|
|
type: [String],
|
|
default: undefined
|
|
},
|
|
|
|
useCaseTags: {
|
|
type: [String],
|
|
default: undefined
|
|
},
|
|
|
|
targetRegions: {
|
|
type: [String],
|
|
default: undefined
|
|
},
|
|
|
|
recommendationPriority: {
|
|
type: Number,
|
|
min: 0,
|
|
max: 100,
|
|
default: undefined
|
|
},
|
|
|
|
excludeFromRecommendations: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
|
|
manuallyVerifiedTags: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
|
|
lastClassificationAt: {
|
|
type: Date,
|
|
default: undefined
|
|
}
|
|
},
|
|
{
|
|
_id: false,
|
|
minimize: true
|
|
}
|
|
);
|
|
|
|
/*
|
|
* Existing packaging options.
|
|
*/
|
|
const packagingOptionSchema = new mongoose.Schema(
|
|
{
|
|
boxType: {
|
|
type: String,
|
|
required: true,
|
|
trim: true
|
|
},
|
|
|
|
quantity: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
price: {
|
|
type: Number,
|
|
min: 0,
|
|
default: undefined
|
|
},
|
|
|
|
offer_price: {
|
|
type: Number,
|
|
min: 0,
|
|
default: undefined
|
|
}
|
|
},
|
|
{
|
|
_id: false
|
|
}
|
|
);
|
|
|
|
const productSchema = new mongoose.Schema(
|
|
{
|
|
/*
|
|
* =========================================================
|
|
* 1. PRODUCT IDENTITY
|
|
* =========================================================
|
|
*/
|
|
|
|
_id: {
|
|
type: String,
|
|
default: uuidv4
|
|
},
|
|
|
|
UId: {
|
|
type: String,
|
|
unique: true,
|
|
sparse: true,
|
|
index: true,
|
|
trim: true
|
|
},
|
|
|
|
name: {
|
|
type: String,
|
|
required: true,
|
|
trim: true
|
|
},
|
|
|
|
/*
|
|
* Optional e-commerce URL identifier.
|
|
* Can be deferred if current routes use _id or UId.
|
|
*/
|
|
slug: {
|
|
type: String,
|
|
lowercase: true,
|
|
trim: true,
|
|
sparse: true,
|
|
default: undefined
|
|
},
|
|
|
|
barcode: {
|
|
type: String,
|
|
trim: true,
|
|
sparse: true,
|
|
default: undefined
|
|
},
|
|
|
|
/*
|
|
* =========================================================
|
|
* 2. CATEGORY CONNECTIONS
|
|
* =========================================================
|
|
*/
|
|
|
|
catId: {
|
|
type: String,
|
|
ref: "NurseryCategory",
|
|
required: true,
|
|
index: true
|
|
},
|
|
|
|
/*
|
|
* Retain for existing pages.
|
|
*/
|
|
catName: {
|
|
type: String,
|
|
required: true,
|
|
trim: true
|
|
},
|
|
|
|
subCatId: {
|
|
type: String,
|
|
ref: "NurserySubCategory",
|
|
default: null,
|
|
index: true
|
|
},
|
|
|
|
/*
|
|
* Retain for existing pages.
|
|
*/
|
|
subCatName: {
|
|
type: String,
|
|
default: null,
|
|
trim: true
|
|
},
|
|
|
|
/*
|
|
* Snapshot from Category.categoryType.
|
|
*
|
|
* Examples:
|
|
* Nursery, Landscaping, Fashion, Electronics.
|
|
*
|
|
* Backend derives this from catId.
|
|
*/
|
|
categoryType: {
|
|
type: String,
|
|
trim: true,
|
|
default: undefined,
|
|
index: true
|
|
},
|
|
|
|
/*
|
|
* Snapshot from Category.type.
|
|
*
|
|
* Examples:
|
|
* plant, Apparel, Mobile_Device, Home_Appliance.
|
|
*
|
|
* Backend derives this from catId.
|
|
*/
|
|
attributeType: {
|
|
type: String,
|
|
trim: true,
|
|
default: undefined,
|
|
index: true
|
|
},
|
|
|
|
/*
|
|
* 1 or missing = existing legacy product
|
|
* 2 = attributes/e-commerce metadata product
|
|
*/
|
|
schemaVersion: {
|
|
type: Number,
|
|
enum: [1, 2],
|
|
default: 2,
|
|
index: true
|
|
},
|
|
|
|
/*
|
|
* =========================================================
|
|
* 3. OWNERSHIP AND VENDOR
|
|
* =========================================================
|
|
*/
|
|
|
|
userId: {
|
|
type: String,
|
|
ref: "NurseryUser",
|
|
required: true
|
|
},
|
|
|
|
vendor: [
|
|
{
|
|
type: String,
|
|
ref: "Vendor"
|
|
}
|
|
],
|
|
|
|
/*
|
|
* =========================================================
|
|
* 4. GLOBAL PRESENTATION AND SEARCH FIELDS
|
|
* =========================================================
|
|
*/
|
|
|
|
brand: {
|
|
type: String,
|
|
trim: true,
|
|
default: undefined,
|
|
index: true
|
|
},
|
|
|
|
imageUrl: {
|
|
type: String,
|
|
trim: true,
|
|
default: undefined
|
|
},
|
|
|
|
shortDescription: {
|
|
type: String,
|
|
trim: true,
|
|
maxlength: 500,
|
|
default: undefined
|
|
},
|
|
|
|
description: {
|
|
type: String,
|
|
trim: true,
|
|
default: undefined
|
|
},
|
|
|
|
/*
|
|
* Retain existing single specification field.
|
|
*/
|
|
specification: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
/*
|
|
* New structured specifications.
|
|
*/
|
|
specifications: {
|
|
type: [specificationSchema],
|
|
default: undefined
|
|
},
|
|
|
|
features: {
|
|
type: [String],
|
|
default: undefined
|
|
},
|
|
|
|
tags: {
|
|
type: [String],
|
|
default: undefined
|
|
},
|
|
|
|
/*
|
|
* Existing schema contains both color and colors.
|
|
* Retain both temporarily for compatibility.
|
|
*/
|
|
color: {
|
|
type: String,
|
|
trim: true,
|
|
default: undefined
|
|
},
|
|
|
|
colors: {
|
|
type: [String],
|
|
default: undefined
|
|
},
|
|
|
|
/*
|
|
* =========================================================
|
|
* 5. PRICE AND TAX
|
|
* =========================================================
|
|
*/
|
|
|
|
price: {
|
|
type: Number,
|
|
required: true,
|
|
min: 0
|
|
},
|
|
|
|
offer_price: {
|
|
type: Number,
|
|
min: 0,
|
|
default: undefined,
|
|
|
|
validate: {
|
|
validator(value) {
|
|
return (
|
|
value === undefined ||
|
|
value === null ||
|
|
this.price === undefined ||
|
|
value <= this.price
|
|
);
|
|
},
|
|
message: "Offer price cannot exceed the regular price"
|
|
}
|
|
},
|
|
|
|
purchase_cost: {
|
|
type: Number,
|
|
min: 0,
|
|
default: undefined
|
|
},
|
|
|
|
purchase_date: {
|
|
type: Date,
|
|
default: undefined
|
|
},
|
|
|
|
taxCategoryId: {
|
|
type: mongoose.Schema.Types.ObjectId,
|
|
ref: "TaxCategory",
|
|
default: undefined
|
|
},
|
|
|
|
nonTaxable: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
|
|
/*
|
|
* =========================================================
|
|
* 6. AUTHORITATIVE INVENTORY
|
|
* =========================================================
|
|
*/
|
|
|
|
quantity: {
|
|
type: Number,
|
|
required: true,
|
|
min: 0
|
|
},
|
|
|
|
/*
|
|
* Subscription inventory reservation.
|
|
*/
|
|
reservedQuantity: {
|
|
type: Number,
|
|
default: 0,
|
|
min: 0
|
|
},
|
|
|
|
/*
|
|
* Regular order inventory reservation.
|
|
*/
|
|
reservedOrderQuantity: {
|
|
type: Number,
|
|
default: 0,
|
|
min: 0
|
|
},
|
|
|
|
restock_level: {
|
|
type: Number,
|
|
default: 0,
|
|
min: 0
|
|
},
|
|
|
|
reorder_level: {
|
|
type: Number,
|
|
default: 0,
|
|
min: 0
|
|
},
|
|
|
|
rack_id: {
|
|
type: String,
|
|
ref: "Rack",
|
|
default: undefined
|
|
},
|
|
|
|
/*
|
|
* =========================================================
|
|
* 7. SUBSCRIPTION-CONTROLLED INVENTORY
|
|
* =========================================================
|
|
*/
|
|
|
|
isSubscriptionControlled: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
|
|
weeklySubscriptionCap: {
|
|
type: Number,
|
|
min: 0,
|
|
default: undefined
|
|
},
|
|
|
|
perSubscriberWeeklyQty: {
|
|
type: Number,
|
|
min: 0,
|
|
default: undefined
|
|
},
|
|
|
|
/*
|
|
* =========================================================
|
|
* 8. PREORDER CONFIGURATION
|
|
* =========================================================
|
|
*/
|
|
|
|
isPreorder: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
|
|
preOrderQuantity: {
|
|
type: Number,
|
|
min: 0,
|
|
default: 0
|
|
},
|
|
|
|
preorderLimit: {
|
|
type: Number,
|
|
min: 0,
|
|
default: undefined
|
|
},
|
|
|
|
preorderPrice: {
|
|
type: Number,
|
|
min: 0,
|
|
default: undefined
|
|
},
|
|
|
|
expectedAvailabilityDate: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
expectedAvailabilityFromDate: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
expectedAvailabilityToDate: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
availabilityFromtime: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
availabilityTotime: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
availabilityType: {
|
|
type: String,
|
|
enum: ["date", "days"],
|
|
default: undefined
|
|
},
|
|
|
|
productAvailableFrom: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
productAvailableTo: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
pickedUpLocation: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
/*
|
|
* =========================================================
|
|
* 9. UNIT AND PACKAGING
|
|
* =========================================================
|
|
*/
|
|
|
|
weightType: {
|
|
type: String,
|
|
trim: true,
|
|
default: undefined
|
|
},
|
|
|
|
weight: {
|
|
type: String,
|
|
trim: true,
|
|
default: undefined
|
|
},
|
|
|
|
unitType: {
|
|
type: String,
|
|
trim: true,
|
|
default: undefined
|
|
},
|
|
|
|
packagingOptions: {
|
|
type: [packagingOptionSchema],
|
|
default: undefined
|
|
},
|
|
|
|
/*
|
|
* =========================================================
|
|
* 10. POS AND E-COMMERCE FLAGS
|
|
* =========================================================
|
|
*/
|
|
|
|
showInEcomm: {
|
|
type: Boolean,
|
|
default: false,
|
|
index: true
|
|
},
|
|
|
|
isDelivery: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
|
|
featureProduct: {
|
|
type: Boolean,
|
|
default: false,
|
|
index: true
|
|
},
|
|
|
|
isFav: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
|
|
posOrder: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
|
|
ecommerceMetadata: {
|
|
type: ecommerceMetadataSchema,
|
|
default: undefined
|
|
},
|
|
|
|
/*
|
|
* =========================================================
|
|
* 11. CATEGORY-SPECIFIC ATTRIBUTES
|
|
* =========================================================
|
|
*
|
|
* Save only fields allowed for Category.type.
|
|
*
|
|
* Plant example:
|
|
* {
|
|
* botanicalName: "Aloe vera",
|
|
* sunlightRequirement: "Full Sun",
|
|
* wateringNeed: "Low"
|
|
* }
|
|
*
|
|
* Apparel example:
|
|
* {
|
|
* gender: "Men",
|
|
* material: "Cotton",
|
|
* sizes: ["M", "L"]
|
|
* }
|
|
*/
|
|
attributes: {
|
|
type: Map,
|
|
of: mongoose.Schema.Types.Mixed,
|
|
default: undefined
|
|
},
|
|
|
|
/*
|
|
* =========================================================
|
|
* 12. COMMERCE INTELLIGENCE
|
|
* =========================================================
|
|
*/
|
|
|
|
commerceIntelligence: {
|
|
type: commerceIntelligenceSchema,
|
|
default: undefined
|
|
},
|
|
|
|
/*
|
|
* =========================================================
|
|
* 13. STATUS AND AUDIT
|
|
* =========================================================
|
|
*/
|
|
|
|
active: {
|
|
type: Boolean,
|
|
default: true,
|
|
index: true
|
|
},
|
|
|
|
isDeleted: {
|
|
type: Boolean,
|
|
default: false,
|
|
index: true
|
|
},
|
|
|
|
deletedBy: {
|
|
type: String,
|
|
ref: "NurseryUser",
|
|
default: undefined
|
|
},
|
|
|
|
deletedAt: {
|
|
type: Date,
|
|
default: undefined
|
|
},
|
|
|
|
updatedUser: {
|
|
type: String,
|
|
ref: "NurseryUser",
|
|
default: undefined
|
|
},
|
|
|
|
actionHistory: {
|
|
type: [actionHistorySchema],
|
|
default: undefined
|
|
},
|
|
|
|
/*
|
|
* Optional migration information.
|
|
*/
|
|
migrationMetadata: {
|
|
migratedAt: {
|
|
type: Date,
|
|
default: undefined
|
|
},
|
|
|
|
migratedBy: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
sourceVersion: {
|
|
type: Number,
|
|
default: undefined
|
|
},
|
|
|
|
warnings: {
|
|
type: [String],
|
|
default: undefined
|
|
}
|
|
}
|
|
},
|
|
{
|
|
timestamps: true,
|
|
|
|
/*
|
|
* Prevent empty nested objects from being stored.
|
|
*/
|
|
minimize: true,
|
|
|
|
/*
|
|
* Retain virtuals if added later.
|
|
*/
|
|
toJSON: {
|
|
virtuals: true
|
|
},
|
|
|
|
toObject: {
|
|
virtuals: true
|
|
}
|
|
}
|
|
);
|
|
|
|
|
|
/**********************************************************************//////////////////////////////////////////////////
|
|
/*
|
|
* PLANT/TREE LEGACY FIELDS
|
|
*/
|
|
productSchema.add({
|
|
medical_name: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
plantType: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
varietyCultivar: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
commonLocalName: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
treeAge: {
|
|
type: Number,
|
|
default: undefined
|
|
},
|
|
|
|
height: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
lifeSpan: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
soilType: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
wateringNeeds: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
sunlightRequirements: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
droughtTolerance: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
growingSeason: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
maturePlantHeight: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
RecommendedFertilizer: {
|
|
type: [String],
|
|
default: undefined
|
|
}
|
|
});
|
|
|
|
////////////////////*****************************************************//////////////////////////////////////////
|
|
/*
|
|
* FOOD/GROCERY LEGACY FIELDS
|
|
*/
|
|
productSchema.add({
|
|
foodType: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
organic: {
|
|
type: Boolean,
|
|
default: undefined
|
|
},
|
|
|
|
nonGMO: {
|
|
type: Boolean,
|
|
default: undefined
|
|
},
|
|
|
|
glutenFree: {
|
|
type: Boolean,
|
|
default: undefined
|
|
},
|
|
|
|
vegan: {
|
|
type: Boolean,
|
|
default: undefined
|
|
},
|
|
|
|
kosher: {
|
|
type: Boolean,
|
|
default: undefined
|
|
},
|
|
|
|
halal: {
|
|
type: Boolean,
|
|
default: undefined
|
|
},
|
|
|
|
packageSize: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
harvestDate: {
|
|
type: Date,
|
|
default: undefined
|
|
},
|
|
|
|
expirationDate: {
|
|
type: Date,
|
|
default: undefined
|
|
},
|
|
|
|
manufacturedDate: {
|
|
type: Date,
|
|
default: undefined
|
|
},
|
|
|
|
storageConditions: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
storageInstructions: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
countryOfOrigin: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
tasteProfile: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
ripeness: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
|
|
nutritionalInfo: {
|
|
type: mongoose.Schema.Types.Mixed,
|
|
default: undefined
|
|
}
|
|
});
|