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

Chapter 15: API Design

RESTful API Architecture for Multi-Tenant POS

This chapter provides the complete API specification for the POS Platform, including all endpoints, request/response formats, and real-time communication patterns.


15.1 Base URL Structure

Tenant-Aware URL Pattern

https://{tenant}.pos-platform.com/api/v1/{resource}

Examples:

https://nexus.pos-platform.com/api/v1/items
https://acme-retail.pos-platform.com/api/v1/sales
https://fashion-outlet.pos-platform.com/api/v1/inventory

Alternative: Header-Based Tenancy

For single-domain deployments:

https://api.pos-platform.com/api/v1/{resource}
X-Tenant-Id: nexus

15.2 API Versioning Strategy

/api/v1/...  ← Current (stable)
/api/v2/...  ← Future (when breaking changes needed)

Version Header:

X-API-Version: 2025-01-15

15.3 Complete Endpoint Reference

15.3.1 Catalog Domain

Items (Products)

# List all items (paginated)
GET /api/v1/items?page=1&pageSize=50&category=apparel&active=true

# Get single item
GET /api/v1/items/{id}

# Get item by SKU
GET /api/v1/items/by-sku/{sku}

# Get item by barcode
GET /api/v1/items/by-barcode/{barcode}

# Create item
POST /api/v1/items

# Update item
PUT /api/v1/items/{id}

# Partial update
PATCH /api/v1/items/{id}

# Delete (soft delete)
DELETE /api/v1/items/{id}

# Bulk operations
POST /api/v1/items/bulk-create
PUT /api/v1/items/bulk-update
POST /api/v1/items/bulk-import

Create Item Request:

{
  "sku": "NXJ-1001-BLK-M",
  "barcode": "0123456789012",
  "name": "Classic Oxford Shirt",
  "description": "Premium cotton oxford shirt",
  "categoryId": "cat_apparel_shirts",
  "vendorId": "vendor_acme",
  "cost": 24.99,
  "price": 59.99,
  "taxable": true,
  "trackInventory": true,
  "reorderPoint": 10,
  "reorderQuantity": 50,
  "attributes": {
    "color": "Black",
    "size": "Medium",
    "material": "100% Cotton"
  }
}

Item Response:

{
  "id": "item_01HQWXYZ123",
  "tenantId": "tenant_nexus",
  "sku": "NXJ-1001-BLK-M",
  "barcode": "0123456789012",
  "name": "Classic Oxford Shirt",
  "description": "Premium cotton oxford shirt",
  "categoryId": "cat_apparel_shirts",
  "categoryName": "Shirts",
  "vendorId": "vendor_acme",
  "vendorName": "ACME Apparel",
  "cost": 24.99,
  "price": 59.99,
  "taxable": true,
  "trackInventory": true,
  "isActive": true,
  "reorderPoint": 10,
  "reorderQuantity": 50,
  "totalQuantityOnHand": 145,
  "attributes": {
    "color": "Black",
    "size": "Medium",
    "material": "100% Cotton"
  },
  "inventoryByLocation": [
    { "locationId": "loc_hq", "locationName": "Warehouse", "quantity": 100 },
    { "locationId": "loc_gm", "locationName": "Greenbrier", "quantity": 25 },
    { "locationId": "loc_lm", "locationName": "Lynnhaven", "quantity": 20 }
  ],
  "createdAt": "2025-01-15T10:30:00Z",
  "updatedAt": "2025-01-20T14:22:00Z",
  "_links": {
    "self": "/api/v1/items/item_01HQWXYZ123",
    "category": "/api/v1/categories/cat_apparel_shirts",
    "vendor": "/api/v1/vendors/vendor_acme",
    "inventory": "/api/v1/inventory?itemId=item_01HQWXYZ123"
  }
}

Categories

GET /api/v1/categories                    # List all (hierarchical)
GET /api/v1/categories/{id}               # Get single
GET /api/v1/categories/{id}/items         # Items in category
POST /api/v1/categories                   # Create
PUT /api/v1/categories/{id}               # Update
DELETE /api/v1/categories/{id}            # Delete

Vendors

