Przejdź do głównej zawartości

Przedsprzedaż, „na zamówienie" i czas realizacji

Framework: React / Next.js
Pobieranie danych jest przenośne — w blokach z przykładami zakładka Raw pokazuje czysty fetch działający w dowolnym frameworku.
UI (komponenty z hookami i JSX) jest specyficzne dla React / Next.js. W innym frameworku weź zapytanie z zakładki Raw i napisz własny widok.

Co zbudujesz​

Blok dostępności na karcie produktu, który mówi klientowi czy i kiedy: „Dostępny od ręki · wysyłka w 1-2 dni robocze", „Przedsprzedaż · premiera: III-IV kwartał 2027", „Sprowadzamy na zamówienie · wysyłka w 60-90 dni roboczych (orientacyjnie do 12.01.2027)". Do tego fragment JSON-LD z Offer.availability (PreOrder / BackOrder / InStock) i deliveryTime, którego Google wymaga w Zakupach i wynikach z produktami.

Wszystko z jednego pola:

const { availability } = data.product.variants.nodes[0]; // VariantAvailability
availability.status; // 'IN_STOCK' | 'PREORDER' | 'BACKORDER' | 'OUT_OF_STOCK' | 'UNTRACKED'
availability.release?.label; // „III-IV kwartał 2027" - etykieta w języku żądania
availability.expectedAvailability; // { from, to, precision } - kiedy towar ma być
availability.handlingTime; // { minDays, maxDays } - dni robocze do nadania paczki
availability.purchasableQuantity; // ile sztuk da się jeszcze kupić; null = bez limitu

Wymagania​

Krok 1 — Skąd biorą się status i terminy​

Merchant ustawia w panelu fakty: termin premiery (dokładna data, miesiąc albo kwartał), czas realizacji na stanie i czas realizacji bez towaru. Status i daty wyprowadza platforma — tą samą regułą, którą widzi merchant w podglądzie „Klient zobaczy" i którą dostaje feed Google Merchant. Sklep nie liczy niczego sam, więc trzy powierzchnie nigdy nie odpowiedzą różnie o ten sam produkt.

statusCo znaczyreleaseexpectedAvailabilityhandlingTime
IN_STOCK / UNTRACKEDdo kupienia od rękinullnullczas na stanie
PREORDERpremiera jeszcze trwa, merchant przyjmuje zamówieniaokno premiery= okno premieryczas na stanie (pakowanie po premierze)
BACKORDERbrak towaru, merchant sprowadza na zamówienienulldziś + czas bez towaru (dni robocze) albo null, gdy merchant nie podał czasuczas bez towaru
OUT_OF_STOCKnie da się kupićnullnullnull

isAvailable na wariancie znaczy teraz status !== OUT_OF_STOCK — przedsprzedaż i „na zamówienie" są kupowalne. Jeśli sklep pokazuje „Niedostępny" po !isAvailable, nic się nie zmienia; ten przepis dokłada dlaczego i kiedy.

Limit sztuk w przedsprzedaży i „na zamówienie"​

Merchant może sprzedawać bez towaru do konkretnej liczby sztuk — tyle, ile zaklepał mu dostawca (np. 6 sztuk premiery). availability.purchasableQuantity mówi, ile sztuk klient może jeszcze kupić: przy towarze na półce to dostępna ilość, w przedsprzedaży i „na zamówienie" z limitem — pozostałe sztuki z przydziału. null znaczy „bez limitu" (stan nieśledzony albo sprzedaż bez towaru bez limitu). Użyj tej liczby jako górnej granicy wyboru ilości na karcie produktu, zamiast availableStock: availableStock liczy wyłącznie towar na półce, więc w przedsprzedaży wynosi 0, choć produkt da się kupić.

Koszyk pilnuje tego samego limitu po stronie serwera — próba dodania większej ilości kończy się błędem z liczbą sztuk możliwych do kupienia, więc ogranicznik w UI to wygoda, nie zabezpieczenie.

Dokładna data to twardy termin

release.precision === 'DATE' oznacza embargo: paczka nie wyjdzie przed tym dniem, nawet gdy towar leży w magazynie. Miesiąc i kwartał to termin orientacyjny — gdy towar dotrze wcześniej, status sam przechodzi w IN_STOCK. Pokaż klientowi tę różnicę w kopii („po premierze" vs „po dotarciu towaru").

Krok 2 — Etykieta okna przychodzi gotowa​

release.label i expectedAvailability.label to etykieta okna w języku żądania (Accept-Language): „III-IV kwartał 2027", „wrzesień 2027", „15.11.2026". Nie formatuj kwartałów sam — kolejny sklep zrobi to inaczej, a Google i panel mówią jednym głosem. Datę dla Google bierz z expectedAvailability.to (koniec okna; Google przyjmuje jedną datę, nie zakres).

