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
Binary file added Development Project Summary.docx
Binary file not shown.
Binary file added Development Project.zip
Binary file not shown.
46 changes: 30 additions & 16 deletions Development Project/Development Project.sln
Original file line number Diff line number Diff line change
@@ -1,60 +1,74 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31410.357
# Visual Studio Version 17
VisualStudioVersion = 17.14.36408.4 d17.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Interview.Web", "Interview.Web\Interview.Web.csproj", "{EE17B748-4D84-46AE-9E83-8D04B92DD6A9}"
EndProject
Project("{00D1A9C2-B5F0-4AF3-8072-F6C62B433612}") = "Sparcpoint.Inventory.Database", "Sparcpoint.Inventory.Database\Sparcpoint.Inventory.Database.sqlproj", "{62E57E63-D706-49CF-B2BF-C9BED8189C36}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sparcpoint.Core", "Sparcpoint.Core\Sparcpoint.Core.csproj", "{C4ECDF4B-3396-4512-84DB-ECECA8C5600E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sparcpoint.SqlServer.Abstractions", "Sparcpoint.SqlServer.Abstractions\Sparcpoint.SqlServer.Abstractions.csproj", "{4E72CD4A-C9FE-4A04-9F07-A770E3AD7297}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sparcpoint.Inventory.Database", "Sparcpoint.Inventory.Database\Sparcpoint.Inventory.Database.csproj", "{5C8CD168-28EE-F797-C2EF-FC6D604BC884}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{EE17B748-4D84-46AE-9E83-8D04B92DD6A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EE17B748-4D84-46AE-9E83-8D04B92DD6A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EE17B748-4D84-46AE-9E83-8D04B92DD6A9}.Debug|x64.ActiveCfg = Debug|Any CPU
{EE17B748-4D84-46AE-9E83-8D04B92DD6A9}.Debug|x64.Build.0 = Debug|Any CPU
{EE17B748-4D84-46AE-9E83-8D04B92DD6A9}.Debug|x86.ActiveCfg = Debug|Any CPU
{EE17B748-4D84-46AE-9E83-8D04B92DD6A9}.Debug|x86.Build.0 = Debug|Any CPU
{EE17B748-4D84-46AE-9E83-8D04B92DD6A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EE17B748-4D84-46AE-9E83-8D04B92DD6A9}.Release|Any CPU.Build.0 = Release|Any CPU
{EE17B748-4D84-46AE-9E83-8D04B92DD6A9}.Release|x64.ActiveCfg = Release|Any CPU
{EE17B748-4D84-46AE-9E83-8D04B92DD6A9}.Release|x64.Build.0 = Release|Any CPU
{EE17B748-4D84-46AE-9E83-8D04B92DD6A9}.Release|x86.ActiveCfg = Release|Any CPU
{EE17B748-4D84-46AE-9E83-8D04B92DD6A9}.Release|x86.Build.0 = Release|Any CPU
{62E57E63-D706-49CF-B2BF-C9BED8189C36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{62E57E63-D706-49CF-B2BF-C9BED8189C36}.Debug|Any CPU.Build.0 = Debug|Any CPU
{62E57E63-D706-49CF-B2BF-C9BED8189C36}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{62E57E63-D706-49CF-B2BF-C9BED8189C36}.Debug|x86.ActiveCfg = Debug|x86
{62E57E63-D706-49CF-B2BF-C9BED8189C36}.Debug|x86.Build.0 = Debug|x86
{62E57E63-D706-49CF-B2BF-C9BED8189C36}.Debug|x86.Deploy.0 = Debug|x86
{62E57E63-D706-49CF-B2BF-C9BED8189C36}.Release|Any CPU.ActiveCfg = Release|Any CPU
{62E57E63-D706-49CF-B2BF-C9BED8189C36}.Release|Any CPU.Build.0 = Release|Any CPU
{62E57E63-D706-49CF-B2BF-C9BED8189C36}.Release|Any CPU.Deploy.0 = Release|Any CPU
{62E57E63-D706-49CF-B2BF-C9BED8189C36}.Release|x86.ActiveCfg = Release|x86
{62E57E63-D706-49CF-B2BF-C9BED8189C36}.Release|x86.Build.0 = Release|x86
{62E57E63-D706-49CF-B2BF-C9BED8189C36}.Release|x86.Deploy.0 = Release|x86
{C4ECDF4B-3396-4512-84DB-ECECA8C5600E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C4ECDF4B-3396-4512-84DB-ECECA8C5600E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C4ECDF4B-3396-4512-84DB-ECECA8C5600E}.Debug|x64.ActiveCfg = Debug|Any CPU
{C4ECDF4B-3396-4512-84DB-ECECA8C5600E}.Debug|x64.Build.0 = Debug|Any CPU
{C4ECDF4B-3396-4512-84DB-ECECA8C5600E}.Debug|x86.ActiveCfg = Debug|Any CPU
{C4ECDF4B-3396-4512-84DB-ECECA8C5600E}.Debug|x86.Build.0 = Debug|Any CPU
{C4ECDF4B-3396-4512-84DB-ECECA8C5600E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C4ECDF4B-3396-4512-84DB-ECECA8C5600E}.Release|Any CPU.Build.0 = Release|Any CPU
{C4ECDF4B-3396-4512-84DB-ECECA8C5600E}.Release|x64.ActiveCfg = Release|Any CPU
{C4ECDF4B-3396-4512-84DB-ECECA8C5600E}.Release|x64.Build.0 = Release|Any CPU
{C4ECDF4B-3396-4512-84DB-ECECA8C5600E}.Release|x86.ActiveCfg = Release|Any CPU
{C4ECDF4B-3396-4512-84DB-ECECA8C5600E}.Release|x86.Build.0 = Release|Any CPU
{4E72CD4A-C9FE-4A04-9F07-A770E3AD7297}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4E72CD4A-C9FE-4A04-9F07-A770E3AD7297}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4E72CD4A-C9FE-4A04-9F07-A770E3AD7297}.Debug|x64.ActiveCfg = Debug|Any CPU
{4E72CD4A-C9FE-4A04-9F07-A770E3AD7297}.Debug|x64.Build.0 = Debug|Any CPU
{4E72CD4A-C9FE-4A04-9F07-A770E3AD7297}.Debug|x86.ActiveCfg = Debug|Any CPU
{4E72CD4A-C9FE-4A04-9F07-A770E3AD7297}.Debug|x86.Build.0 = Debug|Any CPU
{4E72CD4A-C9FE-4A04-9F07-A770E3AD7297}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4E72CD4A-C9FE-4A04-9F07-A770E3AD7297}.Release|Any CPU.Build.0 = Release|Any CPU
{4E72CD4A-C9FE-4A04-9F07-A770E3AD7297}.Release|x64.ActiveCfg = Release|Any CPU
{4E72CD4A-C9FE-4A04-9F07-A770E3AD7297}.Release|x64.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
{5C8CD168-28EE-F797-C2EF-FC6D604BC884}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5C8CD168-28EE-F797-C2EF-FC6D604BC884}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5C8CD168-28EE-F797-C2EF-FC6D604BC884}.Debug|x64.ActiveCfg = Debug|Any CPU
{5C8CD168-28EE-F797-C2EF-FC6D604BC884}.Debug|x64.Build.0 = Debug|Any CPU
{5C8CD168-28EE-F797-C2EF-FC6D604BC884}.Debug|x86.ActiveCfg = Debug|Any CPU
{5C8CD168-28EE-F797-C2EF-FC6D604BC884}.Debug|x86.Build.0 = Debug|Any CPU
{5C8CD168-28EE-F797-C2EF-FC6D604BC884}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5C8CD168-28EE-F797-C2EF-FC6D604BC884}.Release|Any CPU.Build.0 = Release|Any CPU
{5C8CD168-28EE-F797-C2EF-FC6D604BC884}.Release|x64.ActiveCfg = Release|Any CPU
{5C8CD168-28EE-F797-C2EF-FC6D604BC884}.Release|x64.Build.0 = Release|Any CPU
{5C8CD168-28EE-F797-C2EF-FC6D604BC884}.Release|x86.ActiveCfg = Release|Any CPU
{5C8CD168-28EE-F797-C2EF-FC6D604BC884}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
106 changes: 98 additions & 8 deletions Development Project/Interview.Web/Controllers/ProductController.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,109 @@
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Sparcpoint.Core.Abstract;
using Sparcpoint.Core.Models;

