@cartbase/storefront 0.19.0 → 0.20.1

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.
Files changed (105) hide show
  1. package/LICENSE +21 -21
  2. package/package.json +258 -258
  3. package/src/api/auth.ts +108 -108
  4. package/src/api/carts.ts +523 -523
  5. package/src/api/categories.ts +184 -184
  6. package/src/api/checkout.ts +526 -526
  7. package/src/api/collections.ts +130 -130
  8. package/src/api/consent.ts +75 -75
  9. package/src/api/content.ts +125 -125
  10. package/src/api/customers.ts +303 -303
  11. package/src/api/gift-cards.ts +112 -112
  12. package/src/api/http.ts +180 -180
  13. package/src/api/index.ts +30 -30
  14. package/src/api/menus.ts +77 -77
  15. package/src/api/metaobjects.ts +136 -136
  16. package/src/api/orders.ts +290 -290
  17. package/src/api/redirects.ts +37 -37
  18. package/src/api/regions.ts +200 -200
  19. package/src/api/reviews.ts +22 -1
  20. package/src/api/search.ts +163 -163
  21. package/src/api/store.ts +35 -35
  22. package/src/api/types.ts +91 -91
  23. package/src/cart-drawer/context.tsx +778 -778
  24. package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
  25. package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
  26. package/src/cart-drawer/gift-wrap.tsx +82 -82
  27. package/src/cart-drawer/item/index.tsx +162 -162
  28. package/src/cart-drawer/item/upsell.tsx +110 -110
  29. package/src/cart-drawer/labels.ts +123 -123
  30. package/src/cart-drawer/mutation-queue.ts +78 -78
  31. package/src/cart-drawer/notes.tsx +131 -131
  32. package/src/cart-drawer/sticky-footer.tsx +73 -73
  33. package/src/cart-drawer/summary-breakdown.tsx +197 -197
  34. package/src/checkout/address-error-copy.ts +117 -117
  35. package/src/checkout/boxnow-locker-selector.tsx +410 -410
  36. package/src/checkout/compare-addresses.ts +40 -40
  37. package/src/checkout/discount-section.tsx +218 -218
  38. package/src/checkout/error-copy-codes.ts +63 -63
  39. package/src/checkout/geocode.ts +154 -154
  40. package/src/checkout/gift-card-section.tsx +224 -224
  41. package/src/checkout/index.ts +72 -72
  42. package/src/checkout/labels.ts +495 -495
  43. package/src/checkout/payment-button.tsx +372 -372
  44. package/src/checkout/payment-error-copy.ts +154 -154
  45. package/src/checkout/promotion-error-copy.ts +91 -91
  46. package/src/common/country-flag.tsx +52 -52
  47. package/src/common/country-select.tsx +11 -11
  48. package/src/common/index.ts +20 -20
  49. package/src/common/market-select.tsx +57 -57
  50. package/src/index.ts +12 -12
  51. package/src/lib/country-name.ts +59 -59
  52. package/src/lib/get-product-price.ts +133 -133
  53. package/src/lib/media-image.tsx +39 -39
  54. package/src/lib/payment-constants.ts +53 -53
  55. package/src/lib/platform.ts +13 -13
  56. package/src/lib/price.tsx +39 -39
  57. package/src/lib/store-api-error.ts +36 -36
  58. package/src/lib/variant-caption.ts +32 -32
  59. package/src/locales/bg.ts +3 -2
  60. package/src/locales/context.ts +37 -37
  61. package/src/locales/en.ts +26 -26
  62. package/src/locales/es.ts +3 -2
  63. package/src/locales/index.ts +19 -19
  64. package/src/locales/provider.tsx +59 -59
  65. package/src/locales/types.ts +77 -77
  66. package/src/order/index.ts +62 -62
  67. package/src/order/labels.ts +79 -79
  68. package/src/order/order-totals.tsx +250 -250
  69. package/src/primitives/select-field.tsx +93 -93
  70. package/src/products/image-gallery.tsx +43 -43
  71. package/src/products/option-select.tsx +67 -67
  72. package/src/products/product-info.tsx +5 -5
  73. package/src/products/product-price.tsx +69 -69
  74. package/src/products/product-promises.tsx +61 -61
  75. package/src/products/product-specs.tsx +65 -65
  76. package/src/products/product-tabs.tsx +123 -123
  77. package/src/products/purchase-options.tsx +130 -130
  78. package/src/products/sets.ts +110 -110
  79. package/src/products/variant-matching.ts +71 -71
  80. package/src/products/variant-url.ts +74 -74
  81. package/src/reviews-ui/index.ts +12 -0
  82. package/src/reviews-ui/labels.ts +5 -2
  83. package/src/reviews-ui/lightbox-state.ts +46 -46
  84. package/src/reviews-ui/review-header.tsx +166 -0
  85. package/src/reviews-ui/review-lightbox.tsx +271 -271
  86. package/src/reviews-ui/review-list.tsx +193 -191
  87. package/src/reviews-ui/review-widget.tsx +63 -71
  88. package/src/reviews-ui/star-badge.tsx +6 -4
  89. package/src/reviews-ui/widget-options.ts +55 -0
  90. package/src/store/category-template.tsx +136 -136
  91. package/src/store/index.ts +40 -40
  92. package/src/tracking/chatgpt-pixel.tsx +99 -99
  93. package/src/tracking/consent-init.tsx +62 -62
  94. package/src/tracking/events.ts +348 -348
  95. package/src/tracking/ga4.tsx +93 -93
  96. package/src/tracking/google-ads.ts +84 -84
  97. package/src/tracking/gtm.tsx +60 -60
  98. package/src/tracking/inline-script.ts +49 -49
  99. package/src/tracking/oaiq.ts +206 -206
  100. package/src/tracking/tiktok-pixel.tsx +91 -91
  101. package/src/tracking/track-init.tsx +56 -56
  102. package/src/tracking/track-order-purchase.tsx +122 -122
  103. package/src/tracking/ttq.ts +180 -180
  104. package/src/tracking/use-tracking-config.ts +54 -54
  105. package/theme/index.css +25 -25
