Converting server to dotnet

This commit is contained in:
Zeeshaun
2026-05-05 16:22:37 +00:00
parent 74d2170880
commit 095eb4b50d
21 changed files with 636 additions and 296 deletions
+56
View File
@@ -0,0 +1,56 @@
using RanazePos.Api.Models;
namespace RanazePos.Api.Configuration;
public static class Defaults
{
public static readonly PosSettings Settings = new()
{
SizeOptions =
[
new() { Name = "Regular", Delta = 0m },
new() { Name = "Large", Delta = 1m }
],
SweetnessOptions = ["0%", "25%", "50%", "75%", "100%"],
IceOptions = ["No Ice", "Less Ice", "Regular Ice"],
Toppings =
[
new() { Name = "Boba", Price = 0.75m },
new() { Name = "Pudding", Price = 0.85m },
new() { Name = "Cheese Foam", Price = 1.25m },
new() { Name = "Lychee Jelly", Price = 0.75m }
]
};
public static readonly IReadOnlyList<MenuItemDocument> Menu =
[
new()
{
Name = "Classic Milk Tea",
Category = "Milk Tea",
Description = "Black tea, milk, and brown sugar tapioca.",
BasePrice = 5.5m
},
new()
{
Name = "Taro Milk Tea",
Category = "Milk Tea",
Description = "Creamy taro with optional boba or pudding.",
BasePrice = 6m
},
new()
{
Name = "Mango Green Tea",
Category = "Fruit Tea",
Description = "Bright green tea with mango puree.",
BasePrice = 5.75m
},
new()
{
Name = "Strawberry Yakult",
Category = "Specialty",
Description = "Strawberry fruit tea with probiotic yogurt.",
BasePrice = 6.25m
}
];
}
+10
View File
@@ -0,0 +1,10 @@
namespace RanazePos.Api.Configuration;
public sealed class MongoDbOptions
{
public const string SectionName = "MongoDb";
public string ConnectionString { get; set; } = "mongodb://127.0.0.1:27017";
public string DatabaseName { get; set; } = "ranaze-pos";
}
+36
View File
@@ -0,0 +1,36 @@
using System.Text.Json.Serialization;
namespace RanazePos.Api.Configuration;
public sealed class PosSettings
{
[JsonPropertyName("sizeOptions")]
public required IReadOnlyList<SizeOption> SizeOptions { get; init; }
[JsonPropertyName("sweetnessOptions")]
public required IReadOnlyList<string> SweetnessOptions { get; init; }
[JsonPropertyName("iceOptions")]
public required IReadOnlyList<string> IceOptions { get; init; }
[JsonPropertyName("toppings")]
public required IReadOnlyList<ToppingOption> Toppings { get; init; }
}
public sealed class SizeOption
{
[JsonPropertyName("name")]
public required string Name { get; init; }
[JsonPropertyName("delta")]
public decimal Delta { get; init; }
}
public sealed class ToppingOption
{
[JsonPropertyName("name")]
public required string Name { get; init; }
[JsonPropertyName("price")]
public decimal Price { get; init; }
}
+34
View File
@@ -0,0 +1,34 @@
namespace RanazePos.Api.Infrastructure;
public static class DotEnvLoader
{
public static IDictionary<string, string?> Load(string path)
{
var values = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
if (!File.Exists(path))
{
return values;
}
foreach (var rawLine in File.ReadAllLines(path))
{
var line = rawLine.Trim();
if (string.IsNullOrWhiteSpace(line) || line.StartsWith('#'))
{
continue;
}
var separatorIndex = line.IndexOf('=');
if (separatorIndex <= 0)
{
continue;
}
var key = line[..separatorIndex].Trim();
var value = line[(separatorIndex + 1)..].Trim().Trim('"');
values[key] = value;
}
return values;
}
}
+40
View File
@@ -0,0 +1,40 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using System.Text.Json.Serialization;
namespace RanazePos.Api.Models;
public sealed class MenuItemDocument
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
[JsonPropertyName("_id")]
public string? Id { get; set; }
[BsonElement("name")]
[JsonPropertyName("name")]
public required string Name { get; set; }
[BsonElement("category")]
[JsonPropertyName("category")]
public required string Category { get; set; }
[BsonElement("description")]
[JsonPropertyName("description")]
public string Description { get; set; } = "";
[BsonElement("basePrice")]
[BsonRepresentation(BsonType.Decimal128)]
[JsonPropertyName("basePrice")]
public decimal BasePrice { get; set; }
[BsonElement("createdAt")]
[BsonDateTimeOptions(Kind = DateTimeKind.Utc)]
[JsonPropertyName("createdAt")]
public DateTime CreatedAt { get; set; }
[BsonElement("updatedAt")]
[BsonDateTimeOptions(Kind = DateTimeKind.Utc)]
[JsonPropertyName("updatedAt")]
public DateTime UpdatedAt { get; set; }
}
+84
View File
@@ -0,0 +1,84 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using System.Text.Json.Serialization;
namespace RanazePos.Api.Models;
public sealed class OrderDocument
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
[JsonPropertyName("_id")]
public string? Id { get; set; }
[BsonElement("number")]
[JsonPropertyName("number")]
public int Number { get; set; }
[BsonElement("customerName")]
[JsonPropertyName("customerName")]
public string CustomerName { get; set; } = "";
[BsonElement("paymentMethod")]
[JsonPropertyName("paymentMethod")]
public string PaymentMethod { get; set; } = "cash";
[BsonElement("status")]
[JsonPropertyName("status")]
public string Status { get; set; } = "new";
[BsonElement("lines")]
[JsonPropertyName("lines")]
public required List<OrderLineDocument> Lines { get; set; }
[BsonElement("total")]
[BsonRepresentation(BsonType.Decimal128)]
[JsonPropertyName("total")]
public decimal Total { get; set; }
[BsonElement("createdAt")]
[BsonDateTimeOptions(Kind = DateTimeKind.Utc)]
[JsonPropertyName("createdAt")]
public DateTime CreatedAt { get; set; }
[BsonElement("updatedAt")]
[BsonDateTimeOptions(Kind = DateTimeKind.Utc)]
[JsonPropertyName("updatedAt")]
public DateTime UpdatedAt { get; set; }
}
public sealed class OrderLineDocument
{
[BsonElement("menuItemId")]
[JsonPropertyName("menuItemId")]
public required string MenuItemId { get; set; }
[BsonElement("name")]
[JsonPropertyName("name")]
public required string Name { get; set; }
[BsonElement("basePrice")]
[BsonRepresentation(BsonType.Decimal128)]
[JsonPropertyName("basePrice")]
public decimal BasePrice { get; set; }
[BsonElement("size")]
[JsonPropertyName("size")]
public required string Size { get; set; }
[BsonElement("sweetness")]
[JsonPropertyName("sweetness")]
public required string Sweetness { get; set; }
[BsonElement("ice")]
[JsonPropertyName("ice")]
public required string Ice { get; set; }
[BsonElement("toppings")]
[JsonPropertyName("toppings")]
public List<string> Toppings { get; set; } = [];
[BsonElement("notes")]
[JsonPropertyName("notes")]
public string Notes { get; set; } = "";
}
+21
View File
@@ -0,0 +1,21 @@
namespace RanazePos.Api.Models;
public sealed class CreateMenuItemRequest
{
public string? Name { get; set; }
public string? Category { get; set; }
public string? Description { get; set; }
public decimal? BasePrice { get; set; }
}
public sealed class CreateOrderRequest
{
public string? CustomerName { get; set; }
public string? PaymentMethod { get; set; }
public List<OrderLineDocument>? Lines { get; set; }
}
public sealed class UpdateOrderStatusRequest
{
public string? Status { get; set; }
}
+322
View File
@@ -0,0 +1,322 @@
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using RanazePos.Api.Configuration;
using RanazePos.Api.Infrastructure;
using RanazePos.Api.Models;
var builder = WebApplication.CreateBuilder(args);
var allowedPaymentMethods = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "cash", "tab", "unpaid" };
var allowedStatuses = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "new", "making", "ready", "completed" };
var dotEnvPath = Path.Combine(builder.Environment.ContentRootPath, "..", ".env");
builder.Configuration.AddInMemoryCollection(DotEnvLoader.Load(dotEnvPath));
var port = builder.Configuration.GetValue("PORT", 4000);
builder.WebHost.UseUrls($"http://0.0.0.0:{port}");
builder.Services
.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy
.WithOrigins("http://localhost:5173", "http://127.0.0.1:5173")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
builder.Services.Configure<MongoDbOptions>(options =>
{
var connectionString = builder.Configuration["MONGODB_URI"];
var databaseName = builder.Configuration["MONGODB_DATABASE"];
builder.Configuration.GetSection(MongoDbOptions.SectionName).Bind(options);
if (!string.IsNullOrWhiteSpace(connectionString))
{
options.ConnectionString = connectionString;
}
if (!string.IsNullOrWhiteSpace(databaseName))
{
options.DatabaseName = databaseName;
}
else if (!string.IsNullOrWhiteSpace(connectionString))
{
var databaseFromConnection = MongoUrl.Create(connectionString).DatabaseName;
if (!string.IsNullOrWhiteSpace(databaseFromConnection))
{
options.DatabaseName = databaseFromConnection;
}
}
});
builder.Services.AddSingleton<IMongoDatabase>(serviceProvider =>
{
var options = serviceProvider.GetRequiredService<IOptions<MongoDbOptions>>().Value;
var client = new MongoClient(options.ConnectionString);
return client.GetDatabase(options.DatabaseName);
});
builder.Services.AddSingleton(serviceProvider =>
serviceProvider.GetRequiredService<IMongoDatabase>().GetCollection<MenuItemDocument>("MenuItem"));
builder.Services.AddSingleton(serviceProvider =>
serviceProvider.GetRequiredService<IMongoDatabase>().GetCollection<OrderDocument>("Order"));
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.PropertyNamingPolicy = null;
});
var app = builder.Build();
app.UseCors();
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var exception = context.Features.Get<Microsoft.AspNetCore.Diagnostics.IExceptionHandlerFeature>()?.Error;
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new { error = exception?.Message ?? "Internal server error" });
});
});
var menuCollection = app.Services.GetRequiredService<IMongoCollection<MenuItemDocument>>();
var orderCollection = app.Services.GetRequiredService<IMongoCollection<OrderDocument>>();
await EnsureIndexesAsync(orderCollection);
app.MapGet("/api/health", () => Results.Json(new { ok = true }));
app.MapGet("/api/bootstrap", async () =>
{
await EnsureSeedMenuAsync(menuCollection);
var menu = await menuCollection
.Find(Builders<MenuItemDocument>.Filter.Empty)
.SortBy(item => item.Category)
.ThenBy(item => item.Name)
.ToListAsync();
var orders = await orderCollection
.Find(order => order.Status != "completed")
.SortByDescending(order => order.CreatedAt)
.ToListAsync();
return Results.Json(new
{
menu,
orders,
settings = Defaults.Settings
});
});
app.MapPost("/api/menu", async (CreateMenuItemRequest request) =>
{
if (string.IsNullOrWhiteSpace(request.Name) ||
string.IsNullOrWhiteSpace(request.Category) ||
request.BasePrice is null ||
request.BasePrice < 0)
{
return Results.BadRequest(new { error = "Invalid menu item payload." });
}
var timestamp = DateTime.UtcNow;
var menuItem = new MenuItemDocument
{
Name = request.Name.Trim(),
Category = request.Category.Trim(),
Description = request.Description?.Trim() ?? "",
BasePrice = request.BasePrice.Value,
CreatedAt = timestamp,
UpdatedAt = timestamp
};
await menuCollection.InsertOneAsync(menuItem);
return Results.Json(new { menuItem }, statusCode: 201);
});
app.MapDelete("/api/menu/{menuItemId}", async (string menuItemId) =>
{
await menuCollection.DeleteOneAsync(item => item.Id == menuItemId);
return Results.Json(new { ok = true });
});
app.MapPost("/api/menu/reset", async () =>
{
await menuCollection.DeleteManyAsync(Builders<MenuItemDocument>.Filter.Empty);
await SeedMenuAsync(menuCollection);
var menu = await menuCollection
.Find(Builders<MenuItemDocument>.Filter.Empty)
.SortBy(item => item.Category)
.ThenBy(item => item.Name)
.ToListAsync();
return Results.Json(new { menu });
});
app.MapPost("/api/orders", async (CreateOrderRequest request) =>
{
if (request.Lines is null || request.Lines.Count == 0)
{
return Results.BadRequest(new { error = "Orders require at least one line item." });
}
var paymentMethod = string.IsNullOrWhiteSpace(request.PaymentMethod) ? "cash" : request.PaymentMethod.Trim();
if (!allowedPaymentMethods.Contains(paymentMethod))
{
return Results.BadRequest(new { error = "Invalid payment method." });
}
if (request.Lines.Any(line =>
string.IsNullOrWhiteSpace(line.MenuItemId) ||
string.IsNullOrWhiteSpace(line.Name) ||
string.IsNullOrWhiteSpace(line.Size) ||
string.IsNullOrWhiteSpace(line.Sweetness) ||
string.IsNullOrWhiteSpace(line.Ice)))
{
return Results.BadRequest(new { error = "Invalid order payload." });
}
var timestamp = DateTime.UtcNow;
var order = new OrderDocument
{
Number = await NextOrderNumberAsync(orderCollection),
CustomerName = request.CustomerName?.Trim() ?? "",
PaymentMethod = paymentMethod,
Status = "new",
Lines = request.Lines.Select(line => new OrderLineDocument
{
MenuItemId = line.MenuItemId,
Name = line.Name,
BasePrice = line.BasePrice,
Size = line.Size,
Sweetness = line.Sweetness,
Ice = line.Ice,
Toppings = line.Toppings ?? [],
Notes = line.Notes ?? ""
}).ToList(),
Total = ComputeOrderTotal(request.Lines, Defaults.Settings),
CreatedAt = timestamp,
UpdatedAt = timestamp
};
await orderCollection.InsertOneAsync(order);
return Results.Json(new { order }, statusCode: 201);
});
app.MapPatch("/api/orders/{orderId}/status", async (string orderId, UpdateOrderStatusRequest request) =>
{
if (string.IsNullOrWhiteSpace(request.Status) || !allowedStatuses.Contains(request.Status))
{
return Results.BadRequest(new { error = "Invalid order status." });
}
var update = Builders<OrderDocument>.Update
.Set(order => order.Status, request.Status)
.Set(order => order.UpdatedAt, DateTime.UtcNow);
var options = new FindOneAndUpdateOptions<OrderDocument>
{
ReturnDocument = ReturnDocument.After
};
var order = await orderCollection.FindOneAndUpdateAsync(order => order.Id == orderId, update, options);
return Results.Json(new { order });
});
app.MapDelete("/api/orders/{orderId}", async (string orderId) =>
{
await orderCollection.DeleteOneAsync(order => order.Id == orderId);
return Results.Json(new { ok = true });
});
var clientDistPath = Path.GetFullPath(Path.Combine(builder.Environment.ContentRootPath, "..", "client", "dist"));
if (Directory.Exists(clientDistPath))
{
var staticFiles = new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(clientDistPath),
ContentTypeProvider = new FileExtensionContentTypeProvider()
};
app.UseDefaultFiles(new DefaultFilesOptions
{
FileProvider = new PhysicalFileProvider(clientDistPath)
});
app.UseStaticFiles(staticFiles);
app.MapFallback(async context =>
{
if (context.Request.Path.StartsWithSegments("/api"))
{
context.Response.StatusCode = 404;
return;
}
await context.Response.SendFileAsync(Path.Combine(clientDistPath, "index.html"));
});
}
app.Run();
static async Task EnsureIndexesAsync(IMongoCollection<OrderDocument> orders)
{
var indexModel = new CreateIndexModel<OrderDocument>(
Builders<OrderDocument>.IndexKeys.Ascending(order => order.Number),
new CreateIndexOptions { Unique = true });
await orders.Indexes.CreateOneAsync(indexModel);
}
static async Task EnsureSeedMenuAsync(IMongoCollection<MenuItemDocument> menuCollection)
{
var count = await menuCollection.CountDocumentsAsync(Builders<MenuItemDocument>.Filter.Empty);
if (count == 0)
{
await SeedMenuAsync(menuCollection);
}
}
static async Task SeedMenuAsync(IMongoCollection<MenuItemDocument> menuCollection)
{
var now = DateTime.UtcNow;
var seededMenu = Defaults.Menu.Select(item => new MenuItemDocument
{
Name = item.Name,
Category = item.Category,
Description = item.Description,
BasePrice = item.BasePrice,
CreatedAt = now,
UpdatedAt = now
});
await menuCollection.InsertManyAsync(seededMenu);
}
static async Task<int> NextOrderNumberAsync(IMongoCollection<OrderDocument> orders)
{
var latest = await orders.Find(Builders<OrderDocument>.Filter.Empty)
.SortByDescending(order => order.Number)
.FirstOrDefaultAsync();
return (latest?.Number ?? 1000) + 1;
}
static decimal ComputeOrderTotal(IEnumerable<OrderLineDocument> lines, PosSettings settings)
{
return lines.Sum(line =>
{
var sizeDelta = settings.SizeOptions.FirstOrDefault(option => option.Name == line.Size)?.Delta ?? 0m;
var toppingsTotal = (line.Toppings ?? []).Sum(toppingName =>
settings.Toppings.FirstOrDefault(item => item.Name == toppingName)?.Price ?? 0m);
return line.BasePrice + sizeDelta + toppingsTotal;
});
}
+11
View File
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MongoDB.Driver" Version="2.24.0" />
</ItemGroup>
</Project>
+7
View File
@@ -0,0 +1,7 @@
{
"MongoDb": {
"ConnectionString": "mongodb://127.0.0.1:27017",
"DatabaseName": "ranaze-pos"
},
"AllowedHosts": "*"
}
-8
View File
@@ -1,8 +0,0 @@
import dotenv from "dotenv";
dotenv.config();
export const config = {
port: Number(process.env.PORT || 4000),
mongoUri: process.env.MONGODB_URI || "mongodb://localhost:27017/ranaze-pos"
};
-6
View File
@@ -1,6 +0,0 @@
import mongoose from "mongoose";
import { config } from "./config.js";
export async function connectToDatabase() {
await mongoose.connect(config.mongoUri);
}
-41
View File
@@ -1,41 +0,0 @@
export const defaultSettings = {
sizeOptions: [
{ name: "Regular", delta: 0 },
{ name: "Large", delta: 1 }
],
sweetnessOptions: ["0%", "25%", "50%", "75%", "100%"],
iceOptions: ["No Ice", "Less Ice", "Regular Ice"],
toppings: [
{ name: "Boba", price: 0.75 },
{ name: "Pudding", price: 0.85 },
{ name: "Cheese Foam", price: 1.25 },
{ name: "Lychee Jelly", price: 0.75 }
]
};
export const defaultMenu = [
{
name: "Classic Milk Tea",
category: "Milk Tea",
description: "Black tea, milk, and brown sugar tapioca.",
basePrice: 5.5
},
{
name: "Taro Milk Tea",
category: "Milk Tea",
description: "Creamy taro with optional boba or pudding.",
basePrice: 6
},
{
name: "Mango Green Tea",
category: "Fruit Tea",
description: "Bright green tea with mango puree.",
basePrice: 5.75
},
{
name: "Strawberry Yakult",
category: "Specialty",
description: "Strawberry fruit tea with probiotic yogurt.",
basePrice: 6.25
}
];
-143
View File
@@ -1,143 +0,0 @@
import cors from "cors";
import express from "express";
import path from "path";
import { fileURLToPath } from "url";
import { connectToDatabase } from "./db.js";
import { config } from "./config.js";
import { MenuItem } from "./models/MenuItem.js";
import { Order } from "./models/Order.js";
import {
computeOrderTotal,
ensureSeedMenu,
getSettings,
nextOrderNumber,
resetMenuToDefaults
} from "./services.js";
const app = express();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const clientDist = path.resolve(__dirname, "../client/dist");
app.use(cors());
app.use(express.json());
app.get("/api/health", (_request, response) => {
response.json({ ok: true });
});
app.get("/api/bootstrap", async (_request, response, next) => {
try {
await ensureSeedMenu();
const [menu, orders] = await Promise.all([
MenuItem.find().sort({ category: 1, name: 1 }),
Order.find({ status: { $ne: "completed" } }).sort({ createdAt: -1 })
]);
response.json({
menu,
orders,
settings: getSettings()
});
} catch (error) {
next(error);
}
});
app.post("/api/menu", async (request, response, next) => {
try {
const { name, category, description = "", basePrice } = request.body;
if (!name || !category || typeof basePrice !== "number" || Number.isNaN(basePrice)) {
return response.status(400).json({ error: "Invalid menu item payload." });
}
const menuItem = await MenuItem.create({ name, category, description, basePrice });
response.status(201).json({ menuItem });
} catch (error) {
next(error);
}
});
app.delete("/api/menu/:menuItemId", async (request, response, next) => {
try {
await MenuItem.findByIdAndDelete(request.params.menuItemId);
response.json({ ok: true });
} catch (error) {
next(error);
}
});
app.post("/api/menu/reset", async (_request, response, next) => {
try {
const menu = await resetMenuToDefaults();
response.json({ menu });
} catch (error) {
next(error);
}
});
app.post("/api/orders", async (request, response, next) => {
try {
const { customerName = "", paymentMethod = "cash", lines } = request.body;
if (!Array.isArray(lines) || lines.length === 0) {
return response.status(400).json({ error: "Orders require at least one line item." });
}
const total = computeOrderTotal(lines);
const order = await Order.create({
number: await nextOrderNumber(),
customerName,
paymentMethod,
lines,
total
});
response.status(201).json({ order });
} catch (error) {
next(error);
}
});
app.patch("/api/orders/:orderId/status", async (request, response, next) => {
try {
const { status } = request.body;
const order = await Order.findByIdAndUpdate(
request.params.orderId,
{ status },
{ new: true, runValidators: true }
);
response.json({ order });
} catch (error) {
next(error);
}
});
app.delete("/api/orders/:orderId", async (request, response, next) => {
try {
await Order.findByIdAndDelete(request.params.orderId);
response.json({ ok: true });
} catch (error) {
next(error);
}
});
app.use(express.static(clientDist));
app.get("*", (request, response, next) => {
if (request.path.startsWith("/api/")) {
return next();
}
response.sendFile(path.join(clientDist, "index.html"));
});
app.use((error, _request, response, _next) => {
console.error(error);
response.status(500).json({ error: error.message || "Internal server error" });
});
connectToDatabase()
.then(() => {
app.listen(config.port, () => {
console.log(`Ranaze POS server listening on port ${config.port}`);
});
})
.catch((error) => {
console.error("Failed to connect to MongoDB", error);
process.exit(1);
});
-13
View File
@@ -1,13 +0,0 @@
import mongoose from "mongoose";
const menuItemSchema = new mongoose.Schema(
{
name: { type: String, required: true, trim: true },
category: { type: String, required: true, trim: true },
description: { type: String, default: "", trim: true },
basePrice: { type: Number, required: true, min: 0 }
},
{ timestamps: true }
);
export const MenuItem = mongoose.model("MenuItem", menuItemSchema);
-37
View File
@@ -1,37 +0,0 @@
import mongoose from "mongoose";
const orderLineSchema = new mongoose.Schema(
{
menuItemId: { type: mongoose.Schema.Types.ObjectId, ref: "MenuItem", required: true },
name: { type: String, required: true },
basePrice: { type: Number, required: true },
size: { type: String, required: true },
sweetness: { type: String, required: true },
ice: { type: String, required: true },
toppings: { type: [String], default: [] },
notes: { type: String, default: "" }
},
{ _id: false }
);
const orderSchema = new mongoose.Schema(
{
number: { type: Number, required: true, unique: true },
customerName: { type: String, default: "", trim: true },
paymentMethod: {
type: String,
enum: ["cash", "tab", "unpaid"],
default: "cash"
},
status: {
type: String,
enum: ["new", "making", "ready", "completed"],
default: "new"
},
lines: { type: [orderLineSchema], required: true },
total: { type: Number, required: true, min: 0 }
},
{ timestamps: true }
);
export const Order = mongoose.model("Order", orderSchema);
-35
View File
@@ -1,35 +0,0 @@
import { defaultMenu, defaultSettings } from "./defaults.js";
import { MenuItem } from "./models/MenuItem.js";
import { Order } from "./models/Order.js";
export function getSettings() {
return defaultSettings;
}
export async function ensureSeedMenu() {
const count = await MenuItem.countDocuments();
if (count === 0) {
await MenuItem.insertMany(defaultMenu);
}
}
export async function resetMenuToDefaults() {
await MenuItem.deleteMany({});
await MenuItem.insertMany(defaultMenu);
return MenuItem.find().sort({ category: 1, name: 1 });
}
export async function nextOrderNumber() {
const latest = await Order.findOne().sort({ number: -1 }).lean();
return (latest?.number ?? 1000) + 1;
}
export function computeOrderTotal(lines, settings = defaultSettings) {
return lines.reduce((sum, line) => {
const sizeDelta = settings.sizeOptions.find((option) => option.name === line.size)?.delta ?? 0;
const toppings = line.toppings.reduce((toppingSum, toppingName) => {
return toppingSum + (settings.toppings.find((item) => item.name === toppingName)?.price ?? 0);
}, 0);
return sum + line.basePrice + sizeDelta + toppings;
}, 0);
}