Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Appendix E: Code Templates

Version: 1.0.0 Last Updated: December 29, 2025 Language: C# (.NET 8.0)


Overview

This appendix contains copy-paste code templates for common patterns in the POS Platform. All templates follow the established architecture and coding standards.


Table of Contents

  1. Entity Template
  2. Repository Interface Template
  3. Repository Implementation Template
  4. Service Interface Template
  5. Service Implementation Template
  6. Controller Template
  7. DTO Templates
  8. Validator Template
  9. Event Handler Template
  10. Integration Test Template
  11. Unit Test Template
  12. Domain Event Template

1. Entity Template

// File: src/POS.Core/Entities/Product.cs

using System;
using System.Collections.Generic;

namespace POS.Core.Entities;

/// <summary>
/// Represents a product in the catalog.
/// </summary>
public class Product : BaseEntity, IAuditableEntity, ITenantEntity
{
    /// <summary>
    /// Gets or sets the tenant identifier.
    /// </summary>
    public Guid TenantId { get; set; }

    /// <summary>
    /// Gets or sets the SKU (Stock Keeping Unit).
    /// </summary>
    public required string Sku { get; set; }

    /// <summary>
    /// Gets or sets the product name.
    /// </summary>
    public required string Name { get; set; }

    /// <summary>
    /// Gets or sets the product description.
    /// </summary>
    public string? Description { get; set; }

    /// <summary>
    /// Gets or sets the category identifier.
    /// </summary>
    public Guid? CategoryId { get; set; }

    /// <summary>
    /// Gets or sets the vendor identifier.
    /// </summary>
    public Guid? VendorId { get; set; }

    /// <summary>
    /// Gets or sets the base price.
    /// </summary>
    public decimal BasePrice { get; set; }

    /// <summary>
    /// Gets or sets the cost price.
    /// </summary>
    public decimal Cost { get; set; }

    /// <summary>
    /// Gets or sets the product status.
    /// </summary>
    public ProductStatus Status { get; set; } = ProductStatus.Active;

    /// <summary>
    /// Gets or sets the Shopify product ID for integration.
    /// </summary>
    public string? ShopifyProductId { get; set; }

    // Navigation properties
    public virtual Category? Category { get; set; }
    public virtual Vendor? Vendor { get; set; }
    public virtual ICollection<ProductVariant> Variants { get; set; } = new List<ProductVariant>();
    public virtual ICollection<ProductImage> Images { get; set; } = new List<ProductImage>();

    // Audit properties
    public DateTime CreatedAt { get; set; }
    public Guid? CreatedBy { get; set; }
    public DateTime? UpdatedAt { get; set; }
    public Guid? UpdatedBy { get; set; }
}

/// <summary>
/// Product status enumeration.
/// </summary>
public enum ProductStatus
{
    Draft,
    Active,
    Discontinued,
    Archived
}

2. Repository Interface Template

// File: src/POS.Core/Interfaces/Repositories/IProductRepository.cs

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using POS.Core.Entities;

namespace POS.Core.Interfaces.Repositories;

/// <summary>
/// Repository interface for Product entity operations.
/// </summary>
public interface IProductRepository : IRepository<Product>
{
    /// <summary>
    /// Gets a product by SKU.
    /// </summary>
    /// <param name="sku">The SKU to search for.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The product if found, null otherwise.</returns>
    Task<Product?> GetBySkuAsync(string sku, CancellationToken cancellationToken = default);

