perspectapi-ts-sdk 3.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Bundles & Collections client for PerspectAPI SDK
3
+ */
4
+
5
+ import { BaseClient } from './base-client';
6
+ import type { CacheManager } from '../cache/cache-manager';
7
+ import type { CachePolicy } from '../cache/types';
8
+ import type {
9
+ ProductBundleGroup,
10
+ CreateBundleGroupRequest,
11
+ BundleCollection,
12
+ CreateBundleCollectionRequest,
13
+ BundleCollectionItem,
14
+ BundleCollectionItemWithProduct,
15
+ AddCollectionItemRequest,
16
+ ApiResponse,
17
+ } from '../types';
18
+
19
+ export class BundlesClient extends BaseClient {
20
+ constructor(http: any, cache?: CacheManager) {
21
+ super(http, '/api/v1', cache);
22
+ }
23
+
24
+ // ============================================================================
25
+ // BUNDLE GROUPS (structural "pick N items" on a product)
26
+ // ============================================================================
27
+
28
+ async getBundleGroups(
29
+ siteName: string,
30
+ productId: number,
31
+ cachePolicy?: CachePolicy
32
+ ): Promise<ApiResponse<ProductBundleGroup[]>> {
33
+ const endpoint = this.siteScopedEndpoint(
34
+ siteName,
35
+ `/products/${productId}/bundle-groups`,
36
+ { includeSitesSegment: false }
37
+ );
38
+ const path = this.buildPath(endpoint);
39
+
40
+ return this.fetchWithCache<ApiResponse<ProductBundleGroup[]>>(
41
+ endpoint,
42
+ undefined,
43
+ this.buildBundleTags(siteName, [`bundles:product:${productId}`]),
44
+ cachePolicy,
45
+ () => this.http.get<ProductBundleGroup[]>(path)
46
+ );
47
+ }
48
+
49
+ async createBundleGroup(
50
+ siteName: string,
51
+ productId: number,
52
+ data: CreateBundleGroupRequest
53
+ ): Promise<ApiResponse<ProductBundleGroup>> {
54
+ const endpoint = this.siteScopedEndpoint(
55
+ siteName,
56
+ `/products/${productId}/bundle-groups`,
57
+ { includeSitesSegment: false }
58
+ );
59
+ return this.create(endpoint, data);
60
+ }
61
+
62
+ async updateBundleGroup(
63
+ siteName: string,
64
+ productId: number,
65
+ bundleGroupId: number,
66
+ data: Partial<CreateBundleGroupRequest>
67
+ ): Promise<ApiResponse<ProductBundleGroup>> {
68
+ const endpoint = this.siteScopedEndpoint(
69
+ siteName,
70
+ `/products/${productId}/bundle-groups/${bundleGroupId}`,
71
+ { includeSitesSegment: false }
72
+ );
73
+ return this.patch(endpoint, data);
74
+ }
75
+
76
+ async deleteBundleGroup(
77
+ siteName: string,
78
+ productId: number,
79
+ bundleGroupId: number
80
+ ): Promise<ApiResponse<{ message: string }>> {
81
+ const endpoint = this.siteScopedEndpoint(
82
+ siteName,
83
+ `/products/${productId}/bundle-groups/${bundleGroupId}`,
84
+ { includeSitesSegment: false }
85
+ );
86
+ return this.delete<{ message: string }>(endpoint);
87
+ }
88
+
89
+ // ============================================================================
90
+ // BUNDLE COLLECTIONS (time-bounded, site-scoped sets of products)
91
+ // ============================================================================
92
+
93
+ async getCollections(
94
+ siteName: string,
95
+ cachePolicy?: CachePolicy
96
+ ): Promise<ApiResponse<BundleCollection[]>> {
97
+ const endpoint = this.siteScopedEndpoint(
98
+ siteName,
99
+ '/collections',
100
+ { includeSitesSegment: false }
101
+ );
102
+ const path = this.buildPath(endpoint);
103
+
104
+ return this.fetchWithCache<ApiResponse<BundleCollection[]>>(
105
+ endpoint,
106
+ undefined,
107
+ this.buildCollectionTags(siteName, ['collections:list']),
108
+ cachePolicy,
109
+ () => this.http.get<BundleCollection[]>(path)
110
+ );
111
+ }
112
+
113
+ async getCurrentCollection(
114
+ siteName: string,
115
+ cachePolicy?: CachePolicy
116
+ ): Promise<ApiResponse<BundleCollection & { items: BundleCollectionItemWithProduct[] }>> {
117
+ const endpoint = this.siteScopedEndpoint(
118
+ siteName,
119
+ '/collections/current',
120
+ { includeSitesSegment: false }
121
+ );
122
+ const path = this.buildPath(endpoint);
123
+
124
+ return this.fetchWithCache<ApiResponse<BundleCollection & { items: BundleCollectionItemWithProduct[] }>>(
125
+ endpoint,
126
+ undefined,
127
+ this.buildCollectionTags(siteName, ['collections:current']),
128
+ cachePolicy,
129
+ () => this.http.get(path)
130
+ );
131
+ }
132
+
133
+ async getCollection(
134
+ siteName: string,
135
+ collectionId: number,
136
+ cachePolicy?: CachePolicy
137
+ ): Promise<ApiResponse<BundleCollection & { items: BundleCollectionItemWithProduct[] }>> {
138
+ const endpoint = this.siteScopedEndpoint(
139
+ siteName,
140
+ `/collections/${collectionId}`,
141
+ { includeSitesSegment: false }
142
+ );
143
+ const path = this.buildPath(endpoint);
144
+
145
+ return this.fetchWithCache<ApiResponse<BundleCollection & { items: BundleCollectionItemWithProduct[] }>>(
146
+ endpoint,
147
+ undefined,
148
+ this.buildCollectionTags(siteName, [`collections:id:${collectionId}`]),
149
+ cachePolicy,
150
+ () => this.http.get(path)
151
+ );
152
+ }
153
+
154
+ async createCollection(
155
+ siteName: string,
156
+ data: CreateBundleCollectionRequest
157
+ ): Promise<ApiResponse<BundleCollection>> {
158
+ const endpoint = this.siteScopedEndpoint(
159
+ siteName,
160
+ '/collections',
161
+ { includeSitesSegment: false }
162
+ );
163
+ return this.create(endpoint, data);
164
+ }
165
+
166
+ async updateCollection(
167
+ siteName: string,
168
+ collectionId: number,
169
+ data: Partial<CreateBundleCollectionRequest>
170
+ ): Promise<ApiResponse<BundleCollection>> {
171
+ const endpoint = this.siteScopedEndpoint(
172
+ siteName,
173
+ `/collections/${collectionId}`,
174
+ { includeSitesSegment: false }
175
+ );
176
+ return this.patch(endpoint, data);
177
+ }
178
+
179
+ async deleteCollection(
180
+ siteName: string,
181
+ collectionId: number
182
+ ): Promise<ApiResponse<{ message: string }>> {
183
+ const endpoint = this.siteScopedEndpoint(
184
+ siteName,
185
+ `/collections/${collectionId}`,
186
+ { includeSitesSegment: false }
187
+ );
188
+ return this.delete<{ message: string }>(endpoint);
189
+ }
190
+
191
+ // ============================================================================
192
+ // COLLECTION ITEMS
193
+ // ============================================================================
194
+
195
+ async getCollectionItems(
196
+ siteName: string,
197
+ collectionId: number,
198
+ cachePolicy?: CachePolicy
199
+ ): Promise<ApiResponse<BundleCollectionItemWithProduct[]>> {
200
+ const endpoint = this.siteScopedEndpoint(
201
+ siteName,
202
+ `/collections/${collectionId}/items`,
203
+ { includeSitesSegment: false }
204
+ );
205
+ const path = this.buildPath(endpoint);
206
+
207
+ return this.fetchWithCache<ApiResponse<BundleCollectionItemWithProduct[]>>(
208
+ endpoint,
209
+ undefined,
210
+ this.buildCollectionTags(siteName, [`collections:items:${collectionId}`]),
211
+ cachePolicy,
212
+ () => this.http.get(path)
213
+ );
214
+ }
215
+
216
+ async addCollectionItem(
217
+ siteName: string,
218
+ collectionId: number,
219
+ data: AddCollectionItemRequest
220
+ ): Promise<ApiResponse<BundleCollectionItem>> {
221
+ const endpoint = this.siteScopedEndpoint(
222
+ siteName,
223
+ `/collections/${collectionId}/items`,
224
+ { includeSitesSegment: false }
225
+ );
226
+ return this.create(endpoint, data);
227
+ }
228
+
229
+ async removeCollectionItem(
230
+ siteName: string,
231
+ collectionId: number,
232
+ itemId: number
233
+ ): Promise<ApiResponse<{ message: string }>> {
234
+ const endpoint = this.siteScopedEndpoint(
235
+ siteName,
236
+ `/collections/${collectionId}/items/${itemId}`,
237
+ { includeSitesSegment: false }
238
+ );
239
+ return this.delete<{ message: string }>(endpoint);
240
+ }
241
+
242
+ // ============================================================================
243
+ // CACHE TAGS
244
+ // ============================================================================
245
+
246
+ private buildBundleTags(siteName?: string, extraTags: string[] = []): string[] {
247
+ const tags = new Set<string>(['bundles']);
248
+ if (siteName) {
249
+ tags.add(`bundles:site:${siteName}`);
250
+ }
251
+ extraTags.filter(Boolean).forEach(tag => tags.add(tag));
252
+ return Array.from(tags.values());
253
+ }
254
+
255
+ private buildCollectionTags(siteName?: string, extraTags: string[] = []): string[] {
256
+ const tags = new Set<string>(['collections']);
257
+ if (siteName) {
258
+ tags.add(`collections:site:${siteName}`);
259
+ }
260
+ extraTags.filter(Boolean).forEach(tag => tags.add(tag));
261
+ return Array.from(tags.values());
262
+ }
263
+ }
@@ -10,6 +10,8 @@ import type {
10
10
  SiteUserProfile,
11
11
  SiteUserSubscription,
12
12
  SiteUserOrder,
13
+ CreditBalance,
14
+ CreditBalanceWithTransactions,
13
15
  RequestOtpRequest,
14
16
  VerifyOtpRequest,
15
17
  VerifyOtpResponse,
@@ -290,6 +292,68 @@ export class SiteUsersClient extends BaseClient {
290
292
  );
291
293
  }
