UNPKG

pricing4ts

Version:

![NPM Version](https://img.shields.io/npm/v/pricing4ts) Pricing4TS is a TypeScript-based toolkit designed to enhance the server-side functionality of a pricing-driven SaaS by enabling the seamless integration of pricing plans into the application logic. T

1,017 lines (847 loc) 33.5 kB
import type { AddOn } from '../models/pricing2yaml/addon'; import type { AutomationType, Feature, IntegrationType, PaymentType } from '../../types'; import type { Plan } from '../models/pricing2yaml/plan'; import type { Pricing } from '../models/pricing2yaml/pricing'; import type { FeatureType, RenderMode, UsageLimitType, ValueType, } from '../models/pricing2yaml/types'; import type { ContainerUsageLimits, UsageLimit } from '../models/pricing2yaml/usage-limit'; import * as cc from 'currency-codes'; import { isAutomationType, isIntegrationType } from '../models/pricing2yaml/feature'; const VERSION_REGEXP = /^\d+\.\d+$/; const unlimitedValue = 100000000; export function validateName(name: string | null, item: string): string { if (name === null || name === undefined) { throw new Error(`The ${item} must have a name`); } if (typeof name !== 'string') { throw new TypeError(`The ${item} name must be a string`); } const trimmedName = name.trim(); if (trimmedName.length === 0) { throw new Error(`The ${item} name must not be empty`); } if (trimmedName.length < 3) { throw new Error(`The ${item} name must have at least 3 characters`); } if (trimmedName.length > 255) { throw new Error(`The ${item} name must have at most 255 characters`); } return trimmedName; } export function validateSyntaxVersion(version: string): string { if (version === null || version === undefined) { throw new Error( `The syntaxVersion field of the pricing must not be null or undefined. Please ensure that the syntaxVersion field is present and correctly formatted` ); } if (typeof version !== 'string' || !VERSION_REGEXP.test(version)) { throw new TypeError( `The syntaxVersion field of the pricing does not follow the required structure: X.Y (being X and Y numbers). Please ensure it is a string in the format X.Y` ); } return version; } export function validateVersion(version: string | undefined, createdAt: Date): string { version ??= `${createdAt.getFullYear()}-${createdAt.getMonth() + 1}-${createdAt.getDate()}`; if (typeof version !== 'string') { throw new TypeError( `The version field of the pricing must be a string. Please ensure that the version field is present and correctly formatted` ); } return version; } export function validateCreatedAt(createdAt: string | Date | null): Date { if (createdAt === null || createdAt === undefined) { throw new Error( `The createdAt field must not be null or undefined. Please ensure that the createdAt field is present and correctly formatted (as Date or string)` ); } if (typeof createdAt === 'string'){ if (/^\d{4}-\d{2}-\d{2}$/.test(createdAt)) { createdAt = new Date(createdAt); } else { throw new TypeError( `The createdAt field must be a string in the format yyyy-mm-dd or a valid Date object` ); } } if (!(createdAt instanceof Date) || isNaN(createdAt.getTime())) { throw new TypeError( `The createdAt field must be a valid Date object or a string in a recognized date format` ); } const now = new Date(); if (createdAt > now) { throw new Error(`The createdAt field must not be a future date`); } return createdAt; } export function validateCurrency(currency: string | null): string { if (currency === null || currency === undefined) { throw new Error( `The currency field of the pricing must not be null or undefined. Please ensure that the currency field is present and correctly formatted` ); } if (typeof currency !== 'string') { throw new TypeError(`The currency field of the pricing must be a string`); } const trimmedCurrency = currency.trim(); if (trimmedCurrency.length === 0) { throw new Error(`The currency field of the pricing must not be empty`); } const currencyCode = cc.code(trimmedCurrency); if (!currencyCode) { throw new Error(`The currency code ${trimmedCurrency} is not a valid ISO 4217 currency code`); } return currency; } export function validateDescription(description: string | null | undefined): string | undefined { description ??= undefined; return description; } export function validateValueType(valueType: string | null): ValueType { if (valueType === null || valueType === undefined) { throw new Error( `The valueType field of a feature must not be null or undefined. Please ensure that the valueType field is present and it's value correspond to either BOOLEAN, NUMERIC or TEXT` ); } if (typeof valueType !== 'string') { throw new TypeError( `The valueType field of a feature must be a string, and its value must be either BOOLEAN, NUMERIC or TEXT. Received: ${valueType} ` ); } valueType = valueType.trim().toUpperCase(); if (!['NUMERIC', 'BOOLEAN', 'TEXT'].includes(valueType)) { throw new Error( `The valueType field of a feature must be one of NUMERIC, BOOLEAN, or TEXT. Received: ${valueType}` ); } return valueType as ValueType; } export function validateDefaultValue( elem: Feature | UsageLimit, item: string ): number | boolean | string | PaymentType[] { if (elem.defaultValue === null || elem.defaultValue === undefined) { throw new Error( `The defaultValue field of a ${item} must not be null or undefined. Please ensure that the defaultValue field is present and its defaultValue type correspond to the declared valueType` ); } switch (elem.valueType) { case 'NUMERIC': if (typeof elem.defaultValue !== 'number') { throw new TypeError( `The defaultValue field of a ${item} must be a number when its valueType is NUMERIC. Received: ${elem.defaultValue}` ); } if (elem.defaultValue > unlimitedValue){ elem.defaultValue = unlimitedValue; } break; case 'BOOLEAN': if (typeof elem.defaultValue !== 'boolean') { throw new TypeError( `The defaultValue field of a ${item} must be a boolean when its valueType is BOOLEAN. Received: ${elem.defaultValue}` ); } break; case 'TEXT': if (elem.type === 'PAYMENT') { if (!(elem.defaultValue instanceof Array)) { throw new TypeError(`Payment method value must be an array of payment methods`); } for (const paymentMethod of elem.defaultValue) { if ( !['CARD', 'GATEWAY', 'INVOICE', 'ACH', 'WIRE_TRANSFER', 'OTHER'].includes(paymentMethod) ) { throw new Error( `Invalid payment method: ${paymentMethod}. Please provide one of the following: CARD, GATEWAY, INVOICE, ACH, WIRE_TRANSFER, OTHER` ); } } break; } if (typeof elem.defaultValue !== 'string') { throw new Error( `The defaultValue field of a ${item} must be a string when its valueType is TEXT. Received: ${elem.defaultValue}` ); } break; default: throw new Error( `The valueType field of a ${item} must be either BOOLEAN, NUMERIC or TEXT. Received: ${elem.valueType}` ); } return elem.defaultValue; } export function validateExpression( expression: string | null | undefined, item: string ): string | undefined { expression ??= undefined; if (typeof expression === 'string') { if (expression.trim().length === 0) { throw new Error( `The ${item} field of a feature must not be empty. If you don't want to declare an expression for this feature, either use null or undefined` ); } } return expression; } export function validateValue( elem: Feature | UsageLimit, item: string, valueType: ValueType | undefined = undefined ): number | boolean | string | PaymentType[] | undefined { elem.value ??= undefined; const valueTypeToValidate = valueType || elem.valueType; switch (valueTypeToValidate) { case 'NUMERIC': if (typeof elem.value !== 'number' && elem.value !== undefined) { throw new TypeError( `The value field of a ${item} must be a number when its valueType is NUMERIC. Received: ${elem.value}` ); } if (elem.value! > unlimitedValue){ elem.value = unlimitedValue; } break; case 'BOOLEAN': if (typeof elem.value !== 'boolean' && elem.value !== undefined) { throw new TypeError( `The value field of a ${item} must be a boolean when its valueType is BOOLEAN. Received: ${elem.value}` ); } break; case 'TEXT': if (elem.type === 'PAYMENT' && elem.value !== undefined) { if (!(elem.value instanceof Array)) { throw new TypeError(`Payment method value must be an array of payment methods`); } for (const paymentMethod of elem.value) { if ( !['CARD', 'GATEWAY', 'INVOICE', 'ACH', 'WIRE_TRANSFER', 'OTHER'].includes(paymentMethod) ) { throw new Error( `Invalid payment method: ${paymentMethod}. Please provide one of the following: CARD, GATEWAY, INVOICE, ACH, WIRE_TRANSFER, OTHER` ); } } break; } if (typeof elem.value !== 'string' && elem.value !== undefined) { throw new TypeError( `The value field of a ${item} must be a string when its valueType is TEXT. Received: ${elem.value}` ); } break; default: throw new TypeError( `The valueType field of a ${item} must be either BOOLEAN, NUMERIC or TEXT. Received: ${elem.valueType}` ); } return elem.value; } export function validateFeatureType(type: string | null | undefined): FeatureType { if (type === null || type === undefined) { throw new Error( `The type field of a feature must not be null or undefined. Please ensure that the type field is present and it's value correspond to either INFORMATION, INTEGRATION, DOMAIN, AUTOMATION, MANAGEMENT, GUARANTEE, SUPPORT or PAYMENT` ); } if (typeof type !== 'string') { throw new TypeError( `The type field of a feature must be a string, and its value must be either INFORMATION, INTEGRATION, DOMAIN, AUTOMATION, MANAGEMENT, GUARANTEE, SUPPORT or PAYMENT. Received: ${type} ` ); } type = type.trim().toUpperCase(); if ( ![ 'INFORMATION', 'INTEGRATION', 'DOMAIN', 'AUTOMATION', 'MANAGEMENT', 'GUARANTEE', 'SUPPORT', 'PAYMENT', ].includes(type) ) { throw new Error( `The type field of a feature must be one of INFORMATION, INTEGRATION, DOMAIN, AUTOMATION, MANAGEMENT, GUARANTEE, SUPPORT or PAYMENT. Received: ${type}` ); } return type as FeatureType; } export function validateFeatureIntegrationType( integrationType: IntegrationType | null | undefined, featureType: FeatureType ) { if (integrationType === null || integrationType === undefined) { integrationType = undefined; } if (integrationType && typeof integrationType !== 'string') { throw new TypeError( `The integrationType field of a feature must be an IntegrationType ('API' | 'EXTENSION' | 'IDENTITY_PROVIDER' | 'WEB_SAAS' | 'MARKETPLACE' | 'EXTERNAL_DEVICE'). Received: ${integrationType}` ); }else if (featureType === 'INTEGRATION' && !integrationType) { throw new Error( `The integrationType field of a feature of type INTEGRATION must not be null or undefined. Please ensure that the integrationType field is present and it's value correspond to either API, EXTENSION, IDENTITY_PROVIDER, WEB_SAAS, MARKETPLACE or EXTERNAL_DEVICE` ); }else if (integrationType && typeof integrationType === "string" && !isIntegrationType(integrationType)){ throw new Error( `The integrationType field of a feature must be one of API, EXTENSION, IDENTITY_PROVIDER, WEB_SAAS, MARKETPLACE or EXTERNAL_DEVICE. Received: ${integrationType}` ); } return integrationType; } export function validateFeatureAutomationType(automationType: AutomationType | null | undefined, featureType: FeatureType) { if (automationType === null || automationType === undefined) { automationType = undefined; } if (automationType && typeof automationType !== 'string') { throw new TypeError( `The automationType field of a feature must be an AutomationType ('BOT' | 'FILTERING' | 'TRACKING' | 'TASK_AUTOMATION'). Received: ${automationType}` ); } else if (featureType === 'AUTOMATION' && !automationType) { throw new Error( `The automationType field of a feature of type AUTOMATION must not be null or undefined. Please ensure that the automationType field is present and its value corresponds to either BOT, FILTERING, TRACKING, or TASK_AUTOMATION` ); } else if (automationType && typeof automationType === "string" && !isAutomationType(automationType)) { throw new Error( `The automationType field of a feature must be one of BOT, FILTERING, TRACKING, or TASK_AUTOMATION. Received: ${automationType}` ); } return automationType; } export function validateUnit(unit: string | null | undefined): string { if (unit === null || unit === undefined) { unit = ''; } if (typeof unit !== 'string') { throw new TypeError(`The unit field of a usage limit must be a string. Received: ${unit} `); } return unit; } export function validateUsageLimitType(type: string | null | undefined): UsageLimitType { if (type === null || type === undefined) { throw new Error( `The type field of a usage limit must not be null or undefined. Please ensure that the type field is present and it's value correspond to either RENEWABLE or NON_RENEWABLE` ); } if (typeof type !== 'string') { throw new TypeError( `The type field of a usage limit must be a string, and its value must be either RENEWABLE or NON_RENEWABLE. Received: ${type} ` ); } type = type.trim().toUpperCase(); if (!['RENEWABLE', 'NON_RENEWABLE'].includes(type)) { throw new Error( `The type field of a usage limit must be one of RENEWABLE or NON_RENEWABLE. Received: ${type}` ); } return type as UsageLimitType; } export function validateLinkedFeatures( linkedFeatures: string[] | undefined | null, pricing: Pricing ): string[] | undefined { linkedFeatures ??= undefined; // Check if linked features is an array if (Array.isArray(linkedFeatures)) { const pricingFeatures = Object.values(pricing.features).map(f => f.name); for (const featureName of linkedFeatures) { if (!pricingFeatures.includes(featureName)) { throw new Error( `The feature ${featureName}, declared as a linked feature for an usage limit, is not defined in the global features` ); } } } return linkedFeatures; } export function validateRenderMode(renderMode: string | undefined | null): RenderMode { if (renderMode === null || renderMode === undefined) { renderMode ??= 'AUTO'; } renderMode = renderMode.toUpperCase(); if (!['AUTO', 'ENABLED', 'DISABLED'].includes(renderMode)) { throw new Error( `The render field of a feature or usage limit must be one of AUTO, ENABLED or DISABLED. Received: ${renderMode}` ); } return renderMode as RenderMode; } export function validatePlanFeatures( plan: Plan, planFeatures: Record<string, Feature> ): Record<string, Feature> { const featuresModifiedByPlan = plan.features; plan.features = planFeatures; for (const planFeature of Object.values(featuresModifiedByPlan)) { try { if (!Object.values(planFeatures).some(f => f.name === planFeature.name)) { throw new Error(`Feature ${planFeature.name} is not defined in the global features.`); } const featureWithDefaultValue = Object.values(plan.features).find( f => f.name === planFeature.name ) as Feature; featureWithDefaultValue.value = planFeature.value; featureWithDefaultValue.value = validateValue(featureWithDefaultValue, 'feature'); } catch (err) { throw new Error( `Error while parsing the feature ${planFeature.name} of the plan ${plan.name}. Error: ${ (err as Error).message }` ); } } return plan.features; } export function validatePlanUsageLimits( plan: Plan, planUsageLimits: ContainerUsageLimits ): ContainerUsageLimits { const usageLimitsModifiedByPlan = plan.usageLimits!; plan.usageLimits = planUsageLimits; for (const planUsageLimit of Object.values(usageLimitsModifiedByPlan)) { try { if (!Object.values(planUsageLimits).some(l => l.name === planUsageLimit.name)) { throw new Error( `Usage limit ${planUsageLimit.name} is not defined in the global usage limits.` ); } const globalUsageLimit = Object.values(planUsageLimits).find( l => l.name === planUsageLimit.name ) as UsageLimit; globalUsageLimit.value = planUsageLimit.value; globalUsageLimit.value = validateValue(globalUsageLimit, 'usage limit') as string | number | boolean | undefined; } catch (err) { throw new Error( `Error while parsing the usage limit ${planUsageLimit.name} of the plan ${ plan.name }. Error: ${(err as Error).message}` ); } } return plan.usageLimits; } export function validatePrice( price: number | string | undefined | null, variables: { [key: string]: any } = {} ): number | string { if (price === null || price === undefined) { throw new Error( `The price field must not be null or undefined. Please ensure that the price field is present and it's a number` ); } if (typeof price !== 'string' && typeof price !== 'number') { throw new TypeError( `The price field must be a number or a string (which can contain a formula). Received: ${price}` ); } if (typeof price === 'number' && price < 0) { throw new Error(`The price field must be a positive number. Received: ${price} `); } if (typeof price === 'string') { if (price.includes('#')) { for (const [variable, value] of Object.entries(variables)) { let replacement: string; if (typeof value === 'string') { const escaped = (value as string).replace(/'/g, "\\'"); replacement = `'${escaped}'`; } else if (value === null) { replacement = 'null'; } else if (typeof value === 'object') { // Use JSON.stringify and wrap in parentheses so object/array literals // evaluate correctly inside eval (e.g. ({"test":"a"}).test ) replacement = `(${JSON.stringify(value)})`; } else { replacement = String(value); } price = price.replace(new RegExp(`#${variable}`, 'g'), replacement); } try { // eslint-disable-next-line no-eval const evaluatedPrice = eval(price); if (typeof evaluatedPrice !== 'number' || isNaN(evaluatedPrice)) { throw new Error( `The evaluated price must result in a valid number. Current result after evaluation: ${evaluatedPrice}` ); } price = evaluatedPrice; } catch (err) { throw new Error(`Error evaluating the price formula: ${(err as Error).message}`); } } else if (price.match(/^[0-9]+(\.[0-9]+)?$/)) { price = parseFloat(price); } } return price; } export function validateAddonFeatures( addon: AddOn, addOnFeatures: Record<string, Feature> ): Record<string, Feature> { for (const addOnFeature of Object.values(addon.features!)) { try { if (!Object.values(addOnFeatures).some(f => f.name === addOnFeature.name)) { throw new Error(`Feature ${addOnFeature.name} is not defined in the global features.`); } addon.features![addOnFeature.name].value = addOnFeature.value; addon.features![addOnFeature.name].value = validateValue( addon.features![addOnFeature.name], 'feature', addOnFeatures[addOnFeature.name].valueType ); } catch (err) { throw new Error( `Error while parsing the feature ${addOnFeature.name} of the plan ${addon.name}. Error: ${ (err as Error).message }` ); } } return addon.features as Record<string, Feature>; } export function validateAddonUsageLimits( addon: AddOn, addonUsageLimits: ContainerUsageLimits ): ContainerUsageLimits { for (const addonUsageLimit of Object.values(addon.usageLimits!)) { try { if (!Object.values(addonUsageLimits).some(l => l.name === addonUsageLimit.name)) { throw new Error( `Usage limit ${addonUsageLimit.name} is not defined in the global usage limits.` ); } if (!('value' in addonUsageLimit)) { throw new Error( "When declaring a new value for an usage limit or a usage limit extension within an add-on, it must be provided through the 'value' field" ); } addon.usageLimits![addonUsageLimit.name].value = addonUsageLimit.value; addon.usageLimits![addonUsageLimit.name].value = validateValue( addon.usageLimits![addonUsageLimit.name], 'usage limit', addonUsageLimits[addonUsageLimit.name].valueType ) as string | number | boolean | undefined; } catch (err) { throw new Error( `Error while parsing the usage limit ${addonUsageLimit.name} of the add-on ${ addon.name }. Error: ${(err as Error).message}` ); } } return addon.usageLimits as ContainerUsageLimits; } export function validateAddonUsageLimitsExtensions( addon: AddOn, addonUsageLimits: ContainerUsageLimits ): ContainerUsageLimits { for (const addonUsageLimitExtension of Object.values(addon.usageLimitsExtensions!)) { try { if (!Object.values(addonUsageLimits).some(l => l.name === addonUsageLimitExtension.name)) { throw new Error( `Usage limit ${addonUsageLimitExtension.name} is not defined in the global usage limits.` ); } if (!('value' in addonUsageLimitExtension)) { throw new Error( "When declaring a new value for an usage limit or a usage limit extension within an add-on, it must be provided through the 'value' field" ); } addon.usageLimitsExtensions![addonUsageLimitExtension.name].value = addonUsageLimitExtension.value; addon.usageLimitsExtensions![addonUsageLimitExtension.name].value = validateValue( addon.usageLimitsExtensions![addonUsageLimitExtension.name], 'usage limit', addonUsageLimits[addonUsageLimitExtension.name].valueType ) as string | number | boolean | undefined; } catch (err) { throw new Error( `Error while parsing the usage limit ${addonUsageLimitExtension.name} of the add-on ${ addon.name }. Error: ${(err as Error).message}` ); } } return addon.usageLimitsExtensions as ContainerUsageLimits; } export function validateAvailableFor( availableFor: string[] | undefined | null, pricing: Pricing ): string[] { const planNames = pricing.plans ? Object.values(pricing.plans).map(p => p.name) : []; availableFor ??= planNames as string[]; if (!Array.isArray(availableFor)) { throw new TypeError( `The availableFor field must be an array of the plan names for which the addon can be contracted. Received: ${availableFor}` ); } for (const planName of availableFor) { if (!planNames.includes(planName)) { throw new Error(`The plan ${planName} is not defined in the pricing.`); } } return availableFor; } export function validateDependsOnOrExcludes( fieldValue: string[] | undefined | null, pricing: Pricing, fieldType: 'dependsOn' | 'excludes' ): string[] { const addonNames = pricing.addOns ? Object.values(pricing.addOns).map(a => a.name) : []; fieldValue ??= []; if (!Array.isArray(fieldValue)) { throw new TypeError( `The ${fieldType} field must be an array of the addons required to contract the addon. Received: ${fieldValue}` ); } return fieldValue; } export function postValidateDependsOnOrExclude( fieldValue: string[] | undefined, pricing: Pricing ): void { const addonNames = pricing.addOns ? Object.values(pricing.addOns).map(a => a.name) : []; if (!fieldValue) return; for (const addonName of fieldValue) { if (!addonNames.includes(addonName)) { throw new Error(`The addon ${addonName} is not defined in the pricing.`); } } } export function validateTags(tags: string[] | undefined): string[] { tags ??= []; if (!Array.isArray(tags) || tags.some(tag => typeof tag !== 'string')) { throw new TypeError(`The tags field must be an array of strings.`); } return tags; } export function validatePlan(plan: Plan) { if (typeof plan === 'object' && 'features' in plan) { return; } else { throw new TypeError(`The plan must be an object of type Plan`); } } export function validatePlans(plans: object) { if (!(typeof plans === 'object')) { throw new TypeError(`The plans field must be a map of Plan objects`); } } export function validateFeatures(features: object) { if (!(typeof features === 'object') || features === null || features === undefined) { throw new TypeError(`The features field must be a map of Feature objects`); } } export function validateFeature(feature: Feature) { if ( typeof feature === 'object' && 'type' in feature && 'valueType' in feature && 'defaultValue' in feature ) { return; } else { throw new TypeError(`The feature must be an object of type Feature`); } } export function validateUsageLimits(usageLimits: object) { if (!(typeof usageLimits === 'object')) { throw new TypeError(`The usageLimits field must be a map of UsageLimit objects or undefined`); } } export function validateUsageLimit(usageLimit: UsageLimit) { if ( typeof usageLimit === 'object' && 'type' in usageLimit && 'valueType' in usageLimit && 'defaultValue' in usageLimit ) { return; } else { throw new TypeError(`The usage limit must be an object of type UsageLimit`); } } export function validateBilling(billing: { [key: string]: number } | undefined) { if (billing === undefined || billing === null) { billing ??= { monthly: 1, }; } if (!(typeof billing === 'object')) { throw new TypeError(`The billing field must be an object of type {[key: string]: number}`); } for (const [key, value] of Object.entries(billing)) { if (typeof value !== 'number') { throw new TypeError(`The billing entry for ${key} must be a number. Received: ${value}`); } if (value <= 0 || value > 1) { throw new Error( `The billing entry for ${key} must be a value in the range (0,1]. Received: ${value}` ); } } return billing; } export function validateVariables( variables: { [key: string]: any } | undefined ): { [key: string]: any } { variables ??= {}; if (typeof variables !== 'object') { throw new TypeError( `The 'variables' field must be an object of type {[key: string]: any}` ); } // Allow any value types for variables (object, array, string, number, boolean, etc.) return variables; } export function validateUrl(url: string | undefined) { if (url === undefined || url === null) { url ??= undefined; return; } if (typeof url !== 'string') { throw new TypeError(`The url field must be a string. Received: ${url}`); } const urlPattern = /^(https?):\/\/[^\s\/$.?#].[^\s]*$/i; if (!urlPattern.test(url)) { throw new Error( `The url field must be a valid URL with the http or https protocol. Received: ${url}` ); } return url; } export function validatePrivate(isPrivate: boolean | undefined) { if (isPrivate === undefined || isPrivate === null) { isPrivate ??= false; } if (typeof isPrivate !== 'boolean') { throw new TypeError(`The private field must be a boolean. Received: ${isPrivate}`); } return isPrivate; } export function validatePricingUrls( featureType: FeatureType, featureIntegrationType: IntegrationType | undefined, pricingUrls: string[] | undefined ) { pricingUrls ??= undefined; if (pricingUrls !== undefined) { if (!Array.isArray(pricingUrls) || pricingUrls.some(url => typeof url !== 'string')) { throw new TypeError(`The pricingUrls field must be an array of strings.`); } for (const url of pricingUrls) { const urlPattern = /^(https?):\/\/[^^\s\/$.?#].[^\s]*$/i; if (!urlPattern.test(url)) { throw new Error(`The pricingUrls field must contain only valid URLs. Invalid URL: ${url}`); } } if (featureType !== 'INTEGRATION' || featureIntegrationType !== 'WEB_SAAS') { console.log( "[WARNING] The pricingUrls field is only valid for features of 'type' INTEGRATION and 'integrationType' WEB_SAAS. The field will be ignored." ); pricingUrls = undefined; } } return pricingUrls; } export function validateDocUrl(featureType: FeatureType, docUrl: string | undefined) { docUrl ??= undefined; if (docUrl !== undefined) { if (typeof docUrl !== 'string') { throw new TypeError(`The docUrl field must be a string.`); } const urlPattern = /^(https?):\/\/[^^\s\/$.?#].[^\s]*$/i; if (!urlPattern.test(docUrl)) { throw new Error(`The docUrl field must be a valid URL with the http or https protocol. Received: ${docUrl}`); } if (featureType !== 'GUARANTEE') { console.log( "[WARNING] The docUrl field is only valid for features of 'type' GUARANTEE. The field will be ignored." ); docUrl = undefined; } } return docUrl; } export function validatePeriodUnit(periodUnit: string | undefined): "SEC" | "MIN" | "HOUR" | "DAY" | "MONTH" | "YEAR" { periodUnit ??= 'MONTH'; if (typeof periodUnit !== 'string') { throw new TypeError(`The periodUnit field must be one of the following string values: "SEC" | "MIN" | "HOUR" | "DAY" | "MONTH" | "YEAR". Received: ${periodUnit}`); } periodUnit = periodUnit.toUpperCase(); if (!["SEC", "MIN", "HOUR", "DAY", "MONTH", "YEAR"].includes(periodUnit)) { throw new Error( `The periodUnit field must be one of "SEC" | "MIN" | "HOUR" | "DAY" | "MONTH" | "YEAR". Received: ${periodUnit}` ); } return periodUnit as "SEC" | "MIN" | "HOUR" | "DAY" | "MONTH" | "YEAR"; } export function validatePeriodValue(periodValue: number | undefined): number { periodValue ??= 1; if (typeof periodValue !== 'number') { throw new TypeError(`The periodValue field must be a number. Received: ${periodValue}`); } if (periodValue <= 0) { throw new Error(`The periodValue field must be a positive number. Received: ${periodValue}`); } return periodValue; } export function validateTrackable(trackable: boolean | undefined): boolean { trackable ??= false; if (typeof trackable === 'string'){ trackable = (trackable as string).toLowerCase() === 'true'; } if (typeof trackable !== 'boolean') { throw new TypeError(`The trackable field must be a boolean. Received: ${trackable}`); } return trackable; } export function validateSubscriptionConstraintMinQuantity( subscriptionConstraintMinQuantity: number | undefined ): number { subscriptionConstraintMinQuantity ??= 1; if (typeof subscriptionConstraintMinQuantity !== 'number') { throw new TypeError(`The subscriptionConstraintMinQuantity field must be a number. Received: ${subscriptionConstraintMinQuantity}`); } if (subscriptionConstraintMinQuantity < 0) { throw new Error(`The subscriptionConstraintMinQuantity field must be a positive number. Received: ${subscriptionConstraintMinQuantity}`); } return subscriptionConstraintMinQuantity; } export function validateSubscriptionConstraintMaxQuantity(subscriptionConstraintMaxQuantity: number | undefined, minQuantity: number): number { subscriptionConstraintMaxQuantity ??= unlimitedValue; if (typeof subscriptionConstraintMaxQuantity !== 'number') { throw new TypeError(`The subscriptionConstraintMaxQuantity field must be a number. Received: ${subscriptionConstraintMaxQuantity}`); } if (subscriptionConstraintMaxQuantity < minQuantity) { throw new Error(`The subscriptionConstraintMaxQuantity field must be greater than or equal to the min quantity. Received: ${subscriptionConstraintMaxQuantity}`); } return subscriptionConstraintMaxQuantity; } export function validateSubscriptionConstraintQuantityStep(subscriptionConstraintQuantityStep: number | undefined, minQuantity: number): number { subscriptionConstraintQuantityStep ??= 1; if (typeof subscriptionConstraintQuantityStep !== 'number') { throw new TypeError(`The subscriptionConstraintQuantityStep field must be a number. Received: ${subscriptionConstraintQuantityStep}`); } if (!(minQuantity % subscriptionConstraintQuantityStep === 0)) { throw new Error(`The subscriptionConstraintQuantityStep field must be a divisor of, at least, the min quantity. Received: ${subscriptionConstraintQuantityStep}`); } return subscriptionConstraintQuantityStep; } export function validateCustom(custom: { [key: string]: any } | undefined): { [key: string]: any } { custom ??= {}; if (typeof custom !== 'object') { throw new TypeError( `The custom field must be an object of type {[key: string]: any}` ); } return custom; }