-
Notifications
You must be signed in to change notification settings - Fork 0
feat(order): implement Order microservice with CRUD + JWT auth #61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
df1a13e
81e1487
9aafd99
ee77c51
b31336b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,29 +1,142 @@ | ||
| using Microsoft.AspNetCore.Authorization; | ||
| using Microsoft.AspNetCore.Mvc; | ||
| using Microsoft.EntityFrameworkCore; | ||
| using Order.API.DTOs; | ||
| using Order.Domain.Entities; | ||
| using Order.Infrastructure.Data; | ||
|
|
||
| namespace Order.API.Controllers; | ||
|
|
||
| [ApiController] | ||
| [Route("api/[controller]")] | ||
| [Authorize] | ||
| public class OrderController : ControllerBase | ||
| { | ||
| private readonly ILogger<OrderController> _logger; | ||
| private readonly OrderDbContext _context; | ||
|
|
||
| public OrderController(ILogger<OrderController> logger) | ||
| public OrderController(OrderDbContext context) | ||
| { | ||
| _logger = logger; | ||
| _context = context; | ||
| } | ||
|
|
||
| [HttpGet] | ||
| public IActionResult GetAll() | ||
| [ProducesResponseType(typeof(List<OrderDto>), StatusCodes.Status200OK)] | ||
| [ProducesResponseType(StatusCodes.Status401Unauthorized)] | ||
| public async Task<IActionResult> GetAll() | ||
| { | ||
| // TODO: Implement — migrate logic from monolith's OrderController | ||
| return Ok(new { service = "Order", status = "scaffold" }); | ||
| var orders = await _context.Orders | ||
| .Include(o => o.OrderDetails) | ||
| .ToListAsync(); | ||
|
|
||
| var result = orders.Select(MapToDto).ToList(); | ||
| return Ok(result); | ||
| } | ||
|
|
||
| [HttpGet("{id}")] | ||
| public IActionResult GetById(int id) | ||
| [ProducesResponseType(typeof(OrderDto), StatusCodes.Status200OK)] | ||
| [ProducesResponseType(StatusCodes.Status404NotFound)] | ||
| [ProducesResponseType(StatusCodes.Status401Unauthorized)] | ||
| public async Task<IActionResult> GetById(int id) | ||
| { | ||
| var order = await _context.Orders | ||
| .Include(o => o.OrderDetails) | ||
| .FirstOrDefaultAsync(o => o.Id == id); | ||
|
|
||
| if (order is null) | ||
| return NotFound(); | ||
|
|
||
| return Ok(MapToDto(order)); | ||
| } | ||
|
|
||
| [HttpPost] | ||
| [ProducesResponseType(typeof(OrderDto), StatusCodes.Status201Created)] | ||
| [ProducesResponseType(StatusCodes.Status401Unauthorized)] | ||
| public async Task<IActionResult> Create([FromBody] CreateOrderDto dto) | ||
| { | ||
| var order = new Order.Domain.Entities.Order | ||
| { | ||
| Discount = dto.Discount, | ||
| Comments = dto.Comments, | ||
| CashierId = dto.CashierId, | ||
| CustomerId = dto.CustomerId, | ||
| CreatedDate = DateTime.UtcNow, | ||
| UpdatedDate = DateTime.UtcNow | ||
| }; | ||
|
|
||
| foreach (var detailDto in dto.OrderDetails) | ||
| { | ||
| order.OrderDetails.Add(new OrderDetail | ||
| { | ||
| UnitPrice = detailDto.UnitPrice, | ||
| Quantity = detailDto.Quantity, | ||
| Discount = detailDto.Discount, | ||
| ProductId = detailDto.ProductId, | ||
| CreatedDate = DateTime.UtcNow, | ||
| UpdatedDate = DateTime.UtcNow | ||
| }); | ||
| } | ||
|
|
||
| _context.Orders.Add(order); | ||
| await _context.SaveChangesAsync(); | ||
|
|
||
| return CreatedAtAction(nameof(GetById), new { id = order.Id }, MapToDto(order)); | ||
| } | ||
|
|
||
| [HttpPut("{id}")] | ||
| [ProducesResponseType(typeof(OrderDto), StatusCodes.Status200OK)] | ||
| [ProducesResponseType(StatusCodes.Status404NotFound)] | ||
| [ProducesResponseType(StatusCodes.Status401Unauthorized)] | ||
| public async Task<IActionResult> Update(int id, [FromBody] UpdateOrderDto dto) | ||
| { | ||
| // TODO: Implement — migrate logic from monolith | ||
| return Ok(new { service = "Order", id }); | ||
| var order = await _context.Orders | ||
| .Include(o => o.OrderDetails) | ||
| .FirstOrDefaultAsync(o => o.Id == id); | ||
|
|
||
| if (order is null) | ||
| return NotFound(); | ||
|
|
||
| order.Discount = dto.Discount; | ||
| order.Comments = dto.Comments; | ||
| order.CashierId = dto.CashierId; | ||
| order.CustomerId = dto.CustomerId; | ||
| order.UpdatedDate = DateTime.UtcNow; | ||
|
|
||
| await _context.SaveChangesAsync(); | ||
|
|
||
| return Ok(MapToDto(order)); | ||
| } | ||
|
Comment on lines
+86
to
+107
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 PUT endpoint only updates header fields, not order details — partial update semantics The Was this helpful? React with 👍 or 👎 to provide feedback.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By design — |
||
|
|
||
| [HttpDelete("{id}")] | ||
| [ProducesResponseType(StatusCodes.Status204NoContent)] | ||
| [ProducesResponseType(StatusCodes.Status404NotFound)] | ||
| [ProducesResponseType(StatusCodes.Status401Unauthorized)] | ||
| public async Task<IActionResult> Delete(int id) | ||
| { | ||
| var order = await _context.Orders.FindAsync(id); | ||
|
|
||
| if (order is null) | ||
| return NotFound(); | ||
|
|
||
| _context.Orders.Remove(order); | ||
| await _context.SaveChangesAsync(); | ||
|
|
||
| return NoContent(); | ||
| } | ||
|
|
||
| private static OrderDto MapToDto(Order.Domain.Entities.Order order) => new() | ||
| { | ||
| Id = order.Id, | ||
| Discount = order.Discount, | ||
| Comments = order.Comments, | ||
| CashierId = order.CashierId, | ||
| CustomerId = order.CustomerId, | ||
| OrderDetails = order.OrderDetails.Select(od => new OrderDetailDto | ||
| { | ||
| Id = od.Id, | ||
| UnitPrice = od.UnitPrice, | ||
| Quantity = od.Quantity, | ||
| Discount = od.Discount, | ||
| ProductId = od.ProductId | ||
| }).ToList() | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| namespace Order.API.DTOs; | ||
|
|
||
| public class OrderDto | ||
| { | ||
| public int Id { get; set; } | ||
| public decimal Discount { get; set; } | ||
| public string? Comments { get; set; } | ||
| public string? CashierId { get; set; } | ||
| public int CustomerId { get; set; } | ||
| public List<OrderDetailDto> OrderDetails { get; set; } = []; | ||
| } | ||
|
|
||
| public class CreateOrderDto | ||
| { | ||
| public decimal Discount { get; set; } | ||
| public string? Comments { get; set; } | ||
| public string? CashierId { get; set; } | ||
| public int CustomerId { get; set; } | ||
| public List<CreateOrderDetailDto> OrderDetails { get; set; } = []; | ||
| } | ||
|
|
||
| public class UpdateOrderDto | ||
| { | ||
| public decimal Discount { get; set; } | ||
| public string? Comments { get; set; } | ||
| public string? CashierId { get; set; } | ||
| public int CustomerId { get; set; } | ||
| } | ||
|
|
||
| public class OrderDetailDto | ||
| { | ||
| public int Id { get; set; } | ||
| public decimal UnitPrice { get; set; } | ||
| public int Quantity { get; set; } | ||
| public decimal Discount { get; set; } | ||
| public int ProductId { get; set; } | ||
| } | ||
|
|
||
| public class CreateOrderDetailDto | ||
| { | ||
| public decimal UnitPrice { get; set; } | ||
| public int Quantity { get; set; } | ||
| public decimal Discount { get; set; } | ||
| public int ProductId { get; set; } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| namespace Order.Domain.Entities; | ||
|
|
||
| public abstract class BaseEntity | ||
| { | ||
| public int Id { get; set; } | ||
| public string? CreatedBy { get; set; } | ||
| public string? UpdatedBy { get; set; } | ||
| public DateTime CreatedDate { get; set; } | ||
| public DateTime UpdatedDate { get; set; } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| namespace Order.Domain.Entities; | ||
|
|
||
| public class Order : BaseEntity | ||
| { | ||
| public decimal Discount { get; set; } | ||
| public string? Comments { get; set; } | ||
| public string? CashierId { get; set; } | ||
| public int CustomerId { get; set; } | ||
| public ICollection<OrderDetail> OrderDetails { get; set; } = []; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| namespace Order.Domain.Entities; | ||
|
|
||
| public class OrderDetail : BaseEntity | ||
| { | ||
| public decimal UnitPrice { get; set; } | ||
| public int Quantity { get; set; } | ||
| public decimal Discount { get; set; } | ||
| public int ProductId { get; set; } | ||
| public int OrderId { get; set; } | ||
| public Order Order { get; set; } = null!; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚩 Audit fields (CreatedBy/UpdatedBy) are never populated despite being defined on all entities
The
BaseEntityclass (Order.Domain/Entities/BaseEntity.cs:6-7) definesCreatedByandUpdatedByaudit properties, and the DbContext configures max lengths for them (OrderDbContext.cs:25-26,OrderDbContext.cs:40-41). However, theCreateaction (OrderController.cs:56-64) andUpdateaction (OrderController.cs:98-103) never set these fields, even though the[Authorize]attribute guarantees an authenticated user identity is available viaHttpContext.User. These columns will always be NULL in the database, making the audit trail incomplete.Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Acknowledged. The audit fields (
CreatedBy/UpdatedBy) are intentionally left unpopulated in this initial extraction — the monolith's audit logic lives in its sharedApplicationDbContext.SaveChangesAsyncoverride, which is out of scope for this carve-out. Populating them fromHttpContext.Userclaims would be a follow-up enhancement once the identity claim structure is finalized across services.