Guides & specifications
MMCA Business Specification Document
1. System Overview
MMCA is an e-commerce platform built with .NET 10.0 using DDD and Clean Architecture. The business logic is organized as modules (Catalog, Sales, Identity) that have been extracted into per-module service hosts behind a YARP Gateway. It enables customers to browse a product catalog, manage shopping carts, place orders, and process payments. Administrators manage the product catalog, inventory, and order fulfillment.
The system operates in the online retail / e-commerce domain, supporting the full purchase lifecycle from product browsing through payment and delivery.
Major Business Areas:
- Product Catalog Management: categories, products, and product variants with pricing
- Shopping & Ordering: cart management, checkout, order placement
- Payment Processing: Stripe-integrated checkout with webhook confirmation
- Inventory Management: stock tracking per product variant
- Customer Identity & Authentication: registration, login, JWT-based sessions
Technical Stack:
- .NET 10.0 (LangVersion: preview), Blazor Server + WebAssembly hybrid (InteractiveAuto), MudBlazor UI
- SQL Server (primary, via Aspire), SQLite, Cosmos DB (alternative, via strategy pattern)
- Stripe payment gateway, PBKDF2-HMAC-SHA512 password hashing, JWT authentication
- .NET Aspire orchestration (SQL Server container + Gateway :6001 + 3 service hosts + UI)
2. Core Business Entities
2.1 Category
Description: A classification grouping for products. Supports hierarchical (parent-child) structures for nested categorization (e.g., "Jewelry" > "Rings").
Key Properties:
| Property | Description |
|---|---|
| Name | Display name (max 255 chars, required) |
| ParentCategoryId | Optional reference to a parent category |
Relationships:
- A Category may have one parent Category (self-referencing hierarchy)
- A Category contains zero or more Products
Source: Source/Modules/Catalog/MMCA.Store.Catalog.Domain/Categories/Category.cs
2.2 Product
Description: A saleable item in the catalog. Products have descriptive attributes and belong to a category. Each product has one or more purchasable variants.
Key Properties:
| Property | Description |
|---|---|
| Name | Product name (max 100 chars, required) |
| Description | Detailed description (max 4,000 chars, optional) |
| Brand | Manufacturer or brand name (max 100 chars, optional, no whitespace-only) |
| CategoryId | Category this product belongs to (optional) |
Relationships:
- A Product belongs to zero or one Category
- A Product contains one or more Product Variants
Source: Source/Modules/Catalog/MMCA.Store.Catalog.Domain/Products/Product.cs
2.3 Product Variant
Description: A specific purchasable configuration of a product (e.g., "Gold Ring - Size 7"). Variants carry the actual price and SKU used for inventory and order processing.
Key Properties:
| Property | Description |
|---|---|
| SKU | Stock-keeping unit identifier (max 50 chars, globally unique, optional) |
| Price | Unit price as Money (amount + currency, must be non-negative) |
Relationships:
- A Product Variant belongs to exactly one Product
- A Product Variant has zero or one Inventory Item
- A Product Variant can appear in Shopping Cart Items and Order Lines
Source: Source/Modules/Catalog/MMCA.Store.Catalog.Domain/Products/ProductVariant.cs
2.4 Shopping Cart
Description: A temporary collection of items a customer intends to purchase. Each customer has exactly one shopping cart (the cart ID equals the customer ID).
Key Properties:
| Property | Description |
|---|---|
| Status | Active or CheckedOut |
| ShoppingCartItems | Collection of items in the cart |
Relationships:
- A Shopping Cart belongs to exactly one Customer (1:1 relationship, ID = CustomerId)
- A Shopping Cart contains zero or more Shopping Cart Items
Source: Source/Modules/Sales/MMCA.Store.Sales.Domain/ShoppingCarts/ShoppingCart.cs
2.5 Shopping Cart Item
Description: A line item in a shopping cart, representing a desired quantity of a specific product variant.
Key Properties:
| Property | Description |
|---|---|
| ProductVariantId | The product variant being added |
| Quantity | Number of units desired (must be positive) |
Relationships:
- Belongs to exactly one Shopping Cart
- References one Product Variant (cross-module)
Source: Source/Modules/Sales/MMCA.Store.Sales.Domain/ShoppingCarts/ShoppingCartItem.cs
2.6 Order
Description: A confirmed purchase transaction created when a customer checks out their shopping cart. Orders track payment status and progress through a defined lifecycle.
Key Properties:
| Property | Description |
|---|---|
| CustomerId | The customer who placed the order |
| Total | Calculated order total (Money value, sum of all line totals) |
| Status | Current lifecycle stage (see state machine below) |
| StripeSessionId | Payment gateway session reference |
| StripePaymentIntentId | Payment confirmation reference |
Relationships:
- An Order belongs to one Customer
- An Order contains one or more Order Lines
Source: Source/Modules/Sales/MMCA.Store.Sales.Domain/Orders/Order.cs
2.7 Order Line
Description: A single item within an order, capturing the product variant, quantity, and price at time of purchase.
Key Properties:
| Property | Description |
|---|---|
| ProductVariantId | The product variant ordered |
| Quantity | Units ordered (must be positive) |
| UnitPrice | Price per unit at time of order (Money) |
| LineTotal | Computed: UnitPrice x Quantity |
Relationships:
- Belongs to exactly one Order
Source: Source/Modules/Sales/MMCA.Store.Sales.Domain/Orders/OrderLine.cs
2.8 Inventory Item
Description: Tracks available stock for a specific product variant. One inventory record per product variant (the inventory item ID equals the product variant ID).
Key Properties:
| Property | Description |
|---|---|
| AvailableQuantity | Units currently in stock (must be >= 0) |
| IsOutOfStock | Computed: true when AvailableQuantity equals 0 |
Relationships:
- One Inventory Item per Product Variant (1:1 relationship, ID = ProductVariantId)
Source: Source/Modules/Sales/MMCA.Store.Sales.Domain/Inventory/InventoryItem.cs
2.9 Customer
Description: A registered buyer with personal information and optional mailing address.
Key Properties:
| Property | Description |
|---|---|
| FirstName | First name (max 100 chars, required) |
| LastName | Last name (max 100 chars, required) |
| Email address (max 100 chars, unique, required) | |
| Address | Optional mailing address (value object) |
Relationships:
- A Customer is linked 1:1 with a User account
- A Customer has one Shopping Cart
- A Customer has zero or more Orders
Source: Source/Modules/Identity/MMCA.Store.Identity.Domain/Customers/Customer.cs
2.10 User
Description: An authentication account with credentials and role-based access. Users are either administrators or customers.
Key Properties:
| Property | Description |
|---|---|
| Login email (unique, required) | |
| PasswordHash / PasswordSalt | Securely stored credentials (PBKDF2-HMAC-SHA512, 600,000 iterations) |
| Role | "Admin" or "Customer" |
| RefreshToken / RefreshTokenExpiry | Session refresh mechanism (7-day expiry) |
| IsActive | Account active status |
| CustomerId | Link to Customer profile (for Customer role) |
Relationships:
- A User with "Customer" role is linked 1:1 to a Customer entity
- Admin users have no Customer record
Source: Source/Modules/Identity/MMCA.Store.Identity.Domain/Users/User.cs
2.11 Value Objects
| Value Object | Properties | Validation | Source |
|---|---|---|---|
| Money | Amount (decimal), Currency (Currency) | Cannot have negative amount; currency mismatch on add | MMCA.Common.Shared/ValueObjects/Money.cs |
| Currency | Code (string) | Must be "USD" or "EUR" | MMCA.Common.Shared/ValueObjects/Currency.cs |
| Address | AddressLine1 (required, max 200), AddressLine2, City, State, ZipCode, Country | AddressLine1 required; all fields have max lengths | MMCA.Common.Shared/ValueObjects/Address.cs |
3. Business Workflows
3.1 Customer Registration
Trigger: A new user submits registration with first name, last name, email, and password.
Steps:
- Validate registration request (email format, password requirements)
- Verify email is not already registered (uniqueness check)
- Hash the password using PBKDF2-HMAC-SHA512 (600,000 iterations) with a per-user salt
- Create a User entity with "Customer" role
- Generate JWT access token (15 min, with claims: sub, jti, iat, user_id, email, role, customer_id) and refresh token (7-day expiry, 64-byte random)
- Domain event
UserRegisteredis published, which triggers automatic Customer creation:- A new Customer entity is created with the same name, email, and address
- The User is linked to the newly created Customer via
CustomerId
- Return authentication tokens to the caller
Implemented in:
Source/Modules/Identity/MMCA.Store.Identity.Application/Users/AuthenticationService.cs(RegisterAsync)Source/Modules/Identity/MMCA.Store.Identity.Application/Users/DomainEventHandlers/UserRegisteredHandler.cs
3.2 Customer Login
Trigger: A registered user submits email and password.
Steps:
- Validate login request
- Look up user by email
- Verify password using PBKDF2-HMAC-SHA512 with constant-time comparison (legacy HMAC-SHA512 hashes still verify via salt-length detection)
- Verify user is active
- Generate new access token and refresh token
- Update refresh token expiry (7 days from now)
- Return authentication response with both tokens
Implemented in:
Source/Modules/Identity/MMCA.Store.Identity.Application/Users/AuthenticationService.cs(LoginAsync)
3.3 Add Item to Shopping Cart
Trigger: An authenticated customer adds a product variant to their cart.
Steps:
- Validate the request (quantity must be positive)
- Verify the product variant exists in the Catalog (cross-module check via
IProductVariantService) - Fetch or create the customer's shopping cart
- If the cart was previously checked out, reactivate it (clears all old items, sets status back to Active)
- If the same product variant already exists in the cart, increase its quantity
- Otherwise, add a new cart item
- Persist changes
Business Rules Applied:
- Cart must be in Active status to accept items (or will be reactivated)
- Product variant must exist in the catalog
- Quantity must be greater than zero
- Duplicate variants merge (quantities are combined)
- Reactivation clears all previous items
Implemented in:
Source/Modules/Sales/MMCA.Store.Sales.Application/ShoppingCarts/UseCases/AddItem/AddItemHandler.csSource/Modules/Sales/MMCA.Store.Sales.Domain/ShoppingCarts/ShoppingCart.cs(AddShoppingCartItem)
3.4 Checkout (Cart to Order)
Trigger: A customer initiates checkout on their active shopping cart.
Steps:
- Fetch the customer's shopping cart with all items
- Retrieve current unit prices for all product variants from the Catalog module (cross-module call)
- Fetch inventory items for all product variants
- Reject the checkout if any variant is missing from the price map (it was soft-deleted between cart-add and checkout), naming the offending variant
- Execute the checkout domain service which:
a. Validates the cart is not empty
b. Validates inventory exists for every item in the cart
c. Runs a fail-fast sufficiency check per item against the loaded snapshot
d. Creates Order Lines with current prices (price snapshot at time of purchase)
e. Creates the Order with status
PendingPaymentf. Transitions the cart toCheckedOutstatus - Commit the write phase inside one explicit transaction: the atomic conditional inventory decrements first, then the order insert and cart transition
- Return the Order details
Business Rules Applied:
- Cart must contain at least one item
- Every product variant must still exist in the Catalog module
- All product variants must have corresponding inventory records
- Sufficient inventory must be available for each item
- Prices are locked at checkout time (not at cart-add time)
- The write phase is atomic, but the command is deliberately not
ITransactional: the handler opens the transaction itself so the cross-module price fetch stays outside it and cross-service latency never extends lock hold time
Concurrency: the domain service's sufficiency check reads a point-in-time snapshot and is only a
fail-fast. The real oversell guard is IInventoryAllocationService.DecrementAsync, which issues one
atomic conditional UPDATE per variant: a row that no longer has enough stock matches zero rows, the
Result fails, and the whole transaction rolls back. Because that path uses ExecuteUpdateAsync it
bypasses the save pipeline, so no InventoryAdjusted domain event is raised on checkout.
Implemented in:
Source/Modules/Sales/MMCA.Store.Sales.Application/ShoppingCarts/UseCases/CheckOut/CheckOutHandler.csSource/Modules/Sales/MMCA.Store.Sales.Domain/Services/CheckOutDomainService.csSource/Modules/Sales/MMCA.Store.Sales.Infrastructure/Services/InventoryAllocationService.cs
3.5 Payment Processing (Stripe)
Trigger: After checkout, the customer initiates payment for a pending order.
Steps:
- Fetch the order with its order lines
- Call Stripe API to create a checkout session (converts Money to smallest currency unit: cents)
- Transition order status to
PaymentInitiated - Store the Stripe session ID on the order
- Return the checkout URL for the customer to complete payment
Webhook Confirmation (asynchronous):
- Stripe sends a webhook notification to
POST /payments/webhook - System verifies the webhook signature using Stripe's
EventUtility.ConstructEvent() - Based on event type:
checkout.session.completed-> Mark order asPaid, store payment intent IDcheckout.session.expiredorpayment_intent.payment_failed-> Mark order asPaymentFailed
- Idempotent: skips processing if order is already in the appropriate terminal state
Implemented in:
Source/Modules/Sales/MMCA.Store.Sales.Application/Orders/UseCases/CreateCheckoutSession/CreateCheckoutSessionHandler.csSource/Modules/Sales/MMCA.Store.Sales.Application/Orders/UseCases/ProcessPaymentWebhook/ProcessPaymentWebhookHandler.csSource/Modules/Sales/MMCA.Store.Sales.Infrastructure/Services/StripePaymentService.cs
3.6 Order Cancellation
Trigger: A customer or admin cancels an order.
Steps:
- Fetch the order with its order lines
- Validate the order is in a cancellable state (PendingPayment, PaymentInitiated, or PaymentFailed)
- Transition order status to
Cancelledand persist. The cancellation commits on its own; no inventory work happens inside this transaction. - After the commit, the
OrderCancelleddomain event drivesOrderCancelledSagaHandler, which runs in its own DI scope and restores inventory for each order line
Business Rules Applied:
- Orders in PendingPayment, PaymentInitiated, or PaymentFailed can be cancelled
- Paid or delivered orders cannot be cancelled (no refund workflow)
- Inventory is restored by a compensating handler, not inline (ADR-054)
- Restoration is idempotent under at-least-once redelivery:
Order.InventoryRestoredis the marker, and it is committed by the sameSaveChangesAsyncas the inventory increases. The order's rowversion token makes two concurrent deliveries mutually exclusive; the loser fails and the outbox retry then sees the committed marker.
Implemented in:
Source/Modules/Sales/MMCA.Store.Sales.Application/Orders/UseCases/Cancel/CancelOrderHandler.csSource/Modules/Sales/MMCA.Store.Sales.Application/Orders/Saga/OrderCancelledSagaHandler.cs
3.7 Manual Payment Override
Trigger: An administrator manually marks an order as paid (e.g., for cash payments or payment system issues).
Steps:
- Fetch the order
- Validate order status allows manual payment (PendingPayment, PaymentInitiated, or PaymentFailed)
- Set payment intent ID to
"manual-admin-override" - Transition status to
Paid - Persist changes
Implemented in:
Source/Modules/Sales/MMCA.Store.Sales.Application/Orders/UseCases/Pay/PayOrderHandler.csSource/Modules/Sales/MMCA.Store.Sales.Domain/Orders/Order.cs(MarkAsPaidManually)
3.8 Order Delivery
Trigger: An administrator marks a paid order as delivered.
Steps:
- Fetch the order
- Validate order status is
Paid - Transition status to
Delivered - Persist changes
Implemented in:
Source/Modules/Sales/MMCA.Store.Sales.Application/Orders/UseCases/Deliver/DeliverOrderHandler.cs
3.9 Payment Verification (webhook backstop)
Trigger: The customer (or an admin) asks the system to re-check a payment whose webhook never arrived, typically from a "Retry / check payment" affordance on the order.
Steps:
- Validate ownership (owner or admin)
- Query Stripe directly for the order's checkout session
- If Stripe reports the session paid, mark the order
Paidexactly as the webhook path would - If the order is already
Paid, or is not in a verifiable state, succeed without changes
Business Rules Applied:
- Webhooks are the fast path but are not guaranteed; this is the reconciliation backstop that keeps a
paid customer from sitting in
PaymentInitiatedindefinitely (ADR-054) - The operation is idempotent by construction: it converges the order onto whatever Stripe says
Implemented in:
Source/Modules/Sales/MMCA.Store.Sales.Application/Orders/UseCases/VerifyPayment/VerifyPaymentHandler.cs
3.10 Product Images
Trigger: An administrator manages the image collection on a product; any visitor views one.
Behind the CatalogFeatures.ProductImages feature gate. Image binaries are stored as rows in the
Catalog database (ProductImageData, written through ProductImageStorageService), NOT in managed
blob storage: ADR-045 records the BR-116 amendment
for MMCA.ADC avatars and MMCA.Store does not adopt it here. IProductImageStorageService is
deliberately storage-agnostic, so moving to blob storage later is an infrastructure swap rather than
a handler change. Uploads are capped at 6 MB of request body
(the 5 MB domain constraint plus multipart overhead). Reads are anonymous and output-cached for 300
seconds; every mutation evicts the products cache. Both a legacy single-image route set (/image) and
the current collection route set (/images) are exposed, sharing one upload handler. Reordering
reassigns display order from the supplied id list, and the first id becomes the primary image.
Implemented in:
Source/Modules/Catalog/MMCA.Store.Catalog.API/Controllers/ProductImagesController.cs
3.11 Data Subject Rights (GDPR/CCPA)
Trigger: A user exports or erases their own account data; an administrator does it on their behalf.
Export returns the personal data held for the user in a portable JSON format. Erasure deletes the account and irreversibly anonymizes its personal data, which is distinct from the soft-delete used for ordinary lifecycle (ADR-005). Both endpoints authorize the owner or an Admin, enforced in the handlers rather than by a controller-wide policy.
Implemented in:
Source/Modules/Identity/MMCA.Store.Identity.API/Controllers/UsersController.cs
3.12 Password Reset
Trigger: A customer who cannot sign in asks for a reset link from /forgot-password, then sets a new password on /reset-password.
Steps (request):
POST /Auth/forgot-passwordwith{ "email": "..." }. The request validator checks the shape of the address only- Look up the account behind the address without tracking it
- Increment the per-address request counter and check it against the rolling-hour throttle
- Mint a 256-bit single-use token, store only its SHA-256 hash under the address with the configured TTL, and hand the raw token back
- Email the reset link (
{PasswordReset:ResetUrl}?email=...&token=...) plus the raw token, so a client that cannot follow a deep link can have the token entered by hand - Return HTTP 202 Accepted, which is also what an unknown address, a throttled address and a failed email send return: the response never discloses whether an address holds an account
Steps (reset):
POST /Auth/reset-passwordwith{ "email": "...", "token": "...", "newPassword": "..." }. The new password goes through the same strength rules registration uses- Validate the presented token against the stored hash in constant time and consume it before any write, so it cannot be redeemed twice
- Load the account, hash the new password (PBKDF2-HMAC-SHA512), and apply it through the aggregate
- Persist, then clear the account's login brute-force counters so a customer who was locked out can sign in immediately
- Return HTTP 204 No Content. Every rejection (unknown, expired, replayed, mismatched or attempt-capped token, or an unresolvable account) collapses to one generic
Auth.InvalidResetTokenHTTP 401
Both endpoints are anonymous by necessity (the caller has lost the credential), carry the auth-ip per-IP rate limit that login and register carry, and are idempotent. They live on their own controller routed to the same Auth prefix, which the Gateway's /Auth/{**catch-all} route already forwards.
No schema change: the token lifecycle lives entirely in the cache, so expired tokens are reaped by cache TTL rather than by a sweeper and the feature ships without a migration. A cache eviction invalidates outstanding tokens, which costs the user one more request. See ADR-091.
Implemented in:
Source/Modules/Identity/MMCA.Store.Identity.API/Controllers/PasswordResetController.csSource/Modules/Identity/MMCA.Store.Identity.Application/Users/UseCases/ForgotPassword/ForgotPasswordHandler.csSource/Modules/Identity/MMCA.Store.Identity.Application/Users/UseCases/ResetPassword/ResetPasswordHandler.cs
4. Order Status State Machine
+------------------------------------------+
| |
v |
+-----------------+----+ InitiatePayment +--------------+------+
| PendingPayment |--------------------->| PaymentInitiated |
+----------------------+ +---------------------+
| ^ | |
| | | |
| +------------ PaymentFailed <--------+ |
| (can retry) |
| | |
MarkAsCancelled MarkAsCancelled MarkAsPaid
| | (webhook or manual)
v v |
+--------------+ +-----------+-----+
| Cancelled | <---- MarkAsCancelled ------------ | Paid |
+--------------+ (from PaymentInitiated) +-----------------+
|
MarkAsDelivered
|
v
+-----------------+
| Delivered |
+-----------------+
Cancellable States: PendingPayment, PaymentInitiated, PaymentFailed Manual Payment States: PendingPayment, PaymentInitiated, PaymentFailed Terminal States: Cancelled, Delivered
5. Business Rules
5.1 Product & Catalog Rules
| Rule | Description | Location |
|---|---|---|
| Category name required | Category names cannot be empty or whitespace | CategoryInvariants.cs |
| Category name max length | Max 255 characters | CategoryInvariants.cs |
| Category name uniqueness | Category names must be unique (database-enforced) | CategoryConfiguration.cs |
| Product name required | Product names cannot be empty or whitespace | ProductInvariants.cs |
| Product name max length | Max 100 characters | ProductInvariants.cs |
| Description max length | Max 4,000 characters | ProductInvariants.cs |
| Brand not whitespace | If a brand is provided, it cannot be only whitespace | ProductInvariants.cs |
| Brand max length | Max 100 characters | ProductInvariants.cs |
| Price must be non-negative | Product variant prices must have Amount >= 0 | ProductInvariants.cs |
| SKU max length | Max 50 characters | ProductInvariants.cs |
| SKU global uniqueness | SKUs must be globally unique across all product variants (null allowed for multiple variants without SKUs) | AddVariantHandler.cs, ProductVariantConfiguration.cs |
5.2 Shopping Cart Rules
| Rule | Description | Location |
|---|---|---|
| Cart must be active | Items can only be added/removed/changed when cart is Active | ShoppingCartInvariants.cs |
| Positive quantity | Item quantities must be greater than zero | ShoppingCartInvariants.cs |
| Non-empty for checkout | Cart must contain at least one item to check out | ShoppingCartInvariants.cs |
| Variant must exist | Product variant must exist in catalog before adding to cart | AddItemHandler.cs |
| Duplicate merging | Adding a variant already in cart increases its quantity | ShoppingCart.cs |
| Reactivation on add | Adding items to a checked-out cart reactivates it and clears all old items | AddItemHandler.cs |
5.3 Order Rules
| Rule | Description | Location |
|---|---|---|
| Orders via checkout only | Orders can only be created through the shopping cart checkout process | CheckOutHandler.cs |
| Non-empty order | Orders must contain at least one order line | OrderInvariants.cs |
| Positive line quantity | Order line quantities must be greater than zero | OrderInvariants.cs |
| Non-negative line price | Order line unit prices cannot be negative | OrderInvariants.cs |
| Cancellation restriction | Only PendingPayment, PaymentInitiated, or PaymentFailed orders can be cancelled | OrderInvariants.cs |
| Payment initiation restriction | Payment can only be initiated from PendingPayment or PaymentFailed states | OrderInvariants.cs |
| Payment confirmation restriction | Only PaymentInitiated orders can be marked as paid (via webhook) | OrderInvariants.cs |
| Manual payment restriction | Manual payment allowed from PendingPayment, PaymentInitiated, or PaymentFailed | OrderInvariants.cs |
| Delivery restriction | Only Paid orders can be marked as delivered | OrderInvariants.cs |
| Inventory restoration | Cancelling an order restores all order line quantities to inventory | CancelOrderHandler.cs |
| Price snapshot | Order lines capture the unit price at checkout time, not current catalog price | CheckOutDomainService.cs |
5.4 Inventory Rules
| Rule | Description | Location |
|---|---|---|
| Non-negative stock | Available quantity can never go below zero | InventoryItemInvariants.cs |
| Positive adjustment | Increase/decrease amounts must be positive | InventoryItemInvariants.cs |
| Sufficient stock | Decreasing inventory requires sufficient available quantity | InventoryItemInvariants.cs |
| Variant must exist | Inventory can only be created for existing product variants | AdjustInventoryHandler.cs |
5.5 Identity & Authentication Rules
| Rule | Description | Location |
|---|---|---|
| Unique email (User) | User emails must be unique | UserConfiguration.cs |
| Unique email (Customer) | Customer emails must be unique | CustomerConfiguration.cs |
| Valid role | User roles must be either "Admin" or "Customer" | UserInvariants.cs |
| Access token expiry | Access tokens expire after 15 minutes (configurable) | JwtSettings |
| Refresh token expiry | Refresh tokens expire after 7 days (configurable) | JwtSettings |
| Auto customer creation | Registering as a Customer automatically creates a linked Customer entity | UserRegisteredHandler.cs |
| Admin no customer | Admin users do not get an associated Customer record | UserRegisteredHandler.cs |
| Name required | Customer first name and last name cannot be empty or whitespace | CustomerInvariants.cs |
| Email required | Customer email cannot be empty or whitespace | CustomerInvariants.cs |
| Address line 1 required | If address is provided, AddressLine1 is required | AddressInvariants.cs |
| Reset request anti-enumeration | POST /Auth/forgot-password answers 202 for every well-formed address: unknown address, throttled address and failed email send are logged server-side and reported as accepted. Only a malformed address returns 400 |
ForgotPasswordHandler.cs, ForgotPasswordRequestValidator |
| Reset token strength | 256-bit Base64Url token, single-use, SHA-256 hashed at rest, compared in constant time, one active token per address (a new request overwrites the old) | PasswordResetTokenService |
| Reset token expiry | Tokens expire after PasswordReset:TokenLifetimeMinutes (default 30) |
PasswordResetSettings |
| Reset attempt cap | The token record is discarded after PasswordReset:MaxValidationAttempts wrong guesses (default 5); a wrong guess is rewritten with the remaining lifetime, never a fresh one |
PasswordResetTokenService |
| Reset request throttle | Maximum PasswordReset:MaxRequestsPerEmail requests per address per RequestWindowMinutes (default 3 per hour); over-limit still answers 202 and sends nothing |
PasswordResetTokenService |
| Reset clears lockout | A successful reset clears the account's login brute-force counters and its reset-request counter | ResetPasswordHandler.cs |
| Reset password strength | The new password goes through the same strength rules as registration, so a reset cannot bypass the complexity policy | ResetPasswordRequestValidator |
6. Use Cases
Customer-Facing
| Use Case | Actor | Description |
|---|---|---|
| Register | Anonymous | Create a new customer account with email and password |
| Login | Anonymous | Authenticate with email and password to receive JWT tokens |
| Refresh session | Any authenticated | Obtain new tokens using a valid refresh token |
| Revoke session | Any authenticated | Invalidate refresh token to end session |
| Request password reset | Anonymous | Ask for a reset link by email; always accepted for a well-formed address |
| Reset password | Anonymous | Set a new password by redeeming the single-use token from the reset email |
| Browse catalog | Anonymous | View categories, products, and product variants with pricing |
| View product detail | Anonymous | See product details, variants, pricing, and add to cart |
| Add item to cart | Customer | Add a product variant with quantity to shopping cart (via drawer) |
| Change cart item quantity | Customer | Update the quantity of an item already in cart (+/- controls) |
| Remove item from cart | Customer | Remove a product variant from shopping cart |
| Clear cart | Customer | Remove all items from shopping cart |
| Checkout | Customer | Convert shopping cart into an order (reserves inventory) |
| Initiate payment | Customer | Start Stripe checkout session for a pending order |
| View my orders | Customer | List all orders belonging to the authenticated customer |
| View order detail | Customer | See order status, lines, and payment information |
| Cancel order | Customer | Cancel a pending/initiated/failed order (restores inventory) |
| Update profile | Customer | Change name, address, or password via profile page (email change is not offered in the UI) |
| Change password | Customer | Update account password via profile page |
Administrator-Facing
| Use Case | Actor | Description |
|---|---|---|
| Manage categories | Admin | Create, rename, assign parent, delete categories |
| Manage products | Admin | Create, rename, update description/brand/category of products |
| Manage product variants | Admin | Add/remove variants, change SKU and price |
| Manage inventory | Admin | Increase, decrease, or set stock levels per variant |
| Mark order as paid | Admin | Manually override payment for an order |
| Mark order as delivered | Admin | Confirm order has been delivered |
| View all orders | Admin | View orders across all customers with filtering/pagination |
| Cancel order | Admin | Cancel any cancellable order |
| View all shopping carts | Admin | Browse customer shopping carts with status |
| View cart details | Admin | See cart contents, manage items, checkout on behalf |
| Manage customers | Admin | Create, view, edit, and delete customer profiles |
7. Domain Events and State Changes
Catalog Events
| Event | Trigger | Business Meaning |
|---|---|---|
| CategoryCreated | Category added to catalog | New product classification available |
| CategoryDeleted | Category removed (soft delete) | Classification no longer available |
| CategoryNameChanged | Category renamed (only if name actually differs) | Classification label updated |
| ProductCreated | New product added | New item available for sale |
| ProductDeleted | Product removed (soft delete) | Item no longer available |
| ProductNameChanged | Product renamed (only if name actually differs) | Item label updated |
| ProductVariantAdded | Variant added to product | New purchasable option available |
| ProductVariantRemoved | Variant removed (soft delete) | Purchasable option discontinued |
| ProductVariantSkuChanged | SKU updated (only if actually differs) | Inventory tracking identifier changed |
| ProductVariantPriceChanged | Price updated (only if actually differs) | Item pricing adjusted |
Sales Events
| Event | Trigger | Business Meaning |
|---|---|---|
| ShoppingCartCreated | First item added by customer | Customer started shopping |
| ShoppingCartItemAdded | New variant added to cart | Customer interested in a product |
| ShoppingCartItemQuantityAdjusted | Quantity changed (increase on duplicate add, or explicit change) | Customer adjusted desired quantity |
| ShoppingCartItemRemoved | Item removed from cart (soft delete) | Customer no longer wants item |
| ShoppingCartCheckedOut | Checkout completed | Customer committed to purchase |
| ShoppingCartCleared | All items removed | Customer abandoned selections |
| ShoppingCartDeleted | Cart soft-deleted | Cart record removed |
| OrderPlaced | Checkout creates order | Purchase order confirmed |
| OrderPaymentInitiated | Stripe session created | Customer directed to payment |
| OrderPaid | Payment confirmed (webhook or manual) | Revenue collected |
| OrderPaymentFailed | Payment unsuccessful | Payment needs retry or cancellation |
| OrderDelivered | Admin marks delivered | Fulfillment completed |
| OrderCancelled | Order cancelled | Purchase reversed, inventory restored |
| OrderDeleted | Order soft-deleted | Order record removed |
| InventoryItemCreated | Stock record created | Variant now trackable |
| InventoryAdjusted | Stock level changed (only when quantity actually changes) | Available quantity updated |
| InventoryItemDeleted | Stock record soft-deleted | Variant no longer tracked |
Identity Events
| Event | Trigger | Business Meaning |
|---|---|---|
| UserRegistered | New account created | New user in the system |
| UserPasswordChanged | Password updated | Security credentials rotated |
| UserDeactivated | Account disabled | User can no longer access system |
| CustomerCreated | Auto-created on registration | Customer profile established |
| CustomerDeleted | Customer soft-deleted | Profile removed |
| CustomerNameChanged | Name updated (only if differs) | Profile information changed |
| CustomerEmailChanged | Email updated (only if differs) | Contact information changed |
| CustomerAddressChanged | Address updated (only if differs) | Shipping information changed |
8. External Integrations
8.1 Stripe Payment Gateway
Purpose: Processes online customer payments for orders.
Business Impact: Enables the system to collect payments from customers and confirm payment success or failure asynchronously via webhooks.
Integration Points:
- Checkout Session Creation: Creates hosted payment pages with order details, converting internal Money values to Stripe's smallest currency unit (cents). Maps order lines to Stripe line items with product name, quantity, and unit amount.
- Webhook Processing: Receives and verifies payment status notifications using Stripe's signature verification (
EventUtility.ConstructEvent()). Handlescheckout.session.completed,checkout.session.expired, andpayment_intent.payment_failedevents. - Error Handling: Stripe API errors return
Result.Failurewith code"Payment.Stripe.SessionCreationFailed". Signature verification failures return"Payment.SignatureVerification.Failed". - Configuration: Stripe API key (
SecretKey) and webhook secret (WebhookSecret) configured per environment viaStripeSettings(user secrets recommended).
Source: Source/Modules/Sales/MMCA.Store.Sales.Infrastructure/Services/StripePaymentService.cs
8.2 SMTP Email Service
Purpose: Infrastructure for sending email notifications.
Business Impact: Provides the capability for system-to-user communication.
Configuration:
- Host, Port (default 25), Username, Password, EnableSsl, From, To
- Default:
localhost:25with SSL disabled
Source: Source/Common/MMCA.Common.Infrastructure/Services/SmtpEmailSender.cs
Consumers: the forgot-password handler (Section 3.12) sends the reset email through IEmailSender, alongside the Sales domain-event handlers OrderPaidHandler and OrderPaymentFailedSagaHandler. On the reset path delivery is awaited but never fatal: a send failure is logged and the request still answers 202, because reporting it would be an enumeration oracle, and the token stays live so the customer can retry.
9. Authorization Model
| Policy | Access Level | Description |
|---|---|---|
| Anonymous | No auth required | Catalog browsing (GET categories, products), login, registration, password reset (POST /Auth/forgot-password, POST /Auth/reset-password), payment webhooks |
| RequireAuthenticated | Any logged-in user | Shopping cart operations, order viewing (own), profile management |
| RequireCustomer | Customer role | Customer-specific operations |
| RequireAdmin | Admin role | Catalog management, inventory management, manual payment, delivery confirmation |
Ownership Enforcement: The OwnerOrAdminFilter validates that the route parameter id (CustomerIdentifierType) matches the authenticated user's customer ID, or that the user has the Admin role. Applied to shopping cart and order endpoints. Returns 403 Forbidden if unauthorized.
JWT Claims: Access tokens contain: sub, jti, iat, user_id, email, role, and customer_id (when applicable).
10. Cross-Module Communication
The system enforces strict module boundaries. Modules communicate only through shared interface contracts:
| Interface | Provider Module | Consumer Module | Purpose |
|---|---|---|---|
IProductVariantService |
Catalog | Sales | Verify variant existence, check SKU uniqueness, fetch unit prices, get ID by SKU |
Confirmed behaviors:
- Sales module cannot directly access Catalog domain entities
- When Catalog module is disabled, a stub
DisabledProductVariantServiceis registered - Sales module declares a hard dependency on Catalog (
RequiresDependencies = true): it will not start without Catalog - Module discovery uses reflection; registration follows topological dependency order (Kahn's algorithm)
11. User Interface
11.1 Technology
The UI is a Blazor Server + WebAssembly hybrid (InteractiveAuto render mode) using MudBlazor component library. It supports multiple hosting targets:
- Web (Server + WASM):
Source/UI/Hosts/MMCA.Store.UI.Web - WebAssembly Client:
Source/UI/Hosts/MMCA.Store.UI.Web.Client - MAUI (iOS, Android, macOS, Windows):
Source/UI/Hosts/MMCA.Store.UI
11.2 Shopping Cart UX
The shopping cart is exclusively accessible through a 380px right-side drawer (CartDrawer), opened via the cart icon in the top app bar. There is no dedicated cart page.
Features:
- Quantity +/- controls per item when cart is Active
- Remove individual items, clear all items
- "Checkout & Pay" button triggers order creation + Stripe redirect
- "Checked Out" chip and read-only mode for checked-out carts
- Empty state with "Browse Products" navigation
- "Continue Shopping" link to catalog
State Management: The ICartStateService singleton manages cart state centrally. It enriches cart items with product names and SKUs from the Catalog API after each refresh.
11.3 Catalog Browse
The catalog browse page (/catalog) provides:
- Product grid with search by name
- Category filter dropdown
- Sort by name or price
- Quick "Add to Cart" buttons per variant
- "View Details" navigation to product detail page
The product detail page (/catalog/{id}) shows:
- Breadcrumbs (Home > Catalog > Product)
- Product description, brand, category
- Variant list with SKU, price, quantity selector, and "Add to Cart"
- "Buy Now" option (direct Stripe checkout for single variant)
11.4 Navigation Structure
Sidebar (role-based, dynamically populated from IUIModule registrations):
- Customer: Home, Shop, My Orders, My Profile
- Admin: Home, Categories, Products, Inventory, Shopping Carts, Orders, Customers, My Profile
Top App Bar: Cart icon with badge count, user email, Logout button (authenticated) or Login/Register buttons (anonymous)
11.5 Module Registration
UI modules implement IUIModule (providing NavItems and Assembly for route discovery). Modules can be conditionally enabled via UIModuleConfiguration.IsModuleEnabled(configuration, moduleName).
12. Cross-Cutting Infrastructure
12.1 Command/Query Pipeline Decorators
| Decorator | Marker | Behavior |
|---|---|---|
TransactionalCommandDecorator |
ITransactional |
Wraps command in database transaction (begin/commit/rollback) |
CachingCommandDecorator |
ICacheInvalidating |
Invalidates cache entries by prefix on successful command execution |
ProfilingCommandDecorator |
(all commands) | Records MiniProfiler step (when UseMiniProfiler=true) |
ProfilingQueryDecorator |
(all queries) | Records MiniProfiler step (when UseMiniProfiler=true) |
12.2 Idempotency
The IdempotencyFilter (applied via [Idempotent] attribute on Create endpoints) caches the first response for a given Idempotency-Key header value for 24 hours. Duplicate requests receive the cached response with an X-Idempotent-Replay: true header. Per-key SemaphoreSlim locking prevents concurrent duplicate execution.
12.3 Exception Handling
Five exception handlers (registered as middleware in priority order):
- DomainExceptionHandler -> HTTP 400
- ValidationExceptionHandler (FluentValidation) -> HTTP 400 with grouped errors
- DbUpdateExceptionHandler -> HTTP 409 Conflict
- OperationCanceledExceptionHandler -> HTTP 499
- GlobalExceptionHandler -> HTTP 500 (catch-all)
12.4 Multi-Database Strategy
The IDbContextFactory implements a strategy pattern supporting three data sources:
- SQL Server (default, via Aspire container)
- SQLite (alternative)
- Cosmos DB (alternative, no transaction support)
Entity types are routed to data sources via [UseDataSource] attribute on EF configurations. The IDataSourceService caches entity-to-datasource mappings.
13. Testing
13.1 Test Structure
| Type | Projects | Description |
|---|---|---|
| Unit | 15 projects (per module per layer + common) | Entity creation, invariants, domain events, handlers, mappers, validators |
| Architecture | 1 project (12 test files) | NetArchTest.Rules: layer dependencies, module isolation, domain purity, naming conventions |
| Integration | 3 per-service projects (Catalog, Sales, Identity) | Each boots one service host via WebApplicationFactory; Testcontainers.MsSql + Respawn for DB reset (or a real SQL Server via STORE_TEST_SQL_BASE), JWT token generation, full HTTP endpoint testing. Runs via MMCA.Store.Integration.slnf and gates deploy. The old combined single-host MMCA.Store.IntegrationTests has been removed. |
| E2E | 1 project | Playwright (Chromium), page objects, Blazor wait helpers, full user journey tests |
13.2 Key Test Scenarios
- Full customer journey: Register -> Browse -> Add to Cart -> Checkout -> Admin Pay -> Deliver
- Order lifecycle: all state transitions including cancellation with inventory restoration
- Cart operations: add, change quantity, remove, clear, checkout
- Authorization: anonymous, customer, admin role enforcement
- Cross-module: Catalog -> Sales pricing, inventory validation
14. Missing or Unclear Business Logic
14.1 No Email Notifications on Events
Observation: The SMTP email service infrastructure is implemented, but no domain event handlers trigger email notifications for events like order confirmation, payment receipt, or shipping notification. Recommendation: Clarify whether email notifications are planned or intentionally omitted.
14.2 No Return/Refund Workflow
Observation: Once an order reaches Paid or Delivered status, there are no further state transitions available. No return, refund, or exchange workflow exists. Cancellation is only possible before payment succeeds.
Recommendation: Clarify whether returns/refunds are in scope and whether Stripe refund integration is needed.
14.3 No Inventory Check at Cart-Add Time
Observation: When a customer adds an item to the cart, the system verifies the product variant exists but does not check if inventory is available. Inventory is only validated at checkout. Recommendation: This may be intentional (allowing customers to add items that are temporarily out of stock) or may warrant a stock availability indicator on the cart.
14.4 Cart Reactivation Clears All Items
Observation: When a customer adds an item to a previously checked-out cart, all previous items are deleted and the cart is reactivated empty (with only the new item). The business intent behind clearing the cart rather than preserving previous items is unclear. Recommendation: Confirm this is the desired behavior: some systems prefer to retain unchecked-out items.
14.5 No Price Change Protection
Observation: Product variant prices can be changed at any time by administrators. If a customer has items in their cart and prices change before checkout, the customer will be charged the new price (prices are fetched at checkout, not at cart-add time). Recommendation: Determine if customers should be notified of price changes or if cart items should display price warnings.
14.6 Delivery Tracking Absent
Observation: The MarkAsDelivered transition exists but there is no tracking number, carrier information, or estimated delivery date. Delivery is a binary admin action.
Recommendation: Consider whether shipping/tracking details are needed for the business use case.
14.7 No Partial Order Fulfillment
Observation: Orders are delivered as a whole: there is no concept of partial shipments or split deliveries. Recommendation: Clarify if partial fulfillment is a future requirement.
14.8 No Customer/User Deactivation Endpoint
Observation: User.Deactivate() method and UserDeactivated domain event exist in the domain model, but no API endpoint or UI action exposes this functionality.
Recommendation: May be an admin feature not yet implemented.
14.9 Category Deletion Has No Cascade Check
Observation: Deleting a category doesn't check for assigned products. Products with a deleted category may have an orphaned CategoryId.
Recommendation: Consider validating no products reference the category before deletion, or cascading the nullification.
14.10 Inventory List Endpoint Not Exposed
Observation: The InventoryItemsController only exposes GetById, the GetAll, GetPaged, and Lookup endpoints from the base class are not overridden. The UI InventoryItemList page may need these endpoints.
Recommendation: Verify how the inventory list page fetches its data and whether list endpoints should be added.
15. Seed Data (Initial System State)
The system seeds the following data at startup:
Users:
- Admin: one seeded administrator account (Admin role, no Customer record; credentials are environment-specific and not published)
- Customer: one seeded demo customer account (Customer role)
Catalog:
- Categories: "Jewelry" (id=1), "Watches" (id=2)
- Product: "Gold Ring" (id=11, brand: "WhatNot", category: Jewelry)
- Variants: "Gold Ring - Size 6" ($15.00), "Gold Ring - Size 7" ($15.50), "Gold Ring - Size 8" ($16.00): all USD
Inventory: 100,000 units per variant (for all 3 Gold Ring sizes)
Seeding is idempotent: seeders check for existing data via ExistsAsync() before inserting, and only run for enabled modules.
This specification is derived entirely from the source code. All business rules, workflows, and behaviors described above are confirmed implementations traceable to the referenced source files. Last updated: 2026-07-27 (re-verified against MMCA.Store HEAD 1dfdc991).