package/src/api/search.ts CHANGED
@@ -1,163 +1,163 @@
1
- /**
2
- * @cartbase/storefront/api/search — storefront search + related products
3
- * (search-discovery card).
4
- *
5
- * Results are the SAME `StoreProduct` objects the products listing serves —
6
- * reuse your product-card renderer as-is. Matching: full-text
7
- * (Cyrillic-correct, prefix) ∪ title trigram typo tolerance ∪ SKU prefix ∪
8
- * exact tag value; admin synonyms expand bidirectionally one level; admin
9
- * pins for the exact query rank first, then global boosts, then relevance.
10
- * Docs: docs/storefront/search.md.
11
- */
12
- import type { Query, StorefrontClient } from "./http"
13
- import type { PricingContextQuery } from "./types"
14
- import type { StoreProduct } from "./products"
15
-
16
- export interface SearchFacetBucket {
17
- /** Filter value to send back (id, option value, or "min-max" for price). */
18
- value: string
19
- label: string
20
- count: number
21
- /** Range (price) buckets only — pass back as `price_min`/`price_max`. */
22
- min?: number
23
- max?: number | null
24
- }
25
-
26
- export interface SearchFacet {
27
- /** "price" | "availability" | "type" | "tags" | "collection" | "options.<Title>" */
28
- key: string
29
- label: string
30
- type: "value" | "range"
31
- buckets: SearchFacetBucket[]
32
- }
33
-
34
- export interface SearchProductsQuery extends PricingContextQuery {
35
- /** Required, min 1 char. */
36
- q: string
37
- collection_id?: string[]
38
- category_id?: string[]
39
- tag_id?: string[]
40
- type_id?: string[]
41
- /** Major units in the pricing currency, applied to the cheapest base price. */
42
- price_min?: number
43
- price_max?: number
44
- availability?: "in_stock" | "out_of_stock"
45
- /**
46
- * Option filters: title → accepted values (OR within an option, AND
47
- * across options). Serialized as `option.<Title>=v1,v2` params.
48
- */
49
- options?: Record<string, string[]>
50
- /** 1–100, default 20. */
51
- limit?: number
52
- offset?: number
53
- }
54
-
55
- export interface SearchProductsResponse {
56
- results: StoreProduct[]
57
- /**
58
- * Per the tenant's Settings → Search & discovery → Filters config.
59
- * Counts follow the standard faceting rule: each facet is computed on the
60
- * result set filtered by every OTHER active filter (own dimension
61
- * excluded). Empty buckets and empty facets are omitted.
62
- */
63
- facets: SearchFacet[]
64
- total: number
65
- offset: number
66
- limit: number
67
- }
68
-
69
- export interface RelatedProductsQuery extends PricingContextQuery {
70
- /** 1–24, default 12. */
71
- limit?: number
72
- }
73
-
74
- /**
75
- * Complementary products — the merchant's "goes with this" picks, for the
76
- * slot ABOVE the product rather than the carousel below it.
77
- *
78
- * It has no fallback and never will: "similar to this" can be computed from
79
- * collections and tags, "buy this with it" is a merchant judgement, and a
80
- * guessed upsell beside the buy button is worse than an empty slot. An empty
81
- * response means the merchant has not chosen — render nothing.
82
- */
83
- export type ComplementaryProductsQuery = RelatedProductsQuery
84
-
85
- export interface RelatedProductsResponse {
86
- products: StoreProduct[]
87
- count: number
88
- /** True when any item came from the deterministic fallback chain. */
89
- auto_filled: boolean
90
- }
91
-
92
- /** Fold the `options` record into `option.<Title>` wire params. */
93
- function toSearchParams(query: SearchProductsQuery): Query {
94
- const { options, ...rest } = query
95
- const out: Query = { ...rest }
96
- if (options) {
97
- for (const [title, values] of Object.entries(options)) {
98
- if (values.length) out[`option.${title}`] = values
99
- }
100
- }
101
- return out
102
- }
103
-
104
- /**
105
- * Search published products. Draft products, other tenants, and products
106
- * outside the publishable key's channels NEVER appear (pins included).
107
- *
108
- * Auth: anon (`x-client-id`); optional `x-publishable-api-key` scopes the
109
- * catalog to the key's channels (empty results when nothing matches);
110
- * optional Bearer JWT makes `calculated_price` group-aware — never cache
111
- * price-decorated responses shared.
112
- *
113
- * Errors: 400 `missing_client_id`, 400 `validation_failed` (missing/empty
114
- * `q`, bad enum), 400 `invalid_publishable_key`, 400 `invalid_region`.
115
- * Settings: Search & discovery — synonyms, pins/boosts, facet config
116
- * (order/enabled, price bucket strategy).
117
- */
118
- export async function searchProducts(
119
- client: StorefrontClient,
120
- query: SearchProductsQuery
121
- ): Promise<SearchProductsResponse> {
122
- return client.get("/api/store/products/search", { query: toSearchParams(query) })
123
- }
124
-
125
- /**
126
- * Related products for a PDP: manual admin picks first (position order),
127
- * then a deterministic fallback fills to `limit` — same primary collection
128
- * (newest first), then most-shared-tags. Accepts a product id or handle.
129
- *
130
- * Auth: anon (`x-client-id`); optional publishable key — the anchor product
131
- * must be visible to the key (404 otherwise) and scoped-away products never
132
- * appear as related items. Errors: 404 `not_found` (unknown/draft/
133
- * channel-hidden anchor), 400 `validation_failed`, 400 `invalid_region`.
134
- * Settings: manual related picks (Admin → Product → Related).
135
- */
136
- export async function listRelatedProducts(
137
- client: StorefrontClient,
138
- idOrHandle: string,
139
- query?: RelatedProductsQuery
140
- ): Promise<RelatedProductsResponse> {
141
- return client.get(`/api/store/products/${encodeURIComponent(idOrHandle)}/related`, {
142
- query: { ...query },
143
- })
144
- }
145
-
146
- /**
147
- * Complementary products for a PDP: the merchant's manual picks, in their
148
- * order, and nothing else — `auto_filled` is always false.
149
- *
150
- * Same endpoint as related, discriminated by `kind`, because the two lists
151
- * are one table and one code path apart only by meaning. Auth, errors and
152
- * channel scoping are identical to `listRelatedProducts`.
153
- * Settings: Admin → Product → Complementary products.
154
- */
155
- export async function listComplementaryProducts(
156
- client: StorefrontClient,
157
- idOrHandle: string,
158
- query?: ComplementaryProductsQuery
159
- ): Promise<RelatedProductsResponse> {
160
- return client.get(`/api/store/products/${encodeURIComponent(idOrHandle)}/related`, {
161
- query: { ...query, kind: "complementary" },
162
- })
163
- }
1
+ /**
2
+ * @cartbase/storefront/api/search — storefront search + related products
3
+ * (search-discovery card).
4
+ *
5
+ * Results are the SAME `StoreProduct` objects the products listing serves —
6
+ * reuse your product-card renderer as-is. Matching: full-text
7
+ * (Cyrillic-correct, prefix) ∪ title trigram typo tolerance ∪ SKU prefix ∪
8
+ * exact tag value; admin synonyms expand bidirectionally one level; admin
9
+ * pins for the exact query rank first, then global boosts, then relevance.
10
+ * Docs: docs/storefront/search.md.
11
+ */
12
+ import type { Query, StorefrontClient } from "./http"
13
+ import type { PricingContextQuery } from "./types"
14
+ import type { StoreProduct } from "./products"
15
+
16
+ export interface SearchFacetBucket {
17
+ /** Filter value to send back (id, option value, or "min-max" for price). */
18
+ value: string
19
+ label: string
20
+ count: number
21
+ /** Range (price) buckets only — pass back as `price_min`/`price_max`. */
22
+ min?: number
23
+ max?: number | null
24
+ }
25
+
26
+ export interface SearchFacet {
27
+ /** "price" | "availability" | "type" | "tags" | "collection" | "options.<Title>" */
28
+ key: string
29
+ label: string
30
+ type: "value" | "range"
31
+ buckets: SearchFacetBucket[]
32
+ }
33
+
34
+ export interface SearchProductsQuery extends PricingContextQuery {
35
+ /** Required, min 1 char. */
36
+ q: string
37
+ collection_id?: string[]
38
+ category_id?: string[]
39
+ tag_id?: string[]
40
+ type_id?: string[]
41
+ /** Major units in the pricing currency, applied to the cheapest base price. */
42
+ price_min?: number
43
+ price_max?: number
44
+ availability?: "in_stock" | "out_of_stock"
45
+ /**
46
+ * Option filters: title → accepted values (OR within an option, AND
47
+ * across options). Serialized as `option.<Title>=v1,v2` params.
48
+ */
49
+ options?: Record<string, string[]>
50
+ /** 1–100, default 20. */
51
+ limit?: number
52
+ offset?: number
53
+ }
54
+
55
+ export interface SearchProductsResponse {
56
+ results: StoreProduct[]
57
+ /**
58
+ * Per the tenant's Settings → Search & discovery → Filters config.
59
+ * Counts follow the standard faceting rule: each facet is computed on the
60
+ * result set filtered by every OTHER active filter (own dimension
61
+ * excluded). Empty buckets and empty facets are omitted.
62
+ */
63
+ facets: SearchFacet[]
64
+ total: number
65
+ offset: number
66
+ limit: number
67
+ }
68
+
69
+ export interface RelatedProductsQuery extends PricingContextQuery {
70
+ /** 1–24, default 12. */
71
+ limit?: number
72
+ }
73
+
74
+ /**
75
+ * Complementary products — the merchant's "goes with this" picks, for the
76
+ * slot ABOVE the product rather than the carousel below it.
77
+ *
78
+ * It has no fallback and never will: "similar to this" can be computed from
79
+ * collections and tags, "buy this with it" is a merchant judgement, and a
80
+ * guessed upsell beside the buy button is worse than an empty slot. An empty
81
+ * response means the merchant has not chosen — render nothing.
82
+ */
83
+ export type ComplementaryProductsQuery = RelatedProductsQuery
84
+
85
+ export interface RelatedProductsResponse {
86
+ products: StoreProduct[]
87
+ count: number
88
+ /** True when any item came from the deterministic fallback chain. */
89
+ auto_filled: boolean
90
+ }
91
+
92
+ /** Fold the `options` record into `option.<Title>` wire params. */
93
+ function toSearchParams(query: SearchProductsQuery): Query {
94
+ const { options, ...rest } = query
95
+ const out: Query = { ...rest }
96
+ if (options) {
97
+ for (const [title, values] of Object.entries(options)) {
98
+ if (values.length) out[`option.${title}`] = values
99
+ }
100
+ }
101
+ return out
102
+ }
103
+
104
+ /**
105
+ * Search published products. Draft products, other tenants, and products
106
+ * outside the publishable key's channels NEVER appear (pins included).
107
+ *
108
+ * Auth: anon (`x-client-id`); optional `x-publishable-api-key` scopes the
109
+ * catalog to the key's channels (empty results when nothing matches);
110
+ * optional Bearer JWT makes `calculated_price` group-aware — never cache
111
+ * price-decorated responses shared.
112
+ *
113
+ * Errors: 400 `missing_client_id`, 400 `validation_failed` (missing/empty
114
+ * `q`, bad enum), 400 `invalid_publishable_key`, 400 `invalid_region`.
115
+ * Settings: Search & discovery — synonyms, pins/boosts, facet config
116
+ * (order/enabled, price bucket strategy).
117
+ */
118
+ export async function searchProducts(
119
+ client: StorefrontClient,
120
+ query: SearchProductsQuery
121
+ ): Promise<SearchProductsResponse> {
122
+ return client.get("/api/store/products/search", { query: toSearchParams(query) })
123
+ }
124
+
125
+ /**
126
+ * Related products for a PDP: manual admin picks first (position order),
127
+ * then a deterministic fallback fills to `limit` — same primary collection
128
+ * (newest first), then most-shared-tags. Accepts a product id or handle.
129
+ *
130
+ * Auth: anon (`x-client-id`); optional publishable key — the anchor product
131
+ * must be visible to the key (404 otherwise) and scoped-away products never
132
+ * appear as related items. Errors: 404 `not_found` (unknown/draft/
133
+ * channel-hidden anchor), 400 `validation_failed`, 400 `invalid_region`.
134
+ * Settings: manual related picks (Admin → Product → Related).
135
+ */
136
+ export async function listRelatedProducts(
137
+ client: StorefrontClient,
138
+ idOrHandle: string,
139
+ query?: RelatedProductsQuery
140
+ ): Promise<RelatedProductsResponse> {
141
+ return client.get(`/api/store/products/${encodeURIComponent(idOrHandle)}/related`, {
142
+ query: { ...query },
143
+ })
144
+ }
145
+
146
+ /**
147
+ * Complementary products for a PDP: the merchant's manual picks, in their
148
+ * order, and nothing else — `auto_filled` is always false.
149
+ *
150
+ * Same endpoint as related, discriminated by `kind`, because the two lists
151
+ * are one table and one code path apart only by meaning. Auth, errors and
152
+ * channel scoping are identical to `listRelatedProducts`.
153
+ * Settings: Admin → Product → Complementary products.
154
+ */
155
+ export async function listComplementaryProducts(
156
+ client: StorefrontClient,
157
+ idOrHandle: string,
158
+ query?: ComplementaryProductsQuery
159
+ ): Promise<RelatedProductsResponse> {
160
+ return client.get(`/api/store/products/${encodeURIComponent(idOrHandle)}/related`, {
161
+ query: { ...query, kind: "complementary" },
162
+ })
163
+ }
package/src/api/store.ts CHANGED
@@ -1,35 +1,35 @@
1
- /**
2
- * @cartbase/storefront/api/store — the store's identity.
3
- *
4
- * Name, slug and brand (the merchant's Settings → Brand): what a
5
- * storefront needs to title, head and foot itself without hardcoding a
6
- * name. One anonymous read at layout level. Docs: docs/storefront/store.md.
7
- */
8
- import type { StorefrontClient } from "./http"
9
-
10
- export interface StoreBrand {
11
- /** Primary logo, public URL, or null. */
12
- logo_url: string | null
13
- /** Square mark, public URL, or null. */
14
- logo_square_url: string | null
15
- /** `#rrggbb` or null. */
16
- color_primary: string | null
17
- /** `#rrggbb` or null. */
18
- color_secondary: string | null
19
- slogan: string | null
20
- }
21
-
22
- export interface StoreIdentity {
23
- /** The store's public id: what `<PlatformInit storeId>` mounts. */
24
- id: string
25
- /** The store's display name. */
26
- name: string
27
- /** The store's slug; its address is `{slug}.cartbase.net` until a custom domain. */
28
- slug: string
29
- brand: StoreBrand
30
- }
31
-
32
- /** GET /api/store/store — anon. */
33
- export function getStore(client: StorefrontClient): Promise<{ store: StoreIdentity }> {
34
- return client.request<{ store: StoreIdentity }>("/api/store/store")
35
- }
1
+ /**
2
+ * @cartbase/storefront/api/store — the store's identity.
3
+ *
4
+ * Name, slug and brand (the merchant's Settings → Brand): what a
5
+ * storefront needs to title, head and foot itself without hardcoding a
6
+ * name. One anonymous read at layout level. Docs: docs/storefront/store.md.
7
+ */
8
+ import type { StorefrontClient } from "./http"
9
+
10
+ export interface StoreBrand {
11
+ /** Primary logo, public URL, or null. */
12
+ logo_url: string | null
13
+ /** Square mark, public URL, or null. */
14
+ logo_square_url: string | null
15
+ /** `#rrggbb` or null. */
16
+ color_primary: string | null
17
+ /** `#rrggbb` or null. */
18
+ color_secondary: string | null
19
+ slogan: string | null
20
+ }
21
+
22
+ export interface StoreIdentity {
23
+ /** The store's public id: what `<PlatformInit storeId>` mounts. */
24
+ id: string
25
+ /** The store's display name. */
26
+ name: string
27
+ /** The store's slug; its address is `{slug}.cartbase.net` until a custom domain. */
28
+ slug: string
29
+ brand: StoreBrand
30
+ }
31
+
32
+ /** GET /api/store/store — anon. */
33
+ export function getStore(client: StorefrontClient): Promise<{ store: StoreIdentity }> {
34
+ return client.request<{ store: StoreIdentity }>("/api/store/store")
35
+ }
package/src/api/types.ts CHANGED
@@ -1,91 +1,91 @@
1
- /**
2
- * @cartbase/storefront — shared API surface types.
3
- *
4
- * The envelope + error contract every domain module builds on. Shapes are
5
- * the documented store-API contract (docs/contracts/store-api.md and the
6
- * per-domain teaching docs under docs/storefront/): single entities come
7
- * back as `{ <resource>: {...} }`, lists as `{ <resources>: [...], count,
8
- * offset, limit }`, and errors as `{ error, code, details? }`.
9
- *
10
- * Domain modules own their DTOs; only cross-domain primitives live here.
11
- */
12
-
13
- /** List envelope every collection endpoint returns. */
14
- export interface ListEnvelope {
15
- count: number
16
- offset: number
17
- limit: number
18
- }
19
-
20
- /** The wire error body (`{ error, code, details? }`). */
21
- export interface StoreApiErrorBody {
22
- error?: string
23
- message?: string
24
- code?: string
25
- details?: unknown
26
- }
27
-
28
- /**
29
- * Thrown for every non-2xx response. `status` + `code` are the stable
30
- * programmatic surface (codes are contract-listed per endpoint, e.g.
31
- * `invalid_gift_card`, `checkout_method_hidden`, `rate_limited`);
32
- * `body` keeps the raw payload for anything else.
33
- */
34
- export class StoreApiError extends Error {
35
- readonly status: number
36
- readonly code: string | null
37
- readonly body: unknown
38
-
39
- constructor(status: number, body: unknown) {
40
- const parsed = (body ?? {}) as StoreApiErrorBody
41
- super(parsed.error ?? parsed.message ?? `Store API error (${status})`)
42
- this.name = "StoreApiError"
43
- this.status = status
44
- this.code = typeof parsed.code === "string" ? parsed.code : null
45
- this.body = body
46
- }
47
- }
48
-
49
- /** ISO-8601 timestamp string (the wire format for every date field). */
50
- export type IsoDateString = string
51
-
52
- /**
53
- * Money amounts are decimal major units (EUR 45.5, not cents) everywhere in
54
- * the store API. Conversion to minor units happens only at provider
55
- * boundaries server-side — the SDK never does money math (server truth).
56
- */
57
- export type MajorUnitAmount = number
58
-
59
- /** `variant.calculated_price` (b2b-v1 pricing context, store-api.md). */
60
- export interface CalculatedPrice {
61
- calculated_amount: MajorUnitAmount
62
- original_amount: MajorUnitAmount
63
- currency_code: string
64
- is_calculated_price_price_list: boolean
65
- price_list_id: string | null
66
- price_list_type: "sale" | "override" | null
67
- }
68
-
69
- /** Raw base price row on variants (`product.prices[]` shape detail). */
70
- export interface PriceRow {
71
- amount: MajorUnitAmount
72
- currency_code: string
73
- min_quantity?: number | null
74
- max_quantity?: number | null
75
- }
76
-
77
- /**
78
- * Pricing context accepted by every price-decorated listing (products,
79
- * collections products, search, related). Server composes the rest
80
- * (customer groups from the Bearer JWT, never from params).
81
- */
82
- export interface PricingContextQuery {
83
- currency_code?: string
84
- region_id?: string
85
- }
86
-
87
- /** Standard pagination query accepted by list endpoints. */
88
- export interface PaginationQuery {
89
- limit?: number
90
- offset?: number
91
- }
1
+ /**
2
+ * @cartbase/storefront — shared API surface types.
3
+ *
4
+ * The envelope + error contract every domain module builds on. Shapes are
5
+ * the documented store-API contract (docs/contracts/store-api.md and the
6
+ * per-domain teaching docs under docs/storefront/): single entities come
7
+ * back as `{ <resource>: {...} }`, lists as `{ <resources>: [...], count,
8
+ * offset, limit }`, and errors as `{ error, code, details? }`.
9
+ *
10
+ * Domain modules own their DTOs; only cross-domain primitives live here.
11
+ */
12
+
13
+ /** List envelope every collection endpoint returns. */
14
+ export interface ListEnvelope {
15
+ count: number
16
+ offset: number
17
+ limit: number
18
+ }
19
+
20
+ /** The wire error body (`{ error, code, details? }`). */
21
+ export interface StoreApiErrorBody {
22
+ error?: string
23
+ message?: string
24
+ code?: string
25
+ details?: unknown
26
+ }
27
+
28
+ /**
29
+ * Thrown for every non-2xx response. `status` + `code` are the stable
30
+ * programmatic surface (codes are contract-listed per endpoint, e.g.
31
+ * `invalid_gift_card`, `checkout_method_hidden`, `rate_limited`);
32
+ * `body` keeps the raw payload for anything else.
33
+ */
34
+ export class StoreApiError extends Error {
35
+ readonly status: number
36
+ readonly code: string | null
37
+ readonly body: unknown
38
+
39
+ constructor(status: number, body: unknown) {
40
+ const parsed = (body ?? {}) as StoreApiErrorBody
41
+ super(parsed.error ?? parsed.message ?? `Store API error (${status})`)
42
+ this.name = "StoreApiError"
43
+ this.status = status
44
+ this.code = typeof parsed.code === "string" ? parsed.code : null
45
+ this.body = body
46
+ }
47
+ }
48
+
49
+ /** ISO-8601 timestamp string (the wire format for every date field). */
50
+ export type IsoDateString = string
51
+
52
+ /**
53
+ * Money amounts are decimal major units (EUR 45.5, not cents) everywhere in
54
+ * the store API. Conversion to minor units happens only at provider
55
+ * boundaries server-side — the SDK never does money math (server truth).
56
+ */
57
+ export type MajorUnitAmount = number
58
+
59
+ /** `variant.calculated_price` (b2b-v1 pricing context, store-api.md). */
60
+ export interface CalculatedPrice {
61
+ calculated_amount: MajorUnitAmount
62
+ original_amount: MajorUnitAmount
63
+ currency_code: string
64
+ is_calculated_price_price_list: boolean
65
+ price_list_id: string | null
66
+ price_list_type: "sale" | "override" | null
67
+ }
68
+
69
+ /** Raw base price row on variants (`product.prices[]` shape detail). */
70
+ export interface PriceRow {
71
+ amount: MajorUnitAmount
72
+ currency_code: string
73
+ min_quantity?: number | null
74
+ max_quantity?: number | null
75
+ }
76
+
77
+ /**
78
+ * Pricing context accepted by every price-decorated listing (products,
79
+ * collections products, search, related). Server composes the rest
80
+ * (customer groups from the Bearer JWT, never from params).
81
+ */
82
+ export interface PricingContextQuery {
83
+ currency_code?: string
84
+ region_id?: string
85
+ }
86
+
87
+ /** Standard pagination query accepted by list endpoints. */
88
+ export interface PaginationQuery {
89
+ limit?: number
90
+ offset?: number
91
+ }