Distribution_GL Initialization

This commit is contained in:
2026-10-20 13:07:07 +05:30
commit 3f16e0cb44
194 changed files with 35659 additions and 0 deletions
+329
View File
@@ -0,0 +1,329 @@
using General_Ledger_Service.DTOs.Common;
using System.Text.Json;
using General_Ledger_Service.Exceptions;
namespace General_Ledger_Service.Helpers
{
public class DataExtractor
{
public static string GetRequiredString(
Dictionary<string, JsonElement> data,
string key)
{
if (!data.TryGetValue(key, out var element))
throw new BadRequestException($"Missing required field '{key}'.");
if (element.ValueKind != JsonValueKind.String)
throw new BadRequestException($"Field '{key}' must be a string.");
var value = element.GetString();
if (string.IsNullOrWhiteSpace(value))
return "";
//throw new BadRequestException($"Field '{key}' cannot be empty.");
return value;
}
public static string? GetOptionalString(
Dictionary<string, JsonElement> data,
string key)
{
if (!data.TryGetValue(key, out var element) || element.ValueKind == JsonValueKind.Null)
return null;
if (element.ValueKind == JsonValueKind.String)
{
var value = element.GetString();
return string.IsNullOrWhiteSpace(value) ? null : value;
}
if (element.ValueKind == JsonValueKind.Number)
return element.GetRawText();
throw new ArgumentException($"Field '{key}' must be a string.");
}
public static string? GetOptionalStringByKeys(
Dictionary<string, JsonElement> data,
params string[] keys)
{
foreach (var key in keys)
{
if (data.ContainsKey(key))
return GetOptionalString(data, key);
}
return null;
}
public static string GetRequiredStringByKeys(
Dictionary<string, JsonElement> data,
params string[] keys)
{
var value = GetOptionalStringByKeys(data, keys);
if (!string.IsNullOrWhiteSpace(value))
return value;
throw new BadRequestException($"Missing required field '{keys[0]}'.");
}
// ===================== INT =====================
public static int GetRequiredInt(
Dictionary<string, JsonElement> data,
string key)
{
if (!data.TryGetValue(key, out var element))
throw new BadRequestException($"Missing required field '{key}'.");
if (element.TryGetInt32(out var value))
return value;
if (element.ValueKind == JsonValueKind.String &&
int.TryParse(element.GetString(), out value))
{
return value;
}
throw new BadRequestException($"Field '{key}' must be a valid integer.");
}
public static int? GetNullableInt(
Dictionary<string, JsonElement> data,
string key)
{
if (!data.TryGetValue(key, out var element) || element.ValueKind == JsonValueKind.Null)
return null;
if (element.TryGetInt32(out var value))
return value;
if (element.ValueKind == JsonValueKind.String &&
int.TryParse(element.GetString(), out value))
{
return value;
}
throw new BadRequestException($"Field '{key}' must be a valid integer.");
}
// ===================== FLOAT =====================
public static float GetRequiredFloat(
Dictionary<string, JsonElement> data,
string key)
{
if (!data.TryGetValue(key, out var element))
throw new BadRequestException($"Missing required field '{key}'.");
if (element.TryGetSingle(out var value))
return value;
// Allow numeric strings: "12.5"
if (element.ValueKind == JsonValueKind.String &&
float.TryParse(element.GetString(), out value))
{
return value;
}
throw new BadRequestException($"Field '{key}' must be a valid float.");
}
// ===================== DECIMAL =====================
public static decimal GetRequiredDecimal(
Dictionary<string, JsonElement> data,
string key)
{
if (!data.TryGetValue(key, out var element))
throw new ArgumentException($"Missing required field '{key}'.");
if (element.TryGetDecimal(out var value))
return value;
// Allow numeric strings: "12.5"
if (element.ValueKind == JsonValueKind.String &&
decimal.TryParse(element.GetString(), out value))
{
return value;
}
throw new ArgumentException($"Field '{key}' must be a valid decimal.");
}
public static decimal? GetOptionalDecimal(
Dictionary<string, JsonElement> data,
string key)
{
if (!data.TryGetValue(key, out var element) || element.ValueKind == JsonValueKind.Null)
return null;
if (element.TryGetDecimal(out var value))
return value;
if (element.ValueKind == JsonValueKind.String &&
decimal.TryParse(element.GetString(), out value))
{
return value;
}
throw new ArgumentException($"Field '{key}' must be a valid decimal.");
}
public static decimal? GetOptionalDecimalByKeys(
Dictionary<string, JsonElement> data,
params string[] keys)
{
foreach (var key in keys)
{
if (data.ContainsKey(key))
return GetOptionalDecimal(data, key);
}
return null;
}
// ===================== BOOLEAN =====================
public static bool GetRequiredBool(
Dictionary<string, JsonElement> data,
string key)
{
if (!data.TryGetValue(key, out var element))
throw new BadRequestException($"Missing required field '{key}'.");
if (element.ValueKind == JsonValueKind.True) return true;
if (element.ValueKind == JsonValueKind.False) return false;
// Allow string values: "true" / "false"
if (element.ValueKind == JsonValueKind.String &&
bool.TryParse(element.GetString(), out var value))
{
return value;
}
throw new BadRequestException($"Field '{key}' must be a valid boolean.");
}
// ===================== GUID =====================
public static Guid GetRequiredGuid(
Dictionary<string, JsonElement> data,
string key)
{
var value = GetRequiredString(data, key);
if (!Guid.TryParse(value, out var guid))
throw new BadRequestException($"Field '{key}' must be a valid GUID.");
return guid;
}
// ===================== DATETIME =====================
public static DateTime GetRequiredDateTime(
Dictionary<string, JsonElement> data,
string key)
{
if (!data.TryGetValue(key, out var element))
throw new BadRequestException($"Missing required field '{key}'.");
if (element.ValueKind == JsonValueKind.Null)
throw new BadRequestException($"Field '{key}' cannot be null.");
// ISO 8601 string
if (element.ValueKind == JsonValueKind.String &&
DateTime.TryParse(element.GetString(), out var date))
{
return date;
}
// Native JSON DateTime
if (element.TryGetDateTime(out date))
{
return date;
}
throw new BadRequestException($"Field '{key}' must be a valid DateTime.");
}
public static DateTime? GetOptionalDateTime(
Dictionary<string, JsonElement> data,
string key)
{
if (!data.TryGetValue(key, out var element) || element.ValueKind == JsonValueKind.Null)
return null;
if (element.ValueKind == JsonValueKind.String &&
DateTime.TryParse(element.GetString(), out var date))
{
return date;
}
if (element.TryGetDateTime(out date))
return date;
throw new ArgumentException($"Field '{key}' must be a valid DateTime.");
}
// ===================== DATEONLY =====================
public static DateOnly GetRequiredDateOnly(
Dictionary<string, JsonElement> data,
string key)
{
if (!data.TryGetValue(key, out var element))
throw new BadRequestException($"Missing required field '{key}'.");
if (element.ValueKind == JsonValueKind.Null)
throw new BadRequestException($"Field '{key}' cannot be null.");
DateTime date;
// ISO 8601 string (e.g., "2025-02-25")
if (element.ValueKind == JsonValueKind.String &&
DateTime.TryParse(element.GetString(), out date))
{
return DateOnly.FromDateTime(date);
}
if (element.TryGetDateTime(out date))
{
return DateOnly.FromDateTime(date);
}
throw new BadRequestException($"Field '{key}' must be a valid date.");
}
// ===================== USER ID FROM HttpContext =====================
public static Guid GetUserIdFromContext(HttpContext httpContext)
{
if (httpContext == null) throw new ArgumentNullException(nameof(httpContext));
var userIdFromContext = httpContext.Items["UserId"]?.ToString()
?? httpContext.User?.FindFirst("UserId")?.Value;
if (string.IsNullOrWhiteSpace(userIdFromContext) || !Guid.TryParse(userIdFromContext, out var userId))
{
throw new UnauthorizedAccessException("User is not authenticated. Please login and send cookie.");
}
return userId;
}
//============================AuthResponce EXtractor============
public static ApiResponse GetRequiredApiResponse(Dictionary<string, JsonElement> data)
{
ApiResponse authResponse = new()
{
StatusCode = GetRequiredInt(data, "StatusCode"),
Success = GetRequiredBool(data, "success"),
Message = GetRequiredString(data, "message"),
Data = data
};
return authResponse;
}
}
}
+38
View File
@@ -0,0 +1,38 @@
using General_Ledger_Service.Exceptions;
using General_Ledger_Service.Interfaces.Repositories;
namespace General_Ledger_Service.Services
{
// Shared "resulting depth must not exceed the configured max" check, used by every
// recursive-entity Service (Account, CostCenter, and any future one — see
// 01_Full_Development_Plan.md Section 4.2). Not a generic base class (CLAUDE.md Rule 2) —
// just the repeated system_config lookup + comparison + BadRequestException, extracted once.
internal static class DepthGuard
{
public static async Task EnsureWithinMaxDepthAsync(
ISystemConfigRepository systemConfigRepository,
string maxDepthConfigKey,
string? enforceConfigKey,
int resultingDepth,
string exceededMessage)
{
if (enforceConfigKey != null)
{
var enforceConfig = await systemConfigRepository.GetByKeyAsync(enforceConfigKey);
if (enforceConfig?.ConfigValue != "TRUE")
{
return;
}
}
var maxDepthConfig = await systemConfigRepository.GetByKeyAsync(maxDepthConfigKey)
?? throw new NotFoundException($"System config '{maxDepthConfigKey}' not found");
var maxDepth = int.Parse(maxDepthConfig.ConfigValue);
if (resultingDepth > maxDepth)
{
throw new BadRequestException(exceededMessage);
}
}
}
}
+31
View File
@@ -0,0 +1,31 @@
using System.Text.Json;
using General_Ledger_Service.Exceptions;
namespace General_Ledger_Service.Helpers
{
public static class DeserializePayload
{
public static Dictionary<string, JsonElement> Deserialize(object payload)
{
ArgumentNullException.ThrowIfNull(payload);
var json = JsonSerializer.Serialize(payload);
return JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json)
?? throw new BadRequestException("Failed to deserialize payload.");
}
public static T? DeserializeJson<T>(string? json) where T : class
{
if (string.IsNullOrWhiteSpace(json))
return null;
try
{
return JsonSerializer.Deserialize<T>(json);
}
catch
{
return null;
}
}
}
}
+38
View File
@@ -0,0 +1,38 @@
using System.Security.Cryptography;
using System.Text;
namespace General_Ledger_Service.Helpers
{
public static class EncryptionHelper
{
private static readonly byte[] Key = Convert.FromBase64String("6pY8vR2Nf+Xz9WkLm4TqQzB7vA9uS1nE5jR3hG6fM8o=");
public static string Encrypt(string plainText)
{
using Aes aes = Aes.Create();
aes.Key = Key;
aes.GenerateIV();
using var encryptor = aes.CreateEncryptor();
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
byte[] cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
return Convert.ToBase64String(aes.IV.Concat(cipherBytes).ToArray());
}
public static string Decrypt(string combinedText)
{
byte[] combinedBytes = Convert.FromBase64String(combinedText);
using Aes aes = Aes.Create();
aes.Key = Key;
byte[] iv = [.. combinedBytes.Take(16)];
byte[] cipherBytes = [.. combinedBytes.Skip(16)];
using var decryptor = aes.CreateDecryptor(aes.Key, iv);
byte[] plainBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
return Encoding.UTF8.GetString(plainBytes);
}
}
}
+117
View File
@@ -0,0 +1,117 @@
namespace General_Ledger_Service.Repositories
{
// Shared tree logic for every self-referencing entity (Account, CostCenter — see
// 01_Full_Development_Plan.md Section 4.2). LINQ/EF Core cannot express a recursive CTE, so the
// calling repository loads the entity's rows once as TreeRow (a plain id/code/name/parent
// projection) and these methods walk that in-memory list. Pure functions: no DbContext, no SQL,
// so the same code works on any database provider. This is a shared static helper, not a
// generic repository base class (CLAUDE.md Rule 2) — each repository keeps its own public
// interface, method signatures, and result DTOs; only the tree-walking is shared.
internal static class RecursiveTreeQueries
{
internal class TreeRow
{
public long Id { get; set; }
public string Code { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public long? ParentId { get; set; }
public int Depth { get; set; }
}
// Depth a new child of parentId would sit at: the number of nodes from parentId up to its
// root, plus one. A parentId that matches no row yields 1.
public static int GetAncestorDepth(IReadOnlyList<TreeRow> rows, long parentId)
{
var byId = rows.ToDictionary(r => r.Id);
var count = 0;
var visited = new HashSet<long>();
long? current = parentId;
while (current.HasValue && byId.TryGetValue(current.Value, out var node) && visited.Add(node.Id))
{
count++;
current = node.ParentId;
}
return count + 1;
}
// "Root > ... > Entity" path of names, or null if the entity does not exist.
public static string? GetAncestorBreadcrumb(IReadOnlyList<TreeRow> rows, long entityId)
{
var byId = rows.ToDictionary(r => r.Id);
var names = new List<string>();
var visited = new HashSet<long>();
long? current = entityId;
while (current.HasValue && byId.TryGetValue(current.Value, out var node) && visited.Add(node.Id))
{
names.Add(node.Name);
current = node.ParentId;
}
if (names.Count == 0)
{
return null;
}
names.Reverse();
return string.Join(" > ", names);
}
// The entity plus every descendant, depth 1 for the entity itself, ordered by depth then code.
public static List<TreeRow> GetDescendantTree(IReadOnlyList<TreeRow> rows, long entityId)
{
var childrenByParent = rows
.Where(r => r.ParentId.HasValue)
.ToLookup(r => r.ParentId!.Value);
var root = rows.FirstOrDefault(r => r.Id == entityId);
if (root is null)
{
return [];
}
var result = new List<TreeRow>();
var visited = new HashSet<long>();
var queue = new Queue<(TreeRow Node, int Depth)>();
queue.Enqueue((root, 1));
while (queue.Count > 0)
{
var (node, depth) = queue.Dequeue();
if (!visited.Add(node.Id))
{
continue;
}
result.Add(new TreeRow
{
Id = node.Id,
Code = node.Code,
Name = node.Name,
ParentId = node.ParentId,
Depth = depth
});
foreach (var child in childrenByParent[node.Id])
{
queue.Enqueue((child, depth + 1));
}
}
return [.. result
.OrderBy(r => r.Depth)
.ThenBy(r => r.Code, StringComparer.Ordinal)];
}
// Ids of the entity and all of its descendants; empty if the entity does not exist.
public static HashSet<long> GetDescendantIds(IReadOnlyList<TreeRow> rows, long entityId) =>
[.. GetDescendantTree(rows, entityId).Select(r => r.Id)];
// True if newParentId is the entity itself or one of its descendants, i.e. making it the
// entity's parent would create a loop.
public static bool WouldCreateCycle(IReadOnlyList<TreeRow> rows, long entityId, long newParentId) =>
GetDescendantIds(rows, entityId).Contains(newParentId);
}
}
+38
View File
@@ -0,0 +1,38 @@
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
namespace General_Ledger_Service.Repositories
{
// Shared boilerplate for the GetByIdAsync/ExistsAsync/AddAsync one-liners duplicated across
// most repositories (01_Full_Development_Plan.md Section 10.4) - same shape as
// RecursiveTreeQueries.cs: a parameterized static helper, not a generic repository base class
// (CLAUDE.md Rule 2). Each repository keeps its own public interface/method names and calls
// this internally only where its own method body is literally this one-line shape.
internal static class RepositoryHelpers
{
public static async Task<T?> GetByIdAsync<T>(DbSet<T> set, params object[] keyValues) where T : class =>
await set.FindAsync(keyValues);
public static async Task<bool> ExistsAsync<T>(DbSet<T> set, Expression<Func<T, bool>> predicate) where T : class =>
await set.AnyAsync(predicate);
public static async Task<T> AddAsync<T>(DbSet<T> set, T entity) where T : class
{
await set.AddAsync(entity);
return entity;
}
// Paging is only applied when the caller explicitly supplies both page and pageSize -
// no invented defaults (e.g. page=1/pageSize=20) when either is omitted, so a caller that
// doesn't need paging at a given moment gets the full matching result set back untouched.
public static IQueryable<T> ApplyPaging<T>(IQueryable<T> query, int? page, int? pageSize)
{
if (page.HasValue && pageSize.HasValue)
{
query = query.Skip((page.Value - 1) * pageSize.Value).Take(pageSize.Value);
}
return query;
}
}
}
+27
View File
@@ -0,0 +1,27 @@
using General_Ledger_Service.DTOs.Common;
namespace General_Ledger_Service.Services
{
// Shared ApiResponse success-path builder (01_Full_Development_Plan.md Section 10.4) - removes
// the repeated `new ApiResponse { StatusCode = ..., Success = true, ... }` block duplicated in
// nearly every Service method. Does not modify ApiResponse itself (CLAUDE.md Rule 1 foundation,
// off-limits without asking) - this is a separate builder file that constructs one.
internal static class ResponseHelpers
{
public static ApiResponse Ok(string message, object? data) => new()
{
StatusCode = StatusCodes.Status200OK,
Success = true,
Message = message,
Data = data
};
public static ApiResponse Created(string message, object? data) => new()
{
StatusCode = StatusCodes.Status201Created,
Success = true,
Message = message,
Data = data
};
}
}