Skip to main content

Function Examples

Beta Feature

RevOps Functions are a limited access beta feature. Please contact your Account Manager if you are interested in joining the beta. The information in this document is a work in progress and is subject to change.

Please provide any questions or feedback to support@revops.io or via your Slack Connect channel if applicable. Additional features and functionality will be added in the future.

We've included a variety of samples of common use cases that you may find helpful in writing your functions. While the examples are separated by function type, the logic within each sample can often be used across any function type.

Is there a use case that you're looking for that you don't see? Let us know at support@revops.io or via your Slack Connect channel if applicable.

Pre-Submit Validation

Term Value Dependency

When the customer is billed by credit card, they must agree to Due Upon Receipt payment terms as their card will be automatically charged.

export const validateDealBeforeSubmission = (data: PreSubmitTestData): PreSubmitResponse => {
const errors: PreSubmitResponse = [];

const paymentMethod = data.deal.terms.find(term => term.name === "Payment Method") as OptionTerm | undefined
const paymentTerms = data.deal.terms.find(term => term.name === "Payment Terms") as OptionTerm | undefined

console.log("Payment Method", paymentMethod?.value)
console.log("Payment Terms", paymentTerms?.value)
if (paymentMethod?.value === "credit-card" && paymentTerms?.value !== "1") {
errors.push({ message: "Credit Card payments must be Due Upon Receipt" })
}

return errors;
}

Product Family Exclusivity

When multiple tiers of a product are sold, all SKUs must be within that product tier. Deals shouldn't sell multiple tiers on a single agreement.

export const validateDealBeforeSubmission = (data: PreSubmitTestData): PreSubmitResponse => {
const errors: PreSubmitResponse = [];

// Only 1 product tier can be sold
const businessSkus = data.deal.lineItems.filter((lineItem) =>
lineItem.productCode.startsWith("business-")
);
const enterpriseSkus = data.deal.lineItems.filter((lineItem) =>
lineItem.productCode.startsWith("enterprise-")
);
if (businessSkus.length && enterpriseSkus.length) {
console.log(
"Found mixed pricing tiers",
"Business SKUs",
businessSkus.map((sku) => sku.lineItemTitle),
"Enterprise SKUs",
enterpriseSkus.map((sku) => sku.lineItemTitle)
);
errors.push({
message: "Enterprise and business SKUs cannot be mixed",
});
}

return errors;
}

Proposal Expiration Date

When the proposal has expired or will expire on the same day, the sales rep can be warned so they can adjust if necessary. This also enforces that all contracts with an expiration date term must expire within 30 days of submission.

const stringToDate = (dateString: string, endOfDay: boolean = false): Date => {
// Value is ISO date string, separate into year, month, day
const timestampParts = dateString.split("-");

// A date doesn't have a specific time associated with it
// Most dates are considered start of day UTC
// Expiration date represents the value at 11:59:59 PM AOE (UTC-12)
const [hour, minute, second] = endOfDay ? [23, 59, 59] : [0, 0, 0]
const date = new Date(
parseInt(timestampParts[0]),
parseInt(timestampParts[1]) - 1,
parseInt(timestampParts[2]),
hour,
minute,
second
);
if (endOfDay) {
// Server time is UTC, expiration time is UTC-12
date.setTime(date.getTime() + 12 * 60 * 60 * 1000);
}
return date
}

const checkExpirationDateTerm = (data: PreSubmitTestData) => {
const expirationDate: DateTerm | undefined = data.deal.terms.find(
(term) => term.builtInId === "expirationDate"
) as DateTerm | undefined;

if (!expirationDate) {
console.log("No expiration date is present");
return null;
}
const timestampDate = stringToDate(expirationDate.startDate, true)
const now = new Date();
console.log("Expiration", timestampDate, "now", now);
if (timestampDate < now) {
return {
message:
"The proposal expiration date is in the past. This deal will not be able to be signed.",
blocking: false,
};
}
if (timestampDate > new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000)) {
return {
message: "The proposal expiration date is more than 30 days from today.",
blocking: false,
};
}
};

export const validateDealBeforeSubmission = (data: PreSubmitTestData): PreSubmitResponse => {
const errors: PreSubmitResponse = [];

const expirationError = checkExpirationDateTerm(data)
if (expirationError) {
errors.push(expirationError)
}

return errors;
}

