feat: add General Ledger integration and financial reporting features
- Implemented General Ledger client component for managing GL integration. - Created a dedicated page for General Ledger configuration. - Developed Finance Report client component for generating various financial reports. - Added report blocks for structured display of financial data. - Introduced API endpoints for fetching GL accounts and financial reports. - Updated navigation to include links for GL integration and financial reports. - Enhanced permissions to manage GL and view financial reports. - Updated appsettings to include General Ledger API configuration.
This commit is contained in:
@@ -66,6 +66,7 @@ public class AppDBContext(DbContextOptions<AppDBContext> options) : DbContext(op
|
||||
public DbSet<RouteVisitLog> RouteVisitLogs { get; set; }
|
||||
public DbSet<SysConfig> SysConfigs { get; set; }
|
||||
public DbSet<BankAccount> BankAccounts { get; set; }
|
||||
public DbSet<GlPostingLog> GlPostingLogs { get; set; }
|
||||
public DbSet<ChequeBook> ChequeBooks { get; set; }
|
||||
public DbSet<ChequePage> ChequePages { get; set; }
|
||||
public DbSet<ReceivedCheque> ReceivedCheques { get; set; }
|
||||
@@ -582,6 +583,16 @@ public class AppDBContext(DbContextOptions<AppDBContext> options) : DbContext(op
|
||||
.HasIndex(r => r.FK_RegionId)
|
||||
.IsUnique();
|
||||
|
||||
// --- GL posting log ---
|
||||
|
||||
modelBuilder.Entity<GlPostingLog>()
|
||||
.HasIndex(l => new { l.Event, l.SourceId })
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<GlPostingLog>()
|
||||
.Property(l => l.Event)
|
||||
.HasMaxLength(60);
|
||||
|
||||
// --- SysConfig ---
|
||||
|
||||
modelBuilder.Entity<SysConfig>()
|
||||
|
||||
@@ -68,6 +68,8 @@ public static class RoleAndPermissionSeed
|
||||
private static readonly Guid PermSupplierClaimManage= new("20000000-0000-0000-0000-000000000048");
|
||||
private static readonly Guid PermSysConfigView = new("20000000-0000-0000-0000-000000000049");
|
||||
private static readonly Guid PermSysConfigManage = new("20000000-0000-0000-0000-000000000050");
|
||||
private static readonly Guid PermFinanceReportView = new("20000000-0000-0000-0000-000000000051");
|
||||
private static readonly Guid PermGlManage = new("20000000-0000-0000-0000-000000000052");
|
||||
|
||||
public static void Apply(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -134,7 +136,9 @@ public static class RoleAndPermissionSeed
|
||||
new Permission { Id = PermSupplierClaimView, Code = "supplier-claim.view", Description = "View supplier claims", CreatedAt = SeedDate, IsActive = true },
|
||||
new Permission { Id = PermSupplierClaimManage,Code = "supplier-claim.manage", Description = "Create and resolve supplier claims", CreatedAt = SeedDate, IsActive = true },
|
||||
new Permission { Id = PermSysConfigView, Code = "sys-config.view", Description = "View system configuration settings", CreatedAt = SeedDate, IsActive = true },
|
||||
new Permission { Id = PermSysConfigManage, Code = "sys-config.manage", Description = "Update system configuration settings", CreatedAt = SeedDate, IsActive = true }
|
||||
new Permission { Id = PermSysConfigManage, Code = "sys-config.manage", Description = "Update system configuration settings", CreatedAt = SeedDate, IsActive = true },
|
||||
new Permission { Id = PermFinanceReportView, Code = "finance-reports.view", Description = "View financial reports (trial balance, P&L, balance sheet, cash flow...)", CreatedAt = SeedDate, IsActive = true },
|
||||
new Permission { Id = PermGlManage, Code = "gl.manage", Description = "Set up the General Ledger link and retry failed postings", CreatedAt = SeedDate, IsActive = true }
|
||||
);
|
||||
|
||||
// SuperAdmin gets every permission
|
||||
@@ -191,7 +195,9 @@ public static class RoleAndPermissionSeed
|
||||
new RolePermission { FK_RoleId = SuperAdminRoleId, FK_PermissionId = PermSupplierClaimView, CreatedAt = SeedDate, IsActive = true },
|
||||
new RolePermission { FK_RoleId = SuperAdminRoleId, FK_PermissionId = PermSupplierClaimManage,CreatedAt = SeedDate, IsActive = true },
|
||||
new RolePermission { FK_RoleId = SuperAdminRoleId, FK_PermissionId = PermSysConfigView, CreatedAt = SeedDate, IsActive = true },
|
||||
new RolePermission { FK_RoleId = SuperAdminRoleId, FK_PermissionId = PermSysConfigManage, CreatedAt = SeedDate, IsActive = true }
|
||||
new RolePermission { FK_RoleId = SuperAdminRoleId, FK_PermissionId = PermSysConfigManage, CreatedAt = SeedDate, IsActive = true },
|
||||
new RolePermission { FK_RoleId = SuperAdminRoleId, FK_PermissionId = PermFinanceReportView, CreatedAt = SeedDate, IsActive = true },
|
||||
new RolePermission { FK_RoleId = SuperAdminRoleId, FK_PermissionId = PermGlManage, CreatedAt = SeedDate, IsActive = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Distribution.Features.BankAccount;
|
||||
using Distribution.Features.GeneralLedger;
|
||||
using Distribution.Infra.UoW;
|
||||
using Distribution.Models;
|
||||
|
||||
@@ -7,7 +8,8 @@ namespace Distribution.Features.ChequeBook;
|
||||
public class ChequeBookService(
|
||||
IChequeBookRepository repo,
|
||||
IBankAccountRepository bankAccountRepo,
|
||||
IUnitOfWork uow)
|
||||
IUnitOfWork uow,
|
||||
GlPostingService gl)
|
||||
{
|
||||
private const string DefaultCurrency = "LKR";
|
||||
|
||||
@@ -171,6 +173,7 @@ public class ChequeBookService(
|
||||
|
||||
repo.UpdatePage(page);
|
||||
await uow.CommitAsync(ct);
|
||||
await gl.PostIssuedChequeAsync(page, request.Action, ct);
|
||||
return ToPageResponse(page, page.ChequeBook!.ChequeBookNo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Distribution.Features.GeneralLedger;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Distribution.Features.FinanceReport;
|
||||
|
||||
/// <summary>
|
||||
/// Proxies the General Ledger service's reports (TrialBalance, GeneralLedger, BalanceSheet, ProfitAndLoss, CashFlow,
|
||||
/// BudgetVsActual, TaxSummary). Query parameters are forwarded as-is; Json is returned as the GL data payload and
|
||||
/// Pdf/Csv are returned as downloadable files.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/reports/finance")]
|
||||
public class FinanceReportController(IGeneralLedgerClient gl) : ControllerBase
|
||||
{
|
||||
/// <summary>Active GL accounts (code + name) for the report filters' account dropdown.</summary>
|
||||
[HttpGet("accounts")]
|
||||
[Authorize("finance-reports.view")]
|
||||
public async Task<IActionResult> Accounts(CancellationToken ct)
|
||||
{
|
||||
GeneralLedgerResult result;
|
||||
try { result = await gl.SendAsync(HttpMethod.Get, "v1/accounts?isActive=true", null, ct); }
|
||||
catch (HttpRequestException) { return StatusCode(502, new { error = "The General Ledger service is unreachable." }); }
|
||||
|
||||
if (!result.Success || GlJson.Data(result) is not { } data)
|
||||
return StatusCode(502, new { error = GlJson.Message(result) ?? "Could not load GL accounts." });
|
||||
|
||||
var items = GlJson.Get(data, "items");
|
||||
var accounts = items is { ValueKind: JsonValueKind.Array }
|
||||
? items.Value.EnumerateArray()
|
||||
.Select(a => new { accountCode = GlJson.GetString(a, "accountCode") ?? "", accountName = GlJson.GetString(a, "accountName") ?? "" })
|
||||
.OrderBy(a => a.accountCode, StringComparer.Ordinal)
|
||||
.ToList()
|
||||
: [];
|
||||
return Ok(accounts);
|
||||
}
|
||||
|
||||
[HttpGet("{reportType}")]
|
||||
[Authorize("finance-reports.view")]
|
||||
public async Task<IActionResult> Get(string reportType, [FromQuery] string outputFormat = "Json", CancellationToken ct = default)
|
||||
{
|
||||
var query = new StringBuilder($"v1/reports?reportType={Uri.EscapeDataString(reportType)}&outputFormat={Uri.EscapeDataString(outputFormat)}");
|
||||
foreach (var (key, values) in Request.Query)
|
||||
{
|
||||
if (key is "reportType" or "outputFormat") continue;
|
||||
foreach (var v in values)
|
||||
query.Append('&').Append(Uri.EscapeDataString(key)).Append('=').Append(Uri.EscapeDataString(v ?? ""));
|
||||
}
|
||||
|
||||
GeneralLedgerResult result;
|
||||
try { result = await gl.SendAsync(HttpMethod.Get, query.ToString(), null, ct); }
|
||||
catch (HttpRequestException) { return StatusCode(502, new { error = "The General Ledger service is unreachable." }); }
|
||||
|
||||
if (!result.Success)
|
||||
return StatusCode(result.StatusCode is >= 400 and < 500 ? result.StatusCode : 502,
|
||||
new { error = GlJson.Message(result) ?? "The General Ledger service rejected the request." });
|
||||
|
||||
var data = GlJson.Data(result);
|
||||
if (data is null) return StatusCode(502, new { error = "The General Ledger service returned no data." });
|
||||
|
||||
if (!outputFormat.Equals("Json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var base64 = GlJson.GetString(data.Value, "contentBase64");
|
||||
if (base64 is null) return StatusCode(502, new { error = "The General Ledger service returned no file." });
|
||||
return File(Convert.FromBase64String(base64),
|
||||
GlJson.GetString(data.Value, "contentType") ?? "application/octet-stream",
|
||||
GlJson.GetString(data.Value, "fileName"));
|
||||
}
|
||||
|
||||
return Content(data.Value.GetRawText(), "application/json");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Distribution.Features.GeneralLedger;
|
||||
|
||||
public record GeneralLedgerResult(int StatusCode, bool Success, JsonElement? Body);
|
||||
|
||||
public interface IGeneralLedgerClient
|
||||
{
|
||||
Task<bool> IsHealthyAsync(CancellationToken ct = default);
|
||||
Task<GeneralLedgerResult> SendAsync(HttpMethod method, string path, object? payload = null, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service-to-service client for the General Ledger microservice. The GL service is not user-facing:
|
||||
/// every request carries the shared X-Api-Key (GeneralLedger:ApiKey, must equal the GL service's ApiKey:Value).
|
||||
/// <paramref name="path"/> is relative to GeneralLedger:BaseUrl and carries the API version (e.g. "v1/accounts", "v1/journal-entries"), so moving a call to v2 is a one-path change.
|
||||
/// </summary>
|
||||
public class GeneralLedgerClient(HttpClient http) : IGeneralLedgerClient
|
||||
{
|
||||
public async Task<bool> IsHealthyAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var response = await http.GetAsync("/health", ct);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<GeneralLedgerResult> SendAsync(HttpMethod method, string path, object? payload = null, CancellationToken ct = default)
|
||||
{
|
||||
using var request = new HttpRequestMessage(method, path.TrimStart('/'));
|
||||
if (payload is not null) request.Content = JsonContent.Create(payload);
|
||||
|
||||
using var response = await http.SendAsync(request, ct);
|
||||
JsonElement? body = null;
|
||||
if (response.Content.Headers.ContentLength != 0)
|
||||
{
|
||||
try { body = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: ct); }
|
||||
catch (JsonException) { }
|
||||
}
|
||||
return new GeneralLedgerResult((int)response.StatusCode, response.IsSuccessStatusCode, body);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using Distribution.Data;
|
||||
using Distribution.Infra.Authorization;
|
||||
using Distribution.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.GeneralLedger;
|
||||
|
||||
public record GlSetupRequest(int? Year);
|
||||
|
||||
public record YearEndCloseRequest(int Year);
|
||||
|
||||
public record OpeningBalanceRequest(DateOnly Date, string? Narration, List<OpeningBalanceLine> Lines);
|
||||
|
||||
[ApiController]
|
||||
[Route("api/gl")]
|
||||
public class GlController(
|
||||
GlSetupService setup,
|
||||
GlPostingService posting,
|
||||
IGeneralLedgerClient gl,
|
||||
Distribution.Features.SysConfig.ISysConfigRepository configRepo,
|
||||
AppDBContext context,
|
||||
ICurrentUserService currentUser) : ControllerBase
|
||||
{
|
||||
[HttpGet("status")]
|
||||
[Authorize("gl.manage")]
|
||||
public async Task<IActionResult> Status()
|
||||
{
|
||||
var mapping = new Dictionary<string, string?>();
|
||||
foreach (var key in GlKeys.AllAccountKeys)
|
||||
mapping[key] = (await configRepo.GetByKeyAsync(key))?.Value;
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
glReachable = await gl.IsHealthyAsync(),
|
||||
setupDone = (await configRepo.GetByKeyAsync(GlKeys.SetupDone))?.Value == "true",
|
||||
openingBalancePosted = await context.GlPostingLogs.AnyAsync(l => l.Event == "OpeningBalance" && l.Status == GlPostingStatus.Posted),
|
||||
closedYears = await posting.GetClosedYearsAsync(),
|
||||
mapping,
|
||||
posted = await context.GlPostingLogs.CountAsync(l => l.Status == GlPostingStatus.Posted),
|
||||
failed = await context.GlPostingLogs.CountAsync(l => l.Status != GlPostingStatus.Posted)
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("setup")]
|
||||
[Authorize("gl.manage")]
|
||||
public async Task<IActionResult> Setup(GlSetupRequest request, CancellationToken ct)
|
||||
{
|
||||
try { return Ok(await setup.RunAsync(request.Year, currentUser.UserId, ct)); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPost("opening-balances")]
|
||||
[Authorize("gl.manage")]
|
||||
public async Task<IActionResult> OpeningBalances(OpeningBalanceRequest request, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var row = await posting.PostOpeningBalanceAsync(request.Date, request.Narration, request.Lines, ct);
|
||||
return row.Status == GlPostingStatus.Posted
|
||||
? Ok(new { status = row.Status, glJournalNo = row.GlJournalNo })
|
||||
: StatusCode(502, new { error = row.Error ?? "The General Ledger service did not accept the entry. It was saved and can be retried." });
|
||||
}
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpGet("year-end/preview")]
|
||||
[Authorize("gl.manage")]
|
||||
public async Task<IActionResult> YearEndPreview([FromQuery] int year, CancellationToken ct)
|
||||
{
|
||||
try { return Ok(await posting.GetYearEndPreviewAsync(year, ct)); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPost("year-end/close")]
|
||||
[Authorize("gl.manage")]
|
||||
public async Task<IActionResult> YearEndClose(YearEndCloseRequest request, CancellationToken ct)
|
||||
{
|
||||
try { return Ok(await posting.CloseYearAsync(request.Year, ct)); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpGet("postings")]
|
||||
[Authorize("gl.manage")]
|
||||
public async Task<IActionResult> Postings([FromQuery] GlPostingStatus? status, [FromQuery] int take = 100)
|
||||
{
|
||||
var query = context.GlPostingLogs.AsQueryable();
|
||||
if (status is not null) query = query.Where(l => l.Status == status);
|
||||
var rows = await query.OrderByDescending(l => l.CreatedAt).Take(Math.Clamp(take, 1, 500))
|
||||
.Select(l => new { l.Id, l.Event, l.SourceId, l.Reference, l.Status, l.GlJournalNo, l.Error, l.Attempts, l.LastAttemptAt, l.CreatedAt })
|
||||
.ToListAsync();
|
||||
return Ok(rows);
|
||||
}
|
||||
|
||||
[HttpPost("postings/retry")]
|
||||
[Authorize("gl.manage")]
|
||||
public async Task<IActionResult> Retry(CancellationToken ct) =>
|
||||
Ok(new { posted = await posting.RetryFailedAsync(ct) });
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Distribution.Features.GeneralLedger;
|
||||
|
||||
/// <summary>Small readers for the GL service's ApiResponse envelope ({statusCode, success, message, data}).</summary>
|
||||
public static class GlJson
|
||||
{
|
||||
public static JsonElement? Data(GeneralLedgerResult result) =>
|
||||
result.Body is { ValueKind: JsonValueKind.Object } body && Get(body, "data") is { } data ? data : null;
|
||||
|
||||
public static string? Message(GeneralLedgerResult result) =>
|
||||
result.Body is { ValueKind: JsonValueKind.Object } body ? Get(body, "message")?.GetString()
|
||||
?? Get(body, "Message")?.GetString() : null;
|
||||
|
||||
/// <summary>Case-insensitive property lookup (GL success bodies are camelCase, its error bodies PascalCase).</summary>
|
||||
public static JsonElement? Get(JsonElement element, string name)
|
||||
{
|
||||
if (element.ValueKind != JsonValueKind.Object) return null;
|
||||
foreach (var p in element.EnumerateObject())
|
||||
if (string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase)) return p.Value;
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string? GetString(JsonElement element, string name)
|
||||
{
|
||||
var v = Get(element, name);
|
||||
return v is null || v.Value.ValueKind == JsonValueKind.Null ? null
|
||||
: v.Value.ValueKind == JsonValueKind.String ? v.Value.GetString() : v.Value.ToString();
|
||||
}
|
||||
|
||||
public static long? GetLong(JsonElement element, string name) =>
|
||||
long.TryParse(GetString(element, name), out var n) ? n : null;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
namespace Distribution.Features.GeneralLedger;
|
||||
|
||||
/// <summary>SysConfig keys holding the GL account codes Distribution posts to (created by GlSetupService).</summary>
|
||||
public static class GlKeys
|
||||
{
|
||||
public const string SetupDone = "Gl.Setup.Done";
|
||||
|
||||
public const string CashEquivalents = "Gl.Account.CashEquivalents"; // parent of Cash in Hand + Bank Accounts (GL cash root)
|
||||
public const string CashInHand = "Gl.Account.CashInHand"; // parent of every Distribution Cash account
|
||||
public const string BankAccounts = "Gl.Account.BankAccounts"; // parent of every Distribution Bank account
|
||||
public const string ChequesInHand = "Gl.Account.ChequesInHand";
|
||||
public const string Receivable = "Gl.Account.Receivable";
|
||||
public const string Inventory = "Gl.Account.Inventory";
|
||||
public const string InputVat = "Gl.Account.InputVat";
|
||||
public const string InputNbt = "Gl.Account.InputNbt";
|
||||
public const string Payable = "Gl.Account.Payable";
|
||||
public const string ChequesIssued = "Gl.Account.ChequesIssued";
|
||||
public const string Equity = "Gl.Account.Equity"; // header; parent of the equity accounts below
|
||||
public const string ShareCapital = "Gl.Account.ShareCapital";
|
||||
public const string RetainedEarnings = "Gl.Account.RetainedEarnings";
|
||||
public const string Drawings = "Gl.Account.Drawings";
|
||||
public const string GeneralReserve = "Gl.Account.GeneralReserve";
|
||||
public const string RevaluationReserve = "Gl.Account.RevaluationReserve";
|
||||
public const string Sales = "Gl.Account.Sales";
|
||||
|
||||
public static readonly string[] AllAccountKeys =
|
||||
[
|
||||
CashEquivalents, CashInHand, BankAccounts, ChequesInHand, Receivable, Inventory,
|
||||
InputVat, InputNbt, Payable, ChequesIssued, Equity, ShareCapital, RetainedEarnings, Drawings,
|
||||
GeneralReserve, RevaluationReserve, Sales
|
||||
];
|
||||
|
||||
public static bool IsGlKey(string key) => key == SetupDone || AllAccountKeys.Contains(key);
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using Distribution.Data;
|
||||
using Distribution.Features.Grn;
|
||||
using Distribution.Features.SysConfig;
|
||||
using Distribution.Infra.UoW;
|
||||
using Distribution.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.GeneralLedger;
|
||||
|
||||
public record GlLine(string AccountCode, decimal Debit, decimal Credit);
|
||||
|
||||
/// <summary>What closing a financial year would post. Closable is false with a Reason when it cannot be closed.</summary>
|
||||
public record YearEndPreview(int Year, string StartDate, string EndDate, decimal TotalIncome, decimal TotalExpenses,
|
||||
decimal NetProfit, bool Closable, string? Reason, bool AlreadyClosed);
|
||||
|
||||
/// <summary>
|
||||
/// One opening balance: Account is a key from OpeningAccounts (e.g. "Inventory", "ShareCapital") or "BankAccount"
|
||||
/// with BankAccountId. Amount is the account's normal balance (>= 0); the debit/credit side comes from the account.
|
||||
/// </summary>
|
||||
public record OpeningBalanceLine(string Account, Guid? BankAccountId, decimal Amount);
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors Distribution business events into the General Ledger as journal entries. Every public Post* method is
|
||||
/// called AFTER the business transaction has committed and never throws: a GL problem is recorded in GlPostingLogs
|
||||
/// (status Failed, payload kept) and can be retried later, so it can never block a sale, payment or cheque action.
|
||||
/// </summary>
|
||||
public class GlPostingService(
|
||||
IGeneralLedgerClient gl,
|
||||
ISysConfigRepository configRepo,
|
||||
GlSetupService setup,
|
||||
AppDBContext context,
|
||||
IUnitOfWork uow,
|
||||
ILogger<GlPostingService> logger)
|
||||
{
|
||||
private record Payload(string EntryDate, string JournalType, string SourceModule, string Reference, string Narration,
|
||||
string PreparedBy, List<PayloadLine> Lines);
|
||||
|
||||
private record PayloadLine(string AccountCode, decimal DebitAmount, decimal CreditAmount);
|
||||
|
||||
// ── Events ───────────────────────────────────────────────────────────────
|
||||
|
||||
public Task PostInvoiceFinalizedAsync(Models.Invoice invoice, CancellationToken ct = default) =>
|
||||
PostAsync("InvoiceFinalized", invoice.Id, invoice.FinalizedAt ?? DateTime.Now, "INVOICE", $"Invoice {invoice.Id} finalized",
|
||||
async () => invoice.TotalAmount <= 0 ? null :
|
||||
[
|
||||
new GlLine(await Acct(GlKeys.Receivable), invoice.TotalAmount, 0),
|
||||
new GlLine(await Acct(GlKeys.Sales), 0, invoice.TotalAmount)
|
||||
], ct);
|
||||
|
||||
public Task PostCustomerPaymentAsync(Models.Payment payment, CancellationToken ct = default) =>
|
||||
PostAsync("CustomerPayment", payment.Id, payment.CollectedAt, "PAYMENT", $"Customer payment for invoice {payment.FK_InvoiceId}",
|
||||
async () =>
|
||||
{
|
||||
if (payment.Amount <= 0 || payment.Method == PaymentMethod.Credit) return null;
|
||||
var debit = payment.Method == PaymentMethod.Cash
|
||||
? await BankCode(payment.FK_BankAccountId)
|
||||
: await Acct(GlKeys.ChequesInHand);
|
||||
return [new GlLine(debit, payment.Amount, 0), new GlLine(await Acct(GlKeys.Receivable), 0, payment.Amount)];
|
||||
}, ct);
|
||||
|
||||
/// <summary>Deposit / Return / Cancel of a received cheque that came from a customer payment.</summary>
|
||||
public Task PostReceivedChequeAsync(Models.ReceivedCheque cheque, ReceivedChequeStatusAction action, CancellationToken ct = default)
|
||||
{
|
||||
if (action is not (ReceivedChequeStatusAction.Deposit or ReceivedChequeStatusAction.Return or ReceivedChequeStatusAction.Cancel))
|
||||
return Task.CompletedTask;
|
||||
|
||||
var date = action == ReceivedChequeStatusAction.Deposit ? cheque.DepositDate ?? DateTime.Now : DateTime.Now;
|
||||
return PostAsync($"ReceivedCheque{action}", cheque.Id, date, "CHEQUE", $"Received cheque {cheque.ChequeNo} {action}",
|
||||
async () =>
|
||||
{
|
||||
// Only cheques created by a customer payment were posted to Cheques-in-hand, so only they can move.
|
||||
if (!await context.Payments.AnyAsync(p => p.FK_ReceivedChequeId == cheque.Id, ct)) return null;
|
||||
var inHand = await Acct(GlKeys.ChequesInHand);
|
||||
return action switch
|
||||
{
|
||||
ReceivedChequeStatusAction.Deposit =>
|
||||
[new GlLine(await BankCode(cheque.FK_DepositBankAccountId), cheque.Amount, 0), new GlLine(inHand, 0, cheque.Amount)],
|
||||
ReceivedChequeStatusAction.Return =>
|
||||
[new GlLine(inHand, cheque.Amount, 0), new GlLine(await BankCode(cheque.FK_DepositBankAccountId), 0, cheque.Amount)],
|
||||
_ => [new GlLine(await Acct(GlKeys.Receivable), cheque.Amount, 0), new GlLine(inHand, 0, cheque.Amount)]
|
||||
};
|
||||
}, ct);
|
||||
}
|
||||
|
||||
public Task PostGrnAsync(Models.GoodsReceipt grn, CancellationToken ct = default) =>
|
||||
PostAsync("Grn", grn.Id, grn.ReceivedAt, "GRN", $"Goods received {grn.Id}",
|
||||
async () =>
|
||||
{
|
||||
decimal net = 0, nbt = 0, vat = 0;
|
||||
foreach (var i in grn.Items.Where(i => i.IsActive))
|
||||
{
|
||||
var a = GrnPricing.Line(i.AcceptedQuantity, i.UnitCost, i.DiscountPercent, i.NbtPercent, i.VatPercent);
|
||||
net += a.Gross - a.Discount;
|
||||
nbt += a.Nbt;
|
||||
vat += a.Vat;
|
||||
}
|
||||
var total = net + nbt + vat;
|
||||
if (total <= 0) return null;
|
||||
|
||||
var lines = new List<GlLine> { new(await Acct(GlKeys.Inventory), net, 0) };
|
||||
if (nbt > 0) lines.Add(new GlLine(await Acct(GlKeys.InputNbt), nbt, 0));
|
||||
if (vat > 0) lines.Add(new GlLine(await Acct(GlKeys.InputVat), vat, 0));
|
||||
lines.Add(new GlLine(await Acct(GlKeys.Payable), 0, total));
|
||||
return lines;
|
||||
}, ct);
|
||||
|
||||
public Task PostSupplierPaymentAsync(Models.SupplierPayment payment, CancellationToken ct = default) =>
|
||||
PostAsync("SupplierPayment", payment.Id, payment.PaidAt, "SUPPLIER_PAYMENT", $"Supplier payment {payment.Id}",
|
||||
async () =>
|
||||
{
|
||||
if (payment.Amount <= 0) return null;
|
||||
var credit = payment.Method == PaymentMethod.Cash
|
||||
? await BankCode(payment.FK_BankAccountId)
|
||||
: await Acct(GlKeys.ChequesIssued);
|
||||
return [new GlLine(await Acct(GlKeys.Payable), payment.Amount, 0), new GlLine(credit, 0, payment.Amount)];
|
||||
}, ct);
|
||||
|
||||
/// <summary>Clear / Bounce / Cancel of a cheque issued through a supplier payment.</summary>
|
||||
public Task PostIssuedChequeAsync(Models.ChequePage page, ChequePageStatusAction action, CancellationToken ct = default)
|
||||
{
|
||||
if (action == ChequePageStatusAction.Void) return Task.CompletedTask;
|
||||
|
||||
var date = action == ChequePageStatusAction.Clear ? page.ClearedDate ?? DateTime.Now : DateTime.Now;
|
||||
return PostAsync($"IssuedCheque{action}", page.Id, date, "CHEQUE", $"Issued cheque {page.ChequeNo} {action}",
|
||||
async () =>
|
||||
{
|
||||
var amount = page.Amount ?? 0;
|
||||
if (amount <= 0 || !await context.SupplierPayments.AnyAsync(p => p.FK_ChequePageId == page.Id, ct)) return null;
|
||||
var issued = await Acct(GlKeys.ChequesIssued);
|
||||
if (action == ChequePageStatusAction.Clear)
|
||||
{
|
||||
var bookBankId = await context.ChequeBooks.Where(b => b.Id == page.FK_ChequeBookId)
|
||||
.Select(b => (Guid?)b.FK_BankAccountId).FirstOrDefaultAsync(ct);
|
||||
return [new GlLine(issued, amount, 0), new GlLine(await BankCode(bookBankId), 0, amount)];
|
||||
}
|
||||
// Bounce / Cancel: the cheque no longer settles the payable, so put it back.
|
||||
return [new GlLine(issued, amount, 0), new GlLine(await Acct(GlKeys.Payable), 0, amount)];
|
||||
}, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the opening balances (asset side debited, liability/equity side credited). Zero amounts are skipped, so the
|
||||
/// form can list every account with a default of 0. Reports the outcome so the caller can show a failure.
|
||||
/// </summary>
|
||||
public async Task<GlPostingLog> PostOpeningBalanceAsync(DateOnly date, string? narration, List<OpeningBalanceLine> input, CancellationToken ct = default)
|
||||
{
|
||||
if ((await configRepo.GetByKeyAsync(GlKeys.SetupDone))?.Value != "true")
|
||||
throw new InvalidOperationException("Run the General Ledger setup first.");
|
||||
|
||||
var lines = new List<GlLine>();
|
||||
foreach (var l in input ?? [])
|
||||
{
|
||||
if (l.Amount < 0) throw new InvalidOperationException($"'{l.Account}': the amount cannot be negative.");
|
||||
if (l.Amount == 0) continue;
|
||||
|
||||
if (l.Account == "BankAccount")
|
||||
{
|
||||
lines.Add(new GlLine(await BankCode(l.BankAccountId), l.Amount, 0));
|
||||
continue;
|
||||
}
|
||||
if (!OpeningAccounts.TryGetValue(l.Account, out var def))
|
||||
throw new InvalidOperationException($"Unknown account '{l.Account}'.");
|
||||
var code = await Acct(def.Key);
|
||||
lines.Add(def.DebitNormal ? new GlLine(code, l.Amount, 0) : new GlLine(code, 0, l.Amount));
|
||||
}
|
||||
|
||||
if (lines.Count == 0)
|
||||
throw new InvalidOperationException("Enter at least one opening balance greater than zero.");
|
||||
|
||||
var debit = lines.Sum(x => x.Debit);
|
||||
var credit = lines.Sum(x => x.Credit);
|
||||
if (debit != credit)
|
||||
throw new InvalidOperationException(
|
||||
$"The opening balances do not balance: assets (debits) total {debit:N2} but liabilities + equity (credits) total {credit:N2}. " +
|
||||
"Adjust Share Capital / Retained Earnings so they match.");
|
||||
|
||||
var sourceId = Guid.NewGuid();
|
||||
await PostAsync("OpeningBalance", sourceId, date.ToDateTime(TimeOnly.MinValue), "OPENING_BALANCE",
|
||||
string.IsNullOrWhiteSpace(narration) ? "Opening balances" : narration.Trim(), () => Task.FromResult<List<GlLine>?>(lines), ct);
|
||||
|
||||
return await context.GlPostingLogs.FirstOrDefaultAsync(l => l.Event == "OpeningBalance" && l.SourceId == sourceId, ct)
|
||||
?? throw new InvalidOperationException("The opening balances could not be prepared. Check the server log.");
|
||||
}
|
||||
|
||||
/// <summary>Accounts that can carry an opening balance (besides individual cash/bank accounts) and their normal side.</summary>
|
||||
public static readonly Dictionary<string, (string Key, bool DebitNormal)> OpeningAccounts = new()
|
||||
{
|
||||
["ChequesInHand"] = (GlKeys.ChequesInHand, true),
|
||||
["Receivable"] = (GlKeys.Receivable, true),
|
||||
["Inventory"] = (GlKeys.Inventory, true),
|
||||
["InputVat"] = (GlKeys.InputVat, true),
|
||||
["InputNbt"] = (GlKeys.InputNbt, true),
|
||||
["Drawings"] = (GlKeys.Drawings, true),
|
||||
["Payable"] = (GlKeys.Payable, false),
|
||||
["ChequesIssued"] = (GlKeys.ChequesIssued, false),
|
||||
["ShareCapital"] = (GlKeys.ShareCapital, false),
|
||||
["RetainedEarnings"] = (GlKeys.RetainedEarnings, false),
|
||||
["GeneralReserve"] = (GlKeys.GeneralReserve, false),
|
||||
["RevaluationReserve"] = (GlKeys.RevaluationReserve, false),
|
||||
};
|
||||
|
||||
// ── Year-end close ───────────────────────────────────────────────────────
|
||||
|
||||
private const string YearEndEvent = "YearEndClose";
|
||||
|
||||
private static Guid YearEndSourceId(int year) =>
|
||||
new(System.Security.Cryptography.MD5.HashData(System.Text.Encoding.UTF8.GetBytes($"{YearEndEvent}:{year}")));
|
||||
|
||||
/// <summary>Years that have a posted closing journal.</summary>
|
||||
public async Task<List<int>> GetClosedYearsAsync(CancellationToken ct = default) =>
|
||||
(await context.GlPostingLogs
|
||||
.Where(l => l.Event == YearEndEvent && l.Status == GlPostingStatus.Posted)
|
||||
.Select(l => l.Reference)
|
||||
.ToListAsync(ct))
|
||||
.Select(r => int.TryParse(r.Split(':').Last(), out var y) ? y : 0)
|
||||
.Where(y => y > 0).OrderBy(y => y).ToList();
|
||||
|
||||
private record CloseBuild(List<GlLine> Lines, decimal Income, decimal Expenses);
|
||||
|
||||
/// <summary>Reads the year's own P&L from GL (which excludes earlier closing journals) and builds the closing lines.</summary>
|
||||
private async Task<CloseBuild> BuildYearEndCloseAsync(int year, CancellationToken ct)
|
||||
{
|
||||
var result = await gl.SendAsync(HttpMethod.Get,
|
||||
$"v1/reports?reportType=ProfitAndLoss&outputFormat=Json&periodStart={year}-01-01&periodEnd={year}-12-31", null, ct);
|
||||
if (!result.Success || GlJson.Data(result) is not { } pl)
|
||||
throw new InvalidOperationException($"Could not read the {year} profit and loss from the General Ledger: {GlJson.Message(result) ?? result.StatusCode.ToString()}");
|
||||
|
||||
// Net CREDIT balance per account: income sections carry their amount as is, expense sections are reported
|
||||
// as positive debits (so flip), unclassified rows are already credit - debit.
|
||||
var netCredit = new Dictionary<string, decimal>();
|
||||
decimal income = 0, expenses = 0;
|
||||
void Collect(string section, int sign)
|
||||
{
|
||||
if (GlJson.Get(pl, section) is not { } s || GlJson.Get(s, "lines") is not { ValueKind: JsonValueKind.Array } sectionLines) return;
|
||||
foreach (var l in sectionLines.EnumerateArray())
|
||||
{
|
||||
var code = GlJson.GetString(l, "accountCode");
|
||||
if (code is null || !decimal.TryParse(GlJson.GetString(l, "amount"), NumberStyles.Any, CultureInfo.InvariantCulture, out var amount)) continue;
|
||||
var credit = sign * amount;
|
||||
netCredit[code] = netCredit.GetValueOrDefault(code) + credit;
|
||||
if (credit > 0) income += credit; else expenses += -credit;
|
||||
}
|
||||
}
|
||||
Collect("sales", 1); Collect("otherIncome", 1);
|
||||
Collect("costOfSales", -1); Collect("distributionExpenses", -1); Collect("administrationExpenses", -1);
|
||||
Collect("otherExpenses", -1); Collect("financialExpenses", -1);
|
||||
Collect("unclassified", 1);
|
||||
|
||||
// Closing: an account with a net credit balance is debited, one with a net debit balance is credited.
|
||||
var lines = netCredit.Where(kv => kv.Value != 0)
|
||||
.Select(kv => kv.Value > 0 ? new GlLine(kv.Key, kv.Value, 0) : new GlLine(kv.Key, 0, -kv.Value)).ToList();
|
||||
var debit = lines.Sum(x => x.Debit);
|
||||
var credit2 = lines.Sum(x => x.Credit);
|
||||
if (debit != credit2)
|
||||
{
|
||||
var retained = await Acct(GlKeys.RetainedEarnings);
|
||||
lines.Add(debit > credit2 ? new GlLine(retained, 0, debit - credit2) : new GlLine(retained, credit2 - debit, 0));
|
||||
}
|
||||
return new CloseBuild(lines, income, expenses);
|
||||
}
|
||||
|
||||
public async Task<YearEndPreview> GetYearEndPreviewAsync(int year, CancellationToken ct = default)
|
||||
{
|
||||
var start = $"{year}-01-01";
|
||||
var end = $"{year}-12-31";
|
||||
|
||||
if ((await configRepo.GetByKeyAsync(GlKeys.SetupDone))?.Value != "true")
|
||||
return new(year, start, end, 0, 0, 0, false, "Run the General Ledger setup first.", false);
|
||||
|
||||
var sourceId = YearEndSourceId(year);
|
||||
var existing = await context.GlPostingLogs.FirstOrDefaultAsync(l => l.Event == YearEndEvent && l.SourceId == sourceId, ct);
|
||||
if (existing?.Status == GlPostingStatus.Posted)
|
||||
return new(year, start, end, 0, 0, 0, false, $"{year} has already been closed.", true);
|
||||
|
||||
var build = await BuildYearEndCloseAsync(year, ct);
|
||||
var net = build.Income - build.Expenses;
|
||||
string? reason = null;
|
||||
if (new DateOnly(year, 12, 31) > DateOnly.FromDateTime(DateTime.Now))
|
||||
reason = $"{year} has not ended yet. A year can only be closed after 31 December.";
|
||||
else if (await context.GlPostingLogs.AnyAsync(l => l.Status != GlPostingStatus.Posted && !(l.Event == YearEndEvent && l.SourceId == sourceId), ct))
|
||||
reason = "There are failed or pending General Ledger postings. Retry them first so the year's figures are complete.";
|
||||
else if (build.Lines.Count == 0)
|
||||
reason = $"There is no income or expense activity in {year} to close.";
|
||||
|
||||
return new(year, start, end, build.Income, build.Expenses, net, reason is null, reason, false);
|
||||
}
|
||||
|
||||
/// <summary>Posts the closing journal for the year (income/expense into Retained Earnings), then locks the year's periods.</summary>
|
||||
public async Task<object> CloseYearAsync(int year, CancellationToken ct = default)
|
||||
{
|
||||
var preview = await GetYearEndPreviewAsync(year, ct);
|
||||
if (!preview.Closable) throw new InvalidOperationException(preview.Reason ?? "This year cannot be closed.");
|
||||
|
||||
var build = await BuildYearEndCloseAsync(year, ct);
|
||||
var sourceId = YearEndSourceId(year);
|
||||
await PostAsync(YearEndEvent, sourceId, new DateTime(year, 12, 31), "YEAR_END_CLOSE", $"Year-end closing entry for {year}",
|
||||
() => Task.FromResult<List<GlLine>?>(build.Lines), ct, journalType: "CLOSING", reference: $"{YearEndEvent}:{year}");
|
||||
|
||||
var row = await context.GlPostingLogs.FirstOrDefaultAsync(l => l.Event == YearEndEvent && l.SourceId == sourceId, ct)
|
||||
?? throw new InvalidOperationException("The closing entry could not be prepared. Check the server log.");
|
||||
if (row.Status != GlPostingStatus.Posted)
|
||||
throw new InvalidOperationException((row.Error ?? "The General Ledger did not accept the closing entry.") + " Fix the cause and press Close again.");
|
||||
|
||||
var locked = await LockYearPeriodsAsync(year, ct);
|
||||
return new { year, glJournalNo = row.GlJournalNo, netProfit = preview.NetProfit, periodsLocked = locked };
|
||||
}
|
||||
|
||||
private async Task<int> LockYearPeriodsAsync(int year, CancellationToken ct)
|
||||
{
|
||||
var ids = new HashSet<long>();
|
||||
for (var month = 1; month <= 12; month++)
|
||||
{
|
||||
var p = await gl.SendAsync(HttpMethod.Get, $"v1/fiscal-years/periods/by-date?date={year}-{month:00}-15", null, ct);
|
||||
if (p.Success && GlJson.Data(p) is { } pd && GlJson.GetLong(pd, "periodId") is { } id) ids.Add(id);
|
||||
}
|
||||
|
||||
var locked = 0;
|
||||
foreach (var id in ids)
|
||||
{
|
||||
var r = await gl.SendAsync(HttpMethod.Put, $"v1/periods/{id}/lock", new { action = "Close", reason = $"Year-end close {year}" }, ct);
|
||||
if (r.Success) locked++;
|
||||
else logger.LogWarning("Could not lock GL period {PeriodId} after closing {Year}: {Message}", id, year, GlJson.Message(r));
|
||||
}
|
||||
return locked;
|
||||
}
|
||||
|
||||
// ── Retry ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Re-sends every Failed/Pending posting. Returns how many are now Posted.</summary>
|
||||
public async Task<int> RetryFailedAsync(CancellationToken ct = default)
|
||||
{
|
||||
var rows = await context.GlPostingLogs
|
||||
.Where(l => l.Status != GlPostingStatus.Posted)
|
||||
.OrderBy(l => l.CreatedAt)
|
||||
.ToListAsync(ct);
|
||||
|
||||
var posted = 0;
|
||||
foreach (var row in rows)
|
||||
{
|
||||
await SendAsync(row, ct);
|
||||
if (row.Status == GlPostingStatus.Posted) posted++;
|
||||
}
|
||||
return posted;
|
||||
}
|
||||
|
||||
// ── Core ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private async Task PostAsync(string evt, Guid sourceId, DateTime date, string sourceModule, string narration,
|
||||
Func<Task<List<GlLine>?>> buildLines, CancellationToken ct, string journalType = "GENERAL", string? reference = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if ((await configRepo.GetByKeyAsync(GlKeys.SetupDone))?.Value != "true") return; // GL not set up yet
|
||||
|
||||
var row = await context.GlPostingLogs.FirstOrDefaultAsync(l => l.Event == evt && l.SourceId == sourceId, ct);
|
||||
if (row is not null && row.Status == GlPostingStatus.Posted) return;
|
||||
|
||||
if (row is null)
|
||||
{
|
||||
var lines = await buildLines();
|
||||
if (lines is null || lines.Count == 0) return;
|
||||
|
||||
reference ??= $"{evt}:{sourceId}";
|
||||
var payload = new Payload(date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), journalType, sourceModule, reference,
|
||||
narration, "Distribution", lines.Select(l => new PayloadLine(l.AccountCode, l.Debit, l.Credit)).ToList());
|
||||
|
||||
row = new GlPostingLog
|
||||
{
|
||||
Event = evt,
|
||||
SourceId = sourceId,
|
||||
Reference = reference,
|
||||
PayloadJson = JsonSerializer.Serialize(payload, JsonOptions)
|
||||
};
|
||||
await uow.BeginAsync(ct);
|
||||
try { await context.GlPostingLogs.AddAsync(row, ct); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
}
|
||||
|
||||
await SendAsync(row, ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "GL posting {Event}:{SourceId} failed before it could be sent", evt, sourceId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendAsync(GlPostingLog row, CancellationToken ct)
|
||||
{
|
||||
string? journalNo = null;
|
||||
string? error = null;
|
||||
try
|
||||
{
|
||||
var payload = JsonSerializer.Deserialize<JsonElement>(row.PayloadJson);
|
||||
var entryDate = payload.GetProperty("entryDate").GetString()!;
|
||||
|
||||
var period = await gl.SendAsync(HttpMethod.Get, $"v1/fiscal-years/periods/by-date?date={entryDate}", null, ct);
|
||||
var periodId = period.Success && GlJson.Data(period) is { } pd ? GlJson.GetLong(pd, "periodId") : null;
|
||||
if (periodId is null)
|
||||
{
|
||||
error = $"No accounting period in GL covers {entryDate}. Run GL setup for that year.";
|
||||
}
|
||||
else
|
||||
{
|
||||
var body = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(row.PayloadJson, JsonOptions)!;
|
||||
body["periodId"] = JsonSerializer.SerializeToElement(periodId.Value);
|
||||
var result = await gl.SendAsync(HttpMethod.Post, "v1/journal-entries", body, ct);
|
||||
if (result.Success)
|
||||
journalNo = GlJson.Data(result) is { } d ? GlJson.GetString(d, "journalNo") ?? "" : "";
|
||||
else
|
||||
error = GlJson.Message(result) ?? $"GL returned HTTP {result.StatusCode}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.Message;
|
||||
logger.LogWarning(ex, "GL posting {Reference} failed", row.Reference);
|
||||
}
|
||||
|
||||
row.Attempts++;
|
||||
row.LastAttemptAt = DateTime.Now;
|
||||
row.Status = error is null ? GlPostingStatus.Posted : GlPostingStatus.Failed;
|
||||
row.GlJournalNo = journalNo;
|
||||
row.Error = error is null ? null : error.Length > 1000 ? error[..1000] : error;
|
||||
|
||||
await uow.BeginAsync(ct);
|
||||
try { context.GlPostingLogs.Update(row); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
}
|
||||
|
||||
// ── Account lookups ──────────────────────────────────────────────────────
|
||||
|
||||
private async Task<string> Acct(string key) =>
|
||||
(await configRepo.GetByKeyAsync(key))?.Value is { Length: > 0 } code
|
||||
? code
|
||||
: throw new InvalidOperationException($"GL account mapping '{key}' is missing. Run GL setup.");
|
||||
|
||||
private async Task<string> BankCode(Guid? bankAccountId)
|
||||
{
|
||||
if (bankAccountId is null) throw new InvalidOperationException("No cash/bank account on this transaction.");
|
||||
var account = await context.BankAccounts.FirstOrDefaultAsync(a => a.Id == bankAccountId.Value)
|
||||
?? throw new InvalidOperationException("Cash/bank account not found.");
|
||||
return await setup.EnsureBankAccountAsync(account)
|
||||
?? throw new InvalidOperationException("Could not map the cash/bank account to a GL account. Run GL setup.");
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
using System.Text.Json;
|
||||
using Distribution.Data;
|
||||
using Distribution.Features.SysConfig;
|
||||
using Distribution.Infra.UoW;
|
||||
using Distribution.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.GeneralLedger;
|
||||
|
||||
public record GlSetupResult(bool Done, List<string> Actions);
|
||||
|
||||
/// <summary>
|
||||
/// One-time (re-runnable) bootstrap of the General Ledger service for Distribution: creates the chart of accounts
|
||||
/// Distribution posts to, a fiscal year with its periods, the GL cash-root config, and a GL account for every
|
||||
/// Distribution cash/bank account. The resulting GL account codes are stored in SysConfig (see GlKeys).
|
||||
/// </summary>
|
||||
public class GlSetupService(
|
||||
IGeneralLedgerClient gl,
|
||||
ISysConfigRepository configRepo,
|
||||
AppDBContext context,
|
||||
IUnitOfWork uow)
|
||||
{
|
||||
private const short Asset = 1, Liability = 2, Equity = 3, Income = 4;
|
||||
|
||||
// key, GL account name, account type, parent key, postable, tag name, tag value.
|
||||
// Operating = working-capital account for the Cash Flow report (cashFlowCategory OPERATING).
|
||||
private record AccountDef(string Key, string Name, short TypeId, string? ParentKey, bool Postable, string? TagField, string? TagValue, bool Operating = false);
|
||||
|
||||
private static readonly AccountDef[] Chart =
|
||||
[
|
||||
new(GlKeys.CashEquivalents, "Cash and Cash Equivalents", Asset, null, false, "balanceSheetClassification", "CURRENT_ASSET"),
|
||||
new(GlKeys.CashInHand, "Cash in Hand", Asset, GlKeys.CashEquivalents, false, null, null),
|
||||
new(GlKeys.BankAccounts, "Bank Accounts", Asset, GlKeys.CashEquivalents, false, null, null),
|
||||
new(GlKeys.ChequesInHand, "Cheques in Hand", Asset, null, true, "balanceSheetClassification", "CURRENT_ASSET", true),
|
||||
new(GlKeys.Receivable, "Trade Receivables", Asset, null, true, "balanceSheetClassification", "CURRENT_ASSET", true),
|
||||
new(GlKeys.Inventory, "Inventory", Asset, null, true, "balanceSheetClassification", "CURRENT_ASSET", true),
|
||||
new(GlKeys.InputVat, "Input VAT", Asset, null, true, "balanceSheetClassification", "CURRENT_ASSET", true),
|
||||
new(GlKeys.InputNbt, "Input NBT", Asset, null, true, "balanceSheetClassification", "CURRENT_ASSET", true),
|
||||
new(GlKeys.Payable, "Trade Payables", Liability, null, true, "balanceSheetClassification", "CURRENT_LIABILITY", true),
|
||||
new(GlKeys.ChequesIssued, "Cheques Issued - Uncleared", Liability, null, true, "balanceSheetClassification", "CURRENT_LIABILITY", true),
|
||||
new(GlKeys.Equity, "Owner's Equity", Equity, null, false, null, null),
|
||||
new(GlKeys.ShareCapital, "Share Capital / Owner's Capital", Equity, GlKeys.Equity, true, null, null),
|
||||
new(GlKeys.RetainedEarnings, "Retained Earnings", Equity, GlKeys.Equity, true, null, null),
|
||||
new(GlKeys.Drawings, "Drawings and Dividends", Equity, GlKeys.Equity, true, null, null),
|
||||
new(GlKeys.GeneralReserve, "General Reserve", Equity, GlKeys.Equity, true, null, null),
|
||||
new(GlKeys.RevaluationReserve, "Revaluation Reserve", Equity, GlKeys.Equity, true, null, null),
|
||||
new(GlKeys.Sales, "Sales Revenue", Income, null, true, "incomeStatementSection", "SALES"),
|
||||
];
|
||||
|
||||
public async Task<GlSetupResult> RunAsync(int? year, Guid? userId, CancellationToken ct = default)
|
||||
{
|
||||
if (!await gl.IsHealthyAsync(ct))
|
||||
throw new InvalidOperationException("The General Ledger service is not reachable. Check GeneralLedger:BaseUrl and that the service is running.");
|
||||
|
||||
var actions = new List<string>();
|
||||
var codes = new Dictionary<string, string>();
|
||||
|
||||
foreach (var key in GlKeys.AllAccountKeys)
|
||||
{
|
||||
var existing = (await configRepo.GetByKeyAsync(key))?.Value;
|
||||
if (!string.IsNullOrWhiteSpace(existing)) codes[key] = existing;
|
||||
}
|
||||
|
||||
// 1. Chart of accounts (skips accounts whose code is already stored and still exists in GL).
|
||||
foreach (var def in Chart)
|
||||
{
|
||||
if (codes.TryGetValue(def.Key, out var code) && await AccountIdAsync(code, ct) is not null)
|
||||
continue;
|
||||
|
||||
long? parentId = null;
|
||||
if (def.ParentKey is not null)
|
||||
parentId = await AccountIdAsync(codes[def.ParentKey], ct);
|
||||
|
||||
var created = await CreateAccountAsync(def.Name, def.TypeId, parentId, def.Postable, ct);
|
||||
codes[def.Key] = created.Code;
|
||||
actions.Add($"Created GL account {created.Code} - {def.Name}");
|
||||
}
|
||||
|
||||
// Tags are applied on every run (idempotent), so accounts created by an earlier run are corrected too.
|
||||
foreach (var def in Chart)
|
||||
{
|
||||
var tags = new Dictionary<string, object?>();
|
||||
if (def.TagField is not null) tags[def.TagField] = def.TagValue;
|
||||
if (def.Operating) tags["cashFlowCategory"] = "OPERATING";
|
||||
if (!def.Postable) tags["isPostable"] = false; // header accounts (re-applied so older runs are corrected)
|
||||
if (tags.Count > 0) await UpdateAccountAsync(codes[def.Key], tags, ct);
|
||||
}
|
||||
|
||||
// 2. Fiscal year + periods for the requested year (default: current year).
|
||||
var fy = year ?? DateTime.Now.Year;
|
||||
var probe = await gl.SendAsync(HttpMethod.Get, $"v1/fiscal-years/periods/by-date?date={fy}-06-30", null, ct);
|
||||
if (!probe.Success)
|
||||
{
|
||||
var fyResult = await gl.SendAsync(HttpMethod.Post, "v1/fiscal-years", new
|
||||
{
|
||||
yearName = $"FY{fy}",
|
||||
startDate = $"{fy}-01-01",
|
||||
endDate = $"{fy}-12-31",
|
||||
autoGeneratePeriods = true,
|
||||
periodCount = 12
|
||||
}, ct);
|
||||
if (!fyResult.Success)
|
||||
throw new InvalidOperationException($"Could not create fiscal year {fy} in GL: {GlJson.Message(fyResult) ?? fyResult.StatusCode.ToString()}");
|
||||
actions.Add($"Created fiscal year FY{fy} with 12 periods");
|
||||
}
|
||||
|
||||
// 3. GL cash root (GL locks this key after one edit, so only set it when empty).
|
||||
var cashRootId = await AccountIdAsync(codes[GlKeys.CashEquivalents], ct);
|
||||
var cfgResult = await gl.SendAsync(HttpMethod.Get, "v1/system-config", null, ct);
|
||||
var currentRoot = ReadGlConfigValue(cfgResult, "CASH_ACCOUNT_ROOT_ID");
|
||||
if (string.IsNullOrWhiteSpace(currentRoot) && cashRootId is not null)
|
||||
{
|
||||
var set = await gl.SendAsync(HttpMethod.Put, "v1/system-config/CASH_ACCOUNT_ROOT_ID",
|
||||
new { configValue = cashRootId.Value.ToString() }, ct);
|
||||
if (!set.Success)
|
||||
throw new InvalidOperationException($"Could not set CASH_ACCOUNT_ROOT_ID in GL: {GlJson.Message(set) ?? set.StatusCode.ToString()}");
|
||||
actions.Add("Set GL CASH_ACCOUNT_ROOT_ID");
|
||||
}
|
||||
|
||||
// 4. Persist the mapping, then mirror every existing Distribution cash/bank account.
|
||||
await SaveConfigAsync(codes, userId, ct);
|
||||
|
||||
foreach (var account in await context.BankAccounts.Where(a => a.GlAccountCode == null).ToListAsync(ct))
|
||||
{
|
||||
var code = await EnsureBankAccountAsync(account, ct);
|
||||
if (code is not null) actions.Add($"Mirrored {account.AccountName} as GL account {code}");
|
||||
}
|
||||
|
||||
await SaveConfigAsync(new Dictionary<string, string> { [GlKeys.SetupDone] = "true" }, userId, ct);
|
||||
return new GlSetupResult(true, actions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the GL account code for a Distribution cash/bank account, creating the GL child account on first use.
|
||||
/// Saves the code on the BankAccount. Returns null if GL setup has not been run.
|
||||
/// </summary>
|
||||
public async Task<string?> EnsureBankAccountAsync(Models.BankAccount account, CancellationToken ct = default)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(account.GlAccountCode)) return account.GlAccountCode;
|
||||
|
||||
var parentKey = account.AccountType == BankAccountType.Bank ? GlKeys.BankAccounts : GlKeys.CashInHand;
|
||||
var parentCode = (await configRepo.GetByKeyAsync(parentKey))?.Value;
|
||||
if (string.IsNullOrWhiteSpace(parentCode)) return null;
|
||||
|
||||
var parentId = await AccountIdAsync(parentCode, ct);
|
||||
if (parentId is null) return null;
|
||||
|
||||
var name = account.AccountType == BankAccountType.Bank && !string.IsNullOrWhiteSpace(account.BankName)
|
||||
? $"{account.BankName} - {account.AccountName}"
|
||||
: account.AccountName;
|
||||
|
||||
var created = await CreateAccountAsync(name, Asset, parentId, true, ct);
|
||||
|
||||
account.GlAccountCode = created.Code;
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
context.BankAccounts.Update(account);
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
return created.Code;
|
||||
}
|
||||
|
||||
private async Task SaveConfigAsync(Dictionary<string, string> values, Guid? userId, CancellationToken ct)
|
||||
{
|
||||
await uow.BeginAsync(ct);
|
||||
try
|
||||
{
|
||||
foreach (var (key, value) in values)
|
||||
await configRepo.UpsertAsync(new Models.SysConfig { Key = key, Value = value, UpdatedAt = DateTime.Now, UpdatedBy = userId });
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
}
|
||||
|
||||
private async Task<long?> AccountIdAsync(string code, CancellationToken ct)
|
||||
{
|
||||
var result = await gl.SendAsync(HttpMethod.Get, $"v1/accounts/{Uri.EscapeDataString(code)}", null, ct);
|
||||
if (!result.Success) return null;
|
||||
var data = GlJson.Data(result);
|
||||
if (data is null) return null;
|
||||
// GET /accounts/{code} wraps the account: { account: {...}, ... }
|
||||
var account = GlJson.Get(data.Value, "account") ?? data.Value;
|
||||
return GlJson.GetLong(account, "accountId");
|
||||
}
|
||||
|
||||
private async Task<(long Id, string Code)> CreateAccountAsync(string name, short typeId, long? parentId, bool postable, CancellationToken ct)
|
||||
{
|
||||
var result = await gl.SendAsync(HttpMethod.Post, "v1/accounts", new
|
||||
{
|
||||
accountName = name,
|
||||
parentAccountId = parentId,
|
||||
accountTypeId = typeId,
|
||||
isControlAccount = false,
|
||||
isPostable = postable
|
||||
}, ct);
|
||||
if (!result.Success)
|
||||
throw new InvalidOperationException($"Could not create GL account '{name}': {GlJson.Message(result) ?? result.StatusCode.ToString()}");
|
||||
|
||||
var data = GlJson.Data(result) ?? throw new InvalidOperationException($"GL returned no data when creating account '{name}'.");
|
||||
var id = GlJson.GetLong(data, "accountId");
|
||||
var code = GlJson.GetString(data, "accountCode");
|
||||
if (id is null || code is null)
|
||||
throw new InvalidOperationException($"GL returned an unexpected body when creating account '{name}'.");
|
||||
return (id.Value, code);
|
||||
}
|
||||
|
||||
private async Task UpdateAccountAsync(string code, Dictionary<string, object?> fields, CancellationToken ct)
|
||||
{
|
||||
var result = await gl.SendAsync(HttpMethod.Put, $"v1/accounts/{Uri.EscapeDataString(code)}", fields, ct);
|
||||
if (!result.Success)
|
||||
throw new InvalidOperationException($"Could not update GL account {code}: {GlJson.Message(result) ?? result.StatusCode.ToString()}");
|
||||
}
|
||||
|
||||
private static string? ReadGlConfigValue(GeneralLedgerResult result, string key)
|
||||
{
|
||||
if (!result.Success || GlJson.Data(result) is not { } data) return null;
|
||||
var items = data.ValueKind == JsonValueKind.Array ? data : GlJson.Get(data, "items");
|
||||
if (items is not { ValueKind: JsonValueKind.Array }) return null;
|
||||
foreach (var item in items.Value.EnumerateArray())
|
||||
if (GlJson.GetString(item, "configKey") == key) return GlJson.GetString(item, "configValue");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using Distribution.Features.GeneralLedger;
|
||||
using Distribution.Features.Po;
|
||||
using Distribution.Features.Stock;
|
||||
using Distribution.Features.SysConfig;
|
||||
@@ -13,7 +14,8 @@ public class GrnService(
|
||||
IUnitOfWork uow,
|
||||
Distribution.Features.Item.IItemRepository itemRepo,
|
||||
Distribution.Features.Supplier.ISupplierRepository supplierRepo,
|
||||
Distribution.Features.SysConfig.SysConfigService sysConfig)
|
||||
Distribution.Features.SysConfig.SysConfigService sysConfig,
|
||||
GlPostingService gl)
|
||||
{
|
||||
private static GrnItemResponse MapItem(Models.GoodsReceiptItem i)
|
||||
{
|
||||
@@ -482,6 +484,7 @@ public class GrnService(
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
var saved = await repo.GetByIdAsync(goodsReceipt.Id);
|
||||
await gl.PostGrnAsync(saved!, ct);
|
||||
return Map(saved!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Distribution.Features.Stock;
|
||||
using Distribution.Features.GeneralLedger;
|
||||
using Distribution.Features.Stock;
|
||||
using Distribution.Infra.Authorization;
|
||||
using Distribution.Infra.UoW;
|
||||
using Distribution.Models;
|
||||
@@ -8,7 +9,8 @@ namespace Distribution.Features.Invoice;
|
||||
public class InvoiceService(
|
||||
IInvoiceRepository repo,
|
||||
IUnitOfWork uow,
|
||||
ICurrentUserService currentUser)
|
||||
ICurrentUserService currentUser,
|
||||
GlPostingService gl)
|
||||
{
|
||||
private static InvoiceResponse ToResponse(Models.Invoice i) =>
|
||||
new(i.Id, i.FK_ShopPOId,
|
||||
@@ -96,6 +98,8 @@ public class InvoiceService(
|
||||
try { repo.Update(invoice); await uow.CommitAsync(ct); }
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
await gl.PostInvoiceFinalizedAsync(invoice, ct);
|
||||
|
||||
return ToResponse(invoice);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Distribution.Features.BankAccount;
|
||||
using Distribution.Features.GeneralLedger;
|
||||
using Distribution.Features.Invoice;
|
||||
using Distribution.Features.ReceivedCheque;
|
||||
using Distribution.Features.StoreCredit;
|
||||
@@ -15,7 +16,8 @@ public class PaymentService(
|
||||
IReceivedChequeRepository receivedChequeRepo,
|
||||
IStoreCreditMovementRepository creditRepo,
|
||||
IUnitOfWork uow,
|
||||
ICurrentUserService currentUser)
|
||||
ICurrentUserService currentUser,
|
||||
GlPostingService gl)
|
||||
{
|
||||
private static PaymentResponse ToResponse(Models.Payment p) =>
|
||||
new(p.Id, p.FK_InvoiceId, p.Amount, p.Method, p.EntryMode,
|
||||
@@ -150,14 +152,13 @@ public class PaymentService(
|
||||
|
||||
invoiceRepo.Update(invoice);
|
||||
|
||||
// TODO ledger: when accounting is wired in, post a journal entry here —
|
||||
// Debit Cash/Bank (BankAccount) OR Cheques-in-hand (for Cheque payments)
|
||||
// Credit Accounts Receivable (invoice.Store)
|
||||
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
// Mirrors to the General Ledger after commit: Dr Cash/Bank (or Cheques-in-hand) / Cr Receivable. Never throws.
|
||||
await gl.PostCustomerPaymentAsync(payment, ct);
|
||||
|
||||
return ToResponse(payment);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Distribution.Features.BankAccount;
|
||||
using Distribution.Features.ChequeBook;
|
||||
using Distribution.Features.GeneralLedger;
|
||||
using Distribution.Infra.UoW;
|
||||
using Distribution.Models;
|
||||
|
||||
@@ -8,7 +9,8 @@ namespace Distribution.Features.ReceivedCheque;
|
||||
public class ReceivedChequeService(
|
||||
IReceivedChequeRepository repo,
|
||||
IBankAccountRepository bankAccountRepo,
|
||||
IUnitOfWork uow)
|
||||
IUnitOfWork uow,
|
||||
GlPostingService gl)
|
||||
{
|
||||
private static ReceivedChequeResponse ToResponse(Models.ReceivedCheque c) =>
|
||||
new(c.Id, c.CompanyId, c.BranchId, c.ReceivedFromType, c.ReceivedFromId, c.ReceivedFromName,
|
||||
@@ -110,6 +112,7 @@ public class ReceivedChequeService(
|
||||
|
||||
repo.Update(cheque);
|
||||
await uow.CommitAsync(ct);
|
||||
await gl.PostReceivedChequeAsync(cheque, request.Action, ct);
|
||||
return ToResponse(cheque);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Distribution.Data;
|
||||
using Distribution.Features.BankAccount;
|
||||
using Distribution.Features.ChequeBook;
|
||||
using Distribution.Features.GeneralLedger;
|
||||
using Distribution.Features.Grn;
|
||||
using Distribution.Features.Supplier;
|
||||
using Distribution.Infra.Authorization;
|
||||
@@ -17,7 +18,8 @@ public class SupplierPaymentService(
|
||||
IBankAccountRepository bankAccountRepo,
|
||||
IChequeBookRepository chequeBookRepo,
|
||||
IUnitOfWork uow,
|
||||
ICurrentUserService currentUser)
|
||||
ICurrentUserService currentUser,
|
||||
GlPostingService gl)
|
||||
{
|
||||
private static SupplierPaymentResponse ToResponse(Models.SupplierPayment p) =>
|
||||
new(p.Id, p.FK_SupplierId, p.Amount, p.Method,
|
||||
@@ -161,14 +163,13 @@ public class SupplierPaymentService(
|
||||
|
||||
await repo.AddAsync(payment);
|
||||
|
||||
// TODO ledger: when accounting is wired in, post a journal entry here —
|
||||
// Debit Accounts Payable (supplier)
|
||||
// Credit Cash/Bank (BankAccount) OR Cheques-outstanding (for Cheque payments)
|
||||
|
||||
await uow.CommitAsync(ct);
|
||||
}
|
||||
catch { await uow.RollbackAsync(ct); throw; }
|
||||
|
||||
// Mirrors to the General Ledger after commit: Dr Payable / Cr Cash/Bank (or Cheques-issued). Never throws.
|
||||
await gl.PostSupplierPaymentAsync(payment, ct);
|
||||
|
||||
return ToResponse(payment);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,12 @@ namespace Distribution.Migrations
|
||||
table: "DeliveryTasks",
|
||||
newName: "FK_ShopPOId");
|
||||
|
||||
// MariaDB drops the ascii collation on CHANGE COLUMN; restore it so the FK to ShopPOs.Id can be created.
|
||||
migrationBuilder.Sql("ALTER TABLE `DeliveryTasks` MODIFY `FK_ShopPOId` char(36) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL;");
|
||||
migrationBuilder.Sql("ALTER TABLE `SchemeRedemptions` MODIFY `FK_ShopPOId` char(36) CHARACTER SET ascii COLLATE ascii_general_ci NULL;");
|
||||
migrationBuilder.Sql("ALTER TABLE `SaleReturns` MODIFY `FK_ShopPOId` char(36) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL;");
|
||||
migrationBuilder.Sql("ALTER TABLE `Payments` MODIFY `FK_InvoiceId` char(36) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL;");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_DeliveryTasks_FK_OrderId",
|
||||
table: "DeliveryTasks",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,814 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||
|
||||
namespace Distribution.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class GlIntegration : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "GlAccountCode",
|
||||
table: "BankAccounts",
|
||||
type: "longtext",
|
||||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "GlPostingLogs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
Event = table.Column<string>(type: "varchar(60)", maxLength: 60, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
SourceId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
Reference = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Status = table.Column<int>(type: "int", nullable: false),
|
||||
PayloadJson = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
GlJournalNo = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Error = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Attempts = table.Column<int>(type: "int", nullable: false),
|
||||
LastAttemptAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_GlPostingLogs", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "BankAccounts",
|
||||
keyColumn: "Id",
|
||||
keyValue: new Guid("11111111-1111-1111-1111-000000000001"),
|
||||
column: "GlAccountCode",
|
||||
value: null);
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "Permissions",
|
||||
columns: new[] { "Id", "Code", "CreatedAt", "Description", "IsActive" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ new Guid("20000000-0000-0000-0000-000000000051"), "finance-reports.view", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "View financial reports (trial balance, P&L, balance sheet, cash flow...)", true },
|
||||
{ new Guid("20000000-0000-0000-0000-000000000052"), "gl.manage", new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), "Set up the General Ledger link and retry failed postings", true }
|
||||
});
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000001"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("52dedbb8-6251-4131-82de-41c2064a663b"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000002"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("47450af3-cf95-4725-a208-eeca124022b7"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000003"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("44deed45-fc6f-420c-a026-f4483efab782"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000004"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("12ae0586-2ff1-4333-92be-9e4660d0f34d"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000005"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("cd258515-3abd-448f-9972-1b9ae9a953d8"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000006"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("9d2dcbbe-cf49-4842-bbcd-1ed0c3bf5f05"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000007"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("16ad892c-b785-4872-a426-1015f32c90b9"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000008"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("3ac5579e-2ba0-44e2-b194-e8a34c91d831"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000009"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("498230b5-7d98-4826-afaf-944123658f69"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000010"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("e960268d-ad0e-4cdc-a387-e2b4fa4ede47"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000011"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("61b598f0-1138-4733-8dac-b84911b1ac18"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000012"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("91ab95eb-c25b-44f9-be90-9b0ffeba4e7a"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000013"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("96ffe8c1-2d62-4d56-8933-02bc4f79972e"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000014"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("1f38d01e-e333-40cd-811c-cc1bcaaaa97b"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000015"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("8c768b0a-0920-4b28-b48a-3f62ba8a16d5"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000016"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("7a87ddf5-e185-4978-9390-09388ab1b264"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000017"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("86229744-f68d-4e7e-9b07-2aad58584331"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000018"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("646c3d0c-801a-4382-8a50-3a8d57d3225e"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000019"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("e2b513a0-5fae-4642-8bf5-c2e3dc734aca"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000020"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("59ce3bc1-2466-4090-97fe-9ae81d718a83"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("48772797-f181-423d-bfb3-f6226a9abe11"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("e99740a5-b681-4b6b-9a42-754ca3114e00"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000023"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("a1b180a1-fbc5-4f18-aeaa-82b443a4fc3f"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000024"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("83070521-f8f2-4b54-ae59-8cd2972de2a1"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000025"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("132b8143-0836-4a3b-b5b3-90cdbe6df12d"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000026"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("9ad2e440-e355-41c8-9e9e-00a78de66abb"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000027"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("b76e221a-fb92-43a3-a723-dcd5fa3327f6"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000028"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("8f63c426-38bb-456d-adff-75e477857607"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000029"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("e4a559d1-bc00-438b-b636-a85803a7ef02"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000030"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("d0a1ab21-0809-4605-a1d2-29e546cb4ae8"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("1e3fb35b-0de5-4d15-860f-51e0c9ec1bb1"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("12e98296-456e-40a3-b11c-61d3393b335e"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000033"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("1d47dc32-9faa-418e-8241-a0dc8a32c974"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000034"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("5e701ad5-f462-4642-86d1-303148323a32"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("52dfa802-8a12-4554-b7ab-1e286998b3b9"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000036"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("7944f075-4eb3-4ccd-98bf-584f79f9f4d5"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000037"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("6ce15356-7240-4a79-bfc8-340c8f31e270"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000038"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("ac248603-3c71-4c1b-84dc-174f25cade65"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000039"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("7805ec5e-8b99-4b45-b47a-be01405a2151"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000040"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("7ea67b95-2918-48e9-b569-71123adb82a0"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000041"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("a2515783-f099-4332-a07c-be81f2e85160"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000042"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("2bff0498-d66e-4564-b0ec-5924cbab96a3"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000043"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("5fa95034-9d7f-4350-b754-9a16b03804fe"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000044"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("f1a82d54-68b8-4992-a4ba-28bd58dd5281"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000045"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("fa183267-8eb0-4831-8f5f-ba1af4d5a78d"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000046"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("fd75e8b3-8751-4b67-870b-6370098a5e1e"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000047"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("c6632177-d567-4cce-98dd-80d735c2e5db"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000048"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("b463deda-4bb9-4d86-a0fc-513d10fc57b1"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000049"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("b44bfcb4-ff1d-4848-9098-1ae54bace236"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000050"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("93c036e4-d811-4915-b73c-1cb358f17065"));
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "RolePermissions",
|
||||
columns: new[] { "FK_PermissionId", "FK_RoleId", "CreatedAt", "Id", "IsActive" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ new Guid("20000000-0000-0000-0000-000000000051"), new Guid("10000000-0000-0000-0000-000000000007"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("9c27d325-1915-41c6-9b62-d85a12f102e1"), true },
|
||||
{ new Guid("20000000-0000-0000-0000-000000000052"), new Guid("10000000-0000-0000-0000-000000000007"), new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), new Guid("1f48dd7b-2f14-451d-b80d-71579d5625e6"), true }
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_GlPostingLogs_Event_SourceId",
|
||||
table: "GlPostingLogs",
|
||||
columns: new[] { "Event", "SourceId" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "GlPostingLogs");
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000051"), new Guid("10000000-0000-0000-0000-000000000007") });
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000052"), new Guid("10000000-0000-0000-0000-000000000007") });
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "Permissions",
|
||||
keyColumn: "Id",
|
||||
keyValue: new Guid("20000000-0000-0000-0000-000000000051"));
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "Permissions",
|
||||
keyColumn: "Id",
|
||||
keyValue: new Guid("20000000-0000-0000-0000-000000000052"));
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "GlAccountCode",
|
||||
table: "BankAccounts");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000001"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("be712eda-98a5-4a30-820a-1737e84d1e4b"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000002"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("47ea9420-d6a0-4376-a2b8-211639d95b27"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000003"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("369eed1f-df4c-4131-a3c6-ebbe63d2dc91"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000004"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("14b6944f-2d4a-4378-a974-c6fc0f9dfb81"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000005"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("14a01e0b-7be5-4c65-ae6a-899f9aa7ad28"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000006"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("5d486ccd-3866-4e7d-8ef1-fb177fb637c1"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000007"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("d830e59b-f451-4bde-9a77-78daea5f9a61"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000008"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("096d3837-a649-4184-8987-2ea0dfa2d4a1"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000009"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("5e43bbde-689b-4d07-baf9-0764078b5586"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000010"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("f317d30c-1644-44f0-b0aa-6bcf1fa66f44"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000011"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("31ffac6f-2aa8-48ee-bc87-213f35933de4"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000012"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("fb525bc4-9f16-455a-bfd5-ca6f3e7ad141"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000013"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("2388918f-6392-4384-b269-0a1469f5bf4c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000014"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("7159012e-5f25-4881-b5e8-5300e382332e"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000015"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("cc4b2e82-ad42-4440-92ba-9b2100bb5f84"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000016"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("6b15c133-ea0e-473d-b58a-7f140e9f03bf"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000017"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("c2dd52a9-380f-479f-8e5e-6b56bfc4738f"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000018"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("bf5f6c7a-2fde-4a01-83f4-aacf91333c4c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000019"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("162a0346-c4c0-4c4e-96e9-aa8154d25b39"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000020"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("5cc76807-25d1-45e5-9da3-cfd6d6f3ad7c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000021"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("51a2653e-4b1a-4f6f-80c2-f533b26109ea"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000022"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("e2a382e2-d712-4a97-a7ec-cd6232d77a78"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000023"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("b60260b5-70b0-4f9e-9ab9-ba4abbbdfb02"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000024"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("a73fe794-383c-4157-89cf-227c4e487a6d"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000025"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("a9743e96-15b2-4c1d-b410-698cd6db0b7b"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000026"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("a94d92ab-66d8-4dd9-8ba3-c242eb450e68"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000027"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("9cb1a028-a98e-492a-aafc-70456fe46c96"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000028"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("5bc99a62-f5d7-48fe-b25c-518e4774614c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000029"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("eceee678-5cee-4ee9-b16d-8d8d21fbb5ea"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000030"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("3a049ed7-75b7-4b1d-b3bb-402ca77c350f"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000031"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("aeaab85c-5b02-4bc2-b0e3-318c0f5b07d3"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000032"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("b79800e0-baa6-4add-9374-7d1e2ed650e8"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000033"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("0da6bf0b-26ff-46b9-bb8c-f9c641b37a89"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000034"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("ff84aeeb-93c0-465d-9a4a-8b686ba7bc75"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000035"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("921f8073-7a66-49bb-ae23-cf8fcde12c9c"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000036"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("bff05501-4988-4c8a-b61d-e32389e795a8"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000037"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("2c4f7432-e697-4d65-97df-069f8913a662"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000038"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("18d27746-3178-4c57-afd5-b75f0061637f"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000039"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("a5d1978b-c497-4314-857b-a7e3b8323a53"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000040"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("88300763-b8c3-4bd2-803b-df0e43bc691e"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000041"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("7aaad5a9-4497-429f-b3dc-9b59664e2e19"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000042"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("2db130c1-4b6f-4719-b3bb-7bd8a0662d75"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000043"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("be349d14-38b5-4a13-a152-9ea055de26b1"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000044"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("5292e02d-9a7d-4a18-be14-86c2f3351cac"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000045"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("97021474-1218-4d21-9115-4b9b2726e38b"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000046"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("56f68355-7b1b-49e4-829c-1a4d47fc93de"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000047"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("3017c8cb-f9e4-41c8-ac2e-f03d23fe944a"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000048"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("619a08e5-f6cb-4515-8b4d-a094d19277d8"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000049"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("cb8b4e96-7026-4485-8b52-3145b5289350"));
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "RolePermissions",
|
||||
keyColumns: new[] { "FK_PermissionId", "FK_RoleId" },
|
||||
keyValues: new object[] { new Guid("20000000-0000-0000-0000-000000000050"), new Guid("10000000-0000-0000-0000-000000000007") },
|
||||
column: "Id",
|
||||
value: new Guid("f0f57dfd-21a5-4a0b-a77e-e641f00300fa"));
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,5 +13,8 @@ namespace Distribution.Models
|
||||
public string CurrencyCode { get; set; } = "LKR";
|
||||
|
||||
public bool IsDefault { get; set; }
|
||||
|
||||
/// <summary>Code of the mirrored General Ledger account (child of GL Cash in Hand / Bank Accounts). Null until synced.</summary>
|
||||
public string? GlAccountCode { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace Distribution.Models
|
||||
{
|
||||
public enum GlPostingStatus { Pending, Posted, Failed }
|
||||
|
||||
/// <summary>
|
||||
/// One row per journal entry Distribution sends to the General Ledger service. (Event, SourceId) is unique,
|
||||
/// so a business event can never be posted twice; Failed rows keep their payload and can be retried.
|
||||
/// </summary>
|
||||
public class GlPostingLog : Template
|
||||
{
|
||||
public string Event { get; set; } = string.Empty;
|
||||
public Guid SourceId { get; set; }
|
||||
public string Reference { get; set; } = string.Empty;
|
||||
public GlPostingStatus Status { get; set; } = GlPostingStatus.Pending;
|
||||
|
||||
/// <summary>The journal entry body (without periodId, which is resolved from EntryDate at send time).</summary>
|
||||
public string PayloadJson { get; set; } = string.Empty;
|
||||
|
||||
public string? GlJournalNo { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public int Attempts { get; set; }
|
||||
public DateTime? LastAttemptAt { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ using Distribution.Features.ChequeBook;
|
||||
using Distribution.Features.ReceivedCheque;
|
||||
using Distribution.Features.DriverComplianceConfig;
|
||||
using Distribution.Features.DriverProfile;
|
||||
using Distribution.Features.GeneralLedger;
|
||||
using Distribution.Features.Grn;
|
||||
using Distribution.Features.Item;
|
||||
using Distribution.Features.ShopPO;
|
||||
@@ -141,6 +142,8 @@ builder.Services.AddScoped<ChequeBookService>();
|
||||
builder.Services.AddScoped<ReceivedChequeService>();
|
||||
builder.Services.AddScoped<SupplierPaymentService>();
|
||||
builder.Services.AddScoped<SysConfigService>();
|
||||
builder.Services.AddScoped<GlSetupService>();
|
||||
builder.Services.AddScoped<GlPostingService>();
|
||||
|
||||
builder.Services.AddMemoryCache();
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
@@ -150,6 +153,13 @@ builder.Services.AddHttpClient<IAuthHexClient, AuthHexClient>(client =>
|
||||
var baseUrl = builder.Configuration["AuthHex:BaseUrl"] ?? throw new InvalidOperationException("AuthHex:BaseUrl is not configured.");
|
||||
client.BaseAddress = new Uri(baseUrl);
|
||||
});
|
||||
builder.Services.AddHttpClient<IGeneralLedgerClient, GeneralLedgerClient>(client =>
|
||||
{
|
||||
var baseUrl = builder.Configuration["GeneralLedger:BaseUrl"] ?? throw new InvalidOperationException("GeneralLedger:BaseUrl is not configured.");
|
||||
var apiKey = builder.Configuration["GeneralLedger:ApiKey"] ?? throw new InvalidOperationException("GeneralLedger:ApiKey is not configured.");
|
||||
client.BaseAddress = new Uri(baseUrl);
|
||||
client.DefaultRequestHeaders.Add("X-Api-Key", apiKey);
|
||||
});
|
||||
builder.Services.AddScoped<IPermissionRepository, PermissionRepository>();
|
||||
builder.Services.AddScoped<IScopeRepository, ScopeRepository>();
|
||||
builder.Services.AddScoped<IPermissionService, PermissionService>();
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
{
|
||||
"Urls": "http://localhost:8802",
|
||||
"ConnectionStrings": {
|
||||
// "DefaultConnection": "Server=localhost;Port=3306;Database=distribution;User=root;Password="
|
||||
// live server
|
||||
"DefaultConnection": "Server=187.127.102.190;Port=3306;Database=distribution;User=kalana;Password=kalana@hexdive"
|
||||
},
|
||||
"GeneralLedger": {
|
||||
"BaseUrl": "http://localhost:6704/api/",
|
||||
"ApiKey": "ff303a4b1919092501a2ab2b8725709177bbb8f0025c58d39de4ad678e2b4eda"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
|
||||
Reference in New Issue
Block a user