Skip to main content

Function Examples

We have 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 Discount Limit/Maximum

A company offers discounts on their starter tier SKU for small companies. However, the maximum discount that can be offered is 10%.

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

// Starter seat licenses can only be discounted up to 10%
const starterSeats = data.deal.lineItems.filter(lineItem => lineItem.productCode === "starter-seats")
starterSeats.forEach(li => li.pricingSchedule.map(period => {
const discount = period.listPrice !== 0 ? (period.listPrice - period.adjustedPrice) / period.listPrice : NaN
if (discount > .10) {
errors.push({
message: "Starter seat licenses discounts cannot be greater than 10%"
})
}
}))

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

Total Contract Value

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.overview.revenueMetrics?.totalContractValue
// Check for undefined for non-priced agreements
if (totalPrice !== undefined && 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;
}

Annual Recurring Revenue

This example uses the same logic as above, but applies the $10,000 minimum to annual recurring revenue.

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

// Contracts all have a $10K minimum
const arr = data.deal.overview.revenueMetrics?.annualRecurringRevenue
// Check for undefined for non-priced agreements
if (arr !== undefined && arr < 10000) {
errors.push({
message: `Annual Recurring Revenue must be over $10K. $${arr} does not qualify for a contract and should use PAYG.`
})
}

return errors;
}

Total Contract Value by Year

In this variation, the $10,000 minimum applies to the total contract amount for each year.

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

// Contracts all have a $10K minimum per year
const invalidYears = (data.deal.overview.revenueMetrics?.annualBreakdown ?? []).filter(year => year.totalContractValue < 10000)
if (invalidYears.length) {
errors.push({
message: `Total Contract Value must be over $10K each year. $${invalidYears.join(", ")} do not qualify for a contract and should use PAYG.`
})
}

return errors;
}

Using CRM Data

These examples use the RevOps SDK to query CRM data.

Term Conditions Based on Opportunity Segment

Many organizations group their customers into various segments based on company size or other factors. This segment is then used to set different limits. In this example, the Segment__c field on the Salesforce Opportunity lists the customer segment. That segment is used to determine what payment terms are available.

Small customers can use Due Upon Receipt or Net 30 only. Medium customers can also use Net 45 and Net 60. Large customers can use all payment terms, including Net 75 and Net 90. If the segment is missing, it's considered a small customer.

const getSegment = async (data: PreSubmitTestData): Promise<"small" | "medium" | "large" | null> => {
const oppId = data.deal.externalConnections.find(conn => conn.externalSystem === "salesforce" && conn.externalObject === "opportunity")?.externalId
const query = `SELECT Segment__c FROM Opportunity WHERE Id='${oppId}'`

const queryResult = await revops.salesforce.query<{ Segment__c: "small" | "medium" | "large" | null }>(query)
return queryResult[0]?.Segment__c
}

export const validateDealBeforeSubmission = async (data: PreSubmitTestData): Promise<PreSubmitResponse> => {
const errors: PreSubmitResponse = [];
const segment = await getSegment(data)
const paymentTerms = data.deal.terms.find(term => term.name === "Payment Terms") as OptionTerm | undefined

console.log("Payment Terms:", paymentTerms?.value)
console.log("Segment:", segment)

if (
(["small", null].includes(segment) && !["1", "30"].includes(paymentTerms?.value)) ||
(segment === "medium" && !["1", "30", "45", "60"].includes(paymentTerms?.value))
) {
errors.push({
message: `This Opportunity is part of segment \`${segment}\` and is not eligible for **Net-${paymentTerms.value}** payment terms.`,
enableMarkdown: true
})
}

return errors;
};

Blocking Submission for Overdue Accounts

If a customer hasn't paid for their current contract, this often means that they're not eligible for a new contract until they have paid. In this example, the Account object has a PaymentStatus__c field that is either pending, paid, or overdue.

interface QueryReturn {
Account: { PaymentStatus__c: "pending" | "paid" | "overdue" | null } | null
}

const getPaymentStatus = async (data: PreSubmitTestData): Promise<"pending" | "paid" | "overdue" | null> => {
const oppId = data.deal.externalConnections.find(conn => conn.externalSystem === "salesforce" && conn.externalObject === "opportunity")?.externalId
const query = `SELECT Account.PaymentStatus__c FROM Opportunity WHERE Id='${oppId}'`

const queryResult = await revops.salesforce.query<QueryReturn>(query)
return queryResult[0]?.Account?.PaymentStatus__c
}

export const validateDealBeforeSubmission = async (data: PreSubmitTestData): Promise<PreSubmitResponse> => {
const errors: PreSubmitResponse = [];
const paymentStatus = await getPaymentStatus(data)

if (!paymentStatus) {
errors.push({
message: "This Opportunity does not have an Account with a payment status."
})
} else if (paymentStatus === "overdue") {
errors.push({
message: "This Account is overdue. The current contract must be paid before any new contracts."
})
}


return errors;
};

Advanced Formatting

These examples use markdown support to format text to provide more clarity to the rep.

Minimum Contract Requirements by Tier

An organization has a set of contract minimums that change based on the product family that is being sold. If a contract is under the minimum, they want to clearly advise the rep of their options.

This advanced formatting includes adding a link to an external resource, a table of contract minimums by tier, and marking key values as bold.

export const validateDealBeforeSubmission = (
data: PreSubmitTestData
): PreSubmitResponse => {
const errors: PreSubmitResponse = [];
const tiers = [
{ name: "Business", skuPrefix: "business", contractMinimum: 15000 },
{ name: "Professional", skuPrefix: "professional", contractMinimum: 20000 },
{ name: "Enterprise", skuPrefix: "enterprise", contractMinimum: 25000 },
];

const productTier = data.deal.lineItems[0]?.productCode.split("-")?.[0];

if (!productTier) {
errors.push({
message: "No product tier found, unknown contract minimum",
});
return errors;
}

const tier = tiers.find((tier) => tier.skuPrefix === productTier);

// Contracts all have a minimum based on the tier being sold
const totalPrice = data.deal.overview.revenueMetrics?.totalContractValue
// Check for undefined for non-priced agreements
if (totalPrice !== undefined && totalPrice < tier.contractMinimum) {
let message = `Total Contract Value must be over **$${tier.contractMinimum.toLocaleString()}** for ${tier.name} deals. `
message += `This contract's value **$${totalPrice.toLocaleString()}** does not qualify for a contract and should use PAYG or a lower tier level.`

message += "\n\n| Tier | Contract Minimum |\n";
message += "| - | - |\n";
tiers.forEach((tier) => {
message += `| ${
tier.name
} | $${tier.contractMinimum.toLocaleString()} |\n`;
});

message += "\n\nReview the [internal documentation](https://example.com) for more information."

errors.push({
message,
enableMarkdown: true,
});
}

return errors;
};

Example Output

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.overview.revenueMetrics?.totalContractValue
// Check for undefined for non-priced agreements
if (totalPrice !== undefined && 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.overview.revenueMetrics?.totalContractValue
// Check for undefined for non-priced agreements
if (totalPrice !== undefined && 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;
}