Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions Development Project/Development Project.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31410.357
# Visual Studio Version 18
VisualStudioVersion = 18.6.11819.183 stable
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Interview.Web", "Interview.Web\Interview.Web.csproj", "{EE17B748-4D84-46AE-9E83-8D04B92DD6A9}"
EndProject
Expand All @@ -11,6 +11,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sparcpoint.Core", "Sparcpoi
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sparcpoint.SqlServer.Abstractions", "Sparcpoint.SqlServer.Abstractions\Sparcpoint.SqlServer.Abstractions.csproj", "{4E72CD4A-C9FE-4A04-9F07-A770E3AD7297}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sparcpoint.Inventory", "Sparcpoint.Inventory\Sparcpoint.Inventory.csproj", "{B7F9E5D2-3A8C-4F1B-9E2D-5C7A6F8B3D14}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sparcpoint.Inventory.IntegrationTests", "Sparcpoint.Inventory.IntegrationTests\Sparcpoint.Inventory.IntegrationTests.csproj", "{C8E2F4A6-5B9D-4A2E-B3F8-7D6E1C9B4F25}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -55,6 +59,22 @@ Global
{4E72CD4A-C9FE-4A04-9F07-A770E3AD7297}.Release|Any CPU.Build.0 = Release|Any CPU
{4E72CD4A-C9FE-4A04-9F07-A770E3AD7297}.Release|x86.ActiveCfg = Release|Any CPU
{4E72CD4A-C9FE-4A04-9F07-A770E3AD7297}.Release|x86.Build.0 = Release|Any CPU
{B7F9E5D2-3A8C-4F1B-9E2D-5C7A6F8B3D14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B7F9E5D2-3A8C-4F1B-9E2D-5C7A6F8B3D14}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B7F9E5D2-3A8C-4F1B-9E2D-5C7A6F8B3D14}.Debug|x86.ActiveCfg = Debug|Any CPU
{B7F9E5D2-3A8C-4F1B-9E2D-5C7A6F8B3D14}.Debug|x86.Build.0 = Debug|Any CPU
{B7F9E5D2-3A8C-4F1B-9E2D-5C7A6F8B3D14}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B7F9E5D2-3A8C-4F1B-9E2D-5C7A6F8B3D14}.Release|Any CPU.Build.0 = Release|Any CPU
{B7F9E5D2-3A8C-4F1B-9E2D-5C7A6F8B3D14}.Release|x86.ActiveCfg = Release|Any CPU
{B7F9E5D2-3A8C-4F1B-9E2D-5C7A6F8B3D14}.Release|x86.Build.0 = Release|Any CPU
{C8E2F4A6-5B9D-4A2E-B3F8-7D6E1C9B4F25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C8E2F4A6-5B9D-4A2E-B3F8-7D6E1C9B4F25}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C8E2F4A6-5B9D-4A2E-B3F8-7D6E1C9B4F25}.Debug|x86.ActiveCfg = Debug|Any CPU
{C8E2F4A6-5B9D-4A2E-B3F8-7D6E1C9B4F25}.Debug|x86.Build.0 = Debug|Any CPU
{C8E2F4A6-5B9D-4A2E-B3F8-7D6E1C9B4F25}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C8E2F4A6-5B9D-4A2E-B3F8-7D6E1C9B4F25}.Release|Any CPU.Build.0 = Release|Any CPU
{C8E2F4A6-5B9D-4A2E-B3F8-7D6E1C9B4F25}.Release|x86.ActiveCfg = Release|Any CPU
{C8E2F4A6-5B9D-4A2E-B3F8-7D6E1C9B4F25}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using Microsoft.AspNetCore.Mvc;
using Sparcpoint.Inventory.Abstract;
using Sparcpoint.Inventory.Models;
using System;
using System.Threading.Tasks;