    /// <summary>
    /// Gets products by category.
    /// </summary>
    /// <param name="categoryId">The category identifier.</param>
    /// <param name="includeVariants">Whether to include variants.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>List of products in the category.</returns>
    Task<IReadOnlyList<Product>> GetByCategoryAsync(
        Guid categoryId,
        bool includeVariants = false,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Gets products by vendor.
    /// </summary>
    /// <param name="vendorId">The vendor identifier.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>List of products from the vendor.</returns>
    Task<IReadOnlyList<Product>> GetByVendorAsync(
        Guid vendorId,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Searches products by name or SKU.
    /// </summary>
    /// <param name="searchTerm">The search term.</param>
    /// <param name="page">Page number (1-based).</param>
    /// <param name="pageSize">Items per page.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>Paginated list of matching products.</returns>
    Task<PagedResult<Product>> SearchAsync(
        string searchTerm,
        int page = 1,
        int pageSize = 20,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Checks if a SKU exists.
    /// </summary>
    /// <param name="sku">The SKU to check.</param>
    /// <param name="excludeProductId">Product ID to exclude from check.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>True if SKU exists, false otherwise.</returns>
    Task<bool> SkuExistsAsync(
        string sku,
        Guid? excludeProductId = null,
        CancellationToken cancellationToken = default);
}

3. Repository Implementation Template

// File: src/POS.Infrastructure/Repositories/ProductRepository.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using POS.Core.Entities;
using POS.Core.Interfaces.Repositories;
using POS.Infrastructure.Data;

namespace POS.Infrastructure.Repositories;

/// <summary>
/// Repository implementation for Product entity.
/// </summary>
public class ProductRepository : Repository<Product>, IProductRepository
{
    public ProductRepository(ApplicationDbContext context) : base(context)
    {
    }

    /// <inheritdoc />
    public async Task<Product?> GetBySkuAsync(
        string sku,
        CancellationToken cancellationToken = default)
    {
        return await _dbSet
            .Include(p => p.Variants)
            .Include(p => p.Category)
            .FirstOrDefaultAsync(p => p.Sku == sku, cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<Product>> GetByCategoryAsync(
        Guid categoryId,
        bool includeVariants = false,
        CancellationToken cancellationToken = default)
    {
        var query = _dbSet
            .Where(p => p.CategoryId == categoryId)
            .Where(p => p.Status == ProductStatus.Active);

        if (includeVariants)
        {
            query = query.Include(p => p.Variants);
        }

        return await query
            .OrderBy(p => p.Name)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<Product>> GetByVendorAsync(
        Guid vendorId,
        CancellationToken cancellationToken = default)
    {
        return await _dbSet
            .Where(p => p.VendorId == vendorId)
            .Include(p => p.Variants)
            .OrderBy(p => p.Name)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<PagedResult<Product>> SearchAsync(
        string searchTerm,
        int page = 1,
        int pageSize = 20,
        CancellationToken cancellationToken = default)
    {
        var query = _dbSet
            .Where(p => p.Status == ProductStatus.Active)
            .Where(p => EF.Functions.ILike(p.Name, $"%{searchTerm}%") ||
                       EF.Functions.ILike(p.Sku, $"%{searchTerm}%"));

        var totalCount = await query.CountAsync(cancellationToken);

        var items = await query
            .Include(p => p.Variants)
            .OrderBy(p => p.Name)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync(cancellationToken);

        return new PagedResult<Product>(items, totalCount, page, pageSize);
    }

    /// <inheritdoc />
    public async Task<bool> SkuExistsAsync(
        string sku,
        Guid? excludeProductId = null,
        CancellationToken cancellationToken = default)
    {
        var query = _dbSet.Where(p => p.Sku == sku);

        if (excludeProductId.HasValue)
        {
            query = query.Where(p => p.Id != excludeProductId.Value);
        }

        return await query.AnyAsync(cancellationToken);
    }
}

4. Service Interface Template

// File: src/POS.Core/Interfaces/Services/IProductService.cs

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using POS.Core.DTOs;

namespace POS.Core.Interfaces.Services;

/// <summary>
/// Service interface for product operations.
/// </summary>
public interface IProductService
{
    /// <summary>
    /// Gets a product by ID.
    /// </summary>
    Task<ProductDto?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);

    /// <summary>
    /// Gets a product by SKU.
    /// </summary>
    Task<ProductDto?> GetBySkuAsync(string sku, CancellationToken cancellationToken = default);

    /// <summary>
    /// Gets all products with optional filtering.
    /// </summary>
    Task<PagedResult<ProductDto>> GetAllAsync(
        ProductFilterDto filter,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Creates a new product.
    /// </summary>
    Task<ProductDto> CreateAsync(
        CreateProductDto dto,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Updates an existing product.
    /// </summary>
    Task<ProductDto> UpdateAsync(
        Guid id,
        UpdateProductDto dto,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Deletes a product (soft delete).
    /// </summary>
    Task DeleteAsync(Guid id, CancellationToken cancellationToken = default);

    /// <summary>
    /// Adds a variant to a product.
    /// </summary>
    Task<ProductVariantDto> AddVariantAsync(
        Guid productId,
        CreateVariantDto dto,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Updates a product variant.
    /// </summary>
    Task<ProductVariantDto> UpdateVariantAsync(
        Guid variantId,
        UpdateVariantDto dto,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Searches products.
    /// </summary>
    Task<PagedResult<ProductDto>> SearchAsync(
        string searchTerm,
        int page = 1,
        int pageSize = 20,
        CancellationToken cancellationToken = default);
}

5. Service Implementation Template

// File: src/POS.Application/Services/ProductService.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using FluentValidation;
using Microsoft.Extensions.Logging;
using POS.Core.DTOs;
using POS.Core.Entities;
using POS.Core.Exceptions;
using POS.Core.Interfaces.Repositories;
using POS.Core.Interfaces.Services;

namespace POS.Application.Services;

/// <summary>
/// Service implementation for product operations.
/// </summary>
public class ProductService : IProductService
{
    private readonly IProductRepository _productRepository;
    private readonly IUnitOfWork _unitOfWork;
    private readonly IMapper _mapper;
    private readonly IValidator<CreateProductDto> _createValidator;
    private readonly IValidator<UpdateProductDto> _updateValidator;
    private readonly ILogger<ProductService> _logger;
    private readonly IDomainEventDispatcher _eventDispatcher;

    public ProductService(
        IProductRepository productRepository,
        IUnitOfWork unitOfWork,
        IMapper mapper,
        IValidator<CreateProductDto> createValidator,
        IValidator<UpdateProductDto> updateValidator,
        ILogger<ProductService> logger,
        IDomainEventDispatcher eventDispatcher)
    {
        _productRepository = productRepository;
        _unitOfWork = unitOfWork;
        _mapper = mapper;
        _createValidator = createValidator;
        _updateValidator = updateValidator;
        _logger = logger;
        _eventDispatcher = eventDispatcher;
    }

    /// <inheritdoc />
    public async Task<ProductDto?> GetByIdAsync(
        Guid id,
        CancellationToken cancellationToken = default)
    {
        var product = await _productRepository.GetByIdAsync(id, cancellationToken);
        return product is null ? null : _mapper.Map<ProductDto>(product);
    }

    /// <inheritdoc />
    public async Task<ProductDto?> GetBySkuAsync(
        string sku,
        CancellationToken cancellationToken = default)
    {
        var product = await _productRepository.GetBySkuAsync(sku, cancellationToken);
        return product is null ? null : _mapper.Map<ProductDto>(product);
    }

    /// <inheritdoc />
    public async Task<PagedResult<ProductDto>> GetAllAsync(
        ProductFilterDto filter,
        CancellationToken cancellationToken = default)
    {
        var result = await _productRepository.SearchAsync(
            filter.SearchTerm ?? "",
            filter.Page,
            filter.PageSize,
            cancellationToken);

        return new PagedResult<ProductDto>(
            _mapper.Map<List<ProductDto>>(result.Items),
            result.TotalCount,
            result.Page,
            result.PageSize);
    }

    /// <inheritdoc />
    public async Task<ProductDto> CreateAsync(
        CreateProductDto dto,
        CancellationToken cancellationToken = default)
    {
        // Validate
        var validationResult = await _createValidator.ValidateAsync(dto, cancellationToken);
        if (!validationResult.IsValid)
        {
            throw new ValidationException(validationResult.Errors);
        }

        // Check SKU uniqueness
        if (await _productRepository.SkuExistsAsync(dto.Sku, null, cancellationToken))
        {
            throw new BusinessException($"SKU '{dto.Sku}' already exists.");
        }

        // Create entity
        var product = _mapper.Map<Product>(dto);
        product.Status = ProductStatus.Active;

        await _productRepository.AddAsync(product, cancellationToken);
        await _unitOfWork.SaveChangesAsync(cancellationToken);

        _logger.LogInformation("Product created: {ProductId} - {Sku}", product.Id, product.Sku);

        // Dispatch domain event
        await _eventDispatcher.DispatchAsync(new ProductCreatedEvent(product.Id, product.Sku));

        return _mapper.Map<ProductDto>(product);
    }

    /// <inheritdoc />
    public async Task<ProductDto> UpdateAsync(
        Guid id,
        UpdateProductDto dto,
        CancellationToken cancellationToken = default)
    {
        // Validate
        var validationResult = await _updateValidator.ValidateAsync(dto, cancellationToken);
        if (!validationResult.IsValid)
        {
            throw new ValidationException(validationResult.Errors);
        }

        // Get existing product
        var product = await _productRepository.GetByIdAsync(id, cancellationToken);
        if (product is null)
        {
            throw new NotFoundException($"Product with ID {id} not found.");
        }

        // Check SKU uniqueness if changed
        if (dto.Sku != product.Sku &&
            await _productRepository.SkuExistsAsync(dto.Sku, id, cancellationToken))
        {
            throw new BusinessException($"SKU '{dto.Sku}' already exists.");
        }

        // Update entity
        _mapper.Map(dto, product);

        _productRepository.Update(product);
        await _unitOfWork.SaveChangesAsync(cancellationToken);

        _logger.LogInformation("Product updated: {ProductId} - {Sku}", product.Id, product.Sku);

        return _mapper.Map<ProductDto>(product);
    }

    /// <inheritdoc />
    public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default)
    {
        var product = await _productRepository.GetByIdAsync(id, cancellationToken);
        if (product is null)
        {
            throw new NotFoundException($"Product with ID {id} not found.");
        }

        // Soft delete - change status
        product.Status = ProductStatus.Archived;

        _productRepository.Update(product);
        await _unitOfWork.SaveChangesAsync(cancellationToken);

        _logger.LogInformation("Product archived: {ProductId} - {Sku}", product.Id, product.Sku);
    }

    /// <inheritdoc />
    public async Task<ProductVariantDto> AddVariantAsync(
        Guid productId,
        CreateVariantDto dto,
        CancellationToken cancellationToken = default)
    {
        var product = await _productRepository.GetByIdAsync(productId, cancellationToken);
        if (product is null)
        {
            throw new NotFoundException($"Product with ID {productId} not found.");
        }

        var variant = _mapper.Map<ProductVariant>(dto);
        variant.ProductId = productId;

        product.Variants.Add(variant);
        await _unitOfWork.SaveChangesAsync(cancellationToken);

        _logger.LogInformation("Variant added: {VariantId} to Product {ProductId}",
            variant.Id, productId);

        return _mapper.Map<ProductVariantDto>(variant);
    }

    /// <inheritdoc />
    public async Task<ProductVariantDto> UpdateVariantAsync(
        Guid variantId,
        UpdateVariantDto dto,
        CancellationToken cancellationToken = default)
    {
        // Implementation similar to UpdateAsync
        throw new NotImplementedException();
    }

    /// <inheritdoc />
    public async Task<PagedResult<ProductDto>> SearchAsync(
        string searchTerm,
        int page = 1,
        int pageSize = 20,
        CancellationToken cancellationToken = default)
    {
        var result = await _productRepository.SearchAsync(
            searchTerm, page, pageSize, cancellationToken);

        return new PagedResult<ProductDto>(
            _mapper.Map<List<ProductDto>>(result.Items),
            result.TotalCount,
            result.Page,
            result.PageSize);
    }
}

6. Controller Template

// File: src/POS.API/Controllers/ProductsController.cs

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using POS.Core.DTOs;
using POS.Core.Interfaces.Services;

namespace POS.API.Controllers;

/// <summary>
/// API controller for product operations.
/// </summary>
[ApiController]
[Route("api/v1/[controller]")]
[Authorize]
[Produces("application/json")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;
    private readonly ILogger<ProductsController> _logger;

    public ProductsController(
        IProductService productService,
        ILogger<ProductsController> logger)
    {
        _productService = productService;
        _logger = logger;
    }

    /// <summary>
    /// Gets all products with optional filtering.
    /// </summary>
    /// <param name="filter">Filter parameters.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>Paginated list of products.</returns>
    [HttpGet]
    [ProducesResponseType(typeof(PagedResult<ProductDto>), StatusCodes.Status200OK)]
    public async Task<ActionResult<PagedResult<ProductDto>>> GetAll(
        [FromQuery] ProductFilterDto filter,
        CancellationToken cancellationToken)
    {
        var result = await _productService.GetAllAsync(filter, cancellationToken);
        return Ok(result);
    }

    /// <summary>
    /// Gets a product by ID.
    /// </summary>
    /// <param name="id">The product ID.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The product if found.</returns>
    [HttpGet("{id:guid}")]
    [ProducesResponseType(typeof(ProductDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<ProductDto>> GetById(
        Guid id,
        CancellationToken cancellationToken)
    {
        var product = await _productService.GetByIdAsync(id, cancellationToken);

        if (product is null)
        {
            return NotFound();
        }

        return Ok(product);
    }

    /// <summary>
    /// Gets a product by SKU.
    /// </summary>
    /// <param name="sku">The product SKU.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The product if found.</returns>
    [HttpGet("sku/{sku}")]
    [ProducesResponseType(typeof(ProductDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<ProductDto>> GetBySku(
        string sku,
        CancellationToken cancellationToken)
    {
        var product = await _productService.GetBySkuAsync(sku, cancellationToken);

        if (product is null)
        {
            return NotFound();
        }

        return Ok(product);
    }

    /// <summary>
    /// Creates a new product.
    /// </summary>
    /// <param name="dto">The product data.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The created product.</returns>
    [HttpPost]
    [Authorize(Policy = "CanManageProducts")]
    [ProducesResponseType(typeof(ProductDto), StatusCodes.Status201Created)]
    [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
    public async Task<ActionResult<ProductDto>> Create(
        [FromBody] CreateProductDto dto,
        CancellationToken cancellationToken)
    {
        var product = await _productService.CreateAsync(dto, cancellationToken);
        return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
    }

    /// <summary>
    /// Updates an existing product.
    /// </summary>
    /// <param name="id">The product ID.</param>
    /// <param name="dto">The updated product data.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The updated product.</returns>
    [HttpPut("{id:guid}")]
    [Authorize(Policy = "CanManageProducts")]
    [ProducesResponseType(typeof(ProductDto), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<ProductDto>> Update(
        Guid id,
        [FromBody] UpdateProductDto dto,
        CancellationToken cancellationToken)
    {
        var product = await _productService.UpdateAsync(id, dto, cancellationToken);
        return Ok(product);
    }

    /// <summary>
    /// Deletes a product (soft delete).
    /// </summary>
    /// <param name="id">The product ID.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>No content on success.</returns>
    [HttpDelete("{id:guid}")]
    [Authorize(Policy = "CanManageProducts")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Delete(
        Guid id,
        CancellationToken cancellationToken)
    {
        await _productService.DeleteAsync(id, cancellationToken);
        return NoContent();
    }

    /// <summary>
    /// Adds a variant to a product.
    /// </summary>
    /// <param name="id">The product ID.</param>
    /// <param name="dto">The variant data.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The created variant.</returns>
    [HttpPost("{id:guid}/variants")]
    [Authorize(Policy = "CanManageProducts")]
    [ProducesResponseType(typeof(ProductVariantDto), StatusCodes.Status201Created)]
    [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<ProductVariantDto>> AddVariant(
        Guid id,
        [FromBody] CreateVariantDto dto,
        CancellationToken cancellationToken)
    {
        var variant = await _productService.AddVariantAsync(id, dto, cancellationToken);
        return CreatedAtAction(nameof(GetById), new { id }, variant);
    }

    /// <summary>
    /// Searches products by name or SKU.
    /// </summary>
    /// <param name="q">Search query.</param>
    /// <param name="page">Page number.</param>
    /// <param name="pageSize">Page size.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>Matching products.</returns>
    [HttpGet("search")]
    [ProducesResponseType(typeof(PagedResult<ProductDto>), StatusCodes.Status200OK)]
    public async Task<ActionResult<PagedResult<ProductDto>>> Search(
        [FromQuery] string q,
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20,
        CancellationToken cancellationToken = default)
    {
        var result = await _productService.SearchAsync(q, page, pageSize, cancellationToken);
        return Ok(result);
    }
}

7. DTO Templates

// File: src/POS.Core/DTOs/ProductDtos.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace POS.Core.DTOs;

/// <summary>
/// Product data transfer object.
/// </summary>
public record ProductDto
{
    public Guid Id { get; init; }
    public required string Sku { get; init; }
    public required string Name { get; init; }
    public string? Description { get; init; }
    public Guid? CategoryId { get; init; }
    public string? CategoryName { get; init; }
    public Guid? VendorId { get; init; }
    public string? VendorName { get; init; }
    public decimal BasePrice { get; init; }
    public decimal Cost { get; init; }
    public string Status { get; init; } = "Active";
    public List<ProductVariantDto> Variants { get; init; } = new();
    public List<ProductImageDto> Images { get; init; } = new();
    public DateTime CreatedAt { get; init; }
    public DateTime? UpdatedAt { get; init; }
}

/// <summary>
/// Product variant data transfer object.
/// </summary>
public record ProductVariantDto
{
    public Guid Id { get; init; }
    public required string Sku { get; init; }
    public string? Barcode { get; init; }
    public Dictionary<string, string> Options { get; init; } = new();
    public decimal Price { get; init; }
    public decimal? CompareAtPrice { get; init; }
    public decimal Cost { get; init; }
    public bool IsActive { get; init; }
}

/// <summary>
/// Product image data transfer object.
/// </summary>
public record ProductImageDto
{
    public Guid Id { get; init; }
    public required string Url { get; init; }
    public string? AltText { get; init; }
    public int Position { get; init; }
}

/// <summary>
/// DTO for creating a new product.
/// </summary>
public record CreateProductDto
{
    [Required]
    [StringLength(50)]
    public required string Sku { get; init; }

    [Required]
    [StringLength(255)]
    public required string Name { get; init; }

    [StringLength(2000)]
    public string? Description { get; init; }

    public Guid? CategoryId { get; init; }

    public Guid? VendorId { get; init; }

    [Range(0, 999999.99)]
    public decimal BasePrice { get; init; }

    [Range(0, 999999.99)]
    public decimal Cost { get; init; }

    public List<CreateVariantDto>? Variants { get; init; }
}

/// <summary>
/// DTO for updating an existing product.
/// </summary>
public record UpdateProductDto
{
    [Required]
    [StringLength(50)]
    public required string Sku { get; init; }

    [Required]
    [StringLength(255)]
    public required string Name { get; init; }

    [StringLength(2000)]
    public string? Description { get; init; }

    public Guid? CategoryId { get; init; }

    public Guid? VendorId { get; init; }

    [Range(0, 999999.99)]
    public decimal BasePrice { get; init; }

    [Range(0, 999999.99)]
    public decimal Cost { get; init; }

    public string? Status { get; init; }
}

/// <summary>
/// DTO for creating a product variant.
/// </summary>
public record CreateVariantDto
{
    [Required]
    [StringLength(50)]
    public required string Sku { get; init; }

    [StringLength(50)]
    public string? Barcode { get; init; }

    public Dictionary<string, string> Options { get; init; } = new();

    [Range(0, 999999.99)]
    public decimal Price { get; init; }

    [Range(0, 999999.99)]
    public decimal? CompareAtPrice { get; init; }

    [Range(0, 999999.99)]
    public decimal Cost { get; init; }
}

/// <summary>
/// DTO for updating a product variant.
/// </summary>
public record UpdateVariantDto
{
    [Required]
    [StringLength(50)]
    public required string Sku { get; init; }

    [StringLength(50)]
    public string? Barcode { get; init; }

    public Dictionary<string, string>? Options { get; init; }

    [Range(0, 999999.99)]
    public decimal? Price { get; init; }

    [Range(0, 999999.99)]
    public decimal? CompareAtPrice { get; init; }

    [Range(0, 999999.99)]
    public decimal? Cost { get; init; }

    public bool? IsActive { get; init; }
}

/// <summary>
/// Product filter DTO.
/// </summary>
public record ProductFilterDto
{
    public string? SearchTerm { get; init; }
    public Guid? CategoryId { get; init; }
    public Guid? VendorId { get; init; }
    public string? Status { get; init; }

    [Range(1, int.MaxValue)]
    public int Page { get; init; } = 1;

    [Range(1, 100)]
    public int PageSize { get; init; } = 20;
}

/// <summary>
/// Paginated result wrapper.
/// </summary>
public record PagedResult<T>
{
    public IReadOnlyList<T> Items { get; init; }
    public int TotalCount { get; init; }
    public int Page { get; init; }
    public int PageSize { get; init; }
    public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
    public bool HasNextPage => Page < TotalPages;
    public bool HasPreviousPage => Page > 1;

    public PagedResult(IReadOnlyList<T> items, int totalCount, int page, int pageSize)
    {
        Items = items;
        TotalCount = totalCount;
        Page = page;
        PageSize = pageSize;
    }
}

8. Validator Template

// File: src/POS.Application/Validators/CreateProductValidator.cs

using FluentValidation;
using POS.Core.DTOs;
using POS.Core.Interfaces.Repositories;

namespace POS.Application.Validators;

/// <summary>
/// Validator for CreateProductDto.
/// </summary>
public class CreateProductValidator : AbstractValidator<CreateProductDto>
{
    private readonly IProductRepository _productRepository;
    private readonly ICategoryRepository _categoryRepository;

    public CreateProductValidator(
        IProductRepository productRepository,
        ICategoryRepository categoryRepository)
    {
        _productRepository = productRepository;
        _categoryRepository = categoryRepository;

        RuleFor(x => x.Sku)
            .NotEmpty()
                .WithMessage("SKU is required.")
            .MaximumLength(50)
                .WithMessage("SKU cannot exceed 50 characters.")
            .Matches(@"^[A-Z0-9\-]+$")
                .WithMessage("SKU must contain only uppercase letters, numbers, and hyphens.")
            .MustAsync(BeUniqueSku)
                .WithMessage("SKU already exists.");

        RuleFor(x => x.Name)
            .NotEmpty()
                .WithMessage("Product name is required.")
            .MaximumLength(255)
                .WithMessage("Product name cannot exceed 255 characters.");

        RuleFor(x => x.Description)
            .MaximumLength(2000)
                .WithMessage("Description cannot exceed 2000 characters.");

        RuleFor(x => x.BasePrice)
            .GreaterThanOrEqualTo(0)
                .WithMessage("Base price must be zero or greater.");

        RuleFor(x => x.Cost)
            .GreaterThanOrEqualTo(0)
                .WithMessage("Cost must be zero or greater.")
            .LessThanOrEqualTo(x => x.BasePrice)
                .When(x => x.BasePrice > 0)
                .WithMessage("Cost should not exceed the base price.");

        RuleFor(x => x.CategoryId)
            .MustAsync(CategoryExists)
                .When(x => x.CategoryId.HasValue)
                .WithMessage("Category does not exist.");

        RuleForEach(x => x.Variants)
            .SetValidator(new CreateVariantValidator());
    }

    private async Task<bool> BeUniqueSku(string sku, CancellationToken cancellationToken)
    {
        return !await _productRepository.SkuExistsAsync(sku, null, cancellationToken);
    }

    private async Task<bool> CategoryExists(Guid? categoryId, CancellationToken cancellationToken)
    {
        if (!categoryId.HasValue) return true;
        return await _categoryRepository.ExistsAsync(categoryId.Value, cancellationToken);
    }
}

/// <summary>
/// Validator for CreateVariantDto.
/// </summary>
public class CreateVariantValidator : AbstractValidator<CreateVariantDto>
{
    public CreateVariantValidator()
    {
        RuleFor(x => x.Sku)
            .NotEmpty()
                .WithMessage("Variant SKU is required.")
            .MaximumLength(50)
                .WithMessage("Variant SKU cannot exceed 50 characters.");

        RuleFor(x => x.Barcode)
            .MaximumLength(50)
                .WithMessage("Barcode cannot exceed 50 characters.")
            .Matches(@"^[0-9]*$")
                .When(x => !string.IsNullOrEmpty(x.Barcode))
                .WithMessage("Barcode must contain only numbers.");

        RuleFor(x => x.Price)
            .GreaterThanOrEqualTo(0)
                .WithMessage("Price must be zero or greater.");

        RuleFor(x => x.CompareAtPrice)
            .GreaterThan(x => x.Price)
                .When(x => x.CompareAtPrice.HasValue)
                .WithMessage("Compare at price must be greater than regular price.");

        RuleFor(x => x.Cost)
            .GreaterThanOrEqualTo(0)
                .WithMessage("Cost must be zero or greater.");
    }
}

9. Event Handler Template

// File: src/POS.Application/EventHandlers/OrderCompletedEventHandler.cs

using System.Threading;
using System.Threading.Tasks;
using MediatR;
using Microsoft.Extensions.Logging;
using POS.Core.Events;
using POS.Core.Interfaces.Services;

namespace POS.Application.EventHandlers;

/// <summary>
/// Handles the OrderCompleted domain event.
/// </summary>
public class OrderCompletedEventHandler : INotificationHandler<OrderCompletedEvent>
{
    private readonly IInventoryService _inventoryService;
    private readonly ILoyaltyService _loyaltyService;
    private readonly IAnalyticsService _analyticsService;
    private readonly INotificationService _notificationService;
    private readonly ILogger<OrderCompletedEventHandler> _logger;

    public OrderCompletedEventHandler(
        IInventoryService inventoryService,
        ILoyaltyService loyaltyService,
        IAnalyticsService analyticsService,
        INotificationService notificationService,
        ILogger<OrderCompletedEventHandler> logger)
    {
        _inventoryService = inventoryService;
        _loyaltyService = loyaltyService;
        _analyticsService = analyticsService;
        _notificationService = notificationService;
        _logger = logger;
    }

    /// <summary>
    /// Handles the OrderCompleted event.
    /// </summary>
    public async Task Handle(
        OrderCompletedEvent notification,
        CancellationToken cancellationToken)
    {
        _logger.LogInformation(
            "Processing OrderCompleted event for Order {OrderId}",
            notification.OrderId);

        try
        {
            // Commit inventory reservations
            await _inventoryService.CommitReservationsAsync(
                notification.OrderId,
                notification.LineItems,
                cancellationToken);

            // Award loyalty points if customer attached
            if (notification.CustomerId.HasValue)
            {
                await _loyaltyService.AwardPointsAsync(
                    notification.CustomerId.Value,
                    notification.OrderId,
                    notification.Total,
                    cancellationToken);
            }

            // Record analytics
            await _analyticsService.RecordSaleAsync(
                notification.OrderId,
                notification.LocationId,
                notification.Total,
                notification.LineItems.Count,
                cancellationToken);

            // Send receipt notification if requested
            if (notification.SendReceipt)
            {
                await _notificationService.SendReceiptAsync(
                    notification.OrderId,
                    notification.CustomerEmail,
                    notification.ReceiptMethod,
                    cancellationToken);
            }

            _logger.LogInformation(
                "Successfully processed OrderCompleted event for Order {OrderId}",
                notification.OrderId);
        }
        catch (Exception ex)
        {
            _logger.LogError(
                ex,
                "Error processing OrderCompleted event for Order {OrderId}",
                notification.OrderId);

            // Re-throw to trigger retry logic
            throw;
        }
    }
}

10. Integration Test Template

// File: tests/POS.IntegrationTests/Controllers/ProductsControllerTests.cs

using System;
using System.Net;
using System.Net.Http.Json;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using POS.API;
using POS.Core.DTOs;
using POS.IntegrationTests.Fixtures;
using Xunit;

namespace POS.IntegrationTests.Controllers;

/// <summary>
/// Integration tests for ProductsController.
/// </summary>
[Collection("Database")]
public class ProductsControllerTests : IClassFixture<WebApplicationFactory<Program>>, IAsyncLifetime
{
    private readonly WebApplicationFactory<Program> _factory;
    private readonly HttpClient _client;
    private readonly DatabaseFixture _dbFixture;

    public ProductsControllerTests(
        WebApplicationFactory<Program> factory,
        DatabaseFixture dbFixture)
    {
        _factory = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                // Configure test database
                dbFixture.ConfigureServices(services);
            });
        });

        _client = _factory.CreateClient();
        _dbFixture = dbFixture;
    }

    public async Task InitializeAsync()
    {
        await _dbFixture.ResetDatabaseAsync();
        await AuthenticateAsync();
    }

    public Task DisposeAsync() => Task.CompletedTask;

    private async Task AuthenticateAsync()
    {
        var loginDto = new { Email = "test@example.com", Password = "Test123!" };
        var response = await _client.PostAsJsonAsync("/api/v1/auth/login", loginDto);
        var result = await response.Content.ReadFromJsonAsync<LoginResult>();
        _client.DefaultRequestHeaders.Authorization =
            new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", result?.Token);
    }

    [Fact]
    public async Task GetAll_ReturnsProducts()
    {
        // Arrange
        await SeedProductsAsync();

        // Act
        var response = await _client.GetAsync("/api/v1/products");

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.OK);

        var result = await response.Content.ReadFromJsonAsync<PagedResult<ProductDto>>();
        result.Should().NotBeNull();
        result!.Items.Should().NotBeEmpty();
        result.TotalCount.Should().BeGreaterThan(0);
    }

    [Fact]
    public async Task GetById_ExistingProduct_ReturnsProduct()
    {
        // Arrange
        var productId = await CreateTestProductAsync();

        // Act
        var response = await _client.GetAsync($"/api/v1/products/{productId}");

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.OK);

        var product = await response.Content.ReadFromJsonAsync<ProductDto>();
        product.Should().NotBeNull();
        product!.Id.Should().Be(productId);
    }

    [Fact]
    public async Task GetById_NonExistingProduct_ReturnsNotFound()
    {
        // Arrange
        var nonExistingId = Guid.NewGuid();

        // Act
        var response = await _client.GetAsync($"/api/v1/products/{nonExistingId}");

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.NotFound);
    }

    [Fact]
    public async Task Create_ValidProduct_ReturnsCreated()
    {
        // Arrange
        var createDto = new CreateProductDto
        {
            Sku = "TEST-001",
            Name = "Test Product",
            Description = "A test product",
            BasePrice = 29.99m,
            Cost = 12.50m
        };

        // Act
        var response = await _client.PostAsJsonAsync("/api/v1/products", createDto);

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.Created);

        var product = await response.Content.ReadFromJsonAsync<ProductDto>();
        product.Should().NotBeNull();
        product!.Sku.Should().Be("TEST-001");
        product.Name.Should().Be("Test Product");

        // Verify location header
        response.Headers.Location.Should().NotBeNull();
    }

    [Fact]
    public async Task Create_DuplicateSku_ReturnsConflict()
    {
        // Arrange
        var createDto = new CreateProductDto
        {
            Sku = "DUPLICATE-SKU",
            Name = "First Product",
            BasePrice = 29.99m,
            Cost = 12.50m
        };

        await _client.PostAsJsonAsync("/api/v1/products", createDto);

        var duplicateDto = new CreateProductDto
        {
            Sku = "DUPLICATE-SKU",
            Name = "Second Product",
            BasePrice = 39.99m,
            Cost = 15.00m
        };

        // Act
        var response = await _client.PostAsJsonAsync("/api/v1/products", duplicateDto);

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.Conflict);
    }

    [Fact]
    public async Task Create_InvalidData_ReturnsBadRequest()
    {
        // Arrange
        var createDto = new CreateProductDto
        {
            Sku = "", // Invalid - empty
            Name = "", // Invalid - empty
            BasePrice = -10m, // Invalid - negative
            Cost = 12.50m
        };

        // Act
        var response = await _client.PostAsJsonAsync("/api/v1/products", createDto);

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
    }

    [Fact]
    public async Task Update_ValidData_ReturnsUpdatedProduct()
    {
        // Arrange
        var productId = await CreateTestProductAsync();

        var updateDto = new UpdateProductDto
        {
            Sku = "UPDATED-SKU",
            Name = "Updated Product Name",
            BasePrice = 39.99m,
            Cost = 15.00m
        };

        // Act
        var response = await _client.PutAsJsonAsync(
            $"/api/v1/products/{productId}",
            updateDto);

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.OK);

        var product = await response.Content.ReadFromJsonAsync<ProductDto>();
        product!.Name.Should().Be("Updated Product Name");
        product.BasePrice.Should().Be(39.99m);
    }

    [Fact]
    public async Task Delete_ExistingProduct_ReturnsNoContent()
    {
        // Arrange
        var productId = await CreateTestProductAsync();

        // Act
        var response = await _client.DeleteAsync($"/api/v1/products/{productId}");

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.NoContent);

        // Verify product is soft-deleted
        var getResponse = await _client.GetAsync($"/api/v1/products/{productId}");
        var product = await getResponse.Content.ReadFromJsonAsync<ProductDto>();
        product!.Status.Should().Be("Archived");
    }

    private async Task SeedProductsAsync()
    {
        for (int i = 1; i <= 5; i++)
        {
            var dto = new CreateProductDto
            {
                Sku = $"SEED-{i:D3}",
                Name = $"Seeded Product {i}",
                BasePrice = 29.99m,
                Cost = 12.50m
            };
            await _client.PostAsJsonAsync("/api/v1/products", dto);
        }
    }

    private async Task<Guid> CreateTestProductAsync()
    {
        var dto = new CreateProductDto
        {
            Sku = $"TEST-{Guid.NewGuid():N}".Substring(0, 20),
            Name = "Test Product",
            BasePrice = 29.99m,
            Cost = 12.50m
        };

        var response = await _client.PostAsJsonAsync("/api/v1/products", dto);
        var product = await response.Content.ReadFromJsonAsync<ProductDto>();
        return product!.Id;
    }
}

record LoginResult(string Token);

11. Unit Test Template

// File: tests/POS.UnitTests/Services/ProductServiceTests.cs

using System;
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using FluentAssertions;
using FluentValidation;
using FluentValidation.Results;
using Microsoft.Extensions.Logging;
using Moq;
using POS.Application.Services;
using POS.Core.DTOs;
using POS.Core.Entities;
using POS.Core.Exceptions;
using POS.Core.Interfaces.Repositories;
using POS.Core.Interfaces.Services;
using Xunit;

namespace POS.UnitTests.Services;

/// <summary>
/// Unit tests for ProductService.
/// </summary>
public class ProductServiceTests
{
    private readonly Mock<IProductRepository> _productRepositoryMock;
    private readonly Mock<IUnitOfWork> _unitOfWorkMock;
    private readonly Mock<IMapper> _mapperMock;
    private readonly Mock<IValidator<CreateProductDto>> _createValidatorMock;
    private readonly Mock<IValidator<UpdateProductDto>> _updateValidatorMock;
    private readonly Mock<ILogger<ProductService>> _loggerMock;
    private readonly Mock<IDomainEventDispatcher> _eventDispatcherMock;
    private readonly ProductService _sut;

    public ProductServiceTests()
    {
        _productRepositoryMock = new Mock<IProductRepository>();
        _unitOfWorkMock = new Mock<IUnitOfWork>();
        _mapperMock = new Mock<IMapper>();
        _createValidatorMock = new Mock<IValidator<CreateProductDto>>();
        _updateValidatorMock = new Mock<IValidator<UpdateProductDto>>();
        _loggerMock = new Mock<ILogger<ProductService>>();
        _eventDispatcherMock = new Mock<IDomainEventDispatcher>();

        _sut = new ProductService(
            _productRepositoryMock.Object,
            _unitOfWorkMock.Object,
            _mapperMock.Object,
            _createValidatorMock.Object,
            _updateValidatorMock.Object,
            _loggerMock.Object,
            _eventDispatcherMock.Object);
    }

    [Fact]
    public async Task GetByIdAsync_ExistingProduct_ReturnsProductDto()
    {
        // Arrange
        var productId = Guid.NewGuid();
        var product = new Product
        {
            Id = productId,
            Sku = "TEST-001",
            Name = "Test Product"
        };
        var productDto = new ProductDto
        {
            Id = productId,
            Sku = "TEST-001",
            Name = "Test Product"
        };

        _productRepositoryMock
            .Setup(x => x.GetByIdAsync(productId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(product);

        _mapperMock
            .Setup(x => x.Map<ProductDto>(product))
            .Returns(productDto);

        // Act
        var result = await _sut.GetByIdAsync(productId);

        // Assert
        result.Should().NotBeNull();
        result!.Id.Should().Be(productId);
        result.Sku.Should().Be("TEST-001");
    }

    [Fact]
    public async Task GetByIdAsync_NonExistingProduct_ReturnsNull()
    {
        // Arrange
        var productId = Guid.NewGuid();

        _productRepositoryMock
            .Setup(x => x.GetByIdAsync(productId, It.IsAny<CancellationToken>()))
            .ReturnsAsync((Product?)null);

        // Act
        var result = await _sut.GetByIdAsync(productId);

        // Assert
        result.Should().BeNull();
    }

    [Fact]
    public async Task CreateAsync_ValidDto_CreatesAndReturnsProduct()
    {
        // Arrange
        var createDto = new CreateProductDto
        {
            Sku = "NEW-001",
            Name = "New Product",
            BasePrice = 29.99m,
            Cost = 12.50m
        };

        var product = new Product
        {
            Id = Guid.NewGuid(),
            Sku = "NEW-001",
            Name = "New Product"
        };

        var productDto = new ProductDto
        {
            Id = product.Id,
            Sku = "NEW-001",
            Name = "New Product"
        };

        _createValidatorMock
            .Setup(x => x.ValidateAsync(createDto, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new ValidationResult());

        _productRepositoryMock
            .Setup(x => x.SkuExistsAsync("NEW-001", null, It.IsAny<CancellationToken>()))
            .ReturnsAsync(false);

        _mapperMock
            .Setup(x => x.Map<Product>(createDto))
            .Returns(product);

        _mapperMock
            .Setup(x => x.Map<ProductDto>(product))
            .Returns(productDto);

        // Act
        var result = await _sut.CreateAsync(createDto);

        // Assert
        result.Should().NotBeNull();
        result.Sku.Should().Be("NEW-001");

        _productRepositoryMock.Verify(
            x => x.AddAsync(It.IsAny<Product>(), It.IsAny<CancellationToken>()),
            Times.Once);

        _unitOfWorkMock.Verify(
            x => x.SaveChangesAsync(It.IsAny<CancellationToken>()),
            Times.Once);

        _eventDispatcherMock.Verify(
            x => x.DispatchAsync(It.IsAny<ProductCreatedEvent>()),
            Times.Once);
    }

    [Fact]
    public async Task CreateAsync_DuplicateSku_ThrowsBusinessException()
    {
        // Arrange
        var createDto = new CreateProductDto
        {
            Sku = "EXISTING-SKU",
            Name = "New Product",
            BasePrice = 29.99m,
            Cost = 12.50m
        };

        _createValidatorMock
            .Setup(x => x.ValidateAsync(createDto, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new ValidationResult());

        _productRepositoryMock
            .Setup(x => x.SkuExistsAsync("EXISTING-SKU", null, It.IsAny<CancellationToken>()))
            .ReturnsAsync(true);

        // Act
        var act = () => _sut.CreateAsync(createDto);

        // Assert
        await act.Should().ThrowAsync<BusinessException>()
            .WithMessage("*EXISTING-SKU*already exists*");
    }

    [Fact]
    public async Task CreateAsync_InvalidDto_ThrowsValidationException()
    {
        // Arrange
        var createDto = new CreateProductDto
        {
            Sku = "",
            Name = "",
            BasePrice = -10m,
            Cost = 12.50m
        };

        var validationResult = new ValidationResult(new[]
        {
            new ValidationFailure("Sku", "SKU is required."),
            new ValidationFailure("Name", "Name is required.")
        });

        _createValidatorMock
            .Setup(x => x.ValidateAsync(createDto, It.IsAny<CancellationToken>()))
            .ReturnsAsync(validationResult);

        // Act
        var act = () => _sut.CreateAsync(createDto);

        // Assert
        await act.Should().ThrowAsync<ValidationException>();
    }

    [Fact]
    public async Task UpdateAsync_ExistingProduct_UpdatesAndReturnsProduct()
    {
        // Arrange
        var productId = Guid.NewGuid();
        var updateDto = new UpdateProductDto
        {
            Sku = "UPDATED-SKU",
            Name = "Updated Name",
            BasePrice = 39.99m,
            Cost = 15.00m
        };

        var existingProduct = new Product
        {
            Id = productId,
            Sku = "OLD-SKU",
            Name = "Old Name"
        };

        var updatedProductDto = new ProductDto
        {
            Id = productId,
            Sku = "UPDATED-SKU",
            Name = "Updated Name"
        };

        _updateValidatorMock
            .Setup(x => x.ValidateAsync(updateDto, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new ValidationResult());

        _productRepositoryMock
            .Setup(x => x.GetByIdAsync(productId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(existingProduct);

        _productRepositoryMock
            .Setup(x => x.SkuExistsAsync("UPDATED-SKU", productId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(false);

        _mapperMock
            .Setup(x => x.Map<ProductDto>(existingProduct))
            .Returns(updatedProductDto);

        // Act
        var result = await _sut.UpdateAsync(productId, updateDto);

        // Assert
        result.Should().NotBeNull();
        result.Sku.Should().Be("UPDATED-SKU");

        _productRepositoryMock.Verify(
            x => x.Update(It.IsAny<Product>()),
            Times.Once);

        _unitOfWorkMock.Verify(
            x => x.SaveChangesAsync(It.IsAny<CancellationToken>()),
            Times.Once);
    }

    [Fact]
    public async Task UpdateAsync_NonExistingProduct_ThrowsNotFoundException()
    {
        // Arrange
        var productId = Guid.NewGuid();
        var updateDto = new UpdateProductDto
        {
            Sku = "UPDATED-SKU",
            Name = "Updated Name",
            BasePrice = 39.99m,
            Cost = 15.00m
        };

        _updateValidatorMock
            .Setup(x => x.ValidateAsync(updateDto, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new ValidationResult());

        _productRepositoryMock
            .Setup(x => x.GetByIdAsync(productId, It.IsAny<CancellationToken>()))
            .ReturnsAsync((Product?)null);

        // Act
        var act = () => _sut.UpdateAsync(productId, updateDto);

        // Assert
        await act.Should().ThrowAsync<NotFoundException>()
            .WithMessage($"*{productId}*not found*");
    }

    [Fact]
    public async Task DeleteAsync_ExistingProduct_SoftDeletesProduct()
    {
        // Arrange
        var productId = Guid.NewGuid();
        var product = new Product
        {
            Id = productId,
            Sku = "TO-DELETE",
            Name = "Product to Delete",
            Status = ProductStatus.Active
        };

        _productRepositoryMock
            .Setup(x => x.GetByIdAsync(productId, It.IsAny<CancellationToken>()))
            .ReturnsAsync(product);

        // Act
        await _sut.DeleteAsync(productId);

        // Assert
        product.Status.Should().Be(ProductStatus.Archived);

        _productRepositoryMock.Verify(
            x => x.Update(product),
            Times.Once);

        _unitOfWorkMock.Verify(
            x => x.SaveChangesAsync(It.IsAny<CancellationToken>()),
            Times.Once);
    }
}

12. Domain Event Template

// File: src/POS.Core/Events/OrderCompletedEvent.cs

using System;
using System.Collections.Generic;
using MediatR;

namespace POS.Core.Events;

/// <summary>
/// Domain event raised when an order is completed.
/// </summary>
public record OrderCompletedEvent : INotification
{
    /// <summary>
    /// Gets the event ID.
    /// </summary>
    public Guid EventId { get; init; } = Guid.NewGuid();

    /// <summary>
    /// Gets the timestamp when the event occurred.
    /// </summary>
    public DateTime Timestamp { get; init; } = DateTime.UtcNow;

    /// <summary>
    /// Gets the order ID.
    /// </summary>
    public required Guid OrderId { get; init; }

    /// <summary>
    /// Gets the order number.
    /// </summary>
    public required string OrderNumber { get; init; }

    /// <summary>
    /// Gets the receipt number.
    /// </summary>
    public required string ReceiptNumber { get; init; }

    /// <summary>
    /// Gets the location ID.
    /// </summary>
    public required Guid LocationId { get; init; }

    /// <summary>
    /// Gets the register ID.
    /// </summary>
    public Guid? RegisterId { get; init; }

    /// <summary>
    /// Gets the customer ID.
    /// </summary>
    public Guid? CustomerId { get; init; }

    /// <summary>
    /// Gets the customer email.
    /// </summary>
    public string? CustomerEmail { get; init; }

    /// <summary>
    /// Gets the line items.
    /// </summary>
    public required IReadOnlyList<OrderLineItemEvent> LineItems { get; init; }

    /// <summary>
    /// Gets the payment details.
    /// </summary>
    public required IReadOnlyList<PaymentEvent> Payments { get; init; }

    /// <summary>
    /// Gets the subtotal.
    /// </summary>
    public decimal Subtotal { get; init; }

    /// <summary>
    /// Gets the discount total.
    /// </summary>
    public decimal DiscountTotal { get; init; }

    /// <summary>
    /// Gets the tax total.
    /// </summary>
    public decimal TaxTotal { get; init; }

    /// <summary>
    /// Gets the order total.
    /// </summary>
    public required decimal Total { get; init; }

    /// <summary>
    /// Gets the loyalty points earned.
    /// </summary>
    public int LoyaltyPointsEarned { get; init; }

    /// <summary>
    /// Gets whether to send receipt.
    /// </summary>
    public bool SendReceipt { get; init; }

    /// <summary>
    /// Gets the receipt delivery method.
    /// </summary>
    public string? ReceiptMethod { get; init; }

    /// <summary>
    /// Gets the user who completed the order.
    /// </summary>
    public required Guid CompletedBy { get; init; }

    /// <summary>
    /// Gets the shift ID.
    /// </summary>
    public Guid? ShiftId { get; init; }
}

/// <summary>
/// Order line item event data.
/// </summary>
public record OrderLineItemEvent
{
    public required Guid LineItemId { get; init; }
    public required Guid VariantId { get; init; }
    public required string Sku { get; init; }
    public required string Name { get; init; }
    public required int Quantity { get; init; }
    public required decimal UnitPrice { get; init; }
    public decimal DiscountAmount { get; init; }
    public decimal TaxAmount { get; init; }
    public required decimal LineTotal { get; init; }
    public decimal Cost { get; init; }
}

/// <summary>
/// Payment event data.
/// </summary>
public record PaymentEvent
{
    public required Guid PaymentId { get; init; }
    public required string Method { get; init; }
    public required decimal Amount { get; init; }
    public string? AuthorizationCode { get; init; }
    public string? LastFour { get; init; }
}

Usage Notes

  1. Entity Template: Inherit from BaseEntity and implement tenant/audit interfaces as needed.

  2. Repository Interface: Define only operations specific to the entity; generic CRUD is in IRepository<T>.

  3. Repository Implementation: Use Entity Framework Core’s DbSet and LINQ for queries.

  4. Service Interface: Keep it focused on business operations, not CRUD.

  5. Service Implementation: Handle validation, business rules, and coordinate between repositories.

  6. Controller Template: Use [FromBody] for complex objects, [FromQuery] for filters.

  7. DTOs: Use records for immutability; separate Create/Update/Response DTOs.

  8. Validators: Use FluentValidation with async rules for database checks.

  9. Event Handlers: Handle one event type per handler; keep handlers focused.

  10. Integration Tests: Use WebApplicationFactory and test against real database.

  11. Unit Tests: Use Moq for dependencies; test business logic in isolation.

  12. Domain Events: Use MediatR INotification; include all relevant data in the event.


These templates provide the foundation for consistent, maintainable code across the POS Platform.