using System.Diagnostics; using System.Text.Json; using System.Xml.Serialization; using FluentValidation; using Microsoft.Extensions.Options; using Somnia.GuestLog.Enums.Models; using Somnia.GuestLog.Models.Configuration; using Somnia.GuestLog.Models.DTOs; using Somnia.GuestLog.Services.SpainCodigoMunicipio; namespace Somnia.GuestLog.Services.StayExport; public class StayExportProviderSpainParteViajeros( ILogger logger, ISpainCodigoMunicipioService codigoMunicipioService, IOptions configOptions ) : IStayExportFormatProvider { private const string fileExtension = "xml"; private const string ExportDetailsResult_Error = "Error"; private const string ExportDetailsResult_Ok = "Ok"; private const string PersonRole = "VI"; private const string PersonRelation = "OT"; private const string PersonTipoDoc_NIF = "NIF"; private const string PersonTipoDoc_NIE = "NIE"; private const string PersonTipoDoc_PAS = "PAS"; private const string PersonTipoDoc_OTRO = "OTRO"; private const string PersonDireccionPais_ESP = "ESP"; private const string XmlNamespace_Peticion = "http://www.neg.hospedajes.mir.es/altaParteHospedaje"; private const string XmlNamespace_Peticion_Prefix = "ns2"; private readonly StayExportSpainParteViajerosConfig config = configOptions.Value; public StayExportFormat GetFormat() => StayExportFormat.SpainParteViajeros; public string GetFilename(DateTime exportTime) => $"{Enum.GetName(GetFormat())}_{exportTime.ToFileTimeUtc()}.{fileExtension}"; public async Task<(StayExportFileResult fileResult, string? exportDetails)> ExportDataAsync(IEnumerable stayList, Stream dataStream) { if (string.IsNullOrEmpty(config.CodigoEstablecimiento)) { logger.LogError("StayExportProviderSpainParteViajeros - Config error: CodigoEstablecimiento is not configured!"); return (fileResult: StayExportFileResult.ExportError, exportDetails: null); } if (!stayList.Any()) { return (fileResult: StayExportFileResult.NoData, exportDetails: null); } List stayExportDetails = []; ComunicacionValidator comunicacionValidator = new(); List comunicacionsList = []; foreach (StayEntryDTO stay in stayList) { (Comunicacion? comunicacion, string errorMsg) = await GetComunicacionFromStay(stay); if (comunicacion == null) { logger.LogWarning("StayExportProviderSpainParteViajeros - Stay (ID: {id}) can't be exported: Error: {code}", stay.Id, errorMsg); stayExportDetails.Add(new(StayId: stay.Id ?? -1, ExportResult: ExportDetailsResult_Error, Details: errorMsg)); continue; } var errors = comunicacionValidator.Validate(comunicacion); if (!errors.IsValid) { logger.LogWarning("StayExportProviderSpainParteViajeros - Stay (ID: {id}) is invalid: Errors: {errors}", stay.Id, errors.Errors); stayExportDetails.Add(new(StayId: stay.Id ?? -1, ExportResult: ExportDetailsResult_Error, Details: errors.ToString())); continue; } comunicacionsList.Add(comunicacion); logger.LogInformation("StayExportProviderSpainParteViajeros - Stay (ID: {id}) Exported OK", stay.Id); stayExportDetails.Add(new(StayId: stay.Id ?? -1, ExportResult: ExportDetailsResult_Ok, Details: null)); } Peticion peticion = new() { solicitud = new() { codigoEstablecimiento = config.CodigoEstablecimiento, comunicacion = comunicacionsList, } }; //Create XmlSerializerNamespaces to add prefix-namespace pairs. var docNamespaces = new XmlSerializerNamespaces(); docNamespaces.Add(XmlNamespace_Peticion_Prefix, XmlNamespace_Peticion); XmlSerializer serializer = new(typeof(Peticion)); serializer.Serialize(stream: dataStream, o: peticion, namespaces: docNamespaces); string details = JsonSerializer.Serialize(stayExportDetails); return (fileResult: StayExportFileResult.OK, exportDetails: details); } #region Internal Methods private async Task<(Comunicacion? comunicacion, string errorMsg)> GetComunicacionFromStay(StayEntryDTO stay) { Debug.Assert(stay.Id != null && stay.DateIn != null && stay.DateOut != null && stay.GuestDocuments != null ); //Call Fn to validate Stay. Contrato contrato = new() { referencia = stay.Id.Value.ToString(), fechaContrato = stay.DateIn.Value.Date, fechaEntrada = stay.DateIn.Value, fechaSalida = stay.DateOut.Value, internet = config.InternetAvailable, numHabitaciones = stay.RoomCount ?? -1, numPersonas = stay.GuestDocuments.Count, pago = new() { tipoPago = GetTipoPagoFromStay(stay)! } }; List personasList = []; foreach (GuestDocumentDTO doc in stay.GuestDocuments) { Direccion dir = new() { direccion = doc.Address!, codigoPostal = doc.ZipCode!, pais = doc.AddressCountry!, }; //Get Municipio given current country. if (doc.AddressCountry != PersonDireccionPais_ESP) { List addressParts = [doc.AddressCity, doc.AddressProvince, doc.AddressState]; dir.nombreMunicipio = string.Join(", ", addressParts.Where(p => !string.IsNullOrEmpty(p))); } if (doc.AddressCountry == PersonDireccionPais_ESP) { dir.codigoMunicipio = await GetMunicipioFromGuestAddressAsync(doc); if (dir.codigoMunicipio == null) { return ( comunicacion: null, errorMsg: $"Can't get codigo municipio for {doc.AddressProvince}-{doc.AddressCity} in doc {doc.FullDocument}" ); } } Persona persona = new() { rol = PersonRole, nombre = doc.Name!, apellido1 = doc.Surname!, apellido2 = doc.Surname2, tipoDocumento = GetTipoDocumentoFromGuest(doc)!, numeroDocumento = doc.DocumentNum!, soporteDocumento = doc.SupportNum, fechaNacimiento = doc.BirthDate?.Date ?? DateTime.MaxValue, nacionalidad = doc.Nationality!, sexo = GetSexoFromGuest(doc)!, telefono = doc.PhoneNumber, correo = doc.EmailAddress, parentesco = PersonRelation, direccion = dir, }; personasList.Add(persona); } return ( comunicacion: new() { contrato = contrato, persona = personasList }, errorMsg: "OK"); } private static string? GetTipoDocumentoFromGuest(GuestDocumentDTO doc) { return doc.DocumentType switch { GuestConsts.DocumentType_DNI => PersonTipoDoc_NIF, GuestConsts.DocumentType_NIE => PersonTipoDoc_NIE, GuestConsts.DocumentType_PASSPORT => PersonTipoDoc_PAS, null => null, _ => PersonTipoDoc_OTRO }; } private static string? GetSexoFromGuest(GuestDocumentDTO doc) { return doc.Gender switch { GuestConsts.GenderOptions_MALE => "H", GuestConsts.GenderOptions_FEMALE => "M", null => null, _ => "O" }; } private static string? GetTipoPagoFromStay(StayEntryDTO stay) { return stay.PaymentMethod; } private async Task GetMunicipioFromGuestAddressAsync(GuestDocumentDTO doc) { if (string.IsNullOrEmpty(doc.AddressCity) || string.IsNullOrEmpty(doc.AddressProvince)) { logger.LogWarning("Found doc ({docNum}) without full address", doc.FullDocument); return null; } //Get codMunicipio from API. return await codigoMunicipioService.GetCodigoMunicipioAsync(doc.AddressCity, doc.AddressProvince); } #endregion #region Validator private sealed class ComunicacionValidator : AbstractValidator { //TODO: Validate values in list for: Pago.tipoPago, Persona.sexo, Persona.tipoDocumento public ComunicacionValidator() { RuleFor(comm => comm.contrato) .NotNull() .ChildRules(contrato => { contrato.RuleFor(p => p.referencia) .NotEmpty() .MaximumLength(50); contrato.RuleFor(p => p.fechaEntrada) .LessThan(p => p.fechaSalida); contrato.RuleFor(p => p.fechaSalida) .GreaterThan(p => p.fechaEntrada); contrato.RuleFor(p => p.numHabitaciones) .GreaterThanOrEqualTo(0); //BLOQUE PAGO contrato.RuleFor(p => p.pago) .ChildRules(e => { e.RuleFor(p => p.tipoPago) .NotEmpty(); }); }); RuleForEach(comm => comm.persona) .ChildRules(person => { person.RuleFor(p => p.rol) .Equal(PersonRole); person.RuleFor(p => p.nombre) .NotEmpty() .MaximumLength(50); person.RuleFor(p => p.apellido1) .NotEmpty() .MaximumLength(50); person.RuleFor(p => p.apellido2) .MaximumLength(50); person.RuleFor(p => p.tipoDocumento) .NotEmpty() .MaximumLength(5); person.RuleFor(p => p.numeroDocumento) .NotEmpty() .MaximumLength(15); person.RuleFor(p => p.soporteDocumento) .NotEmpty() .When(p => p.tipoDocumento == PersonTipoDoc_NIF || p.tipoDocumento == PersonTipoDoc_NIE); person.RuleFor(p => p.fechaNacimiento) .NotEmpty(); person.RuleFor(p => p.sexo) .MaximumLength(1); person.RuleFor(p => p.telefono) .MaximumLength(20); person.RuleFor(p => p.correo) .MaximumLength(250); person.RuleFor(p => p.direccion) .ChildRules(d => { d.RuleFor(p => p.direccion) .NotEmpty() .MaximumLength(100); d.RuleFor(p => p.codigoMunicipio) .NotEmpty() .When(p => p.pais == PersonDireccionPais_ESP) .MaximumLength(5); d.RuleFor(p => p.nombreMunicipio) .NotEmpty() .When(p => p.pais != PersonDireccionPais_ESP) .MaximumLength(100); d.RuleFor(p => p.codigoMunicipio) .NotEmpty() .MaximumLength(20); d.RuleFor(p => p.pais) .NotEmpty() .Length(3); }); }); RuleForEach(comm => comm.persona) .Must(p => !string.IsNullOrEmpty(p.telefono) || !string.IsNullOrEmpty(p.correo)) .WithMessage($"It's required to have at least one of the following fields: '{nameof(Persona.telefono)}', '{nameof(Persona.correo)}'"); } } #endregion #region Export Data Classes private record StayExportDetails(long StayId, string ExportResult, string? Details); [XmlRoot(ElementName = "peticion", Namespace = XmlNamespace_Peticion)] public class Peticion { [XmlElement(Namespace = "")] public required Solicitud solicitud; } [XmlRoot(ElementName = "solicitud")] public class Solicitud { public required string codigoEstablecimiento; [XmlElement(ElementName = "comunicacion")] public required List comunicacion; } public class Comunicacion { [XmlElement(ElementName = "contrato")] public required Contrato contrato; [XmlElement(ElementName = "persona")] public required List persona; } public class Contrato { public required string referencia; [XmlElement(DataType = "date")] public required DateTime fechaContrato; [XmlElement(DataType = "dateTime")] public required DateTime fechaEntrada; [XmlElement(DataType = "dateTime")] public required DateTime fechaSalida; public required int numPersonas; public int? numHabitaciones; public bool? internet; //BLOQUE PAGO public required Pago pago; } public class Persona { public required string rol; public required string nombre; public required string apellido1; public string? apellido2; public required string tipoDocumento; public required string numeroDocumento; public string? soporteDocumento; [XmlElement(DataType = "date")] public required DateTime fechaNacimiento; public required string nacionalidad; public required string sexo; // BLOQUE DIRECCION public required Direccion direccion; public string? telefono; public string? correo; public required string parentesco; } public class Pago { public required string tipoPago; // [XmlElement(DataType = "date")] // public required DateTime fechaPago; public string? medioPago; public string? titular; public string? caducidadTarjeta; } public class Direccion { public required string direccion; public string? codigoMunicipio; public string? nombreMunicipio; public required string codigoPostal; public required string pais; } #endregion }