Skip to content

Core Webshop Endpoints

Legacy system

This page documents the remaining "core webshop" SOAP endpoints (per ZWE-392), alongside SubmitOrder. Schemas below are pulled directly from the live WSDL on the NAV test sandbox; example payloads are real responses captured from that sandbox (test data, not production).

The five remaining endpoints identified as core to the new webshop (see Legacy Integration):

Service Operation Direction Purpose (from spreadsheet)
NAV GetOrderStatus NAV → Web Order status updates from NAV
LS Retail GetProduct NAV → Web Full product + variants data
LS Retail GetSalesPrice NAV → Web Price list (by customer/currency/date)
LS Retail GetPeriodicDiscount NAV ↔ Web Periodic promotions (Multibuy, MixMatch, etc.)
Kei GetRealStock NAV → Web Stock quantity per item/variant/location

Pull, not push

All four buffer-style operations below (GetOrderStatus, GetProduct, GetSalesPrice, GetPeriodicDiscount) take the same queueEntryNo / pageIndex / pageSize request shape and return a cursor (return_value) to fetch the next page. This confirms the open question from ZWE-389: price and stock come in via pull, not push. GetRealStock is the exception — a direct single-item lookup, no paging.


GetProduct

Service: LS Retail (wsdlLs, Codeunit/LSSynchronization)

Request fields:

Field Type Notes
queueEntryNo int Cursor from the previous call's return_value; 0 for the first page
pageIndex int Page number
pageSize int Records per page
response (empty element) Must be present but empty in the request — NAV/ASMX quirk: omitting it causes a SOAP fault (Parameter response in method GetProduct ... is null!)

Response fields (LSItem, repeated per product):

Field Type Notes
ItemNo string
Name / ShortDescription string
FullDescription string optional
PriceIncludingVAT / Price decimal incl./excl. VAT
PriceIncludesVAT boolean
Cost / LastCost decimal
StockQuantity / StockThreshold decimal
Blocked boolean
ItemCategoryCode / ProductGroupCode / SpecialGroupCode string
Brand / BrandDescription / VendorNo / VendorName / VendorItemNo string
SeasonCode string
LSItemVariant[] nested Code, Description, StockQuantity2, Blocked, ColourCodeVendor, plus nested LSDimension[] (colour/size attribute dimensions — matches the PLP filter set: colour, size/KLE_CONVER)
LSItemTranslation[] nested per-language ShortDescription/FullDescription
LSRelatedItem[] nested related-item links
LSItemBarcodes[] nested barcodes

Example response (trimmed — real product from the test sandbox):

<GetProduct_Result xmlns="urn:microsoft-dynamics-schemas/codeunit/LSSynchronization">
  <return_value>59827</return_value>  <!-- cursor for next page -->
  <response>
    <LSItem>
      <ItemNo>001000003</ItemNo>
      <Name>fleece no.26</Name>
      <ShortDescription>fleece no.26</ShortDescription>
      <PriceIncludingVAT>299.95</PriceIncludingVAT>
      <Price>247.893</Price>
      <PriceIncludesVAT>false</PriceIncludesVAT>
      <Cost>184.05</Cost>
      <StockQuantity>0</StockQuantity>
      <ItemCategoryCode>KLEDING</ItemCategoryCode>
      <ProductGroupCode>BRK-HARD</ProductGroupCode>
      <Brand>FJALLRAVEN</Brand>
      <BrandDescription>Fjallraven</BrandDescription>
      <VendorItemNo>83248</VendorItemNo>
      <SeasonCode>8W</SeasonCode>
      <LSItemVariant>
        <Code>000</Code>
        <Description>L/DARK GREY</Description>
        <StockQuantity2>0</StockQuantity2>
        <Blocked>false</Blocked>
        <ColourCodeVendor>FJA-030</ColourCodeVendor>
        <!-- + LSDimension[] for colour/size attributes, LSItemVariantRegistration, LSBarcodes -->
      </LSItemVariant>
      <!-- + more LSItemVariant entries for other colour/size combos -->
    </LSItem>
    <!-- + more LSItem entries, up to pageSize -->
  </response>
</GetProduct_Result>

Error scenarios: SOAP fault (ServiceBrokerException) if the empty response element is omitted from the request — confirmed live. No other error cases tested yet.


GetSalesPrice

Service: LS Retail (wsdlLs, Codeunit/LSSynchronization)

Request fields: same shape as GetProduct (queueEntryNo, pageIndex, pageSize, empty response).

Response fields (LSSalesPrice, repeated per price record):

