## What does this PR do?
Fixes an issue where underscores in form field identifiers were being stripped when converting to workflow template variables. According to the documentation, users should be able to use variables like `{COMPANY_NAME}` or `{MONTHLY_INFRASTRUCTURE_SPEND}`, but the regex in `formatIdentifierToVariable` was removing underscores, causing these variables to not match.
**The problem:**
- Form field identifier: `Company_Name` or `Monthly_Infrastructure_Spend`
- Expected variable: `{COMPANY_NAME}` or `{MONTHLY_INFRASTRUCTURE_SPEND}`
- Actual variable (before fix): `{COMPANYNAME}` or `{MONTHLYINFRASTRUCTURESPEND}`
**The fix:**
- Updated the regex from `[^a-zA-Z0-9 ]` to `[^a-zA-Z0-9_ ]` to preserve underscores in `formatIdentifierToVariable`
- Added a private `formatIdentifierToVariableLegacy` function that maintains the old behavior (strips underscores)
- Added `getVariableFormats` helper that returns both current and legacy formats for backward compatibility
- Added `convertResponsesToVariableFormats` helper in `executeAIPhoneCall.ts` to convert form responses to both variable formats
- Updated `customTemplate.ts` matching logic to support both variable formats
- Updated `executeAIPhoneCall.ts` to include both variable formats when building dynamic variables
This ensures existing templates using `{COMPANYNAME}` continue to work while new templates can use the documented `{COMPANY_NAME}` format.
## Mandatory Tasks (DO NOT REMOVE)
- [x] I have self-reviewed the code (A decent size PR without self-review might be rejected).
- [x] I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. N/A - no docs changes needed.
- [x] I confirm automated tests are in place that prove my fix is effective or that my feature works.
## How should this be tested?
1. Create a routing form with a field identifier containing underscores (e.g., `Company_Name`)
2. Create a workflow with a template using `{COMPANY_NAME}` (with underscore)
3. Submit the form and verify the variable is replaced correctly
4. Also test with `{COMPANYNAME}` (without underscore) to verify backward compatibility
**Automated tests added:**
- `customTemplate.test.ts` - 15 tests covering `formatIdentifierToVariable`, `getVariableFormats`, and template variable replacement
- `executeAIPhoneCall.test.ts` - 6 tests covering `convertResponsesToVariableFormats` function for form responses
## Human Review Checklist
- [ ] Verify the regex change `[^a-zA-Z0-9_ ]` correctly preserves underscores
- [ ] Verify backward compatibility: both `{COMPANY_NAME}` and `{COMPANYNAME}` should work for the same form field
- [ ] Verify `convertResponsesToVariableFormats` correctly generates both variable formats
- [ ] Review test coverage for edge cases (empty responses, undefined values)
---
Link to Devin run: https://app.devin.ai/sessions/9d5d90178714493086d94691865c3e07
Requested by: @hariombalhara
<!-- devin-review-badge-begin -->
---
<a href="https://app.devin.ai/review/calcom/cal.com/pull/27571">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1">
<img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin">
</picture>
</a>
<!-- devin-review-badge-end -->
152 lines
4.7 KiB
TypeScript
152 lines
4.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import customTemplate, {
|
|
formatIdentifierToVariable,
|
|
getVariableFormats,
|
|
transformBookingResponsesToVariableFormat,
|
|
} from "./customTemplate";
|
|
|
|
describe("formatIdentifierToVariable", () => {
|
|
it("should convert spaces to underscores and uppercase", () => {
|
|
expect(formatIdentifierToVariable("Company Name")).toBe("COMPANY_NAME");
|
|
});
|
|
|
|
it("should preserve existing underscores", () => {
|
|
expect(formatIdentifierToVariable("Company_Name")).toBe("COMPANY_NAME");
|
|
});
|
|
|
|
it("should handle multiple underscores", () => {
|
|
expect(formatIdentifierToVariable("Monthly_Infrastructure_Spend")).toBe("MONTHLY_INFRASTRUCTURE_SPEND");
|
|
});
|
|
|
|
it("should remove special characters except underscores", () => {
|
|
expect(formatIdentifierToVariable("Company-Name!")).toBe("COMPANYNAME");
|
|
});
|
|
|
|
it("should handle mixed spaces and underscores", () => {
|
|
expect(formatIdentifierToVariable("Company_Name Here")).toBe("COMPANY_NAME_HERE");
|
|
});
|
|
|
|
it("should trim whitespace", () => {
|
|
expect(formatIdentifierToVariable(" Company Name ")).toBe("COMPANY_NAME");
|
|
});
|
|
it("should preserve numbers", () => {
|
|
expect(formatIdentifierToVariable("Company123_Name")).toBe("COMPANY123_NAME");
|
|
});
|
|
});
|
|
|
|
describe("getVariableFormats", () => {
|
|
it("should return both formats when identifier has underscores", () => {
|
|
const formats = getVariableFormats("Company_Name");
|
|
expect(formats).toContain("COMPANY_NAME");
|
|
expect(formats).toContain("COMPANYNAME");
|
|
expect(formats).toHaveLength(2);
|
|
});
|
|
|
|
it("should return single format when no underscores in identifier", () => {
|
|
const formats = getVariableFormats("Company Name");
|
|
expect(formats).toEqual(["COMPANY_NAME"]);
|
|
});
|
|
|
|
it("should return single format when identifier has no special chars", () => {
|
|
const formats = getVariableFormats("CompanyName");
|
|
expect(formats).toEqual(["COMPANYNAME"]);
|
|
});
|
|
|
|
it("should handle multiple underscores", () => {
|
|
const formats = getVariableFormats("Monthly_Infrastructure_Spend");
|
|
expect(formats).toContain("MONTHLY_INFRASTRUCTURE_SPEND");
|
|
expect(formats).toContain("MONTHLYINFRASTRUCTURESPEND");
|
|
expect(formats).toHaveLength(2);
|
|
});
|
|
});
|
|
|
|
describe("customTemplate - variable replacement", () => {
|
|
const baseVariables = {
|
|
eventName: "Test Event",
|
|
organizerName: "Test Organizer",
|
|
attendeeName: "Test Attendee",
|
|
timeZone: "UTC",
|
|
};
|
|
|
|
it("should replace variables with underscores in template", () => {
|
|
const responses = transformBookingResponsesToVariableFormat({
|
|
Company_Name: { value: "Acme Corp", label: "Company Name" },
|
|
});
|
|
|
|
const result = customTemplate(
|
|
"Company: {COMPANY_NAME}",
|
|
{ ...baseVariables, responses },
|
|
"en",
|
|
undefined,
|
|
true
|
|
);
|
|
|
|
expect(result.text).toBe("Company: Acme Corp");
|
|
});
|
|
|
|
it("should replace legacy variables without underscores in template (backward compatibility)", () => {
|
|
const responses = transformBookingResponsesToVariableFormat({
|
|
Company_Name: { value: "Acme Corp", label: "Company Name" },
|
|
});
|
|
|
|
const result = customTemplate(
|
|
"Company: {COMPANYNAME}",
|
|
{ ...baseVariables, responses },
|
|
"en",
|
|
undefined,
|
|
true
|
|
);
|
|
|
|
expect(result.text).toBe("Company: Acme Corp");
|
|
});
|
|
|
|
it("should handle both variable formats in the same template", () => {
|
|
const responses = transformBookingResponsesToVariableFormat({
|
|
Company_Name: { value: "Acme Corp", label: "Company Name" },
|
|
Monthly_Spend: { value: "$5000", label: "Monthly Spend" },
|
|
});
|
|
|
|
const result = customTemplate(
|
|
"Company: {COMPANY_NAME}, Spend: {MONTHLYSPEND}",
|
|
{ ...baseVariables, responses },
|
|
"en",
|
|
undefined,
|
|
true
|
|
);
|
|
|
|
expect(result.text).toBe("Company: Acme Corp, Spend: $5000");
|
|
});
|
|
|
|
it("should handle identifiers with spaces (converted to underscores)", () => {
|
|
const responses = transformBookingResponsesToVariableFormat({
|
|
"Company Name": { value: "Acme Corp", label: "Company Name" },
|
|
});
|
|
|
|
const result = customTemplate(
|
|
"Company: {COMPANY_NAME}",
|
|
{ ...baseVariables, responses },
|
|
"en",
|
|
undefined,
|
|
true
|
|
);
|
|
|
|
expect(result.text).toBe("Company: Acme Corp");
|
|
});
|
|
|
|
it("should handle array values", () => {
|
|
const responses = transformBookingResponsesToVariableFormat({
|
|
Selected_Options: { value: ["Option A", "Option B", "Option C"], label: "Selected Options" },
|
|
});
|
|
|
|
const result = customTemplate(
|
|
"Options: {SELECTED_OPTIONS}",
|
|
{ ...baseVariables, responses },
|
|
"en",
|
|
undefined,
|
|
true
|
|
);
|
|
|
|
expect(result.text).toBe("Options: Option A, Option B, Option C");
|
|
});
|
|
});
|