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.