diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3729ff0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/AutoMapperProfile.cs b/AutoMapperProfile.cs new file mode 100644 index 0000000..36e2151 --- /dev/null +++ b/AutoMapperProfile.cs @@ -0,0 +1,87 @@ +using AutoMapper; +using GsServer.Protobufs; +using GsServer.Models; + +namespace GsServer.MapperProfile; + +public class AutoMapperProfile : Profile +{ + public AutoMapperProfile() + { + CreateMap() + .ForMember(dest => dest.AttendeesStatuses, src => src.MapFrom(src => src.AttendeesStatuses)); + CreateMap() + .ForMember(dest => dest.AttendeesStatuses, src => src.MapFrom(src => src.AttendeesStatuses)); + CreateMap(); + + CreateMap() + .ForMember(dest => dest.Dependents, src => src.MapFrom(src => src.Dependents)); + CreateMap() + .ForMember(dest => dest.Dependents, src => src.MapFrom(src => src.Dependents)); + CreateMap() + .ForMember(dest => dest.Dependents, src => src.MapFrom(src => src.Dependents)); + + CreateMap() + .ForMember(dest => dest.ClassDays, src => src.MapFrom(src => src.ClassDays)); + CreateMap() + .ForMember(dest => dest.ClassDays, src => src.MapFrom(src => src.ClassDays)); + + CreateMap(); + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + + // TODO + // CreateMap(); + // CreateMap(); + // CreateMap(); + + CreateMap() + .ForMember(dest => dest.Installments, src => src.MapFrom(src => src.Installments)); + CreateMap() + .ForMember(dest => dest.Installments, src => src.MapFrom(src => src.Installments)); + CreateMap(); + + CreateMap(); + + CreateMap() + .ForMember(dest => dest.Variants, src => src.MapFrom(src => src.Variants)); + CreateMap() + .ForMember(dest => dest.Variants, src => src.MapFrom(src => src.Variants)); + CreateMap(); + CreateMap(); + // TODO + // CreateMap(); + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap() + .ForMember(dest => dest.ItemsReturned, src => src.MapFrom(src => src.ItemsReturned)); + CreateMap() + .ForMember(dest => dest.ItemsReturned, src => src.MapFrom(src => src.ItemsReturned)); + CreateMap(); + + CreateMap() + .ForMember(dest => dest.ItemsSold, src => src.MapFrom(src => src.ItemsSold)); + CreateMap() + .ForMember(dest => dest.ItemsSold, src => src.MapFrom(src => src.ItemsSold)); + CreateMap(); + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap(); + } +} \ No newline at end of file diff --git a/BackgroundJobs/SubscriptionInvoiceBackgroundJob.cs b/BackgroundJobs/SubscriptionInvoiceBackgroundJob.cs index bea0458..b279076 100644 --- a/BackgroundJobs/SubscriptionInvoiceBackgroundJob.cs +++ b/BackgroundJobs/SubscriptionInvoiceBackgroundJob.cs @@ -6,13 +6,12 @@ namespace GsServer.BackgroundServices; /// the customer should pay and may include detailed client information. /// An invoice may indicate the existence of credit, as payment is not immediate. /// -public class SubscriptionInvoiceBackgroundJob : BackgroundService +public class SubscriptionInvoiceBackgroundJob +( + ILogger logger +) : BackgroundService { - private readonly ILogger _logger; - public SubscriptionInvoiceBackgroundJob(ILogger logger) - { - _logger = logger; - } + private readonly ILogger _logger = logger; /// /// Generates invoices in the background, runs daily at 12 AM UTC time. @@ -25,27 +24,42 @@ protected async override Task ExecuteAsync(CancellationToken stoppingToken) { try { - _logger.LogInformation("Background subscription invoice service started"); + _logger.LogInformation( + "Executing Background Service: {BackgroundServiceName} started", + typeof(SubscriptionInvoiceBackgroundJob).Name + ); // Delay for Until 12 AM UTC time asynchronously. // 9 AM BRT (Brasília Time), UTC/GMT -3 hours. await Task.Delay((int)Math.Ceiling(MillisecondsUntilTwelveAmUtc()), stoppingToken); - _logger.LogInformation("Background subscription invoice service, generating invoices"); - // TODO + // TODO store job in DB + _logger.LogInformation( + "Executing Background Service: {BackgroundServiceName}, generating invoices", + typeof(SubscriptionInvoiceBackgroundJob).Name + ); + // TODO generate invoices - _logger.LogInformation("Background subscription invoice service, sending emails"); - // TODO + _logger.LogInformation( + "Executing Background Service: {BackgroundServiceName}, sending notifications", + typeof(SubscriptionInvoiceBackgroundJob).Name + ); + // TODO alert send emails or push notifications - _logger.LogInformation("Background subscription invoice service work completed"); + _logger.LogInformation( + "Executing Background Service: {BackgroundServiceName} work completed", + typeof(SubscriptionInvoiceBackgroundJob).Name + ); + // TODO Update `isCompleted = true;` in DB } catch (Exception Exception) { _logger.LogError( - "Background invoice service stopped because of an error {Exception}", + "Executing Background Service: {BackgroundServiceName} stopped because of an error {Exception}", + typeof(SubscriptionInvoiceBackgroundJob).Name, Exception ); - // break; // TODO do I need to break? + break; // TODO do I need to break? } } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 473785d..e876be0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,10 +14,11 @@ Todas as alterações notáveis ​​neste projeto serão documentadas neste ar O formato é baseado em [Keep a Changelog (PT-BR)](https://keepachangelog.com/pt-BR/1.0.0/), e este projeto adere a [Versionamento Semântico (PT-BR)](https://semver.org/lang/pt-BR/). - +## 1.0.0-RC-1 (Apr 18, 2024) + +- Finish basic implementation of gRPC services with the new protobufs, some stuff will be implemented later, but it's not needed for critical functionality. +- Add Middleware `GlobalExceptionHandler` to be able to log and return even when unknown errors occur, but at the moment does not work with gRPC. +- Update protobufs from `a1d46b748` to `0adaf3a56`. ## 0.11.0-BETA (Mar 22, 2024) @@ -27,8 +28,8 @@ e este projeto adere a [Versionamento Semântico (PT-BR)](https://semver.org/lan ## 0.10.0-BETA (Mar 22, 2024) - Create "RequestTracerId" in every log using `HttpContext.TraceIdentifier`. -- Add Untested `AwsS3Service.cs`. -- Add Untested `AwsCloudWatch` for logging. +- Add **Untested** `AwsS3Service.cs`. +- Add **Untested** `AwsCloudWatch` for logging. - Implement "DecimalValue" conversion to "C# Decimal". - Update protobufs from `9e568008b` to `617c788f5`. diff --git a/CustomTypes/CalendarDate.cs b/CustomTypes/CalendarDate.cs new file mode 100644 index 0000000..48bb798 --- /dev/null +++ b/CustomTypes/CalendarDate.cs @@ -0,0 +1,30 @@ +namespace GsServer.Protobufs; + +public partial class CalendarDate +{ + public CalendarDate(int year, int month, int day) + { + Year = year; + Month = month; + Day = day; + } + + public static implicit operator DateOnly(CalendarDate date) + { + return new DateOnly( + date.Year, + date.Month, + date.Day + ); + } + + public static implicit operator CalendarDate(DateOnly date) + { + return new CalendarDate() + { + Day = date.Day, + Month = date.Month, + Year = date.Year + }; + } +} diff --git a/CustomTypes/DayOfWeek.cs b/CustomTypes/DayOfWeek.cs new file mode 100644 index 0000000..da2b8df --- /dev/null +++ b/CustomTypes/DayOfWeek.cs @@ -0,0 +1,31 @@ +// namespace GsServer.Protobufs; + +// public partial class DayOfWeek +// { +// public DayOfWeek() +// { +// // TODO +// } + +// public static implicit operator DayOfWeek(Protobufs.DayOfWeek time) +// { +// return new System.DayOfWeek(); +// } + +// public static implicit operator DayOfWeek(System.DayOfWeek time) +// { +// return new Protobufs.DayOfWeek(); +// } + +// readonly Dictionary DayOfWeekMapping = new() +// { +// { DayOfWeek.Sunday, System.DayOfWeek.Sunday }, +// { DayOfWeek.Monday, System.DayOfWeek.Monday }, +// { DayOfWeek.Tuesday, System.DayOfWeek.Tuesday }, +// { DayOfWeek.Wednesday, System.DayOfWeek.Wednesday }, +// { DayOfWeek.Thursday, System.DayOfWeek.Thursday }, +// { DayOfWeek.Friday, System.DayOfWeek.Friday }, +// { DayOfWeek.Saturday, System.DayOfWeek.Saturday } +// }; +// } + diff --git a/CustomTypes/TimeOfDay.cs b/CustomTypes/TimeOfDay.cs new file mode 100644 index 0000000..1fbfcf1 --- /dev/null +++ b/CustomTypes/TimeOfDay.cs @@ -0,0 +1,27 @@ +namespace GsServer.Protobufs; + +public partial class TimeOfDay +{ + public TimeOfDay(int hour, int minute) + { + Hour = hour; + Minute = minute; + } + + public static implicit operator TimeOnly(TimeOfDay time) + { + return new TimeOnly( + time.Hour, + time.Minute + ); + } + + public static implicit operator TimeOfDay(TimeOnly time) + { + return new TimeOfDay() + { + Hour = time.Hour, + Minute = time.Minute + }; + } +} diff --git a/Data/DatabaseContext.cs b/Data/DatabaseContext.cs index 2b3e9ea..14a4787 100644 --- a/Data/DatabaseContext.cs +++ b/Data/DatabaseContext.cs @@ -1,6 +1,4 @@ global using Microsoft.EntityFrameworkCore; -using GsServer; -using GsServer.EntityConfigurations; using GsServer.Models; public class DatabaseContext( @@ -13,39 +11,50 @@ IConfiguration configuration protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); + /// This line should never be used in production and is only for debugging + // optionsBuilder.LogTo(str => Debug.WriteLine(str)); optionsBuilder.UseNpgsql(_configuration.GetConnectionString("db")); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.UseSerialColumns(); - modelBuilder.ApplyConfiguration(new AttendanceConfiguration()); - modelBuilder.ApplyConfiguration(new CustomerConfiguration()); - modelBuilder.ApplyConfiguration(new DisciplineConfiguration()); - modelBuilder.ApplyConfiguration(new OrderConfiguration()); - modelBuilder.ApplyConfiguration(new PersonConfiguration()); - modelBuilder.ApplyConfiguration(new ProductConfiguration()); - modelBuilder.ApplyConfiguration(new RefreshTokenConfiguration()); - modelBuilder.ApplyConfiguration(new SaleConfiguration()); - modelBuilder.ApplyConfiguration(new InstructorConfiguration()); - modelBuilder.ApplyConfiguration(new UserConfiguration()); + // modelBuilder.ApplyConfiguration(new AttendanceConfiguration()); + // modelBuilder.ApplyConfiguration(new BackgroundJobConfiguration()); + // modelBuilder.ApplyConfiguration(new CustomerConfiguration()); + // modelBuilder.ApplyConfiguration(new DisciplineConfiguration()); + // modelBuilder.ApplyConfiguration(new InstructorConfiguration()); + // modelBuilder.ApplyConfiguration(new NotificationConfiguration()); + // modelBuilder.ApplyConfiguration(new OrderConfiguration()); + // modelBuilder.ApplyConfiguration(new PaymentConfiguration()); + // modelBuilder.ApplyConfiguration(new PersonConfiguration()); + // modelBuilder.ApplyConfiguration(new ProductConfiguration()); + // modelBuilder.ApplyConfiguration(new PromotionConfiguration()); + // modelBuilder.ApplyConfiguration(new RefreshTokenConfiguration()); + // modelBuilder.ApplyConfiguration(new ReturnConfiguration()); + // modelBuilder.ApplyConfiguration(new SaleConfiguration()); + // modelBuilder.ApplyConfiguration(new SaleBillingConfiguration()); + // modelBuilder.ApplyConfiguration(new SubscriptionConfiguration()); + // modelBuilder.ApplyConfiguration(new SubscriptionBillingConfiguration()); + // modelBuilder.ApplyConfiguration(new UserConfiguration()); } - public DbSet Attendances => Set(); - public DbSet BackgroundJobs => Set(); - public DbSet Customers => Set(); - public DbSet Disciplines => Set(); - public DbSet Instructors => Set(); - public DbSet Notifications => Set(); - public DbSet Orders => Set(); - public DbSet Payments => Set(); - public DbSet People => Set(); - public DbSet Products => Set(); - public DbSet Promotions => Set(); - public DbSet RefreshTokens => Set(); - public DbSet Sales => Set(); - public DbSet SalesBilling => Set(); - public DbSet Subscriptions => Set(); - public DbSet SubscriptionsBilling => Set(); - public DbSet Users => Set(); + public DbSet Attendances => Set(); + public DbSet BackgroundJobs => Set(); + public DbSet Customers => Set(); + public DbSet Disciplines => Set(); + public DbSet Instructors => Set(); + public DbSet Notifications => Set(); + public DbSet Orders => Set(); + public DbSet Payments => Set(); + public DbSet Persons => Set(); // Yes, it could also be called "People" + public DbSet Products => Set(); + public DbSet Promotions => Set(); + public DbSet RefreshTokens => Set(); + public DbSet Returns => Set(); + public DbSet Sales => Set(); + public DbSet SaleBillings => Set(); + public DbSet Subscriptions => Set(); + public DbSet SubscriptionBillings => Set(); + public DbSet Users => Set(); } diff --git a/Data/EntityConfigurations/AttendanceConfiguration.cs b/Data/EntityConfigurations/AttendanceConfiguration.cs deleted file mode 100644 index 9b35a81..0000000 --- a/Data/EntityConfigurations/AttendanceConfiguration.cs +++ /dev/null @@ -1,12 +0,0 @@ -using GsServer.Models; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace GsServer.EntityConfigurations; - -public class AttendanceConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder typeBuilder) - { - - } -} \ No newline at end of file diff --git a/Data/EntityConfigurations/BackgroundJobConfiguration.cs b/Data/EntityConfigurations/BackgroundJobConfiguration.cs deleted file mode 100644 index 5defd3d..0000000 --- a/Data/EntityConfigurations/BackgroundJobConfiguration.cs +++ /dev/null @@ -1,12 +0,0 @@ -using GsServer.Models; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace GsServer.EntityConfigurations; - -public class BackgroundJobConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder typeBuilder) - { - - } -} \ No newline at end of file diff --git a/Data/EntityConfigurations/CustomerConfiguration.cs b/Data/EntityConfigurations/CustomerConfiguration.cs deleted file mode 100644 index 5b4a3f4..0000000 --- a/Data/EntityConfigurations/CustomerConfiguration.cs +++ /dev/null @@ -1,12 +0,0 @@ -using GsServer.Models; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace GsServer.EntityConfigurations; - -public class CustomerConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder typeBuilder) - { - - } -} \ No newline at end of file diff --git a/Data/EntityConfigurations/DisciplineConfiguration.cs b/Data/EntityConfigurations/DisciplineConfiguration.cs deleted file mode 100644 index be3dab8..0000000 --- a/Data/EntityConfigurations/DisciplineConfiguration.cs +++ /dev/null @@ -1,13 +0,0 @@ -using GsServer.Models; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace GsServer.EntityConfigurations; - -public class DisciplineConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder typeBuilder) - { - typeBuilder.HasIndex(x => x.Name).IsUnique(); - typeBuilder.Property(x => x.Name).HasColumnType("varchar(20)"); - } -} \ No newline at end of file diff --git a/Data/EntityConfigurations/InstructorConfiguration.cs b/Data/EntityConfigurations/InstructorConfiguration.cs deleted file mode 100644 index d873d3b..0000000 --- a/Data/EntityConfigurations/InstructorConfiguration.cs +++ /dev/null @@ -1,12 +0,0 @@ -using GsServer.Models; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace GsServer.EntityConfigurations; - -public class InstructorConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder typeBuilder) - { - - } -} \ No newline at end of file diff --git a/Data/EntityConfigurations/NotificationConfiguration.cs b/Data/EntityConfigurations/NotificationConfiguration.cs deleted file mode 100644 index a8bea01..0000000 --- a/Data/EntityConfigurations/NotificationConfiguration.cs +++ /dev/null @@ -1,12 +0,0 @@ -using GsServer.Models; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace GsServer.EntityConfigurations; - -public class NotificationConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder typeBuilder) - { - - } -} \ No newline at end of file diff --git a/Data/EntityConfigurations/OrderConfiguration.cs b/Data/EntityConfigurations/OrderConfiguration.cs deleted file mode 100644 index 340243c..0000000 --- a/Data/EntityConfigurations/OrderConfiguration.cs +++ /dev/null @@ -1,12 +0,0 @@ -using GsServer.Models; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace GsServer.EntityConfigurations; - -public class OrderConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder typeBuilder) - { - - } -} \ No newline at end of file diff --git a/Data/EntityConfigurations/PaymentConfiguration.cs b/Data/EntityConfigurations/PaymentConfiguration.cs deleted file mode 100644 index 162393b..0000000 --- a/Data/EntityConfigurations/PaymentConfiguration.cs +++ /dev/null @@ -1,12 +0,0 @@ -using GsServer.Models; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace GsServer.EntityConfigurations; - -public class PaymentConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder typeBuilder) - { - - } -} \ No newline at end of file diff --git a/Data/EntityConfigurations/PersonConfiguration.cs b/Data/EntityConfigurations/PersonConfiguration.cs deleted file mode 100644 index 4bb3b2a..0000000 --- a/Data/EntityConfigurations/PersonConfiguration.cs +++ /dev/null @@ -1,16 +0,0 @@ -using GsServer.Models; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace GsServer.EntityConfigurations; - -public class PersonConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder typeBuilder) - { - typeBuilder.HasIndex(x => x.Name).IsUnique(); - typeBuilder.Property(x => x.Name).HasColumnType("varchar(150)"); - typeBuilder.Property(x => x.MobilePhoneNumber).HasColumnType("varchar(16)"); - typeBuilder.Property(x => x.BirthDate).HasColumnType("varchar(10)"); - typeBuilder.Property(x => x.Cpf).HasColumnType("varchar(14)"); - } -} \ No newline at end of file diff --git a/Data/EntityConfigurations/ProductConfiguration.cs b/Data/EntityConfigurations/ProductConfiguration.cs deleted file mode 100644 index 7efb449..0000000 --- a/Data/EntityConfigurations/ProductConfiguration.cs +++ /dev/null @@ -1,12 +0,0 @@ -using GsServer.Models; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace GsServer.EntityConfigurations; - -public class ProductConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder typeBuilder) - { - - } -} \ No newline at end of file diff --git a/Data/EntityConfigurations/PromotionConfiguration.cs b/Data/EntityConfigurations/PromotionConfiguration.cs deleted file mode 100644 index 581e58a..0000000 --- a/Data/EntityConfigurations/PromotionConfiguration.cs +++ /dev/null @@ -1,12 +0,0 @@ -using GsServer.Models; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace GsServer.EntityConfigurations; - -public class PromotionConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder typeBuilder) - { - - } -} \ No newline at end of file diff --git a/Data/EntityConfigurations/RefreshTokenConfiguration.cs b/Data/EntityConfigurations/RefreshTokenConfiguration.cs deleted file mode 100644 index a601cbb..0000000 --- a/Data/EntityConfigurations/RefreshTokenConfiguration.cs +++ /dev/null @@ -1,12 +0,0 @@ -using GsServer.Models; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace GsServer.EntityConfigurations; - -public class RefreshTokenConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder typeBuilder) - { - - } -} \ No newline at end of file diff --git a/Data/EntityConfigurations/SaleBillingConfiguration.cs b/Data/EntityConfigurations/SaleBillingConfiguration.cs deleted file mode 100644 index fe98951..0000000 --- a/Data/EntityConfigurations/SaleBillingConfiguration.cs +++ /dev/null @@ -1,12 +0,0 @@ -using GsServer.Models; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace GsServer.EntityConfigurations; - -public class SaleBillingConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder typeBuilder) - { - - } -} \ No newline at end of file diff --git a/Data/EntityConfigurations/SaleConfiguration.cs b/Data/EntityConfigurations/SaleConfiguration.cs deleted file mode 100644 index 0c3cd44..0000000 --- a/Data/EntityConfigurations/SaleConfiguration.cs +++ /dev/null @@ -1,13 +0,0 @@ -using GsServer.Models; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace GsServer.EntityConfigurations; - -public class SaleConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder typeBuilder) - { - typeBuilder.Property(x => x.Comments).HasColumnType("varchar(80)"); - - } -} \ No newline at end of file diff --git a/Data/EntityConfigurations/SubscriptionBillingConfiguration.cs b/Data/EntityConfigurations/SubscriptionBillingConfiguration.cs deleted file mode 100644 index bd48a68..0000000 --- a/Data/EntityConfigurations/SubscriptionBillingConfiguration.cs +++ /dev/null @@ -1,12 +0,0 @@ -using GsServer.Models; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace GsServer.EntityConfigurations; - -public class SubscriptionBillingConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder typeBuilder) - { - - } -} \ No newline at end of file diff --git a/Data/EntityConfigurations/SubscriptionConfiguration.cs b/Data/EntityConfigurations/SubscriptionConfiguration.cs deleted file mode 100644 index 5f11675..0000000 --- a/Data/EntityConfigurations/SubscriptionConfiguration.cs +++ /dev/null @@ -1,12 +0,0 @@ -using GsServer.Models; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace GsServer.EntityConfigurations; - -public class SubscriptionConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder typeBuilder) - { - - } -} \ No newline at end of file diff --git a/Data/EntityConfigurations/UserConfiguration.cs b/Data/EntityConfigurations/UserConfiguration.cs deleted file mode 100644 index 31390f3..0000000 --- a/Data/EntityConfigurations/UserConfiguration.cs +++ /dev/null @@ -1,12 +0,0 @@ -using GsServer.Models; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace GsServer.EntityConfigurations; - -public class UserConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder typeBuilder) - { - - } -} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..64140da --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src +COPY ["GsServer.csproj", "GsServer/"] +RUN dotnet restore "GsServer.csproj" +COPY . . +WORKDIR "/src/GsServer" +RUN dotnet build "GsServer.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "GsServer.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "GsServer.dll"] \ No newline at end of file diff --git a/Middlewares/GlobalExceptionHandler.cs b/Middlewares/GlobalExceptionHandler.cs new file mode 100644 index 0000000..d18ed19 --- /dev/null +++ b/Middlewares/GlobalExceptionHandler.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Mvc; + +namespace GsServer.Middlewares; + +/// +/// This is a Middleware for unexpected errors. +/// +// Exceptions are ONLY for exceptional circumstances, they are good use for I/O +// errors, don't use exceptions for control flow, because exceptions are slow. +public class GlobalExceptionHandler( + ILogger logger + ) : IExceptionHandler +{ + private readonly ILogger _logger = logger; + public async ValueTask TryHandleAsync( + HttpContext httpContext, + Exception exception, + CancellationToken cancellationToken + ) + { + _logger.LogError(exception, "Unknown Internal Server Error"); + + /// Problem Details for HTTP APIs (Request for Comments: 7807), it proposes + /// a standardized way to communicate errors in HTTP APIs, instead of just + /// sending a generic error code, it can use a specific format outlined in + /// RFC 7807 to provide more details about the problem. This helps + /// developers using the API understand what went wrong and how to fix it. + /// https://datatracker.ietf.org/doc/html/rfc7807 + + ProblemDetails details = new() + { + Status = StatusCodes.Status500InternalServerError, + Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1", + Title = "Internal Server Error", + /// The following message translates to: + /// "Something went wrong on our end. We apologize for the inconvenience, + /// our team has been notified and is working to fix the issue. Please try + /// again later." + Detail = "Algo deu errado do nosso lado. Pedimos desculpas pelo transtorno, nossa equipe foi notificada e está trabalhando para solucionar o problema. Por favor, tente novamente mais tarde.", + }; + + httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; + httpContext.Response.ContentType = "application/json"; + await httpContext.Response.WriteAsJsonAsync(details, cancellationToken: cancellationToken); + + /// TODO Is there a way of doing RFC 7807 with gRPC? something like + // throw new RpcException(new Status( + // StatusCode.Internal, $"Algo deu errado do nosso lado. Pedimos desculpas pelo transtorno, nossa equipe foi notificada e está trabalhando para solucionar o problema. Por favor, tente novamente mais tarde." + // )); + + return true; + } +} \ No newline at end of file diff --git a/Models/Attendance.cs b/Models/Attendance.cs new file mode 100644 index 0000000..4980b15 --- /dev/null +++ b/Models/Attendance.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace GsServer.Models; + +[Index(nameof(Date), IsUnique = false)] +public class Attendance +{ + public int AttendanceId { get; init; } + public required int DisciplineId { get; init; } + public virtual Discipline Discipline { get; init; } = null!; + public required DateOnly Date { get; set; } + public required ICollection AttendeesStatuses { get; set; } + [MaxLength(500)] + public string Observations { get; set; } = string.Empty; + public DateTime CreatedAt { get; init; } = DateTime.UtcNow; + [Required(ErrorMessage = "CreatedBy é obrigatório")] + public int? CreatedBy { get; set; } +} diff --git a/Models/AttendanceAttendeeStatus.cs b/Models/AttendanceAttendeeStatus.cs new file mode 100644 index 0000000..d5d6461 --- /dev/null +++ b/Models/AttendanceAttendeeStatus.cs @@ -0,0 +1,11 @@ +namespace GsServer.Models; + +public class AttendanceAttendeeStatus +{ + public int AttendanceAttendeeStatusId { get; init; } + public int AttendanceId { get; init; } + public required int PersonId { get; init; } + public virtual Person Person { get; init; } = null!; + public required bool IsPresent { get; init; } + public bool IsAbsent() => !IsPresent; +} diff --git a/Models/AttendanceModel.cs b/Models/AttendanceModel.cs deleted file mode 100644 index 8a656e5..0000000 --- a/Models/AttendanceModel.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace GsServer.Models; - -public class AttendanceModel -{ - public int AttendanceId { get; init; } - public required int DisciplineId { get; init; } - public required DateOnly Date { get; set; } - public List PresentStudentIds { get; set; } = []; - public List AbsentStudentIds { get; set; } = []; - public string Observations { get; set; } = string.Empty; - public DateTime CreatedAt { get; init; } = DateTime.UtcNow; - public required int CreatedBy { get; init; } -} diff --git a/Models/BackgroundJobModel.cs b/Models/BackgroundJobModel.cs deleted file mode 100644 index 2489c0b..0000000 --- a/Models/BackgroundJobModel.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace GsServer.Models; - -public class BackgroundJobModel -{ - public int BackgroundJobId { get; init; } - public required string Name { get; set; } - public bool HasFinished { get; set; } = false; - public DateTime CreatedAt { get; init; } = DateTime.UtcNow; -} \ No newline at end of file diff --git a/Models/BackgroundJobStatus.cs b/Models/BackgroundJobStatus.cs new file mode 100644 index 0000000..ea6b981 --- /dev/null +++ b/Models/BackgroundJobStatus.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace GsServer.Models; + +[Index(nameof(HasFinished), IsUnique = false)] +public class BackgroundJobStatus +{ + public int BackgroundJobStatusId { get; init; } + [Length(4, 64, ErrorMessage = "O nome deve ter entre 4 e 64 caracteres")] + public required string Name { get; set; } + public bool HasFinished { get; set; } = false; + public DateTime CreatedAt { get; init; } = DateTime.UtcNow; +} \ No newline at end of file diff --git a/Models/Customer.cs b/Models/Customer.cs new file mode 100644 index 0000000..dc317cf --- /dev/null +++ b/Models/Customer.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace GsServer.Models; + +public class Customer +{ + public int CustomerId { get; init; } + public int? UserId { get; set; } + public virtual User User { get; set; } = null!; + public required Person Person { get; set; } + public required ICollection Dependents { get; set; } + [Length(4, 64, ErrorMessage = "O Endereço deve ter entre 4 e 64 caracteres")] + public required string BillingAddress { get; set; } + [MaxLength(500)] + public required string AdditionalInformation { get; set; } + public DateTime CreatedAt { get; init; } = DateTime.UtcNow; + [Required(ErrorMessage = "CreatedBy é obrigatório")] + public int? CreatedBy { get; set; } +} diff --git a/Models/CustomerModel.cs b/Models/CustomerModel.cs deleted file mode 100644 index 339b59d..0000000 --- a/Models/CustomerModel.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace GsServer.Models; - -public class CustomerModel -{ - public int CustomerId { get; init; } - public UserModel? User { get; set; } - public required PersonModel Person { get; set; } - public required List Dependents { get; set; } - public required string BillingAddress { get; set; } - // Image path on a Cloud Storage (Like: Imgur, S3, Azure blob). - // All images will be scaled to 128px(w) x 128px(h). - public string? PicturePath { get; set; } - public required string AdditionalInformation { get; set; } - public DateTime CreatedAt { get; init; } = DateTime.UtcNow; - public required int CreatedBy { get; init; } -} diff --git a/Models/Discipline.cs b/Models/Discipline.cs new file mode 100644 index 0000000..e8cae7a --- /dev/null +++ b/Models/Discipline.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace GsServer.Models; + +[Index(nameof(StartTime), nameof(EndTime), nameof(IsActive), IsUnique = false)] +public class Discipline +{ + public int DisciplineId { get; init; } + [MaxLength(16)] + [Length(4, 10, ErrorMessage = "O nome deve ter entre 4 e 16 caracteres")] + public required string Name { get; set; } + [Column(TypeName = "decimal(19, 4)")] + public required decimal TuitionPrice { get; set; } + public required int InstructorId { get; set; } + public virtual Instructor Instructor { get; set; } = null!; + public required TimeOnly StartTime { get; set; } + public required TimeOnly EndTime { get; set; } + public required ICollection ClassDays { get; set; } + public bool IsActive { get; set; } = true; + public DateTime CreatedAt { get; init; } = DateTime.UtcNow; + [Required(ErrorMessage = "CreatedBy é obrigatório")] + public int? CreatedBy { get; set; } +} diff --git a/Models/DisciplineModel.cs b/Models/DisciplineModel.cs deleted file mode 100644 index 81f9666..0000000 --- a/Models/DisciplineModel.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace GsServer.Models; - -public class DisciplineModel -{ - public int DisciplineId { get; init; } - public required string Name { get; set; } - public required decimal TuitionPrice { get; set; } - public required InstructorModel Instructor { get; set; } - public required TimeOnly StartTime { get; set; } - public required TimeOnly EndTime { get; set; } - public required List ClassDays { get; set; } - public bool IsActive { get; set; } = true; - public DateTime CreatedAt { get; init; } = DateTime.UtcNow; - public required int CreatedBy { get; init; } -} - -public enum DayOfWeek -{ - Sunday = 0, - Monday = 1, - Tuesday = 2, - Wednesday = 3, - Thursday = 4, - Friday = 5, - Saturday = 6, -} diff --git a/Models/Instructor.cs b/Models/Instructor.cs new file mode 100644 index 0000000..b42b4ed --- /dev/null +++ b/Models/Instructor.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace GsServer.Models; + +public class Instructor +{ + public int InstructorId { get; init; } + public required Person Person { get; set; } + public int? UserId { get; set; } + public virtual User User { get; set; } = null!; + public DateTime CreatedAt { get; init; } = DateTime.UtcNow; + [Required(ErrorMessage = "CreatedBy é obrigatório")] + public int? CreatedBy { get; set; } +} diff --git a/Models/InstructorModel.cs b/Models/InstructorModel.cs deleted file mode 100644 index 0b6ce50..0000000 --- a/Models/InstructorModel.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace GsServer.Models; - -public class InstructorModel -{ - public int InstructorId { get; init; } - public required PersonModel Person { get; set; } - public required UserModel User { get; set; } - public DateTime CreatedAt { get; init; } = DateTime.UtcNow; - public required int CreatedBy { get; init; } -} diff --git a/Models/Notification.cs b/Models/Notification.cs new file mode 100644 index 0000000..075c085 --- /dev/null +++ b/Models/Notification.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace GsServer.Models; + +// Sends reminders for upcoming events, invoice expirations, etc. +[Index(nameof(CreatedAt), nameof(IsUnread), IsUnique = false)] +public class Notification +{ + public int NotificationId { get; init; } + public int UserId { get; init; } + public virtual User User { get; set; } = null!; + [Length(4, 12, ErrorMessage = "O título deve ter entre 4 e 12 caracteres")] + public required string Title { get; set; } + [Length(4, 120, ErrorMessage = "O título deve ter entre 4 e 64 caracteres")] + public required string Message { get; set; } + public bool IsUnread { get; set; } = true; + public bool IsRead() => !IsUnread; + public DateTime CreatedAt { get; init; } = DateTime.UtcNow; + [Required(ErrorMessage = "CreatedBy é obrigatório")] + public int? CreatedBy { get; set; } +} \ No newline at end of file diff --git a/Models/NotificationModel.cs b/Models/NotificationModel.cs deleted file mode 100644 index 3c6853f..0000000 --- a/Models/NotificationModel.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace GsServer.Models; - -// Sends reminders for upcoming events, invoice expirations, etc. -public class NotificationModel -{ - public int NotificationId { get; init; } - public int UserId { get; init; } - public required string Title { get; set; } - public required string Message { get; set; } - public bool IsUnread { get; set; } = true; - public DateTime CreatedAt { get; init; } = DateTime.UtcNow; - public required int CreatedBy { get; init; } -} \ No newline at end of file diff --git a/Models/Order.cs b/Models/Order.cs new file mode 100644 index 0000000..1725681 --- /dev/null +++ b/Models/Order.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace GsServer.Models; + +public class Order +{ + public int OrderId { get; init; } + public required Sale Sale { get; set; } + public required OrderStatus Status { get; set; } + public DateTime CreatedAt { get; init; } = DateTime.UtcNow; + [Required(ErrorMessage = "CreatedBy é obrigatório")] + public int? CreatedBy { get; set; } +} diff --git a/Models/OrderModel.cs b/Models/OrderModel.cs deleted file mode 100644 index 2fda281..0000000 --- a/Models/OrderModel.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace GsServer.Models; - -public class OrderModel -{ - public int OrderId { get; init; } - public required SaleModel Sale { get; set; } - public required OrderStatus Status { get; set; } - public DateTime CreatedAt { get; init; } = DateTime.UtcNow; - public required int CreatedBy { get; init; } -} - -public enum OrderStatus -{ - AwaitingPayment = 0, - Pending = 1, - Delivered = 2, - Canceled = 3, - Returned = 4, -} diff --git a/Models/OrderStatus.cs b/Models/OrderStatus.cs new file mode 100644 index 0000000..85a53ca --- /dev/null +++ b/Models/OrderStatus.cs @@ -0,0 +1,10 @@ +namespace GsServer.Models; + +public enum OrderStatus +{ + AwaitingPayment = 0, + Pending = 1, + Delivered = 2, + Canceled = 3, + Returned = 4, +} diff --git a/Models/Payment.cs b/Models/Payment.cs new file mode 100644 index 0000000..195dc7d --- /dev/null +++ b/Models/Payment.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; + +namespace GsServer.Models; + +public class Payment +{ + public int PaymentId { get; init; } + /// + /// Comments is for things like installment price changed because of returned item. + /// + [Length(4, 240, ErrorMessage = "O comentário deve ter entre 4 e 240 caracteres")] + public required string Comments { get; set; } + public required ICollection Installments { get; set; } + public DateTime CreatedAt { get; init; } = DateTime.UtcNow; + [Required(ErrorMessage = "CreatedBy é obrigatório")] + public int? CreatedBy { get; set; } + public decimal TotalAmountOwed() + { + throw new NotImplementedException(); + } + public decimal TotalAmountPaid() + { + throw new NotImplementedException(); + } +} + +// TODO Implement promotional offers (discounts, trials, upgrades). +// TODO add Subscription History diff --git a/Models/PaymentModel.cs b/Models/PaymentInstallment.cs similarity index 68% rename from Models/PaymentModel.cs rename to Models/PaymentInstallment.cs index 4987346..92ef21e 100644 --- a/Models/PaymentModel.cs +++ b/Models/PaymentInstallment.cs @@ -1,24 +1,7 @@ -namespace GsServer.Models; - -public class PaymentModel -{ - public int PaymentId { get; init; } - // Comments is for things like installment price changed because of returned item. - public required string Comments { get; set; } - public required List Installments { get; set; } - public DateTime CreatedAt { get; init; } = DateTime.UtcNow; - public required int CreatedBy { get; init; } +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; - public decimal TotalAmountOwed() - { - throw new NotImplementedException(); - } - - public decimal TotalAmountPaid() - { - throw new NotImplementedException(); - } -} +namespace GsServer.Models; /* The installment buying definition refers to the process of purchasing an asset @@ -38,15 +21,14 @@ a preset amount and date of the month. Purchases made with a credit card are - Pay later - the entire purchase cost is delayed for a period (usually between 14 to 30 days) and then repaid in full. - Pay on delivery - your customer pays for goods after they have been delivered. An example of this is the service */ -public class PaymentInstallmentModel +public class PaymentInstallment { public int PaymentInstallmentId { get; init; } - public int PaymentId { get; set; } + [Range(1, 12)] public int InstallmentNumber { get; set; } // Sequential number, (e.g, "${installment.InstallmentNumber} of {payment.Installments.Count}" = “2 of 6”) + [Column(TypeName = "decimal(19, 4)")] public decimal InstallmentAmount { get; set; } + [Length(2, 16)] public required string PaymentMethod { get; set; } // (e.g., "money", "credit card", "debit card", ...). - public DateOnly? DueDate { get; set; } // Optional property for due date -} - -// TODO Implement promotional offers (discounts, trials, upgrades). -// TODO add Subscription History + public DateOnly DueDate { get; set; } // Optional property for due date +} \ No newline at end of file diff --git a/Models/Person.cs b/Models/Person.cs new file mode 100644 index 0000000..13ab159 --- /dev/null +++ b/Models/Person.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; + +namespace GsServer.Models; + +[Index(nameof(MobilePhoneNumber), IsUnique = true)] +public class Person +{ + public int PersonId { get; init; } + [Length(5, 55)] + public required string Name { get; set; } + [MaxLength(16)] + public required string MobilePhoneNumber { get; set; } + [Length(10, 10)] + public required string BirthDate { get; set; } + /// + /// Cadastro de Pessoas Físicas (CPF) + /// + [Length(14, 14)] + public required string Cpf { get; set; } + // TODO + /// + /// Carteira de Identidade Nacional (CIN) + /// + // public required string Cin { get; set; } + public DateTime CreatedAt { get; init; } = DateTime.UtcNow; + [Required(ErrorMessage = "CreatedBy é obrigatório")] + public int? CreatedBy { get; set; } +} \ No newline at end of file diff --git a/Models/PersonModel.cs b/Models/PersonModel.cs deleted file mode 100644 index a3d2e45..0000000 --- a/Models/PersonModel.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace GsServer.Models; - -public class PersonModel -{ - public int PersonId { get; init; } - public required string Name { get; set; } - public required string MobilePhoneNumber { get; set; } - public required string BirthDate { get; set; } - // Cadastro de Pessoas Físicas (CPF) - public required string Cpf { get; set; } - // Carteira de Identidade Nacional (CIN) - public required string Cin { get; set; } - public DateTime CreatedAt { get; init; } = DateTime.UtcNow; - public required int CreatedBy { get; init; } -} \ No newline at end of file diff --git a/Models/Product.cs b/Models/Product.cs new file mode 100644 index 0000000..9095897 --- /dev/null +++ b/Models/Product.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace GsServer.Models; + +public class Product +{ + public int ProductId { get; init; } + [Length(5, 32)] + public required string Name { get; set; } + // Image path on a Cloud Storage (Like: Imgur, S3, Azure blob). + // All images will be scaled to 128px(w) x 128px(h). + public string? PicturePath { get; set; } + public required ProductBrand ProductBrand { get; set; } + // (e.g., Hats, Shirts, Pants, Shorts, Shoes, Dresses). + public required ProductCategory ProductCategory { get; set; } + public required ICollection Variants { get; set; } + public DateTime CreatedAt { get; init; } = DateTime.UtcNow; + [Required(ErrorMessage = "CreatedBy é obrigatório")] + public int? CreatedBy { get; set; } +} diff --git a/Models/ProductBrand.cs b/Models/ProductBrand.cs new file mode 100644 index 0000000..dddba2c --- /dev/null +++ b/Models/ProductBrand.cs @@ -0,0 +1,7 @@ +namespace GsServer.Models; + +public class ProductBrand +{ + public int ProductBrandId { get; init; } + public required string Name { get; set; } +} diff --git a/Models/ProductCategory.cs b/Models/ProductCategory.cs new file mode 100644 index 0000000..026d1bf --- /dev/null +++ b/Models/ProductCategory.cs @@ -0,0 +1,7 @@ +namespace GsServer.Models; + +public class ProductCategory +{ + public int ProductCategoryId { get; init; } + public required string Name { get; set; } +} diff --git a/Models/ProductModel.cs b/Models/ProductModel.cs deleted file mode 100644 index 1116d50..0000000 --- a/Models/ProductModel.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace GsServer.Models; - -public class ProductModel -{ - public int ProductId { get; init; } - public required string Name { get; set; } - public required string Description { get; set; } - // Image path on a Cloud Storage (Like: Imgur, S3, Azure blob). - // All images will be scaled to 128px(w) x 128px(h). - public string? PicturePath { get; set; } - public int? ProductBrandId { get; set; } - // (e.g., Hats, Shirts, Pants, Shorts, Shoes, Dresses). - public int? ProductCategoryId { get; set; } - public required List Variants { get; set; } - public DateTime CreatedAt { get; init; } = DateTime.UtcNow; - public required int CreatedBy { get; init; } -} - -public class ProductBrandModel -{ - public int ProductBrandId { get; init; } - public required string Name { get; set; } -} - -public class ProductCategoryModel -{ - public int ProductCategoryId { get; init; } - public required string Name { get; set; } -} - -public class ProductVariantModel -{ - public int ProductVariantId { get; init; } - public required string Color { get; set; } - public required string Size { get; set; } - public required string BarCode { get; set; } - public required string Sku { get; set; } - public required decimal UnitPrice { get; set; } - public required ProductVariantInventoryModel Inventory { get; set; } -} - -public class ProductVariantInventoryModel -{ - public int ProductVariantInventoryId { get; init; } - public int ProductVariantId { get; set; } - public required int QuantityAvailable { get; set; } - // The minimum number of units required to ensure no shortages will occur. - // When the number of product units reaches this threshold level, a purchase - // order must be done. - public required int MinimumStockAmount { get; set; } -} - -// public class ProductStockHistoryModel -// { -// public int Id { get; init; } -// public required int Amount { get; set; } -// public required bool IsSale { get; set; } -// public required bool IsReturnedItem { get; set; } -// public required bool IsReplenishment { get; set; } -// } diff --git a/Models/ProductStockHistory.cs b/Models/ProductStockHistory.cs new file mode 100644 index 0000000..c879543 --- /dev/null +++ b/Models/ProductStockHistory.cs @@ -0,0 +1,8 @@ +namespace GsServer.Models; + +public class ProductStockHistory +{ + public int Id { get; init; } + public required int AmountChanged { get; set; } + public required string ChangeDescription { get; set; } // (e.g., Returned, sold, restocked,...) +} diff --git a/Models/ProductVariant.cs b/Models/ProductVariant.cs new file mode 100644 index 0000000..d3fded6 --- /dev/null +++ b/Models/ProductVariant.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace GsServer.Models; + +public class ProductVariant +{ + public int ProductVariantId { get; init; } + public required string Color { get; set; } + public required string Size { get; set; } + public required string BarCode { get; set; } + public required string Sku { get; set; } + [Column(TypeName = "decimal(19, 4)")] + public required decimal UnitPrice { get; set; } + public required ProductVariantInventory Inventory { get; set; } +} diff --git a/Models/ProductVariantInventory.cs b/Models/ProductVariantInventory.cs new file mode 100644 index 0000000..3879ec6 --- /dev/null +++ b/Models/ProductVariantInventory.cs @@ -0,0 +1,13 @@ +namespace GsServer.Models; + +public class ProductVariantInventory +{ + public int ProductVariantInventoryId { get; init; } + public int ProductVariantId { get; set; } + public virtual ProductVariant ProductVariant { get; set; } = null!; + public required int QuantityAvailable { get; set; } + // The minimum number of units required to ensure no shortages will occur. + // When the number of product units reaches this threshold level, a purchase + // order must be done. + public required int MinimumStockAmount { get; set; } +} diff --git a/Models/PromotionModel.cs b/Models/Promotion.cs similarity index 73% rename from Models/PromotionModel.cs rename to Models/Promotion.cs index 298166d..536dde7 100644 --- a/Models/PromotionModel.cs +++ b/Models/Promotion.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations; + namespace GsServer.Models; /* @@ -7,10 +9,12 @@ namespace GsServer.Models; - Stacking Rules: Decide whether promotions can be combined (e.g., using a discount code during a free trial). - Communication Channels: Notify users about promotions via email, in-app messages, or push notifications. */ -public class PromotionModel // Represents special offers or discounts. +[Index(nameof(IsActive), IsUnique = false)] +public class Promotion // Represents special offers or discounts. { public int PromotionId { get; init; } - public int UserId { get; init; } + public int CustomerId { get; init; } + public virtual Customer Customer { get; set; } = null!; // Name of the offer (e.g., "Summer Sale", "Introductory Discount", "Free Trials", "Referral Bonuses"). public required string Name { get; set; } public required string Description { get; set; } @@ -20,7 +24,8 @@ public class PromotionModel // Represents special offers or discounts. public required DateOnly StartDate { get; set; } // Date the offer expires (optional). public required DateOnly EndDate { get; set; } - public required bool IsActive { get; set; } + public bool IsActive { get; set; } = true; public DateTime CreatedAt { get; init; } = DateTime.UtcNow; - public required int CreatedBy { get; init; } + [Required(ErrorMessage = "CreatedBy é obrigatório")] + public int? CreatedBy { get; set; } } diff --git a/Models/RefreshTokenModel.cs b/Models/RefreshToken.cs similarity index 70% rename from Models/RefreshTokenModel.cs rename to Models/RefreshToken.cs index ad037ef..f5fb4a8 100644 --- a/Models/RefreshTokenModel.cs +++ b/Models/RefreshToken.cs @@ -1,9 +1,12 @@ namespace GsServer.Models; -public class RefreshTokenModel +[Index(nameof(Token), IsUnique = true)] +public class RefreshToken { public int RefreshTokenId { get; init; } public required int UserId { get; init; } + public virtual User User { get; set; } = null!; + // TODO use varchar or char public required string Token { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime ExpiresIn { get; set; } = DateTime.UtcNow.AddDays(28); diff --git a/Models/Return.cs b/Models/Return.cs new file mode 100644 index 0000000..9d737fc --- /dev/null +++ b/Models/Return.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace GsServer.Models; + +public class Return +{ + public int ReturnId { get; init; } + [Column(TypeName = "decimal(19, 4)")] + public decimal TotalAmountRefunded { get; set; } + public required ICollection ItemsReturned { get; set; } + public DateTime CreatedAt { get; init; } = DateTime.UtcNow; + [Required(ErrorMessage = "CreatedBy é obrigatório")] + public int? CreatedBy { get; set; } +} diff --git a/Models/ReturnItem.cs b/Models/ReturnItem.cs new file mode 100644 index 0000000..464c1c4 --- /dev/null +++ b/Models/ReturnItem.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace GsServer.Models; + +public class ReturnItem +{ + public int ReturnItemId { get; init; } + public required int ProductVariantId { get; set; } + public virtual ProductVariant Product { get; set; } = null!; + [Column(TypeName = "decimal(19, 4)")] + public required decimal UnitPrice { get; set; } + public required int QuantityReturned { get; set; } +} diff --git a/Models/ReturnModel.cs b/Models/ReturnModel.cs deleted file mode 100644 index cb5773a..0000000 --- a/Models/ReturnModel.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace GsServer.Models; - -public class ReturnModel -{ - public int ReturnId { get; init; } - public decimal TotalAmountRefunded { get; set; } - public required List ItemsReturned { get; set; } - public DateTime CreatedAt { get; init; } = DateTime.UtcNow; - public required int CreatedBy { get; init; } -} - -public class ReturnItemModel -{ - public int ReturnItemId { get; init; } - public required int ProductVariantId { get; set; } - public required int QuantityReturned { get; set; } -} \ No newline at end of file diff --git a/Models/Sale.cs b/Models/Sale.cs new file mode 100644 index 0000000..09253c0 --- /dev/null +++ b/Models/Sale.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace GsServer.Models; + +public class Sale +{ + public int SaleId { get; init; } + public int? CustomerId { get; set; } + public virtual Customer Customer { get; set; } = null!; + /// + /// For details about returns, discounts and alike + /// + public required string Comments { get; set; } + [Length(4, 240, ErrorMessage = "O comentário deve ter entre 4 e 240 caracteres")] + public required ICollection ItemsSold { get; set; } + public DateTime CreatedAt { get; init; } = DateTime.UtcNow; + [Required(ErrorMessage = "CreatedBy é obrigatório")] + public int? CreatedBy { get; set; } +} diff --git a/Models/SaleBillingModel.cs b/Models/SaleBilling.cs similarity index 56% rename from Models/SaleBillingModel.cs rename to Models/SaleBilling.cs index 8b21929..be6d0b3 100644 --- a/Models/SaleBillingModel.cs +++ b/Models/SaleBilling.cs @@ -1,3 +1,6 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + namespace GsServer.Models; /// @@ -5,13 +8,16 @@ namespace GsServer.Models; /// is used for immediate payment situations and may not include detailed client /// information (unlike an invoice). /// -public class SaleBillingModel +public class SaleBilling { public int SaleBillingId { get; init; } public required int SaleId { get; set; } + [Length(4, 240, ErrorMessage = "O comentário deve ter entre 4 e 240 caracteres")] public required string Comments { get; set; } + [Column(TypeName = "decimal(19, 4)")] public required decimal TotalDiscount { get; init; } - public required PaymentModel Payment { get; init; } + public required Payment Payment { get; init; } public DateTime CreatedAt { get; init; } = DateTime.UtcNow; - public required int CreatedBy { get; init; } + [Required(ErrorMessage = "CreatedBy é obrigatório")] + public int? CreatedBy { get; set; } } diff --git a/Models/SaleItem.cs b/Models/SaleItem.cs new file mode 100644 index 0000000..1d7124a --- /dev/null +++ b/Models/SaleItem.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace GsServer.Models; + +public class SaleItem +{ + public int SaleItemId { get; init; } + public required int ProductVariantId { get; set; } + public virtual ProductVariant ProductVariant { get; set; } = null!; + [Column(TypeName = "decimal(19, 4)")] + public required decimal UnitPrice { get; set; } + public required int QuantitySold { get; set; } +} diff --git a/Models/SaleModel.cs b/Models/SaleModel.cs deleted file mode 100644 index 9f6d5a8..0000000 --- a/Models/SaleModel.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace GsServer.Models; - -public class SaleModel -{ - public int SaleId { get; init; } - public CustomerModel? Customer { get; set; } - // For details about returns, discounts and alike - public required string Comments { get; set; } - public required List ItemsSold { get; set; } - public List Returns { get; set; } = []; - public DateTime CreatedAt { get; init; } = DateTime.UtcNow; - public required int CreatedBy { get; init; } -} - -public class SaleItemModel -{ - public int SaleItemId { get; init; } - public required int ProductVariantId { get; set; } - public required int QuantitySold { get; set; } -} - diff --git a/Models/Subscription.cs b/Models/Subscription.cs new file mode 100644 index 0000000..ceb83f2 --- /dev/null +++ b/Models/Subscription.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace GsServer.Models; + +[Index(nameof(PayDay), nameof(IsActive))] +public class Subscription +{ + public int SubscriptionId { get; init; } + public required int DisciplineId { get; init; } + public virtual Discipline Discipline { get; set; } = null!; + public required int CustomerId { get; init; } + public virtual Customer Customer { get; set; } = null!; + [Range(1, 28, MinimumIsExclusive = false, MaximumIsExclusive = false)] + public required int PayDay { get; set; } + /// + /// Date the subscription has begin + /// + public required DateOnly StartDate { get; set; } + /// + /// Date the subscription was cancelled + /// + public DateOnly? EndDate { get; set; } + [Column(TypeName = "decimal(19, 4)")] + public required decimal Price { get; set; } + public bool IsActive { get; set; } = true; + public DateTime CreatedAt { get; init; } = DateTime.UtcNow; + [Required(ErrorMessage = "CreatedBy é obrigatório")] + public int? CreatedBy { get; set; } +} diff --git a/Models/SubscriptionBillingModel.cs b/Models/SubscriptionBilling.cs similarity index 53% rename from Models/SubscriptionBillingModel.cs rename to Models/SubscriptionBilling.cs index 92e14ba..78031a9 100644 --- a/Models/SubscriptionBillingModel.cs +++ b/Models/SubscriptionBilling.cs @@ -1,3 +1,6 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + namespace GsServer.Models; /// @@ -5,13 +8,17 @@ namespace GsServer.Models; /// is used for immediate payment situations and may not include detailed client /// information (unlike an invoice). /// -public class SubscriptionBillingModel +public class SubscriptionBilling { public int SubscriptionBillingId { get; init; } public required int SubscriptionId { get; init; } + public virtual Subscription Subscription { get; set; } = null!; + [Length(4, 240, ErrorMessage = "O comentário deve ter entre 4 e 240 caracteres")] public required string Comments { get; set; } + [Column(TypeName = "decimal(19, 4)")] public required decimal TotalDiscount { get; init; } - public required PaymentModel Payment { get; init; } + public required Payment Payment { get; init; } public DateTime CreatedAt { get; init; } = DateTime.UtcNow; - public required int CreatedBy { get; init; } + [Required(ErrorMessage = "CreatedBy é obrigatório")] + public int? CreatedBy { get; set; } } diff --git a/Models/SubscriptionModel.cs b/Models/SubscriptionModel.cs deleted file mode 100644 index e35491e..0000000 --- a/Models/SubscriptionModel.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace GsServer.Models; - -public class SubscriptionModel -{ - public int SubscriptionId { get; init; } - public required int DisciplineId { get; init; } - public required int CustomerId { get; init; } - public required int PayDay { get; set; } - public required DateOnly StartDate { get; set; } - // Date the subscription was cancelled - public DateOnly? EndDate { get; set; } - public required decimal Price { get; set; } - public bool IsActive { get; set; } = true; - public DateTime CreatedAt { get; init; } = DateTime.UtcNow; - public required int CreatedBy { get; init; } -} diff --git a/Models/UserModel.cs b/Models/User.cs similarity index 54% rename from Models/UserModel.cs rename to Models/User.cs index bb45b67..55680ae 100644 --- a/Models/UserModel.cs +++ b/Models/User.cs @@ -1,9 +1,15 @@ +using System.ComponentModel.DataAnnotations; + namespace GsServer.Models; -public class UserModel +[Index(nameof(Email), IsUnique = true)] +public class User { public int UserId { get; init; } + [Required(ErrorMessage = "O role (a função) é obrigatório", AllowEmptyStrings = false)] public required string Role { get; set; } + [Required(ErrorMessage = "O e-mail é obrigatório", AllowEmptyStrings = false)] + [EmailAddress] public required string Email { get; set; } // TODO add notification_preferences public required byte[] PasswordHash { get; set; } diff --git a/Program.cs b/Program.cs index 413d5c3..335475a 100644 --- a/Program.cs +++ b/Program.cs @@ -1,6 +1,9 @@ +using System.Net; +using System.Net.Mime; using System.Text; using Amazon.CloudWatchLogs; using GsServer.BackgroundServices; +using GsServer.Middlewares; using GsServer.Services; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; @@ -9,6 +12,8 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args); +builder.Services.AddExceptionHandler(); + builder.Services.Configure(options => { options.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore; @@ -50,6 +55,8 @@ // AWS CloudWatch client // AmazonCloudWatchLogsClient client = new(AwsAccessKeyId, AwsSecretAccessKey); +builder.Services.AddAutoMapper(typeof(Program).Assembly); + builder.Host.UseSerilog( (context, configuration) => { @@ -71,37 +78,51 @@ app.MapGrpcService(); app.MapGrpcService(); +app.MapGrpcService(); app.MapGrpcService(); -app.MapGrpcService(); +// app.MapGrpcService(); +app.MapGrpcService(); +app.MapGrpcService(); app.MapGrpcService(); +app.MapGrpcService(); app.MapGrpcService(); +app.MapGrpcService(); +app.MapGrpcService(); +app.MapGrpcService(); app.MapGrpcService(); -app.MapGrpcService(); +app.MapGrpcService(); +app.MapGrpcService(); app.MapGrpcService(); if (app.Environment.IsDevelopment()) { - // Configure the HTTP request pipeline. - app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client."); + app.MapGrpcReflectionService(); - Log.Debug( - "Server initialized at port 7063" + Log.Information( + "Server running on port 7063" ); } -else -{ - Log.Information("Server initialized"); -} + +// Configure the HTTP request pipeline. +app.MapGet("/", () => + Results.Text( + content: "

Communication with gRPC endpoints must be made through a gRPC client.

", + contentType: MediaTypeNames.Text.Html, + statusCode: StatusCodes.Status405MethodNotAllowed + ) +); + +Log.Information("Server is ready to accept requests"); app.Use(async (context, next) => { // Middleware Log request details (e.g., method, path, headers) Log.Information( - "({RequestIpAddress} - {TraceIdentifier}) Received gRPC request: {Method} {Path}", - context.Connection.RemoteIpAddress?.ToString() ?? string.Empty, + "Handling request ({TraceIdentifier}): {Method} {Path} {RequestIpAddress}", context.TraceIdentifier, context.Request.Method, - context.Request.Path + context.Request.Path, + context.Connection.RemoteIpAddress?.ToString() ?? string.Empty ); // Proceed with handling the request @@ -109,7 +130,7 @@ // Log response details (e.g., status code, headers) Log.Information( - "({TraceIdentifier}) Sent gRPC response", + "Handled request ({TraceIdentifier})", context.TraceIdentifier ); }); diff --git a/README.md b/README.md index 40fa812..63e9ba6 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Licensed under the Apache License, Version 2.0; you may not use this app except ## About -Server used by the company "Gislaine Studio de Dança" in Andradina/Brazil. +Server used by the company "Gislaine Studio" in Andradina-SP, Brazil. > [!Warning]\ > **Guidelines for copyright and trademarks**\ diff --git a/Services/AttendanceRpcService.cs b/Services/AttendanceRpcService.cs index 947827c..99f90ef 100644 --- a/Services/AttendanceRpcService.cs +++ b/Services/AttendanceRpcService.cs @@ -1,40 +1,298 @@ +using System.Security.Claims; +using AutoMapper; using Grpc.Core; +using GsServer.Models; using GsServer.Protobufs; namespace GsServer.Services; public class AttendanceRpcService : AttendanceService.AttendanceServiceBase { - private readonly DatabaseContext _dbContext; private readonly ILogger _logger; - public AttendanceRpcService(ILogger logger, DatabaseContext dbContext) + private readonly DatabaseContext _dbContext; + private readonly IMapper _mapper; + public AttendanceRpcService( + ILogger logger, + DatabaseContext dbContext, + IMapper mapper + ) { _logger = logger; _dbContext = dbContext; + _mapper = mapper; } - public override Task GetPaginated(GetPaginatedAttendancesRequest request, ServerCallContext context) + public override async Task GetPaginatedAsync(GetPaginatedAttendancesRequest request, ServerCallContext context) { - throw new NotImplementedException(); + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing multiple records ({RecordType}) with cursor {Cursor}", + RequestTracerId, + UserId, + typeof(Attendance).Name, + request.Cursor + ); + + IQueryable Query = _dbContext.Attendances.Select( + Attendance => _mapper.Map(Attendance) + ); + + // TODO + // IQueryable Query = _dbContext.Attendances.Select( + // Attendance => new GetAttendanceByIdResponse + // { + // AttendanceId = Attendance.AttendanceId, + // Discipline = Attendance.DisciplineFk, + // Date = new() + // { + // Day = Attendance.Date.Day, + // Month = Attendance.Date.Month, + // Year = Attendance.Date.Year + // }, + // StudentsPresent = { Attendance.StudentsPresentFks }, + // StudentsAbsent = { Attendance.StudentsAbsentFks }, + // } + // ); + + List Attendances = []; + + /// If cursor is bigger than the size of the collection you will get the following error + /// ArgumentOutOfRangeException "Index was out of range. Must be non-negative and less than the size of the collection" + Attendances = await Query + .Where(x => x.AttendanceId > request.Cursor) + .Take(20) + .ToListAsync(); + + GetPaginatedAttendancesResponse response = new(); + + response.Attendances.AddRange(Attendances); + if (Attendances.Count < 20) + { + /// Avoiding `ArgumentOutOfRangeException`, basically, don't fetch if null + response.NextCursor = null; + } + else + { + /// Id of the last element of the list, same value as `Users[Users.Count - 1].Id` + response.NextCursor = Attendances[^1].AttendanceId; + } + + _logger.LogInformation( + "({TraceIdentifier}) multiple records ({RecordType}) accessed successfully", + RequestTracerId, + typeof(Attendance).Name + ); + return response; } - public override Task GetById(GetAttendanceByIdRequest request, ServerCallContext context) + public override async Task GetByIdAsync(GetAttendanceByIdRequest request, ServerCallContext context) { - throw new NotImplementedException(); + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Attendance).Name, + request.AttendanceId + ); + + Attendance? Attendance = await _dbContext.Attendances.FindAsync(request.AttendanceId); + + if (Attendance is null) + { + _logger.LogWarning( + "({TraceIdentifier}) record ({RecordType}) not found", + RequestTracerId, + typeof(Attendance).Name + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Nenhum produto com ID {request.AttendanceId}" + )); + } + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) accessed successfully", + RequestTracerId, + typeof(Attendance).Name + ); + + return _mapper.Map(Attendance); + + // return new GetAttendanceByIdResponse + // { + // AttendanceId = Attendance.AttendanceId, + // Discipline = Attendance.Discipline, + // Discipline = new() + // { + // DisciplinePk = Attendance.Discipline.DisciplinePk, + // Name = Attendance.Discipline.Name, + // TuitionPrice = Attendance.Discipline.TuitionPrice, + // StartTime = Attendance.Discipline.StartTime, + // EndTime = Attendance.Discipline.EndTime, + // ClassDays = Attendance.Discipline.ClassDays, + // IsActive = Attendance.Discipline.IsActive, + // }, + // Date = new() + // { + // Day = Attendance.Date.Day, + // Month = Attendance.Date.Month, + // Year = Attendance.Date.Year + // }, + // AttendeesStatuses = { + // Attendance.AttendeesStatuses.Select( + // AttendeeStatus => new Protobufs.AttendanceAttendeeStatus + // { + // AttendanceAttendeeStatusPk = AttendeeStatus.AttendanceAttendeeStatusPk, + // PersonFk = AttendeeStatus.PersonFk, + // IsPresent = AttendeeStatus.IsPresent, + // } + // ).ToList(), + // }, + // Observations = Attendance.Observations, + // }; } - public override Task Post(CreateAttendanceRequest request, ServerCallContext context) + public override async Task PostAsync(CreateAttendanceRequest request, ServerCallContext context) { - throw new NotImplementedException(); + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} creating new record ({RecordType})", + RequestTracerId, + UserId, + typeof(Attendance).Name + ); + + Attendance Attendance = _mapper.Map(request); + Attendance.CreatedBy = UserId; + + // TODO + // var Attendance = new Attendance + // { + // DisciplineFk = request.DisciplineFk, + // Date = new( + // request.Date.Year, + // request.Date.Month, + // request.Date.Day + // ), + // AttendeesStatuses = request.AttendeesStatuses.Select( + // Installment => new Models.AttendanceAttendeeStatus + // { + // AttendanceAttendeeStatusPk = Installment.AttendanceAttendeeStatusPk, + // PersonFk = Installment.PersonFk, + // IsPresent = Installment.IsPresent, + // } + // ).ToList(), + // Observations = request.Observations, + // CreatedBy = UserId, + // }; + + Attendance.CreatedBy = UserId; + await _dbContext.AddAsync(Attendance); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) created successfully, RecordId {RecordId}", + RequestTracerId, + typeof(Attendance).Name, + Attendance.AttendanceId + ); + + return new CreateAttendanceResponse(); } - public override Task Put(UpdateAttendanceRequest request, ServerCallContext context) + public override Task PutAsync(UpdateAttendanceRequest request, ServerCallContext context) { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} updating record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Attendance).Name, + request.AttendanceId + ); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) updated successfully", + RequestTracerId, + typeof(Attendance).Name + ); + throw new NotImplementedException(); + + // TODO + // if (request.Id <= 0) + // throw new RpcException(new Status(StatusCode.InvalidArgument, "You must supply a valid id")); + + // AttendanceModel? Attendance = await _dbContext.Attendances.FirstOrDefaultAsync(x => x.Id == request.Id); + // if (Attendance is null) + // { + // throw new RpcException(new Status( + // StatusCode.NotFound, $"registro não encontrado" + // )); + // } + + // Attendance.Name = request.Name; + // // TODO Add Another fields + + // await _dbContext.SaveChangesAsync(); + // // TODO Log => Record (record type) ID Y was updated. Old value of (field name): (old value). New value: (new value). (This logs specific changes made to a field within a record) + // return new UpdateAttendanceResponse(); } - public override Task Delete(DeleteAttendanceRequest request, ServerCallContext context) + public override async Task DeleteAsync(DeleteAttendanceRequest request, ServerCallContext context) { - throw new NotImplementedException(); + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} deleting record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Attendance).Name, + request.AttendanceId + ); + + Attendance? Attendance = await _dbContext.Attendances.FindAsync(request.AttendanceId); + + if (Attendance is null) + { + _logger.LogWarning( + "({TraceIdentifier}) Error deleting record ({RecordType}) with ID {Id}, record not found", + RequestTracerId, + typeof(Attendance).Name, + request.AttendanceId + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Erro ao remover registro, nenhum registro com ID {request.AttendanceId}" + )); + } + + /// TODO check if record is being used before deleting it use something like PK or FK + + _dbContext.Attendances.Remove(Attendance); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) deleted successfully", + RequestTracerId, + typeof(Attendance).Name + ); + + return new DeleteAttendanceResponse(); } } diff --git a/Services/AuthRpcService.cs b/Services/AuthRpcService.cs index f5a6634..4c9c946 100644 --- a/Services/AuthRpcService.cs +++ b/Services/AuthRpcService.cs @@ -23,7 +23,7 @@ public AuthRpcService(IConfiguration configuration, ILogger logg } [AllowAnonymous] - public override async Task Login(LoginRequest request, ServerCallContext context) + public override async Task LoginAsync(LoginRequest request, ServerCallContext context) { string RequestTracerId = context.GetHttpContext().TraceIdentifier; @@ -33,7 +33,7 @@ public override async Task Login(LoginRequest request, ServerCall 0 // TODO get something here, maybe phone like (18) XXXXX-3114 ); - UserModel? User = + User? User = await _dbContext.Users.FirstOrDefaultAsync( x => x.Email.Equals(request.Email.Trim().ToLower()) ); @@ -45,7 +45,7 @@ await _dbContext.Users.FirstOrDefaultAsync( RequestTracerId ); throw new RpcException(new Status( - StatusCode.Unauthenticated, "Erro na tentativa de login, Usuário e/ou Senha incorreto" + StatusCode.Unauthenticated, "Erro na tentativa de login, login ou senha incorreto" )); } @@ -63,14 +63,14 @@ await _dbContext.Users.FirstOrDefaultAsync( ); throw new RpcException(new Status( - StatusCode.Unauthenticated, "Erro na tentativa de login, Usuário e/ou Senha incorreto" + StatusCode.Unauthenticated, "Erro na tentativa de login, login ou senha incorreto" )); } string JwtToken = GenerateJwtToken(User); string RefreshToken = GenerateRefreshToken(); - RefreshTokenModel refreshTokenEntity = new() + RefreshToken refreshTokenEntity = new() { UserId = User.UserId, Token = RefreshToken @@ -96,7 +96,7 @@ await _dbContext.Users.FirstOrDefaultAsync( }; } - public override Task Logout(LogoutRequest request, ServerCallContext context) + public override Task LogoutAsync(LogoutRequest request, ServerCallContext context) { string RequestTracerId = context.GetHttpContext().TraceIdentifier; _logger.LogInformation( @@ -108,7 +108,7 @@ public override Task Logout(LogoutRequest request, ServerCallCon } [AllowAnonymous] - public override async Task Register(RegisterRequest request, ServerCallContext context) + public override async Task RegisterAsync(RegisterRequest request, ServerCallContext context) { string RequestTracerId = context.GetHttpContext().TraceIdentifier; _logger.LogInformation( @@ -116,7 +116,7 @@ public override async Task Register(RegisterRequest request, S RequestTracerId ); - UserModel? User = + User? User = await _dbContext.Users.FirstOrDefaultAsync( x => x.Email.Equals(request.Email.Trim().ToLower()) ); @@ -138,7 +138,7 @@ await _dbContext.Users.FirstOrDefaultAsync( CreatePasswordHash(request.Password, out byte[] generatedPasswordHash, out byte[] generatedPasswordSalt); - User = new UserModel + User = new User { Email = request.Email.Trim().ToLower(), Role = "user", @@ -159,7 +159,7 @@ await _dbContext.Users.FirstOrDefaultAsync( return new RegisterResponse(); } - public override async Task RefreshToken(RefreshTokenRequest request, ServerCallContext context) + public override async Task RefreshTokenAsync(RefreshTokenRequest request, ServerCallContext context) { string RequestTracerId = context.GetHttpContext().TraceIdentifier; int UserId = int.Parse( @@ -172,7 +172,7 @@ public override async Task RefreshToken(RefreshTokenReques UserId ); - UserModel? User = await _dbContext.Users.FindAsync(UserId); + User? User = await _dbContext.Users.FindAsync(UserId); if (User is null) { @@ -185,7 +185,7 @@ public override async Task RefreshToken(RefreshTokenReques )); } - RefreshTokenModel? RefreshToken = + RefreshToken? RefreshToken = await _dbContext.RefreshTokens.FirstOrDefaultAsync( x => x.Token.Equals(request.RefreshToken) ); @@ -215,12 +215,12 @@ await _dbContext.RefreshTokens.FirstOrDefaultAsync( }; } - public override Task NewPassword(NewPasswordRequest request, ServerCallContext context) + public override Task NewPasswordAsync(NewPasswordRequest request, ServerCallContext context) { throw new NotImplementedException(); } - public override async Task ChangePassword(ChangePasswordRequest request, ServerCallContext context) + public override async Task ChangePasswordAsync(ChangePasswordRequest request, ServerCallContext context) { string RequestTracerId = context.GetHttpContext().TraceIdentifier; int UserId = int.Parse( @@ -233,7 +233,7 @@ public override async Task ChangePassword(ChangePassword UserId ); - UserModel? User = await _dbContext.Users.FindAsync(UserId); + User? User = await _dbContext.Users.FindAsync(UserId); if (User is null) { @@ -297,7 +297,7 @@ private static bool VerifyPassword(string password, byte[] passwordHash, byte[] return computeHash.SequenceEqual(passwordHash); } private string GenerateJwtToken( - UserModel User + User User ) { List claims = diff --git a/Services/CustomerRpcService.cs b/Services/CustomerRpcService.cs index b22fecf..c66d722 100644 --- a/Services/CustomerRpcService.cs +++ b/Services/CustomerRpcService.cs @@ -1,4 +1,7 @@ +using System.Security.Claims; +using AutoMapper; using Grpc.Core; +using GsServer.Models; using GsServer.Protobufs; namespace GsServer.Services; @@ -7,39 +10,297 @@ public class CustomerRpcService : CustomerService.CustomerServiceBase { private readonly DatabaseContext _dbContext; private readonly ILogger _logger; - public CustomerRpcService(ILogger logger, DatabaseContext dbContext) + private readonly IMapper _mapper; + public CustomerRpcService( + ILogger logger, + DatabaseContext dbContext, + IMapper mapper + ) { _logger = logger; _dbContext = dbContext; + _mapper = mapper; } - public override Task GetPaginated(GetPaginatedCustomersRequest request, ServerCallContext context) + public override async Task GetPaginatedAsync(GetPaginatedCustomersRequest request, ServerCallContext context) { - throw new NotImplementedException(); - } + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing multiple records ({RecordType}) with cursor {Cursor}", + RequestTracerId, + UserId, + typeof(Customer).Name, + request.Cursor + ); - public override Task GetById(GetCustomerByIdRequest request, ServerCallContext context) - { - throw new NotImplementedException(); + IQueryable Query = _dbContext.Customers.Select( + Customer => _mapper.Map(Customer) + ); + + // TODO + // IQueryable < GetCustomerByIdResponse > Query = _dbContext.Customers.Select( + // Customer => new GetCustomerByIdResponse + // { + // CustomerId = Customer.CustomerId, + // Person = new() + // { + // Name = Customer.Person.Name, + // MobilePhoneNumber = Customer.Person.MobilePhoneNumber, + // BirthDate = Customer.Person.BirthDate, + // Cpf = Customer.Person.Cpf, + // // Cin = Customer.Person.Cin, + // }, + // Dependents = { + // Customer.Dependents.Select( + // Dependent => new Protobufs.Person + // { + // Name = Dependent.Name, + // BirthDate = Dependent.BirthDate, + // } + // ).ToList(), + // }, + // BillingAddress = Customer.BillingAddress, + // AdditionalInformation = Customer.AdditionalInformation, + // } + // ); + + List Customers = []; + + /// If cursor is bigger than the size of the collection you will get the following error + /// ArgumentOutOfRangeException "Index was out of range. Must be non-negative and less than the size of the collection" + Customers = await Query + .Where(x => x.CustomerId > request.Cursor) + .Take(20) + .ToListAsync(); + + GetPaginatedCustomersResponse response = new(); + + response.Customers.AddRange(Customers); + if (Customers.Count < 20) + { + /// Avoiding `ArgumentOutOfRangeException`, basically, don't fetch if null + response.NextCursor = null; + } + else + { + /// Id of the last element of the list, same value as `Users[Users.Count - 1].Id` + response.NextCursor = Customers[^1].CustomerId; + } + + _logger.LogInformation( + "({TraceIdentifier}) multiple records ({RecordType}) accessed successfully", + RequestTracerId, + typeof(Customer).Name + ); + return response; } - public override Task GetAllOptions(GetAllCustomersOptionsRequest request, ServerCallContext context) + public override async Task GetByIdAsync(GetCustomerByIdRequest request, ServerCallContext context) { - throw new NotImplementedException(); + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Customer).Name, + request.CustomerId + ); + + Customer? Customer = await _dbContext.Customers.FindAsync(request.CustomerId); + + if (Customer is null) + { + _logger.LogWarning( + "({TraceIdentifier}) record ({RecordType}) not found", + RequestTracerId, + typeof(Customer).Name + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Nenhum produto com ID {request.CustomerId}" + )); + } + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) accessed successfully", + RequestTracerId, + typeof(Customer).Name + ); + + return _mapper.Map(Customer); + // TODO + // return new GetCustomerByIdResponse + // { + // CustomerId = Customer.CustomerId, + // Person = new() + // { + // Name = Customer.Person.Name, + // MobilePhoneNumber = Customer.Person.MobilePhoneNumber, + // BirthDate = Customer.Person.BirthDate, + // Cpf = Customer.Person.Cpf, + // // Cin = Customer.Person.Cin, + // }, + // Dependents = { + // Customer.Dependents.Select( + // Dependent => new Protobufs.Person + // { + // Name = Dependent.Name, + // BirthDate = Dependent.BirthDate, + // } + // ).ToList(), + // }, + // BillingAddress = Customer.BillingAddress, + // AdditionalInformation = Customer.AdditionalInformation, + // }; } - public override Task Post(CreateCustomerRequest request, ServerCallContext context) + public override async Task PostAsync(CreateCustomerRequest request, ServerCallContext context) { - throw new NotImplementedException(); + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} creating new record ({RecordType})", + RequestTracerId, + UserId, + typeof(Customer).Name + ); + + Customer Customer = _mapper.Map(request); + Customer.CreatedBy = UserId; + + // TODO + // var Customer = new Customer + // { + // Person = new() + // { + // Name = request.Person.Name, + // MobilePhoneNumber = request.Person.MobilePhoneNumber, + // BirthDate = request.Person.BirthDate, + // Cpf = request.Person.Cpf, + // Cin = request.Person.Cin, + // CreatedBy = UserId, + // }, + // Dependents = + // request.Dependents.Select( + // Dependent => new Models.Person + // { + // Name = Dependent.Name, + // BirthDate = Dependent.BirthDate, + // MobilePhoneNumber = Dependent.MobilePhoneNumber, + // Cpf = Dependent.Cpf, + // // Cin = Dependent.Cin, + // CreatedBy = UserId, + // } + // ).ToList(), + // BillingAddress = request.BillingAddress, + // AdditionalInformation = request.AdditionalInformation, + // CreatedBy = UserId, + // }; + + await _dbContext.AddAsync(Customer); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) created successfully, RecordId {RecordId}", + RequestTracerId, + typeof(Customer).Name, + Customer.CustomerId + ); + + return new CreateCustomerResponse(); } - public override Task Put(UpdateCustomerRequest request, ServerCallContext context) + public override Task PutAsync(UpdateCustomerRequest request, ServerCallContext context) { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} updating record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Customer).Name, + request.CustomerId + ); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) updated successfully", + RequestTracerId, + typeof(Customer).Name + ); + throw new NotImplementedException(); + + // TODO + // if (request.Id <= 0) + // throw new RpcException(new Status(StatusCode.InvalidArgument, "You must supply a valid id")); + + // CustomerModel? Customer = await _dbContext.Customers.FirstOrDefaultAsync(x => x.Id == request.Id); + // if (Customer is null) + // { + // throw new RpcException(new Status( + // StatusCode.NotFound, $"registro não encontrado" + // )); + // } + + // Customer.Name = request.Name; + // // TODO Add Another fields + + // await _dbContext.SaveChangesAsync(); + // // TODO Log => Record (record type) ID Y was updated. Old value of (field name): (old value). New value: (new value). (This logs specific changes made to a field within a record) + // return new UpdateCustomerResponse(); } - public override Task Delete(DeleteCustomerRequest request, ServerCallContext context) + public override async Task DeleteAsync(DeleteCustomerRequest request, ServerCallContext context) { - throw new NotImplementedException(); + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} deleting record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Customer).Name, + request.CustomerId + ); + + Customer? Customer = await _dbContext.Customers.FindAsync(request.CustomerId); + + if (Customer is null) + { + _logger.LogWarning( + "({TraceIdentifier}) Error deleting record ({RecordType}) with ID {Id}, record not found", + RequestTracerId, + typeof(Customer).Name, + request.CustomerId + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Erro ao remover registro, nenhum registro com ID {request.CustomerId}" + )); + } + + /// TODO check if record is being used before deleting it use something like PK or FK + + _dbContext.Customers.Remove(Customer); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) deleted successfully", + RequestTracerId, + typeof(Customer).Name + ); + + return new DeleteCustomerResponse(); } } diff --git a/Services/DisciplineRpcService.cs b/Services/DisciplineRpcService.cs index 5d86298..23e3b91 100644 --- a/Services/DisciplineRpcService.cs +++ b/Services/DisciplineRpcService.cs @@ -1,40 +1,289 @@ -using Grpc.Core; -using GsServer.Protobufs; - -namespace GsServer.Services; - -public class DisciplineRpcService : DisciplineService.DisciplineServiceBase -{ - private readonly DatabaseContext _dbContext; - private readonly ILogger _logger; - public DisciplineRpcService(ILogger logger, DatabaseContext dbContext) - { - _logger = logger; - _dbContext = dbContext; - } - - public override Task GetPaginated(GetPaginatedDisciplinesRequest request, ServerCallContext context) - { - throw new NotImplementedException(); - } - - public override Task GetById(GetDisciplineByIdRequest request, ServerCallContext context) - { - throw new NotImplementedException(); - } - - public override Task Post(CreateDisciplineRequest request, ServerCallContext context) - { - throw new NotImplementedException(); - } - - public override Task Put(UpdateDisciplineRequest request, ServerCallContext context) - { - throw new NotImplementedException(); - } - - public override Task Delete(DeleteDisciplineRequest request, ServerCallContext context) - { - throw new NotImplementedException(); - } -} +// using System.Security.Claims; +// using AutoMapper; +// using Grpc.Core; +// using GsServer.Models; +// using GsServer.Protobufs; + +// namespace GsServer.Services; + +// public class DisciplineRpcService : DisciplineService.DisciplineServiceBase +// { +// private readonly DatabaseContext _dbContext; +// private readonly ILogger _logger; +// private readonly IMapper _mapper; +// public DisciplineRpcService( +// ILogger logger, +// DatabaseContext dbContext, +// IMapper mapper +// ) +// { +// _logger = logger; +// _dbContext = dbContext; +// _mapper = mapper; +// } + +// public override async Task GetPaginatedAsync(GetPaginatedDisciplinesRequest request, ServerCallContext context) +// { +// string RequestTracerId = context.GetHttpContext().TraceIdentifier; +// int UserId = int.Parse( +// context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! +// ); +// _logger.LogInformation( +// "({TraceIdentifier}) User {UserID} accessing multiple records ({RecordType}) with cursor {Cursor}", +// RequestTracerId, +// UserId, +// typeof(Discipline).Name, +// request.Cursor +// ); + +// IQueryable Query = _dbContext.Disciplines.Select( +// Discipline => _mapper.Map(Discipline) +// ); + +// // TODO +// // IQueryable Query = _dbContext.Disciplines.Select( +// // Discipline => new GetDisciplineByIdResponse +// // { +// // TODO +// // } +// // ); + +// List Disciplines = []; + +// /// If cursor is bigger than the size of the collection you will get the following error +// /// ArgumentOutOfRangeException "Index was out of range. Must be non-negative and less than the size of the collection" +// Disciplines = await Query +// .Where(x => x.DisciplinePk > request.Cursor) +// .Take(20) +// .ToListAsync(); + +// GetPaginatedDisciplinesResponse response = new(); + +// response.Disciplines.AddRange(Disciplines); +// if (Disciplines.Count < 20) +// { +// /// Avoiding `ArgumentOutOfRangeException`, basically, don't fetch if null +// response.NextCursor = null; +// } +// else +// { +// /// Id of the last element of the list, same value as `Users[Users.Count - 1].Id` +// response.NextCursor = Disciplines[^1].DisciplinePk; +// } + +// _logger.LogInformation( +// "({TraceIdentifier}) multiple records ({RecordType}) accessed successfully", +// RequestTracerId, +// typeof(Discipline).Name +// ); +// return response; +// } + +// public override async Task GetByIdAsync(GetDisciplineByIdRequest request, ServerCallContext context) +// { +// string RequestTracerId = context.GetHttpContext().TraceIdentifier; +// int UserId = int.Parse( +// context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! +// ); + +// _logger.LogInformation( +// "({TraceIdentifier}) User {UserID} accessing record ({RecordType}) with ID ({RecordId})", +// RequestTracerId, +// UserId, +// typeof(Discipline).Name, +// request.DisciplinePk +// ); + +// Discipline? Discipline = await _dbContext.Disciplines.FindAsync(request.DisciplinePk); + +// if (Discipline is null) +// { +// _logger.LogWarning( +// "({TraceIdentifier}) record ({RecordType}) not found", +// RequestTracerId, +// typeof(Discipline).Name +// ); +// throw new RpcException(new Status( +// StatusCode.NotFound, $"Nenhum produto com ID {request.DisciplinePk}" +// )); +// } + +// _logger.LogInformation( +// "({TraceIdentifier}) record ({RecordType}) accessed successfully", +// RequestTracerId, +// typeof(Discipline).Name +// ); +// // Create a mapping between the two enums +// Dictionary protobufToCSharpMapping = new Dictionary +// { +// { Protobufs.DayOfWeek.SUNDAY, DayOfWeek.Sunday }, +// { ProtobufDayOfWeek.MONDAY, DayOfWeek.Monday }, +// { ProtobufDayOfWeek.TUESDAY, DayOfWeek.Tuesday }, +// { ProtobufDayOfWeek.WEDNESDAY, DayOfWeek.Wednesday }, +// { ProtobufDayOfWeek.THURSDAY, DayOfWeek.Thursday }, +// { ProtobufDayOfWeek.FRIDAY, DayOfWeek.Friday }, +// { ProtobufDayOfWeek.SATURDAY, DayOfWeek.Saturday } +// }; +// List csharp_list = []; +// List protobuf_list = []; +// foreach (var day in Discipline.ClassDays) +// { +// csharp_list.Add((DayOfWeek)day); +// } + +// return new GetDisciplineByIdResponse +// { +// DisciplinePk = Discipline.DisciplinePk, +// Name = Discipline.Name, +// TuitionPrice = Discipline.TuitionPrice, +// InstructorFk = Discipline.InstructorFk, +// StartTime = new() +// { +// Hour = Discipline.StartTime.Hour, +// Minute = Discipline.StartTime.Minute, +// }, +// EndTime = new() +// { +// Hour = Discipline.EndTime.Hour, +// Minute = Discipline.EndTime.Minute, +// }, +// ClassDays = { +// Discipline.ClassDays.Select( +// ClassDay => ClassDay. +// ).ToList(), +// }, +// IsActive = Discipline.IsActive, +// }; +// } + +// public override async Task PostAsync(CreateDisciplineRequest request, ServerCallContext context) +// { +// string RequestTracerId = context.GetHttpContext().TraceIdentifier; +// int UserId = int.Parse( +// context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! +// ); + +// _logger.LogInformation( +// "({TraceIdentifier}) User {UserID} creating new record ({RecordType})", +// RequestTracerId, +// UserId, +// typeof(Discipline).Name +// ); + + + +// var Discipline = new Discipline +// { +// Name = request.Name, +// TuitionPrice = request.TuitionPrice, +// InstructorFk = request.InstructorFk, +// StartTime = new( +// request.EndTime.Hour, +// request.EndTime.Minute +// ), +// EndTime = new( +// request.EndTime.Hour, +// request.EndTime.Minute +// ), +// ClassDays = request.ClassDays, +// CreatedBy = UserId, +// }; + +// await _dbContext.AddAsync(Discipline); +// await _dbContext.SaveChangesAsync(); + +// _logger.LogInformation( +// "({TraceIdentifier}) record ({RecordType}) created successfully, RecordId {RecordId}", +// RequestTracerId, +// typeof(Discipline).Name, +// Discipline.DisciplinePk +// ); + +// return new CreateDisciplineResponse(); +// } + +// public override Task PutAsync(UpdateDisciplineRequest request, ServerCallContext context) +// { +// string RequestTracerId = context.GetHttpContext().TraceIdentifier; +// int UserId = int.Parse( +// context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! +// ); +// _logger.LogInformation( +// "({TraceIdentifier}) User {UserID} updating record ({RecordType}) with ID ({RecordId})", +// RequestTracerId, +// UserId, +// typeof(Discipline).Name, +// request.DisciplinePk +// ); + +// _logger.LogInformation( +// "({TraceIdentifier}) record ({RecordType}) updated successfully", +// RequestTracerId, +// typeof(Discipline).Name +// ); + +// throw new NotImplementedException(); + +// // TODO +// // if (request.Id <= 0) +// // throw new RpcException(new Status(StatusCode.InvalidArgument, "You must supply a valid id")); + +// // Discipline? Discipline = await _dbContext.Disciplines.FirstOrDefaultAsync(x => x.Id == request.Id); +// // if (Discipline is null) +// // { +// // throw new RpcException(new Status( +// // StatusCode.NotFound, $"registro não encontrado" +// // )); +// // } + +// // Discipline.Name = request.Name; +// // // TODO Add Another fields + +// // await _dbContext.SaveChangesAsync(); +// // // TODO Log => Record (record type) ID Y was updated. Old value of (field name): (old value). New value: (new value). (This logs specific changes made to a field within a record) +// // return new UpdateDisciplineResponse(); +// } + +// public override async Task DeleteAsync(DeleteDisciplineRequest request, ServerCallContext context) +// { +// string RequestTracerId = context.GetHttpContext().TraceIdentifier; +// int UserId = int.Parse( +// context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! +// ); +// _logger.LogInformation( +// "({TraceIdentifier}) User {UserID} deleting record ({RecordType}) with ID ({RecordId})", +// RequestTracerId, +// UserId, +// typeof(Discipline).Name, +// request.DisciplinePk +// ); + +// Discipline? Discipline = await _dbContext.Disciplines.FindAsync(request.DisciplinePk); + +// if (Discipline is null) +// { +// _logger.LogWarning( +// "({TraceIdentifier}) Error deleting record ({RecordType}) with ID {Id}, record not found", +// RequestTracerId, +// typeof(Discipline).Name, +// request.DisciplinePk +// ); +// throw new RpcException(new Status( +// StatusCode.NotFound, $"Erro ao remover registro, nenhum registro com ID {request.DisciplinePk}" +// )); +// } + +// /// TODO check if record is being used before deleting it use something like PK or FK + +// _dbContext.Disciplines.Remove(Discipline); +// await _dbContext.SaveChangesAsync(); + +// _logger.LogInformation( +// "({TraceIdentifier}) record ({RecordType}) deleted successfully", +// RequestTracerId, +// typeof(Discipline).Name +// ); + +// return new DeleteDisciplineResponse(); +// } +// } diff --git a/Services/InstructorRpcService.cs b/Services/InstructorRpcService.cs index 9ec6879..81e325d 100644 --- a/Services/InstructorRpcService.cs +++ b/Services/InstructorRpcService.cs @@ -1,4 +1,7 @@ +using System.Security.Claims; +using AutoMapper; using Grpc.Core; +using GsServer.Models; using GsServer.Protobufs; namespace GsServer.Services; @@ -7,39 +10,261 @@ public class InstructorRpcService : InstructorService.InstructorServiceBase { private readonly DatabaseContext _dbContext; private readonly ILogger _logger; - public InstructorRpcService(ILogger logger, DatabaseContext dbContext) + private readonly IMapper _mapper; + public InstructorRpcService( + ILogger logger, + DatabaseContext dbContext, + IMapper mapper + ) { _logger = logger; _dbContext = dbContext; + _mapper = mapper; } - public override Task GetPaginated(GetPaginatedInstructorsRequest request, ServerCallContext context) + public override async Task GetPaginatedAsync(GetPaginatedInstructorsRequest request, ServerCallContext context) { - throw new NotImplementedException(); - } + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing multiple records ({RecordType}) with cursor {Cursor}", + RequestTracerId, + UserId, + typeof(Instructor).Name, + request.Cursor + ); - public override Task GetById(GetInstructorByIdRequest request, ServerCallContext context) - { - throw new NotImplementedException(); + IQueryable Query = _dbContext.Instructors.Select( + Instructor => _mapper.Map(Instructor) + ); + + // TODO + // IQueryable Query = _dbContext.Instructors.Select( + // Instructor => new GetInstructorByIdResponse + // { + // InstructorId = Instructor.InstructorId, + // Person = new Person + // { + // Name = Instructor.Person.Name, + // MobilePhoneNumber = Instructor.Person.MobilePhoneNumber, + // BirthDate = Instructor.Person.BirthDate, + // Cpf = Instructor.Person.Cpf, + // Cin = Instructor.Person.Cin, + // }, + // } + // ); + + List Instructors = []; + + /// If cursor is bigger than the size of the collection you will get the following error + /// ArgumentOutOfRangeException "Index was out of range. Must be non-negative and less than the size of the collection" + Instructors = await Query + .Where(x => x.InstructorId > request.Cursor) + .Take(20) + .ToListAsync(); + + GetPaginatedInstructorsResponse response = new(); + + response.Instructors.AddRange(Instructors); + if (Instructors.Count < 20) + { + /// Avoiding `ArgumentOutOfRangeException`, basically, don't fetch if null + response.NextCursor = null; + } + else + { + /// Id of the last element of the list, same value as `Users[Users.Count - 1].Id` + response.NextCursor = Instructors[^1].InstructorId; + } + + _logger.LogInformation( + "({TraceIdentifier}) multiple records ({RecordType}) accessed successfully", + RequestTracerId, + typeof(Instructor).Name + ); + return response; } - public override Task GetAllOptions(GetAllInstructorsOptionsRequest request, ServerCallContext context) + public override async Task GetByIdAsync(GetInstructorByIdRequest request, ServerCallContext context) { - throw new NotImplementedException(); + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Instructor).Name, + request.InstructorId + ); + + Instructor? Instructor = await _dbContext.Instructors.FindAsync(request.InstructorId); + + if (Instructor is null) + { + _logger.LogWarning( + "({TraceIdentifier}) record ({RecordType}) not found", + RequestTracerId, + typeof(Instructor).Name + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Nenhum produto com ID {request.InstructorId}" + )); + } + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) accessed successfully", + RequestTracerId, + typeof(Instructor).Name + ); + + return _mapper.Map(Instructor); + // TODO + // return new GetInstructorByIdResponse + // { + // InstructorId = Instructor.InstructorId, + // Person = new Person + // { + // Name = Instructor.Person.Name, + // MobilePhoneNumber = Instructor.Person.MobilePhoneNumber, + // BirthDate = Instructor.Person.BirthDate, + // Cpf = Instructor.Person.Cpf, + // Cin = Instructor.Person.Cin, + // }, + // }; } - public override Task Post(CreateInstructorRequest request, ServerCallContext context) + public override async Task PostAsync(CreateInstructorRequest request, ServerCallContext context) { - throw new NotImplementedException(); + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} creating new record ({RecordType})", + RequestTracerId, + UserId, + typeof(Instructor).Name + ); + + Instructor Instructor = _mapper.Map(request); + Instructor.CreatedBy = UserId; + + // TODO + // var Instructor = new Instructor + // { + // Person = new Person + // { + // Name = request.Person.Name, + // MobilePhoneNumber = request.Person.MobilePhoneNumber, + // BirthDate = request.Person.BirthDate, + // Cpf = request.Person.Cpf, + // Cin = request.Person.Cin, + // CreatedBy = UserId, + // }, + // CreatedBy = UserId, + // }; + + await _dbContext.AddAsync(Instructor); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) created successfully, RecordId {RecordId}", + RequestTracerId, + typeof(Instructor).Name, + Instructor.InstructorId + ); + + return new CreateInstructorResponse(); } - public override Task Put(UpdateInstructorRequest request, ServerCallContext context) + public override Task PutAsync(UpdateInstructorRequest request, ServerCallContext context) { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} updating record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Instructor).Name, + request.InstructorId + ); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) updated successfully", + RequestTracerId, + typeof(Instructor).Name + ); + throw new NotImplementedException(); + + // TODO + // if (request.Id <= 0) + // throw new RpcException(new Status(StatusCode.InvalidArgument, "You must supply a valid id")); + + // InstructorModel? Instructor = await _dbContext.Instructors.FirstOrDefaultAsync(x => x.Id == request.Id); + // if (Instructor is null) + // { + // throw new RpcException(new Status( + // StatusCode.NotFound, $"registro não encontrado" + // )); + // } + + // Instructor.Name = request.Name; + // // TODO Add Another fields + + // await _dbContext.SaveChangesAsync(); + // // TODO Log => Record (record type) ID Y was updated. Old value of (field name): (old value). New value: (new value). (This logs specific changes made to a field within a record) + // return new UpdateInstructorResponse(); } - public override Task Delete(DeleteInstructorRequest request, ServerCallContext context) + public override async Task DeleteAsync(DeleteInstructorRequest request, ServerCallContext context) { - throw new NotImplementedException(); + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} deleting record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Instructor).Name, + request.InstructorId + ); + + Instructor? Instructor = await _dbContext.Instructors.FindAsync(request.InstructorId); + + if (Instructor is null) + { + _logger.LogWarning( + "({TraceIdentifier}) Error deleting record ({RecordType}) with ID {Id}, record not found", + RequestTracerId, + typeof(Instructor).Name, + request.InstructorId + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Erro ao remover registro, nenhum registro com ID {request.InstructorId}" + )); + } + + /// TODO check if record is being used before deleting it use something like PK or FK + + _dbContext.Instructors.Remove(Instructor); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) deleted successfully", + RequestTracerId, + typeof(Instructor).Name + ); + + return new DeleteInstructorResponse(); } } diff --git a/Services/NotificationRpcService.cs b/Services/NotificationRpcService.cs new file mode 100644 index 0000000..925c6ec --- /dev/null +++ b/Services/NotificationRpcService.cs @@ -0,0 +1,254 @@ +using System.Security.Claims; +using AutoMapper; +using Grpc.Core; +using GsServer.Models; +using GsServer.Protobufs; + +namespace GsServer.Services; + +public class NotificationRpcService : NotificationService.NotificationServiceBase +{ + private readonly DatabaseContext _dbContext; + private readonly ILogger _logger; + private readonly IMapper _mapper; + public NotificationRpcService( + ILogger logger, + DatabaseContext dbContext, + IMapper mapper + ) + { + _logger = logger; + _dbContext = dbContext; + _mapper = mapper; + } + + public override async Task GetPaginatedAsync(GetPaginatedNotificationsRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing multiple records ({RecordType}) with cursor {Cursor}", + RequestTracerId, + UserId, + typeof(Notification).Name, + request.Cursor + ); + + IQueryable Query = _dbContext.Notifications.Select( + Notification => _mapper.Map(Notification) + ); + + // Todo + // IQueryable Query = _dbContext.Notifications.Select( + // Notification => new GetNotificationByIdResponse + // { + // NotificationId = Notification.NotificationId, + // Title = Notification.Title, + // Message = Notification.Message, + // IsUnread = Notification.IsUnread, + // } + // ); + + List Notifications = []; + + /// If cursor is bigger than the size of the collection you will get the following error + /// ArgumentOutOfRangeException "Index was out of range. Must be non-negative and less than the size of the collection" + Notifications = await Query + .Where(x => x.NotificationId > request.Cursor) + .Take(20) + .ToListAsync(); + + GetPaginatedNotificationsResponse response = new(); + + response.Notifications.AddRange(Notifications); + if (Notifications.Count < 20) + { + /// Avoiding `ArgumentOutOfRangeException`, basically, don't fetch if null + response.NextCursor = null; + } + else + { + /// Id of the last element of the list, same value as `Users[Users.Count - 1].Id` + response.NextCursor = Notifications[^1].NotificationId; + } + + _logger.LogInformation( + "({TraceIdentifier}) multiple records ({RecordType}) accessed successfully", + RequestTracerId, + typeof(Notification).Name + ); + return response; + } + + public override async Task GetByIdAsync(GetNotificationByIdRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Notification).Name, + request.NotificationId + ); + + Notification? Notification = await _dbContext.Notifications.FindAsync(request.NotificationId); + + if (Notification is null) + { + _logger.LogWarning( + "({TraceIdentifier}) record ({RecordType}) not found", + RequestTracerId, + typeof(Notification).Name + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Nenhum produto com ID {request.NotificationId}" + )); + } + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) accessed successfully", + RequestTracerId, + typeof(Notification).Name + ); + + return _mapper.Map(Notification); + // TODO + // return new GetNotificationByIdResponse + // { + // NotificationId = Notification.NotificationId, + // Title = Notification.Title, + // Message = Notification.Message, + // IsUnread = Notification.IsUnread, + // }; + } + + public override async Task PostAsync(CreateNotificationRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} creating new record ({RecordType})", + RequestTracerId, + UserId, + typeof(Notification).Name + ); + + Notification Notification = _mapper.Map(request); + Notification.CreatedBy = UserId; + + // TODO + // var Notification = new Notification + // { + // UserFk = request.UserFk, + // Title = request.Title, + // Message = request.Message, + // CreatedBy = UserId, + // }; + + await _dbContext.AddAsync(Notification); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) created successfully, RecordId {RecordId}", + RequestTracerId, + typeof(Notification).Name, + Notification.NotificationId + ); + + return new CreateNotificationResponse(); + } + + public override Task PutAsync(UpdateNotificationRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} updating record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Notification).Name, + request.NotificationId + ); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) updated successfully", + RequestTracerId, + typeof(Notification).Name + ); + + throw new NotImplementedException(); + + // TODO + // if (request.Id <= 0) + // throw new RpcException(new Status(StatusCode.InvalidArgument, "You must supply a valid id")); + + // NotificationModel? Notification = await _dbContext.Notifications.FirstOrDefaultAsync(x => x.Id == request.Id); + // if (Notification is null) + // { + // throw new RpcException(new Status( + // StatusCode.NotFound, $"registro não encontrado" + // )); + // } + + // Notification.Name = request.Name; + // // TODO Add Another fields + + // await _dbContext.SaveChangesAsync(); + // // TODO Log => Record (record type) ID Y was updated. Old value of (field name): (old value). New value: (new value). (This logs specific changes made to a field within a record) + // return new UpdateNotificationResponse(); + } + + public override async Task DeleteAsync(DeleteNotificationRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} deleting record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Notification).Name, + request.NotificationId + ); + + Notification? Notification = await _dbContext.Notifications.FindAsync(request.NotificationId); + + if (Notification is null) + { + _logger.LogWarning( + "({TraceIdentifier}) Error deleting record ({RecordType}) with ID {Id}, record not found", + RequestTracerId, + typeof(Notification).Name, + request.NotificationId + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Erro ao remover registro, nenhum registro com ID {request.NotificationId}" + )); + } + + /// TODO check if record is being used before deleting it use something like PK or FK + + _dbContext.Notifications.Remove(Notification); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) deleted successfully", + RequestTracerId, + typeof(Notification).Name + ); + + return new DeleteNotificationResponse(); + } +} diff --git a/Services/OrderRpcService.cs b/Services/OrderRpcService.cs index a2ef3cc..8531448 100644 --- a/Services/OrderRpcService.cs +++ b/Services/OrderRpcService.cs @@ -1,4 +1,6 @@ +using System.Security.Claims; using Grpc.Core; +using GsServer.Models; using GsServer.Protobufs; namespace GsServer.Services; @@ -13,28 +15,212 @@ public OrderRpcService(ILogger logger, DatabaseContext dbContex _dbContext = dbContext; } - public override Task GetPaginated(GetPaginatedOrdersRequest request, ServerCallContext context) + public override async Task GetPaginatedAsync(GetPaginatedOrdersRequest request, ServerCallContext context) { - throw new NotImplementedException(); + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing multiple records ({RecordType}) with cursor {Cursor}", + RequestTracerId, + UserId, + typeof(Order).Name, + request.Cursor + ); + IQueryable Query = _dbContext.Orders.Select( + Order => new GetOrderByIdResponse + { + // TODO + } + ); + + List Orders = []; + + /// If cursor is bigger than the size of the collection you will get the following error + /// ArgumentOutOfRangeException "Index was out of range. Must be non-negative and less than the size of the collection" + Orders = await Query + .Where(x => x.OrderId > request.Cursor) + .Take(20) + .ToListAsync(); + + GetPaginatedOrdersResponse response = new(); + + response.Orders.AddRange(Orders); + if (Orders.Count < 20) + { + /// Avoiding `ArgumentOutOfRangeException`, basically, don't fetch if null + response.NextCursor = null; + } + else + { + /// Id of the last element of the list, same value as `Users[Users.Count - 1].Id` + response.NextCursor = Orders[^1].OrderId; + } + + _logger.LogInformation( + "({TraceIdentifier}) multiple records ({RecordType}) accessed successfully", + RequestTracerId, + typeof(Order).Name + ); + return response; } - public override Task GetById(GetOrderByIdRequest request, ServerCallContext context) + public override async Task GetByIdAsync(GetOrderByIdRequest request, ServerCallContext context) { - throw new NotImplementedException(); + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Order).Name, + request.OrderId + ); + + Order? Order = await _dbContext.Orders.FindAsync(request.OrderId); + + if (Order is null) + { + _logger.LogWarning( + "({TraceIdentifier}) record ({RecordType}) not found", + RequestTracerId, + typeof(Order).Name + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Nenhum produto com ID {request.OrderId}" + )); + } + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) accessed successfully", + RequestTracerId, + typeof(Order).Name + ); + + return new GetOrderByIdResponse + { + // TODO + }; } - public override Task Post(CreateOrderRequest request, ServerCallContext context) + public override async Task PostAsync(CreateOrderRequest request, ServerCallContext context) { - throw new NotImplementedException(); + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} creating new record ({RecordType})", + RequestTracerId, + UserId, + typeof(Order).Name + ); + + // TODO + // var Order = new OrderModel + // { + // }; + + // await _dbContext.AddAsync(Order); + // await _dbContext.SaveChangesAsync(); + + // _logger.LogInformation( + // "({TraceIdentifier}) record ({RecordType}) created successfully, RecordId {RecordId}", + // RequestTracerId, + // typeof(OrderModel).Name, + // Order.OrderId + // ); + + return new CreateOrderResponse(); } - public override Task Put(UpdateOrderRequest request, ServerCallContext context) + public override Task PutAsync(UpdateOrderRequest request, ServerCallContext context) { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} updating record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Order).Name, + request.OrderId + ); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) updated successfully", + RequestTracerId, + typeof(Order).Name + ); + throw new NotImplementedException(); + + // TODO + // if (request.Id <= 0) + // throw new RpcException(new Status(StatusCode.InvalidArgument, "You must supply a valid id")); + + // OrderModel? Order = await _dbContext.Orders.FirstOrDefaultAsync(x => x.Id == request.Id); + // if (Order is null) + // { + // throw new RpcException(new Status( + // StatusCode.NotFound, $"registro não encontrado" + // )); + // } + + // Order.Name = request.Name; + // // TODO Add Another fields + + // await _dbContext.SaveChangesAsync(); + // // TODO Log => Record (record type) ID Y was updated. Old value of (field name): (old value). New value: (new value). (This logs specific changes made to a field within a record) + // return new UpdateOrderResponse(); } - public override Task Delete(DeleteOrderRequest request, ServerCallContext context) + public override async Task DeleteAsync(DeleteOrderRequest request, ServerCallContext context) { - throw new NotImplementedException(); + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} deleting record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Order).Name, + request.OrderId + ); + + Order? Order = await _dbContext.Orders.FindAsync(request.OrderId); + + if (Order is null) + { + _logger.LogWarning( + "({TraceIdentifier}) Error deleting record ({RecordType}) with ID {Id}, record not found", + RequestTracerId, + typeof(Order).Name, + request.OrderId + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Erro ao remover registro, nenhum registro com ID {request.OrderId}" + )); + } + + /// TODO check if record is being used before deleting it use something like PK or FK + + _dbContext.Orders.Remove(Order); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) deleted successfully", + RequestTracerId, + typeof(Order).Name + ); + + return new DeleteOrderResponse(); } } diff --git a/Services/PaymentRpcService.cs b/Services/PaymentRpcService.cs new file mode 100644 index 0000000..b64e936 --- /dev/null +++ b/Services/PaymentRpcService.cs @@ -0,0 +1,301 @@ +using System.Security.Claims; +using AutoMapper; +using Grpc.Core; +using GsServer.Models; +using GsServer.Protobufs; +using Microsoft.VisualBasic; + +namespace GsServer.Services; + +public class PaymentRpcService : PaymentService.PaymentServiceBase +{ + private readonly DatabaseContext _dbContext; + private readonly ILogger _logger; + private readonly IMapper _mapper; + public PaymentRpcService( + ILogger logger, + DatabaseContext dbContext, + IMapper mapper + ) + { + _logger = logger; + _dbContext = dbContext; + _mapper = mapper; + } + + public override async Task GetPaginatedAsync(GetPaginatedPaymentsRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing multiple records ({RecordType}) with cursor {Cursor}", + RequestTracerId, + UserId, + typeof(Payment).Name, + request.Cursor + ); + + IQueryable Query = _dbContext.Payments.Select( + Payment => _mapper.Map(Payment) + ); + + // TODO + // IQueryable Query = _dbContext.Payments.Select( + // Payment => new GetPaymentByIdResponse + // { + // PaymentId = Payment.PaymentId, + // Comments = Payment.Comments, + // Installments = { + // Payment.Installments.Select( + // Installment => new Protobufs.PaymentInstallment + // { + // PaymentInstallmentPk = Installment.PaymentInstallmentPk, + // PaymentFk = Installment.PaymentFk, + // InstallmentNumber = Installment.PaymentFk, + // InstallmentAmount = Installment.InstallmentAmount, + // PaymentMethod = Installment.PaymentMethod, + // DueDate = new () + // { + // Year = Installment.DueDate.Year, + // Month = Installment.DueDate.Month, + // Day = Installment.DueDate.Day, + // }, + // } + // ).ToList(), + // }, + // } + // ); + + List Payments = []; + + /// If cursor is bigger than the size of the collection you will get the following error + /// ArgumentOutOfRangeException "Index was out of range. Must be non-negative and less than the size of the collection" + Payments = await Query + .Where(x => x.PaymentId > request.Cursor) + .Take(20) + .ToListAsync(); + + GetPaginatedPaymentsResponse response = new(); + + response.Payments.AddRange(Payments); + if (Payments.Count < 20) + { + /// Avoiding `ArgumentOutOfRangeException`, basically, don't fetch if null + response.NextCursor = null; + } + else + { + /// Id of the last element of the list, same value as `Users[Users.Count - 1].Id` + response.NextCursor = Payments[^1].PaymentId; + } + + _logger.LogInformation( + "({TraceIdentifier}) multiple records ({RecordType}) accessed successfully", + RequestTracerId, + typeof(Payment).Name + ); + return response; + } + + public override async Task GetByIdAsync(GetPaymentByIdRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Payment).Name, + request.PaymentId + ); + + Payment? Payment = await _dbContext.Payments.FindAsync(request.PaymentId); + + if (Payment is null) + { + _logger.LogWarning( + "({TraceIdentifier}) record ({RecordType}) not found", + RequestTracerId, + typeof(Payment).Name + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Nenhum produto com ID {request.PaymentId}" + )); + } + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) accessed successfully", + RequestTracerId, + typeof(Payment).Name + ); + + return _mapper.Map(Payment); + + // TODO + // return new GetPaymentByIdResponse + // { + // PaymentId = Payment.PaymentId, + // Comments = Payment.Comments, + // Installments = { + // Payment.Installments.Select( + // Installment => new Protobufs.PaymentInstallment + // { + // PaymentInstallmentPk = Installment.PaymentInstallmentPk, + // PaymentFk = Installment.PaymentFk, + // InstallmentNumber = Installment.PaymentFk, + // InstallmentAmount = Installment.InstallmentAmount, + // PaymentMethod = Installment.PaymentMethod, + // DueDate = new () + // { + // Year = Installment.DueDate.Year, + // Month = Installment.DueDate.Month, + // Day = Installment.DueDate.Day, + // }, + // } + // ).ToList(), + // }, + // }; + } + + public override async Task PostAsync(CreatePaymentRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} creating new record ({RecordType})", + RequestTracerId, + UserId, + typeof(Payment).Name + ); + + Payment Payment = _mapper.Map(request); + Payment.CreatedBy = UserId; + + // TODO + // var Payment = new Payment + // { + // Comments = request.Comments, + // Installments = request.Installments.Select( + // Installment => new Models.PaymentInstallment + // { + // PaymentInstallmentPk = Installment.PaymentInstallmentPk, + // PaymentFk = Installment.PaymentFk, + // InstallmentNumber = Installment.PaymentFk, + // InstallmentAmount = Installment.InstallmentAmount, + // PaymentMethod = Installment.PaymentMethod, + // DueDate = new( + // Installment.DueDate.Year, + // Installment.DueDate.Month, + // Installment.DueDate.Day + // ), + // } + // ).ToList(), + // CreatedBy = UserId, + // }; + + await _dbContext.AddAsync(Payment); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) created successfully, RecordId {RecordId}", + RequestTracerId, + typeof(Payment).Name, + Payment.PaymentId + ); + + return new CreatePaymentResponse(); + } + + public override Task PutAsync(UpdatePaymentRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} updating record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Payment).Name, + request.PaymentId + ); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) updated successfully", + RequestTracerId, + typeof(Payment).Name + ); + + throw new NotImplementedException(); + + // TODO + // if (request.Id <= 0) + // throw new RpcException(new Status(StatusCode.InvalidArgument, "You must supply a valid id")); + + // PaymentModel? Payment = await _dbContext.Payments.FirstOrDefaultAsync(x => x.Id == request.Id); + // if (Payment is null) + // { + // throw new RpcException(new Status( + // StatusCode.NotFound, $"registro não encontrado" + // )); + // } + + // Payment.Name = request.Name; + // // TODO Add Another fields + + // await _dbContext.SaveChangesAsync(); + // // TODO Log => Record (record type) ID Y was updated. Old value of (field name): (old value). New value: (new value). (This logs specific changes made to a field within a record) + // return new UpdatePaymentResponse(); + } + + public override async Task DeleteAsync(DeletePaymentRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} deleting record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Payment).Name, + request.PaymentId + ); + + Payment? Payment = await _dbContext.Payments.FindAsync(request.PaymentId); + + if (Payment is null) + { + _logger.LogWarning( + "({TraceIdentifier}) Error deleting record ({RecordType}) with ID {Id}, record not found", + RequestTracerId, + typeof(Payment).Name, + request.PaymentId + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Erro ao remover registro, nenhum registro com ID {request.PaymentId}" + )); + } + + /// TODO check if record is being used before deleting it use something like PK or FK + + _dbContext.Payments.Remove(Payment); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) deleted successfully", + RequestTracerId, + typeof(Payment).Name + ); + + return new DeletePaymentResponse(); + } +} diff --git a/Services/ProductRpcService.cs b/Services/ProductRpcService.cs index a7ab86e..57d7891 100644 --- a/Services/ProductRpcService.cs +++ b/Services/ProductRpcService.cs @@ -1,4 +1,5 @@ using System.Security.Claims; +using AutoMapper; using Grpc.Core; using GsServer.Models; using GsServer.Protobufs; @@ -9,13 +10,19 @@ public class ProductRpcService : ProductService.ProductServiceBase { private readonly DatabaseContext _dbContext; private readonly ILogger _logger; - public ProductRpcService(ILogger logger, DatabaseContext dbContext) + private readonly IMapper _mapper; + public ProductRpcService( + ILogger logger, + DatabaseContext dbContext, + IMapper mapper + ) { _logger = logger; _dbContext = dbContext; + _mapper = mapper; } - public override async Task GetAll(GetAllProductsRequest request, ServerCallContext context) + public override async Task GetAllAsync(GetAllProductsRequest request, ServerCallContext context) { string RequestTracerId = context.GetHttpContext().TraceIdentifier; int UserId = int.Parse( @@ -25,58 +32,68 @@ public override async Task GetAll(GetAllProductsRequest "({TraceIdentifier}) User {UserID} accessing all records ({RecordType})", RequestTracerId, UserId, - typeof(ProductModel).Name + typeof(Product).Name ); - List Products = - await _dbContext.Products - .ToListAsync(); + IQueryable Query = _dbContext.Products.Select( + Product => _mapper.Map(Product) + ); + + // TODO + // response.Products.AddRange( + // Products.Select( + // Product => new GetProductByIdResponse + // { + // Name = Product.Name, + // Description = Product.Description, + // PicturePath = Product.PicturePath, + // ProductBrandFk = Product.ProductBrand, + // ProductCategoryFk = Product.ProductCategory, + // Variants = + // { + // Product.Variants.Select( + // Variant => new Protobufs.ProductVariant + // { + // ProductVariantPk = Variant.ProductVariantPk, + // Color = Variant.Color, + // Size = Variant.Size, + // BarCode = Variant.BarCode, + // Sku = Variant.Sku, + // UnitPrice = Variant.UnitPrice, + // Inventory = new ProductVariantInventory + // { + // ProductVariantInventoryPk = Variant.Inventory.ProductVariantInventoryPk, + // ProductVariantFk = Variant.Inventory.ProductVariantFk, + // QuantityAvailable = Variant.Inventory.QuantityAvailable, + // MinimumStockAmount = Variant.Inventory.MinimumStockAmount, + // } + // } + // ).ToList(), + // }, + // } + // ).ToList() + // ); + + List Products = []; + + /// If cursor is bigger than the size of the collection you will get the following error + /// ArgumentOutOfRangeException "Index was out of range. Must be non-negative and less than the size of the collection" + Products = await Query + .ToListAsync(); GetAllProductsResponse response = new(); - response.Products.AddRange( - Products.Select( - Product => new GetProductByIdResponse - { - Name = Product.Name, - Description = Product.Description, - PicturePath = Product.PicturePath, - ProductBrandId = Product.ProductBrandId, - ProductCategoryId = Product.ProductCategoryId, - Variants = - { - Product.Variants.Select( - Variant => new ProductVariant - { - ProductVariantId = Variant.ProductVariantId, - Color = Variant.Color, - Size = Variant.Size, - BarCode = Variant.BarCode, - Sku = Variant.Sku, - UnitPrice = Variant.UnitPrice, - Inventory = new ProductVariantInventory - { - ProductVariantInventoryId = Variant.Inventory.ProductVariantInventoryId, - ProductVariantId = Variant.Inventory.ProductVariantId, - QuantityAvailable = Variant.Inventory.QuantityAvailable, - MinimumStockAmount = Variant.Inventory.MinimumStockAmount, - } - } - ), - }, - } - ).ToList() - ); + response.Products.AddRange(Products); _logger.LogInformation( "({TraceIdentifier}) all records ({RecordType}) accessed successfully", RequestTracerId, - typeof(ProductModel).Name + typeof(Product).Name ); return response; } - public override async Task GetById(GetProductByIdRequest request, ServerCallContext context) + public override async Task GetByIdAsync(GetProductByIdRequest request, ServerCallContext context) { string RequestTracerId = context.GetHttpContext().TraceIdentifier; int UserId = int.Parse( @@ -87,18 +104,18 @@ public override async Task GetById(GetProductByIdRequest "({TraceIdentifier}) User {UserID} accessing record ({RecordType}) with ID ({RecordId})", RequestTracerId, UserId, - typeof(ProductModel).Name, + typeof(Product).Name, request.ProductId ); - ProductModel? Product = await _dbContext.Products.FindAsync(request.ProductId); + Product? Product = await _dbContext.Products.FindAsync(request.ProductId); if (Product is null) { _logger.LogWarning( "({TraceIdentifier}) record ({RecordType}) not found", RequestTracerId, - typeof(ProductModel).Name + typeof(Product).Name ); throw new RpcException(new Status( StatusCode.NotFound, $"Nenhum produto com ID {request.ProductId}" @@ -108,41 +125,44 @@ public override async Task GetById(GetProductByIdRequest _logger.LogInformation( "({TraceIdentifier}) record ({RecordType}) accessed successfully", RequestTracerId, - typeof(ProductModel).Name + typeof(Product).Name ); - return new GetProductByIdResponse - { - Name = Product.Name, - Description = Product.Description, - PicturePath = Product.PicturePath, - ProductBrandId = Product.ProductBrandId, - ProductCategoryId = Product.ProductCategoryId, - Variants = - { - Product.Variants.Select( - Variant => new ProductVariant - { - ProductVariantId = Variant.ProductVariantId, - Color = Variant.Color, - Size = Variant.Size, - BarCode = Variant.BarCode, - Sku = Variant.Sku, - UnitPrice = Variant.UnitPrice, - Inventory = new ProductVariantInventory - { - ProductVariantInventoryId = Variant.Inventory.ProductVariantInventoryId, - ProductVariantId = Variant.Inventory.ProductVariantId, - QuantityAvailable = Variant.Inventory.QuantityAvailable, - MinimumStockAmount = Variant.Inventory.MinimumStockAmount, - } - } - ), - }, - }; + return _mapper.Map(Product); + + // TODO + // return new GetProductByIdResponse + // { + // Name = Product.Name, + // Description = Product.Description, + // PicturePath = Product.PicturePath, + // ProductBrandFk = Product.ProductBrand, + // ProductCategoryFk = Product.ProductCategory, + // Variants = + // { + // Product.Variants.Select( + // Variant => new ProductVariant + // { + // ProductVariantPk = Variant.ProductVariantPk, + // Color = Variant.Color, + // Size = Variant.Size, + // BarCode = Variant.BarCode, + // Sku = Variant.Sku, + // UnitPrice = Variant.UnitPrice, + // Inventory = new Protobufs.ProductVariantInventory + // { + // ProductVariantInventoryPk = Variant.Inventory.ProductVariantInventoryPk, + // ProductVariantFk = Variant.Inventory.ProductVariantFk, + // QuantityAvailable = Variant.Inventory.QuantityAvailable, + // MinimumStockAmount = Variant.Inventory.MinimumStockAmount, + // } + // } + // ), + // }, + // }; } - public override async Task Post(CreateProductRequest request, ServerCallContext context) + public override async Task PostAsync(CreateProductRequest request, ServerCallContext context) { string RequestTracerId = context.GetHttpContext().TraceIdentifier; int UserId = int.Parse( @@ -153,39 +173,41 @@ public override async Task Post(CreateProductRequest requ "({TraceIdentifier}) User {UserID} creating new record ({RecordType})", RequestTracerId, UserId, - typeof(ProductModel).Name + typeof(Product).Name ); // TODO upload binary from request.PicturePath to aws s3 bucket and get PicturePath back string? PicturePath = null; - var Product = new ProductModel - { - Name = request.Name, - Description = request.Description, - PicturePath = PicturePath, - ProductBrandId = request.ProductBrandId, - ProductCategoryId = request.ProductCategoryId, - Variants = request.Variants.Select( - Variant => new ProductVariantModel - { - ProductVariantId = Variant.ProductVariantId, - Color = Variant.Color, - Size = Variant.Size, - BarCode = Variant.BarCode, - Sku = Variant.Sku, - UnitPrice = Variant.UnitPrice, - Inventory = new ProductVariantInventoryModel - { - ProductVariantInventoryId = Variant.Inventory.ProductVariantInventoryId, - ProductVariantId = Variant.Inventory.ProductVariantId, - QuantityAvailable = Variant.Inventory.QuantityAvailable, - MinimumStockAmount = Variant.Inventory.MinimumStockAmount, - } - } - ).ToList(), - CreatedBy = UserId, - }; + Product Product = _mapper.Map(request); + + // var Product = new Product + // { + // Name = request.Name, + // Description = request.Description, + // PicturePath = PicturePath, + // ProductBrand = request.ProductBrand, + // ProductCategory = request.ProductCategory, + // Variants = request.Variants.Select( + // Variant => new Models.ProductVariant + // { + // ProductVariantPk = Variant.ProductVariantPk, + // Color = Variant.Color, + // Size = Variant.Size, + // BarCode = Variant.BarCode, + // Sku = Variant.Sku, + // UnitPrice = Variant.UnitPrice, + // Inventory = new ProductVariantInventory + // { + // ProductVariantInventoryPk = Variant.Inventory.ProductVariantInventoryPk, + // ProductVariantFk = Variant.Inventory.ProductVariantFk, + // QuantityAvailable = Variant.Inventory.QuantityAvailable, + // MinimumStockAmount = Variant.Inventory.MinimumStockAmount, + // } + // } + // ).ToList(), + // CreatedBy = UserId, + // }; await _dbContext.AddAsync(Product); await _dbContext.SaveChangesAsync(); @@ -193,14 +215,14 @@ public override async Task Post(CreateProductRequest requ _logger.LogInformation( "({TraceIdentifier}) record ({RecordType}) created successfully, RecordId {RecordId}", RequestTracerId, - typeof(ProductModel).Name, + typeof(Product).Name, Product.ProductId ); return new CreateProductResponse(); } - public override Task Put(UpdateProductRequest request, ServerCallContext context) + public override Task PutAsync(UpdateProductRequest request, ServerCallContext context) { string RequestTracerId = context.GetHttpContext().TraceIdentifier; int UserId = int.Parse( @@ -210,14 +232,14 @@ public override Task Put(UpdateProductRequest request, Se "({TraceIdentifier}) User {UserID} updating record ({RecordType}) with ID ({RecordId})", RequestTracerId, UserId, - typeof(ProductModel).Name, + typeof(Product).Name, request.ProductId ); _logger.LogInformation( "({TraceIdentifier}) record ({RecordType}) updated successfully", RequestTracerId, - typeof(ProductModel).Name + typeof(Product).Name ); throw new NotImplementedException(); @@ -243,28 +265,28 @@ public override Task Put(UpdateProductRequest request, Se } - public override async Task Delete(DeleteProductRequest request, ServerCallContext context) + public override async Task DeleteAsync(DeleteProductRequest request, ServerCallContext context) { string RequestTracerId = context.GetHttpContext().TraceIdentifier; int UserId = int.Parse( context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! ); _logger.LogInformation( - "({TraceIdentifier}) User {UserID} deleting record ({RecordType}) with ID ({RecordId})", - RequestTracerId, - UserId, - typeof(ProductModel).Name, - request.ProductId - ); + "({TraceIdentifier}) User {UserID} deleting record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Product).Name, + request.ProductId + ); - ProductModel? Product = await _dbContext.Products.FindAsync(request.ProductId); + Product? Product = await _dbContext.Products.FindAsync(request.ProductId); if (Product is null) { _logger.LogWarning( "({TraceIdentifier}) Error deleting record ({RecordType}) with ID {Id}, record not found", RequestTracerId, - typeof(ProductModel).Name, + typeof(Product).Name, request.ProductId ); throw new RpcException(new Status( @@ -272,37 +294,39 @@ public override async Task Delete(DeleteProductRequest re )); } + /// TODO check if product is being used before deleting it use something like PK or FK + _dbContext.Products.Remove(Product); await _dbContext.SaveChangesAsync(); _logger.LogInformation( - "({TraceIdentifier}) record ({RecordType}) deleted successfully", - RequestTracerId, - typeof(ProductModel).Name - ); + "({TraceIdentifier}) record ({RecordType}) deleted successfully", + RequestTracerId, + typeof(Product).Name + ); return new DeleteProductResponse(); } - public override Task GetAllBrands(GetAllProductBrandsRequest request, ServerCallContext context) + public override Task GetAllBrandsAsync(GetAllProductBrandsRequest request, ServerCallContext context) { string RequestTracerId = context.GetHttpContext().TraceIdentifier; throw new NotImplementedException(); } - public override Task PostBrand(CreateProductBrandRequest request, ServerCallContext context) + public override Task PostBrandAsync(CreateProductBrandRequest request, ServerCallContext context) { string RequestTracerId = context.GetHttpContext().TraceIdentifier; throw new NotImplementedException(); } - public override Task GetAllCategories(GetAllProductCategoriesRequest request, ServerCallContext context) + public override Task GetAllCategoriesAsync(GetAllProductCategoriesRequest request, ServerCallContext context) { string RequestTracerId = context.GetHttpContext().TraceIdentifier; throw new NotImplementedException(); } - public override Task PostCategory(CreateProductCategoryRequest request, ServerCallContext context) + public override Task PostCategoryAsync(CreateProductCategoryRequest request, ServerCallContext context) { string RequestTracerId = context.GetHttpContext().TraceIdentifier; throw new NotImplementedException(); diff --git a/Services/PromotionRpcService.cs b/Services/PromotionRpcService.cs new file mode 100644 index 0000000..f788feb --- /dev/null +++ b/Services/PromotionRpcService.cs @@ -0,0 +1,290 @@ +using System.Security.Claims; +using AutoMapper; +using Grpc.Core; +using GsServer.Models; +using GsServer.Protobufs; + +namespace GsServer.Services; + +public class PromotionRpcService : PromotionService.PromotionServiceBase +{ + private readonly DatabaseContext _dbContext; + private readonly ILogger _logger; + private readonly IMapper _mapper; + public PromotionRpcService( + ILogger logger, + DatabaseContext dbContext, + IMapper mapper + ) + { + _logger = logger; + _dbContext = dbContext; + _mapper = mapper; + } + + public override async Task GetPaginatedAsync(GetPaginatedPromotionsRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing multiple records ({RecordType}) with cursor {Cursor}", + RequestTracerId, + UserId, + typeof(Promotion).Name, + request.Cursor + ); + + IQueryable Query = _dbContext.Promotions.Select( + Promotion => _mapper.Map(Promotion) + ); + + // TODO + // IQueryable Query = _dbContext.Promotions.Select( + // Promotion => new GetPromotionByIdResponse + // { + // Customer = Promotion.CustomerFk, + // Name = Promotion.Name, + // Description = Promotion.Description, + // DiscountType = Promotion.DiscountType, + // StartDate = new() + // { + // Year = Promotion.StartDate.Year, + // Month = Promotion.StartDate.Month, + // Day = Promotion.StartDate.Day, + // }, + // EndDate = new() + // { + // Year = Promotion.EndDate.Year, + // Month = Promotion.EndDate.Month, + // Day = Promotion.EndDate.Day, + // }, + // } + // ); + + List Promotions = []; + + /// If cursor is bigger than the size of the collection you will get the following error + /// ArgumentOutOfRangeException "Index was out of range. Must be non-negative and less than the size of the collection" + Promotions = await Query + .Where(x => x.PromotionId > request.Cursor) + .Take(20) + .ToListAsync(); + + GetPaginatedPromotionsResponse response = new(); + + response.Promotions.AddRange(Promotions); + if (Promotions.Count < 20) + { + /// Avoiding `ArgumentOutOfRangeException`, basically, don't fetch if null + response.NextCursor = null; + } + else + { + /// Id of the last element of the list, same value as `Users[Users.Count - 1].Id` + response.NextCursor = Promotions[^1].PromotionId; + } + + _logger.LogInformation( + "({TraceIdentifier}) multiple records ({RecordType}) accessed successfully", + RequestTracerId, + typeof(Promotion).Name + ); + return response; + } + + public override async Task GetByIdAsync(GetPromotionByIdRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Promotion).Name, + request.PromotionId + ); + + Promotion? Promotion = await _dbContext.Promotions.FindAsync(request.PromotionId); + + if (Promotion is null) + { + _logger.LogWarning( + "({TraceIdentifier}) record ({RecordType}) not found", + RequestTracerId, + typeof(Promotion).Name + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Nenhum produto com ID {request.PromotionId}" + )); + } + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) accessed successfully", + RequestTracerId, + typeof(Promotion).Name + ); + + return _mapper.Map(Promotion); + + // TODO + // return new GetPromotionByIdResponse + // { + // Customer = Promotion.CustomerFk, + // Name = Promotion.Name, + // Description = Promotion.Description, + // DiscountType = Promotion.DiscountType, + // StartDate = new() + // { + // Year = Promotion.StartDate.Year, + // Month = Promotion.StartDate.Month, + // Day = Promotion.StartDate.Day, + // }, + // EndDate = new() + // { + // Year = Promotion.EndDate.Year, + // Month = Promotion.EndDate.Month, + // Day = Promotion.EndDate.Day, + // }, + // }; + } + + public override async Task PostAsync(CreatePromotionRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} creating new record ({RecordType})", + RequestTracerId, + UserId, + typeof(Promotion).Name + ); + + Promotion Promotion = _mapper.Map(request); + Promotion.CreatedBy = UserId; + + // TODO + // var Promotion = new Promotion + // { + // CustomerFk = request.CustomerFk, + // Name = request.Name, + // Description = request.Description, + // DiscountType = request.DiscountType, + // StartDate = new( + // request.StartDate.Year, + // request.StartDate.Month, + // request.StartDate.Day + // ), + // EndDate = new( + // request.EndDate.Year, + // request.EndDate.Month, + // request.EndDate.Day + // ), + // CreatedBy = UserId, + // }; + + await _dbContext.AddAsync(Promotion); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) created successfully, RecordId {RecordId}", + RequestTracerId, + typeof(Promotion).Name, + Promotion.PromotionId + ); + + return new CreatePromotionResponse(); + } + + public override Task PutAsync(UpdatePromotionRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} updating record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Promotion).Name, + request.PromotionId + ); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) updated successfully", + RequestTracerId, + typeof(Promotion).Name + ); + + throw new NotImplementedException(); + + // TODO + // if (request.Id <= 0) + // throw new RpcException(new Status(StatusCode.InvalidArgument, "You must supply a valid id")); + + // PromotionModel? Promotion = await _dbContext.Promotions.FirstOrDefaultAsync(x => x.Id == request.Id); + // if (Promotion is null) + // { + // throw new RpcException(new Status( + // StatusCode.NotFound, $"registro não encontrado" + // )); + // } + + // Promotion.Name = request.Name; + // // TODO Add Another fields + + // await _dbContext.SaveChangesAsync(); + // // TODO Log => Record (record type) ID Y was updated. Old value of (field name): (old value). New value: (new value). (This logs specific changes made to a field within a record) + // return new UpdatePromotionResponse(); + } + + public override async Task DeleteAsync(DeletePromotionRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} deleting record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Promotion).Name, + request.PromotionId + ); + + Promotion? Promotion = await _dbContext.Promotions.FindAsync(request.PromotionId); + + if (Promotion is null) + { + _logger.LogWarning( + "({TraceIdentifier}) Error deleting record ({RecordType}) with ID {Id}, record not found", + RequestTracerId, + typeof(Promotion).Name, + request.PromotionId + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Erro ao remover registro, nenhum registro com ID {request.PromotionId}" + )); + } + + /// TODO check if record is being used before deleting it use something like PK or FK + + _dbContext.Promotions.Remove(Promotion); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) deleted successfully", + RequestTracerId, + typeof(Promotion).Name + ); + + return new DeletePromotionResponse(); + } +} diff --git a/Services/ReturnRpcService.cs b/Services/ReturnRpcService.cs new file mode 100644 index 0000000..7609eb4 --- /dev/null +++ b/Services/ReturnRpcService.cs @@ -0,0 +1,274 @@ +using System.Security.Claims; +using AutoMapper; +using Grpc.Core; +using GsServer.Models; +using GsServer.Protobufs; + +namespace GsServer.Services; + +public class ReturnRpcService : ReturnService.ReturnServiceBase +{ + private readonly DatabaseContext _dbContext; + private readonly ILogger _logger; + private readonly IMapper _mapper; + public ReturnRpcService( + ILogger logger, + DatabaseContext dbContext, + IMapper mapper + ) + { + _logger = logger; + _dbContext = dbContext; + _mapper = mapper; + } + + public override async Task GetPaginatedAsync(GetPaginatedReturnsRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing multiple records ({RecordType}) with cursor {Cursor}", + RequestTracerId, + UserId, + typeof(Return).Name, + request.Cursor + ); + + IQueryable Query = _dbContext.Returns.Select( + Return => _mapper.Map(Return) + ); + + // TODO + // IQueryable Query = _dbContext.Returns.Select( + // Return => new GetReturnByIdResponse + // { + // TotalAmountRefunded = Return.TotalAmountRefunded, + // ItemsReturned = + // { + // Return.ItemsReturned.Select( + // ItemReturned => new ReturnItem + // { + // ProductVariantFk = ItemReturned.ProductVariantFk, + // QuantityReturned = ItemReturned.QuantityReturned, + // } + // ).ToList(), + // } + // } + // ); + + List Returns = []; + + /// If cursor is bigger than the size of the collection you will get the following error + /// ArgumentOutOfRangeException "Index was out of range. Must be non-negative and less than the size of the collection" + Returns = await Query + .Where(x => x.ReturnId > request.Cursor) + .Take(20) + .ToListAsync(); + + GetPaginatedReturnsResponse response = new(); + + response.Returns.AddRange(Returns); + if (Returns.Count < 20) + { + /// Avoiding `ArgumentOutOfRangeException`, basically, don't fetch if null + response.NextCursor = null; + } + else + { + /// Id of the last element of the list, same value as `Users[Users.Count - 1].Id` + response.NextCursor = Returns[^1].ReturnId; + } + + _logger.LogInformation( + "({TraceIdentifier}) multiple records ({RecordType}) accessed successfully", + RequestTracerId, + typeof(Return).Name + ); + return response; + } + + public override async Task GetByIdAsync(GetReturnByIdRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Return).Name, + request.ReturnId + ); + + Return? Return = await _dbContext.Returns.FindAsync(request.ReturnId); + + if (Return is null) + { + _logger.LogWarning( + "({TraceIdentifier}) record ({RecordType}) not found", + RequestTracerId, + typeof(Return).Name + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Nenhum produto com ID {request.ReturnId}" + )); + } + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) accessed successfully", + RequestTracerId, + typeof(Return).Name + ); + + return _mapper.Map(Return); + + // TODO + // return new GetReturnByIdResponse + // { + // TotalAmountRefunded = Return.TotalAmountRefunded, + // ItemsReturned = + // { + // Return.ItemsReturned.Select( + // ItemReturned => new ReturnItem + // { + // ProductVariantFk = ItemReturned.ProductVariantFk, + // QuantityReturned = ItemReturned.QuantityReturned, + // } + // ).ToList(), + // } + // }; + } + + public override async Task PostAsync(CreateReturnRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} creating new record ({RecordType})", + RequestTracerId, + UserId, + typeof(Return).Name + ); + + Return Return = _mapper.Map(request); + Return.CreatedBy = UserId; + + // TODO + // var Return = new Return + // { + // TotalAmountRefunded = request.TotalAmountRefunded, + // ItemsReturned = request.ItemsReturned.Select( + // ItemReturned => new ReturnItem + // { + // ProductVariantFk = ItemReturned.ProductVariantFk, + // QuantityReturned = ItemReturned.QuantityReturned, + // } + // ).ToList(), + // CreatedBy = UserId, + // }; + + await _dbContext.AddAsync(Return); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) created successfully, RecordId {RecordId}", + RequestTracerId, + typeof(Return).Name, + Return.ReturnId + ); + + return new CreateReturnResponse(); + } + + public override Task PutAsync(UpdateReturnRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} updating record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Return).Name, + request.ReturnId + ); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) updated successfully", + RequestTracerId, + typeof(Return).Name + ); + + throw new NotImplementedException(); + + // TODO + // if (request.Id <= 0) + // throw new RpcException(new Status(StatusCode.InvalidArgument, "You must supply a valid id")); + + // ReturnModel? Return = await _dbContext.Returns.FirstOrDefaultAsync(x => x.Id == request.Id); + // if (Return is null) + // { + // throw new RpcException(new Status( + // StatusCode.NotFound, $"registro não encontrado" + // )); + // } + + // Return.Name = request.Name; + // // TODO Add Another fields + + // await _dbContext.SaveChangesAsync(); + // // TODO Log => Record (record type) ID Y was updated. Old value of (field name): (old value). New value: (new value). (This logs specific changes made to a field within a record) + // return new UpdateReturnResponse(); + } + + public override async Task DeleteAsync(DeleteReturnRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} deleting record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Return).Name, + request.ReturnId + ); + + Return? Return = await _dbContext.Returns.FindAsync(request.ReturnId); + + if (Return is null) + { + _logger.LogWarning( + "({TraceIdentifier}) Error deleting record ({RecordType}) with ID {Id}, record not found", + RequestTracerId, + typeof(Return).Name, + request.ReturnId + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Erro ao remover registro, nenhum registro com ID {request.ReturnId}" + )); + } + + /// TODO check if record is being used before deleting it use something like PK or FK + + _dbContext.Returns.Remove(Return); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) deleted successfully", + RequestTracerId, + typeof(Return).Name + ); + + return new DeleteReturnResponse(); + } +} diff --git a/Services/SaleBillingRpcService.cs b/Services/SaleBillingRpcService.cs new file mode 100644 index 0000000..bcdcfcc --- /dev/null +++ b/Services/SaleBillingRpcService.cs @@ -0,0 +1,249 @@ +using System.Security.Claims; +using AutoMapper; +using Grpc.Core; +using GsServer.Models; +using GsServer.Protobufs; + +namespace GsServer.Services; + +public class SaleBillingRpcService : SaleBillingService.SaleBillingServiceBase +{ + private readonly DatabaseContext _dbContext; + private readonly ILogger _logger; + private readonly IMapper _mapper; + public SaleBillingRpcService( + ILogger logger, + DatabaseContext dbContext, + IMapper mapper + ) + { + _logger = logger; + _dbContext = dbContext; + _mapper = mapper; + } + + public override async Task GetPaginatedAsync(GetPaginatedSaleBillingsRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing multiple records ({RecordType}) with cursor {Cursor}", + RequestTracerId, + UserId, + typeof(SaleBilling).Name, + request.Cursor + ); + + IQueryable Query = _dbContext.SaleBillings.Select( + SaleBilling => _mapper.Map(SaleBilling) + ); + + // TODO + // IQueryable Query = _dbContext.SaleBillings.Select( + // SaleBilling => new GetSaleBillingByIdResponse + // { + // TODO + // } + // ); + + List SaleBillings = []; + + /// If cursor is bigger than the size of the collection you will get the following error + /// ArgumentOutOfRangeException "Index was out of range. Must be non-negative and less than the size of the collection" + SaleBillings = await Query + .Where(x => x.SaleBillingId > request.Cursor) + .Take(20) + .ToListAsync(); + + GetPaginatedSaleBillingsResponse response = new(); + + response.SaleBillings.AddRange(SaleBillings); + if (SaleBillings.Count < 20) + { + /// Avoiding `ArgumentOutOfRangeException`, basically, don't fetch if null + response.NextCursor = null; + } + else + { + /// Id of the last element of the list, same value as `Users[Users.Count - 1].Id` + response.NextCursor = SaleBillings[^1].SaleBillingId; + } + + _logger.LogInformation( + "({TraceIdentifier}) multiple records ({RecordType}) accessed successfully", + RequestTracerId, + typeof(SaleBilling).Name + ); + return response; + } + + public override async Task GetByIdAsync(GetSaleBillingByIdRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(SaleBilling).Name, + request.SaleBillingId + ); + + SaleBilling? SaleBilling = await _dbContext.SaleBillings.FindAsync(request.SaleBillingId); + + if (SaleBilling is null) + { + _logger.LogWarning( + "({TraceIdentifier}) record ({RecordType}) not found", + RequestTracerId, + typeof(SaleBilling).Name + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Nenhum produto com ID {request.SaleBillingId}" + )); + } + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) accessed successfully", + RequestTracerId, + typeof(SaleBilling).Name + ); + + return _mapper.Map(SaleBilling); + // TODO + // return new GetSaleBillingByIdResponse + // { + // TODO + // }; + } + + public override async Task PostAsync(CreateSaleBillingRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} creating new record ({RecordType})", + RequestTracerId, + UserId, + typeof(SaleBilling).Name + ); + + SaleBilling SaleBilling = _mapper.Map(request); + SaleBilling.CreatedBy = UserId; + + // TODO + // var SaleBilling = new SaleBilling + // { + // SaleFk = request.SaleFk, + // Comments = request.Comments, + // TotalDiscount = request.TotalDiscount, + // Payment = request.Payment, + // CreatedBy = UserId, + // }; + + await _dbContext.AddAsync(SaleBilling); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) created successfully, RecordId {RecordId}", + RequestTracerId, + typeof(SaleBilling).Name, + SaleBilling.SaleBillingId + ); + + return new CreateSaleBillingResponse(); + } + + public override Task PutAsync(UpdateSaleBillingRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} updating record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(SaleBilling).Name, + request.SaleBillingId + ); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) updated successfully", + RequestTracerId, + typeof(SaleBilling).Name + ); + + throw new NotImplementedException(); + + // TODO + // if (request.Id <= 0) + // throw new RpcException(new Status(StatusCode.InvalidArgument, "You must supply a valid id")); + + // SaleBillingModel? SaleBilling = await _dbContext.SaleBillings.FirstOrDefaultAsync(x => x.Id == request.Id); + // if (SaleBilling is null) + // { + // throw new RpcException(new Status( + // StatusCode.NotFound, $"registro não encontrado" + // )); + // } + + // SaleBilling.Name = request.Name; + // // TODO Add Another fields + + // await _dbContext.SaveChangesAsync(); + // // TODO Log => Record (record type) ID Y was updated. Old value of (field name): (old value). New value: (new value). (This logs specific changes made to a field within a record) + // return new UpdateSaleBillingResponse(); + } + + public override async Task DeleteAsync(DeleteSaleBillingRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} deleting record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(SaleBilling).Name, + request.SaleBillingId + ); + + SaleBilling? SaleBilling = await _dbContext.SaleBillings.FindAsync(request.SaleBillingId); + + if (SaleBilling is null) + { + _logger.LogWarning( + "({TraceIdentifier}) Error deleting record ({RecordType}) with ID {Id}, record not found", + RequestTracerId, + typeof(SaleBilling).Name, + request.SaleBillingId + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Erro ao remover registro, nenhum registro com ID {request.SaleBillingId}" + )); + } + + /// TODO check if record is being used before deleting it use something like PK or FK + + _dbContext.SaleBillings.Remove(SaleBilling); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) deleted successfully", + RequestTracerId, + typeof(SaleBilling).Name + ); + + return new DeleteSaleBillingResponse(); + } +} diff --git a/Services/SaleRpcService.cs b/Services/SaleRpcService.cs index 7cc2a04..c4e8fb4 100644 --- a/Services/SaleRpcService.cs +++ b/Services/SaleRpcService.cs @@ -1,4 +1,7 @@ +using System.Security.Claims; +using AutoMapper; using Grpc.Core; +using GsServer.Models; using GsServer.Protobufs; namespace GsServer.Services; @@ -7,34 +10,246 @@ public class SaleRpcService : SaleService.SaleServiceBase { private readonly DatabaseContext _dbContext; private readonly ILogger _logger; - public SaleRpcService(ILogger logger, DatabaseContext dbContext) + private readonly IMapper _mapper; + public SaleRpcService( + ILogger logger, + DatabaseContext dbContext, + IMapper mapper + ) { _logger = logger; _dbContext = dbContext; + _mapper = mapper; } - public override Task GetPaginated(GetPaginatedSalesRequest request, ServerCallContext context) + public override async Task GetPaginatedAsync(GetPaginatedSalesRequest request, ServerCallContext context) { - throw new NotImplementedException(); + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing multiple records ({RecordType}) with cursor {Cursor}", + RequestTracerId, + UserId, + typeof(Sale).Name, + request.Cursor + ); + + IQueryable Query = _dbContext.Sales.Select( + Sale => _mapper.Map(Sale) + ); + + // TODO + // IQueryable Query = _dbContext.Sales.Select( + // Sale => new GetSaleByIdResponse + // { + // TODO + // } + // ); + + List Sales = []; + + /// If cursor is bigger than the size of the collection you will get the following error + /// ArgumentOutOfRangeException "Index was out of range. Must be non-negative and less than the size of the collection" + Sales = await Query + .Where(x => x.SaleId > request.Cursor) + .Take(20) + .ToListAsync(); + + GetPaginatedSalesResponse response = new(); + + response.Sales.AddRange(Sales); + if (Sales.Count < 20) + { + /// Avoiding `ArgumentOutOfRangeException`, basically, don't fetch if null + response.NextCursor = null; + } + else + { + /// Id of the last element of the list, same value as `Users[Users.Count - 1].Id` + response.NextCursor = Sales[^1].SaleId; + } + + _logger.LogInformation( + "({TraceIdentifier}) multiple records ({RecordType}) accessed successfully", + RequestTracerId, + typeof(Sale).Name + ); + return response; } - public override Task GetById(GetSaleByIdRequest request, ServerCallContext context) + public override async Task GetByIdAsync(GetSaleByIdRequest request, ServerCallContext context) { - throw new NotImplementedException(); + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Sale).Name, + request.SaleId + ); + + Sale? Sale = await _dbContext.Sales.FindAsync(request.SaleId); + + if (Sale is null) + { + _logger.LogWarning( + "({TraceIdentifier}) record ({RecordType}) not found", + RequestTracerId, + typeof(Sale).Name + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Nenhum produto com ID {request.SaleId}" + )); + } + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) accessed successfully", + RequestTracerId, + typeof(Sale).Name + ); + + return _mapper.Map(Sale); + + // TODO + // return new GetSaleByIdResponse + // { + // TODO + // }; } - public override Task Post(CreateSaleRequest request, ServerCallContext context) + public override async Task PostAsync(CreateSaleRequest request, ServerCallContext context) { - throw new NotImplementedException(); + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} creating new record ({RecordType})", + RequestTracerId, + UserId, + typeof(Sale).Name + ); + + Sale Sale = _mapper.Map(request); + Sale.CreatedBy = UserId; + + // TODO + // var Sale = new Sale + // { + // CustomerFk = request.CustomerFk, + // Comments = request.Comments, + // ItemsSold = request.ItemsSold.Select( + // ItemSold => new Models.SaleItem + // { + // ProductVariantFk = ItemSold.ProductVariantFk, + // QuantitySold = ItemSold.QuantitySold, + // } + // ).ToList(), + // CreatedBy = UserId, + // }; + + await _dbContext.AddAsync(Sale); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) created successfully, RecordId {RecordId}", + RequestTracerId, + typeof(Sale).Name, + Sale.SaleId + ); + + return new CreateSaleResponse(); } - public override Task Put(UpdateSaleRequest request, ServerCallContext context) + public override Task PutAsync(UpdateSaleRequest request, ServerCallContext context) { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} updating record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Sale).Name, + request.SaleId + ); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) updated successfully", + RequestTracerId, + typeof(Sale).Name + ); + throw new NotImplementedException(); + + // TODO + // if (request.Id <= 0) + // throw new RpcException(new Status(StatusCode.InvalidArgument, "You must supply a valid id")); + + // SaleModel? Sale = await _dbContext.Sales.FirstOrDefaultAsync(x => x.Id == request.Id); + // if (Sale is null) + // { + // throw new RpcException(new Status( + // StatusCode.NotFound, $"registro não encontrado" + // )); + // } + + // Sale.Name = request.Name; + // // TODO Add Another fields + + // await _dbContext.SaveChangesAsync(); + // // TODO Log => Record (record type) ID Y was updated. Old value of (field name): (old value). New value: (new value). (This logs specific changes made to a field within a record) + // return new UpdateSaleResponse(); } - public override Task Delete(DeleteSaleRequest request, ServerCallContext context) + public override async Task DeleteAsync(DeleteSaleRequest request, ServerCallContext context) { - throw new NotImplementedException(); + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} deleting record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Sale).Name, + request.SaleId + ); + + Sale? Sale = await _dbContext.Sales.FindAsync(request.SaleId); + + if (Sale is null) + { + _logger.LogWarning( + "({TraceIdentifier}) Error deleting record ({RecordType}) with ID {Id}, record not found", + RequestTracerId, + typeof(Sale).Name, + request.SaleId + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Erro ao remover registro, nenhum registro com ID {request.SaleId}" + )); + } + + /// TODO check if record is being used before deleting it use something like PK or FK + + _dbContext.Sales.Remove(Sale); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) deleted successfully", + RequestTracerId, + typeof(Sale).Name + ); + + return new DeleteSaleResponse(); } } diff --git a/Services/SubscriptionBillingRpcService.cs b/Services/SubscriptionBillingRpcService.cs new file mode 100644 index 0000000..1fcbec6 --- /dev/null +++ b/Services/SubscriptionBillingRpcService.cs @@ -0,0 +1,270 @@ +using System.Security.Claims; +using AutoMapper; +using Grpc.Core; +using GsServer.Models; +using GsServer.Protobufs; + +namespace GsServer.Services; + +public class SubscriptionBillingRpcService : SubscriptionBillingService.SubscriptionBillingServiceBase +{ + private readonly DatabaseContext _dbContext; + private readonly ILogger _logger; + private readonly IMapper _mapper; + public SubscriptionBillingRpcService( + ILogger logger, + DatabaseContext dbContext, + IMapper mapper + + ) + { + _logger = logger; + _dbContext = dbContext; + _mapper = mapper; + } + + public override async Task GetPaginatedAsync(GetPaginatedSubscriptionBillingsRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing multiple records ({RecordType}) with cursor {Cursor}", + RequestTracerId, + UserId, + typeof(SubscriptionBilling).Name, + request.Cursor + ); + + IQueryable Query = _dbContext.SubscriptionBillings.Select( + SubscriptionBilling => _mapper.Map(SubscriptionBilling) + ); + + // TODO + // IQueryable Query = _dbContext.SubscriptionBillings.Select( + // SubscriptionBilling => new GetSubscriptionBillingByIdResponse + // { + // TODO + // } + // ); + + List SubscriptionBillings = []; + + /// If cursor is bigger than the size of the collection you will get the following error + /// ArgumentOutOfRangeException "Index was out of range. Must be non-negative and less than the size of the collection" + SubscriptionBillings = await Query + .Where(x => x.SubscriptionBillingId > request.Cursor) + .Take(20) + .ToListAsync(); + + GetPaginatedSubscriptionBillingsResponse response = new(); + + response.SubscriptionBillings.AddRange(SubscriptionBillings); + if (SubscriptionBillings.Count < 20) + { + /// Avoiding `ArgumentOutOfRangeException`, basically, don't fetch if null + response.NextCursor = null; + } + else + { + /// Id of the last element of the list, same value as `Users[Users.Count - 1].Id` + response.NextCursor = SubscriptionBillings[^1].SubscriptionBillingId; + } + + _logger.LogInformation( + "({TraceIdentifier}) multiple records ({RecordType}) accessed successfully", + RequestTracerId, + typeof(SubscriptionBilling).Name + ); + return response; + } + + public override async Task GetByIdAsync(GetSubscriptionBillingByIdRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(SubscriptionBilling).Name, + request.SubscriptionBillingId + ); + + SubscriptionBilling? SubscriptionBilling = await _dbContext.SubscriptionBillings.FindAsync(request.SubscriptionBillingId); + + if (SubscriptionBilling is null) + { + _logger.LogWarning( + "({TraceIdentifier}) record ({RecordType}) not found", + RequestTracerId, + typeof(SubscriptionBilling).Name + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Nenhum produto com ID {request.SubscriptionBillingId}" + )); + } + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) accessed successfully", + RequestTracerId, + typeof(SubscriptionBilling).Name + ); + + return _mapper.Map(SubscriptionBilling); + + // TODO + // return new GetSubscriptionBillingByIdResponse + // { + // todo + // }; + } + + public override async Task PostAsync(CreateSubscriptionBillingRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} creating new record ({RecordType})", + RequestTracerId, + UserId, + typeof(SubscriptionBilling).Name + ); + + SubscriptionBilling SubscriptionBilling = _mapper.Map(request); + SubscriptionBilling.CreatedBy = UserId; + + // TODO + // var SubscriptionBilling = new SubscriptionBilling + // { + // SubscriptionFk = request.SubscriptionFk, + // Comments = request.Comments, + // TotalDiscount = request.TotalDiscount, + // Payment = new Payment + // { + // Comments = request.Payment.Comments, + // Installments = request.Payment.Installments.Select( + // Installment => new Models.PaymentInstallment + // { + // PaymentInstallmentPk = Installment.PaymentInstallmentPk, + // PaymentFk = Installment.PaymentFk, + // InstallmentNumber = Installment.PaymentFk, + // InstallmentAmount = Installment.InstallmentAmount, + // PaymentMethod = Installment.PaymentMethod, + // DueDate = new( + // Installment.DueDate.Year, + // Installment.DueDate.Month, + // Installment.DueDate.Day + // ), + // } + // ).ToList(), + // CreatedBy = UserId, + // }, + // CreatedBy = UserId, + // }; + + await _dbContext.AddAsync(SubscriptionBilling); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) created successfully, RecordId {RecordId}", + RequestTracerId, + typeof(SubscriptionBilling).Name, + SubscriptionBilling.SubscriptionBillingId + ); + + return new CreateSubscriptionBillingResponse(); + } + + public override Task PutAsync(UpdateSubscriptionBillingRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} updating record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(SubscriptionBilling).Name, + request.SubscriptionBillingId + ); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) updated successfully", + RequestTracerId, + typeof(SubscriptionBilling).Name + ); + + throw new NotImplementedException(); + + // TODO + // if (request.Id <= 0) + // throw new RpcException(new Status(StatusCode.InvalidArgument, "You must supply a valid id")); + + // SubscriptionBillingModel? SubscriptionBilling = await _dbContext.SubscriptionBillings.FirstOrDefaultAsync(x => x.Id == request.Id); + // if (SubscriptionBilling is null) + // { + // throw new RpcException(new Status( + // StatusCode.NotFound, $"registro não encontrado" + // )); + // } + + // SubscriptionBilling.Name = request.Name; + // // TODO Add Another fields + + // await _dbContext.SaveChangesAsync(); + // // TODO Log => Record (record type) ID Y was updated. Old value of (field name): (old value). New value: (new value). (This logs specific changes made to a field within a record) + // return new UpdateSubscriptionBillingResponse(); + } + + public override async Task DeleteAsync(DeleteSubscriptionBillingRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} deleting record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(SubscriptionBilling).Name, + request.SubscriptionBillingId + ); + + SubscriptionBilling? SubscriptionBilling = await _dbContext.SubscriptionBillings.FindAsync(request.SubscriptionBillingId); + + if (SubscriptionBilling is null) + { + _logger.LogWarning( + "({TraceIdentifier}) Error deleting record ({RecordType}) with ID {Id}, record not found", + RequestTracerId, + typeof(SubscriptionBilling).Name, + request.SubscriptionBillingId + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Erro ao remover registro, nenhum registro com ID {request.SubscriptionBillingId}" + )); + } + + /// TODO check if record is being used before deleting it use something like PK or FK + + _dbContext.SubscriptionBillings.Remove(SubscriptionBilling); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) deleted successfully", + RequestTracerId, + typeof(SubscriptionBilling).Name + ); + + return new DeleteSubscriptionBillingResponse(); + } +} diff --git a/Services/SubscriptionRpcService.cs b/Services/SubscriptionRpcService.cs new file mode 100644 index 0000000..176bbe7 --- /dev/null +++ b/Services/SubscriptionRpcService.cs @@ -0,0 +1,273 @@ +using System.Security.Claims; +using AutoMapper; +using Grpc.Core; +using GsServer.Models; +using GsServer.Protobufs; + +namespace GsServer.Services; + +public class SubscriptionRpcService : SubscriptionService.SubscriptionServiceBase +{ + private readonly DatabaseContext _dbContext; + private readonly ILogger _logger; + private readonly IMapper _mapper; + public SubscriptionRpcService( + ILogger logger, + DatabaseContext dbContext, + IMapper mapper + ) + { + _logger = logger; + _dbContext = dbContext; + _mapper = mapper; + } + + public override async Task GetPaginatedAsync(GetPaginatedSubscriptionsRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing multiple records ({RecordType}) with cursor {Cursor}", + RequestTracerId, + UserId, + typeof(Subscription).Name, + request.Cursor + ); + + IQueryable Query = _dbContext.Subscriptions.Select( + Subscription => _mapper.Map(Subscription) + ); + + // TODO + // IQueryable Query = _dbContext.Subscriptions.Select( + // Subscription => new GetSubscriptionByIdResponse + // { + // Discipline = Subscription.DisciplineFk, + // Customer = Subscription.CustomerFk, + // PayDay = Subscription.PayDay, + // StartDate = new() + // { + // Year = Subscription.StartDate.Year, + // Month = Subscription.StartDate.Month, + // Day = Subscription.StartDate.Day + // }, + // Price = Subscription.Price, + // } + // ); + + List Subscriptions = []; + + /// If cursor is bigger than the size of the collection you will get the following error + /// ArgumentOutOfRangeException "Index was out of range. Must be non-negative and less than the size of the collection" + Subscriptions = await Query + .Where(x => x.SubscriptionId > request.Cursor) + .Take(20) + .ToListAsync(); + + GetPaginatedSubscriptionsResponse response = new(); + + response.Subscriptions.AddRange(Subscriptions); + if (Subscriptions.Count < 20) + { + /// Avoiding `ArgumentOutOfRangeException`, basically, don't fetch if null + response.NextCursor = null; + } + else + { + /// Id of the last element of the list, same value as `Users[Users.Count - 1].Id` + response.NextCursor = Subscriptions[^1].SubscriptionId; + } + + _logger.LogInformation( + "({TraceIdentifier}) multiple records ({RecordType}) accessed successfully", + RequestTracerId, + typeof(Subscription).Name + ); + return response; + } + + public override async Task GetByIdAsync(GetSubscriptionByIdRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Subscription).Name, + request.SubscriptionId + ); + + Subscription? Subscription = await _dbContext.Subscriptions.FindAsync(request.SubscriptionId); + + if (Subscription is null) + { + _logger.LogWarning( + "({TraceIdentifier}) record ({RecordType}) not found", + RequestTracerId, + typeof(Subscription).Name + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Nenhum produto com ID {request.SubscriptionId}" + )); + } + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) accessed successfully", + RequestTracerId, + typeof(Subscription).Name + ); + + return _mapper.Map(Subscription); + + // TODO + // return new GetSubscriptionByIdResponse + // { + // Discipline = Subscription.DisciplineFk, + // Customer = Subscription.CustomerFk, + // PayDay = Subscription.PayDay, + // StartDate = new() + // { + // Year = Subscription.StartDate.Year, + // Month = Subscription.StartDate.Month, + // Day = Subscription.StartDate.Day + // }, + // Price = Subscription.Price, + // }; + } + + public override async Task PostAsync(CreateSubscriptionRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} creating new record ({RecordType})", + RequestTracerId, + UserId, + typeof(Subscription).Name + ); + + Subscription Subscription = _mapper.Map(request); + Subscription.CreatedBy = UserId; + + // TODO + // var Subscription = new Subscription + // { + // DisciplineFk = request.DisciplineFk, + // CustomerFk = request.CustomerFk, + // PayDay = request.PayDay, + // StartDate = new( + // request.StartDate.Year, + // request.StartDate.Month, + // request.StartDate.Day + // ), + // Price = request.Price, + // CreatedBy = UserId, + // }; + + await _dbContext.AddAsync(Subscription); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) created successfully, RecordId {RecordId}", + RequestTracerId, + typeof(Subscription).Name, + Subscription.SubscriptionId + ); + + return new CreateSubscriptionResponse(); + } + + public override Task PutAsync(UpdateSubscriptionRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} updating record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Subscription).Name, + request.SubscriptionId + ); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) updated successfully", + RequestTracerId, + typeof(Subscription).Name + ); + + throw new NotImplementedException(); + + // TODO + // if (request.Id <= 0) + // throw new RpcException(new Status(StatusCode.InvalidArgument, "You must supply a valid id")); + + // SubscriptionModel? Subscription = await _dbContext.Subscriptions.FirstOrDefaultAsync(x => x.Id == request.Id); + // if (Subscription is null) + // { + // throw new RpcException(new Status( + // StatusCode.NotFound, $"registro não encontrado" + // )); + // } + + // Subscription.Name = request.Name; + // // TODO Add Another fields + + // await _dbContext.SaveChangesAsync(); + // // TODO Log => Record (record type) ID Y was updated. Old value of (field name): (old value). New value: (new value). (This logs specific changes made to a field within a record) + // return new UpdateSubscriptionResponse(); + } + + public override async Task DeleteAsync(DeleteSubscriptionRequest request, ServerCallContext context) + { + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} deleting record ({RecordType}) with ID ({RecordId})", + RequestTracerId, + UserId, + typeof(Subscription).Name, + request.SubscriptionId + ); + + Subscription? Subscription = await _dbContext.Subscriptions.FindAsync(request.SubscriptionId); + + if (Subscription is null) + { + _logger.LogWarning( + "({TraceIdentifier}) Error deleting record ({RecordType}) with ID {Id}, record not found", + RequestTracerId, + typeof(Subscription).Name, + request.SubscriptionId + ); + throw new RpcException(new Status( + StatusCode.NotFound, $"Erro ao remover registro, nenhum registro com ID {request.SubscriptionId}" + )); + } + + /// TODO check if record is being used before deleting it use something like PK or FK + + _dbContext.Subscriptions.Remove(Subscription); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation( + "({TraceIdentifier}) record ({RecordType}) deleted successfully", + RequestTracerId, + typeof(Subscription).Name + ); + + return new DeleteSubscriptionResponse(); + } +} diff --git a/Services/UserRpcService.cs b/Services/UserRpcService.cs index a8ed102..3f4aa98 100644 --- a/Services/UserRpcService.cs +++ b/Services/UserRpcService.cs @@ -17,9 +17,19 @@ public UserRpcService(ILogger logger, DatabaseContext dbContext) _dbContext = dbContext; } - public override async Task GetPaginated(GetPaginatedUsersRequest request, ServerCallContext context) + public override async Task GetPaginatedAsync(GetPaginatedUsersRequest request, ServerCallContext context) { - _logger.LogInformation("Listing Users"); + string RequestTracerId = context.GetHttpContext().TraceIdentifier; + int UserId = int.Parse( + context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! + ); + _logger.LogInformation( + "({TraceIdentifier}) User {UserID} accessing multiple records ({RecordType}) with cursor {Cursor}", + RequestTracerId, + UserId, + typeof(User).Name, + request.Cursor + ); IQueryable Query = _dbContext.Users.Select( User => new GetUserByIdResponse { @@ -31,10 +41,8 @@ public override async Task GetPaginated(GetPaginatedU List Users = []; - // If cursor is bigger than the size of the collection you will get the following error - // ArgumentOutOfRangeException "Index was out of range. Must be non-negative and less than the size of the collection" - // My solution (hack) will be on Front-end: - // if (response.collection.count < 20) don't make GetPaginated request anymore. + /// If cursor is bigger than the size of the collection you will get the following error + /// ArgumentOutOfRangeException "Index was out of range. Must be non-negative and less than the size of the collection" Users = await Query .Where(x => x.UserId > request.Cursor) .Take(20) @@ -43,20 +51,32 @@ public override async Task GetPaginated(GetPaginatedU GetPaginatedUsersResponse response = new(); response.Users.AddRange(Users); - // Id of the last element of the list same as `Users[Users.Count - 1].Id` - response.NextCursor = Users[^1].UserId; + if (Users.Count < 20) + { + /// Avoiding `ArgumentOutOfRangeException`, basically, don't fetch if null + response.NextCursor = null; + } + else + { + /// Id of the last element of the list, same value as `Users[Users.Count - 1].Id` + response.NextCursor = Users[^1].UserId; + } - _logger.LogInformation("Users have been listed successfully"); + _logger.LogInformation( + "({TraceIdentifier}) multiple records ({RecordType}) accessed successfully", + RequestTracerId, + typeof(User).Name + ); return response; } - public override async Task GetById(GetUserByIdRequest request, ServerCallContext context) + public override async Task GetByIdAsync(GetUserByIdRequest request, ServerCallContext context) { _logger.LogInformation( "Searching for User with ID {Id}", request.UserId ); - UserModel? User = await _dbContext.Users.FindAsync(request.UserId); + User? User = await _dbContext.Users.FindAsync(request.UserId); if (User is null) { @@ -81,14 +101,14 @@ public override async Task GetById(GetUserByIdRequest reque }; } - public override async Task Put(UpdateUserRequest request, ServerCallContext context) + public override async Task PutAsync(UpdateUserRequest request, ServerCallContext context) { int UserId = int.Parse( context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier)! ); _logger.LogInformation("Updating User with ID {Id}", UserId); - UserModel? User = await _dbContext.Users.FindAsync(UserId); + User? User = await _dbContext.Users.FindAsync(UserId); if (User is null) { @@ -117,10 +137,10 @@ public override async Task Put(UpdateUserRequest request, Se return new UpdateUserResponse(); } - public override async Task Delete(DeleteUserRequest request, ServerCallContext context) + public override async Task DeleteAsync(DeleteUserRequest request, ServerCallContext context) { _logger.LogInformation("Deleting User with ID {Id}", request.UserId); - UserModel? User = await _dbContext.Users.FindAsync(request.UserId); + User? User = await _dbContext.Users.FindAsync(request.UserId); if (User is null) { @@ -137,9 +157,9 @@ public override async Task Delete(DeleteUserRequest request, await _dbContext.SaveChangesAsync(); _logger.LogInformation( - "User deleted successfully ID {Id}", - request.UserId - ); + "User deleted successfully ID {Id}", + request.UserId + ); return new DeleteUserResponse(); } diff --git a/docs/LOGGING.md b/docs/LOGGING.md index bd18d8d..b34fbb0 100644 --- a/docs/LOGGING.md +++ b/docs/LOGGING.md @@ -13,6 +13,8 @@ Source Source Source +Source +Source ## Serilog diff --git a/gs_protobufs b/gs_protobufs index a1d46b7..0adaf3a 160000 --- a/gs_protobufs +++ b/gs_protobufs @@ -1 +1 @@ -Subproject commit a1d46b74884d671ae7588076d2c836207ec6c676 +Subproject commit 0adaf3a5604d449f8913588338793cce2bbacb00 diff --git a/gs_server.csproj b/gs_server.csproj index a33c8ed..8349da4 100644 --- a/gs_server.csproj +++ b/gs_server.csproj @@ -7,6 +7,7 @@ + @@ -31,28 +32,37 @@ - + + + + + + + + +