Compare commits

..

38 Commits

Author SHA1 Message Date
Naveen cb6b92d221 Add backup images for login and background removal
- Added loginerps.png.bak (1134 KB) as a backup for the login image.
- Added whithoutbackground.png.bak (3455 KB) as a backup for the image without background.
2026-09-19 00:12:41 +05:30
ImanThiyanga 3f10ae3bca changes fix for production 2026-09-19 00:00:42 +05:30
ImanThiyanga 61fe1a0e0c Merge branch 'production' into Dev
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Backend (pull_request) Successful in 31s
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Frontend (pull_request) Failing after 13m12s
2026-09-18 18:23:35 +00:00
ImanThiyanga 941f20d4c5 migrations fix 2026-09-18 23:52:26 +05:30
kalana_Thilakarathna b06d20e649 Merge pull request 'test to yml' (#71) from Dev into production
Reviewed-on: #71
2026-09-18 05:23:55 +00:00
Kalana B. Thilakarathna 445d323bed test to yml
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Backend (pull_request) Failing after 14m24s
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Frontend (pull_request) Failing after 14m59s
2026-09-18 10:52:17 +05:30
kalana_Thilakarathna 9028255800 Merge pull request 'workflowupdate' (#70) from Dev into production
Reviewed-on: #70
2026-09-18 04:51:58 +00:00
Kalana B. Thilakarathna 1b2f244853 workflowupdate
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Backend (pull_request) Failing after 11m21s
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Frontend (pull_request) Failing after 14m6s
2026-09-18 10:21:36 +05:30
kalana_Thilakarathna 475bcfec1a Merge pull request 'pipeline update' (#69) from Dev into production
Reviewed-on: #69
2026-09-18 04:48:49 +00:00
Kalana B. Thilakarathna 85db26ec82 pipeline update
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Backend (pull_request) Failing after 14m30s
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Frontend (pull_request) Failing after 14m58s
2026-09-18 10:18:13 +05:30
kalana_Thilakarathna baee987d26 Merge pull request 'test' (#68) from Dev into production
Reviewed-on: #68
2026-09-17 08:25:31 +00:00
Kalana B. Thilakarathna f45a59f20d test
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Backend (pull_request) Failing after 12m49s
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Frontend (pull_request) Failing after 14m57s
2026-09-17 13:55:06 +05:30
kalana_Thilakarathna 32304cbaff Merge pull request 'user fix at workflow' (#67) from Dev into production
Reviewed-on: #67
2026-09-17 07:35:02 +00:00
Kalana B. Thilakarathna 2b64bb0ebc user fix at workflow
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Backend (pull_request) Failing after 13m17s
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Frontend (pull_request) Has been cancelled
2026-09-17 13:04:15 +05:30
kalana_Thilakarathna ffe5251236 Merge pull request 'workflow fix' (#66) from Dev into production
Reviewed-on: #66
2026-09-17 07:26:14 +00:00
Kalana B. Thilakarathna d04a7fb3bc workflow fix
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Frontend (pull_request) Has been cancelled
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Backend (pull_request) Has been cancelled
2026-09-17 12:55:38 +05:30
kalana_Thilakarathna d76595085e Merge pull request 'workflow fix' (#65) from Dev into production
Reviewed-on: #65
2026-09-17 07:08:22 +00:00
Kalana B. Thilakarathna bad33a8492 workflow fix
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Backend (pull_request) Failing after 13m57s
CI/CD - ERP Core Monorepo / Build & Deploy ERP Core Frontend (pull_request) Has been cancelled
2026-09-17 12:38:05 +05:30
kalana_Thilakarathna b0216fca30 Merge pull request 'triger erpo core workflow' (#64) from Dev into production
Reviewed-on: #64
2026-09-17 07:07:24 +00:00
Kalana B. Thilakarathna 49c112d188 triger erpo core workflow 2026-09-17 12:37:04 +05:30
kalana_Thilakarathna c836b57d54 Merge pull request 'gitea work flow update' (#63) from Dev into production
Reviewed-on: #63
2026-09-17 07:06:02 +00:00
Kalana B. Thilakarathna 3a583f80e5 gitea work flow update 2026-09-17 12:28:31 +05:30
ImanThiyanga 83e0335504 Merge pull request 'Dev' (#62) from Dev into production
Reviewed-on: #62
2026-09-17 05:58:25 +00:00
ImanThiyanga 513343b62a Merge pull request 'Revert/ci cd merges' (#61) from revert/ci-cd-merges into Dev
Reviewed-on: #61
2026-09-17 05:57:56 +00:00
ImanThiyanga 256e39ccff Revert "Merge pull request 'Dev' (#58) from Dev into production"
This reverts commit 821e96e011, reversing
changes made to 0ff39e9e58.
2026-09-17 11:25:33 +05:30
ImanThiyanga efb37fce7a Revert "Merge pull request 'Dev' (#60) from Dev into production"
This reverts commit 01fc2582b6, reversing
changes made to 821e96e011.
2026-09-17 11:25:33 +05:30
ImanThiyanga 01fc2582b6 Merge pull request 'Dev' (#60) from Dev into production
Reviewed-on: #60
2026-09-16 18:47:15 +00:00
ImanThiyanga 39a2dc60be Merge pull request 'Feat/ci cd implementation' (#59) from feat/ci-cd-implementation into Dev
CI/CD - ERP Core / Build & Deploy (pull_request) Failing after 13m56s
CI/CD - ERP Core / detect (pull_request) Failing after 14m48s
CI/CD - ERP Core / scripts (pull_request) Failing after 12m24s
CI/CD - ERP Core / e2e-typecheck (pull_request) Failing after 13m15s
CI/CD - ERP Core / frontend (pull_request) Failing after 14m7s
CI/CD - ERP Core / backend (pull_request) Failing after 14m59s
CI/CD - ERP Core / ci-ok (pull_request) Failing after 14m58s
Reviewed-on: #59
2026-09-16 18:30:42 +00:00
ImanThiyanga 3b87a6e421 Merge branch 'Dev' into feat/ci-cd-implementation
CI/CD - ERP Core / detect (pull_request) Waiting to run
CI/CD - ERP Core / backend (pull_request) Blocked by required conditions
CI/CD - ERP Core / frontend (pull_request) Blocked by required conditions
CI/CD - ERP Core / e2e-typecheck (pull_request) Blocked by required conditions
CI/CD - ERP Core / scripts (pull_request) Blocked by required conditions
CI/CD - ERP Core / ci-ok (pull_request) Blocked by required conditions
CI/CD - ERP Core / Build & Deploy (pull_request) Waiting to run
2026-09-16 18:30:22 +00:00
ImanThiyanga 2a7a44cc27 change in to merge
CI/CD - ERP Core / detect (pull_request) Failing after 13m11s
CI/CD - ERP Core / ci-ok (pull_request) Has been cancelled
CI/CD - ERP Core / Build & Deploy (pull_request) Failing after 12m17s
CI/CD - ERP Core / backend (pull_request) Has been cancelled
CI/CD - ERP Core / frontend (pull_request) Has been cancelled
CI/CD - ERP Core / e2e-typecheck (pull_request) Has been cancelled
CI/CD - ERP Core / scripts (pull_request) Has been cancelled
2026-09-16 23:48:07 +05:30
ImanThiyanga c7cf63e7de initiating connection fixes 2026-09-16 23:47:47 +05:30
ImanThiyanga d1044e4728 final fix 2026-09-16 13:53:57 +05:30
ImanThiyanga 821e96e011 Merge pull request 'Dev' (#58) from Dev into production
Reviewed-on: #58
2026-09-16 06:37:25 +00:00
ImanThiyanga d6f973202d Merge branch 'production' into Dev
CI/CD - ERP Core / Build & Deploy (pull_request) Failing after 10m38s
CI/CD - ERP Core / detect (pull_request) Failing after 11m30s
CI/CD - ERP Core / scripts (pull_request) Failing after 11m31s
CI/CD - ERP Core / e2e-typecheck (pull_request) Failing after 12m23s
CI/CD - ERP Core / frontend (pull_request) Failing after 13m14s
CI/CD - ERP Core / backend (pull_request) Failing after 14m6s
CI/CD - ERP Core / ci-ok (pull_request) Failing after 14m59s
2026-09-16 06:36:19 +00:00
ImanThiyanga 1d5a5cf68e Merge pull request 'Feat/ci cd implementation' (#57) from feat/ci-cd-implementation into Dev
CI/CD - ERP Core / detect (pull_request) Has been cancelled
CI/CD - ERP Core / backend (pull_request) Has been cancelled
CI/CD - ERP Core / frontend (pull_request) Has been cancelled
CI/CD - ERP Core / e2e-typecheck (pull_request) Has been cancelled
CI/CD - ERP Core / scripts (pull_request) Has been cancelled
CI/CD - ERP Core / ci-ok (pull_request) Has been cancelled
CI/CD - ERP Core / Build & Deploy (pull_request) Has been cancelled
Reviewed-on: #57
2026-09-16 06:24:55 +00:00
ImanThiyanga 8e472ac996 initialising for ci-cd pipeline
CI/CD - ERP Core / scripts (pull_request) Failing after 12m21s
CI/CD - ERP Core / e2e-typecheck (pull_request) Failing after 13m13s
CI/CD - ERP Core / frontend (pull_request) Failing after 14m5s
CI/CD - ERP Core / backend (pull_request) Failing after 14m57s
CI/CD - ERP Core / detect (pull_request) Failing after 13m42s
CI/CD - ERP Core / Build & Deploy (pull_request) Failing after 12m47s
CI/CD - ERP Core / ci-ok (pull_request) Failing after 14m58s
2026-09-16 11:53:55 +05:30
ImanThiyanga 666979cf22 initiated ci-cd pipeline for the erp core with fronend changes to detect changes on both front and back 2026-09-16 11:53:17 +05:30
ashan_rusiru 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
327 changed files with 1737 additions and 86963 deletions
+106
View File
@@ -0,0 +1,106 @@
# test
name: CI/CD - ERP Core Monorepo
on:
pull_request:
branches:
- production
types:
- closed
jobs:
backend-build-and-deploy:
name: Build & Deploy ERP Core Backend
runs-on: linux
if: github.event.pull_request.merged == true
steps:
- name: Checkout code
run: |
git clone \
--depth 1 \
--branch production \
https://oauth2:${{ secrets.ERPCORE }}@gitea.hexdive.com/New_REP_SYSTEM/ERP-core.git \
.
- name: Restore backend
working-directory: .gitea/workflows
run: dotnet restore ../../Backend/ERPCore.sln
- name: Publish backend
working-directory: .gitea/workflows
run: |
rm -rf /tmp/erpcore-deploy
mkdir -p /tmp/erpcore-deploy
dotnet publish ../../Backend/ERPCore/ERPCore.csproj \
-c Release \
-r linux-x64 \
--self-contained true \
-o /tmp/erpcore-deploy
- name: Stop backend service
run: systemctl stop erpcore.service
- name: Copy backend to deploy folder
run: |
mkdir -p /home/erp/core/publish
rm -rf /home/erp/core/publish/*
cp -r /tmp/erpcore-deploy/* /home/erp/core/publish/
- name: Fix backend ownership and permissions
run: |
chown -R erpuser:erpuser /home/erp/core/publish
find /home/erp/core/publish -type d -exec chmod 755 {} +
find /home/erp/core/publish -type f -exec chmod 644 {} +
chmod +x /home/erp/core/publish/ERPCore
- name: Restart backend service
run: systemctl restart erpcore.service
- name: Verify backend
run: |
systemctl is-active erpcore.service && echo "Backend service running" || echo "Backend service failed"
frontend-build-and-deploy:
name: Build & Deploy ERP Core Frontend
runs-on: linux
needs: backend-build-and-deploy
if: github.event.pull_request.merged == true
steps:
- name: Checkout code
run: |
git clone \
--depth 1 \
--branch Production \
https://oauth2:${{ secrets.ERPCORE }}@gitea.hexdive.com/New_REP_SYSTEM/ERP-core.git \
.
- name: Build frontend Docker image
working-directory: .gitea/workflows
run: docker build -t erpcore-frontend:latest ../../Frontend/erp-system
- name: Stop & remove existing frontend container
run: |
docker stop erpcore-frontend 2>/dev/null || true
docker rm erpcore-frontend 2>/dev/null || true
- name: Start frontend container
run: |
docker run -d \
--name erpcore-frontend \
-p 6702:6702 \
--add-host=host.docker.internal:host-gateway \
-e BACKEND_ORIGIN=http://host.docker.internal:6701 \
--restart unless-stopped \
--memory="512m" \
--cpus="1.0" \
--cap-drop ALL \
--security-opt no-new-privileges \
erpcore-frontend:latest
- name: Remove old frontend images
run: docker image prune -f
- name: Verify frontend
run: |
docker ps --filter name=erpcore-frontend --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" \
| grep erpcore-frontend && echo "Frontend container running" || echo "Frontend container failed"
@@ -1,5 +1,3 @@
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,7 +14,6 @@ namespace ERPCore.Controllers;
/// </para> /// </para>
/// </summary> /// </summary>
[Route("api/v1/branch-config")] [Route("api/v1/branch-config")]
[Authorize(Policy = NavPolicies.Prefix + "settings")]
public sealed class BranchConfigController : ApiControllerBase public sealed class BranchConfigController : ApiControllerBase
{ {
private readonly IBranchConfigService _config; private readonly IBranchConfigService _config;
@@ -1,5 +1,3 @@
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;
@@ -10,7 +8,6 @@ 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,5 +1,3 @@
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;
@@ -9,7 +7,6 @@ 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,5 +1,3 @@
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;
@@ -14,7 +12,6 @@ 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,5 +1,3 @@
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;
@@ -10,7 +8,6 @@ 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,5 +1,3 @@
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;
@@ -8,7 +6,6 @@ namespace ERPCore.Controllers;
/// <summary>Dashboard overview stats — cross-domain counts, not a stored entity.</summary> /// <summary>Dashboard overview stats — cross-domain counts, not a stored entity.</summary>
[Route("api/v1/dashboard")] [Route("api/v1/dashboard")]
[Authorize(Policy = NavPolicies.Prefix + "dashboard")]
public sealed class DashboardController : ApiControllerBase public sealed class DashboardController : ApiControllerBase
{ {
private readonly IDashboardService _dashboard; private readonly IDashboardService _dashboard;
@@ -1,5 +1,3 @@
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;
@@ -14,7 +12,6 @@ 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,5 +1,3 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces; using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -16,7 +14,6 @@ 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,5 +1,3 @@
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;
@@ -10,7 +8,6 @@ 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;
@@ -94,13 +94,13 @@ public sealed class EmployeesController : ApiControllerBase
/// <summary>Permanently deletes the employee and all dependent HR records. Blocked (409) if payroll history exists.</summary> /// <summary>Permanently deletes the employee and all dependent HR records. Blocked (409) if payroll history exists.</summary>
[HttpDelete("{employeeId:int}")] [HttpDelete("{employeeId:int}")]
[ProducesResponseType(typeof(EmployeeDeleteResultDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)] [ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<EmployeeDeleteResultDto>> Delete(int employeeId, [FromQuery] bool deleteUserAccount, CancellationToken ct) public async Task<IActionResult> Delete(int employeeId, CancellationToken ct)
{ {
var result = await _employees.DeleteAsync(employeeId, deleteUserAccount, ct); await _employees.DeleteAsync(employeeId, ct);
return Ok(result); return NoContent();
} }
[HttpPost("{employeeId:int}/link-user")] [HttpPost("{employeeId:int}/link-user")]
@@ -1,5 +1,3 @@
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;
@@ -15,7 +13,6 @@ 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,5 +1,3 @@
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;
@@ -10,7 +8,6 @@ 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;
@@ -30,17 +27,6 @@ public sealed class ItemsController : ApiControllerBase
CancellationToken ct) CancellationToken ct)
=> Ok(await _items.ListAsync(query, status, categoryId, subCategoryId, brandId, trackingMode, ct)); => Ok(await _items.ListAsync(query, status, categoryId, subCategoryId, brandId, trackingMode, ct));
/// <summary>Barcode/RFID lookup — matches an item's barcode/SKU, or a batch/serial
/// barcode/no (optionally scoped to <paramref name="itemId"/> first).</summary>
[HttpGet("scan")]
[ProducesResponseType(typeof(ItemScanResultDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ItemScanResultDto>> Scan([FromQuery] string code, [FromQuery] int? itemId, CancellationToken ct)
{
var result = await _items.ScanAsync(code, itemId, ct);
return result is null ? NotFound() : Ok(result);
}
/// <summary>Get a single item; returns an <c>ETag</c> for optimistic concurrency.</summary> /// <summary>Get a single item; returns an <c>ETag</c> for optimistic concurrency.</summary>
[HttpGet("{itemId:int}")] [HttpGet("{itemId:int}")]
[ProducesResponseType(typeof(ItemDetailDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ItemDetailDto), StatusCodes.Status200OK)]
@@ -1,5 +1,3 @@
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;
@@ -12,7 +10,6 @@ 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,5 +1,3 @@
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;
@@ -7,7 +5,6 @@ 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;
@@ -1,32 +0,0 @@
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,5 +1,3 @@
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;
@@ -18,7 +16,6 @@ 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,5 +1,3 @@
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;
@@ -10,7 +8,6 @@ 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,5 +1,3 @@
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;
@@ -10,7 +8,6 @@ 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,5 +1,3 @@
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;
@@ -11,7 +9,6 @@ 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,5 +1,3 @@
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;
@@ -10,7 +8,6 @@ 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;
@@ -35,7 +32,7 @@ public sealed class PurchaseOrdersController : ApiControllerBase
return Ok(result.Value); return Ok(result.Value);
} }
/// <summary>Create a PO — non-draft is issued straight into PendingApproval (FR-PROC-04). Totals computed server-side.</summary> /// <summary>Create a PO — auto-approved on creation in Phase 1 (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)]
@@ -60,7 +57,7 @@ public sealed class PurchaseOrdersController : ApiControllerBase
return Ok(result.Value); return Ok(result.Value);
} }
/// <summary>Submit a Draft PO (Draft → PendingApproval). 409 PO_NOT_EDITABLE if not Draft.</summary> /// <summary>Submit a Draft PO (Draft → Approved). 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)]
@@ -70,7 +67,6 @@ 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)]
@@ -80,28 +76,13 @@ public sealed class PurchaseOrdersController : ApiControllerBase
return NoContent(); return NoContent();
} }
/// <summary>Approve — PendingApproval → Approved. 403 PO_APPROVAL_FORBIDDEN if the caller /// <summary>Approve — no-op in Phase 1 (POs auto-approve); transitions PendingApproval→Approved when enabled.</summary>
/// 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,5 +1,3 @@
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;
@@ -9,7 +7,6 @@ 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,5 +1,3 @@
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;
@@ -10,7 +8,6 @@ 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,5 +1,3 @@
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;
@@ -10,7 +8,6 @@ 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,5 +1,3 @@
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;
@@ -10,7 +8,6 @@ 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;
@@ -67,7 +64,6 @@ 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,5 +1,3 @@
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;
@@ -10,7 +8,6 @@ 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,5 +1,3 @@
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;
@@ -9,7 +7,6 @@ using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers; namespace ERPCore.Controllers;
[Route("api/v1/sales-invoices")] [Route("api/v1/sales-invoices")]
[Authorize(Policy = NavPolicies.Prefix + "sales")]
public sealed class SalesInvoicesController : ApiControllerBase public sealed class SalesInvoicesController : ApiControllerBase
{ {
private readonly ISalesInvoiceService _invoices; private readonly ISalesInvoiceService _invoices;
@@ -1,5 +1,3 @@
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;
@@ -7,7 +5,6 @@ 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,5 +1,3 @@
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;
@@ -9,7 +7,6 @@ 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,5 +1,3 @@
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;
@@ -7,7 +5,6 @@ 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,5 +1,3 @@
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;
@@ -9,7 +7,6 @@ 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;
@@ -1,20 +0,0 @@
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,5 +1,3 @@
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;
@@ -9,7 +7,6 @@ namespace ERPCore.Controllers;
/// <summary>Stock-adjustment endpoints (docs/11 §5.5).</summary> /// <summary>Stock-adjustment endpoints (docs/11 §5.5).</summary>
[Route("api/v1/stock-adjustments")] [Route("api/v1/stock-adjustments")]
[Authorize(Policy = NavPolicies.Prefix + "stock")]
public sealed class StockAdjustmentsController : ApiControllerBase public sealed class StockAdjustmentsController : ApiControllerBase
{ {
private readonly IAdjustmentService _adjustments; private readonly IAdjustmentService _adjustments;
@@ -1,5 +1,3 @@
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;
@@ -10,7 +8,6 @@ namespace ERPCore.Controllers;
/// <summary>Stock enquiry, ledger, valuation and reorder-alert endpoints (docs/11 §5.1–5.3, §5.7).</summary> /// <summary>Stock enquiry, ledger, valuation and reorder-alert endpoints (docs/11 §5.1–5.3, §5.7).</summary>
[Route("api/v1/stock")] [Route("api/v1/stock")]
[Authorize(Policy = NavPolicies.Prefix + "stock")]
public sealed class StockController : ApiControllerBase public sealed class StockController : ApiControllerBase
{ {
private readonly IStockService _stock; private readonly IStockService _stock;
@@ -1,5 +1,3 @@
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;
@@ -10,7 +8,6 @@ namespace ERPCore.Controllers;
/// <summary>Stock-count endpoints (docs/11 §5.6).</summary> /// <summary>Stock-count endpoints (docs/11 §5.6).</summary>
[Route("api/v1/stock-counts")] [Route("api/v1/stock-counts")]
[Authorize(Policy = NavPolicies.Prefix + "stock")]
public sealed class StockCountsController : ApiControllerBase public sealed class StockCountsController : ApiControllerBase
{ {
private readonly ICountService _counts; private readonly ICountService _counts;
@@ -1,5 +1,3 @@
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;
@@ -10,7 +8,6 @@ namespace ERPCore.Controllers;
/// <summary>Stock-transfer endpoints (docs/11 §5.4).</summary> /// <summary>Stock-transfer endpoints (docs/11 §5.4).</summary>
[Route("api/v1/stock-transfers")] [Route("api/v1/stock-transfers")]
[Authorize(Policy = NavPolicies.Prefix + "stock")]
public sealed class StockTransfersController : ApiControllerBase public sealed class StockTransfersController : ApiControllerBase
{ {
private readonly ITransferService _transfers; private readonly ITransferService _transfers;
@@ -1,5 +1,3 @@
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;
@@ -12,7 +10,6 @@ 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;
@@ -1,70 +0,0 @@
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);
}
}
@@ -1,70 +0,0 @@
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();
}
}
+14 -5
View File
@@ -1,5 +1,4 @@
using Microsoft.AspNetCore.Authorization; using ERPCore.Domain.Enums;
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;
@@ -9,7 +8,6 @@ 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;
@@ -18,8 +16,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)]
@@ -29,4 +28,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();
}
} }
@@ -1,13 +1,10 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Domain.Enums; using ERPCore.Domain.Enums;
using ERPCore.Dtos.Audit; using ERPCore.Dtos.Audit;
using ERPCore.Dtos.Auth;
using ERPCore.Dtos.Common; using ERPCore.Dtos.Common;
using ERPCore.Dtos.Hrm; using ERPCore.Dtos.Hrm;
using ERPCore.Dtos.Users; using ERPCore.Dtos.Users;
using ERPCore.Infra.Auth; using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces; using ERPCore.Services.Interfaces;
using ERPCore.System.Errors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers; namespace ERPCore.Controllers;
@@ -17,7 +14,6 @@ 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;
@@ -87,20 +83,4 @@ public sealed class UsersController : ApiControllerBase
[ProducesResponseType(typeof(PagedResponse<LoginActivityDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(PagedResponse<LoginActivityDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResponse<LoginActivityDto>>> ListLoginDevices(int userId, [FromQuery] PageQuery query, CancellationToken ct) public async Task<ActionResult<PagedResponse<LoginActivityDto>>> ListLoginDevices(int userId, [FromQuery] PageQuery query, CancellationToken ct)
=> Ok(await _audit.ListLoginDevicesAsync(userId, query, 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,5 +1,3 @@
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;
@@ -10,7 +8,6 @@ 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,5 +1,3 @@
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;
@@ -9,7 +7,6 @@ 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;
@@ -40,24 +37,6 @@ 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,7 +13,4 @@ public class Batch
public string BatchNo { get; set; } = string.Empty; public string BatchNo { get; set; } = string.Empty;
public DateOnly? ExpiryDate { get; set; } public DateOnly? ExpiryDate { get; set; }
/// <summary>Optional scan code for barcode/RFID lookup (unique when set).</summary>
public string? Barcode { get; set; }
} }
@@ -17,12 +17,6 @@ public class BranchConfig
public bool BranchingEnabled { get; set; } = true; public bool BranchingEnabled { get; set; } = true;
/// <summary>Independent sub-toggle: when the global <see cref="BranchingEnabled"/> is on
/// but this is off, Warehouses stop asking for/showing a branch while Employees etc.
/// still do. Meaningless when <see cref="BranchingEnabled"/> is off (everything is
/// single-branch already).</summary>
public bool WarehouseBranchingEnabled { get; set; } = true;
public DateTime? UpdatedAt { get; set; } public DateTime? UpdatedAt { get; set; }
public int? UpdatedBy { get; set; } public int? UpdatedBy { get; set; }
@@ -29,9 +29,6 @@ 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; }
+1 -11
View File
@@ -14,9 +14,6 @@ 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; }
@@ -44,14 +41,7 @@ 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.
@@ -1,22 +0,0 @@
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>();
}
@@ -1,18 +0,0 @@
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,10 +10,6 @@ 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,9 +20,6 @@ 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,10 +5,9 @@ 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).
/// A submitted/issued PO goes to <see cref="PurchaseOrderStatus.PendingApproval"/> and /// Phase 1 auto-approves on creation; <see cref="ApprovalRequired"/> is retained
/// needs a separate Approve action (an Admin who isn't <see cref="CreatedBy"/>) before it /// for the future approval workflow. Totals are computed server-side from lines
/// reaches <see cref="PurchaseOrderStatus.Approved"/>. Totals are computed server-side /// (not stored). Model: docs/10 Part C.2.
/// from lines (not stored). Model: docs/10 Part C.2.
/// </summary> /// </summary>
public class PurchaseOrder public class PurchaseOrder
{ {
@@ -18,25 +17,15 @@ 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,9 +14,6 @@ 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,9 +14,6 @@ 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,5 +1,3 @@
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>
@@ -8,13 +6,6 @@ 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,16 +27,6 @@ public class SalesDayEnd
public decimal TaxTotal { get; set; } public decimal TaxTotal { get; set; }
public decimal GrandTotal { get; set; } public decimal GrandTotal { get; set; }
/// <summary>
/// Sum of <see cref="SalesSlip.PaidAmount"/> across the day's slips (bundle sales have no
/// partial-payment field and are always counted as fully paid) — what actually gets debited
/// to Cash on close. <see cref="BalanceTotal"/> is the rest (<c>GrandTotal - PaidTotal</c>),
/// debited to Accounts Receivable instead. Before partial payments existed these were always
/// equal to GrandTotal/0 respectively (every sale was assumed fully cash).
/// </summary>
public decimal PaidTotal { get; set; }
public decimal BalanceTotal { get; set; }
/// <summary>Sum of the FIFO cost consumed for these slips (from <see cref="StockLedger"/>), for the COGS GL lines.</summary> /// <summary>Sum of the FIFO cost consumed for these slips (from <see cref="StockLedger"/>), for the COGS GL lines.</summary>
public decimal CostOfGoodsSold { get; set; } public decimal CostOfGoodsSold { get; set; }
@@ -19,12 +19,6 @@ 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,7 +13,4 @@ public class Serial
public string SerialNo { get; set; } = string.Empty; public string SerialNo { get; set; } = string.Empty;
public string Status { get; set; } = "InStock"; public string Status { get; set; } = "InStock";
/// <summary>Optional scan code for barcode/RFID lookup (unique when set).</summary>
public string? Barcode { get; set; }
} }
@@ -16,12 +16,6 @@ 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; }
@@ -1,25 +0,0 @@
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>();
}
@@ -1,16 +0,0 @@
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;
}
@@ -1,27 +0,0 @@
using ERPCore.Domain.Enums;
namespace ERPCore.Domain.Entities;
/// <summary>
/// A named tax rate (e.g. "Standard" 15%, "Zero-Rated" 0%, "Exempt" 0%) — the real backing
/// for <see cref="Item.TaxRateId"/>. Before this existed, <c>Item.TaxClass</c> was a free-text
/// label nothing ever read, and every sales/purchase line carried its own hand-typed tax
/// percentage with no link back to the item at all. Now a line's tax defaults to whatever
/// percentage the item's assigned TaxRate carries at the moment the line is created — change
/// the rate here and every new document picks it up; a line already created keeps the percentage
/// it was given (SalesSlipLine.TaxPct etc. are snapshots, never recomputed from this table).
/// </summary>
public class TaxRate
{
public int TaxRateId { get; set; }
public string Code { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public decimal Percentage { get; set; }
public EntityStatus Status { get; set; } = EntityStatus.Active;
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
/// <summary>PostgreSQL xmin-backed optimistic concurrency token (ETag source).</summary>
public uint RowVersion { get; set; }
}
+6
View File
@@ -1,3 +1,5 @@
using ERPCore.Domain.Enums;
namespace ERPCore.Domain.Entities; namespace ERPCore.Domain.Entities;
/// <summary> /// <summary>
@@ -10,4 +12,8 @@ public class Uom
{ {
public int UomId { get; set; } public int UomId { get; set; }
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public EntityStatus Status { get; set; } = EntityStatus.Active;
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
} }
@@ -17,9 +17,6 @@ public class Vendor
public string Currency { get; set; } = "LKR"; public string Currency { get; set; } = "LKR";
public EntityStatus Status { get; set; } = EntityStatus.Active; public EntityStatus Status { get; set; } = EntityStatus.Active;
public int BranchId { get; set; }
public Branch? Branch { get; set; }
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; } public DateTime? UpdatedAt { get; set; }
@@ -9,8 +9,6 @@ public class Warehouse
public int WarehouseId { get; set; } public int WarehouseId { get; set; }
public string Code { get; set; } = string.Empty; public string Code { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public string? Address { get; set; }
public string? Phone { get; set; }
public int BranchId { get; set; } public int BranchId { get; set; }
public Branch? Branch { get; set; } public Branch? Branch { get; set; }
@@ -1,10 +1,9 @@
namespace ERPCore.Domain.Enums; namespace ERPCore.Domain.Enums;
/// <summary> /// <summary>
/// Purchase-order lifecycle (docs/11 §8; docs/10 §B.8.1). Issuing a PO (create /// Purchase-order lifecycle (docs/11 §8; docs/10 §B.8.1). Phase 1 auto-approves on
/// non-draft, or submit a Draft) enters <see cref="PendingApproval"/>; an Admin who /// creation, so <see cref="PendingApproval"/> is reserved (not entered) until the
/// isn't the creator must Approve it to reach <see cref="Approved"/>, or Reject it back /// approval workflow is enabled (FR-PROC-04). Stored as a string.
/// to <see cref="Draft"/> (FR-PROC-04). Stored as a string.
/// </summary> /// </summary>
public enum PurchaseOrderStatus public enum PurchaseOrderStatus
{ {
@@ -1,21 +0,0 @@
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
}
@@ -1,8 +0,0 @@
namespace ERPCore.Domain.Enums;
public enum TicketStatus
{
Open,
InProgress,
Resolved
}
-15
View File
@@ -29,21 +29,6 @@ public sealed class AuthSessionResponse
public int ExpiresIn { get; set; } public int ExpiresIn { get; set; }
} }
/// <summary>Result of AuthHex's admin-only deleteUser function.</summary>
public sealed class DeleteAuthHexUserResult
{
public int DeletedCount { get; set; }
public List<DeletedAuthHexUser>? Deleted { get; set; }
}
public sealed class DeletedAuthHexUser
{
public Guid Id { get; set; }
public string? FullName { get; set; }
public string? UserName { get; set; }
public string? Email { get; set; }
}
public sealed class RegisterRequest public sealed class RegisterRequest
{ {
/// <summary>Optional — AuthHex requires a client-supplied id; ERPCore generates one when omitted.</summary> /// <summary>Optional — AuthHex requires a client-supplied id; ERPCore generates one when omitted.</summary>
@@ -4,7 +4,7 @@ namespace ERPCore.Dtos.Config;
/// <summary>Branch configuration resource — the singleton toggle for whether this deployment /// <summary>Branch configuration resource — the singleton toggle for whether this deployment
/// requires a branch on Employee/Warehouse records at all.</summary> /// requires a branch on Employee/Warehouse records at all.</summary>
public sealed record BranchConfigDto(bool BranchingEnabled, bool WarehouseBranchingEnabled, DateTime? UpdatedAt, int? UpdatedBy); public sealed record BranchConfigDto(bool BranchingEnabled, DateTime? UpdatedAt, int? UpdatedBy);
/// <summary> /// <summary>
/// Full replacement of the flag. <c>UpdatedBy</c> is derived from the token, never posted. /// Full replacement of the flag. <c>UpdatedBy</c> is derived from the token, never posted.
@@ -17,5 +17,4 @@ public sealed record BranchConfigDto(bool BranchingEnabled, bool WarehouseBranch
public sealed class UpdateBranchConfigRequest public sealed class UpdateBranchConfigRequest
{ {
[Required] public bool? BranchingEnabled { get; set; } [Required] public bool? BranchingEnabled { get; set; }
[Required] public bool? WarehouseBranchingEnabled { get; set; }
} }
@@ -21,7 +21,6 @@ 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);
@@ -41,7 +40,6 @@ 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
@@ -60,7 +58,6 @@ 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,12 +4,9 @@ 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,
/// open RFQs, pending leave requests, and overdue sales invoices. Also reused as small /// and open RFQs. Recent movements isn't here — it's just GET /stock/ledger with a small
/// KPI tiles on individual list pages (Purchase Orders, GRN, Requisitions, RFQs, Stock /// pageSize, no aggregation needed. A single computed-on-read object, not a stored
/// Counts, Reorder Alerts, HRM Leave, Sales Invoices), not just the Dashboard home page. /// entity — same as reorder alerts (docs/11 §5.7).
/// 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,
@@ -21,9 +18,7 @@ public sealed record DashboardStatsDto(
int PendingGrns, int PendingGrns,
int OpenRequisitions, int OpenRequisitions,
int PendingCounts, int PendingCounts,
int OpenRfqs, int OpenRfqs);
int PendingLeaveRequests,
int OverdueSalesInvoices);
/// <summary>One bar in the Stock Valuation chart — total FIFO layer value for a warehouse.</summary> /// <summary>One bar in the Stock Valuation chart — total FIFO layer value for a warehouse.</summary>
public sealed record WarehouseValuationDto(int WarehouseId, decimal Total); public sealed record WarehouseValuationDto(int WarehouseId, decimal Total);
+2 -3
View File
@@ -63,9 +63,8 @@ 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. Explicit /// <summary>VAT percentage (0–100). Recoverable — does not affect stock value.</summary>
/// override; omit (null) to use the item's assigned TaxRate percentage instead.</summary> [Range(0, 100)] public decimal VatPct { get; set; }
[Range(0, 100)] public decimal? VatPct { get; set; }
[EnumDataType(typeof(HoldStatus))] public HoldStatus HoldStatus { get; set; } = HoldStatus.Available; [EnumDataType(typeof(HoldStatus))] public HoldStatus HoldStatus { get; set; } = HoldStatus.Available;
public BatchInput? Batch { get; set; } public BatchInput? Batch { get; set; }
/// <summary> /// <summary>
-2
View File
@@ -3,8 +3,6 @@ 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,
+6 -14
View File
@@ -10,11 +10,10 @@ 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,
int? TaxRateId, string? TaxRateName, decimal? TaxRatePercentage, string? TaxClass, decimal? SellingMinPrice, decimal? SellingMaxPrice, decimal? SellingFlatPrice,
decimal? SellingMinPrice, decimal? SellingMaxPrice, decimal? SellingFlatPrice,
decimal? ContentQty, MeasureUnit? ContentUnit, decimal? ContentQty, MeasureUnit? ContentUnit,
decimal? ContentBaseQty, MeasureUnit? ContentBaseUnit, decimal? ContentBaseQty, MeasureUnit? ContentBaseUnit,
EntityStatus Status, string? Barcode); EntityStatus Status);
/// <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);
@@ -32,11 +31,10 @@ 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,
int? TaxRateId, string? TaxRateName, decimal? TaxRatePercentage, string? TaxClass, decimal? SellingMinPrice, decimal? SellingMaxPrice, decimal? SellingFlatPrice,
decimal? SellingMinPrice, decimal? SellingMaxPrice, decimal? SellingFlatPrice,
decimal? ContentQty, MeasureUnit? ContentUnit, decimal? ContentQty, MeasureUnit? ContentUnit,
decimal? ContentBaseQty, MeasureUnit? ContentBaseUnit, decimal? ContentBaseQty, MeasureUnit? ContentBaseUnit,
EntityStatus Status, string? Barcode, IReadOnlyList<ItemReorderDto> Reorder, EntityStatus Status, 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>
@@ -66,8 +64,7 @@ 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; }
/// <summary>The tax rate sales/purchase lines for this item default to — see <see cref="Domain.Entities.TaxRate"/>.</summary> [StringLength(20)] public string? TaxClass { get; set; }
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; }
@@ -80,8 +77,6 @@ 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
@@ -101,8 +96,7 @@ 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; }
/// <summary>The tax rate sales/purchase lines for this item default to — see <see cref="Domain.Entities.TaxRate"/>.</summary> [StringLength(20)] public string? TaxClass { get; set; }
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; }
@@ -115,8 +109,6 @@ 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
@@ -1,11 +0,0 @@
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);
@@ -1,15 +0,0 @@
namespace ERPCore.Dtos.Notifications;
public sealed record NotificationDto(
int NotificationId,
string ModuleKey,
string Title,
string Message,
string? RelatedEntityType,
int? RelatedEntityId,
DateTime CreatedAt,
bool IsRead);
public sealed record NotificationFeedDto(
IReadOnlyList<NotificationDto> Notifications,
int UnreadCount);
@@ -12,14 +12,14 @@ public sealed record PurchaseInvoiceTotalsDto(
public sealed record PurchaseInvoiceDto( public sealed record PurchaseInvoiceDto(
int PurchaseInvoiceId, string DocNo, string VendorInvoiceNo, DateTime VendorInvoiceDate, int PurchaseInvoiceId, string DocNo, string VendorInvoiceNo, DateTime VendorInvoiceDate,
int VendorId, int BranchId, string VendorSnapshotName, string? VendorSnapshotTaxNo, int VendorId, string VendorSnapshotName, string? VendorSnapshotTaxNo,
PurchaseInvoiceStatus Status, int GrnCount, int CreatedBy, DateTime CreatedAt, DateTime? UpdatedAt, PurchaseInvoiceStatus Status, int GrnCount, int CreatedBy, DateTime CreatedAt, DateTime? UpdatedAt,
string? GlJournalNo, DateTime? GlPostedAt, string? GlJournalNo, DateTime? GlPostedAt,
PurchaseInvoiceTotalsDto Totals, IReadOnlyList<PurchaseInvoiceLineDto> Lines); PurchaseInvoiceTotalsDto Totals, IReadOnlyList<PurchaseInvoiceLineDto> Lines);
public sealed record PurchaseInvoiceSummaryDto( public sealed record PurchaseInvoiceSummaryDto(
int PurchaseInvoiceId, string DocNo, string VendorInvoiceNo, DateTime VendorInvoiceDate, int PurchaseInvoiceId, string DocNo, string VendorInvoiceNo, DateTime VendorInvoiceDate,
int VendorId, int BranchId, string VendorSnapshotName, PurchaseInvoiceStatus Status, int GrnCount, int VendorId, string VendorSnapshotName, PurchaseInvoiceStatus Status, int GrnCount,
PurchaseInvoiceTotalsDto Totals, DateTime CreatedAt, string? GlJournalNo); PurchaseInvoiceTotalsDto Totals, DateTime CreatedAt, string? GlJournalNo);
public sealed record PurchaseInvoicePaymentDto( public sealed record PurchaseInvoicePaymentDto(
@@ -12,14 +12,13 @@ public sealed record PoLineDto(
public sealed record PoTotalsDto(decimal SubTotal, decimal Tax, decimal GrandTotal, string Currency); public sealed record PoTotalsDto(decimal SubTotal, decimal Tax, decimal GrandTotal, string Currency);
public sealed record PurchaseOrderDto( public sealed record PurchaseOrderDto(
int PoId, string DocNo, int VendorId, int BranchId, int? RequisitionId, PurchaseOrderStatus Status, int PoId, string DocNo, int VendorId, int? RequisitionId, PurchaseOrderStatus Status,
bool ApprovalRequired, int CreatedBy, int? ApprovedBy, DateTime? ApprovedAt, bool ApprovalRequired, int CreatedBy, DateTime CreatedAt, DateTime? UpdatedAt,
DateTime CreatedAt, DateTime? UpdatedAt,
PoTotalsDto Totals, IReadOnlyList<PoLineDto> Lines); PoTotalsDto Totals, IReadOnlyList<PoLineDto> Lines);
public sealed record PurchaseOrderSummaryDto( public sealed record PurchaseOrderSummaryDto(
int PoId, string DocNo, int VendorId, int BranchId, PurchaseOrderStatus Status, int PoId, string DocNo, int VendorId, PurchaseOrderStatus Status,
bool ApprovalRequired, int CreatedBy, DateTime CreatedAt, PoTotalsDto Totals); bool ApprovalRequired, DateTime CreatedAt, PoTotalsDto Totals);
// Requests — server sets docNo, status, createdBy, timestamps, qtyReceived, totals // Requests — server sets docNo, status, createdBy, timestamps, qtyReceived, totals
@@ -36,13 +35,12 @@ public sealed class CreatePurchaseOrderRequest
{ {
[Required] public int VendorId { get; set; } [Required] public int VendorId { get; set; }
public int? RequisitionId { get; set; } public int? RequisitionId { get; set; }
public int BranchId { get; set; }
[Required, MinLength(1)] public List<CreatePoLineInput> Lines { get; set; } = new(); [Required, MinLength(1)] public List<CreatePoLineInput> Lines { get; set; } = new();
/// <summary> /// <summary>
/// When true the PO is created in <c>Draft</c> (editable/deletable, not yet issued). /// When true the PO is created in <c>Draft</c> (editable/deletable, not yet issued).
/// When false (default) it is issued immediately into <c>PendingApproval</c>, same as /// When false (default) it auto-approves on creation, preserving the Requisition→PO
/// submitting a Draft (docs/11 §3.3). /// and RFQ→PO flows unchanged (docs/11 §3.3).
/// </summary> /// </summary>
public bool SaveAsDraft { get; set; } public bool SaveAsDraft { get; set; }
} }
@@ -51,7 +49,6 @@ public sealed class UpdatePurchaseOrderRequest
{ {
[Required] public int VendorId { get; set; } [Required] public int VendorId { get; set; }
public int? RequisitionId { get; set; } public int? RequisitionId { get; set; }
public int BranchId { get; set; }
[Required, MinLength(1)] public List<CreatePoLineInput> Lines { get; set; } = new(); [Required, MinLength(1)] public List<CreatePoLineInput> Lines { get; set; } = new();
} }
@@ -59,8 +56,3 @@ public sealed class CancelPurchaseOrderRequest
{ {
[StringLength(500)] public string? Reason { get; set; } [StringLength(500)] public string? Reason { get; set; }
} }
public sealed class RejectPurchaseOrderRequest
{
[StringLength(500)] public string? Reason { get; set; }
}
@@ -8,11 +8,11 @@ namespace ERPCore.Dtos.Procurement;
public sealed record RequisitionLineDto(int ReqLineId, int ItemId, decimal Qty, DateOnly? RequiredBy); public sealed record RequisitionLineDto(int ReqLineId, int ItemId, decimal Qty, DateOnly? RequiredBy);
public sealed record RequisitionDto( public sealed record RequisitionDto(
int RequisitionId, string DocNo, RequisitionStatus Status, int RequestedBy, int BranchId, int RequisitionId, string DocNo, RequisitionStatus Status, int RequestedBy,
DateTime CreatedAt, IReadOnlyList<RequisitionLineDto> Lines); DateTime CreatedAt, IReadOnlyList<RequisitionLineDto> Lines);
public sealed record RequisitionSummaryDto( public sealed record RequisitionSummaryDto(
int RequisitionId, string DocNo, RequisitionStatus Status, int RequestedBy, int BranchId, DateTime CreatedAt, int LineCount); int RequisitionId, string DocNo, RequisitionStatus Status, int RequestedBy, DateTime CreatedAt, int LineCount);
// Requests — server sets docNo, status, requestedBy (audit actor), timestamps ---- // Requests — server sets docNo, status, requestedBy (audit actor), timestamps ----
@@ -26,7 +26,4 @@ public sealed class CreateRequisitionLineInput
public sealed class CreateRequisitionRequest public sealed class CreateRequisitionRequest
{ {
[Required, MinLength(1)] public List<CreateRequisitionLineInput> Lines { get; set; } = new(); [Required, MinLength(1)] public List<CreateRequisitionLineInput> Lines { get; set; } = new();
/// <summary>Optional — resolved via <see cref="Services.Interfaces.IBranchConfigService.ResolveBranchIdAsync"/>
/// when branching is enabled; ignored (defaulted) otherwise.</summary>
public int BranchId { get; set; }
} }
+2 -2
View File
@@ -8,11 +8,11 @@ namespace ERPCore.Dtos.Procurement;
public sealed record RfqLineDto(int RfqLineId, int ItemId, decimal Qty); public sealed record RfqLineDto(int RfqLineId, int ItemId, decimal Qty);
public sealed record RfqDto( public sealed record RfqDto(
int RfqId, string DocNo, int RequisitionId, int BranchId, RfqStatus Status, IReadOnlyList<RfqLineDto> Lines); int RfqId, string DocNo, int RequisitionId, RfqStatus Status, IReadOnlyList<RfqLineDto> Lines);
/// <summary>Row shape for <c>GET /rfqs</c> — line/quotation counts instead of the lines themselves.</summary> /// <summary>Row shape for <c>GET /rfqs</c> — line/quotation counts instead of the lines themselves.</summary>
public sealed record RfqSummaryDto( public sealed record RfqSummaryDto(
int RfqId, string DocNo, int RequisitionId, int BranchId, RfqStatus Status, int LineCount, int QuotationCount); int RfqId, string DocNo, int RequisitionId, RfqStatus Status, int LineCount, int QuotationCount);
public sealed record QuotationLineDto(int ItemId, decimal UnitPrice, int LeadDays); public sealed record QuotationLineDto(int ItemId, decimal UnitPrice, int LeadDays);
+1 -1
View File
@@ -11,4 +11,4 @@ namespace ERPCore.Dtos.Rbac;
/// entirely rather than merely restricting them by role.</summary> /// entirely rather than merely restricting them by role.</summary>
public sealed record MeResponseDto( public sealed record MeResponseDto(
string? RoleCode, string? RoleName, IReadOnlyList<string> NavCodes, string? RoleCode, string? RoleName, IReadOnlyList<string> NavCodes,
bool IsAdmin, int? BranchId, string? BranchName, bool BranchingEnabled, bool WarehouseBranchingEnabled); bool IsAdmin, int? BranchId, string? BranchName, bool BranchingEnabled);
+1 -17
View File
@@ -29,22 +29,6 @@ public sealed class AssignRolePermissionsRequest
{ {
public List<int> NavItemIds { get; set; } = new(); public List<int> NavItemIds { get; set; } = new();
public List<int> SubNavItemIds { get; set; } = new(); public List<int> SubNavItemIds { get; set; } = new();
/// <summary>
/// Optional per-nav/sub-nav-item action override, keyed by <c>NavItemId</c>/
/// <c>SubNavItemId</c> and valued with a <see cref="RolePermissionAction"/> bitmask
/// (e.g. <c>View|Create|Edit</c> = 7 to grant a module without Delete/Approve). An id
/// present in <see cref="NavItemIds"/>/<see cref="SubNavItemIds"/> but absent here
/// keeps today's default of <see cref="RolePermissionAction.All"/> — this only narrows,
/// never grants access to an id that isn't also in those lists.
/// </summary>
public Dictionary<int, RolePermissionAction> NavItemActions { get; set; } = new();
public Dictionary<int, RolePermissionAction> SubNavItemActions { get; set; } = new();
} }
public sealed record RolePermissionsDto( public sealed record RolePermissionsDto(int RoleId, List<int> NavItemIds, List<int> SubNavItemIds);
int RoleId,
List<int> NavItemIds,
List<int> SubNavItemIds,
Dictionary<int, RolePermissionAction> NavItemActions,
Dictionary<int, RolePermissionAction> SubNavItemActions);
@@ -20,21 +20,19 @@ public sealed record SalesDayEndInvoiceSummaryDto(
public sealed record SalesDayEndDto( public sealed record SalesDayEndDto(
int SalesDayEndId, string DocNo, int CashierUserId, DateOnly BusinessDate, int SalesDayEndId, string DocNo, int CashierUserId, DateOnly BusinessDate,
int SlipCount, int BundleCount, decimal Subtotal, decimal DiscountTotal, decimal TaxTotal, decimal GrandTotal, int SlipCount, int BundleCount, decimal Subtotal, decimal DiscountTotal, decimal TaxTotal, decimal GrandTotal,
decimal PaidTotal, decimal BalanceTotal,
decimal CostOfGoodsSold, string? GlJournalNo, DateTime? GlPostedAt, int ClosedBy, DateTime ClosedAt, decimal CostOfGoodsSold, string? GlJournalNo, DateTime? GlPostedAt, int ClosedBy, DateTime ClosedAt,
IReadOnlyList<string> SlipNumbers, IReadOnlyList<string> BundleNumbers, IReadOnlyList<SalesDayEndItemLineDto> ItemBreakdown, IReadOnlyList<string> SlipNumbers, IReadOnlyList<string> BundleNumbers, IReadOnlyList<SalesDayEndItemLineDto> ItemBreakdown,
SalesDayEndInvoiceSummaryDto Invoices); SalesDayEndInvoiceSummaryDto Invoices);
public sealed record SalesDayEndSummaryDto( public sealed record SalesDayEndSummaryDto(
int SalesDayEndId, string DocNo, int CashierUserId, DateOnly BusinessDate, int SalesDayEndId, string DocNo, int CashierUserId, DateOnly BusinessDate,
int SlipCount, int BundleCount, decimal GrandTotal, decimal PaidTotal, decimal BalanceTotal, string? GlJournalNo, DateTime ClosedAt); int SlipCount, int BundleCount, decimal GrandTotal, string? GlJournalNo, DateTime ClosedAt);
/// <summary>What closing right now would include — call before <c>POST /sales-day-end</c> so the /// <summary>What closing right now would include — call before <c>POST /sales-day-end</c> so the
/// cashier can see the day's totals, and any still-Draft slips/bundles blocking the close.</summary> /// cashier can see the day's totals, and any still-Draft slips/bundles blocking the close.</summary>
public sealed record SalesDayEndPreviewDto( public sealed record SalesDayEndPreviewDto(
int CashierUserId, DateOnly BusinessDate, bool AlreadyClosed, int CashierUserId, DateOnly BusinessDate, bool AlreadyClosed,
int SlipCount, int BundleCount, decimal Subtotal, decimal DiscountTotal, decimal TaxTotal, decimal GrandTotal, int SlipCount, int BundleCount, decimal Subtotal, decimal DiscountTotal, decimal TaxTotal, decimal GrandTotal,
decimal PaidTotal, decimal BalanceTotal,
IReadOnlyList<string> SlipNumbers, IReadOnlyList<string> BundleNumbers, IReadOnlyList<string> SlipNumbers, IReadOnlyList<string> BundleNumbers,
IReadOnlyList<DraftSlipBlockingCloseDto> DraftSlipsBlockingClose, IReadOnlyList<DraftSlipBlockingCloseDto> DraftSlipsBlockingClose,
IReadOnlyList<DraftBundleBlockingCloseDto> DraftBundleSalesBlockingClose, IReadOnlyList<DraftBundleBlockingCloseDto> DraftBundleSalesBlockingClose,
@@ -56,9 +56,7 @@ public sealed class CreateSalesInvoiceLineRequest
[Range(0, 100)] public decimal DiscountPct { get; set; } [Range(0, 100)] public decimal DiscountPct { get; set; }
[Range(0, double.MaxValue)] public decimal DiscountAmount { get; set; } [Range(0, double.MaxValue)] public decimal DiscountAmount { get; set; }
[Range(0, double.MaxValue)] public decimal DiscountValue { get; set; } [Range(0, double.MaxValue)] public decimal DiscountValue { get; set; }
/// <summary>Explicit override. Omit (null) to use the item's assigned TaxRate — see [Range(0, 100)] public decimal TaxPct { get; set; }
/// <see cref="Services.Interfaces.ISalesDomainService.ResolveLineTaxPctAsync"/>.</summary>
[Range(0, 100)] public decimal? TaxPct { get; set; }
public bool IsFreeIssue { get; set; } public bool IsFreeIssue { get; set; }
public int? ParentLineId { get; set; } public int? ParentLineId { get; set; }
} }
+2 -22
View File
@@ -7,8 +7,7 @@ public sealed record SalesSlipLineDto(
int SalesSlipLineId, int ItemId, string Description, decimal Qty, decimal FreeQty, int SalesSlipLineId, int ItemId, string Description, decimal Qty, decimal FreeQty,
int WarehouseId, decimal UnitPrice, decimal BaseCost, string PriceSource, decimal DiscountPct, int WarehouseId, decimal UnitPrice, decimal BaseCost, string PriceSource, decimal DiscountPct,
decimal DiscountAmount, SalesDiscountMode DiscountMode, decimal NetUnitPrice, decimal LineTotal, decimal DiscountAmount, SalesDiscountMode DiscountMode, decimal NetUnitPrice, decimal LineTotal,
decimal TaxPct, decimal TaxAmount, bool IsFreeIssue, int? ParentLineId, decimal TaxPct, decimal TaxAmount, bool IsFreeIssue, int? ParentLineId);
int? BatchId, int? SerialId);
public sealed record SalesSlipTotalsDto( public sealed record SalesSlipTotalsDto(
decimal Subtotal, decimal DiscountTotal, decimal FreeQtyTotal, decimal TaxTotal, decimal GrandTotal, decimal Subtotal, decimal DiscountTotal, decimal FreeQtyTotal, decimal TaxTotal, decimal GrandTotal,
@@ -55,15 +54,9 @@ public sealed class CreateSalesSlipLineRequest
[Range(0, 100)] public decimal DiscountPct { get; set; } [Range(0, 100)] public decimal DiscountPct { get; set; }
[Range(0, double.MaxValue)] public decimal DiscountAmount { get; set; } [Range(0, double.MaxValue)] public decimal DiscountAmount { get; set; }
[Range(0, double.MaxValue)] public decimal DiscountValue { get; set; } [Range(0, double.MaxValue)] public decimal DiscountValue { get; set; }
/// <summary>Explicit override. Omit (null) to use the item's assigned TaxRate — see [Range(0, 100)] public decimal TaxPct { get; set; }
/// <see cref="Services.Interfaces.ISalesDomainService.ResolveLineTaxPctAsync"/>.</summary>
[Range(0, 100)] public decimal? TaxPct { get; set; }
public bool IsFreeIssue { get; set; } public bool IsFreeIssue { get; set; }
public int? ParentLineId { get; set; } public int? ParentLineId { get; set; }
/// <summary>Pins the line to a specific batch (must belong to <see cref="ItemId"/>), consumed via FIFO.</summary>
public int? BatchId { get; set; }
/// <summary>Pins the line to a specific serial (must belong to <see cref="ItemId"/>); marked Sold on posting.</summary>
public int? SerialId { get; set; }
} }
public sealed class CreateSalesSlipRequest public sealed class CreateSalesSlipRequest
@@ -72,16 +65,6 @@ public sealed class CreateSalesSlipRequest
[Required] public int WarehouseId { get; set; } [Required] public int WarehouseId { get; set; }
[Required] public int CashierUserId { get; set; } [Required] public int CashierUserId { get; set; }
[Required, MinLength(1)] public List<CreateSalesSlipLineRequest> Lines { get; set; } = new(); [Required, MinLength(1)] public List<CreateSalesSlipLineRequest> Lines { get; set; } = new();
/// <summary>
/// How much the customer actually paid at ring-up. Omit (or send null) for the default —
/// fully paid, i.e. <c>PaidAmount == GrandTotal</c> and <c>BalanceAmount == 0</c> — matching
/// every slip's behavior before partial payment existed. Must be between 0 and the computed
/// GrandTotal; anything less than GrandTotal leaves the difference as BalanceAmount owed by
/// the customer "on account," which Sales Day-End then splits between Cash and Accounts
/// Receivable when it posts (see SalesDayEndService.CloseAsync).
/// </summary>
[Range(0, double.MaxValue)] public decimal? PaidAmount { get; set; }
} }
public sealed class UpdateSalesSlipRequest public sealed class UpdateSalesSlipRequest
@@ -90,7 +73,4 @@ public sealed class UpdateSalesSlipRequest
[Required] public int WarehouseId { get; set; } [Required] public int WarehouseId { get; set; }
[Required] public int CashierUserId { get; set; } [Required] public int CashierUserId { get; set; }
[Required, MinLength(1)] public List<CreateSalesSlipLineRequest> Lines { get; set; } = new(); [Required, MinLength(1)] public List<CreateSalesSlipLineRequest> Lines { get; set; } = new();
/// <summary>See <see cref="CreateSalesSlipRequest.PaidAmount"/>.</summary>
[Range(0, double.MaxValue)] public decimal? PaidAmount { get; set; }
} }
@@ -1,16 +0,0 @@
namespace ERPCore.Dtos.Search;
public enum GlobalSearchResultType
{
Item,
PurchaseOrder,
Invoice,
Employee
}
public sealed record GlobalSearchResultDto(
GlobalSearchResultType Type,
int Id,
string Number,
string Title,
string? Subtitle);
+1 -3
View File
@@ -5,9 +5,7 @@ namespace ERPCore.Dtos.Stock;
// Responses (docs/11 §5.6) ------------------------------------------------------ // Responses (docs/11 §5.6) ------------------------------------------------------
public sealed record CountLineDto( public sealed record CountLineDto(int CountLineId, int ItemId, int? BinId, decimal SystemQty, decimal? CountedQty, decimal? Variance);
int CountLineId, int ItemId, int? BinId, decimal SystemQty, decimal? CountedQty, decimal? Variance,
int? BatchId, string? BatchNo, int? SerialId, string? SerialNo);
public sealed record CountDto( public sealed record CountDto(
int CountId, string DocNo, int WarehouseId, int BranchId, CountType CountType, CountStatus Status, int CountId, string DocNo, int WarehouseId, int BranchId, CountType CountType, CountStatus Status,
@@ -1,56 +0,0 @@
using System.ComponentModel.DataAnnotations;
using ERPCore.Domain.Enums;
namespace ERPCore.Dtos.Support;
public sealed class CreateSupportTicketRequest
{
[Required, MaxLength(200)]
public string Subject { get; set; } = string.Empty;
[Required, MaxLength(2000)]
public string Message { get; set; } = string.Empty;
}
public sealed class AddSupportTicketMessageRequest
{
[Required, MaxLength(2000)]
public string Message { get; set; } = string.Empty;
}
public sealed class UpdateSupportTicketStatusRequest
{
[Required]
public TicketStatus Status { get; set; }
}
public sealed record SupportTicketMessageDto(
int MessageId,
int SenderUserId,
string SenderName,
bool IsFromAdmin,
string Message,
DateTime CreatedAt);
public sealed record SupportTicketSummaryDto(
int TicketId,
string Subject,
TicketStatus Status,
int CreatedBy,
string CreatedByName,
DateTime CreatedAt,
int? UpdatedBy,
DateTime? UpdatedAt,
int MessageCount);
public sealed record SupportTicketDto(
int TicketId,
string Subject,
TicketStatus Status,
int CreatedBy,
string CreatedByName,
DateTime CreatedAt,
int? UpdatedBy,
string? UpdatedByName,
DateTime? UpdatedAt,
IReadOnlyList<SupportTicketMessageDto> Messages);
@@ -1,26 +0,0 @@
using System.ComponentModel.DataAnnotations;
using ERPCore.Domain.Enums;
namespace ERPCore.Dtos.TaxRates;
public sealed record TaxRateDto(
int TaxRateId, string Code, string Name, decimal Percentage, EntityStatus Status, DateTime CreatedAt, DateTime? UpdatedAt);
public sealed class CreateTaxRateRequest
{
[Required, StringLength(20)] public string Code { get; set; } = string.Empty;
[Required, StringLength(100)] public string Name { get; set; } = string.Empty;
[Range(0, 100)] public decimal Percentage { get; set; }
}
public sealed class UpdateTaxRateRequest
{
[Required, StringLength(20)] public string Code { get; set; } = string.Empty;
[Required, StringLength(100)] public string Name { get; set; } = string.Empty;
[Range(0, 100)] public decimal Percentage { get; set; }
}
public sealed class UpdateTaxRateStatusRequest
{
[Required, EnumDataType(typeof(EntityStatus))] public EntityStatus Status { get; set; }
}
+7 -1
View File
@@ -1,11 +1,17 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using ERPCore.Domain.Enums;
namespace ERPCore.Dtos.Uoms; namespace ERPCore.Dtos.Uoms;
/// <summary>UOM resource (docs/11-BACKEND-PHASE1.md §2.2).</summary> /// <summary>UOM resource (docs/11-BACKEND-PHASE1.md §2.2).</summary>
public sealed record UomDto(int UomId, string Name); public sealed record UomDto(int UomId, string Name, EntityStatus Status, DateTime CreatedAt, DateTime? UpdatedAt);
public sealed class CreateUomRequest public sealed class CreateUomRequest
{ {
[Required, StringLength(50)] public string Name { get; set; } = string.Empty; [Required, StringLength(50)] public string Name { get; set; } = string.Empty;
} }
public sealed class UpdateUomStatusRequest
{
[Required, EnumDataType(typeof(EntityStatus))] public EntityStatus Status { get; set; }
}
-9
View File
@@ -7,15 +7,6 @@ public sealed record ManagedUserDto(
int UserId, string Username, string DisplayName, string? Email, EntityStatus Status, int UserId, string Username, string DisplayName, string? Email, EntityStatus Status,
int? RoleId, string? RoleCode, string? RoleName); int? RoleId, string? RoleCode, string? RoleName);
/// <summary>At least one identifier must be supplied — matches AuthHex's own OR-across-fields dedupe.</summary>
public sealed class PurgeAuthHexIdentityRequest
{
public string? Email { get; set; }
public string? MobileNumber { get; set; }
public string? Nic { get; set; }
public string? UserName { get; set; }
}
/// <summary> /// <summary>
/// Creates a user in both backends: forwards to AuthHex's `registerUser` (source /// Creates a user in both backends: forwards to AuthHex's `registerUser` (source
/// of truth for credentials), then mirrors the account into ERPCore's local /// of truth for credentials), then mirrors the account into ERPCore's local
+1 -5
View File
@@ -6,7 +6,7 @@ namespace ERPCore.Dtos.Vendors;
/// <summary>Vendor resource (docs/11-BACKEND-PHASE1.md §2.4).</summary> /// <summary>Vendor resource (docs/11-BACKEND-PHASE1.md §2.4).</summary>
public sealed record VendorDto( public sealed record VendorDto(
int VendorId, string Code, string Name, string? Terms, string? TaxReg, int VendorId, string Code, string Name, string? Terms, string? TaxReg,
string Currency, EntityStatus Status, int BranchId, DateTime CreatedAt, DateTime? UpdatedAt); string Currency, EntityStatus Status, DateTime CreatedAt, DateTime? UpdatedAt);
public sealed class CreateVendorRequest public sealed class CreateVendorRequest
{ {
@@ -15,9 +15,6 @@ public sealed class CreateVendorRequest
[StringLength(50)] public string? Terms { get; set; } [StringLength(50)] public string? Terms { get; set; }
[StringLength(50)] public string? TaxReg { get; set; } [StringLength(50)] public string? TaxReg { get; set; }
[Required, StringLength(3, MinimumLength = 3)] public string Currency { get; set; } = "LKR"; [Required, StringLength(3, MinimumLength = 3)] public string Currency { get; set; } = "LKR";
/// <summary>Optional — resolved via <see cref="Services.Interfaces.IBranchConfigService.ResolveBranchIdAsync"/>
/// when branching is enabled; ignored (defaulted) otherwise.</summary>
public int BranchId { get; set; }
} }
public sealed class UpdateVendorRequest public sealed class UpdateVendorRequest
@@ -27,7 +24,6 @@ public sealed class UpdateVendorRequest
[StringLength(50)] public string? Terms { get; set; } [StringLength(50)] public string? Terms { get; set; }
[StringLength(50)] public string? TaxReg { get; set; } [StringLength(50)] public string? TaxReg { get; set; }
[Required, StringLength(3, MinimumLength = 3)] public string Currency { get; set; } = "LKR"; [Required, StringLength(3, MinimumLength = 3)] public string Currency { get; set; } = "LKR";
public int BranchId { get; set; }
} }
public sealed class UpdateVendorStatusRequest public sealed class UpdateVendorStatusRequest
@@ -5,7 +5,7 @@ namespace ERPCore.Dtos.Warehouses;
/// <summary>Warehouse resource (docs/11-BACKEND-PHASE1.md §2.5). <see cref="BranchId"/> is /// <summary>Warehouse resource (docs/11-BACKEND-PHASE1.md §2.5). <see cref="BranchId"/> is
/// the anchor `docs/BRANCH_ISOLATION_PLAN.md` uses to branch-scope everything that carries /// the anchor `docs/BRANCH_ISOLATION_PLAN.md` uses to branch-scope everything that carries
/// a `warehouseId` (Sales, GRN, stock, ...) without a column on every one of those entities.</summary> /// a `warehouseId` (Sales, GRN, stock, ...) without a column on every one of those entities.</summary>
public sealed record WarehouseDto(int WarehouseId, string Code, string Name, string? Address, string? Phone, int BranchId, string? BranchName); public sealed record WarehouseDto(int WarehouseId, string Code, string Name, int BranchId, string? BranchName);
/// <summary>Bin/location resource (docs/11 §2.5).</summary> /// <summary>Bin/location resource (docs/11 §2.5).</summary>
public sealed record BinDto(int BinId, int WarehouseId, string Code, string? BinType); public sealed record BinDto(int BinId, int WarehouseId, string Code, string? BinType);
@@ -14,17 +14,6 @@ public sealed class CreateWarehouseRequest
{ {
[Required, StringLength(50)] public string Code { get; set; } = string.Empty; [Required, StringLength(50)] 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(300)] public string? Address { get; set; }
[StringLength(30)] public string? Phone { get; set; }
[Required] public int BranchId { get; set; }
}
public sealed class UpdateWarehouseRequest
{
[Required, StringLength(50)] public string Code { get; set; } = string.Empty;
[Required, StringLength(200)] public string Name { get; set; } = string.Empty;
[StringLength(300)] public string? Address { get; set; }
[StringLength(30)] public string? Phone { get; set; }
[Required] public int BranchId { get; set; } [Required] public int BranchId { get; set; }
} }
+1 -2
View File
@@ -1,10 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>48915044-7652-476f-ac84-0c82c718098a</UserSecretsId>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -28,13 +28,8 @@ public sealed class AuthHexClient : IAuthHexClient
}; };
private readonly HttpClient _http; private readonly HttpClient _http;
private readonly IConfiguration _config;
public AuthHexClient(HttpClient http, IConfiguration config) public AuthHexClient(HttpClient http) => _http = http;
{
_http = http;
_config = config;
}
// ---- UserManager -------------------------------------------------- // ---- UserManager --------------------------------------------------
@@ -92,19 +87,6 @@ public sealed class AuthHexClient : IAuthHexClient
public Task<TwoFaStatusResponse> GetTwoFaStatusAsync(string bearerToken, CancellationToken ct) public Task<TwoFaStatusResponse> GetTwoFaStatusAsync(string bearerToken, CancellationToken ct)
=> CallAsync<TwoFaStatusResponse>("user", "getTwoFAStatus", new { }, bearerToken, ct); => CallAsync<TwoFaStatusResponse>("user", "getTwoFAStatus", new { }, bearerToken, ct);
public Task<DeleteAuthHexUserResult> DeleteUserAsync(string? email, string? mobileNumber, string? nic, string? username, CancellationToken ct)
{
var adminApiKey = _config["AuthHex:AdminApiKey"];
if (string.IsNullOrEmpty(adminApiKey))
throw new DomainException(ErrorCodes.AuthUpstreamError, "AuthHex:AdminApiKey is not configured — cannot call the admin deleteUser function.", 500);
return CallAsync<DeleteAuthHexUserResult>(
"user", "deleteUser",
new { email, mobileNumber, nic, userName = username },
null, ct,
extraHeaders: new Dictionary<string, string> { ["X-Admin-Api-Key"] = adminApiKey });
}
// ---- RecoveryManager ------------------------------------------------ // ---- RecoveryManager ------------------------------------------------
public Task<ForgotPasswordResponse> ForgotPasswordAsync(ForgotPasswordRequest request, CancellationToken ct) public Task<ForgotPasswordResponse> ForgotPasswordAsync(ForgotPasswordRequest request, CancellationToken ct)
@@ -152,14 +134,11 @@ public sealed class AuthHexClient : IAuthHexClient
private async Task CallVoidAsync(string routeGroup, string functionName, object payload, string? bearerToken, CancellationToken ct) private async Task CallVoidAsync(string routeGroup, string functionName, object payload, string? bearerToken, CancellationToken ct)
=> await CallAsync<JsonElement?>(routeGroup, functionName, payload, bearerToken, ct); => await CallAsync<JsonElement?>(routeGroup, functionName, payload, bearerToken, ct);
private async Task<T> CallAsync<T>(string routeGroup, string functionName, object payload, string? bearerToken, CancellationToken ct, Dictionary<string, string>? extraHeaders = null) private async Task<T> CallAsync<T>(string routeGroup, string functionName, object payload, string? bearerToken, CancellationToken ct)
{ {
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"/api/{routeGroup}"); using var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"/api/{routeGroup}");
if (!string.IsNullOrEmpty(bearerToken)) if (!string.IsNullOrEmpty(bearerToken))
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken); httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);
if (extraHeaders is not null)
foreach (var (key, value) in extraHeaders)
httpRequest.Headers.Add(key, value);
httpRequest.Content = JsonContent.Create( httpRequest.Content = JsonContent.Create(
new AuthHexRequestBody { FunctionName = functionName, Payload = payload, Reference = string.Empty }, new AuthHexRequestBody { FunctionName = functionName, Payload = payload, Reference = string.Empty },
@@ -30,14 +30,6 @@ public interface IAuthHexClient
Task DisableTwoFaAsync(DisableTwoFaRequest request, string bearerToken, CancellationToken ct); Task DisableTwoFaAsync(DisableTwoFaRequest request, string bearerToken, CancellationToken ct);
Task<TwoFaStatusResponse> GetTwoFaStatusAsync(string bearerToken, CancellationToken ct); Task<TwoFaStatusResponse> GetTwoFaStatusAsync(string bearerToken, CancellationToken ct);
/// <summary>
/// Admin-only: permanently deletes every AuthHex identity matching any of the given
/// identifiers, so a stale email/NIC/username/mobile no longer blocks re-registration
/// (CanRegisterUser's duplicate check has no notion of "deleted" — this is the only way
/// to actually clear one). Gated by AuthHex:AdminApiKey — throws if unset or rejected.
/// </summary>
Task<DeleteAuthHexUserResult> DeleteUserAsync(string? email, string? mobileNumber, string? nic, string? username, CancellationToken ct);
// RecoveryManager (POST /api/recovery) // RecoveryManager (POST /api/recovery)
Task<ForgotPasswordResponse> ForgotPasswordAsync(ForgotPasswordRequest request, CancellationToken ct); Task<ForgotPasswordResponse> ForgotPasswordAsync(ForgotPasswordRequest request, CancellationToken ct);
Task<VerifyRecoveryOtpResponse> VerifyRecoveryOtpAsync(VerifyRecoveryOtpRequest request, CancellationToken ct); Task<VerifyRecoveryOtpResponse> VerifyRecoveryOtpAsync(VerifyRecoveryOtpRequest request, CancellationToken ct);
@@ -14,12 +14,4 @@ public static class AuthHexClaims
public const string RoleId = "RoleId"; public const string RoleId = "RoleId";
public const string RoleCode = "RoleCode"; public const string RoleCode = "RoleCode";
public const string Nic = "NIC"; public const string Nic = "NIC";
/// <summary>
/// Local (non-AuthHex) claim injected by <see cref="ShadowUserClaimsTransformation"/> —
/// the resolved local user's <see cref="Entities.Employee.BranchId"/>, used by
/// <see cref="ICurrentUser.BranchId"/>/<see cref="ICurrentBranchContext"/> for
/// branch-scoped query filtering (docs/BRANCH_ISOLATION_PLAN.md).
/// </summary>
public const string BranchId = "erp:BranchId";
} }
@@ -35,13 +35,4 @@ public sealed class CurrentUser : ICurrentUser
public string? RoleCode => _accessor.HttpContext?.User?.FindFirstValue(AuthHexClaims.RoleCode); public string? RoleCode => _accessor.HttpContext?.User?.FindFirstValue(AuthHexClaims.RoleCode);
public bool IsAdmin => string.Equals(RoleCode?.Trim(), "Admin", StringComparison.OrdinalIgnoreCase); public bool IsAdmin => string.Equals(RoleCode?.Trim(), "Admin", StringComparison.OrdinalIgnoreCase);
public int? BranchId
{
get
{
var raw = _accessor.HttpContext?.User?.FindFirstValue(AuthHexClaims.BranchId);
return int.TryParse(raw, out var id) ? id : null;
}
}
} }
@@ -1,34 +0,0 @@
namespace ERPCore.Infra.Auth;
/// <summary>
/// Scoped accessor feeding <see cref="Persistence.ErpDbContext"/>'s global query filters
/// (docs/BRANCH_ISOLATION_PLAN.md — "What this doesn't solve by itself" / enforcement
/// layer). EF Core query filters can't read ambient HTTP state directly, so this is
/// resolved once per request/scope and injected into the DbContext constructor instead.
/// </summary>
public interface ICurrentBranchContext
{
/// <summary>The branch to scope reads to. Null means "don't filter" — used for
/// admins, unauthenticated/system contexts, and any caller with no resolved branch.</summary>
int? BranchId { get; }
/// <summary>False disables filtering entirely (e.g. background jobs, migrations,
/// admin callers) regardless of <see cref="BranchId"/>.</summary>
bool IsActive { get; }
}
/// <summary>
/// Default implementation: derives from <see cref="ICurrentUser"/>. Admins and users with
/// no resolved branch are never filtered — the same bypass <see cref="Services.Interfaces.IBranchAccessService"/>
/// already applies for write-path checks.
/// </summary>
public sealed class CurrentBranchContext : ICurrentBranchContext
{
private readonly ICurrentUser _currentUser;
public CurrentBranchContext(ICurrentUser currentUser) => _currentUser = currentUser;
public int? BranchId => _currentUser.IsAdmin ? null : _currentUser.BranchId;
public bool IsActive => _currentUser.IsAuthenticated && !_currentUser.IsAdmin && _currentUser.BranchId is not null;
}

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