using Microsoft.Extensions.Configuration; using Net.Business.Entities.ServicesLayer; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Security; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; namespace Net.Connection.ServiceLayer { public class ConnectionServiceLayer { public static bool ServicioActivo = false; public static string SLSession; // Se invoca solo cuando SAP rechaza un envio: (ruta del recurso, JSON enviado, respuesta de SAP). // Lo asigna Startup (esta capa no conoce HANA). public static Action ErrorLogger; private static void LogErrorEnvio(string query, object jsonEnviado, string respuesta) { try { ErrorLogger?.Invoke(query, jsonEnviado, respuesta); } catch { /* el log nunca debe romper el flujo principal */ } } private readonly string _url; private readonly string _company; private readonly string _user; private readonly string _password; private readonly string _language; private readonly IHttpClientFactory _httpClient; private ResponseLoginServiceLayer _responseLoginServiceLayer; public ConnectionServiceLayer(IConfiguration configuration, IHttpClientFactory clientFactory) { _url = configuration["SapServiceLayer:url"]; _company = configuration["SapServiceLayer:company"]; _user = configuration["SapServiceLayer:user"]; _password = configuration["SapServiceLayer:password"]; _language = configuration["SapServiceLayer:language"]; _httpClient = clientFactory; _responseLoginServiceLayer = new ResponseLoginServiceLayer(); } // Inicia sesion en el Service Layer (cookie B1SESSION). Usa SapServiceLayer:company/user/password de appsettings.json public async Task Login() { try { // CONSTRUIMOS LA URL DE LA ACCIÓN var urlBuilder_ = new StringBuilder(); urlBuilder_.Append(_url != null ? _url.TrimEnd('/') : "") .Append("/Login"); var url_ = urlBuilder_.ToString(); // RECUPERAMOS EL HttpClient var client_ = _httpClient.CreateClient("bypass-ssl-validation"); using (var request_ = new HttpRequestMessage()) { /////////////////////////////////////// // CONSTRUIMOS LA PETICIÓN (REQUEST) // /////////////////////////////////////// // DEFINIMOS EL Content CON EL OBJETO A ENVIAR SERIALIZADO. request_.Content = new StringContent("{\"CompanyDB\":\"" + _company + "\",\"Password\":\"" + _password + "\",\"UserName\":\"" + _user + "\"}"); // DEFINIMOS EL ContentType, EN ESTE CASO ES "application/json" request_.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); request_.Headers.Add("cache-control", "no-cache"); // DEFINIMOS EL MÉTODO HTTP request_.Method = new HttpMethod("POST"); // DEFINIMOS LA URI request_.RequestUri = new Uri(url_, System.UriKind.RelativeOrAbsolute); // DEFINIMOS EL Accept, EN ESTE CASO ES "application/json" request_.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); ///////////////////////////////////////// // CONSTRUIMOS LA RESPUESTA (RESPONSE) // ///////////////////////////////////////// // Utilizamos ConfigureAwait(false) para evitar el DeadLock. var response_ = await client_.SendAsync(request_).ConfigureAwait(false); // OBTENEMOS EL Content DEL RESPONSE como un String // Utilizamos ConfigureAwait(false) para evitar el DeadLock. var responseText_ = await response_.Content.ReadAsStringAsync().ConfigureAwait(false); // SI ES LA RESPUESTA ESPERADA !! ... if (response_.StatusCode == System.Net.HttpStatusCode.OK) // 200 { // DESERIALIZAMOS Content DEL RESPONSE _responseLoginServiceLayer = JsonConvert.DeserializeObject(responseText_); _responseLoginServiceLayer.ServicioActivo = true; _responseLoginServiceLayer.MensajeLogin = "Autenticación Correcta"; return _responseLoginServiceLayer; } else // SI NO SE ESTÁ AUTORIZADO ... if (response_.StatusCode == System.Net.HttpStatusCode.Unauthorized) // 401 { _responseLoginServiceLayer = JsonConvert.DeserializeObject(responseText_); _responseLoginServiceLayer.ServicioActivo = false; _responseLoginServiceLayer.MensajeLogin = "401 Unauthorized. Las credenciales de acceso del usuario son incorrectas."; return _responseLoginServiceLayer; } else // CUALQUIER OTRA RESPUESTA ... if (response_.StatusCode != System.Net.HttpStatusCode.OK && // 200 response_.StatusCode != System.Net.HttpStatusCode.NoContent) // 204 { _responseLoginServiceLayer = JsonConvert.DeserializeObject(responseText_); _responseLoginServiceLayer.ServicioActivo = false; _responseLoginServiceLayer.MensajeLogin = "401 Unauthorized. Las credenciales de acceso del usuario son incorrectas."; return _responseLoginServiceLayer; } // RETORNAMOS EL OBJETO POR DEFECTO ESPERADO return _responseLoginServiceLayer; } } catch (Exception ex) { _responseLoginServiceLayer.ServicioActivo = false; _responseLoginServiceLayer.MensajeLogin = ex.Message.ToString(); return _responseLoginServiceLayer; } } // GET al Service Layer. dinamico = true: la respuesta es una sola entidad (ej. Invoices(123)); false: coleccion { value: [...] } public async Task> GetAsync(string Query, int maxpagesize,bool dinamico = false) { try { // CONSTRUIMOS LA URL DE LA ACCIÓN var urlBuilder_ = new StringBuilder(); urlBuilder_.Append(_url != null ? _url.TrimEnd('/') : "") .Append("/" + Query); var url_ = urlBuilder_.ToString(); // RECUPERAMOS EL HttpClient var client_ = _httpClient.CreateClient("bypass-ssl-validation"); //AUTENTICAMOS PARA REALIZAR LAS CONSULTAS await Login(); using (var request_ = new HttpRequestMessage()) { /////////////////////////////////////// // CONSTRUIMOS LA PETICIÓN (REQUEST) // /////////////////////////////////////// // DEFINIMOS EL MÉTODO HTTP request_.Method = new HttpMethod("GET"); // DEFINIMOS LA URI request_.RequestUri = new Uri(url_, System.UriKind.RelativeOrAbsolute); // DEFINIMOS EL Accept, EN ESTE CASO ES "application/json" request_.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); request_.Headers.Add("cache-control", "no-cache"); request_.Headers.Add("Prefer", "odata.maxpagesize=" + maxpagesize.ToString()); // ASIGNAMOS A LA CABECERA DE LA PETICIÓN EL TOKEN JWT. if (_responseLoginServiceLayer.ServicioActivo) request_.Headers.Add("Cookie", "B1SESSION=" + _responseLoginServiceLayer.SessionId); ///////////////////////////////////////// // CONSTRUIMOS LA RESPUESTA (RESPONSE) // ///////////////////////////////////////// // Utilizamos ConfigureAwait(false) para evitar el DeadLock. var response_ = await client_.SendAsync(request_, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); // OBTENEMOS EL Content DEL RESPONSE como un String // Utilizamos ConfigureAwait(false) para evitar el DeadLock. var responseText_ = await response_.Content.ReadAsStringAsync().ConfigureAwait(false); // SI ES LA RESPUESTA ESPERADA !! ... if (response_.StatusCode == System.Net.HttpStatusCode.OK) // 200 { // DESERIALIZAMOS Content DEL RESPONSE if (!dinamico) { var responseBody_ = JsonConvert.DeserializeObject>(responseText_); return responseBody_.value; } if (dinamico) { List lista = new List(); dynamic responseBody = JsonConvert.DeserializeObject(responseText_); lista.Add(responseBody); return lista; } } // RETORNAMOS EL OBJETO POR DEFECTO ESPERADO return default(List); } } finally { // NO UTILIZAMOS CATCH, // PASAMOS LA EXCEPCIÓN A LA APP. } } // POST con cuerpo JSON. Crea un documento en SAP. Si SAP rechaza, devuelve Exito = false y guarda el JSON enviado // con ErrorLogger (tabla ITCA_LOG_INTEGRACION) public Task PostAsync(string Query, object obj) { return EnviarPostAsync(Query, obj, true, false); } // POST sin cuerpo para reabrir un documento (ej. Invoices(n)/Reopen). SAP responde 204 sin contenido y se toma como exito public Task PostReOpen(string Query) { return EnviarPostAsync(Query, null, false, true); } // POST sin cuerpo para cerrar un documento (.../Close). SAP responde 204 sin contenido y se toma como exito public Task PostClose(string Query) { return EnviarPostAsync(Query, null, false, true); } // Nucleo comun de los envios POST al Service Layer (PostAsync, PostReOpen, PostClose). Reintenta una vez si la sesion // expiro (401). conCuerpo: envia obj como JSON y, si SAP rechaza, guarda el JSON enviado con ErrorLogger. // aceptaSinContenido: toma el 204 (sin cuerpo) como exito. private async Task EnviarPostAsync(string Query, object obj, bool conCuerpo, bool aceptaSinContenido) { try { // CONSTRUIMOS LA URL DE LA ACCIÓN var urlBuilder_ = new StringBuilder(); urlBuilder_.Append(_url != null ? _url.TrimEnd('/') : "") .Append("/" + Query); var url_ = urlBuilder_.ToString(); // RECUPERAMOS EL HttpClient var client_ = _httpClient.CreateClient("bypass-ssl-validation"); int NumError = 0; band: using (var request_ = new HttpRequestMessage()) { /////////////////////////////////////// // CONSTRUIMOS LA PETICIÓN (REQUEST) // /////////////////////////////////////// // DEFINIMOS EL MÉTODO HTTP GET request_.Method = new HttpMethod("POST"); // DEFINIMOS LA URI request_.RequestUri = new Uri(url_, System.UriKind.RelativeOrAbsolute); // DEFINIMOS EL Accept, EN ESTE CASO ES "application/json" request_.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); if (conCuerpo) request_.Content = new StringContent(JsonConvert.SerializeObject(obj), System.Text.Encoding.UTF8, "application/json"); // ASIGNAMOS A LA CABECERA DE LA PETICIÓN EL TOKEN JWT. if (_responseLoginServiceLayer.ServicioActivo) request_.Headers.Add("Cookie", "B1SESSION=" + _responseLoginServiceLayer.SessionId); ///////////////////////////////////////// // CONSTRUIMOS LA RESPUESTA (RESPONSE) // ///////////////////////////////////////// // Utilizamos ConfigureAwait(false) para evitar el DeadLock. var response_ = await client_.SendAsync(request_, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); // OBTENEMOS EL Content DEL RESPONSE como un String // Utilizamos ConfigureAwait(false) para evitar el DeadLock. var responseText_ = await response_.Content.ReadAsStringAsync().ConfigureAwait(false); // SI ES LA RESPUESTA ESPERADA !! ... if (response_.StatusCode == System.Net.HttpStatusCode.OK || response_.StatusCode == System.Net.HttpStatusCode.Created || (aceptaSinContenido && response_.StatusCode == System.Net.HttpStatusCode.NoContent)) // 200, Created o 204 { // DESERIALIZAMOS Content DEL RESPONSE // Un 204 (Reopen/Close) viene sin cuerpo: Deserialize devolveria null, asi que se crea una instancia vacia dynamic responseBody = (aceptaSinContenido && string.IsNullOrWhiteSpace(responseText_)) ? Activator.CreateInstance() : JsonConvert.DeserializeObject(responseText_); responseBody.Exito = true; var estado = (int)response_.StatusCode; responseBody.CodigoHttp = estado.ToString(); responseBody.Mensaje = "SE ENVIO CORRECTAMENTE A SAP BO "; return responseBody; } else // SI NO SE ESTÁ AUTORIZADO ... if (response_.StatusCode == System.Net.HttpStatusCode.Unauthorized) // 401 { NumError += 1; _responseLoginServiceLayer = JsonConvert.DeserializeObject(responseText_); _responseLoginServiceLayer.ServicioActivo = false; if (NumError == 2) { dynamic responseBody = JsonConvert.DeserializeObject(responseText_); responseBody.Exito = false; var estado = (int)response_.StatusCode; responseBody.CodigoHttp = estado.ToString(); responseBody.Mensaje = "Services Unauthorized : " + _responseLoginServiceLayer.error.message.ToString(); if (conCuerpo) LogErrorEnvio(Query, obj, responseBody.Mensaje); return responseBody; } if (_responseLoginServiceLayer.error.code.ToString() == "301") { await Login(); } goto band; } else // CUALQUIER OTRA RESPUESTA ... if (response_.StatusCode != System.Net.HttpStatusCode.OK && // 200 response_.StatusCode != System.Net.HttpStatusCode.NoContent) // 204 { NumError += 1; _responseLoginServiceLayer = JsonConvert.DeserializeObject(responseText_); _responseLoginServiceLayer.ServicioActivo = false; if (NumError == 2) { dynamic responseBody = JsonConvert.DeserializeObject(responseText_); responseBody.Exito = false; var estado = (int)response_.StatusCode; responseBody.CodigoHttp = estado.ToString(); responseBody.Mensaje = "Error Services : " + _responseLoginServiceLayer.error.message.ToString(); if (conCuerpo) LogErrorEnvio(Query, obj, responseBody.Mensaje); return responseBody; } if (_responseLoginServiceLayer.error.code.ToString() == "301") { await Login(); } goto band; } // RETORNAMOS EL OBJETO POR DEFECTO ESPERADO return default(T); } } finally { // NO UTILIZAMOS CATCH, // PASAMOS LA EXCEPCIÓN A LA APP. } } // === NUEVO (SolicitudTransferencia / Picking HojaRuta Cerrar) === // // PatchNoContentAsync/PostActionAsync no devuelven un T deserializado, solo exito (sin excepcion) // o fallo (excepcion con el mensaje real de SAP). En el Service Layer un PATCH exitoso es un 204 sin cuerpo, // asi que no hay nada que deserializar. (v2.1.14.0: se quito PatchAsyncSBA, que con el 204 devolvia null // sin marcar exito y con un error de red devolvia lo mismo.) public async Task PatchNoContentAsync(string Query, object obj, Dictionary extraHeaders = null) { var urlBuilder_ = new StringBuilder(); urlBuilder_.Append(_url != null ? _url.TrimEnd('/') : "").Append("/" + Query); var url_ = urlBuilder_.ToString(); var client_ = _httpClient.CreateClient("bypass-ssl-validation"); await Login(); using (var request_ = new HttpRequestMessage()) { request_.Method = new HttpMethod("PATCH"); request_.RequestUri = new Uri(url_, UriKind.RelativeOrAbsolute); request_.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); request_.Content = new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, "application/json"); if (_responseLoginServiceLayer.ServicioActivo) request_.Headers.Add("Cookie", "B1SESSION=" + _responseLoginServiceLayer.SessionId); if (extraHeaders != null) { foreach (var kv in extraHeaders) request_.Headers.Add(kv.Key, kv.Value); } var response_ = await client_.SendAsync(request_, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); var responseText_ = await response_.Content.ReadAsStringAsync().ConfigureAwait(false); if (response_.StatusCode == HttpStatusCode.OK || response_.StatusCode == HttpStatusCode.NoContent) { return; // exito real - sin body que parsear } var mensajeError = ExtraerMensajeErrorSap(responseText_); LogErrorEnvio(Query, obj, mensajeError); throw new Exception(mensajeError); } } public async Task PostActionAsync(string Query) { var urlBuilder_ = new StringBuilder(); urlBuilder_.Append(_url != null ? _url.TrimEnd('/') : "").Append("/" + Query); var url_ = urlBuilder_.ToString(); var client_ = _httpClient.CreateClient("bypass-ssl-validation"); await Login(); using (var request_ = new HttpRequestMessage()) { request_.Method = new HttpMethod("POST"); request_.RequestUri = new Uri(url_, UriKind.RelativeOrAbsolute); request_.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); request_.Content = new StringContent("{}", Encoding.UTF8, "application/json"); if (_responseLoginServiceLayer.ServicioActivo) request_.Headers.Add("Cookie", "B1SESSION=" + _responseLoginServiceLayer.SessionId); var response_ = await client_.SendAsync(request_, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); var responseText_ = await response_.Content.ReadAsStringAsync().ConfigureAwait(false); if (response_.StatusCode == HttpStatusCode.OK || response_.StatusCode == HttpStatusCode.Created || response_.StatusCode == HttpStatusCode.NoContent) { return; // exito real - sin body que parsear } throw new Exception(ExtraerMensajeErrorSap(responseText_)); } } private static string ExtraerMensajeErrorSap(string responseText) { try { var parsed = JsonConvert.DeserializeObject(responseText); var mensaje = parsed?.error?.message; if (mensaje == null) return responseText; // "message" esta tipado como object en ErrorServiceLayer (ver // ResponseLoginServiceLayer.cs) - Json.NET lo deja como JObject. if (mensaje is JObject jMensaje) return jMensaje.Value("value") ?? responseText; return mensaje.ToString(); } catch { return responseText; } } } }