Distribution_GL Initialization
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user