namespace Interview.Web.Controllers
{
[Route("api/v1/products")]
public class ProductController : Controller
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
// NOTE: Sample Action
private readonly IProductRepository _repo;

public ProductsController(IProductRepository repo)
{
_repo = repo ?? throw new ArgumentNullException(nameof(repo));
}

[HttpPost]
public async Task<IActionResult> Create([FromBody] ProductCreateRequest request)
{
if (request is null)
return BadRequest("Request body is required.");

// Basic server-side validation (expand in service layer as needed)
if (string.IsNullOrWhiteSpace(request.Name))
return BadRequest("Name is required.");

var productId = await _repo.AddProductAsync(request);

// Return 201 Created with location header
var uri = $"/api/products/{productId}";
return Created(uri, new { Id = productId });
}

/// <summary>
/// Lightweight, URL-query searchable endpoint for common searches.
/// Supports name, description, single sku, categoryId (repeatable), paging and simple ordering.
/// </summary>
[HttpGet]
public Task<IActionResult> GetAllProducts()
public async Task<IActionResult> Get(
[FromQuery] string name = null,
[FromQuery] string description = null,
[FromQuery] string sku = null,
[FromQuery(Name = "categoryId")] int[] categoryIds = null,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 50,
[FromQuery] string orderBy = null,
[FromQuery] bool orderDesc = false)
{
return Task.FromResult((IActionResult)Ok(new object[] { }));
if (page <= 0) page = 1;
if (pageSize <= 0) pageSize = 50;
if (pageSize > 500) pageSize = 500; // defensive cap

var criteria = new ProductSearchCriteria
{
Name = name,
Description = description,
Skus = string.IsNullOrWhiteSpace(sku) ? null : new[] { sku },
CategoryIds = categoryIds,
Page = page,
PageSize = pageSize,
OrderBy = orderBy,
OrderDescending = orderDesc
};

var results = await _repo.SearchProductsAsync(criteria) ?? new System.Collections.Generic.List<ProductSearchResult>();

var response = new ProductSearchResponse
{
Items = results.ToList(),
TotalCount = results.Count, // EVAL: replace with repo-provided total if available
Page = page,
PageSize = pageSize
};

return Ok(response);
}

/// <summary>
/// Complex search using ProductSearchCriteria in the request body.
/// Use this for metadata filters, multiple SKUs, match modes, etc.
/// </summary>
[HttpPost("search")]
public async Task<IActionResult> Search([FromBody] ProductSearchCriteria criteria)
{
if (criteria is null)
return BadRequest("Request body is required.");

if (criteria.Page <= 0) criteria.Page = 1;
if (criteria.PageSize <= 0) criteria.PageSize = 50;
if (criteria.PageSize > 500) criteria.PageSize = 500; // server-side cap

var results = await _repo.SearchProductsAsync(criteria) ?? new System.Collections.Generic.List<ProductSearchResult>();

var response = new ProductSearchResponse
{
Items = results.ToList(),
TotalCount = results.Count, // EVAL: replace with repo-provided total if available
Page = criteria.Page,
PageSize = criteria.PageSize
};

return Ok(response);
}
}
}
10 changes: 10 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,14 @@
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.Text.Json" Version="10.0.7" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Sparcpoint.Core\Sparcpoint.Core.csproj" />
<ProjectReference Include="..\Sparcpoint.Inventory.Database\Sparcpoint.Inventory.Database.csproj" />
<ProjectReference Include="..\Sparcpoint.SqlServer.Abstractions\Sparcpoint.SqlServer.Abstractions.csproj" />
</ItemGroup>

