777 lines
22 KiB
TypeScript
777 lines
22 KiB
TypeScript
/**
|
|
* Squarespace API Client
|
|
* Comprehensive client with OAuth2, pagination, error handling, and retry logic
|
|
*/
|
|
|
|
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios';
|
|
import type {
|
|
SquarespaceConfig,
|
|
OAuthTokenResponse,
|
|
PaginatedResponse,
|
|
PaginationParams,
|
|
// Products
|
|
Product,
|
|
CreateProductRequest,
|
|
UpdateProductRequest,
|
|
GetProductsResponse,
|
|
ProductVariant,
|
|
ProductImage,
|
|
// Orders
|
|
Order,
|
|
CreateOrderRequest,
|
|
FulfillOrderRequest,
|
|
OrdersQueryParams,
|
|
GetOrdersResponse,
|
|
// Inventory
|
|
InventoryItem,
|
|
UpdateInventoryRequest,
|
|
// Transactions
|
|
Transaction,
|
|
CreateRefundRequest,
|
|
// Profiles
|
|
Profile,
|
|
ProfilesQueryParams,
|
|
GetProfilesResponse,
|
|
// Webhooks
|
|
WebhookSubscription,
|
|
CreateWebhookRequest,
|
|
UpdateWebhookRequest,
|
|
WebhookTestNotification,
|
|
GetWebhooksResponse,
|
|
// Site & Pages
|
|
Website,
|
|
Collection,
|
|
Page,
|
|
CreatePageRequest,
|
|
UpdatePageRequest,
|
|
// Forms
|
|
Form,
|
|
FormSubmission,
|
|
FormSubmissionsQueryParams,
|
|
GetFormSubmissionsResponse,
|
|
// Blog
|
|
BlogCollection,
|
|
BlogPost,
|
|
CreateBlogPostRequest,
|
|
UpdateBlogPostRequest,
|
|
GetBlogPostsResponse,
|
|
// Analytics
|
|
AnalyticsParams,
|
|
RevenueMetrics,
|
|
ProductPerformance,
|
|
ConversionMetrics,
|
|
} from '../types/index.js';
|
|
|
|
const DEFAULT_BASE_URL = 'https://api.squarespace.com/1.0';
|
|
const DEFAULT_TIMEOUT = 30000;
|
|
const DEFAULT_RETRY_ATTEMPTS = 3;
|
|
const OAUTH_TOKEN_URL = 'https://login.squarespace.com/api/1/login/oauth/provider/tokens';
|
|
|
|
export class SquarespaceClient {
|
|
private client: AxiosInstance;
|
|
private accessToken: string;
|
|
private refreshToken?: string;
|
|
private clientId?: string;
|
|
private clientSecret?: string;
|
|
private retryAttempts: number;
|
|
private tokenExpiresAt?: Date;
|
|
|
|
constructor(config: SquarespaceConfig) {
|
|
this.accessToken = config.accessToken;
|
|
this.refreshToken = config.refreshToken;
|
|
this.clientId = config.clientId;
|
|
this.clientSecret = config.clientSecret;
|
|
this.retryAttempts = config.retryAttempts ?? DEFAULT_RETRY_ATTEMPTS;
|
|
|
|
this.client = axios.create({
|
|
baseURL: config.baseUrl || DEFAULT_BASE_URL,
|
|
timeout: config.timeout || DEFAULT_TIMEOUT,
|
|
headers: {
|
|
'User-Agent': 'Squarespace-MCP-Server/1.0',
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
// Request interceptor to add auth and handle token refresh
|
|
this.client.interceptors.request.use(
|
|
async (config) => {
|
|
// Check if token needs refresh
|
|
if (this.shouldRefreshToken()) {
|
|
await this.refreshAccessToken();
|
|
}
|
|
|
|
config.headers.Authorization = `Bearer ${this.accessToken}`;
|
|
return config;
|
|
},
|
|
(error) => Promise.reject(error)
|
|
);
|
|
|
|
// Response interceptor for error handling
|
|
this.client.interceptors.response.use(
|
|
(response) => response,
|
|
async (error: AxiosError) => {
|
|
if (error.response?.status === 401 && this.refreshToken) {
|
|
// Token expired, try to refresh
|
|
try {
|
|
await this.refreshAccessToken();
|
|
// Retry the original request
|
|
const config = error.config;
|
|
if (config) {
|
|
config.headers.Authorization = `Bearer ${this.accessToken}`;
|
|
return this.client.request(config);
|
|
}
|
|
} catch (refreshError) {
|
|
return Promise.reject(this.handleError(refreshError as AxiosError));
|
|
}
|
|
}
|
|
return Promise.reject(this.handleError(error));
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if access token should be refreshed
|
|
*/
|
|
private shouldRefreshToken(): boolean {
|
|
if (!this.tokenExpiresAt || !this.refreshToken) {
|
|
return false;
|
|
}
|
|
// Refresh if less than 5 minutes remaining
|
|
const fiveMinutes = 5 * 60 * 1000;
|
|
return Date.now() >= this.tokenExpiresAt.getTime() - fiveMinutes;
|
|
}
|
|
|
|
/**
|
|
* Refresh the access token using refresh token
|
|
*/
|
|
private async refreshAccessToken(): Promise<void> {
|
|
if (!this.refreshToken || !this.clientId || !this.clientSecret) {
|
|
throw new Error('Missing refresh token or OAuth credentials');
|
|
}
|
|
|
|
const auth = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64');
|
|
|
|
try {
|
|
const response = await axios.post<OAuthTokenResponse>(
|
|
OAUTH_TOKEN_URL,
|
|
{
|
|
grant_type: 'refresh_token',
|
|
refresh_token: this.refreshToken,
|
|
},
|
|
{
|
|
headers: {
|
|
'Authorization': `Basic ${auth}`,
|
|
'Content-Type': 'application/json',
|
|
'User-Agent': 'Squarespace-MCP-Server/1.0',
|
|
},
|
|
}
|
|
);
|
|
|
|
this.accessToken = response.data.access_token;
|
|
if (response.data.refresh_token) {
|
|
this.refreshToken = response.data.refresh_token;
|
|
}
|
|
|
|
// Set expiration time (30 minutes from now)
|
|
this.tokenExpiresAt = new Date(Date.now() + 30 * 60 * 1000);
|
|
} catch (error) {
|
|
throw new Error('Failed to refresh access token');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle API errors and convert to SquarespaceAPIError
|
|
*/
|
|
private handleError(error: AxiosError): SquarespaceAPIError {
|
|
const response = error.response;
|
|
const status = response?.status || 500;
|
|
const data = response?.data as any;
|
|
|
|
return new SquarespaceAPIError(
|
|
status,
|
|
data?.type || 'UNKNOWN_ERROR',
|
|
data?.message || error.message || 'An unknown error occurred',
|
|
data?.errors
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Make a request with automatic retry logic
|
|
*/
|
|
private async makeRequest<T>(
|
|
config: AxiosRequestConfig,
|
|
attempt: number = 0
|
|
): Promise<T> {
|
|
try {
|
|
const response = await this.client.request<T>(config);
|
|
return response.data;
|
|
} catch (error) {
|
|
const axiosError = error as AxiosError;
|
|
|
|
// Retry on 429 (rate limit) or 5xx errors
|
|
if (
|
|
attempt < this.retryAttempts &&
|
|
(axiosError.response?.status === 429 ||
|
|
(axiosError.response?.status && axiosError.response.status >= 500))
|
|
) {
|
|
// Exponential backoff: 1s, 2s, 4s, etc.
|
|
const delay = Math.pow(2, attempt) * 1000;
|
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
return this.makeRequest<T>(config, attempt + 1);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Products API
|
|
// ============================================================================
|
|
|
|
async getProducts(params?: PaginationParams): Promise<GetProductsResponse> {
|
|
return this.makeRequest<GetProductsResponse>({
|
|
method: 'GET',
|
|
url: '/commerce/products',
|
|
params,
|
|
});
|
|
}
|
|
|
|
async getProduct(productId: string): Promise<Product> {
|
|
return this.makeRequest<Product>({
|
|
method: 'GET',
|
|
url: `/commerce/products/${productId}`,
|
|
});
|
|
}
|
|
|
|
async createProduct(product: CreateProductRequest): Promise<Product> {
|
|
return this.makeRequest<Product>({
|
|
method: 'POST',
|
|
url: '/commerce/products',
|
|
data: product,
|
|
});
|
|
}
|
|
|
|
async updateProduct(productId: string, updates: UpdateProductRequest): Promise<Product> {
|
|
return this.makeRequest<Product>({
|
|
method: 'PUT',
|
|
url: `/commerce/products/${productId}`,
|
|
data: updates,
|
|
});
|
|
}
|
|
|
|
async deleteProduct(productId: string): Promise<void> {
|
|
await this.makeRequest({
|
|
method: 'DELETE',
|
|
url: `/commerce/products/${productId}`,
|
|
});
|
|
}
|
|
|
|
async createProductVariant(productId: string, variant: any): Promise<ProductVariant> {
|
|
return this.makeRequest<ProductVariant>({
|
|
method: 'POST',
|
|
url: `/commerce/products/${productId}/variants`,
|
|
data: variant,
|
|
});
|
|
}
|
|
|
|
async updateProductVariant(
|
|
productId: string,
|
|
variantId: string,
|
|
updates: any
|
|
): Promise<ProductVariant> {
|
|
return this.makeRequest<ProductVariant>({
|
|
method: 'PUT',
|
|
url: `/commerce/products/${productId}/variants/${variantId}`,
|
|
data: updates,
|
|
});
|
|
}
|
|
|
|
async deleteProductVariant(productId: string, variantId: string): Promise<void> {
|
|
await this.makeRequest({
|
|
method: 'DELETE',
|
|
url: `/commerce/products/${productId}/variants/${variantId}`,
|
|
});
|
|
}
|
|
|
|
async uploadProductImage(productId: string, imageFile: Buffer): Promise<ProductImage> {
|
|
return this.makeRequest<ProductImage>({
|
|
method: 'POST',
|
|
url: `/commerce/products/${productId}/images`,
|
|
data: imageFile,
|
|
headers: {
|
|
'Content-Type': 'image/jpeg',
|
|
},
|
|
});
|
|
}
|
|
|
|
async deleteProductImage(productId: string, imageId: string): Promise<void> {
|
|
await this.makeRequest({
|
|
method: 'DELETE',
|
|
url: `/commerce/products/${productId}/images/${imageId}`,
|
|
});
|
|
}
|
|
|
|
async reorderProductImage(productId: string, imageId: string, position: number): Promise<void> {
|
|
await this.makeRequest({
|
|
method: 'POST',
|
|
url: `/commerce/products/${productId}/images/${imageId}/reorder`,
|
|
data: { position },
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Orders API
|
|
// ============================================================================
|
|
|
|
async getOrders(params?: OrdersQueryParams): Promise<GetOrdersResponse> {
|
|
return this.makeRequest<GetOrdersResponse>({
|
|
method: 'GET',
|
|
url: '/commerce/orders',
|
|
params,
|
|
});
|
|
}
|
|
|
|
async getOrder(orderId: string): Promise<Order> {
|
|
return this.makeRequest<Order>({
|
|
method: 'GET',
|
|
url: `/commerce/orders/${orderId}`,
|
|
});
|
|
}
|
|
|
|
async createOrder(order: CreateOrderRequest): Promise<Order> {
|
|
return this.makeRequest<Order>({
|
|
method: 'POST',
|
|
url: '/commerce/orders',
|
|
data: order,
|
|
});
|
|
}
|
|
|
|
async fulfillOrder(orderId: string, fulfillment: FulfillOrderRequest): Promise<Order> {
|
|
return this.makeRequest<Order>({
|
|
method: 'POST',
|
|
url: `/commerce/orders/${orderId}/fulfillments`,
|
|
data: fulfillment,
|
|
});
|
|
}
|
|
|
|
async addOrderNote(orderId: string, note: string): Promise<Order> {
|
|
return this.makeRequest<Order>({
|
|
method: 'POST',
|
|
url: `/commerce/orders/${orderId}/notes`,
|
|
data: { text: note },
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Inventory API
|
|
// ============================================================================
|
|
|
|
async getInventory(variantId: string): Promise<InventoryItem> {
|
|
return this.makeRequest<InventoryItem>({
|
|
method: 'GET',
|
|
url: `/commerce/inventory/${variantId}`,
|
|
});
|
|
}
|
|
|
|
async updateInventory(variantId: string, updates: UpdateInventoryRequest): Promise<InventoryItem> {
|
|
return this.makeRequest<InventoryItem>({
|
|
method: 'PUT',
|
|
url: `/commerce/inventory/${variantId}`,
|
|
data: updates,
|
|
});
|
|
}
|
|
|
|
async adjustInventory(variantId: string, adjustment: number): Promise<InventoryItem> {
|
|
return this.makeRequest<InventoryItem>({
|
|
method: 'POST',
|
|
url: `/commerce/inventory/${variantId}/adjust`,
|
|
data: { adjustment },
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Transactions API
|
|
// ============================================================================
|
|
|
|
async getTransactions(orderId: string): Promise<Transaction[]> {
|
|
const response = await this.makeRequest<{ transactions: Transaction[] }>({
|
|
method: 'GET',
|
|
url: `/commerce/orders/${orderId}/transactions`,
|
|
});
|
|
return response.transactions;
|
|
}
|
|
|
|
async createRefund(orderId: string, refund: CreateRefundRequest): Promise<Transaction> {
|
|
return this.makeRequest<Transaction>({
|
|
method: 'POST',
|
|
url: `/commerce/orders/${orderId}/refunds`,
|
|
data: refund,
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Profiles API (Customers, Subscribers, Donors)
|
|
// ============================================================================
|
|
|
|
async getProfiles(params?: ProfilesQueryParams): Promise<GetProfilesResponse> {
|
|
return this.makeRequest<GetProfilesResponse>({
|
|
method: 'GET',
|
|
url: '/profiles',
|
|
params,
|
|
});
|
|
}
|
|
|
|
async getProfile(profileId: string): Promise<Profile> {
|
|
return this.makeRequest<Profile>({
|
|
method: 'GET',
|
|
url: `/profiles/${profileId}`,
|
|
});
|
|
}
|
|
|
|
async getCustomers(params?: ProfilesQueryParams): Promise<GetProfilesResponse> {
|
|
return this.makeRequest<GetProfilesResponse>({
|
|
method: 'GET',
|
|
url: '/profiles',
|
|
params: { ...params, type: 'CUSTOMER' },
|
|
});
|
|
}
|
|
|
|
async getSubscribers(params?: ProfilesQueryParams): Promise<GetProfilesResponse> {
|
|
return this.makeRequest<GetProfilesResponse>({
|
|
method: 'GET',
|
|
url: '/profiles',
|
|
params: { ...params, type: 'SUBSCRIBER' },
|
|
});
|
|
}
|
|
|
|
async getDonors(params?: ProfilesQueryParams): Promise<GetProfilesResponse> {
|
|
return this.makeRequest<GetProfilesResponse>({
|
|
method: 'GET',
|
|
url: '/profiles',
|
|
params: { ...params, type: 'DONOR' },
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Webhooks API
|
|
// ============================================================================
|
|
|
|
async getWebhooks(): Promise<WebhookSubscription[]> {
|
|
const response = await this.makeRequest<GetWebhooksResponse>({
|
|
method: 'GET',
|
|
url: '/webhook_subscriptions',
|
|
});
|
|
return response.webhooks;
|
|
}
|
|
|
|
async getWebhook(webhookId: string): Promise<WebhookSubscription> {
|
|
return this.makeRequest<WebhookSubscription>({
|
|
method: 'GET',
|
|
url: `/webhook_subscriptions/${webhookId}`,
|
|
});
|
|
}
|
|
|
|
async createWebhook(webhook: CreateWebhookRequest): Promise<WebhookSubscription> {
|
|
return this.makeRequest<WebhookSubscription>({
|
|
method: 'POST',
|
|
url: '/webhook_subscriptions',
|
|
data: webhook,
|
|
});
|
|
}
|
|
|
|
async updateWebhook(
|
|
webhookId: string,
|
|
updates: UpdateWebhookRequest
|
|
): Promise<WebhookSubscription> {
|
|
return this.makeRequest<WebhookSubscription>({
|
|
method: 'PUT',
|
|
url: `/webhook_subscriptions/${webhookId}`,
|
|
data: updates,
|
|
});
|
|
}
|
|
|
|
async deleteWebhook(webhookId: string): Promise<void> {
|
|
await this.makeRequest({
|
|
method: 'DELETE',
|
|
url: `/webhook_subscriptions/${webhookId}`,
|
|
});
|
|
}
|
|
|
|
async sendWebhookTest(notification: WebhookTestNotification): Promise<void> {
|
|
await this.makeRequest({
|
|
method: 'POST',
|
|
url: `/webhook_subscriptions/${notification.webhookId}/test`,
|
|
data: { topic: notification.topic },
|
|
});
|
|
}
|
|
|
|
async rotateWebhookSecret(webhookId: string): Promise<WebhookSubscription> {
|
|
return this.makeRequest<WebhookSubscription>({
|
|
method: 'POST',
|
|
url: `/webhook_subscriptions/${webhookId}/rotate_secret`,
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Website & Pages API
|
|
// ============================================================================
|
|
|
|
async getWebsite(): Promise<Website> {
|
|
return this.makeRequest<Website>({
|
|
method: 'GET',
|
|
url: '/website',
|
|
});
|
|
}
|
|
|
|
async getCollections(): Promise<Collection[]> {
|
|
const response = await this.makeRequest<{ collections: Collection[] }>({
|
|
method: 'GET',
|
|
url: '/website/collections',
|
|
});
|
|
return response.collections;
|
|
}
|
|
|
|
async getCollection(collectionId: string): Promise<Collection> {
|
|
return this.makeRequest<Collection>({
|
|
method: 'GET',
|
|
url: `/website/collections/${collectionId}`,
|
|
});
|
|
}
|
|
|
|
async getPages(collectionId?: string): Promise<Page[]> {
|
|
const url = collectionId
|
|
? `/website/collections/${collectionId}/pages`
|
|
: '/website/pages';
|
|
const response = await this.makeRequest<{ pages: Page[] }>({
|
|
method: 'GET',
|
|
url,
|
|
});
|
|
return response.pages;
|
|
}
|
|
|
|
async getPage(pageId: string): Promise<Page> {
|
|
return this.makeRequest<Page>({
|
|
method: 'GET',
|
|
url: `/website/pages/${pageId}`,
|
|
});
|
|
}
|
|
|
|
async createPage(page: CreatePageRequest): Promise<Page> {
|
|
return this.makeRequest<Page>({
|
|
method: 'POST',
|
|
url: `/website/collections/${page.collectionId}/pages`,
|
|
data: page,
|
|
});
|
|
}
|
|
|
|
async updatePage(pageId: string, updates: UpdatePageRequest): Promise<Page> {
|
|
return this.makeRequest<Page>({
|
|
method: 'PUT',
|
|
url: `/website/pages/${pageId}`,
|
|
data: updates,
|
|
});
|
|
}
|
|
|
|
async deletePage(pageId: string): Promise<void> {
|
|
await this.makeRequest({
|
|
method: 'DELETE',
|
|
url: `/website/pages/${pageId}`,
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Forms API
|
|
// ============================================================================
|
|
|
|
async getForms(): Promise<Form[]> {
|
|
const response = await this.makeRequest<{ forms: Form[] }>({
|
|
method: 'GET',
|
|
url: '/forms',
|
|
});
|
|
return response.forms;
|
|
}
|
|
|
|
async getForm(formId: string): Promise<Form> {
|
|
return this.makeRequest<Form>({
|
|
method: 'GET',
|
|
url: `/forms/${formId}`,
|
|
});
|
|
}
|
|
|
|
async getFormSubmissions(
|
|
formId?: string,
|
|
params?: FormSubmissionsQueryParams
|
|
): Promise<GetFormSubmissionsResponse> {
|
|
const url = formId ? `/forms/${formId}/submissions` : '/form_submissions';
|
|
return this.makeRequest<GetFormSubmissionsResponse>({
|
|
method: 'GET',
|
|
url,
|
|
params,
|
|
});
|
|
}
|
|
|
|
async getFormSubmission(submissionId: string): Promise<FormSubmission> {
|
|
return this.makeRequest<FormSubmission>({
|
|
method: 'GET',
|
|
url: `/form_submissions/${submissionId}`,
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Blog API
|
|
// ============================================================================
|
|
|
|
async getBlogs(): Promise<BlogCollection[]> {
|
|
const response = await this.makeRequest<{ blogs: BlogCollection[] }>({
|
|
method: 'GET',
|
|
url: '/blogs',
|
|
});
|
|
return response.blogs;
|
|
}
|
|
|
|
async getBlog(blogId: string): Promise<BlogCollection> {
|
|
return this.makeRequest<BlogCollection>({
|
|
method: 'GET',
|
|
url: `/blogs/${blogId}`,
|
|
});
|
|
}
|
|
|
|
async getBlogPosts(blogId: string, params?: PaginationParams): Promise<GetBlogPostsResponse> {
|
|
return this.makeRequest<GetBlogPostsResponse>({
|
|
method: 'GET',
|
|
url: `/blogs/${blogId}/posts`,
|
|
params,
|
|
});
|
|
}
|
|
|
|
async getBlogPost(blogId: string, postId: string): Promise<BlogPost> {
|
|
return this.makeRequest<BlogPost>({
|
|
method: 'GET',
|
|
url: `/blogs/${blogId}/posts/${postId}`,
|
|
});
|
|
}
|
|
|
|
async createBlogPost(blogId: string, post: CreateBlogPostRequest): Promise<BlogPost> {
|
|
return this.makeRequest<BlogPost>({
|
|
method: 'POST',
|
|
url: `/blogs/${blogId}/posts`,
|
|
data: post,
|
|
});
|
|
}
|
|
|
|
async updateBlogPost(
|
|
blogId: string,
|
|
postId: string,
|
|
updates: UpdateBlogPostRequest
|
|
): Promise<BlogPost> {
|
|
return this.makeRequest<BlogPost>({
|
|
method: 'PUT',
|
|
url: `/blogs/${blogId}/posts/${postId}`,
|
|
data: updates,
|
|
});
|
|
}
|
|
|
|
async deleteBlogPost(blogId: string, postId: string): Promise<void> {
|
|
await this.makeRequest({
|
|
method: 'DELETE',
|
|
url: `/blogs/${blogId}/posts/${postId}`,
|
|
});
|
|
}
|
|
|
|
async publishBlogPost(blogId: string, postId: string): Promise<BlogPost> {
|
|
return this.makeRequest<BlogPost>({
|
|
method: 'POST',
|
|
url: `/blogs/${blogId}/posts/${postId}/publish`,
|
|
});
|
|
}
|
|
|
|
async unpublishBlogPost(blogId: string, postId: string): Promise<BlogPost> {
|
|
return this.makeRequest<BlogPost>({
|
|
method: 'POST',
|
|
url: `/blogs/${blogId}/posts/${postId}/unpublish`,
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Analytics API (Custom/Extended)
|
|
// ============================================================================
|
|
|
|
async getRevenueMetrics(params: AnalyticsParams): Promise<RevenueMetrics> {
|
|
// This would need to be built from orders data
|
|
const orders = await this.getOrders({
|
|
modifiedAfter: params.startDate,
|
|
modifiedBefore: params.endDate,
|
|
});
|
|
|
|
const totalRevenue = orders.result.reduce(
|
|
(sum, order) => sum + parseFloat(order.grandTotal.value),
|
|
0
|
|
);
|
|
|
|
return {
|
|
totalRevenue: {
|
|
value: totalRevenue.toFixed(2),
|
|
currency: orders.result[0]?.grandTotal.currency || 'USD',
|
|
},
|
|
orderCount: orders.result.length,
|
|
averageOrderValue: {
|
|
value: orders.result.length > 0 ? (totalRevenue / orders.result.length).toFixed(2) : '0',
|
|
currency: orders.result[0]?.grandTotal.currency || 'USD',
|
|
},
|
|
period: {
|
|
start: params.startDate,
|
|
end: params.endDate,
|
|
},
|
|
};
|
|
}
|
|
|
|
async getTopProducts(params: AnalyticsParams, limit: number = 10): Promise<ProductPerformance[]> {
|
|
// Build from orders data
|
|
const orders = await this.getOrders({
|
|
modifiedAfter: params.startDate,
|
|
modifiedBefore: params.endDate,
|
|
});
|
|
|
|
const productStats = new Map<string, { name: string; units: number; revenue: number; currency: string }>();
|
|
|
|
orders.result.forEach(order => {
|
|
order.lineItems.forEach(item => {
|
|
const existing = productStats.get(item.productId) || {
|
|
name: item.productName,
|
|
units: 0,
|
|
revenue: 0,
|
|
currency: item.lineItemPricePaid.currency,
|
|
};
|
|
existing.units += item.quantity;
|
|
existing.revenue += parseFloat(item.lineItemPricePaid.value);
|
|
productStats.set(item.productId, existing);
|
|
});
|
|
});
|
|
|
|
return Array.from(productStats.entries())
|
|
.map(([productId, stats]) => ({
|
|
productId,
|
|
productName: stats.name,
|
|
unitsSold: stats.units,
|
|
revenue: {
|
|
value: stats.revenue.toFixed(2),
|
|
currency: stats.currency,
|
|
},
|
|
}))
|
|
.sort((a, b) => parseFloat(b.revenue.value) - parseFloat(a.revenue.value))
|
|
.slice(0, limit);
|
|
}
|
|
}
|
|
|
|
export class SquarespaceAPIError extends Error {
|
|
constructor(
|
|
public status: number,
|
|
public type: string,
|
|
message: string,
|
|
public errors?: Array<{ field?: string; message: string }>
|
|
) {
|
|
super(message);
|
|
this.name = 'SquarespaceAPIError';
|
|
}
|
|
}
|