diff --git a/src/Somnia.GuestLog/Enums/Models/StayExportFileResult.cs b/src/Somnia.GuestLog/Enums/Models/StayExportFileResult.cs new file mode 100644 index 0000000..8131045 --- /dev/null +++ b/src/Somnia.GuestLog/Enums/Models/StayExportFileResult.cs @@ -0,0 +1,9 @@ +namespace Somnia.GuestLog.Enums.Models; + +public enum StayExportFileResult +{ + OK = 0, + NoData = 1, + ExportError = 2, + UploadError = 3, +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Enums/Models/StayExportFormat.cs b/src/Somnia.GuestLog/Enums/Models/StayExportFormat.cs new file mode 100644 index 0000000..6eb9693 --- /dev/null +++ b/src/Somnia.GuestLog/Enums/Models/StayExportFormat.cs @@ -0,0 +1,7 @@ +namespace Somnia.GuestLog.Enums.Models; + +public enum StayExportFormat +{ + GuestLogJson = 0, + SpainParteViajeros = 1, +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Enums/Models/StayExportParamType.cs b/src/Somnia.GuestLog/Enums/Models/StayExportParamType.cs new file mode 100644 index 0000000..ad9f382 --- /dev/null +++ b/src/Somnia.GuestLog/Enums/Models/StayExportParamType.cs @@ -0,0 +1,6 @@ +namespace Somnia.GuestLog.Enums.Models; + +public enum StayExportParamType +{ + PeriodDateIn = 0, +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Infrastructure/DbConsts.cs b/src/Somnia.GuestLog/Infrastructure/DbConsts.cs index 700580a..605bd6e 100644 --- a/src/Somnia.GuestLog/Infrastructure/DbConsts.cs +++ b/src/Somnia.GuestLog/Infrastructure/DbConsts.cs @@ -5,4 +5,6 @@ public static class DbConsts public const string DbType_Timestamptz = "timestamp with time zone"; public const string DbType_Timestamptz_0 = "timestamp(0) with time zone"; public const string DbType_Timestamptz_3 = "timestamp(3) with time zone"; + + public const string PK_NotApply = "N/A"; } diff --git a/src/Somnia.GuestLog/Infrastructure/EntityConfigs/StayExportFileEntityConfig.cs b/src/Somnia.GuestLog/Infrastructure/EntityConfigs/StayExportFileEntityConfig.cs new file mode 100644 index 0000000..a76653f --- /dev/null +++ b/src/Somnia.GuestLog/Infrastructure/EntityConfigs/StayExportFileEntityConfig.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Somnia.GuestLog.Models.Entities; + +namespace Somnia.GuestLog.Infrastructure.EntityConfigs; + +public class StayExportFileEntityConfig : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("StayExportFile"); + + //PK + builder.HasKey(e => e.Id); + + builder.Property(e => e.Id) + .IsRequired() + .UseIdentityByDefaultColumn(); + + builder.ComplexProperty(e => e.ExportParams, e => + { + e.ToJson(); + }); + + builder.Property(e => e.ExportedPath) + .HasMaxLength(256); + + + builder.Property(e => e.ExportTime) + .IsRequired() + .HasColumnType(DbConsts.DbType_Timestamptz); + } +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Infrastructure/GuestLogContext.cs b/src/Somnia.GuestLog/Infrastructure/GuestLogContext.cs index eb1eeff..42712e2 100644 --- a/src/Somnia.GuestLog/Infrastructure/GuestLogContext.cs +++ b/src/Somnia.GuestLog/Infrastructure/GuestLogContext.cs @@ -13,11 +13,14 @@ public partial class GuestLogContext : DbContext public DbSet GuestDocuments { get; set; } public DbSet StayEntries { get; set; } - + public DbSet StayExportFiles { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfiguration(new GuestDocumentEntityConfig()); modelBuilder.ApplyConfiguration(new StayEntryEntityConfig()); + modelBuilder.ApplyConfiguration(new StayExportFileEntityConfig()); OnModelCreatingPartial(modelBuilder); base.OnModelCreating(modelBuilder); diff --git a/src/Somnia.GuestLog/Infrastructure/Migrations/20260901161146_AddTable_StayExportFile.Designer.cs b/src/Somnia.GuestLog/Infrastructure/Migrations/20260901161146_AddTable_StayExportFile.Designer.cs new file mode 100644 index 0000000..e13c254 --- /dev/null +++ b/src/Somnia.GuestLog/Infrastructure/Migrations/20260901161146_AddTable_StayExportFile.Designer.cs @@ -0,0 +1,249 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Somnia.GuestLog.Infrastructure; + +#nullable disable + +namespace Somnia.GuestLog.Infrastructure.Migrations +{ + [DbContext(typeof(GuestLogContext))] + [Migration("20260901161146_AddTable_StayExportFile")] + partial class AddTable_StayExportFile + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("GuestDocumentStayEntry", b => + { + b.Property("StayEntryId") + .HasColumnType("bigint"); + + b.Property("GuestDocumentsId") + .HasColumnType("bigint"); + + b.Property("GuestDocumentsVersion") + .HasColumnType("integer"); + + b.HasKey("StayEntryId", "GuestDocumentsId", "GuestDocumentsVersion"); + + b.HasIndex("GuestDocumentsId", "GuestDocumentsVersion"); + + b.ToTable("GuestDocumentStayEntry"); + }); + + modelBuilder.Entity("Somnia.GuestLog.Models.Entities.GuestDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Version") + .HasColumnType("integer"); + + b.Property("BirthDate") + .HasColumnType("date"); + + b.Property("CreateTime") + .HasColumnType("timestamp(3) with time zone"); + + b.Property("DocumentNum") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("EmailAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Gender") + .HasMaxLength(1) + .HasColumnType("character varying(1)"); + + b.Property("IsLastVersion") + .HasColumnType("boolean"); + + b.Property("IssueDate") + .HasColumnType("date"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Nationality") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.Property("PhoneNumber") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("SupportNum") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Surname") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Surname2") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UpdateTime") + .HasColumnType("timestamp(3) with time zone"); + + b.ComplexProperty(typeof(Dictionary), "AddressDetails", "Somnia.GuestLog.Models.Entities.GuestDocument.AddressDetails#GuestAddress", b1 => + { + b1.Property("Address") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b1.Property("AddressCity") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b1.Property("AddressCountry") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b1.Property("AddressProvince") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b1.Property("AddressState") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b1.Property("ZipCode") + .HasMaxLength(12) + .HasColumnType("character varying(12)"); + }); + + b.HasKey("Id", "Version"); + + b.ToTable("GuestDocument", (string)null); + }); + + modelBuilder.Entity("Somnia.GuestLog.Models.Entities.StayEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DateIn") + .HasColumnType("timestamp(0) with time zone"); + + b.Property("DateOut") + .HasColumnType("timestamp(0) with time zone"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("character varying(5)"); + + b.Property("RoomCount") + .HasColumnType("integer"); + + b.Property("RoomNumber") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("StayEntry", (string)null); + }); + + modelBuilder.Entity("Somnia.GuestLog.Models.Entities.StayExportFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ExportFormat") + .HasColumnType("integer"); + + b.Property("ExportResult") + .HasColumnType("integer"); + + b.Property("ExportTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExportedPath") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.ComplexProperty(typeof(Dictionary), "ExportParams", "Somnia.GuestLog.Models.Entities.StayExportFile.ExportParams#StayExportParams", b1 => + { + b1.IsRequired(); + + b1.Property("ExportPeriodEnd"); + + b1.Property("ExportPeriodStart"); + + b1.Property("ExportType"); + + b1 + .ToJson("ExportParams") + .HasColumnType("jsonb"); + }); + + b.HasKey("Id"); + + b.ToTable("StayExportFile", (string)null); + }); + + modelBuilder.Entity("GuestDocumentStayEntry", b => + { + b.HasOne("Somnia.GuestLog.Models.Entities.StayEntry", null) + .WithMany() + .HasForeignKey("StayEntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Somnia.GuestLog.Models.Entities.GuestDocument", null) + .WithMany() + .HasForeignKey("GuestDocumentsId", "GuestDocumentsVersion") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Somnia.GuestLog/Infrastructure/Migrations/20260901161146_AddTable_StayExportFile.cs b/src/Somnia.GuestLog/Infrastructure/Migrations/20260901161146_AddTable_StayExportFile.cs new file mode 100644 index 0000000..b6567aa --- /dev/null +++ b/src/Somnia.GuestLog/Infrastructure/Migrations/20260901161146_AddTable_StayExportFile.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Somnia.GuestLog.Infrastructure.Migrations +{ + /// + public partial class AddTable_StayExportFile : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "StayExportFile", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ExportFormat = table.Column(type: "integer", nullable: false), + ExportResult = table.Column(type: "integer", nullable: false), + ExportedPath = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + ExportTime = table.Column(type: "timestamp with time zone", nullable: false), + ExportParams = table.Column(type: "jsonb", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_StayExportFile", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "StayExportFile"); + } + } +} diff --git a/src/Somnia.GuestLog/Infrastructure/Migrations/GuestLogContextModelSnapshot.cs b/src/Somnia.GuestLog/Infrastructure/Migrations/GuestLogContextModelSnapshot.cs index e54c420..a16ff5b 100644 --- a/src/Somnia.GuestLog/Infrastructure/Migrations/GuestLogContextModelSnapshot.cs +++ b/src/Somnia.GuestLog/Infrastructure/Migrations/GuestLogContextModelSnapshot.cs @@ -185,6 +185,47 @@ namespace Somnia.GuestLog.Infrastructure.Migrations b.ToTable("StayEntry", (string)null); }); + modelBuilder.Entity("Somnia.GuestLog.Models.Entities.StayExportFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ExportFormat") + .HasColumnType("integer"); + + b.Property("ExportResult") + .HasColumnType("integer"); + + b.Property("ExportTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExportedPath") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.ComplexProperty(typeof(Dictionary), "ExportParams", "Somnia.GuestLog.Models.Entities.StayExportFile.ExportParams#StayExportParams", b1 => + { + b1.IsRequired(); + + b1.Property("ExportPeriodEnd"); + + b1.Property("ExportPeriodStart"); + + b1.Property("ExportType"); + + b1 + .ToJson("ExportParams") + .HasColumnType("jsonb"); + }); + + b.HasKey("Id"); + + b.ToTable("StayExportFile", (string)null); + }); + modelBuilder.Entity("GuestDocumentStayEntry", b => { b.HasOne("Somnia.GuestLog.Models.Entities.StayEntry", null) diff --git a/src/Somnia.GuestLog/Models/Configuration/StayExportConfig.cs b/src/Somnia.GuestLog/Models/Configuration/StayExportConfig.cs new file mode 100644 index 0000000..5e5c3fc --- /dev/null +++ b/src/Somnia.GuestLog/Models/Configuration/StayExportConfig.cs @@ -0,0 +1,10 @@ +namespace Somnia.GuestLog.Models.Configuration; + +public class StayExportConfig +{ + public const string SectionName = "StayExport"; + + public bool EnableStayExport { get; set; } = false; + public string? ExportStorageConnectionString { get; set; } + public bool UseSubdirectoryByFormat { get; set; } +} diff --git a/src/Somnia.GuestLog/Models/DTOs/StayExportFileDTO.cs b/src/Somnia.GuestLog/Models/DTOs/StayExportFileDTO.cs new file mode 100644 index 0000000..eed1c97 --- /dev/null +++ b/src/Somnia.GuestLog/Models/DTOs/StayExportFileDTO.cs @@ -0,0 +1,27 @@ +using Somnia.GuestLog.Enums.Models; + +namespace Somnia.GuestLog.Models.DTOs; + +public class StayExportFileDTO +{ + public long? Id { get; set; } + + public StayExportFormat? ExportFormat { get; set; } + + public StayExportParamsDTO? ExportParams { get; set; } + + public StayExportFileResult? ExportResult { get; set; } + + public string? ExportedPath { get; set; } + + + public DateTime? ExportTime { get; set; } +} + +public class StayExportParamsDTO +{ + public StayExportParamType? ExportType { get; set; } + + public DateTime? ExportPeriodStart { get; set; } + public DateTime? ExportPeriodEnd { get; set; } +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Models/Entities/GuestAddress.cs b/src/Somnia.GuestLog/Models/Entities/GuestAddress.cs index 4102080..f043920 100644 --- a/src/Somnia.GuestLog/Models/Entities/GuestAddress.cs +++ b/src/Somnia.GuestLog/Models/Entities/GuestAddress.cs @@ -1,3 +1,5 @@ +using Somnia.GuestLog.Infrastructure; + namespace Somnia.GuestLog.Models.Entities; public class GuestAddress : BaseEntity @@ -9,5 +11,5 @@ public class GuestAddress : BaseEntity public required string AddressCountry { get; set; } public string? ZipCode { get; set; } - public override string GetFullKey() => "N/A"; + public override string GetFullKey() => DbConsts.PK_NotApply; } \ No newline at end of file diff --git a/src/Somnia.GuestLog/Models/Entities/StayExportFile.cs b/src/Somnia.GuestLog/Models/Entities/StayExportFile.cs new file mode 100644 index 0000000..724049e --- /dev/null +++ b/src/Somnia.GuestLog/Models/Entities/StayExportFile.cs @@ -0,0 +1,28 @@ +using Somnia.GuestLog.Enums.Models; + +namespace Somnia.GuestLog.Models.Entities; + +public class StayExportFile : BaseEntity +{ + public long Id { get; set; } + + public required StayExportFormat ExportFormat { get; set; } + public required StayExportParams ExportParams { get; set; } + + public required StayExportFileResult ExportResult { get; set; } + + /// + /// Path to exported data. + /// Should be relative to configured Storage. + /// + public string? ExportedPath { get; set; } + + + /// + /// Date/Time when export file was generated. + /// + public required DateTime ExportTime { get; set; } + + + public override string GetFullKey() => $"{Id}"; +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Models/Entities/StayExportParams.cs b/src/Somnia.GuestLog/Models/Entities/StayExportParams.cs new file mode 100644 index 0000000..9df1dda --- /dev/null +++ b/src/Somnia.GuestLog/Models/Entities/StayExportParams.cs @@ -0,0 +1,19 @@ +using Somnia.GuestLog.Enums.Models; +using Somnia.GuestLog.Infrastructure; + +namespace Somnia.GuestLog.Models.Entities; + +/// +/// This class store params used on StayExport. +/// Different params apply for different export types. +/// +public class StayExportParams : BaseEntity +{ + public required StayExportParamType ExportType { get; set; } + + public DateTime? ExportPeriodStart { get; set; } + public DateTime? ExportPeriodEnd { get; set; } + + + public override string GetFullKey() => DbConsts.PK_NotApply; +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Models/EntityMapping.cs b/src/Somnia.GuestLog/Models/EntityMapping.cs index 68467df..019a902 100644 --- a/src/Somnia.GuestLog/Models/EntityMapping.cs +++ b/src/Somnia.GuestLog/Models/EntityMapping.cs @@ -51,4 +51,24 @@ internal static class EntityMapping }; } + public static StayExportFileDTO MapStayExportFileEntityToDTO(StayExportFile entity) + { + StayExportParamsDTO exportParamsDto = new() + { + ExportType = entity.ExportParams.ExportType, + ExportPeriodStart = entity.ExportParams.ExportPeriodStart, + ExportPeriodEnd = entity.ExportParams.ExportPeriodEnd, + }; + + return new() + { + Id = entity.Id, + ExportFormat = entity.ExportFormat, + ExportParams = exportParamsDto, + ExportResult = entity.ExportResult, + ExportedPath = entity.ExportedPath, + ExportTime = entity.ExportTime, + }; + } + } diff --git a/src/Somnia.GuestLog/Program.cs b/src/Somnia.GuestLog/Program.cs index d257911..42740a5 100644 --- a/src/Somnia.GuestLog/Program.cs +++ b/src/Somnia.GuestLog/Program.cs @@ -10,6 +10,7 @@ using Somnia.GuestLog.Models.DTOs; using Somnia.GuestLog.Services; using Somnia.GuestLog.Services.AddressQuery; using Somnia.GuestLog.Services.Entity; +using Somnia.GuestLog.Services.StayExport; using Somnia.GuestLog.Utils; using Somnia.GuestLog.Validators; @@ -55,19 +56,25 @@ builder.Services.AddSingleton, GuestDocumentDTOVali builder.Services.AddSingleton, GuestDocIdDTOValidator>(); builder.Services.AddSingleton, StayEntryDTOValidator>(); -// Add Services +// Add Entity Services builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +// Add Services builder.Services.AddSingleton(); builder.Services.AddScoped(); // Add Factories (and it's services) builder.Services.AddFactoryDI(); +builder.Services.AddFactoryDI(); // Add Options config builder.Services.AddOptions() .Bind(builder.Configuration.GetSection(AddressQueryConfig.SectionName)); +builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection(StayExportConfig.SectionName)); var app = builder.Build(); diff --git a/src/Somnia.GuestLog/Services/IStayExportFileService.cs b/src/Somnia.GuestLog/Services/IStayExportFileService.cs new file mode 100644 index 0000000..f06eb30 --- /dev/null +++ b/src/Somnia.GuestLog/Services/IStayExportFileService.cs @@ -0,0 +1,13 @@ +using Somnia.GuestLog.Models.DTOs; + +namespace Somnia.GuestLog.Services; + +public interface IStayExportFileService +{ + public Task CreateStayExportAsync(StayExportFileDTO dto); + public Task UpdateStayExportAsync(StayExportFileDTO dto); + + public Task GetDtoByIdAsync(long id); + //public Task> GetDtoListByFilterAsync(StayExportFileFilterDTO filter, int limit = 0); + public Task> GetDtoListAsync(); +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Services/StayExport/ConfigFileStaytExportServiceFactory.cs b/src/Somnia.GuestLog/Services/StayExport/ConfigFileStaytExportServiceFactory.cs new file mode 100644 index 0000000..7257f94 --- /dev/null +++ b/src/Somnia.GuestLog/Services/StayExport/ConfigFileStaytExportServiceFactory.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Options; +using Somnia.GuestLog.Enums.Models; +using Somnia.GuestLog.Models.Configuration; +using Somnia.GuestLog.Utils.DI; + +namespace Somnia.GuestLog.Services.StayExport; + +public class ConfigFileStaytExportServiceFactory( + IServiceProvider serviceProvider, + IOptions config +) : IStayExportServiceFactory, IFactoryDI +{ + public static void RegisterFactoryItems(IServiceCollection services) + { + services.AddKeyedSingleton(StayExportFormat.GuestLogJson); + services.AddKeyedSingleton(StayExportFormat.SpainParteViajeros); + } + + + public IStayExportService GetService(StayExportFormat exportFormat) + { + if (config.Value.EnableStayExport == false) + { + throw new InvalidOperationException("StayExport is disabled. Enable and configure it first!"); + } + + return serviceProvider.GetKeyedService(exportFormat) + ?? throw new InvalidOperationException($"StayExport service not found for ExportFormat: '{exportFormat}'"); + } +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Services/StayExport/IStayExportService.cs b/src/Somnia.GuestLog/Services/StayExport/IStayExportService.cs new file mode 100644 index 0000000..e51fc65 --- /dev/null +++ b/src/Somnia.GuestLog/Services/StayExport/IStayExportService.cs @@ -0,0 +1,11 @@ +using Somnia.GuestLog.Enums.Models; +using Somnia.GuestLog.Models.DTOs; + +namespace Somnia.GuestLog.Services.StayExport; + +public interface IStayExportService +{ + public StayExportFormat GetFormat(); + + public Task> ExportStaysByParams(StayExportParamsDTO exportParams); +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Services/StayExport/IStayExportServiceFactory.cs b/src/Somnia.GuestLog/Services/StayExport/IStayExportServiceFactory.cs new file mode 100644 index 0000000..b538f79 --- /dev/null +++ b/src/Somnia.GuestLog/Services/StayExport/IStayExportServiceFactory.cs @@ -0,0 +1,11 @@ +using Somnia.GuestLog.Enums.Models; + +namespace Somnia.GuestLog.Services.StayExport; + +public interface IStayExportServiceFactory +{ + /// + /// Returns the StayExport service implementation given current configuration. + /// + public IStayExportService GetService(StayExportFormat exportFormat); +} diff --git a/src/Somnia.GuestLog/Services/StayExport/JsonStayExportService.cs b/src/Somnia.GuestLog/Services/StayExport/JsonStayExportService.cs new file mode 100644 index 0000000..44a918e --- /dev/null +++ b/src/Somnia.GuestLog/Services/StayExport/JsonStayExportService.cs @@ -0,0 +1,192 @@ +using System.Text.Json; +using FluentStorage; +using FluentStorage.Storage; +using Microsoft.Extensions.Options; +using Somnia.GuestLog.Enums.Models; +using Somnia.GuestLog.Enums.Services; +using Somnia.GuestLog.Models.Configuration; +using Somnia.GuestLog.Models.DTOs; +using Somnia.GuestLog.Models.QueryConfigs; +using Somnia.GuestLog.Services.Entity; + +namespace Somnia.GuestLog.Services.StayExport; + +public class JsonStayExportService( + ILogger logger, + IStayExportFileService stayExportEntityService, + IStayEntryEntityService stayEntityService, + IOptions exportConfig +) : IStayExportService +{ + private const string fileExtension = "json"; + + private static readonly StayEntryQueryConfig queryConfig = + new(IncludeGuests: true, UseSplitQueries: true, DisableTracking: true); + private readonly StayExportConfig config = exportConfig.Value; + + public StayExportFormat GetFormat() => StayExportFormat.GuestLogJson; + + + public async Task> ExportStaysByParams(StayExportParamsDTO exportParams) + { + //Get data to export. + IEnumerable? stayList; + try + { + stayList = await GetStaysToExportByParamsAsync(exportParams); + } + catch (Exception ex) + { + logger.LogError(ex, "JsonStayExportService - ExportStaysByParams: Error retriving stays to export!"); + return Result.Fail(ResultCode.UnknownError); + } + + //Create initial StayExportFile + DateTime exportTime = DateTime.UtcNow; + StayExportFileDTO stayExport = new() + { + ExportFormat = GetFormat(), + ExportParams = exportParams, + ExportTime = exportTime, + }; + + //Export data. + using Stream exportedDataStream = new MemoryStream(); + + //Generate data to export. + StayExportFileResult? exportResult; + try + { + exportResult = await ExportDataToStream(stayList, exportedDataStream); + + // "Rewind" stream so it can be read from the begining when uploading. + exportedDataStream.Seek(0, SeekOrigin.Begin); + } + catch (Exception ex) + { + logger.LogError(ex, "JsonStayExportService - ExportStaysByParams: Error exporting data!"); + exportResult = StayExportFileResult.ExportError; + } + + //If data exported OK, save to file. + if (exportResult == StayExportFileResult.OK) + { + try + { + string exportPath = GetExportPath(exportTime); + + //Store data. + using IStore exportStore = GetExportStore(); + await exportStore.SetObject(objectPath: exportPath, dataStream: exportedDataStream); + + //Update path in item. + stayExport.ExportedPath = exportPath; + } + catch (Exception ex) + { + logger.LogError(ex, "JsonStayExportService - ExportStaysByParams: Error Uploading exported data!"); + exportResult = StayExportFileResult.UploadError; + } + } + + //Store StayExport in DB. + stayExport.ExportResult = exportResult; + Result? createResult = null; + try + { + createResult = await stayExportEntityService.CreateStayExportAsync(stayExport); + } + catch (Exception ex) + { + logger.LogError(ex, "JsonStayExportService - ExportStaysByParams: Error storing StayExportFile entity to DB: {stayExport}", stayExport); + } + + //If everything is OK, return OK. + if (createResult != null && createResult.ResultCode == ResultCode.Ok) + { + return Result.Ok(stayExport); + } + + //Saving export to DB failed. Delete exported file (if exported) + if (!string.IsNullOrEmpty(stayExport.ExportedPath)) + { + try + { + using IStore exportStore = GetExportStore(); + await exportStore.DeleteObject(objectPath: stayExport.ExportedPath); + } + catch (Exception ex) + { + logger.LogError(ex, "JsonStayExportService - ExportStaysByParams: Error deleting exported file after db saving error: {exportedPath}", stayExport.ExportedPath); + } + } + + logger.LogError( + "JsonStayExportService - ExportStaysByDateIn: Error (code '{code}') storing StayExportFile entity to DB: {stayExport}", + createResult?.ResultCode, stayExport); + return Result.Fail(ResultCode.UnknownError); + } + + + #region Internal Methods + + private async Task> GetStaysToExportByParamsAsync(StayExportParamsDTO exportParams) + { + //Todo - Improve type selection (when there are more types). + if (exportParams.ExportType == StayExportParamType.PeriodDateIn) + { + if (exportParams.ExportPeriodStart == null || exportParams.ExportPeriodEnd == null) + { + throw new InvalidOperationException($"StayExportFile - ExportType {exportParams.ExportType} require date start/end. Null dates found"); + } + + //Get Stays in period. + StayEntryFilterDTO filter = new() + { + DateInStart = exportParams.ExportPeriodStart, + DateInEnd = exportParams.ExportPeriodEnd + }; + return await stayEntityService.GetDtoListByFilterAsync(filter, limit: 0, queryConfig: queryConfig); + } + + throw new InvalidOperationException($"StayExportFile - Error getting stays using params: export type invalid: {exportParams.ExportType}"); + } + + private async Task ExportDataToStream(IEnumerable stayList, Stream exportStream) + { + if (!stayList.Any()) + { + return StayExportFileResult.NoData; + } + + // Convert to JSON. + await JsonSerializer.SerializeAsync(exportStream, stayList); + return StayExportFileResult.OK; + } + + private string GetExportPath(DateTime time) + { + string formatName = GetFormatName(); + string exportPathSubdir = config.UseSubdirectoryByFormat ? formatName : ""; + string exportFileName = $"{formatName}_{time.ToFileTimeUtc()}.{fileExtension}"; + return Path.Combine(exportPathSubdir, exportFileName); + } + + private string GetFormatName() + { + return Enum.GetName(GetFormat()) ?? throw new InvalidOperationException("Can't get StayExportFormat name!"); + } + + private IStore GetExportStore() + { + string? storageConnString = config.ExportStorageConnectionString; + if (string.IsNullOrEmpty(storageConnString)) + { + throw new InvalidOperationException("No Stay Export Storage configured. Stays can't be exported"); + } + + return StorageFactory.FromConnectionString(storageConnString); + } + + #endregion +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Services/StayExport/SpainParteViajerosStayExportService.cs b/src/Somnia.GuestLog/Services/StayExport/SpainParteViajerosStayExportService.cs new file mode 100644 index 0000000..3bc58e7 --- /dev/null +++ b/src/Somnia.GuestLog/Services/StayExport/SpainParteViajerosStayExportService.cs @@ -0,0 +1,17 @@ +using Somnia.GuestLog.Enums.Models; +using Somnia.GuestLog.Models.DTOs; + +namespace Somnia.GuestLog.Services.StayExport; + +public class SpainParteViajerosStayExportService( + +) : IStayExportService +{ + public StayExportFormat GetFormat() => StayExportFormat.SpainParteViajeros; + + + public Task> ExportStaysByParams(StayExportParamsDTO exportParams) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Services/StayExportFileService.cs b/src/Somnia.GuestLog/Services/StayExportFileService.cs new file mode 100644 index 0000000..8f2df56 --- /dev/null +++ b/src/Somnia.GuestLog/Services/StayExportFileService.cs @@ -0,0 +1,107 @@ +using FluentValidation; +using Microsoft.EntityFrameworkCore; +using Somnia.GuestLog.Infrastructure; +using Somnia.GuestLog.Models; +using Somnia.GuestLog.Models.DTOs; +using Somnia.GuestLog.Models.Entities; + +namespace Somnia.GuestLog.Services; + +public class StayExportFileService( + ILogger logger, + IDbContextFactory contextFactory +) : IStayExportFileService +{ + public async Task CreateStayExportAsync(StayExportFileDTO dto) + { + ArgumentNullException.ThrowIfNull(dto, nameof(dto)); + + //Check for null values in the DTO. + if (dto.ExportFormat == null || dto.ExportResult == null || dto.ExportTime == null) + { + throw new InvalidOperationException("StayExportFile dto contains null fields"); + } + + if (dto.ExportParams == null) + { + throw new InvalidOperationException("StayExportFile dto contains null ExportParams"); + } + + StayExportParams exportParams = GetExportParamsFromDTO(dto.ExportParams); + StayExportFile dbItem = new() + { + ExportFormat = dto.ExportFormat.Value, + ExportParams = exportParams, + ExportResult = dto.ExportResult.Value, + ExportedPath = dto.ExportedPath, + ExportTime = dto.ExportTime?.ToUniversalTime() ?? DateTime.UtcNow, + }; + + try + { + using var dbContext = contextFactory.CreateDbContext(); + await dbContext.AddAsync(dbItem); + await dbContext.SaveChangesAsync(); + + logger.LogInformation("StayExportFileService - CreateStayExportAsync: GuestDocument Created: '{id}'", dbItem.GetFullKey()); + return Result.Ok(); + } + catch (Exception ex) + { + logger.LogError(ex, "StayExportFileService - CreateStayExportAsync: Error creating GuestDocument"); + return Result.FailUnknownError(); + } + } + + public Task UpdateStayExportAsync(StayExportFileDTO dto) + { + throw new NotImplementedException(); + } + + + public async Task GetDtoByIdAsync(long id) + { + using var dbContext = contextFactory.CreateDbContext(); + StayExportFile? dbItem = await dbContext.StayExportFiles + .Where(e => e.Id == id) + .FirstOrDefaultAsync(); + + return dbItem is not null ? GetDtoFromDb(dbItem) : null; + } + + public async Task> GetDtoListAsync() + { + using var dbContext = contextFactory.CreateDbContext(); + + var dbList = await dbContext.StayExportFiles + .OrderByDescending(e => e.ExportTime) + .ToListAsync(); + + return dbList.Select(e => GetDtoFromDb(e)); + } + + + #region Internal Methods + + private static StayExportFileDTO GetDtoFromDb(StayExportFile dbItem) + { + return EntityMapping.MapStayExportFileEntityToDTO(dbItem); + } + + private static StayExportParams GetExportParamsFromDTO(StayExportParamsDTO dto) + { + if (dto.ExportType == null) + { + throw new InvalidOperationException("Dto with invalid data"); + } + + return new() + { + ExportType = dto.ExportType.Value, + ExportPeriodStart = dto.ExportPeriodStart?.ToUniversalTime(), + ExportPeriodEnd = dto.ExportPeriodEnd?.ToUniversalTime() + }; + } + + #endregion +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/appsettings.json b/src/Somnia.GuestLog/appsettings.json index 6c41ac1..ac9b3f2 100644 --- a/src/Somnia.GuestLog/appsettings.json +++ b/src/Somnia.GuestLog/appsettings.json @@ -20,5 +20,10 @@ "ApiKey": "", "UserAgent": "Somnia-GuestLog-Dev", "ResultLang": "es" + }, + "StayExport": { + "EnableStayExport": true, + "ExportStorageConnectionString": "disk://path=/SomniaGuestLog/StayExportFiles", + "UseSubdirectoryByFormat": true } } \ No newline at end of file