Matching Quantities

A customer is buying a platform product and an add-on. The number of add-on feature seats purchased must equal the number of platform seat licenses purchased.

export const validateDealBeforeSubmission = (
data: PreSubmitTestData
): PreSubmitResponse => {
const errors: PreSubmitResponse = [];

const seatSku = data.deal.lineItems.find((lineItem) =>
lineItem.productCode.endsWith("-seats")
);
const supportSku = data.deal.lineItems.find((lineItem) =>
lineItem.productCode.endsWith("-support")
);
if (
seatSku &&
supportSku &&
(seatSku.pricingSchedule.some(
(period, i) => supportSku.pricingSchedule[i]?.quantity !== period.quantity
) ||
seatSku.pricingSchedule.length !== supportSku.pricingSchedule.length)
) {
errors.push({
message:
"Support must be sold for the same number of seats as standard seat licenses",
});
}

return errors;
};

SKU and Term Dependency

A customer is buying a specific product that requires special licensing terms. Any deal with that product must include the licensing terms.

export const validateDealBeforeSubmission = (data: PreSubmitTestData): PreSubmitResponse => {
const errors: PreSubmitResponse = [];

if (
data.deal.lineItems.some(
lineItem => ["business-support", "enterprise-support"].includes(lineItem.productCode)
) &&
!data.deal.terms.some(term => term.name === "Premium Support SLA")
) {
errors.push({ message: "Any premimum support SKU must include the Premium Support SLA SKU" })
}

return errors;
}

Contract Duration Rules

Company policy is that all new orders or renewals must be in a multiple of 12 months. Deals for any other interval are invalid.

export const validateDealBeforeSubmission = (data: PreSubmitTestData): PreSubmitResponse => {
const errors: PreSubmitResponse = [];

// New Orders and Renewals must be for whole years only
if (["new-order", "renewal"].includes(data.deal.agreementType) && (!data.deal.overview.contractDuration || data.deal.overview.contractDuration % 12 !== 0)) {
errors.push({
message: "New Orders and renewals must be for whole years. Adjust contract duration to be a multiple of 12."
})
}

return errors;
}

Account ID Validation

Every account ID at Acme, Inc is ACME-0000-AAAA where 0 is any digit 1-9 and A is any letter A-Z . Account IDs that don't match this format are incorrect and should be rejected.

This function utilizes a regular expression (regex) to perform this check.

export const validateDealBeforeSubmission = (data: PreSubmitTestData): PreSubmitResponse => {
const errors: PreSubmitResponse = [];

const accountId = data.deal.terms.find(term => term.builtInId === "externalAccountId") as ValueTerm | undefined

// All Account IDs are ACME-0000-AAAA where 0 is any digit 1-9 and A is any letter A-Z
if (accountId?.value && !accountId.value.match(/ACME-[0-9]{4}-[A-Z]{4}/)) {
errors.push(`${accountId.value} is not a valid account ID. Account IDs should match ACME-0000-AAAA.`)
}

return errors;
}

SKU Quantity Limit/Minimum

A company offers a starter tier SKU for small companies. However, this SKU can only be sold with 5-10 seats (inclusive).

export const validateDealBeforeSubmission = (data: PreSubmitTestData): PreSubmitResponse => {
const errors: PreSubmitResponse = [];

// Starter seat licenses can only have 5-10 seats
const invalidSeatCount = data.deal.lineItems.filter(
lineItem => lineItem.productCode === "starter-seats" && lineItem.pricingSchedule.some(period => period.quantity < 5 || period.quantity > 10))
if (invalidSeatCount.length) {
errors.push({
message: "Starter seat licenses can only have 5-10 seats"
})
}

return errors;
}

SKU Multiples

A company seals seats licenses and has a policy they can only be sold in seats of 5. If any number other than 5 is entered, the deal cannot be submitted.

This example takes advantage of template literals to return dynamic text based on the names of the associated line items to correct.

