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:
| Type | Description | Example |
|---|---|---|
percentage | Percentage off | 25% off |
fixed_amount | Fixed dollar off | $10 off |
buy_x_get_y | Buy X get Y free/discounted | Buy 2 get 1 free |
bundle | Bundle pricing | 3 shirts for $99 |
threshold | Spend X save Y | Spend $100 save $20 |
bogo | Buy one get one | BOGO 50% off |
Applies To Options:
| Target | Description |
|---|---|
all | Entire transaction |
category | Specific categories |
item | Specific items |
vendor | Items from vendor |
tag | Items 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:
| Status | Type | Description |
|---|---|---|
| 400 | validation-error | Request validation failed |
| 401 | authentication-required | No valid credentials |
| 403 | permission-denied | Insufficient permissions |
| 404 | resource-not-found | Entity does not exist |
| 409 | conflict | Duplicate or state conflict |
| 422 | business-rule-violation | Domain logic failure |
| 500 | internal-error | Server 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 Type | Limit | Window |
|---|---|---|
| Standard API | 1000/hour | Per tenant |
| Bulk Operations | 10/hour | Per tenant |
| Reports | 100/hour | Per tenant |
| Auth Endpoints | 20/minute | Per 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.