@doswiftly/storefront-sdk 12.0.0 → 13.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,244 @@
1
1
  # Changelog
2
2
 
3
+ ## 13.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2e137ad: Added cart-aware shipping methods discovery: `Cart.requiresShipping`, `CartLine.requiresShipping`, and `cart.availableShippingMethods(address)`.
8
+
9
+ **What's new for the storefront**
10
+ - **`cart.requiresShipping: Boolean!`** — single signal whether the cart needs physical shipping at all. `false` for carts that contain only digital goods (downloads, gift cards, services, subscriptions). Use it to skip the shipping picker step in checkout entirely.
11
+ - **`cart.lines.nodes[].requiresShipping: Boolean!`** — per-line classification. Useful in cart drawer UI to flag physical vs digital lines independently.
12
+ - **`cart.availableShippingMethods(address: ShippingAddressInput!): AvailableShippingMethodsPayload!`** — field on `Cart` that returns shipping methods for the cart's contents at the given destination. The backend pulls subtotal and weight from the cart aggregate — you no longer need to compute them on the client. For digital-only carts the response is `{ methods: [], userErrors: [{ code: 'DIGITAL_ONLY_NO_SHIPPING' }] }`.
13
+ - **`CartAvailableShippingMethods` query** — new named operation ready to drop into your storefront codegen pipeline.
14
+
15
+ **Recommended checkout flow**
16
+
17
+ ```graphql
18
+ query CartAvailableShippingMethods(
19
+ $cartId: ID!
20
+ $address: ShippingAddressInput!
21
+ ) {
22
+ cart(id: $cartId) {
23
+ id
24
+ requiresShipping
25
+ availableShippingMethods(address: $address) {
26
+ methods {
27
+ id
28
+ name
29
+ price {
30
+ amount
31
+ currencyCode
32
+ }
33
+ }
34
+ userErrors {
35
+ code
36
+ message
37
+ field
38
+ }
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ If `cart.requiresShipping === false`, skip rendering the shipping picker and proceed straight to payment. Calling `cartSelectShippingMethod` on a digital-only cart returns `userErrors[{ code: 'FORBIDDEN_SHIPPING_METHOD' }]`, matching the readiness check that `cartComplete` already enforces.
45
+
46
+ **Backward compatibility**
47
+ - The existing standalone `availableShippingMethods(address, cart: CartShippingInput)` query is **unchanged**. Keep using it for pre-cart shipping calculators on product detail pages (when the customer has not created a cart yet).
48
+ - The new field and new query are purely additive. No existing operations change shape.
49
+
50
+ **Free-shipping progress + delivery estimates are now localized**
51
+
52
+ The `freeShippingProgress.message` field and `estimatedDelivery.description` field used to be hardcoded in English. They now respect the `Accept-Language` header (defaults to Polish — `pl`). If your storefront previously parsed these strings, switch to the structured fields (`qualifies`, `progressPercent`, `remaining.amount`, `minDays`, `maxDays`) instead.
53
+
54
+ **Reason codes on shipping `userErrors`**
55
+
56
+ | Code | When | UI reaction |
57
+ | -------------------------- | -------------------------------- | -------------------------------------------- |
58
+ | `DIGITAL_ONLY_NO_SHIPPING` | Cart contains only digital items | Skip shipping picker, go straight to payment |
59
+ | `NO_SHIPPING_METHODS` | Address has no matching zone | Show "no delivery to your country" |
60
+ | `SHIPPING_ERROR` | Internal service error (rare) | Retry or fall back to pre-cart preview query |
61
+
62
+ **Why both packages bump together**
63
+
64
+ `@doswiftly/storefront-sdk` ships local copies of the cart fragments that this update extends with `requiresShipping`, so the SDK release pairs with the schema/operations release. `@doswiftly/commerce-policy` exports `NON_PHYSICAL_TYPES` for downstream tooling that wants to mirror the same classification on the client side.
65
+
66
+ ## 13.0.0
67
+
68
+ ### Major Changes
69
+
70
+ - 783ce01: Faceted aggregation accuracy + DX naming consolidation w `productFilters` API. Plus boolean facet count dla "availableForSale" + payload cleanup w Cart query.
71
+
72
+ ## BREAKING — `AvailableFilters.matchCount` removed → `totalCount`
73
+
74
+ Pole `matchCount` zostało usunięte. Użyj `totalCount` — semantyka identyczna (produkty w current context PRZED zaaplikowaniem faceted filters), nazwa zaligned z `ProductConnection.totalCount` (Relay Connection spec).
75
+
76
+ **Migration** (1:1 rename):
77
+
78
+ ```graphql
79
+ # PRZED
80
+ query Listing {
81
+ productFilters {
82
+ matchCount # ❌ removed
83
+ activeCount
84
+ }
85
+ }
86
+
87
+ # PO
88
+ query Listing {
89
+ productFilters {
90
+ totalCount # ✅ same semantics, Relay-aligned
91
+ activeCount
92
+ }
93
+ }
94
+ ```
95
+
96
+ ## BREAKING — `ProductFilter.available: true` semantics
97
+
98
+ Filter teraz includuje untracked produkty (gift cards, digital, made-to-order — wszystko z `trackQuantity = false`). Semantyka zgodna z `Product.isAvailable` resolver — single source of truth dla "co jest sellable".
99
+
100
+ **Pre-fix**: `available: true` zwracał tylko produkty z fizycznym stockiem (`available > 0`), wykluczał untracked. Storefront widział "Dostępne (3)" w sidebar (computed via `isAvailable`), po kliku dostawał 2 produkty (SQL filter różny).
101
+
102
+ **Post-fix**: oba źródła zwracają to samo.
103
+
104
+ Jeśli polegałeś na exclusion untracked przez `available: true` (np. żeby ukryć gift cards) — użyj `ProductFilter.type` lub osobnej logiki klienckiej.
105
+
106
+ ## NEW — `AvailableFilters.availableCount: Int!`
107
+
108
+ Nowy boolean facet count — liczba produktów spełniających `Product.isAvailable` w current context. Storefront UI renderuje "Dostępne (N)" w sidebar facet panel.
109
+
110
+ Exclude-self: gdy `AvailableFiltersInput.available` jest provided w `currentFilters`, `availableCount` IGNORUJE ten filter (pokazuje "ile produktów ten facet odsłoni gdy włączysz") — inne facets aplikują go normalnie.
111
+
112
+ ## NEW — `AvailableFiltersInput.available: Boolean`
113
+
114
+ Storefront może passować obecny `available` filter context do `productFilters` query — wymagane dla exclude-self semantyki. Pomiń (null/undefined) gdy filtr "Dostępne" nie jest aktywny w UI.
115
+
116
+ ## IMPROVEMENT — Faceted count accuracy (`productCount` mismatch eliminated)
117
+
118
+ Per-attribute / per-brand / per-category `productCount` teraz używa exclude-self aggregation pattern. Facet count MATCHUJE `products(filters).totalCount` gdy storefront aplikuje facet jako filter.
119
+
120
+ **Pre-fix**: `productFilters({categoryId, available: true}).attributes[producent].filterValues[Funko].productCount = 4` (ignoring `available`), `products({categoryId, available: true, attributes: [Funko]}).totalCount = 3` (3 sellable + 1 OOS Funko). Storefront pokazywał "Funko (4)", user dostawał 3 — utracone zaufanie do countów.
121
+
122
+ **Post-fix**: oba zwracają 3. Facet ignoruje WYŁĄCZNIE swój wymiar w `currentFilters`, aplikuje pozostałe + `available` + context (categoryId/collectionId/searchQuery).
123
+
124
+ ## IMPROVEMENT — Cart query payload size
125
+
126
+ `Cart.lines` query w SDK używał obu form (`edges + nodes`) z których konsumowane były wyłącznie `nodes`. Backend serializował `CartLine` fragment 2× per request. Usunięcie duplikatu — redukcja payload typowo 5-20KB per cart request (proportional do liczby pozycji).
127
+
128
+ Brak API change widzialnej dla consumer — SDK używa `cart.lines.nodes` jak wcześniej. Internal SDK improvement.
129
+
130
+ ## Example: end-to-end facet sidebar query (post-rename)
131
+
132
+ ```graphql
133
+ query ProductListing($input: AvailableFiltersInput) {
134
+ productFilters(input: $input) {
135
+ totalCount # ile produktów w current context (przed faceted filters)
136
+ availableCount # ile sellable (boolean facet, exclude-self)
137
+ activeCount # input.currentFilters.length
138
+ attributes {
139
+ handle
140
+ filterValues {
141
+ value
142
+ productCount
143
+ } # per-value facet count (exclude-self per attrDef)
144
+ }
145
+ brands {
146
+ slug
147
+ name
148
+ productCount
149
+ } # per-brand facet count
150
+ categories {
151
+ slug
152
+ name
153
+ productCount
154
+ } # per-category facet count
155
+ priceRange {
156
+ min {
157
+ amount
158
+ currencyCode
159
+ }
160
+ max {
161
+ amount
162
+ currencyCode
163
+ }
164
+ }
165
+ }
166
+
167
+ products(
168
+ filters: [
169
+ { available: true }
170
+ { attributes: [{ attributeId: "producent", values: ["Funko"] }] }
171
+ ]
172
+ ) {
173
+ totalCount
174
+ nodes {
175
+ id
176
+ title
177
+ isAvailable
178
+ }
179
+ }
180
+ }
181
+ ```
182
+
183
+ Gdy storefront aplikuje "Dostępne + Producent=Funko" jako filter w UI:
184
+
185
+ ```graphql
186
+ variables: {
187
+ input: {
188
+ available: true,
189
+ currentFilters: [{ attributeId: "producent", values: ["Funko"] }]
190
+ }
191
+ }
192
+ ```
193
+
194
+ - `availableCount` = liczba sellable Funko w context (ignoring `input.available`, applying `producent: Funko`)
195
+ - `attributes[producent].filterValues[Funko].productCount` = liczba sellable produktów (ignoring `producent`, applying `available: true`) = `products(filters).totalCount`
196
+
197
+ ### Minor Changes
198
+
199
+ - e64cfc5: Brand entity available in Storefront API: filter products by canonical brand + read brand details per product.
200
+
201
+ **What's new in Storefront API**
202
+ - **`Product.brand: BrandSummary`** — every product now exposes its canonical brand (when assigned). Returns `{ id, name, slug, logo }` or `null` if the product has no brand. Resolved via DataLoader so listing 50 products incurs at most 1 brand query (no N+1).
203
+ - **`ProductFilter.brand: BrandFilter`** — new filter input accepting either `{ id: "uuid" }` or `{ slug: "funko" }`. Multiple `brand` entries in `filters[]` use OR semantics. Combine with other filters (price, tags, attributes) using AND across different field names.
204
+ - **`AvailableFilters.brands: [BrandFilterValue!]!`** — `productFilters` query now returns aggregated brands present in the current product set, each with `productCount`. Build a brand facet (checkbox list or dropdown with logo) directly from this response. Only active brands with at least one product in context are included.
205
+
206
+ **Example: brand facet + filter on a listing page**
207
+
208
+ ```graphql
209
+ query Listing {
210
+ productFilters {
211
+ brands {
212
+ id
213
+ name
214
+ slug
215
+ logo
216
+ productCount
217
+ }
218
+ }
219
+ products(filters: [{ brand: { slug: "funko" } }]) {
220
+ totalCount
221
+ nodes {
222
+ id
223
+ title
224
+ brand {
225
+ name
226
+ slug
227
+ logo
228
+ }
229
+ }
230
+ }
231
+ }
232
+ ```
233
+
234
+ **Migration**
235
+ - Existing `Product.vendor: String` (legacy free-text) remains supported. New brand-based filtering uses `ProductFilter.brand` instead of `productVendor`.
236
+ - No breaking changes. All new fields are nullable / optional — queries written before this release continue to work.
237
+
238
+ **SDK**
239
+
240
+ Regenerated types include `BrandSummary`, `BrandFilter`, `BrandFilterValue`, and `Product.brand`. Import them from `@doswiftly/storefront-operations` schema types.
241
+
3
242
  ## 12.0.0
4
243
 
5
244
  ### Major Changes
@@ -1 +1 @@
1
- {"version":3,"file":"cart.d.ts","sourceRoot":"","sources":["../../../src/core/operations/cart.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAwSH,eAAO,MAAM,UAAU,QAOrB,CAAC;AAMH,eAAO,MAAM,WAAW,QAWtB,CAAC;AAEH,eAAO,MAAM,cAAc,QAWzB,CAAC;AAEH,eAAO,MAAM,iBAAiB,QAW5B,CAAC;AAEH,eAAO,MAAM,iBAAiB,QAW5B,CAAC;AAEH,eAAO,MAAM,0BAA0B,QAWrC,CAAC;AAEH,eAAO,MAAM,gBAAgB,QAW3B,CAAC;AAEH,eAAO,MAAM,0BAA0B,QAWrC,CAAC;AAMH,eAAO,MAAM,yBAAyB,QAWpC,CAAC;AAEH,eAAO,MAAM,wBAAwB,QAWnC,CAAC;AAEH,eAAO,MAAM,2BAA2B,QAWtC,CAAC;AAEH,eAAO,MAAM,0BAA0B,QAWrC,CAAC;AAEH,eAAO,MAAM,oBAAoB,QAW/B,CAAC;AAEH,eAAO,MAAM,qBAAqB,QAWhC,CAAC;AAEH,eAAO,MAAM,+BAA+B,QAW1C,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,QAWxB,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,cAAc,QASzB,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,QAoBtC,CAAC"}
1
+ {"version":3,"file":"cart.d.ts","sourceRoot":"","sources":["../../../src/core/operations/cart.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAsSH,eAAO,MAAM,UAAU,QAOrB,CAAC;AAMH,eAAO,MAAM,WAAW,QAWtB,CAAC;AAEH,eAAO,MAAM,cAAc,QAWzB,CAAC;AAEH,eAAO,MAAM,iBAAiB,QAW5B,CAAC;AAEH,eAAO,MAAM,iBAAiB,QAW5B,CAAC;AAEH,eAAO,MAAM,0BAA0B,QAWrC,CAAC;AAEH,eAAO,MAAM,gBAAgB,QAW3B,CAAC;AAEH,eAAO,MAAM,0BAA0B,QAWrC,CAAC;AAMH,eAAO,MAAM,yBAAyB,QAWpC,CAAC;AAEH,eAAO,MAAM,wBAAwB,QAWnC,CAAC;AAEH,eAAO,MAAM,2BAA2B,QAWtC,CAAC;AAEH,eAAO,MAAM,0BAA0B,QAWrC,CAAC;AAEH,eAAO,MAAM,oBAAoB,QAW/B,CAAC;AAEH,eAAO,MAAM,qBAAqB,QAWhC,CAAC;AAEH,eAAO,MAAM,+BAA+B,QAW1C,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,QAWxB,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,cAAc,QASzB,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,QAoBtC,CAAC"}
@@ -113,6 +113,7 @@ const CART_LINE_FRAGMENT = `
113
113
  productTitle
