@doswiftly/storefront-operations 6.0.0 → 7.0.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,271 @@
1
1
  # Changelog
2
2
 
3
+ ## 7.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 227629d: # Storefront GraphQL Operations 7.0 — Breaking changes + nowe rozszerzenia API
8
+
9
+ Major bump wprowadza zmiany breaking w schemie storefront GraphQL plus 3 znaczące nowe funkcjonalności: per-address tax info dla B2B, generic Meta Properties (extensible custom fields), oraz strukturalne userErrors[] z typed codes dla wszystkich mutacji.
10
+
11
+ ## Breaking changes
12
+
13
+ ### 1. `Product.category` (free-text) → `Product.categories` + `Product.primaryCategory` (structured)
14
+
15
+ Wcześniej `Product.category: String` zwracał free-text label — tekst nie był powiązany z entity Category. Klienci nie mogli pobrać slug/name kategorii ani zbudować breadcrumb. Po 7.0:
16
+
17
+ ```graphql
18
+ # PRZED (6.x):
19
+ product { category } # → "Pop Vinyl" (free-text string, brak slug/parent)
20
+
21
+ # PO (7.0):
22
+ product {
23
+ categories { id slug name parent { slug } } # M2M lista (junction table)
24
+ primaryCategory { slug name } # categories[0] dla breadcrumb
25
+ }
26
+ ```
27
+
28
+ **Migration**:
29
+ - Klient renderujący breadcrumb po category name: zamień `product.category` na `product.primaryCategory.name`.
30
+ - Klient filtrujący kategorie z `Product.category` jako string: użyj `product.categories[]` array.
31
+ - Filter `productCategory` w `ProductFilter` usunięty — używaj `category: { id }` (junction lookup, działało wcześniej).
32
+
33
+ ### 2. `categories` query — `CategoryTree` → `CategoryConnection` (Relay)
34
+
35
+ Wcześniej `categories` zwracał outlier shape `{ roots, totalCount }` vs reszta schemy używała Relay Connection. Klienci próbujący `categories { nodes }` dostawali GraphQL error. Po 7.0:
36
+
37
+ ```graphql
38
+ # PRZED (6.x):
39
+ categories(first: 20, rootsOnly: true) {
40
+ roots { id name children { id name } }
41
+ totalCount
42
+ }
43
+
44
+ # PO (7.0):
45
+ categories(first: 20, rootsOnly: true) {
46
+ nodes { id name children { id name } }
47
+ edges { cursor node { id } }
48
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
49
+ totalCount
50
+ }
51
+ ```
52
+
53
+ **Migration**:
54
+ - Zamień `categories.roots` na `categories.nodes`.
55
+ - Tree shape rebuild po stronie klienta — używaj `Category.parent` / `Category.children` field resolvers (DataLoader N+1 safe). Filtr `rootsOnly: true` zwraca tylko top-level (parent IS NULL).
56
+ - Pełen Relay args: `first/after/last/before` zamiast tylko `first/after`.
57
+
58
+ ### 3. `Customer.defaultAddress` — czytelny stan (poprzednio zawsze null)
59
+
60
+ Wcześniej `Customer.defaultAddress` zwracał `null` mimo że adres miał `isDefault: true` w `addresses`. Niespójność łamała storefront breadcrumb i powodowała duplikaty w formularzu dostawy. Po 7.0 zwraca poprawnie wartość — single source of truth z `addresses[].isDefaultShipping` flag.
61
+
62
+ **Migration**: brak — klient po prostu zacznie dostawać poprawne wartości.
63
+
64
+ ### 4. Apollo response — bez `extensions.stacktrace` w produkcji
65
+
66
+ Wcześniej w środowisku produkcyjnym GraphQL response zawierał stacktrace z absolutnymi ścieżkami systemu plików backendu. Po 7.0 production response strippuje wszystko poza `code`, `message`, `locations`, `path` + banner `extensions.environment`. Stacktrace dostępny **tylko** w dev/staging dla DX.
67
+
68
+ **Migration**: brak — klient produkcyjny nie powinien był polegać na stacktrace.
69
+
70
+ ### 5. Validation errors → `userErrors[]` z typed codes (tech debt resolution)
71
+
72
+ Wcześniej walidacja DTO leciała jako `body.errors[].extensions.code = "INTERNAL_SERVER_ERROR"` envelope (wyglądało jak crash). Po 7.0 mutations zwracają strukturalne `userErrors[]` z explicit codes per validation rule:
73
+
74
+ ```graphql
75
+ # Invalid NIP, password too short, malformed email — wszystkie zwracają payload
76
+ mutation {
77
+ customerUpdate(customer: { taxId: "1234567890" }) {
78
+ customer {
79
+ id
80
+ }
81
+ userErrors {
82
+ field
83
+ code
84
+ message
85
+ }
86
+ # → [{ field: ["taxId"], code: "INVALID_TAX_ID_CHECKSUM", message: "..." }]
87
+ }
88
+ }
89
+ ```
90
+
91
+ **Typed codes** dla 25 walidatorów: `INVALID_TAX_ID_CHECKSUM`, `INVALID_VAT_NUMBER`, `INVALID_REGON`, `INVALID_EMAIL_FORMAT`, `INVALID_FORMAT`, `INVALID_PHONE_FORMAT`, `INVALID_UUID`, `INVALID_URL`, `INVALID_DATE`, `INVALID_ENUM_VALUE`, `INVALID_TYPE`, `INVALID_COUNTRY_CODE`, `INVALID_POSTAL_CODE`, `TOO_SHORT`, `TOO_LONG`, `TOO_MANY`, `TOO_FEW`, `OUT_OF_RANGE`, `REQUIRED`, `INVALID_NESTED`, `VALIDATION_ERROR`, etc.
92
+
93
+ **Migration**: klient widzący poprzednio `body.errors[].extensions.code = "INTERNAL_SERVER_ERROR"` — sprawdź `data.<mutation>.userErrors[]` zamiast envelope.
94
+
95
+ ## New features
96
+
97
+ ### 6. `MailingAddress.taxId` + `vatNumber` — per-address B2B tax info
98
+
99
+ Adres ma teraz opcjonalne `taxId` (polski NIP, 10 cyfr z checksumą) + `vatNumber` (VAT UE, np. PL5260250274). Use case: B2B z różnymi danymi firmowymi per adres dostawy/billing (faktura na firmę matkę, dostawa na oddział z osobnym NIP-em).
100
+
101
+ ```graphql
102
+ mutation {
103
+ customerAddAddress(
104
+ address: {
105
+ streetLine1: "ul. Marszałkowska 100"
106
+ city: "Warszawa"
107
+ postalCode: "00-001"
108
+ country: PL
109
+ company: "GameGoods Sp. z o.o."
110
+ taxId: "5260250274"
111
+ vatNumber: "PL5260250274"
112
+ }
113
+ ) {
114
+ address {
115
+ taxId
116
+ vatNumber
117
+ }
118
+ userErrors {
119
+ code
120
+ }
121
+ }
122
+ }
123
+ ```
124
+
125
+ Walidacja format/checksum aktywna — invalid NIP zwraca `userErrors` z `INVALID_TAX_ID_CHECKSUM`.
126
+
127
+ ### 7. Meta Properties — extensible custom fields per encji
128
+
129
+ Generic mechanizm rozszerzania encji (Customer/Product/ProductVariant/Order) o storefront-specific dane bez wymogu zmian schematu po stronie platformy. Per-customer birthday, per-product warranty_years, per-order gift_message — wszystko jako typed key-value entries w polymorphic table.
130
+
131
+ ```graphql
132
+ # Customer self-edit (Bearer token klienta)
133
+ mutation {
134
+ customerMetaPropertiesSet(
135
+ properties: [
136
+ {
137
+ namespace: "storefront"
138
+ key: "birthday"
139
+ value: "1990-05-15"
140
+ type: DATE
141
+ }
142
+ {
143
+ namespace: "storefront"
144
+ key: "favorite_color"
145
+ value: "#FF6600"
146
+ type: COLOR
147
+ }
148
+ ]
149
+ ) {
150
+ metaProperties {
151
+ id
152
+ namespace
153
+ key
154
+ value
155
+ type
156
+ }
157
+ userErrors {
158
+ code
159
+ message
160
+ }
161
+ }
162
+ }
163
+
164
+ # Read na każdej encji
165
+ query {
166
+ customer {
167
+ metaProperty(namespace: "storefront", key: "birthday") {
168
+ value
169
+ }
170
+ }
171
+ product(id: "...") {
172
+ metaProperties(first: 10, namespace: "warranty") {
173
+ nodes {
174
+ key
175
+ value
176
+ }
177
+ }
178
+ }
179
+ }
180
+ ```
181
+
182
+ 10 typed values: `STRING/TEXT/INTEGER/DECIMAL/BOOLEAN/JSON/DATE/DATE_TIME/URL/COLOR`. Visibility flag `isPrivate` (storefront API filtruje `isPrivate=true` na public encjach jak Product/ProductVariant). Reserved namespace prefix `doswiftly:` dla platform fields.
183
+
184
+ **Faza 1 MVP scope**: Customer self-edit only (`customerMetaPropertiesSet` / `customerMetaPropertyDelete` z auth Bearer token klienta). Cross-resource writes (Product/Order/Variant) wymagają Admin layer (Faza 2).
185
+
186
+ ### 8. NIP/REGON sanity guard — odrzucone all-zeros / repeated-digit patterns
187
+
188
+ Walidator NIP wcześniej akceptował `'0000000000'` (suma kontrolna 0=0). Po 7.0 odrzucamy wszystkie repeated-digit patterns (`'0000000000'`, `'1111111111'`, ..., `'9999999999'`) — to oczywiste fake test data / NULL placeholders, żaden urząd ich nie wystawia. Analogicznie REGON 9-digit i 14-digit.
189
+
190
+ ### 9. Empty string clear-value semantyka (wszystkie nullable input fields)
191
+
192
+ Customer kasujący wartość w UI inputie (np. `<input value="" />`) wysyła `phone: ""` zamiast `null`. Po 7.0 backend traktuje empty string jako "wyczyść pole" (DB → null) — natywne form library behavior, klient nie wymaga specjalnej logiki rozróżniającej `null` vs `""`.
193
+
194
+ Pokrycie: wszystkie nullable string/enum fields w `CustomerCreateInput`, `CustomerUpdateInput`, `MailingAddressInput`.
195
+
196
+ ## Notes
197
+ - 7.0 jest cumulative — kumulacja zmian z 6.x patches (B2B fields w `customerUpdate`, marketing consent flow) + breaking changes opisane wyżej.
198
+ - Wszystkie zmiany pokryte test coverage — backend integration tests gwarantują regression-free contract.
199
+
200
+ ## 6.1.0
201
+
202
+ ### Minor Changes
203
+
204
+ - fbc6a94: Rozszerzono `CustomerUpdateInput` w GraphQL Storefront API o pełen kontrakt B2B — storefront developer może teraz zaktualizować dane firmowe klienta (nazwa firmy, NIP, VAT UE, REGON) oraz typ klienta (osoba fizyczna / firma) bezpośrednio przez mutację `customerUpdate`. Wcześniej te pola były dostępne wyłącznie z poziomu panelu administratora.
205
+
206
+ **Nowe pola Input**:
207
+ - `customerType: CustomerType` — `INDIVIDUAL` (B2C) lub `COMPANY` (B2B). Opcjonalne — gdy pominięte, backend dokona inteligentnej inferencji (patrz „Strict hybrid" niżej).
208
+ - `companyName: String` — nazwa firmy (wymagana dla `COMPANY`).
209
+ - `taxId: String` — polski NIP (10 cyfr z sumą kontrolną).
210
+ - `vatNumber: String` — numer VAT UE (np. `PL5260250274`, `DE123456789`).
211
+ - `regon: String` — polski REGON (9 lub 14 cyfr z sumą kontrolną).
212
+
213
+ **Nowe pola czytelne na typie `Customer`** (dostępne automatycznie przez fragment `Customer`):
214
+ - `customerType`, `companyName`, `taxId`, `vatNumber`, `regon`.
215
+
216
+ **Strict hybrid — kontrakt aktualizacji typu klienta**:
217
+ 1. Jeśli `customerType` jest w payloadzie, wygrywa explicit (nawet jeśli inne pola sugerowałyby co innego).
218
+ 2. Jeśli `customerType` jest pominięty, backend dokonuje implicit upgrade `INDIVIDUAL → COMPANY` tylko gdy podano niepuste pole firmowe. Implicit downgrade nigdy się nie zdarza — wymaga jawnego `customerType: INDIVIDUAL`.
219
+ 3. Każde efektywne `COMPANY` (jawne lub przez inferencję) wymaga niepustej `companyName` — w przeciwnym razie mutacja zwraca `userErrors` z kodem `CUSTOMER_UPDATE_FAILED`.
220
+ 4. `undefined` w payloadzie = brak zmiany pola; `null` = świadome czyszczenie wartości.
221
+
222
+ **Walidacja format**: NIP, VAT UE i REGON walidowane przez wspólny dekorator (regex + algorytm sumy kontrolnej). Niepoprawny format → błąd walidacji input'u GraphQL.
223
+
224
+ **Concurrency safety**: `customerUpdate` używa optimistic locking — równoczesne aktualizacje tego samego klienta (np. profil w wielu zakładkach) zwracają `userErrors` z message o konflikcie wersji zamiast cichego nadpisania.
225
+
226
+ **Use cases dla storefront-developera**:
227
+ - Strona „moje dane" w sklepie — pełen formularz B2C/B2B z radio pickerem typu klienta.
228
+ - Checkout pre-fill — możliwość uzupełnienia danych do faktury bez wchodzenia do panelu sklepu.
229
+ - Rejestracja firm / sole-trader scenarios — implicit upgrade gdy klient wpisze NIP w polu opcjonalnym.
230
+
231
+ **Test coverage**: 8 service-level scenariuszy (każda z 4 reguł strict hybrid + concurrency conflict + audit trail + EventEmitter) + 3 GraphQL surface scenariusze (full B2B payload, error mapping, format validation).
232
+
233
+ - c91b700: Rozszerzono storefront GraphQL API o pełną obsługę zgód marketingowych — storefront-developer może teraz zbudować checkbox marketingu przy rejestracji, toggle preferencji w panelu klienta, oraz widget „zapisz się do newslettera" w stopce sklepu. Wszystkie 3 use case'y działają end-to-end (z double opt-in dla anonimowego widget'u i audit trail dla zgodności z RODO).
234
+
235
+ **Nowe pola Input (signup)** — `CustomerCreateInput`:
236
+ - `acceptsMarketing: Boolean` — checkbox marketingu przy `customerSignup`. `true` → state SUBSCRIBED bezpośrednio (signup = adres potwierdzony). `false`/`null` → bez zmian (NOT_SUBSCRIBED).
237
+ - `marketingOptInLevel: MarketingOptInLevel` (`SINGLE_OPT_IN | CONFIRMED_OPT_IN | UNKNOWN`) — opcjonalny override. Ustaw `CONFIRMED_OPT_IN` aby wymusić double opt-in (state PENDING + automatyczny email z linkiem potwierdzającym).
238
+
239
+ **Nowe pole Input (panel klienta)** — `CustomerUpdateInput`:
240
+ - `acceptsMarketing: Boolean` — toggle dla zalogowanego klienta. `true` → SUBSCRIBED, `false` → UNSUBSCRIBED. Działa tylko z access tokenem (auth-required mutation).
241
+
242
+ **Nowe mutacje (newsletter widget — guest, bez auth)**:
243
+ - `customerSubscribeToMarketing(input: { email, marketingOptInLevel? }): SubscribeToMarketingPayload` — anonimowy opt-in. Backend zawsze stosuje double opt-in (state PENDING → email z linkiem → klik → SUBSCRIBED) niezależnie od `marketingOptInLevel` w input — bez tego ktoś mógłby zapisywać cudze adresy.
244
+ - `customerUnsubscribeFromMarketing(input: { email }): UnsubscribeFromMarketingPayload` — anonimowy opt-out, idempotent.
245
+
246
+ **Payloady — `accepted: Boolean!` semantyka (anti-enumeration)**: obie mutacje guest zwracają `accepted: true` niezależnie od stanu emaila (registered / nieistniejący / już SUBSCRIBED). Storefront-developer dostaje deterministyczny shape response — atakujący nie może przez to API enumerate'ować adresów ani odróżnić invalid email format od valid (orzeczenie: też swallowed jako `accepted: true`). Stan końcowy zawsze widoczny przez `Customer.emailMarketing` (gdy klient autoryzowany).
247
+
248
+ **Mutacje C są throttlowane per IP** (limit 10/min) i pod botprotection guardem — storefront powinien zintegrować odpowiedni captcha provider. Email-bombing przez wpisywanie cudzego adresu w widget'cie blokowany dodatkowo na poziomie backendu (max 1 email potwierdzenia na adres na dobę).
249
+
250
+ **Strict-hybrid kontrakt update'u**:
251
+ - Brak pola w Input = no-op dla tego pola (zachowanie back-compat dla istniejących storefrontów bez nowych pól).
252
+ - `acceptsMarketing: null` na auth'd customer = explicit no-change (nie unsubscribe — do unsubscribe użyj `false`).
253
+
254
+ **Double opt-in dla widget'u — co dzieje się po kliknięciu „Zapisz"**:
255
+ 1. Mutation `customerSubscribeToMarketing` → backend zapisuje stan PENDING + uruchamia wysłanie maila potwierdzającego.
256
+ 2. Email idzie z domeny sklepu (per-shop branding — `From: "Sklep <noreply@shop-domain>"`) z CTA buttonem prowadzącym pod URL potwierdzający (token JWT 24h).
257
+ 3. Klient klika link → backend transitionuje stan z PENDING na SUBSCRIBED.
258
+ 4. Po SUBSCRIBED storefront może wysyłać maile marketingowe (gate consent jest egzekwowane przez backend pre-send).
259
+
260
+ **Read surface — `Customer.emailMarketing: EmailMarketingState!`** (pole już istniejące, dla auth'd customer): storefront odczytuje `NOT_SUBSCRIBED | PENDING | SUBSCRIBED | UNSUBSCRIBED | INVALID | REDACTED` aby zdecydować jak renderować checkbox/toggle w UI (np. PENDING = pokaż „sprawdź email", SUBSCRIBED = checkbox zaznaczony).
261
+
262
+ **Use cases**:
263
+ - Strona `/auth/register` — checkbox „Chcę otrzymywać newsletter" zapisany razem z signupem.
264
+ - Strona `/account/preferences` — toggle marketing on/off dla zalogowanego klienta.
265
+ - Stopka layoutu sklepu — input email + button „Zapisz się" → mutation guest subscribe → automatyczny email z linkiem potwierdzającym.
266
+
267
+ **Suppression list awareness**: jeśli adres trafił wcześniej na suppression list (hard bounce / complaint feedback), kolejna subskrypcja zwraca `accepted: true` ale email potwierdzający nie wychodzi. Klient pozostanie w stanie PENDING — admin sklepu musi ręcznie odblokować adres przez panel administracyjny.
268
+
3
269
  ## 6.0.0
4
270
 
5
271
  ### Major Changes
package/README.md CHANGED
@@ -96,7 +96,7 @@ export function ProductList() {
96
96
  | `Collection` | Collection with its products |
97
97
  | `Collections` | All collections |
98
98
  | `Category` | Category with hierarchy |
99
- | `Categories` | Category tree |
99
+ | `Categories` | Paginowana lista kategorii (Relay Connection) — drzewo budujesz po stronie klienta z `parent`/`children` |
100
100
  | `Cart` | Cart contents by ID |
101
101
  | `Customer` | Logged-in customer data |
102
102
  | `CustomerOrders` | Customer order history |
@@ -116,23 +116,26 @@ export function ProductList() {
116
116
 
117
117
  **Authentication**
118
118
 
119
- - `CustomerCreate` - Register new customer
119
+ - `CustomerSignup` - Register new customer
120
120
  - `CustomerLogin` - Login and get token
121
121
  - `CustomerLogout` - Logout
122
- - `CustomerTokenRenew` - Refresh auth token
122
+ - `CustomerRefreshToken` - Refresh auth token
123
+ - `CustomerActivate` - Activate account via email link
124
+ - `CustomerMetaPropertiesSet` - Set custom key-value entries on the logged-in customer
125
+ - `CustomerMetaPropertyDelete` - Delete a custom key-value entry from the logged-in customer
123
126
 
124
127
  **Customer Profile**
125
128
 
126
129
  - `CustomerUpdate` - Update profile info
127
- - `CustomerAddressCreate` - Add new address
128
- - `CustomerAddressUpdate` - Update address
129
- - `CustomerAddressDelete` - Delete address
130
- - `CustomerDefaultAddressUpdate` - Set default address
130
+ - `CustomerAddAddress` - Add new address
131
+ - `CustomerUpdateAddress` - Update address
132
+ - `CustomerRemoveAddress` - Remove address
133
+ - `CustomerSetDefaultAddress` - Set default address
131
134
 
132
135
  **Password**
133
136
 
134
- - `CustomerPasswordRecover` - Request password reset email
135
- - `CustomerPasswordReset` - Reset password with token
137
+ - `CustomerRequestPasswordReset` - Request password reset email
138
+ - `CustomerResetPassword` - Reset password with token
136
139
 
137
140
  ## Schema
138
141
 
package/fragments.graphql CHANGED
@@ -285,6 +285,11 @@ fragment Customer on Customer {
285
285
  isEmailVerified
286
286
  emailMarketing
287
287
  tags
288
+ customerType
289
+ companyName
290
+ taxId
291
+ vatNumber
292
+ regon
288
293
  defaultAddress {
289
294
  ...MailingAddress
290
295
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doswiftly/storefront-operations",
3
- "version": "6.0.0",
3
+ "version": "7.0.0",
4
4
  "description": "GraphQL operations for DoSwiftly Storefront - SSOT from backend",
5
5
  "homepage": "https://doswiftly.pl",
6
6
  "publishConfig": {
package/schema.graphql CHANGED
@@ -1182,11 +1182,14 @@ type Category {
1182
1182
  sortOrder: Int!
1183
1183
  }
1184
1184
 
1185
- """Paginated category list"""
1185
+ """Paginated category list (Relay Connection)"""
1186
1186
  type CategoryConnection {
1187
- """List of category edges"""
1187
+ """List of category edges (cursor + node pairs)"""
1188
1188
  edges: [CategoryEdge!]!
1189
1189
 
1190
+ """List of category nodes (shortcut without cursor)"""
1191
+ nodes: [Category!]!
1192
+
1190
1193
  """Pagination info"""
1191
1194
  pageInfo: PageInfo!
1192
1195
 
@@ -1230,15 +1233,6 @@ type CategoryFilterOption {
1230
1233
  slug: String!
1231
1234
  }
1232
1235
 
1233
- """Category tree structure"""
1234
- type CategoryTree {
1235
- """Root categories"""
1236
- roots: [Category!]!
1237
-
1238
- """Total categories count"""
1239
- totalCount: Int!
1240
- }
1241
-
1242
1236
  """Checkout object"""
1243
1237
  type Checkout {
1244
1238
  """Applied gift cards"""
@@ -2010,9 +2004,15 @@ type Customer implements Node {
2010
2004
  """Saved addresses (Relay Connection)"""
2011
2005
  addresses(after: String, before: String, first: Int, last: Int): MailingAddressConnection!
2012
2006
 
2007
+ """Company name (populated for COMPANY type)"""
2008
+ companyName: String
2009
+
2013
2010
  """Account creation date"""
2014
2011
  createdAt: DateTime!
2015
2012
 
2013
+ """Business type discriminator (INDIVIDUAL/COMPANY)"""
2014
+ customerType: CustomerType!
2015
+
2016
2016
  """Default address"""
2017
2017
  defaultAddress: MailingAddress
2018
2018
 
@@ -2037,6 +2037,16 @@ type Customer implements Node {
2037
2037
  """Last name"""
2038
2038
  lastName: String
2039
2039
 
2040
+ """
2041
+ Lista meta properties (Relay Connection) — opcjonalnie scoped do namespace
2042
+ """
2043
+ metaProperties(first: Int = 10, namespace: String): MetaPropertyConnection!
2044
+
2045
+ """
2046
+ Pojedyncze meta property po (namespace, key) — dla zalogowanego klienta zwraca także private
2047
+ """
2048
+ metaProperty(key: String!, namespace: String!): MetaProperty
2049
+
2040
2050
  """Total orders count (UnsignedInt64 — BigInt-safe)"""
2041
2051
  orderCount: UnsignedInt64!
2042
2052
 
@@ -2046,14 +2056,23 @@ type Customer implements Node {
2046
2056
  """Phone number"""
2047
2057
  phone: String
2048
2058
 
2059
+ """Polish business registry number — REGON"""
2060
+ regon: String
2061
+
2049
2062
  """Customer tags for segmentation (e.g. vip, wholesale, b2b)"""
2050
2063
  tags: [String!]!
2051
2064
 
2065
+ """Polish tax ID — NIP"""
2066
+ taxId: String
2067
+
2052
2068
  """Total amount spent"""
2053
2069
  totalSpent: Money!
2054
2070
 
2055
2071
  """Last update date"""
2056
2072
  updatedAt: DateTime!
2073
+
2074
+ """EU VAT number"""
2075
+ vatNumber: String
2057
2076
  }
2058
2077
 
2059
2078
  """Customer access token"""
@@ -2103,6 +2122,11 @@ type CustomerAddAddressPayload {
2103
2122
 
2104
2123
  """Input for customer registration"""
2105
2124
  input CustomerCreateInput {
2125
+ """
2126
+ Opt-in to email marketing checkbox. true → state SUBSCRIBED (single opt-in) unless `marketingOptInLevel: CONFIRMED_OPT_IN` is also set (then PENDING + double opt-in confirmation email). false/null → no consent change.
2127
+ """
2128
+ acceptsMarketing: Boolean
2129
+
2106
2130
  """Email address"""
2107
2131
  email: String!
2108
2132
 
@@ -2112,6 +2136,11 @@ input CustomerCreateInput {
2112
2136
  """Last name"""
2113
2137
  lastName: String
2114
2138
 
2139
+ """
2140
+ Opt-in level. Default SINGLE_OPT_IN (signup = email proven implicitly). Set CONFIRMED_OPT_IN to force double opt-in via confirmation email.
2141
+ """
2142
+ marketingOptInLevel: MarketingOptInLevel
2143
+
2115
2144
  """Password"""
2116
2145
  password: String!
2117
2146
 
@@ -2234,6 +2263,29 @@ type CustomerSignupPayload {
2234
2263
  userErrors: [UserError!]!
2235
2264
  }
2236
2265
 
2266
+ """Input for newsletter subscribe (guest-friendly)."""
2267
+ input CustomerSubscribeToMarketingInput {
2268
+ """Email address to subscribe to newsletter"""
2269
+ email: String!
2270
+
2271
+ """
2272
+ Opt-in level. Guest mutation always enforces CONFIRMED_OPT_IN regardless of value.
2273
+ """
2274
+ marketingOptInLevel: MarketingOptInLevel
2275
+ }
2276
+
2277
+ """Customer business type — INDIVIDUAL (B2C) or COMPANY (B2B)."""
2278
+ enum CustomerType {
2279
+ COMPANY
2280
+ INDIVIDUAL
2281
+ }
2282
+
2283
+ """Input for newsletter unsubscribe (guest-friendly)."""
2284
+ input CustomerUnsubscribeFromMarketingInput {
2285
+ """Email address to unsubscribe"""
2286
+ email: String!
2287
+ }
2288
+
2237
2289
  """Result of address update"""
2238
2290
  type CustomerUpdateAddressPayload {
2239
2291
  """Updated address"""
@@ -2247,6 +2299,17 @@ type CustomerUpdateAddressPayload {
2247
2299
 
2248
2300
  """Input for customer update"""
2249
2301
  input CustomerUpdateInput {
2302
+ """
2303
+ Email marketing toggle. true → state SUBSCRIBED bezpośrednio (auth'd customer = email proven). false → UNSUBSCRIBED. null/undefined → no change.
2304
+ """
2305
+ acceptsMarketing: Boolean
2306
+
2307
+ """Company name (required when customerType is COMPANY)"""
2308
+ companyName: String
2309
+
2310
+ """Business type discriminator (INDIVIDUAL/COMPANY)"""
2311
+ customerType: CustomerType
2312
+
2250
2313
  """First name"""
2251
2314
  firstName: String
2252
2315
 
@@ -2255,6 +2318,15 @@ input CustomerUpdateInput {
2255
2318
 
2256
2319
  """Phone number"""
2257
2320
  phone: String
2321
+
2322
+ """Polish business registry number — REGON (9 or 14 digits with checksum)"""
2323
+ regon: String
2324
+
2325
+ """Polish tax ID — NIP (10 digits with checksum)"""
2326
+ taxId: String
2327
+
2328
+ """EU VAT number (e.g. PL1234567890)"""
2329
+ vatNumber: String
2258
2330
  }
2259
2331
 
2260
2332
  """Result of customer update"""
@@ -3215,6 +3287,16 @@ type MailingAddress implements Node {
3215
3287
 
3216
3288
  """Second line of street address"""
3217
3289
  streetLine2: String
3290
+
3291
+ """
3292
+ Per-address tax ID (Polish NIP, 10 digits z checksum). B2B use case: różne dane firmowe per adres — np. faktura na firmę matkę z NIP A, dostawa na oddział z NIP B. Distinct from `Customer.taxId` (customer-level globally).
3293
+ """
3294
+ taxId: String
3295
+
3296
+ """
3297
+ Per-address EU VAT number (e.g. PL1234567890). B2B cross-border: różny VAT per adres dostawy. Distinct from `Customer.vatNumber` (customer-level globally).
3298
+ """
3299
+ vatNumber: String
3218
3300
  }
3219
3301
 
3220
3302
  """Paginated mailing addresses (Relay Connection)"""
@@ -3272,6 +3354,23 @@ input MailingAddressInput {
3272
3354
 
3273
3355
  """Second line of street address"""
3274
3356
  streetLine2: String
3357
+
3358
+ """
3359
+ Per-address Polish tax ID — NIP (10 digits with checksum). B2B use case: różne dane firmowe per adres dostawy/billing.
3360
+ """
3361
+ taxId: String
3362
+
3363
+ """Per-address EU VAT number (e.g. PL1234567890). Cross-border B2B."""
3364
+ vatNumber: String
3365
+ }
3366
+
3367
+ """
3368
+ Email marketing opt-in level — SINGLE_OPT_IN, CONFIRMED_OPT_IN, UNKNOWN.
3369
+ """
3370
+ enum MarketingOptInLevel {
3371
+ CONFIRMED_OPT_IN
3372
+ SINGLE_OPT_IN
3373
+ UNKNOWN
3275
3374
  }
3276
3375
 
3277
3376
  """Navigation menu"""
@@ -3335,6 +3434,119 @@ enum MenuItemType {
3335
3434
  SEARCH
3336
3435
  }
3337
3436
 
3437
+ """
3438
+ Payload `customerMetaPropertiesSet` — bulk upsert własnych meta properties klienta
3439
+ """
3440
+ type MetaPropertiesSetPayload {
3441
+ """Upserted meta properties (puste gdy userErrors)"""
3442
+ metaProperties: [MetaProperty!]!
3443
+
3444
+ """User errors (code z `MetaPropertyErrorCode` enum)"""
3445
+ userErrors: [UserError!]!
3446
+ }
3447
+
3448
+ """Pojedyncze pole dodatkowe (meta property) na encji commerce"""
3449
+ type MetaProperty implements Node {
3450
+ """Created at"""
3451
+ createdAt: DateTime!
3452
+
3453
+ """Unique identifier"""
3454
+ id: ID!
3455
+
3456
+ """
3457
+ true = readable tylko przez authenticated context (admin/customer self). false (default) = readable również przez anonymous Storefront API.
3458
+ """
3459
+ isPrivate: Boolean!
3460
+
3461
+ """
3462
+ Key w obrębie namespace (3-64 chars). Unique per (ownerType, ownerId, namespace).
3463
+ """
3464
+ key: String!
3465
+
3466
+ """
3467
+ Namespace (3-64 chars). Zapobiega kolizji kluczy między aplikacjami. Reserved prefix: `doswiftly:` (platform).
3468
+ """
3469
+ namespace: String!
3470
+
3471
+ """Typ wartości — informuje klienta jak parse value."""
3472
+ type: MetaPropertyValueType!
3473
+
3474
+ """Last updated at"""
3475
+ updatedAt: DateTime!
3476
+
3477
+ """
3478
+ Wartość — zawsze String. Klient parsuje zgodnie z polem `type` (np. INTEGER → parseInt, JSON → JSON.parse).
3479
+ """
3480
+ value: String!
3481
+ }
3482
+
3483
+ """Paginated meta property list (Relay Connection)"""
3484
+ type MetaPropertyConnection {
3485
+ """Edges (cursor + node pairs)"""
3486
+ edges: [MetaPropertyEdge!]!
3487
+
3488
+ """Nodes (shortcut bez cursor)"""
3489
+ nodes: [MetaProperty!]!
3490
+
3491
+ """Page info"""
3492
+ pageInfo: PageInfo!
3493
+
3494
+ """Total count (max po wszystkich pages)"""
3495
+ totalCount: Int!
3496
+ }
3497
+
3498
+ """Payload `customerMetaPropertyDelete`"""
3499
+ type MetaPropertyDeletePayload {
3500
+ """ID usuniętego meta property (null gdy userErrors)"""
3501
+ deletedId: ID
3502
+
3503
+ """User errors (code z `MetaPropertyErrorCode` enum)"""
3504
+ userErrors: [UserError!]!
3505
+ }
3506
+
3507
+ """Meta property edge (Relay pagination)"""
3508
+ type MetaPropertyEdge {
3509
+ """Cursor for pagination"""
3510
+ cursor: String!
3511
+
3512
+ """Meta property node"""
3513
+ node: MetaProperty!
3514
+ }
3515
+
3516
+ """Input dla pojedynczego meta property w bulk set"""
3517
+ input MetaPropertyInput {
3518
+ """Visibility — true = admin/customer-only, false = storefront-readable"""
3519
+ isPrivate: Boolean = false
3520
+
3521
+ """Key (3-64 chars)"""
3522
+ key: String!
3523
+
3524
+ """Namespace (3-64 chars, NOT starting with `doswiftly:`)"""
3525
+ namespace: String!
3526
+
3527
+ """Typ wartości (driver walidacji backend-side)"""
3528
+ type: MetaPropertyValueType!
3529
+
3530
+ """Wartość jako String (parse zgodnie z type)"""
3531
+ value: String!
3532
+ }
3533
+
3534
+ """
3535
+ Typy wartości — STRING (max 255 chars), TEXT (no cap), INTEGER, DECIMAL, BOOLEAN ("true"/"false"), JSON (stringified), DATE (YYYY-MM-DD), DATE_TIME (ISO 8601), URL (http(s)://...), COLOR (#RRGGBB / #RRGGBBAA hex).
3536
+ """
3537
+ enum MetaPropertyValueType {
3538
+ BOOLEAN
3539
+ COLOR
3540
+ DATE
3541
+ DATE_TIME
3542
+ DECIMAL
3543
+ INTEGER
3544
+ JSON
3545
+ STRING
3546
+ TEXT
3547
+ URL
3548
+ }
3549
+
3338
3550
  """Monetary value with currency"""
3339
3551
  type Money {
3340
3552
  """Decimal money amount"""
@@ -3432,6 +3644,12 @@ type Mutation {
3432
3644
  """Logout customer (clears auth cookie)"""
3433
3645
  customerLogout: CustomerLogoutPayload!
3434
3646
 
3647
+ """Bulk upsert własnych meta properties klienta (Bearer token wymagany)"""
3648
+ customerMetaPropertiesSet(properties: [MetaPropertyInput!]!): MetaPropertiesSetPayload!
3649
+
3650
+ """Usuwa pojedyncze meta property klienta po (namespace, key)"""
3651
+ customerMetaPropertyDelete(key: String!, namespace: String!): MetaPropertyDeletePayload!
3652
+
3435
3653
  """Refresh access token"""
3436
3654
  customerRefreshToken: CustomerRefreshTokenPayload!
3437
3655
 
@@ -3455,6 +3673,12 @@ type Mutation {
3455
3673
  """Register new customer"""
3456
3674
  customerSignup(input: CustomerCreateInput!): CustomerSignupPayload!
3457
3675
 
3676
+ """Subscribe an email to the newsletter (guest-friendly, double opt-in)."""
3677
+ customerSubscribeToMarketing(input: CustomerSubscribeToMarketingInput!): SubscribeToMarketingPayload!
3678
+
3679
+ """Unsubscribe an email from the newsletter (guest-friendly, idempotent)."""
3680
+ customerUnsubscribeFromMarketing(input: CustomerUnsubscribeFromMarketingInput!): UnsubscribeFromMarketingPayload!
3681
+
3458
3682
  """Update customer profile"""
3459
3683
  customerUpdate(customer: CustomerUpdateInput!): CustomerUpdatePayload!
3460
3684
 
@@ -3541,6 +3765,12 @@ type Order implements Node {
3541
3765
  """Line items count"""
3542
3766
  itemCount: Int!
3543
3767
 
3768
+ """Lista meta properties (Relay Connection)"""
3769
+ metaProperties(first: Int = 10, namespace: String): MetaPropertyConnection!
3770
+
3771
+ """Pojedyncze meta property po (namespace, key)"""
3772
+ metaProperty(key: String!, namespace: String!): MetaProperty
3773
+
3544
3774
  """Order number (human-readable)"""
3545
3775
  orderNumber: String!
3546
3776
 
@@ -3820,9 +4050,9 @@ type Product implements Node {
3820
4050
  averageRating: Float
3821
4051
 
3822
4052
  """
3823
- Product category (free-text classification, e.g. "Footwear", "Electronics")
4053
+ Wszystkie kategorie do których produkt należy (M2M przez ProductCategory junction). Posortowane po junction.sortOrder ASC. Empty list gdy produkt nie jest w żadnej kategorii.
3824
4054
  """
3825
- category: String
4055
+ categories: [Category!]!
3826
4056
 
3827
4057
  """
3828
4058
  Compare-at price range (Money pair). Null gdy żaden variant nie ma compareAtPrice.
@@ -3861,6 +4091,12 @@ type Product implements Node {
3861
4091
  """
3862
4092
  isPurchasable: Boolean!
3863
4093
 
4094
+ """Lista meta properties — Storefront API filtruje isPrivate=false"""
4095
+ metaProperties(first: Int = 10, namespace: String): MetaPropertyConnection!
4096
+
4097
+ """Pojedyncze meta property — Storefront API filtruje isPrivate=false"""
4098
+ metaProperty(key: String!, namespace: String!): MetaProperty
4099
+
3864
4100
  """
3865
4101
  Per-product option definitions (Color, Size, …) with their available values. Use these to build a variant picker without aggregating `selectedOptions` manually.
3866
4102
  """
@@ -3874,6 +4110,11 @@ type Product implements Node {
3874
4110
  """
3875
4111
  priceRangeWithConversion: ConvertedPriceRange
3876
4112
 
4113
+ """
4114
+ Domyślna kategoria dla breadcrumb/nav (= categories[0] po sortOrder). Null gdy produkt nie jest w żadnej kategorii.
4115
+ """
4116
+ primaryCategory: Category
4117
+
3877
4118
  """Similar products recommendations"""
3878
4119
  recommendations(first: Int = 4): ProductRecommendations
3879
4120
 
@@ -4060,11 +4301,6 @@ input ProductFilter {
4060
4301
  """Filter by variant price range"""
4061
4302
  price: PriceRangeFilter
4062
4303
 
4063
- """
4064
- Filter by product category (free-text classification, stored on Product.category). Distinct from `category: CategoryFilter` which selects by structured Category entity.
4065
- """
4066
- productCategory: String
4067
-
4068
4304
  """
4069
4305
  Filter by product type enum (PHYSICAL/DIGITAL/SERVICE/SUBSCRIPTION/GIFT_CARD). Distinct from `productCategory` (free-text classification).
4070
4306
  """
@@ -4262,6 +4498,12 @@ type ProductVariant {
4262
4498
  """Whether variant is available for purchase"""
4263
4499
  isAvailable: Boolean!
4264
4500
 
4501
+ """Lista meta properties — Storefront API filtruje isPrivate=false"""
4502
+ metaProperties(first: Int = 10, namespace: String): MetaPropertyConnection!
4503
+
4504
+ """Pojedyncze meta property — Storefront API filtruje isPrivate=false"""
4505
+ metaProperty(key: String!, namespace: String!): MetaProperty
4506
+
4265
4507
  """Variant price (Money). Default field — industry-standard schema."""
4266
4508
  price: Money!
4267
4509
 
@@ -4395,20 +4637,30 @@ type Query {
4395
4637
  """Get cart by ID"""
4396
4638
  cart(id: ID!): Cart
4397
4639
 
4398
- """Get category tree"""
4640
+ """
4641
+ Lista kategorii (Relay Connection) z opcjonalnym filtrem rootsOnly/parentId
4642
+ """
4399
4643
  categories(
4400
- """Cursor for pagination"""
4644
+ """Forward pagination cursor (after this element)"""
4401
4645
  after: String
4402
4646
 
4403
- """Number of items to fetch"""
4404
- first: Int! = 20
4647
+ """Backward pagination cursor (before this element)"""
4648
+ before: String
4649
+
4650
+ """Forward pagination — first N elements"""
4651
+ first: Int
4405
4652
 
4406
- """Filter by parent category ID"""
4653
+ """Backward pagination last N elements"""
4654
+ last: Int
4655
+
4656
+ """
4657
+ Filter by parent category ID — używaj `null` semantically dla "roots" via `rootsOnly` flag
4658
+ """
4407
4659
  parentId: ID
4408
4660
 
4409
- """Only root categories"""
4661
+ """Tylko root categories (parentId IS NULL)"""
4410
4662
  rootsOnly: Boolean! = false
4411
- ): CategoryTree!
4663
+ ): CategoryConnection!
4412
4664
 
4413
4665
  """Get category by ID or slug"""
4414
4666
  category(
@@ -5662,6 +5914,17 @@ enum StorefrontOrderStatus {
5662
5914
  PROCESSING
5663
5915
  }
5664
5916
 
5917
+ """Payload for customerSubscribeToMarketing mutation"""
5918
+ type SubscribeToMarketingPayload {
5919
+ """
5920
+ Always true (anti-enumeration). Backend may silently skip enqueue if rate-limited or already SUBSCRIBED.
5921
+ """
5922
+ accepted: Boolean!
5923
+
5924
+ """User-facing errors"""
5925
+ userErrors: [UserError!]!
5926
+ }
5927
+
5665
5928
  """Tax line item"""
5666
5929
  type TaxLine {
5667
5930
  """Tax amount"""
@@ -5756,6 +6019,15 @@ Unsigned 64-bit integer, JSON-serialized as String (BigInt-safe). Format: "{inte
5756
6019
  """
5757
6020
  scalar UnsignedInt64
5758
6021
 
6022
+ """Payload for customerUnsubscribeFromMarketing mutation"""
6023
+ type UnsubscribeFromMarketingPayload {
6024
+ """Always true (anti-enumeration)."""
6025
+ accepted: Boolean!
6026
+
6027
+ """User-facing errors"""
6028
+ userErrors: [UserError!]!
6029
+ }
6030
+
5759
6031
  """URL redirect (SEO, store migration)"""
5760
6032
  type UrlRedirect {
5761
6033
  """Source path"""