feat: add vendor optional fields and configuration hooks

- Implemented VendorOptionalFields component to handle optional vendor details (mobile, email, address) with validation.
- Created useCustomerConfig hook to load optional customer field settings from Sales configuration.
- Developed useProcurementConfig hook to manage Procurement configuration flags.
- Introduced accessConfigApi for managing access configuration settings.
- Added openingBalancesApi for handling opening balances operations.
- Created procurementConfigApi for managing procurement configuration settings.
- Implemented nav-routes for permission-based routing.
- Developed PDF generation for purchase orders with dynamic vendor and item details.
- Added purchase-order tax helpers for tax calculations and formatting.
- Defined types for opening balances and related operations.
This commit is contained in:
Dhananjaya99
2026-10-18 12:46:31 +05:30
parent c27db06880
commit e7b1f270a5
202 changed files with 99906 additions and 1894 deletions
@@ -0,0 +1,40 @@
using ERPCore.Dtos.Config;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers;
/// <summary>
/// Access configuration — the default role for people who have none. Both reading and changing it need the
/// <c>settings</c> nav permission (it decides what a brand-new sign-in can do).
/// </summary>
[Route("api/v1/access-config")]
[Authorize(Policy = NavPolicies.Prefix + "settings")]
public sealed class AccessConfigController : ApiControllerBase
{
private readonly IAccessConfigService _config;
public AccessConfigController(IAccessConfigService config) => _config = config;
[HttpGet]
[ProducesResponseType(typeof(AccessConfigDto), StatusCodes.Status200OK)]
public async Task<ActionResult<AccessConfigDto>> Get(CancellationToken ct)
{
var result = await _config.GetAsync(ct);
SetETag(result.RowVersion);
return Ok(result.Value);
}
[HttpPut]
[ProducesResponseType(typeof(AccessConfigDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
public async Task<ActionResult<AccessConfigDto>> Update([FromBody] UpdateAccessConfigRequest request, CancellationToken ct)
{
var expected = RequireIfMatch();
var result = await _config.UpdateAsync(request, expected, ct);
SetETag(result.RowVersion);
return Ok(result.Value);
}
}
@@ -1,6 +1,8 @@
using ERPCore.Dtos.Audit;
using ERPCore.Dtos.Common;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers;
@@ -10,6 +12,7 @@ namespace ERPCore.Controllers;
/// the audit trail is required (AR-01 compensating control) and read access is the
/// only way to consume it.
/// </summary>
[Authorize(Policy = NavPolicies.Prefix + "settings.audit-logs")]
[Route("api/v1/audit-logs")]
public sealed class AuditLogsController : ApiControllerBase
{
@@ -47,8 +47,7 @@ public sealed class AuthController : ControllerBase
[ProducesResponseType(typeof(MeResponseDto), StatusCodes.Status200OK)]
public async Task<ActionResult<MeResponseDto>> Me(CancellationToken ct)
{
var roleCode = User.FindFirst(AuthHexClaims.RoleCode)?.Value;
return Ok(await _roles.GetMeAsync(roleCode, ct));
return Ok(await _roles.GetMeAsync(ct));
}
// ---- Session-issuing (UserManager) ------------------------------------
@@ -25,8 +25,9 @@ public sealed class GrnsController : ApiControllerBase
[ProducesResponseType(typeof(PagedResponse<GrnSummaryDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResponse<GrnSummaryDto>>> List(
[FromQuery] PageQuery query, [FromQuery] GrnStatus? status, [FromQuery] int? poId,
[FromQuery] int? vendorId, [FromQuery] int? warehouseId, [FromQuery] int? branchId, CancellationToken ct)
=> Ok(await _grns.ListAsync(query, status, poId, vendorId, warehouseId, branchId, ct));
[FromQuery] int? vendorId, [FromQuery] int? warehouseId, [FromQuery] int? branchId,
[FromQuery] DateOnly? from, [FromQuery] DateOnly? to, CancellationToken ct)
=> Ok(await _grns.ListAsync(query, status, poId, vendorId, warehouseId, branchId, from, to, ct));
[HttpGet("{grnId:int}")]
[ProducesResponseType(typeof(GrnDto), StatusCodes.Status200OK)]
@@ -3,11 +3,13 @@ using ERPCore.Dtos.Common;
using ERPCore.Dtos.Hrm;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers.Hrm;
/// <summary>Attendance upload batch endpoints (docs/13-BACKEND-HRM-API.md §4).</summary>
[Authorize(Policy = NavPolicies.Prefix + "hrm.attendance")]
[Route("api/v1/attendance-batches")]
public sealed class AttendanceBatchesController : ApiControllerBase
{
@@ -44,6 +46,7 @@ public sealed class AttendanceBatchesController : ApiControllerBase
return result is null ? NotFound() : Ok(result);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.attendance" + NavPolicies.ActionSeparator + NavPolicies.CreateAction)]
[HttpPost]
[RequestSizeLimit(20 * 1024 * 1024)]
[ProducesResponseType(typeof(AttendanceUploadBatchDto), StatusCodes.Status201Created)]
@@ -62,6 +65,7 @@ public sealed class AttendanceBatchesController : ApiControllerBase
int batchId, [FromQuery] RowValidationStatus? status, CancellationToken ct)
=> Ok(await _attendance.ListRecordsAsync(batchId, status, ct));
[Authorize(Policy = NavPolicies.Prefix + "hrm.attendance" + NavPolicies.ActionSeparator + NavPolicies.EditAction)]
[HttpPut("{batchId:int}/records/{recordId:int}")]
[ProducesResponseType(typeof(AttendanceRecordDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -70,6 +74,7 @@ public sealed class AttendanceBatchesController : ApiControllerBase
int batchId, int recordId, [FromBody] UpdateAttendanceRecordRequest request, CancellationToken ct)
=> Ok(await _attendance.UpdateRecordAsync(batchId, recordId, request, _currentUser.AuditUserId, ct));
[Authorize(Policy = NavPolicies.Prefix + "hrm.attendance" + NavPolicies.ActionSeparator + NavPolicies.EditAction)]
[HttpPost("{batchId:int}/resolve-duplicate")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
@@ -79,18 +84,21 @@ public sealed class AttendanceBatchesController : ApiControllerBase
return NoContent();
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.attendance" + NavPolicies.ActionSeparator + NavPolicies.EditAction)]
[HttpPost("{batchId:int}/validate")]
[ProducesResponseType(typeof(AttendanceUploadBatchDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
public async Task<ActionResult<AttendanceUploadBatchDto>> Validate(int batchId, CancellationToken ct)
=> Ok(await _attendance.ValidateAsync(batchId, ct));
[Authorize(Policy = NavPolicies.Prefix + "hrm.attendance" + NavPolicies.ActionSeparator + NavPolicies.ApproveAction)]
[HttpPost("{batchId:int}/confirm")]
[ProducesResponseType(typeof(AttendanceUploadBatchDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<AttendanceUploadBatchDto>> Confirm(int batchId, CancellationToken ct)
=> Ok(await _attendance.ConfirmAsync(batchId, _currentUser.AuditUserId, ct));
[Authorize(Policy = NavPolicies.Prefix + "hrm.attendance" + NavPolicies.ActionSeparator + NavPolicies.ApproveAction)]
[HttpPost("{batchId:int}/unlock")]
[ProducesResponseType(typeof(AttendanceUploadBatchDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
@@ -1,7 +1,9 @@
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common;
using ERPCore.Dtos.Hrm;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers.Hrm;
@@ -31,6 +33,7 @@ public sealed class BranchesController : ApiControllerBase
return Ok(result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "settings.branches")]
[HttpPost]
[ProducesResponseType(typeof(BranchDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
@@ -41,6 +44,7 @@ public sealed class BranchesController : ApiControllerBase
return Created($"/api/v1/branches/{result.Value.BranchId}", result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "settings.branches")]
[HttpPut("{branchId:int}")]
[ProducesResponseType(typeof(BranchDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -53,6 +57,7 @@ public sealed class BranchesController : ApiControllerBase
return Ok(result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "settings.branches")]
[HttpPatch("{branchId:int}/status")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -62,6 +67,7 @@ public sealed class BranchesController : ApiControllerBase
return NoContent();
}
[Authorize(Policy = NavPolicies.Prefix + "settings.branches")]
[HttpDelete("{branchId:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -1,12 +1,15 @@
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common;
using ERPCore.Dtos.Hrm;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers.Hrm;
/// <summary>Department master endpoints (docs/13-BACKEND-HRM-API.md §2).</summary>
[Authorize(Policy = NavPolicies.Prefix + "hrm")]
[Route("api/v1/departments")]
public sealed class DepartmentsController : ApiControllerBase
{
@@ -31,6 +34,7 @@ public sealed class DepartmentsController : ApiControllerBase
return Ok(result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpPost]
[ProducesResponseType(typeof(DepartmentDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
@@ -41,6 +45,7 @@ public sealed class DepartmentsController : ApiControllerBase
return Created($"/api/v1/departments/{result.Value.DepartmentId}", result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpPut("{departmentId:int}")]
[ProducesResponseType(typeof(DepartmentDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -54,6 +59,7 @@ public sealed class DepartmentsController : ApiControllerBase
return Ok(result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpPatch("{departmentId:int}/status")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -63,6 +69,7 @@ public sealed class DepartmentsController : ApiControllerBase
return NoContent();
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpDelete("{departmentId:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -1,12 +1,15 @@
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common;
using ERPCore.Dtos.Hrm;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers.Hrm;
/// <summary>Designation (job title) master endpoints (docs/13-BACKEND-HRM-API.md §2).</summary>
[Authorize(Policy = NavPolicies.Prefix + "hrm")]
[Route("api/v1/designations")]
public sealed class DesignationsController : ApiControllerBase
{
@@ -31,6 +34,7 @@ public sealed class DesignationsController : ApiControllerBase
return Ok(result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpPost]
[ProducesResponseType(typeof(DesignationDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
@@ -41,6 +45,7 @@ public sealed class DesignationsController : ApiControllerBase
return Created($"/api/v1/designations/{result.Value.DesignationId}", result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpPut("{designationId:int}")]
[ProducesResponseType(typeof(DesignationDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -53,6 +58,7 @@ public sealed class DesignationsController : ApiControllerBase
return Ok(result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpPatch("{designationId:int}/status")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -62,6 +68,7 @@ public sealed class DesignationsController : ApiControllerBase
return NoContent();
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpDelete("{designationId:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -3,6 +3,7 @@ using ERPCore.Dtos.Common;
using ERPCore.Dtos.Hrm;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers.Hrm;
@@ -11,6 +12,9 @@ namespace ERPCore.Controllers.Hrm;
/// Employee (staff) endpoints, incl. the Employee&lt;-&gt;User cross-link and staff
/// document sub-resources (docs/13-BACKEND-HRM-API.md §3).
/// </summary>
// Any HR role may list employees (the leave and payroll screens pick from it); everything else about a person
// - the record itself, documents, pay data - is behind the Employees (or Payroll) permission, endpoint by endpoint.
[Authorize(Policy = NavPolicies.Prefix + "hrm")]
[Route("api/v1/employees")]
public sealed class EmployeesController : ApiControllerBase
{
@@ -44,11 +48,13 @@ public sealed class EmployeesController : ApiControllerBase
=> Ok(await _employees.ListAsync(query, status, departmentId, designationId, branchId, ct));
/// <summary>Advisory reverse-direction lookup: does a System User already exist with this email? (docs/12-BACKEND-HRM.md A.5)</summary>
[Authorize(Policy = NavPolicies.Prefix + "hrm.employees")]
[HttpGet("email-lookup")]
[ProducesResponseType(typeof(UserMatchResponse), StatusCodes.Status200OK)]
public async Task<ActionResult<UserMatchResponse>> EmailLookup([FromQuery] string email, CancellationToken ct)
=> Ok(new UserMatchResponse(await _links.FindUserCandidateByEmailAsync(email, ct)));
[Authorize(Policy = NavPolicies.Prefix + "hrm.employees")]
[HttpGet("{employeeId:int}")]
[ProducesResponseType(typeof(EmployeeDetailDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -60,6 +66,7 @@ public sealed class EmployeesController : ApiControllerBase
return Ok(result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.employees" + NavPolicies.ActionSeparator + NavPolicies.CreateAction)]
[HttpPost]
[ProducesResponseType(typeof(EmployeeDetailDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
@@ -71,6 +78,7 @@ public sealed class EmployeesController : ApiControllerBase
return Created($"/api/v1/employees/{result.Value.EmployeeId}", result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.employees" + NavPolicies.ActionSeparator + NavPolicies.EditAction)]
[HttpPut("{employeeId:int}")]
[ProducesResponseType(typeof(EmployeeDetailDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -83,6 +91,7 @@ public sealed class EmployeesController : ApiControllerBase
return Ok(result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.employees" + NavPolicies.ActionSeparator + NavPolicies.EditAction)]
[HttpPatch("{employeeId:int}/status")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -93,6 +102,7 @@ public sealed class EmployeesController : ApiControllerBase
}
/// <summary>Permanently deletes the employee and all dependent HR records. Blocked (409) if payroll history exists.</summary>
[Authorize(Policy = NavPolicies.Prefix + "hrm.employees" + NavPolicies.ActionSeparator + NavPolicies.DeleteAction)]
[HttpDelete("{employeeId:int}")]
[ProducesResponseType(typeof(EmployeeDeleteResultDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -103,6 +113,7 @@ public sealed class EmployeesController : ApiControllerBase
return Ok(result);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.employees" + NavPolicies.ActionSeparator + NavPolicies.EditAction)]
[HttpPost("{employeeId:int}/link-user")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -113,6 +124,7 @@ public sealed class EmployeesController : ApiControllerBase
return NoContent();
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.employees" + NavPolicies.ActionSeparator + NavPolicies.EditAction)]
[HttpDelete("{employeeId:int}/link-user")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -122,11 +134,13 @@ public sealed class EmployeesController : ApiControllerBase
return NoContent();
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.payroll")]
[HttpGet("{employeeId:int}/bank-details")]
[ProducesResponseType(typeof(List<EmployeeBankDetailDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<List<EmployeeBankDetailDto>>> ListBankDetails(int employeeId, CancellationToken ct)
=> Ok(await _employees.ListBankDetailsAsync(employeeId, ct));
[Authorize(Policy = NavPolicies.Prefix + "hrm.payroll" + NavPolicies.ActionSeparator + NavPolicies.EditAction)]
[HttpPut("{employeeId:int}/bank-details")]
[ProducesResponseType(typeof(List<EmployeeBankDetailDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -140,6 +154,7 @@ public sealed class EmployeesController : ApiControllerBase
public async Task<ActionResult<List<LeaveBalanceDto>>> ListLeaveBalances(int employeeId, [FromQuery] int? year, CancellationToken ct)
=> Ok(await _leaveBalances.ListAsync(employeeId, year, ct));
[Authorize(Policy = NavPolicies.Prefix + "hrm.employees" + NavPolicies.ActionSeparator + NavPolicies.EditAction)]
[HttpPut("{employeeId:int}/leave-balances")]
[ProducesResponseType(typeof(List<LeaveBalanceDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -147,11 +162,13 @@ public sealed class EmployeesController : ApiControllerBase
int employeeId, [FromBody] UpdateLeaveBalancesRequest request, CancellationToken ct)
=> Ok(await _leaveBalances.ApplyAdjustmentsAsync(employeeId, request, ct));
[Authorize(Policy = NavPolicies.Prefix + "hrm.payroll")]
[HttpGet("{employeeId:int}/salary-structure")]
[ProducesResponseType(typeof(List<EmployeeSalaryStructureDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<List<EmployeeSalaryStructureDto>>> GetSalaryStructureHistory(int employeeId, CancellationToken ct)
=> Ok(await _salaryStructures.ListHistoryAsync(employeeId, ct));
[Authorize(Policy = NavPolicies.Prefix + "hrm.payroll" + NavPolicies.ActionSeparator + NavPolicies.EditAction)]
[HttpPost("{employeeId:int}/salary-structure")]
[ProducesResponseType(typeof(EmployeeSalaryStructureDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -163,11 +180,13 @@ public sealed class EmployeesController : ApiControllerBase
return Created($"/api/v1/employees/{employeeId}/salary-structure", result);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.payroll")]
[HttpGet("{employeeId:int}/loans")]
[ProducesResponseType(typeof(List<EmployeeLoanDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<List<EmployeeLoanDto>>> ListLoans(int employeeId, CancellationToken ct)
=> Ok(await _loans.ListAsync(employeeId, ct));
[Authorize(Policy = NavPolicies.Prefix + "hrm.payroll")]
[HttpGet("{employeeId:int}/loans/{loanId:int}")]
[ProducesResponseType(typeof(EmployeeLoanDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -177,6 +196,7 @@ public sealed class EmployeesController : ApiControllerBase
return result is null ? NotFound() : Ok(result);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.payroll" + NavPolicies.ActionSeparator + NavPolicies.CreateAction)]
[HttpPost("{employeeId:int}/loans")]
[ProducesResponseType(typeof(EmployeeLoanDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -186,11 +206,13 @@ public sealed class EmployeesController : ApiControllerBase
return Created($"/api/v1/employees/{employeeId}/loans/{result.EmployeeLoanId}", result);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.employees")]
[HttpGet("{employeeId:int}/documents")]
[ProducesResponseType(typeof(List<EmployeeDocumentDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<List<EmployeeDocumentDto>>> ListDocuments(int employeeId, CancellationToken ct)
=> Ok(await _documents.ListAsync(employeeId, ct));
[Authorize(Policy = NavPolicies.Prefix + "hrm.employees" + NavPolicies.ActionSeparator + NavPolicies.EditAction)]
[HttpPost("{employeeId:int}/documents")]
[RequestSizeLimit(20 * 1024 * 1024)]
[ProducesResponseType(typeof(EmployeeDocumentDto), StatusCodes.Status201Created)]
@@ -206,6 +228,7 @@ public sealed class EmployeesController : ApiControllerBase
return Created($"/api/v1/employees/{employeeId}/documents/{result.EmployeeDocumentId}", result);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.employees")]
[HttpGet("{employeeId:int}/documents/{documentId:int}/download")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -215,6 +238,7 @@ public sealed class EmployeesController : ApiControllerBase
return File(content, contentType, fileName);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.employees" + NavPolicies.ActionSeparator + NavPolicies.EditAction)]
[HttpPatch("{employeeId:int}/documents/{documentId:int}/status")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -1,12 +1,15 @@
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common;
using ERPCore.Dtos.Hrm;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers.Hrm;
/// <summary>EmploymentType master endpoints (docs/13-BACKEND-HRM-API.md §2).</summary>
[Authorize(Policy = NavPolicies.Prefix + "hrm")]
[Route("api/v1/employment-types")]
public sealed class EmploymentTypesController : ApiControllerBase
{
@@ -31,6 +34,7 @@ public sealed class EmploymentTypesController : ApiControllerBase
return Ok(result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpPost]
[ProducesResponseType(typeof(EmploymentTypeDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
@@ -41,6 +45,7 @@ public sealed class EmploymentTypesController : ApiControllerBase
return Created($"/api/v1/employment-types/{result.Value.EmploymentTypeId}", result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpPut("{employmentTypeId:int}")]
[ProducesResponseType(typeof(EmploymentTypeDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -53,6 +58,7 @@ public sealed class EmploymentTypesController : ApiControllerBase
return Ok(result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpPatch("{employmentTypeId:int}/status")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -62,6 +68,7 @@ public sealed class EmploymentTypesController : ApiControllerBase
return NoContent();
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpDelete("{employmentTypeId:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -1,12 +1,15 @@
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common;
using ERPCore.Dtos.Hrm;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers.Hrm;
/// <summary>Staff document-type catalog ("DocType") endpoints (docs/13-BACKEND-HRM-API.md §2).</summary>
[Authorize(Policy = NavPolicies.Prefix + "hrm")]
[Route("api/v1/hr-document-types")]
public sealed class HrDocumentTypesController : ApiControllerBase
{
@@ -31,6 +34,7 @@ public sealed class HrDocumentTypesController : ApiControllerBase
return Ok(result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpPost]
[ProducesResponseType(typeof(HrDocumentTypeDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
@@ -41,6 +45,7 @@ public sealed class HrDocumentTypesController : ApiControllerBase
return Created($"/api/v1/hr-document-types/{result.Value.HrDocumentTypeId}", result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpPut("{hrDocumentTypeId:int}")]
[ProducesResponseType(typeof(HrDocumentTypeDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -53,6 +58,7 @@ public sealed class HrDocumentTypesController : ApiControllerBase
return Ok(result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpPatch("{hrDocumentTypeId:int}/status")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -62,6 +68,7 @@ public sealed class HrDocumentTypesController : ApiControllerBase
return NoContent();
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpDelete("{hrDocumentTypeId:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -1,10 +1,13 @@
using ERPCore.Dtos.Hrm;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers.Hrm;
/// <summary>Read-only HRM reports (FR-HR-RPT, docs/13-BACKEND-HRM-API.md §6). No new entities — aggregation over existing tables.</summary>
[Authorize(Policy = NavPolicies.Prefix + "hrm.reports")]
[Route("api/v1/reports/hrm")]
public sealed class HrReportsController : ApiControllerBase
{
@@ -30,11 +33,13 @@ public sealed class HrReportsController : ApiControllerBase
[FromQuery] int periodYear, [FromQuery] int periodMonth, CancellationToken ct)
=> Ok(await _reports.LateArrivalReportAsync(periodYear, periodMonth, ct));
[Authorize(Policy = NavPolicies.Prefix + "hrm.payroll")]
[HttpGet("payroll-register")]
[ProducesResponseType(typeof(List<PayrollRegisterRowDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<List<PayrollRegisterRowDto>>> PayrollRegister([FromQuery] int payrollRunId, CancellationToken ct)
=> Ok(await _reports.PayrollRegisterAsync(payrollRunId, ct));
[Authorize(Policy = NavPolicies.Prefix + "hrm.payroll")]
[HttpGet("salary-history")]
[ProducesResponseType(typeof(List<SalaryHistoryRowDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<List<SalaryHistoryRowDto>>> SalaryHistory([FromQuery] int employeeId, CancellationToken ct)
@@ -3,11 +3,13 @@ using ERPCore.Dtos.Common;
using ERPCore.Dtos.Hrm;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers.Hrm;
/// <summary>Leave request endpoints (docs/13-BACKEND-HRM-API.md §5).</summary>
[Authorize(Policy = NavPolicies.Prefix + "hrm.leave")]
[Route("api/v1/leave-requests")]
public sealed class LeaveRequestsController : ApiControllerBase
{
@@ -35,6 +37,7 @@ public sealed class LeaveRequestsController : ApiControllerBase
return result is null ? NotFound() : Ok(result);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.leave" + NavPolicies.ActionSeparator + NavPolicies.CreateAction)]
[HttpPost]
[ProducesResponseType(typeof(LeaveRequestDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -45,24 +48,28 @@ public sealed class LeaveRequestsController : ApiControllerBase
return Created($"/api/v1/leave-requests/{result.LeaveRequestId}", result);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.leave" + NavPolicies.ActionSeparator + NavPolicies.CreateAction)]
[HttpPost("{leaveRequestId:int}/submit")]
[ProducesResponseType(typeof(LeaveRequestDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<LeaveRequestDto>> Submit(int leaveRequestId, CancellationToken ct)
=> Ok(await _requests.SubmitAsync(leaveRequestId, ct));
[Authorize(Policy = NavPolicies.Prefix + "hrm.leave" + NavPolicies.ActionSeparator + NavPolicies.ApproveAction)]
[HttpPost("{leaveRequestId:int}/approve")]
[ProducesResponseType(typeof(LeaveRequestDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<LeaveRequestDto>> Approve(int leaveRequestId, CancellationToken ct)
=> Ok(await _requests.ApproveAsync(leaveRequestId, _currentUser.AuditUserId, ct));
[Authorize(Policy = NavPolicies.Prefix + "hrm.leave" + NavPolicies.ActionSeparator + NavPolicies.ApproveAction)]
[HttpPost("{leaveRequestId:int}/reject")]
[ProducesResponseType(typeof(LeaveRequestDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<LeaveRequestDto>> Reject(int leaveRequestId, [FromBody] RejectLeaveRequestRequest request, CancellationToken ct)
=> Ok(await _requests.RejectAsync(leaveRequestId, request.Reason, _currentUser.AuditUserId, ct));
[Authorize(Policy = NavPolicies.Prefix + "hrm.leave" + NavPolicies.ActionSeparator + NavPolicies.CreateAction)]
[HttpPost("{leaveRequestId:int}/cancel")]
[ProducesResponseType(typeof(LeaveRequestDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
@@ -1,12 +1,15 @@
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common;
using ERPCore.Dtos.Hrm;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers.Hrm;
/// <summary>Leave type master endpoints (docs/13-BACKEND-HRM-API.md §5).</summary>
[Authorize(Policy = NavPolicies.Prefix + "hrm")]
[Route("api/v1/leave-types")]
public sealed class LeaveTypesController : ApiControllerBase
{
@@ -31,6 +34,7 @@ public sealed class LeaveTypesController : ApiControllerBase
return Ok(result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpPost]
[ProducesResponseType(typeof(LeaveTypeDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
@@ -41,6 +45,7 @@ public sealed class LeaveTypesController : ApiControllerBase
return Created($"/api/v1/leave-types/{result.Value.LeaveTypeId}", result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpPut("{leaveTypeId:int}")]
[ProducesResponseType(typeof(LeaveTypeDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -53,6 +58,7 @@ public sealed class LeaveTypesController : ApiControllerBase
return Ok(result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpPatch("{leaveTypeId:int}/status")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -62,6 +68,7 @@ public sealed class LeaveTypesController : ApiControllerBase
return NoContent();
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpDelete("{leaveTypeId:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -3,11 +3,13 @@ using ERPCore.Dtos.Common;
using ERPCore.Dtos.Hrm;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers.Hrm;
/// <summary>Payroll run endpoints (docs/13-BACKEND-HRM-API.md §6).</summary>
[Authorize(Policy = NavPolicies.Prefix + "hrm.payroll")]
[Route("api/v1/payroll-runs")]
public sealed class PayrollRunsController : ApiControllerBase
{
@@ -50,6 +52,7 @@ public sealed class PayrollRunsController : ApiControllerBase
return result is null ? NotFound() : Ok(result);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.payroll" + NavPolicies.ActionSeparator + NavPolicies.CreateAction)]
[HttpPost]
[ProducesResponseType(typeof(PayrollRunDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
@@ -59,24 +62,28 @@ public sealed class PayrollRunsController : ApiControllerBase
return Created($"/api/v1/payroll-runs/{result.PayrollRunId}", result);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.payroll" + NavPolicies.ActionSeparator + NavPolicies.ApproveAction)]
[HttpPost("{payrollRunId:int}/approve")]
[ProducesResponseType(typeof(PayrollRunDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<PayrollRunDto>> Approve(int payrollRunId, CancellationToken ct)
=> Ok(await _payrollRuns.ApproveAsync(payrollRunId, _currentUser.AuditUserId, ct));
[Authorize(Policy = NavPolicies.Prefix + "hrm.payroll" + NavPolicies.ActionSeparator + NavPolicies.ApproveAction)]
[HttpPost("{payrollRunId:int}/lock")]
[ProducesResponseType(typeof(PayrollRunDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<PayrollRunDto>> Lock(int payrollRunId, CancellationToken ct)
=> Ok(await _payrollRuns.LockAsync(payrollRunId, _currentUser.AuditUserId, ct));
[Authorize(Policy = NavPolicies.Prefix + "hrm.payroll" + NavPolicies.ActionSeparator + NavPolicies.ApproveAction)]
[HttpPost("{payrollRunId:int}/unlock")]
[ProducesResponseType(typeof(PayrollRunDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<PayrollRunDto>> Unlock(int payrollRunId, [FromBody] UnlockPayrollRunRequest request, CancellationToken ct)
=> Ok(await _payrollRuns.UnlockAsync(payrollRunId, request.Reason, _currentUser.AuditUserId, ct));
[Authorize(Policy = NavPolicies.Prefix + "hrm.payroll" + NavPolicies.ActionSeparator + NavPolicies.CreateAction)]
[HttpPost("{payrollRunId:int}/generate-payslips")]
[ProducesResponseType(typeof(List<PayslipDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
@@ -1,11 +1,13 @@
using ERPCore.Dtos.Hrm;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers.Hrm;
/// <summary>Effective-dated EPF/ETF settings (docs/13-BACKEND-HRM-API.md §6).</summary>
[Authorize(Policy = NavPolicies.Prefix + "hrm")]
[Route("api/v1/payroll-statutory-settings")]
public sealed class PayrollStatutorySettingsController : ApiControllerBase
{
@@ -23,6 +25,7 @@ public sealed class PayrollStatutorySettingsController : ApiControllerBase
public async Task<ActionResult<List<PayrollStatutorySettingDto>>> List(CancellationToken ct)
=> Ok(await _settings.ListAsync(ct));
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpPost]
[ProducesResponseType(typeof(PayrollStatutorySettingDto), StatusCodes.Status201Created)]
public async Task<ActionResult<PayrollStatutorySettingDto>> Create([FromBody] UpsertPayrollStatutorySettingRequest request, CancellationToken ct)
@@ -1,10 +1,13 @@
using ERPCore.Dtos.Hrm;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers.Hrm;
/// <summary>Payslip retrieval + HTML print view (docs/13-BACKEND-HRM-API.md §6).</summary>
[Authorize(Policy = NavPolicies.Prefix + "hrm.payroll")]
[Route("api/v1/payslips")]
public sealed class PayslipsController : ApiControllerBase
{
@@ -1,12 +1,15 @@
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common;
using ERPCore.Dtos.Hrm;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers.Hrm;
/// <summary>SalaryComponent master endpoints (docs/13-BACKEND-HRM-API.md §6).</summary>
[Authorize(Policy = NavPolicies.Prefix + "hrm")]
[Route("api/v1/salary-components")]
public sealed class SalaryComponentsController : ApiControllerBase
{
@@ -31,6 +34,7 @@ public sealed class SalaryComponentsController : ApiControllerBase
return Ok(result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpPost]
[ProducesResponseType(typeof(SalaryComponentDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
@@ -41,6 +45,7 @@ public sealed class SalaryComponentsController : ApiControllerBase
return Created($"/api/v1/salary-components/{result.Value.SalaryComponentId}", result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpPut("{salaryComponentId:int}")]
[ProducesResponseType(typeof(SalaryComponentDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -53,6 +58,7 @@ public sealed class SalaryComponentsController : ApiControllerBase
return Ok(result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpPatch("{salaryComponentId:int}/status")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -62,6 +68,7 @@ public sealed class SalaryComponentsController : ApiControllerBase
return NoContent();
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpDelete("{salaryComponentId:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -1,10 +1,13 @@
using ERPCore.Dtos.Hrm;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers.Hrm;
/// <summary>Configurable APIT-style tax slabs (docs/13-BACKEND-HRM-API.md §6).</summary>
[Authorize(Policy = NavPolicies.Prefix + "hrm")]
[Route("api/v1/tax-slabs")]
public sealed class TaxSlabsController : ApiControllerBase
{
@@ -17,6 +20,7 @@ public sealed class TaxSlabsController : ApiControllerBase
public async Task<ActionResult<List<TaxSlabDto>>> List(CancellationToken ct)
=> Ok(await _taxSlabs.ListAsync(ct));
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpPost]
[ProducesResponseType(typeof(TaxSlabDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
@@ -1,12 +1,15 @@
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common;
using ERPCore.Dtos.Hrm;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers.Hrm;
/// <summary>WorkShift master endpoints (docs/13-BACKEND-HRM-API.md §2).</summary>
[Authorize(Policy = NavPolicies.Prefix + "hrm")]
[Route("api/v1/work-shifts")]
public sealed class WorkShiftsController : ApiControllerBase
{
@@ -31,6 +34,7 @@ public sealed class WorkShiftsController : ApiControllerBase
return Ok(result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpPost]
[ProducesResponseType(typeof(WorkShiftDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
@@ -41,6 +45,7 @@ public sealed class WorkShiftsController : ApiControllerBase
return Created($"/api/v1/work-shifts/{result.Value.WorkShiftId}", result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpPut("{workShiftId:int}")]
[ProducesResponseType(typeof(WorkShiftDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -53,6 +58,7 @@ public sealed class WorkShiftsController : ApiControllerBase
return Ok(result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpPatch("{workShiftId:int}/status")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -62,6 +68,7 @@ public sealed class WorkShiftsController : ApiControllerBase
return NoContent();
}
[Authorize(Policy = NavPolicies.Prefix + "hrm.settings")]
[HttpDelete("{workShiftId:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -0,0 +1,71 @@
using ERPCore.Dtos.OpeningBalances;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers;
/// <summary>
/// One-time opening balances. Gated on the <c>accounts.opening-balances</c> nav code (Admin always passes;
/// other roles need it granted under Settings → Roles).
/// </summary>
[Route("api/v1/opening-balances")]
[Authorize(Policy = NavPolicies.Prefix + "accounts.opening-balances")]
public sealed class OpeningBalancesController : ApiControllerBase
{
private readonly IOpeningBalanceService _openingBalances;
private readonly IOpeningStockService _openingStock;
public OpeningBalancesController(IOpeningBalanceService openingBalances, IOpeningStockService openingStock)
{
_openingBalances = openingBalances;
_openingStock = openingStock;
}
/// <summary>The cash and bank accounts to enter opening amounts for — or the posted record, once done.</summary>
[HttpGet("cash-bank")]
[ProducesResponseType(typeof(CashBankOpeningStateDto), StatusCodes.Status200OK)]
public async Task<ActionResult<CashBankOpeningStateDto>> GetCashBank(CancellationToken ct) =>
Ok(await _openingBalances.GetCashBankAsync(ct));
/// <summary>Posts the cash and bank opening balances as one journal. Allowed once; later calls get 409.</summary>
[HttpPost("cash-bank")]
[ProducesResponseType(typeof(OpeningBalanceRunDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<OpeningBalanceRunDto>> PostCashBank(
[FromBody] PostCashBankOpeningRequest request, CancellationToken ct)
{
var run = await _openingBalances.PostCashBankAsync(request, ct);
return StatusCode(StatusCodes.Status201Created, run);
}
/// <summary>Opening-stock runs posted so far, newest first.</summary>
[HttpGet("stock")]
[ProducesResponseType(typeof(IReadOnlyList<OpeningStockRunSummaryDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<IReadOnlyList<OpeningStockRunSummaryDto>>> ListStock(CancellationToken ct) =>
Ok(await _openingStock.ListAsync(ct));
/// <summary>One opening-stock run with its lines.</summary>
[HttpGet("stock/{runId:int}")]
[ProducesResponseType(typeof(OpeningStockRunDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<OpeningStockRunDto>> GetStock(int runId, CancellationToken ct)
{
var run = await _openingStock.GetAsync(runId, ct);
return run is null ? NotFound() : Ok(run);
}
/// <summary>
/// Posts opening stock: a FIFO layer per line and one journal (Dr Inventory / Cr Opening Balance Equity).
/// An item that already has stock movements in that warehouse is refused with 409.
/// </summary>
[HttpPost("stock")]
[ProducesResponseType(typeof(OpeningStockRunDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<OpeningStockRunDto>> PostStock([FromBody] PostOpeningStockRequest request, CancellationToken ct)
{
var run = await _openingStock.PostAsync(request, ct);
return StatusCode(StatusCodes.Status201Created, run);
}
}
@@ -0,0 +1,46 @@
using ERPCore.Dtos.Config;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers;
/// <summary>
/// Procurement configuration endpoints — optional vendor fields and PO tax.
/// <para>
/// <b>Authorization:</b> reading is open to any signed-in ERP user, because the vendor and purchase-order
/// forms must read the flags and those screens sit under different nav codes. The flags are not
/// sensitive. Changing them needs the <c>settings</c> nav policy, like <see cref="BranchConfigController"/>.
/// </para>
/// </summary>
[Route("api/v1/procurement-config")]
public sealed class ProcurementConfigController : ApiControllerBase
{
private readonly IProcurementConfigService _config;
public ProcurementConfigController(IProcurementConfigService config) => _config = config;
[HttpGet]
[ProducesResponseType(typeof(ProcurementConfigDto), StatusCodes.Status200OK)]
public async Task<ActionResult<ProcurementConfigDto>> Get(CancellationToken ct)
{
var result = await _config.GetAsync(ct);
SetETag(result.RowVersion);
return Ok(result.Value);
}
[HttpPut]
[Authorize(Policy = NavPolicies.Prefix + "settings")]
[ProducesResponseType(typeof(ProcurementConfigDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
public async Task<ActionResult<ProcurementConfigDto>> Update(
[FromBody] UpdateProcurementConfigRequest request, CancellationToken ct)
{
var expected = RequireIfMatch();
var result = await _config.UpdateAsync(request, expected, ct);
SetETag(result.RowVersion);
return Ok(result.Value);
}
}
@@ -20,8 +20,9 @@ public sealed class PurchaseOrdersController : ApiControllerBase
[HttpGet]
[ProducesResponseType(typeof(PagedResponse<PurchaseOrderSummaryDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResponse<PurchaseOrderSummaryDto>>> List(
[FromQuery] PageQuery query, [FromQuery] PurchaseOrderStatus? status, [FromQuery] int? vendorId, CancellationToken ct)
=> Ok(await _pos.ListAsync(query, status, vendorId, ct));
[FromQuery] PageQuery query, [FromQuery] PurchaseOrderStatus? status, [FromQuery] int? vendorId,
[FromQuery] DateOnly? from, [FromQuery] DateOnly? to, CancellationToken ct)
=> Ok(await _pos.ListAsync(query, status, vendorId, from, to, ct));
[HttpGet("{poId:int}")]
[ProducesResponseType(typeof(PurchaseOrderDto), StatusCodes.Status200OK)]
@@ -1,7 +1,9 @@
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common;
using ERPCore.Dtos.Reference;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers;
@@ -20,6 +22,7 @@ public sealed class ReasonCodesController : ApiControllerBase
[FromQuery] ReasonContext? context, [FromQuery] PageQuery query, CancellationToken ct)
=> Ok(await _codes.ListAsync(context, query, ct));
[Authorize(Policy = NavPolicies.Prefix + "settings")]
[HttpPost]
[ProducesResponseType(typeof(ReasonCodeDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
@@ -1,15 +1,15 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Dtos.Common;
using ERPCore.Dtos.Sales;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers;
/// <summary>Sales-return endpoints — customer returns of previously sold goods.</summary>
[Route("api/v1/sales-returns")]
[Authorize(Policy = NavPolicies.Prefix + "sales")]
[Authorize(Policy = NavPolicies.Prefix + "sales-returns")]
public sealed class SalesReturnsController : ApiControllerBase
{
private readonly ISalesReturnService _returns;
+18 -2
View File
@@ -41,8 +41,8 @@ public sealed class UsersController : ApiControllerBase
[HttpGet]
[ProducesResponseType(typeof(PagedResponse<ManagedUserDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResponse<ManagedUserDto>>> List([FromQuery] PageQuery query, [FromQuery] EntityStatus? status, [FromQuery] string? roleCode, CancellationToken ct)
=> Ok(await _users.ListAsync(query, status, roleCode, ct));
public async Task<ActionResult<PagedResponse<ManagedUserDto>>> List([FromQuery] PageQuery query, [FromQuery] EntityStatus? status, [FromQuery] string? roleCode, [FromQuery] bool? isAdmin, CancellationToken ct)
=> Ok(await _users.ListAsync(query, status, roleCode, isAdmin, ct));
/// <summary>AuthHex UserType options for the create-user form's select.</summary>
[HttpGet("user-types")]
@@ -117,4 +117,20 @@ public sealed class UsersController : ApiControllerBase
return Ok(await _users.PurgeAuthHexIdentityAsync(request, ct));
}
/// <summary>
/// One-time migration helper (administrators only): copies each role-less user's role from AuthHex into the ERP.
/// Safe to repeat — users that already have a role are left alone. See
/// <see cref="IUserManagementService.BackfillRolesFromAuthHexAsync"/>.
/// </summary>
[HttpPost("backfill-roles")]
[ProducesResponseType(typeof(BackfillRolesResultDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult<BackfillRolesResultDto>> BackfillRoles(CancellationToken ct)
{
if (!_currentUser.IsAdmin)
throw new DomainException("FORBIDDEN", "Only an administrator can run the role backfill.", 403);
return Ok(await _users.BackfillRolesFromAuthHexAsync(ct));
}
}
@@ -1,16 +1,16 @@
using Microsoft.AspNetCore.Authorization;
using ERPCore.Infra.Auth;
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common;
using ERPCore.Dtos.Vendors;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers;
/// <summary>Vendor master endpoints (docs/11-BACKEND-PHASE1.md §2.4).</summary>
[Route("api/v1/vendors")]
[Authorize(Policy = NavPolicies.Prefix + "vendors")]
[Authorize(Policy = NavPolicies.Prefix + "procurement")]
public sealed class VendorsController : ApiControllerBase
{
private readonly IVendorService _vendors;
@@ -35,6 +35,7 @@ public sealed class VendorsController : ApiControllerBase
return Ok(result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "procurement.vendors" + NavPolicies.ActionSeparator + NavPolicies.CreateAction)]
[HttpPost]
[ProducesResponseType(typeof(VendorDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
@@ -45,6 +46,7 @@ public sealed class VendorsController : ApiControllerBase
return Created($"/api/v1/vendors/{result.Value.VendorId}", result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "procurement.vendors" + NavPolicies.ActionSeparator + NavPolicies.EditAction)]
[HttpPut("{vendorId:int}")]
[ProducesResponseType(typeof(VendorDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -57,6 +59,7 @@ public sealed class VendorsController : ApiControllerBase
return Ok(result.Value);
}
[Authorize(Policy = NavPolicies.Prefix + "procurement.vendors" + NavPolicies.ActionSeparator + NavPolicies.EditAction)]
[HttpPatch("{vendorId:int}/status")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -67,7 +70,7 @@ public sealed class VendorsController : ApiControllerBase
}
[HttpDelete("{vendorId:int}")]
[Authorize(Policy = NavPolicies.Prefix + "vendors" + NavPolicies.ActionSeparator + NavPolicies.DeleteAction)]
[Authorize(Policy = NavPolicies.Prefix + "procurement.vendors" + NavPolicies.ActionSeparator + NavPolicies.DeleteAction)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]