@kasuvia/sdk 0.0.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 +201 -0
- package/README.md +112 -0
- package/dist/index.d.mts +1891 -0
- package/dist/index.d.ts +1891 -0
- package/dist/index.js +3540 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3314 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +59 -0
- package/src/client/authClient.ts +135 -0
- package/src/client/graphqlClient.ts +53 -0
- package/src/client/httpClient.ts +298 -0
- package/src/config/index.ts +102 -0
- package/src/graphql/collections.graphql.ts +56 -0
- package/src/graphql/index.ts +7 -0
- package/src/graphql/products.graphql.ts +710 -0
- package/src/graphql/wishlist.graphql.ts +33 -0
- package/src/hooks/index.ts +26 -0
- package/src/hooks/use-auth.ts +182 -0
- package/src/hooks/use-bespoke-availability.ts +20 -0
- package/src/hooks/use-cart.ts +207 -0
- package/src/hooks/use-cms.ts +165 -0
- package/src/hooks/use-collections.ts +40 -0
- package/src/hooks/use-consultation-stream.ts +141 -0
- package/src/hooks/use-consultations.ts +76 -0
- package/src/hooks/use-countries.ts +24 -0
- package/src/hooks/use-currencies.ts +27 -0
- package/src/hooks/use-google-translate.ts +164 -0
- package/src/hooks/use-is-mounted.ts +9 -0
- package/src/hooks/use-languages.ts +33 -0
- package/src/hooks/use-order-stream.ts +195 -0
- package/src/hooks/use-orders.ts +64 -0
- package/src/hooks/use-payment.ts +68 -0
- package/src/hooks/use-products.ts +113 -0
- package/src/hooks/use-service-booking.ts +118 -0
- package/src/hooks/use-states.ts +28 -0
- package/src/hooks/use-wishlist.ts +121 -0
- package/src/index.ts +65 -0
- package/src/services/app.service.ts +17 -0
- package/src/services/auth.service.ts +332 -0
- package/src/services/bespoke.service.ts +17 -0
- package/src/services/cart.service.ts +245 -0
- package/src/services/cms.service.ts +112 -0
- package/src/services/collection.service.ts +77 -0
- package/src/services/config.service.ts +42 -0
- package/src/services/consultation.service.ts +51 -0
- package/src/services/index.ts +21 -0
- package/src/services/logistics.service.ts +95 -0
- package/src/services/notifications.service.ts +50 -0
- package/src/services/order.service.ts +169 -0
- package/src/services/payment.service.ts +127 -0
- package/src/services/product.service.ts +678 -0
- package/src/services/service-booking.service.ts +86 -0
- package/src/services/wishlist.service.ts +84 -0
- package/src/types/auth/index.ts +126 -0
- package/src/types/bespoke/index.ts +39 -0
- package/src/types/business/index.ts +6 -0
- package/src/types/cms/index.ts +77 -0
- package/src/types/consultations/index.ts +79 -0
- package/src/types/index.ts +27 -0
- package/src/types/media/index.ts +41 -0
- package/src/types/payments/index.ts +120 -0
- package/src/types/services/index.ts +80 -0
- package/src/types/store/index.ts +282 -0
- package/src/types/store/shipping.ts +38 -0
- package/src/types/ui/index.ts +32 -0
- package/src/utils/cache.ts +33 -0
- package/src/utils/checkout.ts +39 -0
- package/src/utils/customer.ts +20 -0
- package/src/utils/error-handler.ts +14 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/logger.ts +79 -0
- package/src/utils/sanitize-url.ts +55 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service booking types for customer-facing storefront
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type ServiceLocationType = "remote" | "physical" | "both";
|
|
6
|
+
export type ServiceType = "appointment" | "project" | "recurring";
|
|
7
|
+
|
|
8
|
+
export type AppointmentStatus =
|
|
9
|
+
| "pending"
|
|
10
|
+
| "confirmed"
|
|
11
|
+
| "in_progress"
|
|
12
|
+
| "completed"
|
|
13
|
+
| "voided"
|
|
14
|
+
| "no_show";
|
|
15
|
+
|
|
16
|
+
export interface AvailableSlot {
|
|
17
|
+
date: string;
|
|
18
|
+
start_time: string;
|
|
19
|
+
end_time: string;
|
|
20
|
+
slots_remaining: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ServiceAppointment {
|
|
24
|
+
id: string;
|
|
25
|
+
product_id: string;
|
|
26
|
+
customer_id: string;
|
|
27
|
+
order_id: string | null;
|
|
28
|
+
status: AppointmentStatus;
|
|
29
|
+
appointment_date: string;
|
|
30
|
+
start_time: string;
|
|
31
|
+
end_time: string;
|
|
32
|
+
location_type: ServiceLocationType;
|
|
33
|
+
customer_address: Record<string, unknown> | null;
|
|
34
|
+
meeting_link: string | null;
|
|
35
|
+
customer_notes: string | null;
|
|
36
|
+
price: number;
|
|
37
|
+
created_at: string;
|
|
38
|
+
updated_at: string;
|
|
39
|
+
completed_at: string | null;
|
|
40
|
+
voided_at: string | null;
|
|
41
|
+
cancellation_reason: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ServiceAppointmentCreatePayload {
|
|
45
|
+
product_id: string;
|
|
46
|
+
scheduled_date: string;
|
|
47
|
+
scheduled_time: string;
|
|
48
|
+
location_type?: ServiceLocationType;
|
|
49
|
+
customer_address?: Record<string, unknown>;
|
|
50
|
+
customer_notes?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface DeliverableStatus {
|
|
54
|
+
pending: "pending";
|
|
55
|
+
in_progress: "in_progress";
|
|
56
|
+
review: "review";
|
|
57
|
+
approved: "approved";
|
|
58
|
+
rejected: "rejected";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type DeliverableStatusType = DeliverableStatus[keyof DeliverableStatus];
|
|
62
|
+
|
|
63
|
+
export interface ServiceDeliverable {
|
|
64
|
+
id: string;
|
|
65
|
+
order_id: string;
|
|
66
|
+
title: string;
|
|
67
|
+
description: string;
|
|
68
|
+
status: DeliverableStatusType;
|
|
69
|
+
due_date: string | null;
|
|
70
|
+
submitted_at: string | null;
|
|
71
|
+
attachments: string[];
|
|
72
|
+
customer_feedback: string | null;
|
|
73
|
+
created_at: string;
|
|
74
|
+
updated_at: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface DeliverableApprovalPayload {
|
|
78
|
+
approved: boolean;
|
|
79
|
+
feedback?: string;
|
|
80
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { DeliveryAddress } from "./shipping";
|
|
2
|
+
|
|
3
|
+
export type ProductType = "physical" | "digital" | "service" | "bespoke";
|
|
4
|
+
|
|
5
|
+
export type { DeliveryAddress } from "./shipping";
|
|
6
|
+
|
|
7
|
+
export interface Address {
|
|
8
|
+
street: string
|
|
9
|
+
city: string
|
|
10
|
+
state: string
|
|
11
|
+
country: string
|
|
12
|
+
zip?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Collection {
|
|
16
|
+
id: string
|
|
17
|
+
business_id?: string
|
|
18
|
+
name: string
|
|
19
|
+
slug: string
|
|
20
|
+
description?: string
|
|
21
|
+
image?: { url: string; alt?: string; id?: string }
|
|
22
|
+
collection_type?: string
|
|
23
|
+
rules?: Record<string, unknown>
|
|
24
|
+
status?: string
|
|
25
|
+
sort_order?: number
|
|
26
|
+
created_at: string
|
|
27
|
+
updated_at: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Inventory {
|
|
31
|
+
id: string
|
|
32
|
+
product_id: string
|
|
33
|
+
variant_id?: string
|
|
34
|
+
quantity: number
|
|
35
|
+
reserved: number
|
|
36
|
+
reorder_point: number
|
|
37
|
+
reorder_quantity: number
|
|
38
|
+
track_quantity: boolean
|
|
39
|
+
allow_backorder: boolean
|
|
40
|
+
available: number
|
|
41
|
+
is_in_stock: boolean
|
|
42
|
+
is_low_stock: boolean
|
|
43
|
+
updated_at: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ProductVariant {
|
|
47
|
+
id: string
|
|
48
|
+
product_id: string
|
|
49
|
+
sku?: string
|
|
50
|
+
attributes: Record<string, string>
|
|
51
|
+
price?: number
|
|
52
|
+
images: Array<{ url: string; alt?: string; id?: string }>
|
|
53
|
+
weight_kg?: number
|
|
54
|
+
is_active: boolean
|
|
55
|
+
inventory?: Inventory
|
|
56
|
+
created_at: string
|
|
57
|
+
updated_at: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface CustomField {
|
|
61
|
+
label: string
|
|
62
|
+
unit?: string
|
|
63
|
+
required: boolean
|
|
64
|
+
type?: string
|
|
65
|
+
key?: string
|
|
66
|
+
placeholder?: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface Product {
|
|
70
|
+
id: string
|
|
71
|
+
business_id: string
|
|
72
|
+
name: string
|
|
73
|
+
slug: string
|
|
74
|
+
description?: string
|
|
75
|
+
brand?: string
|
|
76
|
+
product_type: ProductType
|
|
77
|
+
requires_shipping: boolean
|
|
78
|
+
is_consumable: boolean
|
|
79
|
+
is_bespoke: boolean
|
|
80
|
+
base_price: number
|
|
81
|
+
currency: string
|
|
82
|
+
images: Array<{ url: string; alt?: string; id?: string }>
|
|
83
|
+
tags: string[]
|
|
84
|
+
custom_fields: CustomField[]
|
|
85
|
+
is_published: boolean
|
|
86
|
+
is_featured: boolean
|
|
87
|
+
is_popular: boolean
|
|
88
|
+
is_new: boolean
|
|
89
|
+
is_temporary: boolean
|
|
90
|
+
digital_file_key?: string
|
|
91
|
+
download_limit?: number
|
|
92
|
+
download_expires_at?: string
|
|
93
|
+
weight_kg: number
|
|
94
|
+
length_cm?: number
|
|
95
|
+
width_cm?: number
|
|
96
|
+
height_cm?: number
|
|
97
|
+
created_at: string
|
|
98
|
+
updated_at: string
|
|
99
|
+
variants: ProductVariant[]
|
|
100
|
+
collections: Collection[]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ListParams {
|
|
104
|
+
page?: number
|
|
105
|
+
pageSize?: number
|
|
106
|
+
search?: string
|
|
107
|
+
ordering?: string
|
|
108
|
+
collectionSlug?: string
|
|
109
|
+
excludeProductId?: string
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface PaginatedResponse<T> {
|
|
113
|
+
items: T[]
|
|
114
|
+
total: number
|
|
115
|
+
page: number
|
|
116
|
+
page_size: number
|
|
117
|
+
total_pages: number
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ============================================================================
|
|
121
|
+
// CART TYPES
|
|
122
|
+
// ============================================================================
|
|
123
|
+
|
|
124
|
+
export interface CartItem {
|
|
125
|
+
id: string
|
|
126
|
+
product_id: string
|
|
127
|
+
product_slug: string
|
|
128
|
+
product_name: string
|
|
129
|
+
image?: string
|
|
130
|
+
variant_id?: string
|
|
131
|
+
variant_attributes?: string | Record<string, string>
|
|
132
|
+
unit_price: number
|
|
133
|
+
quantity: number
|
|
134
|
+
custom_measurements?: string
|
|
135
|
+
is_bespoke: boolean
|
|
136
|
+
created_at: string
|
|
137
|
+
updated_at: string
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface CartResponse {
|
|
141
|
+
items: CartItem[]
|
|
142
|
+
subtotal: number
|
|
143
|
+
total: number
|
|
144
|
+
item_count: number
|
|
145
|
+
product_type?: string // "physical", "digital", "bespoke"
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ============================================================================
|
|
149
|
+
// WISHLIST TYPES
|
|
150
|
+
// ============================================================================
|
|
151
|
+
|
|
152
|
+
export interface WishlistItem {
|
|
153
|
+
id: string
|
|
154
|
+
product_id: string
|
|
155
|
+
notes?: string
|
|
156
|
+
created_at: string
|
|
157
|
+
product_name?: string
|
|
158
|
+
product_slug?: string
|
|
159
|
+
product_price?: number
|
|
160
|
+
product_image?: string
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface WishlistListResponse {
|
|
164
|
+
items: WishlistItem[]
|
|
165
|
+
total: number
|
|
166
|
+
page: number
|
|
167
|
+
page_size: number
|
|
168
|
+
total_pages: number
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface WishlistCheckResponse {
|
|
172
|
+
in_wishlist: boolean
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export type OrderStatus =
|
|
176
|
+
| 'pending'
|
|
177
|
+
| 'confirmed'
|
|
178
|
+
| 'processing'
|
|
179
|
+
| 'packed'
|
|
180
|
+
| 'picked_up'
|
|
181
|
+
| 'in_transit'
|
|
182
|
+
| 'shipped'
|
|
183
|
+
| 'delivered'
|
|
184
|
+
| 'voided'
|
|
185
|
+
| 'refunded'
|
|
186
|
+
| 'failed';
|
|
187
|
+
export type BespokeStageKey = "received" | "development" | "finished" | "dispatched";
|
|
188
|
+
|
|
189
|
+
export interface OrderItem {
|
|
190
|
+
id: string;
|
|
191
|
+
productName: string;
|
|
192
|
+
image: string;
|
|
193
|
+
quantity: number;
|
|
194
|
+
price: number;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface Order {
|
|
198
|
+
id: string;
|
|
199
|
+
display_id?: string;
|
|
200
|
+
items: OrderItem[];
|
|
201
|
+
total: number;
|
|
202
|
+
status: OrderStatus;
|
|
203
|
+
customerEmail: string;
|
|
204
|
+
customerName: string;
|
|
205
|
+
createdAt: number;
|
|
206
|
+
updatedAt: number;
|
|
207
|
+
deliveryMethod?: string;
|
|
208
|
+
trackingNumber?: string;
|
|
209
|
+
deliveryAddress?: string;
|
|
210
|
+
shippingAddress?: DeliveryAddress;
|
|
211
|
+
cancellationReason?: string;
|
|
212
|
+
hasBespoke?: boolean;
|
|
213
|
+
bespokeStage?: BespokeStageKey;
|
|
214
|
+
bespokeBrief?: string;
|
|
215
|
+
deliveryStatus?: string;
|
|
216
|
+
carrierName?: string;
|
|
217
|
+
terminalShipmentId?: string;
|
|
218
|
+
guestToken?: string;
|
|
219
|
+
guestEmail?: string;
|
|
220
|
+
guestPhone?: string;
|
|
221
|
+
guestName?: string;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ============================================================================
|
|
225
|
+
// CHECKOUT TYPES
|
|
226
|
+
// ============================================================================
|
|
227
|
+
|
|
228
|
+
export interface CheckoutCustomer {
|
|
229
|
+
first_name: string;
|
|
230
|
+
last_name: string;
|
|
231
|
+
email: string;
|
|
232
|
+
phone: string;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export interface ShippingLocation {
|
|
236
|
+
street: string;
|
|
237
|
+
city: string;
|
|
238
|
+
state: string;
|
|
239
|
+
lga: string;
|
|
240
|
+
country: string;
|
|
241
|
+
postal: string;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ============================================================================
|
|
245
|
+
// ORDER SERVICE PARAMS
|
|
246
|
+
// ============================================================================
|
|
247
|
+
|
|
248
|
+
export interface FindOrderParams {
|
|
249
|
+
email: string;
|
|
250
|
+
order_id: string;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface CreateOrderParams {
|
|
254
|
+
customer_id?: string | null;
|
|
255
|
+
status?: string;
|
|
256
|
+
shipping_address?: DeliveryAddress;
|
|
257
|
+
checkout_session_id?: string;
|
|
258
|
+
items: Array<{
|
|
259
|
+
product_id: string;
|
|
260
|
+
product_name: string;
|
|
261
|
+
quantity: number;
|
|
262
|
+
unit_price: number;
|
|
263
|
+
variant_id?: string;
|
|
264
|
+
variant_details?: Record<string, unknown>;
|
|
265
|
+
custom_measurements?: Record<string, unknown>;
|
|
266
|
+
}>;
|
|
267
|
+
delivery_method?: string;
|
|
268
|
+
delivery_address?: string;
|
|
269
|
+
total?: number;
|
|
270
|
+
payment_method?: string;
|
|
271
|
+
shipping_rate_id?: string;
|
|
272
|
+
guest_token?: string;
|
|
273
|
+
guest_email?: string;
|
|
274
|
+
guest_phone?: string;
|
|
275
|
+
guest_name?: string;
|
|
276
|
+
product_type?: string;
|
|
277
|
+
bespoke_brief?: string;
|
|
278
|
+
bespoke_reference_images?: string[];
|
|
279
|
+
bespoke_measurements?: Record<string, string>;
|
|
280
|
+
bespoke_preferred_timeline?: string;
|
|
281
|
+
bespoke_special_instructions?: string;
|
|
282
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shipping and delivery types for the storefront
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ShippingRate {
|
|
6
|
+
id: string;
|
|
7
|
+
carrier_name: string;
|
|
8
|
+
carrier_logo: string;
|
|
9
|
+
carrier_rate_description: string;
|
|
10
|
+
carrier_slug: string;
|
|
11
|
+
amount: number;
|
|
12
|
+
currency: string;
|
|
13
|
+
delivery_time: string;
|
|
14
|
+
delivery_eta: number;
|
|
15
|
+
pickup_time: string;
|
|
16
|
+
pickup_eta: number;
|
|
17
|
+
pickup_available: boolean;
|
|
18
|
+
dropoff_available?: boolean;
|
|
19
|
+
dropoff_required?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ShippingRatesResponse {
|
|
23
|
+
status: boolean;
|
|
24
|
+
message: string;
|
|
25
|
+
data: ShippingRate[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DeliveryAddress {
|
|
29
|
+
first_name: string;
|
|
30
|
+
last_name: string;
|
|
31
|
+
phone: string;
|
|
32
|
+
street: string;
|
|
33
|
+
city: string;
|
|
34
|
+
state: string;
|
|
35
|
+
lga: string;
|
|
36
|
+
country: string;
|
|
37
|
+
postal: string;
|
|
38
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type Theme = "light" | "dark"
|
|
2
|
+
export type LangCode = "en" | "ha" | "yo" | "ig" | "fr"
|
|
3
|
+
|
|
4
|
+
export interface AppState {
|
|
5
|
+
theme: Theme
|
|
6
|
+
lang: LangCode
|
|
7
|
+
currency: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface AppContextValue extends AppState {
|
|
11
|
+
setTheme: (t: Theme) => void
|
|
12
|
+
setLang: (l: LangCode) => void
|
|
13
|
+
setCurrency: (c: string) => void
|
|
14
|
+
formatPrice: (amount: number, fromCurrency?: string) => string
|
|
15
|
+
resetAppState: () => void
|
|
16
|
+
t: (text: string) => string
|
|
17
|
+
translate: (text: string) => Promise<string>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Country {
|
|
21
|
+
name: string
|
|
22
|
+
code: string
|
|
23
|
+
dial_code: string
|
|
24
|
+
min_age: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface State {
|
|
28
|
+
name: string
|
|
29
|
+
code: string
|
|
30
|
+
full_iso_code: string
|
|
31
|
+
type: string
|
|
32
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Tiers Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides cache configuration for React Query based on environment.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const CACHE_TIERS = {
|
|
8
|
+
none: { staleTime: 0, gcTime: 0 },
|
|
9
|
+
infinity: { staleTime: Infinity, gcTime: 0 },
|
|
10
|
+
low: { staleTime: 1000 * 30, gcTime: 1000 * 60 }, // 30s / 1m
|
|
11
|
+
medium: { staleTime: 1000 * 60 * 5, gcTime: 1000 * 60 * 10 }, // 5m / 10m
|
|
12
|
+
high: { staleTime: 1000 * 60 * 30, gcTime: 1000 * 60 * 60 }, // 30m / 1h
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export type CacheTier = keyof typeof CACHE_TIERS;
|
|
16
|
+
|
|
17
|
+
let isDevelopment = true;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Set the environment mode for cache configuration
|
|
21
|
+
*/
|
|
22
|
+
export function setEnvironmentMode(development: boolean): void {
|
|
23
|
+
isDevelopment = development;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get cache configuration for a given tier
|
|
28
|
+
* In development, returns 'none' tier to disable caching
|
|
29
|
+
*/
|
|
30
|
+
export const getCacheTier = (tier: CacheTier) => {
|
|
31
|
+
if (isDevelopment) return CACHE_TIERS.none;
|
|
32
|
+
return CACHE_TIERS[tier];
|
|
33
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { buildFullName } from "./customer";
|
|
2
|
+
import type { CheckoutCustomer, DeliveryAddress, Order, ShippingLocation } from "../types";
|
|
3
|
+
|
|
4
|
+
export function buildDeliveryAddress(customer: CheckoutCustomer, location: ShippingLocation): DeliveryAddress {
|
|
5
|
+
return {
|
|
6
|
+
first_name: customer.first_name,
|
|
7
|
+
last_name: customer.last_name,
|
|
8
|
+
phone: customer.phone,
|
|
9
|
+
...location,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getCheckoutCustomerName(customer: CheckoutCustomer): string {
|
|
14
|
+
return buildFullName(customer.first_name, customer.last_name) ?? customer.email.split("@")[0];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function enrichCompletedOrder(
|
|
18
|
+
order: Order,
|
|
19
|
+
options: {
|
|
20
|
+
customer: CheckoutCustomer;
|
|
21
|
+
location?: ShippingLocation;
|
|
22
|
+
bespokeBrief?: string;
|
|
23
|
+
productType?: "physical" | "digital" | "bespoke" | null;
|
|
24
|
+
},
|
|
25
|
+
): Order {
|
|
26
|
+
const customerName = getCheckoutCustomerName(options.customer);
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
...order,
|
|
30
|
+
customerName: order.customerName || customerName,
|
|
31
|
+
customerEmail: order.customerEmail || options.customer.email,
|
|
32
|
+
shippingAddress:
|
|
33
|
+
order.shippingAddress ??
|
|
34
|
+
(options.productType === "physical" && options.location
|
|
35
|
+
? buildDeliveryAddress(options.customer, options.location)
|
|
36
|
+
: undefined),
|
|
37
|
+
bespokeBrief: order.bespokeBrief ?? options.bespokeBrief,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CustomerUser } from "../types";
|
|
2
|
+
|
|
3
|
+
export function buildFullName(firstname?: string, lastname?: string): string | undefined {
|
|
4
|
+
const name = [firstname, lastname].filter(Boolean).join(' ').trim()
|
|
5
|
+
return name || undefined
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function splitFullName(fullName?: string): { firstname: string; lastname: string } {
|
|
9
|
+
if (!fullName?.trim()) return { firstname: '', lastname: '' }
|
|
10
|
+
const parts = fullName.trim().split(/\s+/)
|
|
11
|
+
return {
|
|
12
|
+
firstname: parts[0] ?? '',
|
|
13
|
+
lastname: parts.slice(1).join(' '),
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getCustomerDisplayName(user: CustomerUser): string {
|
|
18
|
+
const fromParts = buildFullName(user.firstname, user.lastname)
|
|
19
|
+
return fromParts || user.full_name || user.email.split('@')[0]
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type-safe way to extract an error message from an API error.
|
|
5
|
+
*/
|
|
6
|
+
export function getErrorMessage(error: unknown, fallback: string = "Something went wrong"): string {
|
|
7
|
+
if (axios.isAxiosError(error)) {
|
|
8
|
+
return error.response?.data?.detail || error.response?.data?.message || error.message || fallback;
|
|
9
|
+
}
|
|
10
|
+
if (error instanceof Error) {
|
|
11
|
+
return error.message;
|
|
12
|
+
}
|
|
13
|
+
return fallback;
|
|
14
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger Utility
|
|
3
|
+
*
|
|
4
|
+
* Environment-gated logger that suppresses output in production.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
let isDevelopment = true;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Set the environment mode for logging
|
|
11
|
+
*/
|
|
12
|
+
export function setLoggerEnvironmentMode(development: boolean): void {
|
|
13
|
+
isDevelopment = development;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const logger = {
|
|
17
|
+
log: (...args: unknown[]) => {
|
|
18
|
+
if (isDevelopment) {
|
|
19
|
+
console.log(...args)
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
warn: (...args: unknown[]) => {
|
|
24
|
+
if (isDevelopment) {
|
|
25
|
+
console.warn(...args)
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Logs a warning to the console in development only.
|
|
31
|
+
* Alias for warn() for Python logging compatibility.
|
|
32
|
+
* In production, this is a no-op.
|
|
33
|
+
*/
|
|
34
|
+
warning: (...args: unknown[]) => {
|
|
35
|
+
if (isDevelopment) {
|
|
36
|
+
console.warn(...args)
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
error: (...args: unknown[]) => {
|
|
41
|
+
if (isDevelopment) {
|
|
42
|
+
console.error(...args)
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
info: (...args: unknown[]) => {
|
|
47
|
+
if (isDevelopment) {
|
|
48
|
+
console.info(...args)
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
createScope: (scope: string) => ({
|
|
53
|
+
log: (...args: unknown[]) => {
|
|
54
|
+
if (isDevelopment) {
|
|
55
|
+
console.log(`[${scope}]`, ...args)
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
warn: (...args: unknown[]) => {
|
|
59
|
+
if (isDevelopment) {
|
|
60
|
+
console.warn(`[${scope}]`, ...args)
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
warning: (...args: unknown[]) => {
|
|
64
|
+
if (isDevelopment) {
|
|
65
|
+
console.warn(`[${scope}]`, ...args)
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
error: (...args: unknown[]) => {
|
|
69
|
+
if (isDevelopment) {
|
|
70
|
+
console.error(`[${scope}]`, ...args)
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
info: (...args: unknown[]) => {
|
|
74
|
+
if (isDevelopment) {
|
|
75
|
+
console.info(`[${scope}]`, ...args)
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
79
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export class InvalidBaseURLError extends Error {
|
|
2
|
+
constructor(message: string) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "InvalidBaseURLError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const ALLOWED_SCHEMES = new Set(["http:", "https:", "ws:", "wss:"]);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Normalizes a URL by trimming whitespace and stripping trailing slashes.
|
|
12
|
+
*/
|
|
13
|
+
export function sanitizeBaseUrl(
|
|
14
|
+
url: string | undefined | null,
|
|
15
|
+
name = "URL"
|
|
16
|
+
): string {
|
|
17
|
+
if (url == null) {
|
|
18
|
+
throw new InvalidBaseURLError(`${name} is not set`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const cleaned0 = url.trim();
|
|
22
|
+
if (!cleaned0) {
|
|
23
|
+
throw new InvalidBaseURLError(`${name} is empty`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Strip all trailing slashes
|
|
27
|
+
const cleaned = cleaned0.replace(/\/+$/, "");
|
|
28
|
+
|
|
29
|
+
let parsed: URL;
|
|
30
|
+
try {
|
|
31
|
+
parsed = new URL(cleaned);
|
|
32
|
+
} catch {
|
|
33
|
+
throw new InvalidBaseURLError(
|
|
34
|
+
`${name} must start with http://, https://, ws://, or wss://, got: ${url}`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!ALLOWED_SCHEMES.has(parsed.protocol)) {
|
|
39
|
+
throw new InvalidBaseURLError(
|
|
40
|
+
`${name} must start with http://, https://, ws://, or wss://, got: ${url}`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!parsed.hostname) {
|
|
45
|
+
throw new InvalidBaseURLError(`${name} is missing a host: ${url}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (parsed.search || parsed.hash) {
|
|
49
|
+
throw new InvalidBaseURLError(
|
|
50
|
+
`${name} should not contain a query or fragment: ${url}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return cleaned;
|
|
55
|
+
}
|