diff --git a/Fin-Backend.sln.DotSettings.user b/Fin-Backend.sln.DotSettings.user index 18eaaeb..5036aa5 100644 --- a/Fin-Backend.sln.DotSettings.user +++ b/Fin-Backend.sln.DotSettings.user @@ -2,8 +2,10 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -20,6 +22,7 @@ <Project Location="/home/rafaelchicovis/git/fin-backend/Fin.Test" Presentation="&lt;Fin.Test&gt;" /> </SessionState> True + False True False diff --git a/Fin.Api/appsettings.json b/Fin.Api/appsettings.json index b2ab7e9..6a2c99d 100644 --- a/Fin.Api/appsettings.json +++ b/Fin.Api/appsettings.json @@ -6,7 +6,8 @@ } }, "ConnectionStrings": { - "DefaultConnection": "Host=localhost;Port=5432;Database=fin_app;Username=fin_app;Password=fin_app" + "DefaultConnection": "Host=localhost;Port=5432;Database=fin_app;Username=fin_app;Password=fin_app", + "MongoDbConnection": "mongodb://localhost:27017" }, "AllowedHosts": "*", "ApiSettings": { diff --git a/Fin.Domain/CardBrands/Entities/CardBrand.cs b/Fin.Domain/CardBrands/Entities/CardBrand.cs index 48b8989..269e6dc 100644 --- a/Fin.Domain/CardBrands/Entities/CardBrand.cs +++ b/Fin.Domain/CardBrands/Entities/CardBrand.cs @@ -4,7 +4,7 @@ namespace Fin.Domain.CardBrands.Entities; -public class CardBrand: IAuditedEntity +public class CardBrand: IAuditedEntity, ILoggable { public string Name { get; set; } public string Icon { get; set; } @@ -36,4 +36,19 @@ public void Update(CardBrandInput input) Color = input.Color; } + + public object GetLog() + { + return new + { + Id, + CreatedAt, + CreatedBy, + UpdatedAt, + UpdatedBy, + Name, + Color, + Icon + }; + } } diff --git a/Fin.Domain/CreditCards/Entities/CreditCard.cs b/Fin.Domain/CreditCards/Entities/CreditCard.cs index 81f94fd..a7ed055 100644 --- a/Fin.Domain/CreditCards/Entities/CreditCard.cs +++ b/Fin.Domain/CreditCards/Entities/CreditCard.cs @@ -6,7 +6,7 @@ namespace Fin.Domain.CreditCards.Entities; -public class CreditCard: IAuditedTenantEntity +public class CreditCard: ILoggableAuditedTenantEntity { public string Name { get; private set; } public string Color { get; private set; } @@ -63,4 +63,25 @@ public void Update(CreditCardInput input) } public void ToggleInactivated() => Inactivated = !Inactivated; + + public object GetLog() + { + return new + { + Id, + CreatedAt, + CreatedBy, + UpdatedAt, + UpdatedBy, + TenantId, + Name, + Color, + Icon, + Limit, + DueDay, + ClosingDay, + DebitWalletId, + FinancialInstitutionId, + }; + } } \ No newline at end of file diff --git a/Fin.Domain/Fin.Domain.csproj b/Fin.Domain/Fin.Domain.csproj index 6005f43..7b8dbdc 100644 --- a/Fin.Domain/Fin.Domain.csproj +++ b/Fin.Domain/Fin.Domain.csproj @@ -5,8 +5,4 @@ enable - - - - diff --git a/Fin.Domain/FinancialInstitutions/Entities/FinancialInstitution.cs b/Fin.Domain/FinancialInstitutions/Entities/FinancialInstitution.cs index f7d2dd3..2af52d3 100644 --- a/Fin.Domain/FinancialInstitutions/Entities/FinancialInstitution.cs +++ b/Fin.Domain/FinancialInstitutions/Entities/FinancialInstitution.cs @@ -1,12 +1,13 @@ using Fin.Domain.CreditCards.Entities; using Fin.Domain.FinancialInstitutions.Dtos; using Fin.Domain.FinancialInstitutions.Enums; +using Fin.Domain.Global.Decorators; using Fin.Domain.Global.Interfaces; using Fin.Domain.Wallets.Entities; namespace Fin.Domain.FinancialInstitutions.Entities; -public class FinancialInstitution : IAuditedEntity +public class FinancialInstitution : IAuditedEntity, ILoggable { public string Name { get; set; } public string Code { get; set; } @@ -47,4 +48,21 @@ public void Update(FinancialInstitutionInput input) } public void ToggleInactive() => Inactive = !Inactive; + public object GetLog() + { + return new + { + Id, + CreatedAt, + CreatedBy, + UpdatedAt, + UpdatedBy, + Code, + Icon, + Color, + Inactive, + Type, + TypeDescription = Type.GetTranslateKey(), + }; + } } diff --git a/Fin.Domain/FinancialInstitutions/Enums/FinancialInstitutionType.cs b/Fin.Domain/FinancialInstitutions/Enums/FinancialInstitutionType.cs index a45eb91..b422d6d 100644 --- a/Fin.Domain/FinancialInstitutions/Enums/FinancialInstitutionType.cs +++ b/Fin.Domain/FinancialInstitutions/Enums/FinancialInstitutionType.cs @@ -1,9 +1,14 @@ +using Fin.Domain.Global.Decorators; + namespace Fin.Domain.FinancialInstitutions.Enums { public enum FinancialInstitutionType { + [FrontTranslateKey("finCore.features.financialInstitutions.type.bank")] Bank = 0, + [FrontTranslateKey("finCore.features.financialInstitutions.type.digitalBank")] DigitalBank = 1, + [FrontTranslateKey("finCore.features.financialInstitutions.type.foodCard")] FoodCard = 2, } } diff --git a/Fin.Domain/Global/Decorators/FrontTranslateKeyAttribute.cs b/Fin.Domain/Global/Decorators/FrontTranslateKeyAttribute.cs new file mode 100644 index 0000000..812cf17 --- /dev/null +++ b/Fin.Domain/Global/Decorators/FrontTranslateKeyAttribute.cs @@ -0,0 +1,32 @@ +using System.Reflection; + +namespace Fin.Domain.Global.Decorators; + +[AttributeUsage(AttributeTargets.Field)] +public class FrontTranslateKeyAttribute(string translateKey): Attribute +{ + public string TranslateKey { get; } = translateKey; +} + + +public static class FrontTranslateKeyExtension +{ + public static string GetTranslateKey(this T valeu, bool throwIfNotFoundMessage = true) + { + var type = valeu.GetType(); + var memberInfo = type.GetMember(valeu.ToString() ?? string.Empty); + + if (memberInfo.Length > 0) + { + var attribute = memberInfo[0].GetCustomAttribute(); + if (attribute != null) + { + return attribute.TranslateKey; + } + } + + return throwIfNotFoundMessage + ? throw new ArgumentException($"Cannot get translate key {valeu}") + : string.Empty; + } +} \ No newline at end of file diff --git a/Fin.Domain/Global/Interfaces/ILoggable.cs b/Fin.Domain/Global/Interfaces/ILoggable.cs new file mode 100644 index 0000000..bd466e6 --- /dev/null +++ b/Fin.Domain/Global/Interfaces/ILoggable.cs @@ -0,0 +1,6 @@ +namespace Fin.Domain.Global.Interfaces; + +public interface ILoggable +{ + object GetLog(); +} \ No newline at end of file diff --git a/Fin.Domain/Global/Interfaces/ILoggableAuditedTenantEntity.cs b/Fin.Domain/Global/Interfaces/ILoggableAuditedTenantEntity.cs new file mode 100644 index 0000000..92718bf --- /dev/null +++ b/Fin.Domain/Global/Interfaces/ILoggableAuditedTenantEntity.cs @@ -0,0 +1,6 @@ +namespace Fin.Domain.Global.Interfaces; + +public interface ILoggableAuditedTenantEntity: ILoggable, IAuditedEntity, ITenantEntity +{ + +} \ No newline at end of file diff --git a/Fin.Domain/Menus/Entities/Menu.cs b/Fin.Domain/Menus/Entities/Menu.cs index be250db..40145e1 100644 --- a/Fin.Domain/Menus/Entities/Menu.cs +++ b/Fin.Domain/Menus/Entities/Menu.cs @@ -1,10 +1,11 @@ -using Fin.Domain.Global.Interfaces; +using Fin.Domain.Global.Decorators; +using Fin.Domain.Global.Interfaces; using Fin.Domain.Menus.Dtos; using Fin.Domain.Menus.Enums; namespace Fin.Domain.Menus.Entities; -public class Menu: IAuditedEntity +public class Menu: IAuditedEntity, ILoggable { public string FrontRoute { get; set; } public string Name { get; set; } @@ -45,4 +46,24 @@ public void Update(MenuInput input) OnlyForAdmin = input.OnlyForAdmin; Position = input.Position; } + + public object GetLog() + { + return new + { + Id, + CreatedAt, + CreatedBy, + UpdatedAt, + UpdatedBy, + FrontRoute, + Name, + Icon, + Color, + KeyWords, + OnlyForAdmin, + Position, + PositionDescription = Position.GetTranslateKey() + }; + } } diff --git a/Fin.Domain/Menus/Enums/MenuPosition.cs b/Fin.Domain/Menus/Enums/MenuPosition.cs index 955a527..2a3674c 100644 --- a/Fin.Domain/Menus/Enums/MenuPosition.cs +++ b/Fin.Domain/Menus/Enums/MenuPosition.cs @@ -1,8 +1,13 @@ -namespace Fin.Domain.Menus.Enums; +using Fin.Domain.Global.Decorators; + +namespace Fin.Domain.Menus.Enums; public enum MenuPosition { + [FrontTranslateKey("finCore.features.menus.hide")] Hide = 0, + [FrontTranslateKey("finCore.features.menus.leftTop")] LeftTop = 1, + [FrontTranslateKey("finCore.features.menus.leftBottom")] LeftBottom = 2 } \ No newline at end of file diff --git a/Fin.Domain/Notifications/Entities/Notification.cs b/Fin.Domain/Notifications/Entities/Notification.cs index f12be5c..412efc8 100644 --- a/Fin.Domain/Notifications/Entities/Notification.cs +++ b/Fin.Domain/Notifications/Entities/Notification.cs @@ -1,4 +1,5 @@ using System.Collections.ObjectModel; +using Fin.Domain.Global.Decorators; using Fin.Domain.Global.Extensions; using Fin.Domain.Global.Interfaces; using Fin.Domain.Notifications.Dtos; @@ -6,7 +7,7 @@ namespace Fin.Domain.Notifications.Entities; -public class Notification: IAuditedEntity +public class Notification: IAuditedEntity, ILoggable { public List Ways { get; set; } = []; public string TextBody { get; set; } @@ -90,4 +91,29 @@ public List UpdateAndReturnToRemoveDeliveries(Notifica return deliveriesToDelete; } + + public object GetLog() + { + return new + { + Id, + CreatedAt, + CreatedBy, + UpdatedAt, + UpdatedBy, + Ways, + WaysDescriptions = Ways.Select(way => way.GetTranslateKey()), + TextBody, + HtmlBody, + NormalizedTextBody, + Title, + Continuous, + NormalizedTitle, + StartToDelivery, + StopToDelivery, + Link, + Severity, + SeverityDescriptions = Severity.GetTranslateKey(), + }; + } } \ No newline at end of file diff --git a/Fin.Domain/Notifications/Entities/NotificationUserDelivery.cs b/Fin.Domain/Notifications/Entities/NotificationUserDelivery.cs index 21cf2b4..e750299 100644 --- a/Fin.Domain/Notifications/Entities/NotificationUserDelivery.cs +++ b/Fin.Domain/Notifications/Entities/NotificationUserDelivery.cs @@ -1,8 +1,9 @@ -using Fin.Domain.Users.Entities; +using Fin.Domain.Global.Interfaces; +using Fin.Domain.Users.Entities; namespace Fin.Domain.Notifications.Entities; -public class NotificationUserDelivery +public class NotificationUserDelivery: ILoggable { public Guid NotificationId { get; set; } public Guid UserId { get; set; } @@ -33,4 +34,16 @@ public void MarkAsVisualized() { Visualized = true; } + + public object GetLog() + { + return new + { + NotificationId, + UserId, + Delivery, + Visualized, + BackgroundJobId + }; + } } \ No newline at end of file diff --git a/Fin.Domain/Notifications/Entities/UserNotificationSettings.cs b/Fin.Domain/Notifications/Entities/UserNotificationSettings.cs index 205121d..601b140 100644 --- a/Fin.Domain/Notifications/Entities/UserNotificationSettings.cs +++ b/Fin.Domain/Notifications/Entities/UserNotificationSettings.cs @@ -1,11 +1,12 @@ -using Fin.Domain.Global.Interfaces; +using Fin.Domain.Global.Decorators; +using Fin.Domain.Global.Interfaces; using Fin.Domain.Notifications.Dtos; using Fin.Domain.Notifications.Enums; using Fin.Domain.Users.Entities; namespace Fin.Domain.Notifications.Entities; -public class UserNotificationSettings: IAuditedTenantEntity +public class UserNotificationSettings: ILoggableAuditedTenantEntity { public Guid UserId { get; set; } public bool Enabled { get; set; } @@ -61,4 +62,22 @@ public void RemoveTokens(List tokens) { FirebaseTokens = FirebaseTokens.Except(tokens).ToList(); } + + public object GetLog() + { + return new + { + Id, + CreatedAt, + CreatedBy, + UpdatedAt, + UpdatedBy, + TenantId, + UserId, + Enabled, + AllowedWays, + AllowedWaysDesciptions = AllowedWays.Select(way => way.GetTranslateKey()), + FirebaseTokens + }; + } } \ No newline at end of file diff --git a/Fin.Domain/Notifications/Entities/UserRememberUseSetting.cs b/Fin.Domain/Notifications/Entities/UserRememberUseSetting.cs index 3a52436..43aeeef 100644 --- a/Fin.Domain/Notifications/Entities/UserRememberUseSetting.cs +++ b/Fin.Domain/Notifications/Entities/UserRememberUseSetting.cs @@ -1,11 +1,12 @@ -using Fin.Domain.Global.Interfaces; +using Fin.Domain.Global.Decorators; +using Fin.Domain.Global.Interfaces; using Fin.Domain.Notifications.Dtos; using Fin.Domain.Notifications.Enums; using Fin.Domain.Users.Entities; namespace Fin.Domain.Notifications.Entities; -public class UserRememberUseSetting : IAuditedTenantEntity +public class UserRememberUseSetting : ILoggableAuditedTenantEntity { public Guid UserId { get; set; } public List Ways { get; set; } = new(); @@ -42,4 +43,22 @@ public void Update(UserRememberUseSettingInput input) WeekDays = input.WeekDays; NotifyOn = input.NotifyOn; } + + public object GetLog() + { + return new + { + Id, + CreatedAt, + CreatedBy, + UpdatedAt, + UpdatedBy, + TenantId, + UserId, + Ways, + WaysDescriptions = Ways.Select(way => way.GetTranslateKey()), + NotifyOn, + WeekDays, + }; + } } \ No newline at end of file diff --git a/Fin.Domain/Notifications/Enums/NotificationSeverity.cs b/Fin.Domain/Notifications/Enums/NotificationSeverity.cs index 6964b79..6564bcc 100644 --- a/Fin.Domain/Notifications/Enums/NotificationSeverity.cs +++ b/Fin.Domain/Notifications/Enums/NotificationSeverity.cs @@ -1,10 +1,17 @@ +using Fin.Domain.Global.Decorators; + namespace Fin.Domain.Notifications.Enums; public enum NotificationSeverity { + [FrontTranslateKey("finCore.features.notifications.severity.default")] Default = 0, + [FrontTranslateKey("finCore.features.notifications.severity.success")] Success = 1, + [FrontTranslateKey("finCore.features.notifications.severity.error")] Error = 2, + [FrontTranslateKey("finCore.features.notifications.severity.warning")] Warning = 3, + [FrontTranslateKey("finCore.features.notifications.severity.info")] Info = 4, } \ No newline at end of file diff --git a/Fin.Domain/Notifications/Enums/NotificationWay.cs b/Fin.Domain/Notifications/Enums/NotificationWay.cs index 231b117..cf0dac4 100644 --- a/Fin.Domain/Notifications/Enums/NotificationWay.cs +++ b/Fin.Domain/Notifications/Enums/NotificationWay.cs @@ -1,9 +1,15 @@ -namespace Fin.Domain.Notifications.Enums; +using Fin.Domain.Global.Decorators; + +namespace Fin.Domain.Notifications.Enums; public enum NotificationWay { + [FrontTranslateKey("finCore.features.notifications.ways.snack")] Snack = 0, + [FrontTranslateKey("finCore.features.notifications.ways.message")] Message = 1, + [FrontTranslateKey("finCore.features.notifications.ways.push")] Push = 2, + [FrontTranslateKey("finCore.features.notifications.ways.email")] Email = 3 } \ No newline at end of file diff --git a/Fin.Domain/People/Entities/Person.cs b/Fin.Domain/People/Entities/Person.cs index bea8b17..cb46587 100644 --- a/Fin.Domain/People/Entities/Person.cs +++ b/Fin.Domain/People/Entities/Person.cs @@ -4,7 +4,7 @@ namespace Fin.Domain.People.Entities; -public class Person: IAuditedTenantEntity +public class Person: ILoggableAuditedTenantEntity { public string Name { get; private set; } public bool Inactivated { get; private set; } @@ -37,4 +37,19 @@ public void ToggleInactivated() { Inactivated = !Inactivated; } + + public object GetLog() + { + return new + { + Id, + CreatedAt, + CreatedBy, + UpdatedAt, + UpdatedBy, + TenantId, + Name, + Inactivated + }; + } } \ No newline at end of file diff --git a/Fin.Domain/People/Entities/TitlePerson.cs b/Fin.Domain/People/Entities/TitlePerson.cs index 11e2709..8d348e5 100644 --- a/Fin.Domain/People/Entities/TitlePerson.cs +++ b/Fin.Domain/People/Entities/TitlePerson.cs @@ -4,7 +4,7 @@ namespace Fin.Domain.People.Entities; -public class TitlePerson: ITenant, IAudited +public class TitlePerson: ITenant, IAudited, ILoggable { public Guid PersonId { get; private set; } public virtual Person Person { get; set; } @@ -35,4 +35,19 @@ public void Update(decimal percentage) public Guid UpdatedBy { get; set; } public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } + + public object GetLog() + { + return new + { + CreatedAt, + CreatedBy, + UpdatedAt, + UpdatedBy, + TenantId, + PersonId, + TitleId, + Percentage + }; + } } \ No newline at end of file diff --git a/Fin.Domain/Tenants/Entities/Tenant.cs b/Fin.Domain/Tenants/Entities/Tenant.cs index e838a60..1ad6ccf 100644 --- a/Fin.Domain/Tenants/Entities/Tenant.cs +++ b/Fin.Domain/Tenants/Entities/Tenant.cs @@ -3,7 +3,7 @@ namespace Fin.Domain.Tenants.Entities; -public class Tenant: IEntity +public class Tenant: IEntity, ILoggable { public Guid Id { get; set; } public DateTime CreatedAt { get; private set; } @@ -25,5 +25,17 @@ public Tenant(DateTime now, string timezone, string locale) UpdatedAt = now; Locale = locale ?? "pt-BR"; Timezone = timezone ?? "America/Sao_Paulo"; - } + } + + public object GetLog() + { + return new + { + Id, + CreatedAt, + UpdatedAt, + Locale, + Timezone + }; + } } \ No newline at end of file diff --git a/Fin.Domain/Tenants/Entities/TenantUser.cs b/Fin.Domain/Tenants/Entities/TenantUser.cs index b1ab27f..bd9f489 100644 --- a/Fin.Domain/Tenants/Entities/TenantUser.cs +++ b/Fin.Domain/Tenants/Entities/TenantUser.cs @@ -1,7 +1,18 @@ -namespace Fin.Domain.Tenants.Entities; +using Fin.Domain.Global.Interfaces; -public class TenantUser +namespace Fin.Domain.Tenants.Entities; + +public class TenantUser: ILoggable { public Guid TenantId { get; set; } public Guid UserId { get; set; } + + public object GetLog() + { + return new + { + TenantId, + UserId + }; + } } \ No newline at end of file diff --git a/Fin.Domain/TitleCategories/Entities/TitleCategory.cs b/Fin.Domain/TitleCategories/Entities/TitleCategory.cs index 94d18b6..a5210e6 100644 --- a/Fin.Domain/TitleCategories/Entities/TitleCategory.cs +++ b/Fin.Domain/TitleCategories/Entities/TitleCategory.cs @@ -1,3 +1,4 @@ +using Fin.Domain.Global.Decorators; using Fin.Domain.Global.Interfaces; using Fin.Domain.TitleCategories.Dtos; using Fin.Domain.TitleCategories.Enums; @@ -5,7 +6,7 @@ namespace Fin.Domain.TitleCategories.Entities; -public class TitleCategory: IAuditedTenantEntity +public class TitleCategory: ILoggableAuditedTenantEntity { public bool Inactivated { get; private set; } public string Name { get; private set; } @@ -43,4 +44,23 @@ public void Update(TitleCategoryInput input) } public void ToggleInactivated() => Inactivated = !Inactivated; + + public object GetLog() + { + return new + { + Id, + CreatedAt, + CreatedBy, + UpdatedAt, + UpdatedBy, + TenantId, + Inactivated, + Name, + Icon, + Color, + Type, + TypeDescription = Type.GetTranslateKey(), + }; + } } \ No newline at end of file diff --git a/Fin.Domain/TitleCategories/Entities/TitleTitleCategory.cs b/Fin.Domain/TitleCategories/Entities/TitleTitleCategory.cs index a47326c..baca2ad 100644 --- a/Fin.Domain/TitleCategories/Entities/TitleTitleCategory.cs +++ b/Fin.Domain/TitleCategories/Entities/TitleTitleCategory.cs @@ -1,8 +1,9 @@ +using Fin.Domain.Global.Interfaces; using Fin.Domain.Titles.Entities; namespace Fin.Domain.TitleCategories.Entities; -public class TitleTitleCategory +public class TitleTitleCategory: ILoggable { public Guid TitleId { get; set; } public virtual Title Title { get; set; } @@ -19,4 +20,13 @@ public TitleTitleCategory(Guid categoryId, Guid titleId) TitleId = titleId; TitleCategoryId = categoryId; } + + public object GetLog() + { + return new + { + TitleId, + TitleCategoryId + }; + } } \ No newline at end of file diff --git a/Fin.Domain/TitleCategories/Enums/TitleCategoryType.cs b/Fin.Domain/TitleCategories/Enums/TitleCategoryType.cs index 630c43c..1758610 100644 --- a/Fin.Domain/TitleCategories/Enums/TitleCategoryType.cs +++ b/Fin.Domain/TitleCategories/Enums/TitleCategoryType.cs @@ -1,11 +1,15 @@ +using Fin.Domain.Global.Decorators; using Fin.Domain.Titles.Enums; namespace Fin.Domain.TitleCategories.Enums; public enum TitleCategoryType: byte { + [FrontTranslateKey("finCore.features.titleCategory.type.expense")] Expense = 0, + [FrontTranslateKey("finCore.features.titleCategory.type.income")] Income = 1, + [FrontTranslateKey("finCore.features.titleCategory.type.both")] Both = 2 } diff --git a/Fin.Domain/Titles/Entities/Title.cs b/Fin.Domain/Titles/Entities/Title.cs index 63c17a7..879891a 100644 --- a/Fin.Domain/Titles/Entities/Title.cs +++ b/Fin.Domain/Titles/Entities/Title.cs @@ -1,4 +1,5 @@ using System.Collections.ObjectModel; +using Fin.Domain.Global.Decorators; using Fin.Domain.Global.Interfaces; using Fin.Domain.People.Dtos; using Fin.Domain.People.Entities; @@ -9,9 +10,10 @@ namespace Fin.Domain.Titles.Entities; -public class Title: IAuditedTenantEntity +public class Title: IAuditedTenantEntity, ILoggable { public decimal Value { get; set; } + public TitleType Type { get; set; } public string Description { get; set; } @@ -143,4 +145,27 @@ public List SyncPeopleAndReturnToRemove(List tit return titlePeopleToDelete; } + + public object GetLog() + { + return new + { + Id = Id, + Date = Date, + Description = Description, + + Type = Type, + TypeDescription = Type.GetTranslateKey(), + OriginalValue = Value, + EffectiveValue = EffectiveValue, + PreviousBalance = PreviousBalance, + ResultingBalance = ResultingBalance, + + WalletId = WalletId, + TenantId = TenantId, + + CreatedBy = CreatedBy, + CreatedAt = CreatedAt + }; + } } \ No newline at end of file diff --git a/Fin.Domain/Titles/Enums/TitleType.cs b/Fin.Domain/Titles/Enums/TitleType.cs index 6beb346..e0d2cb7 100644 --- a/Fin.Domain/Titles/Enums/TitleType.cs +++ b/Fin.Domain/Titles/Enums/TitleType.cs @@ -1,7 +1,13 @@ +using System.ComponentModel; +using Fin.Domain.Global.Decorators; + namespace Fin.Domain.Titles.Enums; public enum TitleType: byte { + [FrontTranslateKey("finCore.features.title.type.expense")] Expense = 0, + + [FrontTranslateKey("finCore.features.title.type.income")] Income = 1 } \ No newline at end of file diff --git a/Fin.Domain/Users/Entities/User.cs b/Fin.Domain/Users/Entities/User.cs index 107bcb8..ecb9e10 100644 --- a/Fin.Domain/Users/Entities/User.cs +++ b/Fin.Domain/Users/Entities/User.cs @@ -5,7 +5,7 @@ namespace Fin.Domain.Users.Entities; -public class User: IEntity +public class User: IEntity, ILoggable { public Guid Id { get; set; } @@ -88,4 +88,19 @@ public void MakeAdmin() { IsAdmin = true; } + + public object GetLog() + { + return new + { + Id, + FirstName, + LastName, + DisplayName, + CreatedAt, + UpdatedAt, + IsAdmin, + IsActivity + }; + } } \ No newline at end of file diff --git a/Fin.Domain/Users/Entities/UserDeleteRequest.cs b/Fin.Domain/Users/Entities/UserDeleteRequest.cs index ffe18f3..3fdae09 100644 --- a/Fin.Domain/Users/Entities/UserDeleteRequest.cs +++ b/Fin.Domain/Users/Entities/UserDeleteRequest.cs @@ -2,7 +2,7 @@ namespace Fin.Domain.Users.Entities; -public class UserDeleteRequest : IAuditedEntity +public class UserDeleteRequest : IAuditedEntity, ILoggable { public Guid UserId { get; set; } public virtual User User { get; set; } @@ -44,4 +44,21 @@ public void Abort(Guid userAbortedId, DateTime abortedAt) if (!User.IsActivity) User.ToggleActivity(); } + public object GetLog() + { + return new + { + Id, + CreatedAt, + CreatedBy, + UpdatedAt, + UpdatedBy, + UserId, + UserAbortedId, + AbortedAt, + Aborted, + DeleteRequestedAt, + DeleteEffectivatedAt + }; + } } \ No newline at end of file diff --git a/Fin.Domain/Wallets/Entities/Wallet.cs b/Fin.Domain/Wallets/Entities/Wallet.cs index 4934bf7..a1abc06 100644 --- a/Fin.Domain/Wallets/Entities/Wallet.cs +++ b/Fin.Domain/Wallets/Entities/Wallet.cs @@ -7,7 +7,7 @@ namespace Fin.Domain.Wallets.Entities; -public class Wallet: IAuditedTenantEntity +public class Wallet: ILoggableAuditedTenantEntity { public Guid Id { get; set; } public Guid CreatedBy { get; set; } @@ -66,4 +66,23 @@ public decimal CalculateBalanceAt(DateTime dateTime) return lastTitle?.ResultingBalance ?? InitialBalance; } + + public object GetLog() + { + return new + { + Id, + CreatedBy, + CreatedAt, + UpdatedAt, + UpdatedBy, + TenantId, + Name, + Color, + Icon, + Inactivated, + FinancialInstitutionId, + InitialBalance + }; + } } \ No newline at end of file diff --git a/Fin.Infrastructure/Audits/AuditEntry.cs b/Fin.Infrastructure/Audits/AuditEntry.cs new file mode 100644 index 0000000..06b4235 --- /dev/null +++ b/Fin.Infrastructure/Audits/AuditEntry.cs @@ -0,0 +1,19 @@ +using Fin.Domain.Global.Interfaces; +using Fin.Infrastructure.AmbientDatas; +using Fin.Infrastructure.Audits.Enums; +using Fin.Infrastructure.Audits.Interfaces; +using Microsoft.EntityFrameworkCore.ChangeTracking; + +namespace Fin.Infrastructure.Audits; + +public class AuditEntry(IAmbientData ambientData) +{ + public string EntityName { get; set; } + public string EntityId { get; set; } + public object NewValue { get; set; } + public object OldValue { get; set; } + + public AuditLogAction Action { get; set; } + public Guid UserId { get; set; } = ambientData.UserId.GetValueOrDefault(); + public Guid TenantId { get; set; } = ambientData.TenantId.GetValueOrDefault(); +} \ No newline at end of file diff --git a/Fin.Infrastructure/Audits/AuditLogDocument.cs b/Fin.Infrastructure/Audits/AuditLogDocument.cs new file mode 100644 index 0000000..e20a572 --- /dev/null +++ b/Fin.Infrastructure/Audits/AuditLogDocument.cs @@ -0,0 +1,24 @@ +using Fin.Infrastructure.Audits.Enums; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Fin.Infrastructure.Audits; + +public class AuditLogDocument +{ + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string InternalId { get; set; } + + public string EntityName { get; set; } + public string EntityId { get; set; } + + public AuditLogAction Action { get; set; } + public DateTime DateTime { get; set; } + + public object NewValue { get; set; } + public object OldValue { get; set; } + + public Guid UserId { get; set; } + public Guid TenantId { get; set; } +} \ No newline at end of file diff --git a/Fin.Infrastructure/Audits/AuditLogExtensions.cs b/Fin.Infrastructure/Audits/AuditLogExtensions.cs new file mode 100644 index 0000000..4ca34d0 --- /dev/null +++ b/Fin.Infrastructure/Audits/AuditLogExtensions.cs @@ -0,0 +1,33 @@ +using Fin.Infrastructure.Audits.Interfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using MongoDB.Driver; + +namespace Fin.Infrastructure.Audits; + +public static class AuditLogExtensions +{ + public static IServiceCollection AddAuditLog(this IServiceCollection services, IConfiguration configuration) + { + try + { + BsonSerializer.RegisterSerializer(new GuidSerializer(BsonType.String)); + } + catch (BsonSerializationException) {} + + var settings = MongoClientSettings.FromConnectionString(configuration.GetConnectionString("MongoDbConnection")); + var mongoClient = new MongoClient(settings); + + services.AddSingleton(_ => mongoClient); + services.AddScoped(sp => sp.GetRequiredService().GetDatabase("LogsDB")); + services.AddScoped(); + + services.AddScoped(); + + + return services; + } +} \ No newline at end of file diff --git a/Fin.Infrastructure/Audits/AuditLogInterceptor.cs b/Fin.Infrastructure/Audits/AuditLogInterceptor.cs new file mode 100644 index 0000000..1ebf06a --- /dev/null +++ b/Fin.Infrastructure/Audits/AuditLogInterceptor.cs @@ -0,0 +1,156 @@ +using System.Text.Json; +using Fin.Domain.Global.Interfaces; +using Fin.Infrastructure.AmbientDatas; +using Fin.Infrastructure.Audits.Enums; +using Fin.Infrastructure.Audits.Interfaces; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Diagnostics; + +namespace Fin.Infrastructure.Audits; + +public class AuditLogInterceptor( + IAuditLogService auditService, + IAmbientData ambientData +) : SaveChangesInterceptor +{ + private List _pendingLogs; + + public override InterceptionResult SavingChanges( + DbContextEventData eventData, + InterceptionResult result) + { + CaptureChanges(eventData.Context); + return base.SavingChanges(eventData, result); + } + + public override async ValueTask> SavingChangesAsync( + DbContextEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + { + CaptureChanges(eventData.Context); + return await base.SavingChangesAsync(eventData, result, cancellationToken); + } + + public override int SavedChanges(SaveChangesCompletedEventData eventData, int result) + { + SaveLogs(); + return base.SavedChanges(eventData, result); + } + + public override async ValueTask SavedChangesAsync( + SaveChangesCompletedEventData eventData, + int result, + CancellationToken cancellationToken = default) + { + await SaveLogsAsync(); + return await base.SavedChangesAsync(eventData, result, cancellationToken); + } + + private void CaptureChanges(DbContext context) + { + _pendingLogs = new List(); + + var entries = context.ChangeTracker.Entries() + .Where(e => e.Entity is ILoggable && + (e.State == EntityState.Added || + e.State == EntityState.Modified || + e.State == EntityState.Deleted)) + .ToList(); + + foreach (var entry in entries) + { + if (entry.Entity is not ILoggable loggable) continue; + + var entityType = entry.Entity.GetType(); + var entityName = entityType.Name; + + var logEntry = new AuditEntry(ambientData) + { + EntityName = entityName, + EntityId = GetEntityId(entry), + }; + + switch (entry.State) + { + case EntityState.Added: + logEntry.Action = AuditLogAction.Created; + logEntry.NewValue = loggable.GetLog(); + logEntry.OldValue = null; + break; + + case EntityState.Modified: + logEntry.Action = AuditLogAction.Updated; + logEntry.NewValue = loggable.GetLog(); + logEntry.OldValue = GetOriginalValues(entry); + break; + + case EntityState.Deleted: + logEntry.Action = AuditLogAction.Deleted; + logEntry.NewValue = null; + logEntry.OldValue = loggable.GetLog(); + break; + } + + _pendingLogs.Add(logEntry); + } + } + + private string GetEntityId(EntityEntry entry) + { + var keyValues = entry.Properties + .Where(p => p.Metadata.IsKey()) + .Select(p => new { p.Metadata.Name, Value = p.CurrentValue }) + .ToList(); + + if (keyValues.Count == 1) + { + return keyValues[0].Value?.ToString() ?? string.Empty; + } + + var compositeKey = keyValues.ToDictionary( + k => k.Name, + k => k.Value + ); + return JsonSerializer.Serialize(compositeKey); + } + + private object GetOriginalValues(EntityEntry entry) + { + var loggable = entry.Entity as ILoggable; + if (loggable == null) return null; + + var originalEntity = Activator.CreateInstance(entry.Entity.GetType()); + + foreach (var property in entry.Properties) + { + var propInfo = entry.Entity.GetType().GetProperty(property.Metadata.Name); + if (propInfo != null && propInfo.CanWrite) + { + propInfo.SetValue(originalEntity, property.OriginalValue); + } + } + + if (originalEntity is ILoggable loggableOriginal) + { + return loggableOriginal.GetLog(); + } + + return null; + } + + private async Task SaveLogsAsync() + { + if (_pendingLogs.Count == 0) return; + await auditService.LogAsync(_pendingLogs); + _pendingLogs.Clear(); + } + + private void SaveLogs() + { + if (_pendingLogs.Count == 0) return; + auditService.Log(_pendingLogs); + _pendingLogs.Clear(); + } +} \ No newline at end of file diff --git a/Fin.Infrastructure/Audits/Enums/AuditLogAction.cs b/Fin.Infrastructure/Audits/Enums/AuditLogAction.cs new file mode 100644 index 0000000..fc9cff8 --- /dev/null +++ b/Fin.Infrastructure/Audits/Enums/AuditLogAction.cs @@ -0,0 +1,8 @@ +namespace Fin.Infrastructure.Audits.Enums; + +public enum AuditLogAction +{ + Created = 0, + Updated = 1, + Deleted = 2 +} \ No newline at end of file diff --git a/Fin.Infrastructure/Audits/Interfaces/IAuditLogService.cs b/Fin.Infrastructure/Audits/Interfaces/IAuditLogService.cs new file mode 100644 index 0000000..e6daf75 --- /dev/null +++ b/Fin.Infrastructure/Audits/Interfaces/IAuditLogService.cs @@ -0,0 +1,7 @@ +namespace Fin.Infrastructure.Audits.Interfaces; + +public interface IAuditLogService +{ + Task LogAsync(List logs); + void Log(List logs); +} \ No newline at end of file diff --git a/Fin.Infrastructure/Audits/MongoAuditLogService.cs b/Fin.Infrastructure/Audits/MongoAuditLogService.cs new file mode 100644 index 0000000..da55bbd --- /dev/null +++ b/Fin.Infrastructure/Audits/MongoAuditLogService.cs @@ -0,0 +1,64 @@ +using Fin.Infrastructure.Audits.Interfaces; +using Fin.Infrastructure.DateTimes; +using Microsoft.Extensions.Logging; +using MongoDB.Driver; + +namespace Fin.Infrastructure.Audits; + +public class MongoAuditLogService( + IMongoDatabase database, + IDateTimeProvider dateTimeProvider, + ILogger logger + ) : IAuditLogService +{ + private readonly IMongoCollection _collection = database.GetCollection("audit_logs"); + + public async Task LogAsync(List logs) + { + try + { + var entitiesLog = logs.Select(log => new AuditLogDocument + { + EntityName = log.EntityName, + EntityId = log.EntityId, + NewValue = log.NewValue, + OldValue = log.OldValue, + Action = log.Action, + DateTime = dateTimeProvider.UtcNow(), + UserId = log.UserId, + TenantId = log.TenantId + }); + + await _collection.InsertManyAsync(entitiesLog); + } + catch (Exception ex) + { + logger.LogError("Error on saving log: {ExMessage}", ex.Message); + } + } + + public void Log(List logs) + { + try + { + + var entitiesLog = logs.Select(log => new AuditLogDocument + { + EntityName = log.EntityName, + EntityId = log.EntityId, + NewValue = log.NewValue, + OldValue = log.OldValue, + Action = log.Action, + DateTime = dateTimeProvider.UtcNow(), + UserId = log.UserId, + TenantId = log.TenantId + }); + + _collection.InsertMany(entitiesLog); + } + catch (Exception ex) + { + logger.LogError("Error on saving log: {ExMessage}", ex.Message); + } + } +} \ No newline at end of file diff --git a/Fin.Infrastructure/Database/Extensions/AddDatabaseExtension.cs b/Fin.Infrastructure/Database/Extensions/AddDatabaseExtension.cs index ecd6708..aaadffb 100644 --- a/Fin.Infrastructure/Database/Extensions/AddDatabaseExtension.cs +++ b/Fin.Infrastructure/Database/Extensions/AddDatabaseExtension.cs @@ -1,4 +1,5 @@ -using Fin.Infrastructure.Database.Interceptors; +using Fin.Infrastructure.Audits; +using Fin.Infrastructure.Database.Interceptors; using Fin.Infrastructure.Database.Repositories; using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; @@ -19,11 +20,13 @@ public static IServiceCollection AddDatabase(this IServiceCollection services, I { var auditedEntityInterceptor = serviceProvider.GetRequiredService(); var tenantEntityInterceptor = serviceProvider.GetRequiredService(); + var auditLogInterceptor = serviceProvider.GetRequiredService(); op .UseNpgsql(configuration.GetConnectionString("DefaultConnection")) .AddInterceptors(auditedEntityInterceptor) - .AddInterceptors(tenantEntityInterceptor); + .AddInterceptors(tenantEntityInterceptor) + .AddInterceptors(auditLogInterceptor); }) .AddScoped(typeof(IRepository<>), typeof(Repository<>));; diff --git a/Fin.Infrastructure/Extensions/AddInfrastructureExtension.cs b/Fin.Infrastructure/Extensions/AddInfrastructureExtension.cs index e95fb8c..b25320a 100644 --- a/Fin.Infrastructure/Extensions/AddInfrastructureExtension.cs +++ b/Fin.Infrastructure/Extensions/AddInfrastructureExtension.cs @@ -1,4 +1,5 @@ using Fin.Infrastructure.AmbientDatas; +using Fin.Infrastructure.Audits; using Fin.Infrastructure.Authentications; using Fin.Infrastructure.AutoServices.Extensions; using Fin.Infrastructure.BackgroundJobs; @@ -26,6 +27,7 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi .AddScoped() .AddScoped() .AddScoped() + .AddAuditLog(configuration) .AddDatabase(configuration) .AddFirebase(configuration) .AddSeeders() diff --git a/Fin.Infrastructure/Fin.Infrastructure.csproj b/Fin.Infrastructure/Fin.Infrastructure.csproj index 7d78e06..d10c34f 100644 --- a/Fin.Infrastructure/Fin.Infrastructure.csproj +++ b/Fin.Infrastructure/Fin.Infrastructure.csproj @@ -26,6 +26,7 @@ + diff --git a/docker-compose.yml b/docker-compose.yml index baa4cf6..4baca1f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,6 +32,15 @@ services: interval: 10s timeout: 3s retries: 5 + + mongodb: + image: mongo:latest + container_name: mongodb + restart: always + ports: + - "27017:27017" + volumes: + - mongo-data:/data/db volumes: postgres_data: