Files
ERP/src/lib/server/models/Product.ts
T
denis_ireri 0f609d57bb Add a screen to set prices per branch
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>
2026-10-12 10:45:37 +03:00

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);