Compare commits

..

46 Commits

Author SHA1 Message Date
Sasanka 8c3d62223c new updates 2026-09-19 00:22:53 +05:30
ImanThiyanga e1f19fe413 Merge pull request 'Add backup images for login and background removal' (#73) from UI into Dev
Reviewed-on: #73
2026-09-18 18:43:33 +00:00
Naveen cb6b92d221 Add backup images for login and background removal
- Added loginerps.png.bak (1134 KB) as a backup for the login image.
- Added whithoutbackground.png.bak (3455 KB) as a backup for the image without background.
2026-09-19 00:12:41 +05:30
ImanThiyanga 3f10ae3bca changes fix for production 2026-09-19 00:00:42 +05:30
ImanThiyanga 61fe1a0e0c Merge branch 'production' into Dev
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Backend (pull_request) Successful in 31s
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Frontend (pull_request) Failing after 13m12s
2026-09-18 18:23:35 +00:00
ImanThiyanga 941f20d4c5 migrations fix 2026-09-18 23:52:26 +05:30
kalana_Thilakarathna b06d20e649 Merge pull request 'test to yml' (#71) from Dev into production
Reviewed-on: #71
2026-09-18 05:23:55 +00:00
Kalana B. Thilakarathna 445d323bed test to yml
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Backend (pull_request) Failing after 14m24s
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Frontend (pull_request) Failing after 14m59s
2026-09-18 10:52:17 +05:30
kalana_Thilakarathna 9028255800 Merge pull request 'workflowupdate' (#70) from Dev into production
Reviewed-on: #70
2026-09-18 04:51:58 +00:00
Kalana B. Thilakarathna 1b2f244853 workflowupdate
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Backend (pull_request) Failing after 11m21s
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Frontend (pull_request) Failing after 14m6s
2026-09-18 10:21:36 +05:30
kalana_Thilakarathna 475bcfec1a Merge pull request 'pipeline update' (#69) from Dev into production
Reviewed-on: #69
2026-09-18 04:48:49 +00:00
Kalana B. Thilakarathna 85db26ec82 pipeline update
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Backend (pull_request) Failing after 14m30s
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Frontend (pull_request) Failing after 14m58s
2026-09-18 10:18:13 +05:30
kalana_Thilakarathna baee987d26 Merge pull request 'test' (#68) from Dev into production
Reviewed-on: #68
2026-09-17 08:25:31 +00:00
Kalana B. Thilakarathna f45a59f20d test
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Backend (pull_request) Failing after 12m49s
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Frontend (pull_request) Failing after 14m57s
2026-09-17 13:55:06 +05:30
kalana_Thilakarathna 32304cbaff Merge pull request 'user fix at workflow' (#67) from Dev into production
Reviewed-on: #67
2026-09-17 07:35:02 +00:00
Kalana B. Thilakarathna 2b64bb0ebc user fix at workflow
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Backend (pull_request) Failing after 13m17s
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Frontend (pull_request) Has been cancelled
2026-09-17 13:04:15 +05:30
kalana_Thilakarathna ffe5251236 Merge pull request 'workflow fix' (#66) from Dev into production
Reviewed-on: #66
2026-09-17 07:26:14 +00:00
Kalana B. Thilakarathna d04a7fb3bc workflow fix
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Frontend (pull_request) Has been cancelled
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Backend (pull_request) Has been cancelled
2026-09-17 12:55:38 +05:30
kalana_Thilakarathna d76595085e Merge pull request 'workflow fix' (#65) from Dev into production
Reviewed-on: #65
2026-09-17 07:08:22 +00:00
Kalana B. Thilakarathna bad33a8492 workflow fix
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Backend (pull_request) Failing after 13m57s
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Frontend (pull_request) Has been cancelled
2026-09-17 12:38:05 +05:30
kalana_Thilakarathna b0216fca30 Merge pull request 'triger erpo core workflow' (#64) from Dev into production
Reviewed-on: #64
2026-09-17 07:07:24 +00:00
Kalana B. Thilakarathna 49c112d188 triger erpo core workflow 2026-09-17 12:37:04 +05:30
kalana_Thilakarathna c836b57d54 Merge pull request 'gitea work flow update' (#63) from Dev into production
Reviewed-on: #63
2026-09-17 07:06:02 +00:00
Kalana B. Thilakarathna 3a583f80e5 gitea work flow update 2026-09-17 12:28:31 +05:30
ImanThiyanga 83e0335504 Merge pull request 'Dev' (#62) from Dev into production
Reviewed-on: #62
2026-09-17 05:58:25 +00:00
ImanThiyanga 513343b62a Merge pull request 'Revert/ci cd merges' (#61) from revert/ci-cd-merges into Dev
Reviewed-on: #61
2026-09-17 05:57:56 +00:00
ImanThiyanga 256e39ccff Revert "Merge pull request 'Dev' (#58) from Dev into production"
This reverts commit 821e96e011, reversing
changes made to 0ff39e9e58.
2026-09-17 11:25:33 +05:30
ImanThiyanga efb37fce7a Revert "Merge pull request 'Dev' (#60) from Dev into production"
This reverts commit 01fc2582b6, reversing
changes made to 821e96e011.
2026-09-17 11:25:33 +05:30
ImanThiyanga 01fc2582b6 Merge pull request 'Dev' (#60) from Dev into production
Reviewed-on: #60
2026-09-16 18:47:15 +00:00
ImanThiyanga 39a2dc60be Merge pull request 'Feat/ci cd implementation' (#59) from feat/ci-cd-implementation into Dev
CI/CD - ERP Core / Build & Deploy (pull_request) Failing after 13m56s
CI/CD - ERP Core / detect (pull_request) Failing after 14m48s
CI/CD - ERP Core / scripts (pull_request) Failing after 12m24s
CI/CD - ERP Core / e2e-typecheck (pull_request) Failing after 13m15s
CI/CD - ERP Core / frontend (pull_request) Failing after 14m7s
CI/CD - ERP Core / backend (pull_request) Failing after 14m59s
CI/CD - ERP Core / ci-ok (pull_request) Failing after 14m58s
Reviewed-on: #59
2026-09-16 18:30:42 +00:00
ImanThiyanga 3b87a6e421 Merge branch 'Dev' into feat/ci-cd-implementation
CI/CD - ERP Core / detect (pull_request) Waiting to run
CI/CD - ERP Core / backend (pull_request) Blocked by required conditions
CI/CD - ERP Core / frontend (pull_request) Blocked by required conditions
CI/CD - ERP Core / e2e-typecheck (pull_request) Blocked by required conditions
CI/CD - ERP Core / scripts (pull_request) Blocked by required conditions
CI/CD - ERP Core / ci-ok (pull_request) Blocked by required conditions
CI/CD - ERP Core / Build & Deploy (pull_request) Waiting to run
2026-09-16 18:30:22 +00:00
ImanThiyanga 2a7a44cc27 change in to merge
CI/CD - ERP Core / detect (pull_request) Failing after 13m11s
CI/CD - ERP Core / ci-ok (pull_request) Has been cancelled
CI/CD - ERP Core / Build & Deploy (pull_request) Failing after 12m17s
CI/CD - ERP Core / backend (pull_request) Has been cancelled
CI/CD - ERP Core / frontend (pull_request) Has been cancelled
CI/CD - ERP Core / e2e-typecheck (pull_request) Has been cancelled
CI/CD - ERP Core / scripts (pull_request) Has been cancelled
2026-09-16 23:48:07 +05:30
ImanThiyanga c7cf63e7de initiating connection fixes 2026-09-16 23:47:47 +05:30
ImanThiyanga d1044e4728 final fix 2026-09-16 13:53:57 +05:30
ImanThiyanga 821e96e011 Merge pull request 'Dev' (#58) from Dev into production
Reviewed-on: #58
2026-09-16 06:37:25 +00:00
ImanThiyanga d6f973202d Merge branch 'production' into Dev
CI/CD - ERP Core / Build & Deploy (pull_request) Failing after 10m38s
CI/CD - ERP Core / detect (pull_request) Failing after 11m30s
CI/CD - ERP Core / scripts (pull_request) Failing after 11m31s
CI/CD - ERP Core / e2e-typecheck (pull_request) Failing after 12m23s
CI/CD - ERP Core / frontend (pull_request) Failing after 13m14s
CI/CD - ERP Core / backend (pull_request) Failing after 14m6s
CI/CD - ERP Core / ci-ok (pull_request) Failing after 14m59s
2026-09-16 06:36:19 +00:00
ImanThiyanga 1d5a5cf68e Merge pull request 'Feat/ci cd implementation' (#57) from feat/ci-cd-implementation into Dev
CI/CD - ERP Core / detect (pull_request) Has been cancelled
CI/CD - ERP Core / backend (pull_request) Has been cancelled
CI/CD - ERP Core / frontend (pull_request) Has been cancelled
CI/CD - ERP Core / e2e-typecheck (pull_request) Has been cancelled
CI/CD - ERP Core / scripts (pull_request) Has been cancelled
CI/CD - ERP Core / ci-ok (pull_request) Has been cancelled
CI/CD - ERP Core / Build & Deploy (pull_request) Has been cancelled
Reviewed-on: #57
2026-09-16 06:24:55 +00:00
ImanThiyanga 8e472ac996 initialising for ci-cd pipeline
CI/CD - ERP Core / scripts (pull_request) Failing after 12m21s
CI/CD - ERP Core / e2e-typecheck (pull_request) Failing after 13m13s
CI/CD - ERP Core / frontend (pull_request) Failing after 14m5s
CI/CD - ERP Core / backend (pull_request) Failing after 14m57s
CI/CD - ERP Core / detect (pull_request) Failing after 13m42s
CI/CD - ERP Core / Build & Deploy (pull_request) Failing after 12m47s
CI/CD - ERP Core / ci-ok (pull_request) Failing after 14m58s
2026-09-16 11:53:55 +05:30
ImanThiyanga 666979cf22 initiated ci-cd pipeline for the erp core with fronend changes to detect changes on both front and back 2026-09-16 11:53:17 +05:30
ashan_rusiru eb36e0acfa Merge pull request 'Users and employees arrangements' (#56) from users-and-employees-arrangements into Dev
Reviewed-on: #56
2026-09-14 05:23:25 +00:00
ashan_rusiru 0ff39e9e58 Merge pull request 'Users and employees arrangements' (#53) from users-and-employees-arrangements into production
Reviewed-on: #53
2026-09-13 07:52:34 +00:00
ashan_rusiru cbb610fc8b Merge branch 'production' into users-and-employees-arrangements 2026-09-13 07:52:24 +00:00
ashan_rusiru afa9928dcc Merge pull request 'bug fixing' (#55) from bug-fixing into Dev
Reviewed-on: #55
2026-09-13 07:52:06 +00:00
Sasanka 99aa395fb1 updeted 2026-09-13 08:30:18 +05:30
Sasanka cb41d503c4 emloyee and user fixings 2026-09-11 10:32:47 +05:30
ImanThiyanga b4993b202d Merge pull request 'Dev' (#45) from Dev into production
Reviewed-on: #45
2026-08-13 06:52:56 +00:00
396 changed files with 158577 additions and 3165 deletions
Submodule .claude/worktrees/agent-a98805dc618e0f67c added at b4993b202d
+106
View File
@@ -0,0 +1,106 @@
# test
name: CI/CD - ERP Core Monorepo
on:
pull_request:
branches:
- production
types:
- closed
jobs:
backend-build-and-deploy:
name: Build & Deploy ERP Core Backend
runs-on: linux
if: github.event.pull_request.merged == true
steps:
- name: Checkout code
run: |
git clone \
--depth 1 \
--branch production \
https://oauth2:${{ secrets.ERPCORE }}@gitea.hexdive.com/New_REP_SYSTEM/ERP-core.git \
.
- name: Restore backend
working-directory: .gitea/workflows
run: dotnet restore ../../Backend/ERPCore.sln
- name: Publish backend
working-directory: .gitea/workflows
run: |
rm -rf /tmp/erpcore-deploy
mkdir -p /tmp/erpcore-deploy
dotnet publish ../../Backend/ERPCore/ERPCore.csproj \
-c Release \
-r linux-x64 \
--self-contained true \
-o /tmp/erpcore-deploy
- name: Stop backend service
run: systemctl stop erpcore.service
- name: Copy backend to deploy folder
run: |
mkdir -p /home/erp/core/publish
rm -rf /home/erp/core/publish/*
cp -r /tmp/erpcore-deploy/* /home/erp/core/publish/
- name: Fix backend ownership and permissions
run: |
chown -R erpuser:erpuser /home/erp/core/publish
find /home/erp/core/publish -type d -exec chmod 755 {} +
find /home/erp/core/publish -type f -exec chmod 644 {} +
chmod +x /home/erp/core/publish/ERPCore
- name: Restart backend service
run: systemctl restart erpcore.service
- name: Verify backend
run: |
systemctl is-active erpcore.service && echo "Backend service running" || echo "Backend service failed"
frontend-build-and-deploy:
name: Build & Deploy ERP Core Frontend
runs-on: linux
needs: backend-build-and-deploy
if: github.event.pull_request.merged == true
steps:
- name: Checkout code
run: |
git clone \
--depth 1 \
--branch Production \
https://oauth2:${{ secrets.ERPCORE }}@gitea.hexdive.com/New_REP_SYSTEM/ERP-core.git \
.
- name: Build frontend Docker image
working-directory: .gitea/workflows
run: docker build -t erpcore-frontend:latest ../../Frontend/erp-system
- name: Stop & remove existing frontend container
run: |
docker stop erpcore-frontend 2>/dev/null || true
docker rm erpcore-frontend 2>/dev/null || true
- name: Start frontend container
run: |
docker run -d \
--name erpcore-frontend \
-p 6702:6702 \
--add-host=host.docker.internal:host-gateway \
-e BACKEND_ORIGIN=http://host.docker.internal:6701 \
--restart unless-stopped \
--memory="512m" \
--cpus="1.0" \
--cap-drop ALL \
--security-opt no-new-privileges \
erpcore-frontend:latest
- name: Remove old frontend images
run: docker image prune -f
- name: Verify frontend
run: |
docker ps --filter name=erpcore-frontend --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" \
| grep erpcore-frontend && echo "Frontend container running" || echo "Frontend container failed"
@@ -27,13 +27,15 @@ public sealed class AuthController : ControllerBase
private readonly IAuthRecoveryService _recovery; private readonly IAuthRecoveryService _recovery;
private readonly IAuthAltService _alt; private readonly IAuthAltService _alt;
private readonly IRoleService _roles; private readonly IRoleService _roles;
private readonly IAuditService _audit;
public AuthController(IAuthUserService users, IAuthRecoveryService recovery, IAuthAltService alt, IRoleService roles) public AuthController(IAuthUserService users, IAuthRecoveryService recovery, IAuthAltService alt, IRoleService roles, IAuditService audit)
{ {
_users = users; _users = users;
_recovery = recovery; _recovery = recovery;
_alt = alt; _alt = alt;
_roles = roles; _roles = roles;
_audit = audit;
} }
/// <summary> /// <summary>
@@ -68,6 +70,7 @@ public sealed class AuthController : ControllerBase
{ {
var result = await _users.LoginAsync(request, ct); var result = await _users.LoginAsync(request, ct);
AuthCookieWriter.WriteSession(Response, result.AccessToken, result.RefreshToken, result.Body.ExpiresIn); AuthCookieWriter.WriteSession(Response, result.AccessToken, result.RefreshToken, result.Body.ExpiresIn);
if (result.Body.User?.UserId is Guid authUserId) await _audit.RecordLoginAsync(authUserId, ClientIp(), ClientUserAgent(), ct);
return Ok(result.Body); return Ok(result.Body);
} }
@@ -78,9 +81,13 @@ public sealed class AuthController : ControllerBase
{ {
var result = await _users.VerifyOtpForLoginAsync(request, ct); var result = await _users.VerifyOtpForLoginAsync(request, ct);
AuthCookieWriter.WriteSession(Response, result.AccessToken, result.RefreshToken, result.Body.ExpiresIn); AuthCookieWriter.WriteSession(Response, result.AccessToken, result.RefreshToken, result.Body.ExpiresIn);
if (result.Body.User?.UserId is Guid authUserId) await _audit.RecordLoginAsync(authUserId, ClientIp(), ClientUserAgent(), ct);
return Ok(result.Body); return Ok(result.Body);
} }
private string? ClientIp() => HttpContext.Connection.RemoteIpAddress?.ToString();
private string? ClientUserAgent() => Request.Headers.UserAgent.ToString() is { Length: > 0 } ua ? ua : null;
[HttpPost("refresh-token")] [HttpPost("refresh-token")]
[AllowAnonymous] [AllowAnonymous]
[ProducesResponseType(typeof(AuthSessionResponse), StatusCodes.Status200OK)] [ProducesResponseType(typeof(AuthSessionResponse), StatusCodes.Status200OK)]
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Dtos.Config; using ERPCore.Dtos.Config;
using ERPCore.Services.Interfaces; using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -14,6 +16,7 @@ namespace ERPCore.Controllers;
/// </para> /// </para>
/// </summary> /// </summary>
[Route("api/v1/branch-config")] [Route("api/v1/branch-config")]
[Authorize(Policy = NavPolicies.Prefix + "settings")]
public sealed class BranchConfigController : ApiControllerBase public sealed class BranchConfigController : ApiControllerBase
{ {
private readonly IBranchConfigService _config; private readonly IBranchConfigService _config;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Domain.Enums; using ERPCore.Domain.Enums;
using ERPCore.Dtos.Brands; using ERPCore.Dtos.Brands;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
/// <summary>Brand master endpoints (docs/11-BACKEND-PHASE1.md §2.6).</summary> /// <summary>Brand master endpoints (docs/11-BACKEND-PHASE1.md §2.6).</summary>
[Route("api/v1/brands")] [Route("api/v1/brands")]
[Authorize(Policy = NavPolicies.Prefix + "products")]
public sealed class BrandsController : ApiControllerBase public sealed class BrandsController : ApiControllerBase
{ {
private readonly IBrandService _brands; private readonly IBrandService _brands;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Domain.Enums; using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Sales; using ERPCore.Dtos.Sales;
@@ -7,6 +9,7 @@ using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers; namespace ERPCore.Controllers;
[Route("api/v1/bundle-sales")] [Route("api/v1/bundle-sales")]
[Authorize(Policy = NavPolicies.Prefix + "sales")]
public sealed class BundleSalesController : ApiControllerBase public sealed class BundleSalesController : ApiControllerBase
{ {
private readonly IBundleSaleService _bundles; private readonly IBundleSaleService _bundles;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Domain.Enums; using ERPCore.Domain.Enums;
using ERPCore.Dtos.Categories; using ERPCore.Dtos.Categories;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
@@ -12,6 +14,7 @@ namespace ERPCore.Controllers;
/// <c>?tree=true</c> parameter is gone along with the self-nesting model. /// <c>?tree=true</c> parameter is gone along with the self-nesting model.
/// </summary> /// </summary>
[Route("api/v1/categories")] [Route("api/v1/categories")]
[Authorize(Policy = NavPolicies.Prefix + "products")]
public sealed class CategoriesController : ApiControllerBase public sealed class CategoriesController : ApiControllerBase
{ {
private readonly ICategoryService _categories; private readonly ICategoryService _categories;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Domain.Enums; using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Customers; using ERPCore.Dtos.Customers;
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
/// <summary>Customer master endpoints for Phase 1 sales.</summary> /// <summary>Customer master endpoints for Phase 1 sales.</summary>
[Route("api/v1/customers")] [Route("api/v1/customers")]
[Authorize(Policy = NavPolicies.Prefix + "sales")]
public sealed class CustomersController : ApiControllerBase public sealed class CustomersController : ApiControllerBase
{ {
private readonly ICustomerService _customers; private readonly ICustomerService _customers;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Dtos.Dashboard; using ERPCore.Dtos.Dashboard;
using ERPCore.Services.Interfaces; using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -6,6 +8,7 @@ namespace ERPCore.Controllers;
/// <summary>Dashboard overview stats — cross-domain counts, not a stored entity.</summary> /// <summary>Dashboard overview stats — cross-domain counts, not a stored entity.</summary>
[Route("api/v1/dashboard")] [Route("api/v1/dashboard")]
[Authorize(Policy = NavPolicies.Prefix + "dashboard")]
public sealed class DashboardController : ApiControllerBase public sealed class DashboardController : ApiControllerBase
{ {
private readonly IDashboardService _dashboard; private readonly IDashboardService _dashboard;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Domain.Enums; using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Sales; using ERPCore.Dtos.Sales;
@@ -12,6 +14,7 @@ namespace ERPCore.Controllers;
/// the existing sales-slip CRUD surface under a more business-friendly route. /// the existing sales-slip CRUD surface under a more business-friendly route.
/// </summary> /// </summary>
[Route("api/v1/free-issues")] [Route("api/v1/free-issues")]
[Authorize(Policy = NavPolicies.Prefix + "sales")]
public sealed class FreeIssuesController : ApiControllerBase public sealed class FreeIssuesController : ApiControllerBase
{ {
private readonly ISalesSlipService _slips; private readonly ISalesSlipService _slips;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces; using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -14,6 +16,7 @@ namespace ERPCore.Controllers;
/// only and is never exposed to the frontend. /// only and is never exposed to the frontend.
/// </summary> /// </summary>
[Route("api/v1/gl")] [Route("api/v1/gl")]
[Authorize(Policy = NavPolicies.Prefix + "ledgers")]
public sealed class GeneralLedgerController : ApiControllerBase public sealed class GeneralLedgerController : ApiControllerBase
{ {
private readonly IGeneralLedgerService _gl; private readonly IGeneralLedgerService _gl;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Domain.Enums; using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Grn; using ERPCore.Dtos.Grn;
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
/// <summary>Goods-receipt endpoints (docs/11 §4).</summary> /// <summary>Goods-receipt endpoints (docs/11 §4).</summary>
[Route("api/v1/grns")] [Route("api/v1/grns")]
[Authorize(Policy = NavPolicies.Prefix + "receiving")]
public sealed class GrnsController : ApiControllerBase public sealed class GrnsController : ApiControllerBase
{ {
private readonly IGrnService _grns; private readonly IGrnService _grns;
@@ -61,4 +61,14 @@ public sealed class BranchesController : ApiControllerBase
await _branches.SetStatusAsync(branchId, request.Status, ct); await _branches.SetStatusAsync(branchId, request.Status, ct);
return NoContent(); return NoContent();
} }
[HttpDelete("{branchId:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<IActionResult> Delete(int branchId, CancellationToken ct)
{
await _branches.DeleteAsync(branchId, ct);
return NoContent();
}
} }
@@ -62,4 +62,14 @@ public sealed class DepartmentsController : ApiControllerBase
await _departments.SetStatusAsync(departmentId, request.Status, ct); await _departments.SetStatusAsync(departmentId, request.Status, ct);
return NoContent(); return NoContent();
} }
[HttpDelete("{departmentId:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<IActionResult> Delete(int departmentId, CancellationToken ct)
{
await _departments.DeleteAsync(departmentId, ct);
return NoContent();
}
} }
@@ -61,4 +61,14 @@ public sealed class DesignationsController : ApiControllerBase
await _designations.SetStatusAsync(designationId, request.Status, ct); await _designations.SetStatusAsync(designationId, request.Status, ct);
return NoContent(); return NoContent();
} }
[HttpDelete("{designationId:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<IActionResult> Delete(int designationId, CancellationToken ct)
{
await _designations.DeleteAsync(designationId, ct);
return NoContent();
}
} }
@@ -83,7 +83,6 @@ public sealed class EmployeesController : ApiControllerBase
return Ok(result.Value); return Ok(result.Value);
} }
/// <summary>Never a hard delete — Employee is retained forever (docs/12-BACKEND-HRM.md C.2).</summary>
[HttpPatch("{employeeId:int}/status")] [HttpPatch("{employeeId:int}/status")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -93,6 +92,17 @@ public sealed class EmployeesController : ApiControllerBase
return NoContent(); return NoContent();
} }
/// <summary>Permanently deletes the employee and all dependent HR records. Blocked (409) if payroll history exists.</summary>
[HttpDelete("{employeeId:int}")]
[ProducesResponseType(typeof(EmployeeDeleteResultDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<EmployeeDeleteResultDto>> Delete(int employeeId, [FromQuery] bool deleteUserAccount, CancellationToken ct)
{
var result = await _employees.DeleteAsync(employeeId, deleteUserAccount, ct);
return Ok(result);
}
[HttpPost("{employeeId:int}/link-user")] [HttpPost("{employeeId:int}/link-user")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -61,4 +61,14 @@ public sealed class EmploymentTypesController : ApiControllerBase
await _employmentTypes.SetStatusAsync(employmentTypeId, request.Status, ct); await _employmentTypes.SetStatusAsync(employmentTypeId, request.Status, ct);
return NoContent(); return NoContent();
} }
[HttpDelete("{employmentTypeId:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<IActionResult> Delete(int employmentTypeId, CancellationToken ct)
{
await _employmentTypes.DeleteAsync(employmentTypeId, ct);
return NoContent();
}
} }
@@ -61,4 +61,14 @@ public sealed class HrDocumentTypesController : ApiControllerBase
await _types.SetStatusAsync(hrDocumentTypeId, request.Status, ct); await _types.SetStatusAsync(hrDocumentTypeId, request.Status, ct);
return NoContent(); return NoContent();
} }
[HttpDelete("{hrDocumentTypeId:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<IActionResult> Delete(int hrDocumentTypeId, CancellationToken ct)
{
await _types.DeleteAsync(hrDocumentTypeId, ct);
return NoContent();
}
} }
@@ -61,4 +61,14 @@ public sealed class LeaveTypesController : ApiControllerBase
await _leaveTypes.SetStatusAsync(leaveTypeId, request.Status, ct); await _leaveTypes.SetStatusAsync(leaveTypeId, request.Status, ct);
return NoContent(); return NoContent();
} }
[HttpDelete("{leaveTypeId:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<IActionResult> Delete(int leaveTypeId, CancellationToken ct)
{
await _leaveTypes.DeleteAsync(leaveTypeId, ct);
return NoContent();
}
} }
@@ -61,4 +61,14 @@ public sealed class SalaryComponentsController : ApiControllerBase
await _components.SetStatusAsync(salaryComponentId, request.Status, ct); await _components.SetStatusAsync(salaryComponentId, request.Status, ct);
return NoContent(); return NoContent();
} }
[HttpDelete("{salaryComponentId:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<IActionResult> Delete(int salaryComponentId, CancellationToken ct)
{
await _components.DeleteAsync(salaryComponentId, ct);
return NoContent();
}
} }
@@ -61,4 +61,14 @@ public sealed class WorkShiftsController : ApiControllerBase
await _shifts.SetStatusAsync(workShiftId, request.Status, ct); await _shifts.SetStatusAsync(workShiftId, request.Status, ct);
return NoContent(); return NoContent();
} }
[HttpDelete("{workShiftId:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<IActionResult> Delete(int workShiftId, CancellationToken ct)
{
await _shifts.DeleteAsync(workShiftId, ct);
return NoContent();
}
} }
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Domain.Enums; using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.ItemTypes; using ERPCore.Dtos.ItemTypes;
@@ -13,6 +15,7 @@ namespace ERPCore.Controllers;
/// into the client-generated SKU (docs/10 Part C.9). /// into the client-generated SKU (docs/10 Part C.9).
/// </summary> /// </summary>
[Route("api/v1/item-types")] [Route("api/v1/item-types")]
[Authorize(Policy = NavPolicies.Prefix + "products")]
public sealed class ItemTypesController : ApiControllerBase public sealed class ItemTypesController : ApiControllerBase
{ {
private readonly IItemTypeService _itemTypes; private readonly IItemTypeService _itemTypes;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Domain.Enums; using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Items; using ERPCore.Dtos.Items;
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
/// <summary>Item master endpoints (docs/11-BACKEND-PHASE1.md §2.1–2.2).</summary> /// <summary>Item master endpoints (docs/11-BACKEND-PHASE1.md §2.1–2.2).</summary>
[Route("api/v1/items")] [Route("api/v1/items")]
[Authorize(Policy = NavPolicies.Prefix + "products")]
public sealed class ItemsController : ApiControllerBase public sealed class ItemsController : ApiControllerBase
{ {
private readonly IItemService _items; private readonly IItemService _items;
@@ -27,6 +30,17 @@ public sealed class ItemsController : ApiControllerBase
CancellationToken ct) CancellationToken ct)
=> Ok(await _items.ListAsync(query, status, categoryId, subCategoryId, brandId, trackingMode, ct)); => Ok(await _items.ListAsync(query, status, categoryId, subCategoryId, brandId, trackingMode, ct));
/// <summary>Barcode/RFID lookup — matches an item's barcode/SKU, or a batch/serial
/// barcode/no (optionally scoped to <paramref name="itemId"/> first).</summary>
[HttpGet("scan")]
[ProducesResponseType(typeof(ItemScanResultDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ItemScanResultDto>> Scan([FromQuery] string code, [FromQuery] int? itemId, CancellationToken ct)
{
var result = await _items.ScanAsync(code, itemId, ct);
return result is null ? NotFound() : Ok(result);
}
/// <summary>Get a single item; returns an <c>ETag</c> for optimistic concurrency.</summary> /// <summary>Get a single item; returns an <c>ETag</c> for optimistic concurrency.</summary>
[HttpGet("{itemId:int}")] [HttpGet("{itemId:int}")]
[ProducesResponseType(typeof(ItemDetailDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ItemDetailDto), StatusCodes.Status200OK)]
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Dtos.Audit; using ERPCore.Dtos.Audit;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Services.Interfaces; using ERPCore.Services.Interfaces;
@@ -10,6 +12,7 @@ namespace ERPCore.Controllers;
/// Data only — no posting in Phase 1. /// Data only — no posting in Phase 1.
/// </summary> /// </summary>
[Route("api/v1/journal-entries")] [Route("api/v1/journal-entries")]
[Authorize(Policy = NavPolicies.Prefix + "ledgers")]
public sealed class JournalEntriesController : ApiControllerBase public sealed class JournalEntriesController : ApiControllerBase
{ {
private readonly IAuditService _audit; private readonly IAuditService _audit;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Dtos.Notifications; using ERPCore.Dtos.Notifications;
using ERPCore.Services.Interfaces; using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -5,6 +7,7 @@ using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers; namespace ERPCore.Controllers;
[Route("api/v1/notification-config")] [Route("api/v1/notification-config")]
[Authorize(Policy = NavPolicies.Prefix + "settings")]
public sealed class NotificationConfigController : ApiControllerBase public sealed class NotificationConfigController : ApiControllerBase
{ {
private readonly INotificationConfigService _config; private readonly INotificationConfigService _config;
@@ -0,0 +1,32 @@
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers;
[Route("api/v1/notifications")]
public sealed class NotificationsController : ApiControllerBase
{
private readonly INotificationService _notifications;
private readonly ICurrentUser _currentUser;
public NotificationsController(INotificationService notifications, ICurrentUser currentUser)
{
_notifications = notifications;
_currentUser = currentUser;
}
[HttpGet]
public async Task<ActionResult> Get([FromQuery] int take = 20, CancellationToken ct = default)
{
var feed = await _notifications.GetFeedAsync(_currentUser.AuditUserId, take, ct);
return Ok(feed);
}
[HttpPost("mark-all-read")]
public async Task<ActionResult> MarkAllRead(CancellationToken ct)
{
await _notifications.MarkAllReadAsync(_currentUser.AuditUserId, ct);
return NoContent();
}
}
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Dtos.Config; using ERPCore.Dtos.Config;
using ERPCore.Services.Interfaces; using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -16,6 +18,7 @@ namespace ERPCore.Controllers;
/// </para> /// </para>
/// </summary> /// </summary>
[Route("api/v1/product-config")] [Route("api/v1/product-config")]
[Authorize(Policy = NavPolicies.Prefix + "products")]
public sealed class ProductConfigController : ApiControllerBase public sealed class ProductConfigController : ApiControllerBase
{ {
private readonly IProductConfigService _config; private readonly IProductConfigService _config;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Domain.Enums; using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Production; using ERPCore.Dtos.Production;
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
/// <summary>Production run endpoints (docs/30-BACKEND-PHASE2.md §D.2–D.3).</summary> /// <summary>Production run endpoints (docs/30-BACKEND-PHASE2.md §D.2–D.3).</summary>
[Route("api/v1/production-runs")] [Route("api/v1/production-runs")]
[Authorize(Policy = NavPolicies.Prefix + "production")]
public sealed class ProductionRunsController : ApiControllerBase public sealed class ProductionRunsController : ApiControllerBase
{ {
private readonly IProductionRunService _runs; private readonly IProductionRunService _runs;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Domain.Enums; using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Production; using ERPCore.Dtos.Production;
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
/// <summary>Production template endpoints (docs/30-BACKEND-PHASE2.md §D.1).</summary> /// <summary>Production template endpoints (docs/30-BACKEND-PHASE2.md §D.1).</summary>
[Route("api/v1/production-templates")] [Route("api/v1/production-templates")]
[Authorize(Policy = NavPolicies.Prefix + "production")]
public sealed class ProductionTemplatesController : ApiControllerBase public sealed class ProductionTemplatesController : ApiControllerBase
{ {
private readonly IProductionTemplateService _templates; private readonly IProductionTemplateService _templates;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Domain.Enums; using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Grn; using ERPCore.Dtos.Grn;
@@ -9,6 +11,7 @@ namespace ERPCore.Controllers;
/// <summary>Purchase invoice endpoints — vendor invoices consolidating Confirmed GRNs.</summary> /// <summary>Purchase invoice endpoints — vendor invoices consolidating Confirmed GRNs.</summary>
[Route("api/v1/purchase-invoices")] [Route("api/v1/purchase-invoices")]
[Authorize(Policy = NavPolicies.Prefix + "procurement")]
public sealed class PurchaseInvoicesController : ApiControllerBase public sealed class PurchaseInvoicesController : ApiControllerBase
{ {
private readonly IPurchaseInvoiceService _invoices; private readonly IPurchaseInvoiceService _invoices;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Domain.Enums; using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Procurement; using ERPCore.Dtos.Procurement;
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
/// <summary>Purchase-order endpoints (docs/11 §3.3).</summary> /// <summary>Purchase-order endpoints (docs/11 §3.3).</summary>
[Route("api/v1/purchase-orders")] [Route("api/v1/purchase-orders")]
[Authorize(Policy = NavPolicies.Prefix + "procurement")]
public sealed class PurchaseOrdersController : ApiControllerBase public sealed class PurchaseOrdersController : ApiControllerBase
{ {
private readonly IPurchaseOrderService _pos; private readonly IPurchaseOrderService _pos;
@@ -32,7 +35,7 @@ public sealed class PurchaseOrdersController : ApiControllerBase
return Ok(result.Value); return Ok(result.Value);
} }
/// <summary>Create a PO — auto-approved on creation in Phase 1 (FR-PROC-04). Totals computed server-side.</summary> /// <summary>Create a PO — non-draft is issued straight into PendingApproval (FR-PROC-04). Totals computed server-side.</summary>
[HttpPost] [HttpPost]
[ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status201Created)] [ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
@@ -57,7 +60,7 @@ public sealed class PurchaseOrdersController : ApiControllerBase
return Ok(result.Value); return Ok(result.Value);
} }
/// <summary>Submit a Draft PO (Draft → Approved). 409 PO_NOT_EDITABLE if not Draft.</summary> /// <summary>Submit a Draft PO (Draft → PendingApproval). 409 PO_NOT_EDITABLE if not Draft.</summary>
[HttpPost("{poId:int}/submit")] [HttpPost("{poId:int}/submit")]
[ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -67,6 +70,7 @@ public sealed class PurchaseOrdersController : ApiControllerBase
/// <summary>Delete a PO — permitted only while Draft (409 PO_NOT_EDITABLE otherwise).</summary> /// <summary>Delete a PO — permitted only while Draft (409 PO_NOT_EDITABLE otherwise).</summary>
[HttpDelete("{poId:int}")] [HttpDelete("{poId:int}")]
[Authorize(Policy = NavPolicies.Prefix + "procurement" + NavPolicies.ActionSeparator + NavPolicies.DeleteAction)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)] [ProducesResponseType(StatusCodes.Status409Conflict)]
@@ -76,13 +80,28 @@ public sealed class PurchaseOrdersController : ApiControllerBase
return NoContent(); return NoContent();
} }
/// <summary>Approve — no-op in Phase 1 (POs auto-approve); transitions PendingApproval→Approved when enabled.</summary> /// <summary>Approve — PendingApproval → Approved. 403 PO_APPROVAL_FORBIDDEN if the caller
/// isn't an Admin or is the PO's own creator; 409 PO_NOT_PENDING_APPROVAL otherwise.</summary>
[HttpPost("{poId:int}/approve")] [HttpPost("{poId:int}/approve")]
[Authorize(Policy = NavPolicies.Prefix + "procurement" + NavPolicies.ActionSeparator + NavPolicies.ApproveAction)]
[ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<PurchaseOrderDto>> Approve(int poId, CancellationToken ct) public async Task<ActionResult<PurchaseOrderDto>> Approve(int poId, CancellationToken ct)
=> Ok(await _pos.ApproveAsync(poId, ct)); => Ok(await _pos.ApproveAsync(poId, ct));
/// <summary>Reject — PendingApproval → Draft, so the requester can revise and resubmit.
/// Same caller restrictions as <see cref="Approve"/>.</summary>
[HttpPost("{poId:int}/reject")]
[Authorize(Policy = NavPolicies.Prefix + "procurement" + NavPolicies.ActionSeparator + NavPolicies.ApproveAction)]
[ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<PurchaseOrderDto>> Reject(int poId, [FromBody] RejectPurchaseOrderRequest request, CancellationToken ct)
=> Ok(await _pos.RejectAsync(poId, request.Reason, ct));
/// <summary>Cancel — 409 if any goods have been received against the PO.</summary> /// <summary>Cancel — 409 if any goods have been received against the PO.</summary>
[HttpPost("{poId:int}/cancel")] [HttpPost("{poId:int}/cancel")]
[ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status200OK)]
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Procurement; using ERPCore.Dtos.Procurement;
using ERPCore.Services.Interfaces; using ERPCore.Services.Interfaces;
@@ -7,6 +9,7 @@ namespace ERPCore.Controllers;
/// <summary>Purchase-return endpoints (docs/11 §3.4).</summary> /// <summary>Purchase-return endpoints (docs/11 §3.4).</summary>
[Route("api/v1/purchase-returns")] [Route("api/v1/purchase-returns")]
[Authorize(Policy = NavPolicies.Prefix + "procurement")]
public sealed class PurchaseReturnsController : ApiControllerBase public sealed class PurchaseReturnsController : ApiControllerBase
{ {
private readonly IPurchaseReturnService _returns; private readonly IPurchaseReturnService _returns;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Domain.Enums; using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Procurement; using ERPCore.Dtos.Procurement;
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
/// <summary>Purchase-requisition endpoints (docs/11 §3.1).</summary> /// <summary>Purchase-requisition endpoints (docs/11 §3.1).</summary>
[Route("api/v1/requisitions")] [Route("api/v1/requisitions")]
[Authorize(Policy = NavPolicies.Prefix + "procurement")]
public sealed class RequisitionsController : ApiControllerBase public sealed class RequisitionsController : ApiControllerBase
{ {
private readonly IRequisitionService _requisitions; private readonly IRequisitionService _requisitions;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Domain.Enums; using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Procurement; using ERPCore.Dtos.Procurement;
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
/// <summary>RFQ &amp; quotation endpoints (docs/11 §3.2).</summary> /// <summary>RFQ &amp; quotation endpoints (docs/11 §3.2).</summary>
[Route("api/v1/rfqs")] [Route("api/v1/rfqs")]
[Authorize(Policy = NavPolicies.Prefix + "procurement")]
public sealed class RfqsController : ApiControllerBase public sealed class RfqsController : ApiControllerBase
{ {
private readonly IRfqService _rfqs; private readonly IRfqService _rfqs;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Domain.Enums; using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Rbac; using ERPCore.Dtos.Rbac;
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
/// <summary>Role CRUD + permission assignment (mirrors AuthHex's Role; see docs/10 C.9).</summary> /// <summary>Role CRUD + permission assignment (mirrors AuthHex's Role; see docs/10 C.9).</summary>
[Route("api/v1/roles")] [Route("api/v1/roles")]
[Authorize(Policy = NavPolicies.Prefix + "settings")]
public sealed class RolesController : ApiControllerBase public sealed class RolesController : ApiControllerBase
{ {
private readonly IRoleService _roles; private readonly IRoleService _roles;
@@ -64,6 +67,7 @@ public sealed class RolesController : ApiControllerBase
} }
[HttpDelete("{roleId:int}")] [HttpDelete("{roleId:int}")]
[Authorize(Policy = NavPolicies.Prefix + "settings" + NavPolicies.ActionSeparator + NavPolicies.DeleteAction)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)] [ProducesResponseType(StatusCodes.Status409Conflict)]
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Sales; using ERPCore.Dtos.Sales;
using ERPCore.Services.Interfaces; using ERPCore.Services.Interfaces;
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
/// <summary>Cashier day-end close-out — locks a cashier's Posted sales slips for a business /// <summary>Cashier day-end close-out — locks a cashier's Posted sales slips for a business
/// date and posts one consolidated GL journal entry for the day (docs/14 Sales API).</summary> /// date and posts one consolidated GL journal entry for the day (docs/14 Sales API).</summary>
[Route("api/v1/sales-day-end")] [Route("api/v1/sales-day-end")]
[Authorize(Policy = NavPolicies.Prefix + "sales")]
public sealed class SalesDayEndController : ApiControllerBase public sealed class SalesDayEndController : ApiControllerBase
{ {
private readonly ISalesDayEndService _dayEnds; private readonly ISalesDayEndService _dayEnds;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Domain.Enums; using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Sales; using ERPCore.Dtos.Sales;
@@ -7,6 +9,7 @@ using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers; namespace ERPCore.Controllers;
[Route("api/v1/sales-invoices")] [Route("api/v1/sales-invoices")]
[Authorize(Policy = NavPolicies.Prefix + "sales")]
public sealed class SalesInvoicesController : ApiControllerBase public sealed class SalesInvoicesController : ApiControllerBase
{ {
private readonly ISalesInvoiceService _invoices; private readonly ISalesInvoiceService _invoices;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Dtos.Sales; using ERPCore.Dtos.Sales;
using ERPCore.Services.Interfaces; using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -5,6 +7,7 @@ using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers; namespace ERPCore.Controllers;
[Route("api/v1/reports/sales")] [Route("api/v1/reports/sales")]
[Authorize(Policy = NavPolicies.Prefix + "sales")]
public sealed class SalesReportsController : ApiControllerBase public sealed class SalesReportsController : ApiControllerBase
{ {
private readonly ISalesReportService _reports; private readonly ISalesReportService _reports;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Sales; using ERPCore.Dtos.Sales;
using ERPCore.Services.Interfaces; using ERPCore.Services.Interfaces;
@@ -7,6 +9,7 @@ namespace ERPCore.Controllers;
/// <summary>Sales-return endpoints — customer returns of previously sold goods.</summary> /// <summary>Sales-return endpoints — customer returns of previously sold goods.</summary>
[Route("api/v1/sales-returns")] [Route("api/v1/sales-returns")]
[Authorize(Policy = NavPolicies.Prefix + "sales")]
public sealed class SalesReturnsController : ApiControllerBase public sealed class SalesReturnsController : ApiControllerBase
{ {
private readonly ISalesReturnService _returns; private readonly ISalesReturnService _returns;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Dtos.Sales; using ERPCore.Dtos.Sales;
using ERPCore.Services.Interfaces; using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -5,6 +7,7 @@ using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers; namespace ERPCore.Controllers;
[Route("api/v1/sales-slips/{salesSlipId:int}/free-issue-suggestions")] [Route("api/v1/sales-slips/{salesSlipId:int}/free-issue-suggestions")]
[Authorize(Policy = NavPolicies.Prefix + "sales")]
public sealed class SalesSlipPromotionsController : ApiControllerBase public sealed class SalesSlipPromotionsController : ApiControllerBase
{ {
private readonly ISalesPromotionSuggestionService _suggestions; private readonly ISalesPromotionSuggestionService _suggestions;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Domain.Enums; using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Sales; using ERPCore.Dtos.Sales;
@@ -7,6 +9,7 @@ using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers; namespace ERPCore.Controllers;
[Route("api/v1/sales-slips")] [Route("api/v1/sales-slips")]
[Authorize(Policy = NavPolicies.Prefix + "sales")]
public sealed class SalesSlipsController : ApiControllerBase public sealed class SalesSlipsController : ApiControllerBase
{ {
private readonly ISalesSlipService _slips; private readonly ISalesSlipService _slips;
@@ -0,0 +1,20 @@
using ERPCore.Dtos.Search;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers;
[Route("api/v1/search")]
public sealed class SearchController : ApiControllerBase
{
private readonly IGlobalSearchService _search;
public SearchController(IGlobalSearchService search) => _search = search;
[HttpGet]
public async Task<ActionResult<IReadOnlyList<GlobalSearchResultDto>>> Get([FromQuery] string q, CancellationToken ct)
{
var results = await _search.SearchAsync(q, 6, ct);
return Ok(results);
}
}
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Stock; using ERPCore.Dtos.Stock;
using ERPCore.Services.Interfaces; using ERPCore.Services.Interfaces;
@@ -7,6 +9,7 @@ namespace ERPCore.Controllers;
/// <summary>Stock-adjustment endpoints (docs/11 §5.5).</summary> /// <summary>Stock-adjustment endpoints (docs/11 §5.5).</summary>
[Route("api/v1/stock-adjustments")] [Route("api/v1/stock-adjustments")]
[Authorize(Policy = NavPolicies.Prefix + "stock")]
public sealed class StockAdjustmentsController : ApiControllerBase public sealed class StockAdjustmentsController : ApiControllerBase
{ {
private readonly IAdjustmentService _adjustments; private readonly IAdjustmentService _adjustments;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Procurement; using ERPCore.Dtos.Procurement;
using ERPCore.Dtos.Stock; using ERPCore.Dtos.Stock;
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
/// <summary>Stock enquiry, ledger, valuation and reorder-alert endpoints (docs/11 §5.1–5.3, §5.7).</summary> /// <summary>Stock enquiry, ledger, valuation and reorder-alert endpoints (docs/11 §5.1–5.3, §5.7).</summary>
[Route("api/v1/stock")] [Route("api/v1/stock")]
[Authorize(Policy = NavPolicies.Prefix + "stock")]
public sealed class StockController : ApiControllerBase public sealed class StockController : ApiControllerBase
{ {
private readonly IStockService _stock; private readonly IStockService _stock;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Domain.Enums; using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Stock; using ERPCore.Dtos.Stock;
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
/// <summary>Stock-count endpoints (docs/11 §5.6).</summary> /// <summary>Stock-count endpoints (docs/11 §5.6).</summary>
[Route("api/v1/stock-counts")] [Route("api/v1/stock-counts")]
[Authorize(Policy = NavPolicies.Prefix + "stock")]
public sealed class StockCountsController : ApiControllerBase public sealed class StockCountsController : ApiControllerBase
{ {
private readonly ICountService _counts; private readonly ICountService _counts;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Domain.Enums; using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Stock; using ERPCore.Dtos.Stock;
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
/// <summary>Stock-transfer endpoints (docs/11 §5.4).</summary> /// <summary>Stock-transfer endpoints (docs/11 §5.4).</summary>
[Route("api/v1/stock-transfers")] [Route("api/v1/stock-transfers")]
[Authorize(Policy = NavPolicies.Prefix + "stock")]
public sealed class StockTransfersController : ApiControllerBase public sealed class StockTransfersController : ApiControllerBase
{ {
private readonly ITransferService _transfers; private readonly ITransferService _transfers;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Dtos.Categories; using ERPCore.Dtos.Categories;
using ERPCore.Services.Interfaces; using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -10,6 +12,7 @@ namespace ERPCore.Controllers;
/// since a subcategory only exists in the context of one. /// since a subcategory only exists in the context of one.
/// </summary> /// </summary>
[Route("api/v1/subcategories")] [Route("api/v1/subcategories")]
[Authorize(Policy = NavPolicies.Prefix + "products")]
public sealed class SubCategoriesController : ApiControllerBase public sealed class SubCategoriesController : ApiControllerBase
{ {
private readonly ICategoryService _categories; private readonly ICategoryService _categories;
@@ -0,0 +1,70 @@
using ERPCore.Dtos.Support;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers;
[Route("api/v1/support-tickets")]
public sealed class SupportTicketsController : ApiControllerBase
{
private readonly ISupportTicketService _tickets;
private readonly ICurrentUser _currentUser;
public SupportTicketsController(ISupportTicketService tickets, ICurrentUser currentUser)
{
_tickets = tickets;
_currentUser = currentUser;
}
[HttpPost]
[ProducesResponseType(typeof(SupportTicketDto), StatusCodes.Status201Created)]
public async Task<ActionResult<SupportTicketDto>> Create([FromBody] CreateSupportTicketRequest request, CancellationToken ct)
{
var result = await _tickets.CreateAsync(_currentUser.AuditUserId, request, ct);
SetETag(result.RowVersion);
return CreatedAtAction(nameof(GetById), new { id = result.Value.TicketId }, result.Value);
}
[HttpGet]
[ProducesResponseType(typeof(IReadOnlyList<SupportTicketSummaryDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<IReadOnlyList<SupportTicketSummaryDto>>> GetList(CancellationToken ct)
{
var list = await _tickets.GetListAsync(_currentUser.AuditUserId, _currentUser.IsAdmin, ct);
return Ok(list);
}
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(SupportTicketDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<SupportTicketDto>> GetById(int id, CancellationToken ct)
{
var result = await _tickets.GetByIdAsync(id, _currentUser.AuditUserId, _currentUser.IsAdmin, ct);
SetETag(result.RowVersion);
return Ok(result.Value);
}
[HttpPost("{id:int}/messages")]
[ProducesResponseType(typeof(SupportTicketDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
public async Task<ActionResult<SupportTicketDto>> AddMessage(int id, [FromBody] AddSupportTicketMessageRequest request, CancellationToken ct)
{
var expected = RequireIfMatch();
var result = await _tickets.AddMessageAsync(id, _currentUser.AuditUserId, _currentUser.IsAdmin, request, expected, ct);
SetETag(result.RowVersion);
return Ok(result.Value);
}
[HttpPut("{id:int}/status")]
[ProducesResponseType(typeof(SupportTicketDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
public async Task<ActionResult<SupportTicketDto>> UpdateStatus(int id, [FromBody] UpdateSupportTicketStatusRequest request, CancellationToken ct)
{
var expected = RequireIfMatch();
var result = await _tickets.UpdateStatusAsync(id, _currentUser.AuditUserId, _currentUser.IsAdmin, request, expected, ct);
SetETag(result.RowVersion);
return Ok(result.Value);
}
}
@@ -0,0 +1,70 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common;
using ERPCore.Dtos.TaxRates;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers;
/// <summary>Tax rate master endpoints — see <see cref="Domain.Entities.TaxRate"/>.</summary>
[Route("api/v1/tax-rates")]
[Authorize(Policy = NavPolicies.Prefix + "products")]
public sealed class TaxRatesController : ApiControllerBase
{
private readonly ITaxRateService _taxRates;
public TaxRatesController(ITaxRateService taxRates) => _taxRates = taxRates;
[HttpGet]
[ProducesResponseType(typeof(PagedResponse<TaxRateDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResponse<TaxRateDto>>> List(
[FromQuery] PageQuery query, [FromQuery] EntityStatus? status, CancellationToken ct)
=> Ok(await _taxRates.ListAsync(query, status, ct));
[HttpGet("{taxRateId:int}")]
[ProducesResponseType(typeof(TaxRateDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<TaxRateDto>> GetById(int taxRateId, CancellationToken ct)
{
var result = await _taxRates.GetAsync(taxRateId, ct);
if (result is null) return NotFound();
SetETag(result.RowVersion);
return Ok(result.Value);
}
[HttpPost]
[ProducesResponseType(typeof(TaxRateDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<TaxRateDto>> Create([FromBody] CreateTaxRateRequest request, CancellationToken ct)
{
var result = await _taxRates.CreateAsync(request, ct);
SetETag(result.RowVersion);
return Created($"/api/v1/tax-rates/{result.Value.TaxRateId}", result.Value);
}
[HttpPut("{taxRateId:int}")]
[ProducesResponseType(typeof(TaxRateDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
public async Task<ActionResult<TaxRateDto>> Update(int taxRateId, [FromBody] UpdateTaxRateRequest request, CancellationToken ct)
{
var expected = RequireIfMatch();
var result = await _taxRates.UpdateAsync(taxRateId, request, expected, ct);
SetETag(result.RowVersion);
return Ok(result.Value);
}
/// <summary>Deactivate/reactivate. Masters are never hard-deleted (FR-MD-08).</summary>
[HttpPatch("{taxRateId:int}/status")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> SetStatus(int taxRateId, [FromBody] UpdateTaxRateStatusRequest request, CancellationToken ct)
{
await _taxRates.SetStatusAsync(taxRateId, request.Status, ct);
return NoContent();
}
}
+17 -2
View File
@@ -1,3 +1,6 @@
using ERPCore.Domain.Enums;
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Uoms; using ERPCore.Dtos.Uoms;
using ERPCore.Services.Interfaces; using ERPCore.Services.Interfaces;
@@ -7,6 +10,7 @@ namespace ERPCore.Controllers;
/// <summary>Unit-of-measure endpoints (docs/11-BACKEND-PHASE1.md §2.2).</summary> /// <summary>Unit-of-measure endpoints (docs/11-BACKEND-PHASE1.md §2.2).</summary>
[Route("api/v1/uoms")] [Route("api/v1/uoms")]
[Authorize(Policy = NavPolicies.Prefix + "products")]
public sealed class UomsController : ApiControllerBase public sealed class UomsController : ApiControllerBase
{ {
private readonly IUomService _uoms; private readonly IUomService _uoms;
@@ -15,8 +19,9 @@ public sealed class UomsController : ApiControllerBase
[HttpGet] [HttpGet]
[ProducesResponseType(typeof(PagedResponse<UomDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(PagedResponse<UomDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResponse<UomDto>>> List([FromQuery] PageQuery query, CancellationToken ct) public async Task<ActionResult<PagedResponse<UomDto>>> List(
=> Ok(await _uoms.ListAsync(query, ct)); [FromQuery] PageQuery query, [FromQuery] EntityStatus? status, CancellationToken ct)
=> Ok(await _uoms.ListAsync(query, status, ct));
[HttpPost] [HttpPost]
[ProducesResponseType(typeof(UomDto), StatusCodes.Status201Created)] [ProducesResponseType(typeof(UomDto), StatusCodes.Status201Created)]
@@ -26,4 +31,14 @@ public sealed class UomsController : ApiControllerBase
var dto = await _uoms.CreateAsync(request, ct); var dto = await _uoms.CreateAsync(request, ct);
return Created($"/api/v1/uoms/{dto.UomId}", dto); return Created($"/api/v1/uoms/{dto.UomId}", dto);
} }
/// <summary>Deactivate/reactivate. Masters are never hard-deleted (FR-MD-08).</summary>
[HttpPatch("{uomId:int}/status")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> SetStatus(int uomId, [FromBody] UpdateUomStatusRequest request, CancellationToken ct)
{
await _uoms.SetStatusAsync(uomId, request.Status, ct);
return NoContent();
}
} }
+41 -3
View File
@@ -1,8 +1,13 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Audit;
using ERPCore.Dtos.Auth;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Hrm; using ERPCore.Dtos.Hrm;
using ERPCore.Dtos.Users; using ERPCore.Dtos.Users;
using ERPCore.Infra.Auth; using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces; using ERPCore.Services.Interfaces;
using ERPCore.System.Errors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers; namespace ERPCore.Controllers;
@@ -12,16 +17,19 @@ namespace ERPCore.Controllers;
/// account creation orchestrated against AuthHex (see <see cref="IUserManagementService.CreateAsync"/>). /// account creation orchestrated against AuthHex (see <see cref="IUserManagementService.CreateAsync"/>).
/// </summary> /// </summary>
[Route("api/v1/users")] [Route("api/v1/users")]
[Authorize(Policy = NavPolicies.Prefix + "settings")]
public sealed class UsersController : ApiControllerBase public sealed class UsersController : ApiControllerBase
{ {
private readonly IUserManagementService _users; private readonly IUserManagementService _users;
private readonly IEmployeeUserLinkService _links; private readonly IEmployeeUserLinkService _links;
private readonly IAuditService _audit;
private readonly ICurrentUser _currentUser; private readonly ICurrentUser _currentUser;
public UsersController(IUserManagementService users, IEmployeeUserLinkService links, ICurrentUser currentUser) public UsersController(IUserManagementService users, IEmployeeUserLinkService links, IAuditService audit, ICurrentUser currentUser)
{ {
_users = users; _users = users;
_links = links; _links = links;
_audit = audit;
_currentUser = currentUser; _currentUser = currentUser;
} }
@@ -33,8 +41,8 @@ public sealed class UsersController : ApiControllerBase
[HttpGet] [HttpGet]
[ProducesResponseType(typeof(PagedResponse<ManagedUserDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(PagedResponse<ManagedUserDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResponse<ManagedUserDto>>> List([FromQuery] PageQuery query, CancellationToken ct) public async Task<ActionResult<PagedResponse<ManagedUserDto>>> List([FromQuery] PageQuery query, [FromQuery] EntityStatus? status, [FromQuery] string? roleCode, CancellationToken ct)
=> Ok(await _users.ListAsync(query, ct)); => Ok(await _users.ListAsync(query, status, roleCode, ct));
/// <summary>AuthHex UserType options for the create-user form's select.</summary> /// <summary>AuthHex UserType options for the create-user form's select.</summary>
[HttpGet("user-types")] [HttpGet("user-types")]
@@ -65,4 +73,34 @@ public sealed class UsersController : ApiControllerBase
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ManagedUserDto>> UpdateRole(int userId, [FromBody] UpdateUserRoleRequest request, CancellationToken ct) public async Task<ActionResult<ManagedUserDto>> UpdateRole(int userId, [FromBody] UpdateUserRoleRequest request, CancellationToken ct)
=> Ok(await _users.UpdateRoleAsync(userId, request, ct)); => Ok(await _users.UpdateRoleAsync(userId, request, ct));
/// <summary>Revokes/restores standing inside ERPCore only — see <see cref="IUserManagementService.SetStatusAsync"/>.</summary>
[HttpPatch("{userId:int}/status")]
[ProducesResponseType(typeof(ManagedUserDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ManagedUserDto>> UpdateStatus(int userId, [FromBody] UpdateUserStatusRequest request, CancellationToken ct)
=> Ok(await _users.SetStatusAsync(userId, request.Status, ct));
/// <summary>Device/login history ERPCore captures itself — not AuthHex's "currently active sessions"
/// (which only ever reports the caller's own). See <see cref="IAuditService.ListLoginDevicesAsync"/>.</summary>
[HttpGet("{userId:int}/login-devices")]
[ProducesResponseType(typeof(PagedResponse<LoginActivityDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResponse<LoginActivityDto>>> ListLoginDevices(int userId, [FromQuery] PageQuery query, CancellationToken ct)
=> Ok(await _audit.ListLoginDevicesAsync(userId, query, ct));
/// <summary>
/// Admin-only escape hatch: permanently deletes a stale AuthHex identity (by email/mobile/NIC/
/// username) that is blocking new registrations. Does not touch ERPCore's own `users` table.
/// See <see cref="IUserManagementService.PurgeAuthHexIdentityAsync"/>.
/// </summary>
[HttpPost("purge-authhex-identity")]
[ProducesResponseType(typeof(DeleteAuthHexUserResult), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult<DeleteAuthHexUserResult>> PurgeAuthHexIdentity([FromBody] PurgeAuthHexIdentityRequest request, CancellationToken ct)
{
if (!_currentUser.IsAdmin)
throw new DomainException("FORBIDDEN", "Only an administrator can purge an AuthHex identity.", 403);
return Ok(await _users.PurgeAuthHexIdentityAsync(request, ct));
}
} }
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Domain.Enums; using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Vendors; using ERPCore.Dtos.Vendors;
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
/// <summary>Vendor master endpoints (docs/11-BACKEND-PHASE1.md §2.4).</summary> /// <summary>Vendor master endpoints (docs/11-BACKEND-PHASE1.md §2.4).</summary>
[Route("api/v1/vendors")] [Route("api/v1/vendors")]
[Authorize(Policy = NavPolicies.Prefix + "vendors")]
public sealed class VendorsController : ApiControllerBase public sealed class VendorsController : ApiControllerBase
{ {
private readonly IVendorService _vendors; private readonly IVendorService _vendors;
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Warehouses; using ERPCore.Dtos.Warehouses;
using ERPCore.Services.Interfaces; using ERPCore.Services.Interfaces;
@@ -7,6 +9,7 @@ namespace ERPCore.Controllers;
/// <summary>Warehouse &amp; bin endpoints (docs/11-BACKEND-PHASE1.md §2.5).</summary> /// <summary>Warehouse &amp; bin endpoints (docs/11-BACKEND-PHASE1.md §2.5).</summary>
[Route("api/v1/warehouses")] [Route("api/v1/warehouses")]
[Authorize(Policy = NavPolicies.Prefix + "warehouses")]
public sealed class WarehousesController : ApiControllerBase public sealed class WarehousesController : ApiControllerBase
{ {
private readonly IWarehouseService _warehouses; private readonly IWarehouseService _warehouses;
@@ -15,8 +18,9 @@ public sealed class WarehousesController : ApiControllerBase
[HttpGet] [HttpGet]
[ProducesResponseType(typeof(PagedResponse<WarehouseDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(PagedResponse<WarehouseDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResponse<WarehouseDto>>> List([FromQuery] PageQuery query, CancellationToken ct) public async Task<ActionResult<PagedResponse<WarehouseDto>>> List(
=> Ok(await _warehouses.ListAsync(query, ct)); [FromQuery] PageQuery query, [FromQuery] int? branchId, CancellationToken ct)
=> Ok(await _warehouses.ListAsync(query, branchId, ct));
[HttpGet("{warehouseId:int}")] [HttpGet("{warehouseId:int}")]
[ProducesResponseType(typeof(WarehouseDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(WarehouseDto), StatusCodes.Status200OK)]
@@ -36,6 +40,24 @@ public sealed class WarehousesController : ApiControllerBase
return Created($"/api/v1/warehouses/{dto.WarehouseId}", dto); return Created($"/api/v1/warehouses/{dto.WarehouseId}", dto);
} }
[HttpPut("{warehouseId:int}")]
[ProducesResponseType(typeof(WarehouseDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<WarehouseDto>> Update(int warehouseId, [FromBody] UpdateWarehouseRequest request, CancellationToken ct)
=> Ok(await _warehouses.UpdateAsync(warehouseId, request, ct));
[HttpDelete("{warehouseId:int}")]
[Authorize(Policy = NavPolicies.Prefix + "warehouses" + NavPolicies.ActionSeparator + NavPolicies.DeleteAction)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<IActionResult> Delete(int warehouseId, CancellationToken ct)
{
await _warehouses.DeleteAsync(warehouseId, ct);
return NoContent();
}
[HttpGet("{warehouseId:int}/bins")] [HttpGet("{warehouseId:int}/bins")]
[ProducesResponseType(typeof(IReadOnlyList<BinDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(IReadOnlyList<BinDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
+3
View File
@@ -13,4 +13,7 @@ public class Batch
public string BatchNo { get; set; } = string.Empty; public string BatchNo { get; set; } = string.Empty;
public DateOnly? ExpiryDate { get; set; } public DateOnly? ExpiryDate { get; set; }
/// <summary>Optional scan code for barcode/RFID lookup (unique when set).</summary>
public string? Barcode { get; set; }
} }
@@ -17,6 +17,12 @@ public class BranchConfig
public bool BranchingEnabled { get; set; } = true; public bool BranchingEnabled { get; set; } = true;
/// <summary>Independent sub-toggle: when the global <see cref="BranchingEnabled"/> is on
/// but this is off, Warehouses stop asking for/showing a branch while Employees etc.
/// still do. Meaningless when <see cref="BranchingEnabled"/> is off (everything is
/// single-branch already).</summary>
public bool WarehouseBranchingEnabled { get; set; } = true;
public DateTime? UpdatedAt { get; set; } public DateTime? UpdatedAt { get; set; }
public int? UpdatedBy { get; set; } public int? UpdatedBy { get; set; }
@@ -29,6 +29,9 @@ public class Customer
public int? DefaultWarehouseId { get; set; } public int? DefaultWarehouseId { get; set; }
public Warehouse? DefaultWarehouse { get; set; } public Warehouse? DefaultWarehouse { get; set; }
public int BranchId { get; set; }
public Branch? Branch { get; set; }
public EntityStatus Status { get; set; } = EntityStatus.Active; public EntityStatus Status { get; set; } = EntityStatus.Active;
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; } public DateTime? UpdatedAt { get; set; }
+4 -2
View File
@@ -6,8 +6,10 @@ namespace ERPCore.Domain.Entities;
/// Staff record (FR-HR-MD-02) — distinct from <see cref="User"/> (the system login /// Staff record (FR-HR-MD-02) — distinct from <see cref="User"/> (the system login
/// account): not every employee has a login, and not every login belongs to an /// account): not every employee has a login, and not every login belongs to an
/// employee. <see cref="UserId"/> is the optional, explicit, human-confirmed link /// employee. <see cref="UserId"/> is the optional, explicit, human-confirmed link
/// between the two (docs/12-BACKEND-HRM.md A.5/C.2, Part B.3.2). Never hard-deleted — /// between the two (docs/12-BACKEND-HRM.md A.5/C.2, Part B.3.2). Normal separation is
/// separation is recorded via <see cref="Status"/> + <see cref="LastWorkingDate"/>. /// recorded via <see cref="Status"/> + <see cref="LastWorkingDate"/> rather than deletion;
/// an explicit hard delete is also available (<see cref="Services.Interfaces.IEmployeeService.DeleteAsync"/>)
/// and cascades to every dependent HR record, blocked only if payroll history exists.
/// <see cref="EmployeeCode"/> is user-entered (not <see cref="NumberSequence"/>-issued): /// <see cref="EmployeeCode"/> is user-entered (not <see cref="NumberSequence"/>-issued):
/// HR departments keep their own legacy numbering scheme, and NumberSequence's /// HR departments keep their own legacy numbering scheme, and NumberSequence's
/// year-scoping is the wrong shape for an identifier that must never look "reset". /// year-scoping is the wrong shape for an identifier that must never look "reset".
+11 -1
View File
@@ -14,6 +14,9 @@ public class Item
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public string? Description { get; set; } public string? Description { get; set; }
/// <summary>Optional scan code for barcode/RFID lookup (unique when set).</summary>
public string? Barcode { get; set; }
public int CategoryId { get; set; } public int CategoryId { get; set; }
public Category? Category { get; set; } public Category? Category { get; set; }
@@ -41,7 +44,14 @@ public class Item
public Warranty Warranty { get; set; } = Warranty.NonWarranty; public Warranty Warranty { get; set; } = Warranty.NonWarranty;
/// <summary>Coverage length in months (see <see cref="Enums.WarrantyPeriods.AllowedMonths"/>) when <see cref="Warranty"/> is Warranty; null otherwise.</summary> /// <summary>Coverage length in months (see <see cref="Enums.WarrantyPeriods.AllowedMonths"/>) when <see cref="Warranty"/> is Warranty; null otherwise.</summary>
public int? WarrantyPeriodMonths { get; set; } public int? WarrantyPeriodMonths { get; set; }
public string? TaxClass { get; set; }
/// <summary>
/// The tax rate a sales/purchase line for this item defaults to (see
/// <see cref="TaxRate"/>) — replaces the old free-text "tax class" label that nothing
/// ever read. Null means no default; a line must be given an explicit tax percentage.
/// </summary>
public int? TaxRateId { get; set; }
public TaxRate? TaxRate { get; set; }
/// <summary> /// <summary>
/// Optional selling price range and flat price used by Sales only. /// Optional selling price range and flat price used by Sales only.
@@ -0,0 +1,22 @@
namespace ERPCore.Domain.Entities;
/// <summary>
/// One row per successful sign-in, captured directly by ERPCore (not AuthHex) so an
/// admin can see a user's device/login history — AuthHex's own session listing only
/// ever reports the currently-authenticated caller's own sessions, with no admin-scoped
/// call to inspect anyone else's. This is a history of past logins, not a live "currently
/// active sessions" view (that would need AuthHex to expose one).
/// </summary>
public class LoginActivity
{
public int LoginActivityId { get; set; }
public int UserId { get; set; }
public User? User { get; set; }
public string? IpAddress { get; set; }
public string? UserAgent { get; set; }
/// <summary>Best-effort "Browser on OS" label parsed from <see cref="UserAgent"/> for display.</summary>
public string? DeviceLabel { get; set; }
public DateTime LoginAt { get; set; }
}
@@ -0,0 +1,22 @@
namespace ERPCore.Domain.Entities;
/// <summary>
/// A single raised notification event (e.g. "GRN Confirmed"). Broadcast to
/// active users via <see cref="NotificationRecipient"/> rows, gated by
/// whether the module's InApp channel is enabled in <see cref="NotificationConfig"/>.
/// </summary>
public class Notification
{
public int NotificationId { get; set; }
public string ModuleKey { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public string? RelatedEntityType { get; set; }
public int? RelatedEntityId { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public ICollection<NotificationRecipient> Recipients { get; set; } = new List<NotificationRecipient>();
}
@@ -0,0 +1,18 @@
namespace ERPCore.Domain.Entities;
/// <summary>
/// Per-user read state for a broadcast <see cref="Notification"/>.
/// </summary>
public class NotificationRecipient
{
public int NotificationRecipientId { get; set; }
public int NotificationId { get; set; }
public Notification? Notification { get; set; }
public int UserId { get; set; }
public User? User { get; set; }
public bool IsRead { get; set; }
public DateTime? ReadAt { get; set; }
}
@@ -10,6 +10,10 @@ public class NumberSequence
{ {
public int SequenceId { get; set; } public int SequenceId { get; set; }
public string DocType { get; set; } = string.Empty; public string DocType { get; set; } = string.Empty;
/// <summary>Per-branch counter key (docs/BRANCH_ISOLATION_PLAN.md §5) — backfilled to the
/// deployment's default branch for rows that predate per-branch numbering.</summary>
public int BranchId { get; set; }
public Branch? Branch { get; set; }
public int Year { get; set; } public int Year { get; set; }
public int LastNumber { get; set; } public int LastNumber { get; set; }
} }
@@ -20,6 +20,9 @@ public class PurchaseInvoice
public int VendorId { get; set; } public int VendorId { get; set; }
public Vendor? Vendor { get; set; } public Vendor? Vendor { get; set; }
public int BranchId { get; set; }
public Branch? Branch { get; set; }
public string VendorSnapshotName { get; set; } = string.Empty; public string VendorSnapshotName { get; set; } = string.Empty;
public string? VendorSnapshotTaxNo { get; set; } public string? VendorSnapshotTaxNo { get; set; }
@@ -5,9 +5,10 @@ namespace ERPCore.Domain.Entities;
/// <summary> /// <summary>
/// Purchase order header (FR-PROC-03..06). Mutable aggregate with a /// Purchase order header (FR-PROC-03..06). Mutable aggregate with a
/// <see cref="RowVersion"/> ETag token; editable while open (FR-PROC-05). /// <see cref="RowVersion"/> ETag token; editable while open (FR-PROC-05).
/// Phase 1 auto-approves on creation; <see cref="ApprovalRequired"/> is retained /// A submitted/issued PO goes to <see cref="PurchaseOrderStatus.PendingApproval"/> and
/// for the future approval workflow. Totals are computed server-side from lines /// needs a separate Approve action (an Admin who isn't <see cref="CreatedBy"/>) before it
/// (not stored). Model: docs/10 Part C.2. /// reaches <see cref="PurchaseOrderStatus.Approved"/>. Totals are computed server-side
/// from lines (not stored). Model: docs/10 Part C.2.
/// </summary> /// </summary>
public class PurchaseOrder public class PurchaseOrder
{ {
@@ -17,15 +18,25 @@ public class PurchaseOrder
public int VendorId { get; set; } public int VendorId { get; set; }
public Vendor? Vendor { get; set; } public Vendor? Vendor { get; set; }
public int BranchId { get; set; }
public Branch? Branch { get; set; }
public int? RequisitionId { get; set; } public int? RequisitionId { get; set; }
public Requisition? Requisition { get; set; } public Requisition? Requisition { get; set; }
public PurchaseOrderStatus Status { get; set; } = PurchaseOrderStatus.Draft; public PurchaseOrderStatus Status { get; set; } = PurchaseOrderStatus.Draft;
/// <summary>True once the PO has been (or needs to be) routed through approval —
/// i.e. it is not a still-editable Draft.</summary>
public bool ApprovalRequired { get; set; } public bool ApprovalRequired { get; set; }
public int CreatedBy { get; set; } public int CreatedBy { get; set; }
public User? Creator { get; set; } public User? Creator { get; set; }
/// <summary>Who approved this PO — set only when <see cref="Status"/> reaches Approved.</summary>
public int? ApprovedBy { get; set; }
public User? Approver { get; set; }
public DateTime? ApprovedAt { get; set; }
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; } public DateTime? UpdatedAt { get; set; }
@@ -14,6 +14,9 @@ public class Requisition
public int RequestedBy { get; set; } public int RequestedBy { get; set; }
public User? Requester { get; set; } public User? Requester { get; set; }
public int BranchId { get; set; }
public Branch? Branch { get; set; }
public RequisitionStatus Status { get; set; } = RequisitionStatus.Draft; public RequisitionStatus Status { get; set; } = RequisitionStatus.Draft;
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
+3
View File
@@ -14,6 +14,9 @@ public class Rfq
public int RequisitionId { get; set; } public int RequisitionId { get; set; }
public Requisition? Requisition { get; set; } public Requisition? Requisition { get; set; }
public int BranchId { get; set; }
public Branch? Branch { get; set; }
public RfqStatus Status { get; set; } = RfqStatus.Open; public RfqStatus Status { get; set; } = RfqStatus.Open;
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
@@ -1,3 +1,5 @@
using ERPCore.Domain.Enums;
namespace ERPCore.Domain.Entities; namespace ERPCore.Domain.Entities;
/// <summary>Join row granting a <see cref="Role"/> visibility of a <see cref="Permission"/> (nav node).</summary> /// <summary>Join row granting a <see cref="Role"/> visibility of a <see cref="Permission"/> (nav node).</summary>
@@ -6,6 +8,13 @@ public class RolePermission
public int RoleId { get; set; } public int RoleId { get; set; }
public int PermissionId { get; set; } public int PermissionId { get; set; }
/// <summary>
/// Which mutating actions the role may perform within this permission's module
/// (see <see cref="RolePermissionAction"/>); defaults to <see cref="RolePermissionAction.All"/>
/// so existing grants keep today's behavior until an admin narrows them.
/// </summary>
public RolePermissionAction Actions { get; set; } = RolePermissionAction.All;
public Role? Role { get; set; } public Role? Role { get; set; }
public Permission? Permission { get; set; } public Permission? Permission { get; set; }
} }
@@ -27,6 +27,16 @@ public class SalesDayEnd
public decimal TaxTotal { get; set; } public decimal TaxTotal { get; set; }
public decimal GrandTotal { get; set; } public decimal GrandTotal { get; set; }
/// <summary>
/// Sum of <see cref="SalesSlip.PaidAmount"/> across the day's slips (bundle sales have no
/// partial-payment field and are always counted as fully paid) — what actually gets debited
/// to Cash on close. <see cref="BalanceTotal"/> is the rest (<c>GrandTotal - PaidTotal</c>),
/// debited to Accounts Receivable instead. Before partial payments existed these were always
/// equal to GrandTotal/0 respectively (every sale was assumed fully cash).
/// </summary>
public decimal PaidTotal { get; set; }
public decimal BalanceTotal { get; set; }
/// <summary>Sum of the FIFO cost consumed for these slips (from <see cref="StockLedger"/>), for the COGS GL lines.</summary> /// <summary>Sum of the FIFO cost consumed for these slips (from <see cref="StockLedger"/>), for the COGS GL lines.</summary>
public decimal CostOfGoodsSold { get; set; } public decimal CostOfGoodsSold { get; set; }
@@ -19,6 +19,12 @@ public class SalesSlipLine
public int WarehouseId { get; set; } public int WarehouseId { get; set; }
public Warehouse? Warehouse { get; set; } public Warehouse? Warehouse { get; set; }
public int? BatchId { get; set; }
public Batch? Batch { get; set; }
public int? SerialId { get; set; }
public Serial? Serial { get; set; }
public decimal UnitPrice { get; set; } public decimal UnitPrice { get; set; }
public decimal BaseCost { get; set; } public decimal BaseCost { get; set; }
public string PriceSource { get; set; } = string.Empty; public string PriceSource { get; set; } = string.Empty;
@@ -13,4 +13,7 @@ public class Serial
public string SerialNo { get; set; } = string.Empty; public string SerialNo { get; set; } = string.Empty;
public string Status { get; set; } = "InStock"; public string Status { get; set; } = "InStock";
/// <summary>Optional scan code for barcode/RFID lookup (unique when set).</summary>
public string? Barcode { get; set; }
} }
@@ -16,6 +16,12 @@ public class StockCountLine
public int? BinId { get; set; } public int? BinId { get; set; }
public int? BatchId { get; set; }
public Batch? Batch { get; set; }
public int? SerialId { get; set; }
public Serial? Serial { get; set; }
public decimal SystemQty { get; set; } public decimal SystemQty { get; set; }
public decimal? CountedQty { get; set; } public decimal? CountedQty { get; set; }
public decimal? Variance { get; set; } public decimal? Variance { get; set; }
@@ -0,0 +1,25 @@
using ERPCore.Domain.Enums;
namespace ERPCore.Domain.Entities;
/// <summary>
/// A help-desk ticket raised by a user from the Help page. Status is
/// admin-managed; the conversation is a flat thread of <see cref="SupportTicketMessage"/> rows.
/// Field/concurrency shape follows the same conventions as <see cref="Employee"/>
/// (CreatedBy/UpdatedBy actor stamps, RowVersion for optimistic concurrency).
/// </summary>
public class SupportTicket
{
public int TicketId { get; set; }
public string Subject { get; set; } = string.Empty;
public TicketStatus Status { get; set; } = TicketStatus.Open;
public int CreatedBy { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public int? UpdatedBy { get; set; }
public DateTime? UpdatedAt { get; set; }
public uint RowVersion { get; set; }
public ICollection<SupportTicketMessage> Messages { get; set; } = new List<SupportTicketMessage>();
}
@@ -0,0 +1,16 @@
namespace ERPCore.Domain.Entities;
/// <summary>One message in a <see cref="SupportTicket"/> thread — from the submitter or an admin.</summary>
public class SupportTicketMessage
{
public int MessageId { get; set; }
public int TicketId { get; set; }
public SupportTicket? Ticket { get; set; }
public int SenderUserId { get; set; }
public bool IsFromAdmin { get; set; }
public string Message { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
@@ -0,0 +1,27 @@
using ERPCore.Domain.Enums;
namespace ERPCore.Domain.Entities;
/// <summary>
/// A named tax rate (e.g. "Standard" 15%, "Zero-Rated" 0%, "Exempt" 0%) — the real backing
/// for <see cref="Item.TaxRateId"/>. Before this existed, <c>Item.TaxClass</c> was a free-text
/// label nothing ever read, and every sales/purchase line carried its own hand-typed tax
/// percentage with no link back to the item at all. Now a line's tax defaults to whatever
/// percentage the item's assigned TaxRate carries at the moment the line is created — change
/// the rate here and every new document picks it up; a line already created keeps the percentage
/// it was given (SalesSlipLine.TaxPct etc. are snapshots, never recomputed from this table).
/// </summary>
public class TaxRate
{
public int TaxRateId { get; set; }
public string Code { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public decimal Percentage { get; set; }
public EntityStatus Status { get; set; } = EntityStatus.Active;
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
/// <summary>PostgreSQL xmin-backed optimistic concurrency token (ETag source).</summary>
public uint RowVersion { get; set; }
}
+6
View File
@@ -1,3 +1,5 @@
using ERPCore.Domain.Enums;
namespace ERPCore.Domain.Entities; namespace ERPCore.Domain.Entities;
/// <summary> /// <summary>
@@ -10,4 +12,8 @@ public class Uom
{ {
public int UomId { get; set; } public int UomId { get; set; }
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public EntityStatus Status { get; set; } = EntityStatus.Active;
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
} }
@@ -17,6 +17,9 @@ public class Vendor
public string Currency { get; set; } = "LKR"; public string Currency { get; set; } = "LKR";
public EntityStatus Status { get; set; } = EntityStatus.Active; public EntityStatus Status { get; set; } = EntityStatus.Active;
public int BranchId { get; set; }
public Branch? Branch { get; set; }
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; } public DateTime? UpdatedAt { get; set; }
@@ -9,6 +9,8 @@ public class Warehouse
public int WarehouseId { get; set; } public int WarehouseId { get; set; }
public string Code { get; set; } = string.Empty; public string Code { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public string? Address { get; set; }
public string? Phone { get; set; }
public int BranchId { get; set; } public int BranchId { get; set; }
public Branch? Branch { get; set; } public Branch? Branch { get; set; }
+4 -1
View File
@@ -5,5 +5,8 @@ public enum AuditAction
{ {
Create, Create,
Update, Update,
Delete Delete,
/// <summary>A successful sign-in — written explicitly (not via ErpDbContext's automatic
/// entity-change capture, since a login mutates nothing) by AuthController.</summary>
Login
} }
@@ -1,9 +1,10 @@
namespace ERPCore.Domain.Enums; namespace ERPCore.Domain.Enums;
/// <summary> /// <summary>
/// Purchase-order lifecycle (docs/11 §8; docs/10 §B.8.1). Phase 1 auto-approves on /// Purchase-order lifecycle (docs/11 §8; docs/10 §B.8.1). Issuing a PO (create
/// creation, so <see cref="PendingApproval"/> is reserved (not entered) until the /// non-draft, or submit a Draft) enters <see cref="PendingApproval"/>; an Admin who
/// approval workflow is enabled (FR-PROC-04). Stored as a string. /// isn't the creator must Approve it to reach <see cref="Approved"/>, or Reject it back
/// to <see cref="Draft"/> (FR-PROC-04). Stored as a string.
/// </summary> /// </summary>
public enum PurchaseOrderStatus public enum PurchaseOrderStatus
{ {
@@ -0,0 +1,21 @@
namespace ERPCore.Domain.Enums;
/// <summary>
/// Fine-grained actions a <see cref="Entities.RolePermission"/> can grant within a module
/// it already has nav-level access to (see <see cref="Infra.Auth.NavPolicies"/>). Nav-level
/// access answers "can this role reach this module"; this answers "which mutations within
/// it can this role actually perform" — e.g. a role that can see Procurement but should not
/// be able to delete or approve purchase orders.
/// </summary>
[Flags]
public enum RolePermissionAction
{
None = 0,
View = 1,
Create = 2,
Edit = 4,
Delete = 8,
Approve = 16,
All = View | Create | Edit | Delete | Approve
}
@@ -0,0 +1,8 @@
namespace ERPCore.Domain.Enums;
public enum TicketStatus
{
Open,
InProgress,
Resolved
}
+4
View File
@@ -10,3 +10,7 @@ public sealed record AuditLogDto(
/// <summary>A GL-ready journal stub emitted per stock movement (FR-STK-13).</summary> /// <summary>A GL-ready journal stub emitted per stock movement (FR-STK-13).</summary>
public sealed record JournalEntryStubDto( public sealed record JournalEntryStubDto(
int JournalId, string SourceDocType, int SourceDocId, string DebitAccount, string CreditAccount, decimal Amount); int JournalId, string SourceDocType, int SourceDocId, string DebitAccount, string CreditAccount, decimal Amount);
/// <summary>A captured past sign-in — device history, not a live "currently active sessions" view.</summary>
public sealed record LoginActivityDto(
int LoginActivityId, string? IpAddress, string? DeviceLabel, DateTime LoginAt);
+15
View File
@@ -29,6 +29,21 @@ public sealed class AuthSessionResponse
public int ExpiresIn { get; set; } public int ExpiresIn { get; set; }
} }
/// <summary>Result of AuthHex's admin-only deleteUser function.</summary>
public sealed class DeleteAuthHexUserResult
{
public int DeletedCount { get; set; }
public List<DeletedAuthHexUser>? Deleted { get; set; }
}
public sealed class DeletedAuthHexUser
{
public Guid Id { get; set; }
public string? FullName { get; set; }
public string? UserName { get; set; }
public string? Email { get; set; }
}
public sealed class RegisterRequest public sealed class RegisterRequest
{ {
/// <summary>Optional — AuthHex requires a client-supplied id; ERPCore generates one when omitted.</summary> /// <summary>Optional — AuthHex requires a client-supplied id; ERPCore generates one when omitted.</summary>
@@ -4,7 +4,7 @@ namespace ERPCore.Dtos.Config;
/// <summary>Branch configuration resource — the singleton toggle for whether this deployment /// <summary>Branch configuration resource — the singleton toggle for whether this deployment
/// requires a branch on Employee/Warehouse records at all.</summary> /// requires a branch on Employee/Warehouse records at all.</summary>
public sealed record BranchConfigDto(bool BranchingEnabled, DateTime? UpdatedAt, int? UpdatedBy); public sealed record BranchConfigDto(bool BranchingEnabled, bool WarehouseBranchingEnabled, DateTime? UpdatedAt, int? UpdatedBy);
/// <summary> /// <summary>
/// Full replacement of the flag. <c>UpdatedBy</c> is derived from the token, never posted. /// Full replacement of the flag. <c>UpdatedBy</c> is derived from the token, never posted.
@@ -17,4 +17,5 @@ public sealed record BranchConfigDto(bool BranchingEnabled, DateTime? UpdatedAt,
public sealed class UpdateBranchConfigRequest public sealed class UpdateBranchConfigRequest
{ {
[Required] public bool? BranchingEnabled { get; set; } [Required] public bool? BranchingEnabled { get; set; }
[Required] public bool? WarehouseBranchingEnabled { get; set; }
} }
@@ -21,6 +21,7 @@ public sealed record CustomerDto(
int CreditDays, int CreditDays,
int? DefaultWarehouseId, int? DefaultWarehouseId,
EntityStatus Status, EntityStatus Status,
int BranchId,
DateTime CreatedAt, DateTime CreatedAt,
DateTime? UpdatedAt); DateTime? UpdatedAt);
@@ -40,6 +41,7 @@ public sealed class CreateCustomerRequest
[Range(0, double.MaxValue)] public decimal CreditLimit { get; set; } [Range(0, double.MaxValue)] public decimal CreditLimit { get; set; }
[Range(0, int.MaxValue)] public int CreditDays { get; set; } [Range(0, int.MaxValue)] public int CreditDays { get; set; }
public int? DefaultWarehouseId { get; set; } public int? DefaultWarehouseId { get; set; }
public int BranchId { get; set; }
} }
public sealed class UpdateCustomerRequest public sealed class UpdateCustomerRequest
@@ -58,6 +60,7 @@ public sealed class UpdateCustomerRequest
[Range(0, double.MaxValue)] public decimal CreditLimit { get; set; } [Range(0, double.MaxValue)] public decimal CreditLimit { get; set; }
[Range(0, int.MaxValue)] public int CreditDays { get; set; } [Range(0, int.MaxValue)] public int CreditDays { get; set; }
public int? DefaultWarehouseId { get; set; } public int? DefaultWarehouseId { get; set; }
public int BranchId { get; set; }
} }
public sealed class UpdateCustomerStatusRequest public sealed class UpdateCustomerStatusRequest
@@ -4,9 +4,12 @@ namespace ERPCore.Dtos.Dashboard;
/// Aggregate counts for the dashboard overview — mirrors the widget set in /// Aggregate counts for the dashboard overview — mirrors the widget set in
/// docs/dashboard-implementation.pdf: reorder alerts, on-hand summary, stock valuation, /// docs/dashboard-implementation.pdf: reorder alerts, on-hand summary, stock valuation,
/// pending-approval POs, pending GRNs, open requisitions, open counts awaiting posting, /// pending-approval POs, pending GRNs, open requisitions, open counts awaiting posting,
/// and open RFQs. Recent movements isn't here — it's just GET /stock/ledger with a small /// open RFQs, pending leave requests, and overdue sales invoices. Also reused as small
/// pageSize, no aggregation needed. A single computed-on-read object, not a stored /// KPI tiles on individual list pages (Purchase Orders, GRN, Requisitions, RFQs, Stock
/// entity — same as reorder alerts (docs/11 §5.7). /// Counts, Reorder Alerts, HRM Leave, Sales Invoices), not just the Dashboard home page.
/// Recent movements isn't here — it's just GET /stock/ledger with a small pageSize, no
/// aggregation needed. A single computed-on-read object, not a stored entity — same as
/// reorder alerts (docs/11 §5.7).
/// </summary> /// </summary>
public sealed record DashboardStatsDto( public sealed record DashboardStatsDto(
int LowStockAlerts, int LowStockAlerts,
@@ -18,7 +21,9 @@ public sealed record DashboardStatsDto(
int PendingGrns, int PendingGrns,
int OpenRequisitions, int OpenRequisitions,
int PendingCounts, int PendingCounts,
int OpenRfqs); int OpenRfqs,
int PendingLeaveRequests,
int OverdueSalesInvoices);
/// <summary>One bar in the Stock Valuation chart — total FIFO layer value for a warehouse.</summary> /// <summary>One bar in the Stock Valuation chart — total FIFO layer value for a warehouse.</summary>
public sealed record WarehouseValuationDto(int WarehouseId, decimal Total); public sealed record WarehouseValuationDto(int WarehouseId, decimal Total);
+3 -2
View File
@@ -63,8 +63,9 @@ public sealed class CreateGrnLineInput
[Range(0, double.MaxValue)] public decimal UnitCost { get; set; } [Range(0, double.MaxValue)] public decimal UnitCost { get; set; }
/// <summary>Trade discount percentage (0–100). Reduces the inventory cost.</summary> /// <summary>Trade discount percentage (0–100). Reduces the inventory cost.</summary>
[Range(0, 100)] public decimal DiscountPct { get; set; } [Range(0, 100)] public decimal DiscountPct { get; set; }
/// <summary>VAT percentage (0–100). Recoverable — does not affect stock value.</summary> /// <summary>VAT percentage (0–100). Recoverable — does not affect stock value. Explicit
[Range(0, 100)] public decimal VatPct { get; set; } /// override; omit (null) to use the item's assigned TaxRate percentage instead.</summary>
[Range(0, 100)] public decimal? VatPct { get; set; }
[EnumDataType(typeof(HoldStatus))] public HoldStatus HoldStatus { get; set; } = HoldStatus.Available; [EnumDataType(typeof(HoldStatus))] public HoldStatus HoldStatus { get; set; } = HoldStatus.Available;
public BatchInput? Batch { get; set; } public BatchInput? Batch { get; set; }
/// <summary> /// <summary>
+2
View File
@@ -3,6 +3,8 @@ using ERPCore.Domain.Enums;
namespace ERPCore.Dtos.Hrm; namespace ERPCore.Dtos.Hrm;
public sealed record EmployeeDeleteResultDto(bool UserAccountDeleted, bool UserAccountDeactivated, bool AuthHexIdentityPurged = false);
public sealed record EmployeeListItemDto( public sealed record EmployeeListItemDto(
int EmployeeId, string EmployeeCode, string FullName, string? Email, int EmployeeId, string EmployeeCode, string FullName, string? Email,
int DepartmentId, string? DepartmentName, int DesignationId, string? DesignationName, int DepartmentId, string? DepartmentName, int DesignationId, string? DesignationName,
+14 -6
View File
@@ -10,10 +10,11 @@ public sealed record ItemListItemDto(
int ItemId, string Sku, string Name, int CategoryId, int? SubCategoryId, int? BrandId, int ItemId, string Sku, string Name, int CategoryId, int? SubCategoryId, int? BrandId,
int BaseUomId, int? DefaultVendorId, StockNature StockNature, TrackingMode TrackingMode, int BaseUomId, int? DefaultVendorId, StockNature StockNature, TrackingMode TrackingMode,
Warranty Warranty, int? WarrantyPeriodMonths, Warranty Warranty, int? WarrantyPeriodMonths,
string? TaxClass, decimal? SellingMinPrice, decimal? SellingMaxPrice, decimal? SellingFlatPrice, int? TaxRateId, string? TaxRateName, decimal? TaxRatePercentage,
decimal? SellingMinPrice, decimal? SellingMaxPrice, decimal? SellingFlatPrice,
decimal? ContentQty, MeasureUnit? ContentUnit, decimal? ContentQty, MeasureUnit? ContentUnit,
decimal? ContentBaseQty, MeasureUnit? ContentBaseUnit, decimal? ContentBaseQty, MeasureUnit? ContentBaseUnit,
EntityStatus Status); EntityStatus Status, string? Barcode);
/// <summary>A single per-warehouse reorder policy row.</summary> /// <summary>A single per-warehouse reorder policy row.</summary>
public sealed record ItemReorderDto(int WarehouseId, decimal ReorderPoint, decimal ReorderQty); public sealed record ItemReorderDto(int WarehouseId, decimal ReorderPoint, decimal ReorderQty);
@@ -31,10 +32,11 @@ public sealed record ItemDetailDto(
int? SubCategoryId, int? BrandId, int BaseUomId, int? DefaultVendorId, int? SubCategoryId, int? BrandId, int BaseUomId, int? DefaultVendorId,
StockNature StockNature, TrackingMode TrackingMode, StockNature StockNature, TrackingMode TrackingMode,
Warranty Warranty, int? WarrantyPeriodMonths, Warranty Warranty, int? WarrantyPeriodMonths,
string? TaxClass, decimal? SellingMinPrice, decimal? SellingMaxPrice, decimal? SellingFlatPrice, int? TaxRateId, string? TaxRateName, decimal? TaxRatePercentage,
decimal? SellingMinPrice, decimal? SellingMaxPrice, decimal? SellingFlatPrice,
decimal? ContentQty, MeasureUnit? ContentUnit, decimal? ContentQty, MeasureUnit? ContentUnit,
decimal? ContentBaseQty, MeasureUnit? ContentBaseUnit, decimal? ContentBaseQty, MeasureUnit? ContentBaseUnit,
EntityStatus Status, IReadOnlyList<ItemReorderDto> Reorder, EntityStatus Status, string? Barcode, IReadOnlyList<ItemReorderDto> Reorder,
DateTime CreatedAt, DateTime? UpdatedAt); DateTime CreatedAt, DateTime? UpdatedAt);
/// <summary>Response body for <c>PUT /items/{id}/reorder</c>.</summary> /// <summary>Response body for <c>PUT /items/{id}/reorder</c>.</summary>
@@ -64,7 +66,8 @@ public sealed class CreateItemRequest
[EnumDataType(typeof(Warranty))] public Warranty Warranty { get; set; } = Warranty.NonWarranty; [EnumDataType(typeof(Warranty))] public Warranty Warranty { get; set; } = Warranty.NonWarranty;
/// <summary>Required (one of <see cref="Enums.WarrantyPeriods.AllowedMonths"/>) when <see cref="Warranty"/> is Warranty; ignored otherwise.</summary> /// <summary>Required (one of <see cref="Enums.WarrantyPeriods.AllowedMonths"/>) when <see cref="Warranty"/> is Warranty; ignored otherwise.</summary>
public int? WarrantyPeriodMonths { get; set; } public int? WarrantyPeriodMonths { get; set; }
[StringLength(20)] public string? TaxClass { get; set; } /// <summary>The tax rate sales/purchase lines for this item default to — see <see cref="Domain.Entities.TaxRate"/>.</summary>
public int? TaxRateId { get; set; }
/// <summary>Optional fixed sale price (Sales only). Null ⇒ sell at stock/FIFO value.</summary> /// <summary>Optional fixed sale price (Sales only). Null ⇒ sell at stock/FIFO value.</summary>
[Range(0, double.MaxValue)] public decimal? SellingMinPrice { get; set; } [Range(0, double.MaxValue)] public decimal? SellingMinPrice { get; set; }
[Range(0, double.MaxValue)] public decimal? SellingMaxPrice { get; set; } [Range(0, double.MaxValue)] public decimal? SellingMaxPrice { get; set; }
@@ -77,6 +80,8 @@ public sealed class CreateItemRequest
/// </summary> /// </summary>
[Range(0.0001, double.MaxValue)] public decimal? ContentQty { get; set; } [Range(0.0001, double.MaxValue)] public decimal? ContentQty { get; set; }
[EnumDataType(typeof(MeasureUnit))] public MeasureUnit? ContentUnit { get; set; } [EnumDataType(typeof(MeasureUnit))] public MeasureUnit? ContentUnit { get; set; }
/// <summary>Optional scan code for barcode/RFID lookup. Unlike <see cref="Sku"/>, not required.</summary>
[StringLength(100)] public string? Barcode { get; set; }
} }
public sealed class UpdateItemRequest public sealed class UpdateItemRequest
@@ -96,7 +101,8 @@ public sealed class UpdateItemRequest
[EnumDataType(typeof(Warranty))] public Warranty Warranty { get; set; } = Warranty.NonWarranty; [EnumDataType(typeof(Warranty))] public Warranty Warranty { get; set; } = Warranty.NonWarranty;
/// <summary>Required (one of <see cref="Enums.WarrantyPeriods.AllowedMonths"/>) when <see cref="Warranty"/> is Warranty; ignored otherwise.</summary> /// <summary>Required (one of <see cref="Enums.WarrantyPeriods.AllowedMonths"/>) when <see cref="Warranty"/> is Warranty; ignored otherwise.</summary>
public int? WarrantyPeriodMonths { get; set; } public int? WarrantyPeriodMonths { get; set; }
[StringLength(20)] public string? TaxClass { get; set; } /// <summary>The tax rate sales/purchase lines for this item default to — see <see cref="Domain.Entities.TaxRate"/>.</summary>
public int? TaxRateId { get; set; }
/// <summary>Optional fixed sale price (Sales only). Null ⇒ sell at stock/FIFO value.</summary> /// <summary>Optional fixed sale price (Sales only). Null ⇒ sell at stock/FIFO value.</summary>
[Range(0, double.MaxValue)] public decimal? SellingMinPrice { get; set; } [Range(0, double.MaxValue)] public decimal? SellingMinPrice { get; set; }
[Range(0, double.MaxValue)] public decimal? SellingMaxPrice { get; set; } [Range(0, double.MaxValue)] public decimal? SellingMaxPrice { get; set; }
@@ -109,6 +115,8 @@ public sealed class UpdateItemRequest
/// </summary> /// </summary>
[Range(0.0001, double.MaxValue)] public decimal? ContentQty { get; set; } [Range(0.0001, double.MaxValue)] public decimal? ContentQty { get; set; }
[EnumDataType(typeof(MeasureUnit))] public MeasureUnit? ContentUnit { get; set; } [EnumDataType(typeof(MeasureUnit))] public MeasureUnit? ContentUnit { get; set; }
/// <summary>Optional scan code for barcode/RFID lookup. Unlike <see cref="Sku"/>, not required.</summary>
[StringLength(100)] public string? Barcode { get; set; }
} }
public sealed class UpdateItemStatusRequest public sealed class UpdateItemStatusRequest
@@ -0,0 +1,11 @@
using ERPCore.Domain.Enums;
namespace ERPCore.Dtos.Items;
/// <summary>Result of a barcode/RFID scan lookup — an item, or an item resolved via a
/// specific batch/serial. See <see cref="Services.Interfaces.IItemService.ScanAsync"/>.</summary>
public sealed record ItemScanResultDto(
int ItemId, string Sku, string Name, TrackingMode TrackingMode, int BaseUomId,
string MatchedBy, // "Item" | "Batch" | "Serial"
int? BatchId, string? BatchNo, DateOnly? ExpiryDate,
int? SerialId, string? SerialNo, string? SerialStatus);
@@ -0,0 +1,15 @@
namespace ERPCore.Dtos.Notifications;
public sealed record NotificationDto(
int NotificationId,
string ModuleKey,
string Title,
string Message,
string? RelatedEntityType,
int? RelatedEntityId,
DateTime CreatedAt,
bool IsRead);
public sealed record NotificationFeedDto(
IReadOnlyList<NotificationDto> Notifications,
int UnreadCount);
@@ -12,14 +12,14 @@ public sealed record PurchaseInvoiceTotalsDto(
public sealed record PurchaseInvoiceDto( public sealed record PurchaseInvoiceDto(
int PurchaseInvoiceId, string DocNo, string VendorInvoiceNo, DateTime VendorInvoiceDate, int PurchaseInvoiceId, string DocNo, string VendorInvoiceNo, DateTime VendorInvoiceDate,
int VendorId, string VendorSnapshotName, string? VendorSnapshotTaxNo, int VendorId, int BranchId, string VendorSnapshotName, string? VendorSnapshotTaxNo,
PurchaseInvoiceStatus Status, int GrnCount, int CreatedBy, DateTime CreatedAt, DateTime? UpdatedAt, PurchaseInvoiceStatus Status, int GrnCount, int CreatedBy, DateTime CreatedAt, DateTime? UpdatedAt,
string? GlJournalNo, DateTime? GlPostedAt, string? GlJournalNo, DateTime? GlPostedAt,
PurchaseInvoiceTotalsDto Totals, IReadOnlyList<PurchaseInvoiceLineDto> Lines); PurchaseInvoiceTotalsDto Totals, IReadOnlyList<PurchaseInvoiceLineDto> Lines);
public sealed record PurchaseInvoiceSummaryDto( public sealed record PurchaseInvoiceSummaryDto(
int PurchaseInvoiceId, string DocNo, string VendorInvoiceNo, DateTime VendorInvoiceDate, int PurchaseInvoiceId, string DocNo, string VendorInvoiceNo, DateTime VendorInvoiceDate,
int VendorId, string VendorSnapshotName, PurchaseInvoiceStatus Status, int GrnCount, int VendorId, int BranchId, string VendorSnapshotName, PurchaseInvoiceStatus Status, int GrnCount,
PurchaseInvoiceTotalsDto Totals, DateTime CreatedAt, string? GlJournalNo); PurchaseInvoiceTotalsDto Totals, DateTime CreatedAt, string? GlJournalNo);
public sealed record PurchaseInvoicePaymentDto( public sealed record PurchaseInvoicePaymentDto(
@@ -12,13 +12,14 @@ public sealed record PoLineDto(
public sealed record PoTotalsDto(decimal SubTotal, decimal Tax, decimal GrandTotal, string Currency); public sealed record PoTotalsDto(decimal SubTotal, decimal Tax, decimal GrandTotal, string Currency);
public sealed record PurchaseOrderDto( public sealed record PurchaseOrderDto(
int PoId, string DocNo, int VendorId, int? RequisitionId, PurchaseOrderStatus Status, int PoId, string DocNo, int VendorId, int BranchId, int? RequisitionId, PurchaseOrderStatus Status,
bool ApprovalRequired, int CreatedBy, DateTime CreatedAt, DateTime? UpdatedAt, bool ApprovalRequired, int CreatedBy, int? ApprovedBy, DateTime? ApprovedAt,
DateTime CreatedAt, DateTime? UpdatedAt,
PoTotalsDto Totals, IReadOnlyList<PoLineDto> Lines); PoTotalsDto Totals, IReadOnlyList<PoLineDto> Lines);
public sealed record PurchaseOrderSummaryDto( public sealed record PurchaseOrderSummaryDto(
int PoId, string DocNo, int VendorId, PurchaseOrderStatus Status, int PoId, string DocNo, int VendorId, int BranchId, PurchaseOrderStatus Status,
bool ApprovalRequired, DateTime CreatedAt, PoTotalsDto Totals); bool ApprovalRequired, int CreatedBy, DateTime CreatedAt, PoTotalsDto Totals);
// Requests — server sets docNo, status, createdBy, timestamps, qtyReceived, totals // Requests — server sets docNo, status, createdBy, timestamps, qtyReceived, totals
@@ -35,12 +36,13 @@ public sealed class CreatePurchaseOrderRequest
{ {
[Required] public int VendorId { get; set; } [Required] public int VendorId { get; set; }
public int? RequisitionId { get; set; } public int? RequisitionId { get; set; }
public int BranchId { get; set; }
[Required, MinLength(1)] public List<CreatePoLineInput> Lines { get; set; } = new(); [Required, MinLength(1)] public List<CreatePoLineInput> Lines { get; set; } = new();
/// <summary> /// <summary>
/// When true the PO is created in <c>Draft</c> (editable/deletable, not yet issued). /// When true the PO is created in <c>Draft</c> (editable/deletable, not yet issued).
/// When false (default) it auto-approves on creation, preserving the Requisition→PO /// When false (default) it is issued immediately into <c>PendingApproval</c>, same as
/// and RFQ→PO flows unchanged (docs/11 §3.3). /// submitting a Draft (docs/11 §3.3).
/// </summary> /// </summary>
public bool SaveAsDraft { get; set; } public bool SaveAsDraft { get; set; }
} }
@@ -49,6 +51,7 @@ public sealed class UpdatePurchaseOrderRequest
{ {
[Required] public int VendorId { get; set; } [Required] public int VendorId { get; set; }
public int? RequisitionId { get; set; } public int? RequisitionId { get; set; }
public int BranchId { get; set; }
[Required, MinLength(1)] public List<CreatePoLineInput> Lines { get; set; } = new(); [Required, MinLength(1)] public List<CreatePoLineInput> Lines { get; set; } = new();
} }
@@ -56,3 +59,8 @@ public sealed class CancelPurchaseOrderRequest
{ {
[StringLength(500)] public string? Reason { get; set; } [StringLength(500)] public string? Reason { get; set; }
} }
public sealed class RejectPurchaseOrderRequest
{
[StringLength(500)] public string? Reason { get; set; }
}
@@ -8,11 +8,11 @@ namespace ERPCore.Dtos.Procurement;
public sealed record RequisitionLineDto(int ReqLineId, int ItemId, decimal Qty, DateOnly? RequiredBy); public sealed record RequisitionLineDto(int ReqLineId, int ItemId, decimal Qty, DateOnly? RequiredBy);
public sealed record RequisitionDto( public sealed record RequisitionDto(
int RequisitionId, string DocNo, RequisitionStatus Status, int RequestedBy, int RequisitionId, string DocNo, RequisitionStatus Status, int RequestedBy, int BranchId,
DateTime CreatedAt, IReadOnlyList<RequisitionLineDto> Lines); DateTime CreatedAt, IReadOnlyList<RequisitionLineDto> Lines);
public sealed record RequisitionSummaryDto( public sealed record RequisitionSummaryDto(
int RequisitionId, string DocNo, RequisitionStatus Status, int RequestedBy, DateTime CreatedAt, int LineCount); int RequisitionId, string DocNo, RequisitionStatus Status, int RequestedBy, int BranchId, DateTime CreatedAt, int LineCount);
// Requests — server sets docNo, status, requestedBy (audit actor), timestamps ---- // Requests — server sets docNo, status, requestedBy (audit actor), timestamps ----
@@ -26,4 +26,7 @@ public sealed class CreateRequisitionLineInput
public sealed class CreateRequisitionRequest public sealed class CreateRequisitionRequest
{ {
[Required, MinLength(1)] public List<CreateRequisitionLineInput> Lines { get; set; } = new(); [Required, MinLength(1)] public List<CreateRequisitionLineInput> Lines { get; set; } = new();
/// <summary>Optional — resolved via <see cref="Services.Interfaces.IBranchConfigService.ResolveBranchIdAsync"/>
/// when branching is enabled; ignored (defaulted) otherwise.</summary>
public int BranchId { get; set; }
} }
+2 -2
View File
@@ -8,11 +8,11 @@ namespace ERPCore.Dtos.Procurement;
public sealed record RfqLineDto(int RfqLineId, int ItemId, decimal Qty); public sealed record RfqLineDto(int RfqLineId, int ItemId, decimal Qty);
public sealed record RfqDto( public sealed record RfqDto(
int RfqId, string DocNo, int RequisitionId, RfqStatus Status, IReadOnlyList<RfqLineDto> Lines); int RfqId, string DocNo, int RequisitionId, int BranchId, RfqStatus Status, IReadOnlyList<RfqLineDto> Lines);
/// <summary>Row shape for <c>GET /rfqs</c> — line/quotation counts instead of the lines themselves.</summary> /// <summary>Row shape for <c>GET /rfqs</c> — line/quotation counts instead of the lines themselves.</summary>
public sealed record RfqSummaryDto( public sealed record RfqSummaryDto(
int RfqId, string DocNo, int RequisitionId, RfqStatus Status, int LineCount, int QuotationCount); int RfqId, string DocNo, int RequisitionId, int BranchId, RfqStatus Status, int LineCount, int QuotationCount);
public sealed record QuotationLineDto(int ItemId, decimal UnitPrice, int LeadDays); public sealed record QuotationLineDto(int ItemId, decimal UnitPrice, int LeadDays);
+1 -1
View File
@@ -11,4 +11,4 @@ namespace ERPCore.Dtos.Rbac;
/// entirely rather than merely restricting them by role.</summary> /// entirely rather than merely restricting them by role.</summary>
public sealed record MeResponseDto( public sealed record MeResponseDto(
string? RoleCode, string? RoleName, IReadOnlyList<string> NavCodes, string? RoleCode, string? RoleName, IReadOnlyList<string> NavCodes,
bool IsAdmin, int? BranchId, string? BranchName, bool BranchingEnabled); bool IsAdmin, int? BranchId, string? BranchName, bool BranchingEnabled, bool WarehouseBranchingEnabled);
+17 -1
View File
@@ -29,6 +29,22 @@ public sealed class AssignRolePermissionsRequest
{ {
public List<int> NavItemIds { get; set; } = new(); public List<int> NavItemIds { get; set; } = new();
public List<int> SubNavItemIds { get; set; } = new(); public List<int> SubNavItemIds { get; set; } = new();
/// <summary>
/// Optional per-nav/sub-nav-item action override, keyed by <c>NavItemId</c>/
/// <c>SubNavItemId</c> and valued with a <see cref="RolePermissionAction"/> bitmask
/// (e.g. <c>View|Create|Edit</c> = 7 to grant a module without Delete/Approve). An id
/// present in <see cref="NavItemIds"/>/<see cref="SubNavItemIds"/> but absent here
/// keeps today's default of <see cref="RolePermissionAction.All"/> — this only narrows,
/// never grants access to an id that isn't also in those lists.
/// </summary>
public Dictionary<int, RolePermissionAction> NavItemActions { get; set; } = new();
public Dictionary<int, RolePermissionAction> SubNavItemActions { get; set; } = new();
} }
public sealed record RolePermissionsDto(int RoleId, List<int> NavItemIds, List<int> SubNavItemIds); public sealed record RolePermissionsDto(
int RoleId,
List<int> NavItemIds,
List<int> SubNavItemIds,
Dictionary<int, RolePermissionAction> NavItemActions,
Dictionary<int, RolePermissionAction> SubNavItemActions);
@@ -20,19 +20,21 @@ public sealed record SalesDayEndInvoiceSummaryDto(
public sealed record SalesDayEndDto( public sealed record SalesDayEndDto(
int SalesDayEndId, string DocNo, int CashierUserId, DateOnly BusinessDate, int SalesDayEndId, string DocNo, int CashierUserId, DateOnly BusinessDate,
int SlipCount, int BundleCount, decimal Subtotal, decimal DiscountTotal, decimal TaxTotal, decimal GrandTotal, int SlipCount, int BundleCount, decimal Subtotal, decimal DiscountTotal, decimal TaxTotal, decimal GrandTotal,
decimal PaidTotal, decimal BalanceTotal,
decimal CostOfGoodsSold, string? GlJournalNo, DateTime? GlPostedAt, int ClosedBy, DateTime ClosedAt, decimal CostOfGoodsSold, string? GlJournalNo, DateTime? GlPostedAt, int ClosedBy, DateTime ClosedAt,
IReadOnlyList<string> SlipNumbers, IReadOnlyList<string> BundleNumbers, IReadOnlyList<SalesDayEndItemLineDto> ItemBreakdown, IReadOnlyList<string> SlipNumbers, IReadOnlyList<string> BundleNumbers, IReadOnlyList<SalesDayEndItemLineDto> ItemBreakdown,
SalesDayEndInvoiceSummaryDto Invoices); SalesDayEndInvoiceSummaryDto Invoices);
public sealed record SalesDayEndSummaryDto( public sealed record SalesDayEndSummaryDto(
int SalesDayEndId, string DocNo, int CashierUserId, DateOnly BusinessDate, int SalesDayEndId, string DocNo, int CashierUserId, DateOnly BusinessDate,
int SlipCount, int BundleCount, decimal GrandTotal, string? GlJournalNo, DateTime ClosedAt); int SlipCount, int BundleCount, decimal GrandTotal, decimal PaidTotal, decimal BalanceTotal, string? GlJournalNo, DateTime ClosedAt);
/// <summary>What closing right now would include — call before <c>POST /sales-day-end</c> so the /// <summary>What closing right now would include — call before <c>POST /sales-day-end</c> so the
/// cashier can see the day's totals, and any still-Draft slips/bundles blocking the close.</summary> /// cashier can see the day's totals, and any still-Draft slips/bundles blocking the close.</summary>
public sealed record SalesDayEndPreviewDto( public sealed record SalesDayEndPreviewDto(
int CashierUserId, DateOnly BusinessDate, bool AlreadyClosed, int CashierUserId, DateOnly BusinessDate, bool AlreadyClosed,
int SlipCount, int BundleCount, decimal Subtotal, decimal DiscountTotal, decimal TaxTotal, decimal GrandTotal, int SlipCount, int BundleCount, decimal Subtotal, decimal DiscountTotal, decimal TaxTotal, decimal GrandTotal,
decimal PaidTotal, decimal BalanceTotal,
IReadOnlyList<string> SlipNumbers, IReadOnlyList<string> BundleNumbers, IReadOnlyList<string> SlipNumbers, IReadOnlyList<string> BundleNumbers,
IReadOnlyList<DraftSlipBlockingCloseDto> DraftSlipsBlockingClose, IReadOnlyList<DraftSlipBlockingCloseDto> DraftSlipsBlockingClose,
IReadOnlyList<DraftBundleBlockingCloseDto> DraftBundleSalesBlockingClose, IReadOnlyList<DraftBundleBlockingCloseDto> DraftBundleSalesBlockingClose,
@@ -56,7 +56,9 @@ public sealed class CreateSalesInvoiceLineRequest
[Range(0, 100)] public decimal DiscountPct { get; set; } [Range(0, 100)] public decimal DiscountPct { get; set; }
[Range(0, double.MaxValue)] public decimal DiscountAmount { get; set; } [Range(0, double.MaxValue)] public decimal DiscountAmount { get; set; }
[Range(0, double.MaxValue)] public decimal DiscountValue { get; set; } [Range(0, double.MaxValue)] public decimal DiscountValue { get; set; }
[Range(0, 100)] public decimal TaxPct { get; set; } /// <summary>Explicit override. Omit (null) to use the item's assigned TaxRate — see
/// <see cref="Services.Interfaces.ISalesDomainService.ResolveLineTaxPctAsync"/>.</summary>
[Range(0, 100)] public decimal? TaxPct { get; set; }
public bool IsFreeIssue { get; set; } public bool IsFreeIssue { get; set; }
public int? ParentLineId { get; set; } public int? ParentLineId { get; set; }
} }
+22 -2
View File
@@ -7,7 +7,8 @@ public sealed record SalesSlipLineDto(
int SalesSlipLineId, int ItemId, string Description, decimal Qty, decimal FreeQty, int SalesSlipLineId, int ItemId, string Description, decimal Qty, decimal FreeQty,
int WarehouseId, decimal UnitPrice, decimal BaseCost, string PriceSource, decimal DiscountPct, int WarehouseId, decimal UnitPrice, decimal BaseCost, string PriceSource, decimal DiscountPct,
decimal DiscountAmount, SalesDiscountMode DiscountMode, decimal NetUnitPrice, decimal LineTotal, decimal DiscountAmount, SalesDiscountMode DiscountMode, decimal NetUnitPrice, decimal LineTotal,
decimal TaxPct, decimal TaxAmount, bool IsFreeIssue, int? ParentLineId); decimal TaxPct, decimal TaxAmount, bool IsFreeIssue, int? ParentLineId,
int? BatchId, int? SerialId);
public sealed record SalesSlipTotalsDto( public sealed record SalesSlipTotalsDto(
decimal Subtotal, decimal DiscountTotal, decimal FreeQtyTotal, decimal TaxTotal, decimal GrandTotal, decimal Subtotal, decimal DiscountTotal, decimal FreeQtyTotal, decimal TaxTotal, decimal GrandTotal,
@@ -54,9 +55,15 @@ public sealed class CreateSalesSlipLineRequest
[Range(0, 100)] public decimal DiscountPct { get; set; } [Range(0, 100)] public decimal DiscountPct { get; set; }
[Range(0, double.MaxValue)] public decimal DiscountAmount { get; set; } [Range(0, double.MaxValue)] public decimal DiscountAmount { get; set; }
[Range(0, double.MaxValue)] public decimal DiscountValue { get; set; } [Range(0, double.MaxValue)] public decimal DiscountValue { get; set; }
[Range(0, 100)] public decimal TaxPct { get; set; } /// <summary>Explicit override. Omit (null) to use the item's assigned TaxRate — see
/// <see cref="Services.Interfaces.ISalesDomainService.ResolveLineTaxPctAsync"/>.</summary>
[Range(0, 100)] public decimal? TaxPct { get; set; }
public bool IsFreeIssue { get; set; } public bool IsFreeIssue { get; set; }
public int? ParentLineId { get; set; } public int? ParentLineId { get; set; }
/// <summary>Pins the line to a specific batch (must belong to <see cref="ItemId"/>), consumed via FIFO.</summary>
public int? BatchId { get; set; }
/// <summary>Pins the line to a specific serial (must belong to <see cref="ItemId"/>); marked Sold on posting.</summary>
public int? SerialId { get; set; }
} }
public sealed class CreateSalesSlipRequest public sealed class CreateSalesSlipRequest
@@ -65,6 +72,16 @@ public sealed class CreateSalesSlipRequest
[Required] public int WarehouseId { get; set; } [Required] public int WarehouseId { get; set; }
[Required] public int CashierUserId { get; set; } [Required] public int CashierUserId { get; set; }
[Required, MinLength(1)] public List<CreateSalesSlipLineRequest> Lines { get; set; } = new(); [Required, MinLength(1)] public List<CreateSalesSlipLineRequest> Lines { get; set; } = new();
/// <summary>
/// How much the customer actually paid at ring-up. Omit (or send null) for the default —
/// fully paid, i.e. <c>PaidAmount == GrandTotal</c> and <c>BalanceAmount == 0</c> — matching
/// every slip's behavior before partial payment existed. Must be between 0 and the computed
/// GrandTotal; anything less than GrandTotal leaves the difference as BalanceAmount owed by
/// the customer "on account," which Sales Day-End then splits between Cash and Accounts
/// Receivable when it posts (see SalesDayEndService.CloseAsync).
/// </summary>
[Range(0, double.MaxValue)] public decimal? PaidAmount { get; set; }
} }
public sealed class UpdateSalesSlipRequest public sealed class UpdateSalesSlipRequest
@@ -73,4 +90,7 @@ public sealed class UpdateSalesSlipRequest
[Required] public int WarehouseId { get; set; } [Required] public int WarehouseId { get; set; }
[Required] public int CashierUserId { get; set; } [Required] public int CashierUserId { get; set; }
[Required, MinLength(1)] public List<CreateSalesSlipLineRequest> Lines { get; set; } = new(); [Required, MinLength(1)] public List<CreateSalesSlipLineRequest> Lines { get; set; } = new();
/// <summary>See <see cref="CreateSalesSlipRequest.PaidAmount"/>.</summary>
[Range(0, double.MaxValue)] public decimal? PaidAmount { get; set; }
} }

Some files were not shown because too many files have changed in this diff Show More