GET /api/v1/vendors                       # List all
GET /api/v1/vendors/{id}                  # Get single
GET /api/v1/vendors/{id}/items            # Items from vendor
POST /api/v1/vendors                      # Create
PUT /api/v1/vendors/{id}                  # Update
DELETE /api/v1/vendors/{id}               # Delete

15.3.2 Sales Domain

# List sales (paginated, filtered)
GET /api/v1/sales?page=1&pageSize=50&locationId=loc_gm&from=2025-01-01&to=2025-01-31

# Get single sale
GET /api/v1/sales/{id}

# Create sale (complete transaction)
POST /api/v1/sales

# Process return
POST /api/v1/sales/{id}/return

# Void transaction
POST /api/v1/sales/{id}/void

# Get receipt
GET /api/v1/sales/{id}/receipt

# Reprint receipt
POST /api/v1/sales/{id}/receipt/print

Create Sale Request:

{
  "locationId": "loc_gm",
  "registerId": "reg_gm_01",
  "employeeId": "emp_john",
  "customerId": "cust_jane",
  "lineItems": [
    {
      "itemId": "item_01HQWXYZ123",
      "quantity": 2,
      "unitPrice": 59.99,
      "discountAmount": 0
    },
    {
      "itemId": "item_02ABCDEF456",
      "quantity": 1,
      "unitPrice": 29.99,
      "discountAmount": 5.00
    }
  ],
  "discounts": [
    {
      "type": "percentage",
      "value": 10,
      "reason": "Loyalty Member Discount"
    }
  ],
  "payments": [
    {
      "method": "credit_card",
      "amount": 135.42,
      "reference": "ch_3MqL0Z2eZvKYlo2C",
      "cardLast4": "4242",
      "cardBrand": "visa"
    }
  ],
  "taxAmount": 10.45,
  "notes": "Customer requested gift receipt"
}

Sale Response:

{
  "id": "sale_01HQWXYZ789",
  "receiptNumber": "GM-20250115-0042",
  "tenantId": "tenant_nexus",
  "locationId": "loc_gm",
  "locationName": "Greenbrier Mall",
  "registerId": "reg_gm_01",
  "employeeId": "emp_john",
  "employeeName": "John Smith",
  "customerId": "cust_jane",
  "customerName": "Jane Doe",
  "status": "completed",
  "subtotal": 144.97,
  "discountTotal": 19.50,
  "taxTotal": 10.45,
  "grandTotal": 135.92,
  "lineItems": [
    {
      "id": "li_001",
      "itemId": "item_01HQWXYZ123",
      "sku": "NXJ-1001-BLK-M",
      "name": "Classic Oxford Shirt",
      "quantity": 2,
      "unitPrice": 59.99,
      "extendedPrice": 119.98,
      "discountAmount": 0,
      "netPrice": 119.98
    }
  ],
  "payments": [
    {
      "id": "pmt_001",
      "method": "credit_card",
      "amount": 135.92,
      "status": "captured",
      "cardLast4": "4242",
      "cardBrand": "visa"
    }
  ],
  "createdAt": "2025-01-15T14:32:00Z",
  "_links": {
    "self": "/api/v1/sales/sale_01HQWXYZ789",
    "receipt": "/api/v1/sales/sale_01HQWXYZ789/receipt",
    "customer": "/api/v1/customers/cust_jane"
  }
}

Return Request:

{
  "lineItems": [
    {
      "originalLineItemId": "li_001",
      "quantity": 1,
      "reason": "wrong_size"
    }
  ],
  "refundMethod": "original_payment",
  "employeeId": "emp_john"
}

15.3.3 Inventory Domain

# Get inventory levels
GET /api/v1/inventory?locationId=loc_gm&itemId=item_01HQWXYZ123

# Get inventory for all locations
GET /api/v1/inventory/by-item/{itemId}

# Adjust inventory
POST /api/v1/inventory/adjust

# Transfer between locations
POST /api/v1/inventory/transfer

# Start inventory count
POST /api/v1/inventory/count

# Submit count results
PUT /api/v1/inventory/count/{countId}

# Finalize count
POST /api/v1/inventory/count/{countId}/finalize