</Project>
12 changes: 12 additions & 0 deletions Development Project/Interview.Web/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Sparcpoint.Core.Abstract;
using Sparcpoint.Inventory.Database.Data;
using Sparcpoint.SqlServer.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
Expand All @@ -24,6 +27,15 @@ public Startup(IConfiguration configuration)
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();

// Register ISqlExecutor and repository
// Pull connection string from: ConnectionStrings:DefaultConnection or SqlServer:ConnectionString
var connectionString = Configuration.GetConnectionString("DefaultConnection")
?? Configuration["SqlServer:ConnectionString"]
?? throw new InvalidOperationException("Connection string not configured.");

services.AddSingleton<ISqlExecutor>(_ => new SqlServerExecutor(connectionString));
services.AddScoped<IProductRepository, ProductRepository>();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
Expand Down
12 changes: 12 additions & 0 deletions Development Project/Sparcpoint.Core/Abstract/IProductRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Sparcpoint.Core.Models;

namespace Sparcpoint.Core.Abstract
{
public interface IProductRepository
{
Task<int> AddProductAsync(ProductCreateRequest request);
Task<IList<ProductSearchResult>> SearchProductsAsync(ProductSearchCriteria criteria);
}
}
24 changes: 24 additions & 0 deletions Development Project/Sparcpoint.Core/Models/ProductCreateRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace Sparcpoint.Core.Models
{
public class ProductCreateRequest
{
[Required]
[StringLength(256)]
public string Name { get; set; }

[StringLength(256)]
public string Description { get; set; }

public IEnumerable<string> ProductImageUris { get; set; }

public IEnumerable<string> ValidSkus { get; set; }

// Metadata keys/values validated in service/validator
public Dictionary<string, string> Metadata { get; set; } = new Dictionary<string, string>();

public IEnumerable<int> CategoryIds { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Collections.Generic;

namespace Sparcpoint.Core.Models
{
public enum MetadataMatchMode
{
Exact = 0,
Contains = 1
}

public class ProductSearchCriteria
{
// General fields (string matching)
public string Name { get; set; }
public string Description { get; set; }

// SKU filtering
public IEnumerable<string> Skus { get; set; }

// Category filtering
public IEnumerable<int> CategoryIds { get; set; }
// If true, include descendant categories when resolving category filters
public bool IncludeCategoryDescendants { get; set; }

// Metadata key/value filters
public Dictionary<string, string> Metadata { get; set; } = new Dictionary<string, string>();
// Matching behavior for metadata values (Exact vs substring Contains)
public MetadataMatchMode MetadataMatchMode { get; set; } = MetadataMatchMode.Exact;
// If true, all metadata pairs must match (AND). If false, any matching pair satisfies (OR).
public bool MatchAllMetadata { get; set; } = true;

// Paging & sorting
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 50;
// Column name to order by (validate/whitelist server-side)
public string OrderBy { get; set; }
public bool OrderDescending { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

using System.Collections.Generic;

namespace Sparcpoint.Core.Models
{
public class ProductSearchResponse
{
public IList<ProductSearchResult> Items { get; set; } = new List<ProductSearchResult>();
public int TotalCount { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
}
}
26 changes: 26 additions & 0 deletions Development Project/Sparcpoint.Core/Models/ProductSearchResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;

namespace Sparcpoint.Core.Models
{
public class ProductSearchResult
{
public int InstanceId { get; set; }
public string Name { get; set; }
public string Description { get; set; }

// JSON columns deserialized by repository
public IEnumerable<string> ProductImageUris { get; set; }
public IEnumerable<string> ValidSkus { get; set; }

// Key/value metadata from ProductAttributes
public Dictionary<string, string> Metadata { get; set; } = new Dictionary<string, string>();

// Assigned category ids (many-to-many)
public IEnumerable<int> CategoryIds { get; set; }

// Product created timestamp if available in schema
public DateTime? CreatedTimestamp { get; set; }
}
}

1 change: 1 addition & 0 deletions Development Project/Sparcpoint.Core/Sparcpoint.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="3.1.8" />
<PackageReference Include="Microsoft.Extensions.Options" Version="3.1.8" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="System.Text.Json" Version="10.0.7" />
</ItemGroup>

</Project>
Loading