292
294
 
295
+ /**
296
+ * Pause a subscription via Stripe pause_collection
297
+ * @param siteName - The site name
298
+ * @param subscriptionId - Subscription ID
299
+ * @param csrfToken - CSRF token (required)
300
+ * @param resumesAt - Optional ISO date string when subscription should auto-resume
301
+ */
302
+ async pauseSubscription(
303
+ siteName: string,
304
+ subscriptionId: string,
305
+ csrfToken?: string,
306
+ resumesAt?: string
307
+ ): Promise<ApiResponse<{ success: boolean }>> {
308
+ const body: Record<string, string> = {};
309
+ if (resumesAt) {
310
+ body.resumes_at = resumesAt;
311
+ }
312
+ return this.create<Record<string, string>, { success: boolean }>(
313
+ this.siteUserEndpoint(siteName, `/users/me/subscriptions/${encodeURIComponent(subscriptionId)}/pause`),
314
+ body,
315
+ csrfToken
316
+ );
317
+ }
318
+
319
+ /**
320
+ * Resume a paused subscription
321
+ * @param siteName - The site name
322
+ * @param subscriptionId - Subscription ID
323
+ * @param csrfToken - CSRF token (required)
324
+ */
325
+ async resumeSubscription(
326
+ siteName: string,
327
+ subscriptionId: string,
328
+ csrfToken?: string
329
+ ): Promise<ApiResponse<{ success: boolean }>> {
330
+ return this.create<Record<string, never>, { success: boolean }>(
331
+ this.siteUserEndpoint(siteName, `/users/me/subscriptions/${encodeURIComponent(subscriptionId)}/resume`),
332
+ {},
333
+ csrfToken
334
+ );
335
+ }
336
+
337
+ /**
338
+ * Change subscription plan tier
339
+ * @param siteName - The site name
340
+ * @param subscriptionId - Subscription ID
341
+ * @param productId - New product ID to switch to
342
+ * @param csrfToken - CSRF token (required)
343
+ */
344
+ async changeSubscriptionPlan(
345
+ siteName: string,
346
+ subscriptionId: string,
347
+ productId: number,
348
+ csrfToken?: string
349
+ ): Promise<ApiResponse<{ success: boolean }>> {
350
+ return this.create<{ product_id: number }, { success: boolean }>(
351
+ this.siteUserEndpoint(siteName, `/users/me/subscriptions/${encodeURIComponent(subscriptionId)}/change-plan`),
352
+ { product_id: productId },
353
+ csrfToken
354
+ );
355
+ }
356
+
293
357
  /**
294
358
  * Get linked newsletter subscriptions
295
359
  * @param siteName - The site name
@@ -300,6 +364,35 @@ export class SiteUsersClient extends BaseClient {
300
364
  );
301
365
  }
302
366
 
367
+ // ============================================================================
368
+ // CREDIT ENDPOINTS (site user JWT required)
369
+ // ============================================================================
370
+
371
+ /**
372
+ * Get current credit balance
373
+ * @param siteName - The site name
374
+ */
375
+ async getCreditBalance(siteName: string): Promise<ApiResponse<CreditBalance>> {
376
+ return this.getSingle<CreditBalance>(
377
+ this.siteUserEndpoint(siteName, '/users/me/credits/balance')
378
+ );
379
+ }
380
+
381
+ /**
382
+ * Get credit balance and paginated transactions
383
+ * @param siteName - The site name
384
+ * @param params - Pagination params
385
+ */
386
+ async getCreditTransactions(
387
+ siteName: string,
388
+ params?: { limit?: number; offset?: number }
389
+ ): Promise<ApiResponse<CreditBalanceWithTransactions>> {
390
+ return this.http.get<CreditBalanceWithTransactions>(
391
+ this.buildPath(this.siteUserEndpoint(siteName, '/users/me/credits')),
392
+ params
393
+ );
394
+ }
395
+
303
396
  // ============================================================================
