@cartbase/storefront 0.15.0 → 0.16.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.
@@ -1,314 +1,330 @@
1
- /**
2
- * @cartbase/storefront/api/products — products + product variants.
3
- *
4
- * The catalog read surface. Response shapes are the canonical store product
5
- * object every discovery endpoint reuses (search results, collection
6
- * membership pages, related products) — build ONE product-card renderer
7
- * against `StoreProduct` and reuse it everywhere.
8
- *
9
- * Pricing: pass `currency_code` (or `region_id`) to receive
10
- * `variant.calculated_price` (group-aware when a customer JWT is attached).
11
- * Raw `prices` embeds contain BASE rows only — price-list rows are stripped
12
- * server-side (leak rule) and surface exclusively through
13
- * `calculated_price`. Docs: docs/storefront/products.md.
14
- */
15
- import type { StorefrontClient } from "./http"
16
- import type {
17
- CalculatedPrice,
18
- IsoDateString,
19
- ListEnvelope,
20
- PaginationQuery,
21
- PricingContextQuery,
22
- } from "./types"
23
-
24
- /** Raw base price row (`prices` table). Amounts are EUR-style major units. */
25
- export interface StorePriceRow {
26
- id: string
27
- amount: number
28
- currency_code: string
29
- min_quantity: number | null
30
- max_quantity: number | null
31
- price_set_id: string
32
- /** Always null on the store surface — price-list rows are stripped. */
33
- price_list_id: string | null
34
- created_at: IsoDateString
35
- updated_at: IsoDateString
36
- }
37
-
38
- /**
39
- * The wire shape of `variant.prices`: a price-set LINK embed, not a flat
40
- * array. Base rows live at `prices[].price_set.prices[]`.
41
- */
42
- export interface StoreVariantPriceSetLink {
43
- price_set: { prices: StorePriceRow[] | null } | null
44
- }
45
-
46
- /** `variant.options[]` — the option VALUE with its parent option embedded. */
47
- export interface StoreVariantOptionValueLink {
48
- value: {
49
- id: string
50
- value: string
51
- option_id: string
52
- metadata: Record<string, unknown> | null
53
- option: StoreProductOption | null
54
- } | null
55
- }
56
-
57
- export interface StoreProductOptionValue {
58
- id: string
59
- value: string
60
- option_id: string
61
- metadata: Record<string, unknown> | null
62
- created_at: IsoDateString
63
- updated_at: IsoDateString
64
- }
65
-
66
- export interface StoreProductOption {
67
- id: string
68
- title: string
69
- product_id: string
70
- /** The merchant's order of the options on the product page. */
71
- position: number
72
- metadata: Record<string, unknown> | null
73
- created_at: IsoDateString
74
- updated_at: IsoDateString
75
- /** Present on `product.options[]`; absent inside variant option links. */
76
- values?: StoreProductOptionValue[]
77
- }
78
-
79
- export interface StoreProductImage {
80
- id: string
81
- url: string
82
- rank: number
83
- product_id: string
84
- metadata: Record<string, unknown> | null
85
- created_at: IsoDateString
86
- updated_at: IsoDateString
87
- }
88
-
89
- export interface StoreProductVariant {
90
- id: string
91
- title: string
92
- product_id: string
93
- sku: string | null
94
- barcode: string | null
95
- ean: string | null
96
- upc: string | null
97
- thumbnail: string | null
98
- allow_backorder: boolean
99
- manage_inventory: boolean
100
- variant_rank: number
101
- weight: number | null
102
- length: number | null
103
- height: number | null
104
- width: number | null
105
- hs_code: string | null
106
- origin_country: string | null
107
- mid_code: string | null
108
- material: string | null
109
- metadata: Record<string, unknown> | null
110
- created_at: IsoDateString
111
- updated_at: IsoDateString
112
- options: StoreVariantOptionValueLink[]
113
- /** Base-price rows via the price-set link embed (leak rule applied). */
114
- prices: StoreVariantPriceSetLink[]
115
- /**
116
- * Present when a pricing context was resolvable (`currency_code` or
117
- * `region_id` given); null otherwise, and null when no price matches the
118
- * context. Varies by customer group — never cache shared.
119
- */
120
- calculated_price: CalculatedPrice | null
121
- /**
122
- * THE availability predicate, computed server-side (untracked or
123
- * backorderable ⇒ true; otherwise kit-aware available stock > 0).
124
- * Optional only for wire back-compat with platforms that predate it —
125
- * when absent, components fall back to the optimistic legacy behavior.
126
- */
127
- in_stock?: boolean
128
- }
129
-
130
- export interface StoreProduct {
131
- id: string
132
- title: string
133
- subtitle: string | null
134
- description: string | null
135
- handle: string
136
- status: "published"
137
- thumbnail: string | null
138
- is_giftcard: boolean
139
- discountable: boolean
140
- collection_id: string | null
141
- type_id: string | null
142
- external_id: string | null
143
- /** The brand, as plain text on the product (product-vendor card). */
144
- vendor: string | null
145
- weight: number | null
146
- length: number | null
147
- height: number | null
148
- width: number | null
149
- hs_code: string | null
150
- origin_country: string | null
151
- mid_code: string | null
152
- material: string | null
153
- /** SEO override; null = fall back to `title`. */
154
- seo_title: string | null
155
- /** SEO override; null = fall back to the plain-text `description`. */
156
- seo_description: string | null
157
- metadata: Record<string, unknown> | null
158
- created_at: IsoDateString
159
- updated_at: IsoDateString
160
- variants: StoreProductVariant[]
161
- images: StoreProductImage[]
162
- options: StoreProductOption[]
163
- collection: { id: string; title: string; handle: string } | null
164
- categories: Array<{ category: { id: string; name: string; handle: string } | null }>
165
- tags: Array<{ tag: { id: string; value: string } | null }>
166
- type: { id: string; value: string } | null
167
- }
168
-
169
- export interface ListProductsQuery extends PricingContextQuery, PaginationQuery {
170
- /** Case-insensitive substring match on title (full search: use /search). */
171
- q?: string
172
- /** Product ids (joined as CSV on the wire). */
173
- id?: string[]
174
- handle?: string
175
- collection_id?: string[]
176
- category_id?: string[]
177
- tag_id?: string[]
178
- type_id?: string[]
179
- /**
180
- * Sort column; prefix with `-` for descending (e.g. `-created_at`,
181
- * the default).
182
- */
183
- order?: string
184
- }
185
-
186
- export interface ProductListResponse extends ListEnvelope {
187
- products: StoreProduct[]
188
- }
189
-
190
- export interface ProductResponse {
191
- product: StoreProduct
192
- }
193
-
194
- export interface ListProductVariantsQuery extends PricingContextQuery, PaginationQuery {
195
- /** Variant ids as CSV in ONE string (wire format of the route). */
196
- id?: string
197
- product_id?: string
198
- sku?: string
199
- }
200
-
201
- export interface ProductVariantListResponse extends ListEnvelope {
202
- variants: StoreProductVariant[]
203
- }
204
-
205
- export interface ProductVariantResponse {
206
- variant: StoreProductVariant
207
- }
208
-
209
- /**
210
- * List published products (newest first by default).
211
- *
212
- * Auth: anon (`x-client-id`); optional `x-publishable-api-key` restricts the
213
- * catalog to products linked to the key's sales channels (a channel-scoped
214
- * key with no matching products returns an empty list). Optional Bearer JWT
215
- * makes `calculated_price` customer-group-aware.
216
- *
217
- * Errors: 400 `missing_client_id`, 400 `validation_failed`,
218
- * 400 `invalid_publishable_key`, 400 `invalid_region`.
219
- * Settings: price lists (B2B pricing), sales-channel product links,
220
- * publishable-key channel bindings.
221
- */
222
- export async function listProducts(
223
- client: StorefrontClient,
224
- query?: ListProductsQuery
225
- ): Promise<ProductListResponse> {
226
- return client.get("/api/store/products", { query: { ...query } })
227
- }
228
-
229
- /**
230
- * Retrieve one product by id (`prod_…`) or handle storefronts deep-link by
231
- * handle.
232
- *
233
- * Auth: anon (`x-client-id`); optional publishable key + Bearer JWT as on
234
- * the list. Errors: 404 `not_found` (unknown, draft — RLS-hidden — or
235
- * outside the key's channels: invisible, not forbidden),
236
- * 400 `invalid_publishable_key`, 400 `invalid_region`.
237
- */
238
- export async function retrieveProduct(
239
- client: StorefrontClient,
240
- idOrHandle: string,
241
- query?: PricingContextQuery
242
- ): Promise<ProductResponse> {
243
- return client.get(`/api/store/products/${encodeURIComponent(idOrHandle)}`, {
244
- query: { ...query },
245
- })
246
- }
247
-
248
- /**
249
- * List product variants (cart-line hydration). `id` accepts a CSV of variant
250
- * ids for multi-lookup. Ordered by `variant_rank`.
251
- *
252
- * Auth: anon (`x-client-id`); optional Bearer JWT for group-aware
253
- * `calculated_price`. NOTE: this listing is NOT publishable-key
254
- * channel-scoped (scope applies to product reads). Errors:
255
- * 400 `missing_client_id`, 400 `validation_failed`, 400 `invalid_region`.
256
- */
257
- export async function listProductVariants(
258
- client: StorefrontClient,
259
- query?: ListProductVariantsQuery
260
- ): Promise<ProductVariantListResponse> {
261
- return client.get("/api/store/product-variants", { query: { ...query } })
262
- }
263
-
264
- /**
265
- * Retrieve one variant by id, options + base prices hydrated,
266
- * `calculated_price` when a pricing context is given.
267
- *
268
- * Auth: anon (`x-client-id`). Errors: 404 `not_found`, 400 `invalid_region`.
269
- */
270
- export async function retrieveProductVariant(
271
- client: StorefrontClient,
272
- variantId: string,
273
- query?: PricingContextQuery
274
- ): Promise<ProductVariantResponse> {
275
- return client.get(`/api/store/product-variants/${encodeURIComponent(variantId)}`, {
276
- query: { ...query },
277
- })
278
- }
279
-
280
- /** A subscription offer on a product (PDP purchase options). */
281
- export interface StoreSellingPlan {
282
- id: string
283
- name: string
284
- interval: "day" | "week" | "month" | "year"
285
- interval_count: number
286
- /** null = catalog price (the plan only sets the cadence). */
287
- pricing_type: "percent" | "fixed" | null
288
- pricing_value: number | null
289
- min_cycles: number | null
290
- max_cycles: number | null
291
- }
292
-
293
- export interface SellingPlanListResponse {
294
- selling_plans: StoreSellingPlan[]
295
- count: number
296
- }
297
-
298
- /**
299
- * The subscription plans a product can be purchased with — render as PDP
300
- * purchase options (one-time vs each plan). The chosen plan id goes on the
301
- * cart line (`carts.addLineItem` `selling_plan_id`); the server applies the
302
- * plan price. Empty list = product is one-time only.
303
- *
304
- * Auth: anon (`x-client-id`); publishable-key channel scope applies (same
305
- * rule as the product read). Errors: 404 `not_found`.
306
- */
307
- export async function listSellingPlans(
308
- client: StorefrontClient,
309
- idOrHandle: string
310
- ): Promise<SellingPlanListResponse> {
311
- return client.get(
312
- `/api/store/products/${encodeURIComponent(idOrHandle)}/selling-plans`
313
- )
314
- }
1
+ /**
2
+ * @cartbase/storefront/api/products — products + product variants.
3
+ *
4
+ * The catalog read surface. Response shapes are the canonical store product
5
+ * object every discovery endpoint reuses (search results, collection
6
+ * membership pages, related products) — build ONE product-card renderer
7
+ * against `StoreProduct` and reuse it everywhere.
8
+ *
9
+ * Pricing: pass `currency_code` (or `region_id`) to receive
10
+ * `variant.calculated_price` (group-aware when a customer JWT is attached).
11
+ * Raw `prices` embeds contain BASE rows only — price-list rows are stripped
12
+ * server-side (leak rule) and surface exclusively through
13
+ * `calculated_price`. Docs: docs/storefront/products.md.
14
+ */
15
+ import type { StorefrontClient } from "./http"
16
+ import type {
17
+ CalculatedPrice,
18
+ IsoDateString,
19
+ ListEnvelope,
20
+ PaginationQuery,
21
+ PricingContextQuery,
22
+ } from "./types"
23
+
24
+ /** Raw base price row (`prices` table). Amounts are EUR-style major units. */
25
+ export interface StorePriceRow {
26
+ id: string
27
+ amount: number
28
+ currency_code: string
29
+ min_quantity: number | null
30
+ max_quantity: number | null
31
+ price_set_id: string
32
+ /** Always null on the store surface — price-list rows are stripped. */
33
+ price_list_id: string | null
34
+ created_at: IsoDateString
35
+ updated_at: IsoDateString
36
+ }
37
+
38
+ /**
39
+ * The wire shape of `variant.prices`: a price-set LINK embed, not a flat
40
+ * array. Base rows live at `prices[].price_set.prices[]`.
41
+ */
42
+ export interface StoreVariantPriceSetLink {
43
+ price_set: { prices: StorePriceRow[] | null } | null
44
+ }
45
+
46
+ /** `variant.options[]` — the option VALUE with its parent option embedded. */
47
+ export interface StoreVariantOptionValueLink {
48
+ value: {
49
+ id: string
50
+ value: string
51
+ option_id: string
52
+ metadata: Record<string, unknown> | null
53
+ option: StoreProductOption | null
54
+ } | null
55
+ }
56
+
57
+ export interface StoreProductOptionValue {
58
+ id: string
59
+ value: string
60
+ option_id: string
61
+ metadata: Record<string, unknown> | null
62
+ created_at: IsoDateString
63
+ updated_at: IsoDateString
64
+ }
65
+
66
+ export interface StoreProductOption {
67
+ id: string
68
+ title: string
69
+ product_id: string
70
+ /** The merchant's order of the options on the product page. */
71
+ position: number
72
+ metadata: Record<string, unknown> | null
73
+ created_at: IsoDateString
74
+ updated_at: IsoDateString
75
+ /** Present on `product.options[]`; absent inside variant option links. */
76
+ values?: StoreProductOptionValue[]
77
+ }
78
+
79
+ export interface StoreProductImage {
80
+ id: string
81
+ url: string
82
+ rank: number
83
+ product_id: string
84
+ metadata: Record<string, unknown> | null
85
+ created_at: IsoDateString
86
+ updated_at: IsoDateString
87
+ }
88
+
89
+ export interface StoreProductVariant {
90
+ id: string
91
+ title: string
92
+ product_id: string
93
+ sku: string | null
94
+ barcode: string | null
95
+ ean: string | null
96
+ upc: string | null
97
+ thumbnail: string | null
98
+ allow_backorder: boolean
99
+ manage_inventory: boolean
100
+ variant_rank: number
101
+ weight: number | null
102
+ length: number | null
103
+ height: number | null
104
+ width: number | null
105
+ hs_code: string | null
106
+ origin_country: string | null
107
+ mid_code: string | null
108
+ material: string | null
109
+ metadata: Record<string, unknown> | null
110
+ created_at: IsoDateString
111
+ updated_at: IsoDateString
112
+ options: StoreVariantOptionValueLink[]
113
+ /** Base-price rows via the price-set link embed (leak rule applied). */
114
+ prices: StoreVariantPriceSetLink[]
115
+ /**
116
+ * Present when a pricing context was resolvable (`currency_code` or
117
+ * `region_id` given); null otherwise, and null when no price matches the
118
+ * context. Varies by customer group — never cache shared.
119
+ */
120
+ calculated_price: CalculatedPrice | null
121
+ /**
122
+ * THE availability predicate, computed server-side (untracked or
123
+ * backorderable ⇒ true; otherwise kit-aware available stock > 0).
124
+ * Optional only for wire back-compat with platforms that predate it —
125
+ * when absent, components fall back to the optimistic legacy behavior.
126
+ */
127
+ in_stock?: boolean
128
+ }
129
+
130
+ export interface StoreProduct {
131
+ id: string
132
+ title: string
133
+ subtitle: string | null
134
+ description: string | null
135
+ handle: string
136
+ status: "published"
137
+ thumbnail: string | null
138
+ is_giftcard: boolean
139
+ discountable: boolean
140
+ type_id: string | null
141
+ external_id: string | null
142
+ /** The brand, as plain text on the product (product-vendor card). */
143
+ vendor: string | null
144
+ weight: number | null
145
+ length: number | null
146
+ height: number | null
147
+ width: number | null
148
+ hs_code: string | null
149
+ origin_country: string | null
150
+ mid_code: string | null
151
+ material: string | null
152
+ /** SEO override; null = fall back to `title`. */
153
+ seo_title: string | null
154
+ /** SEO override; null = fall back to the plain-text `description`. */
155
+ seo_description: string | null
156
+ metadata: Record<string, unknown> | null
157
+ /**
158
+ * The store's custom fields under their definition keys
159
+ * (`custom.product_label_1`), values as stored: a string for text, a
160
+ * number, a boolean, an array for a list, an id for a metaobject
161
+ * reference. Public definitions only (Settings, Custom fields switches one
162
+ * off); always present, `{}` when the product has none. Served by every
163
+ * product read since 2026-09-14 (docs/storefront/products.md).
164
+ */
165
+ metafields: Record<string, unknown>
166
+ created_at: IsoDateString
167
+ updated_at: IsoDateString
168
+ variants: StoreProductVariant[]
169
+ images: StoreProductImage[]
170
+ options: StoreProductOption[]
171
+ /**
172
+ * EVERY collection the product belongs to, in the shape categories and
173
+ * tags already had. It was a single `collection` object resolved through
174
+ * products.collection_id, a column that held one of a product's
175
+ * collections at most and none at all for the ones a catalogue migration
176
+ * or a smart rule placed; the membership join is the one store since
177
+ * 2026-09-15 (docs/storefront/products.md).
178
+ */
179
+ collections: Array<{ collection: { id: string; title: string; handle: string } | null }>
180
+ categories: Array<{ category: { id: string; name: string; handle: string } | null }>
181
+ tags: Array<{ tag: { id: string; value: string } | null }>
182
+ type: { id: string; value: string } | null
183
+ }
184
+
185
+ export interface ListProductsQuery extends PricingContextQuery, PaginationQuery {
186
+ /** Case-insensitive substring match on title (full search: use /search). */
187
+ q?: string
188
+ /** Product ids (joined as CSV on the wire). */
189
+ id?: string[]
190
+ handle?: string
191
+ collection_id?: string[]
192
+ category_id?: string[]
193
+ tag_id?: string[]
194
+ type_id?: string[]
195
+ /**
196
+ * Sort column; prefix with `-` for descending (e.g. `-created_at`,
197
+ * the default).
198
+ */
199
+ order?: string
200
+ }
201
+
202
+ export interface ProductListResponse extends ListEnvelope {
203
+ products: StoreProduct[]
204
+ }
205
+
206
+ export interface ProductResponse {
207
+ product: StoreProduct
208
+ }
209
+
210
+ export interface ListProductVariantsQuery extends PricingContextQuery, PaginationQuery {
211
+ /** Variant ids as CSV in ONE string (wire format of the route). */
212
+ id?: string
213
+ product_id?: string
214
+ sku?: string
215
+ }
216
+
217
+ export interface ProductVariantListResponse extends ListEnvelope {
218
+ variants: StoreProductVariant[]
219
+ }
220
+
221
+ export interface ProductVariantResponse {
222
+ variant: StoreProductVariant
223
+ }
224
+
225
+ /**
226
+ * List published products (newest first by default).
227
+ *
228
+ * Auth: anon (`x-client-id`); optional `x-publishable-api-key` restricts the
229
+ * catalog to products linked to the key's sales channels (a channel-scoped
230
+ * key with no matching products returns an empty list). Optional Bearer JWT
231
+ * makes `calculated_price` customer-group-aware.
232
+ *
233
+ * Errors: 400 `missing_client_id`, 400 `validation_failed`,
234
+ * 400 `invalid_publishable_key`, 400 `invalid_region`.
235
+ * Settings: price lists (B2B pricing), sales-channel product links,
236
+ * publishable-key channel bindings.
237
+ */
238
+ export async function listProducts(
239
+ client: StorefrontClient,
240
+ query?: ListProductsQuery
241
+ ): Promise<ProductListResponse> {
242
+ return client.get("/api/store/products", { query: { ...query } })
243
+ }
244
+
245
+ /**
246
+ * Retrieve one product by id (`prod_…`) or handle — storefronts deep-link by
247
+ * handle.
248
+ *
249
+ * Auth: anon (`x-client-id`); optional publishable key + Bearer JWT as on
250
+ * the list. Errors: 404 `not_found` (unknown, draft — RLS-hidden — or
251
+ * outside the key's channels: invisible, not forbidden),
252
+ * 400 `invalid_publishable_key`, 400 `invalid_region`.
253
+ */
254
+ export async function retrieveProduct(
255
+ client: StorefrontClient,
256
+ idOrHandle: string,
257
+ query?: PricingContextQuery
258
+ ): Promise<ProductResponse> {
259
+ return client.get(`/api/store/products/${encodeURIComponent(idOrHandle)}`, {
260
+ query: { ...query },
261
+ })
262
+ }
263
+
264
+ /**
265
+ * List product variants (cart-line hydration). `id` accepts a CSV of variant
266
+ * ids for multi-lookup. Ordered by `variant_rank`.
267
+ *
268
+ * Auth: anon (`x-client-id`); optional Bearer JWT for group-aware
269
+ * `calculated_price`. NOTE: this listing is NOT publishable-key
270
+ * channel-scoped (scope applies to product reads). Errors:
271
+ * 400 `missing_client_id`, 400 `validation_failed`, 400 `invalid_region`.
272
+ */
273
+ export async function listProductVariants(
274
+ client: StorefrontClient,
275
+ query?: ListProductVariantsQuery
276
+ ): Promise<ProductVariantListResponse> {
277
+ return client.get("/api/store/product-variants", { query: { ...query } })
278
+ }
279
+
280
+ /**
281
+ * Retrieve one variant by id, options + base prices hydrated,
282
+ * `calculated_price` when a pricing context is given.
283
+ *
284
+ * Auth: anon (`x-client-id`). Errors: 404 `not_found`, 400 `invalid_region`.
285
+ */
286
+ export async function retrieveProductVariant(
287
+ client: StorefrontClient,
288
+ variantId: string,
289
+ query?: PricingContextQuery
290
+ ): Promise<ProductVariantResponse> {
291
+ return client.get(`/api/store/product-variants/${encodeURIComponent(variantId)}`, {
292
+ query: { ...query },
293
+ })
294
+ }
295
+
296
+ /** A subscription offer on a product (PDP purchase options). */
297
+ export interface StoreSellingPlan {
298
+ id: string
299
+ name: string
300
+ interval: "day" | "week" | "month" | "year"
301
+ interval_count: number
302
+ /** null = catalog price (the plan only sets the cadence). */
303
+ pricing_type: "percent" | "fixed" | null
304
+ pricing_value: number | null
305
+ min_cycles: number | null
306
+ max_cycles: number | null
307
+ }
308
+
309
+ export interface SellingPlanListResponse {
310
+ selling_plans: StoreSellingPlan[]
311
+ count: number
312
+ }
313
+
314
+ /**
315
+ * The subscription plans a product can be purchased with — render as PDP
316
+ * purchase options (one-time vs each plan). The chosen plan id goes on the
317
+ * cart line (`carts.addLineItem` `selling_plan_id`); the server applies the
318
+ * plan price. Empty list = product is one-time only.
319
+ *
320
+ * Auth: anon (`x-client-id`); publishable-key channel scope applies (same
321
+ * rule as the product read). Errors: 404 `not_found`.
322
+ */
323
+ export async function listSellingPlans(
324
+ client: StorefrontClient,
325
+ idOrHandle: string
326
+ ): Promise<SellingPlanListResponse> {
327
+ return client.get(
328
+ `/api/store/products/${encodeURIComponent(idOrHandle)}/selling-plans`
329
+ )
330
+ }
package/src/locales/bg.ts CHANGED
@@ -28,6 +28,9 @@ export const bg: StorefrontLocale = {
28
28
  selectVariant: "Изберете вариант",
29
29
  outOfStock: "Изчерпан",
30
30
  addToCart: "Добави в кошницата",
31
+ quantity: "Количество",
32
+ decreaseQuantity: "По-малко",
33
+ increaseQuantity: "Повече",
31
34
  from: "От",
32
35
  original: "Оригинална цена:",
33
36
  productInformation: "Информация за продукта",