From 438ab2cdbda028460fb58988b086d545be618017 Mon Sep 17 00:00:00 2001 From: ImanThiyanga Date: Thu, 9 Jul 2026 10:02:08 +0530 Subject: [PATCH] first commit --- .gitignore | 31 ++ Backend/ERPCore.sln | 34 ++ Backend/ERPCore/Common/Models/PagedResult.cs | 24 + Backend/ERPCore/Common/Models/Result.cs | 26 + Backend/ERPCore/Controllers/MetaController.cs | 21 + Backend/ERPCore/ERPCore.csproj | 33 ++ Backend/ERPCore/Infra/Auth/CurrentUser.cs | 31 ++ Backend/ERPCore/Infra/Auth/ICurrentUser.cs | 15 + .../ERPCore/Infra/Auth/JwtAuthExtensions.cs | 39 ++ .../ERPCore/Infra/Persistence/ErpDbContext.cs | 25 + Backend/ERPCore/Infra/UoW/IUnitOfWork.cs | 19 + Backend/ERPCore/Infra/UoW/UnitOfWork.cs | 47 ++ Backend/ERPCore/Program.cs | 61 +++ .../ERPCore/Properties/launchSettings.json | 23 + .../Repositories/Interfaces/IRepository.cs | 19 + Backend/ERPCore/Repositories/Repository.cs | 37 ++ .../ERPCore/System/Errors/DomainException.cs | 32 ++ .../System/Errors/DomainExceptionHandler.cs | 40 ++ Backend/ERPCore/System/Errors/ErrorCodes.cs | 14 + Backend/ERPCore/appsettings.json | 18 + Backend/PROGRESS.md | 63 +++ Frontend/PROGRESS.md | 55 ++ README.md | 68 +++ docs/00-CORE.md | 366 +++++++++++++ docs/01-DOC-GUIDE.md | 191 +++++++ docs/10-BACKEND-PHASE1.md | 352 +++++++++++++ docs/11-BACKEND-PHASE1.md | 480 ++++++++++++++++++ 27 files changed, 2164 insertions(+) create mode 100644 .gitignore create mode 100644 Backend/ERPCore.sln create mode 100644 Backend/ERPCore/Common/Models/PagedResult.cs create mode 100644 Backend/ERPCore/Common/Models/Result.cs create mode 100644 Backend/ERPCore/Controllers/MetaController.cs create mode 100644 Backend/ERPCore/ERPCore.csproj create mode 100644 Backend/ERPCore/Infra/Auth/CurrentUser.cs create mode 100644 Backend/ERPCore/Infra/Auth/ICurrentUser.cs create mode 100644 Backend/ERPCore/Infra/Auth/JwtAuthExtensions.cs create mode 100644 Backend/ERPCore/Infra/Persistence/ErpDbContext.cs create mode 100644 Backend/ERPCore/Infra/UoW/IUnitOfWork.cs create mode 100644 Backend/ERPCore/Infra/UoW/UnitOfWork.cs create mode 100644 Backend/ERPCore/Program.cs create mode 100644 Backend/ERPCore/Properties/launchSettings.json create mode 100644 Backend/ERPCore/Repositories/Interfaces/IRepository.cs create mode 100644 Backend/ERPCore/Repositories/Repository.cs create mode 100644 Backend/ERPCore/System/Errors/DomainException.cs create mode 100644 Backend/ERPCore/System/Errors/DomainExceptionHandler.cs create mode 100644 Backend/ERPCore/System/Errors/ErrorCodes.cs create mode 100644 Backend/ERPCore/appsettings.json create mode 100644 Backend/PROGRESS.md create mode 100644 Frontend/PROGRESS.md create mode 100644 README.md create mode 100644 docs/00-CORE.md create mode 100644 docs/01-DOC-GUIDE.md create mode 100644 docs/10-BACKEND-PHASE1.md create mode 100644 docs/11-BACKEND-PHASE1.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e4f2245 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/Backend/ERPCore.sln b/Backend/ERPCore.sln new file mode 100644 index 0000000..c779b85 --- /dev/null +++ b/Backend/ERPCore.sln @@ -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 diff --git a/Backend/ERPCore/Common/Models/PagedResult.cs b/Backend/ERPCore/Common/Models/PagedResult.cs new file mode 100644 index 0000000..f8359f6 --- /dev/null +++ b/Backend/ERPCore/Common/Models/PagedResult.cs @@ -0,0 +1,24 @@ +namespace ERPCore.Common.Models; + +/// Pagination envelope metadata returned alongside a page of items. +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; +} + +/// A single page of plus its pagination metadata. +public sealed class PagedResult +{ + public IReadOnlyList Items { get; init; } = Array.Empty(); + public PaginationMeta Meta { get; init; } = new(1, 0, 0); + + public PagedResult() { } + + public PagedResult(IReadOnlyList items, int page, int pageSize, int totalCount) + { + Items = items; + Meta = new PaginationMeta(page, pageSize, totalCount); + } +} diff --git a/Backend/ERPCore/Common/Models/Result.cs b/Backend/ERPCore/Common/Models/Result.cs new file mode 100644 index 0000000..d03cf25 --- /dev/null +++ b/Backend/ERPCore/Common/Models/Result.cs @@ -0,0 +1,26 @@ +namespace ERPCore.Common.Models; + +/// +/// 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). +/// +public readonly struct Result +{ + 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 Success(T value) => new(true, value, null, null); + public static Result Failure(string code, string message) => new(false, default, code, message); +} diff --git a/Backend/ERPCore/Controllers/MetaController.cs b/Backend/ERPCore/Controllers/MetaController.cs new file mode 100644 index 0000000..0c05bb4 --- /dev/null +++ b/Backend/ERPCore/Controllers/MetaController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Mvc; + +namespace ERPCore.Controllers; + +/// +/// Non-domain metadata endpoint. Confirms the controller pipeline, Swagger and +/// routing are wired; domain endpoints are defined per docs/11-BACKEND-PHASE1.md. +/// +[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 + }); +} diff --git a/Backend/ERPCore/ERPCore.csproj b/Backend/ERPCore/ERPCore.csproj new file mode 100644 index 0000000..9bf3c00 --- /dev/null +++ b/Backend/ERPCore/ERPCore.csproj @@ -0,0 +1,33 @@ + + + + net10.0 + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + diff --git a/Backend/ERPCore/Infra/Auth/CurrentUser.cs b/Backend/ERPCore/Infra/Auth/CurrentUser.cs new file mode 100644 index 0000000..780f200 --- /dev/null +++ b/Backend/ERPCore/Infra/Auth/CurrentUser.cs @@ -0,0 +1,31 @@ +using System.Security.Claims; + +namespace ERPCore.Infra.Auth; + +/// +/// Resolves the audit actor from the current HTTP request's principal +/// (token `sub` / NameIdentifier). Falls back to +/// when the request is unauthenticated (e.g. health checks, seeding). +/// +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; + } + } +} diff --git a/Backend/ERPCore/Infra/Auth/ICurrentUser.cs b/Backend/ERPCore/Infra/Auth/ICurrentUser.cs new file mode 100644 index 0000000..ad7860d --- /dev/null +++ b/Backend/ERPCore/Infra/Auth/ICurrentUser.cs @@ -0,0 +1,15 @@ +namespace ERPCore.Infra.Auth; + +/// +/// 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). +/// +public interface ICurrentUser +{ + /// The audit actor identity (token `sub`), or "system" when unauthenticated. + string UserId { get; } + + /// True when the request carries an authenticated principal. + bool IsAuthenticated { get; } +} diff --git a/Backend/ERPCore/Infra/Auth/JwtAuthExtensions.cs b/Backend/ERPCore/Infra/Auth/JwtAuthExtensions.cs new file mode 100644 index 0000000..10ca70f --- /dev/null +++ b/Backend/ERPCore/Infra/Auth/JwtAuthExtensions.cs @@ -0,0 +1,39 @@ +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; + +namespace ERPCore.Infra.Auth; + +/// +/// JWT bearer wiring. Authentication only — RBAC/authorization policies are +/// deferred for Phase 1; the validated principal exists solely so that +/// can stamp the audit actor. +/// +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; + } +} diff --git a/Backend/ERPCore/Infra/Persistence/ErpDbContext.cs b/Backend/ERPCore/Infra/Persistence/ErpDbContext.cs new file mode 100644 index 0000000..e9368c9 --- /dev/null +++ b/Backend/ERPCore/Infra/Persistence/ErpDbContext.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; + +namespace ERPCore.Infra.Persistence; + +/// +/// EF Core context for the ERP database. The 38 Phase 1 entities and their +/// 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. +/// +public class ErpDbContext : DbContext +{ + public ErpDbContext(DbContextOptions 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); + } +} diff --git a/Backend/ERPCore/Infra/UoW/IUnitOfWork.cs b/Backend/ERPCore/Infra/UoW/IUnitOfWork.cs new file mode 100644 index 0000000..5a90e24 --- /dev/null +++ b/Backend/ERPCore/Infra/UoW/IUnitOfWork.cs @@ -0,0 +1,19 @@ +namespace ERPCore.Infra.UoW; + +/// +/// 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. +/// +public interface IUnitOfWork +{ + /// Persist tracked changes without an explicit transaction scope. + Task SaveChangesAsync(CancellationToken ct = default); + + /// Run inside a transaction, saving and committing on success. + Task ExecuteInTransactionAsync(Func action, CancellationToken ct = default); + + /// Transactional variant that returns a value from . + Task ExecuteInTransactionAsync(Func> action, CancellationToken ct = default); +} diff --git a/Backend/ERPCore/Infra/UoW/UnitOfWork.cs b/Backend/ERPCore/Infra/UoW/UnitOfWork.cs new file mode 100644 index 0000000..f6148e1 --- /dev/null +++ b/Backend/ERPCore/Infra/UoW/UnitOfWork.cs @@ -0,0 +1,47 @@ +using ERPCore.Infra.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace ERPCore.Infra.UoW; + +/// +/// EF Core-backed unit of work. Uses the provider execution strategy so the +/// transactional path is safe under connection resiliency / retries. +/// +public sealed class UnitOfWork : IUnitOfWork +{ + private readonly ErpDbContext _db; + + public UnitOfWork(ErpDbContext db) => _db = db; + + public Task SaveChangesAsync(CancellationToken ct = default) => _db.SaveChangesAsync(ct); + + public async Task ExecuteInTransactionAsync(Func action, CancellationToken ct = default) + { + await ExecuteInTransactionAsync(async token => + { + await action(token); + return null; + }, ct); + } + + public async Task ExecuteInTransactionAsync(Func> 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; + } + }); + } +} diff --git a/Backend/ERPCore/Program.cs b/Backend/ERPCore/Program.cs new file mode 100644 index 0000000..054ad8c --- /dev/null +++ b/Backend/ERPCore/Program.cs @@ -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(o => + o.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); + +// ProblemDetails (RFC 7807) + domain-exception mapping +builder.Services.AddProblemDetails(); +builder.Services.AddExceptionHandler(); + +// 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(); + +// Unit of work + generic repository base +builder.Services.AddScoped(); +builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); + +// Health checks (EF Core DB) +builder.Services.AddHealthChecks().AddDbContextCheck(); + +// 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(); diff --git a/Backend/ERPCore/Properties/launchSettings.json b/Backend/ERPCore/Properties/launchSettings.json new file mode 100644 index 0000000..3d53c17 --- /dev/null +++ b/Backend/ERPCore/Properties/launchSettings.json @@ -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" + } + } + } +} diff --git a/Backend/ERPCore/Repositories/Interfaces/IRepository.cs b/Backend/ERPCore/Repositories/Interfaces/IRepository.cs new file mode 100644 index 0000000..cd0d4c0 --- /dev/null +++ b/Backend/ERPCore/Repositories/Interfaces/IRepository.cs @@ -0,0 +1,19 @@ +namespace ERPCore.Repositories.Interfaces; + +/// +/// 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. +/// +public interface IRepository where T : class +{ + Task GetByIdAsync(object id, CancellationToken ct = default); + Task> ListAsync(CancellationToken ct = default); + + /// Composable query root for service-defined filtering/paging. + IQueryable Query(); + + Task AddAsync(T entity, CancellationToken ct = default); + void Update(T entity); + void Remove(T entity); +} diff --git a/Backend/ERPCore/Repositories/Repository.cs b/Backend/ERPCore/Repositories/Repository.cs new file mode 100644 index 0000000..4331bca --- /dev/null +++ b/Backend/ERPCore/Repositories/Repository.cs @@ -0,0 +1,37 @@ +using ERPCore.Infra.Persistence; +using ERPCore.Repositories.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace ERPCore.Repositories; + +/// +/// Default implementation over . +/// Registered open-generically so any entity gets a repository for free; specialise +/// by deriving when an aggregate needs bespoke queries. +/// +public class Repository : IRepository where T : class +{ + protected readonly ErpDbContext Db; + protected readonly DbSet Set; + + public Repository(ErpDbContext db) + { + Db = db; + Set = db.Set(); + } + + public virtual async Task GetByIdAsync(object id, CancellationToken ct = default) + => await Set.FindAsync(new[] { id }, ct); + + public virtual async Task> ListAsync(CancellationToken ct = default) + => await Set.AsNoTracking().ToListAsync(ct); + + public IQueryable 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); +} diff --git a/Backend/ERPCore/System/Errors/DomainException.cs b/Backend/ERPCore/System/Errors/DomainException.cs new file mode 100644 index 0000000..33c24fc --- /dev/null +++ b/Backend/ERPCore/System/Errors/DomainException.cs @@ -0,0 +1,32 @@ +namespace ERPCore.System.Errors; + +/// +/// Base for domain / business-rule violations. Carries a stable +/// (see the error catalog in docs/11-BACKEND-PHASE1.md) and an HTTP +/// ; mapped to RFC 7807 ProblemDetails by +/// . +/// +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; + } +} + +/// 404 — a referenced resource does not exist. +public sealed class NotFoundException : DomainException +{ + public NotFoundException(string message) : base(ErrorCodes.NotFound, message, 404) { } +} + +/// 409 — the request conflicts with current server state. +public sealed class ConflictException : DomainException +{ + public ConflictException(string message) : base(ErrorCodes.Conflict, message, 409) { } +} diff --git a/Backend/ERPCore/System/Errors/DomainExceptionHandler.cs b/Backend/ERPCore/System/Errors/DomainExceptionHandler.cs new file mode 100644 index 0000000..e10e6e1 --- /dev/null +++ b/Backend/ERPCore/System/Errors/DomainExceptionHandler.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Mvc; + +namespace ERPCore.System.Errors; + +/// +/// Translates into an RFC 7807 ProblemDetails +/// response, preserving the stable domain code as an extension. Non-domain +/// exceptions are left unhandled so the framework returns a 500 ProblemDetails. +/// Registered via AddExceptionHandler and invoked by UseExceptionHandler. +/// +public sealed class DomainExceptionHandler : IExceptionHandler +{ + private readonly IProblemDetailsService _problemDetails; + + public DomainExceptionHandler(IProblemDetailsService problemDetails) + => _problemDetails = problemDetails; + + public async ValueTask 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 } + } + }); + } +} diff --git a/Backend/ERPCore/System/Errors/ErrorCodes.cs b/Backend/ERPCore/System/Errors/ErrorCodes.cs new file mode 100644 index 0000000..ce28772 --- /dev/null +++ b/Backend/ERPCore/System/Errors/ErrorCodes.cs @@ -0,0 +1,14 @@ +namespace ERPCore.System.Errors; + +/// +/// Stable domain error codes carried in the code 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. +/// +public static class ErrorCodes +{ + public const string Validation = "validation_error"; + public const string NotFound = "not_found"; + public const string Conflict = "conflict"; +} diff --git a/Backend/ERPCore/appsettings.json b/Backend/ERPCore/appsettings.json new file mode 100644 index 0000000..0b08225 --- /dev/null +++ b/Backend/ERPCore/appsettings.json @@ -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": "*" +} diff --git a/Backend/PROGRESS.md b/Backend/PROGRESS.md new file mode 100644 index 0000000..e215b62 --- /dev/null +++ b/Backend/PROGRESS.md @@ -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 + diff --git a/Frontend/PROGRESS.md b/Frontend/PROGRESS.md new file mode 100644 index 0000000..b408e04 --- /dev/null +++ b/Frontend/PROGRESS.md @@ -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 + diff --git a/README.md b/README.md new file mode 100644 index 0000000..b9d2a2b --- /dev/null +++ b/README.md @@ -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:/swagger` +- Health: `https://localhost:/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: +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. diff --git a/docs/00-CORE.md b/docs/00-CORE.md new file mode 100644 index 0000000..a562a46 --- /dev/null +++ b/docs/00-CORE.md @@ -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, PaginationMeta, Result +├── 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 + + + + net10.0 + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + +``` +> **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(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(); + +// 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:/swagger` +- Health at `https://localhost:/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: +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`.* diff --git a/docs/01-DOC-GUIDE.md b/docs/01-DOC-GUIDE.md new file mode 100644 index 0000000..3ab5da1 --- /dev/null +++ b/docs/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 + +``` + +### 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 + +``` + +--- + +## 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.* diff --git a/docs/10-BACKEND-PHASE1.md b/docs/10-BACKEND-PHASE1.md new file mode 100644 index 0000000..8a1ce63 --- /dev/null +++ b/docs/10-BACKEND-PHASE1.md @@ -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`, `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--`; 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`, 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`.* diff --git a/docs/11-BACKEND-PHASE1.md b/docs/11-BACKEND-PHASE1.md new file mode 100644 index 0000000..240ed7d --- /dev/null +++ b/docs/11-BACKEND-PHASE1.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 +``` +- 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`.*