@doswiftly/storefront-operations 6.0.0 → 6.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 +69 -0
- package/fragments.graphql +5 -0
- package/package.json +1 -1
- package/schema.graphql +103 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 6.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
**Nowe pola Input**:
|
|
10
|
+
- `customerType: CustomerType` — `INDIVIDUAL` (B2C) lub `COMPANY` (B2B). Opcjonalne — gdy pominięte, backend dokona inteligentnej inferencji (patrz „Strict hybrid" niżej).
|
|
11
|
+
- `companyName: String` — nazwa firmy (wymagana dla `COMPANY`).
|
|
12
|
+
- `taxId: String` — polski NIP (10 cyfr z sumą kontrolną).
|
|
13
|
+
- `vatNumber: String` — numer VAT UE (np. `PL5260250274`, `DE123456789`).
|
|
14
|
+
- `regon: String` — polski REGON (9 lub 14 cyfr z sumą kontrolną).
|
|
15
|
+
|
|
16
|
+
**Nowe pola czytelne na typie `Customer`** (dostępne automatycznie przez fragment `Customer`):
|
|
17
|
+
- `customerType`, `companyName`, `taxId`, `vatNumber`, `regon`.
|
|
18
|
+
|
|
19
|
+
**Strict hybrid — kontrakt aktualizacji typu klienta**:
|
|
20
|
+
1. Jeśli `customerType` jest w payloadzie, wygrywa explicit (nawet jeśli inne pola sugerowałyby co innego).
|
|
21
|
+
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`.
|
|
22
|
+
3. Każde efektywne `COMPANY` (jawne lub przez inferencję) wymaga niepustej `companyName` — w przeciwnym razie mutacja zwraca `userErrors` z kodem `CUSTOMER_UPDATE_FAILED`.
|
|
23
|
+
4. `undefined` w payloadzie = brak zmiany pola; `null` = świadome czyszczenie wartości.
|
|
24
|
+
|
|
25
|
+
**Walidacja format**: NIP, VAT UE i REGON walidowane przez wspólny dekorator (regex + algorytm sumy kontrolnej). Niepoprawny format → błąd walidacji input'u GraphQL.
|
|
26
|
+
|
|
27
|
+
**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.
|
|
28
|
+
|
|
29
|
+
**Use cases dla storefront-developera**:
|
|
30
|
+
- Strona „moje dane" w sklepie — pełen formularz B2C/B2B z radio pickerem typu klienta.
|
|
31
|
+
- Checkout pre-fill — możliwość uzupełnienia danych do faktury bez wchodzenia do panelu sklepu.
|
|
32
|
+
- Rejestracja firm / sole-trader scenarios — implicit upgrade gdy klient wpisze NIP w polu opcjonalnym.
|
|
33
|
+
|
|
34
|
+
**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).
|
|
35
|
+
|
|
36
|
+
- 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).
|
|
37
|
+
|
|
38
|
+
**Nowe pola Input (signup)** — `CustomerCreateInput`:
|
|
39
|
+
- `acceptsMarketing: Boolean` — checkbox marketingu przy `customerSignup`. `true` → state SUBSCRIBED bezpośrednio (signup = adres potwierdzony). `false`/`null` → bez zmian (NOT_SUBSCRIBED).
|
|
40
|
+
- `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).
|
|
41
|
+
|
|
42
|
+
**Nowe pole Input (panel klienta)** — `CustomerUpdateInput`:
|
|
43
|
+
- `acceptsMarketing: Boolean` — toggle dla zalogowanego klienta. `true` → SUBSCRIBED, `false` → UNSUBSCRIBED. Działa tylko z access tokenem (auth-required mutation).
|
|
44
|
+
|
|
45
|
+
**Nowe mutacje (newsletter widget — guest, bez auth)**:
|
|
46
|
+
- `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.
|
|
47
|
+
- `customerUnsubscribeFromMarketing(input: { email }): UnsubscribeFromMarketingPayload` — anonimowy opt-out, idempotent.
|
|
48
|
+
|
|
49
|
+
**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).
|
|
50
|
+
|
|
51
|
+
**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ę).
|
|
52
|
+
|
|
53
|
+
**Strict-hybrid kontrakt update'u**:
|
|
54
|
+
- Brak pola w Input = no-op dla tego pola (zachowanie back-compat dla istniejących storefrontów bez nowych pól).
|
|
55
|
+
- `acceptsMarketing: null` na auth'd customer = explicit no-change (nie unsubscribe — do unsubscribe użyj `false`).
|
|
56
|
+
|
|
57
|
+
**Double opt-in dla widget'u — co dzieje się po kliknięciu „Zapisz"**:
|
|
58
|
+
1. Mutation `customerSubscribeToMarketing` → backend zapisuje stan PENDING + uruchamia wysłanie maila potwierdzającego.
|
|
59
|
+
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).
|
|
60
|
+
3. Klient klika link → backend transitionuje stan z PENDING na SUBSCRIBED.
|
|
61
|
+
4. Po SUBSCRIBED storefront może wysyłać maile marketingowe (gate consent jest egzekwowane przez backend pre-send).
|
|
62
|
+
|
|
63
|
+
**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).
|
|
64
|
+
|
|
65
|
+
**Use cases**:
|
|
66
|
+
- Strona `/auth/register` — checkbox „Chcę otrzymywać newsletter" zapisany razem z signupem.
|
|
67
|
+
- Strona `/account/preferences` — toggle marketing on/off dla zalogowanego klienta.
|
|
68
|
+
- Stopka layoutu sklepu — input email + button „Zapisz się" → mutation guest subscribe → automatyczny email z linkiem potwierdzającym.
|
|
69
|
+
|
|
70
|
+
**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.
|
|
71
|
+
|
|
3
72
|
## 6.0.0
|
|
4
73
|
|
|
5
74
|
### Major Changes
|
package/fragments.graphql
CHANGED
package/package.json
CHANGED
package/schema.graphql
CHANGED
|
@@ -2010,9 +2010,15 @@ type Customer implements Node {
|
|
|
2010
2010
|
"""Saved addresses (Relay Connection)"""
|
|
2011
2011
|
addresses(after: String, before: String, first: Int, last: Int): MailingAddressConnection!
|
|
2012
2012
|
|
|
2013
|
+
"""Company name (populated for COMPANY type)"""
|
|
2014
|
+
companyName: String
|
|
2015
|
+
|
|
2013
2016
|
"""Account creation date"""
|
|
2014
2017
|
createdAt: DateTime!
|
|
2015
2018
|
|
|
2019
|
+
"""Business type discriminator (INDIVIDUAL/COMPANY)"""
|
|
2020
|
+
customerType: CustomerType!
|
|
2021
|
+
|
|
2016
2022
|
"""Default address"""
|
|
2017
2023
|
defaultAddress: MailingAddress
|
|
2018
2024
|
|
|
@@ -2046,14 +2052,23 @@ type Customer implements Node {
|
|
|
2046
2052
|
"""Phone number"""
|
|
2047
2053
|
phone: String
|
|
2048
2054
|
|
|
2055
|
+
"""Polish business registry number — REGON"""
|
|
2056
|
+
regon: String
|
|
2057
|
+
|
|
2049
2058
|
"""Customer tags for segmentation (e.g. vip, wholesale, b2b)"""
|
|
2050
2059
|
tags: [String!]!
|
|
2051
2060
|
|
|
2061
|
+
"""Polish tax ID — NIP"""
|
|
2062
|
+
taxId: String
|
|
2063
|
+
|
|
2052
2064
|
"""Total amount spent"""
|
|
2053
2065
|
totalSpent: Money!
|
|
2054
2066
|
|
|
2055
2067
|
"""Last update date"""
|
|
2056
2068
|
updatedAt: DateTime!
|
|
2069
|
+
|
|
2070
|
+
"""EU VAT number"""
|
|
2071
|
+
vatNumber: String
|
|
2057
2072
|
}
|
|
2058
2073
|
|
|
2059
2074
|
"""Customer access token"""
|
|
@@ -2103,6 +2118,11 @@ type CustomerAddAddressPayload {
|
|
|
2103
2118
|
|
|
2104
2119
|
"""Input for customer registration"""
|
|
2105
2120
|
input CustomerCreateInput {
|
|
2121
|
+
"""
|
|
2122
|
+
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.
|
|
2123
|
+
"""
|
|
2124
|
+
acceptsMarketing: Boolean
|
|
2125
|
+
|
|
2106
2126
|
"""Email address"""
|
|
2107
2127
|
email: String!
|
|
2108
2128
|
|
|
@@ -2112,6 +2132,11 @@ input CustomerCreateInput {
|
|
|
2112
2132
|
"""Last name"""
|
|
2113
2133
|
lastName: String
|
|
2114
2134
|
|
|
2135
|
+
"""
|
|
2136
|
+
Opt-in level. Default SINGLE_OPT_IN (signup = email proven implicitly). Set CONFIRMED_OPT_IN to force double opt-in via confirmation email.
|
|
2137
|
+
"""
|
|
2138
|
+
marketingOptInLevel: MarketingOptInLevel
|
|
2139
|
+
|
|
2115
2140
|
"""Password"""
|
|
2116
2141
|
password: String!
|
|
2117
2142
|
|
|
@@ -2234,6 +2259,29 @@ type CustomerSignupPayload {
|
|
|
2234
2259
|
userErrors: [UserError!]!
|
|
2235
2260
|
}
|
|
2236
2261
|
|
|
2262
|
+
"""Input for newsletter subscribe (guest-friendly)."""
|
|
2263
|
+
input CustomerSubscribeToMarketingInput {
|
|
2264
|
+
"""Email address to subscribe to newsletter"""
|
|
2265
|
+
email: String!
|
|
2266
|
+
|
|
2267
|
+
"""
|
|
2268
|
+
Opt-in level. Guest mutation always enforces CONFIRMED_OPT_IN regardless of value.
|
|
2269
|
+
"""
|
|
2270
|
+
marketingOptInLevel: MarketingOptInLevel
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
"""Customer business type — INDIVIDUAL (B2C) or COMPANY (B2B)."""
|
|
2274
|
+
enum CustomerType {
|
|
2275
|
+
COMPANY
|
|
2276
|
+
INDIVIDUAL
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
"""Input for newsletter unsubscribe (guest-friendly)."""
|
|
2280
|
+
input CustomerUnsubscribeFromMarketingInput {
|
|
2281
|
+
"""Email address to unsubscribe"""
|
|
2282
|
+
email: String!
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2237
2285
|
"""Result of address update"""
|
|
2238
2286
|
type CustomerUpdateAddressPayload {
|
|
2239
2287
|
"""Updated address"""
|
|
@@ -2247,6 +2295,17 @@ type CustomerUpdateAddressPayload {
|
|
|
2247
2295
|
|
|
2248
2296
|
"""Input for customer update"""
|
|
2249
2297
|
input CustomerUpdateInput {
|
|
2298
|
+
"""
|
|
2299
|
+
Email marketing toggle. true → state SUBSCRIBED bezpośrednio (auth'd customer = email proven). false → UNSUBSCRIBED. null/undefined → no change.
|
|
2300
|
+
"""
|
|
2301
|
+
acceptsMarketing: Boolean
|
|
2302
|
+
|
|
2303
|
+
"""Company name (required when customerType is COMPANY)"""
|
|
2304
|
+
companyName: String
|
|
2305
|
+
|
|
2306
|
+
"""Business type discriminator (INDIVIDUAL/COMPANY)"""
|
|
2307
|
+
customerType: CustomerType
|
|
2308
|
+
|
|
2250
2309
|
"""First name"""
|
|
2251
2310
|
firstName: String
|
|
2252
2311
|
|
|
@@ -2255,6 +2314,15 @@ input CustomerUpdateInput {
|
|
|
2255
2314
|
|
|
2256
2315
|
"""Phone number"""
|
|
2257
2316
|
phone: String
|
|
2317
|
+
|
|
2318
|
+
"""Polish business registry number — REGON (9 or 14 digits with checksum)"""
|
|
2319
|
+
regon: String
|
|
2320
|
+
|
|
2321
|
+
"""Polish tax ID — NIP (10 digits with checksum)"""
|
|
2322
|
+
taxId: String
|
|
2323
|
+
|
|
2324
|
+
"""EU VAT number (e.g. PL1234567890)"""
|
|
2325
|
+
vatNumber: String
|
|
2258
2326
|
}
|
|
2259
2327
|
|
|
2260
2328
|
"""Result of customer update"""
|
|
@@ -3274,6 +3342,15 @@ input MailingAddressInput {
|
|
|
3274
3342
|
streetLine2: String
|
|
3275
3343
|
}
|
|
3276
3344
|
|
|
3345
|
+
"""
|
|
3346
|
+
Email marketing opt-in level — SINGLE_OPT_IN, CONFIRMED_OPT_IN, UNKNOWN.
|
|
3347
|
+
"""
|
|
3348
|
+
enum MarketingOptInLevel {
|
|
3349
|
+
CONFIRMED_OPT_IN
|
|
3350
|
+
SINGLE_OPT_IN
|
|
3351
|
+
UNKNOWN
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3277
3354
|
"""Navigation menu"""
|
|
3278
3355
|
type Menu {
|
|
3279
3356
|
"""URL handle (e.g., main-menu, footer)"""
|
|
@@ -3455,6 +3532,12 @@ type Mutation {
|
|
|
3455
3532
|
"""Register new customer"""
|
|
3456
3533
|
customerSignup(input: CustomerCreateInput!): CustomerSignupPayload!
|
|
3457
3534
|
|
|
3535
|
+
"""Subscribe an email to the newsletter (guest-friendly, double opt-in)."""
|
|
3536
|
+
customerSubscribeToMarketing(input: CustomerSubscribeToMarketingInput!): SubscribeToMarketingPayload!
|
|
3537
|
+
|
|
3538
|
+
"""Unsubscribe an email from the newsletter (guest-friendly, idempotent)."""
|
|
3539
|
+
customerUnsubscribeFromMarketing(input: CustomerUnsubscribeFromMarketingInput!): UnsubscribeFromMarketingPayload!
|
|
3540
|
+
|
|
3458
3541
|
"""Update customer profile"""
|
|
3459
3542
|
customerUpdate(customer: CustomerUpdateInput!): CustomerUpdatePayload!
|
|
3460
3543
|
|
|
@@ -5662,6 +5745,17 @@ enum StorefrontOrderStatus {
|
|
|
5662
5745
|
PROCESSING
|
|
5663
5746
|
}
|
|
5664
5747
|
|
|
5748
|
+
"""Payload for customerSubscribeToMarketing mutation"""
|
|
5749
|
+
type SubscribeToMarketingPayload {
|
|
5750
|
+
"""
|
|
5751
|
+
Always true (anti-enumeration). Backend may silently skip enqueue if rate-limited or already SUBSCRIBED.
|
|
5752
|
+
"""
|
|
5753
|
+
accepted: Boolean!
|
|
5754
|
+
|
|
5755
|
+
"""User-facing errors"""
|
|
5756
|
+
userErrors: [UserError!]!
|
|
5757
|
+
}
|
|
5758
|
+
|
|
5665
5759
|
"""Tax line item"""
|
|
5666
5760
|
type TaxLine {
|
|
5667
5761
|
"""Tax amount"""
|
|
@@ -5756,6 +5850,15 @@ Unsigned 64-bit integer, JSON-serialized as String (BigInt-safe). Format: "{inte
|
|
|
5756
5850
|
"""
|
|
5757
5851
|
scalar UnsignedInt64
|
|
5758
5852
|
|
|
5853
|
+
"""Payload for customerUnsubscribeFromMarketing mutation"""
|
|
5854
|
+
type UnsubscribeFromMarketingPayload {
|
|
5855
|
+
"""Always true (anti-enumeration)."""
|
|
5856
|
+
accepted: Boolean!
|
|
5857
|
+
|
|
5858
|
+
"""User-facing errors"""
|
|
5859
|
+
userErrors: [UserError!]!
|
|
5860
|
+
}
|
|
5861
|
+
|
|
5759
5862
|
"""URL redirect (SEO, store migration)"""
|
|
5760
5863
|
type UrlRedirect {
|
|
5761
5864
|
"""Source path"""
|