diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 021e5034f..8520197f5 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -61,6 +61,7 @@ services: - MongoConnectionString=mongodb://nosql.data - MongoDatabase=MarketingDb - EventBusConnection=rabbitmq + - ExternalCatalogBaseUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5110 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. - identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. ports: - "5110:80" @@ -73,6 +74,7 @@ services: - OrderingUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5102 - IdentityUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. - BasketUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5103 + - MarketingUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5110 - CatalogUrlHC=http://catalog.api/hc - OrderingUrlHC=http://ordering.api/hc - IdentityUrlHC=http://identity.api/hc #Local: Use ${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}, if using external IP or DNS name from browser. @@ -87,7 +89,8 @@ services: - CatalogUrl=http://catalog.api - OrderingUrl=http://ordering.api - BasketUrl=http://basket.api - - IdentityUrl=http://10.0.75.1:5105 #Local: Use 10.0.75.1 in a "Docker for Windows" environment, if using "localhost" from browser. + - IdentityUrl=http://10.0.75.1:5105 + - MarketingUrl=http://marketing.api #Local: Use 10.0.75.1 in a "Docker for Windows" environment, if using "localhost" from browser. #Remote: Use ${ESHOP_EXTERNAL_DNS_NAME_OR_IP} if using external IP or DNS name from browser. ports: - "5100:80" diff --git a/docker-compose.yml b/docker-compose.yml index 334e11537..e728e125d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -76,6 +76,7 @@ services: - ordering.api - identity.api - basket.api + - marketing.api sql.data: image: microsoft/mssql-server-linux @@ -108,7 +109,7 @@ services: - rabbitmq locations.api: - image: locations.api + image: eshop/locations.api build: context: ./src/Services/Location/Locations.API dockerfile: Dockerfile diff --git a/src/Services/Identity/Identity.API/Configuration/Config.cs b/src/Services/Identity/Identity.API/Configuration/Config.cs index a99eeb0c1..4aa12d4ce 100644 --- a/src/Services/Identity/Identity.API/Configuration/Config.cs +++ b/src/Services/Identity/Identity.API/Configuration/Config.cs @@ -50,7 +50,9 @@ namespace Identity.API.Configuration IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "orders", - "basket" + "basket", + "locations", + "marketing" } }, new Client @@ -74,7 +76,8 @@ namespace Identity.API.Configuration IdentityServerConstants.StandardScopes.OfflineAccess, "orders", "basket", - "locations" + "locations", + "marketing" }, //Allow requesting refresh tokens for long lived API access AllowOfflineAccess = true, @@ -108,7 +111,8 @@ namespace Identity.API.Configuration IdentityServerConstants.StandardScopes.OfflineAccess, "orders", "basket", - "locations" + "locations", + "marketing" }, } }; diff --git a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs index f95ba8285..cbcbb382b 100644 --- a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs +++ b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs @@ -1,29 +1,34 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Controllers { - using Infrastructure.Repositories; - using Microsoft.AspNetCore.Mvc; - using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure; - using System.Threading.Tasks; - using Microsoft.eShopOnContainers.Services.Marketing.API.Model; - using Microsoft.EntityFrameworkCore; - using Microsoft.eShopOnContainers.Services.Marketing.API.Dto; - using System.Collections.Generic; - using Microsoft.AspNetCore.Authorization; using System; using System.Linq; + using System.Collections.Generic; + using Infrastructure.Repositories; + using AspNetCore.Mvc; + using Infrastructure; + using System.Threading.Tasks; + using Model; + using EntityFrameworkCore; + using Dto; + using AspNetCore.Authorization; + using Extensions.Options; + using Microsoft.eShopOnContainers.Services.Marketing.API.ViewModel; [Route("api/v1/[controller]")] [Authorize] public class CampaignsController : Controller { private readonly MarketingContext _context; + private readonly MarketingSettings _settings; private readonly IMarketingDataRepository _marketingDataRepository; public CampaignsController(MarketingContext context, - IMarketingDataRepository marketingDataRepository) + IMarketingDataRepository marketingDataRepository, + IOptionsSnapshot settings) { _context = context; _marketingDataRepository = marketingDataRepository; + _settings = settings.Value; } [HttpGet] @@ -88,10 +93,11 @@ return NotFound(); } + campaignToUpdate.Name = campaignDto.Name; campaignToUpdate.Description = campaignDto.Description; campaignToUpdate.From = campaignDto.From; campaignToUpdate.To = campaignDto.To; - campaignToUpdate.Url = campaignDto.Url; + campaignToUpdate.PictureUri = campaignDto.PictureUri; await _context.SaveChangesAsync(); @@ -119,35 +125,41 @@ } [HttpGet("user/{userId:guid}")] - public async Task GetCampaignsByUserId(Guid userId) + public async Task GetCampaignsByUserId(Guid userId, int pageSize = 10, int pageIndex = 0) { var marketingData = await _marketingDataRepository.GetAsync(userId.ToString()); - if (marketingData is null) - { - return NotFound(); - } - var campaignDtoList = new List(); - - //Get User Location Campaign - foreach(var userLocation in marketingData.Locations) + + if (marketingData != null) { + var locationIdCandidateList = marketingData.Locations.Select(x => x.LocationId); var userCampaignList = await _context.Rules .OfType() .Include(c => c.Campaign) - .Where(c => c.LocationId == userLocation.LocationId) - .Select(c => c.Campaign) - .ToListAsync(); + .Where(c => c.Campaign.From <= DateTime.Now + && c.Campaign.To >= DateTime.Now + && locationIdCandidateList.Contains(c.LocationId)) + .Select(c => c.Campaign) + .ToListAsync(); if (userCampaignList != null && userCampaignList.Any()) { var userCampaignDtoList = MapCampaignModelListToDtoList(userCampaignList); campaignDtoList.AddRange(userCampaignDtoList); } + } - return Ok(campaignDtoList); + var totalItems = campaignDtoList.Count(); + campaignDtoList = campaignDtoList + .Skip(pageSize * pageIndex) + .Take(pageSize).ToList(); + + var model = new PaginatedItemsViewModel( + pageIndex, pageSize, totalItems, campaignDtoList); + + return Ok(model); } @@ -166,10 +178,11 @@ return new CampaignDTO { Id = campaign.Id, + Name = campaign.Name, Description = campaign.Description, From = campaign.From, To = campaign.To, - Url = campaign.Url, + PictureUri = GetUriPlaceholder(campaign.PictureUri) }; } @@ -178,11 +191,21 @@ return new Campaign { Id = campaignDto.Id, + Name = campaignDto.Name, Description = campaignDto.Description, From = campaignDto.From, To = campaignDto.To, - Url = campaignDto.Url + PictureUri = campaignDto.PictureUri }; } + + private string GetUriPlaceholder(string campaignUri) + { + var baseUri = _settings.ExternalCatalogBaseUrl; + + campaignUri = campaignUri.Replace("http://externalcatalogbaseurltobereplaced", baseUri); + + return campaignUri; + } } } \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Controllers/PicController.cs b/src/Services/Marketing/Marketing.API/Controllers/PicController.cs new file mode 100644 index 000000000..9c2b73c36 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Controllers/PicController.cs @@ -0,0 +1,28 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Controllers +{ + using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Mvc; + using System.IO; + + + public class PicController : Controller + { + private readonly IHostingEnvironment _env; + public PicController(IHostingEnvironment env) + { + _env = env; + } + + [HttpGet] + [Route("api/v1/campaigns/{campaignId:int}/pic")] + public IActionResult GetImage(int campaignId) + { + var webRoot = _env.WebRootPath; + var path = Path.Combine(webRoot, campaignId + ".png"); + + var buffer = System.IO.File.ReadAllBytes(path); + + return File(buffer, "image/png"); + } + } +} diff --git a/src/Services/Marketing/Marketing.API/Dto/CampaignDTO.cs b/src/Services/Marketing/Marketing.API/Dto/CampaignDTO.cs index a43dcdbda..829011ce5 100644 --- a/src/Services/Marketing/Marketing.API/Dto/CampaignDTO.cs +++ b/src/Services/Marketing/Marketing.API/Dto/CampaignDTO.cs @@ -6,12 +6,14 @@ { public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } public DateTime From { get; set; } public DateTime To { get; set; } - public string Url { get; set; } + public string PictureUri { get; set; } } } \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs index 65131663b..f530fae37 100644 --- a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs @@ -31,8 +31,8 @@ .ForSqlServerUseSequenceHiLo("campaign_hilo") .IsRequired(); - builder.Property(m => m.Description) - .HasColumnName("Description") + builder.Property(m => m.Name) + .HasColumnName("Name") .IsRequired(); builder.Property(m => m.From) @@ -47,6 +47,10 @@ .HasColumnName("Description") .IsRequired(); + builder.Property(m => m.PictureUri) + .HasColumnName("PictureUri") + .IsRequired(); + builder.HasMany(m => m.Rules) .WithOne(r => r.Campaign) .HasForeignKey(r => r.CampaignId) diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs index de4322e48..fc8c02b4c 100644 --- a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs @@ -9,7 +9,7 @@ using System.Linq; using System.Threading.Tasks; - public class MarketingContextSeed + public static class MarketingContextSeed { public static async Task SeedAsync(IApplicationBuilder applicationBuilder, ILoggerFactory loggerFactory, int? retry = 0) { @@ -33,31 +33,33 @@ { new Campaign { - Description = "Campaign1", + Name = ".NET Bot Black Hoodie 50% OFF", + Description = "Campaign Description 1", From = DateTime.Now, To = DateTime.Now.AddDays(7), - Url = "http://CampaignUrl.test/12f09ed3cef54187123f500ad", + PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/campaigns/1/pic", Rules = new List { new UserLocationRule { - Description = "UserLocationRule1", + Description = "Campaign is only for United States users.", LocationId = 1 } } }, new Campaign { - Description = "Campaign2", - From = DateTime.Now.AddDays(7), + Name = "Roslyn Red T-Shirt 3x2", + Description = "Campaign Description 2", + From = DateTime.Now.AddDays(-7), To = DateTime.Now.AddDays(14), - Url = "http://CampaignUrl.test/02a59eda65f241871239000ff", + PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/campaigns/2/pic", Rules = new List { new UserLocationRule { - Description = "UserLocationRule2", - LocationId = 6 + Description = "Campaign is only for Seattle users.", + LocationId = 3 } } } diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170609104915_Initial.Designer.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170615163431_Init.Designer.cs similarity index 92% rename from src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170609104915_Initial.Designer.cs rename to src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170615163431_Init.Designer.cs index a9d63ee9b..b4696a8f3 100644 --- a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170609104915_Initial.Designer.cs +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170615163431_Init.Designer.cs @@ -8,8 +8,8 @@ using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure; namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.MarketingMigrations { [DbContext(typeof(MarketingContext))] - [Migration("20170609104915_Initial")] - partial class Initial + [Migration("20170615163431_Init")] + partial class Init { protected override void BuildTargetModel(ModelBuilder modelBuilder) { @@ -33,11 +33,17 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Mark b.Property("From") .HasColumnName("From"); + b.Property("Name") + .IsRequired() + .HasColumnName("Name"); + + b.Property("PictureUri") + .IsRequired() + .HasColumnName("PictureUri"); + b.Property("To") .HasColumnName("To"); - b.Property("Url"); - b.HasKey("Id"); b.ToTable("Campaign"); diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170609104915_Initial.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170615163431_Init.cs similarity index 93% rename from src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170609104915_Initial.cs rename to src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170615163431_Init.cs index d33fbb3d0..e4e33f060 100644 --- a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170609104915_Initial.cs +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170615163431_Init.cs @@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore.Migrations; namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.MarketingMigrations { - public partial class Initial : Migration + public partial class Init : Migration { protected override void Up(MigrationBuilder migrationBuilder) { @@ -23,8 +23,9 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Mark Id = table.Column(nullable: false), Description = table.Column(nullable: false), From = table.Column(nullable: false), - To = table.Column(nullable: false), - Url = table.Column(nullable: true) + Name = table.Column(nullable: false), + PictureUri = table.Column(nullable: false), + To = table.Column(nullable: false) }, constraints: table => { diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/MarketingContextModelSnapshot.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/MarketingContextModelSnapshot.cs index 3cfc5fd66..bcac40659 100644 --- a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/MarketingContextModelSnapshot.cs +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/MarketingContextModelSnapshot.cs @@ -32,11 +32,17 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Mark b.Property("From") .HasColumnName("From"); + b.Property("Name") + .IsRequired() + .HasColumnName("Name"); + + b.Property("PictureUri") + .IsRequired() + .HasColumnName("PictureUri"); + b.Property("To") .HasColumnName("To"); - b.Property("Url"); - b.HasKey("Id"); b.ToTable("Campaign"); diff --git a/src/Services/Marketing/Marketing.API/Marketing.API.csproj b/src/Services/Marketing/Marketing.API/Marketing.API.csproj index 556602f3b..4a6e2d4ea 100644 --- a/src/Services/Marketing/Marketing.API/Marketing.API.csproj +++ b/src/Services/Marketing/Marketing.API/Marketing.API.csproj @@ -12,6 +12,10 @@ + + PreserveNewest + + @@ -53,4 +57,13 @@ + + + + Always + + + PreserveNewest + + diff --git a/src/Services/Marketing/Marketing.API/MarketingSettings.cs b/src/Services/Marketing/Marketing.API/MarketingSettings.cs index d88726dcf..f83200019 100644 --- a/src/Services/Marketing/Marketing.API/MarketingSettings.cs +++ b/src/Services/Marketing/Marketing.API/MarketingSettings.cs @@ -5,5 +5,6 @@ public string ConnectionString { get; set; } public string MongoConnectionString { get; set; } public string MongoDatabase { get; set; } + public string ExternalCatalogBaseUrl { get; set; } } } diff --git a/src/Services/Marketing/Marketing.API/Model/Campaign.cs b/src/Services/Marketing/Marketing.API/Model/Campaign.cs index c628b4a72..51a4c017c 100644 --- a/src/Services/Marketing/Marketing.API/Model/Campaign.cs +++ b/src/Services/Marketing/Marketing.API/Model/Campaign.cs @@ -7,13 +7,15 @@ { public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } public DateTime From { get; set; } public DateTime To { get; set; } - public string Url { get; set; } + public string PictureUri { get; set; } public List Rules { get; set; } diff --git a/src/Services/Marketing/Marketing.API/Pics/1.png b/src/Services/Marketing/Marketing.API/Pics/1.png new file mode 100644 index 000000000..22a6a946f Binary files /dev/null and b/src/Services/Marketing/Marketing.API/Pics/1.png differ diff --git a/src/Services/Marketing/Marketing.API/Pics/2.png b/src/Services/Marketing/Marketing.API/Pics/2.png new file mode 100644 index 000000000..e23063bc4 Binary files /dev/null and b/src/Services/Marketing/Marketing.API/Pics/2.png differ diff --git a/src/Services/Marketing/Marketing.API/Program.cs b/src/Services/Marketing/Marketing.API/Program.cs index 981e797c1..2bf3b3d9c 100644 --- a/src/Services/Marketing/Marketing.API/Program.cs +++ b/src/Services/Marketing/Marketing.API/Program.cs @@ -12,6 +12,7 @@ .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseStartup() + .UseWebRoot("Pics") .Build(); host.Run(); diff --git a/src/Services/Marketing/Marketing.API/Startup.cs b/src/Services/Marketing/Marketing.API/Startup.cs index 20eb97a12..a31055d39 100644 --- a/src/Services/Marketing/Marketing.API/Startup.cs +++ b/src/Services/Marketing/Marketing.API/Startup.cs @@ -20,6 +20,9 @@ using Infrastructure.Repositories; using Autofac; using Autofac.Extensions.DependencyInjection; + using Polly; + using System.Threading.Tasks; + using System.Data.SqlClient; public class Startup { @@ -133,10 +136,12 @@ c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); }); - ConfigureEventBus(app); + var context = (MarketingContext)app + .ApplicationServices.GetService(typeof(MarketingContext)); - MarketingContextSeed.SeedAsync(app, loggerFactory) - .Wait(); + WaitForSqlAvailabilityAsync(context, loggerFactory, app).Wait(); + + ConfigureEventBus(app); } protected virtual void ConfigureAuth(IApplicationBuilder app) @@ -166,5 +171,28 @@ eventBus.Subscribe>(); } + + private async Task WaitForSqlAvailabilityAsync(MarketingContext ctx, ILoggerFactory loggerFactory, IApplicationBuilder app, int retries = 0) + { + var logger = loggerFactory.CreateLogger(nameof(Startup)); + var policy = CreatePolicy(retries, logger, nameof(WaitForSqlAvailabilityAsync)); + await policy.ExecuteAsync(async () => + { + await MarketingContextSeed.SeedAsync(app, loggerFactory); + }); + } + + private Policy CreatePolicy(int retries, ILogger logger, string prefix) + { + return Policy.Handle(). + WaitAndRetryAsync( + retryCount: retries, + sleepDurationProvider: retry => TimeSpan.FromSeconds(5), + onRetry: (exception, timeSpan, retry, ctx) => + { + logger.LogTrace($"[{prefix}] Exception {exception.GetType().Name} with message ${exception.Message} detected on attempt {retry} of {retries}"); + } + ); + } } } diff --git a/src/Services/Marketing/Marketing.API/ViewModel/PaginatedItemsViewModel.cs b/src/Services/Marketing/Marketing.API/ViewModel/PaginatedItemsViewModel.cs new file mode 100644 index 000000000..73225b4cf --- /dev/null +++ b/src/Services/Marketing/Marketing.API/ViewModel/PaginatedItemsViewModel.cs @@ -0,0 +1,23 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.ViewModel +{ + using System.Collections.Generic; + + public class PaginatedItemsViewModel where TEntity : class + { + public int PageIndex { get; private set; } + + public int PageSize { get; private set; } + + public long Count { get; private set; } + + public IEnumerable Data { get; private set; } + + public PaginatedItemsViewModel(int pageIndex, int pageSize, long count, IEnumerable data) + { + this.PageIndex = pageIndex; + this.PageSize = pageSize; + this.Count = count; + this.Data = data; + } + } +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/appsettings.json b/src/Services/Marketing/Marketing.API/appsettings.json index aefa3526f..a05a01836 100644 --- a/src/Services/Marketing/Marketing.API/appsettings.json +++ b/src/Services/Marketing/Marketing.API/appsettings.json @@ -8,5 +8,6 @@ "ConnectionString": "127.0.0.1", "MongoConnectionString": "mongodb://nosql.data", "MongoDatabase": "MarketingDb", - "IdentityUrl": "http://localhost:5105" + "IdentityUrl": "http://localhost:5105", + "ExternalCatalogBaseUrl": "http://localhost:5110" } diff --git a/src/Web/WebMVC/AppSettings.cs b/src/Web/WebMVC/AppSettings.cs index 84d5c43b7..b15167647 100644 --- a/src/Web/WebMVC/AppSettings.cs +++ b/src/Web/WebMVC/AppSettings.cs @@ -11,6 +11,7 @@ namespace Microsoft.eShopOnContainers.WebMVC public string CatalogUrl { get; set; } public string OrderingUrl { get; set; } public string BasketUrl { get; set; } + public string MarketingUrl { get; set; } public Logging Logging { get; set; } } diff --git a/src/Web/WebMVC/Controllers/CampaignsController.cs b/src/Web/WebMVC/Controllers/CampaignsController.cs new file mode 100644 index 000000000..3030c99db --- /dev/null +++ b/src/Web/WebMVC/Controllers/CampaignsController.cs @@ -0,0 +1,64 @@ +namespace Microsoft.eShopOnContainers.WebMVC.Controllers +{ + using AspNetCore.Authorization; + using AspNetCore.Mvc; + using Services; + using ViewModels; + using System.Threading.Tasks; + using System; + using ViewModels.Pagination; + using global::WebMVC.ViewModels; + + [Authorize] + public class CampaignsController : Controller + { + private readonly ICampaignService _campaignService; + + public CampaignsController(ICampaignService campaignService) => + _campaignService = campaignService; + + public async Task Index(int page = 0, int pageSize = 10) + { + var campaignList = await _campaignService.GetCampaigns(pageSize, page); + + var vm = new CampaignViewModel() + { + CampaignItems = campaignList.Data, + PaginationInfo = new PaginationInfo() + { + ActualPage = page, + ItemsPerPage = pageSize, + TotalItems = campaignList.Count, + TotalPages = (int)Math.Ceiling(((decimal)campaignList.Count / pageSize)) + } + }; + + vm.PaginationInfo.Next = (vm.PaginationInfo.ActualPage == vm.PaginationInfo.TotalPages - 1) ? "is-disabled" : ""; + vm.PaginationInfo.Previous = (vm.PaginationInfo.ActualPage == 0) ? "is-disabled" : ""; + + return View(vm); + } + + public async Task Details(int id) + { + var campaignDto = await _campaignService.GetCampaignById(id); + + if (campaignDto is null) + { + return NotFound(); + } + + var campaign = new CampaignItem + { + Id = campaignDto.Id, + Name = campaignDto.Name, + Description = campaignDto.Description, + From = campaignDto.From, + To = campaignDto.To, + PictureUri = campaignDto.PictureUri + }; + + return View(campaign); + } + } +} \ No newline at end of file diff --git a/src/Web/WebMVC/Infrastructure/API.cs b/src/Web/WebMVC/Infrastructure/API.cs index fc170ffcc..2dcf555df 100644 --- a/src/Web/WebMVC/Infrastructure/API.cs +++ b/src/Web/WebMVC/Infrastructure/API.cs @@ -1,4 +1,6 @@ -namespace WebMVC.Infrastructure +using System; + +namespace WebMVC.Infrastructure { public static class API { @@ -79,5 +81,18 @@ return $"{baseUri}catalogTypes"; } } + + public static class Marketing + { + public static string GetAllCampaigns(string baseUri, string userId, int take, int page) + { + return $"{baseUri}user/{userId}?pageSize={take}&pageIndex={page}"; + } + + public static string GetAllCampaignById(string baseUri, int id) + { + return $"{baseUri}{id}"; + } + } } -} +} \ No newline at end of file diff --git a/src/Web/WebMVC/Services/CampaignService.cs b/src/Web/WebMVC/Services/CampaignService.cs new file mode 100644 index 000000000..e90be9590 --- /dev/null +++ b/src/Web/WebMVC/Services/CampaignService.cs @@ -0,0 +1,70 @@ +namespace Microsoft.eShopOnContainers.WebMVC.Services +{ + using global::WebMVC.Infrastructure; + using AspNetCore.Authentication; + using AspNetCore.Http; + using BuildingBlocks.Resilience.Http; + using ViewModels; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; + using Newtonsoft.Json; + using System; + using System.Threading.Tasks; + + public class CampaignService : ICampaignService + { + private readonly IOptionsSnapshot _settings; + private readonly IHttpClient _apiClient; + private readonly ILogger _logger; + private readonly string _remoteServiceBaseUrl; + private readonly IHttpContextAccessor _httpContextAccesor; + + public CampaignService(IOptionsSnapshot settings, IHttpClient httpClient, + ILogger logger, IHttpContextAccessor httpContextAccesor) + { + _settings = settings; + _apiClient = httpClient; + _logger = logger; + + _remoteServiceBaseUrl = $"{_settings.Value.MarketingUrl}/api/v1/campaigns/"; + _httpContextAccesor = httpContextAccesor ?? throw new ArgumentNullException(nameof(httpContextAccesor)); + } + + public async Task GetCampaigns(int pageSize, int pageIndex) + { + var userId = GetUserIdentity(); + var allCampaignItemsUri = API.Marketing.GetAllCampaigns(_remoteServiceBaseUrl, + userId, pageSize, pageIndex); + + var authorizationToken = await GetUserTokenAsync(); + var dataString = await _apiClient.GetStringAsync(allCampaignItemsUri, authorizationToken); + + var response = JsonConvert.DeserializeObject(dataString); + + return response; + } + + public async Task GetCampaignById(int id) + { + var campaignByIdItemUri = API.Marketing.GetAllCampaignById(_remoteServiceBaseUrl, id); + + var authorizationToken = await GetUserTokenAsync(); + var dataString = await _apiClient.GetStringAsync(campaignByIdItemUri, authorizationToken); + + var response = JsonConvert.DeserializeObject(dataString); + + return response; + } + + private string GetUserIdentity() + { + return _httpContextAccesor.HttpContext.User.FindFirst("sub").Value; + } + + private async Task GetUserTokenAsync() + { + var context = _httpContextAccesor.HttpContext; + return await context.Authentication.GetTokenAsync("access_token"); + } + } +} \ No newline at end of file diff --git a/src/Web/WebMVC/Services/ICampaignService.cs b/src/Web/WebMVC/Services/ICampaignService.cs new file mode 100644 index 000000000..ab80e930a --- /dev/null +++ b/src/Web/WebMVC/Services/ICampaignService.cs @@ -0,0 +1,13 @@ +namespace Microsoft.eShopOnContainers.WebMVC.Services +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using ViewModels; + + public interface ICampaignService + { + Task GetCampaigns(int pageSize, int pageIndex); + + Task GetCampaignById(int id); + } +} \ No newline at end of file diff --git a/src/Web/WebMVC/Startup.cs b/src/Web/WebMVC/Startup.cs index f119e1d8e..8147f6720 100644 --- a/src/Web/WebMVC/Startup.cs +++ b/src/Web/WebMVC/Startup.cs @@ -70,6 +70,7 @@ namespace Microsoft.eShopOnContainers.WebMVC services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient, IdentityParser>(); if (Configuration.GetValue("UseResilientHttp") == bool.TrueString) @@ -125,7 +126,7 @@ namespace Microsoft.eShopOnContainers.WebMVC SaveTokens = true, GetClaimsFromUserInfoEndpoint = true, RequireHttpsMetadata = false, - Scope = { "openid", "profile", "orders", "basket" } + Scope = { "openid", "profile", "orders", "basket", "marketing" } }; //Wait untill identity service is ready on compose. diff --git a/src/Web/WebMVC/ViewModels/Campaign.cs b/src/Web/WebMVC/ViewModels/Campaign.cs new file mode 100644 index 000000000..11841fc3f --- /dev/null +++ b/src/Web/WebMVC/ViewModels/Campaign.cs @@ -0,0 +1,12 @@ +namespace Microsoft.eShopOnContainers.WebMVC.ViewModels +{ + using System.Collections.Generic; + + public class Campaign + { + public int PageIndex { get; set; } + public int PageSize { get; set; } + public int Count { get; set; } + public List Data { get; set; } + } +} \ No newline at end of file diff --git a/src/Web/WebMVC/ViewModels/CampaignItem.cs b/src/Web/WebMVC/ViewModels/CampaignItem.cs new file mode 100644 index 000000000..a0bfbaf66 --- /dev/null +++ b/src/Web/WebMVC/ViewModels/CampaignItem.cs @@ -0,0 +1,19 @@ +namespace Microsoft.eShopOnContainers.WebMVC.ViewModels +{ + using System; + + public class CampaignItem + { + public int Id { get; set; } + + public string Name { get; set; } + + public string Description { get; set; } + + public DateTime From { get; set; } + + public DateTime To { get; set; } + + public string PictureUri { get; set; } + } +} \ No newline at end of file diff --git a/src/Web/WebMVC/ViewModels/CampaignViewModel/CampaignViewModel.cs b/src/Web/WebMVC/ViewModels/CampaignViewModel/CampaignViewModel.cs new file mode 100644 index 000000000..67fdf9cbb --- /dev/null +++ b/src/Web/WebMVC/ViewModels/CampaignViewModel/CampaignViewModel.cs @@ -0,0 +1,12 @@ +namespace WebMVC.ViewModels +{ + using System.Collections.Generic; + using Microsoft.eShopOnContainers.WebMVC.ViewModels; + using Microsoft.eShopOnContainers.WebMVC.ViewModels.Pagination; + + public class CampaignViewModel + { + public IEnumerable CampaignItems { get; set; } + public PaginationInfo PaginationInfo { get; set; } + } +} \ No newline at end of file diff --git a/src/Web/WebMVC/Views/Campaigns/Details.cshtml b/src/Web/WebMVC/Views/Campaigns/Details.cshtml new file mode 100644 index 000000000..2cb2648b6 --- /dev/null +++ b/src/Web/WebMVC/Views/Campaigns/Details.cshtml @@ -0,0 +1,28 @@ +@{ + ViewData["Title"] = "Campaign details"; + @model CampaignItem +} +
+
+ +
+
+ +@Html.Partial("_Header", new List
() { + new Header() { Controller = "Catalog", Text = "Back to catalog" }, + new Header() { Controller = "Campaigns", Text = "Back to Campaigns" } }) + +
+
+ Card image cap +
+