Field Type Notes
ItemNo string
SalesCode string e.g. ALL
CurrencyCode string optional, empty for base currency
UnitPrice decimal excl. VAT
UnitPriceIncludingVAT decimal incl. VAT — the only field marked min=1 (always present), rest are optional
PriceIncludesVAT boolean
VATBusPostingGrPrice string e.g. BINNENLAND
SalesType string numeric code, e.g. 1
MinimumQuantity decimal
StartingDate / EndingDate date optional, validity window
VariantCode string optional
AllowInvoiceDisc / AllowLineDisc boolean

Example response (real data from the test sandbox):

<GetSalesPrice_Result xmlns="urn:microsoft-dynamics-schemas/codeunit/LSSynchronization">
  <return_value>60987</return_value>
  <response>
    <LSSalesPrice>
      <ItemNo>001000013</ItemNo>
      <SalesCode>ALL</SalesCode>
      <UnitPrice>16.488</UnitPrice>
      <PriceIncludesVAT>false</PriceIncludesVAT>
      <AllowInvoiceDisc>true</AllowInvoiceDisc>
      <VATBusPostingGrPrice>BINNENLAND</VATBusPostingGrPrice>
      <SalesType>1</SalesType>
      <MinimumQuantity>0.00</MinimumQuantity>
      <AllowLineDisc>true</AllowLineDisc>
      <UnitPriceIncludingVAT>19.95</UnitPriceIncludingVAT>
    </LSSalesPrice>
    <LSSalesPrice>
      <ItemNo>001000014</ItemNo>
      <SalesCode>ALL</SalesCode>
      <UnitPrice>15.55</UnitPrice>
      <UnitPriceIncludingVAT>16.95</UnitPriceIncludingVAT>
      <!-- same shape as above -->
    </LSSalesPrice>
  </response>
</GetSalesPrice_Result>

Error scenarios: same response-element-required fault as GetProduct if omitted — confirmed live.


GetPeriodicDiscount

Service: LS Retail (wsdlLs, Codeunit/LSSynchronization)

Request fields: same shape as GetProduct/GetSalesPrice.

Response fields (LSPeriodicDiscount, repeated per discount/offer):

Field Type Notes
No / Description string
Status / Type / OfferType string
PriceGroup string
Priority int
StartingDate / EndingDate date validity window
BlockPeriodicDiscount boolean
AmountToTrigger decimal Multibuy threshold
DiscountType string
DealPriceValue / DiscountAmountValue / DiscountPercValue decimal
LSPeriodicDiscountLine[] nested per-item discount lines: No, VariantCode, StandardPrice(IncludingVAT), OfferPrice(IncludingVAT), DiscType, Status
LSMultibuyDiscountLine[] nested MinQuantity, Discount, CurrencyCode — the Multibuy mechanics mentioned in the legacy operations table

Example response (real active discount from the test sandbox):

<GetPeriodicDiscount_Result xmlns="urn:microsoft-dynamics-schemas/codeunit/LSSynchronization">
  <return_value>421</return_value>
  <response>
    <LSPeriodicDiscount>
      <No>P1006</No>
      <Description>Korting 30% Geen punten</Description>
      <Status>1</Status>
      <Type>6</Type>
      <PriceGroup>ALL</PriceGroup>
      <OfferType>8</OfferType>
      <Priority>50</Priority>
      <BlockPeriodicDiscount>false</BlockPeriodicDiscount>
      <DiscountPercValue>30.00</DiscountPercValue>
      <LSPeriodicDiscountLine>
        <OfferNo>P1006</OfferNo>
        <LineNo>10000</LineNo>
        <DealPriceDiscPerc>30.00</DealPriceDiscPerc>
        <PriceGroup>ALL</PriceGroup>
        <Status>1</Status>
      </LSPeriodicDiscountLine>
      <LSMultibuyDiscountLine>
        <MinQuantity>5</MinQuantity>
        <Discount>20</Discount>
      </LSMultibuyDiscountLine>
    </LSPeriodicDiscount>
    <!-- + more LSPeriodicDiscount entries, up to pageSize -->
  </response>
</GetPeriodicDiscount_Result>

Error scenarios: same response-element-required fault as GetProduct if omitted — confirmed live.

Multibuy is synced but never applied to the displayed price

Confirmed against the legacy codebase (K3_periodicDiscount::getDiscount(), application/models/K3_periodicDiscount.php): LSMultibuyDiscountLine data (the MinQuantity/Discount mechanics, e.g. "5+ units → 20% off") is synced from NAV and persisted, but getDiscount() — the method the legacy website actually uses to compute a displayed price — filters on OfferType => offertype_DiscOffer only. Multibuy/MixMatch offers are stored but never turned into a computed webshop price; no other code reads NoOfItemsNeeded.

