diff --git a/cli-windows/add-firewall-rules-for-sts-auth-thru-docker.ps1 b/cli-windows/add-firewall-rules-for-sts-auth-thru-docker.ps1 index be63d8a25..e3545c584 100644 --- a/cli-windows/add-firewall-rules-for-sts-auth-thru-docker.ps1 +++ b/cli-windows/add-firewall-rules-for-sts-auth-thru-docker.ps1 @@ -21,6 +21,6 @@ try { Write-Host "Rule found" } catch [Exception] { - New-NetFirewallRule -DisplayName eShopOnContainers-Inbound -Confirm -Description "eShopOnContainers Inbound Rule for port range 5100-5105" -LocalAddress Any -LocalPort 5100-5105 -Protocol tcp -RemoteAddress Any -RemotePort Any -Direction Inbound - New-NetFirewallRule -DisplayName eShopOnContainers-Outbound -Confirm -Description "eShopOnContainers Outbound Rule for port range 5100-5105" -LocalAddress Any -LocalPort 5100-5105 -Protocol tcp -RemoteAddress Any -RemotePort Any -Direction Outbound + New-NetFirewallRule -DisplayName eShopOnContainers-Inbound -Confirm -Description "eShopOnContainers Inbound Rule for port range 5100-5110" -LocalAddress Any -LocalPort 5100-5110 -Protocol tcp -RemoteAddress Any -RemotePort Any -Direction Inbound + New-NetFirewallRule -DisplayName eShopOnContainers-Outbound -Confirm -Description "eShopOnContainers Outbound Rule for port range 5100-5110" -LocalAddress Any -LocalPort 5100-5110 -Protocol tcp -RemoteAddress Any -RemotePort Any -Direction Outbound } \ No newline at end of file diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 04d1c4d9c..1348a6e9b 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -49,6 +49,15 @@ services: ports: - "5102:80" + marketing.api: + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://0.0.0.0:80 + - ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.MarketingDb;User Id=sa;Password=Pass@word + - 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" + webspa: environment: - ASPNETCORE_ENVIRONMENT=Development @@ -94,4 +103,4 @@ services: - mvc=http://webmvc/hc - spa=http://webspa/hc ports: - - "5107:80" + - "5107:80" \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index c5d8839ea..b9c46b4c2 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -54,6 +54,15 @@ services: ports: - "5102:80" + marketing.api: + environment: + - ASPNETCORE_ENVIRONMENT=Production + - ASPNETCORE_URLS=http://0.0.0.0:80 + - ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.MarketingDb;User Id=sa;Password=Pass@word + - 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" + webspa: environment: - ASPNETCORE_ENVIRONMENT=Production diff --git a/docker-compose.vs.debug.yml b/docker-compose.vs.debug.yml index 2e7145637..85195a493 100644 --- a/docker-compose.vs.debug.yml +++ b/docker-compose.vs.debug.yml @@ -61,6 +61,22 @@ services: labels: - "com.microsoft.visualstudio.targetoperatingsystem=linux" + marketing.api: + image: eshop/marketing.api:dev + build: + args: + source: ${DOCKER_BUILD_SOURCE} + environment: + - DOTNET_USE_POLLING_FILE_WATCHER=1 + volumes: + - ./src/Services/Marketing/Marketing.API:/app + - ~/.nuget/packages:/root/.nuget/packages:ro + - ~/clrdbg:/clrdbg:ro + entrypoint: tail -f /dev/null + labels: + - "com.microsoft.visualstudio.targetoperatingsystem=linux" + + webspa: image: eshop/webspa:dev build: @@ -105,3 +121,4 @@ services: entrypoint: tail -f /dev/null labels: - "com.microsoft.visualstudio.targetoperatingsystem=linux" + diff --git a/docker-compose.vs.release.yml b/docker-compose.vs.release.yml index d1ca5b2c6..f01b21eff 100644 --- a/docker-compose.vs.release.yml +++ b/docker-compose.vs.release.yml @@ -41,6 +41,16 @@ services: labels: - "com.microsoft.visualstudio.targetoperatingsystem=linux" + marketing.api: + build: + args: + source: ${DOCKER_BUILD_SOURCE} + volumes: + - ~/clrdbg:/clrdbg:ro + entrypoint: tail -f /dev/null + labels: + - "com.microsoft.visualstudio.targetoperatingsystem=linux" + webspa: build: args: diff --git a/docker-compose.yml b/docker-compose.yml index 21f3972f2..9612b38fb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,6 +36,15 @@ services: depends_on: - sql.data + marketing.api: + image: eshop/marketing.api + build: + context: ./src/Services/Marketing/Marketing.API + dockerfile: Dockerfile + depends_on: + - sql.data + - identity.api + webspa: image: eshop/webspa build: @@ -74,4 +83,4 @@ services: build: context: ./src/Web/WebStatus dockerfile: Dockerfile - \ No newline at end of file + diff --git a/eShopOnContainers-ServicesAndWebApps.sln b/eShopOnContainers-ServicesAndWebApps.sln index edd18d523..6e890dd9e 100644 --- a/eShopOnContainers-ServicesAndWebApps.sln +++ b/eShopOnContainers-ServicesAndWebApps.sln @@ -80,6 +80,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DataProtection", "DataProte EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataProtection", "src\BuildingBlocks\DataProtection\DataProtection\DataProtection.csproj", "{23A33F9B-7672-426D-ACF9-FF8436ADC81A}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Marketing", "Marketing", "{A5260DE0-1FDD-467E-9CC1-A028AB081CEE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Marketing.API", "src\Services\Marketing\Marketing.API\Marketing.API.csproj", "{DF395F85-B010-465D-857A-7EBCC512C0C2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Ad-Hoc|Any CPU = Ad-Hoc|Any CPU @@ -1054,6 +1058,54 @@ Global {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Release|x64.Build.0 = Release|Any CPU {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Release|x86.ActiveCfg = Release|Any CPU {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Release|x86.Build.0 = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|x64.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|ARM.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|ARM.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|iPhone.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|x64.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|x64.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|x86.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|x86.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|ARM.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|ARM.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|iPhone.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|x64.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|x64.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|x86.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|x86.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|Any CPU.Build.0 = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|ARM.ActiveCfg = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|ARM.Build.0 = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|iPhone.ActiveCfg = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|iPhone.Build.0 = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|x64.ActiveCfg = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|x64.Build.0 = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|x86.ActiveCfg = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1090,6 +1142,8 @@ Global {22A0F9C1-2D4A-4107-95B7-8459E6688BC5} = {A81ECBC2-6B00-4DCD-8388-469174033379} {4BD76717-3102-4969-8C2C-BAAA3F0263B6} = {A81ECBC2-6B00-4DCD-8388-469174033379} {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D} = {807BB76E-B2BB-47A2-A57B-3D1B20FF5E7F} + {A5260DE0-1FDD-467E-9CC1-A028AB081CEE} = {91CF7717-08AB-4E65-B10E-0B426F01E2E8} + {DF395F85-B010-465D-857A-7EBCC512C0C2} = {A5260DE0-1FDD-467E-9CC1-A028AB081CEE} {88B22DBB-AA8F-4290-A454-2C109352C345} = {DB0EFB20-B024-4E5E-A75C-52143C131D25} {23A33F9B-7672-426D-ACF9-FF8436ADC81A} = {88B22DBB-AA8F-4290-A454-2C109352C345} EndGlobalSection diff --git a/src/Services/Identity/Identity.API/Configuration/Config.cs b/src/Services/Identity/Identity.API/Configuration/Config.cs index d10e91df9..cc52447d5 100644 --- a/src/Services/Identity/Identity.API/Configuration/Config.cs +++ b/src/Services/Identity/Identity.API/Configuration/Config.cs @@ -12,7 +12,8 @@ namespace Identity.API.Configuration return new List { new ApiResource("orders", "Orders Service"), - new ApiResource("basket", "Basket Service") + new ApiResource("basket", "Basket Service"), + new ApiResource("marketing", "Marketing Service") }; } diff --git a/src/Services/Marketing/Marketing.API/.dockerignore b/src/Services/Marketing/Marketing.API/.dockerignore new file mode 100644 index 000000000..d8f8175f6 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/.dockerignore @@ -0,0 +1,3 @@ +* +!obj/Docker/publish/* +!obj/Docker/empty/ diff --git a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs new file mode 100644 index 000000000..f3dde55ef --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs @@ -0,0 +1,151 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Controllers +{ + 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; + + [Route("api/v1/[controller]")] + [Authorize] + public class CampaignsController : Controller + { + private readonly MarketingContext _context; + + public CampaignsController(MarketingContext context) + { + _context = context; + } + + [HttpGet] + public async Task GetAllCampaigns() + { + var campaignList = await _context.Campaigns + .ToListAsync(); + + if (campaignList is null) + { + return Ok(); + } + + var campaignDtoList = MapCampaignModelListToDtoList(campaignList); + + return Ok(campaignDtoList); + } + + [HttpGet("{id:int}")] + public async Task GetCampaignById(int id) + { + var campaign = await _context.Campaigns + .SingleOrDefaultAsync(c => c.Id == id); + + if (campaign is null) + { + return NotFound(); + } + + var campaignDto = MapCampaignModelToDto(campaign); + + return Ok(campaignDto); + } + + [HttpPost] + public async Task CreateCampaign([FromBody] CampaignDTO campaignDto) + { + if (campaignDto is null) + { + return BadRequest(); + } + + var campaign = MapCampaignDtoToModel(campaignDto); + + await _context.Campaigns.AddAsync(campaign); + await _context.SaveChangesAsync(); + + return CreatedAtAction(nameof(GetCampaignById), new { id = campaign.Id }, null); + } + + [HttpPut("{id:int}")] + public async Task UpdateCampaign(int id, [FromBody] CampaignDTO campaignDto) + { + if (id < 1 || campaignDto is null) + { + return BadRequest(); + } + + var campaignToUpdate = await _context.Campaigns.FindAsync(id); + if (campaignToUpdate is null) + { + return NotFound(); + } + + campaignToUpdate.Description = campaignDto.Description; + campaignToUpdate.From = campaignDto.From; + campaignToUpdate.To = campaignDto.To; + campaignToUpdate.Url = campaignDto.Url; + + await _context.SaveChangesAsync(); + + return CreatedAtAction(nameof(GetCampaignById), new { id = campaignToUpdate.Id }, null); + } + + [HttpDelete("{id:int}")] + public async Task Delete(int id) + { + if (id < 1) + { + return BadRequest(); + } + + var campaignToDelete = await _context.Campaigns.FindAsync(id); + if (campaignToDelete is null) + { + return NotFound(); + } + + _context.Campaigns.Remove(campaignToDelete); + await _context.SaveChangesAsync(); + + return NoContent(); + } + + + + private List MapCampaignModelListToDtoList(List campaignList) + { + var campaignDtoList = new List(); + + campaignList.ForEach(campaign => campaignDtoList + .Add(MapCampaignModelToDto(campaign))); + + return campaignDtoList; + } + + private CampaignDTO MapCampaignModelToDto(Campaign campaign) + { + return new CampaignDTO + { + Id = campaign.Id, + Description = campaign.Description, + From = campaign.From, + To = campaign.To, + Url = campaign.Url, + }; + } + + private Campaign MapCampaignDtoToModel(CampaignDTO campaignDto) + { + return new Campaign + { + Id = campaignDto.Id, + Description = campaignDto.Description, + From = campaignDto.From, + To = campaignDto.To, + Url = campaignDto.Url + }; + } + } +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Controllers/HomeController.cs b/src/Services/Marketing/Marketing.API/Controllers/HomeController.cs new file mode 100644 index 000000000..d37386b9a --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Controllers/HomeController.cs @@ -0,0 +1,13 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Controllers +{ + using Microsoft.AspNetCore.Mvc; + + // GET: // + public class HomeController : Controller + { + public IActionResult Index() + { + return new RedirectResult("~/swagger"); + } + } +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Controllers/LocationsController.cs b/src/Services/Marketing/Marketing.API/Controllers/LocationsController.cs new file mode 100644 index 000000000..843e58030 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Controllers/LocationsController.cs @@ -0,0 +1,146 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Controllers +{ + using Microsoft.AspNetCore.Authorization; + using Microsoft.AspNetCore.Mvc; + using Microsoft.eShopOnContainers.Services.Marketing.API.Dto; + using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure; + using Microsoft.eShopOnContainers.Services.Marketing.API.Model; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + + [Authorize] + public class LocationsController : Controller + { + private readonly MarketingContext _context; + + public LocationsController(MarketingContext context) + { + _context = context; + } + + [HttpGet] + [Route("api/v1/campaigns/{campaignId:int}/locations/{userLocationRuleId:int}")] + public IActionResult GetLocationByCampaignAndLocationRuleId(int campaignId, + int userLocationRuleId) + { + if (campaignId < 1 || userLocationRuleId < 1) + { + return BadRequest(); + } + + var location = _context.Rules + .OfType() + .SingleOrDefault(c => c.CampaignId == campaignId && c.Id == userLocationRuleId); + + if (location is null) + { + return NotFound(); + } + + var locationDto = MapUserLocationRuleModelToDto(location); + + return Ok(locationDto); + } + + [HttpGet] + [Route("api/v1/campaigns/{campaignId:int}/locations")] + public IActionResult GetAllLocationsByCampaignId(int campaignId) + { + if (campaignId < 1) + { + return BadRequest(); + } + + var locationList = _context.Rules + .OfType() + .Where(c => c.CampaignId == campaignId) + .ToList(); + + if(locationList is null) + { + return Ok(); + } + + var locationDtoList = MapUserLocationRuleModelListToDtoList(locationList); + + return Ok(locationDtoList); + } + + [HttpPost] + [Route("api/v1/campaigns/{campaignId:int}/locations")] + public async Task CreateLocation(int campaignId, + [FromBody] UserLocationRuleDTO locationRuleDto) + { + if (campaignId < 1 || locationRuleDto is null) + { + return BadRequest(); + } + + var locationRule = MapUserLocationRuleDtoToModel(locationRuleDto); + locationRule.CampaignId = campaignId; + + await _context.Rules.AddAsync(locationRule); + await _context.SaveChangesAsync(); + + return CreatedAtAction(nameof(GetLocationByCampaignAndLocationRuleId), + new { campaignId = campaignId, locationRuleId = locationRule.Id }, null); + } + + [HttpDelete] + [Route("api/v1/campaigns/{campaignId:int}/locations/{userLocationRuleId:int}")] + public async Task DeleteLocationById(int campaignId, int userLocationRuleId) + { + if (campaignId < 1 || userLocationRuleId < 1) + { + return BadRequest(); + } + + var locationToDelete = _context.Rules + .OfType() + .SingleOrDefault(c => c.CampaignId == campaignId && c.Id == userLocationRuleId); + + if (locationToDelete is null) + { + return NotFound(); + } + + _context.Rules.Remove(locationToDelete); + await _context.SaveChangesAsync(); + + return NoContent(); + } + + + + private List MapUserLocationRuleModelListToDtoList(List userLocationRuleList) + { + var userLocationRuleDtoList = new List(); + + userLocationRuleList.ForEach(userLocationRule => userLocationRuleDtoList + .Add(MapUserLocationRuleModelToDto(userLocationRule))); + + return userLocationRuleDtoList; + } + + private UserLocationRuleDTO MapUserLocationRuleModelToDto(UserLocationRule userLocationRule) + { + return new UserLocationRuleDTO + { + Id = userLocationRule.Id, + Description = userLocationRule.Description, + LocationId = userLocationRule.LocationId + }; + } + + private UserLocationRule MapUserLocationRuleDtoToModel(UserLocationRuleDTO userLocationRuleDto) + { + return new UserLocationRule + { + Id = userLocationRuleDto.Id, + Description = userLocationRuleDto.Description, + LocationId = userLocationRuleDto.LocationId + }; + } + } +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Dockerfile b/src/Services/Marketing/Marketing.API/Dockerfile new file mode 100644 index 000000000..54a79a5e4 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Dockerfile @@ -0,0 +1,6 @@ +FROM microsoft/aspnetcore:1.1.2 +ARG source +WORKDIR /app +EXPOSE 80 +COPY ${source:-obj/Docker/publish} . +ENTRYPOINT ["dotnet", "Marketing.API.dll"] diff --git a/src/Services/Marketing/Marketing.API/Dto/CampaignDTO.cs b/src/Services/Marketing/Marketing.API/Dto/CampaignDTO.cs new file mode 100644 index 000000000..a43dcdbda --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Dto/CampaignDTO.cs @@ -0,0 +1,17 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Dto +{ + using System; + + public class CampaignDTO + { + public int Id { get; set; } + + public string Description { get; set; } + + public DateTime From { get; set; } + + public DateTime To { get; set; } + + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Dto/UserLocationRuleDTO.cs b/src/Services/Marketing/Marketing.API/Dto/UserLocationRuleDTO.cs new file mode 100644 index 000000000..a76eb90a0 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Dto/UserLocationRuleDTO.cs @@ -0,0 +1,11 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Dto +{ + public class UserLocationRuleDTO + { + public int Id { get; set; } + + public int LocationId { get; set; } + + public string Description { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/ActionResult/InternalServerErrorObjectResult.cs b/src/Services/Marketing/Marketing.API/Infrastructure/ActionResult/InternalServerErrorObjectResult.cs new file mode 100644 index 000000000..5975d92af --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Infrastructure/ActionResult/InternalServerErrorObjectResult.cs @@ -0,0 +1,14 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.ActionResults +{ + using AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + + public class InternalServerErrorObjectResult : ObjectResult + { + public InternalServerErrorObjectResult(object error) + : base(error) + { + StatusCode = StatusCodes.Status500InternalServerError; + } + } +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/Exceptions/MarketingDomainException.cs b/src/Services/Marketing/Marketing.API/Infrastructure/Exceptions/MarketingDomainException.cs new file mode 100644 index 000000000..68ccd9e52 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Infrastructure/Exceptions/MarketingDomainException.cs @@ -0,0 +1,21 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Exceptions +{ + using System; + + /// + /// Exception type for app exceptions + /// + public class MarketingDomainException : Exception + { + public MarketingDomainException() + { } + + public MarketingDomainException(string message) + : base(message) + { } + + public MarketingDomainException(string message, Exception innerException) + : base(message, innerException) + { } + } +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs b/src/Services/Marketing/Marketing.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs new file mode 100644 index 000000000..14e79775d --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs @@ -0,0 +1,67 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Filters +{ + using AspNetCore.Mvc; + using global::Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Exceptions; + using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Mvc.Filters; + using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.ActionResults; + using Microsoft.Extensions.Logging; + using System.Net; + + public class HttpGlobalExceptionFilter : IExceptionFilter + { + private readonly IHostingEnvironment env; + private readonly ILogger logger; + + public HttpGlobalExceptionFilter(IHostingEnvironment env, ILogger logger) + { + this.env = env; + this.logger = logger; + } + + public void OnException(ExceptionContext context) + { + logger.LogError(new EventId(context.Exception.HResult), + context.Exception, + context.Exception.Message); + + if (context.Exception.GetType() == typeof(MarketingDomainException)) + { + var json = new JsonErrorResponse + { + Messages = new[] { context.Exception.Message } + }; + + // Result asigned to a result object but in destiny the response is empty. This is a known bug of .net core 1.1 + //It will be fixed in .net core 1.1.2. See https://github.com/aspnet/Mvc/issues/5594 for more information + context.Result = new BadRequestObjectResult(json); + context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; + } + else + { + var json = new JsonErrorResponse + { + Messages = new[] { "An error occur.Try it again." } + }; + + if (env.IsDevelopment()) + { + json.DeveloperMessage = context.Exception; + } + + // Result asigned to a result object but in destiny the response is empty. This is a known bug of .net core 1.1 + // It will be fixed in .net core 1.1.2. See https://github.com/aspnet/Mvc/issues/5594 for more information + context.Result = new InternalServerErrorObjectResult(json); + context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + } + context.ExceptionHandled = true; + } + + private class JsonErrorResponse + { + public string[] Messages { get; set; } + + public object DeveloperMessage { 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 new file mode 100644 index 000000000..15be03431 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs @@ -0,0 +1,83 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure +{ + using Microsoft.EntityFrameworkCore; + using Microsoft.EntityFrameworkCore.Metadata.Builders; + using Microsoft.eShopOnContainers.Services.Marketing.API.Model; + + public class MarketingContext : DbContext + { + public MarketingContext(DbContextOptions options) : base(options) + { + } + + public DbSet Campaigns { get; set; } + + public DbSet Rules { get; set; } + + protected override void OnModelCreating(ModelBuilder builder) + { + builder.Entity(ConfigureCampaigns); + builder.Entity(ConfigureRules); + builder.Entity(ConfigureUserLocationRules); + } + + void ConfigureCampaigns(EntityTypeBuilder builder) + { + builder.ToTable("Campaign"); + + builder.HasKey(m => m.Id); + + builder.Property(m => m.Id) + .ForSqlServerUseSequenceHiLo("campaign_hilo") + .IsRequired(); + + builder.Property(m => m.Description) + .HasColumnName("Description") + .IsRequired(); + + builder.Property(m => m.From) + .HasColumnName("From") + .IsRequired(); + + builder.Property(m => m.To) + .HasColumnName("To") + .IsRequired(); + + builder.Property(m => m.Description) + .HasColumnName("Description") + .IsRequired(); + + builder.HasMany(m => m.Rules) + .WithOne(r => r.Campaign) + .HasForeignKey(r => r.CampaignId) + .IsRequired(); + } + + void ConfigureRules(EntityTypeBuilder builder) + { + builder.ToTable("Rule"); + + builder.HasKey(r => r.Id); + + builder.Property(r => r.Id) + .ForSqlServerUseSequenceHiLo("rule_hilo") + .IsRequired(); + + builder.HasDiscriminator("RuleTypeId") + .HasValue((int)RuleTypeEnum.UserProfileRule) + .HasValue((int)RuleTypeEnum.PurchaseHistoryRule) + .HasValue((int)RuleTypeEnum.UserLocationRule); + + builder.Property(r => r.Description) + .HasColumnName("Description") + .IsRequired(); + } + + void ConfigureUserLocationRules(EntityTypeBuilder builder) + { + builder.Property(r => r.LocationId) + .HasColumnName("LocationId") + .IsRequired(); + } + } +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs new file mode 100644 index 000000000..5f15a725a --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs @@ -0,0 +1,67 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure +{ + using Microsoft.AspNetCore.Builder; + using Microsoft.EntityFrameworkCore; + using Microsoft.eShopOnContainers.Services.Marketing.API.Model; + using Microsoft.Extensions.Logging; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + + public class MarketingContextSeed + { + public static async Task SeedAsync(IApplicationBuilder applicationBuilder, ILoggerFactory loggerFactory, int? retry = 0) + { + var context = (MarketingContext)applicationBuilder + .ApplicationServices.GetService(typeof(MarketingContext)); + + context.Database.Migrate(); + + if (!context.Campaigns.Any()) + { + context.Campaigns.AddRange( + GetPreconfiguredMarketings()); + + await context.SaveChangesAsync(); + } + } + + static List GetPreconfiguredMarketings() + { + return new List + { + new Campaign + { + Description = "Campaign1", + From = DateTime.Now, + To = DateTime.Now.AddDays(7), + Url = "http://CampaignUrl.test/12f09ed3cef54187123f500ad", + Rules = new List + { + new UserLocationRule + { + Description = "UserLocationRule1", + LocationId = 1 + } + } + }, + new Campaign + { + Description = "Campaign2", + From = DateTime.Now.AddDays(7), + To = DateTime.Now.AddDays(14), + Url = "http://CampaignUrl.test/02a59eda65f241871239000ff", + Rules = new List + { + new UserLocationRule + { + Description = "UserLocationRule2", + LocationId = 3 + } + } + } + }; + } + } +} diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170602122539_Initial.Designer.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170602122539_Initial.Designer.cs new file mode 100644 index 000000000..5a5a6b66d --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170602122539_Initial.Designer.cs @@ -0,0 +1,111 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure; + +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.MarketingMigrations +{ + [DbContext(typeof(MarketingContext))] + [Migration("20170602122539_Initial")] + partial class Initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.2") + .HasAnnotation("SqlServer:Sequence:.campaign_hilo", "'campaign_hilo', '', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:Sequence:.rule_hilo", "'rule_hilo', '', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Campaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "campaign_hilo") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("Description") + .IsRequired() + .HasColumnName("Description"); + + b.Property("From") + .HasColumnName("From"); + + b.Property("To") + .HasColumnName("To"); + + b.Property("Url"); + + b.HasKey("Id"); + + b.ToTable("Campaign"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "rule_hilo") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("CampaignId"); + + b.Property("Description") + .IsRequired() + .HasColumnName("Description"); + + b.Property("RuleTypeId"); + + b.HasKey("Id"); + + b.HasIndex("CampaignId"); + + b.ToTable("Rule"); + + b.HasDiscriminator("RuleTypeId"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.PurchaseHistoryRule", b => + { + b.HasBaseType("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule"); + + + b.ToTable("PurchaseHistoryRule"); + + b.HasDiscriminator().HasValue(2); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.UserLocationRule", b => + { + b.HasBaseType("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule"); + + b.Property("LocationId") + .HasColumnName("LocationId"); + + b.ToTable("UserLocationRule"); + + b.HasDiscriminator().HasValue(3); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.UserProfileRule", b => + { + b.HasBaseType("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule"); + + + b.ToTable("UserProfileRule"); + + b.HasDiscriminator().HasValue(1); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule", b => + { + b.HasOne("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Campaign") + .WithMany("Rules") + .HasForeignKey("CampaignId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170602122539_Initial.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170602122539_Initial.cs new file mode 100644 index 000000000..d33fbb3d0 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170602122539_Initial.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.MarketingMigrations +{ + public partial class Initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateSequence( + name: "campaign_hilo", + incrementBy: 10); + + migrationBuilder.CreateSequence( + name: "rule_hilo", + incrementBy: 10); + + migrationBuilder.CreateTable( + name: "Campaign", + columns: table => new + { + 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) + }, + constraints: table => + { + table.PrimaryKey("PK_Campaign", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Rule", + columns: table => new + { + Id = table.Column(nullable: false), + CampaignId = table.Column(nullable: false), + Description = table.Column(nullable: false), + RuleTypeId = table.Column(nullable: false), + LocationId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Rule", x => x.Id); + table.ForeignKey( + name: "FK_Rule_Campaign_CampaignId", + column: x => x.CampaignId, + principalTable: "Campaign", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Rule_CampaignId", + table: "Rule", + column: "CampaignId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Rule"); + + migrationBuilder.DropTable( + name: "Campaign"); + + migrationBuilder.DropSequence( + name: "campaign_hilo"); + + migrationBuilder.DropSequence( + name: "rule_hilo"); + } + } +} diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/MarketingContextModelSnapshot.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/MarketingContextModelSnapshot.cs new file mode 100644 index 000000000..865daa028 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/MarketingContextModelSnapshot.cs @@ -0,0 +1,110 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure; + +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.MarketingMigrations +{ + [DbContext(typeof(MarketingContext))] + partial class MarketingContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.2") + .HasAnnotation("SqlServer:Sequence:.campaign_hilo", "'campaign_hilo', '', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:Sequence:.rule_hilo", "'rule_hilo', '', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Campaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "campaign_hilo") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("Description") + .IsRequired() + .HasColumnName("Description"); + + b.Property("From") + .HasColumnName("From"); + + b.Property("To") + .HasColumnName("To"); + + b.Property("Url"); + + b.HasKey("Id"); + + b.ToTable("Campaign"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "rule_hilo") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("CampaignId"); + + b.Property("Description") + .IsRequired() + .HasColumnName("Description"); + + b.Property("RuleTypeId"); + + b.HasKey("Id"); + + b.HasIndex("CampaignId"); + + b.ToTable("Rule"); + + b.HasDiscriminator("RuleTypeId"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.PurchaseHistoryRule", b => + { + b.HasBaseType("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule"); + + + b.ToTable("PurchaseHistoryRule"); + + b.HasDiscriminator().HasValue(2); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.UserLocationRule", b => + { + b.HasBaseType("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule"); + + b.Property("LocationId") + .HasColumnName("LocationId"); + + b.ToTable("UserLocationRule"); + + b.HasDiscriminator().HasValue(3); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.UserProfileRule", b => + { + b.HasBaseType("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule"); + + + b.ToTable("UserProfileRule"); + + b.HasDiscriminator().HasValue(1); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule", b => + { + b.HasOne("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Campaign") + .WithMany("Rules") + .HasForeignKey("CampaignId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/Services/Marketing/Marketing.API/Marketing.API.csproj b/src/Services/Marketing/Marketing.API/Marketing.API.csproj new file mode 100644 index 000000000..34d594e51 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Marketing.API.csproj @@ -0,0 +1,50 @@ + + + + netcoreapp1.1 + 1.1.2 + Exe + ..\..\..\..\docker-compose.dcproj + Microsoft.eShopOnContainers.Services.Marketing.API + portable-net45+win8 + aspnet-Marketing.API-20161122013619 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Services/Marketing/Marketing.API/MarketingSettings.cs b/src/Services/Marketing/Marketing.API/MarketingSettings.cs new file mode 100644 index 000000000..c6e1dfc40 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/MarketingSettings.cs @@ -0,0 +1,7 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API +{ + public class MarketingSettings + { + public string ConnectionString { get; set; } + } +} diff --git a/src/Services/Marketing/Marketing.API/Model/Campaign.cs b/src/Services/Marketing/Marketing.API/Model/Campaign.cs new file mode 100644 index 000000000..c628b4a72 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Model/Campaign.cs @@ -0,0 +1,26 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Model +{ + using System; + using System.Collections.Generic; + + public class Campaign + { + public int Id { get; set; } + + public string Description { get; set; } + + public DateTime From { get; set; } + + public DateTime To { get; set; } + + public string Url { get; set; } + + public List Rules { get; set; } + + + public Campaign() + { + Rules = new List(); + } + } +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Model/Rule.cs b/src/Services/Marketing/Marketing.API/Model/Rule.cs new file mode 100644 index 000000000..e02128fa6 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Model/Rule.cs @@ -0,0 +1,33 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Model +{ + public abstract class Rule + { + public int Id { get; set; } + + public int CampaignId { get; set; } + + public Campaign Campaign { get; set; } + + public string Description { get; set; } + + public abstract int RuleTypeId { get;} + } + + + public class UserProfileRule : Rule + { + public override int RuleTypeId => (int)RuleTypeEnum.UserProfileRule; + } + + public class PurchaseHistoryRule : Rule + { + public override int RuleTypeId => (int)RuleTypeEnum.PurchaseHistoryRule; + } + + public class UserLocationRule : Rule + { + public override int RuleTypeId => (int)RuleTypeEnum.UserLocationRule; + + public int LocationId { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Model/RuleTypeEnum.cs b/src/Services/Marketing/Marketing.API/Model/RuleTypeEnum.cs new file mode 100644 index 000000000..c58dbf75c --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Model/RuleTypeEnum.cs @@ -0,0 +1,20 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Model +{ + using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Exceptions; + using System; + + public enum RuleTypeEnum { UserProfileRule = 1, PurchaseHistoryRule = 2, UserLocationRule = 3 } + + public static class RuleType + { + public static RuleTypeEnum From(int id) + { + if (!Enum.IsDefined(typeof(RuleTypeEnum), id)) + { + throw new MarketingDomainException($"Invalid value for RuleType, RuleTypeId: {id}"); + } + + return (RuleTypeEnum)id; + } + } +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Program.cs b/src/Services/Marketing/Marketing.API/Program.cs new file mode 100644 index 000000000..981e797c1 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Program.cs @@ -0,0 +1,20 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API +{ + using System.IO; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Hosting; + + public class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/src/Services/Marketing/Marketing.API/Properties/launchSettings.json b/src/Services/Marketing/Marketing.API/Properties/launchSettings.json new file mode 100644 index 000000000..ec47d57ce --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5110", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Marketing.API": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:52059" + } + } +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Startup.cs b/src/Services/Marketing/Marketing.API/Startup.cs new file mode 100644 index 000000000..9609f903f --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Startup.cs @@ -0,0 +1,119 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API +{ + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Hosting; + using Microsoft.EntityFrameworkCore.Infrastructure; + using Microsoft.EntityFrameworkCore; + using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Logging; + using System.Reflection; + using System; + using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Filters; + + public class Startup + { + public Startup(IHostingEnvironment env) + { + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) + .AddEnvironmentVariables(); + + if (env.IsDevelopment()) + { + builder.AddUserSecrets(typeof(Startup).GetTypeInfo().Assembly); + } + + builder.AddEnvironmentVariables(); + + Configuration = builder.Build(); + } + + public IConfigurationRoot Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + // Add framework services. + services.AddMvc(options => + { + options.Filters.Add(typeof(HttpGlobalExceptionFilter)); + }).AddControllersAsServices(); //Injecting Controllers themselves thru DIFor further info see: http://docs.autofac.org/en/latest/integration/aspnetcore.html#controllers-as-services + + services.AddDbContext(options => + { + options.UseSqlServer(Configuration["ConnectionString"], + sqlServerOptionsAction: sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); + //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency + sqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); + }); + + // Changing default behavior when client evaluation occurs to throw. + // Default in EF Core would be to log a warning when client evaluation is performed. + options.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning)); + //Check Client vs. Server evaluation: https://docs.microsoft.com/en-us/ef/core/querying/client-eval + }); + + // Add framework services. + services.AddSwaggerGen(options => + { + options.DescribeAllEnumsAsStrings(); + options.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info + { + Title = "Marketing HTTP API", + Version = "v1", + Description = "The Marketing Service HTTP API", + TermsOfService = "Terms Of Service" + }); + }); + + services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials()); + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + loggerFactory.AddConsole(Configuration.GetSection("Logging")); + loggerFactory.AddDebug(); + + app.UseCors("CorsPolicy"); + + ConfigureAuth(app); + + app.UseMvcWithDefaultRoute(); + + app.UseSwagger() + .UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + MarketingContextSeed.SeedAsync(app, loggerFactory) + .Wait(); + } + + + protected virtual void ConfigureAuth(IApplicationBuilder app) + { + var identityUrl = Configuration.GetValue("IdentityUrl"); + app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions + { + Authority = identityUrl.ToString(), + ApiName = "marketing", + RequireHttpsMetadata = false + }); + } + } +} diff --git a/src/Services/Marketing/Marketing.API/appsettings.Development.json b/src/Services/Marketing/Marketing.API/appsettings.Development.json new file mode 100644 index 000000000..5fff67bac --- /dev/null +++ b/src/Services/Marketing/Marketing.API/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning" + } + } +} diff --git a/src/Services/Marketing/Marketing.API/appsettings.json b/src/Services/Marketing/Marketing.API/appsettings.json new file mode 100644 index 000000000..36bdcad2c --- /dev/null +++ b/src/Services/Marketing/Marketing.API/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning" + } + }, + "ConnectionString": "127.0.0.1", + "IdentityUrl": "http://localhost:5105" +} diff --git a/test/Services/IntegrationTests/IntegrationTests.csproj b/test/Services/IntegrationTests/IntegrationTests.csproj index 60911dd4d..a44e57ccb 100644 --- a/test/Services/IntegrationTests/IntegrationTests.csproj +++ b/test/Services/IntegrationTests/IntegrationTests.csproj @@ -20,6 +20,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -28,6 +31,7 @@ + diff --git a/test/Services/IntegrationTests/Services/Marketing/CampaignScenarioBase.cs b/test/Services/IntegrationTests/Services/Marketing/CampaignScenarioBase.cs new file mode 100644 index 000000000..a23fd1677 --- /dev/null +++ b/test/Services/IntegrationTests/Services/Marketing/CampaignScenarioBase.cs @@ -0,0 +1,30 @@ +namespace IntegrationTests.Services.Marketing +{ + public class CampaignScenarioBase : MarketingScenarioBase + { + public static class Get + { + public static string Campaigns = CampaignsUrlBase; + + public static string CampaignBy(int id) + => $"{CampaignsUrlBase}/{id}"; + } + + public static class Post + { + public static string AddNewCampaign = CampaignsUrlBase; + } + + public static class Put + { + public static string CampaignBy(int id) + => $"{CampaignsUrlBase}/{id}"; + } + + public static class Delete + { + public static string CampaignBy(int id) + => $"{CampaignsUrlBase}/{id}"; + } + } +} \ No newline at end of file diff --git a/test/Services/IntegrationTests/Services/Marketing/CampaignScenarios.cs b/test/Services/IntegrationTests/Services/Marketing/CampaignScenarios.cs new file mode 100644 index 000000000..8e27958b9 --- /dev/null +++ b/test/Services/IntegrationTests/Services/Marketing/CampaignScenarios.cs @@ -0,0 +1,127 @@ +namespace IntegrationTests.Services.Marketing +{ + using System.Net.Http; + using System.Text; + using System.Threading.Tasks; + using Xunit; + using System; + using Newtonsoft.Json; + using System.Net; + using Microsoft.eShopOnContainers.Services.Marketing.API.Dto; + + public class CampaignScenarios + : CampaignScenarioBase + { + [Fact] + public async Task Get_get_all_campaigns_and_response_ok_status_code() + { + using (var server = CreateServer()) + { + var response = await server.CreateClient() + .GetAsync(Get.Campaigns); + + response.EnsureSuccessStatusCode(); + } + } + + [Fact] + public async Task Get_get_campaign_by_id_and_response_ok_status_code() + { + var campaignId = 1; + using (var server = CreateServer()) + { + var response = await server.CreateClient() + .GetAsync(Get.CampaignBy(campaignId)); + + response.EnsureSuccessStatusCode(); + } + } + + [Fact] + public async Task Get_get_campaign_by_id_and_response_not_found_status_code() + { + using (var server = CreateServer()) + { + var response = await server.CreateClient() + .GetAsync(Get.CampaignBy(int.MaxValue)); + + Assert.True(response.StatusCode == HttpStatusCode.NotFound); + } + } + + [Fact] + public async Task Post_add_new_campaign_and_response_ok_status_code() + { + using (var server = CreateServer()) + { + var fakeCampaignDto = GetFakeCampaignDto(); + var content = new StringContent(JsonConvert.SerializeObject(fakeCampaignDto), Encoding.UTF8, "application/json"); + var response = await server.CreateClient() + .PostAsync(Post.AddNewCampaign, content); + + response.EnsureSuccessStatusCode(); + } + } + + [Fact] + public async Task Delete_delete_campaign_and_response_not_content_status_code() + { + using (var server = CreateServer()) + { + var fakeCampaignDto = GetFakeCampaignDto(); + var content = new StringContent(JsonConvert.SerializeObject(fakeCampaignDto), Encoding.UTF8, "application/json"); + + //add campaign + var campaignResponse = await server.CreateClient() + .PostAsync(Post.AddNewCampaign, content); + + if (int.TryParse(campaignResponse.Headers.Location.Segments[4], out int id)) + { + var response = await server.CreateClient() + .DeleteAsync(Delete.CampaignBy(id)); + + Assert.True(response.StatusCode == HttpStatusCode.NoContent); + } + + campaignResponse.EnsureSuccessStatusCode(); + } + } + + [Fact] + public async Task Put_update_campaign_and_response_not_content_status_code() + { + using (var server = CreateServer()) + { + var fakeCampaignDto = GetFakeCampaignDto(); + var content = new StringContent(JsonConvert.SerializeObject(fakeCampaignDto), Encoding.UTF8, "application/json"); + + //add campaign + var campaignResponse = await server.CreateClient() + .PostAsync(Post.AddNewCampaign, content); + + if (int.TryParse(campaignResponse.Headers.Location.Segments[4], out int id)) + { + fakeCampaignDto.Description = "FakeCampaignUpdatedDescription"; + content = new StringContent(JsonConvert.SerializeObject(fakeCampaignDto), Encoding.UTF8, "application/json"); + var response = await server.CreateClient() + .PutAsync(Put.CampaignBy(id), content); + + Assert.True(response.StatusCode == HttpStatusCode.Created); + } + + campaignResponse.EnsureSuccessStatusCode(); + } + } + + private static CampaignDTO GetFakeCampaignDto() + { + return new CampaignDTO() + { + Description = "FakeCampaignDescription", + From = DateTime.Now, + To = DateTime.Now.AddDays(7), + Url = "http://CampaignUrl.test/fdaf91ad0cef5419719f50198", + }; + } + } +} diff --git a/test/Services/IntegrationTests/Services/Marketing/MarketingScenariosBase.cs b/test/Services/IntegrationTests/Services/Marketing/MarketingScenariosBase.cs new file mode 100644 index 000000000..832fb1b82 --- /dev/null +++ b/test/Services/IntegrationTests/Services/Marketing/MarketingScenariosBase.cs @@ -0,0 +1,20 @@ +namespace IntegrationTests.Services.Marketing +{ + using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.TestHost; + using System.IO; + + public class MarketingScenarioBase + { + public static string CampaignsUrlBase => "api/v1/campaigns"; + + public TestServer CreateServer() + { + var webHostBuilder = new WebHostBuilder(); + webHostBuilder.UseContentRoot(Directory.GetCurrentDirectory() + "\\Services\\Marketing"); + webHostBuilder.UseStartup(); + + return new TestServer(webHostBuilder); + } + } +} \ No newline at end of file diff --git a/test/Services/IntegrationTests/Services/Marketing/MarketingTestsStartup.cs b/test/Services/IntegrationTests/Services/Marketing/MarketingTestsStartup.cs new file mode 100644 index 000000000..b8d337ab2 --- /dev/null +++ b/test/Services/IntegrationTests/Services/Marketing/MarketingTestsStartup.cs @@ -0,0 +1,26 @@ +namespace IntegrationTests.Services.Marketing +{ + using Microsoft.eShopOnContainers.Services.Marketing.API; + using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Builder; + using IntegrationTests.Middleware; + + public class MarketingTestsStartup : Startup + { + public MarketingTestsStartup(IHostingEnvironment env) : base(env) + { + } + + protected override void ConfigureAuth(IApplicationBuilder app) + { + if (Configuration["isTest"] == bool.TrueString.ToLowerInvariant()) + { + app.UseMiddleware(); + } + else + { + base.ConfigureAuth(app); + } + } + } +} diff --git a/test/Services/IntegrationTests/Services/Marketing/UserLocationRoleScenarios.cs b/test/Services/IntegrationTests/Services/Marketing/UserLocationRoleScenarios.cs new file mode 100644 index 000000000..f7fbd6cee --- /dev/null +++ b/test/Services/IntegrationTests/Services/Marketing/UserLocationRoleScenarios.cs @@ -0,0 +1,80 @@ +namespace IntegrationTests.Services.Marketing +{ + using System.Net.Http; + using System.Text; + using System.Threading.Tasks; + using Xunit; + using System; + using Newtonsoft.Json; + using System.Net; + using Microsoft.eShopOnContainers.Services.Marketing.API.Dto; + + public class UserLocationRoleScenarios + : UserLocationRoleScenariosBase + { + [Fact] + public async Task Get_get_all_user_location_rules_by_campaignId_and_response_ok_status_code() + { + var campaignId = 1; + + using (var server = CreateServer()) + { + var response = await server.CreateClient() + .GetAsync(Get.UserLocationRulesByCampaignId(campaignId)); + + response.EnsureSuccessStatusCode(); + } + } + + [Fact] + public async Task Post_add_new_user_location_rule_and_response_ok_status_code() + { + var campaignId = 1; + + using (var server = CreateServer()) + { + var fakeCampaignDto = GetFakeUserLocationRuleDto(); + var content = new StringContent(JsonConvert.SerializeObject(fakeCampaignDto), Encoding.UTF8, "application/json"); + var response = await server.CreateClient() + .PostAsync(Post.AddNewuserLocationRule(campaignId), content); + + response.EnsureSuccessStatusCode(); + } + } + + [Fact] + public async Task Delete_delete_user_location_role_and_response_not_content_status_code() + { + var campaignId = 1; + + using (var server = CreateServer()) + { + var fakeCampaignDto = GetFakeUserLocationRuleDto(); + var content = new StringContent(JsonConvert.SerializeObject(fakeCampaignDto), Encoding.UTF8, "application/json"); + + //add user location role + var campaignResponse = await server.CreateClient() + .PostAsync(Post.AddNewuserLocationRule(campaignId), content); + + if (int.TryParse(campaignResponse.Headers.Location.Segments[6], out int userLocationRuleId)) + { + var response = await server.CreateClient() + .DeleteAsync(Delete.UserLocationRoleBy(campaignId, userLocationRuleId)); + + Assert.True(response.StatusCode == HttpStatusCode.NoContent); + } + + campaignResponse.EnsureSuccessStatusCode(); + } + } + + private static UserLocationRuleDTO GetFakeUserLocationRuleDto() + { + return new UserLocationRuleDTO + { + LocationId = 20, + Description = "FakeUserLocationRuleDescription" + }; + } + } +} diff --git a/test/Services/IntegrationTests/Services/Marketing/UserLocationRoleScenariosBase.cs b/test/Services/IntegrationTests/Services/Marketing/UserLocationRoleScenariosBase.cs new file mode 100644 index 000000000..cd6fcc9f3 --- /dev/null +++ b/test/Services/IntegrationTests/Services/Marketing/UserLocationRoleScenariosBase.cs @@ -0,0 +1,40 @@ +namespace IntegrationTests.Services.Marketing +{ + public class UserLocationRoleScenariosBase : MarketingScenarioBase + { + private const string EndpointLocationName = "locations"; + public static class Get + { + public static string UserLocationRulesByCampaignId(int campaignId) + => GetUserLocationRolesUrlBase(campaignId); + + public static string UserLocationRuleByCampaignAndUserLocationRuleId(int campaignId, + int userLocationRuleId) + => $"{GetUserLocationRolesUrlBase(campaignId)}/{userLocationRuleId}"; + } + + public static class Post + { + public static string AddNewuserLocationRule(int campaignId) + => GetUserLocationRolesUrlBase(campaignId); + } + + public static class Put + { + public static string UserLocationRoleBy(int campaignId, + int userLocationRuleId) + => $"{GetUserLocationRolesUrlBase(campaignId)}/{userLocationRuleId}"; + } + + public static class Delete + { + public static string UserLocationRoleBy(int campaignId, + int userLocationRuleId) + => $"{GetUserLocationRolesUrlBase(campaignId)}/{userLocationRuleId}"; + } + + + private static string GetUserLocationRolesUrlBase(int campaignId) + => $"{CampaignsUrlBase}/{campaignId}/{EndpointLocationName}"; + } +} \ No newline at end of file diff --git a/test/Services/IntegrationTests/Services/Marketing/appsettings.json b/test/Services/IntegrationTests/Services/Marketing/appsettings.json new file mode 100644 index 000000000..8e3e07891 --- /dev/null +++ b/test/Services/IntegrationTests/Services/Marketing/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionString": "Server=tcp:127.0.0.1,5433;Initial Catalog=Microsoft.eShopOnContainers.Services.MarketingDb;User Id=sa;Password=Pass@word", + "IdentityUrl": "http://localhost:5105", + "isTest": "true" +}