# Get adjustment history
GET /api/v1/inventory/adjustments?itemId={itemId}&from={date}&to={date}

Adjust Inventory Request:

{
  "locationId": "loc_gm",
  "adjustments": [
    {
      "itemId": "item_01HQWXYZ123",
      "quantityChange": -2,
      "reason": "damaged",
      "notes": "Water damage from roof leak"
    }
  ],
  "employeeId": "emp_manager"
}

Transfer Request:

{
  "fromLocationId": "loc_hq",
  "toLocationId": "loc_gm",
  "items": [
    {
      "itemId": "item_01HQWXYZ123",
      "quantity": 20
    },
    {
      "itemId": "item_02ABCDEF456",
      "quantity": 15
    }
  ],
  "notes": "Weekly replenishment",
  "employeeId": "emp_warehouse"
}

Transfer Response:

{
  "id": "transfer_01HQWXYZ",
  "transferNumber": "TRF-20250115-001",
  "status": "pending",
  "fromLocationId": "loc_hq",
  "fromLocationName": "Warehouse",
  "toLocationId": "loc_gm",
  "toLocationName": "Greenbrier Mall",
  "items": [
    {
      "itemId": "item_01HQWXYZ123",
      "sku": "NXJ-1001-BLK-M",
      "name": "Classic Oxford Shirt",
      "quantity": 20
    }
  ],
  "createdAt": "2025-01-15T09:00:00Z",
  "createdBy": "emp_warehouse"
}

15.3.4 Customers Domain

# Search customers
GET /api/v1/customers/search?q=jane&email=jane@example.com

# List customers (paginated)
GET /api/v1/customers?page=1&pageSize=50

# Get single customer
GET /api/v1/customers/{id}

# Create customer
POST /api/v1/customers

# Update customer
PUT /api/v1/customers/{id}

# Get customer purchase history
GET /api/v1/customers/{id}/purchases

# Get customer loyalty points
GET /api/v1/customers/{id}/loyalty

Customer Response:

{
  "id": "cust_jane",
  "firstName": "Jane",
  "lastName": "Doe",
  "email": "jane.doe@example.com",
  "phone": "+1-555-123-4567",
  "loyaltyTier": "gold",
  "loyaltyPoints": 2450,
  "totalPurchases": 45,
  "totalSpent": 3245.67,
  "lastVisit": "2025-01-15T14:32:00Z",
  "preferredLocationId": "loc_gm",
  "marketingOptIn": true,
  "createdAt": "2024-03-15T10:00:00Z"
}

15.3.5 Employees Domain

# List employees
GET /api/v1/employees?locationId=loc_gm&active=true

# Get single employee
GET /api/v1/employees/{id}

# Clock in
POST /api/v1/employees/{id}/clock-in

# Clock out
POST /api/v1/employees/{id}/clock-out

# Get time entries
GET /api/v1/employees/{id}/time-entries?from=2025-01-01&to=2025-01-15

# Get sales performance
GET /api/v1/employees/{id}/performance?period=month

Clock-In Request:

{
  "locationId": "loc_gm",
  "pin": "1234",
  "registerId": "reg_gm_01"
}

Clock-In Response:

{
  "timeEntryId": "time_01HQWXYZ",
  "employeeId": "emp_john",
  "employeeName": "John Smith",
  "locationId": "loc_gm",
  "clockInTime": "2025-01-15T09:00:00Z",
  "status": "clocked_in"
}

15.3.6 Reports Domain

# Sales Summary
GET /api/v1/reports/sales-summary?from=2025-01-01&to=2025-01-31&locationId=loc_gm

# Inventory Value
GET /api/v1/reports/inventory-value?locationId=loc_gm

# Employee Performance
GET /api/v1/reports/employee-performance?from=2025-01-01&to=2025-01-31

# Category Sales
GET /api/v1/reports/category-sales?from=2025-01-01&to=2025-01-31

# Top Sellers
GET /api/v1/reports/top-sellers?limit=20&period=month

# Slow Movers
GET /api/v1/reports/slow-movers?daysWithoutSale=30

Sales Summary Response:

{
  "period": {
    "from": "2025-01-01T00:00:00Z",
    "to": "2025-01-31T23:59:59Z"
  },
  "summary": {
    "totalTransactions": 1250,
    "totalGrossSales": 89500.00,
    "totalDiscounts": 4250.00,
    "totalReturns": 1200.00,
    "totalNetSales": 84050.00,
    "totalTax": 6723.00,
    "averageTransactionValue": 67.24,
    "itemsSold": 3450
  },
  "byLocation": [
    {
      "locationId": "loc_gm",
      "locationName": "Greenbrier Mall",
      "transactions": 450,
      "netSales": 32500.00
    }
  ],
  "byPaymentMethod": [
    { "method": "credit_card", "amount": 65000.00, "count": 980 },
    { "method": "cash", "amount": 15000.00, "count": 220 },
    { "method": "gift_card", "amount": 4050.00, "count": 50 }
  ]
}

15.3.7 Promotions Domain (Learned from Retail Pro)

Competitive Insight: Retail Pro’s promotions engine is their crown jewel - offering minute-level scheduling, complex stacking rules, and customer-specific discounts. We implement similar capabilities.

# List active promotions
GET /api/v1/promotions?active=true&locationId=loc_gm

# Get single promotion
GET /api/v1/promotions/{id}

# Create promotion
POST /api/v1/promotions

# Update promotion
PUT /api/v1/promotions/{id}

# Delete promotion
DELETE /api/v1/promotions/{id}

# Check applicable promotions for cart
POST /api/v1/promotions/evaluate

# Get promotion usage history
GET /api/v1/promotions/{id}/usage?from=2025-01-01&to=2025-01-31

Create Promotion Request:

{
  "name": "Holiday Weekend Sale",
  "code": "HOLIDAY25",
  "description": "25% off all apparel",
  "type": "percentage",
  "value": 25,
  "appliesTo": "category",
  "targetIds": ["cat_apparel"],
  "conditions": {
    "minPurchaseAmount": 50.00,
    "minQuantity": 2,
    "customerTypes": ["gold", "platinum"],
    "excludedItems": ["item_clearance_001"],
    "excludedCategories": ["cat_gift_cards"]
  },
  "schedule": {
    "startAt": "2025-12-20T00:00:00Z",
    "endAt": "2025-12-26T23:59:59Z",
    "activeDays": ["friday", "saturday", "sunday"],
    "activeHours": {
      "start": "10:00",
      "end": "21:00"
    }
  },
  "limits": {
    "maxUsesTotal": 1000,
    "maxUsesPerCustomer": 3,
    "maxUsesPerDay": 500
  },
  "stacking": {
    "stackable": false,
    "priority": 10,
    "excludeWithCodes": ["CLEARANCE", "EMPLOYEE"]
  },
  "locationIds": ["loc_gm", "loc_hm", "loc_lm"]
}

Promotion Response:

{
  "id": "promo_holiday25",
  "tenantId": "tenant_nexus",
  "name": "Holiday Weekend Sale",
  "code": "HOLIDAY25",
  "description": "25% off all apparel",
  "type": "percentage",
  "value": 25,
  "appliesTo": "category",
  "targetIds": ["cat_apparel"],
  "conditions": {
    "minPurchaseAmount": 50.00,
    "minQuantity": 2,
    "customerTypes": ["gold", "platinum"],
    "excludedItems": ["item_clearance_001"],
    "excludedCategories": ["cat_gift_cards"]
  },
  "schedule": {
    "startAt": "2025-12-20T00:00:00Z",
    "endAt": "2025-12-26T23:59:59Z",
    "activeDays": ["friday", "saturday", "sunday"],
    "activeHours": { "start": "10:00", "end": "21:00" }
  },
  "limits": {
    "maxUsesTotal": 1000,
    "maxUsesPerCustomer": 3,
    "maxUsesPerDay": 500,
    "currentUsesTotal": 245,
    "currentUsesToday": 32
  },
  "stacking": {
    "stackable": false,
    "priority": 10,
    "excludeWithCodes": ["CLEARANCE", "EMPLOYEE"]
  },
  "locationIds": ["loc_gm", "loc_hm", "loc_lm"],
  "status": "active",
  "createdAt": "2025-12-01T10:00:00Z",
  "updatedAt": "2025-12-15T14:30:00Z"
}

