Skip to content

nShift Checkout — Shipping Module

If you're building a Hantera app that needs nShift shipping options on its own ingress — for example a storefront-checkout bridge that renders carrier products inside a third-party checkout iframe — you can import the public getOptions function from the nShift Checkout app's Filtrera module.

OAuth tokens, nShift sessions, and per-session caching are handled for you behind a session ticket actor. You call one function, get back options, render them, and on selection write two delivery dynamic fields — the app's rules take it from there.

Module path

apps/nshift-checkout/nshift.module.hrc

Declaring the dependency

A consumer app cannot import this module by path alone. Declare a requires.modules entry in your h_app.yaml, naming the module path and the export type you use. The declaration is type-checked against the producer's real source when your app is activated.

yaml
requires:
  modules:
    apps/nshift-checkout/nshift.module.hrc:
      exports:
        getOptions:
          type: >-
            (sessionActorId: uuid | nothing, request: {
              totalPrice: number, totalVolumeCm3: number, totalWeightKg: number,
              localeId: text, currencyCode: text, languageCode: text,
              packages: [value], variables: { text -> value },
              receiver: {
                name: text, address1: text, postalCode: text,
                city: text, country: text, phone: text | nothing,
                email: text | nothing
              }
            }, context: {
              delivery: {
                deliveryId: uuid | nothing, deliveryAddress: value,
                dynamic: { text -> value },
                lines: [{
                  orderLineId: uuid | nothing,
                  productNumber: text | nothing, quantity: number,
                  dynamic: { text -> value }, taxFactor: number | nothing
                }],
                order: {
                  orderId: uuid | nothing, channelKey: text,
                  currencyCode: text, locale: text | nothing,
                  dynamic: { text -> value } 
                }
              }
            } | nothing) => {
              sessionActorId: uuid,
              options: [{
                optionId: text, sessionActorId: uuid,
                shippingProductNumber: text, carrierId: text,
                carrierProductId: text, name: text, title: text | nothing,
                price: number, originalPrice: number | nothing,
                shippingTax: number | nothing,
                shippingTaxFactor: number | nothing,
                priceDescription: text | nothing,
                logoUrl: text | nothing, texts: [text] | nothing,
                valid: bool | nothing,
                additionalValues: { text -> value } | nothing,
                pickupPoints: [value] | nothing, raw: { text -> value }
              }]
            } | { error: { code: text, message: text, details: value } }

Inline the record shapes

Cross-app module contracts don't support named-type aliasing. Inline the request, context, and option record shapes in the requires.modules block — as above — rather than referencing NShiftCheckoutShippingRequest or NShiftCheckoutVariablesContext by name. The producer's real source is the source of truth and is type-checked against your declaration at activation.

See Declaring App Dependencies for the surrounding context.

Importing

filtrera
import {
  getOptions
} from 'apps/nshift-checkout/nshift.module.hrc'

getOptions(sessionActorId, request, context)

The single function you call.

filtrera
getOptions(
  sessionActorId: uuid | nothing,
  request: NShiftCheckoutShippingRequest,
  context: NShiftCheckoutVariablesContext | nothing
) => NShiftCheckoutOptionsResult | { error: { code: text, message: text, details: value } }
ParameterTypeDescription
sessionActorIduuid | nothingThe id of an existing nShiftCheckoutSession ticket, if any. Pass nothing on the first call and the value returned by a previous call on subsequent calls.
requestNShiftCheckoutShippingRequestThe lookup request — totals, locale, receiver, and provider-specific variables. Must include channelKey (or nshiftCheckoutConnectionId) in variables. See below.
contextNShiftCheckoutVariablesContext | nothingThe delivery-rooted context the hooks operate on. See Context.

Context

context is the same delivery-rooted record consumed by the OnNShiftCheckoutVariables hook (delivery + its order + order lines, each with dynamic). It is used internally for two things:

  1. Variable resolution. getOptions fires OnNShiftCheckoutVariables with this context, collects listener-emitted variables, and seeds channelKey. Any keys you pass in request.variables win over hook-resolved values for the same key.
  2. Shipping-tax resolution. getOptions fires the OnNShiftCheckoutShippingOptionTax hook with this context plus the freshly fetched options, then stamps each option with the resolved shippingTax / shippingTaxFactor.

context is optional to keep the surface forward-compatible. Pass it whenever you have one. If you pass nothing:

  • The variables hook does not fire (only request.variables you supply directly are used).
  • The shipping-tax hook does not fire, and options come back with shippingTax and shippingTaxFactor both nothing.

Both delivery.deliveryId and delivery.order.orderId are uuid | nothing, to allow fetching options before an order is creation, for example for a cart/checkout scenario.

Returns either an NShiftCheckoutOptionsResult on success or an error record on failure. See Errors.

