perspectapi-ts-sdk 3.0.2 → 3.2.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,9 @@ import type {
10
10
  SiteUserProfile,
11
11
  SiteUserSubscription,
12
12
  SiteUserOrder,
13
+ CreditBalance,
14
+ CreditBalanceWithTransactions,
15
+ GrantCreditRequest,
13
16
  RequestOtpRequest,
14
17
  VerifyOtpRequest,
15
18
  VerifyOtpResponse,
@@ -290,6 +293,68 @@ export class SiteUsersClient extends BaseClient {
290
293
  );
291
294
  }
292
295
 
296
+ /**
297
+ * Pause a subscription via Stripe pause_collection
298
+ * @param siteName - The site name
299
+ * @param subscriptionId - Subscription ID
300
+ * @param csrfToken - CSRF token (required)
301
+ * @param resumesAt - Optional ISO date string when subscription should auto-resume
302
+ */
303
+ async pauseSubscription(
304
+ siteName: string,
305
+ subscriptionId: string,
306
+ csrfToken?: string,
307
+ resumesAt?: string
308
+ ): Promise<ApiResponse<{ success: boolean }>> {
309
+ const body: Record<string, string> = {};
310
+ if (resumesAt) {
311
+ body.resumes_at = resumesAt;
312
+ }
313
+ return this.create<Record<string, string>, { success: boolean }>(
314
+ this.siteUserEndpoint(siteName, `/users/me/subscriptions/${encodeURIComponent(subscriptionId)}/pause`),
315
+ body,
316
+ csrfToken
317
+ );
318
+ }
319
+
320
+ /**
321
+ * Resume a paused subscription
322
+ * @param siteName - The site name
323
+ * @param subscriptionId - Subscription ID
324
+ * @param csrfToken - CSRF token (required)
325
+ */
326
+ async resumeSubscription(
327
+ siteName: string,
328
+ subscriptionId: string,
329
+ csrfToken?: string
330
+ ): Promise<ApiResponse<{ success: boolean }>> {
331
+ return this.create<Record<string, never>, { success: boolean }>(
332
+ this.siteUserEndpoint(siteName, `/users/me/subscriptions/${encodeURIComponent(subscriptionId)}/resume`),
333
+ {},
334
+ csrfToken
335
+ );
336
+ }
337
+
338
+ /**
339
+ * Change subscription plan tier
340
+ * @param siteName - The site name
341
+ * @param subscriptionId - Subscription ID
342
+ * @param productId - New product ID to switch to
343
+ * @param csrfToken - CSRF token (required)
344
+ */
345
+ async changeSubscriptionPlan(
346
+ siteName: string,
347
+ subscriptionId: string,
348
+ productId: number,
349
+ csrfToken?: string
350
+ ): Promise<ApiResponse<{ success: boolean }>> {
351
+ return this.create<{ product_id: number }, { success: boolean }>(
352
+ this.siteUserEndpoint(siteName, `/users/me/subscriptions/${encodeURIComponent(subscriptionId)}/change-plan`),
353
+ { product_id: productId },
354
+ csrfToken
355
+ );
356
+ }
357
+
293
358
  /**
294
359
  * Get linked newsletter subscriptions
295
360
  * @param siteName - The site name
@@ -300,6 +365,35 @@ export class SiteUsersClient extends BaseClient {
300
365
  );
301
366
  }
302
367
 
368
+ // ============================================================================
369
+ // CREDIT ENDPOINTS (site user JWT required)
370
+ // ============================================================================
371
+
372
+ /**
373
+ * Get current credit balance
374
+ * @param siteName - The site name
375
+ */
376
+ async getCreditBalance(siteName: string): Promise<ApiResponse<CreditBalance>> {
377
+ return this.getSingle<CreditBalance>(
378
+ this.siteUserEndpoint(siteName, '/users/me/credits/balance')
379
+ );
380
+ }
381
+
382
+ /**
383
+ * Get credit balance and paginated transactions
384
+ * @param siteName - The site name
385
+ * @param params - Pagination params
386
+ */
387
+ async getCreditTransactions(
388
+ siteName: string,
389
+ params?: { limit?: number; offset?: number }
390
+ ): Promise<ApiResponse<CreditBalanceWithTransactions>> {
391
+ return this.http.get<CreditBalanceWithTransactions>(
392
+ this.buildPath(this.siteUserEndpoint(siteName, '/users/me/credits')),
393
+ params
394
+ );
395
+ }
396
+
303
397
  // ============================================================================