Evaluate Promotions Request (at checkout):

{
  "locationId": "loc_gm",
  "customerId": "cust_jane",
  "lineItems": [
    { "itemId": "item_shirt_001", "categoryId": "cat_apparel", "quantity": 2, "unitPrice": 45.00 },
    { "itemId": "item_pants_002", "categoryId": "cat_apparel", "quantity": 1, "unitPrice": 65.00 }
  ],
  "subtotal": 155.00,
  "appliedCodes": ["HOLIDAY25"]
}

Evaluate Promotions Response:

{
  "applicablePromotions": [
    {
      "promotionId": "promo_holiday25",
      "code": "HOLIDAY25",
      "name": "Holiday Weekend Sale",
      "discountAmount": 38.75,
      "appliedToItems": ["item_shirt_001", "item_pants_002"],
      "message": "25% off apparel applied!"
    }
  ],
  "autoAppliedPromotions": [
    {
      "promotionId": "promo_loyalty_gold",
      "name": "Gold Member Bonus",
      "discountAmount": 5.00,
      "message": "Gold member discount applied"
    }
  ],
  "ineligiblePromotions": [
    {
      "promotionId": "promo_clearance",
      "code": "CLEARANCE",
      "reason": "Cannot stack with HOLIDAY25"
    }
  ],
  "totalDiscount": 43.75,
  "newSubtotal": 111.25
}

Promotion Types:

TypeDescriptionExample
percentagePercentage off25% off
fixed_amountFixed dollar off$10 off
buy_x_get_yBuy X get Y free/discountedBuy 2 get 1 free
bundleBundle pricing3 shirts for $99
thresholdSpend X save YSpend $100 save $20
bogoBuy one get oneBOGO 50% off

Applies To Options:

TargetDescription
allEntire transaction
categorySpecific categories
itemSpecific items
vendorItems from vendor
tagItems with tag

15.3.8 UPC Lookup Service (Learned from Lightspeed)

Competitive Insight: Lightspeed has 8M+ preloaded products. We integrate with UPC databases to provide similar auto-fill capability.

# Lookup barcode in external database
GET /api/v1/upc/lookup/{barcode}

# Search external database
GET /api/v1/upc/search?q=nike+running+shoes

# Import from UPC lookup to catalog
POST /api/v1/upc/import

UPC Lookup Response:

{
  "found": true,
  "source": "upcitemdb",
  "barcode": "0883419084587",
  "suggestedData": {
    "name": "Nike Air Max 270 Running Shoes",
    "brand": "Nike",
    "category": "Footwear > Athletic > Running",
    "description": "Men's running shoes with Air Max cushioning",
    "imageUrl": "https://cdn.upcitemdb.com/images/883419084587.jpg",
    "msrp": 150.00,
    "attributes": {
      "color": "Black/White",
      "size": "Various",
      "material": "Mesh/Synthetic"
    }
  },
  "alreadyInCatalog": false
}

Import from UPC Request:

{
  "barcode": "0883419084587",
  "overrides": {
    "sku": "NIKE-AM270-BLK",
    "price": 139.99,
    "cost": 75.00,
    "categoryId": "cat_footwear"
  },
  "createVariants": true,
  "sizes": ["8", "9", "10", "11", "12"]
}

15.4 Pagination Pattern

All list endpoints use cursor-based or offset pagination:

Request:

GET /api/v1/items?page=2&pageSize=50&sortBy=name&sortOrder=asc

Response Envelope:

{
  "data": [...],
  "pagination": {
    "page": 2,
    "pageSize": 50,
    "totalItems": 1250,
    "totalPages": 25,
    "hasNextPage": true,
    "hasPreviousPage": true
  },
  "_links": {
    "self": "/api/v1/items?page=2&pageSize=50",
    "first": "/api/v1/items?page=1&pageSize=50",
    "prev": "/api/v1/items?page=1&pageSize=50",
    "next": "/api/v1/items?page=3&pageSize=50",
    "last": "/api/v1/items?page=25&pageSize=50"
  }
}

15.5 Error Response Format