namespace Interview.Web.Controllers
{
[ApiController]
[Route("api/v1/categories")]
public class CategoryController : ControllerBase
{
private readonly ICategoryRepository _Categories;

public CategoryController(ICategoryRepository categories)
{
_Categories = categories;
}

[HttpGet]
public async Task<IActionResult> GetAll()
{
var categories = await _Categories.GetAllAsync();
return Ok(categories);
}

[HttpGet("{categoryId:int}")]
public async Task<IActionResult> GetById(int categoryId)
{
var category = await _Categories.GetByIdAsync(categoryId);
if (category == null) return NotFound();
return Ok(category);
}

[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateCategoryRequest request)
{
if (string.IsNullOrWhiteSpace(request?.Name))
return BadRequest("Name is required.");

try
{
var categoryId = await _Categories.CreateAsync(request);
var created = await _Categories.GetByIdAsync(categoryId);
return CreatedAtAction(nameof(GetById), new { categoryId }, created);
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using Microsoft.AspNetCore.Mvc;
using Sparcpoint.Inventory.Abstract;
using Sparcpoint.Inventory.Models;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Interview.Web.Controllers
{
// EVAL: Three endpoints map directly to inventory requirements: POST /transactions covers
// requirement #4 (bulk add/remove via signed quantities), DELETE /transactions/{id} covers
// requirement #6 (undo a transaction), and GET /count covers requirement #5 (count by product
// id and/or subset of metadata).
[ApiController]
[Route("api/v1/inventory")]
public class InventoryController : ControllerBase
{
private readonly IInventoryRepository _Inventory;

public InventoryController(IInventoryRepository inventory)
{
_Inventory = inventory;
}

[HttpPost("transactions")]
public async Task<IActionResult> CreateTransactions([FromBody] IEnumerable<InventoryAdjustment> adjustments)
{
if (adjustments == null)
return BadRequest("At least one adjustment is required.");

try
{
var transactions = await _Inventory.CreateTransactionsAsync(adjustments);
return Ok(transactions);
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
}

[HttpDelete("transactions/{transactionId:int}")]
public async Task<IActionResult> DeleteTransaction(int transactionId)
{
try
{
await _Inventory.DeleteTransactionAsync(transactionId);
return NoContent();
}
catch (KeyNotFoundException)
{
return NotFound();
}
}

[HttpGet("count")]
public async Task<IActionResult> GetCount([FromQuery] int? productId, [FromQuery] string attributes)
{
var query = new InventoryCountQuery { ProductId = productId };

if (!string.IsNullOrWhiteSpace(attributes))
{
try
{
query.AttributeFilters = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, string>>(attributes);
}
catch
{
return BadRequest("'attributes' must be a JSON object of key/value pairs.");
}
}

var count = await _Inventory.GetCountAsync(query);
return Ok(new { Count = count });
}
}
}
70 changes: 63 additions & 7 deletions Development Project/Interview.Web/Controllers/ProductController.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,75 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Sparcpoint.Inventory.Abstract;
using Sparcpoint.Inventory.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Interview.Web.Controllers
{
// EVAL: Route is explicitly versioned (/api/v1/...) so future breaking changes can ship as v2
// without disrupting existing clients (per the recommendation about extending the API without
// impacting older customers). Per requirement #1, there is intentionally no DELETE endpoint —
// products cannot be deleted from the system.
[ApiController]
[Route("api/v1/products")]
public class ProductController : Controller
public class ProductController : ControllerBase
{
// NOTE: Sample Action
private readonly IProductRepository _Products;

public ProductController(IProductRepository products)
{
_Products = products;
}

[HttpGet]
public Task<IActionResult> GetAllProducts()
public async Task<IActionResult> Search([FromQuery] string name, [FromQuery] int[] categoryIds, [FromQuery] string attributes)
{
return Task.FromResult((IActionResult)Ok(new object[] { }));
var criteria = new ProductSearchCriteria
{
NameContains = name,
CategoryIds = categoryIds
};

if (!string.IsNullOrWhiteSpace(attributes))
{
try
{
criteria.AttributeFilters = Newtonsoft.Json.JsonConvert.DeserializeObject<System.Collections.Generic.Dictionary<string, string>>(attributes);
}
catch
{
return BadRequest("'attributes' must be a JSON object of key/value pairs.");
}
}

var results = await _Products.SearchAsync(criteria);
return Ok(results);
}

[HttpGet("{productId:int}")]
public async Task<IActionResult> GetById(int productId)
{
var product = await _Products.GetByIdAsync(productId);
if (product == null) return NotFound();
return Ok(product);
}

[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateProductRequest request)
{
if (string.IsNullOrWhiteSpace(request?.Name))
return BadRequest("Name is required.");

try
{
var productId = await _Products.CreateAsync(request);
var created = await _Products.GetByIdAsync(productId);
return CreatedAtAction(nameof(GetById), new { productId }, created);
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
}
}
}
4 changes: 4 additions & 0 deletions Development Project/Interview.Web/Interview.Web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Sparcpoint.Inventory\Sparcpoint.Inventory.csproj" />
</ItemGroup>

</Project>
19 changes: 3 additions & 16 deletions Development Project/Interview.Web/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Sparcpoint.Inventory.Extensions;

namespace Interview.Web
{
Expand All @@ -20,13 +16,12 @@ public Startup(IConfiguration configuration)

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddInventoryServices(Configuration.GetConnectionString("InventoryDatabase"));
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
Expand All @@ -36,21 +31,13 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
}
}
5 changes: 4 additions & 1 deletion Development Project/Interview.Web/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"ConnectionStrings": {
"InventoryDatabase": "Server=(localdb)\\MSSQLLocalDB;Database=SparcpointInventory;Integrated Security=true;"
}
}
Loading