diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index 84885c6..4341aa1 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -10,7 +10,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.0.100 + dotnet-version: 3.1.101 - name: Build with dotnet run: dotnet build PlanB.Butler.sln --configuration Release @@ -23,4 +23,15 @@ jobs: with: dotnet-version: 2.2.108 - name: Test with dotnet - run: dotnet test PlanB.Butler.Library/PlanB.Butler.Library.Test/PlanB.Butler.Library.Test.csproj --configuration Release + run: dotnet test PlanB.Butler.Library/PlanB.Butler.Library.Test/PlanB.Butler.Library.Test.csproj --configuration Release --collect:"Code Coverage" + + test-service-library: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.101 + - name: Test with dotnet + run: dotnet test PlanB.Butler.Services/PlanB.Butler.Services.Test/PlanB.Butler.Services.Test.csproj --configuration Release --collect:"Code Coverage" diff --git a/PlanB.Butler.Admin/PlanB.Butler.Admin/Contracts/IMealService.cs b/PlanB.Butler.Admin/PlanB.Butler.Admin/Contracts/IMealService.cs index eb05bf5..bf6306f 100644 --- a/PlanB.Butler.Admin/PlanB.Butler.Admin/Contracts/IMealService.cs +++ b/PlanB.Butler.Admin/PlanB.Butler.Admin/Contracts/IMealService.cs @@ -23,8 +23,8 @@ public interface IMealService /// Creates the meal. /// /// The meal. - /// True or false. - Task CreateMeal(MealViewModel meal); + /// Meal. + Task CreateMeal(MealViewModel meal); /// /// Updates the meal. @@ -40,5 +40,11 @@ public interface IMealService /// Meal by Id. Task GetMeal(string id); + /// + /// Deletes the meal. + /// + /// The identifier. + /// Succes or failure. + Task DeleteMeal(string id); } } diff --git a/PlanB.Butler.Admin/PlanB.Butler.Admin/Controllers/MealController.cs b/PlanB.Butler.Admin/PlanB.Butler.Admin/Controllers/MealController.cs index 48ff4f7..06f0ef6 100644 --- a/PlanB.Butler.Admin/PlanB.Butler.Admin/Controllers/MealController.cs +++ b/PlanB.Butler.Admin/PlanB.Butler.Admin/Controllers/MealController.cs @@ -81,7 +81,6 @@ public async Task Edit(string id, [Bind("Id,CorrelationId,Date,Pr if (this.ModelState.IsValid) { - var result = await this.mealService.UpdateMeal(meal); return this.RedirectToAction(nameof(this.Index)); } @@ -94,7 +93,7 @@ public async Task Edit(string id, [Bind("Id,CorrelationId,Date,Pr /// /// The identifier. /// Meal. - public async Task Edit(string? id) + public async Task Edit(string id) { if (string.IsNullOrEmpty(id)) { @@ -110,5 +109,42 @@ public async Task Edit(string? id) return this.View(meal); } + + /// + /// Deletes the specified identifier. + /// + /// The identifier. + /// IActionResult. + public async Task Delete(string id) + { + if (string.IsNullOrEmpty(id)) + { + return this.NotFound(); + } + + var meal = await this.mealService.GetMeal(id); + + if (meal == null) + { + return this.NotFound(); + } + + return this.View(meal); + } + + /// + /// Deletes the confirmed. + /// + /// The identifier. + /// IActionResult. + [HttpPost] + [ActionName("Delete")] + [ValidateAntiForgeryToken] + public async Task DeleteConfirmed(string id) + { + await this.mealService.DeleteMeal(id); + + return this.RedirectToAction(nameof(this.Index)); + } } } diff --git a/PlanB.Butler.Admin/PlanB.Butler.Admin/Services/MealService.cs b/PlanB.Butler.Admin/PlanB.Butler.Admin/Services/MealService.cs index 715a2cf..5d0e706 100644 --- a/PlanB.Butler.Admin/PlanB.Butler.Admin/Services/MealService.cs +++ b/PlanB.Butler.Admin/PlanB.Butler.Admin/Services/MealService.cs @@ -47,7 +47,7 @@ public MealService(HttpClient httpClient, IConfiguration configuration) /// /// True or false. /// - public async Task CreateMeal(MealViewModel meal) + public async Task CreateMeal(MealViewModel meal) { Guid correlationId = Guid.NewGuid(); meal.CorrelationId = correlationId; @@ -63,8 +63,26 @@ public async Task CreateMeal(MealViewModel meal) Util.AddDefaultEsbHeaders(httpRequestMessage, correlationId, this.config["FunctionsKey"]); var result = await this.httpClient.SendAsync(httpRequestMessage); result.EnsureSuccessStatusCode(); - var success = result.IsSuccessStatusCode; - return success; + + MealViewModel responseModel = JsonConvert.DeserializeObject(result.Content.ReadAsStringAsync().Result); + return responseModel; + } + + /// + /// Deletes the meal. + /// + /// The identifier. + /// + /// Succes or failure. + /// + public async Task DeleteMeal(string id) + { + var uri = this.config["MealsUri"].TrimEnd('/') + "/" + id; + + this.httpClient.DefaultRequestHeaders.Add(Constants.FunctionsKeyHeader, this.config["FunctionsKey"]); + var response = await this.httpClient.DeleteAsync(uri); + + return response.IsSuccessStatusCode; } /// diff --git a/PlanB.Butler.Admin/PlanB.Butler.Admin/Views/Meal/Delete.cshtml b/PlanB.Butler.Admin/PlanB.Butler.Admin/Views/Meal/Delete.cshtml new file mode 100644 index 0000000..64973a3 --- /dev/null +++ b/PlanB.Butler.Admin/PlanB.Butler.Admin/Views/Meal/Delete.cshtml @@ -0,0 +1,50 @@ +@model PlanB.Butler.Admin.Models.MealViewModel + +@{ + ViewData["Title"] = "Delete"; +} + +

Delete

+ +

Are you sure you want to delete this?

+
+

MealViewModel