Krok 3 — Komponent i JSON-LD (React)​

Kod jest weryfikowany typami przeciw @doswiftly/storefront-sdk — literówka w nazwie pola albo zły status nie przejdą kompilacji:

AvailabilityNotice.tsx
import type { DeliveryEstimate, ProductVariant, VariantAvailability } from '@doswiftly/storefront-sdk';

// Dostępność wariantu na karcie produktu: plakietka i zdanie o terminie liczone z
// `variant.availability` (status, okno premiery, szacowana dostępność, czas realizacji)
// oraz dane strukturalne dla Google (`Offer.availability` + `deliveryTime`).
//
// Sklep NIE liczy statusu sam - dostaje go gotowy z API tą samą regułą, którą panel
// pokazuje merchantowi i którą feed Google Merchant raportuje. Etykieta okna (`label`)
// też przychodzi z API w języku żądania, więc kwartałów nie formatujesz ręcznie.

const SCHEMA_AVAILABILITY = {
UNTRACKED: 'https://schema.org/InStock',
IN_STOCK: 'https://schema.org/InStock',
PREORDER: 'https://schema.org/PreOrder',
BACKORDER: 'https://schema.org/BackOrder',
OUT_OF_STOCK: 'https://schema.org/OutOfStock',
} as const;

function days(range: { minDays: number; maxDays: number }): string {
return `${range.minDays}-${range.maxDays} dni roboczych`;
}

/** Przewóz kurierem ma sens tylko z obiema liczbami - metoda bez szacunku nie dokłada nic. */
function transitDays(delivery?: DeliveryEstimate | null): { minDays: number; maxDays: number } | null {
return delivery?.minDays != null && delivery.maxDays != null
? { minDays: delivery.minDays, maxDays: delivery.maxDays }
: null;
}

/** Zdanie pod ceną - jedna obietnica, którą powtarzasz w koszyku, kasie i na koncie klienta. */
export function availabilityCopy(availability: VariantAvailability, delivery?: DeliveryEstimate | null): string {
const courier = transitDays(delivery);
const transit = courier ? ` · dostawa ${courier.minDays}-${courier.maxDays} dni` : '';
const handling = availability.handlingTime ? days(availability.handlingTime) : null;

switch (availability.status) {
case 'IN_STOCK':
case 'UNTRACKED':
return `Dostępny od ręki${handling ? ` · wysyłka w ${handling}` : ''}${transit}`;
case 'PREORDER': {
// `release.label` - gotowa etykieta okna („III-IV kwartał 2027", „15.11.2026").
const when = availability.release?.label ?? '';
const after = availability.release?.precision === 'DATE' ? 'po premierze' : 'po dotarciu towaru';
return `Przedsprzedaż · premiera: ${when}${handling ? ` · wysyłka w ${handling} ${after}` : ''}${transit}`;
}
case 'BACKORDER':
// Bez zadeklarowanego czasu nie ma daty - nie wymyślaj terminu, którego merchant nie obiecał.
return availability.expectedAvailability && handling
? `Sprowadzamy na zamówienie · wysyłka w ${handling} (orientacyjnie do ${availability.expectedAvailability.to})${transit}`
: 'Sprowadzamy na zamówienie';
case 'OUT_OF_STOCK':
return 'Chwilowo niedostępny';
}
}

/** Fragment `Offer` dla JSON-LD produktu: status i czas dostawy, których Google wymaga w Zakupach. */
export function offerAvailabilityJsonLd(availability: VariantAvailability, delivery?: DeliveryEstimate | null) {
const courier = transitDays(delivery);
return {
availability: SCHEMA_AVAILABILITY[availability.status],
// Google przyjmuje JEDNĄ datę: koniec okna (dla zakresu kwartałów - koniec ostatniego).
...(availability.expectedAvailability ? { availabilityStarts: availability.expectedAvailability.to } : {}),
...(availability.handlingTime
? {
shippingDetails: {
'@type': 'OfferShippingDetails',
deliveryTime: {
'@type': 'ShippingDeliveryTime',
handlingTime: {
'@type': 'QuantitativeValue',
minValue: availability.handlingTime.minDays,
maxValue: availability.handlingTime.maxDays,
unitCode: 'DAY',
},
...(courier
? {
transitTime: {
'@type': 'QuantitativeValue',
minValue: courier.minDays,
maxValue: courier.maxDays,
unitCode: 'DAY',
},
}
: {}),
},
},
}
: {}),
};
}