export const validateDealBeforeSubmission = (data: PreSubmitTestData): PreSubmitResponse => {
const errors: PreSubmitResponse = [];

// All seat SKUs (product code ending in "-seats") must be sold in a multiple of 5
// Quantity 0 cannot be sold
const invalidSeatCount = data.deal.lineItems.filter(
lineItem => lineItem.productCode.endsWith("-seats") && lineItem.pricingSchedule.some(period => period.quantity % 5 !== 0 || period.quantity === 0))
if (invalidSeatCount.length) {
errors.push({
message: `Seat licenses must be sold in seats of 5. These line items have an invalid quantity: ${invalidSeatCount.map(li => li.name).join(", ")}`
})
}

return errors;
}

Empty Editable Fields

A company wants to ensure that the buyer always enters their own PO number at time of signing. If a PO number is pre-filled in, have the rep clear it out.

export const validateDealBeforeSubmission = (data: PreSubmitTestData): PreSubmitResponse => {
const errors: PreSubmitResponse = [];

const poNumber = data.deal.terms.find(term => term.name === "PO Number") as ValueTerm | undefined

// PO Number should always be blank when submitting for approval
if (!!poNumber?.value) {
errors.push("PO Number should always be blank when submitting for approval")
}

return errors;
}

Minimum Contract Requirements

Deals under $10,000 are not eligible for a contract and must instead use the PAYG service. If a contact is under $10K, advise the rep of their options.

export const validateDealBeforeSubmission = (data: PreSubmitTestData): PreSubmitResponse => {
const errors: PreSubmitResponse = [];

// Contracts all have a $10K minimum
const totalPrice = data.deal.lineItems.reduce((t, a) => t + a.pricingSchedule.reduce((t, a) => t + a.netPrice, 0), 0)
if (totalPrice < 10000) {
errors.push({
message: `Total Contract Value must be over $10K. $${totalPrice} does not qualify for a contract and should use PAYG.`
})
}

return errors;
}

Approval Triggers

SKU/Term Bundle Requirements

A company sells premium support and requires special terms be present on any contract with premium support. Any contract that has either the support SLA term or a premium support SKU needs to have the other as well.

export const evaluateApprovalTrigger = (data: ApprovalTriggerTestData): boolean => {
const includesPremiumSupportSKU = data.deal.lineItems.some(
lineItem => ["business-support", "enterprise-support"].includes(lineItem.productCode)
)
const includesPremiumSupportSLA = data.deal.terms.some(term => term.name === "Premium Support SLA")
console.log("Includes Support SKU", includesPremiumSupportSKU)
console.log("Includes Support SLA", includesPremiumSupportSLA)
if (includesPremiumSupportSKU != includesPremiumSupportSLA) {
return false
}

return true;
}

Product Family Exclusivity

When multiple tiers of a product are sold, all SKUs must be within that product tier. Deals that sell multiple tiers on a single agreement require approval.

export const evaluateApprovalTrigger = (data: ApprovalTriggerTestData): boolean => {
// Create an empty set of product tiers
// A set will automatically de-duplicate entries on addition
const tiers = new Set<string>()

data.deal.lineItems.forEach(lineItem => {
// Find the first part of the product code
// i.e. enterprise-seats -> enterprise
const productPrefix = lineItem.productCode.split("-")[0]
// Add it to the tiers set
tiers.add(productPrefix)
})
console.log("Tiers:", tiers)

if (tiers.size > 1) {
console.warn("Tiers has a size of", tiers.size)
return true
}

return false
}

Generic Helpers

These are snippets that you can use within your function. These snippets have no impact by themselves, can be combined with other samples to customize your logic.

Date Conversion

RevOps Functions default to types supported by the JSON standard and provides dates as ISO date strings (2024-01-31). This value can be converted into a JavaScript Date with this helper method.

const stringToDate = (dateString: string, endOfDay: boolean = false): Date => {
// Value is ISO date string, separate into year, month, day
const timestampParts = dateString.split("-");

// A date doesn't have a specific time associated with it
// Most dates are considered start of day UTC
// Expiration date represents the value at 11:59:59 PM AOE (UTC-12)
const [hour, minute, second] = endOfDay ? [23, 59, 59] : [0, 0, 0]
const date = new Date(
parseInt(timestampParts[0]),
parseInt(timestampParts[1]) - 1,
parseInt(timestampParts[2]),
hour,
minute,
second
);
if (endOfDay) {
// Server time is UTC, expiration time is UTC-12
date.setTime(date.getTime() + 12 * 60 * 60 * 1000);
}
return date
}

This snippet can be seen in use in the following samples:

Proposal Expiration Date

