Compare commits

...

88 Commits

Author SHA1 Message Date
Sasanka b2646c6cb3 ui and bug fixers 29/09 2026-09-29 14:49:11 +05:30
Sasanka d24072e20a fixing recipts in sales 2026-09-24 17:02:33 +05:30
ashan_rusiru 9185d39c39 Merge pull request 'Dev' (#88) from Dev into production
Reviewed-on: #88
2026-09-24 07:12:59 +00:00
ashan_rusiru a2790c13bb Merge branch 'production' into Dev
CI/CD - ERP Core Monorepo / Build & Deploy Frontend (pull_request) Failing after 13m26s
CI/CD - ERP Core Monorepo / Build & Deploy Backend (pull_request) Failing after 15m18s
2026-09-24 07:12:52 +00:00
ashan_rusiru 940583552a Merge pull request 'Ui fixers and responsivenes' (#87) from ui-fixers-and-responsivenes into Dev
Reviewed-on: #87
2026-09-24 07:09:52 +00:00
Sasanka 049c98298a Ui fixings 2026-09-24 12:30:21 +05:30
Sasanka f5e8e0e5b9 responsiveness and ui fixers 2026-09-23 16:55:46 +05:30
ashan_rusiru f35439e364 Merge pull request 'Dev' (#86) from Dev into production
Reviewed-on: #86
2026-09-21 19:03:14 +00:00
ashan_rusiru 4c5e9b347a Merge branch 'production' into Dev
CI/CD - ERP Core Monorepo / Build & Deploy Frontend (pull_request) Failing after 13m41s
CI/CD - ERP Core Monorepo / Build & Deploy Backend (pull_request) Failing after 15m5s
2026-09-21 19:03:08 +00:00
Dhananjaya99 fdd104284e feat: add ItemOverviewDto for enriched item data in items list
feat: implement CustomerDetailPage for detailed customer view and editing

feat: create CustomersPage for listing and managing customers with filters

feat: add ColumnsMenu component for customizable table columns visibility

feat: implement CSV export functionality for data download

feat: add customer code generation logic for unique customer identifiers
2026-09-22 00:32:40 +05:30
ImanThiyanga 72819e5f7b production change 2026-09-21 18:02:55 +05:30
kalana_Thilakarathna edd1011330 Merge pull request 'chore: update next.js version to ^16.3.5 in package.json' (#85) from Dev into production
Reviewed-on: #85
2026-09-20 12:41:34 +00:00
Kalana B. Thilakarathna 3ae14fad72 chore: update next.js version to ^16.3.5 in package.json
CI/CD - ERP Core Monorepo / Build & Deploy Backend (pull_request) Failing after 11m45s
CI/CD - ERP Core Monorepo / Build & Deploy Frontend (pull_request) Failing after 15m23s
2026-09-20 18:11:04 +05:30
kalana_Thilakarathna 84d3318f76 Merge pull request 'Dev' (#84) from Dev into production
Reviewed-on: #84
2026-09-20 12:27:45 +00:00
kalana_Thilakarathna bfb8e83a82 Update .gitea/workflows/ci-cd.yml
CI/CD - ERP Core Monorepo / Build & Deploy Backend (pull_request) Failing after 10m33s
CI/CD - ERP Core Monorepo / Build & Deploy Frontend (pull_request) Failing after 14m9s
2026-09-20 12:27:31 +00:00
kalana_Thilakarathna 40c1d6da27 Update Frontend/erp-system/Dockerfile 2026-09-20 12:26:57 +00:00
kalana_Thilakarathna 5aa709db17 Merge pull request 'Update .gitea/workflows/ci-cd.yml' (#83) from Dev into production
Reviewed-on: #83
2026-09-20 12:03:21 +00:00
kalana_Thilakarathna cf964a19a9 Update .gitea/workflows/ci-cd.yml
CI/CD - ERP Core Monorepo / Build & Deploy Frontend (pull_request) Failing after 13m37s
CI/CD - ERP Core Monorepo / Build & Deploy Backend (pull_request) Failing after 14m59s
2026-09-20 12:03:07 +00:00
kalana_Thilakarathna a40c6bf598 Merge pull request 'Update .gitea/workflows/ci-cd.yml' (#82) from Dev into production
Reviewed-on: #82
2026-09-20 11:57:04 +00:00
kalana_Thilakarathna f951700b58 Update .gitea/workflows/ci-cd.yml
CI/CD - ERP Core Monorepo / Build & Deploy Backend (pull_request) Failing after 11m16s
CI/CD - ERP Core Monorepo / Build & Deploy Frontend (pull_request) Failing after 14m55s
2026-09-20 11:56:50 +00:00
kalana_Thilakarathna b3939e0685 Merge pull request 'Update .gitea/workflows/ci-cd.yml' (#81) from Dev into production
Reviewed-on: #81
2026-09-20 11:50:14 +00:00
kalana_Thilakarathna af6f02023d Update .gitea/workflows/ci-cd.yml
CI/CD - ERP Core Monorepo / Build & Deploy Frontend (pull_request) Failing after 11m43s
CI/CD - ERP Core Monorepo / Build & Deploy Backend (pull_request) Failing after 13m5s
2026-09-20 11:49:55 +00:00
kalana_Thilakarathna 9d569a1504 Merge pull request 'Update .gitea/workflows/ci-cd.yml' (#80) from Dev into production
Reviewed-on: #80
2026-09-20 11:41:23 +00:00
kalana_Thilakarathna 5a44702cf3 Update .gitea/workflows/ci-cd.yml
CI/CD - ERP Core Monorepo / Build & Deploy Frontend (pull_request) Failing after 10m33s
CI/CD - ERP Core Monorepo / Build & Deploy Backend (pull_request) Failing after 11m56s
2026-09-20 11:41:10 +00:00
kalana_Thilakarathna 2a5dee3fef Merge pull request 'Update .gitea/workflows/ci-cd.yml' (#79) from Dev into production
Reviewed-on: #79
2026-09-20 11:34:43 +00:00
kalana_Thilakarathna 2584ab8715 Update .gitea/workflows/ci-cd.yml
CI/CD - ERP Core Monorepo / Build & Deploy Frontend (pull_request) Failing after 12m8s
CI/CD - ERP Core Monorepo / Build & Deploy Backend (pull_request) Failing after 13m36s
2026-09-20 11:34:29 +00:00
ImanThiyanga 6b9d191bf8 Merge pull request 'fixes on ci-cd' (#78) from Dev into production
Reviewed-on: #78
2026-09-18 19:32:33 +00:00
ImanThiyanga 283fa21e87 Merge branch 'production' into Dev
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Backend (pull_request) Failing after 10m46s
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Frontend (pull_request) Failing after 14m22s
2026-09-18 19:32:25 +00:00
ImanThiyanga 97d19d83ab fixes on ci-cd 2026-09-19 01:01:30 +05:30
ImanThiyanga 01d5159d27 Merge pull request 'changed front needs' (#77) from Dev into production
Reviewed-on: #77
2026-09-18 19:21:39 +00:00
ImanThiyanga 80899ed88c Merge branch 'production' into Dev
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Frontend (pull_request) Failing after 10m18s
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Backend (pull_request) Failing after 11m40s
2026-09-18 19:21:32 +00:00
ImanThiyanga cfe2466fec changed front needs 2026-09-19 00:51:11 +05:30
ImanThiyanga 9a124c8c63 Merge pull request 'fixed ci-cd' (#76) from Dev into production
Reviewed-on: #76
2026-09-18 19:05:55 +00:00
ImanThiyanga 42ab7e5964 Merge branch 'production' into Dev
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Backend (pull_request) Failing after 12m25s
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Frontend (pull_request) Failing after 14m58s
2026-09-18 19:05:48 +00:00
ImanThiyanga e7735135ca fixed ci-cd 2026-09-19 00:35:05 +05:30
ImanThiyanga f61d1f95b8 Merge pull request 'Dev' (#75) from Dev into production
Reviewed-on: #75
2026-09-18 18:54:54 +00:00
ImanThiyanga f632c5fc99 Merge branch 'production' into Dev
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Backend (pull_request) Failing after 13m26s
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Frontend (pull_request) Failing after 15m0s
2026-09-18 18:54:46 +00:00
ImanThiyanga 70179e1abc Merge pull request 'new updates' (#74) from new-updates into Dev
Reviewed-on: #74
2026-09-18 18:53:13 +00:00
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 7274b89b28 Merge pull request 'migrations fix' (#72) from Dev into production
Reviewed-on: #72
2026-09-18 18:23:44 +00:00
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
Sasanka 629f3d1f44 bug fixing 2026-09-09 14:38:53 +05:30
ImanThiyanga b3b6a25ce8 Merge pull request 'Branch crud' (#52) from branch-crud into Dev
Reviewed-on: #52
2026-09-08 07:13:47 +00:00
Sasanka f664961fce branch crud and intergration 2026-09-08 12:13:49 +05:30
ImanThiyanga b4993b202d Merge pull request 'Dev' (#45) from Dev into production
Reviewed-on: #45
2026-08-13 06:52:56 +00:00
514 changed files with 248715 additions and 6357 deletions
Submodule .claude/worktrees/agent-a98805dc618e0f67c added at b4993b202d
+92
View File
@@ -0,0 +1,92 @@
name: CI/CD - ERP Core Monorepo
on:
pull_request:
branches:
- production
types:
- closed
jobs:
build-and-deploy-backend:
name: Build & Deploy 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
run: dotnet restore Backend/ERPCore.sln
- name: Publish
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 service
run: systemctl stop erpcore.service
- name: Copy 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 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 service
run: systemctl restart erpcore.service
- name: Verify
run: |
systemctl is-active erpcore.service && echo "✅ Service running" || echo "❌ Service failed"
build-and-deploy-frontend:
name: Build & Deploy Frontend
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: Build Docker image
run: |
docker build --progress=plain --no-cache \
-t erpcore-frontend:latest \
./Frontend/erp-system
- name: Stop & remove existing container
run: |
docker stop erpcore-frontend 2>/dev/null || true
docker rm erpcore-frontend 2>/dev/null || true
- name: Start 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 images
run: docker image prune -f
- name: Verify
run: |
docker ps --filter name=erpcore-frontend --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" \
| grep erpcore-frontend && echo "✅ Container running" || echo "❌ Container failed"
+16 -1
View File
@@ -27,13 +27,21 @@ 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;
private readonly IUserManagementService _userManagement;
private readonly ICurrentUser _currentUser;
public AuthController(IAuthUserService users, IAuthRecoveryService recovery, IAuthAltService alt, IRoleService roles) public AuthController(
IAuthUserService users, IAuthRecoveryService recovery, IAuthAltService alt, IRoleService roles, IAuditService audit,
IUserManagementService userManagement, ICurrentUser currentUser)
{ {
_users = users; _users = users;
_recovery = recovery; _recovery = recovery;
_alt = alt; _alt = alt;
_roles = roles; _roles = roles;
_audit = audit;
_userManagement = userManagement;
_currentUser = currentUser;
} }
/// <summary> /// <summary>
@@ -68,6 +76,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 +87,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)]
@@ -132,6 +145,8 @@ public sealed class AuthController : ControllerBase
public async Task<IActionResult> ChangePassword([FromBody] ChangeUserPasswordRequest request, CancellationToken ct) public async Task<IActionResult> ChangePassword([FromBody] ChangeUserPasswordRequest request, CancellationToken ct)
{ {
await _users.ChangeUserPasswordAsync(request, RequireBearerToken(), ct); await _users.ChangeUserPasswordAsync(request, RequireBearerToken(), ct);
// AuthHex accepted the new password — release the first-login forced change.
await _userManagement.ClearMustChangePasswordAsync(_currentUser.AuditUserId, ct);
AuthCookieWriter.ClearSession(Response); AuthCookieWriter.ClearSession(Response);
return NoContent(); return NoContent();
} }
@@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Dtos.Config;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers;
/// <summary>
/// Branch configuration endpoints — the singleton toggle for whether this deployment
/// requires a branch on Employee/Warehouse records at all.
/// <para>
/// <b>Authorization:</b> writes are admitted by the inherited ERP door policy only, same
/// Phase-1 posture as <see cref="ProductConfigController"/> — any ERP-admitted user can
/// flip this flag until per-endpoint RBAC lands.
/// </para>
/// </summary>
[Route("api/v1/branch-config")]
[Authorize(Policy = NavPolicies.Prefix + "settings")]
public sealed class BranchConfigController : ApiControllerBase
{
private readonly IBranchConfigService _config;
public BranchConfigController(IBranchConfigService config) => _config = config;
[HttpGet]
[ProducesResponseType(typeof(BranchConfigDto), StatusCodes.Status200OK)]
public async Task<ActionResult<BranchConfigDto>> Get(CancellationToken ct)
{
var result = await _config.GetAsync(ct);
SetETag(result.RowVersion);
return Ok(result.Value);
}
[HttpPut]
[ProducesResponseType(typeof(BranchConfigDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
public async Task<ActionResult<BranchConfigDto>> Update(
[FromBody] UpdateBranchConfigRequest request, CancellationToken ct)
{
var expected = RequireIfMatch();
var result = await _config.UpdateAsync(request, expected, ct);
SetETag(result.RowVersion);
return Ok(result.Value);
}
}
@@ -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;
@@ -15,6 +18,6 @@ public sealed class DashboardController : ApiControllerBase
/// <summary>Aggregate counts for stock, GRN, and procurement.</summary> /// <summary>Aggregate counts for stock, GRN, and procurement.</summary>
[HttpGet("stats")] [HttpGet("stats")]
[ProducesResponseType(typeof(DashboardStatsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(DashboardStatsDto), StatusCodes.Status200OK)]
public async Task<ActionResult<DashboardStatsDto>> GetStats(CancellationToken ct) public async Task<ActionResult<DashboardStatsDto>> GetStats([FromQuery] int? branchId, CancellationToken ct)
=> Ok(await _dashboard.GetStatsAsync(ct)); => Ok(await _dashboard.GetStatsAsync(branchId, 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.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;
@@ -22,8 +25,8 @@ public sealed class GrnsController : ApiControllerBase
[ProducesResponseType(typeof(PagedResponse<GrnSummaryDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(PagedResponse<GrnSummaryDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResponse<GrnSummaryDto>>> List( public async Task<ActionResult<PagedResponse<GrnSummaryDto>>> List(
[FromQuery] PageQuery query, [FromQuery] GrnStatus? status, [FromQuery] int? poId, [FromQuery] PageQuery query, [FromQuery] GrnStatus? status, [FromQuery] int? poId,
[FromQuery] int? vendorId, [FromQuery] int? warehouseId, CancellationToken ct) [FromQuery] int? vendorId, [FromQuery] int? warehouseId, [FromQuery] int? branchId, CancellationToken ct)
=> Ok(await _grns.ListAsync(query, status, poId, vendorId, warehouseId, ct)); => Ok(await _grns.ListAsync(query, status, poId, vendorId, warehouseId, branchId, ct));
[HttpGet("{grnId:int}")] [HttpGet("{grnId:int}")]
[ProducesResponseType(typeof(GrnDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(GrnDto), StatusCodes.Status200OK)]
@@ -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,30 @@ 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>Items list enriched with stock value, low-stock, last-purchase and
/// trailing-30-day sales aggregates, for the Items overview table.</summary>
[HttpGet("overview")]
[ProducesResponseType(typeof(PagedResponse<ItemOverviewDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResponse<ItemOverviewDto>>> Overview(
[FromQuery] PageQuery query,
[FromQuery] EntityStatus? status,
[FromQuery] int? categoryId,
[FromQuery] int? subCategoryId,
[FromQuery] int? brandId,
CancellationToken ct)
=> Ok(await _items.GetOverviewAsync(query, status, categoryId, subCategoryId, brandId, 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;
@@ -25,8 +28,11 @@ public sealed class SalesInvoicesController : ApiControllerBase
[FromQuery] SalesInvoiceStatus? status, [FromQuery] SalesInvoiceStatus? status,
[FromQuery] int? customerId, [FromQuery] int? customerId,
[FromQuery] int? warehouseId, [FromQuery] int? warehouseId,
[FromQuery] int? branchId,
[FromQuery] DateOnly? from,
[FromQuery] DateOnly? to,
CancellationToken ct) CancellationToken ct)
=> Ok(await _invoices.ListAsync(query, status, customerId, warehouseId, ct)); => Ok(await _invoices.ListAsync(query, status, customerId, warehouseId, branchId, from, to, ct));
[HttpGet("{salesInvoiceId:int}")] [HttpGet("{salesInvoiceId:int}")]
[ProducesResponseType(typeof(SalesInvoiceDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(SalesInvoiceDto), StatusCodes.Status200OK)]
@@ -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;
@@ -17,8 +20,8 @@ public sealed class StockAdjustmentsController : ApiControllerBase
[HttpGet] [HttpGet]
[ProducesResponseType(typeof(PagedResponse<AdjustmentSummaryDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(PagedResponse<AdjustmentSummaryDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResponse<AdjustmentSummaryDto>>> List( public async Task<ActionResult<PagedResponse<AdjustmentSummaryDto>>> List(
[FromQuery] PageQuery query, [FromQuery] int? warehouseId, [FromQuery] int? reasonCodeId, CancellationToken ct) [FromQuery] PageQuery query, [FromQuery] int? warehouseId, [FromQuery] int? branchId, [FromQuery] int? reasonCodeId, CancellationToken ct)
=> Ok(await _adjustments.ListAsync(query, warehouseId, reasonCodeId, ct)); => Ok(await _adjustments.ListAsync(query, warehouseId, branchId, reasonCodeId, ct));
/// <summary>Get one adjustment with its lines and the ledger entries it posted.</summary> /// <summary>Get one adjustment with its lines and the ledger entries it posted.</summary>
[HttpGet("{adjustmentId:int}")] [HttpGet("{adjustmentId:int}")]
@@ -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;
@@ -28,8 +31,8 @@ public sealed class StockController : ApiControllerBase
[HttpGet("on-hand/list")] [HttpGet("on-hand/list")]
[ProducesResponseType(typeof(PagedResponse<StockOnHandDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(PagedResponse<StockOnHandDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResponse<StockOnHandDto>>> OnHandList( public async Task<ActionResult<PagedResponse<StockOnHandDto>>> OnHandList(
[FromQuery] int? itemId, [FromQuery] int? warehouseId, [FromQuery] PageQuery query, CancellationToken ct) [FromQuery] int? itemId, [FromQuery] int? warehouseId, [FromQuery] int? branchId, [FromQuery] PageQuery query, CancellationToken ct)
=> Ok(await _stock.GetOnHandListAsync(itemId, warehouseId, query, ct)); => Ok(await _stock.GetOnHandListAsync(itemId, warehouseId, branchId, query, ct));
/// <summary> /// <summary>
/// Immutable movement history. <c>sourceDocType</c>/<c>sourceDocId</c> answer "what did /// Immutable movement history. <c>sourceDocType</c>/<c>sourceDocId</c> answer "what did
@@ -39,11 +42,11 @@ public sealed class StockController : ApiControllerBase
[HttpGet("ledger")] [HttpGet("ledger")]
[ProducesResponseType(typeof(PagedResponse<StockLedgerRowDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(PagedResponse<StockLedgerRowDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResponse<StockLedgerRowDto>>> Ledger( public async Task<ActionResult<PagedResponse<StockLedgerRowDto>>> Ledger(
[FromQuery] int? itemId, [FromQuery] int? warehouseId, [FromQuery] int? itemId, [FromQuery] int? warehouseId, [FromQuery] int? branchId,
[FromQuery] DateOnly? from, [FromQuery] DateOnly? to, [FromQuery] DateOnly? from, [FromQuery] DateOnly? to,
[FromQuery] string? sourceDocType, [FromQuery] int? sourceDocId, [FromQuery] string? sourceDocType, [FromQuery] int? sourceDocId,
[FromQuery] PageQuery query, CancellationToken ct) [FromQuery] PageQuery query, CancellationToken ct)
=> Ok(await _stock.GetLedgerAsync(itemId, warehouseId, from, to, sourceDocType, sourceDocId, query, ct)); => Ok(await _stock.GetLedgerAsync(itemId, warehouseId, branchId, from, to, sourceDocType, sourceDocId, query, ct));
[HttpGet("valuation")] [HttpGet("valuation")]
[ProducesResponseType(typeof(StockValuationDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(StockValuationDto), StatusCodes.Status200OK)]
@@ -54,8 +57,8 @@ public sealed class StockController : ApiControllerBase
[HttpGet("reorder-alerts")] [HttpGet("reorder-alerts")]
[ProducesResponseType(typeof(PagedResponse<ReorderAlertDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(PagedResponse<ReorderAlertDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResponse<ReorderAlertDto>>> ReorderAlerts( public async Task<ActionResult<PagedResponse<ReorderAlertDto>>> ReorderAlerts(
[FromQuery] int? warehouseId, [FromQuery] PageQuery query, CancellationToken ct) [FromQuery] int? warehouseId, [FromQuery] int? branchId, [FromQuery] PageQuery query, CancellationToken ct)
=> Ok(await _reorder.GetAlertsAsync(warehouseId, query, ct)); => Ok(await _reorder.GetAlertsAsync(warehouseId, branchId, query, ct));
/// <summary>Create a draft requisition for an item's suggested reorder quantity.</summary> /// <summary>Create a draft requisition for an item's suggested reorder quantity.</summary>
[HttpPost("reorder-alerts/{itemId:int}/requisition")] [HttpPost("reorder-alerts/{itemId:int}/requisition")]
@@ -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;
@@ -18,8 +21,8 @@ public sealed class StockCountsController : ApiControllerBase
[HttpGet] [HttpGet]
[ProducesResponseType(typeof(PagedResponse<CountSummaryDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(PagedResponse<CountSummaryDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResponse<CountSummaryDto>>> List( public async Task<ActionResult<PagedResponse<CountSummaryDto>>> List(
[FromQuery] PageQuery query, [FromQuery] CountStatus? status, [FromQuery] int? warehouseId, CancellationToken ct) [FromQuery] PageQuery query, [FromQuery] CountStatus? status, [FromQuery] int? warehouseId, [FromQuery] int? branchId, CancellationToken ct)
=> Ok(await _counts.ListAsync(query, status, warehouseId, ct)); => Ok(await _counts.ListAsync(query, status, warehouseId, branchId, ct));
[HttpGet("{countId:int}")] [HttpGet("{countId:int}")]
[ProducesResponseType(typeof(CountDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(CountDto), StatusCodes.Status200OK)]
@@ -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;
@@ -19,8 +22,8 @@ public sealed class StockTransfersController : ApiControllerBase
[ProducesResponseType(typeof(PagedResponse<TransferSummaryDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(PagedResponse<TransferSummaryDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResponse<TransferSummaryDto>>> List( public async Task<ActionResult<PagedResponse<TransferSummaryDto>>> List(
[FromQuery] PageQuery query, [FromQuery] TransferStatus? status, [FromQuery] PageQuery query, [FromQuery] TransferStatus? status,
[FromQuery] int? srcWarehouseId, [FromQuery] int? destWarehouseId, CancellationToken ct) [FromQuery] int? srcWarehouseId, [FromQuery] int? destWarehouseId, [FromQuery] int? branchId, CancellationToken ct)
=> Ok(await _transfers.ListAsync(query, status, srcWarehouseId, destWarehouseId, ct)); => Ok(await _transfers.ListAsync(query, status, srcWarehouseId, destWarehouseId, branchId, ct));
[HttpGet("{transferId:int}")] [HttpGet("{transferId:int}")]
[ProducesResponseType(typeof(TransferDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(TransferDto), StatusCodes.Status200OK)]
@@ -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();
}
} }
+45 -4
View File
@@ -1,7 +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.Services.Interfaces; using ERPCore.Services.Interfaces;
using ERPCore.System.Errors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers; namespace ERPCore.Controllers;
@@ -11,15 +17,20 @@ 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;
public UsersController(IUserManagementService users, IEmployeeUserLinkService links) public UsersController(IUserManagementService users, IEmployeeUserLinkService links, IAuditService audit, ICurrentUser currentUser)
{ {
_users = users; _users = users;
_links = links; _links = links;
_audit = audit;
_currentUser = currentUser;
} }
/// <summary>Advisory forward-direction lookup: does a Staff record already exist with this email? (docs/12-BACKEND-HRM.md A.5)</summary> /// <summary>Advisory forward-direction lookup: does a Staff record already exist with this email? (docs/12-BACKEND-HRM.md A.5)</summary>
@@ -30,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")]
@@ -53,7 +64,7 @@ public sealed class UsersController : ApiControllerBase
[ProducesResponseType(StatusCodes.Status409Conflict)] [ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<ManagedUserDto>> Create([FromBody] CreateUserRequest request, CancellationToken ct) public async Task<ActionResult<ManagedUserDto>> Create([FromBody] CreateUserRequest request, CancellationToken ct)
{ {
var result = await _users.CreateAsync(request, ct); var result = await _users.CreateAsync(request, _currentUser.AuditUserId, ct);
return Created($"/api/v1/users/{result.UserId}", result); return Created($"/api/v1/users/{result.UserId}", result);
} }
@@ -62,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; }
} }
+5 -2
View File
@@ -3,16 +3,19 @@ using ERPCore.Domain.Enums;
namespace ERPCore.Domain.Entities; namespace ERPCore.Domain.Entities;
/// <summary> /// <summary>
/// Branch/location master (FR-HR-MD-01) — multi-branch readiness. Referenced /// Branch/location master (FR-HR-MD-01) — multi-branch readiness. Required by
/// optionally by <see cref="Employee.BranchId"/> and <see cref="PayrollRun.BranchId"/>. /// <see cref="Employee.BranchId"/>, optionally referenced by <see cref="PayrollRun.BranchId"/>.
/// Deactivated, not deleted, when referenced. Model: docs/12-BACKEND-HRM.md Part C.1. /// Deactivated, not deleted, when referenced. Model: docs/12-BACKEND-HRM.md Part C.1.
/// </summary> /// </summary>
public class Branch public class Branch
{ {
public int BranchId { get; set; } public int BranchId { get; set; }
/// <summary>Server-generated on create ("BR-001", "BR-002", ...) — never accepted from the client.</summary>
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? Address { get; set; }
public string? PhoneNumber { 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; }
@@ -0,0 +1,39 @@
namespace ERPCore.Domain.Entities;
/// <summary>
/// Branch configuration — a singleton row (single-tenant, docs/00-CORE §1) gating whether
/// this deployment uses branches at all. When <see cref="BranchingEnabled"/> is off, the
/// Employee/Warehouse create forms stop asking for a branch and every new record is
/// silently assigned to the deployment's one default branch instead — schema-wise
/// <c>Employee.BranchId</c>/<c>Warehouse.BranchId</c> stay <c>NOT NULL</c> either way, only
/// the UI/UX requirement toggles. See <see cref="Services.BranchConfigService.ResolveBranchIdAsync"/>.
/// </summary>
public class BranchConfig
{
/// <summary>Always 1 - the singleton row's id.</summary>
public const int SingletonId = 1;
public int ConfigId { get; set; }
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;
/// <summary>Independent sub-toggle for Purchase Orders. On: each PO belongs to one branch,
/// non-admins only see their own branch's POs, and line warehouses must be in that branch.
/// Off: POs form one shared pool across branches (kept on the default branch, visible to
/// everyone, any warehouse allowed). Meaningless when <see cref="BranchingEnabled"/> is off.</summary>
public bool PurchaseOrderBranchingEnabled { get; set; } = true;
public DateTime? UpdatedAt { get; set; }
public int? UpdatedBy { get; set; }
public User? UpdatedByUser { get; set; }
/// <summary>PostgreSQL xmin-backed optimistic concurrency token (ETag source).</summary>
public uint RowVersion { 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; }
+5 -3
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".
@@ -50,7 +52,7 @@ public class Employee
public Designation? Designation { get; set; } public Designation? Designation { get; set; }
public int EmploymentTypeId { get; set; } public int EmploymentTypeId { get; set; }
public EmploymentType? EmploymentType { get; set; } public EmploymentType? EmploymentType { get; set; }
public int? BranchId { get; set; } public int BranchId { get; set; }
public Branch? Branch { get; set; } public Branch? Branch { get; set; }
public int WorkShiftId { get; set; } public int WorkShiftId { get; set; }
public WorkShift? WorkShift { get; set; } public WorkShift? WorkShift { get; set; }
+5
View File
@@ -22,6 +22,11 @@ public class Grn
public int WarehouseId { get; set; } public int WarehouseId { get; set; }
public Warehouse? Warehouse { get; set; } public Warehouse? Warehouse { get; set; }
/// <summary>Owning branch, set explicitly at creation — warehouses are shared across
/// branches, so branch scope can no longer be derived from <see cref="Warehouse"/>.</summary>
public int BranchId { get; set; }
public Branch? Branch { get; set; }
public GrnStatus Status { get; set; } = GrnStatus.Draft; public GrnStatus Status { get; set; } = GrnStatus.Draft;
public int CreatedBy { get; set; } public int CreatedBy { get; set; }
+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; }
@@ -17,6 +17,11 @@ public class SalesInvoice
public int WarehouseId { get; set; } public int WarehouseId { get; set; }
public Warehouse? Warehouse { get; set; } public Warehouse? Warehouse { get; set; }
/// <summary>Owning branch, set explicitly at creation — warehouses are shared across
/// branches, so branch scope can no longer be derived from <see cref="Warehouse"/>.</summary>
public int BranchId { get; set; }
public Branch? Branch { get; set; }
public SalesInvoiceType InvoiceType { get; set; } = SalesInvoiceType.B2C; public SalesInvoiceType InvoiceType { get; set; } = SalesInvoiceType.B2C;
public SalesInvoiceStatus Status { get; set; } = SalesInvoiceStatus.Draft; public SalesInvoiceStatus Status { get; set; } = SalesInvoiceStatus.Draft;
@@ -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; }
} }
@@ -19,6 +19,11 @@ public class StockAdjustment
public int ReasonCodeId { get; set; } public int ReasonCodeId { get; set; }
public ReasonCode? ReasonCode { get; set; } public ReasonCode? ReasonCode { get; set; }
/// <summary>Owning branch, set explicitly at creation — warehouses are shared across
/// branches, so branch scope can no longer be derived from <see cref="Warehouse"/>.</summary>
public int BranchId { get; set; }
public Branch? Branch { get; set; }
public AdjustmentStatus Status { get; set; } = AdjustmentStatus.Posted; public AdjustmentStatus Status { get; set; } = AdjustmentStatus.Posted;
public int CreatedBy { get; set; } public int CreatedBy { get; set; }
@@ -16,6 +16,11 @@ public class StockCount
public int WarehouseId { get; set; } public int WarehouseId { get; set; }
public Warehouse? Warehouse { get; set; } public Warehouse? Warehouse { get; set; }
/// <summary>Owning branch, set explicitly at creation — warehouses are shared across
/// branches, so branch scope can no longer be derived from <see cref="Warehouse"/>.</summary>
public int BranchId { get; set; }
public Branch? Branch { get; set; }
public CountType CountType { get; set; } public CountType CountType { get; set; }
public CountStatus Status { get; set; } = CountStatus.Draft; public CountStatus Status { get; set; } = CountStatus.Draft;
@@ -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; }
@@ -19,6 +19,11 @@ public class StockTransfer
public int DestWarehouseId { get; set; } public int DestWarehouseId { get; set; }
public Warehouse? DestWarehouse { get; set; } public Warehouse? DestWarehouse { get; set; }
/// <summary>Owning branch, set explicitly at creation — warehouses are shared across
/// branches, so branch scope can no longer be derived from src/dest warehouse.</summary>
public int BranchId { get; set; }
public Branch? Branch { get; set; }
public TransferStatus Status { get; set; } = TransferStatus.Draft; public TransferStatus Status { get; set; } = TransferStatus.Draft;
public int CreatedBy { get; set; } public int CreatedBy { 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; }
} }
+8
View File
@@ -31,6 +31,14 @@ public class User
/// </summary> /// </summary>
public string? Email { get; set; } public string? Email { get; set; }
/// <summary>
/// Set when an admin creates the account with an issued (temporary) password
/// (<c>UserManagementService.CreateAsync</c>); surfaced via <c>GET /auth/me</c> so the
/// frontend forces a password change on first login. Cleared by
/// <c>POST /auth/change-password</c>.
/// </summary>
public bool MustChangePassword { get; set; }
/// <summary>Local shadow <see cref="Role"/> assignment; null until an admin assigns one.</summary> /// <summary>Local shadow <see cref="Role"/> assignment; null until an admin assigns one.</summary>
public int? RoleId { get; set; } public int? RoleId { get; set; }
public Role? Role { get; set; } public Role? Role { get; set; }
@@ -14,9 +14,14 @@ public class Vendor
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public string? Terms { get; set; } public string? Terms { get; set; }
public string? TaxReg { get; set; } public string? TaxReg { get; set; }
public string? Address { get; set; }
public string? Phone { get; set; }
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,10 @@ 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 Branch? Branch { get; set; }
public ICollection<Bin> Bins { get; set; } = new List<Bin>(); public ICollection<Bin> Bins { get; set; } = new List<Bin>();
} }
+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>
@@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations;
namespace ERPCore.Dtos.Config;
/// <summary>Branch configuration resource — the singleton toggle for whether this deployment
/// requires a branch on Employee/Warehouse records at all.</summary>
public sealed record BranchConfigDto(
bool BranchingEnabled, bool WarehouseBranchingEnabled, bool PurchaseOrderBranchingEnabled, DateTime? UpdatedAt, int? UpdatedBy);
/// <summary>
/// Full replacement of the flag. <c>UpdatedBy</c> is derived from the token, never posted.
/// <para>
/// <see cref="BranchingEnabled"/> is <see cref="bool"/>? deliberately: <c>[Required]</c> on a
/// non-nullable bool is a no-op, so an omitted flag would otherwise silently bind to
/// <c>false</c>. Nullable makes the requirement actually bind — an omitted flag is a 400.
/// </para>
/// </summary>
public sealed class UpdateBranchConfigRequest
{
[Required] public bool? BranchingEnabled { get; set; }
[Required] public bool? WarehouseBranchingEnabled { get; set; }
/// <summary>Optional so existing callers that only know the first two flags keep working —
/// omitted means "leave as is".</summary>
public bool? PurchaseOrderBranchingEnabled { 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,
@@ -15,10 +18,13 @@ public sealed record DashboardStatsDto(
decimal StockValuationTotal, decimal StockValuationTotal,
IReadOnlyList<WarehouseValuationDto> StockValuationByWarehouse, IReadOnlyList<WarehouseValuationDto> StockValuationByWarehouse,
int PendingApprovalPurchaseOrders, int PendingApprovalPurchaseOrders,
int FullyReceivedPurchaseOrders,
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);
+9 -5
View File
@@ -15,14 +15,14 @@ public sealed record GrnLineDto(
HoldStatus HoldStatus, int? BatchId, IReadOnlyList<GrnLineWarrantyNumberDto> WarrantyNumbers); HoldStatus HoldStatus, int? BatchId, IReadOnlyList<GrnLineWarrantyNumberDto> WarrantyNumbers);
public sealed record GrnDto( public sealed record GrnDto(
int GrnId, string DocNo, int? PoId, int VendorId, int WarehouseId, GrnStatus Status, int GrnId, string DocNo, int? PoId, int VendorId, int WarehouseId, int BranchId, GrnStatus Status,
int CreatedBy, DateTime CreatedAt, DateTime? PostedAt, string? GlJournalNo, int CreatedBy, DateTime CreatedAt, DateTime? PostedAt, string? GlJournalNo,
int? PurchaseInvoiceId, string? PurchaseInvoiceNo, IReadOnlyList<GrnLineDto> Lines); int? PurchaseInvoiceId, string? PurchaseInvoiceNo, IReadOnlyList<GrnLineDto> Lines);
/// <summary>Row shape for <c>GET /grns</c> — line count instead of the lines themselves.</summary> /// <summary>Row shape for <c>GET /grns</c> — line count instead of the lines themselves.</summary>
public sealed record GrnSummaryDto( public sealed record GrnSummaryDto(
int GrnId, string DocNo, int? PoId, int VendorId, int WarehouseId, GrnStatus Status, int GrnId, string DocNo, int? PoId, int VendorId, int WarehouseId, int BranchId, GrnStatus Status,
int CreatedBy, DateTime CreatedAt, DateTime? PostedAt, int LineCount, int CreatedBy, DateTime CreatedAt, DateTime? PostedAt, int LineCount, decimal TotalValue,
int? PurchaseInvoiceId, string? PurchaseInvoiceNo); int? PurchaseInvoiceId, string? PurchaseInvoiceNo);
public sealed record CreatedLayerDto( public sealed record CreatedLayerDto(
@@ -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>
@@ -81,6 +82,9 @@ public sealed class CreateGrnRequest
/// <summary>Required for a direct receipt (no PO); ignored otherwise (vendor derives from the PO).</summary> /// <summary>Required for a direct receipt (no PO); ignored otherwise (vendor derives from the PO).</summary>
public int? VendorId { get; set; } public int? VendorId { get; set; }
[Required] public int WarehouseId { get; set; } [Required] public int WarehouseId { get; set; }
/// <summary>Owning branch for this GRN — warehouses are shared, so this is what actually
/// scopes the document (resolved via IBranchConfigService; required when branching is enabled).</summary>
[Required] public int BranchId { get; set; }
[Required, MinLength(1)] public List<CreateGrnLineInput> Lines { get; set; } = new(); [Required, MinLength(1)] public List<CreateGrnLineInput> Lines { get; set; } = new();
} }
+6 -4
View File
@@ -3,10 +3,12 @@ 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,
int EmploymentTypeId, string? EmploymentTypeName, int? BranchId, string? BranchName, int EmploymentTypeId, string? EmploymentTypeName, int BranchId, string? BranchName,
EmployeeStatus Status, bool HasUserLink, DateTime HireDate); EmployeeStatus Status, bool HasUserLink, DateTime HireDate);
public sealed record EmployeeDetailDto( public sealed record EmployeeDetailDto(
@@ -16,7 +18,7 @@ public sealed record EmployeeDetailDto(
string? City, string? PostalCode, string? Country, string? City, string? PostalCode, string? Country,
string? EmergencyContactName, string? EmergencyContactRelationship, string? EmergencyContactPhone, string? EmergencyContactName, string? EmergencyContactRelationship, string? EmergencyContactPhone,
DateTime HireDate, DateTime? ConfirmationDate, DateTime? LastWorkingDate, DateTime HireDate, DateTime? ConfirmationDate, DateTime? LastWorkingDate,
int DepartmentId, int DesignationId, int EmploymentTypeId, int? BranchId, int WorkShiftId, int DepartmentId, int DesignationId, int EmploymentTypeId, int BranchId, int WorkShiftId,
int? ReportingManagerId, string? EpfNumber, string? EtfNumber, string? TaxIdentificationNumber, int? ReportingManagerId, string? EpfNumber, string? EtfNumber, string? TaxIdentificationNumber,
int? UserId, EmployeeStatus Status, DateTime CreatedAt, DateTime? UpdatedAt); int? UserId, EmployeeStatus Status, DateTime CreatedAt, DateTime? UpdatedAt);
@@ -45,7 +47,7 @@ public sealed class CreateEmployeeRequest
[Required] public int DepartmentId { get; set; } [Required] public int DepartmentId { get; set; }
[Required] public int DesignationId { get; set; } [Required] public int DesignationId { get; set; }
[Required] public int EmploymentTypeId { get; set; } [Required] public int EmploymentTypeId { get; set; }
public int? BranchId { get; set; } [Required] public int BranchId { get; set; }
[Required] public int WorkShiftId { get; set; } [Required] public int WorkShiftId { get; set; }
public int? ReportingManagerId { get; set; } public int? ReportingManagerId { get; set; }
@@ -82,7 +84,7 @@ public sealed class UpdateEmployeeRequest
[Required] public int DepartmentId { get; set; } [Required] public int DepartmentId { get; set; }
[Required] public int DesignationId { get; set; } [Required] public int DesignationId { get; set; }
[Required] public int EmploymentTypeId { get; set; } [Required] public int EmploymentTypeId { get; set; }
public int? BranchId { get; set; } [Required] public int BranchId { get; set; }
[Required] public int WorkShiftId { get; set; } [Required] public int WorkShiftId { get; set; }
public int? ReportingManagerId { get; set; } public int? ReportingManagerId { get; set; }
+4 -2
View File
@@ -7,20 +7,22 @@ namespace ERPCore.Dtos.Hrm;
// ids, timestamps) excluded from create/update requests (02-SECURITY B.6/C.1). // ids, timestamps) excluded from create/update requests (02-SECURITY B.6/C.1).
public sealed record BranchDto( public sealed record BranchDto(
int BranchId, string Code, string Name, string? Address, EntityStatus Status, int BranchId, string Code, string Name, string? Address, string? PhoneNumber, EntityStatus Status,
DateTime CreatedAt, DateTime? UpdatedAt); DateTime CreatedAt, DateTime? UpdatedAt);
// Code is server-generated (BranchService.GenerateCodeAsync) — never accepted from the client.
public sealed class CreateBranchRequest public sealed class CreateBranchRequest
{ {
[Required, StringLength(20)] public string Code { get; set; } = string.Empty;
[Required, StringLength(200)] public string Name { get; set; } = string.Empty; [Required, StringLength(200)] public string Name { get; set; } = string.Empty;
[StringLength(500)] public string? Address { get; set; } [StringLength(500)] public string? Address { get; set; }
[StringLength(30)] public string? PhoneNumber { get; set; }
} }
public sealed class UpdateBranchRequest public sealed class UpdateBranchRequest
{ {
[Required, StringLength(200)] public string Name { get; set; } = string.Empty; [Required, StringLength(200)] public string Name { get; set; } = string.Empty;
[StringLength(500)] public string? Address { get; set; } [StringLength(500)] public string? Address { get; set; }
[StringLength(30)] public string? PhoneNumber { get; set; }
} }
public sealed record DepartmentDto( public sealed record DepartmentDto(
+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,18 @@
using ERPCore.Domain.Enums;
namespace ERPCore.Dtos.Items;
/// <summary>
/// Item row enriched with cross-module aggregates (current FIFO stock value, low-stock
/// flag, last purchase, trailing-30-day sales) for the Items list page. Deliberately a
/// separate endpoint from <see cref="ItemListItemDto"/>'s <c>GET /items</c> — that list
/// also backs the GRN/PO/Requisition/RFQ item pickers, which don't need these aggregates
/// computed on every keystroke.
/// </summary>
public sealed record ItemOverviewDto(
int ItemId, string Sku, string Name,
int CategoryId, int? SubCategoryId, int? BrandId,
EntityStatus Status,
decimal StockQty, decimal StockValue, bool IsLowStock,
DateTime? LastPurchaseDate, int? LastSupplierId, string? LastSupplierName,
decimal SalesQtyLast30Days);
@@ -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);

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