diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml new file mode 100644 index 000000000..ad34a345a --- /dev/null +++ b/.github/workflows/dotnet-build-and-test.yml @@ -0,0 +1,29 @@ +name: .NET Build and Test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '6.0.x' + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --no-restore --configuration Release + + - name: Test + run: dotnet test --no-build --verbosity normal --configuration Release diff --git a/Src/VTEX/VTEXContext.cs b/Src/VTEX/VTEXContext.cs index 8091a7023..5607d773a 100644 --- a/Src/VTEX/VTEXContext.cs +++ b/Src/VTEX/VTEXContext.cs @@ -1,1597 +1,1605 @@ -// *********************************************************************** -// Assembly : VTEX -// Author : Guilherme Branco Stracini -// Created : 01-15-2023 -// -// Last Modified By : Guilherme Branco Stracini -// Last Modified On : 01-16-2023 -// *********************************************************************** -// -// © 2020 Guilherme Branco Stracini. All rights reserved. -// -// -// *********************************************************************** -namespace VTEX -{ - using System; - using System.Collections.Generic; - using System.ComponentModel; - using System.Diagnostics.Contracts; - using System.Globalization; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using CrispyWaffle.Extensions; - using CrispyWaffle.Log; - using CrispyWaffle.Serialization; - using Newtonsoft.Json; - using VTEX.DataEntities; - using VTEX.Enums; - using VTEX.Extensions; - using VTEX.GoodPractices; - using VTEX.Health; - using VTEX.Transport; - using VTEX.Transport.Bridge; - - /// - /// A VTEX Context, that consumes the VTEX Wrapper - /// - /// - public sealed class VTEXContext : IDisposable - { - #region Private fields - - /// - /// The wrapper - /// - private readonly VTEXWrapper _wrapper; - - #endregion - - #region ~Ctor - - /// - /// Initializes a new instance of the class. - /// - /// Name of the account. - /// The application key. - /// The application token. - /// The cookie. - /// appKey - /// appToken - public VTEXContext(string accountName, string appKey, string appToken, string cookie = null) - { - _wrapper = new VTEXWrapper(accountName); - if (string.IsNullOrWhiteSpace(appKey)) - { - throw new ArgumentNullException(nameof(appKey)); - } - - if (string.IsNullOrWhiteSpace(appToken)) - { - throw new ArgumentNullException(nameof(appToken)); - } - - _wrapper.SetRestCredentials(appKey, appToken); - if (string.IsNullOrWhiteSpace(cookie)) - { - return; - } - - _wrapper.SetVtexIdClientAuthCookie(cookie); - } - - /// - /// Retrieves a list of orders based on specified filtering criteria. - /// - /// The status of the orders to filter by (optional). - /// The start date for filtering orders (optional). - /// The end date for filtering orders (optional). - /// The sales channel to filter by (optional). - /// The affiliated ID to filter by (optional). - /// The payment system name to filter by (optional). - /// A generic query string for additional filtering (optional). - /// An instance of containing the filtered orders. - /// - /// This method constructs a query string based on the provided parameters to filter the orders. - /// It supports pagination and retrieves orders in pages of 50 until no more orders are found. - /// The filtering criteria include order status, sales channel, affiliated ID, payment system name, - /// and a date range defined by start and end dates. The results are logged indicating the number of orders found. - /// - private OrdersList GetOrdersListInternal( - string status = null, - DateTime? startDate = null, - DateTime? endDate = null, - string salesChannel = null, - string affiliatedId = null, - string paymentSystemName = null, - string genericQuery = null - ) - { - OrdersList result = null; - var currentPage = 1; - var queryString = new Dictionary - { - { @"page", @"0" }, - { @"per_page", @"50" }, - }; - if (!string.IsNullOrWhiteSpace(status)) - { - queryString.Add(@"f_status", status); - } - - if (!string.IsNullOrWhiteSpace(salesChannel)) - { - queryString.Add(@"f_salesChannel", salesChannel); - } - - if (!string.IsNullOrWhiteSpace(affiliatedId)) - { - queryString.Add(@"f_affiliateId", affiliatedId); - } - - if (!string.IsNullOrWhiteSpace(paymentSystemName)) - { - queryString.Add(@"f_paymentNames", paymentSystemName); - } - - if (startDate.HasValue && endDate.HasValue) - { - queryString.Add( - @"f_creationDate", - $@"creationDate:[{startDate.Value.ToUniversalTime():s}Z TO {endDate.Value.ToUniversalTime():s}Z]" - ); - } - - if (!string.IsNullOrWhiteSpace(genericQuery)) - { - queryString.Add(@"q", genericQuery); - } - - queryString.Add(@"orderBy", @"creationDate,asc"); - while (GetOrderListsValueInternal(queryString, currentPage, ref result)) - { - currentPage++; - } - - LogConsumer.Info("{0} orders found", result.List.Length); - return result; - } - - /// - /// Gets the order lists value internal. - /// - /// The query string. - /// The current page. - /// The result. - /// true if XXXX, false otherwise. - /// - private bool GetOrderListsValueInternal( - Dictionary queryString, - int currentPage, - ref OrdersList result - ) - { - var json = string.Empty; - try - { - LogConsumer.Trace("Getting page {0} of orders list", currentPage); - queryString[@"page"] = currentPage.ToString(CultureInfo.InvariantCulture); - - json = _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.GET, - PlatformConstants.OmsOrders, - CancellationToken.None, - queryString - ) - .Result; - var temp = SerializerFactory.GetSerializer().Deserialize(json); - if (result == null) - { - result = temp; - } - else - { - result.List = result.List.Concat(temp.List).ToArray(); - } - - if (temp.Paging.Pages == 1 || temp.Paging.CurrentPage >= temp.Paging.Pages) - { - return false; - } - - if (currentPage == 1) - { - LogConsumer.Trace("{0} pages of orders list", temp.Paging.Pages); - } - - return true; - } - catch (JsonSerializationException e) - { - throw new UnexpectedApiResponseException(json, e); - } - } - - /// - /// Gets the orders by order's ids. - /// - /// The order's ids. - /// IEnumerable<Order>. - private IEnumerable GetOrdersInternal(IEnumerable ordersIds) - { - var list = new List(); - Parallel.ForEach(ordersIds, orderId => list.Add(GetOrder(orderId))); - return list; - } - - /// - /// Get a order by order id - /// - /// The id of the order - /// Order. - /// - /// - private Order GetOrderInternal(string orderId) - { - LogConsumer.Trace("Getting order {0}", orderId); - var json = _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.GET, - $"{PlatformConstants.OmsOrders}/{orderId}", - CancellationToken.None - ) - .Result; - if (json == null) - { - return null; - } - - try - { - var order = SerializerFactory.GetSerializer().Deserialize(json); - - #region Payment - - var transaction = order.PaymentData.Transactions.First(); - var payment = transaction.Payments.FirstOrDefault(); - if ( - payment != null - && payment.PaymentSystem == 0 - && !string.IsNullOrWhiteSpace(order.AffiliateId) - ) - { - LogConsumer.Info(@"Marketplace {0}", order.AffiliateId); - } - else if ( - transaction.TransactionId != null - && !transaction.TransactionId.Equals( - @"NO-PAYMENT", - StringComparison.InvariantCultureIgnoreCase - ) - ) - { - LogConsumer.Info(@"Bank bill {0}", order.Sequence); - } - else if (order.Totals.Sum(t => t.Value) == 0) - { - LogConsumer.Warning("Promotion / discount coupon - order subsidized"); - } - else - { - throw new InvalidPaymentDataException(orderId); - } - - #endregion - - #region Email - - if (!string.IsNullOrWhiteSpace(order.ClientProfileData.UserProfileId)) - { - var client = SearchAsync( - @"userId", - order.ClientProfileData.UserProfileId, - CancellationToken.None - ).Result; - if (client != null && !string.IsNullOrWhiteSpace(client.Email)) - { - order.ClientProfileData.Email = client.Email; - } - - if ( - order.ClientProfileData.Email.IndexOf( - @"ct.vtex", - StringComparison.InvariantCultureIgnoreCase - ) != -1 - ) - { - order.ClientProfileData.Email = @"pedido@editorainovacao.com.br"; - } - } - - #endregion - - LogConsumer.Debug(order, $"vtex-order-{orderId}.js"); - var affiliated = string.IsNullOrWhiteSpace(order.AffiliateId) - ? string.Empty - : $" - Affiliated: {order.AffiliateId}"; - LogConsumer.Info( - "Order: {0} - Sequence: {1} - Status: {2} - Sales channel: {3}{4}", - order.OrderId, - order.Sequence, - order.Status.GetHumanReadableValue(), - order.SalesChannel, - affiliated - ); - return order; - } - catch (JsonSerializationException e) - { - throw new UnexpectedApiResponseException(json, e); - } - } - - #endregion - - #region Public Methods - - #region OMS - - /// - /// Gets the feed. - /// - /// The maximum lot. - /// IEnumerable<OrderFeed>. - public IEnumerable GetFeed(int maxLot = 20) - { - //VTEX limitation - if (maxLot > 20) - { - maxLot = 20; - } - - LogConsumer.Trace("Getting up to {0} events in order feed", maxLot); - var json = _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.GET, - $"{PlatformConstants.OmsFeed}", - CancellationToken.None, - new Dictionary { { @"maxLot", maxLot.ToString() } } - ) - .Result; - return SerializerFactory.GetSerializer>().Deserialize(json); - } - - /// - /// Commits the feed. - /// - /// The feed. - public void CommitFeed(OrderFeed feed) - { - LogConsumer.Trace("Commiting feed of order {0}", feed.OrderId); - var data = (string) - new OrderFeedCommit { CommitToken = feed.CommitToken }.GetSerializer(); - _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.POST, - $"{PlatformConstants.OmsFeed}confirm", - CancellationToken.None, - data: data - ) - .Wait(); - } - - /// - /// Get a order by order id - /// - /// The id of the order - /// Order. - /// - public Order GetOrder(string orderId) - { - return GetOrderInternal(orderId); - } - - /// - /// Gets the orders list metadata. - /// - /// The status. - /// IEnumerable<List>. - public IEnumerable GetOrdersList(OrderStatus status) - { - LogConsumer.Warning("Getting orders with status {0}", status.GetHumanReadableValue()); - var orders = GetOrdersListInternal(status.GetInternalValue()); - return orders.List; - } - - /// - /// Get a Enumerable list of Order by status. - /// - /// The status of the orders to get - /// IEnumerable<Order>. - public IEnumerable GetOrders(OrderStatus status) - { - var ordersIds = GetOrdersList(status).Select(order => order.OrderId).ToList(); - if (ordersIds.Any()) - { - return GetOrdersInternal(ordersIds); - } - - LogConsumer.Warning("No orders with status {0} found", status.GetHumanReadableValue()); - return new Order[0]; - } - - /// - /// Gets the orders list by a date range of order's placed date. - /// - /// The start date of the range. - /// The end date of the range. - /// IEnumerable<String>. - public IEnumerable GetOrdersList(DateTime startDate, DateTime endDate) - { - LogConsumer.Warning("Getting orders between {0:G} and {1:G}", startDate, endDate); - var orders = GetOrdersListInternal(startDate: startDate, endDate: endDate); - return orders.List; - } - - /// - /// Get a Enumerable list of Order by a date range of order's placed date. - /// - /// The start date of the range - /// The end date of the range - /// IEnumerable<Order>. - public IEnumerable GetOrders(DateTime startDate, DateTime endDate) - { - var ordersIds = GetOrdersList(startDate, endDate) - .Select(order => order.OrderId) - .ToList(); - if (ordersIds.Any()) - { - return GetOrdersInternal(ordersIds); - } - - LogConsumer.Warning("No orders between {0:G} and {1:G} found", startDate, endDate); - return new Order[0]; - } - - /// - /// Gets the orders list by status and date range of order's placed date. - /// - /// The status of orders to get. - /// The start date of the range. - /// The end date of the range. - /// IEnumerable<String>. - public IEnumerable GetOrdersList( - OrderStatus status, - DateTime startDate, - DateTime endDate - ) - { - LogConsumer.Warning( - "Getting orders with status {0} between {1:G} and {2:G}", - status.GetHumanReadableValue(), - startDate, - endDate - ); - var orders = GetOrdersListInternal(status.GetInternalValue(), startDate, endDate); - return orders.List; - } - - /// - /// Get a Enumerable list of Order by status and date range of order's placed date. - /// - /// The status of orders to get. - /// The start date of the range. - /// The end date of the range. - /// IEnumerable<Order>. - public IEnumerable GetOrders( - OrderStatus status, - DateTime startDate, - DateTime endDate - ) - { - var ordersIds = GetOrdersList(status, startDate, endDate) - .Select(order => order.OrderId) - .ToList(); - if (ordersIds.Any()) - { - return GetOrdersInternal(ordersIds); - } - - LogConsumer.Warning( - "No order with status {0} between {1:G} and {2:G} found", - status.GetHumanReadableValue(), - startDate, - endDate - ); - return new Order[0]; - } - - /// - /// Gets the orders list by status and affiliated identifier (AKA marketplace). - /// - /// The status of orders to get. - /// The affiliated identifier - /// IEnumerable<String>. - public IEnumerable GetOrdersList(OrderStatus status, string affiliatedId) - { - LogConsumer.Warning( - "Getting orders with status {0} and affiliated {1}", - status.GetHumanReadableValue(), - affiliatedId - ); - var orders = GetOrdersListInternal( - status.GetInternalValue(), - affiliatedId: affiliatedId - ); - return orders.List; - } - - /// - /// Get a Enumerable list of Order by status and affiliated identifier (AKA marketplace). - /// - /// The status of orders to get. - /// The affiliated identifier - /// IEnumerable<Order>. - public IEnumerable GetOrders(OrderStatus status, string affiliatedId) - { - var ordersIds = GetOrdersList(status, affiliatedId) - .Select(order => order.OrderId) - .ToList(); - if (ordersIds.Any()) - { - return GetOrdersInternal(ordersIds); - } - - LogConsumer.Warning( - "No order with status {0} and affiliated {1} found", - status.GetHumanReadableValue(), - affiliatedId - ); - return new Order[0]; - } - - /// - /// Gets the orders list by generic query (order id, client's document, sequence, etc). - /// - /// The query to lookup in orders. - /// IEnumerable<String>. - public IEnumerable GetOrdersList(string query) - { - LogConsumer.Warning("Getting orders with term '{0}'", query); - var orders = GetOrdersListInternal(genericQuery: query); - return orders.List; - } - - /// - /// Gets the orders by generic query (order identifier, client's document, sequence, etc). - /// - /// The query to lookup in orders. - /// IEnumerable<Order>. - public IEnumerable GetOrders(string query) - { - var ordersIds = GetOrdersList(query).Select(order => order.OrderId).ToList(); - if (ordersIds.Any()) - { - return GetOrdersInternal(ordersIds); - } - - LogConsumer.Warning("No orders with term '{0}' found", query); - return new Order[0]; - } - - /// - /// Gets the orders by the array of orders identifiers - /// - /// The orders ids. - /// IEnumerable<Order>. - public IEnumerable GetOrders(string[] ordersIds) - { - return GetOrdersInternal(ordersIds); - } - - /// - /// Cancels the order asynchronous. - /// - /// The order identifier. - /// A Task<System.String> representing the asynchronous operation. - /// Order {orderId} cannot be canceled because isn't in pending payment status on VTEX - public async Task CancelOrderAsync(string orderId) - { - try - { - LogConsumer.Warning("Cancelling order {0}", orderId); - var source = new CancellationTokenSource(new TimeSpan(0, 5, 0)); - var order = GetOrder(orderId); - if (order.Status == OrderStatus.CANCELED) - { - return string.Empty; - } - - if ( - order.Status != OrderStatus.PAYMENT_PENDING - && order.Status != OrderStatus.AWAITING_AUTHORIZATION_TO_DISPATCH - ) - { - throw new InvalidOperationException( - $"Order {orderId} cannot be canceled because isn't in pending payment status on VTEX" - ); - } - - var json = await _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.POST, - $"{PlatformConstants.OmsOrders}/{orderId}/cancel", - source.Token - ) - .ConfigureAwait(false); - var receipt = SerializerFactory - .GetSerializer() - .Deserialize(json); - LogConsumer.Info( - "Order {0} successfully canceled. Receipt: {1}", - order.Sequence, - receipt.Receipt - ); - return receipt.Receipt; - } - catch (Exception e) - { - LogConsumer.Handle(new CancelOrderException(orderId, e)); - return string.Empty; - } - } - - /// - /// Changes the order status asynchronous. - /// - /// The order identifier. - /// The new status. - /// A Task representing the asynchronous operation. - /// - public async Task ChangeOrderStatusAsync(string orderId, OrderStatus newStatus) - { - try - { - LogConsumer.Info( - "Changing order {0} status to {1}", - orderId, - newStatus.GetHumanReadableValue() - ); - var source = new CancellationTokenSource(new TimeSpan(0, 5, 0)); - var json = await _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.POST, - $"{PlatformConstants.OmsOrders}/{orderId}/changestate/{newStatus.GetInternalValue()}", - source.Token - ) - .ConfigureAwait(false); - LogConsumer.Info(json); - } - catch (AggregateException e) - { - var ae = e.InnerExceptions.First(); - throw new ChangeStatusOrderException( - orderId, - newStatus.GetHumanReadableValue(), - ae - ); - } - catch (Exception e) - { - throw new ChangeStatusOrderException(orderId, newStatus.GetHumanReadableValue(), e); - } - } - - /// - /// Notifies the order paid asynchronous. - /// - /// The order identifier. - /// A Task representing the asynchronous operation. - public async Task NotifyOrderPaidAsync(string orderId) - { - try - { - LogConsumer.Info("Sending payment notification of order {0}", orderId); - var source = new CancellationTokenSource(new TimeSpan(0, 5, 0)); - var order = GetOrder(orderId); - if ( - order.Status != OrderStatus.PAYMENT_PENDING - && order.Status != OrderStatus.AWAITING_AUTHORIZATION_TO_DISPATCH - ) - { - return; - } - - if (order.Status == OrderStatus.AWAITING_AUTHORIZATION_TO_DISPATCH) - { - await ChangeOrderStatusAsync(order.OrderId, OrderStatus.AUTHORIZE_FULFILLMENT) - .ConfigureAwait(false); - return; - } - var paymentId = order.PaymentData.Transactions.First().Payments.First().Id; - _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.POST, - $"{PlatformConstants.OmsOrders}/{order.OrderId}/payments/{paymentId}/payment-notification", - source.Token - ) - .Wait(source.Token); - } - catch (Exception e) - { - LogConsumer.Handle(new PaymentNotificationOrderException(orderId, e)); - } - } - - /// - /// Notifies the order shipped. - /// - /// The order identifier. - /// The notification. - /// - public void NotifyOrderShipped(string orderId, ShippingNotification notification) - { - NotifyOrderShippedAsync(orderId, notification, CancellationToken.None).Wait(); - } - - /// - /// Notifies the order shipped async. - /// - /// The order identifier. - /// The notification. - /// The token. - /// A Task<System.String> representing the asynchronous operation. - /// - public async Task NotifyOrderShippedAsync( - string orderId, - ShippingNotification notification, - CancellationToken token - ) - { - try - { - LogConsumer.Info("Sending shipping notification of order {0}", orderId); - LogConsumer.Debug( - notification, - $"vtex-shipping-notification-{orderId}-{notification.InvoiceNumber}.js" - ); - var json = await _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.POST, - $"{PlatformConstants.OmsInvoices}/{orderId}/invoice", - token, - data: (string)notification.GetSerializer() - ) - .ConfigureAwait(false); - var receipt = SerializerFactory.GetSerializer().Deserialize(json); - LogConsumer.Trace(receipt.Receipt); - return receipt.Receipt; - } - catch (AggregateException e) - { - var ae = e.InnerExceptions.First(); - throw new ShippingNotificationOrderException(orderId, ae); - } - catch (Exception e) - { - throw new ShippingNotificationOrderException(orderId, e); - } - } - - /// - /// Notifies the order delivered - /// - /// The tracking. - /// System.String. - /// - public async ValueTask NotifyOrderDelivered(Tracking tracking) - { - try - { - LogConsumer.Info("Sending tracking info of order {0}", tracking.OrderId); - var source = new CancellationTokenSource(new TimeSpan(0, 5, 0)); - LogConsumer.Debug( - tracking, - $"vtex-tracking-info-{tracking.OrderId}-{tracking.InvoiceNumber}.js" - ); - var json = await _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.PUT, - string.Format( - PlatformConstants.OmsTracking, - tracking.OrderId, - tracking.InvoiceNumber - ), - source.Token, - data: (string)tracking.GetSerializer() - ) - .ConfigureAwait(false); - var receipt = SerializerFactory.GetSerializer().Deserialize(json); - LogConsumer.Trace(receipt.Receipt); - return receipt.Receipt; - } - catch (Exception e) - { - throw new TrackingNotificationOrderException(tracking.OrderId, e); - } - } - - /// - /// Updates the order invoice. - /// - /// The order identifier. - /// The invoice identifier. - /// The notification. - /// - public void UpdateOrderInvoice( - string orderId, - string invoiceId, - ShippingNotificationPatch notification - ) - { - try - { - LogConsumer.Info("Patching fiscal invoice {1} of order {0}", orderId, invoiceId); - var source = new CancellationTokenSource(new TimeSpan(0, 5, 0)); - LogConsumer.Debug( - notification, - $"vtex-shipping-notification-{orderId}-{invoiceId}.js" - ); - var json = _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.PATCH, - $"{PlatformConstants.OmsOrders}/{orderId}/invoice/{invoiceId}", - source.Token, - data: (string)notification.GetSerializer() - ) - .Result; - var receipt = SerializerFactory.GetSerializer().Deserialize(json); - LogConsumer.Trace(receipt.Receipt); - } - catch (Exception e) - { - throw new ShippingNotificationOrderException(orderId, e); - } - } - - /// - /// Changes the order. - /// - /// The order identifier. - /// The change. - /// - public void ChangeOrder(string orderId, ChangeOrder change) - { - try - { - LogConsumer.Info("Changing order {0}", orderId); - LogConsumer.Debug(change, $"vtex-change-order-{orderId}.js"); - var json = _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.POST, - $"{PlatformConstants.OmsOrders}/{orderId}/changes", - CancellationToken.None, - data: (string)change.GetSerializer() - ) - .Result; - var receipt = SerializerFactory.GetSerializer().Deserialize(json); - LogConsumer.Trace(receipt.Receipt); - } - catch (Exception e) - { - throw new ChangeOrderException(orderId, e); - } - } - - #endregion - - #region PCI Gateway - - /// - /// Gets the transaction interactions. - /// - /// The transaction identifier. - /// IEnumerable<TransactionInteraction>. - /// - [Pure] - public IEnumerable GetTransactionInteractions(string transactionId) - { - try - { - LogConsumer.Info("Getting interactions of transaction {0}", transactionId); - var json = _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.GET, - $"{PlatformConstants.PciTransactions}/{transactionId}/interactions", - CancellationToken.None, - restEndpoint: RequestEndpoint.PAYMENTS - ) - .Result; - return SerializerFactory - .GetSerializer>() - .Deserialize(json); - } - catch (Exception e) - { - throw new TransactionException(transactionId, e); - } - } - - #endregion - - #region Stock - - /// - /// get sku reservations as an asynchronous operation. - /// - /// The sku identifier. - /// The warehouse identifier. - /// A Task<System.Int32> representing the asynchronous operation. - public async Task GetSkuReservationsAsync(int skuId, string warehouseId) - { - try - { - LogConsumer.Info( - "Getting reservations of SKU {0} in the warehouse {1}", - skuId, - warehouseId - ); - var source = new CancellationTokenSource(new TimeSpan(0, 5, 0)); - var json = await _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.GET, - $"{PlatformConstants.LogReservations}/{warehouseId}/{skuId}", - source.Token - ) - .ConfigureAwait(false); - var reservations = SerializerFactory - .GetSerializer() - .Deserialize(json); - LogConsumer.Debug(reservations, $"vtex-sku-reservations-{skuId}.js"); - var total = !reservations.Items.Any() ? 0 : reservations.Items.Sum(r => r.Quantity); - LogConsumer.Info( - "The SKU {0} has {1} units reserved in warehouse {2}", - skuId, - total, - warehouseId - ); - return total; - } - catch (Exception e) - { - LogConsumer.Handle(new ProductExportException(skuId, e)); - return 0; - } - } - - /// - /// Gets the sku inventory. - /// - /// The sku identifier. - /// Inventory. - public async Task GetSkuInventoryAsync(int skuId) - { - LogConsumer.Info("Getting inventory of SKU {0}", skuId); - var source = new CancellationTokenSource(new TimeSpan(0, 5, 0)); - var json = await _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.GET, - $"{PlatformConstants.LogInventory}/{skuId}", - source.Token, - restEndpoint: RequestEndpoint.LOGISTICS - ) - .ConfigureAwait(false); - var inventory = SerializerFactory.GetSerializer().Deserialize(json); - LogConsumer.Debug(inventory, $"vtex-sku-inventory-{skuId}.js"); - return inventory; - } - - /// - /// Updates the sku stock. - /// - /// The stock information. - /// A Task representing the asynchronous operation. - /// - public async Task UpdateSkuStockAsync(StockInfo stockInfo) - { - try - { - if (stockInfo.Quantity < 0) - { - stockInfo.Quantity = 0; - } - - stockInfo.DateUtcOnBalanceSystem = null; - if (!stockInfo.UnlimitedQuantity) - { - stockInfo.Quantity += await GetSkuReservationsAsync( - stockInfo.ItemId, - stockInfo.WareHouseId - ) - .ConfigureAwait(false); - } - - LogConsumer.Info( - "Updating inventory of SKU {0} on warehouse {1} with {2} units", - stockInfo.ItemId, - stockInfo.WareHouseId, - stockInfo.Quantity - ); - var source = new CancellationTokenSource(new TimeSpan(0, 5, 0)); - var data = @"[" + (string)stockInfo.GetSerializer() + @"]"; - LogConsumer.Debug(stockInfo, $"vtex-sku-stock-{stockInfo.ItemId}.js"); - await _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.POST, - PlatformConstants.LogWarehouses, - source.Token, - data: data - ) - .ConfigureAwait(false); - } - catch (Exception e) - { - throw new UpdateStockInfoSKUException(stockInfo.ItemId, e); - } - } - - #endregion - - #region Pricing - - /// - /// Get the prices for an SKU. - /// It is possible that on the property "fixedPrices" exists a list of specific prices for Trade Policies and Minimum Quantities of the SKU.Fixed Prices may also be scheduled. - /// - /// The stock keeping unit identifier - /// A task of price - public async Task GetPriceAsync(int skuId) - { - LogConsumer.Info("Getting the price of sku {0}", skuId); - var source = new CancellationTokenSource(new TimeSpan(0, 5, 0)); - try - { - var json = await _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.GET, - $@"{PlatformConstants.Pricing}/{skuId}", - source.Token, - restEndpoint: RequestEndpoint.API - ) - .ConfigureAwait(false); - return SerializerFactory.GetSerializer().Deserialize(json); - } - catch (UnexpectedApiResponseException e) - { - if (e.StatusCode == 404) - { - return new Price(); - } - - throw; - } - } - - /// - /// This method will create or update an SKU Price. - /// The property "basePrice" is the base selling price of the SKU.The property "fixedPrices" is an array where each item is a Fixed Price. - /// The Fixed Price is the price of the SKU for an specific Trade Policy with an specific Minimum Quantity to be activated. - /// A Fixed Price may optionally be scheduled by using the property dateRange. - /// A Fixed Price may optionally overwrite the listPrice specified in the Base Price by using the inner property listPrice. - /// If you don't have specific prices for different Trade Policies, you do not need to send the property fixedPrices. - /// - /// The price data - /// The stock keeping unit identifier - /// The cancellation token. - /// A Task representing the asynchronous operation. - /// - public async Task UpdatePriceAsync(Price price, int skuId, CancellationToken token) - { - try - { - var oldPrice = await GetPriceAsync(skuId).ConfigureAwait(false); - if (oldPrice?.FixedPrices != null && oldPrice.FixedPrices.Any()) - { - await DeletePriceAsync(skuId, token).ConfigureAwait(false); - } - - LogConsumer.Info( - "Updating the price of sku {0} to {1} (list price: {2})", - skuId, - price.CostPrice.ToMonetary(), - price.ListPrice.HasValue ? price.ListPrice.Value.ToMonetary() : "no" - ); - await _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.PUT, - $@"{PlatformConstants.Pricing}/{skuId}", - token, - data: (string)price.GetSerializer(), - restEndpoint: RequestEndpoint.API - ) - .ConfigureAwait(false); - } - catch (Exception e) - { - throw new UpdatePriceInfoSkuException(skuId, e); - } - } - - /// - /// Removes an SKU price. - /// This action removes both Base Price and all available Fixed Prices for and SKU in all trade policies. - /// - /// The stock keeping unit identifier. - /// The cancellation token. - /// Task - public async Task DeletePriceAsync(int skuId, CancellationToken token) - { - LogConsumer.Info("Deleting the price of sku {0}", skuId); - await _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.DELETE, - $@"{PlatformConstants.Pricing}/{skuId}", - token, - restEndpoint: RequestEndpoint.API - ) - .ConfigureAwait(false); - } - - /// - /// Retrieves a collection of bridge facets based on the specified query and optional keywords. - /// - /// The query string used to filter the bridge facets. - /// Optional keywords to further refine the search for bridge facets. - /// An enumerable collection of objects that match the specified query and keywords. - /// - /// This method constructs a query to fetch bridge facets from a remote service. It logs the action of retrieving facets - /// and sets a timeout of 5 minutes for the operation. The method builds a dictionary of query parameters, including - /// facets to retrieve and the specified query. If keywords are provided, they are added to the query parameters as well. - /// The method then invokes the service asynchronously and deserializes the resulting JSON response into a list of - /// objects. If an exception occurs during this process, a custom - /// is thrown, encapsulating the original exception and the query that caused the failure. - /// - /// Thrown when an error occurs while retrieving bridge facets. - [Pure] - public IEnumerable GetBridgeFacets( - [Localizable(false)] string query, - [Localizable(false)] string keywords = null - ) - { - try - { - LogConsumer.Info( - "Getting facets in bridge module that satisfy the condition '{0}'", - query - ); - var source = new CancellationTokenSource(new TimeSpan(0, 5, 0)); - var queryString = new Dictionary - { - { @"_facets", @"Origin,Status" }, - { @"_where", query }, - }; - if (!string.IsNullOrWhiteSpace(keywords)) - { - queryString.Add(@"_keywords", $@"*{keywords}*"); - } - - var json = _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.GET, - $"{PlatformConstants.BridgeSearch}/facets", - source.Token, - queryString, - restEndpoint: RequestEndpoint.BRIDGE - ) - .Result; - return SerializerFactory.GetSerializer>().Deserialize(json); - } - catch (Exception e) - { - throw new BridgeException(query, e); - } - } - - /// - /// Retrieves a collection of bridge items based on the specified query parameters. - /// - /// The query string used to filter the bridge items. - /// The sorting criteria for the returned items. - /// Additional keywords to refine the search results. - /// The starting point for the items to be retrieved. - /// The maximum number of items to return. - /// An enumerable collection of that match the specified criteria. - /// - /// This method interacts with an external service to fetch bridge items based on the provided query, sort, and keywords. - /// It logs the request details and checks for an offset limit to avoid exceeding the maximum allowed items from the service. - /// If the offset exceeds 10,000, a warning is logged, and an empty list is returned. - /// The method uses a cancellation token to set a timeout for the service call, ensuring that it does not hang indefinitely. - /// In case of an error during the service call, it throws a custom exception with details about the failure. - /// - /// - /// Thrown when an error occurs while retrieving bridge items from the external service. - /// - [Pure] - public IEnumerable GetBridgeItems( - [Localizable(false)] string query, - [Localizable(false)] string sort, - [Localizable(false)] string keywords, - int offSet, - int limit - ) - { - try - { - LogConsumer.Info( - "Getting {0} items from {1} in bridge module that satisfy the condition '{2}'", - limit, - offSet, - query - ); - if (offSet >= 10000) - { - LogConsumer.Warning( - "Cannot get more than 10000 items from Bridge / Master Data (VTEX Elastic Search limitation)" - ); - return new List(); - } - var source = new CancellationTokenSource(new TimeSpan(0, 5, 0)); - var queryString = new Dictionary - { - { @"_where", query }, - { @"_sort", sort }, - { @"offSet", offSet.ToString() }, - { @"limit", limit.ToString() }, - }; - if (!string.IsNullOrWhiteSpace(keywords)) - { - queryString.Add(@"_keywords", $@"*{keywords}*"); - } - - var json = _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.GET, - PlatformConstants.BridgeSearch, - source.Token, - queryString, - restEndpoint: RequestEndpoint.BRIDGE - ) - .Result; - return SerializerFactory.GetSerializer>().Deserialize(json); - } - catch (AggregateException e) - { - throw new BridgeException( - query, - e.InnerExceptions.FirstOrDefault() ?? e.InnerException ?? e - ); - } - catch (Exception e) - { - throw new BridgeException(query, e); - } - } - - /// - /// Gets all bridge items. - /// - /// The query. - /// The sort. - /// The keywords. - /// Name of the facet. - /// The facet value. - /// IEnumerable<BridgeItem>. - [Pure] - public IEnumerable GetAllBridgeItems( - [Localizable(false)] string query, - [Localizable(false)] string sort, - [Localizable(false)] string keywords, - [Localizable(false)] string facetName, - [Localizable(false)] string facetValue - ) - { - const int perPage = 100; - var facets = GetBridgeFacets(query, keywords); - var total = facets.Single(f => f.Field.Equals(facetName)).Facets[facetValue].ToInt32(); - - var result = new List(total); - var pages = (total / perPage) + 1; - for (var x = 0; x < pages; x++) - { - result.AddRange(GetBridgeItems(query, sort, keywords, x * perPage, perPage)); - } - - return result; - } - - #endregion - - #region Platform status - - /// - /// Gets the platform status. - /// - /// IEnumerable<PlatformStatus>. - public IEnumerable GetPlatformStatus() - { - return GetPlatformStatusAsync(CancellationToken.None).Result; - } - - /// - /// Gets the platform status asynchronous. - /// - /// The token. - /// A Task<IEnumerable`1> representing the asynchronous operation. - [Pure] - public async Task> GetPlatformStatusAsync( - CancellationToken token - ) - { - LogConsumer.Info("Getting platform status"); - var json = await _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.GET, - string.Empty, - token, - restEndpoint: RequestEndpoint.HEALTH - ) - .ConfigureAwait(false); - var status = SerializerFactory.GetSerializer>().Deserialize(json); - LogConsumer.Debug(status, "vtex-platform-status.js"); - return status; - } - - #endregion - - #region Order payments - - /// - /// Gets the order payments. - /// - /// The transaction identifier. - /// List<PciPayment>. - [Pure] - public List GetOrderPayments(string transactionId) - { - var json = _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.GET, - $"{PlatformConstants.PciTransactions}/{transactionId}/payments", - CancellationToken.None, - restEndpoint: RequestEndpoint.PAYMENTS - ) - .Result; - if (json == null) - { - return new List(); - } - - var data = SerializerFactory - .GetCustomSerializer>(SerializerFormat.Json) - .Deserialize(json); - LogConsumer.Debug(data, $"vtex-order-payemnts-{transactionId}.js"); - return data; - } - - #endregion - - #region Catalog - - #region Specification - - /// - /// Gets the specification field asynchronous. - /// - /// The field identifier. - /// The token. - /// A Task<SpecificationField> representing the asynchronous operation. - [Pure] - public async Task GetSpecificationFieldAsync( - int fieldId, - CancellationToken token - ) - { - LogConsumer.Info("Getting field for the field id {0}", fieldId); - var json = await _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.GET, - $@"{PlatformConstants.CatalogPub}/specification/fieldGet/{fieldId}", - token - ) - .ConfigureAwait(false); - var field = SerializerFactory.GetSerializer().Deserialize(json); - LogConsumer.Debug(field, $"vtex-specification-field-{fieldId}.js"); - return field; - } - - /// - /// Gets the specification field values asynchronous. - /// - /// The field identifier. - /// The token. - /// A Task<ICollection`1> representing the asynchronous operation. - [Pure] - public async Task> GetSpecificationFieldValuesAsync( - int fieldId, - CancellationToken token - ) - { - LogConsumer.Info("Getting field values for the field id {0}", fieldId); - var json = await _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.GET, - $@"{PlatformConstants.CatalogPub}/specification/fieldvalue/{fieldId}", - token - ) - .ConfigureAwait(false); - var fieldValues = SerializerFactory - .GetSerializer>() - .Deserialize(json); - LogConsumer.Debug(fieldValues, $"vtex-specification-values-{fieldId}.js"); - return fieldValues; - } - - /// - /// Updates the product specification asynchronous. - /// - /// The specification. - /// The product identifier. - /// The token. - /// A Task representing the asynchronous operation. - public async Task UpdateProductSpecificationAsync( - Specification specification, - int productId, - CancellationToken token - ) - { - await UpdateProductSpecificationsAsync( - new List(new[] { specification }), - productId, - token - ) - .ConfigureAwait(false); - } - - /// - /// Updates the product specifications asynchronous. - /// - /// The specifications list. - /// The product identifier. - /// The token. - /// A Task representing the asynchronous operation. - public async Task UpdateProductSpecificationsAsync( - List specifications, - int productId, - CancellationToken token - ) - { - LogConsumer.Info( - "Updating the specifications {1} of product {0}", - productId, - string.Join(@",", specifications.Select(s => s.Id)) - ); - - var data = (string)specifications.GetSerializer(); - await _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.POST, - $@"{PlatformConstants.Catalog}/products/{productId}/specification", - token, - data: data - ) - .ConfigureAwait(false); - } - - /// - /// Inserts the specification field value asynchronous. - /// - /// The field value. - /// The token. - /// A Task representing the asynchronous operation. - public async Task InsertSpecificationFieldValueAsync( - SpecificationFieldValue fieldValue, - CancellationToken token - ) - { - LogConsumer.Info("Creating field value of field id {0}", fieldValue.FieldId); - var data = (string)fieldValue.GetSerializer(); - await _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.POST, - $@"{PlatformConstants.Catalog}/specification/fieldValue", - token, - data: data - ) - .ConfigureAwait(false); - } - - /// - /// Asynchronously searches for a data entity based on a specified field and value. - /// - /// The type of the data entity to search for, which must implement . - /// The field of the data entity to search against. - /// The value to search for in the specified field. - /// A cancellation token to monitor for cancellation requests. - /// A task that represents the asynchronous operation. The task result contains the found data entity of type or null if no entity is found. - /// - /// This method performs an asynchronous search for a data entity by sending a GET request to the specified endpoint. - /// It constructs a query string using the provided field and value, and invokes a service to retrieve the data. - /// If the search value is null or whitespace, an is thrown. - /// In case of an unexpected API response, an is thrown, containing the JSON response and the original exception. - /// The method logs the retrieved entity for debugging purposes. - /// - /// Thrown when is null or whitespace. - /// Thrown when the API response is unexpected. - [Pure] - public async Task SearchAsync( - string searchedField, - string searchedValue, - CancellationToken token - ) - where TDataEntity : class, IDataEntity, new() - { - if (string.IsNullOrWhiteSpace(searchedValue)) - { - throw new ArgumentNullException(nameof(searchedValue)); - } - - var queryString = new Dictionary - { - { searchedField, searchedValue }, - { @"_fields", @"_all" }, - }; - var json = string.Empty; - try - { - var entityName = typeof(TDataEntity).GetDataEntityName(); - json = await _wrapper - .ServiceInvokerAsync( - HttpRequestMethod.GET, - $@"dataentities/{entityName}/search/", - token, - queryString, - restEndpoint: RequestEndpoint.MASTER_DATA - ) - .ConfigureAwait(false); - var entity = SerializerFactory - .GetSerializer>() - .Deserialize(json) - .FirstOrDefault(); - if (entity == null) - { - return null; - } - - LogConsumer.Debug( - entity, - $@"vtex-masterdata-entity-{entityName}-{searchedField}-{searchedValue}.js" - ); - return entity; - } - catch (Exception e) - { - throw new UnexpectedApiResponseException(json, e); - } - } - - #endregion - - #endregion - - #endregion - - #region IDisposable - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public void Dispose() - { - _wrapper.Dispose(); - } - - #endregion - } -} +using System; +using System.Collections.Generic; + +namespace VTEX +{ + public class Collection + { + public int Id { get; set; } + public string Name { get; set; } + } +// *********************************************************************** +// Assembly : VTEX +// Author : Guilherme Branco Stracini +// Created : 01-15-2023 +// +// Last Modified By : Guilherme Branco Stracini +// Last Modified On : 01-16-2023 +// *********************************************************************** +// +// © 2020 Guilherme Branco Stracini. All rights reserved. +// +// +// *********************************************************************** +namespace VTEX +{ + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using CrispyWaffle.Extensions; + using CrispyWaffle.Log; + using CrispyWaffle.Serialization; + using Newtonsoft.Json; + using VTEX.DataEntities; + using VTEX.Enums; + using VTEX.Extensions; + using VTEX.GoodPractices; + using VTEX.Health; + using VTEX.Transport; + using VTEX.Transport.Bridge; + + /// + /// A VTEX Context, that consumes the VTEX Wrapper + /// + /// + public sealed class VTEXContext : IDisposable + { + #region Private fields + + /// + /// The wrapper + /// + private readonly VTEXWrapper _wrapper; + + #endregion + + #region ~Ctor + + /// + /// Initializes a new instance of the class. + /// + /// Name of the account. + /// The application key. + /// The application token. + /// The cookie. + /// appKey + /// appToken + public VTEXContext(string accountName, string appKey, string appToken, string cookie = null) + { + _wrapper = new VTEXWrapper(accountName); + if (string.IsNullOrWhiteSpace(appKey)) + { + throw new ArgumentNullException(nameof(appKey)); + } + + if (string.IsNullOrWhiteSpace(appToken)) + { + throw new ArgumentNullException(nameof(appToken)); + } + + _wrapper.SetRestCredentials(appKey, appToken); + if (string.IsNullOrWhiteSpace(cookie)) + { + return; + } + + _wrapper.SetVtexIdClientAuthCookie(cookie); + } + + /// + /// Retrieves a list of orders based on specified filtering criteria. + /// + /// The status of the orders to filter by (optional). + /// The start date for filtering orders (optional). + /// The end date for filtering orders (optional). + /// The sales channel to filter by (optional). + /// The affiliated ID to filter by (optional). + /// The payment system name to filter by (optional). + /// A generic query string for additional filtering (optional). + /// An instance of containing the filtered orders. + /// + /// This method constructs a query string based on the provided parameters to filter the orders. + /// It supports pagination and retrieves orders in pages of 50 until no more orders are found. + /// The filtering criteria include order status, sales channel, affiliated ID, payment system name, + /// and a date range defined by start and end dates. The results are logged indicating the number of orders found. + /// + private OrdersList GetOrdersListInternal( + string status = null, + DateTime? startDate = null, + DateTime? endDate = null, + string salesChannel = null, + string affiliatedId = null, + string paymentSystemName = null, + string genericQuery = null + ) + { + OrdersList result = null; + var currentPage = 1; + var queryString = new Dictionary + { + { @"page", @"0" }, + { @"per_page", @"50" }, + }; + if (!string.IsNullOrWhiteSpace(status)) + { + queryString.Add(@"f_status", status); + } + + if (!string.IsNullOrWhiteSpace(salesChannel)) + { + queryString.Add(@"f_salesChannel", salesChannel); + } + + if (!string.IsNullOrWhiteSpace(affiliatedId)) + { + queryString.Add(@"f_affiliateId", affiliatedId); + } + + if (!string.IsNullOrWhiteSpace(paymentSystemName)) + { + queryString.Add(@"f_paymentNames", paymentSystemName); + } + + if (startDate.HasValue && endDate.HasValue) + { + queryString.Add( + @"f_creationDate", + $@"creationDate:[{startDate.Value.ToUniversalTime():s}Z TO {endDate.Value.ToUniversalTime():s}Z]" + ); + } + + if (!string.IsNullOrWhiteSpace(genericQuery)) + { + queryString.Add(@"q", genericQuery); + } + + queryString.Add(@"orderBy", @"creationDate,asc"); + while (GetOrderListsValueInternal(queryString, currentPage, ref result)) + { + currentPage++; + } + + LogConsumer.Info("{0} orders found", result.List.Length); + return result; + } + + /// + /// Gets the order lists value internal. + /// + /// The query string. + /// The current page. + /// The result. + /// true if XXXX, false otherwise. + /// + private bool GetOrderListsValueInternal( + Dictionary queryString, + int currentPage, + ref OrdersList result + ) + { + var json = string.Empty; + try + { + LogConsumer.Trace("Getting page {0} of orders list", currentPage); + queryString[@"page"] = currentPage.ToString(CultureInfo.InvariantCulture); + + json = _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.GET, + PlatformConstants.OmsOrders, + CancellationToken.None, + queryString + ) + .Result; + var temp = SerializerFactory.GetSerializer().Deserialize(json); + if (result == null) + { + result = temp; + } + else + { + result.List = result.List.Concat(temp.List).ToArray(); + } + + if (temp.Paging.Pages == 1 || temp.Paging.CurrentPage >= temp.Paging.Pages) + { + return false; + } + + if (currentPage == 1) + { + LogConsumer.Trace("{0} pages of orders list", temp.Paging.Pages); + } + + return true; + } + catch (JsonSerializationException e) + { + throw new UnexpectedApiResponseException(json, e); + } + } + + /// + /// Gets the orders by order's ids. + /// + /// The order's ids. + /// IEnumerable<Order>. + private IEnumerable GetOrdersInternal(IEnumerable ordersIds) + { + var list = new List(); + Parallel.ForEach(ordersIds, orderId => list.Add(GetOrder(orderId))); + return list; + } + + /// + /// Get a order by order id + /// + /// The id of the order + /// Order. + /// + /// + private Order GetOrderInternal(string orderId) + { + LogConsumer.Trace("Getting order {0}", orderId); + var json = _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.GET, + $"{PlatformConstants.OmsOrders}/{orderId}", + CancellationToken.None + ) + .Result; + if (json == null) + { + return null; + } + + try + { + var order = SerializerFactory.GetSerializer().Deserialize(json); + + #region Payment + + var transaction = order.PaymentData.Transactions.First(); + var payment = transaction.Payments.FirstOrDefault(); + if ( + payment != null + && payment.PaymentSystem == 0 + && !string.IsNullOrWhiteSpace(order.AffiliateId) + ) + { + LogConsumer.Info(@"Marketplace {0}", order.AffiliateId); + } + else if ( + transaction.TransactionId != null + && !transaction.TransactionId.Equals( + @"NO-PAYMENT", + StringComparison.InvariantCultureIgnoreCase + ) + ) + { + LogConsumer.Info(@"Bank bill {0}", order.Sequence); + } + else if (order.Totals.Sum(t => t.Value) == 0) + { + LogConsumer.Warning("Promotion / discount coupon - order subsidized"); + } + else + { + throw new InvalidPaymentDataException(orderId); + } + + #endregion + + #region Email + + if (!string.IsNullOrWhiteSpace(order.ClientProfileData.UserProfileId)) + { + var client = SearchAsync( + @"userId", + order.ClientProfileData.UserProfileId, + CancellationToken.None + ).Result; + if (client != null && !string.IsNullOrWhiteSpace(client.Email)) + { + order.ClientProfileData.Email = client.Email; + } + + if ( + order.ClientProfileData.Email.IndexOf( + @"ct.vtex", + StringComparison.InvariantCultureIgnoreCase + ) != -1 + ) + { + order.ClientProfileData.Email = @"pedido@editorainovacao.com.br"; + } + } + + #endregion + + LogConsumer.Debug(order, $"vtex-order-{orderId}.js"); + var affiliated = string.IsNullOrWhiteSpace(order.AffiliateId) + ? string.Empty + : $" - Affiliated: {order.AffiliateId}"; + LogConsumer.Info( + "Order: {0} - Sequence: {1} - Status: {2} - Sales channel: {3}{4}", + order.OrderId, + order.Sequence, + order.Status.GetHumanReadableValue(), + order.SalesChannel, + affiliated + ); + return order; + } + catch (JsonSerializationException e) + { + throw new UnexpectedApiResponseException(json, e); + } + } + + #endregion + + #region Public Methods + + #region OMS + + /// + /// Gets the feed. + /// + /// The maximum lot. + /// IEnumerable<OrderFeed>. + public IEnumerable GetFeed(int maxLot = 20) + { + //VTEX limitation + if (maxLot > 20) + { + maxLot = 20; + } + + LogConsumer.Trace("Getting up to {0} events in order feed", maxLot); + var json = _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.GET, + $"{PlatformConstants.OmsFeed}", + CancellationToken.None, + new Dictionary { { @"maxLot", maxLot.ToString() } } + ) + .Result; + return SerializerFactory.GetSerializer>().Deserialize(json); + } + + /// + /// Commits the feed. + /// + /// The feed. + public void CommitFeed(OrderFeed feed) + { + LogConsumer.Trace("Commiting feed of order {0}", feed.OrderId); + var data = (string) + new OrderFeedCommit { CommitToken = feed.CommitToken }.GetSerializer(); + _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.POST, + $"{PlatformConstants.OmsFeed}confirm", + CancellationToken.None, + data: data + ) + .Wait(); + } + + /// + /// Get a order by order id + /// + /// The id of the order + /// Order. + /// + public Order GetOrder(string orderId) + { + return GetOrderInternal(orderId); + } + + /// + /// Gets the orders list metadata. + /// + /// The status. + /// IEnumerable<List>. + public IEnumerable GetOrdersList(OrderStatus status) + { + LogConsumer.Warning("Getting orders with status {0}", status.GetHumanReadableValue()); + var orders = GetOrdersListInternal(status.GetInternalValue()); + return orders.List; + } + + /// + /// Get a Enumerable list of Order by status. + /// + /// The status of the orders to get + /// IEnumerable<Order>. + public IEnumerable GetOrders(OrderStatus status) + { + var ordersIds = GetOrdersList(status).Select(order => order.OrderId).ToList(); + if (ordersIds.Any()) + { + return GetOrdersInternal(ordersIds); + } + + LogConsumer.Warning("No orders with status {0} found", status.GetHumanReadableValue()); + return new Order[0]; + } + + /// + /// Gets the orders list by a date range of order's placed date. + /// + /// The start date of the range. + /// The end date of the range. + /// IEnumerable<String>. + public IEnumerable GetOrdersList(DateTime startDate, DateTime endDate) + { + LogConsumer.Warning("Getting orders between {0:G} and {1:G}", startDate, endDate); + var orders = GetOrdersListInternal(startDate: startDate, endDate: endDate); + return orders.List; + } + + /// + /// Get a Enumerable list of Order by a date range of order's placed date. + /// + /// The start date of the range + /// The end date of the range + /// IEnumerable<Order>. + public IEnumerable GetOrders(DateTime startDate, DateTime endDate) + { + var ordersIds = GetOrdersList(startDate, endDate) + .Select(order => order.OrderId) + .ToList(); + if (ordersIds.Any()) + { + return GetOrdersInternal(ordersIds); + } + + LogConsumer.Warning("No orders between {0:G} and {1:G} found", startDate, endDate); + return new Order[0]; + } + + /// + /// Gets the orders list by status and date range of order's placed date. + /// + /// The status of orders to get. + /// The start date of the range. + /// The end date of the range. + /// IEnumerable<String>. + public IEnumerable GetOrdersList( + OrderStatus status, + DateTime startDate, + DateTime endDate + ) + { + LogConsumer.Warning( + "Getting orders with status {0} between {1:G} and {2:G}", + status.GetHumanReadableValue(), + startDate, + endDate + ); + var orders = GetOrdersListInternal(status.GetInternalValue(), startDate, endDate); + return orders.List; + } + + /// + /// Get a Enumerable list of Order by status and date range of order's placed date. + /// + /// The status of orders to get. + /// The start date of the range. + /// The end date of the range. + /// IEnumerable<Order>. + public IEnumerable GetOrders( + OrderStatus status, + DateTime startDate, + DateTime endDate + ) + { + var ordersIds = GetOrdersList(status, startDate, endDate) + .Select(order => order.OrderId) + .ToList(); + if (ordersIds.Any()) + { + return GetOrdersInternal(ordersIds); + } + + LogConsumer.Warning( + "No order with status {0} between {1:G} and {2:G} found", + status.GetHumanReadableValue(), + startDate, + endDate + ); + return new Order[0]; + } + + /// + /// Gets the orders list by status and affiliated identifier (AKA marketplace). + /// + /// The status of orders to get. + /// The affiliated identifier + /// IEnumerable<String>. + public IEnumerable GetOrdersList(OrderStatus status, string affiliatedId) + { + LogConsumer.Warning( + "Getting orders with status {0} and affiliated {1}", + status.GetHumanReadableValue(), + affiliatedId + ); + var orders = GetOrdersListInternal( + status.GetInternalValue(), + affiliatedId: affiliatedId + ); + return orders.List; + } + + /// + /// Get a Enumerable list of Order by status and affiliated identifier (AKA marketplace). + /// + /// The status of orders to get. + /// The affiliated identifier + /// IEnumerable<Order>. + public IEnumerable GetOrders(OrderStatus status, string affiliatedId) + { + var ordersIds = GetOrdersList(status, affiliatedId) + .Select(order => order.OrderId) + .ToList(); + if (ordersIds.Any()) + { + return GetOrdersInternal(ordersIds); + } + + LogConsumer.Warning( + "No order with status {0} and affiliated {1} found", + status.GetHumanReadableValue(), + affiliatedId + ); + return new Order[0]; + } + + /// + /// Gets the orders list by generic query (order id, client's document, sequence, etc). + /// + /// The query to lookup in orders. + /// IEnumerable<String>. + public IEnumerable GetOrdersList(string query) + { + LogConsumer.Warning("Getting orders with term '{0}'", query); + var orders = GetOrdersListInternal(genericQuery: query); + return orders.List; + } + + /// + /// Gets the orders by generic query (order identifier, client's document, sequence, etc). + /// + /// The query to lookup in orders. + /// IEnumerable<Order>. + public IEnumerable GetOrders(string query) + { + var ordersIds = GetOrdersList(query).Select(order => order.OrderId).ToList(); + if (ordersIds.Any()) + { + return GetOrdersInternal(ordersIds); + } + + LogConsumer.Warning("No orders with term '{0}' found", query); + return new Order[0]; + } + + /// + /// Gets the orders by the array of orders identifiers + /// + /// The orders ids. + /// IEnumerable<Order>. + public IEnumerable GetOrders(string[] ordersIds) + { + return GetOrdersInternal(ordersIds); + } + + /// + /// Cancels the order asynchronous. + /// + /// The order identifier. + /// A Task<System.String> representing the asynchronous operation. + /// Order {orderId} cannot be canceled because isn't in pending payment status on VTEX + public async Task CancelOrderAsync(string orderId) + { + try + { + LogConsumer.Warning("Cancelling order {0}", orderId); + var source = new CancellationTokenSource(new TimeSpan(0, 5, 0)); + var order = GetOrder(orderId); + if (order.Status == OrderStatus.CANCELED) + { + return string.Empty; + } + + if ( + order.Status != OrderStatus.PAYMENT_PENDING + && order.Status != OrderStatus.AWAITING_AUTHORIZATION_TO_DISPATCH + ) + { + throw new InvalidOperationException( + $"Order {orderId} cannot be canceled because isn't in pending payment status on VTEX" + ); + } + + var json = await _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.POST, + $"{PlatformConstants.OmsOrders}/{orderId}/cancel", + source.Token + ) + .ConfigureAwait(false); + var receipt = SerializerFactory + .GetSerializer() + .Deserialize(json); + LogConsumer.Info( + "Order {0} successfully canceled. Receipt: {1}", + order.Sequence, + receipt.Receipt + ); + return receipt.Receipt; + } + catch (Exception e) + { + LogConsumer.Handle(new CancelOrderException(orderId, e)); + return string.Empty; + } + } + + /// + /// Changes the order status asynchronous. + /// + /// The order identifier. + /// The new status. + /// A Task representing the asynchronous operation. + /// + public async Task ChangeOrderStatusAsync(string orderId, OrderStatus newStatus) + { + try + { + LogConsumer.Info( + "Changing order {0} status to {1}", + orderId, + newStatus.GetHumanReadableValue() + ); + var source = new CancellationTokenSource(new TimeSpan(0, 5, 0)); + var json = await _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.POST, + $"{PlatformConstants.OmsOrders}/{orderId}/changestate/{newStatus.GetInternalValue()}", + source.Token + ) + .ConfigureAwait(false); + LogConsumer.Info(json); + } + catch (AggregateException e) + { + var ae = e.InnerExceptions.First(); + throw new ChangeStatusOrderException( + orderId, + newStatus.GetHumanReadableValue(), + ae + ); + } + catch (Exception e) + { + throw new ChangeStatusOrderException(orderId, newStatus.GetHumanReadableValue(), e); + } + } + + /// + /// Notifies the order paid asynchronous. + /// + /// The order identifier. + /// A Task representing the asynchronous operation. + public async Task NotifyOrderPaidAsync(string orderId) + { + try + { + LogConsumer.Info("Sending payment notification of order {0}", orderId); + var source = new CancellationTokenSource(new TimeSpan(0, 5, 0)); + var order = GetOrder(orderId); + if ( + order.Status != OrderStatus.PAYMENT_PENDING + && order.Status != OrderStatus.AWAITING_AUTHORIZATION_TO_DISPATCH + ) + { + return; + } + + if (order.Status == OrderStatus.AWAITING_AUTHORIZATION_TO_DISPATCH) + { + await ChangeOrderStatusAsync(order.OrderId, OrderStatus.AUTHORIZE_FULFILLMENT) + .ConfigureAwait(false); + return; + } + var paymentId = order.PaymentData.Transactions.First().Payments.First().Id; + _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.POST, + $"{PlatformConstants.OmsOrders}/{order.OrderId}/payments/{paymentId}/payment-notification", + source.Token + ) + .Wait(source.Token); + } + catch (Exception e) + { + LogConsumer.Handle(new PaymentNotificationOrderException(orderId, e)); + } + } + + /// + /// Notifies the order shipped. + /// + /// The order identifier. + /// The notification. + /// + public void NotifyOrderShipped(string orderId, ShippingNotification notification) + { + NotifyOrderShippedAsync(orderId, notification, CancellationToken.None).Wait(); + } + + /// + /// Notifies the order shipped async. + /// + /// The order identifier. + /// The notification. + /// The token. + /// A Task<System.String> representing the asynchronous operation. + /// + public async Task NotifyOrderShippedAsync( + string orderId, + ShippingNotification notification, + CancellationToken token + ) + { + try + { + LogConsumer.Info("Sending shipping notification of order {0}", orderId); + LogConsumer.Debug( + notification, + $"vtex-shipping-notification-{orderId}-{notification.InvoiceNumber}.js" + ); + var json = await _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.POST, + $"{PlatformConstants.OmsInvoices}/{orderId}/invoice", + token, + data: (string)notification.GetSerializer() + ) + .ConfigureAwait(false); + var receipt = SerializerFactory.GetSerializer().Deserialize(json); + LogConsumer.Trace(receipt.Receipt); + return receipt.Receipt; + } + catch (AggregateException e) + { + var ae = e.InnerExceptions.First(); + throw new ShippingNotificationOrderException(orderId, ae); + } + catch (Exception e) + { + throw new ShippingNotificationOrderException(orderId, e); + } + } + + /// + /// Notifies the order delivered + /// + /// The tracking. + /// System.String. + /// + public async ValueTask NotifyOrderDelivered(Tracking tracking) + { + try + { + LogConsumer.Info("Sending tracking info of order {0}", tracking.OrderId); + var source = new CancellationTokenSource(new TimeSpan(0, 5, 0)); + LogConsumer.Debug( + tracking, + $"vtex-tracking-info-{tracking.OrderId}-{tracking.InvoiceNumber}.js" + ); + var json = await _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.PUT, + string.Format( + PlatformConstants.OmsTracking, + tracking.OrderId, + tracking.InvoiceNumber + ), + source.Token, + data: (string)tracking.GetSerializer() + ) + .ConfigureAwait(false); + var receipt = SerializerFactory.GetSerializer().Deserialize(json); + LogConsumer.Trace(receipt.Receipt); + return receipt.Receipt; + } + catch (Exception e) + { + throw new TrackingNotificationOrderException(tracking.OrderId, e); + } + } + + /// + /// Updates the order invoice. + /// + /// The order identifier. + /// The invoice identifier. + /// The notification. + /// + public void UpdateOrderInvoice( + string orderId, + string invoiceId, + ShippingNotificationPatch notification + ) + { + try + { + LogConsumer.Info("Patching fiscal invoice {1} of order {0}", orderId, invoiceId); + var source = new CancellationTokenSource(new TimeSpan(0, 5, 0)); + LogConsumer.Debug( + notification, + $"vtex-shipping-notification-{orderId}-{invoiceId}.js" + ); + var json = _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.PATCH, + $"{PlatformConstants.OmsOrders}/{orderId}/invoice/{invoiceId}", + source.Token, + data: (string)notification.GetSerializer() + ) + .Result; + var receipt = SerializerFactory.GetSerializer().Deserialize(json); + LogConsumer.Trace(receipt.Receipt); + } + catch (Exception e) + { + throw new ShippingNotificationOrderException(orderId, e); + } + } + + /// + /// Changes the order. + /// + /// The order identifier. + /// The change. + /// + public void ChangeOrder(string orderId, ChangeOrder change) + { + try + { + LogConsumer.Info("Changing order {0}", orderId); + LogConsumer.Debug(change, $"vtex-change-order-{orderId}.js"); + var json = _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.POST, + $"{PlatformConstants.OmsOrders}/{orderId}/changes", + CancellationToken.None, + data: (string)change.GetSerializer() + ) + .Result; + var receipt = SerializerFactory.GetSerializer().Deserialize(json); + LogConsumer.Trace(receipt.Receipt); + } + catch (Exception e) + { + throw new ChangeOrderException(orderId, e); + } + } + + #endregion + + #region PCI Gateway + + /// + /// Gets the transaction interactions. + /// + /// The transaction identifier. + /// IEnumerable<TransactionInteraction>. + /// + [Pure] + public IEnumerable GetTransactionInteractions(string transactionId) + { + try + { + LogConsumer.Info("Getting interactions of transaction {0}", transactionId); + var json = _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.GET, + $"{PlatformConstants.PciTransactions}/{transactionId}/interactions", + CancellationToken.None, + restEndpoint: RequestEndpoint.PAYMENTS + ) + .Result; + return SerializerFactory + .GetSerializer>() + .Deserialize(json); + } + catch (Exception e) + { + throw new TransactionException(transactionId, e); + } + } + + #endregion + + #region Stock + + /// + /// get sku reservations as an asynchronous operation. + /// + /// The sku identifier. + /// The warehouse identifier. + /// A Task<System.Int32> representing the asynchronous operation. + public async Task GetSkuReservationsAsync(int skuId, string warehouseId) + { + try + { + LogConsumer.Info( + "Getting reservations of SKU {0} in the warehouse {1}", + skuId, + warehouseId + ); + var source = new CancellationTokenSource(new TimeSpan(0, 5, 0)); + var json = await _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.GET, + $"{PlatformConstants.LogReservations}/{warehouseId}/{skuId}", + source.Token + ) + .ConfigureAwait(false); + var reservations = SerializerFactory + .GetSerializer() + .Deserialize(json); + LogConsumer.Debug(reservations, $"vtex-sku-reservations-{skuId}.js"); + var total = !reservations.Items.Any() ? 0 : reservations.Items.Sum(r => r.Quantity); + LogConsumer.Info( + "The SKU {0} has {1} units reserved in warehouse {2}", + skuId, + total, + warehouseId + ); + return total; + } + catch (Exception e) + { + LogConsumer.Handle(new ProductExportException(skuId, e)); + return 0; + } + } + + /// + /// Gets the sku inventory. + /// + /// The sku identifier. + /// Inventory. + public async Task GetSkuInventoryAsync(int skuId) + { + LogConsumer.Info("Getting inventory of SKU {0}", skuId); + var source = new CancellationTokenSource(new TimeSpan(0, 5, 0)); + var json = await _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.GET, + $"{PlatformConstants.LogInventory}/{skuId}", + source.Token, + restEndpoint: RequestEndpoint.LOGISTICS + ) + .ConfigureAwait(false); + var inventory = SerializerFactory.GetSerializer().Deserialize(json); + LogConsumer.Debug(inventory, $"vtex-sku-inventory-{skuId}.js"); + return inventory; + } + + /// + /// Updates the sku stock. + /// + /// The stock information. + /// A Task representing the asynchronous operation. + /// + public async Task UpdateSkuStockAsync(StockInfo stockInfo) + { + try + { + if (stockInfo.Quantity < 0) + { + stockInfo.Quantity = 0; + } + + stockInfo.DateUtcOnBalanceSystem = null; + if (!stockInfo.UnlimitedQuantity) + { + stockInfo.Quantity += await GetSkuReservationsAsync( + stockInfo.ItemId, + stockInfo.WareHouseId + ) + .ConfigureAwait(false); + } + + LogConsumer.Info( + "Updating inventory of SKU {0} on warehouse {1} with {2} units", + stockInfo.ItemId, + stockInfo.WareHouseId, + stockInfo.Quantity + ); + var source = new CancellationTokenSource(new TimeSpan(0, 5, 0)); + var data = @"[" + (string)stockInfo.GetSerializer() + @"]"; + LogConsumer.Debug(stockInfo, $"vtex-sku-stock-{stockInfo.ItemId}.js"); + await _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.POST, + PlatformConstants.LogWarehouses, + source.Token, + data: data + ) + .ConfigureAwait(false); + } + catch (Exception e) + { + throw new UpdateStockInfoSKUException(stockInfo.ItemId, e); + } + } + + #endregion + + #region Pricing + + /// + /// Get the prices for an SKU. + /// It is possible that on the property "fixedPrices" exists a list of specific prices for Trade Policies and Minimum Quantities of the SKU.Fixed Prices may also be scheduled. + /// + /// The stock keeping unit identifier + /// A task of price + public async Task GetPriceAsync(int skuId) + { + LogConsumer.Info("Getting the price of sku {0}", skuId); + var source = new CancellationTokenSource(new TimeSpan(0, 5, 0)); + try + { + var json = await _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.GET, + $@"{PlatformConstants.Pricing}/{skuId}", + source.Token, + restEndpoint: RequestEndpoint.API + ) + .ConfigureAwait(false); + return SerializerFactory.GetSerializer().Deserialize(json); + } + catch (UnexpectedApiResponseException e) + { + if (e.StatusCode == 404) + { + return new Price(); + } + + throw; + } + } + + /// + /// This method will create or update an SKU Price. + /// The property "basePrice" is the base selling price of the SKU.The property "fixedPrices" is an array where each item is a Fixed Price. + /// The Fixed Price is the price of the SKU for an specific Trade Policy with an specific Minimum Quantity to be activated. + /// A Fixed Price may optionally be scheduled by using the property dateRange. + /// A Fixed Price may optionally overwrite the listPrice specified in the Base Price by using the inner property listPrice. + /// If you don't have specific prices for different Trade Policies, you do not need to send the property fixedPrices. + /// + /// The price data + /// The stock keeping unit identifier + /// The cancellation token. + /// A Task representing the asynchronous operation. + /// + public async Task UpdatePriceAsync(Price price, int skuId, CancellationToken token) + { + try + { + var oldPrice = await GetPriceAsync(skuId).ConfigureAwait(false); + if (oldPrice?.FixedPrices != null && oldPrice.FixedPrices.Any()) + { + await DeletePriceAsync(skuId, token).ConfigureAwait(false); + } + + LogConsumer.Info( + "Updating the price of sku {0} to {1} (list price: {2})", + skuId, + price.CostPrice.ToMonetary(), + price.ListPrice.HasValue ? price.ListPrice.Value.ToMonetary() : "no" + ); + await _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.PUT, + $@"{PlatformConstants.Pricing}/{skuId}", + token, + data: (string)price.GetSerializer(), + restEndpoint: RequestEndpoint.API + ) + .ConfigureAwait(false); + } + catch (Exception e) + { + throw new UpdatePriceInfoSkuException(skuId, e); + } + } + + /// + /// Removes an SKU price. + /// This action removes both Base Price and all available Fixed Prices for and SKU in all trade policies. + /// + /// The stock keeping unit identifier. + /// The cancellation token. + /// Task + public async Task DeletePriceAsync(int skuId, CancellationToken token) + { + LogConsumer.Info("Deleting the price of sku {0}", skuId); + await _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.DELETE, + $@"{PlatformConstants.Pricing}/{skuId}", + token, + restEndpoint: RequestEndpoint.API + ) + .ConfigureAwait(false); + } + + /// + /// Retrieves a collection of bridge facets based on the specified query and optional keywords. + /// + /// The query string used to filter the bridge facets. + /// Optional keywords to further refine the search for bridge facets. + /// An enumerable collection of objects that match the specified query and keywords. + /// + /// This method constructs a query to fetch bridge facets from a remote service. It logs the action of retrieving facets + /// and sets a timeout of 5 minutes for the operation. The method builds a dictionary of query parameters, including + /// facets to retrieve and the specified query. If keywords are provided, they are added to the query parameters as well. + /// The method then invokes the service asynchronously and deserializes the resulting JSON response into a list of + /// objects. If an exception occurs during this process, a custom + /// is thrown, encapsulating the original exception and the query that caused the failure. + /// + /// Thrown when an error occurs while retrieving bridge facets. + [Pure] + public IEnumerable GetBridgeFacets( + [Localizable(false)] string query, + [Localizable(false)] string keywords = null + ) + { + try + { + LogConsumer.Info( + "Getting facets in bridge module that satisfy the condition '{0}'", + query + ); + var source = new CancellationTokenSource(new TimeSpan(0, 5, 0)); + var queryString = new Dictionary + { + { @"_facets", @"Origin,Status" }, + { @"_where", query }, + }; + if (!string.IsNullOrWhiteSpace(keywords)) + { + queryString.Add(@"_keywords", $@"*{keywords}*"); + } + + var json = _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.GET, + $"{PlatformConstants.BridgeSearch}/facets", + source.Token, + queryString, + restEndpoint: RequestEndpoint.BRIDGE + ) + .Result; + return SerializerFactory.GetSerializer>().Deserialize(json); + } + catch (Exception e) + { + throw new BridgeException(query, e); + } + } + + /// + /// Retrieves a collection of bridge items based on the specified query parameters. + /// + /// The query string used to filter the bridge items. + /// The sorting criteria for the returned items. + /// Additional keywords to refine the search results. + /// The starting point for the items to be retrieved. + /// The maximum number of items to return. + /// An enumerable collection of that match the specified criteria. + /// + /// This method interacts with an external service to fetch bridge items based on the provided query, sort, and keywords. + /// It logs the request details and checks for an offset limit to avoid exceeding the maximum allowed items from the service. + /// If the offset exceeds 10,000, a warning is logged, and an empty list is returned. + /// The method uses a cancellation token to set a timeout for the service call, ensuring that it does not hang indefinitely. + /// In case of an error during the service call, it throws a custom exception with details about the failure. + /// + /// + /// Thrown when an error occurs while retrieving bridge items from the external service. + /// + [Pure] + public IEnumerable GetBridgeItems( + [Localizable(false)] string query, + [Localizable(false)] string sort, + [Localizable(false)] string keywords, + int offSet, + int limit + ) + { + try + { + LogConsumer.Info( + "Getting {0} items from {1} in bridge module that satisfy the condition '{2}'", + limit, + offSet, + query + ); + if (offSet >= 10000) + { + LogConsumer.Warning( + "Cannot get more than 10000 items from Bridge / Master Data (VTEX Elastic Search limitation)" + ); + return new List(); + } + var source = new CancellationTokenSource(new TimeSpan(0, 5, 0)); + var queryString = new Dictionary + { + { @"_where", query }, + { @"_sort", sort }, + { @"offSet", offSet.ToString() }, + { @"limit", limit.ToString() }, + }; + if (!string.IsNullOrWhiteSpace(keywords)) + { + queryString.Add(@"_keywords", $@"*{keywords}*"); + } + + var json = _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.GET, + PlatformConstants.BridgeSearch, + source.Token, + queryString, + restEndpoint: RequestEndpoint.BRIDGE + ) + .Result; + return SerializerFactory.GetSerializer>().Deserialize(json); + } + catch (AggregateException e) + { + throw new BridgeException( + query, + e.InnerExceptions.FirstOrDefault() ?? e.InnerException ?? e + ); + } + catch (Exception e) + { + throw new BridgeException(query, e); + } + } + + /// + /// Gets all bridge items. + /// + /// The query. + /// The sort. + /// The keywords. + /// Name of the facet. + /// The facet value. + /// IEnumerable<BridgeItem>. + [Pure] + public IEnumerable GetAllBridgeItems( + [Localizable(false)] string query, + [Localizable(false)] string sort, + [Localizable(false)] string keywords, + [Localizable(false)] string facetName, + [Localizable(false)] string facetValue + ) + { + const int perPage = 100; + var facets = GetBridgeFacets(query, keywords); + var total = facets.Single(f => f.Field.Equals(facetName)).Facets[facetValue].ToInt32(); + + var result = new List(total); + var pages = (total / perPage) + 1; + for (var x = 0; x < pages; x++) + { + result.AddRange(GetBridgeItems(query, sort, keywords, x * perPage, perPage)); + } + + return result; + } + + #endregion + + #region Platform status + + /// + /// Gets the platform status. + /// + /// IEnumerable<PlatformStatus>. + public IEnumerable GetPlatformStatus() + { + return GetPlatformStatusAsync(CancellationToken.None).Result; + } + + /// + /// Gets the platform status asynchronous. + /// + /// The token. + /// A Task<IEnumerable`1> representing the asynchronous operation. + [Pure] + public async Task> GetPlatformStatusAsync( + CancellationToken token + ) + { + LogConsumer.Info("Getting platform status"); + var json = await _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.GET, + string.Empty, + token, + restEndpoint: RequestEndpoint.HEALTH + ) + .ConfigureAwait(false); + var status = SerializerFactory.GetSerializer>().Deserialize(json); + LogConsumer.Debug(status, "vtex-platform-status.js"); + return status; + } + + #endregion + + #region Order payments + + /// + /// Gets the order payments. + /// + /// The transaction identifier. + /// List<PciPayment>. + [Pure] + public List GetOrderPayments(string transactionId) + { + var json = _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.GET, + $"{PlatformConstants.PciTransactions}/{transactionId}/payments", + CancellationToken.None, + restEndpoint: RequestEndpoint.PAYMENTS + ) + .Result; + if (json == null) + { + return new List(); + } + + var data = SerializerFactory + .GetCustomSerializer>(SerializerFormat.Json) + .Deserialize(json); + LogConsumer.Debug(data, $"vtex-order-payemnts-{transactionId}.js"); + return data; + } + + #endregion + + #region Catalog + + #region Specification + + /// + /// Gets the specification field asynchronous. + /// + /// The field identifier. + /// The token. + /// A Task<SpecificationField> representing the asynchronous operation. + [Pure] + public async Task GetSpecificationFieldAsync( + int fieldId, + CancellationToken token + ) + { + LogConsumer.Info("Getting field for the field id {0}", fieldId); + var json = await _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.GET, + $@"{PlatformConstants.CatalogPub}/specification/fieldGet/{fieldId}", + token + ) + .ConfigureAwait(false); + var field = SerializerFactory.GetSerializer().Deserialize(json); + LogConsumer.Debug(field, $"vtex-specification-field-{fieldId}.js"); + return field; + } + + /// + /// Gets the specification field values asynchronous. + /// + /// The field identifier. + /// The token. + /// A Task<ICollection`1> representing the asynchronous operation. + [Pure] + public async Task> GetSpecificationFieldValuesAsync( + int fieldId, + CancellationToken token + ) + { + LogConsumer.Info("Getting field values for the field id {0}", fieldId); + var json = await _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.GET, + $@"{PlatformConstants.CatalogPub}/specification/fieldvalue/{fieldId}", + token + ) + .ConfigureAwait(false); + var fieldValues = SerializerFactory + .GetSerializer>() + .Deserialize(json); + LogConsumer.Debug(fieldValues, $"vtex-specification-values-{fieldId}.js"); + return fieldValues; + } + + /// + /// Updates the product specification asynchronous. + /// + /// The specification. + /// The product identifier. + /// The token. + /// A Task representing the asynchronous operation. + public async Task UpdateProductSpecificationAsync( + Specification specification, + int productId, + CancellationToken token + ) + { + await UpdateProductSpecificationsAsync( + new List(new[] { specification }), + productId, + token + ) + .ConfigureAwait(false); + } + + /// + /// Updates the product specifications asynchronous. + /// + /// The specifications list. + /// The product identifier. + /// The token. + /// A Task representing the asynchronous operation. + public async Task UpdateProductSpecificationsAsync( + List specifications, + int productId, + CancellationToken token + ) + { + LogConsumer.Info( + "Updating the specifications {1} of product {0}", + productId, + string.Join(@",", specifications.Select(s => s.Id)) + ); + + var data = (string)specifications.GetSerializer(); + await _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.POST, + $@"{PlatformConstants.Catalog}/products/{productId}/specification", + token, + data: data + ) + .ConfigureAwait(false); + } + + /// + /// Inserts the specification field value asynchronous. + /// + /// The field value. + /// The token. + /// A Task representing the asynchronous operation. + public async Task InsertSpecificationFieldValueAsync( + SpecificationFieldValue fieldValue, + CancellationToken token + ) + { + LogConsumer.Info("Creating field value of field id {0}", fieldValue.FieldId); + var data = (string)fieldValue.GetSerializer(); + await _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.POST, + $@"{PlatformConstants.Catalog}/specification/fieldValue", + token, + data: data + ) + .ConfigureAwait(false); + } + + /// + /// Asynchronously searches for a data entity based on a specified field and value. + /// + /// The type of the data entity to search for, which must implement . + /// The field of the data entity to search against. + /// The value to search for in the specified field. + /// A cancellation token to monitor for cancellation requests. + /// A task that represents the asynchronous operation. The task result contains the found data entity of type or null if no entity is found. + /// + /// This method performs an asynchronous search for a data entity by sending a GET request to the specified endpoint. + /// It constructs a query string using the provided field and value, and invokes a service to retrieve the data. + /// If the search value is null or whitespace, an is thrown. + /// In case of an unexpected API response, an is thrown, containing the JSON response and the original exception. + /// The method logs the retrieved entity for debugging purposes. + /// + /// Thrown when is null or whitespace. + /// Thrown when the API response is unexpected. + [Pure] + public async Task SearchAsync( + string searchedField, + string searchedValue, + CancellationToken token + ) + where TDataEntity : class, IDataEntity, new() + { + if (string.IsNullOrWhiteSpace(searchedValue)) + { + throw new ArgumentNullException(nameof(searchedValue)); + } + + var queryString = new Dictionary + { + { searchedField, searchedValue }, + { @"_fields", @"_all" }, + }; + var json = string.Empty; + try + { + var entityName = typeof(TDataEntity).GetDataEntityName(); + json = await _wrapper + .ServiceInvokerAsync( + HttpRequestMethod.GET, + $@"dataentities/{entityName}/search/", + token, + queryString, + restEndpoint: RequestEndpoint.MASTER_DATA + ) + .ConfigureAwait(false); + var entity = SerializerFactory + .GetSerializer>() + .Deserialize(json) + .FirstOrDefault(); + if (entity == null) + { + return null; + } + + LogConsumer.Debug( + entity, + $@"vtex-masterdata-entity-{entityName}-{searchedField}-{searchedValue}.js" + ); + return entity; + } + catch (Exception e) + { + throw new UnexpectedApiResponseException(json, e); + } + } + + #endregion + + #endregion + + #endregion + + #region IDisposable + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + _wrapper.Dispose(); + } + + #endregion diff --git a/Src/VTEX/VTEXWrapper.cs b/Src/VTEX/VTEXWrapper.cs index 3140fe613..10520a1b3 100644 --- a/Src/VTEX/VTEXWrapper.cs +++ b/Src/VTEX/VTEXWrapper.cs @@ -1,531 +1,621 @@ -// *********************************************************************** -// Assembly : VTEX -// Author : Guilherme Branco Stracini -// Created : 01-15-2023 -// -// Last Modified By : Guilherme Branco Stracini -// Last Modified On : 01-16-2023 -// *********************************************************************** -// -// © 2020 Guilherme Branco Stracini. All rights reserved. -// -// -// *********************************************************************** -namespace VTEX -{ - using System; - using System.Collections.Generic; - using System.ComponentModel; - using System.Linq; - using System.Net; - using System.Net.Http; - using System.Net.Http.Headers; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - using CrispyWaffle.Extensions; - using CrispyWaffle.Log; - using CrispyWaffle.Telemetry; - using CrispyWaffle.Utilities; - using Enums; - using GoodPractices; - - /// - /// Class Wrapper. This class cannot be inherited. - /// - /// - // TODO change public to internal after remove from Integração Service - public sealed class VTEXWrapper : IDisposable - { - #region Private fields - - /// - /// The application key - /// - private string _appKey; - - /// - /// The application token - /// - private string _appToken; - - /// - /// The authentication cookie - /// - private string _authCookie; - - /// - /// The account name - /// - private readonly string _accountName; - - /// - /// The internal user agent - /// - private static string _internalUserAgent; - - /// - /// Gets the internal user agent. - /// - /// The internal user agent. - private static string InternalUserAgent - { - get - { - if (!string.IsNullOrWhiteSpace(_internalUserAgent)) - { - return _internalUserAgent; - } - - var assembly = System - .Reflection.Assembly.GetAssembly(typeof(VTEXWrapper)) - .GetName(); - _internalUserAgent = $@"{assembly.Name}/{assembly.Version}"; - return _internalUserAgent; - } - } - - /// - /// The request mediator - /// - private readonly ManualResetEvent _requestMediator = new ManualResetEvent(false); - - #endregion - - #region ~Ctor - - /// - /// Initializes a new instance of the class. - /// - /// The account name. - public VTEXWrapper(string accountName) - { - _accountName = accountName; - _requestMediator.Set(); - } - - #endregion - - #region Implementation of IDisposable - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public void Dispose() - { - _appKey = null; - _appToken = null; - _requestMediator.Dispose(); - } - - #endregion - - #region Private methods - - /// - /// Services the invoker internal. - /// - /// The method. - /// The endpoint. - /// The token. - /// The data. - /// The URI builder. - /// The cookie. - /// if set to true [requires authentication]. - /// if set to true [is retry]. - /// System.String. - private async Task ServiceInvokerInternal( - HttpRequestMethod method, - string endpoint, - CancellationToken token, - string data, - UriBuilder uriBuilder, - Cookie cookie, - bool requiresAuthentication, - bool isRetry = false - ) - { - HttpResponseMessage response = null; - string result = null; - Exception exr; - try - { - _requestMediator.WaitOne(); - - LogConsumer.Trace( - "ServiceInvokerAsync -> Method: {0} | Endpoint: {1}", - method.GetHumanReadableValue(), - endpoint - ); - - LogConsumer.Debug(uriBuilder.ToString()); - - var cookieContainer = new CookieContainer(); - - using var handler = new HttpClientHandler { CookieContainer = cookieContainer }; - - using var client = new HttpClient(handler); - - ConfigureClient(client, requiresAuthentication); - - if (cookie != null) - { - cookieContainer.Add(uriBuilder.Uri, cookie); - } - - response = await RequestInternalAsync(method, token, data, client, uriBuilder) - .ConfigureAwait(false); - - token.ThrowIfCancellationRequested(); - - result = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - - response.EnsureSuccessStatusCode(); - - return result; - } - catch (AggregateException e) - { - var ex = e.InnerExceptions.FirstOrDefault() ?? e.InnerException ?? e; - - exr = HandleException(ex, response, uriBuilder.Uri, method, data, result); - - if (isRetry) - { - throw exr; - } - } - catch (Exception e) - { - exr = HandleException(e, response, uriBuilder.Uri, method, data, result); - - if (isRetry) - { - throw exr; - } - } - - return await ServiceInvokerInternal( - method, - endpoint, - token, - data, - uriBuilder, - cookie, - requiresAuthentication, - true - ) - .ConfigureAwait(false); - } - - /// - /// Handles the exception. - /// - /// The exception. - /// The response. - /// The URI. - /// The method. - /// The data. - /// The result. - /// Exception. - /// - private Exception HandleException( - Exception exception, - HttpResponseMessage response, - Uri uri, - HttpRequestMethod method, - string data, - string result - ) - { - var statusCode = 0; - if (response != null) - { - statusCode = (int)response.StatusCode; - } - - var ex = new UnexpectedApiResponseException( - uri, - method.ToString(), - data, - result, - statusCode, - exception - ); - if (statusCode == 429 || statusCode == 503) - { - _requestMediator.Reset(); - LogConsumer.Warning( - "HTTP {2} status code on method {0} - uri {1}", - method.ToString(), - uri, - statusCode - ); - Thread.Sleep(60 * 1000); - _requestMediator.Set(); - return ex; - } - if (statusCode != 0 && statusCode != 408 && statusCode != 500 && statusCode != 502) - { - throw ex; - } - - LogConsumer.Warning("Retrying the {0} request", method.ToString()); - TelemetryAnalytics.TrackHit( - $"VTEX_handle_exception_retrying_{method.ToString()}_request" - ); - return ex; - } - - /// - /// Configures the client. - /// - /// The client. - /// if set to true [requires authentication]. - private void ConfigureClient(HttpClient client, bool requiresAuthentication) - { - client.DefaultRequestHeaders.ExpectContinue = false; - client.DefaultRequestHeaders.Accept.Clear(); - client.DefaultRequestHeaders.Accept.Add( - new MediaTypeWithQualityHeaderValue(@"application/json") - ); - client.DefaultRequestHeaders.TryAddWithoutValidation( - @"User-Agent", - $@"guiBranco-VTEX-SDK-dotnet {InternalUserAgent} +https://github.com/guibranco/VTEX-SDK-dotnet" - ); - if (!requiresAuthentication) - { - return; - } - - client.DefaultRequestHeaders.Add(@"X-VTEX-API-AppKey", _appKey); - client.DefaultRequestHeaders.Add(@"X-VTEX-API-AppToken", _appToken); - } - - /// - /// Sends an HTTP request asynchronously using the specified method and returns the response. - /// - /// The HTTP method to use for the request (e.g., GET, POST, DELETE, etc.). - /// A cancellation token to cancel the operation if needed. - /// The data to be sent in the request body, if applicable. - /// The HttpClient instance used to send the request. - /// The UriBuilder that constructs the URI for the request. - /// A task that represents the asynchronous operation, containing the HttpResponseMessage received from the server. - /// - /// This method handles different HTTP methods such as GET, POST, PUT, DELETE, and PATCH. - /// It constructs the appropriate request based on the provided method and sends it using the specified HttpClient. - /// If the method requires a body (like POST, PUT, or PATCH), it creates a StringContent object with the provided data. - /// The method also supports cancellation through the provided CancellationToken. - /// The response from the server is returned as an HttpResponseMessage, which can be used to inspect the result of the request. - /// - /// Thrown when an unsupported HTTP method is provided. - private static async Task RequestInternalAsync( - HttpRequestMethod method, - CancellationToken token, - string data, - HttpClient client, - UriBuilder uriBuilder - ) - { - HttpResponseMessage response; - StringContent content = null; - if (!string.IsNullOrWhiteSpace(data)) - { - content = new StringContent(data, Encoding.UTF8, @"application/json"); - } - - switch (method) - { - case HttpRequestMethod.DELETE: - response = await client - .DeleteAsync(uriBuilder.Uri, token) - .ConfigureAwait(false); - break; - case HttpRequestMethod.GET: - response = await client.GetAsync(uriBuilder.Uri, token).ConfigureAwait(false); - break; - case HttpRequestMethod.POST: - response = await client - .PostAsync(uriBuilder.Uri, content, token) - .ConfigureAwait(false); - break; - case HttpRequestMethod.PUT: - response = await client - .PutAsync(uriBuilder.Uri, content, token) - .ConfigureAwait(false); - break; - case HttpRequestMethod.PATCH: - var request = new HttpRequestMessage(new HttpMethod(@"PATCH"), uriBuilder.Uri) - { - Content = content, - }; - response = await client.SendAsync(request, token).ConfigureAwait(false); - request.Dispose(); - break; - default: - throw new ArgumentOutOfRangeException(nameof(method), method, null); - } - - return response; - } - - #endregion - - #region Public methods - - /// - /// Sets the rest credentials. - /// - /// The application key. - /// The application token. - public void SetRestCredentials(string appKey, string appToken) - { - _appKey = appKey; - _appToken = appToken; - } - - /// - /// Sets the vtex identifier client authentication cookie. - /// - /// The cookie value. - public void SetVtexIdClientAuthCookie(string cookieValue) - { - _authCookie = cookieValue; - } - - /// - /// Asynchronously invokes a service endpoint with the specified HTTP method and parameters. - /// - /// The HTTP request method to be used (e.g., GET, POST). - /// The endpoint of the service to be invoked. This should not be localizable. - /// A cancellation token to observe while waiting for the task to complete. - /// An optional dictionary of query string parameters to be included in the request. - /// An optional string containing data to be sent with the request. - /// An optional parameter specifying the REST endpoint type. Defaults to . - /// A task that represents the asynchronous operation, containing the response as a string. - /// - /// This method constructs a URI using the provided endpoint and query string parameters, - /// and then invokes the service asynchronously. It handles authentication and cookie management - /// as needed based on the service requirements. The method is designed to work with various - /// HTTP methods and can send data in the request body if specified. - /// The response from the service is returned as a string, allowing for further processing or - /// parsing as needed by the caller. - /// - public async Task ServiceInvokerAsync( - HttpRequestMethod method, - [Localizable(false)] string endpoint, - CancellationToken token, - Dictionary queryString = null, - string data = null, - RequestEndpoint restEndpoint = RequestEndpoint.DEFAULT - ) - { - Cookie cookie = null; - var requiresAuthentication = true; - var protocol = @"https"; - var port = 443; - var host = GetHostData( - ref endpoint, - ref queryString, - restEndpoint, - ref cookie, - ref protocol, - ref port, - ref requiresAuthentication - ); - var query = string.Empty; - if (queryString is { Count: > 0 }) - { - query = new QueryStringBuilder().AddRange(queryString).ToString(); - } - - var builder = new UriBuilder(protocol, host, port, endpoint) - { - Query = query.Replace(@"?", string.Empty), - }; - return await ServiceInvokerInternal( - method, - endpoint, - token, - data, - builder, - cookie, - requiresAuthentication - ) - .ConfigureAwait(false); - } - - /// - /// Gets the host data. - /// - /// The endpoint. - /// The query string. - /// The rest endpoint. - /// The cookie. - /// The protocol. - /// The port. - /// if set to true [requires authentication]. - /// System.String. - /// restEndpoint - null - private string GetHostData( - ref string endpoint, - ref Dictionary queryString, - RequestEndpoint restEndpoint, - ref Cookie cookie, - ref string protocol, - ref int port, - ref bool requiresAuthentication - ) - { - string host; - switch (restEndpoint) - { - case RequestEndpoint.DEFAULT: - host = $@"{_accountName}.{VTEXConstants.PlatformStableDomain}"; - endpoint = $@"api/{endpoint}"; - break; - case RequestEndpoint.PAYMENTS: - host = $@"{_accountName}.{VTEXConstants.PaymentsDomain}"; - endpoint = $@"api/{endpoint}"; - break; - case RequestEndpoint.LOGISTICS: - host = VTEXConstants.LogisticsDomain; - endpoint = $@"api/{endpoint}"; - if (queryString == null) - { - queryString = new(); - } - - queryString.Add(@"an", _accountName); - break; - case RequestEndpoint.API: - case RequestEndpoint.MASTER_DATA: - host = VTEXConstants.ApiDomain; - endpoint = $@"{_accountName}/{endpoint}"; - break; - case RequestEndpoint.BRIDGE: - host = $@"{_accountName}.{VTEXConstants.MyVtexDomain}"; - endpoint = $@"api/{endpoint}"; - if (!string.IsNullOrWhiteSpace(_authCookie)) - { - cookie = new(VTEXConstants.VtexIdClientAuthCookieName, _authCookie); - } - - break; - case RequestEndpoint.HEALTH: - protocol = @"http"; - port = 80; - host = VTEXConstants.MonitoringDomain; - endpoint = @"api/healthcheck/modules"; - requiresAuthentication = false; - break; - default: - throw new ArgumentOutOfRangeException(nameof(restEndpoint), restEndpoint, null); - } - - return host; - } - - #endregion - } -} +// *********************************************************************** +// Assembly : VTEX +// Author : Guilherme Branco Stracini +// Created : 01-15-2023 +// +// Last Modified By : Guilherme Branco Stracini +// Last Modified On : 01-16-2023 +// *********************************************************************** +// +// © 2020 Guilherme Branco Stracini. All rights reserved. +// +// +using System.Net.Http.Headers; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Net; +using System.Net.Http; +namespace VTEX +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using CrispyWaffle.Extensions; +using CrispyWaffle.Log; + + /// + /// Class Wrapper. This class cannot be inherited. + /// + /// + // TODO change public to internal after remove from Integração Service + public sealed class VTEXWrapper : IDisposable + + { + #region Private fields + + /// + /// The application key + /// + private string _appKey; + + /// + /// The application token + /// + private string _appToken; + + /// + + /// The authentication cookie + /// + private string _authCookie; + + /// + /// The account name + /// + private readonly string _accountName; + + + /// + /// The internal user agent + /// + private static string _internalUserAgent; + + /// + /// Gets the internal user agent. + /// + /// The internal user agent. + + private static string InternalUserAgent + { + get + { + if (!string.IsNullOrWhiteSpace(_internalUserAgent)) + { + return _internalUserAgent; + } + + + var assembly = System + .Reflection.Assembly.GetAssembly(typeof(VTEXWrapper)) + .GetName(); + _internalUserAgent = $"{assembly.Name}/{assembly.Version}"; + return _internalUserAgent; + } + } + + /// + + /// The request mediator + /// + private readonly ManualResetEvent _requestMediator = new ManualResetEvent(false); + + #endregion + + #region ~Ctor + + /// + + /// Initializes a new instance of the class. + /// + /// The account name. + public VTEXWrapper(string accountName) + { + _accountName = accountName; + _requestMediator.Set(); + } + + + #endregion + + #region Implementation of IDisposable + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + + _appKey = null; + _appToken = null; + _requestMediator.Dispose(); + } + + + #endregion + + #region Private methods + + /// + /// Services the invoker internal. + /// + /// The method. + /// The endpoint. + /// The token. + /// The data. + /// The URI builder. + /// The cookie. + + /// if set to true [requires authentication]. + /// if set to true [is retry]. + /// System.String. + private async Task ServiceInvokerInternal( + HttpRequestMethod method, + string endpoint, + CancellationToken token, + string data, + UriBuilder uriBuilder, + + Cookie cookie, + bool requiresAuthentication, + bool isRetry = false + ) + { + HttpResponseMessage response = null; + string result = null; + Exception exr; + try + + { + _requestMediator.WaitOne(); + + LogConsumer.Trace( + "ServiceInvokerAsync -> Method: {0} | Endpoint: {1}", + method.GetHumanReadableValue(), + endpoint + ); + + + LogConsumer.Debug(uriBuilder.ToString()); + + var cookieContainer = new CookieContainer(); + + using var handler = new HttpClientHandler { CookieContainer = cookieContainer }; + + using var client = new HttpClient(handler); + + ConfigureClient(client, requiresAuthentication); + + + if (cookie != null) + { + cookieContainer.Add(uriBuilder.Uri, cookie); + } + + response = await RequestInternalAsync(method, token, data, client, uriBuilder) + .ConfigureAwait(false); + + + token.ThrowIfCancellationRequested(); + + result = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + + return result; + } + catch (AggregateException e) + + { + var ex = e.InnerExceptions.FirstOrDefault() ?? e.InnerException ?? e; + + exr = HandleException(ex, response, uriBuilder.Uri, method, data, result); + + if (isRetry) + { + throw exr; + } + + } + catch (Exception e) + { + exr = HandleException(e, response, uriBuilder.Uri, method, data, result); + + if (isRetry) + { + throw exr; + } + + } + + return await ServiceInvokerInternal( + method, + endpoint, + token, + data, + uriBuilder, + cookie, + + requiresAuthentication, + true + ) + .ConfigureAwait(false); + } + + /// + /// Handles the exception. + /// + + /// The exception. + /// The response. + /// The URI. + /// The method. + /// The data. + /// The result. + /// Exception. + /// + private Exception HandleException( + + Exception exception, + HttpResponseMessage response, + Uri uri, + HttpRequestMethod method, + string data, + string result + ) + { + var statusCode = 0; + + if (response != null) + { + statusCode = (int)response.StatusCode; + } + + var ex = new UnexpectedApiResponseException( + uri, + method.ToString(), + data, + + result, + statusCode, + exception + ); + if (statusCode == 429 || statusCode == 503) + { + _requestMediator.Reset(); + LogConsumer.Warning( + "HTTP {2} status code on method {0} - uri {1}", + + method.ToString(), + uri, + statusCode + ); + Thread.Sleep(60 * 1000); + _requestMediator.Set(); + return ex; + } + if (statusCode != 0 && statusCode != 408 && statusCode != 500 && statusCode != 502) + + { + throw ex; + } + + LogConsumer.Warning("Retrying the {0} request", method.ToString()); + TelemetryAnalytics.TrackHit( + $"VTEX_handle_exception_retrying_{method.ToString()}_request" + ); + return ex; + + } + + /// + /// Configures the client. + /// + /// The client. + /// if set to true [requires authentication]. + private void ConfigureClient(HttpClient client, bool requiresAuthentication) + { + + client.DefaultRequestHeaders.ExpectContinue = false; + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue(@"application/json") + ); + client.DefaultRequestHeaders.TryAddWithoutValidation( + @"User-Agent", + $@"guiBranco-VTEX-SDK-dotnet {InternalUserAgent} +https://github.com/guibranco/VTEX-SDK-dotnet" + ); + + if (!requiresAuthentication) + { + return; + } + + client.DefaultRequestHeaders.Add(@"X-VTEX-API-AppKey", _appKey); + client.DefaultRequestHeaders.Add(@"X-VTEX-API-AppToken", _appToken); + } + + + /// + /// Sends an HTTP request asynchronously using the specified method and returns the response. + /// + /// The HTTP method to use for the request (e.g., GET, POST, DELETE, etc.). + /// A cancellation token to cancel the operation if needed. + /// The data to be sent in the request body, if applicable. + /// The HttpClient instance used to send the request. + /// The UriBuilder that constructs the URI for the request. + /// A task that represents the asynchronous operation, containing the HttpResponseMessage received from the server. + + /// + /// This method handles different HTTP methods such as GET, POST, PUT, DELETE, and PATCH. + /// It constructs the appropriate request based on the provided method and sends it using the specified HttpClient. + /// If the method requires a body (like POST, PUT, or PATCH), it creates a StringContent object with the provided data. + /// The method also supports cancellation through the provided CancellationToken. + /// The response from the server is returned as an HttpResponseMessage, which can be used to inspect the result of the request. + /// + /// Thrown when an unsupported HTTP method is provided. + private static async Task RequestInternalAsync( + + HttpRequestMethod method, + CancellationToken token, + string data, + HttpClient client, + UriBuilder uriBuilder + ) + { + HttpResponseMessage response; + StringContent content = null; + + if (!string.IsNullOrWhiteSpace(data)) + { + content = new StringContent(data, Encoding.UTF8, @"application/json"); + } + + switch (method) + { + case HttpRequestMethod.DELETE: + response = await client + + .DeleteAsync(uriBuilder.Uri, token) + .ConfigureAwait(false); + break; + case HttpRequestMethod.GET: + response = await client.GetAsync(uriBuilder.Uri, token).ConfigureAwait(false); + break; + case HttpRequestMethod.POST: + response = await client + .PostAsync(uriBuilder.Uri, content, token) + + .ConfigureAwait(false); + break; + case HttpRequestMethod.PUT: + response = await client + .PutAsync(uriBuilder.Uri, content, token) + .ConfigureAwait(false); + break; + case HttpRequestMethod.PATCH: + var request = new HttpRequestMessage(new HttpMethod(@"PATCH"), uriBuilder.Uri) + + { + Content = content, + }; + response = await client.SendAsync(request, token).ConfigureAwait(false); + request.Dispose(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(method), method, null); + } + + + return response; + } + + #endregion + + #region Public methods + + /// + + /// Retrieves a list of all collections. + /// + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation, containing the list of collections as a string. + public async Task GetCollectionsAsync(CancellationToken token) + { + return await ServiceInvokerAsync(HttpRequestMethod.GET, "collections", token); + } + + + /// + /// Updates an existing collection. + /// + /// The identifier of the collection to be updated. + /// The data representing the updated collection. + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation, containing the response as a string. + public async Task UpdateCollectionAsync(int id, string data, CancellationToken token) + { + + return await ServiceInvokerAsync(HttpRequestMethod.PUT, $"collections/{id}", token, data: data); +} + /// Deletes a collection. + /// + /// The identifier of the collection to be deleted. + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation, containing the response as a string. + public async Task DeleteCollectionAsync(int id, CancellationToken token) + { + return await ServiceInvokerAsync(HttpRequestMethod.DELETE, $"collections/{id}", token); + + } + /// + /// Creates a new collection. + /// + /// The data representing the new collection to be created. + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation, containing the response as a string. + public async Task CreateCollectionAsync(string data, CancellationToken token) + { + return await ServiceInvokerAsync(HttpRequestMethod.POST, "collections", token, data: data); + + } + + /// + /// Sets the rest credentials. + /// + /// The application key. + /// The application token. + public void SetRestCredentials(string appKey, string appToken) + { + _appKey = appKey; + _appToken = appToken; + } + + + /// + /// Sets the vtex identifier client authentication cookie. + /// + /// The cookie value. + public void SetVtexIdClientAuthCookie(string cookieValue) + { + _authCookie = cookieValue; + } + + + /// + /// Asynchronously invokes a service endpoint with the specified HTTP method and parameters. + /// + /// The HTTP request method to be used (e.g., GET, POST). + /// The endpoint of the service to be invoked. This should not be localizable. + /// A cancellation token to observe while waiting for the task to complete. + /// An optional dictionary of query string parameters to be included in the request. + /// An optional string containing data to be sent with the request. + /// An optional parameter specifying the REST endpoint type. Defaults to . + /// A task that represents the asynchronous operation, containing the response as a string. + /// + /// This method constructs a URI using the provided endpoint and query string parameters, + /// and then invokes the service asynchronously. It handles authentication and cookie management + /// as needed based on the service requirements. The method is designed to work with various + + /// HTTP methods and can send data in the request body if specified. + /// The response from the service is returned as a string, allowing for further processing or + /// parsing as needed by the caller. + /// + public async Task ServiceInvokerAsync( + HttpRequestMethod method, + [Localizable(false)] string endpoint, + CancellationToken token, + Dictionary queryString = null, + + string data = null, + RequestEndpoint restEndpoint = RequestEndpoint.DEFAULT + ) + { + Cookie cookie = null; + var requiresAuthentication = true; + var protocol = @"https"; + var port = 443; + var host = GetHostData( + + ref endpoint, + ref queryString, + restEndpoint, + ref cookie, + ref protocol, + ref port, + ref requiresAuthentication + ); + var query = string.Empty; + + if (queryString is { Count: > 0 }) + { + query = new QueryStringBuilder().AddRange(queryString).ToString(); + } + + var builder = new UriBuilder(protocol, host, port, endpoint) + { + Query = query.Replace(@"?", string.Empty), + }; + + return await ServiceInvokerInternal( + method, + endpoint, + token, + data, + builder, + cookie, + requiresAuthentication + ) + .ConfigureAwait(false); + } + + + /// + /// Gets the host data. + /// + /// The endpoint. + /// The query string. + /// The rest endpoint. + /// The cookie. + /// The protocol. + /// The port. + /// if set to true [requires authentication]. + /// System.String. + private string GetHostData( + ref string endpoint, + + ref Dictionary queryString, + RequestEndpoint restEndpoint, + ref Cookie cookie, + ref string protocol, + ref int port, + ref bool requiresAuthentication + ) + { + string host; + + switch (restEndpoint) + { + case RequestEndpoint.DEFAULT: + host = $@"{_accountName}.{VTEXConstants.PlatformStableDomain}"; + endpoint = $@"api/{endpoint}"; + break; + case RequestEndpoint.PAYMENTS: + host = $@"{_accountName}.{VTEXConstants.PaymentsDomain}"; + endpoint = $@"api/{endpoint}"; + + break; + case RequestEndpoint.LOGISTICS: + host = VTEXConstants.LogisticsDomain; + endpoint = $@"api/{endpoint}"; + if (queryString == null) + { + queryString = new(); + } + + + queryString.Add(@"an", _accountName); + break; + case RequestEndpoint.API: + case RequestEndpoint.MASTER_DATA: + host = VTEXConstants.ApiDomain; + endpoint = $@"{_accountName}/{endpoint}"; + break; + case RequestEndpoint.BRIDGE: + host = $@"{_accountName}.{VTEXConstants.MyVtexDomain}"; + + endpoint = $@"api/{endpoint}"; + if (!string.IsNullOrWhiteSpace(_authCookie)) + { + cookie = new(VTEXConstants.VtexIdClientAuthCookieName, _authCookie); + } + + break; + case RequestEndpoint.HEALTH: + protocol = @"http"; + + port = 80; + host = VTEXConstants.MonitoringDomain; + endpoint = @"api/healthcheck/modules"; + requiresAuthentication = false; + break; + default: + throw new ArgumentOutOfRangeException(nameof(restEndpoint), restEndpoint, null); + } + + + return host; + } + + #endregion +