This is a real open decision for the Laravel migration: is multibuy pricing POS/NAV-only by design, or a genuine gap that should show on the webshop? Needs confirming before the connection is built, not assumed either way.

\"No points\" maps to MemberPoints / BlockMemberPoints

A discount description like "Korting 30% Geen punten" ("30% off, no points") is captured via the loyalty link, not a separate field: K3_periodicDiscountLine::$MemberPoints (inbound) and BlockMemberPoints (outbound, PeriodicDiscount.php). This is the same loyalty/member-account infrastructure documented in Legacy Integration — real and actively synced, but decoupled from the order-submit/order-status flow (see the GetOrderStatus note below).


GetRealStock

Service: Kei (wdslKei, Codeunit/KeiSynchronization)

Unlike the four operations above, this is a direct single-item lookup, not a paged buffer read.

Request fields:

Field Type Notes
itemNo string required
code string variant code — required by the schema but accepted empty in testing

Response fields:

Field Type Notes
return_value int This is the entire response — no nested stock object. Given the request is a single item+variant lookup, this value is most likely the stock quantity itself, not a status/error code — needs confirming with a known-in-stock item, since our test item returned 0.

Example response (real call, itemNo=001000023, code empty):

<GetRealStock_Result xmlns="urn:microsoft-dynamics-schemas/codeunit/KeiSynchronization">
  <return_value>0</return_value>
</GetRealStock_Result>

Error scenarios: not yet triggered — the call succeeded even with an empty code, so unclear yet what NAV does with an invalid itemNo.


GetOrderStatus

Service: NAV (wdslNav, Codeunit/NavSynchronization)

Request fields: same paged-buffer shape as GetProduct (queueEntryNo, pageIndex, pageSize, empty response).

Response fields (NavOrderStatus, repeated per order):

Field Type Notes
OrderId int matches NavOrder->Id from SubmitOrder — NAV's own counter, not the website order number
OrderStatus int see the status constants table in SubmitOrder
ShippingStatus int same constants table
PaymentStatus int same constants table

Example response (real order already in the queue — no test order needed to be submitted):

Not anonymized, and not customer/payment data

OrderId=19 is NAV's own internal order counter from the test sandbox (zwerfkei_test_up) — not a customer id, address, or payment detail. Per the sensitive data handling decision recorded on ZWE-503, test-sandbox identifiers are out of scope for the anonymization requirement; only real production customer/order/payment PII would need it, and none was ever captured for this milestone.

<GetOrderStatus_Result xmlns="urn:microsoft-dynamics-schemas/codeunit/NavSynchronization">
  <return_value>124864</return_value>
  <response>
    <NavOrderStatus>
      <OrderId>19</OrderId>
      <OrderStatus>20</OrderStatus>     <!-- Processing -->
      <ShippingStatus>40</ShippingStatus> <!-- Shipped -->
      <PaymentStatus>30</PaymentStatus>   <!-- Paid -->
    </NavOrderStatus>
  </response>
</GetOrderStatus_Result>

Processing, not Complete — resolved: NAV overrides the status internally, it's advisory only

SubmitOrder always sends OrderStatus=30 (Complete) — confirmed the only two places OrderStatusId is ever set in the entire legacy codebase (Order_positive.php/Order_negative.php) hardcode 30/40/30, with no code path anywhere sending 20 (Processing).

Not POS-related — ruled out. The noodkassa (emergency till) flow creates a regular Order and goes through the exact same sendOrderToNav()/SubmitOrder payload as normal checkout. Real POS terminal sales are a separate, one-way inbound flow (GetTransactions) that never touches OrderStatusId at all.

What actually happens: NAV computes/manages OrderStatus internally (its own picking/invoicing workflow) and effectively ignores the value sent with SubmitOrder — confirmed via a real captured NAV response for an order the webshop submitted as 30/Complete, which NAV reports back as 20/Processing. The status sent by the webshop is advisory only. The legacy website itself never notices this: Process::getOrderStatus() only ever checks PaymentStatus == 30; OrderStatus/ShippingStatus are read but never acted on or persisted (no k3_order_status table exists).

Real decision for the Laravel migration: do we want the new backend to actually persist/act on OrderStatus/ShippingStatus from NAV, or keep reacting only to PaymentStatus as the legacy site does today? Not yet decided — flagged for the technical approach review.

Error scenarios: same response-element-required pattern expected, not yet confirmed for this specific operation.


SubmitOrder Legacy Integration K3 Data Models