The function decides whether to create a new nShift session, refresh an existing one in place, or simply return previously cached options based on the supplied sessionActorId and a fingerprint of request. Consumers don't need to think about the underlying session-ticket model.

Resolving the connection

The module resolves the nShift checkout connection id from one of two places, in order:

  1. Direct override: request.variables.nshiftCheckoutConnectionId (text).
  2. Channel registry lookup: if the request includes request.variables.channelKey, the module reads channels/{channelKey}.nshiftCheckoutConnectionId.

A storefront-facing consumer typically passes channelKey and lets the channel registry resolve the connection. A test or admin caller may pass nshiftCheckoutConnectionId directly.

If neither resolves to a connection id, the function returns the NSHIFT_NO_CHECKOUT_CONNECTION error.

Request

filtrera
{
  totalPrice: number
  totalVolumeCm3: number
  totalWeightKg: number
  localeId: text
  currencyCode: text
  languageCode: text
  packages: [value]
  variables: { text -> value }
  receiver: {
    name: text
    address1: text
    postalCode: text
    city: text
    country: text
    phone: text | nothing
    email: text | nothing
  }
}
FieldDescription
totalPriceCart/delivery total used by nShift's freemium / threshold rules. Pass 0 if not applicable.
totalVolumeCm3Total volume in cm³. Pass 0 if you don't track volume.
totalWeightKgTotal weight in kg. Pass 0 if you don't track weight.
localeIdLocale identifier (e.g. sv-SE). nShift uses it for translated option names and currency/decimal formatting.
currencyCodeISO 4217 currency code.
languageCodeISO 639-1 language code (e.g. sv). Usually the first segment of localeId.
packagesPer-package details (dimensions, content classification). Provider-defined; pass [] if not used.
variablesProvider-specific variables (e.g. fromwarehouse). Must include channelKey or nshiftCheckoutConnectionId. See OnNShiftCheckoutVariables.
receiverDestination address. phone and email are optional.

Successful result

filtrera
{
  sessionActorId: uuid
  options: [NShiftCheckoutOption]
}
FieldDescription
sessionActorIdThe id of the nShiftCheckoutSession ticket actor that backs this set of options. Pass it on the next call to getOptions to reuse or refresh the session efficiently.
optionsThe available options, in the order returned by nShift. Each option also carries sessionActorId so a UI can pass the two selection keys forward atomically.

NShiftCheckoutOption

filtrera
{
  optionId: text
  sessionActorId: uuid
  shippingProductNumber: text
  carrierId: text
  carrierProductId: text
  name: text
  title: text | nothing
  price: number
  originalPrice: number | nothing
  shippingTax: number | nothing
  shippingTaxFactor: number | nothing
  priceDescription: text | nothing
  logoUrl: text | nothing
  texts: [text] | nothing
  valid: bool | nothing
  additionalValues: { text -> value } | nothing
  pickupPoints: [value] | nothing
  raw: { text -> value }
}
FieldDescription
optionIdnShift's own unique id for this option within the session. This is the selection key — pair it with sessionActorId to select.
sessionActorIdRepeats the result-level sessionActorId per option so it can ride along in a single UI blob.
shippingProductNumberStable id formed as <carrierId>-<carrierProductId>. Suitable for display or as a public-facing product number.
carrierIdnShift carrier id.
carrierProductIdnShift carrier product id within the carrier.
nameDisplay name (localized to localeId).
titleAlternate display title, when nShift provides one.
priceEffective price.
originalPricePre-discount price, when the option is on offer.
shippingTaxAbsolute shipping tax amount in the request's currency, when resolved. Takes precedence over shippingTaxFactor. See below.
shippingTaxFactorShipping tax as a factor (e.g. 0.25 for 25%), when resolved. See below.
priceDescriptionShort price annotation (e.g. Free over 500 SEK).
logoUrlCarrier logo URL.
textsAdditional descriptive lines.
validWhether nShift considers the option currently valid for this session.
additionalValuesProvider-specific extra fields.
pickupPointsPickup points for parcel-shop options. Pass through verbatim; surface a picker if your carrier requires one.
rawThe unmodified option record from nShift. Useful when you need a field this type doesn't expose.

Shipping tax resolution

shippingTax and shippingTaxFactor are mutually exclusive: at most one is set on a given option, and both may be nothing when no rate or amount could be determined. shippingTax is an absolute amount and takes precedence over shippingTaxFactor (a factor in [0, 1]).

For each option, getOptions resolves the effective tax in this order:

  1. nShift's own taxRate (when present in the option payload) — used as shippingTaxFactor.
  2. OnNShiftCheckoutShippingOptionTax hook emission for this optionId — an emitted shippingTax wins over a shippingTaxFactor.
  3. Line-derived defaultshippingTaxFactor = max(taxFactor) across the context's order lines. Skipped when no line carries a positive taxFactor.
  4. Otherwise — both fields stay nothing.

