first commit
This commit is contained in:
+31
@@ -0,0 +1,31 @@
|
||||
# ── Secrets / local config ─────────────────────────────────────────────
|
||||
# Keep the base appsettings.json (no real secrets); ignore env-specific ones.
|
||||
**/appsettings.*.json
|
||||
!**/appsettings.json
|
||||
.env
|
||||
.env.*
|
||||
!**/.env*.example
|
||||
|
||||
# ── .NET ───────────────────────────────────────────────────────────────
|
||||
bin/
|
||||
obj/
|
||||
[Dd]ebug/
|
||||
[Rr]elease/
|
||||
*.user
|
||||
.vs/
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# ── Node / Next.js ─────────────────────────────────────────────────────
|
||||
node_modules/
|
||||
.next/
|
||||
out/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# ── OS / editor ────────────────────────────────────────────────────────
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.idea/
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ERPCore", "ERPCore\ERPCore.csproj", "{B38203ED-80EA-41C6-8B7D-B664451299E3}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{B38203ED-80EA-41C6-8B7D-B664451299E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B38203ED-80EA-41C6-8B7D-B664451299E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B38203ED-80EA-41C6-8B7D-B664451299E3}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B38203ED-80EA-41C6-8B7D-B664451299E3}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B38203ED-80EA-41C6-8B7D-B664451299E3}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B38203ED-80EA-41C6-8B7D-B664451299E3}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B38203ED-80EA-41C6-8B7D-B664451299E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B38203ED-80EA-41C6-8B7D-B664451299E3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B38203ED-80EA-41C6-8B7D-B664451299E3}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B38203ED-80EA-41C6-8B7D-B664451299E3}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B38203ED-80EA-41C6-8B7D-B664451299E3}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B38203ED-80EA-41C6-8B7D-B664451299E3}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace ERPCore.Common.Models;
|
||||
|
||||
/// <summary>Pagination envelope metadata returned alongside a page of items.</summary>
|
||||
public sealed record PaginationMeta(int Page, int PageSize, int TotalCount)
|
||||
{
|
||||
public int TotalPages => PageSize <= 0 ? 0 : (int)Math.Ceiling(TotalCount / (double)PageSize);
|
||||
public bool HasPrevious => Page > 1;
|
||||
public bool HasNext => Page < TotalPages;
|
||||
}
|
||||
|
||||
/// <summary>A single page of <typeparamref name="T"/> plus its pagination metadata.</summary>
|
||||
public sealed class PagedResult<T>
|
||||
{
|
||||
public IReadOnlyList<T> Items { get; init; } = Array.Empty<T>();
|
||||
public PaginationMeta Meta { get; init; } = new(1, 0, 0);
|
||||
|
||||
public PagedResult() { }
|
||||
|
||||
public PagedResult(IReadOnlyList<T> items, int page, int pageSize, int totalCount)
|
||||
{
|
||||
Items = items;
|
||||
Meta = new PaginationMeta(page, pageSize, totalCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace ERPCore.Common.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight success/failure envelope for service-layer results where an
|
||||
/// exception would be overkill. Domain errors that must reach the client as
|
||||
/// RFC 7807 ProblemDetails should throw a DomainException instead
|
||||
/// (see System/Errors).
|
||||
/// </summary>
|
||||
public readonly struct Result<T>
|
||||
{
|
||||
public bool IsSuccess { get; }
|
||||
public T? Value { get; }
|
||||
public string? ErrorCode { get; }
|
||||
public string? ErrorMessage { get; }
|
||||
|
||||
private Result(bool ok, T? value, string? code, string? message)
|
||||
{
|
||||
IsSuccess = ok;
|
||||
Value = value;
|
||||
ErrorCode = code;
|
||||
ErrorMessage = message;
|
||||
}
|
||||
|
||||
public static Result<T> Success(T value) => new(true, value, null, null);
|
||||
public static Result<T> Failure(string code, string message) => new(false, default, code, message);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Non-domain metadata endpoint. Confirms the controller pipeline, Swagger and
|
||||
/// routing are wired; domain endpoints are defined per docs/11-BACKEND-PHASE1.md.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public sealed class MetaController : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public IActionResult Get() => Ok(new
|
||||
{
|
||||
name = "ERPCore API",
|
||||
phase = "Phase 1 — Inventory & Supply Chain",
|
||||
version = "v1",
|
||||
utc = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.9" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Common\Models\" />
|
||||
<Folder Include="Infra\UoW\" />
|
||||
<Folder Include="System\Errors\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace ERPCore.Infra.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the audit actor from the current HTTP request's principal
|
||||
/// (token `sub` / NameIdentifier). Falls back to <see cref="SystemActor"/>
|
||||
/// when the request is unauthenticated (e.g. health checks, seeding).
|
||||
/// </summary>
|
||||
public sealed class CurrentUser : ICurrentUser
|
||||
{
|
||||
public const string SystemActor = "system";
|
||||
|
||||
private readonly IHttpContextAccessor _accessor;
|
||||
|
||||
public CurrentUser(IHttpContextAccessor accessor) => _accessor = accessor;
|
||||
|
||||
public bool IsAuthenticated =>
|
||||
_accessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false;
|
||||
|
||||
public string UserId
|
||||
{
|
||||
get
|
||||
{
|
||||
var user = _accessor.HttpContext?.User;
|
||||
var sub = user?.FindFirstValue(ClaimTypes.NameIdentifier)
|
||||
?? user?.FindFirstValue("sub");
|
||||
return string.IsNullOrWhiteSpace(sub) ? SystemActor : sub;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace ERPCore.Infra.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Abstraction over the authenticated principal, used purely as the audit actor
|
||||
/// (RBAC is deferred for Phase 1). The actor is derived from the token's `sub`
|
||||
/// claim — never from the request body (see 00-CORE §4).
|
||||
/// </summary>
|
||||
public interface ICurrentUser
|
||||
{
|
||||
/// <summary>The audit actor identity (token `sub`), or "system" when unauthenticated.</summary>
|
||||
string UserId { get; }
|
||||
|
||||
/// <summary>True when the request carries an authenticated principal.</summary>
|
||||
bool IsAuthenticated { get; }
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace ERPCore.Infra.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// JWT bearer wiring. Authentication only — RBAC/authorization policies are
|
||||
/// deferred for Phase 1; the validated principal exists solely so that
|
||||
/// <see cref="ICurrentUser"/> can stamp the audit actor.
|
||||
/// </summary>
|
||||
public static class JwtAuthExtensions
|
||||
{
|
||||
public static IServiceCollection AddErpJwtAuth(this IServiceCollection services, IConfiguration config)
|
||||
{
|
||||
var issuer = config["Jwt:Issuer"];
|
||||
var audience = config["Jwt:Audience"];
|
||||
var signingKey = config["Jwt:SigningKey"] ?? string.Empty;
|
||||
|
||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = issuer,
|
||||
ValidateAudience = true,
|
||||
ValidAudience = audience,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey)),
|
||||
ClockSkew = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
});
|
||||
|
||||
services.AddAuthorization();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ERPCore.Infra.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// EF Core context for the ERP database. The 38 Phase 1 entities and their
|
||||
/// <see cref="IEntityTypeConfiguration{TEntity}"/> configurations are added under
|
||||
/// Domain/Entities and Infra/Persistence/Configurations as they are implemented.
|
||||
/// The authoritative schema lives in docs/10-BACKEND-PHASE1.md — do not invent it here.
|
||||
/// </summary>
|
||||
public class ErpDbContext : DbContext
|
||||
{
|
||||
public ErpDbContext(DbContextOptions<ErpDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// Pick up every IEntityTypeConfiguration in this assembly
|
||||
// (Infra/Persistence/Configurations/*).
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ErpDbContext).Assembly);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace ERPCore.Infra.UoW;
|
||||
|
||||
/// <summary>
|
||||
/// Transaction boundary for stock-affecting operations (00-CORE §4). Every
|
||||
/// GRN confirm, transfer dispatch/receive, adjustment, return and count post
|
||||
/// runs inside a single UoW transaction so FIFO layer consumption and ledger
|
||||
/// writes commit or roll back together.
|
||||
/// </summary>
|
||||
public interface IUnitOfWork
|
||||
{
|
||||
/// <summary>Persist tracked changes without an explicit transaction scope.</summary>
|
||||
Task<int> SaveChangesAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>Run <paramref name="action"/> inside a transaction, saving and committing on success.</summary>
|
||||
Task ExecuteInTransactionAsync(Func<CancellationToken, Task> action, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Transactional variant that returns a value from <paramref name="action"/>.</summary>
|
||||
Task<T> ExecuteInTransactionAsync<T>(Func<CancellationToken, Task<T>> action, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using ERPCore.Infra.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ERPCore.Infra.UoW;
|
||||
|
||||
/// <summary>
|
||||
/// EF Core-backed unit of work. Uses the provider execution strategy so the
|
||||
/// transactional path is safe under connection resiliency / retries.
|
||||
/// </summary>
|
||||
public sealed class UnitOfWork : IUnitOfWork
|
||||
{
|
||||
private readonly ErpDbContext _db;
|
||||
|
||||
public UnitOfWork(ErpDbContext db) => _db = db;
|
||||
|
||||
public Task<int> SaveChangesAsync(CancellationToken ct = default) => _db.SaveChangesAsync(ct);
|
||||
|
||||
public async Task ExecuteInTransactionAsync(Func<CancellationToken, Task> action, CancellationToken ct = default)
|
||||
{
|
||||
await ExecuteInTransactionAsync<object?>(async token =>
|
||||
{
|
||||
await action(token);
|
||||
return null;
|
||||
}, ct);
|
||||
}
|
||||
|
||||
public async Task<T> ExecuteInTransactionAsync<T>(Func<CancellationToken, Task<T>> action, CancellationToken ct = default)
|
||||
{
|
||||
var strategy = _db.Database.CreateExecutionStrategy();
|
||||
return await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
await using var tx = await _db.Database.BeginTransactionAsync(ct);
|
||||
try
|
||||
{
|
||||
var result = await action(ct);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
await tx.CommitAsync(ct);
|
||||
return result;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await tx.RollbackAsync(ct);
|
||||
throw;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Infra.Persistence;
|
||||
using ERPCore.Infra.UoW;
|
||||
using ERPCore.Repositories;
|
||||
using ERPCore.Repositories.Interfaces;
|
||||
using ERPCore.System.Errors;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.OpenApi;
|
||||
using Serilog;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Serilog (file sink) — reads levels from configuration, plus a rolling daily file.
|
||||
builder.Host.UseSerilog((ctx, cfg) => cfg
|
||||
.ReadFrom.Configuration(ctx.Configuration)
|
||||
.WriteTo.File("logs/erpcore-.log", rollingInterval: RollingInterval.Day));
|
||||
|
||||
builder.Services.AddControllers();
|
||||
|
||||
// EF Core + PostgreSQL
|
||||
builder.Services.AddDbContext<ErpDbContext>(o =>
|
||||
o.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||
|
||||
// ProblemDetails (RFC 7807) + domain-exception mapping
|
||||
builder.Services.AddProblemDetails();
|
||||
builder.Services.AddExceptionHandler<DomainExceptionHandler>();
|
||||
|
||||
// JWT bearer auth (RBAC deferred; identity used only for the audit stamp)
|
||||
builder.Services.AddErpJwtAuth(builder.Configuration);
|
||||
|
||||
// Current-user (audit actor) derived from token `sub`
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddScoped<ICurrentUser, CurrentUser>();
|
||||
|
||||
// Unit of work + generic repository base
|
||||
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
|
||||
|
||||
// Health checks (EF Core DB)
|
||||
builder.Services.AddHealthChecks().AddDbContextCheck<ErpDbContext>();
|
||||
|
||||
// Swagger / OpenAPI (Swashbuckle v10 → OpenAPI 3.1)
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(o => o.SwaggerDoc("v1", new OpenApiInfo { Title = "ERPCore API", Version = "v1" }));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseSerilogRequestLogging();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseExceptionHandler(); // -> DomainExceptionHandler / ProblemDetails
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
app.MapHealthChecks("/health");
|
||||
app.Run();
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5224",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7112;http://localhost:5224",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace ERPCore.Repositories.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Generic data-access base. Repositories own EF entities; business rules stay
|
||||
/// in services (00-CORE §4). Concrete repositories extend this with query
|
||||
/// methods specific to their aggregate.
|
||||
/// </summary>
|
||||
public interface IRepository<T> where T : class
|
||||
{
|
||||
Task<T?> GetByIdAsync(object id, CancellationToken ct = default);
|
||||
Task<IReadOnlyList<T>> ListAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>Composable query root for service-defined filtering/paging.</summary>
|
||||
IQueryable<T> Query();
|
||||
|
||||
Task AddAsync(T entity, CancellationToken ct = default);
|
||||
void Update(T entity);
|
||||
void Remove(T entity);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using ERPCore.Infra.Persistence;
|
||||
using ERPCore.Repositories.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ERPCore.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Default <see cref="IRepository{T}"/> implementation over <see cref="ErpDbContext"/>.
|
||||
/// Registered open-generically so any entity gets a repository for free; specialise
|
||||
/// by deriving when an aggregate needs bespoke queries.
|
||||
/// </summary>
|
||||
public class Repository<T> : IRepository<T> where T : class
|
||||
{
|
||||
protected readonly ErpDbContext Db;
|
||||
protected readonly DbSet<T> Set;
|
||||
|
||||
public Repository(ErpDbContext db)
|
||||
{
|
||||
Db = db;
|
||||
Set = db.Set<T>();
|
||||
}
|
||||
|
||||
public virtual async Task<T?> GetByIdAsync(object id, CancellationToken ct = default)
|
||||
=> await Set.FindAsync(new[] { id }, ct);
|
||||
|
||||
public virtual async Task<IReadOnlyList<T>> ListAsync(CancellationToken ct = default)
|
||||
=> await Set.AsNoTracking().ToListAsync(ct);
|
||||
|
||||
public IQueryable<T> Query() => Set.AsQueryable();
|
||||
|
||||
public virtual async Task AddAsync(T entity, CancellationToken ct = default)
|
||||
=> await Set.AddAsync(entity, ct);
|
||||
|
||||
public virtual void Update(T entity) => Set.Update(entity);
|
||||
|
||||
public virtual void Remove(T entity) => Set.Remove(entity);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace ERPCore.System.Errors;
|
||||
|
||||
/// <summary>
|
||||
/// Base for domain / business-rule violations. Carries a stable <see cref="Code"/>
|
||||
/// (see the error catalog in docs/11-BACKEND-PHASE1.md) and an HTTP
|
||||
/// <see cref="StatusCode"/>; mapped to RFC 7807 ProblemDetails by
|
||||
/// <see cref="DomainExceptionHandler"/>.
|
||||
/// </summary>
|
||||
public class DomainException : Exception
|
||||
{
|
||||
public string Code { get; }
|
||||
public int StatusCode { get; }
|
||||
|
||||
public DomainException(string code, string message, int statusCode = 400)
|
||||
: base(message)
|
||||
{
|
||||
Code = code;
|
||||
StatusCode = statusCode;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>404 — a referenced resource does not exist.</summary>
|
||||
public sealed class NotFoundException : DomainException
|
||||
{
|
||||
public NotFoundException(string message) : base(ErrorCodes.NotFound, message, 404) { }
|
||||
}
|
||||
|
||||
/// <summary>409 — the request conflicts with current server state.</summary>
|
||||
public sealed class ConflictException : DomainException
|
||||
{
|
||||
public ConflictException(string message) : base(ErrorCodes.Conflict, message, 409) { }
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.System.Errors;
|
||||
|
||||
/// <summary>
|
||||
/// Translates <see cref="DomainException"/> into an RFC 7807 ProblemDetails
|
||||
/// response, preserving the stable domain <c>code</c> as an extension. Non-domain
|
||||
/// exceptions are left unhandled so the framework returns a 500 ProblemDetails.
|
||||
/// Registered via AddExceptionHandler and invoked by UseExceptionHandler.
|
||||
/// </summary>
|
||||
public sealed class DomainExceptionHandler : IExceptionHandler
|
||||
{
|
||||
private readonly IProblemDetailsService _problemDetails;
|
||||
|
||||
public DomainExceptionHandler(IProblemDetailsService problemDetails)
|
||||
=> _problemDetails = problemDetails;
|
||||
|
||||
public async ValueTask<bool> TryHandleAsync(
|
||||
HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
|
||||
{
|
||||
if (exception is not DomainException domain)
|
||||
return false;
|
||||
|
||||
httpContext.Response.StatusCode = domain.StatusCode;
|
||||
|
||||
return await _problemDetails.TryWriteAsync(new ProblemDetailsContext
|
||||
{
|
||||
HttpContext = httpContext,
|
||||
Exception = domain,
|
||||
ProblemDetails = new ProblemDetails
|
||||
{
|
||||
Status = domain.StatusCode,
|
||||
Title = "Domain rule violation",
|
||||
Detail = domain.Message,
|
||||
Extensions = { ["code"] = domain.Code }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace ERPCore.System.Errors;
|
||||
|
||||
/// <summary>
|
||||
/// Stable domain error codes carried in the <c>code</c> extension of RFC 7807
|
||||
/// ProblemDetails responses. The authoritative catalog lives in
|
||||
/// docs/11-BACKEND-PHASE1.md; add codes here as endpoints are implemented so
|
||||
/// the two stay in sync.
|
||||
/// </summary>
|
||||
public static class ErrorCodes
|
||||
{
|
||||
public const string Validation = "validation_error";
|
||||
public const string NotFound = "not_found";
|
||||
public const string Conflict = "conflict";
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=ERPCore;Username=CHANGE_ME;Password=CHANGE_ME"
|
||||
},
|
||||
"Jwt": {
|
||||
"Issuer": "ERPCore",
|
||||
"Audience": "ERPCore.Clients",
|
||||
"SigningKey": "CHANGE_ME_DEV_ONLY_32+_CHARS",
|
||||
"AccessTokenMinutes": 120
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
# Backend — PROGRESS (Phase 1: Inventory & Supply Chain)
|
||||
|
||||
Legend: `[ ]` not started · `[~]` in progress · `[x]` done
|
||||
Spec: `docs/10-BACKEND-PHASE1.md` (model + rules) · `docs/11-BACKEND-PHASE1.md` (API)
|
||||
Convention: `docs/01-DOC-GUIDE.md §6`. Update this file in the **same commit** as the code. When ticking `[x]`, append a short note + any deviation.
|
||||
|
||||
## 0. Bootstrap
|
||||
- [ ] Solution + Web API project (`net10.0`), packages restored (00-CORE §5.4)
|
||||
- [ ] Folder structure per 00-CORE §5.3
|
||||
- [ ] `ErpDbContext` + Npgsql wired; `InitialCreate` migration applied
|
||||
- [ ] Serilog, JWT, Swagger, HealthChecks, ProblemDetails in `Program.cs`
|
||||
- [ ] `IUnitOfWork` + `UnitOfWork` (transaction boundary)
|
||||
- [ ] Generic repository base + interfaces
|
||||
- [ ] `ICurrentUser` (audit stamp from token `sub`)
|
||||
- [ ] ProblemDetails middleware + domain exception → `code` mapping (System/Errors)
|
||||
|
||||
## 1. Master Data
|
||||
- [ ] Item: entity + config + enums (ItemType, TrackingMode)
|
||||
- [ ] Item: repository + service + controller (CRUD, DTOs, ETag)
|
||||
- [ ] UOM + UOM conversions
|
||||
- [ ] Category (hierarchy, `?tree=true`)
|
||||
- [ ] Vendor
|
||||
- [ ] Warehouse + Bin
|
||||
- [ ] Item reorder settings (`PUT /items/{id}/reorder`)
|
||||
|
||||
## 2. Procurement
|
||||
- [ ] Requisition (+ lines) + submit
|
||||
- [ ] RFQ + quotations + comparison
|
||||
- [ ] Purchase Order: create (auto-approve, `approvalRequired` flag), edit-while-open, approve (no-op), cancel
|
||||
- [ ] Purchase Return (outbound movement, reason code)
|
||||
|
||||
## 3. Goods Receipt
|
||||
- [ ] GRN create (against PO / direct), over-receipt tolerance
|
||||
- [ ] GRN confirm → FIFO layer + ledger + PO `qtyReceived` (single UoW txn, Idempotency-Key)
|
||||
- [ ] Inspection hold release / reject
|
||||
|
||||
## 4. Stock Core
|
||||
- [ ] StockLayer + StockLedger entities/config (ledger append-only)
|
||||
- [ ] `FifoCostingService` (consume oldest-first with row lock; valuation)
|
||||
- [ ] Stock enquiry (onHand / available / onHold / inTransit)
|
||||
- [ ] Ledger query · Valuation query
|
||||
|
||||
## 5. Stock Transactions
|
||||
- [ ] Transfer: create → dispatch (consume, In-Transit) → receive (dest layer, cost-preserving)
|
||||
- [ ] Adjustment (auto-post, mandatory reason code)
|
||||
- [ ] Count (cycle/full → enter counts → variance → post)
|
||||
- [ ] Reorder alerts (query) + suggest requisition
|
||||
|
||||
## 6. Cross-cutting
|
||||
- [ ] Audit log on every mutation (who/when/old→new)
|
||||
- [ ] Document numbering sequences (per type, per year)
|
||||
- [ ] Auth: simple in-app login → JWT (`POST /auth/login`)
|
||||
- [ ] JournalEntryStub emitted per stock movement (data only)
|
||||
- [ ] Negative-stock policy enforcement (default block)
|
||||
- [ ] FEFO picking for perishables; block expired / on-hold issue
|
||||
|
||||
## Deferred (Phase 2+ — do NOT build now, hooks only)
|
||||
- [ ] Vendor invoice + three-way match
|
||||
- [ ] Reservation/allocation fulfilment
|
||||
- [ ] RBAC policy enforcement + approval workflow activation
|
||||
|
||||
## Done
|
||||
<!-- move [x] items here with date + note if the active list grows long -->
|
||||
@@ -0,0 +1,55 @@
|
||||
# Frontend — PROGRESS (Phase 1: Inventory & Supply Chain)
|
||||
|
||||
Legend: `[ ]` not started · `[~]` in progress · `[x]` done
|
||||
Spec: `docs/20-FRONTEND.md` (flows + rules) · `docs/11-BACKEND-PHASE1.md` (API contract)
|
||||
Convention: `docs/01-DOC-GUIDE.md §6`. Update this file in the **same commit** as the code. When ticking `[x]`, append a short note + any deviation.
|
||||
|
||||
## 0. Foundation
|
||||
- [ ] `NEXT_PUBLIC_API_BASE_URL` wired (`.env.local`)
|
||||
- [ ] Typed API client / fetch wrapper (one method per endpoint) + bearer token handling
|
||||
- [ ] Shared TS types mirroring API DTOs (`types/`)
|
||||
- [ ] Dependency-free client validation helpers (format/required/range)
|
||||
- [ ] `ProblemDetails` normalizer + `code → message` map (`lib/`)
|
||||
|
||||
## 1. Auth
|
||||
- [ ] Login screen → `POST /auth/login`; store token; attach to client
|
||||
|
||||
## 2. Master Data screens
|
||||
- [ ] Items (list + create/edit, ETag handling)
|
||||
- [ ] UOM + conversions
|
||||
- [ ] Categories (tree)
|
||||
- [ ] Vendors
|
||||
- [ ] Warehouses + Bins
|
||||
- [ ] Item reorder settings
|
||||
|
||||
## 3. Procurement screens
|
||||
- [ ] Requisition (create + submit)
|
||||
- [ ] RFQ + quotations + comparison view
|
||||
- [ ] Purchase Order (create, edit-while-open, cancel)
|
||||
- [ ] Purchase Return
|
||||
|
||||
## 4. Receiving screens
|
||||
- [ ] GRN create (qty, bin, batch/serial capture)
|
||||
- [ ] GRN confirm (render created layers / ledger refs)
|
||||
- [ ] Inspection hold release / reject
|
||||
|
||||
## 5. Stock screens
|
||||
- [ ] Stock enquiry (onHand/available/onHold/inTransit)
|
||||
- [ ] Ledger view · Valuation view
|
||||
- [ ] Transfer (create → dispatch → receive)
|
||||
- [ ] Adjustment (reason code required)
|
||||
- [ ] Count (cycle/full → enter → post)
|
||||
- [ ] Reorder alerts (+ create requisition)
|
||||
|
||||
## 6. Validation posture (20-FRONTEND §3)
|
||||
- [ ] Client format/required/range checks on all forms
|
||||
- [ ] Surface server `ProblemDetails` incl. domain codes; map to fields/messages
|
||||
- [ ] `412` conflict → prompt refetch before retry
|
||||
- [ ] No client-side gating on stock/availability/status (server-authoritative)
|
||||
|
||||
## 7. UX states
|
||||
- [ ] Loading / empty / error states on every list
|
||||
- [ ] Transactional actions show server-returned side effects as confirmation
|
||||
|
||||
## Done
|
||||
<!-- move [x] items here with date + note if the active list grows long -->
|
||||
@@ -0,0 +1,68 @@
|
||||
# ERP System — Inventory & Supply Chain (Phase 1)
|
||||
|
||||
A modular ERP built in phases. **Phase 1** delivers Inventory & Supply Chain: master data, procurement, goods receipt, and stock management with **FIFO** costing, **multi-warehouse**, single-tenant.
|
||||
|
||||
Monorepo: a .NET 10 REST API backend and a Next.js frontend, with all documentation under `docs/`.
|
||||
|
||||
## Repository layout
|
||||
```
|
||||
erp-monorepo/
|
||||
├── Backend/ # ASP.NET Core Web API (.NET 10, PostgreSQL) + PROGRESS.md
|
||||
├── Frontend/ # Next.js (App Router, TypeScript) + PROGRESS.md
|
||||
└── docs/ # all documentation — START AT docs/00-CORE.md
|
||||
```
|
||||
|
||||
## Start here
|
||||
**All work begins at [`docs/00-CORE.md`](docs/00-CORE.md)** — the hub. It defines structure, tech stack, backend setup, and routes you to the right doc.
|
||||
|
||||
| Doc | Purpose |
|
||||
|---|---|
|
||||
| `docs/00-CORE.md` | Hub: structure, stack, backend init, routing |
|
||||
| `docs/01-DOC-GUIDE.md` | Doc map, reading order, tracking conventions |
|
||||
| `docs/10-BACKEND-PHASE1.md` | Backend spec: SRS + ER model + architecture |
|
||||
| `docs/11-BACKEND-PHASE1.md` | Backend API reference (complete req/res) |
|
||||
| `docs/20-FRONTEND.md` | Frontend user-flows + rules + validation posture |
|
||||
| `Backend/PROGRESS.md`, `Frontend/PROGRESS.md` | Change checklists (git-shared) |
|
||||
|
||||
## Prerequisites
|
||||
- .NET 10 SDK (`dotnet --version` → `10.0.x`)
|
||||
- PostgreSQL running on localhost
|
||||
- `dotnet-ef` tool (`dotnet tool install --global dotnet-ef`)
|
||||
- Node.js (for the frontend)
|
||||
|
||||
## Run the backend
|
||||
```bash
|
||||
# create the database in your local Postgres first, e.g.:
|
||||
# createdb -U postgres ERPCore (or: CREATE DATABASE "ERPCore"; in psql)
|
||||
cd Backend/ERPCore
|
||||
# set Username/Password in appsettings.Development.json to your local Postgres login
|
||||
dotnet ef database update
|
||||
dotnet run
|
||||
```
|
||||
- Swagger UI: `https://localhost:<port>/swagger`
|
||||
- Health: `https://localhost:<port>/health` → `Healthy`
|
||||
|
||||
Full setup (packages, folder scaffold, Program.cs wiring) is in `docs/00-CORE.md §5`.
|
||||
|
||||
## Run the frontend
|
||||
```bash
|
||||
cd Frontend
|
||||
cp .env.local.example .env.local # set NEXT_PUBLIC_API_BASE_URL=https://localhost:<port>
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Configuration & secrets
|
||||
- Three appsettings files: base (`appsettings.json`, placeholders only), `Development` (localhost), `Production` (env vars).
|
||||
- **Local dev only** currently. Before any shared/staging/production use, move secrets to User Secrets (dev) and environment variables (prod), keep `appsettings.Development.json` / `appsettings.Production.json` out of git, and rotate any credential ever committed.
|
||||
|
||||
## Tech stack
|
||||
**Backend:** .NET 10, ASP.NET Core Web API, EF Core 10, PostgreSQL (Npgsql), JWT bearer auth, Serilog, Swashbuckle/Swagger, HealthChecks. Layering: Controller → Service → Repository → UnitOfWork, DTOs at the controller boundary, FIFO in a domain service.
|
||||
**Frontend:** Next.js (App Router) + TypeScript, plain React hooks, dependency-free validation (client for UX; **server is authoritative**).
|
||||
|
||||
## Conventions
|
||||
- Documentation: one source of truth per topic; edit in place; navigate from `docs/00-CORE.md`.
|
||||
- Progress: update the relevant `PROGRESS.md` in the same commit as the code (`docs/01-DOC-GUIDE.md §6`).
|
||||
|
||||
## Roadmap (future phases)
|
||||
Sales & CRM · Manufacturing & Production · QC/QA · Accounting · HRM. Phase 1 reserves the integration seams (GL-ready journal entries, reservation status, goods-issue movement, inspection hold, user identity) so later phases integrate without a schema migration.
|
||||
+366
@@ -0,0 +1,366 @@
|
||||
# 00 · CORE — ERP System (Phase 1: Inventory & Supply Chain)
|
||||
|
||||
> **This file is the hub. Start every task here.**
|
||||
> It defines the repository structure, the tech stack, how to stand up a runnable backend, and — most importantly — **where to go next** for any piece of work. Do not begin backend or frontend work without first reading the relevant section below and following the routing table in §7.
|
||||
|
||||
---
|
||||
|
||||
## 1. What this project is
|
||||
|
||||
A modular ERP built in phases. **Phase 1** delivers the **Inventory & Supply Chain** subsystem: master data, procurement, goods receipt, and stock management (FIFO costing, multi-warehouse, single-tenant).
|
||||
|
||||
| Decision | Value |
|
||||
|---|---|
|
||||
| Costing method | **FIFO** (cost-layer tracking, per item per warehouse) |
|
||||
| Warehouses | **Multi-warehouse** |
|
||||
| Tenancy | **Single-tenant** |
|
||||
| Approvals (PO, adjustment) | **Auto-approve**, config-gated off (RBAC deferred) |
|
||||
| Access control | Authentication only; **RBAC deferred**, user identity stamped for audit |
|
||||
| Vendor invoice / 3-way match | **Deferred** to Accounting phase (GRN retains hooks) |
|
||||
|
||||
Full requirements live in the backend spec (see §7). This file does **not** duplicate them.
|
||||
|
||||
---
|
||||
|
||||
## 2. Repository structure
|
||||
|
||||
```
|
||||
erp-monorepo/
|
||||
├── README.md # Root: what this is + how to run both sides
|
||||
├── .gitignore # ignore appsettings.*.json secrets, .env*, bin/, obj/, node_modules/
|
||||
│
|
||||
├── Backend/ # ASP.NET Core Web API (.NET 10) — see §5 to initialize
|
||||
│ ├── ERPCore.sln
|
||||
│ ├── PROGRESS.md # ← backend change checklist (Claude-maintained, git-shared)
|
||||
│ └── ERPCore/ # single Web API project (folders, not multi-project)
|
||||
│
|
||||
├── Frontend/ # Next.js (App Router + TypeScript) — already initialized
|
||||
│ └── PROGRESS.md # ← frontend change checklist (Claude-maintained, git-shared)
|
||||
│
|
||||
└── docs/ # ALL documentation
|
||||
├── 00-CORE.md # ← you are here (hub)
|
||||
├── 01-DOC-GUIDE.md # documentation map + tracking conventions
|
||||
├── 10-BACKEND-PHASE1.md # backend spec: SRS + ER/entities + tech + architecture
|
||||
├── 11-BACKEND-PHASE1.md # backend API reference (complete req/res)
|
||||
└── 20-FRONTEND.md # frontend user-flows + architecture rules + validation posture
|
||||
```
|
||||
|
||||
The `Backend/ERPCore/` internal layout is created in §5.3.
|
||||
|
||||
---
|
||||
|
||||
## 3. Tech stack
|
||||
|
||||
### Backend
|
||||
| Concern | Choice |
|
||||
|---|---|
|
||||
| Runtime | **.NET 10** (`net10.0`) |
|
||||
| Framework | ASP.NET Core Web API (controllers) |
|
||||
| ORM | EF Core 10 |
|
||||
| Database | **PostgreSQL** (Npgsql provider) |
|
||||
| Auth | JWT bearer (simple in-app login; RBAC deferred) |
|
||||
| Logging | Serilog (file sink) |
|
||||
| API docs | Swashbuckle / Swagger UI |
|
||||
| Health | HealthChecks + EF Core DB check → `GET /health` |
|
||||
|
||||
### Frontend
|
||||
| Concern | Choice |
|
||||
|---|---|
|
||||
| Framework | **Next.js** (App Router) + TypeScript |
|
||||
| State/forms | **Plain React hooks** (`useState` / custom hooks) |
|
||||
| Validation | **Dependency-free** (hand-rolled) — client-side for UX only; **server is authoritative** |
|
||||
| API access | Typed fetch client against the backend (`NEXT_PUBLIC_API_BASE_URL`) |
|
||||
|
||||
> The Frontend project is **already initialized**. Do not scaffold it here. For all frontend architecture, flows, and validation rules, go to `20-FRONTEND.md`.
|
||||
|
||||
### Database
|
||||
PostgreSQL, running on **localhost** for development. Secrets management (User Secrets / environment variables) is deferred; see §5.4 for the current local configuration and the note on secrets.
|
||||
|
||||
---
|
||||
|
||||
## 4. Architecture (high-level, both sides)
|
||||
|
||||
### Backend layering — strict, one direction
|
||||
```
|
||||
Controller → Service → Repository → UnitOfWork/DbContext
|
||||
(DTOs) (logic) (entities) (transaction boundary)
|
||||
```
|
||||
|
||||
Rules (non-negotiable for Phase 1):
|
||||
- **DTOs at the controller boundary.** Controllers accept and return DTOs only — **never** EF entities and **never** `DbContext`.
|
||||
- **Services hold business logic** and return DTOs. Controllers stay thin (HTTP concerns only).
|
||||
- **Repositories own EF entities** and data access. No business rules in repositories.
|
||||
- **UnitOfWork is the transaction boundary.** Every stock-affecting operation (GRN confirm, transfer dispatch/receive, adjustment, return, count post) runs inside a single UoW transaction.
|
||||
- **FIFO lives in a domain service** (`FifoCostingService`), invoked by stock services inside the UoW transaction — **not** in a controller or repository. Layer consumption must lock affected rows for concurrency safety.
|
||||
- **Errors use RFC 7807 `ProblemDetails`** (ASP.NET Core default). Domain errors carry a stable `code`. The error catalog is in `11-BACKEND-PHASE1.md`.
|
||||
- **Audit actor** is derived from the authenticated principal (token `sub`), never from the request body.
|
||||
|
||||
### Frontend (summary — full rules in `20-FRONTEND.md`)
|
||||
- Stick to the existing initialized architecture; do not restructure.
|
||||
- **Validate on the client for UX, but never trust it.** The server re-validates everything. Business rules that depend on server state (stock availability, negative-stock block, over-receipt tolerance, FIFO sufficiency) are **server-authoritative** and must not be assumed valid client-side.
|
||||
|
||||
---
|
||||
|
||||
## 5. Backend initialization (runnable)
|
||||
|
||||
Follow in order. Commands assume repo root `erp-monorepo/`.
|
||||
|
||||
### 5.1 Prerequisites
|
||||
- **.NET 10 SDK** (`dotnet --version` → `10.0.x`)
|
||||
- **PostgreSQL** running on localhost, and a login you can use
|
||||
- **EF Core CLI**: `dotnet tool install --global dotnet-ef` (or `dotnet tool update --global dotnet-ef`)
|
||||
- Node.js (for the Frontend, handled separately)
|
||||
|
||||
### 5.2 Create solution and project
|
||||
```bash
|
||||
mkdir -p Backend && cd Backend
|
||||
dotnet new sln -n ERPCore
|
||||
dotnet new webapi -n ERPCore --use-controllers -f net10.0
|
||||
dotnet sln add ERPCore/ERPCore.csproj
|
||||
cd ERPCore
|
||||
```
|
||||
|
||||
### 5.3 Create the internal folder structure
|
||||
```bash
|
||||
# from Backend/ERPCore/
|
||||
mkdir -p Controllers Services/Interfaces Services/Stock \
|
||||
Repositories/Interfaces \
|
||||
Domain/Entities Domain/Enums \
|
||||
Dtos/Common \
|
||||
Common/Models \
|
||||
Infra/UoW Infra/Persistence/Configurations Infra/Persistence/Migrations Infra/Auth Infra/Logging \
|
||||
System/Errors \
|
||||
HealthChecks
|
||||
```
|
||||
Target internal layout:
|
||||
```
|
||||
ERPCore/
|
||||
├── ERPCore.csproj
|
||||
├── Program.cs
|
||||
├── appsettings.json # base (no real secrets)
|
||||
├── appsettings.Development.json # local dev (localhost Postgres)
|
||||
├── appsettings.Production.json # env-var driven
|
||||
├── Controllers/ # HTTP; DTOs in/out
|
||||
├── Services/ # business logic → DTOs
|
||||
│ ├── Interfaces/
|
||||
│ └── Stock/ (StockService, FifoCostingService)
|
||||
├── Repositories/ # EF data access (entities)
|
||||
│ └── Interfaces/
|
||||
├── Domain/
|
||||
│ ├── Entities/ # 38 EF entities
|
||||
│ └── Enums/ # ItemType, TrackingMode, HoldStatus, Direction, *Status
|
||||
├── Dtos/ # request/response DTOs (+ Common: paging)
|
||||
├── Common/Models/ # PagedResult<T>, PaginationMeta, Result<T>
|
||||
├── Infra/
|
||||
│ ├── UoW/ # IUnitOfWork, UnitOfWork
|
||||
│ ├── Persistence/ # ErpDbContext, Configurations/, Migrations/
|
||||
│ ├── Auth/ # JWT setup, ICurrentUser (audit source)
|
||||
│ └── Logging/ # Serilog config
|
||||
├── System/Errors/ # domain exceptions, error codes, ProblemDetails middleware
|
||||
└── HealthChecks/ # EF Core DB health check
|
||||
```
|
||||
|
||||
### 5.4 Add packages
|
||||
Pinned versions below are current for .NET 10 at time of writing. All `Microsoft.EntityFrameworkCore.*` packages **must share the same version**.
|
||||
|
||||
```bash
|
||||
# from Backend/ERPCore/
|
||||
dotnet add package Microsoft.EntityFrameworkCore --version 10.0.9
|
||||
dotnet add package Microsoft.EntityFrameworkCore.Design --version 10.0.9
|
||||
dotnet add package Microsoft.EntityFrameworkCore.Tools --version 10.0.9
|
||||
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 10.0.2
|
||||
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 10.0.9
|
||||
dotnet add package Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore --version 10.0.9
|
||||
dotnet add package Serilog.AspNetCore --version 9.0.0
|
||||
dotnet add package Serilog.Sinks.File --version 6.0.0
|
||||
dotnet add package Swashbuckle.AspNetCore --version 10.2.3
|
||||
```
|
||||
|
||||
Resulting `ERPCore.csproj`:
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.9" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Common\Models\" />
|
||||
<Folder Include="Infra\UoW\" />
|
||||
<Folder Include="System\Errors\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
```
|
||||
> **Swashbuckle v10 note:** v10 upgraded to `Microsoft.OpenApi` 2.x and emits **OpenAPI 3.1**. `Program.cs` uses `using Microsoft.OpenApi;` and `AddSwaggerGen(...)` / `UseSwagger()` / `UseSwaggerUI()`.
|
||||
|
||||
### 5.5 App settings (three files)
|
||||
`appsettings.json` (base — no real secrets; placeholder only):
|
||||
```json
|
||||
{
|
||||
"Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } },
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=ERPCore;Username=CHANGE_ME;Password=CHANGE_ME"
|
||||
},
|
||||
"Jwt": {
|
||||
"Issuer": "ERPCore",
|
||||
"Audience": "ERPCore.Clients",
|
||||
"SigningKey": "CHANGE_ME_DEV_ONLY_32+_CHARS",
|
||||
"AccessTokenMinutes": 120
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
```
|
||||
`appsettings.Development.json` (local dev — used by `dotnet run` in Development):
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=ERPCore;Username=postgres;Password=postgres"
|
||||
},
|
||||
"Jwt": { "SigningKey": "dev-only-signing-key-please-change-me-0123456789" }
|
||||
}
|
||||
```
|
||||
`appsettings.Production.json` (values injected from environment variables at deploy time):
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": { "DefaultConnection": "" },
|
||||
"Jwt": { "SigningKey": "" }
|
||||
}
|
||||
```
|
||||
> **Secrets note (current phase):** we are on **localhost dev only**, so the Development file holds local credentials for convenience. Before any shared/staging/production use, move secrets to **User Secrets** (dev) and **environment variables** (prod), keep `appsettings.Development.json` / `appsettings.Production.json` out of git (see root `.gitignore`), and rotate any credential that was ever committed.
|
||||
|
||||
### 5.6 Minimal `Program.cs` wiring (outline)
|
||||
```csharp
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.OpenApi;
|
||||
using Serilog;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Serilog (file sink)
|
||||
builder.Host.UseSerilog((ctx, cfg) => cfg
|
||||
.ReadFrom.Configuration(ctx.Configuration)
|
||||
.WriteTo.File("logs/erpcore-.log", rollingInterval: RollingInterval.Day));
|
||||
|
||||
builder.Services.AddControllers();
|
||||
|
||||
// EF Core + PostgreSQL
|
||||
builder.Services.AddDbContext<ErpDbContext>(o =>
|
||||
o.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||
|
||||
// ProblemDetails (RFC 7807)
|
||||
builder.Services.AddProblemDetails();
|
||||
|
||||
// JWT bearer auth (RBAC deferred; identity used for audit stamp)
|
||||
builder.Services.AddAuthentication(/* JwtBearer options from config */);
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
// Health checks (EF Core DB)
|
||||
builder.Services.AddHealthChecks().AddDbContextCheck<ErpDbContext>();
|
||||
|
||||
// Swagger
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(o => o.SwaggerDoc("v1", new() { Title = "ERPCore API", Version = "v1" }));
|
||||
|
||||
// DI: register UoW, repositories, services, ICurrentUser, FifoCostingService here
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
app.UseExceptionHandler(); // maps to ProblemDetails
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
app.MapHealthChecks("/health");
|
||||
app.Run();
|
||||
```
|
||||
|
||||
### 5.7 Create the database and run
|
||||
```bash
|
||||
# ensure the ERPCore database exists in your local Postgres, then:
|
||||
dotnet ef migrations add InitialCreate -o Infra/Persistence/Migrations
|
||||
dotnet ef database update
|
||||
dotnet run
|
||||
```
|
||||
**Verify:**
|
||||
- Swagger UI at `https://localhost:<port>/swagger`
|
||||
- Health at `https://localhost:<port>/health` → `Healthy`
|
||||
|
||||
Entity/DbContext modeling (the 38 entities, configurations, enums) is specified in `10-BACKEND-PHASE1.md`. Do not invent the schema here — follow that file.
|
||||
|
||||
---
|
||||
|
||||
## 6. Running the frontend
|
||||
|
||||
The Frontend is already initialized. Point it at the backend:
|
||||
```bash
|
||||
cd Frontend
|
||||
cp .env.local.example .env.local # set NEXT_PUBLIC_API_BASE_URL=https://localhost:<port>
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
All frontend work is governed by `20-FRONTEND.md`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Routing — where to go next
|
||||
|
||||
**Every task starts here, then jumps to the right doc.** Do not work from memory; open the target doc.
|
||||
|
||||
| If you are working on… | Go to |
|
||||
|---|---|
|
||||
| Requirements, business rules, entities, ER model, data types, architecture detail | **`10-BACKEND-PHASE1.md`** |
|
||||
| API endpoints, request/response shapes, error catalog, enums | **`11-BACKEND-PHASE1.md`** |
|
||||
| Frontend user-flows, screen flow, architecture rules, validation posture | **`20-FRONTEND.md`** |
|
||||
| Understanding the doc system, reading order, tracking conventions | **`01-DOC-GUIDE.md`** |
|
||||
| Recording backend changes made | **`Backend/PROGRESS.md`** |
|
||||
| Recording frontend changes made | **`Frontend/PROGRESS.md`** |
|
||||
|
||||
Quick resolver:
|
||||
- *"Where is the model / an entity defined?"* → `10-BACKEND-PHASE1.md` (schema is authoritative there).
|
||||
- *"What does this endpoint accept/return?"* → `11-BACKEND-PHASE1.md`.
|
||||
- *"How should the UI flow / what do I validate where?"* → `20-FRONTEND.md`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Progress tracking (mandatory)
|
||||
|
||||
Two checklists track what has actually been built, and travel with code via git:
|
||||
- **`Backend/PROGRESS.md`** — backend changes
|
||||
- **`Frontend/PROGRESS.md`** — frontend changes
|
||||
|
||||
These are **Claude-maintained**: whenever a change is made on either side, the corresponding `PROGRESS.md` must be updated (tick items, add entries). The format and rules for these files are defined in **`01-DOC-GUIDE.md` §Tracking**. Both files must exist before feature work begins.
|
||||
|
||||
---
|
||||
|
||||
## 9. Documentation maintenance rule
|
||||
|
||||
There is **one source of truth** per topic. The SRS and API content live **inside** the docs above (not in separate scattered files). When a requirement or endpoint changes, edit it **in place** in `10-`/`11-BACKEND-PHASE1.md` — do not fork copies. See `01-DOC-GUIDE.md` for the full maintenance and navigation rules.
|
||||
|
||||
---
|
||||
|
||||
*End of 00-CORE.md — the hub. Next: `01-DOC-GUIDE.md`.*
|
||||
@@ -0,0 +1,191 @@
|
||||
# 01 · DOC-GUIDE — Documentation Map & Tracking Conventions
|
||||
|
||||
> **Purpose:** This file explains the documentation system itself — what every doc is for, the order to read them, the single navigation rule, and the conventions for the two `PROGRESS.md` tracking files. It does **not** contain requirements or API detail; it tells you where those live and how to keep everything in sync.
|
||||
|
||||
---
|
||||
|
||||
## 1. The one navigation rule
|
||||
|
||||
**All navigation starts at `00-CORE.md`.**
|
||||
|
||||
`00-CORE.md` is the hub. Whatever the task, open it first and use its routing table (§7 there) to jump to the correct document. Do not work from memory or jump straight into a spec file — the hub exists so there is a single, consistent entry point for humans and for Claude.
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
any task → │ 00-CORE.md │ (hub: structure, stack, setup, routing)
|
||||
└────────┬────────┘
|
||||
┌────────────────┼─────────────────┬───────────────────┐
|
||||
▼ ▼ ▼ ▼
|
||||
10-BACKEND-PHASE1 11-BACKEND-PHASE1 20-FRONTEND 01-DOC-GUIDE
|
||||
(SRS+ER+arch) (API req/res) (flows+rules) (this file)
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
Backend/PROGRESS.md (record changes) Frontend/PROGRESS.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Document index
|
||||
|
||||
| Doc | Level | Role | Read when | Who maintains |
|
||||
|---|---|---|---|---|
|
||||
| `00-CORE.md` | High | **Hub.** Structure, tech stack, runnable backend init, routing. | First — every task. | Claude + humans |
|
||||
| `01-DOC-GUIDE.md` | High | **This file.** Doc map, reading order, tracking conventions. | To understand the doc system. | Claude + humans |
|
||||
| `10-BACKEND-PHASE1.md` | High | Backend spec: **full SRS**, **ER model / 38-entity list**, tech stack detail, layer/architecture rules. Schema is **authoritative** here. | Any backend model / business-rule / requirement work. | Claude + humans |
|
||||
| `11-BACKEND-PHASE1.md` | High | Backend **API reference**: every endpoint with complete request/response, error catalog, enums. | Any API contract / controller / client work. | Claude + humans |
|
||||
| `20-FRONTEND.md` | High | Frontend **user-flows**, architecture rules to follow, **validation posture**. | Any frontend work. | Claude + humans |
|
||||
| `Backend/PROGRESS.md` | Low | Backend **change checklist**, git-shared. | After making backend changes. | **Claude** |
|
||||
| `Frontend/PROGRESS.md` | Low | Frontend **change checklist**, git-shared. | After making frontend changes. | **Claude** |
|
||||
|
||||
Numbering convention: `0x` = high-level hub/guide, `1x` = backend, `2x` = frontend. Numbers sort in read order in any file browser.
|
||||
|
||||
---
|
||||
|
||||
## 3. Reading order (first time)
|
||||
|
||||
1. **`00-CORE.md`** — understand the project, structure, stack, and how to stand up the backend.
|
||||
2. **`01-DOC-GUIDE.md`** (this file) — understand the doc system and tracking.
|
||||
3. Then, per task, jump via the hub to `10-`, `11-`, or `20-`.
|
||||
|
||||
You do not need to read `10/11/20` end-to-end before starting; open the section relevant to your task via the hub.
|
||||
|
||||
---
|
||||
|
||||
## 4. Which doc answers which question
|
||||
|
||||
| Question | Answer lives in |
|
||||
|---|---|
|
||||
| "What is the business rule for X?" / "What entity/field is this?" | `10-BACKEND-PHASE1.md` |
|
||||
| "What does endpoint Y accept and return?" / "What's the error code?" | `11-BACKEND-PHASE1.md` |
|
||||
| "How does the user move through the UI?" / "What do I validate on the client vs server?" | `20-FRONTEND.md` |
|
||||
| "How do I set up / run the project?" | `00-CORE.md` |
|
||||
| "Where do I record what I changed?" | `Backend/PROGRESS.md` or `Frontend/PROGRESS.md` (this file, §6) |
|
||||
|
||||
If a question spans backend + frontend (e.g. a new feature), read the backend spec/API first (the contract), then the frontend doc (how the UI consumes it).
|
||||
|
||||
---
|
||||
|
||||
## 5. Maintenance rules (single source of truth)
|
||||
|
||||
- **One source of truth per topic.** Requirements live only in `10-`; API contracts live only in `11-`; frontend rules live only in `20-`. Do not copy content between docs — link instead.
|
||||
- **Edit in place.** When a requirement or endpoint changes, update it where it lives. Never fork a second copy.
|
||||
- **Contract before consumer.** Change the backend spec/API doc first, then update the frontend doc and code to match.
|
||||
- **Keep the hub thin.** `00-CORE.md` routes and sets up; it must not accumulate requirement/API detail.
|
||||
- **Every material change updates a `PROGRESS.md`** (see §6). Docs and trackers move together in the same commit as the code.
|
||||
|
||||
---
|
||||
|
||||
## 6. Tracking convention — `PROGRESS.md`
|
||||
|
||||
Two low-level checklists record what has actually been built. They live **inside** each project so they travel with code across branches and are diff-visible in git:
|
||||
- `Backend/PROGRESS.md`
|
||||
- `Frontend/PROGRESS.md`
|
||||
|
||||
### 6.1 Rules
|
||||
- **Claude maintains these.** On every change to a side, update that side's `PROGRESS.md` in the **same commit** as the code.
|
||||
- Use checkboxes: `- [ ]` not started · `- [~]` in progress · `- [x]` done.
|
||||
- Each entry is a concrete, verifiable unit of work (an endpoint, an entity + its configuration, a service method, a screen, a validation rule) — not vague ("did stuff").
|
||||
- Group by module/area so the file mirrors the spec structure.
|
||||
- When ticking `- [x]`, append a short note: what was done + any deviation from the spec (and why).
|
||||
- Never delete history; move completed items under a **Done** section if the active list gets long.
|
||||
- If a change alters a spec doc (`10/11/20`), reference it: `(updates 11-BACKEND-PHASE1 §5.4)`.
|
||||
|
||||
### 6.2 Backend `PROGRESS.md` starter template
|
||||
```markdown
|
||||
# Backend — PROGRESS (Phase 1: Inventory & Supply Chain)
|
||||
|
||||
Legend: [ ] not started · [~] in progress · [x] done
|
||||
Spec: docs/10-BACKEND-PHASE1.md (model) · docs/11-BACKEND-PHASE1.md (API)
|
||||
|
||||
## 0. Bootstrap
|
||||
- [ ] Solution + Web API project (net10.0), packages restored
|
||||
- [ ] Folder structure per 00-CORE §5.3
|
||||
- [ ] ErpDbContext + Npgsql wired; InitialCreate migration applied
|
||||
- [ ] Serilog, JWT, Swagger, HealthChecks, ProblemDetails in Program.cs
|
||||
- [ ] UnitOfWork + generic repository base
|
||||
- [ ] ICurrentUser (audit stamp from token)
|
||||
|
||||
## 1. Master Data
|
||||
- [ ] Item: entity + config + enum(s)
|
||||
- [ ] Item: repository + service + controller (CRUD, DTOs)
|
||||
- [ ] UOM + conversions
|
||||
- [ ] Category (hierarchy)
|
||||
- [ ] Vendor
|
||||
- [ ] Warehouse + Bin
|
||||
- [ ] Item reorder settings
|
||||
|
||||
## 2. Procurement
|
||||
- [ ] Requisition (+ lines)
|
||||
- [ ] RFQ + quotations + comparison
|
||||
- [ ] Purchase Order (create, edit-while-open, auto-approve, cancel)
|
||||
- [ ] Purchase Return
|
||||
|
||||
## 3. Goods Receipt
|
||||
- [ ] GRN (create against PO / direct)
|
||||
- [ ] GRN confirm → FIFO layer + ledger + PO qtyReceived
|
||||
- [ ] Inspection hold release/reject
|
||||
|
||||
## 4. Stock Core
|
||||
- [ ] StockLayer + StockLedger entities/config
|
||||
- [ ] FifoCostingService (consume oldest-first, valuation)
|
||||
- [ ] Stock enquiry (onHand/available/onHold/inTransit)
|
||||
- [ ] Ledger query · Valuation query
|
||||
|
||||
## 5. Stock Transactions
|
||||
- [ ] Transfer (create → dispatch → receive, in-transit, cost-preserving)
|
||||
- [ ] Adjustment (auto-post, reason code)
|
||||
- [ ] Count (cycle/full → variance → post)
|
||||
- [ ] Reorder alerts
|
||||
|
||||
## 6. Cross-cutting
|
||||
- [ ] ProblemDetails error catalog + domain exceptions
|
||||
- [ ] Audit log on mutations
|
||||
- [ ] Auth (simple in-app login → JWT)
|
||||
- [ ] Document numbering sequences
|
||||
|
||||
## Done
|
||||
<!-- move [x] items here with date + note if the active list grows -->
|
||||
```
|
||||
|
||||
### 6.3 Frontend `PROGRESS.md` starter template
|
||||
```markdown
|
||||
# Frontend — PROGRESS (Phase 1: Inventory & Supply Chain)
|
||||
|
||||
Legend: [ ] not started · [~] in progress · [x] done
|
||||
Spec: docs/20-FRONTEND.md (flows + rules) · docs/11-BACKEND-PHASE1.md (API contract)
|
||||
|
||||
## 0. Foundation
|
||||
- [ ] API base URL env wired (NEXT_PUBLIC_API_BASE_URL)
|
||||
- [ ] Typed API client / fetch wrapper + auth token handling
|
||||
- [ ] Shared types mirroring API DTOs
|
||||
- [ ] Client-side validation helpers (dependency-free)
|
||||
|
||||
## 1. Screens / flows (per 20-FRONTEND user-flows)
|
||||
- [ ] Login
|
||||
- [ ] Items (list + create/edit)
|
||||
- [ ] Vendors · Warehouses/Bins
|
||||
- [ ] Requisition → PO flow
|
||||
- [ ] GRN (receive + hold handling)
|
||||
- [ ] Stock: enquiry · transfer · adjustment · count · reorder
|
||||
|
||||
## 2. Validation posture (client for UX; server authoritative)
|
||||
- [ ] Client format/required/range checks on forms
|
||||
- [ ] Surface server ProblemDetails errors (incl. domain codes)
|
||||
- [ ] Never assume stock/availability rules client-side
|
||||
|
||||
## Done
|
||||
<!-- move [x] items here with date + note if the active list grows -->
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Adding future docs
|
||||
|
||||
When later phases arrive (Sales & CRM, Manufacturing, QC/QA, Accounting, HRM), follow the same scheme:
|
||||
- Backend spec/API for a phase → new `1x-` files (e.g. `30-BACKEND-PHASE2.md`), linked from the hub.
|
||||
- Frontend additions → extend `20-FRONTEND.md` or add `2x-` files.
|
||||
- Always register the new doc in `00-CORE.md` routing (§7) and in this index (§2).
|
||||
|
||||
---
|
||||
|
||||
*End of 01-DOC-GUIDE.md. Return to `00-CORE.md` to route to your task.*
|
||||
@@ -0,0 +1,352 @@
|
||||
# 10 · BACKEND — Phase 1 Spec (Inventory & Supply Chain)
|
||||
|
||||
> **Authoritative for:** backend architecture, business rules, and the data model (the 38-entity schema).
|
||||
> **Navigation:** you arrived here from `00-CORE.md`. API request/response contracts are in `11-BACKEND-PHASE1.md`. Frontend rules are in `20-FRONTEND.md`. Record work in `Backend/PROGRESS.md`.
|
||||
> **Scope basis:** SRS v1.1. Costing = FIFO · Multi-warehouse · Single-tenant · RBAC deferred (user identity stamped) · approvals auto/config-gated · vendor invoice + 3-way match deferred to Accounting.
|
||||
|
||||
---
|
||||
|
||||
# Part A — Architecture & Layer Rules
|
||||
|
||||
The high-level layering is introduced in `00-CORE.md §4`; this part is the authoritative detail the implementation must follow.
|
||||
|
||||
## A.1 Layered flow (one direction only)
|
||||
```
|
||||
HTTP ─► Controller ─► Service ─► Repository ─► UnitOfWork / ErpDbContext ─► PostgreSQL
|
||||
(DTOs) (logic, (entities) (transaction boundary)
|
||||
returns DTOs)
|
||||
```
|
||||
|
||||
**Controller**
|
||||
- HTTP concerns only: routing, model binding, status codes, `[ProducesResponseType]`.
|
||||
- Accepts and returns **DTOs only**. Never references EF entities or `ErpDbContext`.
|
||||
- No business logic, no data access.
|
||||
|
||||
**Service**
|
||||
- Owns business logic and orchestration. Returns DTOs.
|
||||
- Maps entity ⇄ DTO (manual mapping is fine and dependency-free; keep mapping in the service or a dedicated mapper class).
|
||||
- Opens the UnitOfWork transaction for any operation that spans more than one write.
|
||||
|
||||
**Repository**
|
||||
- EF Core data access over **entities**. One repository per aggregate (Item, PurchaseOrder, Grn, Stock…).
|
||||
- Query + persistence only. **No business rules.** Returns entities or projections to services.
|
||||
|
||||
**UnitOfWork**
|
||||
- The transaction boundary. Wraps a single `ErpDbContext`; exposes `SaveChangesAsync()` and an explicit transaction scope (`BeginTransactionAsync`) for multi-step stock operations.
|
||||
- Every stock-affecting operation (GRN confirm, transfer dispatch/receive, adjustment, purchase return, count post) runs inside **one** UoW transaction so it commits or rolls back atomically (NFR-02, NFR-05).
|
||||
|
||||
## A.2 FIFO placement (critical)
|
||||
- FIFO cost-layer consumption and valuation live in a **domain service**: `Services/Stock/FifoCostingService`.
|
||||
- It is invoked by stock services **inside** the UoW transaction — never from a controller or repository.
|
||||
- On any issue (transfer-out, adjustment-out, purchase return, future goods issue), it consumes open layers **oldest-first**, decrements `qtyRemaining`, and returns the costed movement for the ledger.
|
||||
- Layer rows being consumed **must be locked** for the duration of the transaction to stay concurrency-safe (NFR-02). Use `SELECT … FOR UPDATE` semantics (EF: query within the transaction with appropriate locking) so two concurrent issues can't consume the same remaining quantity.
|
||||
|
||||
## A.3 DTO boundary
|
||||
- Every request body and response is a DTO in `Dtos/`. Group by module (`Dtos/Items`, `Dtos/Procurement`, `Dtos/Grn`, `Dtos/Stock`, `Dtos/Common`).
|
||||
- `Dtos/Common` holds shared shapes: `PagedResult<T>`, `PaginationMeta`, and the error/problem shapes.
|
||||
- Entities in `Domain/Entities` never leave the service layer.
|
||||
|
||||
## A.4 Cross-cutting
|
||||
- **Errors:** RFC 7807 `ProblemDetails` (framework default). Domain exceptions in `System/Errors` carry a stable `code`; a middleware maps them to `ProblemDetails`. Catalog in `11-BACKEND-PHASE1.md §7`.
|
||||
- **Audit actor:** an `ICurrentUser` abstraction (`Infra/Auth`) resolves the acting user from the JWT `sub`. Services stamp mutations with it. **Never** trust a `createdBy` from the request body.
|
||||
- **Concurrency:** mutable resources carry a `RowVersion` (`[Timestamp] byte[]`), surfaced as `ETag`; `PUT`/`PATCH` require `If-Match` → `412` on mismatch.
|
||||
- **Numbering:** document numbers come from `NumberSequence` (per doc type, per year), issued inside the same transaction as the document.
|
||||
|
||||
## A.5 DI registration (lifetimes)
|
||||
- `ErpDbContext`: scoped (default).
|
||||
- `IUnitOfWork`, repositories, services, `ICurrentUser`, `FifoCostingService`: **scoped**.
|
||||
- Register in `Program.cs` (or an `AddApplication()` extension) after `AddDbContext`.
|
||||
|
||||
---
|
||||
|
||||
# Part B — Software Requirements Specification (v1.1)
|
||||
|
||||
> Basis: ISO/IEC/IEEE 29148. Requirement IDs `FR-<AREA>-<n>`; priority **M**/**S**/**C** (Must/Should/Could).
|
||||
|
||||
## B.1 Introduction
|
||||
|
||||
### B.1.1 Purpose
|
||||
Specifies Phase 1 — the Inventory & Supply Chain subsystem: master data, procurement, goods receipt, stock and warehouse management, plus the integration seams reserved for later phases.
|
||||
|
||||
### B.1.2 Scope
|
||||
|
||||
**In scope:** master data (Item, UOM & conversions, Category, Vendor, Warehouse/Bin); procurement (Requisition → RFQ → PO with approval + amendments → Purchase Return); GRN with inspection hold and partial receipt; stock management (count, transfer with in-transit, adjustment, ledger, FIFO valuation, reorder alerts); warehouse management (multi-warehouse, bin/location, batch/expiry and serial, basic putaway/pick); cross-cutting services (user identity + audit, document numbering, reason codes).
|
||||
|
||||
**Out of scope (deferred, hooks retained):** vendor invoice & three-way match → Accounting (GRN retains PO ref, received qty, received value per line); GL posting (Phase 1 emits GL-ready journal entries as data, no ledger); sales orders / reservation fulfilment / manufacturing consumption / formal QC dispositions (interfaces stubbed, §B.7); landed-cost apportionment (decision §B.1.2.1).
|
||||
|
||||
#### B.1.2.1 Landed cost decision (open)
|
||||
Imports carry freight + duty + VAT, so true unit cost ≠ PO price.
|
||||
- **(A) Defer** to Accounting — layers valued at PO price + directly-attributable line charges only. Simpler; valuation understates true cost.
|
||||
- **(B) Include** apportionment at GRN — distribute additional charges across received lines into the FIFO layer cost. More accurate; one extra workflow.
|
||||
|
||||
Affects FR-GRN-06 and FR-STK-14. Recommendation: B if import duties are material; else A.
|
||||
|
||||
### B.1.3 Definitions
|
||||
GRN = Goods Receipt Note · PO = Purchase Order · RFQ = Request for Quotation · PR = Purchase Requisition · UOM = Unit of Measure · FIFO = First-In-First-Out costing · FEFO = First-Expiry-First-Out picking · Cost layer = quantity received at a specific unit cost, consumed FIFO · In-transit = stock left source, not yet confirmed at destination · ROP = reorder point · RBAC = role-based access control · GL = general ledger.
|
||||
|
||||
### B.1.4 Overview
|
||||
§B.2 overall description; §B.3 functional requirements; §B.4 data model; §B.5 external interfaces; §B.6 NFRs; §B.7 future seams; §B.8 appendices. The entity model is expanded in **Part C**.
|
||||
|
||||
## B.2 Overall Description
|
||||
|
||||
### B.2.1 Product perspective
|
||||
Foundation of a modular ERP. **Single-tenant**, **multi-warehouse**. All later modules depend on the Item master, stock ledger, and vendor master defined here. Module boundaries are isolated behind services (Part A).
|
||||
|
||||
### B.2.2 Product functions
|
||||
Maintain master data; raise/approve procurement through PO and return; receive goods with inspection hold; track stock movements in a costed FIFO ledger across warehouses; perform counts/transfers/adjustments with audit; track batch/expiry/serial and locate by bin; raise reorder alerts.
|
||||
|
||||
### B.2.3 User classes
|
||||
> **Phase-1 note:** role-based permissions are **deferred** (FR-X-01). Phase 1 runs a **single operational user context** — any user may perform any action, but each action is stamped with the authenticated user's identity for audit. The roles below are the functional blueprint for future RBAC, **not** enforced boundaries.
|
||||
|
||||
Storekeeper/Warehouse operator (receive, count, transfer, pick) · Procurement officer (requisitions, POs, vendors) · Approver/Manager (authorizes once approvals enabled) · Inventory controller (valuation, adjustments, reorder policy) · Auditor (read-only) · System administrator (users, numbering, config).
|
||||
|
||||
### B.2.4 Operating environment
|
||||
Web application; responsive UI incl. handheld/scanner. Relational DB with transactions + row-level locking (FIFO consumption needs it). Barcode/QR capable (designed-for).
|
||||
|
||||
### B.2.5 Constraints
|
||||
FIFO is a system-wide constraint; ledger **must** track cost layers. Multi-warehouse mandatory day 1; multi-tenancy out of scope. Single base currency (LKR) in Phase 1. Every stock transaction atomic + immutable ledger entry.
|
||||
|
||||
### B.2.6 Assumptions & dependencies
|
||||
One base currency; invoicing/3-way match in Accounting (GRN carries data); users/warehouses configured before transactions; landed-cost scope (§B.1.2.1) resolved before GRN valuation finalized.
|
||||
|
||||
## B.3 Functional Requirements
|
||||
|
||||
### B.3.1 Master Data (FR-MD)
|
||||
| ID | Requirement | Pri |
|
||||
|---|---|---|
|
||||
| FR-MD-01 | Maintain **Item master**: SKU (unique), name, description, category, item type (Stocked/NonStocked/Service), tracking mode (None/Batch/Serial), status, tax class, default vendor. | M |
|
||||
| FR-MD-02 | Maintain **UOM master** with base UOM per item and **conversion factors** (purchase→stock→base). | M |
|
||||
| FR-MD-03 | Convert quantities between UOMs on every transaction; store base-UOM quantity in the ledger. | M |
|
||||
| FR-MD-04 | Maintain **hierarchical item categories**. | S |
|
||||
| FR-MD-05 | Hold **reorder point** and **reorder quantity** per item, optionally per warehouse. | M |
|
||||
| FR-MD-06 | Maintain **Vendor master**: code, name, contact, terms, tax reg, status, currency. | M |
|
||||
| FR-MD-07 | Maintain **Warehouse master** and, within each, a **bin/location** structure. | M |
|
||||
| FR-MD-08 | Prevent deletion of any master referenced by a transaction; deactivate instead. | M |
|
||||
|
||||
### B.3.2 Procurement (FR-PROC)
|
||||
| ID | Requirement | Pri |
|
||||
|---|---|---|
|
||||
| FR-PROC-01 | Create **Purchase Requisition** with lines (item, qty, required-by, requester). | M |
|
||||
| FR-PROC-02 | Optional **RFQ**: issue to vendors, record quotations for comparison. | S |
|
||||
| FR-PROC-03 | Generate **PO** from PR/RFQ or directly (item, UOM, qty, price, tax, delivery date, warehouse). | M |
|
||||
| FR-PROC-04 | **[Phase 1: auto-approve]** Auto-approve PO on creation (status `Approved`). Config flag `approvalRequired` (default off) gates a future approval workflow (authorization matrix); when on, PO cannot issue until approved. `PendingApproval` state + approval fields retained in schema (no migration to enable). | M |
|
||||
| FR-PROC-05 | **[Phase 1: Option B — edit-while-open]** PO may be **freely edited while open** (not fully received/closed); changes take effect immediately with an audit entry. Versioned amendments deferred; schema must not preclude adding a version field later. | S |
|
||||
| FR-PROC-06 | PO lifecycle: Draft → (PendingApproval →) Approved → PartiallyReceived → FullyReceived → Closed/Cancelled. Phase 1 bypasses PendingApproval via auto-approve. | M |
|
||||
| FR-PROC-07 | Support **partial receipt**; PO stays open until fully received or manually closed. | M |
|
||||
| FR-PROC-08 | Support **Purchase Return** referencing original GRN/PO line; generates outbound movement. | M |
|
||||
| FR-PROC-09 | Retain data sufficient for future **three-way match** without schema change. | M |
|
||||
|
||||
### B.3.3 Goods Receipt (FR-GRN)
|
||||
| ID | Requirement | Pri |
|
||||
|---|---|---|
|
||||
| FR-GRN-01 | Create **GRN** against an approved PO, defaulting lines/quantities from open PO lines. | M |
|
||||
| FR-GRN-02 | Support **GRN without PO** (direct/emergency) by permission, flagged for review. | S |
|
||||
| FR-GRN-03 | Support **over/under-receipt tolerances** (per item or global); warn or block beyond tolerance. | S |
|
||||
| FR-GRN-04 | Capture **batch + expiry** and/or **serial numbers** for tracked items on receipt. | M |
|
||||
| FR-GRN-05 | Allow receipt into **inspection/quarantine hold** (not issuable) pending QC, before QC module exists. | M |
|
||||
| FR-GRN-06 | On confirm, each line **creates a FIFO cost layer** at unit cost (PO price + attributable charges; landed cost per §B.1.2.1) and posts an inbound ledger entry. | M |
|
||||
| FR-GRN-07 | Record **received value per line** and PO reference for downstream matching. | M |
|
||||
| FR-GRN-08 | Assign received stock to a **bin/location** (putaway). | S |
|
||||
|
||||
### B.3.4 Stock Management (FR-STK)
|
||||
| ID | Requirement | Pri |
|
||||
|---|---|---|
|
||||
| FR-STK-01 | Maintain an **immutable, append-only stock ledger**: item, warehouse, bin, batch/serial, qty (base UOM), unit cost, value, running balance, source doc, user, timestamp. | M |
|
||||
| FR-STK-02 | Maintain **FIFO cost layers** per item **per warehouse** (received qty, remaining qty, unit cost, receipt date). | M |
|
||||
| FR-STK-03 | On any issue, **consume oldest layers first**, posting cost at each layer's unit cost. | M |
|
||||
| FR-STK-04 | Compute **valuation** = Σ(remaining qty × unit cost) over open layers, per item/warehouse and total. | M |
|
||||
| FR-STK-05 | Support **transfer** between warehouses/bins with **in-transit**: out consumes source layers; in confirms and creates destination layer. | M |
|
||||
| FR-STK-06 | Transfers are **cost-preserving**: destination layer inherits consumed source cost. | M |
|
||||
| FR-STK-07 | **[Phase 1: auto-post]** **Adjustments** (increase/decrease/write-off) with mandatory **reason code** post immediately; a config flag (default off) gates threshold approval later. Reason code + user stamp always mandatory. | M |
|
||||
| FR-STK-08 | Support **cycle count** and **full physical count** workflows; post variance adjustments on confirmation. | M |
|
||||
| FR-STK-09 | Define **negative-stock policy** (default: block issues that would drive on-hand negative; configurable per item). | M |
|
||||
| FR-STK-10 | Provide **reorder alerts** when available ≤ reorder point; optionally suggest a requisition. | M |
|
||||
| FR-STK-11 | Expose a **reservation/allocation** status on stock (stubbed Phase 1; consumed by Sales later) to distinguish on-hand vs available. | S |
|
||||
| FR-STK-12 | Stock enquiry by item/warehouse/bin/batch/serial showing on-hand, in-transit, on-hold, available. | M |
|
||||
| FR-STK-13 | Every stock transaction generates a **GL-ready journal entry** as data (no posting in Phase 1). | S |
|
||||
| FR-STK-14 | With landed cost enabled (Option B), apportion additional charges into FIFO layer costs at GRN by configurable basis. | C |
|
||||
|
||||
### B.3.5 Warehouse Management (FR-WH)
|
||||
| ID | Requirement | Pri |
|
||||
|---|---|---|
|
||||
| FR-WH-01 | Support **multiple warehouses**, each with a bin/location hierarchy. | M |
|
||||
| FR-WH-02 | Track stock **to bin level**; support bin-to-bin movement within a warehouse. | M |
|
||||
| FR-WH-03 | Support **batch/lot** tracking with expiry; enforce **FEFO picking** for perishables (distinct from FIFO costing). | M |
|
||||
| FR-WH-04 | Support **serial-number** tracking across lifecycle (receipt → issue). | M |
|
||||
| FR-WH-05 | Provide basic **putaway** and **pick** steps. | S |
|
||||
| FR-WH-06 | Be **barcode/QR-scan ready** for item, bin, batch, serial. | C |
|
||||
| FR-WH-07 | Block issue/pick of **on-hold/quarantine** or **expired** stock. | M |
|
||||
|
||||
### B.3.6 Cross-cutting (FR-X)
|
||||
| ID | Requirement | Pri |
|
||||
|---|---|---|
|
||||
| FR-X-01 | **[Phase 1: user identity only]** Authenticate users and **stamp every transaction with the acting user's identity** for audit. Full RBAC (role→permission matrix, from which approvals derive) is **deferred**; reserve role/permission structures for no-migration enablement. | M |
|
||||
| FR-X-02 | Maintain an **immutable audit trail** for every create/update/delete and stock movement (who/when/old→new/reason). | M |
|
||||
| FR-X-03 | Generate **document numbers** from configurable sequences (per type, per year), unique and gap-controlled. | M |
|
||||
| FR-X-04 | Maintain configurable **reason-code** lists (adjustments, returns, count variances). | M |
|
||||
| FR-X-05 | No editing/deleting confirmed/posted transactions; corrections via reversing entries. | M |
|
||||
|
||||
## B.4 Data Model (summary)
|
||||
Key entities and relationships are enumerated in **Part C**. The commitment: Item, UOM/UOMConversion, Category, Vendor, Warehouse/Bin, ItemReorder, Requisition(+Line), RFQ(+Line)/VendorQuotation, PurchaseOrder(+Line), GRN(+Line), PurchaseReturn(+Line), StockLayer (FIFO), StockLedger (immutable), Batch, Serial, StockTransfer(+Line), StockAdjustment(+Line), StockCount(+Line), User, ReasonCode, NumberSequence, AuditLog, JournalEntryStub, and reserved RBAC (Role, Permission, UserRole, RolePermission).
|
||||
|
||||
## B.5 External Interfaces
|
||||
UI: responsive; count/pick screens handheld-friendly; status badges; mandatory-field validation. Hardware: barcode/QR (designed-for). Software: relational DB with transactional integrity + row locking; internal service interfaces/events for Phase-2+ modules.
|
||||
|
||||
## B.6 Non-Functional Requirements
|
||||
| ID | Category | Requirement |
|
||||
|---|---|---|
|
||||
| NFR-01 | Performance | Single item/warehouse enquiry + valuation < 2s under normal load; ledger posting transactional, < 1s per line. |
|
||||
| NFR-02 | Integrity | FIFO layer consumption atomic and concurrency-safe; no double-consumption of remaining qty. |
|
||||
| NFR-03 | Security | Users authenticated; passwords hashed; every action attributed to a user and logged. (Role-based enforcement deferred, FR-X-01.) |
|
||||
| NFR-04 | Auditability | Audit trail immutable, retained per policy; ledger append-only. |
|
||||
| NFR-05 | Reliability | No stock transaction partially commits; full rollback on failure. |
|
||||
| NFR-06 | Scalability | Growth in items/warehouses/ledger without redesign; ledger indexed for time-series queries. |
|
||||
| NFR-07 | Usability | Receive/count/transfer achievable with minimal training; scan-first where hardware present. |
|
||||
| NFR-08 | Maintainability | Modular service boundaries; Phase-2+ integrates without altering Phase-1 schema. |
|
||||
| NFR-09 | Availability | **Not a Phase-1 engineering commitment.** Uptime SLA to be set for production; HA infra (failover, redundancy) deferred to a later hardening phase. |
|
||||
| NFR-10 | Configurability | Numbering, tolerances, reason codes, negative-stock and reorder policies configurable without code change. |
|
||||
|
||||
## B.7 Future Modules & Integration Seams
|
||||
| Module | Seam in Phase 1 |
|
||||
|---|---|
|
||||
| Accounting | GL-ready journal entries per movement (FR-STK-13); GRN retains PO ref + received value for 3-way match (FR-PROC-09, FR-GRN-07). |
|
||||
| Sales & CRM | Reservation/allocation status distinguishing on-hand vs available (FR-STK-11). |
|
||||
| Manufacturing | Generic goods-issue/consumption movement type BOM will consume through (extends FR-STK-03). |
|
||||
| QC / QA | GRN inspection/quarantine hold (FR-GRN-05); hold blocks issue (FR-WH-07). |
|
||||
| HRM | User identity foundation (FR-X-01) reusable for employee-linked permissions. |
|
||||
| RBAC & Approvals | Config flags + retained `PendingApproval`/role structures enable PO & adjustment approvals with no schema change. |
|
||||
|
||||
## B.8 Appendices
|
||||
|
||||
**B.8.1 Status lifecycles**
|
||||
PO: Draft → (PendingApproval →) Approved → PartiallyReceived → FullyReceived → Closed/Cancelled · GRN: Draft → Confirmed → Closed · Transfer: Draft → InTransit → Received → Closed · Adjustment/Count: Draft → (PendingApproval →) Posted.
|
||||
|
||||
**B.8.2 Document numbering (examples)**
|
||||
`PR-YYYY-#####`, `PO-YYYY-#####`, `GRN-YYYY-#####`, `TRF-YYYY-#####`, `ADJ-YYYY-#####`, `CNT-YYYY-#####`, `PRET-YYYY-#####` (FR-X-03).
|
||||
|
||||
**B.8.3 Reason codes (seed)**
|
||||
Adjustment: Damage, Theft/Loss, Count Variance, Expiry Write-off, System Correction · Return: Defective, Wrong Item, Over-supply, Quality Reject.
|
||||
|
||||
**B.8.4 Open decisions log**
|
||||
| # | Decision | Status |
|
||||
|---|---|---|
|
||||
| 1 | Landed-cost scope (§B.1.2.1, A vs B) | **Open** |
|
||||
| 2 | Negative-stock: global block vs per-item (FR-STK-09) | Proposed: block |
|
||||
| 3 | GRN-without-PO permission scope (FR-GRN-02) | Open |
|
||||
| 4 | Availability/uptime SLA target (NFR-09) | Deferred to production |
|
||||
| 5 | PO & adjustment approvals | **Resolved:** deferred — auto-approve, config-gated off |
|
||||
| 6 | RBAC | **Resolved:** deferred — user identity only |
|
||||
| 7 | PO amendments | **Resolved:** Option B, edit-while-open |
|
||||
| 8 | Costing method | **Resolved:** FIFO |
|
||||
| 9 | Tenancy | **Resolved:** single-tenant |
|
||||
|
||||
---
|
||||
|
||||
# Part C — ER Model (38 entities)
|
||||
|
||||
Costing: FIFO · Multi-warehouse · Single-tenant. Legend: **PK** primary key · **FK** foreign key. Companion visual diagrams (Mermaid / draw.io ERD) accompany this repo; this part is the authoritative textual model.
|
||||
|
||||
## C.1 Master Data
|
||||
```
|
||||
CATEGORY(category_id PK, parent_id FK→CATEGORY, name)
|
||||
UOM(uom_id PK, name)
|
||||
UOM_CONVERSION(conversion_id PK, item_id FK→ITEM, from_uom FK→UOM, to_uom FK→UOM, factor)
|
||||
ITEM(item_id PK, sku, name, category_id FK→CATEGORY, base_uom_id FK→UOM,
|
||||
default_vendor_id FK→VENDOR, item_type, tracking_mode, tax_class, status)
|
||||
ITEM_REORDER(reorder_id PK, item_id FK→ITEM, warehouse_id FK→WAREHOUSE, reorder_point, reorder_qty)
|
||||
VENDOR(vendor_id PK, code, name, terms, tax_reg, currency, status)
|
||||
WAREHOUSE(warehouse_id PK, code, name)
|
||||
BIN(bin_id PK, warehouse_id FK→WAREHOUSE, code, bin_type)
|
||||
```
|
||||
|
||||
## C.2 Procurement
|
||||
```
|
||||
REQUISITION(requisition_id PK, doc_no, requested_by FK→USER, status, created_at)
|
||||
REQUISITION_LINE(req_line_id PK, requisition_id FK→REQUISITION, item_id FK→ITEM, qty, required_by)
|
||||
RFQ(rfq_id PK, doc_no, requisition_id FK→REQUISITION, status)
|
||||
RFQ_LINE(rfq_line_id PK, rfq_id FK→RFQ, item_id FK→ITEM, qty)
|
||||
VENDOR_QUOTATION(quotation_id PK, rfq_id FK→RFQ, vendor_id FK→VENDOR, unit_price, lead_days)
|
||||
PURCHASE_ORDER(po_id PK, doc_no, vendor_id FK→VENDOR, requisition_id FK→REQUISITION,
|
||||
status, approval_required, created_by FK→USER, created_at)
|
||||
PO_LINE(po_line_id PK, po_id FK→PURCHASE_ORDER, item_id FK→ITEM, uom_id FK→UOM,
|
||||
warehouse_id FK→WAREHOUSE, qty, unit_price, tax, qty_received)
|
||||
PURCHASE_RETURN(return_id PK, doc_no, vendor_id FK→VENDOR, warehouse_id FK→WAREHOUSE,
|
||||
reason_code_id FK→REASON_CODE, created_by FK→USER)
|
||||
PURCHASE_RETURN_LINE(return_line_id PK, return_id FK→PURCHASE_RETURN,
|
||||
grn_line_id FK→GRN_LINE, item_id FK→ITEM, qty)
|
||||
```
|
||||
|
||||
## C.3 Goods Receipt
|
||||
```
|
||||
GRN(grn_id PK, doc_no, po_id FK→PURCHASE_ORDER, vendor_id FK→VENDOR,
|
||||
warehouse_id FK→WAREHOUSE, status, created_by FK→USER, created_at)
|
||||
GRN_LINE(grn_line_id PK, grn_id FK→GRN, po_line_id FK→PO_LINE, item_id FK→ITEM, uom_id FK→UOM,
|
||||
bin_id FK→BIN, batch_id FK→BATCH, qty, unit_cost, received_value, hold_status)
|
||||
```
|
||||
|
||||
## C.4 Batch / Serial
|
||||
```
|
||||
BATCH(batch_id PK, item_id FK→ITEM, batch_no, expiry_date)
|
||||
SERIAL(serial_id PK, item_id FK→ITEM, serial_no, status)
|
||||
```
|
||||
|
||||
## C.5 Stock Core (FIFO + Ledger)
|
||||
```
|
||||
STOCK_LAYER(layer_id PK, item_id FK→ITEM, warehouse_id FK→WAREHOUSE, batch_id FK→BATCH,
|
||||
serial_id FK→SERIAL, grn_line_id FK→GRN_LINE,
|
||||
qty_received, qty_remaining, unit_cost, receipt_date) -- FIFO layers
|
||||
STOCK_LEDGER(ledger_id PK, item_id FK→ITEM, warehouse_id FK→WAREHOUSE, bin_id FK→BIN,
|
||||
batch_id FK→BATCH, serial_id FK→SERIAL, user_id FK→USER,
|
||||
direction, qty_base, unit_cost, value, running_balance,
|
||||
source_doc_type, source_doc_id, created_at) -- immutable journal
|
||||
```
|
||||
|
||||
## C.6 Stock Transactions
|
||||
```
|
||||
STOCK_TRANSFER(transfer_id PK, doc_no, src_warehouse_id FK→WAREHOUSE,
|
||||
dest_warehouse_id FK→WAREHOUSE, status, created_by FK→USER)
|
||||
STOCK_TRANSFER_LINE(transfer_line_id PK, transfer_id FK→STOCK_TRANSFER, item_id FK→ITEM,
|
||||
src_bin_id FK→BIN, dest_bin_id FK→BIN, batch_id FK→BATCH, serial_id FK→SERIAL, qty)
|
||||
STOCK_ADJUSTMENT(adjustment_id PK, doc_no, warehouse_id FK→WAREHOUSE,
|
||||
reason_code_id FK→REASON_CODE, created_by FK→USER, created_at)
|
||||
STOCK_ADJUSTMENT_LINE(adj_line_id PK, adjustment_id FK→STOCK_ADJUSTMENT, item_id FK→ITEM,
|
||||
bin_id FK→BIN, batch_id FK→BATCH, serial_id FK→SERIAL, qty_delta)
|
||||
STOCK_COUNT(count_id PK, doc_no, warehouse_id FK→WAREHOUSE, count_type, status, created_by FK→USER)
|
||||
STOCK_COUNT_LINE(count_line_id PK, count_id FK→STOCK_COUNT, item_id FK→ITEM, bin_id FK→BIN,
|
||||
system_qty, counted_qty, variance)
|
||||
```
|
||||
|
||||
## C.7 Cross-cutting
|
||||
```
|
||||
USER(user_id PK, username, display_name, status)
|
||||
REASON_CODE(reason_code_id PK, code, description, context)
|
||||
NUMBER_SEQUENCE(sequence_id PK, doc_type, year, last_number)
|
||||
AUDIT_LOG(audit_id PK, user_id FK→USER, entity_type, entity_id, action, change_set, created_at)
|
||||
JOURNAL_ENTRY_STUB(journal_id PK, source_doc_type, source_doc_id, debit_account, credit_account, amount)
|
||||
```
|
||||
|
||||
## C.8 Reserved (RBAC — deferred, schema placeholder only)
|
||||
```
|
||||
ROLE(role_id PK, name)
|
||||
PERMISSION(permission_id PK, code)
|
||||
USER_ROLE(user_id FK→USER, role_id FK→ROLE)
|
||||
ROLE_PERMISSION(role_id FK→ROLE, permission_id FK→PERMISSION)
|
||||
```
|
||||
|
||||
## C.9 Modeling notes (load-bearing)
|
||||
- **FIFO = two structures.** `STOCK_LAYER` answers valuation ("what's on hand and at what cost"); `STOCK_LEDGER` answers history ("what moved, when, by whom"). Layers are keyed per item **per warehouse**.
|
||||
- **Polymorphic source.** `STOCK_LEDGER.source_doc_type/source_doc_id` (and `AUDIT_LOG`, `JOURNAL_ENTRY_STUB`) reference the originating document without a hard FK per type — new transaction types (Sales, Manufacturing) write to the ledger without a schema change.
|
||||
- **In-transit + cost-preserving transfer.** `STOCK_TRANSFER` holds `src`/`dest` warehouse; dispatch consumes source layers into in-transit, receive creates the destination layer at the **inherited** source cost.
|
||||
- **FEFO ≠ FIFO.** FIFO governs *costing*; FEFO governs *physical picking* of perishables via `BATCH.expiry_date`.
|
||||
- **Reserved RBAC.** Role/Permission/UserRole/RolePermission exist for schema-completeness only; only `USER` is live (audit stamp).
|
||||
- **Reorder alerts are a query**, not an entity — computed from `ITEM_REORDER` vs available. Add a table only if alert history is required.
|
||||
|
||||
## C.10 Entity → implementation mapping
|
||||
- Entities → `Domain/Entities`; enums (`ItemType`, `TrackingMode`, `HoldStatus`, `Direction`, `*Status`, `CountType`) → `Domain/Enums`.
|
||||
- EF configurations (`IEntityTypeConfiguration<T>`, one per entity) → `Infra/Persistence/Configurations`.
|
||||
- FIFO logic → `Services/Stock/FifoCostingService` (Part A.2). Ledger writes only via stock services inside the UoW transaction.
|
||||
- `RowVersion` (concurrency) on mutable aggregates: Item, Vendor, PurchaseOrder, GRN, transfers/adjustments/counts headers.
|
||||
|
||||
---
|
||||
|
||||
*End of 10-BACKEND-PHASE1.md. API contracts: `11-BACKEND-PHASE1.md`. Record work: `Backend/PROGRESS.md`.*
|
||||
@@ -0,0 +1,480 @@
|
||||
# 11 · BACKEND — Phase 1 API Reference (Inventory & Supply Chain)
|
||||
|
||||
> **Authoritative for:** the REST API contract — every endpoint with complete request/response bodies, the error catalog, and enums.
|
||||
> **Navigation:** you arrived from `00-CORE.md`. Business rules, architecture, and the entity model are in `10-BACKEND-PHASE1.md`. Frontend consumers follow `20-FRONTEND.md`. Record work in `Backend/PROGRESS.md`.
|
||||
> **Consistency:** field names match the entity model in `10-BACKEND-PHASE1.md Part C`. This document should match the Swashbuckle-generated OpenAPI; an OpenAPI 3.1 YAML can be produced from it.
|
||||
|
||||
---
|
||||
|
||||
## 1. Conventions
|
||||
|
||||
### 1.1 Base URL & versioning
|
||||
```
|
||||
https://{host}/api/v1
|
||||
```
|
||||
Path-based versioning. Breaking changes bump the major version.
|
||||
|
||||
### 1.2 Authentication & authorization
|
||||
```
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
- Every endpoint requires a valid **Bearer JWT**; unauthenticated → `401`.
|
||||
- **RBAC is NOT enforced in Phase 1** (FR-X-01): any authenticated user may call any endpoint.
|
||||
- The token subject (`sub`) is the **audit actor** on every mutation. Clients never send `createdBy`; the server derives it.
|
||||
- Tokens are issued by a simple in-app login (`/auth/login`, §2.0).
|
||||
|
||||
### 1.3 Content type & encoding
|
||||
`application/json`, UTF-8, **camelCase**. Timestamps ISO 8601 UTC (`2026-07-07T09:30:00Z`); dates `YYYY-MM-DD`. Base currency **LKR** in Phase 1.
|
||||
|
||||
### 1.4 List envelope
|
||||
```json
|
||||
{ "items": [ /* ... */ ],
|
||||
"pagination": { "page": 1, "pageSize": 20, "totalItems": 137, "totalPages": 7 } }
|
||||
```
|
||||
|
||||
### 1.5 Pagination / filtering / sorting
|
||||
`page` (1-based, default 1) · `pageSize` (default 20, max 200) · `sort` (`name` / `-createdAt`) · `q` (free text) · resource filters documented per endpoint.
|
||||
|
||||
### 1.6 Concurrency & idempotency
|
||||
Mutable resources expose `ETag` (EF `RowVersion`); `PUT`/`PATCH` send `If-Match` → `412` on mismatch. Transactional POSTs (GRN confirm, transfer dispatch/receive, adjustment) accept an optional `Idempotency-Key` header.
|
||||
|
||||
### 1.7 Status codes
|
||||
`200` read/update · `201` created (+`Location`) · `204` no content · `400` validation · `401` unauth · `404` not found · `409` domain conflict · `412` ETag mismatch · `422` semantically invalid.
|
||||
|
||||
### 1.8 Error format (RFC 7807)
|
||||
```json
|
||||
{ "type": "https://errors.erp.local/validation",
|
||||
"title": "One or more validation errors occurred.",
|
||||
"status": 400, "traceId": "00-6f1c...-01",
|
||||
"errors": { "sku": ["The sku field is required."], "lines": ["At least one line is required."] } }
|
||||
```
|
||||
Domain errors add a stable `code` (catalog §7):
|
||||
```json
|
||||
{ "type": "https://errors.erp.local/insufficient-stock",
|
||||
"title": "Insufficient stock to fulfil the issue.",
|
||||
"status": 409, "code": "STOCK_NEGATIVE_BLOCKED",
|
||||
"detail": "Available 4 < requested 10 for item ITM-1001 at WH-MAIN.", "traceId": "00-9a2f...-01" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Master Data
|
||||
|
||||
### 2.0 Auth
|
||||
#### `POST /auth/login`
|
||||
```json
|
||||
{ "username": "storekeeper01", "password": "••••••••" }
|
||||
```
|
||||
**200 OK**
|
||||
```json
|
||||
{ "accessToken": "eyJhbGciOi...", "tokenType": "Bearer", "expiresInMinutes": 120, "userId": 17, "displayName": "Nimal Perera" }
|
||||
```
|
||||
`401` on bad credentials.
|
||||
|
||||
### 2.1 Items
|
||||
#### `GET /items`
|
||||
Query: `q`, `status` (`Active|Inactive`), `categoryId`, `trackingMode` (`None|Batch|Serial`), + paging.
|
||||
**200 OK**
|
||||
```json
|
||||
{ "items": [ { "itemId": 1001, "sku": "ITM-1001", "name": "Steel Bolt M8x40",
|
||||
"categoryId": 12, "baseUomId": 1, "defaultVendorId": 5,
|
||||
"itemType": "Stocked", "trackingMode": "Batch", "taxClass": "STD", "status": "Active" } ],
|
||||
"pagination": { "page": 1, "pageSize": 20, "totalItems": 1, "totalPages": 1 } }
|
||||
```
|
||||
|
||||
#### `GET /items/{itemId}` → **200 OK** (header `ETag: "AAAAAAAAB9E="`)
|
||||
```json
|
||||
{ "itemId": 1001, "sku": "ITM-1001", "name": "Steel Bolt M8x40",
|
||||
"description": "Grade 8.8 zinc-plated hex bolt", "categoryId": 12, "baseUomId": 1,
|
||||
"defaultVendorId": 5, "itemType": "Stocked", "trackingMode": "Batch", "taxClass": "STD",
|
||||
"status": "Active", "reorder": [ { "warehouseId": 1, "reorderPoint": 500, "reorderQty": 2000 } ],
|
||||
"createdAt": "2026-06-01T08:00:00Z", "updatedAt": "2026-07-01T10:15:00Z" }
|
||||
```
|
||||
|
||||
#### `POST /items`
|
||||
```json
|
||||
{ "sku": "ITM-1002", "name": "Steel Nut M8", "description": "Grade 8 zinc-plated hex nut",
|
||||
"categoryId": 12, "baseUomId": 1, "defaultVendorId": 5,
|
||||
"itemType": "Stocked", "trackingMode": "None", "taxClass": "STD" }
|
||||
```
|
||||
**201 Created** — `Location: /api/v1/items/1002`
|
||||
```json
|
||||
{ "itemId": 1002, "sku": "ITM-1002", "name": "Steel Nut M8", "categoryId": 12, "baseUomId": 1,
|
||||
"defaultVendorId": 5, "itemType": "Stocked", "trackingMode": "None", "taxClass": "STD",
|
||||
"status": "Active", "createdAt": "2026-07-07T09:30:00Z" }
|
||||
```
|
||||
`400` → `code: SKU_DUPLICATE` if SKU exists.
|
||||
|
||||
#### `PUT /items/{itemId}`
|
||||
Full update; requires `If-Match`. → **200 OK** updated resource; `412` on ETag mismatch.
|
||||
|
||||
#### `PATCH /items/{itemId}/status`
|
||||
```json
|
||||
{ "status": "Inactive" }
|
||||
```
|
||||
**204 No Content**. Masters are deactivated, not hard-deleted (FR-MD-08); hard `DELETE` of a referenced master → `409 MASTER_IN_USE`.
|
||||
|
||||
#### `PUT /items/{itemId}/reorder`
|
||||
```json
|
||||
{ "settings": [ { "warehouseId": 1, "reorderPoint": 500, "reorderQty": 2000 },
|
||||
{ "warehouseId": 2, "reorderPoint": 100, "reorderQty": 400 } ] }
|
||||
```
|
||||
**200 OK** → persisted settings array.
|
||||
|
||||
### 2.2 Units of Measure
|
||||
#### `GET /uoms` · `POST /uoms`
|
||||
```json
|
||||
{ "name": "Box-12" }
|
||||
```
|
||||
**201 Created** → `{ "uomId": 7, "name": "Box-12" }`
|
||||
|
||||
#### `PUT /items/{itemId}/uom-conversions`
|
||||
```json
|
||||
{ "conversions": [ { "fromUom": 7, "toUom": 1, "factor": 12 } ] }
|
||||
```
|
||||
**200 OK**
|
||||
```json
|
||||
{ "itemId": 1001, "baseUomId": 1,
|
||||
"conversions": [ { "conversionId": 33, "fromUom": 7, "toUom": 1, "factor": 12 } ] }
|
||||
```
|
||||
|
||||
### 2.3 Categories
|
||||
#### `POST /categories`
|
||||
```json
|
||||
{ "name": "Fasteners", "parentId": 3 }
|
||||
```
|
||||
**201 Created** → `{ "categoryId": 12, "name": "Fasteners", "parentId": 3 }`
|
||||
`GET /categories?tree=true` returns a nested tree.
|
||||
|
||||
### 2.4 Vendors
|
||||
#### `POST /vendors`
|
||||
```json
|
||||
{ "code": "VN-005", "name": "Lanka Steel Traders (Pvt) Ltd", "terms": "NET30",
|
||||
"taxReg": "134567890-7000", "currency": "LKR" }
|
||||
```
|
||||
**201 Created**
|
||||
```json
|
||||
{ "vendorId": 5, "code": "VN-005", "name": "Lanka Steel Traders (Pvt) Ltd", "terms": "NET30",
|
||||
"taxReg": "134567890-7000", "currency": "LKR", "status": "Active", "createdAt": "2026-07-07T09:31:00Z" }
|
||||
```
|
||||
`GET /vendors`, `GET /vendors/{id}`, `PUT /vendors/{id}`, `PATCH /vendors/{id}/status` follow the Item pattern.
|
||||
|
||||
### 2.5 Warehouses & Bins
|
||||
#### `POST /warehouses`
|
||||
```json
|
||||
{ "code": "WH-MAIN", "name": "Main Warehouse - Negombo" }
|
||||
```
|
||||
**201 Created** → `{ "warehouseId": 1, "code": "WH-MAIN", "name": "Main Warehouse - Negombo" }`
|
||||
|
||||
#### `POST /warehouses/{warehouseId}/bins`
|
||||
```json
|
||||
{ "code": "A-01-01", "binType": "Shelf" }
|
||||
```
|
||||
**201 Created** → `{ "binId": 45, "warehouseId": 1, "code": "A-01-01", "binType": "Shelf" }`
|
||||
`GET /warehouses/{warehouseId}/bins` lists bins.
|
||||
|
||||
---
|
||||
|
||||
## 3. Procurement
|
||||
|
||||
### 3.1 Requisitions
|
||||
#### `POST /requisitions`
|
||||
```json
|
||||
{ "lines": [ { "itemId": 1001, "qty": 5000, "requiredBy": "2026-07-20" },
|
||||
{ "itemId": 1002, "qty": 8000, "requiredBy": "2026-07-20" } ] }
|
||||
```
|
||||
**201 Created** (`requestedBy` from token)
|
||||
```json
|
||||
{ "requisitionId": 210, "docNo": "PR-2026-00210", "status": "Draft", "requestedBy": 17,
|
||||
"createdAt": "2026-07-07T09:35:00Z",
|
||||
"lines": [ { "reqLineId": 501, "itemId": 1001, "qty": 5000, "requiredBy": "2026-07-20" },
|
||||
{ "reqLineId": 502, "itemId": 1002, "qty": 8000, "requiredBy": "2026-07-20" } ] }
|
||||
```
|
||||
`POST /requisitions/{id}/submit` → **200 OK** `status: "Submitted"`.
|
||||
|
||||
### 3.2 RFQs & Quotations
|
||||
#### `POST /rfqs`
|
||||
```json
|
||||
{ "requisitionId": 210, "vendorIds": [5, 8, 11],
|
||||
"lines": [ { "itemId": 1001, "qty": 5000 }, { "itemId": 1002, "qty": 8000 } ] }
|
||||
```
|
||||
**201 Created**
|
||||
```json
|
||||
{ "rfqId": 88, "docNo": "RFQ-2026-00088", "requisitionId": 210, "status": "Open",
|
||||
"lines": [ { "rfqLineId": 701, "itemId": 1001, "qty": 5000 },
|
||||
{ "rfqLineId": 702, "itemId": 1002, "qty": 8000 } ] }
|
||||
```
|
||||
|
||||
#### `POST /rfqs/{rfqId}/quotations`
|
||||
```json
|
||||
{ "vendorId": 5, "lines": [ { "itemId": 1001, "unitPrice": 12.50, "leadDays": 7 },
|
||||
{ "itemId": 1002, "unitPrice": 6.20, "leadDays": 7 } ] }
|
||||
```
|
||||
**201 Created**
|
||||
```json
|
||||
{ "quotationId": 140, "rfqId": 88, "vendorId": 5,
|
||||
"lines": [ { "itemId": 1001, "unitPrice": 12.50, "leadDays": 7 },
|
||||
{ "itemId": 1002, "unitPrice": 6.20, "leadDays": 7 } ] }
|
||||
```
|
||||
`GET /rfqs/{rfqId}/comparison` → vendor-by-line price matrix.
|
||||
|
||||
### 3.3 Purchase Orders
|
||||
> **Phase 1:** `approvalRequired` defaults `false` → PO **auto-approved on creation**. Approve endpoint exists but is a no-op unless enabled (FR-PROC-04). PO **freely editable while open** (Option B, FR-PROC-05).
|
||||
|
||||
#### `POST /purchase-orders`
|
||||
```json
|
||||
{ "vendorId": 5, "requisitionId": 210,
|
||||
"lines": [ { "itemId": 1001, "uomId": 1, "warehouseId": 1, "qty": 5000, "unitPrice": 12.50, "tax": 0.18 },
|
||||
{ "itemId": 1002, "uomId": 1, "warehouseId": 1, "qty": 8000, "unitPrice": 6.20, "tax": 0.18 } ] }
|
||||
```
|
||||
**201 Created** — `Location: /api/v1/purchase-orders/342`
|
||||
```json
|
||||
{ "poId": 342, "docNo": "PO-2026-00342", "vendorId": 5, "requisitionId": 210,
|
||||
"status": "Approved", "approvalRequired": false, "createdBy": 17, "createdAt": "2026-07-07T09:40:00Z",
|
||||
"totals": { "subTotal": 112100.00, "tax": 20178.00, "grandTotal": 132278.00, "currency": "LKR" },
|
||||
"lines": [ { "poLineId": 900, "itemId": 1001, "uomId": 1, "warehouseId": 1, "qty": 5000, "unitPrice": 12.50, "tax": 0.18, "qtyReceived": 0 },
|
||||
{ "poLineId": 901, "itemId": 1002, "uomId": 1, "warehouseId": 1, "qty": 8000, "unitPrice": 6.20, "tax": 0.18, "qtyReceived": 0 } ] }
|
||||
```
|
||||
|
||||
`GET /purchase-orders?status=Approved&vendorId=5` → list envelope of PO summaries.
|
||||
|
||||
#### `PUT /purchase-orders/{poId}`
|
||||
Edit while open (not FullyReceived/Closed/Cancelled); requires `If-Match`. → **200 OK** updated resource; `409 PO_NOT_EDITABLE` if closed.
|
||||
|
||||
#### `POST /purchase-orders/{poId}/approve` → **200 OK** (no-op in Phase 1; transitions PendingApproval→Approved when enabled).
|
||||
|
||||
#### `POST /purchase-orders/{poId}/cancel`
|
||||
```json
|
||||
{ "reason": "Duplicate order" }
|
||||
```
|
||||
**200 OK** `status: "Cancelled"`; `409` if any receipt exists.
|
||||
|
||||
### 3.4 Purchase Returns
|
||||
#### `POST /purchase-returns`
|
||||
```json
|
||||
{ "vendorId": 5, "warehouseId": 1, "reasonCodeId": 22,
|
||||
"lines": [ { "grnLineId": 1300, "itemId": 1001, "qty": 200 } ] }
|
||||
```
|
||||
**201 Created**
|
||||
```json
|
||||
{ "returnId": 61, "docNo": "PRET-2026-00061", "vendorId": 5, "warehouseId": 1, "reasonCodeId": 22,
|
||||
"status": "Posted", "createdBy": 17,
|
||||
"lines": [ { "returnLineId": 120, "grnLineId": 1300, "itemId": 1001, "qty": 200 } ],
|
||||
"ledgerRefs": [ 55021 ] }
|
||||
```
|
||||
`409 STOCK_NEGATIVE_BLOCKED` if return qty exceeds available.
|
||||
|
||||
---
|
||||
|
||||
## 4. Goods Receipt (GRN)
|
||||
> On **confirm**, each line creates a **FIFO cost layer** and posts an **inbound ledger** entry (FR-GRN-06). Goods may land `holdStatus: "OnHold"` (not issuable) until released.
|
||||
|
||||
### 4.1 `POST /grns`
|
||||
Against a PO (lines default from open PO lines) or direct (`poId: null`, by permission).
|
||||
```json
|
||||
{ "poId": 342, "warehouseId": 1,
|
||||
"lines": [ { "poLineId": 900, "itemId": 1001, "uomId": 1, "binId": 45, "qty": 5000,
|
||||
"unitCost": 12.50, "holdStatus": "OnHold",
|
||||
"batch": { "batchNo": "B-2607", "expiryDate": "2028-07-01" } } ] }
|
||||
```
|
||||
**201 Created** — status `Draft`
|
||||
```json
|
||||
{ "grnId": 780, "docNo": "GRN-2026-00780", "poId": 342, "vendorId": 5, "warehouseId": 1,
|
||||
"status": "Draft", "createdBy": 17,
|
||||
"lines": [ { "grnLineId": 1300, "poLineId": 900, "itemId": 1001, "uomId": 1, "binId": 45,
|
||||
"qty": 5000, "unitCost": 12.50, "receivedValue": 62500.00, "holdStatus": "OnHold", "batchId": 410 } ] }
|
||||
```
|
||||
`422 OVER_RECEIPT_TOLERANCE` if qty exceeds open PO qty beyond tolerance.
|
||||
|
||||
### 4.2 `POST /grns/{grnId}/confirm`
|
||||
Header: optional `Idempotency-Key`. **200 OK** — creates FIFO layers + ledger; updates PO line `qtyReceived`.
|
||||
```json
|
||||
{ "grnId": 780, "status": "Confirmed", "postedAt": "2026-07-07T10:05:00Z",
|
||||
"createdLayers": [ { "layerId": 9001, "itemId": 1001, "warehouseId": 1, "batchId": 410,
|
||||
"qtyReceived": 5000, "qtyRemaining": 5000, "unitCost": 12.50, "receiptDate": "2026-07-07T10:05:00Z" } ],
|
||||
"ledgerRefs": [ 55010 ], "poStatus": "Fully Received" }
|
||||
```
|
||||
Received `OnHold` → the layer is **not** available until released.
|
||||
|
||||
### 4.3 `POST /grns/{grnId}/lines/{grnLineId}/release`
|
||||
```json
|
||||
{ "action": "Release" }
|
||||
```
|
||||
**200 OK** → `{ "grnLineId": 1300, "holdStatus": "Available" }`
|
||||
`action: "Reject"` moves the quantity to a return workflow instead.
|
||||
|
||||
---
|
||||
|
||||
## 5. Stock Management
|
||||
|
||||
### 5.1 `GET /stock/on-hand?itemId=1001&warehouseId=1`
|
||||
```json
|
||||
{ "itemId": 1001, "warehouseId": 1, "onHand": 5000, "available": 0, "onHold": 5000,
|
||||
"inTransit": 0, "reserved": 0, "asOf": "2026-07-07T10:06:00Z" }
|
||||
```
|
||||
`available = onHand − onHold − reserved − inTransit(out)`. `reserved` always `0` in Phase 1 (stub, FR-STK-11).
|
||||
|
||||
### 5.2 `GET /stock/ledger?itemId=1001&warehouseId=1&from=2026-07-01&to=2026-07-07`
|
||||
```json
|
||||
{ "items": [ { "ledgerId": 55010, "itemId": 1001, "warehouseId": 1, "binId": 45, "batchId": 410,
|
||||
"serialId": null, "direction": "In", "qtyBase": 5000, "unitCost": 12.50, "value": 62500.00,
|
||||
"runningBalance": 5000, "sourceDocType": "GRN", "sourceDocId": 780, "userId": 17,
|
||||
"createdAt": "2026-07-07T10:05:00Z" } ],
|
||||
"pagination": { "page": 1, "pageSize": 20, "totalItems": 1, "totalPages": 1 } }
|
||||
```
|
||||
|
||||
### 5.3 `GET /stock/valuation?itemId=1001&warehouseId=1`
|
||||
```json
|
||||
{ "itemId": 1001, "warehouseId": 1,
|
||||
"layers": [ { "layerId": 9001, "qtyRemaining": 5000, "unitCost": 12.50, "value": 62500.00,
|
||||
"receiptDate": "2026-07-07T10:05:00Z" } ],
|
||||
"totalQty": 5000, "totalValue": 62500.00, "currency": "LKR", "costingMethod": "FIFO" }
|
||||
```
|
||||
|
||||
### 5.4 Transfers (in-transit)
|
||||
> create → dispatch → receive. Dispatch consumes source FIFO layers into in-transit; receive creates the destination layer at inherited cost (cost-preserving, FR-STK-06).
|
||||
|
||||
#### `POST /stock-transfers`
|
||||
```json
|
||||
{ "srcWarehouseId": 1, "destWarehouseId": 2,
|
||||
"lines": [ { "itemId": 1001, "srcBinId": 45, "destBinId": 90, "batchId": 410, "qty": 1000 } ] }
|
||||
```
|
||||
**201 Created**
|
||||
```json
|
||||
{ "transferId": 55, "docNo": "TRF-2026-00055", "srcWarehouseId": 1, "destWarehouseId": 2,
|
||||
"status": "Draft",
|
||||
"lines": [ { "transferLineId": 300, "itemId": 1001, "srcBinId": 45, "destBinId": 90, "batchId": 410, "qty": 1000 } ] }
|
||||
```
|
||||
|
||||
#### `POST /stock-transfers/{id}/dispatch` → **200 OK** (status `InTransit`)
|
||||
```json
|
||||
{ "transferId": 55, "status": "InTransit",
|
||||
"consumedLayers": [ { "layerId": 9001, "qtyConsumed": 1000, "unitCost": 12.50 } ],
|
||||
"ledgerRefs": [ 55033 ] }
|
||||
```
|
||||
`409 STOCK_NEGATIVE_BLOCKED` if source available < requested.
|
||||
|
||||
#### `POST /stock-transfers/{id}/receive`
|
||||
```json
|
||||
{ "lines": [ { "transferLineId": 300, "qty": 1000 } ] }
|
||||
```
|
||||
**200 OK** (status `Received`)
|
||||
```json
|
||||
{ "transferId": 55, "status": "Received",
|
||||
"createdLayers": [ { "layerId": 9040, "warehouseId": 2, "qtyReceived": 1000, "unitCost": 12.50 } ],
|
||||
"ledgerRefs": [ 55034 ] }
|
||||
```
|
||||
|
||||
### 5.5 Adjustments (auto-post)
|
||||
> Reason code mandatory. Decrease consumes FIFO layers; increase creates a layer at supplied/last cost (FR-STK-07).
|
||||
|
||||
#### `POST /stock-adjustments`
|
||||
```json
|
||||
{ "warehouseId": 1, "reasonCodeId": 4,
|
||||
"lines": [ { "itemId": 1001, "binId": 45, "batchId": 410, "qtyDelta": -15 } ] }
|
||||
```
|
||||
**201 Created**
|
||||
```json
|
||||
{ "adjustmentId": 77, "docNo": "ADJ-2026-00077", "warehouseId": 1, "reasonCodeId": 4,
|
||||
"status": "Posted", "createdBy": 17, "createdAt": "2026-07-07T10:20:00Z",
|
||||
"lines": [ { "adjLineId": 210, "itemId": 1001, "binId": 45, "batchId": 410, "qtyDelta": -15 } ],
|
||||
"ledgerRefs": [ 55050 ] }
|
||||
```
|
||||
`400 REASON_CODE_REQUIRED` if `reasonCodeId` omitted.
|
||||
|
||||
### 5.6 Counts
|
||||
#### `POST /stock-counts`
|
||||
```json
|
||||
{ "warehouseId": 1, "countType": "Cycle", "itemIds": [1001, 1002] }
|
||||
```
|
||||
**201 Created** — status `Draft`, system quantities snapshotted
|
||||
```json
|
||||
{ "countId": 30, "docNo": "CNT-2026-00030", "warehouseId": 1, "countType": "Cycle", "status": "Draft",
|
||||
"lines": [ { "countLineId": 400, "itemId": 1001, "binId": 45, "systemQty": 4985, "countedQty": null, "variance": null } ] }
|
||||
```
|
||||
|
||||
#### `PUT /stock-counts/{id}/counts`
|
||||
```json
|
||||
{ "lines": [ { "countLineId": 400, "countedQty": 4980 } ] }
|
||||
```
|
||||
**200 OK** → `{ "lines": [ { "countLineId": 400, "systemQty": 4985, "countedQty": 4980, "variance": -5 } ] }`
|
||||
|
||||
#### `POST /stock-counts/{id}/post` → **200 OK** (posts variance adjustment, closes count)
|
||||
```json
|
||||
{ "countId": 30, "status": "Posted", "adjustmentId": 78, "ledgerRefs": [ 55060 ] }
|
||||
```
|
||||
|
||||
### 5.7 Reorder alerts
|
||||
#### `GET /stock/reorder-alerts?warehouseId=1`
|
||||
Items at/below ROP (FR-STK-10); computed on read, no stored entity.
|
||||
```json
|
||||
{ "items": [ { "itemId": 1002, "warehouseId": 1, "available": 90, "reorderPoint": 100,
|
||||
"reorderQty": 400, "suggestedRequisitionQty": 400 } ],
|
||||
"pagination": { "page": 1, "pageSize": 20, "totalItems": 1, "totalPages": 1 } }
|
||||
```
|
||||
`POST /stock/reorder-alerts/{itemId}/requisition?warehouseId=1` → creates a draft requisition for the suggested qty.
|
||||
|
||||
---
|
||||
|
||||
## 6. Reference Data
|
||||
#### `GET /reason-codes?context=Adjustment`
|
||||
```json
|
||||
{ "items": [ { "reasonCodeId": 4, "code": "DMG", "description": "Damage", "context": "Adjustment" },
|
||||
{ "reasonCodeId": 22, "code": "QREJ", "description": "Quality Reject", "context": "Return" } ],
|
||||
"pagination": { "page": 1, "pageSize": 20, "totalItems": 2, "totalPages": 1 } }
|
||||
```
|
||||
`POST /reason-codes` (admin). Number sequences are server-managed; no write API in Phase 1.
|
||||
|
||||
---
|
||||
|
||||
## 7. Domain Error Catalog
|
||||
| `code` | HTTP | When |
|
||||
|---|---|---|
|
||||
| `SKU_DUPLICATE` | 400 | Item SKU already exists. |
|
||||
| `MASTER_IN_USE` | 409 | Hard delete of a referenced master (use deactivate). |
|
||||
| `PO_NOT_EDITABLE` | 409 | Editing a PO that is fully received / closed / cancelled. |
|
||||
| `OVER_RECEIPT_TOLERANCE` | 422 | GRN qty exceeds PO open qty beyond tolerance. |
|
||||
| `STOCK_NEGATIVE_BLOCKED` | 409 | Issue/transfer/adjustment would drive available stock negative. |
|
||||
| `EXPIRED_BATCH_BLOCKED` | 409 | Issue/pick of an expired batch. |
|
||||
| `ONHOLD_NOT_ISSUABLE` | 409 | Issue against on-hold/quarantined stock. |
|
||||
| `REASON_CODE_REQUIRED` | 400 | Adjustment/return without a reason code. |
|
||||
| `CONCURRENCY_CONFLICT` | 412 | ETag / RowVersion mismatch. |
|
||||
| `IDEMPOTENCY_REPLAY` | 200 | Duplicate `Idempotency-Key`; original result returned. |
|
||||
|
||||
Example (`409`, `application/problem+json`):
|
||||
```json
|
||||
{ "type": "https://errors.erp.local/onhold-not-issuable",
|
||||
"title": "Stock is on inspection hold and cannot be issued.",
|
||||
"status": 409, "code": "ONHOLD_NOT_ISSUABLE",
|
||||
"detail": "5000 units of ITM-1001 at WH-MAIN are OnHold; release via GRN inspection before issue.",
|
||||
"traceId": "00-1b7c...-01" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Enumerations
|
||||
| Enum | Values |
|
||||
|---|---|
|
||||
| `itemType` | `Stocked`, `NonStocked`, `Service` |
|
||||
| `trackingMode` | `None`, `Batch`, `Serial` |
|
||||
| `holdStatus` | `Available`, `OnHold`, `Rejected` |
|
||||
| `direction` (ledger) | `In`, `Out` |
|
||||
| PO `status` | `Draft`, `PendingApproval`, `Approved`, `PartiallyReceived`, `FullyReceived`, `Closed`, `Cancelled` |
|
||||
| GRN `status` | `Draft`, `Confirmed`, `Closed` |
|
||||
| Transfer `status` | `Draft`, `InTransit`, `Received`, `Closed` |
|
||||
| Count `status` | `Draft`, `Counted`, `Posted` |
|
||||
| `countType` | `Cycle`, `Full` |
|
||||
|
||||
---
|
||||
|
||||
## 9. Implementation notes (ASP.NET Core)
|
||||
- Serve via **Swashbuckle**; annotate controllers with `[ProducesResponseType]` per status so generated OpenAPI matches this document.
|
||||
- Use **`ProblemDetails` / `ValidationProblemDetails`** for all errors (§1.8) — framework default.
|
||||
- Map `ETag`/`If-Match` to EF `[Timestamp] byte[] RowVersion`.
|
||||
- Wrap every stock-affecting operation (GRN confirm, transfer dispatch/receive, adjustment, return, count post) in **one** UoW transaction; FIFO layer consumption locks affected layer rows (NFR-02). See `10-BACKEND-PHASE1.md Part A`.
|
||||
- Derive audit actor from `User.FindFirst("sub")`, never from the body.
|
||||
- Deferred (Phase 2+): vendor invoice + 3-way match, reservation/allocation, RBAC policy attributes — all additive, no breaking change to these routes.
|
||||
|
||||
---
|
||||
|
||||
*End of 11-BACKEND-PHASE1.md. Model & rules: `10-BACKEND-PHASE1.md`. Record work: `Backend/PROGRESS.md`.*
|
||||
Reference in New Issue
Block a user