All errors follow RFC 7807 Problem Details:

{
  "type": "https://pos-platform.com/errors/validation-error",
  "title": "Validation Error",
  "status": 400,
  "detail": "One or more validation errors occurred.",
  "instance": "/api/v1/items",
  "traceId": "00-abc123-def456-01",
  "errors": {
    "sku": ["SKU is required", "SKU must be unique"],
    "price": ["Price must be greater than 0"]
  }
}

Common Error Types:

StatusTypeDescription
400validation-errorRequest validation failed
401authentication-requiredNo valid credentials
403permission-deniedInsufficient permissions
404resource-not-foundEntity does not exist
409conflictDuplicate or state conflict
422business-rule-violationDomain logic failure
500internal-errorServer error

15.6 SignalR Hub Events

Real-time events for POS clients:

Hub Endpoint

wss://{tenant}.pos-platform.com/hubs/pos

Event Catalog

// Server → Client Events
public interface IPosHubClient
{
    // Inventory changes
    Task InventoryUpdated(InventoryUpdateEvent data);

    // Price changes
    Task PriceUpdated(PriceUpdateEvent data);

    // Item changes
    Task ItemUpdated(ItemUpdateEvent data);
    Task ItemCreated(ItemCreateEvent data);
    Task ItemDeleted(string itemId);

    // Register events
    Task RegisterStatusChanged(RegisterStatusEvent data);

    // Shift events
    Task ShiftStarted(ShiftEvent data);
    Task ShiftEnded(ShiftEvent data);

    // Sync commands
    Task SyncRequired(SyncCommand data);
    Task CacheInvalidated(CacheInvalidateEvent data);
}

Event Payload Examples:

// InventoryUpdated
{
  "eventType": "InventoryUpdated",
  "timestamp": "2025-01-15T14:32:00Z",
  "data": {
    "itemId": "item_01HQWXYZ123",
    "locationId": "loc_gm",
    "previousQuantity": 25,
    "newQuantity": 23,
    "changeType": "sale"
  }
}

// SyncRequired
{
  "eventType": "SyncRequired",
  "timestamp": "2025-01-15T14:32:00Z",
  "data": {
    "scope": "items",
    "reason": "bulk_import",
    "affectedCount": 150
  }
}

15.7 Request/Response Headers

Required Request Headers

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json
Accept: application/json
X-Request-Id: uuid-for-tracing
X-Location-Id: loc_gm              # For POS operations
X-Register-Id: reg_gm_01           # For POS operations

Response Headers

X-Request-Id: uuid-for-tracing
X-Tenant-Id: tenant_nexus
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 995
X-RateLimit-Reset: 1705330800

15.8 Rate Limiting

Endpoint TypeLimitWindow
Standard API1000/hourPer tenant
Bulk Operations10/hourPer tenant
Reports100/hourPer tenant
Auth Endpoints20/minutePer IP

Response when rate limited:

{
  "type": "https://pos-platform.com/errors/rate-limit-exceeded",
  "title": "Rate Limit Exceeded",
  "status": 429,
  "detail": "You have exceeded the rate limit. Try again in 300 seconds.",
  "retryAfter": 300
}

15.9 API Controller Implementation

// File: src/POS.Api/Controllers/ItemsController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using POS.Core.Catalog;
using POS.Core.Common;

namespace POS.Api.Controllers;

[ApiController]
[Route("api/v1/items")]
[Authorize]
public class ItemsController : ControllerBase
{
    private readonly IItemService _itemService;
    private readonly ITenantContext _tenantContext;
    private readonly ILogger<ItemsController> _logger;

    public ItemsController(
        IItemService itemService,
        ITenantContext tenantContext,
        ILogger<ItemsController> logger)
    {
        _itemService = itemService;
        _tenantContext = tenantContext;
        _logger = logger;
    }

    [HttpGet]
    [Authorize(Policy = "catalog.items.read")]
    public async Task<ActionResult<PagedResult<ItemDto>>> GetItems(
        [FromQuery] ItemQueryParams query,
        CancellationToken ct)
    {
        var result = await _itemService.GetItemsAsync(query, ct);
        return Ok(result);
    }

