Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 049c98298a | |||
| f5e8e0e5b9 | |||
| 4c5e9b347a | |||
| fdd104284e | |||
| 72819e5f7b | |||
| edd1011330 | |||
| 3ae14fad72 | |||
| 84d3318f76 | |||
| bfb8e83a82 | |||
| 40c1d6da27 | |||
| 5aa709db17 | |||
| cf964a19a9 | |||
| a40c6bf598 | |||
| f951700b58 | |||
| b3939e0685 | |||
| af6f02023d | |||
| 9d569a1504 | |||
| 5a44702cf3 | |||
| 2a5dee3fef | |||
| 2584ab8715 | |||
| 6b9d191bf8 | |||
| 283fa21e87 | |||
| 97d19d83ab | |||
| 01d5159d27 | |||
| 80899ed88c | |||
| cfe2466fec | |||
| 9a124c8c63 | |||
| 42ab7e5964 | |||
| e7735135ca | |||
| f61d1f95b8 | |||
| f632c5fc99 | |||
| 70179e1abc | |||
| 8c3d62223c | |||
| e1f19fe413 | |||
| cb6b92d221 | |||
| 3f10ae3bca | |||
| 7274b89b28 | |||
| 61fe1a0e0c | |||
| 941f20d4c5 | |||
| b06d20e649 | |||
| 445d323bed | |||
| 9028255800 | |||
| 1b2f244853 | |||
| 475bcfec1a | |||
| 85db26ec82 | |||
| baee987d26 | |||
| f45a59f20d | |||
| 32304cbaff | |||
| 2b64bb0ebc | |||
| ffe5251236 | |||
| d04a7fb3bc | |||
| d76595085e | |||
| bad33a8492 | |||
| b0216fca30 | |||
| 49c112d188 | |||
| c836b57d54 |
+26
-36
@@ -3,13 +3,13 @@ name: CI/CD - ERP Core Monorepo
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- Production
|
||||
- production
|
||||
types:
|
||||
- closed
|
||||
|
||||
jobs:
|
||||
backend-build-and-deploy:
|
||||
name: Build & Deploy ERP Core Backend
|
||||
build-and-deploy-backend:
|
||||
name: Build & Deploy Backend
|
||||
runs-on: linux
|
||||
if: github.event.pull_request.merged == true
|
||||
steps:
|
||||
@@ -17,14 +17,12 @@ jobs:
|
||||
run: |
|
||||
git clone \
|
||||
--depth 1 \
|
||||
--branch Production \
|
||||
--branch production \
|
||||
https://oauth2:${{ secrets.ERPCORE }}@gitea.hexdive.com/New_REP_SYSTEM/ERP-core.git \
|
||||
.
|
||||
|
||||
- name: Restore backend
|
||||
- name: Restore
|
||||
run: dotnet restore Backend/ERPCore.sln
|
||||
|
||||
- name: Publish backend
|
||||
- name: Publish
|
||||
run: |
|
||||
rm -rf /tmp/erpcore-deploy
|
||||
mkdir -p /tmp/erpcore-deploy
|
||||
@@ -33,53 +31,47 @@ jobs:
|
||||
-r linux-x64 \
|
||||
--self-contained true \
|
||||
-o /tmp/erpcore-deploy
|
||||
|
||||
- name: Stop backend service
|
||||
- name: Stop service
|
||||
run: systemctl stop erpcore.service
|
||||
|
||||
- name: Copy backend to deploy folder
|
||||
- name: Copy to deploy folder
|
||||
run: |
|
||||
mkdir -p /home/erp/core/publish
|
||||
rm -rf /home/erp/core/publish/*
|
||||
cp -r /tmp/erpcore-deploy/* /home/erp/core/publish/
|
||||
|
||||
- name: Fix backend ownership and permissions
|
||||
- name: Fix ownership and permissions
|
||||
run: |
|
||||
chown -R erpcore:erpcore /home/erp/core/publish
|
||||
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
|
||||
- name: Restart service
|
||||
run: systemctl restart erpcore.service
|
||||
|
||||
- name: Verify backend
|
||||
- name: Verify
|
||||
run: |
|
||||
systemctl is-active erpcore.service && echo "Backend service running" || echo "Backend service failed"
|
||||
systemctl is-active erpcore.service && echo "✅ Service running" || echo "❌ Service failed"
|
||||
|
||||
frontend-build-and-deploy:
|
||||
name: Build & Deploy ERP Core Frontend
|
||||
build-and-deploy-frontend:
|
||||
name: Build & Deploy 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 \
|
||||
--branch production \
|
||||
https://oauth2:${{ secrets.ERPCORE }}@gitea.hexdive.com/New_REP_SYSTEM/ERP-core.git \
|
||||
.
|
||||
|
||||
- name: Build frontend Docker image
|
||||
run: docker build -t erpcore-frontend:latest Frontend/erp-system
|
||||
|
||||
- name: Stop & remove existing frontend container
|
||||
- name: Build Docker image
|
||||
run: |
|
||||
docker build --progress=plain --no-cache \
|
||||
-t erpcore-frontend:latest \
|
||||
./Frontend/erp-system
|
||||
- name: Stop & remove existing container
|
||||
run: |
|
||||
docker stop erpcore-frontend 2>/dev/null || true
|
||||
docker rm erpcore-frontend 2>/dev/null || true
|
||||
|
||||
- name: Start frontend container
|
||||
- name: Start container
|
||||
run: |
|
||||
docker run -d \
|
||||
--name erpcore-frontend \
|
||||
@@ -92,11 +84,9 @@ jobs:
|
||||
--cap-drop ALL \
|
||||
--security-opt no-new-privileges \
|
||||
erpcore-frontend:latest
|
||||
|
||||
- name: Remove old frontend images
|
||||
- name: Remove old images
|
||||
run: docker image prune -f
|
||||
|
||||
- name: Verify frontend
|
||||
- name: Verify
|
||||
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"
|
||||
| grep erpcore-frontend && echo "✅ Container running" || echo "❌ Container failed"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Dtos.Config;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -14,6 +16,7 @@ namespace ERPCore.Controllers;
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Route("api/v1/branch-config")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "settings")]
|
||||
public sealed class BranchConfigController : ApiControllerBase
|
||||
{
|
||||
private readonly IBranchConfigService _config;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Brands;
|
||||
using ERPCore.Dtos.Common;
|
||||
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Brand master endpoints (docs/11-BACKEND-PHASE1.md §2.6).</summary>
|
||||
[Route("api/v1/brands")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "products")]
|
||||
public sealed class BrandsController : ApiControllerBase
|
||||
{
|
||||
private readonly IBrandService _brands;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Sales;
|
||||
@@ -7,6 +9,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
[Route("api/v1/bundle-sales")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "sales")]
|
||||
public sealed class BundleSalesController : ApiControllerBase
|
||||
{
|
||||
private readonly IBundleSaleService _bundles;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Categories;
|
||||
using ERPCore.Dtos.Common;
|
||||
@@ -12,6 +14,7 @@ namespace ERPCore.Controllers;
|
||||
/// <c>?tree=true</c> parameter is gone along with the self-nesting model.
|
||||
/// </summary>
|
||||
[Route("api/v1/categories")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "products")]
|
||||
public sealed class CategoriesController : ApiControllerBase
|
||||
{
|
||||
private readonly ICategoryService _categories;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Customers;
|
||||
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Customer master endpoints for Phase 1 sales.</summary>
|
||||
[Route("api/v1/customers")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "sales")]
|
||||
public sealed class CustomersController : ApiControllerBase
|
||||
{
|
||||
private readonly ICustomerService _customers;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Dtos.Dashboard;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -6,6 +8,7 @@ namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Dashboard overview stats — cross-domain counts, not a stored entity.</summary>
|
||||
[Route("api/v1/dashboard")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "dashboard")]
|
||||
public sealed class DashboardController : ApiControllerBase
|
||||
{
|
||||
private readonly IDashboardService _dashboard;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Sales;
|
||||
@@ -12,6 +14,7 @@ namespace ERPCore.Controllers;
|
||||
/// the existing sales-slip CRUD surface under a more business-friendly route.
|
||||
/// </summary>
|
||||
[Route("api/v1/free-issues")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "sales")]
|
||||
public sealed class FreeIssuesController : ApiControllerBase
|
||||
{
|
||||
private readonly ISalesSlipService _slips;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -14,6 +16,7 @@ namespace ERPCore.Controllers;
|
||||
/// only and is never exposed to the frontend.
|
||||
/// </summary>
|
||||
[Route("api/v1/gl")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "ledgers")]
|
||||
public sealed class GeneralLedgerController : ApiControllerBase
|
||||
{
|
||||
private readonly IGeneralLedgerService _gl;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Grn;
|
||||
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Goods-receipt endpoints (docs/11 §4).</summary>
|
||||
[Route("api/v1/grns")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "receiving")]
|
||||
public sealed class GrnsController : ApiControllerBase
|
||||
{
|
||||
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>
|
||||
[HttpDelete("{employeeId:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(typeof(EmployeeDeleteResultDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<IActionResult> Delete(int employeeId, CancellationToken ct)
|
||||
public async Task<ActionResult<EmployeeDeleteResultDto>> Delete(int employeeId, [FromQuery] bool deleteUserAccount, CancellationToken ct)
|
||||
{
|
||||
await _employees.DeleteAsync(employeeId, ct);
|
||||
return NoContent();
|
||||
var result = await _employees.DeleteAsync(employeeId, deleteUserAccount, ct);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpPost("{employeeId:int}/link-user")]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.ItemTypes;
|
||||
@@ -13,6 +15,7 @@ namespace ERPCore.Controllers;
|
||||
/// into the client-generated SKU (docs/10 Part C.9).
|
||||
/// </summary>
|
||||
[Route("api/v1/item-types")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "products")]
|
||||
public sealed class ItemTypesController : ApiControllerBase
|
||||
{
|
||||
private readonly IItemTypeService _itemTypes;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Items;
|
||||
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Item master endpoints (docs/11-BACKEND-PHASE1.md §2.1–2.2).</summary>
|
||||
[Route("api/v1/items")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "products")]
|
||||
public sealed class ItemsController : ApiControllerBase
|
||||
{
|
||||
private readonly IItemService _items;
|
||||
@@ -27,6 +30,30 @@ public sealed class ItemsController : ApiControllerBase
|
||||
CancellationToken ct)
|
||||
=> Ok(await _items.ListAsync(query, status, categoryId, subCategoryId, brandId, trackingMode, ct));
|
||||
|
||||
/// <summary>Items list enriched with stock value, low-stock, last-purchase and
|
||||
/// trailing-30-day sales aggregates, for the Items overview table.</summary>
|
||||
[HttpGet("overview")]
|
||||
[ProducesResponseType(typeof(PagedResponse<ItemOverviewDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<ItemOverviewDto>>> Overview(
|
||||
[FromQuery] PageQuery query,
|
||||
[FromQuery] EntityStatus? status,
|
||||
[FromQuery] int? categoryId,
|
||||
[FromQuery] int? subCategoryId,
|
||||
[FromQuery] int? brandId,
|
||||
CancellationToken ct)
|
||||
=> Ok(await _items.GetOverviewAsync(query, status, categoryId, subCategoryId, brandId, ct));
|
||||
|
||||
/// <summary>Barcode/RFID lookup — matches an item's barcode/SKU, or a batch/serial
|
||||
/// barcode/no (optionally scoped to <paramref name="itemId"/> first).</summary>
|
||||
[HttpGet("scan")]
|
||||
[ProducesResponseType(typeof(ItemScanResultDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<ItemScanResultDto>> Scan([FromQuery] string code, [FromQuery] int? itemId, CancellationToken ct)
|
||||
{
|
||||
var result = await _items.ScanAsync(code, itemId, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>Get a single item; returns an <c>ETag</c> for optimistic concurrency.</summary>
|
||||
[HttpGet("{itemId:int}")]
|
||||
[ProducesResponseType(typeof(ItemDetailDto), StatusCodes.Status200OK)]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Dtos.Audit;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Services.Interfaces;
|
||||
@@ -10,6 +12,7 @@ namespace ERPCore.Controllers;
|
||||
/// Data only — no posting in Phase 1.
|
||||
/// </summary>
|
||||
[Route("api/v1/journal-entries")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "ledgers")]
|
||||
public sealed class JournalEntriesController : ApiControllerBase
|
||||
{
|
||||
private readonly IAuditService _audit;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Dtos.Notifications;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -5,6 +7,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
[Route("api/v1/notification-config")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "settings")]
|
||||
public sealed class NotificationConfigController : ApiControllerBase
|
||||
{
|
||||
private readonly INotificationConfigService _config;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
[Route("api/v1/notifications")]
|
||||
public sealed class NotificationsController : ApiControllerBase
|
||||
{
|
||||
private readonly INotificationService _notifications;
|
||||
private readonly ICurrentUser _currentUser;
|
||||
|
||||
public NotificationsController(INotificationService notifications, ICurrentUser currentUser)
|
||||
{
|
||||
_notifications = notifications;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult> Get([FromQuery] int take = 20, CancellationToken ct = default)
|
||||
{
|
||||
var feed = await _notifications.GetFeedAsync(_currentUser.AuditUserId, take, ct);
|
||||
return Ok(feed);
|
||||
}
|
||||
|
||||
[HttpPost("mark-all-read")]
|
||||
public async Task<ActionResult> MarkAllRead(CancellationToken ct)
|
||||
{
|
||||
await _notifications.MarkAllReadAsync(_currentUser.AuditUserId, ct);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Dtos.Config;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -16,6 +18,7 @@ namespace ERPCore.Controllers;
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Route("api/v1/product-config")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "products")]
|
||||
public sealed class ProductConfigController : ApiControllerBase
|
||||
{
|
||||
private readonly IProductConfigService _config;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Production;
|
||||
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Production run endpoints (docs/30-BACKEND-PHASE2.md §D.2–D.3).</summary>
|
||||
[Route("api/v1/production-runs")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "production")]
|
||||
public sealed class ProductionRunsController : ApiControllerBase
|
||||
{
|
||||
private readonly IProductionRunService _runs;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Production;
|
||||
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Production template endpoints (docs/30-BACKEND-PHASE2.md §D.1).</summary>
|
||||
[Route("api/v1/production-templates")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "production")]
|
||||
public sealed class ProductionTemplatesController : ApiControllerBase
|
||||
{
|
||||
private readonly IProductionTemplateService _templates;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Grn;
|
||||
@@ -9,6 +11,7 @@ namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Purchase invoice endpoints — vendor invoices consolidating Confirmed GRNs.</summary>
|
||||
[Route("api/v1/purchase-invoices")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "procurement")]
|
||||
public sealed class PurchaseInvoicesController : ApiControllerBase
|
||||
{
|
||||
private readonly IPurchaseInvoiceService _invoices;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Procurement;
|
||||
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Purchase-order endpoints (docs/11 §3.3).</summary>
|
||||
[Route("api/v1/purchase-orders")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "procurement")]
|
||||
public sealed class PurchaseOrdersController : ApiControllerBase
|
||||
{
|
||||
private readonly IPurchaseOrderService _pos;
|
||||
@@ -32,7 +35,7 @@ public sealed class PurchaseOrdersController : ApiControllerBase
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
/// <summary>Create a PO — auto-approved on creation in Phase 1 (FR-PROC-04). Totals computed server-side.</summary>
|
||||
/// <summary>Create a PO — non-draft is issued straight into PendingApproval (FR-PROC-04). Totals computed server-side.</summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
|
||||
@@ -57,7 +60,7 @@ public sealed class PurchaseOrdersController : ApiControllerBase
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
/// <summary>Submit a Draft PO (Draft → Approved). 409 PO_NOT_EDITABLE if not Draft.</summary>
|
||||
/// <summary>Submit a Draft PO (Draft → PendingApproval). 409 PO_NOT_EDITABLE if not Draft.</summary>
|
||||
[HttpPost("{poId:int}/submit")]
|
||||
[ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
@@ -67,6 +70,7 @@ public sealed class PurchaseOrdersController : ApiControllerBase
|
||||
|
||||
/// <summary>Delete a PO — permitted only while Draft (409 PO_NOT_EDITABLE otherwise).</summary>
|
||||
[HttpDelete("{poId:int}")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "procurement" + NavPolicies.ActionSeparator + NavPolicies.DeleteAction)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
@@ -76,13 +80,28 @@ public sealed class PurchaseOrdersController : ApiControllerBase
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>Approve — no-op in Phase 1 (POs auto-approve); transitions PendingApproval→Approved when enabled.</summary>
|
||||
/// <summary>Approve — PendingApproval → Approved. 403 PO_APPROVAL_FORBIDDEN if the caller
|
||||
/// isn't an Admin or is the PO's own creator; 409 PO_NOT_PENDING_APPROVAL otherwise.</summary>
|
||||
[HttpPost("{poId:int}/approve")]
|
||||
[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>> Approve(int poId, CancellationToken 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>
|
||||
[HttpPost("{poId:int}/cancel")]
|
||||
[ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status200OK)]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Procurement;
|
||||
using ERPCore.Services.Interfaces;
|
||||
@@ -7,6 +9,7 @@ namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Purchase-return endpoints (docs/11 §3.4).</summary>
|
||||
[Route("api/v1/purchase-returns")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "procurement")]
|
||||
public sealed class PurchaseReturnsController : ApiControllerBase
|
||||
{
|
||||
private readonly IPurchaseReturnService _returns;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Procurement;
|
||||
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Purchase-requisition endpoints (docs/11 §3.1).</summary>
|
||||
[Route("api/v1/requisitions")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "procurement")]
|
||||
public sealed class RequisitionsController : ApiControllerBase
|
||||
{
|
||||
private readonly IRequisitionService _requisitions;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Procurement;
|
||||
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>RFQ & quotation endpoints (docs/11 §3.2).</summary>
|
||||
[Route("api/v1/rfqs")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "procurement")]
|
||||
public sealed class RfqsController : ApiControllerBase
|
||||
{
|
||||
private readonly IRfqService _rfqs;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Rbac;
|
||||
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Role CRUD + permission assignment (mirrors AuthHex's Role; see docs/10 C.9).</summary>
|
||||
[Route("api/v1/roles")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "settings")]
|
||||
public sealed class RolesController : ApiControllerBase
|
||||
{
|
||||
private readonly IRoleService _roles;
|
||||
@@ -64,6 +67,7 @@ public sealed class RolesController : ApiControllerBase
|
||||
}
|
||||
|
||||
[HttpDelete("{roleId:int}")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "settings" + NavPolicies.ActionSeparator + NavPolicies.DeleteAction)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Sales;
|
||||
using ERPCore.Services.Interfaces;
|
||||
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
|
||||
/// <summary>Cashier day-end close-out — locks a cashier's Posted sales slips for a business
|
||||
/// date and posts one consolidated GL journal entry for the day (docs/14 Sales API).</summary>
|
||||
[Route("api/v1/sales-day-end")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "sales")]
|
||||
public sealed class SalesDayEndController : ApiControllerBase
|
||||
{
|
||||
private readonly ISalesDayEndService _dayEnds;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Sales;
|
||||
@@ -7,6 +9,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
[Route("api/v1/sales-invoices")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "sales")]
|
||||
public sealed class SalesInvoicesController : ApiControllerBase
|
||||
{
|
||||
private readonly ISalesInvoiceService _invoices;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Dtos.Sales;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -5,6 +7,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
[Route("api/v1/reports/sales")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "sales")]
|
||||
public sealed class SalesReportsController : ApiControllerBase
|
||||
{
|
||||
private readonly ISalesReportService _reports;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Sales;
|
||||
using ERPCore.Services.Interfaces;
|
||||
@@ -7,6 +9,7 @@ namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Sales-return endpoints — customer returns of previously sold goods.</summary>
|
||||
[Route("api/v1/sales-returns")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "sales")]
|
||||
public sealed class SalesReturnsController : ApiControllerBase
|
||||
{
|
||||
private readonly ISalesReturnService _returns;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Dtos.Sales;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -5,6 +7,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
[Route("api/v1/sales-slips/{salesSlipId:int}/free-issue-suggestions")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "sales")]
|
||||
public sealed class SalesSlipPromotionsController : ApiControllerBase
|
||||
{
|
||||
private readonly ISalesPromotionSuggestionService _suggestions;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Sales;
|
||||
@@ -7,6 +9,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
[Route("api/v1/sales-slips")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "sales")]
|
||||
public sealed class SalesSlipsController : ApiControllerBase
|
||||
{
|
||||
private readonly ISalesSlipService _slips;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using ERPCore.Dtos.Search;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
[Route("api/v1/search")]
|
||||
public sealed class SearchController : ApiControllerBase
|
||||
{
|
||||
private readonly IGlobalSearchService _search;
|
||||
|
||||
public SearchController(IGlobalSearchService search) => _search = search;
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IReadOnlyList<GlobalSearchResultDto>>> Get([FromQuery] string q, CancellationToken ct)
|
||||
{
|
||||
var results = await _search.SearchAsync(q, 6, ct);
|
||||
return Ok(results);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Stock;
|
||||
using ERPCore.Services.Interfaces;
|
||||
@@ -7,6 +9,7 @@ namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Stock-adjustment endpoints (docs/11 §5.5).</summary>
|
||||
[Route("api/v1/stock-adjustments")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "stock")]
|
||||
public sealed class StockAdjustmentsController : ApiControllerBase
|
||||
{
|
||||
private readonly IAdjustmentService _adjustments;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Procurement;
|
||||
using ERPCore.Dtos.Stock;
|
||||
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Stock enquiry, ledger, valuation and reorder-alert endpoints (docs/11 §5.1–5.3, §5.7).</summary>
|
||||
[Route("api/v1/stock")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "stock")]
|
||||
public sealed class StockController : ApiControllerBase
|
||||
{
|
||||
private readonly IStockService _stock;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Stock;
|
||||
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Stock-count endpoints (docs/11 §5.6).</summary>
|
||||
[Route("api/v1/stock-counts")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "stock")]
|
||||
public sealed class StockCountsController : ApiControllerBase
|
||||
{
|
||||
private readonly ICountService _counts;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Stock;
|
||||
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Stock-transfer endpoints (docs/11 §5.4).</summary>
|
||||
[Route("api/v1/stock-transfers")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "stock")]
|
||||
public sealed class StockTransfersController : ApiControllerBase
|
||||
{
|
||||
private readonly ITransferService _transfers;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Dtos.Categories;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -10,6 +12,7 @@ namespace ERPCore.Controllers;
|
||||
/// since a subcategory only exists in the context of one.
|
||||
/// </summary>
|
||||
[Route("api/v1/subcategories")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "products")]
|
||||
public sealed class SubCategoriesController : ApiControllerBase
|
||||
{
|
||||
private readonly ICategoryService _categories;
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
using ERPCore.Dtos.Support;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
[Route("api/v1/support-tickets")]
|
||||
public sealed class SupportTicketsController : ApiControllerBase
|
||||
{
|
||||
private readonly ISupportTicketService _tickets;
|
||||
private readonly ICurrentUser _currentUser;
|
||||
|
||||
public SupportTicketsController(ISupportTicketService tickets, ICurrentUser currentUser)
|
||||
{
|
||||
_tickets = tickets;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(SupportTicketDto), StatusCodes.Status201Created)]
|
||||
public async Task<ActionResult<SupportTicketDto>> Create([FromBody] CreateSupportTicketRequest request, CancellationToken ct)
|
||||
{
|
||||
var result = await _tickets.CreateAsync(_currentUser.AuditUserId, request, ct);
|
||||
SetETag(result.RowVersion);
|
||||
return CreatedAtAction(nameof(GetById), new { id = result.Value.TicketId }, result.Value);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(IReadOnlyList<SupportTicketSummaryDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<IReadOnlyList<SupportTicketSummaryDto>>> GetList(CancellationToken ct)
|
||||
{
|
||||
var list = await _tickets.GetListAsync(_currentUser.AuditUserId, _currentUser.IsAdmin, ct);
|
||||
return Ok(list);
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(typeof(SupportTicketDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<SupportTicketDto>> GetById(int id, CancellationToken ct)
|
||||
{
|
||||
var result = await _tickets.GetByIdAsync(id, _currentUser.AuditUserId, _currentUser.IsAdmin, ct);
|
||||
SetETag(result.RowVersion);
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/messages")]
|
||||
[ProducesResponseType(typeof(SupportTicketDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
|
||||
public async Task<ActionResult<SupportTicketDto>> AddMessage(int id, [FromBody] AddSupportTicketMessageRequest request, CancellationToken ct)
|
||||
{
|
||||
var expected = RequireIfMatch();
|
||||
var result = await _tickets.AddMessageAsync(id, _currentUser.AuditUserId, _currentUser.IsAdmin, request, expected, ct);
|
||||
SetETag(result.RowVersion);
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
[HttpPut("{id:int}/status")]
|
||||
[ProducesResponseType(typeof(SupportTicketDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
|
||||
public async Task<ActionResult<SupportTicketDto>> UpdateStatus(int id, [FromBody] UpdateSupportTicketStatusRequest request, CancellationToken ct)
|
||||
{
|
||||
var expected = RequireIfMatch();
|
||||
var result = await _tickets.UpdateStatusAsync(id, _currentUser.AuditUserId, _currentUser.IsAdmin, request, expected, ct);
|
||||
SetETag(result.RowVersion);
|
||||
return Ok(result.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.TaxRates;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Tax rate master endpoints — see <see cref="Domain.Entities.TaxRate"/>.</summary>
|
||||
[Route("api/v1/tax-rates")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "products")]
|
||||
public sealed class TaxRatesController : ApiControllerBase
|
||||
{
|
||||
private readonly ITaxRateService _taxRates;
|
||||
|
||||
public TaxRatesController(ITaxRateService taxRates) => _taxRates = taxRates;
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResponse<TaxRateDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<TaxRateDto>>> List(
|
||||
[FromQuery] PageQuery query, [FromQuery] EntityStatus? status, CancellationToken ct)
|
||||
=> Ok(await _taxRates.ListAsync(query, status, ct));
|
||||
|
||||
[HttpGet("{taxRateId:int}")]
|
||||
[ProducesResponseType(typeof(TaxRateDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<TaxRateDto>> GetById(int taxRateId, CancellationToken ct)
|
||||
{
|
||||
var result = await _taxRates.GetAsync(taxRateId, ct);
|
||||
if (result is null) return NotFound();
|
||||
|
||||
SetETag(result.RowVersion);
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(TaxRateDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<TaxRateDto>> Create([FromBody] CreateTaxRateRequest request, CancellationToken ct)
|
||||
{
|
||||
var result = await _taxRates.CreateAsync(request, ct);
|
||||
SetETag(result.RowVersion);
|
||||
return Created($"/api/v1/tax-rates/{result.Value.TaxRateId}", result.Value);
|
||||
}
|
||||
|
||||
[HttpPut("{taxRateId:int}")]
|
||||
[ProducesResponseType(typeof(TaxRateDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
|
||||
public async Task<ActionResult<TaxRateDto>> Update(int taxRateId, [FromBody] UpdateTaxRateRequest request, CancellationToken ct)
|
||||
{
|
||||
var expected = RequireIfMatch();
|
||||
var result = await _taxRates.UpdateAsync(taxRateId, request, expected, ct);
|
||||
SetETag(result.RowVersion);
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
/// <summary>Deactivate/reactivate. Masters are never hard-deleted (FR-MD-08).</summary>
|
||||
[HttpPatch("{taxRateId:int}/status")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> SetStatus(int taxRateId, [FromBody] UpdateTaxRateStatusRequest request, CancellationToken ct)
|
||||
{
|
||||
await _taxRates.SetStatusAsync(taxRateId, request.Status, ct);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Uoms;
|
||||
using ERPCore.Services.Interfaces;
|
||||
@@ -7,6 +10,7 @@ namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Unit-of-measure endpoints (docs/11-BACKEND-PHASE1.md §2.2).</summary>
|
||||
[Route("api/v1/uoms")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "products")]
|
||||
public sealed class UomsController : ApiControllerBase
|
||||
{
|
||||
private readonly IUomService _uoms;
|
||||
@@ -15,8 +19,9 @@ public sealed class UomsController : ApiControllerBase
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResponse<UomDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<UomDto>>> List([FromQuery] PageQuery query, CancellationToken ct)
|
||||
=> Ok(await _uoms.ListAsync(query, ct));
|
||||
public async Task<ActionResult<PagedResponse<UomDto>>> List(
|
||||
[FromQuery] PageQuery query, [FromQuery] EntityStatus? status, CancellationToken ct)
|
||||
=> Ok(await _uoms.ListAsync(query, status, ct));
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(UomDto), StatusCodes.Status201Created)]
|
||||
@@ -26,4 +31,14 @@ public sealed class UomsController : ApiControllerBase
|
||||
var dto = await _uoms.CreateAsync(request, ct);
|
||||
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,10 +1,13 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Audit;
|
||||
using ERPCore.Dtos.Auth;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Hrm;
|
||||
using ERPCore.Dtos.Users;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using ERPCore.System.Errors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
@@ -14,6 +17,7 @@ namespace ERPCore.Controllers;
|
||||
/// account creation orchestrated against AuthHex (see <see cref="IUserManagementService.CreateAsync"/>).
|
||||
/// </summary>
|
||||
[Route("api/v1/users")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "settings")]
|
||||
public sealed class UsersController : ApiControllerBase
|
||||
{
|
||||
private readonly IUserManagementService _users;
|
||||
@@ -83,4 +87,20 @@ public sealed class UsersController : ApiControllerBase
|
||||
[ProducesResponseType(typeof(PagedResponse<LoginActivityDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<LoginActivityDto>>> ListLoginDevices(int userId, [FromQuery] PageQuery query, CancellationToken ct)
|
||||
=> Ok(await _audit.ListLoginDevicesAsync(userId, query, ct));
|
||||
|
||||
/// <summary>
|
||||
/// Admin-only escape hatch: permanently deletes a stale AuthHex identity (by email/mobile/NIC/
|
||||
/// username) that is blocking new registrations. Does not touch ERPCore's own `users` table.
|
||||
/// See <see cref="IUserManagementService.PurgeAuthHexIdentityAsync"/>.
|
||||
/// </summary>
|
||||
[HttpPost("purge-authhex-identity")]
|
||||
[ProducesResponseType(typeof(DeleteAuthHexUserResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult<DeleteAuthHexUserResult>> PurgeAuthHexIdentity([FromBody] PurgeAuthHexIdentityRequest request, CancellationToken ct)
|
||||
{
|
||||
if (!_currentUser.IsAdmin)
|
||||
throw new DomainException("FORBIDDEN", "Only an administrator can purge an AuthHex identity.", 403);
|
||||
|
||||
return Ok(await _users.PurgeAuthHexIdentityAsync(request, ct));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Vendors;
|
||||
@@ -8,6 +10,7 @@ namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Vendor master endpoints (docs/11-BACKEND-PHASE1.md §2.4).</summary>
|
||||
[Route("api/v1/vendors")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "vendors")]
|
||||
public sealed class VendorsController : ApiControllerBase
|
||||
{
|
||||
private readonly IVendorService _vendors;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Warehouses;
|
||||
using ERPCore.Services.Interfaces;
|
||||
@@ -7,6 +9,7 @@ namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Warehouse & bin endpoints (docs/11-BACKEND-PHASE1.md §2.5).</summary>
|
||||
[Route("api/v1/warehouses")]
|
||||
[Authorize(Policy = NavPolicies.Prefix + "warehouses")]
|
||||
public sealed class WarehousesController : ApiControllerBase
|
||||
{
|
||||
private readonly IWarehouseService _warehouses;
|
||||
@@ -37,6 +40,24 @@ public sealed class WarehousesController : ApiControllerBase
|
||||
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")]
|
||||
[ProducesResponseType(typeof(IReadOnlyList<BinDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
|
||||
@@ -13,4 +13,7 @@ public class Batch
|
||||
|
||||
public string BatchNo { get; set; } = string.Empty;
|
||||
public DateOnly? ExpiryDate { get; set; }
|
||||
|
||||
/// <summary>Optional scan code for barcode/RFID lookup (unique when set).</summary>
|
||||
public string? Barcode { get; set; }
|
||||
}
|
||||
|
||||
@@ -17,6 +17,12 @@ public class BranchConfig
|
||||
|
||||
public bool BranchingEnabled { get; set; } = true;
|
||||
|
||||
/// <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 int? UpdatedBy { get; set; }
|
||||
|
||||
@@ -29,6 +29,9 @@ public class Customer
|
||||
public int? DefaultWarehouseId { 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 DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
@@ -14,6 +14,9 @@ public class Item
|
||||
public string Name { get; set; } = string.Empty;
|
||||
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 Category? Category { get; set; }
|
||||
|
||||
@@ -41,7 +44,14 @@ public class Item
|
||||
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>
|
||||
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>
|
||||
/// Optional selling price range and flat price used by Sales only.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A single raised notification event (e.g. "GRN Confirmed"). Broadcast to
|
||||
/// active users via <see cref="NotificationRecipient"/> rows, gated by
|
||||
/// whether the module's InApp channel is enabled in <see cref="NotificationConfig"/>.
|
||||
/// </summary>
|
||||
public class Notification
|
||||
{
|
||||
public int NotificationId { get; set; }
|
||||
|
||||
public string ModuleKey { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
public string? RelatedEntityType { get; set; }
|
||||
public int? RelatedEntityId { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public ICollection<NotificationRecipient> Recipients { get; set; } = new List<NotificationRecipient>();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Per-user read state for a broadcast <see cref="Notification"/>.
|
||||
/// </summary>
|
||||
public class NotificationRecipient
|
||||
{
|
||||
public int NotificationRecipientId { get; set; }
|
||||
|
||||
public int NotificationId { get; set; }
|
||||
public Notification? Notification { get; set; }
|
||||
|
||||
public int UserId { get; set; }
|
||||
public User? User { get; set; }
|
||||
|
||||
public bool IsRead { get; set; }
|
||||
public DateTime? ReadAt { get; set; }
|
||||
}
|
||||
@@ -10,6 +10,10 @@ public class NumberSequence
|
||||
{
|
||||
public int SequenceId { get; set; }
|
||||
public 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 LastNumber { get; set; }
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ public class PurchaseInvoice
|
||||
public int VendorId { 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? VendorSnapshotTaxNo { get; set; }
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@ namespace ERPCore.Domain.Entities;
|
||||
/// <summary>
|
||||
/// Purchase order header (FR-PROC-03..06). Mutable aggregate with a
|
||||
/// <see cref="RowVersion"/> ETag token; editable while open (FR-PROC-05).
|
||||
/// Phase 1 auto-approves on creation; <see cref="ApprovalRequired"/> is retained
|
||||
/// for the future approval workflow. Totals are computed server-side from lines
|
||||
/// (not stored). Model: docs/10 Part C.2.
|
||||
/// A submitted/issued PO goes to <see cref="PurchaseOrderStatus.PendingApproval"/> and
|
||||
/// needs a separate Approve action (an Admin who isn't <see cref="CreatedBy"/>) before it
|
||||
/// reaches <see cref="PurchaseOrderStatus.Approved"/>. Totals are computed server-side
|
||||
/// from lines (not stored). Model: docs/10 Part C.2.
|
||||
/// </summary>
|
||||
public class PurchaseOrder
|
||||
{
|
||||
@@ -17,15 +18,25 @@ public class PurchaseOrder
|
||||
public int VendorId { get; set; }
|
||||
public Vendor? Vendor { get; set; }
|
||||
|
||||
public int BranchId { get; set; }
|
||||
public Branch? Branch { get; set; }
|
||||
|
||||
public int? RequisitionId { get; set; }
|
||||
public Requisition? Requisition { get; set; }
|
||||
|
||||
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 int CreatedBy { 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? UpdatedAt { get; set; }
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ public class Requisition
|
||||
public int RequestedBy { 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 DateTime CreatedAt { get; set; }
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ public class Rfq
|
||||
public int RequisitionId { 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 DateTime CreatedAt { get; set; }
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>Join row granting a <see cref="Role"/> visibility of a <see cref="Permission"/> (nav node).</summary>
|
||||
@@ -6,6 +8,13 @@ public class RolePermission
|
||||
public int RoleId { get; set; }
|
||||
public int 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 Permission? Permission { get; set; }
|
||||
}
|
||||
|
||||
@@ -27,6 +27,16 @@ public class SalesDayEnd
|
||||
public decimal TaxTotal { 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>
|
||||
public decimal CostOfGoodsSold { get; set; }
|
||||
|
||||
|
||||
@@ -19,6 +19,12 @@ public class SalesSlipLine
|
||||
public int WarehouseId { 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 BaseCost { get; set; }
|
||||
public string PriceSource { get; set; } = string.Empty;
|
||||
|
||||
@@ -13,4 +13,7 @@ public class Serial
|
||||
|
||||
public string SerialNo { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = "InStock";
|
||||
|
||||
/// <summary>Optional scan code for barcode/RFID lookup (unique when set).</summary>
|
||||
public string? Barcode { get; set; }
|
||||
}
|
||||
|
||||
@@ -16,6 +16,12 @@ public class StockCountLine
|
||||
|
||||
public int? BinId { get; set; }
|
||||
|
||||
public int? 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? CountedQty { get; set; }
|
||||
public decimal? Variance { get; set; }
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A help-desk ticket raised by a user from the Help page. Status is
|
||||
/// admin-managed; the conversation is a flat thread of <see cref="SupportTicketMessage"/> rows.
|
||||
/// Field/concurrency shape follows the same conventions as <see cref="Employee"/>
|
||||
/// (CreatedBy/UpdatedBy actor stamps, RowVersion for optimistic concurrency).
|
||||
/// </summary>
|
||||
public class SupportTicket
|
||||
{
|
||||
public int TicketId { get; set; }
|
||||
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
public TicketStatus Status { get; set; } = TicketStatus.Open;
|
||||
|
||||
public int CreatedBy { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public int? UpdatedBy { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public uint RowVersion { get; set; }
|
||||
|
||||
public ICollection<SupportTicketMessage> Messages { get; set; } = new List<SupportTicketMessage>();
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>One message in a <see cref="SupportTicket"/> thread — from the submitter or an admin.</summary>
|
||||
public class SupportTicketMessage
|
||||
{
|
||||
public int MessageId { get; set; }
|
||||
|
||||
public int TicketId { get; set; }
|
||||
public SupportTicket? Ticket { get; set; }
|
||||
|
||||
public int SenderUserId { get; set; }
|
||||
public bool IsFromAdmin { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A named tax rate (e.g. "Standard" 15%, "Zero-Rated" 0%, "Exempt" 0%) — the real backing
|
||||
/// for <see cref="Item.TaxRateId"/>. Before this existed, <c>Item.TaxClass</c> was a free-text
|
||||
/// label nothing ever read, and every sales/purchase line carried its own hand-typed tax
|
||||
/// percentage with no link back to the item at all. Now a line's tax defaults to whatever
|
||||
/// percentage the item's assigned TaxRate carries at the moment the line is created — change
|
||||
/// the rate here and every new document picks it up; a line already created keeps the percentage
|
||||
/// it was given (SalesSlipLine.TaxPct etc. are snapshots, never recomputed from this table).
|
||||
/// </summary>
|
||||
public class TaxRate
|
||||
{
|
||||
public int TaxRateId { get; set; }
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public decimal Percentage { get; set; }
|
||||
public EntityStatus Status { get; set; } = EntityStatus.Active;
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>PostgreSQL xmin-backed optimistic concurrency token (ETag source).</summary>
|
||||
public uint RowVersion { get; set; }
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
@@ -10,4 +12,8 @@ public class Uom
|
||||
{
|
||||
public int UomId { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public EntityStatus Status { get; set; } = EntityStatus.Active;
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ public class Vendor
|
||||
public string Currency { get; set; } = "LKR";
|
||||
public EntityStatus Status { get; set; } = EntityStatus.Active;
|
||||
|
||||
public int BranchId { get; set; }
|
||||
public Branch? Branch { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ public class Warehouse
|
||||
public int WarehouseId { get; set; }
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Address { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
public int BranchId { get; set; }
|
||||
public Branch? Branch { get; set; }
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
namespace ERPCore.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Purchase-order lifecycle (docs/11 §8; docs/10 §B.8.1). Phase 1 auto-approves on
|
||||
/// creation, so <see cref="PendingApproval"/> is reserved (not entered) until the
|
||||
/// approval workflow is enabled (FR-PROC-04). Stored as a string.
|
||||
/// Purchase-order lifecycle (docs/11 §8; docs/10 §B.8.1). Issuing a PO (create
|
||||
/// non-draft, or submit a Draft) enters <see cref="PendingApproval"/>; an Admin who
|
||||
/// isn't the creator must Approve it to reach <see cref="Approved"/>, or Reject it back
|
||||
/// to <see cref="Draft"/> (FR-PROC-04). Stored as a string.
|
||||
/// </summary>
|
||||
public enum PurchaseOrderStatus
|
||||
{
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace ERPCore.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Fine-grained actions a <see cref="Entities.RolePermission"/> can grant within a module
|
||||
/// it already has nav-level access to (see <see cref="Infra.Auth.NavPolicies"/>). Nav-level
|
||||
/// access answers "can this role reach this module"; this answers "which mutations within
|
||||
/// it can this role actually perform" — e.g. a role that can see Procurement but should not
|
||||
/// be able to delete or approve purchase orders.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum RolePermissionAction
|
||||
{
|
||||
None = 0,
|
||||
View = 1,
|
||||
Create = 2,
|
||||
Edit = 4,
|
||||
Delete = 8,
|
||||
Approve = 16,
|
||||
|
||||
All = View | Create | Edit | Delete | Approve
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ERPCore.Domain.Enums;
|
||||
|
||||
public enum TicketStatus
|
||||
{
|
||||
Open,
|
||||
InProgress,
|
||||
Resolved
|
||||
}
|
||||
@@ -29,6 +29,21 @@ public sealed class AuthSessionResponse
|
||||
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
|
||||
{
|
||||
/// <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
|
||||
/// requires a branch on Employee/Warehouse records at all.</summary>
|
||||
public sealed record BranchConfigDto(bool BranchingEnabled, DateTime? UpdatedAt, int? UpdatedBy);
|
||||
public sealed record BranchConfigDto(bool BranchingEnabled, bool WarehouseBranchingEnabled, DateTime? UpdatedAt, int? UpdatedBy);
|
||||
|
||||
/// <summary>
|
||||
/// Full replacement of the flag. <c>UpdatedBy</c> is derived from the token, never posted.
|
||||
@@ -17,4 +17,5 @@ public sealed record BranchConfigDto(bool BranchingEnabled, DateTime? UpdatedAt,
|
||||
public sealed class UpdateBranchConfigRequest
|
||||
{
|
||||
[Required] public bool? BranchingEnabled { get; set; }
|
||||
[Required] public bool? WarehouseBranchingEnabled { get; set; }
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ public sealed record CustomerDto(
|
||||
int CreditDays,
|
||||
int? DefaultWarehouseId,
|
||||
EntityStatus Status,
|
||||
int BranchId,
|
||||
DateTime CreatedAt,
|
||||
DateTime? UpdatedAt);
|
||||
|
||||
@@ -40,6 +41,7 @@ public sealed class CreateCustomerRequest
|
||||
[Range(0, double.MaxValue)] public decimal CreditLimit { get; set; }
|
||||
[Range(0, int.MaxValue)] public int CreditDays { get; set; }
|
||||
public int? DefaultWarehouseId { get; set; }
|
||||
public int BranchId { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UpdateCustomerRequest
|
||||
@@ -58,6 +60,7 @@ public sealed class UpdateCustomerRequest
|
||||
[Range(0, double.MaxValue)] public decimal CreditLimit { get; set; }
|
||||
[Range(0, int.MaxValue)] public int CreditDays { get; set; }
|
||||
public int? DefaultWarehouseId { get; set; }
|
||||
public int BranchId { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UpdateCustomerStatusRequest
|
||||
|
||||
@@ -4,9 +4,12 @@ namespace ERPCore.Dtos.Dashboard;
|
||||
/// Aggregate counts for the dashboard overview — mirrors the widget set in
|
||||
/// docs/dashboard-implementation.pdf: reorder alerts, on-hand summary, stock valuation,
|
||||
/// pending-approval POs, pending GRNs, open requisitions, open counts awaiting posting,
|
||||
/// and open RFQs. Recent movements isn't here — it's just GET /stock/ledger with a small
|
||||
/// pageSize, no aggregation needed. A single computed-on-read object, not a stored
|
||||
/// entity — same as reorder alerts (docs/11 §5.7).
|
||||
/// open RFQs, pending leave requests, and overdue sales invoices. Also reused as small
|
||||
/// KPI tiles on individual list pages (Purchase Orders, GRN, Requisitions, RFQs, Stock
|
||||
/// Counts, Reorder Alerts, HRM Leave, Sales Invoices), not just the Dashboard home page.
|
||||
/// Recent movements isn't here — it's just GET /stock/ledger with a small pageSize, no
|
||||
/// aggregation needed. A single computed-on-read object, not a stored entity — same as
|
||||
/// reorder alerts (docs/11 §5.7).
|
||||
/// </summary>
|
||||
public sealed record DashboardStatsDto(
|
||||
int LowStockAlerts,
|
||||
@@ -18,7 +21,9 @@ public sealed record DashboardStatsDto(
|
||||
int PendingGrns,
|
||||
int OpenRequisitions,
|
||||
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>
|
||||
public sealed record WarehouseValuationDto(int WarehouseId, decimal Total);
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed record GrnDto(
|
||||
/// <summary>Row shape for <c>GET /grns</c> — line count instead of the lines themselves.</summary>
|
||||
public sealed record GrnSummaryDto(
|
||||
int GrnId, string DocNo, int? PoId, int VendorId, int WarehouseId, int BranchId, GrnStatus Status,
|
||||
int CreatedBy, DateTime CreatedAt, DateTime? PostedAt, int LineCount,
|
||||
int CreatedBy, DateTime CreatedAt, DateTime? PostedAt, int LineCount, decimal TotalValue,
|
||||
int? PurchaseInvoiceId, string? PurchaseInvoiceNo);
|
||||
|
||||
public sealed record CreatedLayerDto(
|
||||
@@ -63,8 +63,9 @@ public sealed class CreateGrnLineInput
|
||||
[Range(0, double.MaxValue)] public decimal UnitCost { get; set; }
|
||||
/// <summary>Trade discount percentage (0–100). Reduces the inventory cost.</summary>
|
||||
[Range(0, 100)] public decimal DiscountPct { get; set; }
|
||||
/// <summary>VAT percentage (0–100). Recoverable — does not affect stock value.</summary>
|
||||
[Range(0, 100)] public decimal VatPct { get; set; }
|
||||
/// <summary>VAT percentage (0–100). Recoverable — does not affect stock value. Explicit
|
||||
/// override; omit (null) to use the item's assigned TaxRate percentage instead.</summary>
|
||||
[Range(0, 100)] public decimal? VatPct { get; set; }
|
||||
[EnumDataType(typeof(HoldStatus))] public HoldStatus HoldStatus { get; set; } = HoldStatus.Available;
|
||||
public BatchInput? Batch { get; set; }
|
||||
/// <summary>
|
||||
|
||||
@@ -3,6 +3,8 @@ using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Dtos.Hrm;
|
||||
|
||||
public sealed record EmployeeDeleteResultDto(bool UserAccountDeleted, bool UserAccountDeactivated, bool AuthHexIdentityPurged = false);
|
||||
|
||||
public sealed record EmployeeListItemDto(
|
||||
int EmployeeId, string EmployeeCode, string FullName, string? Email,
|
||||
int DepartmentId, string? DepartmentName, int DesignationId, string? DesignationName,
|
||||
|
||||
@@ -10,10 +10,11 @@ public sealed record ItemListItemDto(
|
||||
int ItemId, string Sku, string Name, int CategoryId, int? SubCategoryId, int? BrandId,
|
||||
int BaseUomId, int? DefaultVendorId, StockNature StockNature, TrackingMode TrackingMode,
|
||||
Warranty Warranty, int? WarrantyPeriodMonths,
|
||||
string? TaxClass, decimal? SellingMinPrice, decimal? SellingMaxPrice, decimal? SellingFlatPrice,
|
||||
int? TaxRateId, string? TaxRateName, decimal? TaxRatePercentage,
|
||||
decimal? SellingMinPrice, decimal? SellingMaxPrice, decimal? SellingFlatPrice,
|
||||
decimal? ContentQty, MeasureUnit? ContentUnit,
|
||||
decimal? ContentBaseQty, MeasureUnit? ContentBaseUnit,
|
||||
EntityStatus Status);
|
||||
EntityStatus Status, string? Barcode);
|
||||
|
||||
/// <summary>A single per-warehouse reorder policy row.</summary>
|
||||
public sealed record ItemReorderDto(int WarehouseId, decimal ReorderPoint, decimal ReorderQty);
|
||||
@@ -31,10 +32,11 @@ public sealed record ItemDetailDto(
|
||||
int? SubCategoryId, int? BrandId, int BaseUomId, int? DefaultVendorId,
|
||||
StockNature StockNature, TrackingMode TrackingMode,
|
||||
Warranty Warranty, int? WarrantyPeriodMonths,
|
||||
string? TaxClass, decimal? SellingMinPrice, decimal? SellingMaxPrice, decimal? SellingFlatPrice,
|
||||
int? TaxRateId, string? TaxRateName, decimal? TaxRatePercentage,
|
||||
decimal? SellingMinPrice, decimal? SellingMaxPrice, decimal? SellingFlatPrice,
|
||||
decimal? ContentQty, MeasureUnit? ContentUnit,
|
||||
decimal? ContentBaseQty, MeasureUnit? ContentBaseUnit,
|
||||
EntityStatus Status, IReadOnlyList<ItemReorderDto> Reorder,
|
||||
EntityStatus Status, string? Barcode, IReadOnlyList<ItemReorderDto> Reorder,
|
||||
DateTime CreatedAt, DateTime? UpdatedAt);
|
||||
|
||||
/// <summary>Response body for <c>PUT /items/{id}/reorder</c>.</summary>
|
||||
@@ -64,7 +66,8 @@ public sealed class CreateItemRequest
|
||||
[EnumDataType(typeof(Warranty))] public Warranty Warranty { get; set; } = Warranty.NonWarranty;
|
||||
/// <summary>Required (one of <see cref="Enums.WarrantyPeriods.AllowedMonths"/>) when <see cref="Warranty"/> is Warranty; ignored otherwise.</summary>
|
||||
public int? WarrantyPeriodMonths { get; set; }
|
||||
[StringLength(20)] public string? TaxClass { get; set; }
|
||||
/// <summary>The tax rate sales/purchase lines for this item default to — see <see cref="Domain.Entities.TaxRate"/>.</summary>
|
||||
public int? TaxRateId { get; set; }
|
||||
/// <summary>Optional fixed sale price (Sales only). Null ⇒ sell at stock/FIFO value.</summary>
|
||||
[Range(0, double.MaxValue)] public decimal? SellingMinPrice { get; set; }
|
||||
[Range(0, double.MaxValue)] public decimal? SellingMaxPrice { get; set; }
|
||||
@@ -77,6 +80,8 @@ public sealed class CreateItemRequest
|
||||
/// </summary>
|
||||
[Range(0.0001, double.MaxValue)] public decimal? ContentQty { 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
|
||||
@@ -96,7 +101,8 @@ public sealed class UpdateItemRequest
|
||||
[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>
|
||||
public int? WarrantyPeriodMonths { get; set; }
|
||||
[StringLength(20)] public string? TaxClass { get; set; }
|
||||
/// <summary>The tax rate sales/purchase lines for this item default to — see <see cref="Domain.Entities.TaxRate"/>.</summary>
|
||||
public int? TaxRateId { get; set; }
|
||||
/// <summary>Optional fixed sale price (Sales only). Null ⇒ sell at stock/FIFO value.</summary>
|
||||
[Range(0, double.MaxValue)] public decimal? SellingMinPrice { get; set; }
|
||||
[Range(0, double.MaxValue)] public decimal? SellingMaxPrice { get; set; }
|
||||
@@ -109,6 +115,8 @@ public sealed class UpdateItemRequest
|
||||
/// </summary>
|
||||
[Range(0.0001, double.MaxValue)] public decimal? ContentQty { 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
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Dtos.Items;
|
||||
|
||||
/// <summary>
|
||||
/// Item row enriched with cross-module aggregates (current FIFO stock value, low-stock
|
||||
/// flag, last purchase, trailing-30-day sales) for the Items list page. Deliberately a
|
||||
/// separate endpoint from <see cref="ItemListItemDto"/>'s <c>GET /items</c> — that list
|
||||
/// also backs the GRN/PO/Requisition/RFQ item pickers, which don't need these aggregates
|
||||
/// computed on every keystroke.
|
||||
/// </summary>
|
||||
public sealed record ItemOverviewDto(
|
||||
int ItemId, string Sku, string Name,
|
||||
int CategoryId, int? SubCategoryId, int? BrandId,
|
||||
EntityStatus Status,
|
||||
decimal StockQty, decimal StockValue, bool IsLowStock,
|
||||
DateTime? LastPurchaseDate, int? LastSupplierId, string? LastSupplierName,
|
||||
decimal SalesQtyLast30Days);
|
||||
@@ -0,0 +1,11 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Dtos.Items;
|
||||
|
||||
/// <summary>Result of a barcode/RFID scan lookup — an item, or an item resolved via a
|
||||
/// specific batch/serial. See <see cref="Services.Interfaces.IItemService.ScanAsync"/>.</summary>
|
||||
public sealed record ItemScanResultDto(
|
||||
int ItemId, string Sku, string Name, TrackingMode TrackingMode, int BaseUomId,
|
||||
string MatchedBy, // "Item" | "Batch" | "Serial"
|
||||
int? BatchId, string? BatchNo, DateOnly? ExpiryDate,
|
||||
int? SerialId, string? SerialNo, string? SerialStatus);
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace ERPCore.Dtos.Notifications;
|
||||
|
||||
public sealed record NotificationDto(
|
||||
int NotificationId,
|
||||
string ModuleKey,
|
||||
string Title,
|
||||
string Message,
|
||||
string? RelatedEntityType,
|
||||
int? RelatedEntityId,
|
||||
DateTime CreatedAt,
|
||||
bool IsRead);
|
||||
|
||||
public sealed record NotificationFeedDto(
|
||||
IReadOnlyList<NotificationDto> Notifications,
|
||||
int UnreadCount);
|
||||
@@ -12,14 +12,14 @@ public sealed record PurchaseInvoiceTotalsDto(
|
||||
|
||||
public sealed record PurchaseInvoiceDto(
|
||||
int PurchaseInvoiceId, string DocNo, string VendorInvoiceNo, DateTime VendorInvoiceDate,
|
||||
int VendorId, string VendorSnapshotName, string? VendorSnapshotTaxNo,
|
||||
int VendorId, int BranchId, string VendorSnapshotName, string? VendorSnapshotTaxNo,
|
||||
PurchaseInvoiceStatus Status, int GrnCount, int CreatedBy, DateTime CreatedAt, DateTime? UpdatedAt,
|
||||
string? GlJournalNo, DateTime? GlPostedAt,
|
||||
PurchaseInvoiceTotalsDto Totals, IReadOnlyList<PurchaseInvoiceLineDto> Lines);
|
||||
|
||||
public sealed record PurchaseInvoiceSummaryDto(
|
||||
int PurchaseInvoiceId, string DocNo, string VendorInvoiceNo, DateTime VendorInvoiceDate,
|
||||
int VendorId, string VendorSnapshotName, PurchaseInvoiceStatus Status, int GrnCount,
|
||||
int VendorId, int BranchId, string VendorSnapshotName, PurchaseInvoiceStatus Status, int GrnCount,
|
||||
PurchaseInvoiceTotalsDto Totals, DateTime CreatedAt, string? GlJournalNo);
|
||||
|
||||
public sealed record PurchaseInvoicePaymentDto(
|
||||
|
||||
@@ -12,13 +12,14 @@ public sealed record PoLineDto(
|
||||
public sealed record PoTotalsDto(decimal SubTotal, decimal Tax, decimal GrandTotal, string Currency);
|
||||
|
||||
public sealed record PurchaseOrderDto(
|
||||
int PoId, string DocNo, int VendorId, int? RequisitionId, PurchaseOrderStatus Status,
|
||||
bool ApprovalRequired, int CreatedBy, DateTime CreatedAt, DateTime? UpdatedAt,
|
||||
int PoId, string DocNo, int VendorId, int BranchId, int? RequisitionId, PurchaseOrderStatus Status,
|
||||
bool ApprovalRequired, int CreatedBy, int? ApprovedBy, DateTime? ApprovedAt,
|
||||
DateTime CreatedAt, DateTime? UpdatedAt,
|
||||
PoTotalsDto Totals, IReadOnlyList<PoLineDto> Lines);
|
||||
|
||||
public sealed record PurchaseOrderSummaryDto(
|
||||
int PoId, string DocNo, int VendorId, PurchaseOrderStatus Status,
|
||||
bool ApprovalRequired, DateTime CreatedAt, PoTotalsDto Totals);
|
||||
int PoId, string DocNo, int VendorId, int BranchId, PurchaseOrderStatus Status,
|
||||
bool ApprovalRequired, int CreatedBy, DateTime CreatedAt, PoTotalsDto Totals);
|
||||
|
||||
// Requests — server sets docNo, status, createdBy, timestamps, qtyReceived, totals
|
||||
|
||||
@@ -35,12 +36,13 @@ public sealed class CreatePurchaseOrderRequest
|
||||
{
|
||||
[Required] public int VendorId { get; set; }
|
||||
public int? RequisitionId { get; set; }
|
||||
public int BranchId { get; set; }
|
||||
[Required, MinLength(1)] public List<CreatePoLineInput> Lines { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// When true the PO is created in <c>Draft</c> (editable/deletable, not yet issued).
|
||||
/// When false (default) it auto-approves on creation, preserving the Requisition→PO
|
||||
/// and RFQ→PO flows unchanged (docs/11 §3.3).
|
||||
/// When false (default) it is issued immediately into <c>PendingApproval</c>, same as
|
||||
/// submitting a Draft (docs/11 §3.3).
|
||||
/// </summary>
|
||||
public bool SaveAsDraft { get; set; }
|
||||
}
|
||||
@@ -49,6 +51,7 @@ public sealed class UpdatePurchaseOrderRequest
|
||||
{
|
||||
[Required] public int VendorId { get; set; }
|
||||
public int? RequisitionId { get; set; }
|
||||
public int BranchId { get; set; }
|
||||
[Required, MinLength(1)] public List<CreatePoLineInput> Lines { get; set; } = new();
|
||||
}
|
||||
|
||||
@@ -56,3 +59,8 @@ public sealed class CancelPurchaseOrderRequest
|
||||
{
|
||||
[StringLength(500)] public string? Reason { get; set; }
|
||||
}
|
||||
|
||||
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 RequisitionDto(
|
||||
int RequisitionId, string DocNo, RequisitionStatus Status, int RequestedBy,
|
||||
int RequisitionId, string DocNo, RequisitionStatus Status, int RequestedBy, int BranchId,
|
||||
DateTime CreatedAt, IReadOnlyList<RequisitionLineDto> Lines);
|
||||
|
||||
public sealed record RequisitionSummaryDto(
|
||||
int RequisitionId, string DocNo, RequisitionStatus Status, int RequestedBy, DateTime CreatedAt, int LineCount);
|
||||
int RequisitionId, string DocNo, RequisitionStatus Status, int RequestedBy, int BranchId, DateTime CreatedAt, int LineCount);
|
||||
|
||||
// Requests — server sets docNo, status, requestedBy (audit actor), timestamps ----
|
||||
|
||||
@@ -26,4 +26,7 @@ public sealed class CreateRequisitionLineInput
|
||||
public sealed class CreateRequisitionRequest
|
||||
{
|
||||
[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; }
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ namespace ERPCore.Dtos.Procurement;
|
||||
public sealed record RfqLineDto(int RfqLineId, int ItemId, decimal Qty);
|
||||
|
||||
public sealed record RfqDto(
|
||||
int RfqId, string DocNo, int RequisitionId, RfqStatus Status, IReadOnlyList<RfqLineDto> Lines);
|
||||
int RfqId, string DocNo, int RequisitionId, int BranchId, RfqStatus Status, IReadOnlyList<RfqLineDto> Lines);
|
||||
|
||||
/// <summary>Row shape for <c>GET /rfqs</c> — line/quotation counts instead of the lines themselves.</summary>
|
||||
public sealed record RfqSummaryDto(
|
||||
int RfqId, string DocNo, int RequisitionId, RfqStatus Status, int LineCount, int QuotationCount);
|
||||
int RfqId, string DocNo, int RequisitionId, int BranchId, RfqStatus Status, int LineCount, int QuotationCount);
|
||||
|
||||
public sealed record QuotationLineDto(int ItemId, decimal UnitPrice, int LeadDays);
|
||||
|
||||
|
||||
@@ -11,4 +11,4 @@ namespace ERPCore.Dtos.Rbac;
|
||||
/// entirely rather than merely restricting them by role.</summary>
|
||||
public sealed record MeResponseDto(
|
||||
string? RoleCode, string? RoleName, IReadOnlyList<string> NavCodes,
|
||||
bool IsAdmin, int? BranchId, string? BranchName, bool BranchingEnabled);
|
||||
bool IsAdmin, int? BranchId, string? BranchName, bool BranchingEnabled, bool WarehouseBranchingEnabled);
|
||||
|
||||
@@ -29,6 +29,22 @@ public sealed class AssignRolePermissionsRequest
|
||||
{
|
||||
public List<int> NavItemIds { get; set; } = new();
|
||||
public List<int> SubNavItemIds { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Optional per-nav/sub-nav-item action override, keyed by <c>NavItemId</c>/
|
||||
/// <c>SubNavItemId</c> and valued with a <see cref="RolePermissionAction"/> bitmask
|
||||
/// (e.g. <c>View|Create|Edit</c> = 7 to grant a module without Delete/Approve). An id
|
||||
/// present in <see cref="NavItemIds"/>/<see cref="SubNavItemIds"/> but absent here
|
||||
/// keeps today's default of <see cref="RolePermissionAction.All"/> — this only narrows,
|
||||
/// never grants access to an id that isn't also in those lists.
|
||||
/// </summary>
|
||||
public Dictionary<int, RolePermissionAction> NavItemActions { get; set; } = new();
|
||||
public Dictionary<int, RolePermissionAction> SubNavItemActions { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed record RolePermissionsDto(int RoleId, List<int> NavItemIds, List<int> SubNavItemIds);
|
||||
public sealed record RolePermissionsDto(
|
||||
int RoleId,
|
||||
List<int> NavItemIds,
|
||||
List<int> SubNavItemIds,
|
||||
Dictionary<int, RolePermissionAction> NavItemActions,
|
||||
Dictionary<int, RolePermissionAction> SubNavItemActions);
|
||||
|
||||
@@ -20,19 +20,21 @@ public sealed record SalesDayEndInvoiceSummaryDto(
|
||||
public sealed record SalesDayEndDto(
|
||||
int SalesDayEndId, string DocNo, int CashierUserId, DateOnly BusinessDate,
|
||||
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,
|
||||
IReadOnlyList<string> SlipNumbers, IReadOnlyList<string> BundleNumbers, IReadOnlyList<SalesDayEndItemLineDto> ItemBreakdown,
|
||||
SalesDayEndInvoiceSummaryDto Invoices);
|
||||
|
||||
public sealed record SalesDayEndSummaryDto(
|
||||
int SalesDayEndId, string DocNo, int CashierUserId, DateOnly BusinessDate,
|
||||
int SlipCount, int BundleCount, decimal GrandTotal, string? GlJournalNo, DateTime ClosedAt);
|
||||
int SlipCount, int BundleCount, decimal GrandTotal, decimal PaidTotal, decimal BalanceTotal, string? GlJournalNo, DateTime ClosedAt);
|
||||
|
||||
/// <summary>What closing right now would include — call before <c>POST /sales-day-end</c> so the
|
||||
/// cashier can see the day's totals, and any still-Draft slips/bundles blocking the close.</summary>
|
||||
public sealed record SalesDayEndPreviewDto(
|
||||
int CashierUserId, DateOnly BusinessDate, bool AlreadyClosed,
|
||||
int SlipCount, int BundleCount, decimal Subtotal, decimal DiscountTotal, decimal TaxTotal, decimal GrandTotal,
|
||||
decimal PaidTotal, decimal BalanceTotal,
|
||||
IReadOnlyList<string> SlipNumbers, IReadOnlyList<string> BundleNumbers,
|
||||
IReadOnlyList<DraftSlipBlockingCloseDto> DraftSlipsBlockingClose,
|
||||
IReadOnlyList<DraftBundleBlockingCloseDto> DraftBundleSalesBlockingClose,
|
||||
|
||||
@@ -56,7 +56,9 @@ public sealed class CreateSalesInvoiceLineRequest
|
||||
[Range(0, 100)] public decimal DiscountPct { get; set; }
|
||||
[Range(0, double.MaxValue)] public decimal DiscountAmount { get; set; }
|
||||
[Range(0, double.MaxValue)] public decimal DiscountValue { get; set; }
|
||||
[Range(0, 100)] public decimal TaxPct { get; set; }
|
||||
/// <summary>Explicit override. Omit (null) to use the item's assigned TaxRate — see
|
||||
/// <see cref="Services.Interfaces.ISalesDomainService.ResolveLineTaxPctAsync"/>.</summary>
|
||||
[Range(0, 100)] public decimal? TaxPct { get; set; }
|
||||
public bool IsFreeIssue { get; set; }
|
||||
public int? ParentLineId { get; set; }
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ public sealed record SalesSlipLineDto(
|
||||
int SalesSlipLineId, int ItemId, string Description, decimal Qty, decimal FreeQty,
|
||||
int WarehouseId, decimal UnitPrice, decimal BaseCost, string PriceSource, decimal DiscountPct,
|
||||
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(
|
||||
decimal Subtotal, decimal DiscountTotal, decimal FreeQtyTotal, decimal TaxTotal, decimal GrandTotal,
|
||||
@@ -54,9 +55,15 @@ public sealed class CreateSalesSlipLineRequest
|
||||
[Range(0, 100)] public decimal DiscountPct { get; set; }
|
||||
[Range(0, double.MaxValue)] public decimal DiscountAmount { get; set; }
|
||||
[Range(0, double.MaxValue)] public decimal DiscountValue { get; set; }
|
||||
[Range(0, 100)] public decimal TaxPct { get; set; }
|
||||
/// <summary>Explicit override. Omit (null) to use the item's assigned TaxRate — see
|
||||
/// <see cref="Services.Interfaces.ISalesDomainService.ResolveLineTaxPctAsync"/>.</summary>
|
||||
[Range(0, 100)] public decimal? TaxPct { get; set; }
|
||||
public bool IsFreeIssue { get; set; }
|
||||
public 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
|
||||
@@ -65,6 +72,16 @@ public sealed class CreateSalesSlipRequest
|
||||
[Required] public int WarehouseId { get; set; }
|
||||
[Required] public int CashierUserId { get; set; }
|
||||
[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
|
||||
@@ -73,4 +90,7 @@ public sealed class UpdateSalesSlipRequest
|
||||
[Required] public int WarehouseId { get; set; }
|
||||
[Required] public int CashierUserId { get; set; }
|
||||
[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; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
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);
|
||||
@@ -5,7 +5,9 @@ namespace ERPCore.Dtos.Stock;
|
||||
|
||||
// Responses (docs/11 §5.6) ------------------------------------------------------
|
||||
|
||||
public sealed record CountLineDto(int CountLineId, int ItemId, int? BinId, decimal SystemQty, decimal? CountedQty, decimal? Variance);
|
||||
public sealed record CountLineDto(
|
||||
int CountLineId, int ItemId, int? BinId, decimal SystemQty, decimal? CountedQty, decimal? Variance,
|
||||
int? BatchId, string? BatchNo, int? SerialId, string? SerialNo);
|
||||
|
||||
public sealed record CountDto(
|
||||
int CountId, string DocNo, int WarehouseId, int BranchId, CountType CountType, CountStatus Status,
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
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);
|
||||
@@ -0,0 +1,26 @@
|
||||
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; }
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Dtos.Uoms;
|
||||
|
||||
/// <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
|
||||
{
|
||||
[Required, StringLength(50)] public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class UpdateUomStatusRequest
|
||||
{
|
||||
[Required, EnumDataType(typeof(EntityStatus))] public EntityStatus Status { get; set; }
|
||||
}
|
||||
|
||||
@@ -7,6 +7,15 @@ public sealed record ManagedUserDto(
|
||||
int UserId, string Username, string DisplayName, string? Email, EntityStatus Status,
|
||||
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>
|
||||
/// Creates a user in both backends: forwards to AuthHex's `registerUser` (source
|
||||
/// of truth for credentials), then mirrors the account into ERPCore's local
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace ERPCore.Dtos.Vendors;
|
||||
/// <summary>Vendor resource (docs/11-BACKEND-PHASE1.md §2.4).</summary>
|
||||
public sealed record VendorDto(
|
||||
int VendorId, string Code, string Name, string? Terms, string? TaxReg,
|
||||
string Currency, EntityStatus Status, DateTime CreatedAt, DateTime? UpdatedAt);
|
||||
string Currency, EntityStatus Status, int BranchId, DateTime CreatedAt, DateTime? UpdatedAt);
|
||||
|
||||
public sealed class CreateVendorRequest
|
||||
{
|
||||
@@ -15,6 +15,9 @@ public sealed class CreateVendorRequest
|
||||
[StringLength(50)] public string? Terms { get; set; }
|
||||
[StringLength(50)] public string? TaxReg { get; set; }
|
||||
[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
|
||||
@@ -24,6 +27,7 @@ public sealed class UpdateVendorRequest
|
||||
[StringLength(50)] public string? Terms { get; set; }
|
||||
[StringLength(50)] public string? TaxReg { get; set; }
|
||||
[Required, StringLength(3, MinimumLength = 3)] public string Currency { get; set; } = "LKR";
|
||||
public int BranchId { get; set; }
|
||||
}
|
||||
|
||||
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
|
||||
/// 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>
|
||||
public sealed record WarehouseDto(int WarehouseId, string Code, string Name, int BranchId, string? BranchName);
|
||||
public sealed record WarehouseDto(int WarehouseId, string Code, string Name, string? Address, string? Phone, int BranchId, string? BranchName);
|
||||
|
||||
/// <summary>Bin/location resource (docs/11 §2.5).</summary>
|
||||
public sealed record BinDto(int BinId, int WarehouseId, string Code, string? BinType);
|
||||
@@ -14,6 +14,17 @@ public sealed class CreateWarehouseRequest
|
||||
{
|
||||
[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; }
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>48915044-7652-476f-ac84-0c82c718098a</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -28,8 +28,13 @@ public sealed class AuthHexClient : IAuthHexClient
|
||||
};
|
||||
|
||||
private readonly HttpClient _http;
|
||||
private readonly IConfiguration _config;
|
||||
|
||||
public AuthHexClient(HttpClient http) => _http = http;
|
||||
public AuthHexClient(HttpClient http, IConfiguration config)
|
||||
{
|
||||
_http = http;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
// ---- UserManager --------------------------------------------------
|
||||
|
||||
@@ -87,6 +92,19 @@ public sealed class AuthHexClient : IAuthHexClient
|
||||
public Task<TwoFaStatusResponse> GetTwoFaStatusAsync(string bearerToken, CancellationToken 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 ------------------------------------------------
|
||||
|
||||
public Task<ForgotPasswordResponse> ForgotPasswordAsync(ForgotPasswordRequest request, CancellationToken ct)
|
||||
@@ -134,11 +152,14 @@ public sealed class AuthHexClient : IAuthHexClient
|
||||
private async Task CallVoidAsync(string routeGroup, string functionName, object payload, string? bearerToken, CancellationToken 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)
|
||||
private async Task<T> CallAsync<T>(string routeGroup, string functionName, object payload, string? bearerToken, CancellationToken ct, Dictionary<string, string>? extraHeaders = null)
|
||||
{
|
||||
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"/api/{routeGroup}");
|
||||
if (!string.IsNullOrEmpty(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(
|
||||
new AuthHexRequestBody { FunctionName = functionName, Payload = payload, Reference = string.Empty },
|
||||
|
||||
@@ -30,6 +30,14 @@ public interface IAuthHexClient
|
||||
Task DisableTwoFaAsync(DisableTwoFaRequest request, 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)
|
||||
Task<ForgotPasswordResponse> ForgotPasswordAsync(ForgotPasswordRequest request, CancellationToken ct);
|
||||
Task<VerifyRecoveryOtpResponse> VerifyRecoveryOtpAsync(VerifyRecoveryOtpRequest request, CancellationToken ct);
|
||||
|
||||
@@ -14,4 +14,12 @@ public static class AuthHexClaims
|
||||
public const string RoleId = "RoleId";
|
||||
public const string RoleCode = "RoleCode";
|
||||
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,4 +35,13 @@ public sealed class CurrentUser : ICurrentUser
|
||||
public string? RoleCode => _accessor.HttpContext?.User?.FindFirstValue(AuthHexClaims.RoleCode);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user