@cartbase/storefront 0.17.0 → 0.18.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.
- package/LICENSE +21 -21
- package/package.json +3 -1
- package/src/api/auth.ts +108 -108
- package/src/api/carts.ts +523 -523
- package/src/api/categories.ts +184 -184
- package/src/api/checkout.ts +526 -526
- package/src/api/collections.ts +130 -130
- package/src/api/consent.ts +75 -75
- package/src/api/content.ts +125 -125
- package/src/api/customers.ts +303 -303
- package/src/api/gift-cards.ts +112 -112
- package/src/api/http.ts +180 -180
- package/src/api/index.ts +30 -30
- package/src/api/menus.ts +77 -77
- package/src/api/metaobjects.ts +136 -136
- package/src/api/orders.ts +290 -290
- package/src/api/products.ts +43 -1
- package/src/api/redirects.ts +37 -37
- package/src/api/regions.ts +200 -200
- package/src/api/reviews.ts +259 -259
- package/src/api/search.ts +163 -163
- package/src/api/store.ts +35 -35
- package/src/api/types.ts +91 -91
- package/src/cart-drawer/context.tsx +367 -168
- package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
- package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
- package/src/cart-drawer/gift-wrap.tsx +82 -82
- package/src/cart-drawer/index.ts +73 -68
- package/src/cart-drawer/item/index.tsx +162 -162
- package/src/cart-drawer/item/quantity.tsx +8 -14
- package/src/cart-drawer/item/upsell.tsx +110 -110
- package/src/cart-drawer/labels.ts +123 -123
- package/src/cart-drawer/mutation-queue.ts +78 -0
- package/src/cart-drawer/notes.tsx +131 -131
- package/src/cart-drawer/sticky-footer.tsx +73 -73
- package/src/cart-drawer/summary-breakdown.tsx +197 -197
- package/src/checkout/address-error-copy.ts +117 -117
- package/src/checkout/boxnow-locker-selector.tsx +410 -410
- package/src/checkout/compare-addresses.ts +40 -40
- package/src/checkout/discount-section.tsx +218 -218
- package/src/checkout/error-copy-codes.ts +63 -63
- package/src/checkout/geocode.ts +154 -154
- package/src/checkout/gift-card-section.tsx +224 -224
- package/src/checkout/index.ts +72 -72
- package/src/checkout/labels.ts +495 -495
- package/src/checkout/payment-button.tsx +372 -372
- package/src/checkout/payment-error-copy.ts +154 -154
- package/src/checkout/promotion-error-copy.ts +91 -91
- package/src/checkout/use-checkout-orchestration.ts +1604 -1592
- package/src/common/country-flag.tsx +52 -52
- package/src/common/country-select.tsx +11 -11
- package/src/common/index.ts +20 -20
- package/src/common/market-select.tsx +57 -57
- package/src/index.ts +12 -12
- package/src/lib/cookie-names.ts +14 -0
- package/src/lib/country-name.ts +59 -59
- package/src/lib/get-product-price.ts +133 -133
- package/src/lib/media-image.tsx +39 -39
- package/src/lib/payment-constants.ts +53 -53
- package/src/lib/platform.ts +13 -13
- package/src/lib/price.tsx +39 -39
- package/src/lib/store-api-error.ts +36 -36
- package/src/lib/variant-caption.ts +32 -32
- package/src/locales/bg.ts +467 -467
- package/src/locales/context.ts +37 -37
- package/src/locales/en.ts +26 -26
- package/src/locales/es.ts +466 -466
- package/src/locales/index.ts +19 -19
- package/src/locales/provider.tsx +59 -59
- package/src/locales/types.ts +77 -77
- package/src/order/index.ts +62 -62
- package/src/order/labels.ts +79 -79
- package/src/order/order-totals.tsx +250 -250
- package/src/primitives/select-field.tsx +93 -93
- package/src/products/image-gallery.tsx +43 -43
- package/src/products/index.ts +68 -59
- package/src/products/option-select.tsx +67 -67
- package/src/products/product-actions-wrapper.tsx +63 -62
- package/src/products/product-actions.tsx +151 -150
- package/src/products/product-price.tsx +69 -69
- package/src/products/product-promises.tsx +61 -61
- package/src/products/product-specs.tsx +65 -65
- package/src/products/product-tabs.tsx +123 -123
- package/src/products/product-template.tsx +163 -162
- package/src/products/purchase-options.tsx +130 -130
- package/src/products/sets.ts +110 -0
- package/src/products/use-product-actions.ts +199 -164
- package/src/products/variant-matching.ts +71 -71
- package/src/products/variant-url.ts +74 -74
- package/src/reviews-ui/index.ts +73 -73
- package/src/store/category-template.tsx +136 -136
- package/src/store/index.ts +40 -40
- package/src/tracking/chatgpt-pixel.tsx +99 -99
- package/src/tracking/consent-init.tsx +62 -62
- package/src/tracking/events.ts +348 -348
- package/src/tracking/ga4.tsx +93 -93
- package/src/tracking/google-ads.ts +84 -84
- package/src/tracking/gtm.tsx +60 -60
- package/src/tracking/inline-script.ts +49 -49
- package/src/tracking/oaiq.ts +206 -206
- package/src/tracking/tiktok-pixel.tsx +91 -91
- package/src/tracking/track-init.tsx +56 -56
- package/src/tracking/track-order-purchase.tsx +122 -122
- package/src/tracking/ttq.ts +180 -180
- package/src/tracking/use-tracking-config.ts +54 -54
- package/theme/index.css +25 -25
- package/theme/theme.css +150 -150
- package/theme/tokens.css +106 -106
package/src/api/customers.ts
CHANGED
|
@@ -1,303 +1,303 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @cartbase/storefront/api/customers — customer profile, addresses, documents.
|
|
3
|
-
*
|
|
4
|
-
* Ground truth: src/app/api/store/customers/** + src/app/api/_lib/customer.ts.
|
|
5
|
-
*
|
|
6
|
-
* Auth model: every endpoint here except none is CUSTOMER-authenticated —
|
|
7
|
-
* `authorization: Bearer <supabase jwt>` (minted by the passwordless code
|
|
8
|
-
* flow in ./auth or a client-side supabase password sign-in) PLUS the anon
|
|
9
|
-
* `x-client-id` tenant header the client always sends. Missing/invalid JWT →
|
|
10
|
-
* `401 unauthenticated`.
|
|
11
|
-
*
|
|
12
|
-
* Lazy registration: `/customers/me` resolves the customer row by
|
|
13
|
-
* (client_id, JWT email) and CREATES it on first authenticated call
|
|
14
|
-
* (has_account=true, account_status per the store's approval policy) — a
|
|
15
|
-
* fresh login never 404s on "me".
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import type { StorefrontClient } from "./http"
|
|
19
|
-
import type { IsoDateString, PaginationQuery } from "./types"
|
|
20
|
-
|
|
21
|
-
/** One row of `customer.addresses` (barter_commerce.customer_addresses), as the store serves it. */
|
|
22
|
-
export interface CustomerAddress {
|
|
23
|
-
id: string
|
|
24
|
-
customer_id: string
|
|
25
|
-
address_name: string | null
|
|
26
|
-
first_name: string | null
|
|
27
|
-
last_name: string | null
|
|
28
|
-
company: string | null
|
|
29
|
-
address_1: string | null
|
|
30
|
-
address_2: string | null
|
|
31
|
-
city: string | null
|
|
32
|
-
country_code: string | null
|
|
33
|
-
province: string | null
|
|
34
|
-
postal_code: string | null
|
|
35
|
-
phone: string | null
|
|
36
|
-
is_default_billing: boolean
|
|
37
|
-
is_default_shipping: boolean
|
|
38
|
-
metadata: Record<string, unknown> | null
|
|
39
|
-
created_at: IsoDateString
|
|
40
|
-
updated_at: IsoDateString
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* The customer payload: the customer's own row as the store serves it, plus
|
|
45
|
-
* addresses. Named columns since 2026-09-14: no tenant id, no soft-delete
|
|
46
|
-
* stamp, no staff identity, no provider account id, and no merchant tags
|
|
47
|
-
* (the labels a merchant puts on a customer are staff knowledge and never
|
|
48
|
-
* reach the shopper).
|
|
49
|
-
* `account_status` (`pending` | `approved`) gates B2B storefront content
|
|
50
|
-
* when the store's approval policy is on (b2b-v1).
|
|
51
|
-
*/
|
|
52
|
-
export interface StoreCustomer {
|
|
53
|
-
id: string
|
|
54
|
-
email: string | null
|
|
55
|
-
first_name: string | null
|
|
56
|
-
last_name: string | null
|
|
57
|
-
phone: string | null
|
|
58
|
-
company_name: string | null
|
|
59
|
-
/** Bulgarian company id (ЕИК) — feeds invoice-required checkout. */
|
|
60
|
-
company_eik: string | null
|
|
61
|
-
vat_number: string | null
|
|
62
|
-
has_account: boolean
|
|
63
|
-
/** `pending` | `approved` — store approval policy (customer-accounts card). */
|
|
64
|
-
account_status: string
|
|
65
|
-
metadata: Record<string, unknown> | null
|
|
66
|
-
created_at: IsoDateString
|
|
67
|
-
updated_at: IsoDateString
|
|
68
|
-
addresses: CustomerAddress[]
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface CustomerResponse {
|
|
72
|
-
customer: StoreCustomer
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface CreateCustomerInput {
|
|
76
|
-
/** Must equal the session JWT's email — mismatch → 403 email_mismatch. */
|
|
77
|
-
email: string
|
|
78
|
-
first_name?: string
|
|
79
|
-
last_name?: string
|
|
80
|
-
phone?: string
|
|
81
|
-
company_name?: string
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* POST /api/store/customers — guest-to-registered conversion (password
|
|
86
|
-
* sign-up flow): call `supabase.auth.signUp({email, password})` client-side
|
|
87
|
-
* first, then this with the same email. Upserts the (client_id, email)
|
|
88
|
-
* customer row with has_account=true. NOT needed for the passwordless code
|
|
89
|
-
* flow — verify already lazy-creates the customer.
|
|
90
|
-
*
|
|
91
|
-
* Auth: Bearer JWT (+ x-client-id).
|
|
92
|
-
* Errors: 401 unauthenticated · 403 email_mismatch (body email ≠ JWT email)
|
|
93
|
-
* · 400 validation_failed.
|
|
94
|
-
* Settings: store approval policy decides account_status of a NEW row.
|
|
95
|
-
*/
|
|
96
|
-
export async function createCustomer(
|
|
97
|
-
client: StorefrontClient,
|
|
98
|
-
input: CreateCustomerInput
|
|
99
|
-
): Promise<CustomerResponse> {
|
|
100
|
-
return client.post("/api/store/customers", input)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* GET /api/store/customers/me — the signed-in customer, addresses embedded.
|
|
105
|
-
* Lazy-creates the customer row on first call for this (tenant, email).
|
|
106
|
-
*
|
|
107
|
-
* Auth: Bearer JWT (+ x-client-id).
|
|
108
|
-
* Errors: 401 unauthenticated · 400 missing_client_id.
|
|
109
|
-
*/
|
|
110
|
-
export async function getMe(client: StorefrontClient): Promise<CustomerResponse> {
|
|
111
|
-
return client.get("/api/store/customers/me")
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export interface UpdateCustomerInput {
|
|
115
|
-
first_name?: string | null
|
|
116
|
-
last_name?: string | null
|
|
117
|
-
phone?: string | null
|
|
118
|
-
company_name?: string | null
|
|
119
|
-
/** ЕИК / VAT (customer-master-data) — loose format, never shape-blocked. */
|
|
120
|
-
company_eik?: string | null
|
|
121
|
-
vat_number?: string | null
|
|
122
|
-
metadata?: Record<string, unknown>
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* POST /api/store/customers/me — update own profile. Unknown keys are
|
|
127
|
-
* stripped server-side; returns the refreshed customer.
|
|
128
|
-
*
|
|
129
|
-
* Auth: Bearer JWT (+ x-client-id).
|
|
130
|
-
* Errors: 401 unauthenticated · 400 validation_failed.
|
|
131
|
-
*/
|
|
132
|
-
export async function updateMe(
|
|
133
|
-
client: StorefrontClient,
|
|
134
|
-
input: UpdateCustomerInput
|
|
135
|
-
): Promise<CustomerResponse> {
|
|
136
|
-
return client.post("/api/store/customers/me", input)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export interface AddressListResponse {
|
|
140
|
-
addresses: CustomerAddress[]
|
|
141
|
-
/** Not truly paginated: offset is always 0 and limit === count. */
|
|
142
|
-
count: number
|
|
143
|
-
offset: number
|
|
144
|
-
limit: number
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* GET /api/store/customers/me/addresses — all of the customer's addresses
|
|
149
|
-
* (created_at asc). The list envelope is nominal — the route returns EVERY
|
|
150
|
-
* address (offset 0, limit = count); no query params are read.
|
|
151
|
-
*
|
|
152
|
-
* Auth: Bearer JWT (+ x-client-id). Errors: 401 unauthenticated.
|
|
153
|
-
*/
|
|
154
|
-
export async function listAddresses(
|
|
155
|
-
client: StorefrontClient
|
|
156
|
-
): Promise<AddressListResponse> {
|
|
157
|
-
return client.get("/api/store/customers/me/addresses")
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export interface CreateAddressInput {
|
|
161
|
-
address_name?: string
|
|
162
|
-
first_name?: string
|
|
163
|
-
last_name?: string
|
|
164
|
-
company?: string
|
|
165
|
-
address_1?: string
|
|
166
|
-
address_2?: string
|
|
167
|
-
city?: string
|
|
168
|
-
/** Lower-cased server-side. */
|
|
169
|
-
country_code?: string
|
|
170
|
-
province?: string
|
|
171
|
-
postal_code?: string
|
|
172
|
-
phone?: string
|
|
173
|
-
/** true clears the flag on every sibling first (one default per kind). */
|
|
174
|
-
is_default_billing?: boolean
|
|
175
|
-
is_default_shipping?: boolean
|
|
176
|
-
metadata?: Record<string, unknown>
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* POST /api/store/customers/me/addresses — create an address. Returns the
|
|
181
|
-
* FULL refreshed customer (`{customer}`, addresses embedded) — not the
|
|
182
|
-
* created address alone; find it in `customer.addresses`.
|
|
183
|
-
*
|
|
184
|
-
* Auth: Bearer JWT (+ x-client-id).
|
|
185
|
-
* Errors: 401 unauthenticated · 400 validation_failed.
|
|
186
|
-
*/
|
|
187
|
-
export async function createAddress(
|
|
188
|
-
client: StorefrontClient,
|
|
189
|
-
input: CreateAddressInput
|
|
190
|
-
): Promise<CustomerResponse> {
|
|
191
|
-
return client.post("/api/store/customers/me/addresses", input)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export interface AddressResponse {
|
|
195
|
-
address: CustomerAddress
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* GET /api/store/customers/me/addresses/:id — one owned address.
|
|
200
|
-
*
|
|
201
|
-
* Auth: Bearer JWT (+ x-client-id).
|
|
202
|
-
* Errors: 401 unauthenticated · 404 not_found (unknown OR another
|
|
203
|
-
* customer's address — ownership 404s, it never 403s).
|
|
204
|
-
*/
|
|
205
|
-
export async function getAddress(
|
|
206
|
-
client: StorefrontClient,
|
|
207
|
-
addressId: string
|
|
208
|
-
): Promise<AddressResponse> {
|
|
209
|
-
return client.get(`/api/store/customers/me/addresses/${encodeURIComponent(addressId)}`)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export interface UpdateAddressInput {
|
|
213
|
-
address_name?: string | null
|
|
214
|
-
first_name?: string | null
|
|
215
|
-
last_name?: string | null
|
|
216
|
-
company?: string | null
|
|
217
|
-
address_1?: string | null
|
|
218
|
-
address_2?: string | null
|
|
219
|
-
city?: string | null
|
|
220
|
-
country_code?: string | null
|
|
221
|
-
province?: string | null
|
|
222
|
-
postal_code?: string | null
|
|
223
|
-
phone?: string | null
|
|
224
|
-
is_default_billing?: boolean
|
|
225
|
-
is_default_shipping?: boolean
|
|
226
|
-
metadata?: Record<string, unknown>
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* POST /api/store/customers/me/addresses/:id — partial update; default
|
|
231
|
-
* flags clear siblings. Returns the FULL refreshed customer.
|
|
232
|
-
*
|
|
233
|
-
* Auth: Bearer JWT (+ x-client-id).
|
|
234
|
-
* Errors: 401 unauthenticated · 404 not_found · 400 validation_failed.
|
|
235
|
-
*/
|
|
236
|
-
export async function updateAddress(
|
|
237
|
-
client: StorefrontClient,
|
|
238
|
-
addressId: string,
|
|
239
|
-
input: UpdateAddressInput
|
|
240
|
-
): Promise<CustomerResponse> {
|
|
241
|
-
return client.post(
|
|
242
|
-
`/api/store/customers/me/addresses/${encodeURIComponent(addressId)}`,
|
|
243
|
-
input
|
|
244
|
-
)
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* DELETE /api/store/customers/me/addresses/:id — soft-delete an owned
|
|
249
|
-
* address. Returns the FULL refreshed customer (the address gone from
|
|
250
|
-
* `customer.addresses`).
|
|
251
|
-
*
|
|
252
|
-
* Auth: Bearer JWT (+ x-client-id).
|
|
253
|
-
* Errors: 401 unauthenticated · 404 not_found.
|
|
254
|
-
*/
|
|
255
|
-
export async function deleteAddress(
|
|
256
|
-
client: StorefrontClient,
|
|
257
|
-
addressId: string
|
|
258
|
-
): Promise<CustomerResponse> {
|
|
259
|
-
return client.delete(
|
|
260
|
-
`/api/store/customers/me/addresses/${encodeURIComponent(addressId)}`
|
|
261
|
-
)
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/** One issued (non-void) order document — the account "Invoices" row. */
|
|
265
|
-
export interface CustomerDocument {
|
|
266
|
-
id: string
|
|
267
|
-
order_id: string
|
|
268
|
-
order_display_id: number | null
|
|
269
|
-
doc_type: string
|
|
270
|
-
number: string
|
|
271
|
-
issued_at: IsoDateString
|
|
272
|
-
pdf_url: string | null
|
|
273
|
-
due_date: IsoDateString | null
|
|
274
|
-
paid_at: IsoDateString | null
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
export interface ListDocumentsQuery extends PaginationQuery {
|
|
278
|
-
/** Filter by document type (e.g. `invoice`). */
|
|
279
|
-
doc_type?: string
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
export interface DocumentListResponse {
|
|
283
|
-
documents: CustomerDocument[]
|
|
284
|
-
count: number
|
|
285
|
-
offset: number
|
|
286
|
-
limit: number
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* GET /api/store/customers/me/documents — the customer's issued order
|
|
291
|
-
* documents, newest first (issued_at desc). Voided documents excluded.
|
|
292
|
-
* Ownership is INNER-join enforced (session customer id AND client_id) —
|
|
293
|
-
* cross-customer reads are impossible. `limit` ≤ 200, default 20.
|
|
294
|
-
*
|
|
295
|
-
* Auth: Bearer JWT (+ x-client-id).
|
|
296
|
-
* Errors: 401 unauthenticated · 400 validation_failed.
|
|
297
|
-
*/
|
|
298
|
-
export async function listMyDocuments(
|
|
299
|
-
client: StorefrontClient,
|
|
300
|
-
query?: ListDocumentsQuery
|
|
301
|
-
): Promise<DocumentListResponse> {
|
|
302
|
-
return client.get("/api/store/customers/me/documents", { query: { ...query } })
|
|
303
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @cartbase/storefront/api/customers — customer profile, addresses, documents.
|
|
3
|
+
*
|
|
4
|
+
* Ground truth: src/app/api/store/customers/** + src/app/api/_lib/customer.ts.
|
|
5
|
+
*
|
|
6
|
+
* Auth model: every endpoint here except none is CUSTOMER-authenticated —
|
|
7
|
+
* `authorization: Bearer <supabase jwt>` (minted by the passwordless code
|
|
8
|
+
* flow in ./auth or a client-side supabase password sign-in) PLUS the anon
|
|
9
|
+
* `x-client-id` tenant header the client always sends. Missing/invalid JWT →
|
|
10
|
+
* `401 unauthenticated`.
|
|
11
|
+
*
|
|
12
|
+
* Lazy registration: `/customers/me` resolves the customer row by
|
|
13
|
+
* (client_id, JWT email) and CREATES it on first authenticated call
|
|
14
|
+
* (has_account=true, account_status per the store's approval policy) — a
|
|
15
|
+
* fresh login never 404s on "me".
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { StorefrontClient } from "./http"
|
|
19
|
+
import type { IsoDateString, PaginationQuery } from "./types"
|
|
20
|
+
|
|
21
|
+
/** One row of `customer.addresses` (barter_commerce.customer_addresses), as the store serves it. */
|
|
22
|
+
export interface CustomerAddress {
|
|
23
|
+
id: string
|
|
24
|
+
customer_id: string
|
|
25
|
+
address_name: string | null
|
|
26
|
+
first_name: string | null
|
|
27
|
+
last_name: string | null
|
|
28
|
+
company: string | null
|
|
29
|
+
address_1: string | null
|
|
30
|
+
address_2: string | null
|
|
31
|
+
city: string | null
|
|
32
|
+
country_code: string | null
|
|
33
|
+
province: string | null
|
|
34
|
+
postal_code: string | null
|
|
35
|
+
phone: string | null
|
|
36
|
+
is_default_billing: boolean
|
|
37
|
+
is_default_shipping: boolean
|
|
38
|
+
metadata: Record<string, unknown> | null
|
|
39
|
+
created_at: IsoDateString
|
|
40
|
+
updated_at: IsoDateString
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The customer payload: the customer's own row as the store serves it, plus
|
|
45
|
+
* addresses. Named columns since 2026-09-14: no tenant id, no soft-delete
|
|
46
|
+
* stamp, no staff identity, no provider account id, and no merchant tags
|
|
47
|
+
* (the labels a merchant puts on a customer are staff knowledge and never
|
|
48
|
+
* reach the shopper).
|
|
49
|
+
* `account_status` (`pending` | `approved`) gates B2B storefront content
|
|
50
|
+
* when the store's approval policy is on (b2b-v1).
|
|
51
|
+
*/
|
|
52
|
+
export interface StoreCustomer {
|
|
53
|
+
id: string
|
|
54
|
+
email: string | null
|
|
55
|
+
first_name: string | null
|
|
56
|
+
last_name: string | null
|
|
57
|
+
phone: string | null
|
|
58
|
+
company_name: string | null
|
|
59
|
+
/** Bulgarian company id (ЕИК) — feeds invoice-required checkout. */
|
|
60
|
+
company_eik: string | null
|
|
61
|
+
vat_number: string | null
|
|
62
|
+
has_account: boolean
|
|
63
|
+
/** `pending` | `approved` — store approval policy (customer-accounts card). */
|
|
64
|
+
account_status: string
|
|
65
|
+
metadata: Record<string, unknown> | null
|
|
66
|
+
created_at: IsoDateString
|
|
67
|
+
updated_at: IsoDateString
|
|
68
|
+
addresses: CustomerAddress[]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface CustomerResponse {
|
|
72
|
+
customer: StoreCustomer
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface CreateCustomerInput {
|
|
76
|
+
/** Must equal the session JWT's email — mismatch → 403 email_mismatch. */
|
|
77
|
+
email: string
|
|
78
|
+
first_name?: string
|
|
79
|
+
last_name?: string
|
|
80
|
+
phone?: string
|
|
81
|
+
company_name?: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* POST /api/store/customers — guest-to-registered conversion (password
|
|
86
|
+
* sign-up flow): call `supabase.auth.signUp({email, password})` client-side
|
|
87
|
+
* first, then this with the same email. Upserts the (client_id, email)
|
|
88
|
+
* customer row with has_account=true. NOT needed for the passwordless code
|
|
89
|
+
* flow — verify already lazy-creates the customer.
|
|
90
|
+
*
|
|
91
|
+
* Auth: Bearer JWT (+ x-client-id).
|
|
92
|
+
* Errors: 401 unauthenticated · 403 email_mismatch (body email ≠ JWT email)
|
|
93
|
+
* · 400 validation_failed.
|
|
94
|
+
* Settings: store approval policy decides account_status of a NEW row.
|
|
95
|
+
*/
|
|
96
|
+
export async function createCustomer(
|
|
97
|
+
client: StorefrontClient,
|
|
98
|
+
input: CreateCustomerInput
|
|
99
|
+
): Promise<CustomerResponse> {
|
|
100
|
+
return client.post("/api/store/customers", input)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* GET /api/store/customers/me — the signed-in customer, addresses embedded.
|
|
105
|
+
* Lazy-creates the customer row on first call for this (tenant, email).
|
|
106
|
+
*
|
|
107
|
+
* Auth: Bearer JWT (+ x-client-id).
|
|
108
|
+
* Errors: 401 unauthenticated · 400 missing_client_id.
|
|
109
|
+
*/
|
|
110
|
+
export async function getMe(client: StorefrontClient): Promise<CustomerResponse> {
|
|
111
|
+
return client.get("/api/store/customers/me")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface UpdateCustomerInput {
|
|
115
|
+
first_name?: string | null
|
|
116
|
+
last_name?: string | null
|
|
117
|
+
phone?: string | null
|
|
118
|
+
company_name?: string | null
|
|
119
|
+
/** ЕИК / VAT (customer-master-data) — loose format, never shape-blocked. */
|
|
120
|
+
company_eik?: string | null
|
|
121
|
+
vat_number?: string | null
|
|
122
|
+
metadata?: Record<string, unknown>
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* POST /api/store/customers/me — update own profile. Unknown keys are
|
|
127
|
+
* stripped server-side; returns the refreshed customer.
|
|
128
|
+
*
|
|
129
|
+
* Auth: Bearer JWT (+ x-client-id).
|
|
130
|
+
* Errors: 401 unauthenticated · 400 validation_failed.
|
|
131
|
+
*/
|
|
132
|
+
export async function updateMe(
|
|
133
|
+
client: StorefrontClient,
|
|
134
|
+
input: UpdateCustomerInput
|
|
135
|
+
): Promise<CustomerResponse> {
|
|
136
|
+
return client.post("/api/store/customers/me", input)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface AddressListResponse {
|
|
140
|
+
addresses: CustomerAddress[]
|
|
141
|
+
/** Not truly paginated: offset is always 0 and limit === count. */
|
|
142
|
+
count: number
|
|
143
|
+
offset: number
|
|
144
|
+
limit: number
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* GET /api/store/customers/me/addresses — all of the customer's addresses
|
|
149
|
+
* (created_at asc). The list envelope is nominal — the route returns EVERY
|
|
150
|
+
* address (offset 0, limit = count); no query params are read.
|
|
151
|
+
*
|
|
152
|
+
* Auth: Bearer JWT (+ x-client-id). Errors: 401 unauthenticated.
|
|
153
|
+
*/
|
|
154
|
+
export async function listAddresses(
|
|
155
|
+
client: StorefrontClient
|
|
156
|
+
): Promise<AddressListResponse> {
|
|
157
|
+
return client.get("/api/store/customers/me/addresses")
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface CreateAddressInput {
|
|
161
|
+
address_name?: string
|
|
162
|
+
first_name?: string
|
|
163
|
+
last_name?: string
|
|
164
|
+
company?: string
|
|
165
|
+
address_1?: string
|
|
166
|
+
address_2?: string
|
|
167
|
+
city?: string
|
|
168
|
+
/** Lower-cased server-side. */
|
|
169
|
+
country_code?: string
|
|
170
|
+
province?: string
|
|
171
|
+
postal_code?: string
|
|
172
|
+
phone?: string
|
|
173
|
+
/** true clears the flag on every sibling first (one default per kind). */
|
|
174
|
+
is_default_billing?: boolean
|
|
175
|
+
is_default_shipping?: boolean
|
|
176
|
+
metadata?: Record<string, unknown>
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* POST /api/store/customers/me/addresses — create an address. Returns the
|
|
181
|
+
* FULL refreshed customer (`{customer}`, addresses embedded) — not the
|
|
182
|
+
* created address alone; find it in `customer.addresses`.
|
|
183
|
+
*
|
|
184
|
+
* Auth: Bearer JWT (+ x-client-id).
|
|
185
|
+
* Errors: 401 unauthenticated · 400 validation_failed.
|
|
186
|
+
*/
|
|
187
|
+
export async function createAddress(
|
|
188
|
+
client: StorefrontClient,
|
|
189
|
+
input: CreateAddressInput
|
|
190
|
+
): Promise<CustomerResponse> {
|
|
191
|
+
return client.post("/api/store/customers/me/addresses", input)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface AddressResponse {
|
|
195
|
+
address: CustomerAddress
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* GET /api/store/customers/me/addresses/:id — one owned address.
|
|
200
|
+
*
|
|
201
|
+
* Auth: Bearer JWT (+ x-client-id).
|
|
202
|
+
* Errors: 401 unauthenticated · 404 not_found (unknown OR another
|
|
203
|
+
* customer's address — ownership 404s, it never 403s).
|
|
204
|
+
*/
|
|
205
|
+
export async function getAddress(
|
|
206
|
+
client: StorefrontClient,
|
|
207
|
+
addressId: string
|
|
208
|
+
): Promise<AddressResponse> {
|
|
209
|
+
return client.get(`/api/store/customers/me/addresses/${encodeURIComponent(addressId)}`)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface UpdateAddressInput {
|
|
213
|
+
address_name?: string | null
|
|
214
|
+
first_name?: string | null
|
|
215
|
+
last_name?: string | null
|
|
216
|
+
company?: string | null
|
|
217
|
+
address_1?: string | null
|
|
218
|
+
address_2?: string | null
|
|
219
|
+
city?: string | null
|
|
220
|
+
country_code?: string | null
|
|
221
|
+
province?: string | null
|
|
222
|
+
postal_code?: string | null
|
|
223
|
+
phone?: string | null
|
|
224
|
+
is_default_billing?: boolean
|
|
225
|
+
is_default_shipping?: boolean
|
|
226
|
+
metadata?: Record<string, unknown>
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* POST /api/store/customers/me/addresses/:id — partial update; default
|
|
231
|
+
* flags clear siblings. Returns the FULL refreshed customer.
|
|
232
|
+
*
|
|
233
|
+
* Auth: Bearer JWT (+ x-client-id).
|
|
234
|
+
* Errors: 401 unauthenticated · 404 not_found · 400 validation_failed.
|
|
235
|
+
*/
|
|
236
|
+
export async function updateAddress(
|
|
237
|
+
client: StorefrontClient,
|
|
238
|
+
addressId: string,
|
|
239
|
+
input: UpdateAddressInput
|
|
240
|
+
): Promise<CustomerResponse> {
|
|
241
|
+
return client.post(
|
|
242
|
+
`/api/store/customers/me/addresses/${encodeURIComponent(addressId)}`,
|
|
243
|
+
input
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* DELETE /api/store/customers/me/addresses/:id — soft-delete an owned
|
|
249
|
+
* address. Returns the FULL refreshed customer (the address gone from
|
|
250
|
+
* `customer.addresses`).
|
|
251
|
+
*
|
|
252
|
+
* Auth: Bearer JWT (+ x-client-id).
|
|
253
|
+
* Errors: 401 unauthenticated · 404 not_found.
|
|
254
|
+
*/
|
|
255
|
+
export async function deleteAddress(
|
|
256
|
+
client: StorefrontClient,
|
|
257
|
+
addressId: string
|
|
258
|
+
): Promise<CustomerResponse> {
|
|
259
|
+
return client.delete(
|
|
260
|
+
`/api/store/customers/me/addresses/${encodeURIComponent(addressId)}`
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** One issued (non-void) order document — the account "Invoices" row. */
|
|
265
|
+
export interface CustomerDocument {
|
|
266
|
+
id: string
|
|
267
|
+
order_id: string
|
|
268
|
+
order_display_id: number | null
|
|
269
|
+
doc_type: string
|
|
270
|
+
number: string
|
|
271
|
+
issued_at: IsoDateString
|
|
272
|
+
pdf_url: string | null
|
|
273
|
+
due_date: IsoDateString | null
|
|
274
|
+
paid_at: IsoDateString | null
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export interface ListDocumentsQuery extends PaginationQuery {
|
|
278
|
+
/** Filter by document type (e.g. `invoice`). */
|
|
279
|
+
doc_type?: string
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export interface DocumentListResponse {
|
|
283
|
+
documents: CustomerDocument[]
|
|
284
|
+
count: number
|
|
285
|
+
offset: number
|
|
286
|
+
limit: number
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* GET /api/store/customers/me/documents — the customer's issued order
|
|
291
|
+
* documents, newest first (issued_at desc). Voided documents excluded.
|
|
292
|
+
* Ownership is INNER-join enforced (session customer id AND client_id) —
|
|
293
|
+
* cross-customer reads are impossible. `limit` ≤ 200, default 20.
|
|
294
|
+
*
|
|
295
|
+
* Auth: Bearer JWT (+ x-client-id).
|
|
296
|
+
* Errors: 401 unauthenticated · 400 validation_failed.
|
|
297
|
+
*/
|
|
298
|
+
export async function listMyDocuments(
|
|
299
|
+
client: StorefrontClient,
|
|
300
|
+
query?: ListDocumentsQuery
|
|
301
|
+
): Promise<DocumentListResponse> {
|
|
302
|
+
return client.get("/api/store/customers/me/documents", { query: { ...query } })
|
|
303
|
+
}
|