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 == storeId
  • QuantityOnHand > 0
  • WarehouseId != null for warehouse fulfillment, WarehouseId == null for 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:

  1. Allocation plan is built: for each requested SKU, inventory rows are assigned quantities to cover the requested amount (BuildAllocationPlan for warehouse, BuildStoreFrontAllocationPlan for storefront).
  2. Order entity is created with:
FieldSource
OrganizationIdstore's organization
StoreIdfrom JWT
PublicIdgenerated short ID
DeliveryCompanyIdresolved in §3g
AddressProvinceCodeuppercased from request
AddressAreaIdfrom request
OrderValuesum(qty × unitPrice) across all items
DeliveryFeeOrderPricing:DefaultDeliveryFee config (default 0)
PaymentMethodOnline if paymentGateway set; else request value or Cash
CodAmountOrderValue + DeliveryFee when Cash; 0 when Online
StatusPending
PackageTyperequest packageType lowercased, or "standard"
  1. db.Orders.Add(order)SaveChangesAsync (order gets its Id).
  2. OrderItem rows are added for each SKU, referencing the first allocated InventoryItem.
  3. Inventory QuantityOnHand is decremented by allocated quantities.
  4. SaveChangesAsync persists 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:

FieldMeaning
ExternalOrderIdProvider's order ID (qr_id for Al-Waseet, uid for Boxy)
PushStatusSuccess, Failed
ErrorMessageReason for failure
PushedAtUTC 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

ConditionHTTPMessage
Subscription limit reached400subscription message
Warehouse fulfillment not in plan400feature message
Invalid province or area400"Province/area not found"
Invalid Iraq phone number400validation message
Unknown or inactive SKU400"One or more SKUs are not in your store catalog…"
Insufficient stock400"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 store400"The preferred warehouse was not found…"
Preferred DC not found / inactive400"The selected delivery company was not found or is inactive."
Preferred DC is external but store has no active link400"The selected delivery company was not found or is inactive."
Payment initiation failed400gateway 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

ValueInventory SourceWarehouse Required
warehouseInventoryItems with WarehouseId != nullYes — auto-selected or preferredWarehouseId
storefrontInventoryItems with WarehouseId == nullNo

10) Source References

LayerFile
ControllerSendy.Api/Controllers/V1/Store/StoreOrdersController.cs
Service interfaceSendy.Application/Interfaces/Services/Store/IStoreOrderService.cs
Service implementationSendy.Application/Services/Store/StoreOrderService.cs
Request DTOSendy.Application/DTOs/Requests/Store/CreateOrderRequest.cs
Order entitySendy.Domain/Entities/Orders/Order.cs
External delivery trackingSendy.Domain/Entities/Orders/OrderExternalDelivery.cs
Push dispatchSendy.Application/Services/StoreArea/StoreDeliveryProviderService.csTryPushOrderAsync
DC resolution helperStoreOrderService.ResolveNearestDeliveryCompanyIdAsync
Subscription guardSendy.Application/Services/Store/SubscriptionGuardService.cs
Address validationSendy.Application/Validation/OrderDeliveryAddressValidation.cs
Phone validationSendy.Application/Validation/IraqPhoneValidation.cs
Provider link entitySendy.Domain/Entities/Store/StoreDeliveryProviderLink.cs
Al-Waseet push detailal-waseet-delivery-provider-flow.md
Boxy push detailboxy-delivery-provider-flow.md

Source: DOCS/flows/store-create-order-flow.md