feat: Implement new General Ledger frontend section with comprehensive report screens and cash/bank account management
- Added a new Ledgers sidebar section for statutory-format financial reports and cash/bank-account management. - Introduced dedicated GL client for API interactions, handling response envelopes and error management. - Developed report screens for Trial Balance, Balance Sheet, General Ledger, Profit & Loss, Cash Flow, Budget vs Actual, and a new Tax Report. - Implemented CSV download functionality alongside existing PDF downloads for all report screens. - Separated Cash and Bank accounts into distinct tables/endpoints, with updated create forms and unified list view. - Created a new Accounts section for Cheque Management, moving Cash/Bank Accounts from the Ledgers section. - Updated RBAC navigation to include new permissions and sub-navigation items for the added features. - Ensured compliance with GL's updated API contract, including renaming fields and adjusting response shapes. - Addressed various bugs and presentation issues, enhancing user experience across the new module.
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Generic reverse proxy into the external General Ledger service — forwards every
|
||||
/// method/path/query/body under this prefix verbatim via
|
||||
/// <see cref="IGeneralLedgerService"/> and returns GL's response (status, content-type,
|
||||
/// body) unchanged. No endpoint-specific shape lives here; see
|
||||
/// docs/12-GENERAL-LEDGER-INTEGRATION.md for the full GL contract and what this proxy
|
||||
/// does and doesn't do. Gated by the same ERP door policy as every other v1 endpoint
|
||||
/// (<see cref="ApiControllerBase"/>) — the shared GL API key is attached server-side
|
||||
/// only and is never exposed to the frontend.
|
||||
/// </summary>
|
||||
[Route("api/v1/gl")]
|
||||
public sealed class GeneralLedgerController : ApiControllerBase
|
||||
{
|
||||
private readonly IGeneralLedgerService _gl;
|
||||
|
||||
public GeneralLedgerController(IGeneralLedgerService gl) => _gl = gl;
|
||||
|
||||
[HttpGet("{**path}")]
|
||||
public Task<IActionResult> Get(string path, CancellationToken ct) => ForwardAsync(HttpMethod.Get, path, ct);
|
||||
|
||||
[HttpPost("{**path}")]
|
||||
public Task<IActionResult> Post(string path, CancellationToken ct) => ForwardAsync(HttpMethod.Post, path, ct);
|
||||
|
||||
[HttpPut("{**path}")]
|
||||
public Task<IActionResult> Put(string path, CancellationToken ct) => ForwardAsync(HttpMethod.Put, path, ct);
|
||||
|
||||
private async Task<IActionResult> ForwardAsync(HttpMethod method, string path, CancellationToken ct)
|
||||
{
|
||||
var body = method == HttpMethod.Get ? null : Request.Body;
|
||||
var result = await _gl.ForwardAsync(method, path, Request.QueryString.Value, Request.ContentType, body, ct);
|
||||
return new ContentResult
|
||||
{
|
||||
StatusCode = result.StatusCode,
|
||||
Content = result.Body,
|
||||
ContentType = result.ContentType ?? "application/json"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using ERPCore.System.Errors;
|
||||
|
||||
namespace ERPCore.Infra.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP implementation of <see cref="IGeneralLedgerClient"/>. Registered as a typed
|
||||
/// client (`AddHttpClient<IGeneralLedgerClient, GeneralLedgerClient>`) with its
|
||||
/// `BaseAddress` bound from `GeneralLedgerService:BaseUrl`. Every call attaches the
|
||||
/// shared `GeneralLedgerService:ApiKey` as `X-Api-Key` and streams the request/response
|
||||
/// body straight through, unparsed — GL's own response (status, content-type, body) is
|
||||
/// returned exactly as received; nothing here reshapes it.
|
||||
/// </summary>
|
||||
public sealed class GeneralLedgerClient(HttpClient http, IConfiguration configuration) : IGeneralLedgerClient
|
||||
{
|
||||
private readonly HttpClient _http = http;
|
||||
private readonly string _apiKey = configuration["GeneralLedgerService:ApiKey"] ?? string.Empty;
|
||||
|
||||
public async Task<GeneralLedgerResponse> SendAsync(
|
||||
HttpMethod method, string path, string? queryString, string? contentType, Stream? body, CancellationToken ct)
|
||||
{
|
||||
var relativeUri = path.TrimStart('/') + queryString;
|
||||
using var request = new HttpRequestMessage(method, relativeUri);
|
||||
request.Headers.TryAddWithoutValidation("X-Api-Key", _apiKey);
|
||||
|
||||
if (body is not null && method != HttpMethod.Get)
|
||||
{
|
||||
var content = new StreamContent(body);
|
||||
if (!string.IsNullOrEmpty(contentType))
|
||||
content.Headers.TryAddWithoutValidation("Content-Type", contentType);
|
||||
request.Content = content;
|
||||
}
|
||||
|
||||
HttpResponseMessage response;
|
||||
try
|
||||
{
|
||||
response = await _http.SendAsync(request, ct);
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
throw new DomainException(ErrorCodes.GlServiceUnavailable, "The General Ledger service is unreachable.", 503);
|
||||
}
|
||||
catch (TaskCanceledException) when (!ct.IsCancellationRequested)
|
||||
{
|
||||
throw new DomainException(ErrorCodes.GlServiceUnavailable, "The General Ledger service timed out.", 503);
|
||||
}
|
||||
|
||||
var responseBody = await response.Content.ReadAsStringAsync(ct);
|
||||
return new GeneralLedgerResponse
|
||||
{
|
||||
StatusCode = (int)response.StatusCode,
|
||||
ContentType = response.Content.Headers.ContentType?.ToString(),
|
||||
Body = responseBody
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace ERPCore.Infra.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// Raw HTTP result from the external General Ledger service — status code, content
|
||||
/// type, and body exactly as GL returned them. Deliberately un-reshaped: GL's own
|
||||
/// envelope (see the GL service's own API reference) is passed through byte-for-byte
|
||||
/// so its camelCase-success/PascalCase-error inconsistency and full decimal precision
|
||||
/// survive the hop unchanged (docs/12-GENERAL-LEDGER-INTEGRATION.md).
|
||||
/// </summary>
|
||||
public sealed class GeneralLedgerResponse
|
||||
{
|
||||
public int StatusCode { get; init; }
|
||||
public string? ContentType { get; init; }
|
||||
public string Body { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace ERPCore.Infra.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// Typed HTTP transport to the external General Ledger service. Injects the shared
|
||||
/// `X-Api-Key` secret and forwards method/path/query/body/content-type verbatim —
|
||||
/// see docs/12-GENERAL-LEDGER-INTEGRATION.md. Internal: only
|
||||
/// <see cref="ERPCore.Services.Interfaces.IGeneralLedgerService"/> consumes this.
|
||||
/// </summary>
|
||||
public interface IGeneralLedgerClient
|
||||
{
|
||||
Task<GeneralLedgerResponse> SendAsync(
|
||||
HttpMethod method, string path, string? queryString, string? contentType, Stream? body, CancellationToken ct);
|
||||
}
|
||||
@@ -36,7 +36,9 @@ public sealed class NavItemConfiguration : IEntityTypeConfiguration<NavItem>
|
||||
new NavItem { NavItemId = 7, Code = "warehouses", Label = "Warehouses", Href = "/dashboard/warehouse", SortOrder = 7 },
|
||||
new NavItem { NavItemId = 8, Code = "orders", Label = "Orders", Href = "/dashboard/orders", SortOrder = 8 },
|
||||
new NavItem { NavItemId = 9, Code = "settings", Label = "Settings", Href = "/dashboard/settings", SortOrder = 9 },
|
||||
new NavItem { NavItemId = 10, Code = "help", Label = "Help", Href = "/dashboard/help", SortOrder = 10 }
|
||||
new NavItem { NavItemId = 10, Code = "help", Label = "Help", Href = "/dashboard/help", SortOrder = 10 },
|
||||
new NavItem { NavItemId = 11, Code = "ledgers", Label = "Ledgers", Href = "/dashboard/ledgers", SortOrder = 11 },
|
||||
new NavItem { NavItemId = 12, Code = "accounts", Label = "Accounts", Href = "/dashboard/accounts", SortOrder = 12 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,10 +42,26 @@ public sealed class PermissionConfiguration : IEntityTypeConfiguration<Permissio
|
||||
new Permission { PermissionId = 16, Code = "NAV:products.configuration", SubNavItemId = 6 },
|
||||
new Permission { PermissionId = 17, Code = "NAV:settings.roles", SubNavItemId = 7 },
|
||||
new Permission { PermissionId = 18, Code = "NAV:settings.users", SubNavItemId = 8 },
|
||||
new Permission { PermissionId = 19, Code = "NAV:procurement.requisitions", SubNavItemId = 9 },
|
||||
new Permission { PermissionId = 20, Code = "NAV:procurement.rfqs", SubNavItemId = 10 },
|
||||
new Permission { PermissionId = 21, Code = "NAV:procurement.purchase-orders", SubNavItemId = 11 },
|
||||
new Permission { PermissionId = 22, Code = "NAV:procurement.purchase-returns", SubNavItemId = 12 }
|
||||
// IDs 28-31 (not 19-22): 19-22 were already claimed by the Ledgers permissions below;
|
||||
// these procurement rows were never actually migrated into the database before now.
|
||||
new Permission { PermissionId = 28, Code = "NAV:procurement.requisitions", SubNavItemId = 17 },
|
||||
new Permission { PermissionId = 29, Code = "NAV:procurement.rfqs", SubNavItemId = 18 },
|
||||
new Permission { PermissionId = 30, Code = "NAV:procurement.purchase-orders", SubNavItemId = 19 },
|
||||
new Permission { PermissionId = 31, Code = "NAV:procurement.purchase-returns", SubNavItemId = 20 },
|
||||
new Permission { PermissionId = 19, Code = "NAV:ledgers", NavItemId = 11 },
|
||||
new Permission { PermissionId = 20, Code = "NAV:ledgers.trial-balance", SubNavItemId = 9 },
|
||||
new Permission { PermissionId = 21, Code = "NAV:ledgers.balance-sheet", SubNavItemId = 10 },
|
||||
new Permission { PermissionId = 22, Code = "NAV:ledgers.general-ledger", SubNavItemId = 11 },
|
||||
new Permission { PermissionId = 23, Code = "NAV:ledgers.profit-and-loss", SubNavItemId = 12 },
|
||||
new Permission { PermissionId = 24, Code = "NAV:ledgers.cash-flow", SubNavItemId = 13 },
|
||||
new Permission { PermissionId = 25, Code = "NAV:ledgers.budget-vs-actual", SubNavItemId = 14 },
|
||||
new Permission { PermissionId = 27, Code = "NAV:ledgers.tax-report", SubNavItemId = 16 },
|
||||
// Moved under the new Accounts nav item (2026-07-31) — same PermissionId (26), just a
|
||||
// renamed Code, so any role already granted this permission keeps it.
|
||||
new Permission { PermissionId = 26, Code = "NAV:accounts.bank-accounts", SubNavItemId = 15 },
|
||||
new Permission { PermissionId = 32, Code = "NAV:accounts", NavItemId = 12 },
|
||||
new Permission { PermissionId = 33, Code = "NAV:accounts.cheque-books", SubNavItemId = 21 },
|
||||
new Permission { PermissionId = 34, Code = "NAV:accounts.received-cheques", SubNavItemId = 22 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,10 +34,25 @@ public sealed class SubNavItemConfiguration : IEntityTypeConfiguration<SubNavIte
|
||||
new SubNavItem { SubNavItemId = 7, NavItemId = 9, Code = "settings.roles", Label = "Roles", Href = "/dashboard/settings/roles", SortOrder = 1 },
|
||||
new SubNavItem { SubNavItemId = 8, NavItemId = 9, Code = "settings.users", Label = "Users", Href = "/dashboard/settings/users", SortOrder = 2 },
|
||||
// Procurement (NavItemId 4) children — mirror the hub page order.
|
||||
new SubNavItem { SubNavItemId = 9, NavItemId = 4, Code = "procurement.requisitions", Label = "Requisitions", Href = "/dashboard/procurement/requisitions", SortOrder = 1 },
|
||||
new SubNavItem { SubNavItemId = 10, NavItemId = 4, Code = "procurement.rfqs", Label = "RFQs", Href = "/dashboard/procurement/rfqs", SortOrder = 2 },
|
||||
new SubNavItem { SubNavItemId = 11, NavItemId = 4, Code = "procurement.purchase-orders", Label = "Purchase Orders", Href = "/dashboard/procurement/purchase-orders", SortOrder = 3 },
|
||||
new SubNavItem { SubNavItemId = 12, NavItemId = 4, Code = "procurement.purchase-returns", Label = "Purchase Returns", Href = "/dashboard/procurement/purchase-returns", SortOrder = 4 }
|
||||
// IDs 17-20 (not 9-12): 9-12 were already claimed by the Ledgers sub-items below;
|
||||
// these procurement rows were never actually migrated into the database before now.
|
||||
new SubNavItem { SubNavItemId = 17, NavItemId = 4, Code = "procurement.requisitions", Label = "Requisitions", Href = "/dashboard/procurement/requisitions", SortOrder = 1 },
|
||||
new SubNavItem { SubNavItemId = 18, NavItemId = 4, Code = "procurement.rfqs", Label = "RFQs", Href = "/dashboard/procurement/rfqs", SortOrder = 2 },
|
||||
new SubNavItem { SubNavItemId = 19, NavItemId = 4, Code = "procurement.purchase-orders", Label = "Purchase Orders", Href = "/dashboard/procurement/purchase-orders", SortOrder = 3 },
|
||||
new SubNavItem { SubNavItemId = 20, NavItemId = 4, Code = "procurement.purchase-returns", Label = "Purchase Returns", Href = "/dashboard/procurement/purchase-returns", SortOrder = 4 },
|
||||
new SubNavItem { SubNavItemId = 9, NavItemId = 11, Code = "ledgers.trial-balance", Label = "Trial Balance", Href = "/dashboard/ledgers/trial-balance", SortOrder = 1 },
|
||||
new SubNavItem { SubNavItemId = 10, NavItemId = 11, Code = "ledgers.balance-sheet", Label = "Balance Sheet", Href = "/dashboard/ledgers/balance-sheet", SortOrder = 2 },
|
||||
new SubNavItem { SubNavItemId = 11, NavItemId = 11, Code = "ledgers.general-ledger", Label = "General Ledger", Href = "/dashboard/ledgers/general-ledger", SortOrder = 3 },
|
||||
new SubNavItem { SubNavItemId = 12, NavItemId = 11, Code = "ledgers.profit-and-loss", Label = "Profit & Loss", Href = "/dashboard/ledgers/profit-and-loss", SortOrder = 4 },
|
||||
new SubNavItem { SubNavItemId = 13, NavItemId = 11, Code = "ledgers.cash-flow", Label = "Cash Flow", Href = "/dashboard/ledgers/cash-flow", SortOrder = 5 },
|
||||
new SubNavItem { SubNavItemId = 14, NavItemId = 11, Code = "ledgers.budget-vs-actual", Label = "Budget vs Actual", Href = "/dashboard/ledgers/budget-vs-actual", SortOrder = 6 },
|
||||
new SubNavItem { SubNavItemId = 16, NavItemId = 11, Code = "ledgers.tax-report", Label = "Tax Report", Href = "/dashboard/ledgers/tax-report", SortOrder = 7 },
|
||||
// Moved under the new Accounts nav item (2026-07-31) — kept the same SubNavItemId (15)
|
||||
// rather than delete+recreate, so any role that already had this permission granted
|
||||
// doesn't silently lose it just because the section it lives under changed.
|
||||
new SubNavItem { SubNavItemId = 15, NavItemId = 12, Code = "accounts.bank-accounts", Label = "Cash / Bank Accounts", Href = "/dashboard/accounts/bank-accounts", SortOrder = 1 },
|
||||
new SubNavItem { SubNavItemId = 21, NavItemId = 12, Code = "accounts.cheque-books", Label = "Cheque Books", Href = "/dashboard/accounts/cheque-books", SortOrder = 2 },
|
||||
new SubNavItem { SubNavItemId = 22, NavItemId = 12, Code = "accounts.received-cheques", Label = "Received Cheques", Href = "/dashboard/accounts/received-cheques", SortOrder = 3 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1832,6 +1832,24 @@ namespace ERPCore.Infra.Persistence.Migrations
|
||||
Label = "Help",
|
||||
SortOrder = 10,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
NavItemId = 11,
|
||||
Code = "ledgers",
|
||||
Href = "/dashboard/ledgers",
|
||||
Label = "Ledgers",
|
||||
SortOrder = 11,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
NavItemId = 12,
|
||||
Code = "accounts",
|
||||
Href = "/dashboard/accounts",
|
||||
Label = "Accounts",
|
||||
SortOrder = 12,
|
||||
Status = "Active"
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2285,27 +2303,99 @@ namespace ERPCore.Infra.Persistence.Migrations
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 19,
|
||||
PermissionId = 28,
|
||||
Code = "NAV:procurement.requisitions",
|
||||
SubNavItemId = 9
|
||||
SubNavItemId = 17
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 29,
|
||||
Code = "NAV:procurement.rfqs",
|
||||
SubNavItemId = 18
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 30,
|
||||
Code = "NAV:procurement.purchase-orders",
|
||||
SubNavItemId = 19
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 31,
|
||||
Code = "NAV:procurement.purchase-returns",
|
||||
SubNavItemId = 20
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 19,
|
||||
Code = "NAV:ledgers",
|
||||
NavItemId = 11
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 20,
|
||||
Code = "NAV:procurement.rfqs",
|
||||
SubNavItemId = 10
|
||||
Code = "NAV:ledgers.trial-balance",
|
||||
SubNavItemId = 9
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 21,
|
||||
Code = "NAV:procurement.purchase-orders",
|
||||
SubNavItemId = 11
|
||||
Code = "NAV:ledgers.balance-sheet",
|
||||
SubNavItemId = 10
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 22,
|
||||
Code = "NAV:procurement.purchase-returns",
|
||||
Code = "NAV:ledgers.general-ledger",
|
||||
SubNavItemId = 11
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 23,
|
||||
Code = "NAV:ledgers.profit-and-loss",
|
||||
SubNavItemId = 12
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 24,
|
||||
Code = "NAV:ledgers.cash-flow",
|
||||
SubNavItemId = 13
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 25,
|
||||
Code = "NAV:ledgers.budget-vs-actual",
|
||||
SubNavItemId = 14
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 27,
|
||||
Code = "NAV:ledgers.tax-report",
|
||||
SubNavItemId = 16
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 26,
|
||||
Code = "NAV:accounts.bank-accounts",
|
||||
SubNavItemId = 15
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 32,
|
||||
Code = "NAV:accounts",
|
||||
NavItemId = 12
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 33,
|
||||
Code = "NAV:accounts.cheque-books",
|
||||
SubNavItemId = 21
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 34,
|
||||
Code = "NAV:accounts.received-cheques",
|
||||
SubNavItemId = 22
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3948,7 +4038,7 @@ namespace ERPCore.Infra.Persistence.Migrations
|
||||
},
|
||||
new
|
||||
{
|
||||
SubNavItemId = 9,
|
||||
SubNavItemId = 17,
|
||||
Code = "procurement.requisitions",
|
||||
Href = "/dashboard/procurement/requisitions",
|
||||
Label = "Requisitions",
|
||||
@@ -3958,7 +4048,7 @@ namespace ERPCore.Infra.Persistence.Migrations
|
||||
},
|
||||
new
|
||||
{
|
||||
SubNavItemId = 10,
|
||||
SubNavItemId = 18,
|
||||
Code = "procurement.rfqs",
|
||||
Href = "/dashboard/procurement/rfqs",
|
||||
Label = "RFQs",
|
||||
@@ -3968,7 +4058,7 @@ namespace ERPCore.Infra.Persistence.Migrations
|
||||
},
|
||||
new
|
||||
{
|
||||
SubNavItemId = 11,
|
||||
SubNavItemId = 19,
|
||||
Code = "procurement.purchase-orders",
|
||||
Href = "/dashboard/procurement/purchase-orders",
|
||||
Label = "Purchase Orders",
|
||||
@@ -3978,13 +4068,113 @@ namespace ERPCore.Infra.Persistence.Migrations
|
||||
},
|
||||
new
|
||||
{
|
||||
SubNavItemId = 12,
|
||||
SubNavItemId = 20,
|
||||
Code = "procurement.purchase-returns",
|
||||
Href = "/dashboard/procurement/purchase-returns",
|
||||
Label = "Purchase Returns",
|
||||
NavItemId = 4,
|
||||
SortOrder = 4,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
SubNavItemId = 9,
|
||||
Code = "ledgers.trial-balance",
|
||||
Href = "/dashboard/ledgers/trial-balance",
|
||||
Label = "Trial Balance",
|
||||
NavItemId = 11,
|
||||
SortOrder = 1,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
SubNavItemId = 10,
|
||||
Code = "ledgers.balance-sheet",
|
||||
Href = "/dashboard/ledgers/balance-sheet",
|
||||
Label = "Balance Sheet",
|
||||
NavItemId = 11,
|
||||
SortOrder = 2,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
SubNavItemId = 11,
|
||||
Code = "ledgers.general-ledger",
|
||||
Href = "/dashboard/ledgers/general-ledger",
|
||||
Label = "General Ledger",
|
||||
NavItemId = 11,
|
||||
SortOrder = 3,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
SubNavItemId = 12,
|
||||
Code = "ledgers.profit-and-loss",
|
||||
Href = "/dashboard/ledgers/profit-and-loss",
|
||||
Label = "Profit & Loss",
|
||||
NavItemId = 11,
|
||||
SortOrder = 4,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
SubNavItemId = 13,
|
||||
Code = "ledgers.cash-flow",
|
||||
Href = "/dashboard/ledgers/cash-flow",
|
||||
Label = "Cash Flow",
|
||||
NavItemId = 11,
|
||||
SortOrder = 5,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
SubNavItemId = 14,
|
||||
Code = "ledgers.budget-vs-actual",
|
||||
Href = "/dashboard/ledgers/budget-vs-actual",
|
||||
Label = "Budget vs Actual",
|
||||
NavItemId = 11,
|
||||
SortOrder = 6,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
SubNavItemId = 16,
|
||||
Code = "ledgers.tax-report",
|
||||
Href = "/dashboard/ledgers/tax-report",
|
||||
Label = "Tax Report",
|
||||
NavItemId = 11,
|
||||
SortOrder = 7,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
SubNavItemId = 15,
|
||||
Code = "accounts.bank-accounts",
|
||||
Href = "/dashboard/accounts/bank-accounts",
|
||||
Label = "Cash / Bank Accounts",
|
||||
NavItemId = 12,
|
||||
SortOrder = 1,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
SubNavItemId = 21,
|
||||
Code = "accounts.cheque-books",
|
||||
Href = "/dashboard/accounts/cheque-books",
|
||||
Label = "Cheque Books",
|
||||
NavItemId = 12,
|
||||
SortOrder = 2,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
SubNavItemId = 22,
|
||||
Code = "accounts.received-cheques",
|
||||
Href = "/dashboard/accounts/received-cheques",
|
||||
Label = "Received Cheques",
|
||||
NavItemId = 12,
|
||||
SortOrder = 3,
|
||||
Status = "Active"
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Infra.Auth.AuthHex;
|
||||
using ERPCore.Infra.Gl;
|
||||
using ERPCore.Infra.Persistence;
|
||||
using ERPCore.Infra.Storage;
|
||||
using ERPCore.Infra.UoW;
|
||||
@@ -51,6 +52,15 @@ builder.Services.AddScoped<IAuthUserService, AuthUserService>();
|
||||
builder.Services.AddScoped<IAuthRecoveryService, AuthRecoveryService>();
|
||||
builder.Services.AddScoped<IAuthAltService, AuthAltService>();
|
||||
|
||||
// General Ledger service proxy → external GL microservice (docs/12-GENERAL-LEDGER-INTEGRATION.md)
|
||||
builder.Services.AddHttpClient<IGeneralLedgerClient, GeneralLedgerClient>(c =>
|
||||
{
|
||||
var baseUrl = builder.Configuration["GeneralLedgerService:BaseUrl"]
|
||||
?? throw new InvalidOperationException("GeneralLedgerService:BaseUrl is not configured.");
|
||||
c.BaseAddress = new Uri(baseUrl);
|
||||
});
|
||||
builder.Services.AddScoped<IGeneralLedgerService, GeneralLedgerService>();
|
||||
|
||||
// Current-user (audit actor). AuthHex has no sub/nameid → a claims transformation
|
||||
// JIT-provisions a local shadow user and injects the local `int` id as `nameid`.
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using ERPCore.Infra.Gl;
|
||||
using ERPCore.Services.Interfaces;
|
||||
|
||||
namespace ERPCore.Services;
|
||||
|
||||
/// <inheritdoc cref="IGeneralLedgerService"/>
|
||||
public sealed class GeneralLedgerService : IGeneralLedgerService
|
||||
{
|
||||
private readonly IGeneralLedgerClient _client;
|
||||
|
||||
public GeneralLedgerService(IGeneralLedgerClient client) => _client = client;
|
||||
|
||||
public Task<GeneralLedgerResponse> ForwardAsync(
|
||||
HttpMethod method, string path, string? queryString, string? contentType, Stream? body, CancellationToken ct)
|
||||
=> _client.SendAsync(method, path, queryString, contentType, body, ct);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using ERPCore.Infra.Gl;
|
||||
|
||||
namespace ERPCore.Services.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Single entry point into the external General Ledger service — the one function
|
||||
/// used both by <see cref="ERPCore.Controllers.GeneralLedgerController"/> (frontend
|
||||
/// requests, forwarded verbatim) and, once wired, other ERPCore services that need to
|
||||
/// post directly to GL (e.g. GRN confirm, adjustments — docs/12-GENERAL-LEDGER-INTEGRATION.md).
|
||||
/// No business logic lives here yet; this pass only connects the transport.
|
||||
/// </summary>
|
||||
public interface IGeneralLedgerService
|
||||
{
|
||||
Task<GeneralLedgerResponse> ForwardAsync(
|
||||
HttpMethod method, string path, string? queryString, string? contentType, Stream? body, CancellationToken ct);
|
||||
}
|
||||
@@ -66,4 +66,7 @@ public static class ErrorCodes
|
||||
public const string LeftoverExceedsConsumed = "LEFTOVER_EXCEEDS_CONSUMED";
|
||||
public const string RunCostClosed = "RUN_COST_CLOSED";
|
||||
public const string RunNotCancellable = "RUN_NOT_CANCELLABLE";
|
||||
|
||||
// General Ledger service proxy (GeneralLedgerController → external GL service, docs/12)
|
||||
public const string GlServiceUnavailable = "GL_SERVICE_UNAVAILABLE";
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=ERPCore;Username=postgres;Password=root"
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=ERPCore;Username=postgres;Password=dbuser"
|
||||
},
|
||||
"AuthHex": {
|
||||
"BaseUrl": "http://localhost:5011"
|
||||
|
||||
@@ -22,5 +22,9 @@
|
||||
"RootPath": "App_Data/hr-documents",
|
||||
"MaxSizeBytes": 10485760
|
||||
},
|
||||
"GeneralLedgerService": {
|
||||
"BaseUrl": "https://localhost:7024/api/v1/",
|
||||
"ApiKey": "1A5FE0E389C029B1FBCD2A94650CEBF0656083BF06FF48E5E11987040ABA262D"
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user