Converting server to dotnet
This commit is contained in:
@@ -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;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user