304
398
  // ADMIN ENDPOINTS (API key auth required)
305
399
  // ============================================================================
@@ -349,4 +443,25 @@ export class SiteUsersClient extends BaseClient {
349
443
  csrfToken
350
444
  );
351
445
  }
446
+
447
+ /**
448
+ * Grant credit to a user (admin only)
449
+ * Adds credit to the user's ledger and Stripe Customer Balance.
450
+ * @param siteName - The site name
451
+ * @param userId - User ID
452
+ * @param data - Amount in cents and description
453
+ * @param csrfToken - CSRF token (required)
454
+ */
455
+ async grantCredit(
456
+ siteName: string,
457
+ userId: string,
458
+ data: GrantCreditRequest,
459
+ csrfToken?: string
460
+ ): Promise<ApiResponse<{ success: boolean; new_balance_cents: number }>> {
461
+ return this.create<GrantCreditRequest, { success: boolean; new_balance_cents: number }>(
462
+ this.siteUserEndpoint(siteName, `/users/${encodeURIComponent(userId)}/credits/grant`),
463
+ data,
464
+ csrfToken
465
+ );
466
+ }
352
467
  }
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';
@@ -620,6 +624,29 @@ export interface SiteUserOrder {
620
624
  completed_at?: string;
621
625
  }
622
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[];
643
+ }
644
+
645
+ export interface GrantCreditRequest {
646
+ amount_cents: number;
647
+ description: string;
648
+ }
649
+
623
650
  export interface RequestOtpRequest {
624
651
  email: string;
625
652
  waitlist?: boolean; // Mark user as waitlist signup
@@ -654,6 +681,71 @@ export interface SetProfileValueRequest {
654
681
  value: string;
655
682
  }
656
683
 
684
+ // Bundle Groups (structural "pick N items" metadata on a product)
685
+ export interface ProductBundleGroup {
686
+ bundle_group_id: number;
687
+ product_id: number;
688
+ name: string;
689
+ description?: string;
690
+ quantity: number;
691
+ position: number;
692
+ created_at: string;
693
+ updated_at: string;
694
+ }
695
+
696
+ export interface CreateBundleGroupRequest {
697
+ name: string;
698
+ description?: string;
699
+ quantity: number;
700
+ position?: number;
701
+ }
702
+
703
+ // Bundle Collections (time-bounded, site-scoped sets of products)
704
+ export interface BundleCollection {
705
+ collection_id: number;
706
+ site_id: string;
707
+ site_name: string;
708
+ name: string;
709
+ description?: string;
710
+ available_from?: string;
711
+ available_until?: string;
712
+ published: number;
713
+ created_at: string;
714
+ updated_at: string;
715
+ }
716
+
717
+ export interface CreateBundleCollectionRequest {
718
+ name: string;
719
+ description?: string;
720
+ available_from?: string;
721
+ available_until?: string;
722
+ published?: number;
723
+ }
724
+
725
+ export interface BundleCollectionItem {
726
+ id: number;
727
+ collection_id: number;
728
+ product_id: number;
729
+ max_quantity?: number;
730
+ position: number;
731
+ created_at: string;
732
+ updated_at: string;
733
+ }
734
+
735
+ export interface BundleCollectionItemWithProduct extends BundleCollectionItem {
736
+ product_name: string;
737
+ slug: string;
738
+ unit_amount: number;
739
+ currency: string;
740
+ image_url?: string;
741
+ }
742
+
743
+ export interface AddCollectionItemRequest {
744
+ product_id: number;
745
+ max_quantity?: number;
746
+ position?: number;
747
+ }
748
+
657
749
  // Error Types
658
750
  export interface ApiError {
659
751
  message: string;