114
114
  productHandle
115
115
  productType
116
+ requiresShipping
116
117
  }
117
118
  ${CART_LINE_COST_FRAGMENT}
118
119
  ${PRODUCT_VARIANT_FRAGMENT}
@@ -191,10 +192,6 @@ const CART_FRAGMENT = `
191
192
  totalQuantity
192
193
  cost { ...CartCost }
193
194
  lines(first: 100) {
194
- edges {
195
- cursor
196
- node { ... on CartLine { ...CartLine } }
197
- }
198
195
  nodes { ... on CartLine { ...CartLine } }
199
196
  pageInfo { ...PageInfo }
200
197
  totalCount
@@ -211,6 +208,7 @@ const CART_FRAGMENT = `
211
208
  selectedShippingMethod { ...CartShippingMethod }
212
209
  selectedPaymentMethod { ...CartSelectedPaymentMethod }
213
210
  appliedGiftCards { ...CartAppliedGiftCard }
211
+ requiresShipping
214
212
  createdAt
215
213
  updatedAt
216
214
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doswiftly/storefront-sdk",
3
- "version": "12.0.0",
3
+ "version": "13.1.0",
4
4
  "description": "Storefront runtime SDK for DoSwiftly Commerce — layered transport, middleware pipeline, React providers, Zustand stores, cache strategies. 0 runtime dependencies in core.",
5
5
  "type": "module",
6
6
  "sideEffects": false,