See OnNShiftCheckoutShippingOptionTax Hook for how to compute custom rates. Because the hook only fires when a context is supplied, options come back with both fields nothing if you call getOptions with context = nothing.

Selection

Selection is not a separate module function. To select an option, write two fields onto the relevant delivery's dynamic fields (the field names are the contract):

filtrera
{
  nShiftCheckoutSessionActorId = <option.sessionActorId>
  nShiftCheckoutOptionId       = <option.optionId>
}

When those two fields land on a delivery (typically because the portal's select-product flow ran, or because commerce copied them from cart.deliveryDynamicFields during cart-to-order conversion), the app's bridge rule:

  1. Records the selection on the session ticket and completes it.
  2. The session's OnTicketComplete rule then writes the rich option fields onto the delivery (shippingProductNumber, shippingPrice, shippingDescription, carrier metadata) and clears the two selection keys.
  3. The same rule schedules the createPartialShipment job, which dispatches the shipment to nShift after commit.

Optional fields the bridge will forward to the session ticket if present alongside the two selection keys:

FieldTypeUsed for
nShiftCheckoutPickupPointIdtextCarriers that require a pickup point. If omitted, the shipment job falls back to the first pickup point on the selected option, if any.
nShiftCheckoutTimeSlotIdtextCarriers that support time slots.

Errors

On any failure, the function returns an error record of shape { error: { code, message, details } }. Handle it in your caller — don't assume the result is always a successful one.

codeWhen it happens
NSHIFT_NO_CLIENT_IDThe app's clientId is unset — finish setup before calling.
NSHIFT_NO_CLIENT_SECRETThe app's clientSecret is unset.
NSHIFT_NO_CHECKOUT_CONNECTIONNo checkout connection id could be resolved from request.variables or the channel registry.
NSHIFT_TOKEN_FAILEDOAuth token request failed (network error or non-200 from nShift).
NSHIFT_SESSION_FAILEDSession creation returned a non-200 or an unexpected payload from nShift.
NSHIFT_OPTIONS_FAILEDShipping-options request returned a non-200 or an unexpected payload from nShift.
NSHIFT_TICKET_WRITE_FAILEDFailed to persist the session ticket (e.g. transient platform error).

details carries the underlying response when available — useful when surfacing or logging the failure.

Example: a storefront-facing ingress

A typical pattern is to expose nShift as a small HTTP ingress your storefront posts to. With structured body parsing the host parses the JSON body and binds each top-level field to a matching param of the declared type — so you don't need to deserialize manually. See HTTP Ingress Parameters for the full mechanism.

filtrera
import 'text'
import { getOptions } from 'apps/nshift-checkout/nshift.module.hrc'

param channelKey: text
param sessionActorId: uuid | nothing
param currencyCode: text
param localeId: text
param totalPrice: number
param receiver: {
  name: text
  address1: text
  postalCode: text
  city: text
  country: text
  phone: text | nothing
  email: text | nothing
}

let languageCode = localeId explode '-' first match
  (l: text) |> l
  |> localeId

// Inject `channelKey` so the module resolves the checkout connection id
// from the channel registry.
let request = {
  totalPrice = totalPrice
  totalVolumeCm3 = 0
  totalWeightKg = 0
  localeId = localeId
  currencyCode = currencyCode
  languageCode = languageCode
  packages = []
  variables = { channelKey = channelKey }
  receiver = receiver
}

// A pre-order storefront flow has no delivery yet, so pass `nothing` for
// context. The OnNShiftCheckoutVariables and OnNShiftCheckoutShippingOptionTax
// hooks don't fire, and options come back without shipping tax. If you can
// build a delivery-rooted context (see the variables-hook page), pass it here
// to enable merchant variable rules and per-option shipping tax.
let result = getOptions(sessionActorId, request, nothing)

from result match
  { error: { code: text } } |> {
    statusCode = 502
    content = result.error
  }
  { sessionActorId: uuid, options: [value] } |> {
    statusCode = 200
    content = {
      sessionActorId = result.sessionActorId
      options =
        result.options
        select o => {
          id              = o.optionId
          sessionActorId  = o.sessionActorId
          name            = o.name
          price           = o.price
          carrierId       = o.carrierId
          productId       = o.carrierProductId
        }
    }
  }

To select an option, your storefront writes the two nShiftCheckout* keys onto the cart's deliveryDynamicFields (so they ride along to the delivery at cart-to-order conversion), or directly onto a portal-style delivery's dynamic. No separate module call is required.

See Also

© 2024 Hantera AB. All rights reserved.