Business Logic & Domain Entities
Complete documentation of 55+ domain entities with business rules, validation, and workflows
Domain-Driven Design (DDD) Overview
The Shirinzad platform implements Domain-Driven Design principles, placing business logic within rich domain entities rather than in services. This approach ensures business rules are encapsulated, testable, and maintainable.
Core Principle: Domain entities contain business logic and validation rules. Application services orchestrate these entities but don't contain business logic themselves.
Key DDD Concepts
Entities
- Identity: Each entity has a unique identifier (Guid)
- State: Encapsulates business data and rules
- Behavior: Methods that change state following business rules
- Validation: Ensures data integrity before persistence
Aggregate Roots
- Consistency Boundary: Ensures transactional consistency
- Access Control: Entry point for related entities
- Invariants: Enforces business rules across aggregates
- Examples: Product, Order, Cart, Wishlist
Domain Entity Statistics
55+
Domain Entities
15
Aggregate Roots
100+
Business Methods
80+
Validation Rules
Domain Model Structure
Entity Hierarchy
graph TB
subgraph Base["ABP Framework Base Classes"]
Entity[Entity<Guid>]
AuditedEntity[AuditedEntity<Guid>]
FullAuditedEntity[FullAuditedEntity<Guid>]
AggregateRoot[AggregateRoot<Guid>]
FullAuditedAggregateRoot[FullAuditedAggregateRoot<Guid>]
end
subgraph Catalog["Catalog Domain"]
Product[Product]
Category[Category]
Brand[Brand]
ProductReview[ProductReview]
end
subgraph Orders["Orders Domain"]
Order[Order]
Cart[Cart]
Payment[Payment]
Shipment[Shipment]
end
subgraph User["User Domain"]
UserProfile[UserProfile]
UserAddress[UserAddress]
Wishlist[Wishlist]
end
Entity --> AuditedEntity
AuditedEntity --> FullAuditedEntity
Entity --> AggregateRoot
AggregateRoot --> FullAuditedAggregateRoot
FullAuditedAggregateRoot --> Product
FullAuditedAggregateRoot --> Category
FullAuditedAggregateRoot --> Order
FullAuditedAggregateRoot --> Cart
FullAuditedAggregateRoot --> Wishlist
FullAuditedEntity --> UserProfile
Domain Entities by Module
Product Catalog Domain (19 Entities)
Product (Aggregate Root)
Purpose: Main product entity with pricing, stock, and metadata.
public class Product : FullAuditedAggregateRoot<Guid>
{
// Properties
public string Name { get; set; }
public string Slug { get; set; }
public decimal? Price { get; set; }
public decimal? DiscountPrice { get; set; }
public int? StockQuantity { get; set; }
public bool IsActive { get; set; }
public bool IsFeatured { get; set; }
public int ViewCount { get; set; }
public decimal AverageRating { get; set; }
public int RatingCount { get; set; }
// Business Methods
public void DecreaseStock(int quantity)
{
if (quantity <= 0)
throw new BusinessException("Quantity must be positive");
if (StockQuantity < quantity)
throw new BusinessException("Insufficient stock");
StockQuantity -= quantity;
}
public void IncreaseStock(int quantity)
{
if (quantity <= 0)
throw new BusinessException("Quantity must be positive");
StockQuantity += quantity;
}
public void UpdateRating(decimal newRating)
{
// Recalculate average rating
var totalRating = (AverageRating * RatingCount) + newRating;
RatingCount++;
AverageRating = totalRating / RatingCount;
}
public bool IsInStock() => StockQuantity.HasValue && StockQuantity > 0;
public decimal GetEffectivePrice()
{
return DiscountPrice.HasValue && DiscountPrice > 0
? DiscountPrice.Value
: Price ?? 0;
}
public bool IsDiscounted() => DiscountPrice.HasValue && DiscountPrice < Price;
public decimal GetDiscountPercentage()
{
if (!IsDiscounted() || !Price.HasValue || Price == 0)
return 0;
return ((Price.Value - DiscountPrice.Value) / Price.Value) * 100;
}
public bool IsLowStock()
{
return LowStockThreshold.HasValue &&
StockQuantity.HasValue &&
StockQuantity <= LowStockThreshold;
}
}
Key Entities
| Entity | Key Business Methods | Business Rules |
|---|---|---|
| Product |
DecreaseStock(quantity)IncreaseStock(quantity)UpdateRating(rating)GetEffectivePrice()IsLowStock()
|
- Stock cannot go negative - Slug must be unique - Rating must be 1-5 - Discount price < regular price |
| Category |
SetParent(category)HasChildren()GetAncestors()GetDescendants()
|
- Cannot be parent of itself - Circular references prevented - Slug must be unique - Active parent required |
| ProductReview |
Approve(userId)Reject()AddReply(text, author)MarkAsVerifiedPurchase()
|
- Rating 1-5 required - Must have content - Approved by admin - One review per user per product |
| Wishlist |
AddItem(product, variant)RemoveItem(itemId)SetAsDefault()MakePublic()/MakePrivate()
|
- One default wishlist per user - Max 100 items per wishlist - Cannot duplicate items - Public wishlists shareable |
Orders Domain (13 Entities)
Order (Aggregate Root)
Purpose: Customer order with complete order lifecycle management.
public class Order : FullAuditedAggregateRoot<Guid>
{
public string OrderNumber { get; set; }
public Guid? UserId { get; set; }
public OrderStatus Status { get; set; }
public decimal Subtotal { get; set; }
public decimal DiscountAmount { get; set; }
public decimal TaxAmount { get; set; }
public decimal ShippingCost { get; set; }
public decimal Total { get; set; }
public ICollection<OrderItem> Items { get; set; }
public ICollection<OrderStatusHistory> StatusHistory { get; set; }
// Business Methods
public void UpdateStatus(OrderStatus newStatus, Guid? changedByUserId, string notes = null)
{
// Validate status transition
if (!CanTransitionTo(newStatus))
{
throw new BusinessException($"Cannot change order status from {Status} to {newStatus}");
}
var oldStatus = Status;
Status = newStatus;
// Add to history
var history = new OrderStatusHistory
{
OrderId = Id,
FromStatus = oldStatus,
ToStatus = newStatus,
ChangedAt = DateTime.UtcNow,
ChangedByUserId = changedByUserId,
Notes = notes
};
StatusHistory.Add(history);
}
private bool CanTransitionTo(OrderStatus newStatus)
{
return (Status, newStatus) switch
{
(OrderStatus.Pending, OrderStatus.Processing) => true,
(OrderStatus.Pending, OrderStatus.Cancelled) => true,
(OrderStatus.Processing, OrderStatus.Shipped) => true,
(OrderStatus.Processing, OrderStatus.Cancelled) => true,
(OrderStatus.Shipped, OrderStatus.Delivered) => true,
(OrderStatus.Delivered, OrderStatus.Returned) => true,
_ => false
};
}
public void CalculateTotals()
{
Subtotal = Items.Sum(i => i.LineTotal);
Total = Subtotal - DiscountAmount + TaxAmount + ShippingCost;
}
public void ApplyDiscount(decimal amount)
{
if (amount < 0 || amount > Subtotal)
throw new BusinessException("Invalid discount amount");
DiscountAmount = amount;
CalculateTotals();
}
public bool CanBeCancelled()
{
return Status == OrderStatus.Pending || Status == OrderStatus.Processing;
}
public void Cancel(string reason)
{
if (!CanBeCancelled())
throw new BusinessException("Order cannot be cancelled");
Status = OrderStatus.Cancelled;
// Restore product stock
foreach (var item in Items)
{
// This would be done in domain service
}
}
}
Cart (Aggregate Root)
public class Cart : FullAuditedAggregateRoot<Guid>
{
public Guid? UserId { get; set; }
public string? SessionId { get; set; }
public bool IsActive { get; set; }
public DateTime ExpiresAt { get; set; }
public decimal Subtotal { get; set; }
public decimal DiscountAmount { get; set; }
public decimal Total { get; set; }
public ICollection<CartItem> Items { get; set; }
public void AddItem(Guid productId, string productName, decimal price, int quantity)
{
if (quantity <= 0)
throw new BusinessException("Quantity must be positive");
// Check if item already exists
var existingItem = Items.FirstOrDefault(i => i.ProductId == productId);
if (existingItem != null)
{
existingItem.Quantity += quantity;
}
else
{
var newItem = new CartItem
{
CartId = Id,
ProductId = productId,
ProductName = productName,
Price = price,
Quantity = quantity
};
Items.Add(newItem);
}
CalculateTotals();
}
public void UpdateItemQuantity(Guid itemId, int quantity)
{
if (quantity <= 0)
throw new BusinessException("Quantity must be positive");
var item = Items.FirstOrDefault(i => i.Id == itemId);
if (item == null)
throw new BusinessException("Item not found in cart");
item.Quantity = quantity;
CalculateTotals();
}
public void RemoveItem(Guid itemId)
{
var item = Items.FirstOrDefault(i => i.Id == itemId);
if (item != null)
{
Items.Remove(item);
CalculateTotals();
}
}
public void Clear()
{
Items.Clear();
Subtotal = 0;
Total = 0;
DiscountAmount = 0;
}
public void CalculateTotals()
{
Subtotal = Items.Sum(i => i.Price * i.Quantity);
Total = Subtotal - DiscountAmount;
}
public bool IsEmpty() => !Items.Any();
public int GetTotalItems() => Items.Sum(i => i.Quantity);
}
User Domain (4 Entities)
| Entity | Key Business Methods | Business Rules |
|---|---|---|
| UserProfile |
UpdateProfile(input)SetAvatar(url)ValidateNationalCode()GetFullName()
|
- One profile per user - National code must be valid (10 digits) - Gender: Male/Female/Other - Birth date must be in past |
| UserAddress |
SetAsDefault()Validate()GetFullAddress()
|
- One default address per user - Postal code 10 digits - Phone number required - Province and city required |
| LoginAttempt |
RecordSuccess()RecordFailure()IsAccountLocked()
|
- Track all login attempts - Lock after 5 failed attempts - Auto-unlock after 30 minutes - Store IP address and user agent |
Automotive Domain (9 Entities)
| Entity | Key Business Methods | Business Rules |
|---|---|---|
| Vehicle |
MarkAsSold()MarkAsAvailable()UpdatePrice(price)AddService(serviceId)
|
- VIN must be unique - Year must be valid (1900-current) - Price must be positive - Condition: New/Used |
| VehiclePart |
AddCompatibility(modelId, years)RemoveCompatibility(modelId)IsCompatibleWith(modelId, year)
|
- Part number must be unique - Compatibility years valid - Stock quantity >= 0 - Price must be positive |
| Service |
SetPrice(price)SetDuration(minutes)AddToVehicle(vehicleId)
|
- Price and duration required - Category must be active - Service name unique per category |
Blog & CMS Domain (10 Entities)
| Entity | Key Business Methods | Business Rules |
|---|---|---|
| BlogPost |
Publish()Unpublish()IncrementViewCount()AddComment(comment)AddTag(tagId)
|
- Slug must be unique - Published date required for publish - Author required - Content max 50,000 chars |
| BlogComment |
Approve()Reject()Reply(text, userId)
|
- Must have content - Requires moderation - Parent comment validation - Max depth: 3 levels |
| Page |
Publish()Unpublish()SetTemplate(template)
|
- Slug must be unique - Template must exist - Content required - Title max 300 chars |
| Slider |
Activate()Deactivate()SetDateRange(start, end)
|
- Start date < end date - Image URL required - Location: Home/Category/Product - Display order for sorting |
Business Workflows
Order Lifecycle Workflow
stateDiagram-v2
[*] --> Pending: Create Order
Pending --> Processing: Payment Confirmed
Pending --> Cancelled: Payment Failed/User Cancel
Processing --> Shipped: Shipment Created
Processing --> Cancelled: Admin Cancel/Stock Issue
Shipped --> Delivered: Delivery Confirmed
Shipped --> Processing: Return to Processing
Delivered --> Returned: Return Requested
Delivered --> [*]: Order Complete
Cancelled --> [*]
Returned --> [*]
note right of Pending
- Cart converted to order
- Stock reserved
- Payment initiated
end note
note right of Processing
- Payment verified
- Stock deducted
- Shipment prepared
end note
note right of Shipped
- Tracking number assigned
- Customer notified
- In transit
end note
note right of Delivered
- Customer received
- Review invitation sent
- Transaction complete
end note
Cart to Order Conversion
sequenceDiagram
participant Customer
participant Cart
participant Order
participant Product
participant Payment
participant Notification
Customer->>Cart: Add items to cart
Cart->>Cart: Calculate totals
Customer->>Cart: Apply discount code
Cart->>Cart: Recalculate totals
Customer->>Order: Checkout
Order->>Cart: Get cart items
Order->>Product: Check stock availability
alt Stock Available
Product-->>Order: Confirm stock
Order->>Order: Create order items
Order->>Product: Decrease stock
Order->>Payment: Initiate payment
alt Payment Success
Payment-->>Order: Payment confirmed
Order->>Order: Update status to Processing
Order->>Cart: Clear cart
Order->>Notification: Send confirmation email
Notification-->>Customer: Email sent
else Payment Failed
Payment-->>Order: Payment failed
Order->>Product: Restore stock
Order->>Order: Update status to Cancelled
Order->>Notification: Send failure notification
end
else Stock Unavailable
Product-->>Order: Stock insufficient
Order-->>Customer: Show error message
end
Product Review Workflow
flowchart TD
Start([Customer submits review]) --> Validate{Validation}
Validate -->|Valid| CheckPurchase{Verified Purchase?}
Validate -->|Invalid| Error[Show validation errors]
CheckPurchase -->|Yes| MarkVerified[Mark as verified purchase]
CheckPurchase -->|No| MarkUnverified[Regular review]
MarkVerified --> SaveReview[Save to database]
MarkUnverified --> SaveReview
SaveReview --> Moderation{Auto-approve?}
Moderation -->|Yes - High trust user| Approve[Approve review]
Moderation -->|No - Needs moderation| Pending[Pending approval]
Approve --> UpdateProduct[Update product rating]
UpdateProduct --> NotifyUser[Notify customer]
NotifyUser --> End([Review published])
Pending --> AdminReview{Admin reviews}
AdminReview -->|Approve| Approve
AdminReview -->|Reject| Reject[Reject review]
Reject --> NotifyRejection[Notify customer]
NotifyRejection --> End
Business Validation Rules
Product Validation Rules
| Field | Validation Rule | Error Message (Persian) | Error Code |
|---|---|---|---|
| Name | Required, max 200 characters | نام محصول الزامی است | PRODUCT_NAME_REQUIRED |
| Slug | Required, unique, max 250 characters, URL-friendly | نام مستعار باید منحصر به فرد باشد | PRODUCT_SLUG_DUPLICATE |
| Price | Must be >= 0 or null | قیمت باید مثبت باشد | PRODUCT_INVALID_PRICE |
| DiscountPrice | Must be < Price if set | قیمت تخفیف باید کمتر از قیمت اصلی باشد | PRODUCT_INVALID_DISCOUNT |
| StockQuantity | Must be >= 0 | موجودی نمیتواند منفی باشد | PRODUCT_NEGATIVE_STOCK |
| Weight | Must be > 0 if set | وزن باید مثبت باشد | PRODUCT_INVALID_WEIGHT |
Order Validation Rules
| Field/Rule | Validation Rule | Error Message (Persian) | Error Code |
|---|---|---|---|
| OrderNumber | Required, unique, auto-generated | شماره سفارش معتبر نیست | ORDER_INVALID_NUMBER |
| Items | Must have at least 1 item | سفارش باید حداقل یک محصول داشته باشد | ORDER_EMPTY |
| Total | Must match calculated total | مجموع سفارش صحیح نیست | ORDER_TOTAL_MISMATCH |
| CustomerEmail | Required, valid email format | ایمیل معتبر نیست | ORDER_INVALID_EMAIL |
| CustomerPhone | Required, 11 digits starting with 09 | شماره موبایل معتبر نیست | ORDER_INVALID_PHONE |
| ShippingAddress | Required, max 500 characters | آدرس تحویل الزامی است | ORDER_ADDRESS_REQUIRED |
| Status Transition | Must follow valid state machine | تغییر وضعیت سفارش مجاز نیست | ORDER_INVALID_STATUS_CHANGE |
Cart Validation Rules
| Rule | Validation | Error Message (Persian) | Error Code |
|---|---|---|---|
| Item Quantity | Must be > 0 and <= product stock | تعداد درخواستی موجود نیست | CART_INSUFFICIENT_STOCK |
| Maximum Items | Cart cannot have > 50 unique items | حداکثر 50 محصول مختلف مجاز است | CART_MAX_ITEMS_EXCEEDED |
| Item Total Quantity | Total quantity cannot exceed 999 | تعداد کل محصولات بیش از حد مجاز است | CART_MAX_QUANTITY_EXCEEDED |
| Product Availability | Product must be active | این محصول دیگر موجود نیست | CART_PRODUCT_UNAVAILABLE |
| Discount Code | Must be active and within date range | کد تخفیف معتبر نیست | CART_INVALID_DISCOUNT |
Domain Events
Event-Driven Architecture
Domain events are raised by aggregate roots when important business events occur. Other parts of the system can listen to these events and react accordingly.
| Domain Event | Raised When | Event Handlers |
|---|---|---|
| OrderCreatedEvent | New order is placed successfully |
- Send order confirmation email - Create notification - Update analytics - Log audit trail |
| OrderStatusChangedEvent | Order status is updated |
- Send status update email - Create notification - Update inventory if cancelled - Trigger shipment if processing |
| PaymentCompletedEvent | Payment is successfully processed |
- Update order status - Send payment receipt - Create notification - Log payment audit |
| ProductStockChangedEvent | Product stock quantity changes |
- Check low stock threshold - Send low stock notification (admin) - Update search index - Invalidate cache |
| ProductReviewApprovedEvent | Product review is approved |
- Update product rating - Send notification to reviewer - Invalidate product cache - Award loyalty points |
| ShipmentCreatedEvent | Shipment is created for order |
- Send shipping notification email - Create tracking notification - Update order status - Log shipment details |
// Example Domain Event
public class OrderCreatedEvent : IDistributedEvent
{
public Guid OrderId { get; set; }
public string OrderNumber { get; set; }
public Guid? UserId { get; set; }
public decimal Total { get; set; }
public DateTime CreatedAt { get; set; }
}
// Example Event Handler
public class OrderCreatedEventHandler : IDistributedEventHandler<OrderCreatedEvent>
{
private readonly IEmailService _emailService;
private readonly INotificationService _notificationService;
public async Task HandleEventAsync(OrderCreatedEvent eventData)
{
// Send confirmation email
await _emailService.SendOrderConfirmationAsync(eventData.OrderId);
// Create in-app notification
await _notificationService.CreateNotificationAsync(
eventData.UserId,
NotificationType.OrderCreated,
$"سفارش {eventData.OrderNumber} با موفقیت ثبت شد"
);
}
}
Domain Services
When to Use Domain Services
Domain services contain business logic that doesn't naturally fit within a single entity or involves multiple aggregates.
| Domain Service | Responsibility | Key Methods |
|---|---|---|
| OrderDomainService | Complex order operations involving multiple entities |
CreateOrderFromCart(cart, address)CancelOrderAndRestoreStock(order)ProcessRefund(order, amount)
|
| PricingDomainService | Calculate prices with complex business rules |
CalculateProductPrice(product, user)ApplyVolumeDiscounts(items)CalculateShippingCost(cart, address)
|
| InventoryDomainService | Stock management across products and variants |
ReserveStock(productId, quantity)ReleaseStock(productId, quantity)TransferStock(fromId, toId, quantity)
|
| DiscountDomainService | Complex discount calculation logic |
CalculateApplicableDiscounts(cart)ValidateDiscountRules(code, cart)StackDiscounts(discounts)
|
// Example Domain Service
public class OrderDomainService : DomainService
{
private readonly IRepository<Product, Guid> _productRepository;
private readonly IRepository<OrderStatusHistory, Guid> _historyRepository;
public async Task<Order> CreateOrderFromCartAsync(Cart cart, UserAddress address)
{
// Validate cart
if (cart.IsEmpty())
throw new BusinessException("سبد خرید خالی است");
// Check stock availability for all items
foreach (var item in cart.Items)
{
var product = await _productRepository.GetAsync(item.ProductId);
if (!product.IsInStock() || product.StockQuantity < item.Quantity)
{
throw new BusinessException($"محصول {product.Name} موجود نیست");
}
}
// Create order
var order = new Order(GuidGenerator.Create(), await GenerateOrderNumberAsync());
order.UserId = cart.UserId;
order.CustomerName = address.RecipientName;
order.CustomerPhone = address.RecipientPhone;
order.ShippingAddress = address.GetFullAddress();
order.Status = OrderStatus.Pending;
// Add items and decrease stock
foreach (var cartItem in cart.Items)
{
var orderItem = new OrderItem(GuidGenerator.Create());
orderItem.ProductId = cartItem.ProductId;
orderItem.ProductName = cartItem.ProductName;
orderItem.Quantity = cartItem.Quantity;
orderItem.UnitPrice = cartItem.Price;
orderItem.LineTotal = cartItem.Quantity * cartItem.Price;
order.Items.Add(orderItem);
// Decrease stock
var product = await _productRepository.GetAsync(cartItem.ProductId);
product.DecreaseStock(cartItem.Quantity);
}
order.CalculateTotals();
return order;
}
public async Task CancelOrderAndRestoreStockAsync(Order order)
{
if (!order.CanBeCancelled())
throw new BusinessException("این سفارش قابل لغو نیست");
// Restore product stock
foreach (var item in order.Items)
{
var product = await _productRepository.GetAsync(item.ProductId);
product.IncreaseStock(item.Quantity);
}
// Update order status
order.UpdateStatus(OrderStatus.Cancelled, null, "لغو شده توسط سیستم");
}
}