public static async Task SourceUnsourcedOrders(
            [TimerTrigger("0 */5 * * * *")] TimerInfo timer, // every 5 mins
            [CosmosDB(ConnectionStringSetting = "AzureCosmosDBConnectionString"), SwaggerIgnore] DocumentClient documentClient,
            ILogger log)
        {
            // Get orders within past 3 days in atg-orders container that are not in the orders container
            var query = new SqlQuerySpec
            {
                QueryText = "SELECT VALUE c FROM c WHERE c.lastModifiedDate > DateTimeAdd(\"dd\", -03, GetCurrentDateTime())"
            };

            var ordersCollectionUri = UriFactory.CreateDocumentCollectionUri("sourcing-engine", "orders");

            var atgOrdersCollectionUri = UriFactory.CreateDocumentCollectionUri("sourcing-engine", "atg-orders");

            var atgOrderDocs = documentClient.CreateDocumentQuery <Document>(atgOrdersCollectionUri, query, option).AsEnumerable();

            // Run sourcing engine on unsourced orders
            foreach (var atgOrderDoc in atgOrderDocs)
            {
                AtgOrderReq atgOrderReq = (dynamic)atgOrderDoc;

                query = new SqlQuerySpec
                {
                    QueryText  = "SELECT * FROM c WHERE c.id = @id",
                    Parameters = new SqlParameterCollection()
                    {
                        new SqlParameter("@id", atgOrderReq.atgOrderId)
                    }
                };

                var orderDoc           = documentClient.CreateDocumentQuery <Document>(ordersCollectionUri, query, option).AsEnumerable().FirstOrDefault();
                var sourcingController = InitializeSourcingController(log);

                // If the order does not exist in orders container, run sourcing engine
                try
                {
                    if (orderDoc == null)
                    {
                        log.LogInformation($"Order ID: {atgOrderReq.atgOrderId}");
                        log.LogInformation(@"Order: {Order}", atgOrderReq);

                        var atgOrderRes = new AtgOrderRes(atgOrderReq);

                        await sourcingController.StartSourcing(documentClient, atgOrderRes);
                    }
                    else // ensure that each line item has a shipFrom location
                    {
                        AtgOrderRes atgOrderRes = (dynamic)orderDoc;

                        var requiresSourcing = OrderController.ValidateItemShipFroms(atgOrderRes.items);

                        if (requiresSourcing)
                        {
                            await sourcingController.StartSourcing(documentClient, atgOrderRes);
                        }
                    }
                }
                catch (Exception ex)
                {
                    log.LogWarning(@"Missing required field: {E}", ex);

                    var title        = "Error in SourceUnsourcedOrders: ";
                    var text         = $"Error message: {ex.Message}. Stacktrace: {ex.StackTrace}";
                    var teamsMessage = new TeamsMessage(title, text, "red", errorLogsUrl);
                    teamsMessage.LogToTeams(teamsMessage);

                    log.LogError(title + @"{E}", ex);
                }
            }
        }
        public static async Task ReSourceBackorderedItems(
            [TimerTrigger("0 */15 * * * *")] TimerInfo timer, // every 15 mins
            [CosmosDB(ConnectionStringSetting = "AzureCosmosDBConnectionString"), SwaggerIgnore] DocumentClient documentClient,
            ILogger log)
        {
            try
            {
                var currentHour = OrderController.GetCurrentEasternHour();
                log.LogInformation($"Current Hour: {currentHour}");

                // Only run within 6am - 7pm EST
                if (currentHour < 6 || currentHour >= 19)
                {
                    return;
                }

                // Get open orders with backordered item(s)
                var query = new SqlQuerySpec
                {
                    QueryText = @"
                        SELECT VALUE c FROM c 
                        JOIN s IN c.sourcing 
                        JOIN i IN s.items 
                        WHERE c.orderComplete = false 
                            AND c.claimed = false 
                            AND (i.sourcingMessage = 'Backordered.' OR CONTAINS(i.sourcingMessage, 'does not have the required quantity'))"
                };

                var manualOrdersCollectionUri = UriFactory.CreateDocumentCollectionUri("sourcing-engine", "manual-orders");
                var manualOrderDocs           = documentClient.CreateDocumentQuery <Document>(manualOrdersCollectionUri, query, option).AsEnumerable();

                // Get the matching ATG Order from query results and run sourcing
                foreach (var manualOrderDoc in manualOrderDocs)
                {
                    try
                    {
                        ManualOrder manualOrder = (dynamic)manualOrderDoc;
                        log.LogInformation($"Order ID: {manualOrder.atgOrderId}");

                        var atgOrderDoc = await OrderController.GetOrder <AtgOrderRes>(manualOrder.atgOrderId, documentClient);

                        AtgOrderRes atgOrder = (dynamic)atgOrderDoc;

                        await InitializeSourcingController(log).StartSourcing(documentClient, atgOrder);
                    }
                    catch (Exception ex)
                    {
                        var title        = "Error in ReSourceBackorderedItems foreach loop.";
                        var teamsMessage = new TeamsMessage(title, $"Error message: {ex.Message}. Stacktrace: {ex.StackTrace}", "yellow", errorLogsUrl);
                        teamsMessage.LogToTeams(teamsMessage);
                        log.LogError(ex, title);
                    }
                }
            }
            catch (Exception ex)
            {
                var title        = "Error in ReSourceBackorderedItems.";
                var teamsMessage = new TeamsMessage(title, $"Error message: {ex.Message}. Stacktrace: {ex.StackTrace}", "red", errorLogsUrl);
                teamsMessage.LogToTeams(teamsMessage);
                log.LogError(ex, title);
            }
        }
        public async Task CreateOrUpdateManualOrder(
            [CosmosDBTrigger(
                 databaseName: "sourcing-engine",
#if RELEASE
                 collectionName: "orders",
                 LeaseCollectionName = "sourcing-leases",
#endif
#if DEBUG
                 collectionName: "test-orders",
                 LeaseCollectionName = "test-sourcing-leases",
#endif
                 ConnectionStringSetting = "AzureCosmosDBConnectionString",
                 CreateLeaseCollectionIfNotExists = true), SwaggerIgnore] IReadOnlyList <Document> documents,
            [CosmosDB(ConnectionStringSetting = "AzureCosmosDBConnectionString"), SwaggerIgnore] DocumentClient documentClient,
            ILogger log)
        {
            log.LogInformation(@"documents: {Documents}", documents);

            foreach (var document in documents)
            {
                try
                {
                    AtgOrderRes atgOrderRes = (dynamic)document;
                    log.LogInformation($"Order ID: {atgOrderRes.atgOrderId}");

                    if (atgOrderRes.processSourcing)
                    {
                        log.LogInformation("Manual order not required because processSourcing is true.");
                        continue;
                    }
                    log.LogInformation(@"atgOrderRes: {AtgOrderRes}", atgOrderRes);

                    var manualOrdersContainerName = Environment.GetEnvironmentVariable("MANUAL_ORDERS_CONTAINER_NAME");
                    var uri = UriFactory.CreateDocumentCollectionUri("sourcing-engine", manualOrdersContainerName);

                    var manualOrderDoc = await OrderController.GetOrder <ManualOrder>(atgOrderRes.atgOrderId, documentClient);

                    log.LogInformation(@"manualOrderDoc: {ManualOrderDoc}", manualOrderDoc);
                    var orderController = new OrderController(new LocationController());

                    if (manualOrderDoc == null)
                    {
                        log.LogInformation("Creating new ManualOrder.");
                        var manualOrder = orderController.CreateManualOrder(atgOrderRes);

                        await documentClient.CreateDocumentAsync(uri, manualOrder);
                    }
                    else
                    {
                        log.LogInformation("Updating existing ManualOrder.");
                        var updatedManualOrder = orderController.UpdateManualOrder(atgOrderRes, manualOrderDoc);

                        await documentClient.ReplaceDocumentAsync(manualOrderDoc.SelfLink, updatedManualOrder);
                    }
                }
                catch (Exception ex)
                {
                    var title = "Error in CreateOrUpdateManualOrder";
                    log.LogError(ex, title);
#if RELEASE
                    var teamsMessage = new TeamsMessage(title, $"Error message: {ex.Message}. Stacktrace: {ex.StackTrace}", "red", errorLogsUrl);
                    teamsMessage.LogToTeams(teamsMessage);
#endif
                }
            }
        }
        public async Task <double> EstimateShippingCost(double weight, ShippingAddress shipTo, ShippingAddress shipFrom, AtgOrderRes atgOrderRes)
        {
            _logger.LogInformation("EstimateShippingCost start");
            var retryPolicy = Policy.Handle <Exception>().Retry(3, (ex, count) =>
            {
                var title = "Error in EstimateShippingCost";
                _logger.LogWarning($"{title}. Retrying...");

                if (count == 3)
                {
                    var teamsMessage = new TeamsMessage(title, $"Error: {ex.Message}. Stacktrace: {ex.StackTrace}", "yellow", SourcingEngineFunctions.errorLogsUrl);
                    teamsMessage.LogToTeams(teamsMessage);
                    _logger.LogError(ex, title);
                }
            });

            return(await retryPolicy.Execute(async() =>
            {
                var requestBody = new ShipQuoteRequest
                {
                    RateType = "Ground", // Default to Ground unless shipping next day or second day
                    OriginAddress = shipFrom,
                    DestinationAddress = shipTo,
                    Package = new Package()
                    {
                        Weight = weight
                    }
                };

                if (atgOrderRes.shipping.shipViaCode == "SECOND_DAY")
                {
                    requestBody.RateType = "Second Day Air";
                }
                else if (atgOrderRes.shipping.shipViaCode == "NEXT_DAY")
                {
                    requestBody.RateType = "Next Day Air";
                }

                var jsonRequest = JsonConvert.SerializeObject(requestBody);

                var baseUrl = "https://ups-microservices.azurewebsites.net/api/rating";
                var client = new RestClient(baseUrl);

                var request = new RestRequest(Method.POST)
                              .AddHeader("Content-Type", "application/json")
                              .AddQueryParameter("code", Environment.GetEnvironmentVariable("QUOTE_SHIPMENT_KEY"))
                              .AddParameter("application/json; charset=utf-8", jsonRequest, ParameterType.RequestBody);

                var response = await client.ExecuteAsync(request);
                _logger.LogInformation(@"Quote shipment response status code: {0}. Content: {1}", response.StatusCode, response.Content);

                if (response?.StatusCode != HttpStatusCode.OK)
                {
                    throw new ArgumentException("Est Shipping Cost returned bad request object.");
                }

                double.TryParse(response.Content, out double rate);
                _logger.LogInformation($"Branch {shipFrom.branchNumber} Estimated Shipping Cost: {rate}");

                if (rate == 0)
                {
                    var errorTitle = "Warning in EstimateShippingCost.";
                    var message = "Ship Quote returned $0 estimate.";
                    var teamsMessage = new TeamsMessage(errorTitle, message, "yellow", SourcingEngineFunctions.errorLogsUrl);
                    teamsMessage.LogToTeams(teamsMessage);
                    _logger.LogWarning(message);
                }

                _logger.LogInformation("EstimateShippingCost finish");
                return rate;
            }));
        }
        /// <summary>
        ///     Sets the Trilogie fields on the ATG Order, incld. error message, status and order ID.
        /// </summary>
        /// <param name="trilogieReq">Request containing the results of submitting the order to Trilogie.</param>
        /// <param name="atgOrder">ATG Order object containing line items with shipFrom values.</param>
        public static async Task SetTrilogieFieldsOnATGOrder(TrilogieRequest trilogieReq, AtgOrderRes atgOrder)
        {
            atgOrder.trilogieErrorMessage = trilogieReq.TrilogieErrorMessage;
            atgOrder.trilogieOrderId      = trilogieReq.TrilogieOrderId;
            atgOrder.trilogieStatus       = trilogieReq.TrilogieStatus.ToString();
#if DEBUG
            // Sets to false if order failed in Trilogie
            atgOrder.processSourcing = trilogieReq.TrilogieStatus == TrilogieStatus.Pass;
#endif
        }
 /// <summary>
 ///     Returns the matching line on the ATG order. Useful when working from the AllLines class and needing to get the matching
 ///     ATG order line.
 /// </summary>
 /// <param name="lineId">Index of the line.</param>
 /// <returns>Matching line on the ATG order.</returns>
 public ItemRes GetOrderItemByLineId(int lineId, AtgOrderRes atgOrderRes)
 {
     return(atgOrderRes.items.FirstOrDefault(i => i.lineId == lineId));
 }