304
397
  // ADMIN ENDPOINTS (API key auth required)
305
398
  // ============================================================================
package/src/index.ts CHANGED
@@ -20,6 +20,7 @@ export { CheckoutClient } from './client/checkout-client';
20
20
  export { ContactClient } from './client/contact-client';
21
21
  export { NewsletterClient } from './client/newsletter-client';
22
22
  export { SiteUsersClient } from './client/site-users-client';
23
+ export { BundlesClient } from './client/bundles-client';
23
24
 
24
25
  // Base classes
25
26
  export { BaseClient } from './client/base-client';
@@ -116,4 +117,11 @@ export type {
116
117
  VerifyOtpResponse,
117
118
  UpdateSiteUserRequest,
118
119
  SetProfileValueRequest,
120
+ ProductBundleGroup,
121
+ CreateBundleGroupRequest,
122
+ BundleCollection,
123
+ CreateBundleCollectionRequest,
124
+ BundleCollectionItem,
125
+ BundleCollectionItemWithProduct,
126
+ AddCollectionItemRequest,
119
127
  } from './types';
@@ -17,6 +17,7 @@ import { CheckoutClient } from './client/checkout-client';
17
17
  import { ContactClient } from './client/contact-client';
18
18
  import { NewsletterClient } from './client/newsletter-client';