export function AvailabilityNotice({
variant,
delivery,
}: {
variant: ProductVariant;
delivery?: DeliveryEstimate | null;
}) {
// `availability` jest null wyłącznie na lekkim wariancie z linii koszyka - na karcie produktu
// zawsze jest, ale typ tego nie wie, więc zostaje zachowanie sprzed pola.
if (!variant.availability) return <p>{variant.isAvailable ? 'Dostępny' : 'Niedostępny'}</p>;

const { status } = variant.availability;
const badge = status === 'PREORDER' ? 'Przedsprzedaż' : status === 'BACKORDER' ? 'Na zamówienie' : null;

return (
<div>
{badge ? <span>{badge}</span> : null}
<p>{availabilityCopy(variant.availability, delivery)}</p>
{status === 'PREORDER' ? <p>Płatność przy zamówieniu. Możesz anulować do wysyłki.</p> : null}
<button disabled={!variant.isAvailable}>{status === 'PREORDER' ? 'Zamów w przedsprzedaży' : 'Do koszyka'}</button>
</div>
);
}

offerAvailabilityJsonLd dokładasz do Offer w JSON-LD produktu obok ceny; transitTime bierze się z Shop.deliveryEstimate(countryCode) — min/max dni przewozu z metod wysyłki merchanta dla kraju sklepu (pobierasz raz przy ładowaniu sklepu, nie per produkt).

Krok 4 — Ta sama obietnica w koszyku, kasie i na koncie​

  • Koszyk i kasa: pod pozycją to samo zdanie co na karcie (linia koszyka niesie lekki wariant bez availability — trzymaj status z karty w stanie koszyka albo dociągnij Product). Przy mieszanym koszyku jedno zdanie nad metodami dostawy: „Część pozycji wyślemy później — zobacz terminy przy produktach".
  • Wykluczenia przewoźników (np. usługa tylko dla towaru od ręki) rób po availability.status, nie po kategorii.
  • Podsumowanie zamówienia i konto klienta: po złożeniu zamówienia obowiązuje termin zapisany na pozycji (OrderItem.expectedShipWindow z availabilitySnapshot) — merchant może później przesunąć premierę produktu, ale obietnica wobec klienta zostaje. Mail potwierdzenia od platformy już go pokazuje.

Krok 5 — Listing „Przedsprzedaż"​

Bez kategorii-proxy: products(filters: [{ availability: [PREORDER] }]). Ten sam filtr przyjmuje kilka stanów naraz ([IN_STOCK, PREORDER] = wszystko, co da się dziś kupić).

Typy​

Renderowane ze schematu GraphQL — nigdy nie rozjeżdżają się z API:

VariantAvailability​

VariantAvailabilityobjectPełna referencja →

Availability of a variant resolved by the platform: status, release window (pre-order), expected availability and handling time. Build product-page copy, cart notes and structured data (`Offer.availability`, `deliveryTime.handlingTime`) from these fields.

PoleTypOpis
expectedAvailabilityAvailabilityWindowWhen the item is expected to become available: the release window for PREORDER, an estimated date range for BACKORDER (today plus the merchant handling time without stock). Null for items available now or not purchasable. Use `to` as the single estimated date for Google (`availability_date`).
handlingTimeBusinessDayRangeHandling time in business days from order to carrier hand-off for the current status (in stock or after release: the regular handling time; backorder: the handling time without stock). Null when the item cannot be purchased or the merchant declared no time. Combine with the shipping method transit time for `deliveryTime` structured data.
purchasableQuantityIntHow many units a customer can still buy: units in stock plus units the merchant can still sell before stock arrives (pre-order or on-order limit). Null when there is no limit (untracked item or unlimited sale beyond stock). Use it as the maximum of the quantity selector.
releaseAvailabilityWindowRelease window set by the merchant (pre-order). Present while the release is pending; null once released or when the product was never a pre-order. DATE precision = hard release day: the parcel cannot leave before it.
statusStockAvailability!Resolved availability state (see enum values).

AvailabilityWindow​

AvailabilityWindowobjectPełna referencja →

A calendar date window with the precision the merchant knows it in. Render `label` as-is (localised for the request language) instead of formatting the dates yourself.

PoleTypOpis
fromString!First day of the window, calendar date YYYY-MM-DD.
labelString!Human-readable label, already localised for the request language (English: e.g. "Q3 2027", "September-October 2027", "11/15/2026"). Render as-is.
precisionReleasePrecision!Precision of the window: exact day, month or quarter.
toString!Last day of the window, calendar date YYYY-MM-DD. For DATE precision equal to `from`.

DeliveryEstimate​

DeliveryEstimateobjectPełna referencja →

Estimated delivery window for a shipping method, in business days.

PoleTypOpis
descriptionStringPre-formatted human-readable estimate (e.g. "1-2 business days"). Localised; render as-is.
maxDaysIntMaximum expected delivery time in business days.
minDaysIntMinimum expected delivery time in business days.

Powiązane​