Combining Samples and Code Readability

Depending on the complexity of your companies rules, you may have several aspects you are checking for. This section shows how some of the above samples can be combined to utilize multiple rules together.

Multiple Blocks

Multiple rules can be combined to return multiple errors. For simple rules, they can be combined within the validateDealBeforeSubmission block.

export const validateDealBeforeSubmission = (data: PreSubmitTestData): PreSubmitResponse => {
const errors: PreSubmitResponse = [];

// Contracts all have a $10K minimum
const totalPrice = data.deal.lineItems.reduce((t, a) => t + a.pricingSchedule.reduce((t, a) => t + a.netPrice, 0), 0)
if (totalPrice < 10000) {
errors.push({
message: `Total Contract Value must be over $10K. $${totalPrice} does not qualify for a contract and should use PAYG.`
})
}

// Credit card payments must be Due Upon Receipt
const paymentMethod = data.deal.terms.find(term => term.name === "Payment Method") as OptionTerm | undefined
const paymentTerms = data.deal.terms.find(term => term.name === "Payment Terms") as OptionTerm | undefined

if (paymentMethod?.value === "credit-card" && paymentTerms?.value !== "1") {
errors.push({ message: "Credit Card payments must be Due Upon Receipt" })
}

// Only 1 product tier can be sold
const businessSkus = data.deal.lineItems.filter((lineItem) =>
lineItem.productCode.startsWith("business-")
);
const enterpriseSkus = data.deal.lineItems.filter((lineItem) =>
lineItem.productCode.startsWith("enterprise-")
);
if (businessSkus.length && enterpriseSkus.length) {
console.log(
"Found mixed pricing tiers",
"Business SKUs",
businessSkus.map((sku) => sku.lineItemTitle),
"Enterprise SKUs",
enterpriseSkus.map((sku) => sku.lineItemTitle)
);
errors.push({
message: "Enterprise and business SKUs cannot be mixed",
});
}

return errors;
}

Utilizing JavaScript Functions

Writing your own named functions and calling them within your validateDealBeforeSubmission export is a great way to keep your code readable and scale to many rules.

const validatePaymentTerms = (data: PreSubmitTestData): PreSubmitResponse => {
const errors: PreSubmitResponse = [];
// Credit card payments must be Due Upon Receipt
const paymentMethod = data.deal.terms.find(term => term.name === "Payment Method") as OptionTerm | undefined
const paymentTerms = data.deal.terms.find(term => term.name === "Payment Terms") as OptionTerm | undefined

if (paymentMethod?.value === "credit-card" && paymentTerms?.value !== "1") {
errors.push({ message: "Credit Card payments must be Due Upon Receipt" })
}

return errors
}

const validateTotalPrice = (data: PreSubmitTestData): PreSubmitResponse => {
const errors: PreSubmitResponse = [];

// Contracts all have a $10K minimum
const totalPrice = data.deal.lineItems.reduce((t, a) => t + a.pricingSchedule.reduce((t, a) => t + a.netPrice, 0), 0)
if (totalPrice < 10000) {
errors.push({
message: `Total Contract Value must be over $10K. $${totalPrice} does not qualify for a contract and should use PAYG.`
})
}

return errors
}

const validateProductTiers = (data: PreSubmitTestData): PreSubmitResponse => {
const errors: PreSubmitResponse = [];

// Only 1 product tier can be sold
const businessSkus = data.deal.lineItems.filter((lineItem) =>
lineItem.productCode.startsWith("business-")
);
const enterpriseSkus = data.deal.lineItems.filter((lineItem) =>
lineItem.productCode.startsWith("enterprise-")
);
if (businessSkus.length && enterpriseSkus.length) {
console.log(
"Found mixed pricing tiers",
"Business SKUs",
businessSkus.map((sku) => sku.lineItemTitle),
"Enterprise SKUs",
enterpriseSkus.map((sku) => sku.lineItemTitle)
);
errors.push({
message: "Enterprise and business SKUs cannot be mixed",
});
}

return errors
}

export const validateDealBeforeSubmission = (data: PreSubmitTestData): PreSubmitResponse => {
const errors: PreSubmitResponse = [];

errors.push(...validatePaymentTerms(data))
errors.push(...validateTotalPrice(data))
errors.push(...validateProductTiers(data))

return errors;
}