19
19
  import { SiteUsersClient } from './client/site-users-client';
20
+ import { BundlesClient } from './client/bundles-client';
20
21
 
21
22
  import type { PerspectApiConfig, ApiResponse } from './types';
22
23
 
@@ -37,6 +38,7 @@ export class PerspectApiClient {
37
38
  public readonly contact: ContactClient;
38
39
  public readonly newsletter: NewsletterClient;
39
40
  public readonly siteUsers: SiteUsersClient;
41
+ public readonly bundles: BundlesClient;
40
42
 
41
43
  constructor(config: PerspectApiConfig) {
42
44
  // Validate required configuration
@@ -61,6 +63,7 @@ export class PerspectApiClient {
61
63
  this.contact = new ContactClient(this.http, this.cache);
62
64
  this.newsletter = new NewsletterClient(this.http, this.cache);
63
65
  this.siteUsers = new SiteUsersClient(this.http, this.cache);
66
+ this.bundles = new BundlesClient(this.http, this.cache);
64
67
  }
65
68
 
66
69
  /**
@@ -464,6 +464,8 @@ export interface CreateCheckoutSessionRequest {
464
464
  quantity: number;
465
465
  // SKU-based checkout (variant products)
466
466
  sku_id?: number;
467
+ // Product ID-based checkout (server resolves to Stripe price)
468
+ product_id?: number;
467
469
  // Or define price data inline
468
470
  price_data?: {
469
471
  currency: string;
@@ -484,6 +486,8 @@ export interface CreateCheckoutSessionRequest {
484
486
  customer_email?: string;
485
487
  customerEmail?: string; // Alternative naming
486
488
  site_user_id?: string; // Authenticated site user — enables checkout prefill
489
+ referral_code?: string; // Referral code for first-checkout discount
490
+ referrer_site_user_id?: string; // Referrer's site_user_id for credit
487
491
  currency?: string;
488
492
  metadata?: CheckoutMetadata;
489
493
  mode?: 'payment' | 'subscription' | 'setup';
@@ -617,7 +621,25 @@ export interface SiteUserOrder {
617
621
  payment_status?: string;
618
622
  line_items: any[];
619
623
  created_at: string;
620
- updated_at: string;
624
+ completed_at?: string;
625
+ }
626
+
627
+ // Credits
628
+ export interface CreditTransaction {
629
+ id: number;
630
+ amount_cents: number;
631
+ balance_after_cents: number;
632
+ type: string;
633
+ description: string | null;
634
+ created_at: string;
635
+ }
636
+
637
+ export interface CreditBalance {
638
+ balance_cents: number;
639
+ }
640
+
641
+ export interface CreditBalanceWithTransactions extends CreditBalance {
642
+ transactions: CreditTransaction[];
621
643
  }
622
644
 
623
645
  export interface RequestOtpRequest {
@@ -654,6 +676,71 @@ export interface SetProfileValueRequest {
654
676
  value: string;
655
677
  }
656
678
 
679
+ // Bundle Groups (structural "pick N items" metadata on a product)
680
+ export interface ProductBundleGroup {
681
+ bundle_group_id: number;
682
+ product_id: number;
683
+ name: string;
684
+ description?: string;
685
+ quantity: number;
686
+ position: number;
687
+ created_at: string;
688
+ updated_at: string;
689
+ }
690
+
691
+ export interface CreateBundleGroupRequest {
692
+ name: string;
693
+ description?: string;
694
+ quantity: number;
695
+ position?: number;
696
+ }
697
+
698
+ // Bundle Collections (time-bounded, site-scoped sets of products)
699
+ export interface BundleCollection {
700
+ collection_id: number;
701
+ site_id: string;
702
+ site_name: string;
703
+ name: string;
704
+ description?: string;
705
+ available_from?: string;
706
+ available_until?: string;
707
+ published: number;
708
+ created_at: string;
709
+ updated_at: string;
710
+ }
711
+
712
+ export interface CreateBundleCollectionRequest {
713
+ name: string;
714
+ description?: string;
715
+ available_from?: string;
716
+ available_until?: string;
717
+ published?: number;
718
+ }
719
+
720
+ export interface BundleCollectionItem {
721
+ id: number;
722
+ collection_id: number;
723
+ product_id: number;
724
+ max_quantity?: number;
725
+ position: number;
726
+ created_at: string;
727
+ updated_at: string;
728
+ }
729
+
730
+ export interface BundleCollectionItemWithProduct extends BundleCollectionItem {
731
+ product_name: string;
732
+ slug: string;
733
+ unit_amount: number;
734
+ currency: string;
735
+ image_url?: string;
736
+ }
737
+
738
+ export interface AddCollectionItemRequest {
739
+ product_id: number;
740
+ max_quantity?: number;
741
+ position?: number;
742
+ }
743
+
657
744
  // Error Types
658
745
  export interface ApiError {
659
746
  message: string;