feat StayExport - StayExportProvider SpainParteViejeros - Initial implementation

- Added initial implementation to export stays in "SpainParteViajeros" format.
- Added required configurations.
This commit is contained in:
2026-09-08 14:31:00 +02:00
parent dc5293bdf7
commit 7b8020879b
6 changed files with 420 additions and 5 deletions
+4
View File
@@ -32,6 +32,10 @@ public static class GuestConsts
public const string DocumentType_NIE = "NIE"; public const string DocumentType_NIE = "NIE";
public const string DocumentType_PASSPORT = "PASSPORT"; public const string DocumentType_PASSPORT = "PASSPORT";
public const string GenderOptions_MALE = "M";
public const string GenderOptions_FEMALE = "F";
public const string GenderOptions_OTHER = "O";
public const int GuestDocument_FirstVersion = 1; public const int GuestDocument_FirstVersion = 1;
@@ -0,0 +1,9 @@
namespace Somnia.GuestLog.Models.Configuration;
public class StayExportSpainParteViajerosConfig
{
public const string SectionName = "StayExportSpainParteViajeros";
public bool InternetAvailable { get; set; } = false;
public string? CodigoEstablecimiento { get; set; }
}
+2
View File
@@ -79,6 +79,8 @@ builder.Services.AddOptions<AddressQueryConfig>()
.Bind(builder.Configuration.GetSection(AddressQueryConfig.SectionName)); .Bind(builder.Configuration.GetSection(AddressQueryConfig.SectionName));
builder.Services.AddOptions<StayExportConfig>() builder.Services.AddOptions<StayExportConfig>()
.Bind(builder.Configuration.GetSection(StayExportConfig.SectionName)); .Bind(builder.Configuration.GetSection(StayExportConfig.SectionName));
builder.Services.AddOptions<StayExportSpainParteViajerosConfig>()
.Bind(builder.Configuration.GetSection(StayExportSpainParteViajerosConfig.SectionName));
var app = builder.Build(); var app = builder.Build();
@@ -13,12 +13,26 @@ public class SpainCodigoMunicipioCatastroService(
private const string Api_Param_Provincia = "Provincia"; private const string Api_Param_Provincia = "Provincia";
private const string Api_Param_Municipio = "Municipio"; private const string Api_Param_Municipio = "Municipio";
//Result Cache.
readonly Dictionary<(string provincia, string municipio), string> codigoMunicipioCache = [];
public async Task<string?> GetCodigoMunicipioAsync(string municipio, string province) public async Task<string?> GetCodigoMunicipioAsync(string municipio, string province)
{ {
var cacheKey = (province.ToUpper(), municipio.ToUpper());
//Try get from cache.
if (codigoMunicipioCache.TryGetValue(cacheKey, out string? cacheCode))
{
logger.LogInformation("SpainCodigoMunicipioCatastroService - Municipio found in cache for: '{key}'", cacheKey);
return cacheCode;
}
try try
{ {
string uri = GetRequestUri(municipio, province); string uri = GetRequestUri(municipio, province);
logger.LogInformation("SpainCodigoMunicipioCatastroService - Getting municipios with query: '{addr}'", uri); logger.LogInformation("SpainCodigoMunicipioCatastroService - Getting municipios from API with query: '{addr}'", uri);
var httpClient = httpFactory.CreateClient(); var httpClient = httpFactory.CreateClient();
using var rsStream = await httpClient.GetStreamAsync(uri); using var rsStream = await httpClient.GetStreamAsync(uri);
@@ -49,7 +63,10 @@ public class SpainCodigoMunicipioCatastroService(
return null; return null;
} }
return $"{codIne.codigoProvincia}{codIne.codigoMunicipio}"; string finalCode = $"{codIne.codigoProvincia}{codIne.codigoMunicipio}";
codigoMunicipioCache.Add(cacheKey, finalCode);
return finalCode;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -1,20 +1,399 @@
using System.Diagnostics;
using System.Xml.Serialization;
using FluentValidation;
using Microsoft.Extensions.Options;
using Somnia.GuestLog.Enums.Models; using Somnia.GuestLog.Enums.Models;
using Somnia.GuestLog.Models.Configuration;
using Somnia.GuestLog.Models.DTOs; using Somnia.GuestLog.Models.DTOs;
using Somnia.GuestLog.Services.SpainCodigoMunicipio;
namespace Somnia.GuestLog.Services.StayExport; namespace Somnia.GuestLog.Services.StayExport;
public class StayExportProviderSpainParteViajeros( public class StayExportProviderSpainParteViajeros(
ILogger<StayExportProviderSpainParteViajeros> logger,
ISpainCodigoMunicipioService codigoMunicipioService,
IOptions<StayExportSpainParteViajerosConfig> configOptions
) : IStayExportFormatProvider ) : IStayExportFormatProvider
{ {
private const string fileExtension = "xml"; private const string fileExtension = "xml";
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 readonly StayExportSpainParteViajerosConfig config = configOptions.Value;
public StayExportFormat GetFormat() => StayExportFormat.SpainParteViajeros; public StayExportFormat GetFormat() => StayExportFormat.SpainParteViajeros;
public string GetFilename(DateTime exportTime) public string GetFilename(DateTime exportTime)
=> $"{Enum.GetName(GetFormat())}_{exportTime.ToFileTimeUtc()}.{fileExtension}"; => $"{Enum.GetName(GetFormat())}_{exportTime.ToFileTimeUtc()}.{fileExtension}";
public Task<StayExportFileResult> ExportDataAsync(IEnumerable<StayEntryDTO> stayList, Stream dataStream) public async Task<StayExportFileResult> ExportDataAsync(IEnumerable<StayEntryDTO> stayList, Stream dataStream)
{ {
throw new NotImplementedException(); if (string.IsNullOrEmpty(config.CodigoEstablecimiento))
{
logger.LogError("StayExportProviderSpainParteViajeros - Config error: CodigoEstablecimiento is not configured!");
return StayExportFileResult.ExportError;
}
if (!stayList.Any())
{
return StayExportFileResult.NoData;
}
ComunicacionValidator comunicacionValidator = new();
List<Comunicacion> comunicacionsList = [];
foreach (StayEntryDTO stay in stayList)
{
(Comunicacion? comunicacion, string code) = await GetComunicacionFromStay(stay);
if (comunicacion == null)
{
logger.LogWarning("StayExportProviderSpainParteViajeros - Stay (ID: {id}) can't be exported: Code: {code}", stay.Id, code);
continue;
}
var errors = comunicacionValidator.Validate(comunicacion);
if (!errors.IsValid)
{
logger.LogWarning("StayExportProviderSpainParteViajeros - Stay (ID: {id}) is invalid: Errors: {errors}", stay.Id, errors.Errors);
continue;
}
comunicacionsList.Add(comunicacion);
logger.LogInformation("StayExportProviderSpainParteViajeros - Stay (ID: {id}) Exported OK", stay.Id);
}
Peticion peticion = new()
{
solicitud = new()
{
codigoEstablecimiento = config.CodigoEstablecimiento,
comunicacion = comunicacionsList,
}
};
XmlSerializer serializer = new(typeof(Peticion));
serializer.Serialize(dataStream, peticion);
return StayExportFileResult.OK;
}
#region Internal Methods
private async Task<(Comunicacion? comunicacion, string code)> 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<Persona> 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<string?> 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,
code: "NoCodigoMunicipio"
);
} }
} }
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
},
code: "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 => "F",
null => null,
_ => "O"
};
}
private static string? GetTipoPagoFromStay(StayEntryDTO stay)
{
return stay.PaymentMethod;
}
private async Task<string?> GetMunicipioFromGuestAddressAsync(GuestDocumentDTO doc)
{
if (string.IsNullOrEmpty(doc.AddressCity) || string.IsNullOrEmpty(doc.AddressProvince))
{
return null;
}
//Get codMunicipio from API.
return await codigoMunicipioService.GetCodigoMunicipioAsync(doc.AddressCity, doc.AddressProvince);
}
#endregion
#region Validator
private sealed class ComunicacionValidator : AbstractValidator<Comunicacion>
{
//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
[XmlRoot(ElementName = "peticion", Namespace = XmlNamespace_Peticion)]
public class Peticion
{
public required Solicitud solicitud;
}
[XmlRoot(ElementName = "solicitud")]
public class Solicitud
{
public required string codigoEstablecimiento;
public required List<Comunicacion> comunicacion;
}
[XmlRoot(ElementName = "comunicacion")]
public class Comunicacion
{
public required Contrato contrato;
public required List<Persona> persona;
}
[XmlRoot(ElementName = "contrato")]
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;
}
[XmlRoot(ElementName = "persona")]
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;
public string? telefono;
public string? correo;
public required string parentesco;
// BLOQUE DIRECCION
public required Direccion direccion;
}
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
}
+4
View File
@@ -25,5 +25,9 @@
"EnableStayExport": true, "EnableStayExport": true,
"ExportStorageConnectionString": "disk://path=/SomniaGuestLog/StayExportFiles", "ExportStorageConnectionString": "disk://path=/SomniaGuestLog/StayExportFiles",
"UseSubdirectoryByFormat": true "UseSubdirectoryByFormat": true
},
"StayExportSpainParteViajeros": {
"InternetAvailable": true,
"CodigoEstablecimiento": ""
} }
} }