diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..10f0222 Binary files /dev/null and b/.DS_Store differ diff --git a/package.json b/package.json index 9b1f74e..141dcbd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@spresso-sdk/price_optimization", - "version": "2.2.1", + "version": "2.3.0", "description": "Spresso Price Optimization SDK for Node", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", diff --git a/src/index.ts b/src/index.ts index 63c155f..1419a5f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,7 @@ const AUTH_EXPIRATION_PAD_MS = 30 * 60 * 1000; export interface ILogger { error(message?: any, ...optionalParams: any[]): void; info?(message?: any, ...optionalParams: any[]): void; -} +}; export type PricingRequest = { defaultPrice?: number; @@ -43,17 +43,23 @@ export type SDKOptions = { type AuthResponse = { access_token: string; expires_in: number; -} +}; type UserAgent = { name: string; regexp: RegExp; -} +}; type UserAgentResponse = { name: string; regexp: string; -} +}; + +type OptimizedSkuResponse = { + expiresAt: number; + skus: string[]; + skipInactive: boolean; +}; class SpressoSDK { private authToken: string | null = null; @@ -63,6 +69,9 @@ class SpressoSDK { private readonly clientSecret: string; private readonly logger: ILogger | undefined; private tokenExpiration: number | null = null; + private optimizedSkus: Set; + private skipInactive: boolean | null = null; + private skuExpiration: number | null = null; constructor(options: SDKOptions) { this.axiosInstance = axios.create({ @@ -82,18 +91,24 @@ class SpressoSDK { timeout: options.keepAliveTimeoutMS ?? DEFAULT_KEEPALIVE_TIMEOUT_MS, }); + this.optimizedSkus = new Set(); this.logger = options.logger; this.clientID = options.clientID; this.clientSecret = options.clientSecret; - // Pre-emptively fetch auth token and bot user agents + // Pre-emptively fetch auth token, bot user agents and optimized skus this.authenticate() .then(() => this.getBotUserAgents()) + .then(() => this.getOptimizedSkus()) .catch(() => {}); // intentional no-op } async getPrice(request: PricingRequest, userAgent: string | undefined): Promise { try { + if (this.skipInactiveSkus([request])) { + return this.emptyResponse(request); + } + const response = await this.makeRequest( 'get', request, @@ -116,6 +131,10 @@ class SpressoSDK { async getPrices(requests: PricingRequest[], userAgent: string | undefined): Promise { try { + if (this.skipInactiveSkus(requests)) { + return this.emptyResponses(requests); + } + const response = await this.makeRequest( 'post', undefined, @@ -136,6 +155,24 @@ class SpressoSDK { } } + private skipInactiveSkus(requests: PricingRequest[]): boolean { + // Fetch optimized skus, don't await! + this.getOptimizedSkus().catch(() => {}); + + /* We only skip if ALL skus are non optimized + * Why? The point of this is to minimize the API roundtrip. + * If even one sku IS optimized we won't be able to skip, so no point adding the extra complexity of partial skips + */ + const someOptimized = requests.some(request => this.optimizedSkus.has(request.itemId)); + if (this.skipInactive && !someOptimized) { + this.logInfo('All SKUs are non-optimized, short-circuiting...'); + return true; + } + + this.logInfo('Found optimized SKU, making API request...'); + return false; + } + private async authenticate(): Promise { const now = new Date().getTime(); if (this.authToken != null && this.tokenExpiration != null && now < (this.tokenExpiration - AUTH_EXPIRATION_PAD_MS)) { @@ -184,6 +221,28 @@ class SpressoSDK { }); } + private async getOptimizedSkus(): Promise { + const now = new Date().getTime(); + if (this.skuExpiration != null && now < this.skuExpiration) { + return Promise.resolve(); // Optimized skus has already been fetched and has not expired + } + + this.logInfo('Fetching optimized skus...'); + return this.axiosInstance.request({ + headers: { + 'Authorization': this.authHeader() + }, + method: 'get', + url: '/pim/v1/variants/optimizedSKUs', + }).then(response => { + const optimizedSkuResponse = (response.data as OptimizedSkuResponse); + this.logInfo(`${optimizedSkuResponse.skus.length} optimized skus fetched!`); + this.skuExpiration = optimizedSkuResponse.expiresAt * 1000; + this.optimizedSkus = new Set(optimizedSkuResponse.skus); + this.skipInactive = optimizedSkuResponse.skipInactive; + }); + } + private async makeRequest( method: string, params: any | undefined,