@Model.Name

+

@Model.Description

+

+ + From @Model.From.ToString("MMMM dd, yyyy") until @Model.To.ToString("MMMM dd, yyyy") + +

+
+
+
\ No newline at end of file diff --git a/src/Web/WebMVC/Views/Campaigns/Index.cshtml b/src/Web/WebMVC/Views/Campaigns/Index.cshtml new file mode 100644 index 000000000..d4c8abe48 --- /dev/null +++ b/src/Web/WebMVC/Views/Campaigns/Index.cshtml @@ -0,0 +1,37 @@ +@{ + ViewData["Title"] = "Campaigns"; +@model WebMVC.ViewModels.CampaignViewModel +} + +
+
+ +
+
+ +@Html.Partial("_Header", new List
() { + new Header() { Controller = "Catalog", Text = "Back to catalog" } }) + +
+ @if (Model.CampaignItems != null && Model.CampaignItems.Any()) + { + @Html.Partial("_pagination", Model.PaginationInfo) + +
+ @foreach (var catalogItem in Model.CampaignItems) + { +
+ @Html.Partial("_campaign", catalogItem) +
+ } +
+ + @Html.Partial("_pagination", Model.PaginationInfo) + } + else + { +
+ THERE ARE NO CAMPAIGNS +
+ } +
diff --git a/src/Web/WebMVC/Views/Campaigns/_campaign.cshtml b/src/Web/WebMVC/Views/Campaigns/_campaign.cshtml new file mode 100644 index 000000000..de3b52657 --- /dev/null +++ b/src/Web/WebMVC/Views/Campaigns/_campaign.cshtml @@ -0,0 +1,17 @@ +@model CampaignItem + + + +
+
+