    [HttpGet("{id}")]
    [Authorize(Policy = "catalog.items.read")]
    public async Task<ActionResult<ItemDto>> GetItem(string id, CancellationToken ct)
    {
        var item = await _itemService.GetByIdAsync(id, ct);
        if (item is null)
            return NotFound(ProblemFactory.NotFound("Item", id));

        return Ok(item);
    }

    [HttpGet("by-sku/{sku}")]
    [Authorize(Policy = "catalog.items.read")]
    public async Task<ActionResult<ItemDto>> GetItemBySku(string sku, CancellationToken ct)
    {
        var item = await _itemService.GetBySkuAsync(sku, ct);
        if (item is null)
            return NotFound(ProblemFactory.NotFound("Item", sku));

        return Ok(item);
    }

    [HttpGet("by-barcode/{barcode}")]
    [Authorize(Policy = "catalog.items.read")]
    public async Task<ActionResult<ItemDto>> GetItemByBarcode(
        string barcode,
        CancellationToken ct)
    {
        var item = await _itemService.GetByBarcodeAsync(barcode, ct);
        if (item is null)
            return NotFound(ProblemFactory.NotFound("Item", barcode));

        return Ok(item);
    }

    [HttpPost]
    [Authorize(Policy = "catalog.items.write")]
    public async Task<ActionResult<ItemDto>> CreateItem(
        [FromBody] CreateItemRequest request,
        CancellationToken ct)
    {
        var result = await _itemService.CreateAsync(request, ct);

        return result.Match<ActionResult<ItemDto>>(
            success => CreatedAtAction(
                nameof(GetItem),
                new { id = success.Id },
                success),
            error => BadRequest(ProblemFactory.FromError(error))
        );
    }

    [HttpPut("{id}")]
    [Authorize(Policy = "catalog.items.write")]
    public async Task<ActionResult<ItemDto>> UpdateItem(
        string id,
        [FromBody] UpdateItemRequest request,
        CancellationToken ct)
    {
        var result = await _itemService.UpdateAsync(id, request, ct);

        return result.Match<ActionResult<ItemDto>>(
            success => Ok(success),
            error => error.Code switch
            {
                "NOT_FOUND" => NotFound(ProblemFactory.NotFound("Item", id)),
                _ => BadRequest(ProblemFactory.FromError(error))
            }
        );
    }

    [HttpDelete("{id}")]
    [Authorize(Policy = "catalog.items.delete")]
    public async Task<IActionResult> DeleteItem(string id, CancellationToken ct)
    {
        var result = await _itemService.DeleteAsync(id, ct);

        return result.Match<IActionResult>(
            success => NoContent(),
            error => NotFound(ProblemFactory.NotFound("Item", id))
        );
    }

    [HttpPost("bulk-import")]
    [Authorize(Policy = "catalog.items.bulk")]
    [RequestSizeLimit(10_000_000)] // 10MB
    public async Task<ActionResult<BulkImportResult>> BulkImport(
        [FromBody] BulkImportRequest request,
        CancellationToken ct)
    {
        var result = await _itemService.BulkImportAsync(request, ct);
        return Ok(result);
    }
}

15.10 Query Parameters and Filtering

// File: src/POS.Core/Common/ItemQueryParams.cs
public record ItemQueryParams
{
    public int Page { get; init; } = 1;
    public int PageSize { get; init; } = 50;
    public string? Search { get; init; }
    public string? CategoryId { get; init; }
    public string? VendorId { get; init; }
    public bool? Active { get; init; }
    public bool? TrackInventory { get; init; }
    public decimal? MinPrice { get; init; }
    public decimal? MaxPrice { get; init; }
    public string SortBy { get; init; } = "name";
    public string SortOrder { get; init; } = "asc";
}

Summary

This chapter defined the complete REST API structure for the POS Platform:

  • Tenant-aware URL structure with subdomain routing
  • Six domain areas: Catalog, Sales, Inventory, Customers, Employees, Reports
  • Consistent patterns for pagination, errors, and HATEOAS links
  • Real-time SignalR events for inventory and price updates
  • Complete controller implementation with authorization policies

Next: Chapter 16 covers the service layer that implements this API.