Skip to content

fulfillment

Delivery and pickup quoting, and the list of pickup points. Both calls require an authenticated customer, including the pickup-locations list, which is not public.

ts
mawjod.fulfillment.quotes(input)
mawjod.fulfillment.pickupLocations()

fulfillment.quotes()

ts
quotes(input: FulfillmentQuoteInput): Promise<FulfillmentQuote>

POST /api/v1/customer/fulfillment/quotes. Prices a delivery or a pickup and returns the ETA plus the payment methods allowed for it.

ts
interface FulfillmentQuoteInput {
  method: 'delivery' | 'pickup'
  subtotal_minor: number              // tax-inclusive cart subtotal, >= 0
  address_id?: string | null          // a saved address owned by the caller
  pickup_location_id?: string | null
  position?: GeoPosition | null       // an ad-hoc destination
}
ts
const quote = await mawjod.cart.quote()

const delivery = await mawjod.fulfillment.quotes({
  method: 'delivery',
  subtotal_minor: quote.discounted_subtotal.minor,
  address_id: address.id,
})

subtotal_minor is minor units. See Money. Feed it CartQuote.discounted_subtotal.minor so the free-delivery threshold is evaluated against what the shopper will actually pay.

For a buyer who has not saved an address yet, pass position instead of address_id:

ts
await mawjod.fulfillment.quotes({
  method: 'delivery',
  subtotal_minor: quote.discounted_subtotal.minor,
  position: { longitude: 31.2357, latitude: 30.0444 },
})

For pickup, pass pickup_location_id.

FulfillmentQuote

ts
interface FulfillmentQuote {
  method: 'delivery' | 'pickup'
  zone_id: string | null
  pickup_location_id: string | null
  rule_version: number
  subtotal: Money
  fee: Money
  minimum_order: Money
  free_threshold: Money | null
  free_threshold_applied: boolean
  eta: { minimum_minutes: number; maximum_minutes: number }
  allowed_payment_methods: string[]
}

free_threshold is null when the store has no free-delivery threshold at all. Do not render a "spend X more for free delivery" prompt without checking for that.

minimum_order is the smallest order this zone or pickup point accepts. Compare it against the cart subtotal before letting the shopper move on.

allowed_payment_methods is the authoritative list for this quote, narrower than the store-wide set from store.settings(), because a particular pickup point may accept less than the store does in general. When you have a quote, render from it.

Outside the service area

ts
import { isMawjodApiError } from '@mawjod/api'

try {
  await mawjod.fulfillment.quotes(input)
} catch (error) {
  if (isMawjodApiError(error) && error.code === 'outside_service_area') {
    // The destination is outside every active zone. Offer pickup instead.
  } else {
    throw error
  }
}

422 outside_service_area means the destination is outside every active delivery zone. Show it on the address step, not on the payment step. It is a fact about the address, and the shopper should find out while they are still looking at addresses.

fulfillment.pickupLocations()

ts
pickupLocations(): Promise<PickupLocation[]>

GET /api/v1/customer/fulfillment/pickup-locations. Active pickup locations. Not paginated: a bare array.

ts
interface PickupLocation {
  id: string
  name_ar: string
  name_en: string
  address_line: string
  contact_phone: string | null
  ready: { minimum_minutes: number; maximum_minutes: number }
  allowed_payment_methods: string[]
  operating_windows: OperatingWindow[]
  collection_instructions_ar: string | null
  collection_instructions_en: string | null
  is_active: boolean
  area: AdministrativeAreaRef | null
  created_at: string | null
  updated_at: string | null
}

name_ar / name_en and collection_instructions_ar / collection_instructions_en carry both locales; pick by the locale you are rendering.

operating_windows is the weekly collection schedule, and a theme renders the opening hours from it:

ts
interface OperatingWindow {
  day: number      // 0 to 6
  opens: string    // "HH:MM"
  closes: string   // "HH:MM"
}

An empty array means the location is always open. That is a statement, not a gap in the data, so render it as "open 24 hours" rather than hiding the hours or reading it as closed. ready is the separate field, and is the one to show as "ready in 30 to 60 minutes".

is_active is on the shape, but the endpoint only returns active locations, so filtering on it should never change anything.

Errors

CodeStatusWhere
outside_service_area422quotes
unauthenticated401both
validation_failed422both
rate_limited429both
store_unavailable503both

In Nuxt

ts
const { pickupLocations, quote, lastQuote, quoting, quoteError } = useFulfillment()

Pickup locations are page data and load on setup; a quote is a POST that depends on the chosen address, so it is invoked.

TIP

useFulfillment() fetches pickup locations on setup. Pass { immediate: false } when a page only needs quote():

ts
const { quote } = useFulfillment({ immediate: false })

See Composables → useFulfillment.