Flows
Create Order Flow
How orders are created, validated, and pushed to delivery.
Store Create Order Flow
Describes the full lifecycle of POST api/v1/store/orders — from request validation through inventory allocation, delivery company resolution, optional external provider push, and optional online payment initiation.
1) Architecture Overview
Store JWT → StoreOrdersController
│
▼
StoreOrderService.CreateOrderAsync
│
┌─────────┼─────────────────────┐
▼ ▼ ▼
SubscriptionGuard InventoryAllocation DeliveryCompanyResolution
│
┌─────────┴──────────┐
▼ ▼
Internal DC External DC
(same org) (StoreDeliveryProviderLink)
│
StoreDeliveryProviderService
.TryPushOrderAsync
│
┌───────────┴───────────┐
▼ ▼
Boxy API Al-Waseet API
(PushOrderToBoxyAsync) (PushOrderToAlWaseetAsync)
│
OrderExternalDelivery
Permission required: store.orders.create
2) Request Body
POST api/v1/store/orders
{
"fulfillmentType": "warehouse", // "warehouse" | "storefront"
"customerName": "Ahmed Ali",
"customerPhone": "07701234567",
"customerAddress": "Al-Karrada, Building 5",
"addressProvinceCode": "BGH", // from address reference API
"addressAreaId": "<uuid>", // from address reference API
"deliveryLat": 33.3152,
"deliveryLng": 44.3661,
"items": [
{ "sku": "SHIRT-L-RED", "quantity": 2 },
{ "sku": "PANTS-32", "quantity": 1 }
],
"packageType": "standard", // optional; default "standard"
"notes": "Handle with care", // optional
"paymentMethod": "cash", // optional; default "cash"; ignored if paymentGateway is set
"preferredDeliveryCompanyId": "<uuid>", // optional; auto-resolved by nearest warehouse if omitted
"preferredWarehouseId": "<uuid>", // optional; warehouse fulfillment only
"paymentGateway": "QiCard", // optional; triggers online payment session
"returnUrl": "https://mystore.com/ty" // required when paymentGateway is set
}
3) Validation Pipeline
Steps execute sequentially; any failure returns early with HTTP 400.
3a — Subscription Guard
SubscriptionGuard.EnsureCanCreateOrdersAsync — verifies the store's active subscription allows creating one more order. If fulfillmentType == "warehouse", a second check (EnsureCanUseWarehouseFulfillmentAsync) confirms the plan includes warehouse fulfillment.
3b — Address Validation
OrderDeliveryAddressValidation.ValidateProvinceAndAreaAsync — confirms addressProvinceCode exists in AddressProvinces and addressAreaId exists under that province.
3c — Phone Canonicalization
IraqPhoneValidation.TryGetCanonical — normalizes the customer phone to +964… format. Rejects invalid Iraqi numbers with HTTP 400.
3d — SKU Validation
All requested SKUs are checked against StoreProducts for the store (IsActive && !IsDeleted). Any unknown or inactive SKU is rejected, listing the offending SKUs in the error response.
3e — Inventory Check
InventoryItems are loaded filtered by:
StoreId == storeIdQuantityOnHand > 0WarehouseId != nullfor warehouse fulfillment,WarehouseId == nullfor storefront
For each SKU the total available quantity across all matching rows must cover the requested quantity, otherwise HTTP 400.
3f — Warehouse Selection (warehouse fulfillment only)
If preferredWarehouseId is set: the warehouse must exist, be active, and be owned by the store (WarehouseOwnerType.Store) and have stock for the requested items.
If omitted: all candidate warehouses (from inventory rows) that have coordinates are ranked by distance to (deliveryLat, deliveryLng) via IDistanceService. The closest one is selected. Fails if no warehouse has coordinates.
3g — Delivery Company Resolution
preferredDeliveryCompanyId provided?
├── YES → Load DC (IsActive, !IsDeleted)
│ ├── dc.OrganizationId == storeOrgId → INTERNAL (allowed)
│ └── dc.Slug != null
│ && active StoreDeliveryProviderLink for this store → EXTERNAL (allowed)
│ └── neither → 400 "not found or inactive"
└── NO → ResolveNearestDeliveryCompanyIdAsync
(nearest DC-owned warehouse to delivery coordinates; may return null)
ResolveNearestDeliveryCompanyIdAsync queries all warehouses with OwnerType == DeliveryCompany that are active and have coordinates, calculates distance from each to the customer location, and returns the DeliveryCompanyId of the closest warehouse. Returns null if no DC warehouses exist.
4) Order & Item Creation
After all validations pass:
- Allocation plan is built: for each requested SKU, inventory rows are assigned quantities to cover the requested amount (
BuildAllocationPlanfor warehouse,BuildStoreFrontAllocationPlanfor storefront). Orderentity is created with:
| Field | Source |
|---|---|
OrganizationId | store's organization |
StoreId | from JWT |
PublicId | generated short ID |
DeliveryCompanyId | resolved in §3g |
AddressProvinceCode | uppercased from request |
AddressAreaId | from request |
OrderValue | sum(qty × unitPrice) across all items |
DeliveryFee | OrderPricing:DefaultDeliveryFee config (default 0) |
PaymentMethod | Online if paymentGateway set; else request value or Cash |
CodAmount | OrderValue + DeliveryFee when Cash; 0 when Online |
Status | Pending |
PackageType | request packageType lowercased, or "standard" |
db.Orders.Add(order)→SaveChangesAsync(order gets itsId).OrderItemrows are added for each SKU, referencing the first allocatedInventoryItem.- Inventory
QuantityOnHandis decremented by allocated quantities. SaveChangesAsyncpersists items and inventory changes.
5) External Provider Push
StoreDeliveryProviderService.TryPushOrderAsync(storeId, order, ct) is called immediately after the order is saved. This never throws — all errors are caught and logged.
order.DeliveryCompanyId is null? → skip (no DC assigned)
StoreDeliveryProviderLink exists for (storeId, DeliveryCompanyId)?
└── NO → skip (internal DC or no link)
└── YES → branch on DeliveryCompany.Slug
├── "boxy" → PushOrderToBoxyAsync
└── "al-waseet" → PushOrderToAlWaseetAsync
An OrderExternalDelivery row is written in all cases (Success / Failed) recording:
| Field | Meaning |
|---|---|
ExternalOrderId | Provider's order ID (qr_id for Al-Waseet, uid for Boxy) |
PushStatus | Success, Failed |
ErrorMessage | Reason for failure |
PushedAt | UTC timestamp when push succeeded |
See al-waseet-delivery-provider-flow.md §4 and boxy-delivery-provider-flow.md §4 for the detailed field mappings used during push.
A failed push does not roll back or cancel the Sendy order — it remains Pending. The store can retry via POST api/v1/store/delivery-providers/orders/{orderId}/sync-status or by re-pushing manually.
6) Online Payment Initiation (optional)
If paymentGateway is set, InitiateOnlinePaymentAsync is called after the external push. On success, payment_url is returned in the response and the store should redirect the customer there. On failure the endpoint returns HTTP 400 — the order has already been saved but no payment session exists; the store may retry.
Supported gateways: QiCard, AsiaPay, ZainCash, Wayl.
7) Success Response
{
"success": true,
"message": "Order created successfully",
"data": {
"id": "<uuid>",
"public_id": "ORD-XXXX",
"status": "pending",
"warehouse_id": "<uuid or null>",
"order_value": 35000,
"delivery_fee": 5000,
"cod_amount": 40000,
"fulfillment_type": "warehouse",
"payment_method": "cash",
"delivery_company_id": "<uuid or null>",
"delivery_lat": 33.3152,
"delivery_lng": 44.3661,
"payment_url": null
}
}
payment_url is non-null only when paymentGateway was set and initiation succeeded.
8) Error Cases
| Condition | HTTP | Message |
|---|---|---|
| Subscription limit reached | 400 | subscription message |
| Warehouse fulfillment not in plan | 400 | feature message |
| Invalid province or area | 400 | "Province/area not found" |
| Invalid Iraq phone number | 400 | validation message |
| Unknown or inactive SKU | 400 | "One or more SKUs are not in your store catalog…" |
| Insufficient stock | 400 | "Insufficient stock for one or more SKUs." |
| No warehouse with coordinates (auto-select) | 400 | "Candidate warehouses are missing location coordinates." |
| Preferred warehouse not found / wrong store | 400 | "The preferred warehouse was not found…" |
| Preferred DC not found / inactive | 400 | "The selected delivery company was not found or is inactive." |
| Preferred DC is external but store has no active link | 400 | "The selected delivery company was not found or is inactive." |
| Payment initiation failed | 400 | gateway error message |
External push failures are not surfaced as HTTP errors — the order is created and the failure is recorded in OrderExternalDelivery.ErrorMessage.
9) Fulfillment Types
| Value | Inventory Source | Warehouse Required |
|---|---|---|
warehouse | InventoryItems with WarehouseId != null | Yes — auto-selected or preferredWarehouseId |
storefront | InventoryItems with WarehouseId == null | No |
10) Source References
| Layer | File |
|---|---|
| Controller | Sendy.Api/Controllers/V1/Store/StoreOrdersController.cs |
| Service interface | Sendy.Application/Interfaces/Services/Store/IStoreOrderService.cs |
| Service implementation | Sendy.Application/Services/Store/StoreOrderService.cs |
| Request DTO | Sendy.Application/DTOs/Requests/Store/CreateOrderRequest.cs |
| Order entity | Sendy.Domain/Entities/Orders/Order.cs |
| External delivery tracking | Sendy.Domain/Entities/Orders/OrderExternalDelivery.cs |
| Push dispatch | Sendy.Application/Services/StoreArea/StoreDeliveryProviderService.cs → TryPushOrderAsync |
| DC resolution helper | StoreOrderService.ResolveNearestDeliveryCompanyIdAsync |
| Subscription guard | Sendy.Application/Services/Store/SubscriptionGuardService.cs |
| Address validation | Sendy.Application/Validation/OrderDeliveryAddressValidation.cs |
| Phone validation | Sendy.Application/Validation/IraqPhoneValidation.cs |
| Provider link entity | Sendy.Domain/Entities/Store/StoreDeliveryProviderLink.cs |
| Al-Waseet push detail | al-waseet-delivery-provider-flow.md |
| Boxy push detail | boxy-delivery-provider-flow.md |
Source: DOCS/flows/store-create-order-flow.md