0f609d57bb
Branch price overrides were already applied at the till, in cash sales and in M-Pesa, but nothing could set them. - Products -> Branch prices (/admin/prices): pick a branch and set its price for any product; setting it back to the default removes the override, and "Use default" resets it. Branch managers see their own branch's prices read-only. - /api/branch-prices: GET (owner: any branch; manager: own), PUT and DELETE (owner only). Branch and product must belong to the business; prices must be above zero; who changed a price is recorded. - Branch pricing is a plan feature, as the Growth plan already advertised: Growth and Enterprise, plus any business on its free trial. A paid Starter business keeps any prices set during its trial and can reset them, but cannot set new ones. - The catalogue view on the Products page always shows default prices; the till's view shows what the branch charges. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
63 lines
2.4 KiB
TypeScript
63 lines
2.4 KiB
TypeScript
import { Schema, model, models, Model, Types, Document } from "mongoose";
|
|
|
|
export interface IProduct extends Document {
|
|
_id: Types.ObjectId;
|
|
businessId: Types.ObjectId; // tenant this product belongs to
|
|
label: string;
|
|
// Size measured in `unit` (litres for water, a count for side items).
|
|
// The field keeps its historical name so existing data needs no migration.
|
|
sizeLiters: number;
|
|
unit: string; // see PRODUCT_UNITS in src/lib/units.ts: "L", "pcs", "tray"...
|
|
category: string; // e.g. "Water", "Eggs"
|
|
unitPrice: number;
|
|
sortOrder: number;
|
|
isActive: boolean;
|
|
lowStockThreshold: number; // a branch at or below this many units shows a low-stock warning
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
}
|
|
|
|
const productSchema = new Schema<IProduct>(
|
|
{
|
|
businessId: { type: Schema.Types.ObjectId, ref: "Business", required: true, index: true },
|
|
label: { type: String, required: true, trim: true },
|
|
sizeLiters: { type: Number, required: true, min: 0.1 },
|
|
unit: { type: String, default: "L", trim: true },
|
|
category: { type: String, default: "Water", trim: true },
|
|
unitPrice: { type: Number, required: true, min: 0 },
|
|
sortOrder: { type: Number, default: 0 },
|
|
isActive: { type: Boolean, default: true, index: true },
|
|
lowStockThreshold: { type: Number, default: 10, min: 0 },
|
|
},
|
|
{ timestamps: true }
|
|
);
|
|
|
|
export const Product =
|
|
(models.Product as Model<IProduct>) || model<IProduct>("Product", productSchema);
|
|
|
|
// Branch-specific price override — optional, falls back to Product.unitPrice.
|
|
export interface IBranchPricing extends Document {
|
|
businessId: Types.ObjectId;
|
|
branchId: Types.ObjectId;
|
|
productId: Types.ObjectId;
|
|
unitPrice: number;
|
|
updatedBy: Types.ObjectId | null;
|
|
updatedAt: Date;
|
|
}
|
|
|
|
const branchPricingSchema = new Schema<IBranchPricing>(
|
|
{
|
|
businessId: { type: Schema.Types.ObjectId, ref: "Business", required: true, index: true },
|
|
branchId: { type: Schema.Types.ObjectId, ref: "Branch", required: true },
|
|
productId: { type: Schema.Types.ObjectId, ref: "Product", required: true },
|
|
unitPrice: { type: Number, required: true, min: 0 },
|
|
updatedBy: { type: Schema.Types.ObjectId, ref: "User", default: null },
|
|
},
|
|
{ timestamps: { createdAt: false, updatedAt: true } }
|
|
);
|
|
branchPricingSchema.index({ branchId: 1, productId: 1 }, { unique: true });
|
|
|
|
export const BranchPricing =
|
|
(models.BranchPricing as Model<IBranchPricing>) ||
|
|
model<IBranchPricing>("BranchPricing", branchPricingSchema);
|