+
+
+
+ @Html.DisplayNameFor(model => model.Id) +
+
+ @Html.DisplayFor(model => model.Id) +
+
+ @Html.DisplayNameFor(model => model.Date) +
+
+ @Html.DisplayFor(model => model.Date) +
+
+ @Html.DisplayNameFor(model => model.Price) +
+
+ @Html.DisplayFor(model => model.Price) +
+
+ @Html.DisplayNameFor(model => model.Name) +
+
+ @Html.DisplayFor(model => model.Name) +
+
+ @Html.DisplayNameFor(model => model.Restaurant) +
+
+ @Html.DisplayFor(model => model.Restaurant) +
+
+ +
+ | + Back to List +
+
diff --git a/PlanB.Butler.Bot/Bots/TeamsBot.cs b/PlanB.Butler.Bot/Bots/TeamsBot.cs index 0792c6b..65751c8 100644 --- a/PlanB.Butler.Bot/Bots/TeamsBot.cs +++ b/PlanB.Butler.Bot/Bots/TeamsBot.cs @@ -1,18 +1,24 @@ -namespace PlanB.Butler.Bot -{ - using System.Collections.Generic; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.Bot.Builder; - using Microsoft.Bot.Builder.Dialogs; - using Microsoft.Bot.Schema; - using Microsoft.Extensions.Logging; - using System.Resources; - using System.Reflection; +// Copyright (c) PlanB. GmbH. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - // This bot is derived (view DialogBot) from the TeamsACtivityHandler class currently included as part of this sample. +using System.Collections.Generic; +using System.Reflection; +using System.Resources; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Schema; +using Microsoft.Extensions.Logging; +namespace PlanB.Butler.Bot +{ + /// + /// This bot is derived (view DialogBot) from the TeamsACtivityHandler class currently included as part of this sample. + /// + /// + /// public class TeamsBot : DialogBot where T : Dialog { /// diff --git a/PlanB.Butler.Bot/Dialogs/NextOrder.cs b/PlanB.Butler.Bot/Dialogs/NextOrder.cs index fb7b599..2bb11ad 100644 --- a/PlanB.Butler.Bot/Dialogs/NextOrder.cs +++ b/PlanB.Butler.Bot/Dialogs/NextOrder.cs @@ -121,14 +121,14 @@ public NextOrder(IOptions config, IBotTelemetryClient telemetryClient // This array defines how the Waterfall will execute. var waterfallSteps = new WaterfallStep[] { - CompanyStepAsync, + this.CompanyStepAsync, NameStepAsync, RestaurantStepAsync, - QuantatyStepAsync, + QuantityStepAsync, FoodStepAsync, - MealQuantatyStepAsync, + MealQuantityStepAsync, PriceStepAsync, - SummaryStepAsync, + this.SummaryStepAsync, }; // Add named dialogs to the DialogSet. These names are saved in the dialog state. @@ -280,7 +280,13 @@ private static async Task RestaurantStepAsync(WaterfallStepCon } } - private static async Task QuantatyStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) + /// + /// Quantities the step asynchronous. + /// + /// The step context. + /// The cancellation token. + /// DialogTurnResult. + private static async Task QuantityStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) { try { @@ -364,8 +370,13 @@ private static async Task FoodStepAsync(WaterfallStepContext s } } - - private static async Task MealQuantatyStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) + /// + /// Meals the quantity step asynchronous. + /// + /// The step context. + /// The cancellation token. + /// DialogTurnResult. + private static async Task MealQuantityStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) { var obj = ((FoundChoice)stepContext.Result).Value; if (stepContext.Values["rest1"].ToString() == "yes") diff --git a/PlanB.Butler.Bot/GlobalSuppressions.cs b/PlanB.Butler.Bot/GlobalSuppressions.cs new file mode 100644 index 0000000..8054593 --- /dev/null +++ b/PlanB.Butler.Bot/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Makes no sense due to Generic", Scope = "type", Target = "~T:PlanB.Butler.Bot.TeamsBot`1")] diff --git a/PlanB.Butler.Bot/PlanB.Butler.Bot.csproj b/PlanB.Butler.Bot/PlanB.Butler.Bot.csproj index e6a1291..f997d18 100644 --- a/PlanB.Butler.Bot/PlanB.Butler.Bot.csproj +++ b/PlanB.Butler.Bot/PlanB.Butler.Bot.csproj @@ -3,14 +3,21 @@ netcoreapp2.2 latest + PlanB. GmbH + PlanB Butler Bot + PlanB. GmbH + bin\Release\netcoreapp2.2\ + bin\Release\netcoreapp2.2\PlanB.Butler.Bot.xml + bin\Debug\netcoreapp2.2\ + bin\Debug\netcoreapp2.2\PlanB.Butler.Bot.xml @@ -28,7 +35,6 @@ - diff --git a/PlanB.Butler.Bot/Program.cs b/PlanB.Butler.Bot/Program.cs index 7125613..a119161 100644 --- a/PlanB.Butler.Bot/Program.cs +++ b/PlanB.Butler.Bot/Program.cs @@ -3,18 +3,30 @@ // // Generated with Bot Builder V4 SDK Template for Visual Studio EchoBot v4.5.0 +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; + namespace PlanB.Butler.Bot { - using Microsoft.AspNetCore; - using Microsoft.AspNetCore.Hosting; - + /// + /// Program. + /// public class Program { + /// + /// Defines the entry point of the application. + /// + /// The arguments. public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } + /// + /// Creates the web host builder. + /// + /// The arguments. + /// IWebHostBuilder. public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup(); diff --git a/PlanB.Butler.Bot/Startup.cs b/PlanB.Butler.Bot/Startup.cs index 06a263b..2c9c8fa 100644 --- a/PlanB.Butler.Bot/Startup.cs +++ b/PlanB.Butler.Bot/Startup.cs @@ -5,6 +5,7 @@ using System.Collections; using System.Collections.Generic; using System.Globalization; + using Microsoft.ApplicationInsights.Extensibility; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; diff --git a/PlanB.Butler.Library/PlanB.Butler.Library/BackendCommunication.cs b/PlanB.Butler.Library/PlanB.Butler.Library/BackendCommunication.cs index 4a58b85..58f77f8 100644 --- a/PlanB.Butler.Library/PlanB.Butler.Library/BackendCommunication.cs +++ b/PlanB.Butler.Library/PlanB.Butler.Library/BackendCommunication.cs @@ -27,6 +27,7 @@ public class BackendCommunication /// The storage account URL. /// The storage account key. /// + [Obsolete("Call function instead")] public string GetDocument(string container, string resourceName, string storageAccountUrl, string storageAccountKey) { using (HttpClient httpClient = new HttpClient()) @@ -92,6 +93,7 @@ public string GenerateStorageSasTokenWrite(string resourceName, string storageAc return sasToken; } + [Obsolete("Call function instead")] public HttpStatusCode PutDocument(string container, string resourceName, string body, string queueName, string serviceBusConnectionString) { string label = $"{container}/{resourceName}"; @@ -121,6 +123,7 @@ public HttpStatusCode PutDocument(string container, string resourceName, string /// Name of the queue. /// The service bus connection string. /// + [Obsolete("Call function instead")] public HttpStatusCode PutDocumentByteArray(string container, string resourceName, byte[] body, string queueName, string serviceBusConnectionString) { string label = $"{container}/{resourceName}"; @@ -139,6 +142,7 @@ public HttpStatusCode PutDocumentByteArray(string container, string resourceName } } + [Obsolete("Call function instead")] public string GenerateServiceBusSasToken(string serviceBusConnectionString, string que) { var connectionString = serviceBusConnectionString; diff --git a/PlanB.Butler.Library/PlanB.Butler.Library/BotModels/Order.cs b/PlanB.Butler.Library/PlanB.Butler.Library/BotModels/Order.cs index e8bbcf5..82a6bcb 100644 --- a/PlanB.Butler.Library/PlanB.Butler.Library/BotModels/Order.cs +++ b/PlanB.Butler.Library/PlanB.Butler.Library/BotModels/Order.cs @@ -21,8 +21,17 @@ public class Order public double Price { get; set; } + [Obsolete("Please use 'Quantity'")] public int Quantaty { get; set; } + /// + /// Gets or sets the quantity. + /// + /// + /// The quantity. + /// + public int Quantity { get; set; } + public double Grand { get; set; } } } diff --git a/PlanB.Butler.Services/PlanB.Butler.Services.Test/MealServiceMockTest.cs b/PlanB.Butler.Services/PlanB.Butler.Services.Test/MealServiceMockTest.cs index e8fdd11..d5aca4d 100644 --- a/PlanB.Butler.Services/PlanB.Butler.Services.Test/MealServiceMockTest.cs +++ b/PlanB.Butler.Services/PlanB.Butler.Services.Test/MealServiceMockTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -13,6 +14,7 @@ using Microsoft.WindowsAzure.Storage.Blob; using Moq; using Newtonsoft.Json; +using PlanB.Butler.Services.Controllers; using PlanB.Butler.Services.Models; namespace PlanB.Butler.Services.Test @@ -38,6 +40,11 @@ public class MealServiceMockTest /// private Mock mockBlobContainer; + /// + /// The mock BLOB. + /// + private Mock mockBlob; + /// /// The correlation identifier. /// @@ -59,8 +66,11 @@ public void Init() this.context = new Microsoft.Azure.WebJobs.ExecutionContext() { FunctionName = nameof(MealService) }; this.log = new FunctionTestLogger(); - var mockBlobUri = new Uri("http://bogus/myaccount/blob"); + var mockBlobUri = new Uri("http://localhost/container"); this.mockBlobContainer = new Mock(MockBehavior.Loose, mockBlobUri); + this.mockBlob = new Mock(new Uri("http://localhost/blob")); + this.mockBlob.Setup(n => n.UploadTextAsync(It.IsAny())).Returns(Task.FromResult(true)); + this.mockBlobContainer.Setup(n => n.GetBlockBlobReference(It.IsAny())).Returns(this.mockBlob.Object); } /// @@ -75,14 +85,14 @@ public void CreateMealTest() Date = DateTime.Now, Name = "Kässpätzle", Price = 2.3, - Restaurant = "Gasthof Adler", + Restaurant = "Gasthof Adler " + DateTime.Now.Ticks, }; // Setup Mock var httpRequest = CreateMockRequest(mealModel); var result = MealService.CreateMeal(httpRequest.Object, this.mockBlobContainer.Object, this.log, this.context).Result; Assert.IsNotNull(result); - Assert.AreEqual(typeof(OkResult), result.GetType()); + Assert.AreEqual(typeof(OkObjectResult), result.GetType()); } /// diff --git a/PlanB.Butler.Services/PlanB.Butler.Services.Test/MealServiceTest.cs b/PlanB.Butler.Services/PlanB.Butler.Services.Test/MealServiceTest.cs index 0d44564..37f3a31 100644 --- a/PlanB.Butler.Services/PlanB.Butler.Services.Test/MealServiceTest.cs +++ b/PlanB.Butler.Services/PlanB.Butler.Services.Test/MealServiceTest.cs @@ -2,7 +2,10 @@ // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; +using PlanB.Butler.Services.Controllers; +using PlanB.Butler.Services.Models; namespace PlanB.Butler.Services.Test { @@ -189,5 +192,82 @@ public void IsDateInRangeCheckEndEqualCheck() var result = MealService.IsDateInRange(startDate, endDate, toCheckDate); Assert.AreEqual(true, result); } + + /// + /// Validates the meal test ok. + /// + [TestMethod] + public void ValidateMealTestOk() + { + Guid correlationId = Guid.NewGuid(); + + MealModel mealModel = new MealModel() + { + Name = "Test", + Restaurant = "TestRestaurant", + Date = DateTime.Now, + }; + + var result = MealService.Validate(mealModel, correlationId, out ErrorModel errorModel); + Assert.AreEqual(true, result); + Assert.IsNull(errorModel); + } + + /// + /// Validates the meal test missing meal. + /// + [TestMethod] + public void ValidateMealTestMissingMeal() + { + Guid correlationId = Guid.NewGuid(); + + MealModel mealModel = new MealModel() + { + Restaurant = "TestRestaurant", + Date = DateTime.Now, + }; + + var result = MealService.Validate(mealModel, correlationId, out ErrorModel errorModel); + Assert.AreEqual(false, result); + Assert.IsNotNull(errorModel); + } + + /// + /// Validates the meal test missing restaurant. + /// + [TestMethod] + public void ValidateMealTestMissingRestaurant() + { + Guid correlationId = Guid.NewGuid(); + + MealModel mealModel = new MealModel() + { + Name = "Test", + Date = DateTime.Now, + }; + + var result = MealService.Validate(mealModel, correlationId, out ErrorModel errorModel); + Assert.AreEqual(false, result); + Assert.IsNotNull(errorModel); + } + + /// + /// Validates the meal test missing date. + /// + [TestMethod] + public void ValidateMealTestMissingDate() + { + Guid correlationId = Guid.NewGuid(); + + MealModel mealModel = new MealModel() + { + Name = "Test", + Restaurant = "TestRestaurant", + }; + + var result = MealService.Validate(mealModel, correlationId, out ErrorModel errorModel); + Assert.AreEqual(false, result); + Assert.IsNotNull(errorModel); + } } } diff --git a/PlanB.Butler.Services/PlanB.Butler.Services.Test/PlanB.Butler.Services.Test.csproj b/PlanB.Butler.Services/PlanB.Butler.Services.Test/PlanB.Butler.Services.Test.csproj index f5caa69..25d22f6 100644 --- a/PlanB.Butler.Services/PlanB.Butler.Services.Test/PlanB.Butler.Services.Test.csproj +++ b/PlanB.Butler.Services/PlanB.Butler.Services.Test/PlanB.Butler.Services.Test.csproj @@ -1,13 +1,13 @@ - netcoreapp3.0 + netcoreapp3.1 false - bin\Debug\netcoreapp3.0\ + bin\Debug\netcoreapp3.1\ bin\Debug\netcoreapp3.1\PlanB.Butler.Services.Test.xml diff --git a/PlanB.Butler.Services/PlanB.Butler.Services.Test/RestaurantServiceMockTest.cs b/PlanB.Butler.Services/PlanB.Butler.Services.Test/RestaurantServiceMockTest.cs new file mode 100644 index 0000000..efe9928 --- /dev/null +++ b/PlanB.Butler.Services/PlanB.Butler.Services.Test/RestaurantServiceMockTest.cs @@ -0,0 +1,151 @@ +// Copyright (c) PlanB. GmbH. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.ServiceBus; +using Microsoft.Extensions.Primitives; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.WindowsAzure.Storage.Blob; +using Moq; +using Newtonsoft.Json; +using PlanB.Butler.Services.Controllers; +using PlanB.Butler.Services.Models; + +namespace PlanB.Butler.Services.Test +{ + /// + /// RestaurantServiceMockTest. + /// + [TestClass] + public class RestaurantServiceMockTest + { + /// + /// The context. + /// + private Microsoft.Azure.WebJobs.ExecutionContext context; + + /// + /// The log. + /// + private FunctionTestLogger log; + + /// + /// The mock BLOB container. + /// + private Mock mockBlobContainer; + + /// + /// The mock BLOB. + /// + private Mock mockBlob; + + /// + /// The correlation identifier. + /// + private Guid correlationId; + + /// + /// The message header. + /// + private Message messageHeader; + + /// + /// Initializes this instance. + /// + [TestInitialize] + public void Init() + { + this.correlationId = Guid.NewGuid(); + this.messageHeader = new Message() { CorrelationId = this.correlationId.ToString() }; + this.context = new Microsoft.Azure.WebJobs.ExecutionContext() { FunctionName = nameof(MealService) }; + this.log = new FunctionTestLogger(); + + var mockBlobUri = new Uri("http://localhost/container"); + this.mockBlobContainer = new Mock(MockBehavior.Loose, mockBlobUri); + this.mockBlob = new Mock(new Uri("http://localhost/blob")); + this.mockBlob.Setup(n => n.UploadTextAsync(It.IsAny())).Returns(Task.FromResult(true)); + this.mockBlobContainer.Setup(n => n.GetBlockBlobReference(It.IsAny())).Returns(this.mockBlob.Object); + } + + /// + /// Creates the restaurant test. + /// + [TestMethod] + public void CreateRestaurantOkTest() + { + RestaurantModel restaurantModel = new RestaurantModel() + { + City = "Main City", + EmailAddress = "restaurant@domain.com", + Name = "The Restaurant", + PhoneNumber = "32168", + }; + + // Setup Mock + var httpRequest = CreateMockRequest(restaurantModel); + var result = RestaurantService.CreateRestaurant(httpRequest.Object, this.mockBlobContainer.Object, this.log, this.context).Result; + Assert.IsNotNull(result); + Assert.AreEqual(typeof(OkObjectResult), result.GetType()); + } + + /// + /// Creates the restaurant fail name test. + /// + [TestMethod] + public void CreateRestaurantFailNameTest() + { + RestaurantModel restaurantModel = new RestaurantModel() + { + City = "Main City", + EmailAddress = "restaurant@domain.com", + PhoneNumber = "32168", + }; + + // Setup Mock + var httpRequest = CreateMockRequest(restaurantModel); + var result = RestaurantService.CreateRestaurant(httpRequest.Object, this.mockBlobContainer.Object, this.log, this.context).Result; + Assert.IsNotNull(result); + Assert.AreEqual(typeof(BadRequestObjectResult), result.GetType()); + } + + /// + /// Creates the mock request. + /// + /// The body. + /// HttpRequest. + private static Mock CreateMockRequest(object body) + { + var ms = new MemoryStream(); + var sw = new StreamWriter(ms); + + var json = JsonConvert.SerializeObject(body); + + sw.Write(json); + sw.Flush(); + + ms.Position = 0; + var mockContext = new Mock(); + var mockResponse = new Mock(); + var mockHeaderDictionary = new Mock(); + + mockContext.Setup(c => c.Response).Returns(mockResponse.Object); + mockResponse.Setup(c => c.Headers).Returns(mockHeaderDictionary.Object); + + var mockRequest = new Mock(); + + // mockRequest.Setup(req => req.Query).Returns(new QueryCollection(query)); + Dictionary header = new Dictionary(); + mockRequest.Setup(req => req.Headers).Returns(new HeaderDictionary(header)); + mockRequest.SetupGet(req => req.HttpContext).Returns(mockContext.Object); + mockRequest.Setup(x => x.Body).Returns(ms); + + return mockRequest; + } + } +} diff --git a/PlanB.Butler.Services/PlanB.Butler.Services/FinanceService.cs b/PlanB.Butler.Services/PlanB.Butler.Services/Controllers/FinanceService.cs similarity index 99% rename from PlanB.Butler.Services/PlanB.Butler.Services/FinanceService.cs rename to PlanB.Butler.Services/PlanB.Butler.Services/Controllers/FinanceService.cs index 1f10a80..875c35f 100644 --- a/PlanB.Butler.Services/PlanB.Butler.Services/FinanceService.cs +++ b/PlanB.Butler.Services/PlanB.Butler.Services/Controllers/FinanceService.cs @@ -19,7 +19,7 @@ using Newtonsoft.Json; using PlanB.Butler.Services.Extensions; -namespace PlanB.Butler.Services +namespace PlanB.Butler.Services.Controllers { /// /// Finance. diff --git a/PlanB.Butler.Services/PlanB.Butler.Services/Controllers/MealService.cs b/PlanB.Butler.Services/PlanB.Butler.Services/Controllers/MealService.cs new file mode 100644 index 0000000..01bdb4e --- /dev/null +++ b/PlanB.Butler.Services/PlanB.Butler.Services/Controllers/MealService.cs @@ -0,0 +1,631 @@ +// Copyright (c) PlanB. GmbH. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Net.Mime; +using System.Reflection; +using System.Threading.Tasks; +using System.Web; + +using AzureFunctions.Extensions.Swashbuckle.Attribute; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Extensions.Logging; +using Microsoft.WindowsAzure.Storage; +using Microsoft.WindowsAzure.Storage.Blob; +using Newtonsoft.Json; +using PlanB.Butler.Services.Extensions; +using PlanB.Butler.Services.Models; + +namespace PlanB.Butler.Services.Controllers +{ + /// + /// MealService. + /// + public static class MealService + { + /// + /// The meta date. + /// + private const string MetaDate = "date"; + + /// + /// The meta restaurant. + /// + private const string MetaRestaurant = "restaurant"; + + /// + /// Create meal. + /// + /// The req. + /// The cloud BLOB container. + /// The log. + /// The context. + /// + /// IActionResult. + /// + [FunctionName("CreateMeal")] + [Consumes(MediaTypeNames.Application.Json)] + [ProducesResponseType(typeof(MealModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)] + public static async Task CreateMeal( + [HttpTrigger(AuthorizationLevel.Function, "post", Route = "meals")] + [RequestBodyType(typeof(MealModel), "Meal request")]HttpRequest req, + [Blob("meals", FileAccess.ReadWrite, Connection = "StorageSend")] CloudBlobContainer cloudBlobContainer, + ILogger log, + ExecutionContext context) + { + Guid correlationId = Util.ReadCorrelationId(req.Headers); + var methodName = MethodBase.GetCurrentMethod().Name; + var trace = new Dictionary(); + EventId eventId = new EventId(correlationId.GetHashCode(), Constants.ButlerCorrelationTraceName); + + IActionResult actionResult = null; + using (log.BeginScope("Method:{methodName} CorrelationId:{CorrelationId} Label:{Label}", methodName, correlationId.ToString(), context.InvocationId.ToString())) + { + try + { + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + trace.Add("requestBody", requestBody); + + MealModel mealModel = JsonConvert.DeserializeObject(requestBody); + if (mealModel.CorrelationId == null || mealModel.CorrelationId.Equals(Guid.Empty)) + { + mealModel.CorrelationId = correlationId; + } + + bool isValid = Validate(mealModel, correlationId, out ErrorModel errorModel); + + if (isValid) + { + var fileName = CreateFileName(mealModel); + trace.Add($"fileName", fileName); + mealModel.Id = fileName; + + var fullFileName = $"{fileName}.json"; + trace.Add($"fullFileName", fullFileName); + + req.HttpContext.Response.Headers.Add(Constants.ButlerCorrelationTraceHeader, correlationId.ToString()); + + CloudBlockBlob blob = cloudBlobContainer.GetBlockBlobReference($"{fullFileName}"); + if (blob != null) + { + blob.Properties.ContentType = "application/json"; + var metaDate = mealModel.Date.ToString("yyyyMMdd", CultureInfo.InvariantCulture); + blob.Metadata.Add(MetaDate, metaDate); + blob.Metadata.Add(MetaRestaurant, mealModel.Restaurant); + blob.Metadata.Add(Constants.ButlerCorrelationTraceName, correlationId.ToString().Replace("-", string.Empty)); + var meal = JsonConvert.SerializeObject(mealModel); + trace.Add("meal", meal); + + Task task = blob.UploadTextAsync(requestBody); + task.Wait(); + actionResult = new OkObjectResult(mealModel); + log.LogInformation(correlationId, $"'{methodName}' - success", trace); + } + } + else + { + actionResult = new BadRequestObjectResult(errorModel); + log.LogInformation(correlationId, $"'{methodName}' - is not valid", trace); + } + } + catch (Exception e) + { + trace.Add(string.Format("{0} - {1}", methodName, "rejected"), e.Message); + trace.Add(string.Format("{0} - {1} - StackTrace", methodName, "rejected"), e.StackTrace); + log.LogInformation(correlationId, $"'{methodName}' - rejected", trace); + log.LogError(correlationId, $"'{methodName}' - rejected", trace); + ErrorModel errorModel = new ErrorModel() + { + CorrelationId = correlationId, + Details = e.StackTrace, + Message = e.Message, + }; + + actionResult = new BadRequestObjectResult(errorModel); + } + finally + { + log.LogTrace(eventId, $"'{methodName}' - finished"); + log.LogInformation(correlationId, $"'{methodName}' - finished", trace); + } + } + + return actionResult; + } + + /// + /// Deletes the meal by identifier. + /// + /// The req. + /// The identifier. + /// The BLOB. + /// The log. + /// The context. + /// IActionResult. + [ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status200OK)] + [FunctionName("DeleteMealById")] + public static IActionResult DeleteMealById( + [HttpTrigger(AuthorizationLevel.Function, "delete", Route = "meals/{id}")] HttpRequest req, + string id, + [Blob("meals/{id}.json", FileAccess.ReadWrite, Connection = "StorageSend")] CloudBlockBlob blob, + ILogger log, + ExecutionContext context) + { + Guid correlationId = Util.ReadCorrelationId(req.Headers); + var methodName = MethodBase.GetCurrentMethod().Name; + var trace = new Dictionary(); + EventId eventId = new EventId(correlationId.GetHashCode(), Constants.ButlerCorrelationTraceName); + IActionResult actionResult = null; + + using (log.BeginScope("Method:{methodName} CorrelationId:{CorrelationId} Label:{Label}", methodName, correlationId.ToString(), context.InvocationId.ToString())) + { + try + { + trace.Add(Constants.ButlerCorrelationTraceName, correlationId.ToString()); + trace.Add("id", id); + + if (blob != null) + { + Task task = blob.DeleteIfExistsAsync(); + task.Wait(); + + actionResult = new OkResult(); + log.LogInformation(correlationId, $"'{methodName}' - success", trace); + } + } + catch (Exception e) + { + trace.Add(string.Format("{0} - {1}", methodName, "rejected"), e.Message); + trace.Add(string.Format("{0} - {1} - StackTrace", methodName, "rejected"), e.StackTrace); + log.LogInformation(correlationId, $"'{methodName}' - rejected", trace); + log.LogError(correlationId, $"'{methodName}' - rejected", trace); + + ErrorModel errorModel = new ErrorModel() + { + CorrelationId = correlationId, + Details = e.StackTrace, + Message = e.Message, + }; + actionResult = new BadRequestObjectResult(errorModel); + } + finally + { + log.LogTrace(eventId, $"'{methodName}' - finished"); + log.LogInformation(correlationId, $"'{methodName}' - finished", trace); + } + } + + return actionResult; + } + + /// + /// Updates the meal by identifier. + /// + /// The req. + /// The identifier. + /// The BLOB. + /// The cloud BLOB container. + /// The log. + /// The context. + /// IActionResult. + [FunctionName("UpdateMealById")] + [Consumes(MediaTypeNames.Application.Json)] + [ProducesResponseType(typeof(MealModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)] + public static async Task UpdateMealById( + [HttpTrigger(AuthorizationLevel.Function, "put", Route = "meals/{id}")] HttpRequest req, + string id, + [Blob("meals/{id}.json", FileAccess.ReadWrite, Connection = "StorageSend")] string existingContent, + [Blob("meals", FileAccess.ReadWrite, Connection = "StorageSend")] CloudBlobContainer cloudBlobContainer, + ILogger log, + ExecutionContext context) + { + Guid correlationId = Util.ReadCorrelationId(req.Headers); + var methodName = MethodBase.GetCurrentMethod().Name; + var trace = new Dictionary(); + EventId eventId = new EventId(correlationId.GetHashCode(), Constants.ButlerCorrelationTraceName); + IActionResult actionResult = null; + + MealModel mealModel = null; + using (log.BeginScope("Method:{methodName} CorrelationId:{CorrelationId} Label:{Label}", methodName, correlationId.ToString(), context.InvocationId.ToString())) + { + try + { + trace.Add(Constants.ButlerCorrelationTraceName, correlationId.ToString()); + trace.Add("id", id); + mealModel = JsonConvert.DeserializeObject(existingContent); + + var date = mealModel.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + + var filename = $"{date}-{mealModel.Restaurant}.json"; + trace.Add($"filename", filename); + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + trace.Add("requestBody", requestBody); + mealModel = JsonConvert.DeserializeObject(requestBody); + + req.HttpContext.Response.Headers.Add(Constants.ButlerCorrelationTraceHeader, correlationId.ToString()); + + bool isValid = Validate(mealModel, correlationId, out ErrorModel errorModel); + if (isValid) + { + CloudBlockBlob blob = cloudBlobContainer.GetBlockBlobReference($"{filename}"); + if (blob != null) + { + blob.Properties.ContentType = "application/json"; + var metaDate = mealModel.Date.ToString("yyyyMMdd", CultureInfo.InvariantCulture); + blob.Metadata.Add(MetaDate, metaDate); + blob.Metadata.Add(MetaRestaurant, mealModel.Restaurant); + blob.Metadata.Add(Constants.ButlerCorrelationTraceName, correlationId.ToString().Replace("-", string.Empty)); + var meal = JsonConvert.SerializeObject(mealModel); + trace.Add("meal", meal); + + Task task = blob.UploadTextAsync(meal); + task.Wait(); + actionResult = new OkObjectResult(mealModel); + log.LogInformation(correlationId, $"'{methodName}' - success", trace); + } + } + else + { + actionResult = new BadRequestObjectResult(errorModel); + log.LogInformation(correlationId, $"'{methodName}' - is not valid", trace); + } + } + catch (Exception e) + { + trace.Add(string.Format("{0} - {1}", methodName, "rejected"), e.Message); + trace.Add(string.Format("{0} - {1} - StackTrace", methodName, "rejected"), e.StackTrace); + log.LogInformation(correlationId, $"'{methodName}' - rejected", trace); + log.LogError(correlationId, $"'{methodName}' - rejected", trace); + + ErrorModel errorModel = new ErrorModel() + { + CorrelationId = correlationId, + Details = e.StackTrace, + Message = e.Message, + }; + actionResult = new BadRequestObjectResult(mealModel); + } + finally + { + log.LogTrace(eventId, $"'{methodName}' - finished"); + log.LogInformation(correlationId, $"'{methodName}' - finished", trace); + } + } + + return actionResult; + } + + /// + /// Reads the meals. + /// + /// The req. + /// The cloud BLOB container. + /// The log. + /// The context. + /// + /// All meals. + /// + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)] + [FunctionName("GetMeals")] + public static async Task GetMeals( + [HttpTrigger(AuthorizationLevel.Function, "get", Route = "meals")] HttpRequest req, + [Blob("meals", FileAccess.ReadWrite, Connection = "StorageSend")] CloudBlobContainer cloudBlobContainer, + ILogger log, + ExecutionContext context) + { + Guid correlationId = Util.ReadCorrelationId(req.Headers); + var methodName = MethodBase.GetCurrentMethod().Name; + var trace = new Dictionary(); + EventId eventId = new EventId(correlationId.GetHashCode(), Constants.ButlerCorrelationTraceName); + IActionResult actionResult = null; + + List meals = new List(); + using (log.BeginScope("Method:{methodName} CorrelationId:{CorrelationId} Label:{Label}", methodName, correlationId.ToString(), context.InvocationId.ToString())) + { + try + { + trace.Add(Constants.ButlerCorrelationTraceName, correlationId.ToString()); + string startDateQuery = req.Query["startDate"]; + string endDateQuery = req.Query["endDate"]; + string restaurantQuery = req.Query["restaurant"]; + string prefix = string.Empty; + + bool checkForDate = false; + DateTime start = DateTime.MinValue; + DateTime end = DateTime.MinValue; + + if (!(string.IsNullOrEmpty(startDateQuery) && string.IsNullOrEmpty(endDateQuery))) + { + checkForDate = true; + DateTime.TryParse(startDateQuery, out start); + DateTime.TryParse(endDateQuery, out end); + } + + if (checkForDate) + { + prefix = CreateBlobPrefix(startDateQuery, endDateQuery); + } + + BlobContinuationToken blobContinuationToken = null; + var options = new BlobRequestOptions(); + var operationContext = new OperationContext(); + + List cloudBlockBlobs = new List(); + do + { + var blobs = await cloudBlobContainer.ListBlobsSegmentedAsync(prefix, true, BlobListingDetails.All, null, blobContinuationToken, options, operationContext).ConfigureAwait(false); + blobContinuationToken = blobs.ContinuationToken; + cloudBlockBlobs.AddRange(blobs.Results); + } + while (blobContinuationToken != null); + + foreach (var item in cloudBlockBlobs) + { + CloudBlockBlob blob = (CloudBlockBlob)item; + if (checkForDate) + { + await blob.FetchAttributesAsync(); + if (blob.Metadata.ContainsKey(MetaDate)) + { + var mealMetaDate = blob.Metadata[MetaDate]; + DateTime mealDate = DateTime.MinValue; + if (DateTime.TryParse(mealMetaDate, out mealDate)) + { + var isDateInRange = IsDateInRange(start, end, mealDate); + if (isDateInRange) + { + var blobContent = blob.DownloadTextAsync(); + var blobMeal = JsonConvert.DeserializeObject(await blobContent); + meals.Add(blobMeal); + } + } + } + } + else + { + var content = blob.DownloadTextAsync(); + var meal = JsonConvert.DeserializeObject(await content); + meals.Add(meal); + } + } + + log.LogInformation(correlationId, $"'{methodName}' - success", trace); + actionResult = new OkObjectResult(meals); + } + catch (Exception e) + { + trace.Add(string.Format("{0} - {1}", methodName, "rejected"), e.Message); + trace.Add(string.Format("{0} - {1} - StackTrace", methodName, "rejected"), e.StackTrace); + log.LogInformation(correlationId, $"'{methodName}' - rejected", trace); + log.LogError(correlationId, $"'{methodName}' - rejected", trace); + + ErrorModel errorModel = new ErrorModel() + { + CorrelationId = correlationId, + Details = e.StackTrace, + Message = e.Message, + }; + actionResult = new BadRequestObjectResult(errorModel); + } + finally + { + log.LogTrace(eventId, $"'{methodName}' - finished"); + log.LogInformation(correlationId, $"'{methodName}' - finished", trace); + } + } + + return actionResult; + } + + /// + /// Gets the meal by id. + /// + /// The req. + /// The identifier. + /// The BLOB. + /// The log. + /// The context. + /// + /// Meal by id. + /// + [ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(MealModel), StatusCodes.Status200OK)] + [FunctionName("GetMealById")] + public static IActionResult GetMealById( + [HttpTrigger(AuthorizationLevel.Function, "get", Route = "meals/{id}")] HttpRequest req, + string id, + [Blob("meals/{id}.json", FileAccess.ReadWrite, Connection = "StorageSend")] string blob, + ILogger log, + ExecutionContext context) + { + Guid correlationId = Util.ReadCorrelationId(req.Headers); + var methodName = MethodBase.GetCurrentMethod().Name; + var trace = new Dictionary(); + EventId eventId = new EventId(correlationId.GetHashCode(), Constants.ButlerCorrelationTraceName); + IActionResult actionResult = null; + + MealModel mealModel = null; + using (log.BeginScope("Method:{methodName} CorrelationId:{CorrelationId} Label:{Label}", methodName, correlationId.ToString(), context.InvocationId.ToString())) + { + try + { + trace.Add(Constants.ButlerCorrelationTraceName, correlationId.ToString()); + trace.Add("id", id); + mealModel = JsonConvert.DeserializeObject(blob); + + log.LogInformation(correlationId, $"'{methodName}' - success", trace); + actionResult = new OkObjectResult(mealModel); + } + catch (Exception e) + { + trace.Add(string.Format("{0} - {1}", methodName, "rejected"), e.Message); + trace.Add(string.Format("{0} - {1} - StackTrace", methodName, "rejected"), e.StackTrace); + log.LogInformation(correlationId, $"'{methodName}' - rejected", trace); + log.LogError(correlationId, $"'{methodName}' - rejected", trace); + + ErrorModel errorModel = new ErrorModel() + { + CorrelationId = correlationId, + Details = e.StackTrace, + Message = e.Message, + }; + actionResult = new BadRequestObjectResult(mealModel); + } + finally + { + log.LogTrace(eventId, $"'{methodName}' - finished"); + log.LogInformation(correlationId, $"'{methodName}' - finished", trace); + } + } + + return actionResult; + } + + /// + /// Determines whether the date in range compared to start and end. + /// + /// The start. + /// The end. + /// To check. + /// + /// true if date is in range; otherwise, false. + /// + internal static bool IsDateInRange(DateTime start, DateTime end, DateTime toCheck) + { + if (toCheck < start) + { + return false; + } + + if (end < toCheck) + { + return false; + } + + if (start.Equals(toCheck)) + { + return true; + } + + if (end.Equals(toCheck)) + { + return true; + } + + if (start.Equals(end) && start.Equals(toCheck)) + { + return true; + } + + long difference = toCheck.Ticks - start.Ticks; + long sum = start.Ticks + difference; + + if (sum < end.Ticks) + { + return true; + } + + return false; + } + + /// + /// Creates the BLOB prefix. + /// + /// The start date. + /// The end date. + /// Prefix. + internal static string CreateBlobPrefix(string startDate, string endDate) + { + string prefix = string.Empty; + if (string.IsNullOrEmpty(startDate) || string.IsNullOrEmpty(endDate)) + { + return prefix; + } + + if (startDate.Length == endDate.Length) + { + for (int i = 0; i < startDate.Length; i++) + { + if (startDate[i] == endDate[i]) + { + prefix += startDate[i]; + } + else + { + break; + } + } + } + + return prefix; + } + + /// + /// Validates the meal. + /// + /// The meal model. + /// The correlation identifier. + /// The error model. + /// True if data is valid; otherwise False. + internal static bool Validate(MealModel mealModel, Guid correlationId, out ErrorModel errorModel) + { + bool isValid = true; + errorModel = null; + if (string.IsNullOrEmpty(mealModel.Name)) + { + errorModel = new ErrorModel() + { + CorrelationId = correlationId, + Message = "No meal name!", + }; + isValid = false; + } + + if (string.IsNullOrEmpty(mealModel.Restaurant)) + { + errorModel = new ErrorModel() + { + CorrelationId = correlationId, + Message = "No meal restaurant!", + }; + isValid = false; + } + + if (mealModel.Date == null || mealModel.Date == DateTime.MinValue) + { + errorModel = new ErrorModel() + { + CorrelationId = correlationId, + Message = "No meal date!", + }; + isValid = false; + } + + return isValid; + } + + /// + /// Creates the name of the file. + /// + /// The model. + /// FileName without extension. + internal static string CreateFileName(MealModel model) + { + var date = model.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + string fileName = $"{date}-{model.Restaurant}"; + fileName = HttpUtility.UrlEncode(fileName); + return fileName; + } + } +} diff --git a/PlanB.Butler.Services/PlanB.Butler.Services/OrderService.cs b/PlanB.Butler.Services/PlanB.Butler.Services/Controllers/OrderService.cs similarity index 99% rename from PlanB.Butler.Services/PlanB.Butler.Services/OrderService.cs rename to PlanB.Butler.Services/PlanB.Butler.Services/Controllers/OrderService.cs index 428c313..8dda202 100644 --- a/PlanB.Butler.Services/PlanB.Butler.Services/OrderService.cs +++ b/PlanB.Butler.Services/PlanB.Butler.Services/Controllers/OrderService.cs @@ -22,7 +22,7 @@ using PlanB.Butler.Services.Extensions; using PlanB.Butler.Services.Models; -namespace PlanB.Butler.Services +namespace PlanB.Butler.Services.Controllers { /// /// OrderService. diff --git a/PlanB.Butler.Services/PlanB.Butler.Services/Controllers/RestaurantService.cs b/PlanB.Butler.Services/PlanB.Butler.Services/Controllers/RestaurantService.cs new file mode 100644 index 0000000..8670425 --- /dev/null +++ b/PlanB.Butler.Services/PlanB.Butler.Services/Controllers/RestaurantService.cs @@ -0,0 +1,402 @@ +// Copyright (c) PlanB. GmbH. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Web; + +using AzureFunctions.Extensions.Swashbuckle.Attribute; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Extensions.Logging; +using Microsoft.WindowsAzure.Storage; +using Microsoft.WindowsAzure.Storage.Blob; +using Newtonsoft.Json; +using PlanB.Butler.Services.Extensions; +using PlanB.Butler.Services.Models; + +namespace PlanB.Butler.Services.Controllers +{ + /// + /// RestaurantService. + /// + public static class RestaurantService + { + /// + /// The meta restaurant. + /// + private const string MetaRestaurant = "restaurant"; + + /// + /// The meta city. + /// + private const string MetaCity = "city"; + + /// + /// Gets the restaurants. + /// + /// The req. + /// The cloud BLOB container. + /// The log. + /// The context. + /// All Restaurants. + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)] + [FunctionName("GetRestaurants")] + public static async Task GetRestaurants( + [HttpTrigger(AuthorizationLevel.Function, "get", Route = "restaurants")] HttpRequest req, + [Blob("restaurants", FileAccess.ReadWrite, Connection = "StorageSend")] CloudBlobContainer cloudBlobContainer, + ILogger log, + ExecutionContext context) + { + Guid correlationId = Util.ReadCorrelationId(req.Headers); + var methodName = MethodBase.GetCurrentMethod().Name; + var trace = new Dictionary(); + EventId eventId = new EventId(correlationId.GetHashCode(), Constants.ButlerCorrelationTraceName); + IActionResult actionResult = null; + + List restaurant = new List(); + using (log.BeginScope("Method:{methodName} CorrelationId:{CorrelationId} Label:{Label}", methodName, correlationId.ToString(), context.InvocationId.ToString())) + { + try + { + BlobContinuationToken blobContinuationToken = null; + var options = new BlobRequestOptions(); + var operationContext = new OperationContext(); + + List cloudBlockBlobs = new List(); + do + { + var blobs = await cloudBlobContainer.ListBlobsSegmentedAsync(null, true, BlobListingDetails.All, null, blobContinuationToken, options, operationContext).ConfigureAwait(false); + blobContinuationToken = blobs.ContinuationToken; + cloudBlockBlobs.AddRange(blobs.Results); + } + while (blobContinuationToken != null); + + log.LogInformation(correlationId, $"'{methodName}' - success", trace); + actionResult = new OkObjectResult(restaurant); + } + catch (Exception e) + { + trace.Add(string.Format("{0} - {1}", methodName, "rejected"), e.Message); + trace.Add(string.Format("{0} - {1} - StackTrace", methodName, "rejected"), e.StackTrace); + log.LogInformation(correlationId, $"'{methodName}' - rejected", trace); + log.LogError(correlationId, $"'{methodName}' - rejected", trace); + + ErrorModel errorModel = new ErrorModel() + { + CorrelationId = correlationId, + Details = e.StackTrace, + Message = e.Message, + }; + actionResult = new BadRequestObjectResult(errorModel); + } + finally + { + log.LogTrace(eventId, $"'{methodName}' - finished"); + log.LogInformation(correlationId, $"'{methodName}' - finished", trace); + } + } + + return actionResult; + } + + /// + /// Gets the restaurant by identifier. + /// + /// The req. + /// The identifier. + /// The BLOB. + /// The log. + /// The context. + /// Restaurant. + [ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(RestaurantModel), StatusCodes.Status200OK)] + [FunctionName("GetRestaurantById")] + public static IActionResult GetRestaurantById( + [HttpTrigger(AuthorizationLevel.Function, "get", Route = "restaurants/{id}")] HttpRequest req, + string id, + [Blob("restaurants/{id}.json", FileAccess.ReadWrite, Connection = "StorageSend")] string blob, + ILogger log, + ExecutionContext context) + { + Guid correlationId = Util.ReadCorrelationId(req.Headers); + var methodName = MethodBase.GetCurrentMethod().Name; + var trace = new Dictionary(); + EventId eventId = new EventId(correlationId.GetHashCode(), Constants.ButlerCorrelationTraceName); + IActionResult actionResult = null; + + using (log.BeginScope("Method:{methodName} CorrelationId:{CorrelationId} Label:{Label}", methodName, correlationId.ToString(), context.InvocationId.ToString())) + { + try + { + trace.Add(Constants.ButlerCorrelationTraceName, correlationId.ToString()); + trace.Add("id", id); + RestaurantModel model = JsonConvert.DeserializeObject(blob); + + log.LogInformation(correlationId, $"'{methodName}' - success", trace); + actionResult = new OkObjectResult(model); + } + catch (Exception e) + { + trace.Add(string.Format("{0} - {1}", methodName, "rejected"), e.Message); + trace.Add(string.Format("{0} - {1} - StackTrace", methodName, "rejected"), e.StackTrace); + log.LogInformation(correlationId, $"'{methodName}' - rejected", trace); + log.LogError(correlationId, $"'{methodName}' - rejected", trace); + + ErrorModel errorModel = new ErrorModel() + { + CorrelationId = correlationId, + Details = e.StackTrace, + Message = e.Message, + }; + actionResult = new BadRequestObjectResult(errorModel); + } + finally + { + log.LogTrace(eventId, $"'{methodName}' - finished"); + log.LogInformation(correlationId, $"'{methodName}' - finished", trace); + } + } + + return actionResult; + } + + /// + /// Deletes the restaurant by identifier. + /// + /// The req. + /// The identifier. + /// The BLOB. + /// The log. + /// The context. + /// IActionResult. + [ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status200OK)] + [FunctionName("DeleteRestaurantById")] + public static IActionResult DeleteRestaurantById( + [HttpTrigger(AuthorizationLevel.Function, "delete", Route = "restaurants/{id}")] HttpRequest req, + string id, + [Blob("restaurants/{id}.json", FileAccess.ReadWrite, Connection = "StorageSend")] CloudBlockBlob blob, + ILogger log, + ExecutionContext context) + { + Guid correlationId = Util.ReadCorrelationId(req.Headers); + var methodName = MethodBase.GetCurrentMethod().Name; + var trace = new Dictionary(); + EventId eventId = new EventId(correlationId.GetHashCode(), Constants.ButlerCorrelationTraceName); + IActionResult actionResult = null; + + using (log.BeginScope("Method:{methodName} CorrelationId:{CorrelationId} Label:{Label}", methodName, correlationId.ToString(), context.InvocationId.ToString())) + { + try + { + trace.Add(Constants.ButlerCorrelationTraceName, correlationId.ToString()); + trace.Add("id", id); + + if (blob != null) + { + Task task = blob.DeleteIfExistsAsync(); + task.Wait(); + + actionResult = new OkResult(); + log.LogInformation(correlationId, $"'{methodName}' - success", trace); + } + } + catch (Exception e) + { + trace.Add(string.Format("{0} - {1}", methodName, "rejected"), e.Message); + trace.Add(string.Format("{0} - {1} - StackTrace", methodName, "rejected"), e.StackTrace); + log.LogInformation(correlationId, $"'{methodName}' - rejected", trace); + log.LogError(correlationId, $"'{methodName}' - rejected", trace); + + ErrorModel errorModel = new ErrorModel() + { + CorrelationId = correlationId, + Details = e.StackTrace, + Message = e.Message, + }; + actionResult = new BadRequestObjectResult(errorModel); + } + finally + { + log.LogTrace(eventId, $"'{methodName}' - finished"); + log.LogInformation(correlationId, $"'{methodName}' - finished", trace); + } + } + + return actionResult; + } + + /// + /// Creates the restaurant. + /// + /// The req. + /// The cloud BLOB container. + /// The log. + /// The context. + /// IActionResult. + [FunctionName("CreateRestaurant")] + [ProducesResponseType(typeof(RestaurantModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)] + public static async Task CreateRestaurant( + [HttpTrigger(AuthorizationLevel.Function, "post", Route = "restaurants")] + [RequestBodyType(typeof(RestaurantModel), "Restaurant request")]HttpRequest req, + [Blob("restaurants", FileAccess.ReadWrite, Connection = "StorageSend")] CloudBlobContainer cloudBlobContainer, + ILogger log, + ExecutionContext context) + { + Guid correlationId = Util.ReadCorrelationId(req.Headers); + var methodName = MethodBase.GetCurrentMethod().Name; + var trace = new Dictionary(); + EventId eventId = new EventId(correlationId.GetHashCode(), Constants.ButlerCorrelationTraceName); + + IActionResult actionResult = null; + using (log.BeginScope("Method:{methodName} CorrelationId:{CorrelationId} Label:{Label}", methodName, correlationId.ToString(), context.InvocationId.ToString())) + { + try + { + trace.Add(Constants.ButlerCorrelationTraceName, correlationId.ToString()); + string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + trace.Add("requestBody", requestBody); + + RestaurantModel restaurantModel = JsonConvert.DeserializeObject(requestBody); + + bool isValid = Validate(restaurantModel, correlationId, log, out ErrorModel errorModel); + + if (isValid) + { + var fileName = CreateFileName(restaurantModel); + trace.Add($"fileName", fileName); + restaurantModel.Id = fileName; + + var fullFileName = $"{fileName}.json"; + trace.Add($"fullFileName", fullFileName); + + req.HttpContext.Response.Headers.Add(Constants.ButlerCorrelationTraceHeader, correlationId.ToString()); + + CloudBlockBlob blob = cloudBlobContainer.GetBlockBlobReference($"{fullFileName}"); + if (blob != null) + { + blob.Properties.ContentType = "application/json"; + blob.Metadata.Add(Constants.ButlerCorrelationTraceName, correlationId.ToString().Replace("-", string.Empty)); + blob.Metadata.Add(MetaRestaurant, System.Web.HttpUtility.HtmlEncode(restaurantModel.Name)); + blob.Metadata.Add(MetaCity, System.Web.HttpUtility.HtmlEncode(restaurantModel.City)); + var restaurant = JsonConvert.SerializeObject(restaurantModel); + trace.Add("restaurant", restaurant); + + Task task = blob.UploadTextAsync(requestBody); + task.Wait(); + + actionResult = new OkObjectResult(restaurantModel); + log.LogInformation(correlationId, $"'{methodName}' - success", trace); + } + } + else + { + actionResult = new BadRequestObjectResult(errorModel); + log.LogInformation(correlationId, $"'{methodName}' - is not valid", trace); + } + } + catch (Exception e) + { + trace.Add(string.Format("{0} - {1}", methodName, "rejected"), e.Message); + trace.Add(string.Format("{0} - {1} - StackTrace", methodName, "rejected"), e.StackTrace); + log.LogInformation(correlationId, $"'{methodName}' - rejected", trace); + log.LogError(correlationId, $"'{methodName}' - rejected", trace); + ErrorModel errorModel = new ErrorModel() + { + CorrelationId = correlationId, + Details = e.StackTrace, + Message = e.Message, + }; + + actionResult = new BadRequestObjectResult(errorModel); + } + finally + { + log.LogTrace(eventId, $"'{methodName}' - finished"); + log.LogInformation(correlationId, $"'{methodName}' - finished", trace); + } + } + + return actionResult; + } + + /// + /// Validates the specified model. + /// + /// The model. + /// The correlation identifier. + /// The log. + /// The error model. + /// True if data is valid; otherwise False. + internal static bool Validate(RestaurantModel model, Guid correlationId, ILogger log, out ErrorModel errorModel) + { + bool isValid = true; + errorModel = null; + var trace = new Dictionary(); + var methodName = MethodBase.GetCurrentMethod().Name; + trace.Add(Constants.ButlerCorrelationTraceName, correlationId.ToString()); + + StringBuilder message = new StringBuilder(); + if (string.IsNullOrEmpty(model.Name)) + { + message.Append("No restaurant name!"); + isValid = false; + } + + if (string.IsNullOrEmpty(model.City)) + { + message.Append("No restaurant city!"); + isValid = false; + } + + if (string.IsNullOrEmpty(model.PhoneNumber)) + { + message.Append("No restaurant phone!"); + isValid = false; + } + + if (!isValid) + { + errorModel = new ErrorModel() + { + CorrelationId = correlationId, + Message = message.ToString(), + }; + trace.Add("Message", errorModel.Message); + log.LogInformation(correlationId, $"'{methodName}' - rejected", trace); + } + else + { + log.LogInformation(correlationId, $"'{methodName}' - success", trace); + } + + log.LogInformation(correlationId, $"'{methodName}' - finished", trace); + + return isValid; + } + + /// + /// Creates the name of the file. + /// + /// The model. + /// FileName without extension. + internal static string CreateFileName(RestaurantModel model) + { + string fileName = $"{model.Name}-{model.City}"; + fileName = HttpUtility.UrlEncode(fileName); + return fileName; + } + } +} diff --git a/PlanB.Butler.Services/PlanB.Butler.Services/MealService.cs b/PlanB.Butler.Services/PlanB.Butler.Services/MealService.cs deleted file mode 100644 index e5bd164..0000000 --- a/PlanB.Butler.Services/PlanB.Butler.Services/MealService.cs +++ /dev/null @@ -1,503 +0,0 @@ -// Copyright (c) PlanB. GmbH. All Rights Reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Net.Mime; -using System.Reflection; -using System.Threading.Tasks; - -using AzureFunctions.Extensions.Swashbuckle.Attribute; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.Extensions.Logging; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Blob; -using Newtonsoft.Json; -using PlanB.Butler.Services.Extensions; -using PlanB.Butler.Services.Models; - -namespace PlanB.Butler.Services -{ - /// - /// MealService. - /// - public static class MealService - { - /// - /// The meta date. - /// - private const string MetaDate = "date"; - - /// - /// The meta restaurant. - /// - private const string MetaRestaurant = "restaurant"; - - /// - /// Create meal. - /// - /// The req. - /// The cloud BLOB container. - /// The log. - /// The context. - /// - /// IActionResult. - /// - [FunctionName("CreateMeal")] - [Consumes(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)] - public static async Task CreateMeal( - [HttpTrigger(AuthorizationLevel.Function, "post", Route = "meals")] - [RequestBodyType(typeof(MealModel), "Meal request")]HttpRequest req, - [Blob("meals", FileAccess.ReadWrite, Connection = "StorageSend")] CloudBlobContainer cloudBlobContainer, - ILogger log, - ExecutionContext context) - { - Guid correlationId = Util.ReadCorrelationId(req.Headers); - var methodName = MethodBase.GetCurrentMethod().Name; - var trace = new Dictionary(); - EventId eventId = new EventId(correlationId.GetHashCode(), Constants.ButlerCorrelationTraceName); - - IActionResult actionResult = null; - try - { - string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); - trace.Add("requestBody", requestBody); - - MealModel mealModel = JsonConvert.DeserializeObject(requestBody); - if (mealModel.CorrelationId == null || mealModel.CorrelationId.Equals(Guid.Empty)) - { - mealModel.CorrelationId = correlationId; - } - - bool isValid = true; - if (string.IsNullOrEmpty(mealModel.Name)) - { - ErrorModel errorModel = new ErrorModel() - { - CorrelationId = correlationId, - Message = "No meal name!", - }; - isValid = false; - actionResult = new BadRequestObjectResult(errorModel); - } - - if (string.IsNullOrEmpty(mealModel.Restaurant)) - { - ErrorModel errorModel = new ErrorModel() - { - CorrelationId = correlationId, - Message = "No meal restaurant!", - }; - isValid = false; - actionResult = new BadRequestObjectResult(errorModel); - } - - if (isValid) - { - var date = mealModel.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); - - var filename = $"{date}-{mealModel.Restaurant}.json"; - trace.Add($"filename", filename); - - req.HttpContext.Response.Headers.Add(Constants.ButlerCorrelationTraceHeader, correlationId.ToString()); - - CloudBlockBlob blob = cloudBlobContainer.GetBlockBlobReference($"{filename}"); - if (blob != null) - { - blob.Properties.ContentType = "application/json"; - var metaDate = mealModel.Date.ToString("yyyyMMdd", CultureInfo.InvariantCulture); - blob.Metadata.Add(MetaDate, metaDate); - blob.Metadata.Add(MetaRestaurant, mealModel.Restaurant); - blob.Metadata.Add(Constants.ButlerCorrelationTraceName, correlationId.ToString().Replace("-", string.Empty)); - var meal = JsonConvert.SerializeObject(mealModel); - trace.Add("meal", meal); - - Task task = blob.UploadTextAsync(requestBody); - task.Wait(); - } - - actionResult = new OkResult(); - log.LogInformation(correlationId, $"'{methodName}' - success", trace); - } - - log.LogInformation(correlationId, $"'{methodName}' - is not valid", trace); - } - catch (Exception e) - { - trace.Add(string.Format("{0} - {1}", methodName, "rejected"), e.Message); - trace.Add(string.Format("{0} - {1} - StackTrace", methodName, "rejected"), e.StackTrace); - log.LogInformation(correlationId, $"'{methodName}' - rejected", trace); - log.LogError(correlationId, $"'{methodName}' - rejected", trace); - ErrorModel errorModel = new ErrorModel() - { - CorrelationId = correlationId, - Details = e.StackTrace, - Message = e.Message, - }; - - actionResult = new BadRequestObjectResult(errorModel); - } - finally - { - log.LogTrace(eventId, $"'{methodName}' - finished"); - log.LogInformation(correlationId, $"'{methodName}' - finished", trace); - } - - return actionResult; - } - - /// - /// Updates the meal by identifier. - /// - /// The req. - /// The identifier. - /// The BLOB. - /// The cloud BLOB container. - /// The log. - /// The context. - /// IActionResult. - [FunctionName("UpdateMealById")] - public static async Task UpdateMealById( - [HttpTrigger(AuthorizationLevel.Function, "put", Route = "meals/{id}")] HttpRequest req, - string id, - [Blob("meals/{id}.json", FileAccess.ReadWrite, Connection = "StorageSend")] string existingContent, - [Blob("meals", FileAccess.ReadWrite, Connection = "StorageSend")] CloudBlobContainer cloudBlobContainer, - ILogger log, - ExecutionContext context) - { - Guid correlationId = Util.ReadCorrelationId(req.Headers); - var methodName = MethodBase.GetCurrentMethod().Name; - var trace = new Dictionary(); - EventId eventId = new EventId(correlationId.GetHashCode(), Constants.ButlerCorrelationTraceName); - IActionResult actionResult = null; - - MealModel mealModel = null; - - try - { - trace.Add(Constants.ButlerCorrelationTraceName, correlationId.ToString()); - trace.Add("id", id); - mealModel = JsonConvert.DeserializeObject(existingContent); - - var date = mealModel.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); - - var filename = $"{date}-{mealModel.Restaurant}.json"; - trace.Add($"filename", filename); - string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); - trace.Add("requestBody", requestBody); - mealModel = JsonConvert.DeserializeObject(requestBody); - - req.HttpContext.Response.Headers.Add(Constants.ButlerCorrelationTraceHeader, correlationId.ToString()); - - CloudBlockBlob blob = cloudBlobContainer.GetBlockBlobReference($"{filename}"); - if (blob != null) - { - blob.Properties.ContentType = "application/json"; - var metaDate = mealModel.Date.ToString("yyyyMMdd", CultureInfo.InvariantCulture); - blob.Metadata.Add(MetaDate, metaDate); - blob.Metadata.Add(MetaRestaurant, mealModel.Restaurant); - blob.Metadata.Add(Constants.ButlerCorrelationTraceName, correlationId.ToString().Replace("-", string.Empty)); - var meal = JsonConvert.SerializeObject(mealModel); - trace.Add("meal", meal); - - Task task = blob.UploadTextAsync(meal); - task.Wait(); - } - - log.LogInformation(correlationId, $"'{methodName}' - success", trace); - actionResult = new OkObjectResult(mealModel); - } - catch (Exception e) - { - trace.Add(string.Format("{0} - {1}", methodName, "rejected"), e.Message); - trace.Add(string.Format("{0} - {1} - StackTrace", methodName, "rejected"), e.StackTrace); - log.LogInformation(correlationId, $"'{methodName}' - rejected", trace); - log.LogError(correlationId, $"'{methodName}' - rejected", trace); - - ErrorModel errorModel = new ErrorModel() - { - CorrelationId = correlationId, - Details = e.StackTrace, - Message = e.Message, - }; - actionResult = new BadRequestObjectResult(mealModel); - } - finally - { - log.LogTrace(eventId, $"'{methodName}' - finished"); - log.LogInformation(correlationId, $"'{methodName}' - finished", trace); - } - - return actionResult; - } - - /// - /// Reads the meals. - /// - /// The req. - /// The cloud BLOB container. - /// The log. - /// The context. - /// - /// All meals. - /// - [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)] - [FunctionName("GetMeals")] - public static async Task GetMeals( - [HttpTrigger(AuthorizationLevel.Function, "get", Route = "meals")] HttpRequest req, - [Blob("meals", FileAccess.ReadWrite, Connection = "StorageSend")] CloudBlobContainer cloudBlobContainer, - ILogger log, - ExecutionContext context) - { - Guid correlationId = Util.ReadCorrelationId(req.Headers); - var methodName = MethodBase.GetCurrentMethod().Name; - var trace = new Dictionary(); - EventId eventId = new EventId(correlationId.GetHashCode(), Constants.ButlerCorrelationTraceName); - IActionResult actionResult = null; - - List meals = new List(); - - try - { - trace.Add(Constants.ButlerCorrelationTraceName, correlationId.ToString()); - string startDateQuery = req.Query["startDate"]; - string endDateQuery = req.Query["endDate"]; - string restaurantQuery = req.Query["restaurant"]; - string prefix = string.Empty; - - bool checkForDate = false; - DateTime start = DateTime.MinValue; - DateTime end = DateTime.MinValue; - - if (!(string.IsNullOrEmpty(startDateQuery) && string.IsNullOrEmpty(endDateQuery))) - { - checkForDate = true; - DateTime.TryParse(startDateQuery, out start); - DateTime.TryParse(endDateQuery, out end); - } - - if (checkForDate) - { - prefix = CreateBlobPrefix(startDateQuery, endDateQuery); - } - - BlobContinuationToken blobContinuationToken = null; - var options = new BlobRequestOptions(); - var operationContext = new OperationContext(); - - List cloudBlockBlobs = new List(); - do - { - var blobs = await cloudBlobContainer.ListBlobsSegmentedAsync(prefix, true, BlobListingDetails.All, null, blobContinuationToken, options, operationContext).ConfigureAwait(false); - blobContinuationToken = blobs.ContinuationToken; - cloudBlockBlobs.AddRange(blobs.Results); - } - while (blobContinuationToken != null); - - foreach (var item in cloudBlockBlobs) - { - CloudBlockBlob blob = (CloudBlockBlob)item; - if (checkForDate) - { - await blob.FetchAttributesAsync(); - if (blob.Metadata.ContainsKey(MetaDate)) - { - var mealMetaDate = blob.Metadata[MetaDate]; - DateTime mealDate = DateTime.MinValue; - if (DateTime.TryParse(mealMetaDate, out mealDate)) - { - var isDateInRange = IsDateInRange(start, end, mealDate); - if (isDateInRange) - { - var blobContent = blob.DownloadTextAsync(); - var blobMeal = JsonConvert.DeserializeObject(await blobContent); - meals.Add(blobMeal); - } - } - } - } - else - { - var content = blob.DownloadTextAsync(); - var meal = JsonConvert.DeserializeObject(await content); - meals.Add(meal); - } - } - - log.LogInformation(correlationId, $"'{methodName}' - success", trace); - actionResult = new OkObjectResult(meals); - } - catch (Exception e) - { - trace.Add(string.Format("{0} - {1}", methodName, "rejected"), e.Message); - trace.Add(string.Format("{0} - {1} - StackTrace", methodName, "rejected"), e.StackTrace); - log.LogInformation(correlationId, $"'{methodName}' - rejected", trace); - log.LogError(correlationId, $"'{methodName}' - rejected", trace); - - ErrorModel errorModel = new ErrorModel() - { - CorrelationId = correlationId, - Details = e.StackTrace, - Message = e.Message, - }; - actionResult = new BadRequestObjectResult(errorModel); - } - finally - { - log.LogTrace(eventId, $"'{methodName}' - finished"); - log.LogInformation(correlationId, $"'{methodName}' - finished", trace); - } - - return actionResult; - } - - /// - /// Gets the meal by id. - /// - /// The req. - /// The identifier. - /// The BLOB. - /// The log. - /// The context. - /// - /// Meal by id. - /// - [ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)] - [ProducesResponseType(typeof(MealModel), StatusCodes.Status200OK)] - [FunctionName("GetMealById")] - public static IActionResult GetMealById( - [HttpTrigger(AuthorizationLevel.Function, "get", Route = "meals/{id}")] HttpRequest req, - string id, - [Blob("meals/{id}.json", FileAccess.ReadWrite, Connection = "StorageSend")] string blob, - ILogger log, - ExecutionContext context) - { - Guid correlationId = Util.ReadCorrelationId(req.Headers); - var methodName = MethodBase.GetCurrentMethod().Name; - var trace = new Dictionary(); - EventId eventId = new EventId(correlationId.GetHashCode(), Constants.ButlerCorrelationTraceName); - IActionResult actionResult = null; - - MealModel mealModel = null; - - try - { - trace.Add(Constants.ButlerCorrelationTraceName, correlationId.ToString()); - trace.Add("id", id); - mealModel = JsonConvert.DeserializeObject(blob); - - log.LogInformation(correlationId, $"'{methodName}' - success", trace); - actionResult = new OkObjectResult(mealModel); - } - catch (Exception e) - { - trace.Add(string.Format("{0} - {1}", methodName, "rejected"), e.Message); - trace.Add(string.Format("{0} - {1} - StackTrace", methodName, "rejected"), e.StackTrace); - log.LogInformation(correlationId, $"'{methodName}' - rejected", trace); - log.LogError(correlationId, $"'{methodName}' - rejected", trace); - - ErrorModel errorModel = new ErrorModel() - { - CorrelationId = correlationId, - Details = e.StackTrace, - Message = e.Message, - }; - actionResult = new BadRequestObjectResult(mealModel); - } - finally - { - log.LogTrace(eventId, $"'{methodName}' - finished"); - log.LogInformation(correlationId, $"'{methodName}' - finished", trace); - } - - return actionResult; - } - - /// - /// Determines whether the date in range compared to start and end. - /// - /// The start. - /// The end. - /// To check. - /// - /// true if date is in range; otherwise, false. - /// - internal static bool IsDateInRange(DateTime start, DateTime end, DateTime toCheck) - { - if (toCheck < start) - { - return false; - } - - if (end < toCheck) - { - return false; - } - - if (start.Equals(toCheck)) - { - return true; - } - - if (end.Equals(toCheck)) - { - return true; - } - - if (start.Equals(end) && start.Equals(toCheck)) - { - return true; - } - - long difference = toCheck.Ticks - start.Ticks; - long sum = start.Ticks + difference; - - if (sum < end.Ticks) - { - return true; - } - - return false; - } - - /// - /// Creates the BLOB prefix. - /// - /// The start date. - /// The end date. - /// Prefix. - internal static string CreateBlobPrefix(string startDate, string endDate) - { - string prefix = string.Empty; - if (string.IsNullOrEmpty(startDate) || string.IsNullOrEmpty(endDate)) - { - return prefix; - } - - if (startDate.Length == endDate.Length) - { - for (int i = 0; i < startDate.Length; i++) - { - if (startDate[i] == endDate[i]) - { - prefix += startDate[i]; - } - else - { - break; - } - } - } - - return prefix; - } - } -} diff --git a/PlanB.Butler.Services/PlanB.Butler.Services/Models/MealModel.cs b/PlanB.Butler.Services/PlanB.Butler.Services/Models/MealModel.cs index 176ad51..bf4118a 100644 --- a/PlanB.Butler.Services/PlanB.Butler.Services/Models/MealModel.cs +++ b/PlanB.Butler.Services/PlanB.Butler.Services/Models/MealModel.cs @@ -14,21 +14,13 @@ namespace PlanB.Butler.Services.Models public class MealModel { /// - /// Gets the identifier. + /// Gets or sets the identifier. /// /// /// The identifier. /// [JsonProperty("id")] - public string Id - { - get - { - var date = this.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); - var id = $"{date}-{this.Restaurant}"; - return id; - } - } + public string Id { get; set; } /// /// Gets or sets the correlation identifier. diff --git a/PlanB.Butler.Services/PlanB.Butler.Services/Models/OrderModel.cs b/PlanB.Butler.Services/PlanB.Butler.Services/Models/OrderModel.cs index 0a6081d..ead99e6 100644 --- a/PlanB.Butler.Services/PlanB.Butler.Services/Models/OrderModel.cs +++ b/PlanB.Butler.Services/PlanB.Butler.Services/Models/OrderModel.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using System; -using System.Collections.Generic; namespace PlanB.Butler.Services.Models { @@ -12,33 +11,7 @@ namespace PlanB.Butler.Services.Models public class OrderModel { /// - /// Gets or sets the company status Enum. - /// - public enum OrderRelationship - { - /// - /// External. - /// - External = 1, - - /// - /// Internal. - /// - Internal = 2, - - /// - /// Client. - /// - Client = 3, - - /// - /// Intership. - /// - Intership = 4, - } - - /// - /// Gets or sets the name. + /// Gets or sets the Relationship. /// /// /// The name. @@ -108,25 +81,5 @@ public enum OrderRelationship /// The benefit. /// public double Benefit { get; set; } - - //public OrderModel(string companyStatus, DateTime date, string name, string companyName, string restaurant, string meal, double price, int quantity, double benefit) - //{ - // Dictionary lookUpCompanyStatus = new Dictionary(); - // lookUpCompanyStatus.Add("Extern", OrderRelationship.External); - // lookUpCompanyStatus.Add("Intern", OrderRelationship.Internal); - // lookUpCompanyStatus.Add("Kunde", OrderRelationship.Client); - // lookUpCompanyStatus.Add("Praktikant", OrderRelationship.Intership); - - // OrderRelationship selected = lookUpCompanyStatus[companyStatus]; - // this.Relationship = selected; - // this.Date = date; - // this.Name = name; - // this.CompanyName = companyName; - // this.Restaurant = restaurant; - // this.Meal = meal; - // this.Price = price; - // this.Quantity = quantity; - // this.Benefit = benefit; - //} } } diff --git a/PlanB.Butler.Services/PlanB.Butler.Services/Models/OrderRelationship.cs b/PlanB.Butler.Services/PlanB.Butler.Services/Models/OrderRelationship.cs new file mode 100644 index 0000000..b7c267d --- /dev/null +++ b/PlanB.Butler.Services/PlanB.Butler.Services/Models/OrderRelationship.cs @@ -0,0 +1,31 @@ +// Copyright (c) PlanB. GmbH. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +namespace PlanB.Butler.Services.Models +{ + /// + /// OrderRelationship. + /// + public enum OrderRelationship + { + /// + /// External. + /// + External = 1, + + /// + /// Internal. + /// + Internal = 2, + + /// + /// Client. + /// + Client = 3, + + /// + /// Intership. + /// + Intership = 4, + } +} diff --git a/PlanB.Butler.Services/PlanB.Butler.Services/Models/RestaurantModel.cs b/PlanB.Butler.Services/PlanB.Butler.Services/Models/RestaurantModel.cs index b2aa4cd..39acb14 100644 --- a/PlanB.Butler.Services/PlanB.Butler.Services/Models/RestaurantModel.cs +++ b/PlanB.Butler.Services/PlanB.Butler.Services/Models/RestaurantModel.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Text; using Newtonsoft.Json; @@ -23,8 +24,7 @@ public class RestaurantModel [JsonProperty("id")] public string Id { - get; - set; + get; set; } /// diff --git a/PlanB.Butler.Services/PlanB.Butler.Services/PlanB.Butler.Services.csproj b/PlanB.Butler.Services/PlanB.Butler.Services/PlanB.Butler.Services.csproj index c2f0a3c..270390b 100644 --- a/PlanB.Butler.Services/PlanB.Butler.Services/PlanB.Butler.Services.csproj +++ b/PlanB.Butler.Services/PlanB.Butler.Services/PlanB.Butler.Services.csproj @@ -1,16 +1,16 @@  - netcoreapp3.0 + netcoreapp3.1 v2 PlanB.Butler.Services - bin\Debug\netcoreapp3.0\ - bin\Debug\netcoreapp3.0\PlanB.Butler.Services.xml + + bin\Debug\netcoreapp3.1\PlanB.Butler.Services.xml - bin\Release\netcoreapp3.0\PlanB.Butler.Services.xml - bin\Release\netcoreapp3.0\ + bin\Release\netcoreapp3.1\PlanB.Butler.Services.xml + bin\Release\netcoreapp3.1\ @@ -24,9 +24,9 @@ - + - + all diff --git a/PlanB.Butler.Services/PlanB.Butler.Services/RestaurantService.cs b/PlanB.Butler.Services/PlanB.Butler.Services/RestaurantService.cs deleted file mode 100644 index bc5a923..0000000 --- a/PlanB.Butler.Services/PlanB.Butler.Services/RestaurantService.cs +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright (c) PlanB. GmbH. All Rights Reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; - -using AzureFunctions.Extensions.Swashbuckle.Attribute; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.Extensions.Logging; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Blob; -using Newtonsoft.Json; -using PlanB.Butler.Services.Extensions; -using PlanB.Butler.Services.Models; - -namespace PlanB.Butler.Services -{ - /// - /// RestaurantService. - /// - public static class RestaurantService - { - private const string MetaRestaurant = "restaurant"; - private const string MetaCity = "city"; - - /// - /// Gets the restaurants. - /// - /// The req. - /// The cloud BLOB container. - /// The log. - /// The context. - /// All Restaurants. - [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)] - [FunctionName("GetRestaurants")] - public static async Task GetRestaurants( - [HttpTrigger(AuthorizationLevel.Function, "get", Route = "restaurants")] HttpRequest req, - [Blob("restaurants", FileAccess.ReadWrite, Connection = "StorageSend")] CloudBlobContainer cloudBlobContainer, - ILogger log, - ExecutionContext context) - { - Guid correlationId = Util.ReadCorrelationId(req.Headers); - var methodName = MethodBase.GetCurrentMethod().Name; - var trace = new Dictionary(); - EventId eventId = new EventId(correlationId.GetHashCode(), Constants.ButlerCorrelationTraceName); - IActionResult actionResult = null; - - List restaurant = new List(); - - try - { - BlobContinuationToken blobContinuationToken = null; - var options = new BlobRequestOptions(); - var operationContext = new OperationContext(); - - List cloudBlockBlobs = new List(); - do - { - var blobs = await cloudBlobContainer.ListBlobsSegmentedAsync(null, true, BlobListingDetails.All, null, blobContinuationToken, options, operationContext).ConfigureAwait(false); - blobContinuationToken = blobs.ContinuationToken; - cloudBlockBlobs.AddRange(blobs.Results); - } - while (blobContinuationToken != null); - - log.LogInformation(correlationId, $"'{methodName}' - success", trace); - actionResult = new OkObjectResult(restaurant); - } - catch (Exception e) - { - trace.Add(string.Format("{0} - {1}", methodName, "rejected"), e.Message); - trace.Add(string.Format("{0} - {1} - StackTrace", methodName, "rejected"), e.StackTrace); - log.LogInformation(correlationId, $"'{methodName}' - rejected", trace); - log.LogError(correlationId, $"'{methodName}' - rejected", trace); - - ErrorModel errorModel = new ErrorModel() - { - CorrelationId = correlationId, - Details = e.StackTrace, - Message = e.Message, - }; - actionResult = new BadRequestObjectResult(errorModel); - } - finally - { - log.LogTrace(eventId, $"'{methodName}' - finished"); - log.LogInformation(correlationId, $"'{methodName}' - finished", trace); - } - - return actionResult; - } - - /// - /// Creates the restaurant. - /// - /// The req. - /// The cloud BLOB container. - /// The log. - /// The context. - /// IActionResult. - [FunctionName("CreateRestaurant")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ErrorModel), StatusCodes.Status400BadRequest)] - public static async Task CreateRestaurant( - [HttpTrigger(AuthorizationLevel.Function, "post", Route = "restaurants")] - [RequestBodyType(typeof(RestaurantModel), "Restaurant request")]HttpRequest req, - [Blob("restaurants", FileAccess.ReadWrite, Connection = "StorageSend")] CloudBlobContainer cloudBlobContainer, - ILogger log, - ExecutionContext context) - { - Guid correlationId = Util.ReadCorrelationId(req.Headers); - var methodName = MethodBase.GetCurrentMethod().Name; - var trace = new Dictionary(); - EventId eventId = new EventId(correlationId.GetHashCode(), Constants.ButlerCorrelationTraceName); - - IActionResult actionResult = null; - try - { - string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); - trace.Add("requestBody", requestBody); - - RestaurantModel restaurantModel = JsonConvert.DeserializeObject(requestBody); - - var filename = $"{restaurantModel.Name}-{restaurantModel.City}.json"; - trace.Add($"filename", filename); - - req.HttpContext.Response.Headers.Add(Constants.ButlerCorrelationTraceHeader, correlationId.ToString()); - - CloudBlockBlob blob = cloudBlobContainer.GetBlockBlobReference($"{filename}"); - if (blob != null) - { - blob.Properties.ContentType = "application/json"; - blob.Metadata.Add(Constants.ButlerCorrelationTraceName, correlationId.ToString().Replace("-", string.Empty)); - blob.Metadata.Add(MetaRestaurant, System.Web.HttpUtility.HtmlEncode(restaurantModel.Name)); - blob.Metadata.Add(MetaCity, System.Web.HttpUtility.HtmlEncode(restaurantModel.City)); - var restaurant = JsonConvert.SerializeObject(restaurantModel); - trace.Add("restaurant", restaurant); - - Task task = blob.UploadTextAsync(requestBody); - task.Wait(); - } - - log.LogInformation(correlationId, $"'{methodName}' - success", trace); - actionResult = new OkResult(); - } - catch (Exception e) - { - trace.Add(string.Format("{0} - {1}", methodName, "rejected"), e.Message); - trace.Add(string.Format("{0} - {1} - StackTrace", methodName, "rejected"), e.StackTrace); - log.LogInformation(correlationId, $"'{methodName}' - rejected", trace); - log.LogError(correlationId, $"'{methodName}' - rejected", trace); - ErrorModel errorModel = new ErrorModel() - { - CorrelationId = correlationId, - Details = e.StackTrace, - Message = e.Message, - }; - - actionResult = new BadRequestObjectResult(errorModel); - } - finally - { - log.LogTrace(eventId, $"'{methodName}' - finished"); - log.LogInformation(correlationId, $"'{methodName}' - finished", trace); - } - - return actionResult; - } - } -}