diff --git a/SIG.Web/App_Code/Configuration/AppServices.cs b/SIG.Web/App_Code/Configuration/AppServices.cs index 77a3fff..e18f78a 100644 --- a/SIG.Web/App_Code/Configuration/AppServices.cs +++ b/SIG.Web/App_Code/Configuration/AppServices.cs @@ -45,9 +45,11 @@ namespace SIG.Web.AppCode.Configuration public static IUserAdministrationService UserAdministrationService { get; private set; } public static IGeneralCatalogRepository GeneralCatalogRepository { get; private set; } public static IClienteDocumentoService ClienteDocumentoService { get; private set; } + public static ITramiteDocumentoService TramiteDocumentoService { get; private set; } public static IFraccionamientoService FraccionamientoService { get; private set; } public static IFraccionamientoGisService FraccionamientoGisService { get; private set; } public static ITramiteCatalogService TramiteCatalogService { get; private set; } + public static ITramiteSolicitudService TramiteSolicitudService { get; private set; } public static ICfdiTimbradoService CfdiTimbradoService { get; private set; } public static IPublicRegistrationService PublicRegistrationService { get; private set; } public static ILoginAttemptRepository LoginAttemptRepository { get; private set; } @@ -90,9 +92,22 @@ namespace SIG.Web.AppCode.Configuration GeneralCatalogRepository = new GeneralCatalogRepository(DbExecutor); ClienteDocumentoService = new ClienteDocumentoService( new ClienteDocumentoRepository(DbExecutor)); + + ITramiteDocumentoRepository tramiteDocumentoRepository = + new TramiteDocumentoRepository(DbExecutor); + ITramiteDocumentoFileStore tramiteDocumentoFileStore = + new TramiteDocumentoFileStore(); + TramiteDocumentoService = new TramiteDocumentoService( + tramiteDocumentoRepository, + tramiteDocumentoFileStore, + BitacoraService); + + ITramiteSolicitudRepository tramiteSolicitudRepository = + new TramiteSolicitudRepository(DbExecutor); + FraccionamientoService = new FraccionamientoService( new FraccionamientoRepository(DbExecutor), - new TramiteSolicitudRepository(DbExecutor), + tramiteSolicitudRepository, BitacoraService); IFraccionamientoGisRepository gisRepository = @@ -102,10 +117,14 @@ namespace SIG.Web.AppCode.Configuration FraccionamientoGisService = new FraccionamientoGisService( gisRepository, gisFileStore, - BitacoraService); + BitacoraService, + TramiteDocumentoService); TramiteCatalogService = new TramiteCatalogService( new TramiteCatalogRepository(DbExecutor), BitacoraService); + TramiteSolicitudService = new TramiteSolicitudService( + tramiteSolicitudRepository, + BitacoraService); CfdiTimbradoService = new CfdiTimbradoService( new CfdiRepository(DbExecutor), new ProfactCfdi40XmlBuilder(), diff --git a/SIG.Web/App_Code/Contracts/Documentos/ITramiteDocumentoFileStore.cs b/SIG.Web/App_Code/Contracts/Documentos/ITramiteDocumentoFileStore.cs new file mode 100644 index 0000000..721cd88 --- /dev/null +++ b/SIG.Web/App_Code/Contracts/Documentos/ITramiteDocumentoFileStore.cs @@ -0,0 +1,18 @@ +using System.IO; +using SIG.Web.AppCode.Models.Documentos; + +namespace SIG.Web.AppCode.Contracts.Documentos +{ + public interface ITramiteDocumentoFileStore + { + TramiteDocumentoPreparedFile Prepare( + TramiteDocumentoUploadContextModel context, + Stream source, + string previousRelativePath); + + void Promote(TramiteDocumentoPreparedFile preparedFile); + void Rollback(TramiteDocumentoPreparedFile preparedFile); + void Complete(TramiteDocumentoPreparedFile preparedFile); + Stream OpenRead(string relativePath); + } +} diff --git a/SIG.Web/App_Code/Contracts/Documentos/ITramiteDocumentoRepository.cs b/SIG.Web/App_Code/Contracts/Documentos/ITramiteDocumentoRepository.cs new file mode 100644 index 0000000..e2ff062 --- /dev/null +++ b/SIG.Web/App_Code/Contracts/Documentos/ITramiteDocumentoRepository.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using SIG.Web.AppCode.Models.Common; +using SIG.Web.AppCode.Models.Documentos; + +namespace SIG.Web.AppCode.Contracts.Documentos +{ + public interface ITramiteDocumentoRepository + { + DbResult> ListForDevelopment( + int developmentId, + int? stageId, + int? procedureTypeId, + int executingUser, + string domainName); + + DbResult GetUploadContext( + int developmentId, + int requestId, + int documentTypeId, + int executingUser, + string domainName); + + DbResult GetStageUploadContext( + int stageId, + int procedureTypeId, + int documentTypeId, + int executingUser, + string domainName); + + DbResult SaveUpload( + int developmentId, + int requestId, + int documentTypeId, + string originalFileName, + string physicalFileName, + string relativePath, + string contentType, + long contentLength, + string clientComment, + int executingUser, + string domainName); + + DbResult Review( + int developmentId, + int documentRequestId, + bool approved, + string note, + int executingUser, + string domainName); + + DbResult GetDownloadContext( + int developmentId, + int documentRequestId, + int executingUser, + string domainName); + + DbResult Delete( + int developmentId, + int documentRequestId, + int executingUser, + string domainName); + } +} diff --git a/SIG.Web/App_Code/Contracts/Documentos/ITramiteDocumentoService.cs b/SIG.Web/App_Code/Contracts/Documentos/ITramiteDocumentoService.cs new file mode 100644 index 0000000..0feb81f --- /dev/null +++ b/SIG.Web/App_Code/Contracts/Documentos/ITramiteDocumentoService.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.IO; +using SIG.Web.AppCode.Models.Common; +using SIG.Web.AppCode.Models.Documentos; + +namespace SIG.Web.AppCode.Contracts.Documentos +{ + public interface ITramiteDocumentoService + { + DbResult> ListForDevelopment( + int developmentId, + int? stageId, + int? procedureTypeId, + int executingUser, + string domainName); + + DbResult GetUploadContext( + int developmentId, + int requestId, + int documentTypeId, + int executingUser, + string domainName); + + DbResult Upload( + int developmentId, + int requestId, + int documentTypeId, + string originalFileName, + string contentType, + Stream content, + long contentLength, + string clientComment, + int executingUser, + string domainName); + + DbResult Review( + int developmentId, + int documentRequestId, + bool approved, + string note, + int executingUser, + string domainName); + + DbResult Open( + int developmentId, + int documentRequestId, + int executingUser, + string domainName); + + DbResult RegisterExternalStageFile( + int stageId, + int procedureTypeId, + int documentTypeId, + string originalFileName, + string physicalFileName, + string relativePath, + string contentType, + long contentLength, + int executingUser, + string domainName); + + DbResult RemoveExternalStageFile( + int stageId, + int procedureTypeId, + int documentTypeId, + int executingUser, + string domainName); + } +} diff --git a/SIG.Web/App_Code/Contracts/Tramites/ITramiteSolicitudRepository.cs b/SIG.Web/App_Code/Contracts/Tramites/ITramiteSolicitudRepository.cs index c21abd3..ca6017f 100644 --- a/SIG.Web/App_Code/Contracts/Tramites/ITramiteSolicitudRepository.cs +++ b/SIG.Web/App_Code/Contracts/Tramites/ITramiteSolicitudRepository.cs @@ -1,4 +1,6 @@ +using System.Collections.Generic; using SIG.Web.AppCode.Models.Common; +using SIG.Web.AppCode.Models.Tramites; namespace SIG.Web.AppCode.Contracts.Tramites { @@ -9,5 +11,28 @@ namespace SIG.Web.AppCode.Contracts.Tramites int procedureTypeId, int executingUser, string domainName); + + DbResult> ListForDevelopment( + int developmentId, + int? stageId, + int? procedureTypeId, + int? statusId, + int executingUser, + string domainName); + + DbResult Get( + int requestId, + int executingUser, + string domainName); + + DbResult> ListStatusOptions( + int executingUser, + string domainName); + + DbResult ChangeStatus( + int requestId, + int statusId, + int executingUser, + string domainName); } } diff --git a/SIG.Web/App_Code/Contracts/Tramites/ITramiteSolicitudService.cs b/SIG.Web/App_Code/Contracts/Tramites/ITramiteSolicitudService.cs new file mode 100644 index 0000000..57c245f --- /dev/null +++ b/SIG.Web/App_Code/Contracts/Tramites/ITramiteSolicitudService.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using SIG.Web.AppCode.Models.Common; +using SIG.Web.AppCode.Models.Tramites; + +namespace SIG.Web.AppCode.Contracts.Tramites +{ + public interface ITramiteSolicitudService + { + DbResult> ListForDevelopment( + int developmentId, + int? stageId, + int? procedureTypeId, + int? statusId, + int executingUser, + string domainName); + + DbResult Get( + int requestId, + int executingUser, + string domainName); + + DbResult> ListStatusOptions( + int executingUser, + string domainName); + + DbResult ChangeStatus( + int requestId, + int statusId, + int executingUser, + string domainName); + } +} diff --git a/SIG.Web/App_Code/Handlers/Documentos/TramiteDocumentoHandler.cs b/SIG.Web/App_Code/Handlers/Documentos/TramiteDocumentoHandler.cs new file mode 100644 index 0000000..8a3e030 --- /dev/null +++ b/SIG.Web/App_Code/Handlers/Documentos/TramiteDocumentoHandler.cs @@ -0,0 +1,129 @@ +using System; +using System.Globalization; +using System.IO; +using System.Web; +using System.Web.SessionState; +using SIG.Web.AppCode.Configuration; +using SIG.Web.AppCode.Models.Common; +using SIG.Web.AppCode.Models.Documentos; +using SIG.Web.AppCode.Session; + +namespace SIG.Web.Pages.Fraccionamientos +{ + public sealed class TramiteDocumentoHandler + : IHttpHandler, IRequiresSessionState + { + public bool IsReusable + { + get { return false; } + } + + public void ProcessRequest(HttpContext context) + { + UserSessionModel currentUser = GetCurrentUser(); + + if (currentUser == null) + { + context.Response.StatusCode = 401; + context.Response.ContentType = "text/plain"; + context.Response.Write( + "Su sesión no está disponible. Inicie sesión nuevamente."); + return; + } + + int developmentId; + int documentRequestId; + + if (!TryReadInt( + context.Request.QueryString["fraccionamiento"], + out developmentId) || + !TryReadInt( + context.Request.QueryString["documento"], + out documentRequestId)) + { + context.Response.StatusCode = 400; + context.Response.ContentType = "text/plain"; + context.Response.Write("El documento solicitado no es válido."); + return; + } + + DbResult result = + AppServices.TramiteDocumentoService.Open( + developmentId, + documentRequestId, + currentUser.CveUsuario, + currentUser.NombreDominio); + + if (!result.Success || + result.Data == null || + result.Data.Content == null) + { + context.Response.StatusCode = 404; + context.Response.ContentType = "text/plain"; + context.Response.Write( + result == null || + string.IsNullOrWhiteSpace(result.UserMessage) + ? "No se encontró el documento." + : result.UserMessage); + return; + } + + TramiteDocumentoFileModel file = result.Data; + + context.Response.Clear(); + context.Response.ClearHeaders(); + context.Response.TrySkipIisCustomErrors = true; + context.Response.Cache.SetCacheability(HttpCacheability.NoCache); + context.Response.Cache.SetNoStore(); + context.Response.ContentType = + string.IsNullOrWhiteSpace(file.ContentType) + ? "application/octet-stream" + : file.ContentType; + context.Response.AddHeader( + "Content-Disposition", + "attachment; filename=\"" + + SanitizeFileName(file.NombreArchivo) + + "\""); + + using (file.Content) + { + file.Content.CopyTo(context.Response.OutputStream); + } + + context.ApplicationInstance.CompleteRequest(); + } + + private static UserSessionModel GetCurrentUser() + { + return AppServices.UserContext != null && + AppServices.UserContext.IsAuthenticated() + ? AppServices.UserContext.GetCurrentUser() + : null; + } + + private static bool TryReadInt( + string value, + out int result) + { + return int.TryParse( + value, + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out result) && + result > 0; + } + + private static string SanitizeFileName(string fileName) + { + string value = Path.GetFileName( + string.IsNullOrWhiteSpace(fileName) + ? "documento" + : fileName); + + return value + .Replace("\"", string.Empty) + .Replace("\r", string.Empty) + .Replace("\n", string.Empty); + } + } +} diff --git a/SIG.Web/App_Code/Models/Documentos/TramiteDocumentoModels.cs b/SIG.Web/App_Code/Models/Documentos/TramiteDocumentoModels.cs new file mode 100644 index 0000000..91e3ab2 --- /dev/null +++ b/SIG.Web/App_Code/Models/Documentos/TramiteDocumentoModels.cs @@ -0,0 +1,171 @@ +using System; +using System.IO; + +namespace SIG.Web.AppCode.Models.Documentos +{ + public sealed class TramiteDocumentoSolicitudModel + { + public int CveDocumentoSolicitud { get; set; } + public int CveSolicitud { get; set; } + public string Folio { get; set; } + public int CveCliente { get; set; } + public int CveFraccionamiento { get; set; } + public int CveFraccionamientoEtapa { get; set; } + public string Etapa { get; set; } + public int CveTipoTramite { get; set; } + public string TipoTramite { get; set; } + public int CveTipoDocumento { get; set; } + public string TipoDocumento { get; set; } + public string NombreArchivo { get; set; } + public string Nota { get; set; } + public bool? EsAprobado { get; set; } + public DateTime? FechaRevision { get; set; } + public bool EsFinalSolicitud { get; set; } + public bool TieneArchivo { get; set; } + public bool PuedeActualizarArchivo { get; set; } + public bool PuedeRevisar { get; set; } + public bool EsGis { get; set; } + public bool PuedeAdministrarGis { get; set; } + public string EstadoDocumento { get; set; } + } + + public sealed class TramiteDocumentoUploadContextModel + { + public int CveDocumentoSolicitud { get; set; } + public int CveSolicitud { get; set; } + public string Folio { get; set; } + public int CveCliente { get; set; } + public int CveFraccionamiento { get; set; } + public int CveFraccionamientoEtapa { get; set; } + public string Etapa { get; set; } + public int CveTipoTramite { get; set; } + public string TipoTramite { get; set; } + public int CveTipoDocumento { get; set; } + public string TipoDocumento { get; set; } + public string ExtensionesPermitidas { get; set; } + public int? TamanoMaximoMb { get; set; } + public bool EsFinalSolicitud { get; set; } + public string NombreArchivoAnterior { get; set; } + public string NombreArchivoFisicoAnterior { get; set; } + public string RutaArchivoAnterior { get; set; } + public string ContentTypeAnterior { get; set; } + public long? TamanoBytesAnterior { get; set; } + public string ComentarioClienteAnterior { get; set; } + public string ComentarioAdministrativoAnterior { get; set; } + public bool? EsAprobadoAnterior { get; set; } + public int? CveUsuarioRevisaAnterior { get; set; } + public DateTime? FechaRevisionAnterior { get; set; } + } + + public sealed class TramiteDocumentoSaveResultModel + { + public int CveDocumentoSolicitud { get; set; } + public int CveSolicitud { get; set; } + public string Folio { get; set; } + public int CveCliente { get; set; } + public int CveFraccionamiento { get; set; } + public int CveFraccionamientoEtapa { get; set; } + public string Etapa { get; set; } + public int CveTipoTramite { get; set; } + public string TipoTramite { get; set; } + public int CveTipoDocumento { get; set; } + public string TipoDocumento { get; set; } + public string NombreArchivo { get; set; } + public string NombreArchivoFisico { get; set; } + public string RutaArchivo { get; set; } + public string ContentType { get; set; } + public long? TamanoBytes { get; set; } + public string ComentarioCliente { get; set; } + public string ComentarioAdministrativo { get; set; } + public bool? EsAprobado { get; set; } + public int? CveUsuarioRevisa { get; set; } + public DateTime? FechaRevision { get; set; } + public bool EsNuevo { get; set; } + public string NombreArchivoAnterior { get; set; } + public string NombreArchivoFisicoAnterior { get; set; } + public string RutaArchivoAnterior { get; set; } + public string ContentTypeAnterior { get; set; } + public long? TamanoBytesAnterior { get; set; } + public string ComentarioClienteAnterior { get; set; } + public string ComentarioAdministrativoAnterior { get; set; } + public bool? EsAprobadoAnterior { get; set; } + public int? CveUsuarioRevisaAnterior { get; set; } + public DateTime? FechaRevisionAnterior { get; set; } + } + + public sealed class TramiteDocumentoReviewResultModel + { + public int CveDocumentoSolicitud { get; set; } + public int CveSolicitud { get; set; } + public string Folio { get; set; } + public int CveCliente { get; set; } + public int CveFraccionamiento { get; set; } + public int CveFraccionamientoEtapa { get; set; } + public string Etapa { get; set; } + public int CveTipoTramite { get; set; } + public string TipoTramite { get; set; } + public int CveTipoDocumento { get; set; } + public string TipoDocumento { get; set; } + public string NombreArchivo { get; set; } + public bool? EsAprobadoAnterior { get; set; } + public string ComentarioAdministrativoAnterior { get; set; } + public int? CveUsuarioRevisaAnterior { get; set; } + public DateTime? FechaRevisionAnterior { get; set; } + public bool? EsAprobado { get; set; } + public string ComentarioAdministrativo { get; set; } + public int? CveUsuarioRevisa { get; set; } + public DateTime? FechaRevision { get; set; } + public bool HuboCambio { get; set; } + } + + public sealed class TramiteDocumentoDeleteResultModel + { + public int CveDocumentoSolicitud { get; set; } + public int CveSolicitud { get; set; } + public string Folio { get; set; } + public int CveCliente { get; set; } + public int CveFraccionamiento { get; set; } + public int CveFraccionamientoEtapa { get; set; } + public string Etapa { get; set; } + public int CveTipoTramite { get; set; } + public string TipoTramite { get; set; } + public int CveTipoDocumento { get; set; } + public string TipoDocumento { get; set; } + public string NombreArchivo { get; set; } + public string NombreArchivoFisico { get; set; } + public string RutaArchivo { get; set; } + public string ContentType { get; set; } + public long? TamanoBytes { get; set; } + } + + public sealed class TramiteDocumentoDownloadContextModel + { + public int CveDocumentoSolicitud { get; set; } + public int CveSolicitud { get; set; } + public int CveCliente { get; set; } + public int CveFraccionamiento { get; set; } + public int CveFraccionamientoEtapa { get; set; } + public string NombreArchivo { get; set; } + public string NombreArchivoFisico { get; set; } + public string RutaArchivo { get; set; } + public string ContentType { get; set; } + } + + public sealed class TramiteDocumentoFileModel + { + public string NombreArchivo { get; set; } + public string ContentType { get; set; } + public Stream Content { get; set; } + } + + public sealed class TramiteDocumentoPreparedFile + { + public string NombreArchivoFisico { get; set; } + public string RutaArchivo { get; set; } + public string TemporaryPath { get; set; } + public string FinalPath { get; set; } + public string BackupPath { get; set; } + public string PreviousPath { get; set; } + public bool Promoted { get; set; } + } +} diff --git a/SIG.Web/App_Code/Models/Tramites/TramiteSolicitudModels.cs b/SIG.Web/App_Code/Models/Tramites/TramiteSolicitudModels.cs new file mode 100644 index 0000000..abe592e --- /dev/null +++ b/SIG.Web/App_Code/Models/Tramites/TramiteSolicitudModels.cs @@ -0,0 +1,53 @@ +using System; + +namespace SIG.Web.AppCode.Models.Tramites +{ + public sealed class TramiteSolicitudModel + { + public int CveSolicitud { get; set; } + public string Folio { get; set; } + public int CveCliente { get; set; } + public int CveFraccionamiento { get; set; } + public int CveFraccionamientoEtapa { get; set; } + public string Etapa { get; set; } + public int CveTipoTramite { get; set; } + public string TipoTramite { get; set; } + public int CveEstatusSolicitud { get; set; } + public string EstatusSolicitud { get; set; } + public bool EsEstatusCliente { get; set; } + public DateTime? FechaInicio { get; set; } + public DateTime? FechaAutorizacion { get; set; } + public bool PuedeCambiarEstatus { get; set; } + public bool EsAutorizada { get; set; } + } + + public sealed class TramiteSolicitudEstatusOptionModel + { + public int CveEstatusSolicitud { get; set; } + public string Codigo { get; set; } + public string EstatusSolicitud { get; set; } + public int Orden { get; set; } + public bool EsFinal { get; set; } + public bool EsCliente { get; set; } + } + + public sealed class TramiteSolicitudStatusChangeModel + { + public int CveSolicitud { get; set; } + public string Folio { get; set; } + public int CveCliente { get; set; } + public int CveFraccionamiento { get; set; } + public int CveFraccionamientoEtapa { get; set; } + public string Etapa { get; set; } + public int CveTipoTramite { get; set; } + public string TipoTramite { get; set; } + public int CveEstatusSolicitudAnterior { get; set; } + public string EstatusSolicitudAnterior { get; set; } + public bool EsClienteAnterior { get; set; } + public int CveEstatusSolicitud { get; set; } + public string EstatusSolicitud { get; set; } + public bool EsCliente { get; set; } + public DateTime? FechaAutorizacion { get; set; } + public bool HuboCambio { get; set; } + } +} diff --git a/SIG.Web/App_Code/Repositories/Documentos/TramiteDocumentoRepository.cs b/SIG.Web/App_Code/Repositories/Documentos/TramiteDocumentoRepository.cs new file mode 100644 index 0000000..5efc28f --- /dev/null +++ b/SIG.Web/App_Code/Repositories/Documentos/TramiteDocumentoRepository.cs @@ -0,0 +1,198 @@ +using System.Collections.Generic; +using System.Data; +using SIG.Web.AppCode.Contracts.Database; +using SIG.Web.AppCode.Contracts.Documentos; +using SIG.Web.AppCode.Models.Common; +using SIG.Web.AppCode.Models.Documentos; +using SIG.Web.AppCode.Repositories.Database; + +namespace SIG.Web.AppCode.Repositories.Documentos +{ + public sealed class TramiteDocumentoRepository + : RepositoryBase, ITramiteDocumentoRepository + { + public TramiteDocumentoRepository(IDbExecutor db) + : base(db) + { + } + + public DbResult> ListForDevelopment( + int developmentId, + int? stageId, + int? procedureTypeId, + int executingUser, + string domainName) + { + return QueryList( + "dbo.PA_Tramites_DocumentoSolicitudFraccionamientoList", + executingUser, + domainName, + command => + { + StoredProcedureCommand.AddInput( + command, + "@CveFraccionamiento", + developmentId, + DbType.Int32); + StoredProcedureCommand.AddInput( + command, + "@CveFraccionamientoEtapa", + stageId, + DbType.Int32); + StoredProcedureCommand.AddInput( + command, + "@CveTipoTramite", + procedureTypeId, + DbType.Int32); + }); + } + + public DbResult GetUploadContext( + int developmentId, + int requestId, + int documentTypeId, + int executingUser, + string domainName) + { + return QueryOne( + "dbo.PA_Tramites_DocumentoSolicitudUploadContextGet", + executingUser, + domainName, + command => + { + StoredProcedureCommand.AddInput( + command, + "@CveFraccionamiento", + developmentId, + DbType.Int32); + StoredProcedureCommand.AddInput( + command, + "@CveSolicitud", + requestId, + DbType.Int32); + StoredProcedureCommand.AddInput( + command, + "@CveTipoDocumento", + documentTypeId, + DbType.Int32); + }); + } + + public DbResult GetStageUploadContext( + int stageId, + int procedureTypeId, + int documentTypeId, + int executingUser, + string domainName) + { + return QueryOne( + "dbo.PA_Tramites_DocumentoSolicitudStageContextGet", + executingUser, + domainName, + command => + { + StoredProcedureCommand.AddInput( + command, + "@CveFraccionamientoEtapa", + stageId, + DbType.Int32); + StoredProcedureCommand.AddInput( + command, + "@CveTipoTramite", + procedureTypeId, + DbType.Int32); + StoredProcedureCommand.AddInput( + command, + "@CveTipoDocumento", + documentTypeId, + DbType.Int32); + }); + } + + public DbResult SaveUpload( + int developmentId, + int requestId, + int documentTypeId, + string originalFileName, + string physicalFileName, + string relativePath, + string contentType, + long contentLength, + string clientComment, + int executingUser, + string domainName) + { + return QueryOne( + "dbo.PA_Tramites_DocumentoSolicitudSave", + executingUser, + domainName, + command => + { + StoredProcedureCommand.AddInput(command, "@CveFraccionamiento", developmentId, DbType.Int32); + StoredProcedureCommand.AddInput(command, "@CveSolicitud", requestId, DbType.Int32); + StoredProcedureCommand.AddInput(command, "@CveTipoDocumento", documentTypeId, DbType.Int32); + StoredProcedureCommand.AddInput(command, "@NombreArchivo", originalFileName, DbType.String); + StoredProcedureCommand.AddInput(command, "@NombreArchivoFisico", physicalFileName, DbType.String); + StoredProcedureCommand.AddInput(command, "@RutaArchivo", relativePath, DbType.String); + StoredProcedureCommand.AddInput(command, "@ContentType", contentType, DbType.String); + StoredProcedureCommand.AddInput(command, "@TamanoBytes", contentLength, DbType.Int64); + StoredProcedureCommand.AddInput(command, "@ComentarioCliente", clientComment, DbType.String); + }); + } + + public DbResult Review( + int developmentId, + int documentRequestId, + bool approved, + string note, + int executingUser, + string domainName) + { + return QueryOne( + "dbo.PA_Tramites_DocumentoSolicitudReview", + executingUser, + domainName, + command => + { + StoredProcedureCommand.AddInput(command, "@CveFraccionamiento", developmentId, DbType.Int32); + StoredProcedureCommand.AddInput(command, "@CveDocumentoSolicitud", documentRequestId, DbType.Int32); + StoredProcedureCommand.AddInput(command, "@Aprobar", approved, DbType.Boolean); + StoredProcedureCommand.AddInput(command, "@Nota", note, DbType.String); + }); + } + + public DbResult GetDownloadContext( + int developmentId, + int documentRequestId, + int executingUser, + string domainName) + { + return QueryOne( + "dbo.PA_Tramites_DocumentoSolicitudDownloadGet", + executingUser, + domainName, + command => + { + StoredProcedureCommand.AddInput(command, "@CveFraccionamiento", developmentId, DbType.Int32); + StoredProcedureCommand.AddInput(command, "@CveDocumentoSolicitud", documentRequestId, DbType.Int32); + }); + } + + public DbResult Delete( + int developmentId, + int documentRequestId, + int executingUser, + string domainName) + { + return QueryOne( + "dbo.PA_Tramites_DocumentoSolicitudDelete", + executingUser, + domainName, + command => + { + StoredProcedureCommand.AddInput(command, "@CveFraccionamiento", developmentId, DbType.Int32); + StoredProcedureCommand.AddInput(command, "@CveDocumentoSolicitud", documentRequestId, DbType.Int32); + }); + } + } +} diff --git a/SIG.Web/App_Code/Repositories/Tramites/TramiteSolicitudRepository.cs b/SIG.Web/App_Code/Repositories/Tramites/TramiteSolicitudRepository.cs index 3834001..70ef035 100644 --- a/SIG.Web/App_Code/Repositories/Tramites/TramiteSolicitudRepository.cs +++ b/SIG.Web/App_Code/Repositories/Tramites/TramiteSolicitudRepository.cs @@ -1,7 +1,9 @@ +using System.Collections.Generic; using System.Data; using SIG.Web.AppCode.Contracts.Database; using SIG.Web.AppCode.Contracts.Tramites; using SIG.Web.AppCode.Models.Common; +using SIG.Web.AppCode.Models.Tramites; using SIG.Web.AppCode.Repositories.Database; namespace SIG.Web.AppCode.Repositories.Tramites @@ -39,5 +41,94 @@ namespace SIG.Web.AppCode.Repositories.Tramites DbType.Int32); }); } + + public DbResult> ListForDevelopment( + int developmentId, + int? stageId, + int? procedureTypeId, + int? statusId, + int executingUser, + string domainName) + { + return QueryList( + "dbo.PA_Tramites_SolicitudFraccionamientoList", + executingUser, + domainName, + command => + { + StoredProcedureCommand.AddInput( + command, + "@CveFraccionamiento", + developmentId, + DbType.Int32); + StoredProcedureCommand.AddInput( + command, + "@CveFraccionamientoEtapa", + stageId, + DbType.Int32); + StoredProcedureCommand.AddInput( + command, + "@CveTipoTramite", + procedureTypeId, + DbType.Int32); + StoredProcedureCommand.AddInput( + command, + "@CveEstatusSolicitud", + statusId, + DbType.Int32); + }); + } + + public DbResult Get( + int requestId, + int executingUser, + string domainName) + { + return QueryOne( + "dbo.PA_Tramites_SolicitudGet", + executingUser, + domainName, + command => StoredProcedureCommand.AddInput( + command, + "@CveSolicitud", + requestId, + DbType.Int32)); + } + + public DbResult> ListStatusOptions( + int executingUser, + string domainName) + { + return QueryList( + "dbo.PA_Tramites_EstatusSolicitudOptionList", + executingUser, + domainName, + null); + } + + public DbResult ChangeStatus( + int requestId, + int statusId, + int executingUser, + string domainName) + { + return QueryOne( + "dbo.PA_Tramites_SolicitudEstatusUpdate", + executingUser, + domainName, + command => + { + StoredProcedureCommand.AddInput( + command, + "@CveSolicitud", + requestId, + DbType.Int32); + StoredProcedureCommand.AddInput( + command, + "@CveEstatusSolicitud", + statusId, + DbType.Int32); + }); + } } } diff --git a/SIG.Web/App_Code/Services/Documentos/TramiteDocumentoBitacoraFactory.cs b/SIG.Web/App_Code/Services/Documentos/TramiteDocumentoBitacoraFactory.cs new file mode 100644 index 0000000..137a7e5 --- /dev/null +++ b/SIG.Web/App_Code/Services/Documentos/TramiteDocumentoBitacoraFactory.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +using SIG.Web.AppCode.Models.Bitacora; +using SIG.Web.AppCode.Models.Documentos; + +namespace SIG.Web.AppCode.Services.Documentos +{ + internal static class TramiteDocumentoBitacoraFactory + { + public static BitacoraEventoModel Upload( + TramiteDocumentoSaveResultModel change) + { + BitacoraTipoMovimiento movement = change.EsNuevo + ? BitacoraTipoMovimiento.Agregado + : BitacoraTipoMovimiento.Editado; + + var auditEvent = BaseEvent( + change.CveDocumentoSolicitud, + change.CveSolicitud, + change.CveCliente, + change.CveFraccionamiento, + change.CveFraccionamientoEtapa, + movement, + BuildUploadDescription(change)); + + auditEvent.DiscriminarCamposSinCambios = false; + auditEvent.Campos = new List + { + Field("CveSolicitud", change.EsNuevo ? null : (object)change.CveSolicitud, change.CveSolicitud), + Field("CveTipoDocumento", change.EsNuevo ? null : (object)change.CveTipoDocumento, change.CveTipoDocumento), + Field("NombreArchivo", change.NombreArchivoAnterior, change.NombreArchivo), + Field("NombreArchivoFisico", change.NombreArchivoFisicoAnterior, change.NombreArchivoFisico), + Field("RutaArchivo", change.RutaArchivoAnterior, change.RutaArchivo), + Field("ContentType", change.ContentTypeAnterior, change.ContentType), + Field("TamanoBytes", change.TamanoBytesAnterior, change.TamanoBytes), + Field("ComentarioCliente", change.ComentarioClienteAnterior, change.ComentarioCliente), + Field("ComentarioAdministrativo", change.ComentarioAdministrativoAnterior, change.ComentarioAdministrativo), + Field("EsAprobado", change.EsAprobadoAnterior, change.EsAprobado), + Field("CveUsuarioRevisa", change.CveUsuarioRevisaAnterior, change.CveUsuarioRevisa), + Field("FechaRevision", change.FechaRevisionAnterior, change.FechaRevision) + }; + + return auditEvent; + } + + public static BitacoraEventoModel Review( + TramiteDocumentoReviewResultModel change) + { + string action = change.EsAprobado == true + ? "aprobó" + : "rechazó"; + + var auditEvent = BaseEvent( + change.CveDocumentoSolicitud, + change.CveSolicitud, + change.CveCliente, + change.CveFraccionamiento, + change.CveFraccionamientoEtapa, + BitacoraTipoMovimiento.Editado, + "Se " + action + " el documento " + + change.TipoDocumento + " del trámite " + + change.TipoTramite + " de la etapa " + + change.Etapa + "."); + + auditEvent.Campos = new List + { + Field("EsAprobado", change.EsAprobadoAnterior, change.EsAprobado), + Field("ComentarioAdministrativo", change.ComentarioAdministrativoAnterior, change.ComentarioAdministrativo), + Field("CveUsuarioRevisa", change.CveUsuarioRevisaAnterior, change.CveUsuarioRevisa), + Field("FechaRevision", change.FechaRevisionAnterior, change.FechaRevision) + }; + + if (change.EsAprobado == true && change.FechaRevision.HasValue) + { + auditEvent.CveUsuarioAutoriza = change.CveUsuarioRevisa; + auditEvent.FechaAutorizacion = change.FechaRevision; + } + + return auditEvent; + } + + public static BitacoraEventoModel Delete( + TramiteDocumentoDeleteResultModel change) + { + return BaseEvent( + change.CveDocumentoSolicitud, + change.CveSolicitud, + change.CveCliente, + change.CveFraccionamiento, + change.CveFraccionamientoEtapa, + BitacoraTipoMovimiento.Eliminado, + "Se eliminó el registro del documento " + + change.TipoDocumento + " del trámite " + + change.TipoTramite + " de la etapa " + + change.Etapa + "."); + } + + private static string BuildUploadDescription( + TramiteDocumentoSaveResultModel change) + { + if (change.EsNuevo) + { + return "Se agregó el documento " + change.TipoDocumento + + " al trámite " + change.TipoTramite + + " de la etapa " + change.Etapa + "."; + } + + bool teniaRevision = + change.EsAprobadoAnterior.HasValue || + change.FechaRevisionAnterior.HasValue || + !string.IsNullOrWhiteSpace( + change.ComentarioAdministrativoAnterior); + + return "Se actualizó el documento " + change.TipoDocumento + + " del trámite " + change.TipoTramite + + " de la etapa " + change.Etapa + + (teniaRevision + ? ". La revisión anterior se eliminó y el documento quedó pendiente nuevamente." + : ". El documento quedó pendiente de revisión."); + } + + private static BitacoraEventoModel BaseEvent( + int documentRequestId, + int requestId, + int clientId, + int developmentId, + int stageId, + BitacoraTipoMovimiento movement, + string description) + { + var auditEvent = new BitacoraEventoModel + { + NombreTablaSQL = "Tra_DocumentosSolicitud", + TipoMovimiento = movement, + LlaveRegistro = Convert.ToString(documentRequestId), + DescripcionMovimiento = description, + DiscriminarCamposSinCambios = true + }; + + auditEvent.Relaciones.Add( + Relation( + BitacoraTipoRelacion.Directa, + "Tra_DocumentosSolicitud", + "CveDocumentoSolicitud", + documentRequestId, + "Documento de la solicitud.")); + auditEvent.Relaciones.Add( + Relation( + BitacoraTipoRelacion.Relacionada, + "Tra_Solicitudes", + "CveSolicitud", + requestId, + "Solicitud del documento.")); + auditEvent.Relaciones.Add( + Relation( + BitacoraTipoRelacion.Relacionada, + "Clientes", + "CveCliente", + clientId, + "Cliente propietario de la solicitud.")); + auditEvent.Relaciones.Add( + Relation( + BitacoraTipoRelacion.Relacionada, + "Fraccionamientos", + "CveFraccionamiento", + developmentId, + "Fraccionamiento de la solicitud.")); + auditEvent.Relaciones.Add( + Relation( + BitacoraTipoRelacion.Relacionada, + "FraccionamientosEtapas", + "CveFraccionamientoEtapa", + stageId, + "Etapa de la solicitud.")); + + return auditEvent; + } + + private static BitacoraCampoEventoModel Field( + string name, + object previous, + object current) + { + return new BitacoraCampoEventoModel + { + NombreCampoSQL = name, + ValorAnterior = previous, + ValorNuevo = current + }; + } + + private static BitacoraRelacionEventoModel Relation( + BitacoraTipoRelacion relationType, + string tableName, + string fieldName, + object value, + string description) + { + return new BitacoraRelacionEventoModel + { + TipoRelacion = relationType, + NombreTablaSQL = tableName, + NombreCampoSQL = fieldName, + Valor = value, + VisibleDesarrollador = true, + DescripcionRelacion = description + }; + } + } +} diff --git a/SIG.Web/App_Code/Services/Documentos/TramiteDocumentoFileStore.cs b/SIG.Web/App_Code/Services/Documentos/TramiteDocumentoFileStore.cs new file mode 100644 index 0000000..390c1c3 --- /dev/null +++ b/SIG.Web/App_Code/Services/Documentos/TramiteDocumentoFileStore.cs @@ -0,0 +1,277 @@ +using System; +using System.Globalization; +using System.IO; +using System.Web.Hosting; +using SIG.Web.AppCode.Contracts.Documentos; +using SIG.Web.AppCode.Models.Documentos; + +namespace SIG.Web.AppCode.Services.Documentos +{ + public sealed class TramiteDocumentoFileStore + : ITramiteDocumentoFileStore + { + public TramiteDocumentoPreparedFile Prepare( + TramiteDocumentoUploadContextModel context, + Stream source, + string previousRelativePath) + { + if (context == null) + { + throw new ArgumentNullException("context"); + } + + if (source == null) + { + throw new ArgumentNullException("source"); + } + + string root = GetDocumentsRoot(); + string relativeDirectory = Path.Combine( + "Clientes", + context.CveCliente.ToString(CultureInfo.InvariantCulture), + "Solicitudes", + context.CveSolicitud.ToString(CultureInfo.InvariantCulture)); + string directory = Path.Combine(root, relativeDirectory); + + Directory.CreateDirectory(directory); + + string physicalFileName = string.Format( + CultureInfo.InvariantCulture, + "doc-{0}-{1}.bin", + context.CveSolicitud, + context.CveTipoDocumento); + string relativePath = Path.Combine( + relativeDirectory, + physicalFileName); + string finalPath = SafeResolve(relativePath); + string temporaryPath = Path.Combine( + directory, + "." + physicalFileName + "." + + Guid.NewGuid().ToString("N") + ".tmp"); + + if (source.CanSeek) + { + source.Position = 0; + } + + using (var target = new FileStream( + temporaryPath, + FileMode.CreateNew, + FileAccess.Write, + FileShare.None)) + { + source.CopyTo(target); + target.Flush(); + } + + return new TramiteDocumentoPreparedFile + { + NombreArchivoFisico = physicalFileName, + RutaArchivo = relativePath.Replace('\\', '/'), + TemporaryPath = temporaryPath, + FinalPath = finalPath, + PreviousPath = ResolveExistingPath(previousRelativePath), + Promoted = false + }; + } + + public void Promote(TramiteDocumentoPreparedFile preparedFile) + { + ValidatePrepared(preparedFile); + + if (!File.Exists(preparedFile.TemporaryPath)) + { + throw new FileNotFoundException( + "No se encontró el archivo temporal preparado.", + preparedFile.TemporaryPath); + } + + string finalDirectory = Path.GetDirectoryName( + preparedFile.FinalPath); + Directory.CreateDirectory(finalDirectory); + + if (File.Exists(preparedFile.FinalPath)) + { + preparedFile.BackupPath = preparedFile.FinalPath + "." + + Guid.NewGuid().ToString("N") + ".bak"; + File.Move( + preparedFile.FinalPath, + preparedFile.BackupPath); + } + + try + { + File.Move( + preparedFile.TemporaryPath, + preparedFile.FinalPath); + preparedFile.Promoted = true; + } + catch + { + if (!string.IsNullOrWhiteSpace(preparedFile.BackupPath) && + File.Exists(preparedFile.BackupPath) && + !File.Exists(preparedFile.FinalPath)) + { + File.Move( + preparedFile.BackupPath, + preparedFile.FinalPath); + } + + preparedFile.BackupPath = null; + throw; + } + } + + public void Rollback(TramiteDocumentoPreparedFile preparedFile) + { + if (preparedFile == null) + { + return; + } + + if (preparedFile.Promoted && + !string.IsNullOrWhiteSpace(preparedFile.FinalPath) && + File.Exists(preparedFile.FinalPath)) + { + File.Delete(preparedFile.FinalPath); + } + + if (!string.IsNullOrWhiteSpace(preparedFile.BackupPath) && + File.Exists(preparedFile.BackupPath)) + { + File.Move( + preparedFile.BackupPath, + preparedFile.FinalPath); + } + + if (!string.IsNullOrWhiteSpace(preparedFile.TemporaryPath) && + File.Exists(preparedFile.TemporaryPath)) + { + File.Delete(preparedFile.TemporaryPath); + } + + preparedFile.Promoted = false; + } + + public void Complete(TramiteDocumentoPreparedFile preparedFile) + { + if (preparedFile == null) + { + return; + } + + if (!string.IsNullOrWhiteSpace(preparedFile.BackupPath) && + File.Exists(preparedFile.BackupPath)) + { + File.Delete(preparedFile.BackupPath); + } + + if (!string.IsNullOrWhiteSpace(preparedFile.TemporaryPath) && + File.Exists(preparedFile.TemporaryPath)) + { + File.Delete(preparedFile.TemporaryPath); + } + + if (!string.IsNullOrWhiteSpace(preparedFile.PreviousPath) && + !PathsEqual(preparedFile.PreviousPath, preparedFile.FinalPath) && + File.Exists(preparedFile.PreviousPath)) + { + File.Delete(preparedFile.PreviousPath); + } + } + + public Stream OpenRead(string relativePath) + { + string path = SafeResolve(relativePath); + + return new FileStream( + path, + FileMode.Open, + FileAccess.Read, + FileShare.Read); + } + + private static string GetDocumentsRoot() + { + string root = HostingEnvironment.MapPath("~/Documentos/"); + + if (string.IsNullOrWhiteSpace(root)) + { + throw new InvalidOperationException( + "No fue posible resolver el directorio Documentos."); + } + + Directory.CreateDirectory(root); + return Path.GetFullPath(root); + } + + private static string SafeResolve(string storedPath) + { + if (string.IsNullOrWhiteSpace(storedPath)) + { + throw new InvalidOperationException( + "La ruta del documento está vacía."); + } + + string root = GetDocumentsRoot(); + string path = Path.IsPathRooted(storedPath) + ? Path.GetFullPath(storedPath) + : Path.GetFullPath( + Path.Combine( + root, + storedPath.Replace('/', Path.DirectorySeparatorChar))); + + string rootPrefix = root.TrimEnd( + Path.DirectorySeparatorChar, + Path.AltDirectorySeparatorChar) + + Path.DirectorySeparatorChar; + + if (!path.StartsWith( + rootPrefix, + StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException( + "La ruta del documento está fuera del directorio permitido."); + } + + return path; + } + + private static string ResolveExistingPath(string storedPath) + { + if (string.IsNullOrWhiteSpace(storedPath)) + { + return null; + } + + try + { + return SafeResolve(storedPath); + } + catch + { + return null; + } + } + + private static bool PathsEqual(string left, string right) + { + return string.Equals( + Path.GetFullPath(left ?? string.Empty), + Path.GetFullPath(right ?? string.Empty), + StringComparison.OrdinalIgnoreCase); + } + + private static void ValidatePrepared( + TramiteDocumentoPreparedFile preparedFile) + { + if (preparedFile == null || + string.IsNullOrWhiteSpace(preparedFile.TemporaryPath) || + string.IsNullOrWhiteSpace(preparedFile.FinalPath)) + { + throw new InvalidOperationException( + "El archivo preparado no contiene rutas válidas."); + } + } + } +} diff --git a/SIG.Web/App_Code/Services/Documentos/TramiteDocumentoService.cs b/SIG.Web/App_Code/Services/Documentos/TramiteDocumentoService.cs new file mode 100644 index 0000000..89beab4 --- /dev/null +++ b/SIG.Web/App_Code/Services/Documentos/TramiteDocumentoService.cs @@ -0,0 +1,577 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using SIG.Web.AppCode.Contracts.Bitacora; +using SIG.Web.AppCode.Contracts.Documentos; +using SIG.Web.AppCode.Infrastructure.Transactions; +using SIG.Web.AppCode.Models.Bitacora; +using SIG.Web.AppCode.Models.Common; +using SIG.Web.AppCode.Models.Documentos; +using SIG.Web.AppCode.Services.Logging; + +namespace SIG.Web.AppCode.Services.Documentos +{ + public sealed class TramiteDocumentoService + : ITramiteDocumentoService + { + private const int GisDocumentTypeId = 2; + + private readonly ITramiteDocumentoRepository _repository; + private readonly ITramiteDocumentoFileStore _fileStore; + private readonly IBitacoraService _bitacoraService; + + public TramiteDocumentoService( + ITramiteDocumentoRepository repository, + ITramiteDocumentoFileStore fileStore, + IBitacoraService bitacoraService) + { + if (repository == null) + { + throw new ArgumentNullException("repository"); + } + + if (fileStore == null) + { + throw new ArgumentNullException("fileStore"); + } + + if (bitacoraService == null) + { + throw new ArgumentNullException("bitacoraService"); + } + + _repository = repository; + _fileStore = fileStore; + _bitacoraService = bitacoraService; + } + + public DbResult> ListForDevelopment( + int developmentId, + int? stageId, + int? procedureTypeId, + int executingUser, + string domainName) + { + if (developmentId <= 0) + { + return Fail>( + "Seleccione un fraccionamiento válido."); + } + + return _repository.ListForDevelopment( + developmentId, + NormalizeId(stageId), + NormalizeId(procedureTypeId), + executingUser, + domainName); + } + + public DbResult GetUploadContext( + int developmentId, + int requestId, + int documentTypeId, + int executingUser, + string domainName) + { + if (developmentId <= 0 || requestId <= 0 || documentTypeId <= 0) + { + return Fail( + "Seleccione un documento válido."); + } + + if (documentTypeId == GisDocumentTypeId) + { + return Fail( + "El archivo GIS se administra desde la opción Administrar GIS de la etapa."); + } + + return _repository.GetUploadContext( + developmentId, + requestId, + documentTypeId, + executingUser, + domainName); + } + + public DbResult Upload( + int developmentId, + int requestId, + int documentTypeId, + string originalFileName, + string contentType, + Stream content, + long contentLength, + string clientComment, + int executingUser, + string domainName) + { + if (content == null || contentLength <= 0) + { + return Fail( + "Seleccione un archivo válido."); + } + + DbResult contextResult = + GetUploadContext( + developmentId, + requestId, + documentTypeId, + executingUser, + domainName); + + if (!contextResult.Success || contextResult.Data == null) + { + return CopyFailure( + contextResult, + "No fue posible obtener el contexto del documento."); + } + + TramiteDocumentoUploadContextModel uploadContext = + contextResult.Data; + + string validation = ValidateFile( + uploadContext, + originalFileName, + contentLength); + + if (!string.IsNullOrWhiteSpace(validation)) + { + return Fail(validation); + } + + TramiteDocumentoPreparedFile preparedFile = null; + + try + { + preparedFile = _fileStore.Prepare( + uploadContext, + content, + uploadContext.RutaArchivoAnterior); + + DbResult saveResult; + + using (var transaction = new TransactionLogic()) + { + saveResult = _repository.SaveUpload( + developmentId, + requestId, + documentTypeId, + Path.GetFileName(originalFileName), + preparedFile.NombreArchivoFisico, + preparedFile.RutaArchivo, + NormalizeContentType(contentType), + contentLength, + TrimOrNull(clientComment), + executingUser, + domainName); + + if (!saveResult.Success || saveResult.Data == null) + { + _fileStore.Rollback(preparedFile); + return saveResult; + } + + DbResult auditResult = + _bitacoraService.Registrar( + TramiteDocumentoBitacoraFactory.Upload( + saveResult.Data)); + + if (!auditResult.Success) + { + _fileStore.Rollback(preparedFile); + return DbResult.Fail( + auditResult.UserMessage, + auditResult.TechnicalMessage, + auditResult.Exception); + } + + _fileStore.Promote(preparedFile); + transaction.Commit(); + } + + _fileStore.Complete(preparedFile); + return saveResult; + } + catch (Exception ex) + { + try + { + _fileStore.Rollback(preparedFile); + } + catch (Exception rollbackException) + { + ExceptionLogger.Log( + rollbackException, + "TramiteDocumentoService.Upload.Rollback", + "No fue posible restaurar el archivo después de un error."); + } + + const string message = + "No fue posible guardar el documento; favor de contactar a Sistemas."; + + ExceptionLogger.Log( + ex, + "TramiteDocumentoService.Upload", + message); + + return DbResult.Fail( + message, + ex.Message, + ex); + } + } + + public DbResult Review( + int developmentId, + int documentRequestId, + bool approved, + string note, + int executingUser, + string domainName) + { + if (developmentId <= 0 || documentRequestId <= 0) + { + return Fail( + "Seleccione un documento válido."); + } + + note = TrimOrNull(note); + + if (!approved && string.IsNullOrWhiteSpace(note)) + { + return Fail( + "Capture la nota o motivo del rechazo."); + } + + using (var transaction = new TransactionLogic()) + { + DbResult result = + _repository.Review( + developmentId, + documentRequestId, + approved, + note, + executingUser, + domainName); + + if (!result.Success || result.Data == null) + { + return result; + } + + if (result.Data.HuboCambio) + { + DbResult auditResult = + _bitacoraService.Registrar( + TramiteDocumentoBitacoraFactory.Review( + result.Data)); + + if (!auditResult.Success) + { + return DbResult.Fail( + auditResult.UserMessage, + auditResult.TechnicalMessage, + auditResult.Exception); + } + } + + transaction.Commit(); + return result; + } + } + + public DbResult Open( + int developmentId, + int documentRequestId, + int executingUser, + string domainName) + { + DbResult result = + _repository.GetDownloadContext( + developmentId, + documentRequestId, + executingUser, + domainName); + + if (!result.Success || result.Data == null) + { + return CopyFailure( + result, + "No fue posible obtener el documento."); + } + + try + { + return DbResult.Ok( + new TramiteDocumentoFileModel + { + NombreArchivo = result.Data.NombreArchivo, + ContentType = NormalizeContentType( + result.Data.ContentType), + Content = _fileStore.OpenRead( + result.Data.RutaArchivo) + }); + } + catch (Exception ex) + { + const string message = + "No fue posible abrir el documento; favor de contactar a Sistemas."; + ExceptionLogger.Log( + ex, + "TramiteDocumentoService.Open", + message); + return DbResult.Fail( + message, + ex.Message, + ex); + } + } + + public DbResult RegisterExternalStageFile( + int stageId, + int procedureTypeId, + int documentTypeId, + string originalFileName, + string physicalFileName, + string relativePath, + string contentType, + long contentLength, + int executingUser, + string domainName) + { + DbResult contextResult = + _repository.GetStageUploadContext( + stageId, + procedureTypeId, + documentTypeId, + executingUser, + domainName); + + if (!contextResult.Success || contextResult.Data == null) + { + return CopyFailure( + contextResult, + "No fue posible obtener la solicitud asociada a la etapa."); + } + + TramiteDocumentoUploadContextModel uploadContext = + contextResult.Data; + + using (var transaction = new TransactionLogic()) + { + DbResult result = + _repository.SaveUpload( + uploadContext.CveFraccionamiento, + uploadContext.CveSolicitud, + documentTypeId, + Path.GetFileName(originalFileName), + physicalFileName, + relativePath, + NormalizeContentType(contentType), + contentLength, + null, + executingUser, + domainName); + + if (!result.Success || result.Data == null) + { + return result; + } + + DbResult auditResult = + _bitacoraService.Registrar( + TramiteDocumentoBitacoraFactory.Upload( + result.Data)); + + if (!auditResult.Success) + { + return DbResult.Fail( + auditResult.UserMessage, + auditResult.TechnicalMessage, + auditResult.Exception); + } + + transaction.Commit(); + return result; + } + } + + public DbResult RemoveExternalStageFile( + int stageId, + int procedureTypeId, + int documentTypeId, + int executingUser, + string domainName) + { + DbResult contextResult = + _repository.GetStageUploadContext( + stageId, + procedureTypeId, + documentTypeId, + executingUser, + domainName); + + if (!contextResult.Success || contextResult.Data == null) + { + return CopyFailure( + contextResult, + "No fue posible obtener la solicitud asociada a la etapa."); + } + + if (contextResult.Data.CveDocumentoSolicitud <= 0) + { + return DbResult.Ok(true); + } + + using (var transaction = new TransactionLogic()) + { + DbResult deleteResult = + _repository.Delete( + contextResult.Data.CveFraccionamiento, + contextResult.Data.CveDocumentoSolicitud, + executingUser, + domainName); + + if (!deleteResult.Success || deleteResult.Data == null) + { + return DbResult.Fail( + deleteResult.UserMessage, + deleteResult.TechnicalMessage, + deleteResult.Exception); + } + + DbResult auditResult = + _bitacoraService.Registrar( + TramiteDocumentoBitacoraFactory.Delete( + deleteResult.Data)); + + if (!auditResult.Success) + { + return DbResult.Fail( + auditResult.UserMessage, + auditResult.TechnicalMessage, + auditResult.Exception); + } + + transaction.Commit(); + return DbResult.Ok(true); + } + } + + private static string ValidateFile( + TramiteDocumentoUploadContextModel context, + string originalFileName, + long contentLength) + { + string extension = Path.GetExtension( + originalFileName ?? string.Empty); + + if (string.IsNullOrWhiteSpace(extension)) + { + return "El archivo seleccionado no tiene una extensión válida."; + } + + if (!IsExtensionAllowed( + extension, + context.ExtensionesPermitidas)) + { + return string.IsNullOrWhiteSpace( + context.ExtensionesPermitidas) + ? "La extensión del archivo no está permitida." + : "La extensión del archivo no está permitida. Use: " + + context.ExtensionesPermitidas + "."; + } + + if (context.TamanoMaximoMb.HasValue && + context.TamanoMaximoMb.Value > 0) + { + long maxBytes = + (long)context.TamanoMaximoMb.Value * + 1024L * 1024L; + + if (contentLength > maxBytes) + { + return "El archivo no puede exceder " + + context.TamanoMaximoMb.Value + " MB."; + } + } + + return null; + } + + private static bool IsExtensionAllowed( + string extension, + string allowed) + { + if (string.IsNullOrWhiteSpace(allowed)) + { + return true; + } + + string normalized = extension.Trim(); + if (!normalized.StartsWith(".", StringComparison.Ordinal)) + { + normalized = "." + normalized; + } + + return allowed + .Split(new[] { ',', ';', '|', ' ' }, + StringSplitOptions.RemoveEmptyEntries) + .Select(item => item.Trim()) + .Select(item => item.StartsWith(".", StringComparison.Ordinal) + ? item + : "." + item) + .Any(item => string.Equals( + item, + normalized, + StringComparison.OrdinalIgnoreCase)); + } + + private static int? NormalizeId(int? value) + { + return value.HasValue && value.Value > 0 + ? value + : null; + } + + private static string NormalizeContentType(string contentType) + { + return string.IsNullOrWhiteSpace(contentType) + ? "application/octet-stream" + : contentType.Trim(); + } + + private static string TrimOrNull(string value) + { + return string.IsNullOrWhiteSpace(value) + ? null + : value.Trim(); + } + + private static DbResult Fail(string message) + { + return DbResult.Fail( + message, + string.Empty, + null); + } + + private static DbResult CopyFailure( + DbResult source, + string fallback) + { + if (source == null) + { + return Fail(fallback); + } + + return DbResult.Fail( + string.IsNullOrWhiteSpace(source.UserMessage) + ? fallback + : source.UserMessage, + source.TechnicalMessage, + source.Exception); + } + } +} diff --git a/SIG.Web/App_Code/Services/Fraccionamientos/FraccionamientoGisService.cs b/SIG.Web/App_Code/Services/Fraccionamientos/FraccionamientoGisService.cs index 8411e42..5d510f3 100644 --- a/SIG.Web/App_Code/Services/Fraccionamientos/FraccionamientoGisService.cs +++ b/SIG.Web/App_Code/Services/Fraccionamientos/FraccionamientoGisService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using SIG.Web.AppCode.Contracts.Bitacora; +using SIG.Web.AppCode.Contracts.Documentos; using SIG.Web.AppCode.Contracts.Fraccionamientos; using SIG.Web.AppCode.Infrastructure.Transactions; using SIG.Web.AppCode.Models.Bitacora; @@ -21,11 +22,13 @@ namespace SIG.Web.AppCode.Services.Fraccionamientos private readonly IFraccionamientoGisRepository _repository; private readonly IFraccionamientoGisFileStore _fileStore; private readonly IBitacoraService _bitacoraService; + private readonly ITramiteDocumentoService _tramiteDocumentoService; public FraccionamientoGisService( IFraccionamientoGisRepository repository, IFraccionamientoGisFileStore fileStore, - IBitacoraService bitacoraService) + IBitacoraService bitacoraService, + ITramiteDocumentoService tramiteDocumentoService) { if (repository == null) { @@ -42,9 +45,15 @@ namespace SIG.Web.AppCode.Services.Fraccionamientos throw new ArgumentNullException("bitacoraService"); } + if (tramiteDocumentoService == null) + { + throw new ArgumentNullException("tramiteDocumentoService"); + } + _repository = repository; _fileStore = fileStore; _bitacoraService = bitacoraService; + _tramiteDocumentoService = tramiteDocumentoService; } public DbResult Get( @@ -260,6 +269,41 @@ namespace SIG.Web.AppCode.Services.Fraccionamientos { _fileStore.Save(gisContext, content); fileWasWritten = true; + + string physicalFileName = Path.GetFileName( + _fileStore.GetPhysicalPath(gisContext)); + string relativePath = string.Format( + System.Globalization.CultureInfo.InvariantCulture, + "Clientes/{0}/{1}", + gisContext.CveCliente, + physicalFileName); + + DbResult documentResult = + _tramiteDocumentoService.RegisterExternalStageFile( + stageId, + 1, + 2, + originalFileName, + physicalFileName, + relativePath, + GisContentType, + contentLength, + executingUser, + domainName); + + if (!documentResult.Success) + { + RestorePreviousFile( + gisContext, + hadFile, + previousFile); + fileWasWritten = false; + + return DbResult.Fail( + documentResult.UserMessage, + documentResult.TechnicalMessage, + documentResult.Exception); + } } bool linksChanged = !LinksEqual(previousLinks, links); @@ -428,6 +472,31 @@ namespace SIG.Web.AppCode.Services.Fraccionamientos fileWasDeleted = true; } + DbResult documentDeleteResult = + _tramiteDocumentoService.RemoveExternalStageFile( + stageId, + 1, + 2, + executingUser, + domainName); + + if (!documentDeleteResult.Success) + { + if (fileWasDeleted && previousFile != null) + { + using (var backup = new MemoryStream(previousFile, false)) + { + _fileStore.Save(gisContext, backup); + } + fileWasDeleted = false; + } + + return DbResult.Fail( + documentDeleteResult.UserMessage, + documentDeleteResult.TechnicalMessage, + documentDeleteResult.Exception); + } + DbResult auditResult = _bitacoraService.Registrar( FraccionamientoBitacoraFactory.CreateStageGisFileDelete( diff --git a/SIG.Web/App_Code/Services/Tramites/TramiteSolicitudBitacoraFactory.cs b/SIG.Web/App_Code/Services/Tramites/TramiteSolicitudBitacoraFactory.cs new file mode 100644 index 0000000..470ca32 --- /dev/null +++ b/SIG.Web/App_Code/Services/Tramites/TramiteSolicitudBitacoraFactory.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using SIG.Web.AppCode.Models.Bitacora; +using SIG.Web.AppCode.Models.Tramites; + +namespace SIG.Web.AppCode.Services.Tramites +{ + internal static class TramiteSolicitudBitacoraFactory + { + public static BitacoraEventoModel StatusChange( + TramiteSolicitudStatusChangeModel change) + { + bool visibleDeveloper = + change.EsClienteAnterior && + change.EsCliente; + + var auditEvent = new BitacoraEventoModel + { + NombreTablaSQL = "Tra_Solicitudes", + TipoMovimiento = BitacoraTipoMovimiento.Editado, + LlaveRegistro = Convert.ToString(change.CveSolicitud), + DescripcionMovimiento = + "Se cambió el estatus de la solicitud " + + change.Folio + " de " + + change.EstatusSolicitudAnterior + " a " + + change.EstatusSolicitud + ".", + DiscriminarCamposSinCambios = true, + Campos = new List + { + new BitacoraCampoEventoModel + { + NombreCampoSQL = "CveEstatusSolicitud", + ValorAnterior = change.CveEstatusSolicitudAnterior, + ValorNuevo = change.CveEstatusSolicitud + } + } + }; + + auditEvent.Relaciones.Add( + Relation( + BitacoraTipoRelacion.Directa, + "Tra_Solicitudes", + "CveSolicitud", + change.CveSolicitud, + visibleDeveloper, + "Solicitud cuyo estatus fue modificado.")); + + auditEvent.Relaciones.Add( + Relation( + BitacoraTipoRelacion.Relacionada, + "Clientes", + "CveCliente", + change.CveCliente, + visibleDeveloper, + "Cliente propietario de la solicitud.")); + + auditEvent.Relaciones.Add( + Relation( + BitacoraTipoRelacion.Relacionada, + "Fraccionamientos", + "CveFraccionamiento", + change.CveFraccionamiento, + visibleDeveloper, + "Fraccionamiento de la solicitud.")); + + auditEvent.Relaciones.Add( + Relation( + BitacoraTipoRelacion.Relacionada, + "FraccionamientosEtapas", + "CveFraccionamientoEtapa", + change.CveFraccionamientoEtapa, + visibleDeveloper, + "Etapa de la solicitud.")); + + return auditEvent; + } + + private static BitacoraRelacionEventoModel Relation( + BitacoraTipoRelacion relationType, + string tableName, + string fieldName, + object value, + bool visibleDeveloper, + string description) + { + return new BitacoraRelacionEventoModel + { + TipoRelacion = relationType, + NombreTablaSQL = tableName, + NombreCampoSQL = fieldName, + Valor = value, + VisibleDesarrollador = visibleDeveloper, + DescripcionRelacion = description + }; + } + } +} diff --git a/SIG.Web/App_Code/Services/Tramites/TramiteSolicitudService.cs b/SIG.Web/App_Code/Services/Tramites/TramiteSolicitudService.cs new file mode 100644 index 0000000..198daa3 --- /dev/null +++ b/SIG.Web/App_Code/Services/Tramites/TramiteSolicitudService.cs @@ -0,0 +1,157 @@ +using System.Collections.Generic; +using SIG.Web.AppCode.Contracts.Bitacora; +using SIG.Web.AppCode.Contracts.Tramites; +using SIG.Web.AppCode.Infrastructure.Transactions; +using SIG.Web.AppCode.Models.Bitacora; +using SIG.Web.AppCode.Models.Common; +using SIG.Web.AppCode.Models.Tramites; + +namespace SIG.Web.AppCode.Services.Tramites +{ + public sealed class TramiteSolicitudService + : ITramiteSolicitudService + { + private readonly ITramiteSolicitudRepository _repository; + private readonly IBitacoraService _bitacoraService; + + public TramiteSolicitudService( + ITramiteSolicitudRepository repository, + IBitacoraService bitacoraService) + { + _repository = repository; + _bitacoraService = bitacoraService; + } + + public DbResult> ListForDevelopment( + int developmentId, + int? stageId, + int? procedureTypeId, + int? statusId, + int executingUser, + string domainName) + { + if (developmentId <= 0) + { + return Fail>( + "Seleccione un fraccionamiento válido."); + } + + return _repository.ListForDevelopment( + developmentId, + NormalizeId(stageId), + NormalizeId(procedureTypeId), + NormalizeId(statusId), + executingUser, + domainName); + } + + public DbResult Get( + int requestId, + int executingUser, + string domainName) + { + if (requestId <= 0) + { + return Fail( + "Seleccione una solicitud válida."); + } + + return _repository.Get( + requestId, + executingUser, + domainName); + } + + public DbResult> ListStatusOptions( + int executingUser, + string domainName) + { + return _repository.ListStatusOptions( + executingUser, + domainName); + } + + public DbResult ChangeStatus( + int requestId, + int statusId, + int executingUser, + string domainName) + { + if (requestId <= 0) + { + return Fail( + "Seleccione una solicitud válida."); + } + + if (statusId <= 0) + { + return Fail( + "Seleccione un estatus válido."); + } + + using (var transaction = new TransactionLogic()) + { + DbResult result = + _repository.ChangeStatus( + requestId, + statusId, + executingUser, + domainName); + + if (!result.Success || result.Data == null) + { + return result; + } + + if (result.Data.HuboCambio) + { + if (_bitacoraService == null) + { + return Fail( + "El servicio de bitácora no está configurado."); + } + + BitacoraEventoModel auditEvent = + TramiteSolicitudBitacoraFactory.StatusChange( + result.Data); + + if (result.Data.FechaAutorizacion.HasValue) + { + auditEvent.CveUsuarioAutoriza = executingUser; + auditEvent.FechaAutorizacion = + result.Data.FechaAutorizacion; + } + + DbResult auditResult = + _bitacoraService.Registrar(auditEvent); + + if (!auditResult.Success) + { + return DbResult.Fail( + auditResult.UserMessage, + auditResult.TechnicalMessage, + auditResult.Exception); + } + } + + transaction.Commit(); + return result; + } + } + + private static int? NormalizeId(int? value) + { + return value.HasValue && value.Value > 0 + ? value + : null; + } + + private static DbResult Fail(string message) + { + return DbResult.Fail( + message, + string.Empty, + null); + } + } +} diff --git a/SIG.Web/App_Code/SigControls/FiltroActivo.cs b/SIG.Web/App_Code/SigControls/FiltroActivo.cs index 5b5bf92..ca2ff39 100644 --- a/SIG.Web/App_Code/SigControls/FiltroActivo.cs +++ b/SIG.Web/App_Code/SigControls/FiltroActivo.cs @@ -8,6 +8,7 @@ namespace SIG.Web.AppCode.SigControls public string Nombre { get; set; } public string Titulo { get; set; } public string Descripcion { get; set; } + public string Valor { get; set; } public int Orden { get; set; } public bool Visible { get; set; } = true; } diff --git a/SIG.Web/Pages/Fraccionamientos/FraccionamientoDetalle.aspx b/SIG.Web/Pages/Fraccionamientos/FraccionamientoDetalle.aspx index 51454dc..b5d8edf 100644 --- a/SIG.Web/Pages/Fraccionamientos/FraccionamientoDetalle.aspx +++ b/SIG.Web/Pages/Fraccionamientos/FraccionamientoDetalle.aspx @@ -324,46 +324,7 @@ -
-
-

Últimos trámites

- - - Agregar trámite - -
- -
- - - - - - - - - -
- - - - No hay trámites registrados para las etapas. -
-
-
-
-
@@ -590,10 +551,85 @@ -
- -

Los trámites recientes se muestran en la pestaña Etapas.

-
+
+
+

Trámites del fraccionamiento

+
+ + + +
+ + + + + + + + + + + <%#: GetProcedureStatusText(Eval("EstatusSolicitud")) %> + + + + + + + + + + + + Cambiar estatus + + + + + +
+ + + + No hay trámites que coincidan con los filtros seleccionados. +
+
+
+
+
@@ -618,12 +654,137 @@ Documentos + + + -
- -

No hay documentos registrados.

-
+
+
+

Documentos del fraccionamiento

+
+ + + +
+ + + + + + + + + + + + + + + + + + + <%#: Convert.ToString(Eval("EstadoDocumento")) %> + + + + + + + <%#: GetDocumentNoteText(Eval("Nota")) %> + + + + + + + + + + <%#: GetDocumentUploadButtonText(Eval("TieneArchivo")) %> + + + + + Administrar GIS + + + + + Aprobar + + + + + Rechazar + + + + + +
+ + + + No hay documentos que coincidan con los filtros seleccionados. +
+
+
+
+
@@ -647,10 +808,253 @@ + + + + + + + + + @@ -925,6 +1329,76 @@ + +