@Model.Name

+ @Model.Name + +
+ +
diff --git a/src/Web/WebMVC/Views/Campaigns/_pagination.cshtml b/src/Web/WebMVC/Views/Campaigns/_pagination.cshtml new file mode 100644 index 000000000..038222e93 --- /dev/null +++ b/src/Web/WebMVC/Views/Campaigns/_pagination.cshtml @@ -0,0 +1,32 @@ +@model Microsoft.eShopOnContainers.WebMVC.ViewModels.Pagination.PaginationInfo + +
+
+
+ +
+
+
+ diff --git a/src/Web/WebMVC/Views/Shared/_Layout.cshtml b/src/Web/WebMVC/Views/Shared/_Layout.cshtml index 990018b09..ab1a4e542 100644 --- a/src/Web/WebMVC/Views/Shared/_Layout.cshtml +++ b/src/Web/WebMVC/Views/Shared/_Layout.cshtml @@ -13,6 +13,7 @@ + diff --git a/src/Web/WebMVC/Views/Shared/_LoginPartial.cshtml b/src/Web/WebMVC/Views/Shared/_LoginPartial.cshtml index 1b9cde79d..f3ce6a729 100644 --- a/src/Web/WebMVC/Views/Shared/_LoginPartial.cshtml +++ b/src/Web/WebMVC/Views/Shared/_LoginPartial.cshtml @@ -26,6 +26,14 @@ + + +
Campaigns
+ +
+ diff --git a/src/Web/WebMVC/WebMVC.csproj b/src/Web/WebMVC/WebMVC.csproj index a9507394c..48925ab34 100644 --- a/src/Web/WebMVC/WebMVC.csproj +++ b/src/Web/WebMVC/WebMVC.csproj @@ -9,6 +9,18 @@ ..\..\..\docker-compose.dcproj + + + + + + + + + PreserveNewest + + +