mirror of
https://github.com/anomalyco/opencode.git
synced 2026-10-21 20:40:00 +00:00
feat(console): enrich Salesforce leads with Console account status (#54387)
This commit is contained in:
parent
fe1717b309
commit
07c0ff5914
24 changed files with 1460 additions and 37 deletions
|
|
@ -28,6 +28,10 @@ const ENTERPRISE_SALES_INBOX_EMAIL = new sst.Secret("ENTERPRISE_SALES_INBOX_EMAI
|
|||
const SALESFORCE_CLIENT_ID = new sst.Secret("SALESFORCE_CLIENT_ID")
|
||||
const SALESFORCE_CLIENT_SECRET = new sst.Secret("SALESFORCE_CLIENT_SECRET")
|
||||
const SALESFORCE_INSTANCE_URL = new sst.Secret("SALESFORCE_INSTANCE_URL")
|
||||
const CONSOLE_CRM_TOKEN = new sst.Secret("CONSOLE_CRM_TOKEN")
|
||||
const CONSOLE_CRM_LOOKUP_URL = new sst.Linkable("CONSOLE_CRM_LOOKUP_URL", {
|
||||
properties: { value: `https://${domain}/console/api/internal/console-account` },
|
||||
})
|
||||
|
||||
const logProcessor = new sst.cloudflare.Worker("LogProcessor", {
|
||||
handler: "packages/console/function/src/log-processor.ts",
|
||||
|
|
@ -48,6 +52,8 @@ new sst.cloudflare.x.SolidStart("Console", {
|
|||
SALESFORCE_CLIENT_ID,
|
||||
SALESFORCE_CLIENT_SECRET,
|
||||
SALESFORCE_INSTANCE_URL,
|
||||
CONSOLE_CRM_TOKEN,
|
||||
CONSOLE_CRM_LOOKUP_URL,
|
||||
],
|
||||
environment: {
|
||||
//VITE_DOCS_URL: web.url.apply((url) => url!),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"dev": "vite dev --host 0.0.0.0",
|
||||
"dev:enterprise": "bun --watch script/dev-enterprise.ts",
|
||||
"dev:remote": "bun sst shell --stage=dev bun dev",
|
||||
"build": "bun ./script/generate-sitemap.ts && vite build",
|
||||
"start": "vite start"
|
||||
|
|
|
|||
99
packages/console/app/script/dev-enterprise.ts
Normal file
99
packages/console/app/script/dev-enterprise.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import { handleEnterpriseRequest } from "../src/routes/api/enterprise"
|
||||
import { z } from "zod"
|
||||
import { SalesforceCliError, salesforceCliResponse } from "./salesforce-cli-response"
|
||||
import { handleConsoleSignup } from "../src/routes/api/console-signup"
|
||||
import { consoleAccountSchema } from "../src/lib/console-account"
|
||||
|
||||
const org = process.env.SF_TARGET_ORG || "opencode"
|
||||
const crmToken = process.env.CONSOLE_CRM_TOKEN
|
||||
if (!crmToken) throw new Error("Set CONSOLE_CRM_TOKEN to the shared local Console CRM token")
|
||||
const accountUrl = process.env.CONSOLE_CRM_LOOKUP_URL || "http://localhost:3100/console/api/internal/console-account"
|
||||
|
||||
async function authenticatedRequest(
|
||||
path: string,
|
||||
payload?: Record<string, unknown>,
|
||||
method: "GET" | "POST" | "PATCH" = payload ? "POST" : "GET",
|
||||
) {
|
||||
const child = Bun.spawn(
|
||||
[
|
||||
"sf",
|
||||
"api",
|
||||
"request",
|
||||
"rest",
|
||||
path,
|
||||
"--target-org",
|
||||
org,
|
||||
"--json",
|
||||
"--method",
|
||||
method,
|
||||
...(payload ? ["--body", "-", "--header", "Content-Type:application/json"] : []),
|
||||
],
|
||||
{
|
||||
stdin: payload ? new Blob([JSON.stringify(payload)]) : "ignore",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
},
|
||||
)
|
||||
const [output] = await Promise.all([
|
||||
new Response(child.stdout).text(),
|
||||
child.exited,
|
||||
new Response(child.stderr).text(),
|
||||
])
|
||||
return salesforceCliResponse(output)
|
||||
}
|
||||
|
||||
await authenticatedRequest("/services/data/v59.0/")
|
||||
|
||||
const server = Bun.serve({
|
||||
hostname: "127.0.0.1",
|
||||
port: Number(process.env.ENTERPRISE_DEV_PORT || 3101),
|
||||
async fetch(request) {
|
||||
const path = new URL(request.url).pathname
|
||||
if (path === "/health" && request.method === "GET") {
|
||||
return Response.json({ handler: "local-sst-enterprise", salesforceOrg: org })
|
||||
}
|
||||
if (path === "/api/console-signup" && request.method === "POST") {
|
||||
return handleConsoleSignup(request, crmToken, (path, method, payload) =>
|
||||
authenticatedRequest(path, payload, method),
|
||||
)
|
||||
}
|
||||
if (path !== "/api/enterprise") return new Response("Not found", { status: 404 })
|
||||
if (request.method !== "POST") return new Response("Method not allowed", { status: 405 })
|
||||
const failures: string[] = []
|
||||
const response = await handleEnterpriseRequest(request, {
|
||||
salesforceSubmit: async (payload) => {
|
||||
const result = await authenticatedRequest("/services/data/v59.0/sobjects/Lead", payload).catch(
|
||||
(error: unknown) => {
|
||||
const code = error instanceof SalesforceCliError ? error.code : "CLI_ERROR"
|
||||
failures.push(code)
|
||||
console.error(`Local Salesforce submission rejected: ${code}`)
|
||||
return undefined
|
||||
},
|
||||
)
|
||||
return z.object({ success: z.literal(true) }).safeParse(result).success
|
||||
},
|
||||
notifications: false,
|
||||
accountLookup: async (email) => {
|
||||
const response = await fetch(accountUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${crmToken}` },
|
||||
body: JSON.stringify({ email }),
|
||||
signal: AbortSignal.timeout(8000),
|
||||
})
|
||||
if (!response.ok) throw new Error(`Local Console account lookup failed (${response.status})`)
|
||||
return z.object({ account: consoleAccountSchema.nullable() }).parse(await response.json()).account
|
||||
},
|
||||
})
|
||||
if (failures.length > 0) {
|
||||
return Response.json(
|
||||
{ error: "Salesforce rejected the request.", errorCode: failures[0] },
|
||||
{ status: 502, headers: { "X-Enterprise-Handler": "local-sst-enterprise" } },
|
||||
)
|
||||
}
|
||||
response.headers.set("X-Enterprise-Handler", "local-sst-enterprise")
|
||||
return response
|
||||
},
|
||||
})
|
||||
|
||||
console.log(`Local Enterprise handler: ${server.url.origin}/api/enterprise`)
|
||||
console.log(`Salesforce CLI org: ${org}; local email and EmailOctopus delivery disabled.`)
|
||||
40
packages/console/app/script/salesforce-cli-response.test.ts
Normal file
40
packages/console/app/script/salesforce-cli-response.test.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { SalesforceCliError, salesforceCliResponse } from "./salesforce-cli-response"
|
||||
|
||||
test("unwraps a successful Salesforce Lead creation from the CLI HTTP envelope", () => {
|
||||
expect(
|
||||
salesforceCliResponse(
|
||||
JSON.stringify({
|
||||
status: 0,
|
||||
result: { statusCode: 201, headers: {}, body: { id: "test-lead", success: true, errors: [] } },
|
||||
}),
|
||||
),
|
||||
).toEqual({ id: "test-lead", success: true, errors: [] })
|
||||
})
|
||||
|
||||
test("rejects a Salesforce HTTP failure even if the CLI exits successfully", () => {
|
||||
expect(() =>
|
||||
salesforceCliResponse(
|
||||
JSON.stringify({
|
||||
status: 0,
|
||||
result: { statusCode: 400, body: [{ errorCode: "INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST" }] },
|
||||
}),
|
||||
),
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
test("rejects CLI errors and responses missing the HTTP envelope", () => {
|
||||
expect(() => salesforceCliResponse(JSON.stringify({ status: 1, message: "CLI error" }))).toThrow()
|
||||
expect(() => salesforceCliResponse(JSON.stringify({ status: 0, result: { success: true } }))).toThrow()
|
||||
})
|
||||
|
||||
test("preserves the Salesforce rejection code when the CLI exits nonzero", () => {
|
||||
expect(() =>
|
||||
salesforceCliResponse(
|
||||
JSON.stringify({
|
||||
status: 1,
|
||||
result: { statusCode: 400, body: [{ errorCode: "DUPLICATES_DETECTED", message: "Duplicate found" }] },
|
||||
}),
|
||||
),
|
||||
).toThrow(new SalesforceCliError("DUPLICATES_DETECTED"))
|
||||
})
|
||||
24
packages/console/app/script/salesforce-cli-response.ts
Normal file
24
packages/console/app/script/salesforce-cli-response.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { z } from "zod"
|
||||
|
||||
export class SalesforceCliError extends Error {
|
||||
constructor(readonly code: string) {
|
||||
super(`Salesforce request failed: ${code}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function salesforceCliResponse(output: string) {
|
||||
const response = z
|
||||
.object({
|
||||
status: z.number(),
|
||||
result: z.object({ statusCode: z.number().int(), body: z.unknown() }).optional(),
|
||||
})
|
||||
.parse(JSON.parse(output))
|
||||
if (!response.result) throw new SalesforceCliError("CLI_ERROR")
|
||||
if (response.status !== 0 || response.result.statusCode < 200 || response.result.statusCode > 299) {
|
||||
const errors = z
|
||||
.array(z.object({ errorCode: z.string().regex(/^[A-Z][A-Z0-9_]+$/) }))
|
||||
.safeParse(response.result.body)
|
||||
throw new SalesforceCliError(errors.success ? (errors.data[0]?.errorCode ?? "REQUEST_FAILED") : "REQUEST_FAILED")
|
||||
}
|
||||
return response.result.body
|
||||
}
|
||||
31
packages/console/app/src/lib/console-account.ts
Normal file
31
packages/console/app/src/lib/console-account.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { Resource } from "@opencode/console-resource"
|
||||
import { z } from "zod"
|
||||
|
||||
export const consoleAccountSchema = z.object({
|
||||
id: z.string().min(1).max(255),
|
||||
email: z.email(),
|
||||
name: z.string().nullable(),
|
||||
createdAt: z.iso.datetime(),
|
||||
})
|
||||
export type ConsoleAccount = z.infer<typeof consoleAccountSchema>
|
||||
|
||||
export async function lookupConsoleAccount(email: string): Promise<ConsoleAccount | null> {
|
||||
const response = await fetch(`${Resource.CONSOLE_CRM_LOOKUP_URL.value}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${Resource.CONSOLE_CRM_TOKEN.value}` },
|
||||
body: JSON.stringify({ email }),
|
||||
signal: AbortSignal.timeout(3000),
|
||||
})
|
||||
if (!response.ok) throw new Error(`Console account lookup failed (${response.status})`)
|
||||
const result = z.object({ account: consoleAccountSchema.nullable() }).parse(await response.json())
|
||||
return result.account
|
||||
}
|
||||
|
||||
export function consoleAccountFields(account: ConsoleAccount | null) {
|
||||
if (!account) return { Has_Console_Account__c: false }
|
||||
return {
|
||||
Has_Console_Account__c: true,
|
||||
Console_Account_ID__c: account.id,
|
||||
Console_Signup_Date__c: account.createdAt,
|
||||
}
|
||||
}
|
||||
159
packages/console/app/src/lib/console-signup.test.ts
Normal file
159
packages/console/app/src/lib/console-signup.test.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
import { afterEach, expect, test } from "bun:test"
|
||||
import { syncConsoleAccount, type SalesforceRequest } from "./salesforce"
|
||||
import { handleConsoleSignup } from "../routes/api/console-signup"
|
||||
import { z } from "zod"
|
||||
|
||||
const account = {
|
||||
id: "user_console_test",
|
||||
email: "person@example.test",
|
||||
name: "Test Person",
|
||||
createdAt: "2026-10-01T10:00:00.000Z",
|
||||
}
|
||||
const servers = new Set<ReturnType<typeof Bun.serve>>()
|
||||
afterEach(async () => {
|
||||
await Promise.all([...servers].map((server) => server.stop(true)))
|
||||
servers.clear()
|
||||
})
|
||||
|
||||
function fixture(
|
||||
initial: { object: "Lead" | "Contact"; Id: string; LeadSource: string; Company?: string; AccountId?: string }[] = [],
|
||||
email = account.email,
|
||||
) {
|
||||
const people: Record<string, unknown>[] = initial.map((person) => ({ ...person }))
|
||||
const writes: { method: string; path: string; payload: Record<string, unknown> }[] = []
|
||||
const server = Bun.serve({
|
||||
hostname: "127.0.0.1",
|
||||
port: 0,
|
||||
async fetch(request) {
|
||||
const url = new URL(request.url)
|
||||
if (url.pathname.endsWith("/query")) {
|
||||
const query = url.searchParams.get("q") ?? ""
|
||||
expect(query).toContain("Console_Account_ID__c")
|
||||
expect(query).toContain(`Email = '${email.toLowerCase()}'`)
|
||||
const object = query.includes("FROM Contact") ? "Contact" : "Lead"
|
||||
if (object === "Lead") expect(query).toContain("IsConverted = false")
|
||||
return Response.json({
|
||||
done: true,
|
||||
records: people.filter((person) => person.object === object).map((person) => ({ Id: person.Id })),
|
||||
})
|
||||
}
|
||||
const payload = z.record(z.string(), z.unknown()).parse(await request.json())
|
||||
writes.push({ method: request.method, path: url.pathname, payload })
|
||||
if (request.method === "PATCH") {
|
||||
const person = people.find((person) => person.Id === url.pathname.split("/").at(-1))
|
||||
if (!person) return Response.json({ error: "Not found" }, { status: 404 })
|
||||
Object.assign(person, payload)
|
||||
return new Response(null, { status: 204 })
|
||||
}
|
||||
const id = "00Q000000000001AAA"
|
||||
people.push({ object: "Lead", Id: id, ...payload })
|
||||
return Response.json({ success: true, id }, { status: 201 })
|
||||
},
|
||||
})
|
||||
servers.add(server)
|
||||
const request: SalesforceRequest = async (path, method = "GET", payload) => {
|
||||
const response = await fetch(new URL(path, server.url), {
|
||||
method,
|
||||
...(payload ? { body: JSON.stringify(payload), headers: { "Content-Type": "application/json" } } : {}),
|
||||
})
|
||||
if (!response.ok) throw new Error("CRM request failed")
|
||||
return response.status === 204 ? null : response.json()
|
||||
}
|
||||
return { people, writes, request }
|
||||
}
|
||||
|
||||
test("a new signup creates one Console lead and an exact retry updates it", async () => {
|
||||
const crm = fixture()
|
||||
await syncConsoleAccount(account, crm.request)
|
||||
await syncConsoleAccount(account, crm.request)
|
||||
expect(crm.people).toHaveLength(1)
|
||||
expect(crm.writes.filter((write) => write.method === "POST")).toHaveLength(1)
|
||||
expect(crm.people[0]).toMatchObject({
|
||||
Company: "example.test",
|
||||
LeadSource: "console signup",
|
||||
Has_Console_Account__c: true,
|
||||
Console_Account_ID__c: account.id,
|
||||
Console_Signup_Date__c: account.createdAt,
|
||||
})
|
||||
expect(crm.writes[1].payload).not.toHaveProperty("LeadSource")
|
||||
expect(crm.writes[1].payload).not.toHaveProperty("Company")
|
||||
})
|
||||
|
||||
test.each([
|
||||
["hello@acme.com", "acme.com"],
|
||||
["jane@company.co.uk", "company.co.uk"],
|
||||
["HELLO@ACME.COM", "acme.com"],
|
||||
["person@gmail.com", "Individual"],
|
||||
["person+signup@GOOGLEMAIL.COM", "Individual"],
|
||||
["person@outlook.com", "Individual"],
|
||||
["person@hotmail.co.uk", "Individual"],
|
||||
["person@yahoo.com", "Individual"],
|
||||
["person@icloud.com", "Individual"],
|
||||
["person@proton.me", "Individual"],
|
||||
["person@protonmail.com", "Individual"],
|
||||
["person@pm.me", "Individual"],
|
||||
["person@aol.com", "Individual"],
|
||||
["person@gmail.com.example.test", "gmail.com.example.test"],
|
||||
])("sets Company for a new signup with %s to %s", async (email, company) => {
|
||||
const crm = fixture([], email)
|
||||
await syncConsoleAccount({ ...account, email }, crm.request)
|
||||
expect(crm.people).toHaveLength(1)
|
||||
expect(crm.people[0]).toMatchObject({ Company: company, Email: email.toLowerCase(), LeadSource: "console signup" })
|
||||
})
|
||||
|
||||
test("matching Leads and Contacts keep their source and receive the Console fields", async () => {
|
||||
const crm = fixture([
|
||||
{ object: "Contact", Id: "003000000000001AAA", LeadSource: "Partner", AccountId: "001000000000001AAA" },
|
||||
{ object: "Lead", Id: "00Q000000000002AAA", LeadSource: "enterprise website form", Company: "Visitor's Company" },
|
||||
])
|
||||
await syncConsoleAccount(account, crm.request)
|
||||
expect(crm.writes.every((write) => write.method === "PATCH")).toBe(true)
|
||||
expect(crm.people.map((person) => person.LeadSource)).toEqual(["Partner", "enterprise website form"])
|
||||
expect(crm.people[0].AccountId).toBe("001000000000001AAA")
|
||||
expect(crm.people[1].Company).toBe("Visitor's Company")
|
||||
for (const write of crm.writes) {
|
||||
expect(write.payload).not.toHaveProperty("Company")
|
||||
expect(write.payload).not.toHaveProperty("AccountId")
|
||||
}
|
||||
for (const person of crm.people)
|
||||
expect(person).toMatchObject({
|
||||
Has_Console_Account__c: true,
|
||||
Console_Account_ID__c: account.id,
|
||||
Console_Signup_Date__c: account.createdAt,
|
||||
})
|
||||
})
|
||||
|
||||
test("a converted person's Contact is updated without creating another Lead", async () => {
|
||||
const crm = fixture([{ object: "Contact", Id: "003000000000001AAA", LeadSource: "Website" }])
|
||||
await syncConsoleAccount(account, crm.request)
|
||||
expect(crm.people).toHaveLength(1)
|
||||
expect(crm.writes[0].path).toContain("/Contact/")
|
||||
expect(crm.people[0].LeadSource).toBe("Website")
|
||||
})
|
||||
|
||||
test("the signup endpoint requires its server credential before touching Salesforce", async () => {
|
||||
const crm = fixture()
|
||||
const response = await handleConsoleSignup(
|
||||
new Request("http://localhost/api/console-signup", { method: "POST", body: JSON.stringify(account) }),
|
||||
"test-shared-token",
|
||||
crm.request,
|
||||
)
|
||||
expect(response.status).toBe(401)
|
||||
expect(crm.writes).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("the authenticated signup endpoint reports confirmed delivery", async () => {
|
||||
const crm = fixture()
|
||||
const response = await handleConsoleSignup(
|
||||
new Request("http://localhost/api/console-signup", {
|
||||
method: "POST",
|
||||
headers: { Authorization: "Bearer test-shared-token" },
|
||||
body: JSON.stringify(account),
|
||||
}),
|
||||
"test-shared-token",
|
||||
crm.request,
|
||||
)
|
||||
expect(response.status).toBe(200)
|
||||
expect(await response.json()).toMatchObject({ success: true })
|
||||
expect(crm.people).toHaveLength(1)
|
||||
})
|
||||
112
packages/console/app/src/lib/salesforce.test.ts
Normal file
112
packages/console/app/src/lib/salesforce.test.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import { afterAll, expect, test } from "bun:test"
|
||||
|
||||
const leads: Record<string, unknown>[] = []
|
||||
const server = Bun.serve({
|
||||
hostname: "127.0.0.1",
|
||||
port: 0,
|
||||
async fetch(request, instance) {
|
||||
const path = new URL(request.url).pathname
|
||||
if (path === "/services/oauth2/token") {
|
||||
return Response.json({ access_token: "local-test-token", instance_url: instance.url.origin })
|
||||
}
|
||||
if (path === "/services/data/v59.0/sobjects/Lead" && request.method === "POST") {
|
||||
leads.push(await request.json())
|
||||
return Response.json({ id: "local-test-lead", success: true }, { status: 201 })
|
||||
}
|
||||
return new Response("Not found", { status: 404 })
|
||||
},
|
||||
})
|
||||
|
||||
const resources = {
|
||||
SST_RESOURCE_SALESFORCE_INSTANCE_URL: server.url.origin,
|
||||
SST_RESOURCE_SALESFORCE_CLIENT_ID: "local-test-client",
|
||||
SST_RESOURCE_SALESFORCE_CLIENT_SECRET: "local-test-secret",
|
||||
}
|
||||
const previous = Object.fromEntries(Object.keys(resources).map((key) => [key, process.env[key]]))
|
||||
Object.entries(resources).forEach(([key, value]) => {
|
||||
process.env[key] = JSON.stringify({ value })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await server.stop(true)
|
||||
Object.entries(previous).forEach(([key, value]) => {
|
||||
if (value === undefined) {
|
||||
delete process.env[key]
|
||||
return
|
||||
}
|
||||
process.env[key] = value
|
||||
})
|
||||
})
|
||||
|
||||
test("a fake entry keeps the visitor message, spend range, and enterprise lead source in their own fields", async () => {
|
||||
const { createLead } = await import("./salesforce")
|
||||
const accepted = await createLead({
|
||||
name: "Fake Console Visitor",
|
||||
role: "Engineering lead",
|
||||
company: "Example Test Company",
|
||||
email: "fake-visitor@example.test",
|
||||
inferenceSpend: "$10K–$50K",
|
||||
message: " We need SSO for our developers.\nPlease share the enterprise options. ",
|
||||
})
|
||||
|
||||
expect(accepted).toBe(true)
|
||||
expect(leads).toHaveLength(1)
|
||||
expect(leads[0]).toEqual({
|
||||
LastName: "Fake Console Visitor",
|
||||
Company: "Example Test Company",
|
||||
Email: "fake-visitor@example.test",
|
||||
Phone: null,
|
||||
Title: "Engineering lead",
|
||||
Description: " We need SSO for our developers.\nPlease share the enterprise options. ",
|
||||
Current_Monthly_Inference_Spend__c: "$10K–$50K",
|
||||
LeadSource: "enterprise website form",
|
||||
})
|
||||
})
|
||||
|
||||
test("the local Enterprise request handler applies the updated mapping to a form submission", async () => {
|
||||
const { handleEnterpriseRequest } = await import("../routes/api/enterprise")
|
||||
const response = await handleEnterpriseRequest(
|
||||
new Request("http://localhost/api/enterprise", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: "Fake Local Form Visitor",
|
||||
role: "Engineering lead",
|
||||
company: "Example Test Company",
|
||||
email: "fake-local-visitor@example.test",
|
||||
inferenceSpend: "50k-100k",
|
||||
message: " How are you bro!\nThis is a local form test. ",
|
||||
alias: "",
|
||||
}),
|
||||
}),
|
||||
{
|
||||
salesforceSubmit: async (payload) => {
|
||||
const response = await fetch(`${server.url.origin}/services/data/v59.0/sobjects/Lead`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
return response.ok
|
||||
},
|
||||
notifications: false,
|
||||
accountLookup: async () => ({
|
||||
id: "user_test",
|
||||
email: "fake-local-visitor@example.test",
|
||||
name: null,
|
||||
createdAt: "2026-10-01T12:00:00.000Z",
|
||||
}),
|
||||
},
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(await response.json()).toMatchObject({ success: true })
|
||||
expect(leads.at(-1)).toMatchObject({
|
||||
LastName: "Fake Local Form Visitor",
|
||||
Description: " How are you bro!\nThis is a local form test. ",
|
||||
Current_Monthly_Inference_Spend__c: "$50K–$100K",
|
||||
LeadSource: "enterprise website form",
|
||||
Has_Console_Account__c: true,
|
||||
Console_Account_ID__c: "user_test",
|
||||
Console_Signup_Date__c: "2026-10-01T12:00:00.000Z",
|
||||
})
|
||||
})
|
||||
|
|
@ -1,4 +1,54 @@
|
|||
import { Resource } from "@opencode/console-resource"
|
||||
import { z } from "zod"
|
||||
import { consoleAccountFields, type ConsoleAccount } from "./console-account"
|
||||
|
||||
// Match provider domains exactly; business domains hosted by these providers remain business addresses.
|
||||
const personalEmailDomains = new Set([
|
||||
"gmail.com",
|
||||
"googlemail.com",
|
||||
"outlook.com",
|
||||
"outlook.co.uk",
|
||||
"hotmail.com",
|
||||
"hotmail.co.uk",
|
||||
"hotmail.fr",
|
||||
"hotmail.de",
|
||||
"hotmail.it",
|
||||
"live.com",
|
||||
"live.co.uk",
|
||||
"live.ca",
|
||||
"live.com.au",
|
||||
"msn.com",
|
||||
"yahoo.com",
|
||||
"yahoo.co.uk",
|
||||
"yahoo.co.in",
|
||||
"yahoo.com.au",
|
||||
"yahoo.ca",
|
||||
"yahoo.fr",
|
||||
"yahoo.de",
|
||||
"yahoo.it",
|
||||
"yahoo.co.jp",
|
||||
"ymail.com",
|
||||
"rocketmail.com",
|
||||
"icloud.com",
|
||||
"me.com",
|
||||
"mac.com",
|
||||
"proton.me",
|
||||
"protonmail.com",
|
||||
"pm.me",
|
||||
"aol.com",
|
||||
"aim.com",
|
||||
"gmx.com",
|
||||
"gmx.de",
|
||||
"gmx.net",
|
||||
"mail.com",
|
||||
"fastmail.com",
|
||||
"fastmail.fm",
|
||||
"hey.com",
|
||||
"tuta.com",
|
||||
"tuta.io",
|
||||
"tutanota.com",
|
||||
"tutanota.de",
|
||||
])
|
||||
|
||||
async function login() {
|
||||
const url = Resource.SALESFORCE_INSTANCE_URL.value.replace(/\/$/, "")
|
||||
|
|
@ -46,9 +96,25 @@ export interface SalesforceLeadInput {
|
|||
phone?: string
|
||||
inferenceSpend?: string
|
||||
message: string
|
||||
consoleAccount?: ConsoleAccount | null
|
||||
}
|
||||
|
||||
export async function createLead(input: SalesforceLeadInput): Promise<boolean> {
|
||||
export async function createLead(
|
||||
input: SalesforceLeadInput,
|
||||
submit?: (payload: Record<string, unknown>) => Promise<boolean>,
|
||||
): Promise<boolean> {
|
||||
const payload = {
|
||||
LastName: input.name,
|
||||
Company: input.company?.trim() || "Website",
|
||||
Email: input.email,
|
||||
Phone: input.phone ?? null,
|
||||
Title: input.role,
|
||||
Description: input.message,
|
||||
Current_Monthly_Inference_Spend__c: input.inferenceSpend,
|
||||
LeadSource: "enterprise website form",
|
||||
...(input.consoleAccount === undefined ? {} : consoleAccountFields(input.consoleAccount)),
|
||||
}
|
||||
if (submit) return submit(payload)
|
||||
const auth = await login()
|
||||
if (!auth) return false
|
||||
|
||||
|
|
@ -58,18 +124,7 @@ export async function createLead(input: SalesforceLeadInput): Promise<boolean> {
|
|||
Authorization: `Bearer ${auth.token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
LastName: input.name,
|
||||
Company: input.company?.trim() || "Website",
|
||||
Email: input.email,
|
||||
Phone: input.phone ?? null,
|
||||
Title: input.role,
|
||||
Description: input.inferenceSpend
|
||||
? `Current monthly inference spend: ${input.inferenceSpend}\n\n${input.message}`
|
||||
: input.message,
|
||||
Current_Monthly_Inference_Spend__c: input.inferenceSpend,
|
||||
LeadSource: "Website",
|
||||
}),
|
||||
body: JSON.stringify(payload),
|
||||
}).catch((err) => {
|
||||
console.error("Failed to create Salesforce lead:", err)
|
||||
})
|
||||
|
|
@ -83,3 +138,65 @@ export async function createLead(input: SalesforceLeadInput): Promise<boolean> {
|
|||
|
||||
return true
|
||||
}
|
||||
|
||||
export type SalesforceRequest = (
|
||||
path: string,
|
||||
method?: "GET" | "POST" | "PATCH",
|
||||
payload?: Record<string, unknown>,
|
||||
) => Promise<unknown>
|
||||
|
||||
export async function salesforceRequest(): Promise<SalesforceRequest> {
|
||||
const auth = await login()
|
||||
if (!auth) throw new Error("Salesforce authentication unavailable")
|
||||
return async (path, method = "GET", payload) => {
|
||||
const response = await fetch(`${auth.url}${path}`, {
|
||||
method,
|
||||
headers: { Authorization: `Bearer ${auth.token}`, "Content-Type": "application/json" },
|
||||
...(payload ? { body: JSON.stringify(payload) } : {}),
|
||||
signal: AbortSignal.timeout(8000),
|
||||
})
|
||||
if (!response.ok) throw new Error(`Salesforce request failed (${response.status})`)
|
||||
if (response.status === 204) return null
|
||||
return response.json()
|
||||
}
|
||||
}
|
||||
|
||||
/** Update existing people without changing their acquisition source or sales notes. */
|
||||
export async function syncConsoleAccount(account: ConsoleAccount, request: SalesforceRequest) {
|
||||
const quote = (value: string) => value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")
|
||||
const matches = async (object: "Contact" | "Lead") => {
|
||||
const query = `SELECT Id FROM ${object} WHERE ${object === "Lead" ? "IsConverted = false AND " : ""}(Console_Account_ID__c = '${quote(account.id)}' OR Email = '${quote(account.email.toLowerCase())}')`
|
||||
const records: { Id: string }[] = []
|
||||
let path: string | undefined = `/services/data/v59.0/query?q=${encodeURIComponent(query)}`
|
||||
while (path) {
|
||||
const result = z
|
||||
.object({
|
||||
records: z.array(z.object({ Id: z.string().regex(/^[a-zA-Z0-9]{15,18}$/) })),
|
||||
done: z.boolean(),
|
||||
nextRecordsUrl: z.string().optional(),
|
||||
})
|
||||
.parse(await request(path))
|
||||
records.push(...result.records)
|
||||
if (result.done) break
|
||||
if (!result.nextRecordsUrl?.startsWith("/services/data/")) throw new Error("Salesforce query pagination missing")
|
||||
path = result.nextRecordsUrl
|
||||
}
|
||||
return records.map((record) => ({ object, id: record.Id }))
|
||||
}
|
||||
const people = [...(await matches("Contact")), ...(await matches("Lead"))]
|
||||
const fields = consoleAccountFields(account)
|
||||
for (const person of people)
|
||||
await request(`/services/data/v59.0/sobjects/${person.object}/${person.id}`, "PATCH", fields)
|
||||
if (people.length > 0) return { success: true, created: false, updated: people.length }
|
||||
const domain = account.email.slice(account.email.lastIndexOf("@") + 1).toLowerCase()
|
||||
const result = z.object({ success: z.literal(true), id: z.string() }).parse(
|
||||
await request("/services/data/v59.0/sobjects/Lead", "POST", {
|
||||
LastName: (account.name?.trim() || account.email).slice(0, 80),
|
||||
Company: personalEmailDomains.has(domain) ? "Individual" : domain,
|
||||
Email: account.email.toLowerCase(),
|
||||
LeadSource: "console signup",
|
||||
...fields,
|
||||
}),
|
||||
)
|
||||
return { success: true, created: true, id: result.id }
|
||||
}
|
||||
|
|
|
|||
25
packages/console/app/src/routes/api/console-signup.ts
Normal file
25
packages/console/app/src/routes/api/console-signup.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { timingSafeEqual } from "node:crypto"
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { Resource } from "@opencode/console-resource"
|
||||
import { consoleAccountSchema } from "~/lib/console-account"
|
||||
import { salesforceRequest, syncConsoleAccount, type SalesforceRequest } from "~/lib/salesforce"
|
||||
|
||||
export async function POST(event: APIEvent) {
|
||||
return handleConsoleSignup(event.request, Resource.CONSOLE_CRM_TOKEN.value)
|
||||
}
|
||||
|
||||
export async function handleConsoleSignup(request: Request, token: string, salesforce?: SalesforceRequest) {
|
||||
const expected = Buffer.from(`Bearer ${token}`)
|
||||
const actual = Buffer.from(request.headers.get("authorization") ?? "")
|
||||
if (!token || expected.length !== actual.length || !timingSafeEqual(expected, actual))
|
||||
return new Response(null, { status: 401 })
|
||||
const data: unknown = await request.json().catch(() => null)
|
||||
const account = consoleAccountSchema.safeParse(data)
|
||||
if (!account.success) return Response.json({ error: "Invalid Console account" }, { status: 400 })
|
||||
return Promise.resolve(salesforce)
|
||||
.then(async (client) => syncConsoleAccount(account.data, client ?? (await salesforceRequest())))
|
||||
.then(
|
||||
(result) => Response.json(result),
|
||||
() => Response.json({ error: "Salesforce sync unavailable" }, { status: 503 }),
|
||||
)
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import { i18n } from "~/i18n"
|
|||
import { localeFromRequest } from "~/lib/language"
|
||||
import { createLead } from "~/lib/salesforce"
|
||||
import { inferenceSpendOptions } from "~/lib/inference-spend"
|
||||
import { lookupConsoleAccount, type ConsoleAccount } from "~/lib/console-account"
|
||||
|
||||
interface EnterpriseFormData {
|
||||
name: string
|
||||
|
|
@ -61,9 +62,20 @@ function subscribe(email: string, fullName: string) {
|
|||
}
|
||||
|
||||
export async function POST(event: APIEvent) {
|
||||
const dict = i18n(localeFromRequest(event.request))
|
||||
return handleEnterpriseRequest(event.request)
|
||||
}
|
||||
|
||||
export async function handleEnterpriseRequest(
|
||||
request: Request,
|
||||
options: {
|
||||
salesforceSubmit?: (payload: Record<string, unknown>) => Promise<boolean>
|
||||
notifications?: boolean
|
||||
accountLookup?: (email: string) => Promise<ConsoleAccount | null>
|
||||
} = {},
|
||||
) {
|
||||
const dict = i18n(localeFromRequest(request))
|
||||
try {
|
||||
const body = (await event.request.json()) as EnterpriseFormData
|
||||
const body = (await request.json()) as EnterpriseFormData
|
||||
const trap = typeof body.alias === "string" ? body.alias.trim() : ""
|
||||
|
||||
if (trap) {
|
||||
|
|
@ -84,6 +96,11 @@ export async function POST(event: APIEvent) {
|
|||
return Response.json({ error: dict["enterprise.form.error.invalidInferenceSpend"] }, { status: 400 })
|
||||
}
|
||||
|
||||
const account = await (options.accountLookup ?? lookupConsoleAccount)(body.email).catch((err) => {
|
||||
console.error("Failed to look up Console account:", err)
|
||||
return undefined
|
||||
})
|
||||
|
||||
const emailContent = `
|
||||
${body.message}<br><br>
|
||||
${inferenceSpend ? `Current monthly inference spend: ${inferenceSpend.salesforceValue}<br><br>` : ""}
|
||||
|
|
@ -94,31 +111,37 @@ ${body.company ? `${body.company}<br>` : ""}${body.email}<br>
|
|||
${body.phone ? `${body.phone}<br>` : ""}`.trim()
|
||||
|
||||
const [lead, mail, octopus] = await Promise.all([
|
||||
createLead({
|
||||
name: body.name,
|
||||
role: body.role,
|
||||
company: body.company,
|
||||
email: body.email,
|
||||
phone: body.phone,
|
||||
inferenceSpend: inferenceSpend?.salesforceValue,
|
||||
message: body.message,
|
||||
}).catch((err) => {
|
||||
createLead(
|
||||
{
|
||||
name: body.name,
|
||||
role: body.role,
|
||||
company: body.company,
|
||||
email: body.email,
|
||||
phone: body.phone,
|
||||
inferenceSpend: inferenceSpend?.salesforceValue,
|
||||
message: body.message,
|
||||
consoleAccount: account,
|
||||
},
|
||||
options.salesforceSubmit,
|
||||
).catch((err) => {
|
||||
console.error("Failed to create Salesforce lead:", err)
|
||||
return false
|
||||
}),
|
||||
sendEmail({
|
||||
to: Resource.ENTERPRISE_SALES_INBOX_EMAIL.value,
|
||||
subject: `Enterprise Inquiry from ${body.name}`,
|
||||
body: emailContent,
|
||||
replyTo: body.email,
|
||||
}).then(
|
||||
() => true,
|
||||
(err) => {
|
||||
console.error("Failed to send enterprise email:", err)
|
||||
return false
|
||||
},
|
||||
),
|
||||
subscribe(body.email, body.name),
|
||||
options.notifications === false
|
||||
? Promise.resolve(false)
|
||||
: sendEmail({
|
||||
to: Resource.ENTERPRISE_SALES_INBOX_EMAIL.value,
|
||||
subject: `Enterprise Inquiry from ${body.name}`,
|
||||
body: emailContent,
|
||||
replyTo: body.email,
|
||||
}).then(
|
||||
() => true,
|
||||
(err) => {
|
||||
console.error("Failed to send enterprise email:", err)
|
||||
return false
|
||||
},
|
||||
),
|
||||
options.notifications === false ? Promise.resolve(false) : subscribe(body.email, body.name),
|
||||
])
|
||||
|
||||
if (!lead && !mail && !octopus) {
|
||||
|
|
|
|||
38
salesforce/README.md
Normal file
38
salesforce/README.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Console account identity in Salesforce
|
||||
|
||||
This metadata adds the `console signup` Lead Source and these fields on both Lead
|
||||
and Contact:
|
||||
|
||||
- `Has_Console_Account__c` (checkbox)
|
||||
- `Console_Account_ID__c` (non-unique external ID, so an inquiry Lead and a Contact may refer to the same user)
|
||||
- `Console_Signup_Date__c` (account creation timestamp)
|
||||
|
||||
It also adds a Console Account section to the existing Lead and Contact detail
|
||||
layouts, grants field permissions through `Console_Account_Sync`, and maps the three
|
||||
Lead fields to Contact on conversion. Existing records are not backfilled.
|
||||
|
||||
New signup Leads use the lowercased email domain as Company (for example, `acme.com`).
|
||||
Recognized personal-email domains use `Individual`. This is an exact-domain list in
|
||||
`packages/console/app/src/lib/salesforce.ts`, not a legal-company-name lookup.
|
||||
Signup sync preserves existing Lead companies and Contact account associations;
|
||||
Enterprise inquiries use the company supplied by the visitor.
|
||||
|
||||
Before applying to another org, retrieve and preserve that org's current LeadSource
|
||||
values, layouts, and LeadConvertSettings; these checked-in files are the additive
|
||||
changes prepared for the connected OpenCode org.
|
||||
|
||||
From this directory, deployment uses the existing Salesforce CLI authentication:
|
||||
|
||||
```sh
|
||||
sf project deploy start --source-dir force-app --target-org opencode --wait 10
|
||||
sf org assign permset --name Console_Account_Sync --target-org opencode
|
||||
```
|
||||
|
||||
The application integration uses the existing SST Salesforce credentials. Set the
|
||||
same `CONSOLE_CRM_TOKEN` on the SST website and Console API. The SST website
|
||||
links `CONSOLE_CRM_LOOKUP_URL` to the stage's private Console account lookup.
|
||||
|
||||
For local development, run `bun run dev:enterprise` in `packages/console/app` with
|
||||
`CONSOLE_CRM_TOKEN` set and `sf` on PATH. It serves the same Enterprise request and
|
||||
signup-sync handlers on `127.0.0.1:3101`, using the CLI's authenticated transport.
|
||||
It never sends local sales notifications or EmailOctopus subscriptions.
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<LeadConvertSettings xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<allowOwnerChange>true</allowOwnerChange>
|
||||
<objectMapping>
|
||||
<inputObject>Lead</inputObject>
|
||||
<mappingFields>
|
||||
<inputField>Has_Console_Account__c</inputField>
|
||||
<outputField>Has_Console_Account__c</outputField>
|
||||
</mappingFields>
|
||||
<mappingFields>
|
||||
<inputField>Console_Account_ID__c</inputField>
|
||||
<outputField>Console_Account_ID__c</outputField>
|
||||
</mappingFields>
|
||||
<mappingFields>
|
||||
<inputField>Console_Signup_Date__c</inputField>
|
||||
<outputField>Console_Signup_Date__c</outputField>
|
||||
</mappingFields>
|
||||
<outputObject>Contact</outputObject>
|
||||
</objectMapping>
|
||||
<opportunityCreationOptions>VisibleOptional</opportunityCreationOptions>
|
||||
</LeadConvertSettings>
|
||||
|
|
@ -0,0 +1,294 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Layout xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<excludeButtons>DisableCustomerPortal</excludeButtons>
|
||||
<excludeButtons>DisablePartnerPortal</excludeButtons>
|
||||
<excludeButtons>DisablePrmSuperUser</excludeButtons>
|
||||
<excludeButtons>EnablePrmSuperUser</excludeButtons>
|
||||
<excludeButtons>Share</excludeButtons>
|
||||
<layoutSections>
|
||||
<customLabel>true</customLabel><detailHeading>true</detailHeading><editHeading>true</editHeading>
|
||||
<label>Console Account</label>
|
||||
<layoutColumns>
|
||||
<layoutItems><behavior>Readonly</behavior><field>Has_Console_Account__c</field></layoutItems>
|
||||
<layoutItems><behavior>Readonly</behavior><field>Console_Signup_Date__c</field></layoutItems>
|
||||
<layoutItems><behavior>Readonly</behavior><field>Console_Account_ID__c</field></layoutItems>
|
||||
</layoutColumns>
|
||||
<style>OneColumn</style>
|
||||
</layoutSections>
|
||||
<layoutSections>
|
||||
<customLabel>false</customLabel>
|
||||
<detailHeading>false</detailHeading>
|
||||
<editHeading>true</editHeading>
|
||||
<label>Contact Information</label>
|
||||
<layoutColumns>
|
||||
<layoutItems>
|
||||
<behavior>Required</behavior>
|
||||
<field>Name</field>
|
||||
</layoutItems>
|
||||
<layoutItems>
|
||||
<behavior>Required</behavior>
|
||||
<field>AccountId</field>
|
||||
</layoutItems>
|
||||
<layoutItems>
|
||||
<behavior>Edit</behavior>
|
||||
<field>Title</field>
|
||||
</layoutItems>
|
||||
<layoutItems>
|
||||
<behavior>Edit</behavior>
|
||||
<field>Email</field>
|
||||
</layoutItems>
|
||||
<layoutItems>
|
||||
<behavior>Edit</behavior>
|
||||
<field>Phone</field>
|
||||
</layoutItems>
|
||||
<layoutItems>
|
||||
<behavior>Edit</behavior>
|
||||
<field>MobilePhone</field>
|
||||
</layoutItems>
|
||||
</layoutColumns>
|
||||
<layoutColumns>
|
||||
<layoutItems>
|
||||
<behavior>Edit</behavior>
|
||||
<field>OwnerId</field>
|
||||
</layoutItems>
|
||||
<layoutItems>
|
||||
<behavior>Edit</behavior>
|
||||
<field>ReportsToId</field>
|
||||
</layoutItems>
|
||||
<layoutItems>
|
||||
<behavior>Edit</behavior>
|
||||
<field>Department</field>
|
||||
</layoutItems>
|
||||
<layoutItems>
|
||||
<behavior>Edit</behavior>
|
||||
<field>Fax</field>
|
||||
</layoutItems>
|
||||
</layoutColumns>
|
||||
<style>TwoColumnsTopToBottom</style>
|
||||
</layoutSections>
|
||||
<layoutSections>
|
||||
<customLabel>false</customLabel>
|
||||
<detailHeading>true</detailHeading>
|
||||
<editHeading>true</editHeading>
|
||||
<label>Address Information</label>
|
||||
<layoutColumns>
|
||||
<layoutItems>
|
||||
<behavior>Edit</behavior>
|
||||
<field>MailingAddress</field>
|
||||
</layoutItems>
|
||||
</layoutColumns>
|
||||
<layoutColumns/>
|
||||
<style>TwoColumnsTopToBottom</style>
|
||||
</layoutSections>
|
||||
<layoutSections>
|
||||
<customLabel>false</customLabel>
|
||||
<detailHeading>true</detailHeading>
|
||||
<editHeading>true</editHeading>
|
||||
<label>Additional Information</label>
|
||||
<layoutColumns/>
|
||||
<layoutColumns/>
|
||||
<style>TwoColumnsLeftToRight</style>
|
||||
</layoutSections>
|
||||
<layoutSections>
|
||||
<customLabel>false</customLabel>
|
||||
<detailHeading>false</detailHeading>
|
||||
<editHeading>true</editHeading>
|
||||
<label>Description Information</label>
|
||||
<layoutColumns/>
|
||||
<style>OneColumn</style>
|
||||
</layoutSections>
|
||||
<layoutSections>
|
||||
<customLabel>false</customLabel>
|
||||
<detailHeading>true</detailHeading>
|
||||
<editHeading>true</editHeading>
|
||||
<label>System Information</label>
|
||||
<layoutColumns>
|
||||
<layoutItems>
|
||||
<behavior>Readonly</behavior>
|
||||
<field>CreatedById</field>
|
||||
</layoutItems>
|
||||
</layoutColumns>
|
||||
<layoutColumns>
|
||||
<layoutItems>
|
||||
<behavior>Readonly</behavior>
|
||||
<field>LastModifiedById</field>
|
||||
</layoutItems>
|
||||
</layoutColumns>
|
||||
<style>TwoColumnsTopToBottom</style>
|
||||
</layoutSections>
|
||||
<layoutSections>
|
||||
<customLabel>false</customLabel>
|
||||
<detailHeading>false</detailHeading>
|
||||
<editHeading>false</editHeading>
|
||||
<label>Custom Links</label>
|
||||
<layoutColumns>
|
||||
<layoutItems>
|
||||
<customLink>GoogleMaps</customLink>
|
||||
</layoutItems>
|
||||
</layoutColumns>
|
||||
<layoutColumns>
|
||||
<layoutItems>
|
||||
<customLink>GoogleSearch</customLink>
|
||||
</layoutItems>
|
||||
</layoutColumns>
|
||||
<layoutColumns/>
|
||||
<style>CustomLinks</style>
|
||||
</layoutSections>
|
||||
<platformActionList>
|
||||
<actionListContext>Record</actionListContext>
|
||||
<platformActionListItems>
|
||||
<actionName>Edit</actionName>
|
||||
<actionType>StandardButton</actionType>
|
||||
<sortOrder>0</sortOrder>
|
||||
</platformActionListItems>
|
||||
<platformActionListItems>
|
||||
<actionName>Delete</actionName>
|
||||
<actionType>StandardButton</actionType>
|
||||
<sortOrder>1</sortOrder>
|
||||
</platformActionListItems>
|
||||
<platformActionListItems>
|
||||
<actionName>Clone</actionName>
|
||||
<actionType>StandardButton</actionType>
|
||||
<sortOrder>2</sortOrder>
|
||||
</platformActionListItems>
|
||||
<platformActionListItems>
|
||||
<actionName>SendEmail</actionName>
|
||||
<actionType>QuickAction</actionType>
|
||||
<sortOrder>3</sortOrder>
|
||||
</platformActionListItems>
|
||||
<platformActionListItems>
|
||||
<actionName>LogACall</actionName>
|
||||
<actionType>QuickAction</actionType>
|
||||
<sortOrder>4</sortOrder>
|
||||
</platformActionListItems>
|
||||
<platformActionListItems>
|
||||
<actionName>NewTask</actionName>
|
||||
<actionType>QuickAction</actionType>
|
||||
<sortOrder>5</sortOrder>
|
||||
</platformActionListItems>
|
||||
<platformActionListItems>
|
||||
<actionName>NewEvent</actionName>
|
||||
<actionType>QuickAction</actionType>
|
||||
<sortOrder>6</sortOrder>
|
||||
</platformActionListItems>
|
||||
<platformActionListItems>
|
||||
<actionName>EmailHighlightAction</actionName>
|
||||
<actionType>ProductivityAction</actionType>
|
||||
<sortOrder>7</sortOrder>
|
||||
</platformActionListItems>
|
||||
<platformActionListItems>
|
||||
<actionName>FeedItem.ContentNote</actionName>
|
||||
<actionType>QuickAction</actionType>
|
||||
<sortOrder>8</sortOrder>
|
||||
</platformActionListItems>
|
||||
<platformActionListItems>
|
||||
<actionName>FeedItem.TextPost</actionName>
|
||||
<actionType>QuickAction</actionType>
|
||||
<sortOrder>9</sortOrder>
|
||||
</platformActionListItems>
|
||||
<platformActionListItems>
|
||||
<actionName>FeedItem.ContentPost</actionName>
|
||||
<actionType>QuickAction</actionType>
|
||||
<sortOrder>10</sortOrder>
|
||||
</platformActionListItems>
|
||||
</platformActionList>
|
||||
<quickActionList>
|
||||
<quickActionListItems>
|
||||
<quickActionName>FeedItem.TextPost</quickActionName>
|
||||
</quickActionListItems>
|
||||
<quickActionListItems>
|
||||
<quickActionName>FeedItem.ContentPost</quickActionName>
|
||||
</quickActionListItems>
|
||||
<quickActionListItems>
|
||||
<quickActionName>LogACall</quickActionName>
|
||||
</quickActionListItems>
|
||||
<quickActionListItems>
|
||||
<quickActionName>NewTask</quickActionName>
|
||||
</quickActionListItems>
|
||||
<quickActionListItems>
|
||||
<quickActionName>NewEvent</quickActionName>
|
||||
</quickActionListItems>
|
||||
<quickActionListItems>
|
||||
<quickActionName>FeedItem.ContentNote</quickActionName>
|
||||
</quickActionListItems>
|
||||
<quickActionListItems>
|
||||
<quickActionName>FeedItem.LinkPost</quickActionName>
|
||||
</quickActionListItems>
|
||||
<quickActionListItems>
|
||||
<quickActionName>FeedItem.PollPost</quickActionName>
|
||||
</quickActionListItems>
|
||||
<quickActionListItems>
|
||||
<quickActionName>SendEmail</quickActionName>
|
||||
</quickActionListItems>
|
||||
</quickActionList>
|
||||
<relatedLists>
|
||||
<fields>OPPORTUNITY.NAME</fields>
|
||||
<fields>OPPORTUNITY.STAGE_NAME</fields>
|
||||
<fields>OPPORTUNITY.AMOUNT</fields>
|
||||
<fields>OPPORTUNITY.CLOSE_DATE</fields>
|
||||
<relatedList>RelatedOpportunityList</relatedList>
|
||||
</relatedLists>
|
||||
<relatedLists>
|
||||
<fields>QUOTE.QUOTENUMBER</fields>
|
||||
<fields>QUOTE.NAME</fields>
|
||||
<fields>QUOTE.EXPIRATIONDATE</fields>
|
||||
<fields>QUOTE.ISSYNCING</fields>
|
||||
<fields>QUOTE.SUBTOTAL</fields>
|
||||
<fields>QUOTE.TOTALPRICE</fields>
|
||||
<fields>CreatedBy</fields>
|
||||
<relatedList>RelatedQuoteList</relatedList>
|
||||
</relatedLists>
|
||||
<relatedLists>
|
||||
<fields>CASES.CASE_NUMBER</fields>
|
||||
<fields>CASES.STATUS</fields>
|
||||
<fields>CASES.CREATED_DATE_DATE_ONLY</fields>
|
||||
<fields>CASES.SUBJECT</fields>
|
||||
<relatedList>RelatedCaseList</relatedList>
|
||||
</relatedLists>
|
||||
<relatedLists>
|
||||
<fields>CAMPAIGN.NAME</fields>
|
||||
<fields>CAMPAIGN.START_DATE</fields>
|
||||
<fields>CAMPAIGN.CAMPAIGN_TYPE</fields>
|
||||
<fields>CM.STATUS</fields>
|
||||
<fields>CM.RESPONDED</fields>
|
||||
<fields>CM.LAST_UPDATE</fields>
|
||||
<relatedList>RelatedCampaignList</relatedList>
|
||||
</relatedLists>
|
||||
<relatedLists>
|
||||
<relatedList>RelatedFileList</relatedList>
|
||||
</relatedLists>
|
||||
<relatedLists>
|
||||
<relatedList>RelatedContentNoteList</relatedList>
|
||||
</relatedLists>
|
||||
<relatedLists>
|
||||
<fields>TASK.SUBJECT</fields>
|
||||
<fields>TASK.WHAT_NAME</fields>
|
||||
<fields>ACTIVITY.TASK</fields>
|
||||
<fields>TASK.DUE_DATE</fields>
|
||||
<fields>TASK.STATUS</fields>
|
||||
<fields>CORE.USERS.FULL_NAME</fields>
|
||||
<fields>EVENT.LOCATION</fields>
|
||||
<relatedList>RelatedActivityList</relatedList>
|
||||
</relatedLists>
|
||||
<relatedLists>
|
||||
<fields>TASK.SUBJECT</fields>
|
||||
<fields>TASK.WHAT_NAME</fields>
|
||||
<fields>ACTIVITY.TASK</fields>
|
||||
<fields>TASK.DUE_DATE</fields>
|
||||
<fields>EVENT.LOCATION</fields>
|
||||
<fields>CORE.USERS.FULL_NAME</fields>
|
||||
<relatedList>RelatedHistoryList</relatedList>
|
||||
</relatedLists>
|
||||
<relatedObjects>AccountId</relatedObjects>
|
||||
<showEmailCheckbox>false</showEmailCheckbox>
|
||||
<showHighlightsPanel>false</showHighlightsPanel>
|
||||
<showInteractionLogPanel>false</showInteractionLogPanel>
|
||||
<showRunAssignmentRulesCheckbox>false</showRunAssignmentRulesCheckbox>
|
||||
<showSubmitAndAttachButton>false</showSubmitAndAttachButton>
|
||||
<summaryLayout>
|
||||
<masterLabel>00hHn00000diPWw</masterLabel>
|
||||
<sizeX>4</sizeX>
|
||||
<sizeY>0</sizeY>
|
||||
<summaryLayoutStyle>Default</summaryLayoutStyle>
|
||||
</summaryLayout>
|
||||
</Layout>
|
||||
|
|
@ -0,0 +1,293 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Layout xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<excludeButtons>Submit</excludeButtons>
|
||||
<layoutSections>
|
||||
<customLabel>true</customLabel><detailHeading>true</detailHeading><editHeading>true</editHeading>
|
||||
<label>Console Account</label>
|
||||
<layoutColumns>
|
||||
<layoutItems><behavior>Readonly</behavior><field>Has_Console_Account__c</field></layoutItems>
|
||||
<layoutItems><behavior>Readonly</behavior><field>Console_Signup_Date__c</field></layoutItems>
|
||||
<layoutItems><behavior>Readonly</behavior><field>Console_Account_ID__c</field></layoutItems>
|
||||
</layoutColumns>
|
||||
<style>OneColumn</style>
|
||||
</layoutSections>
|
||||
<layoutSections>
|
||||
<customLabel>false</customLabel>
|
||||
<detailHeading>false</detailHeading>
|
||||
<editHeading>false</editHeading>
|
||||
<label>Lead Information</label>
|
||||
<layoutColumns>
|
||||
<layoutItems>
|
||||
<behavior>Required</behavior>
|
||||
<field>Status</field>
|
||||
</layoutItems>
|
||||
<layoutItems>
|
||||
<behavior>Required</behavior>
|
||||
<field>Name</field>
|
||||
</layoutItems>
|
||||
<layoutItems>
|
||||
<behavior>Edit</behavior>
|
||||
<field>Title</field>
|
||||
</layoutItems>
|
||||
<layoutItems>
|
||||
<behavior>Edit</behavior>
|
||||
<field>Email</field>
|
||||
</layoutItems>
|
||||
<layoutItems>
|
||||
<behavior>Edit</behavior>
|
||||
<field>Phone</field>
|
||||
</layoutItems>
|
||||
<layoutItems>
|
||||
<behavior>Edit</behavior>
|
||||
<field>MobilePhone</field>
|
||||
</layoutItems>
|
||||
<layoutItems>
|
||||
<behavior>Edit</behavior>
|
||||
<field>Rating</field>
|
||||
</layoutItems>
|
||||
</layoutColumns>
|
||||
<layoutColumns>
|
||||
<layoutItems>
|
||||
<behavior>Edit</behavior>
|
||||
<field>OwnerId</field>
|
||||
</layoutItems>
|
||||
<layoutItems>
|
||||
<behavior>Edit</behavior>
|
||||
<field>Website</field>
|
||||
</layoutItems>
|
||||
<layoutItems>
|
||||
<behavior>Required</behavior>
|
||||
<field>Company</field>
|
||||
</layoutItems>
|
||||
<layoutItems>
|
||||
<behavior>Edit</behavior>
|
||||
<field>Industry</field>
|
||||
</layoutItems>
|
||||
<layoutItems>
|
||||
<behavior>Edit</behavior>
|
||||
<field>NumberOfEmployees</field>
|
||||
</layoutItems>
|
||||
<layoutItems>
|
||||
<behavior>Edit</behavior>
|
||||
<field>LeadSource</field>
|
||||
</layoutItems>
|
||||
<layoutItems>
|
||||
<behavior>Edit</behavior>
|
||||
<field>Current_Monthly_Inference_Spend__c</field>
|
||||
</layoutItems>
|
||||
<layoutItems>
|
||||
<behavior>Edit</behavior>
|
||||
<field>Description</field>
|
||||
</layoutItems>
|
||||
</layoutColumns>
|
||||
<style>TwoColumnsTopToBottom</style>
|
||||
</layoutSections>
|
||||
<layoutSections>
|
||||
<customLabel>false</customLabel>
|
||||
<detailHeading>true</detailHeading>
|
||||
<editHeading>true</editHeading>
|
||||
<label>Address Information</label>
|
||||
<layoutColumns>
|
||||
<layoutItems>
|
||||
<behavior>Edit</behavior>
|
||||
<field>Address</field>
|
||||
</layoutItems>
|
||||
</layoutColumns>
|
||||
<layoutColumns/>
|
||||
<style>TwoColumnsTopToBottom</style>
|
||||
</layoutSections>
|
||||
<layoutSections>
|
||||
<customLabel>false</customLabel>
|
||||
<detailHeading>true</detailHeading>
|
||||
<editHeading>true</editHeading>
|
||||
<label>Additional Information</label>
|
||||
<layoutColumns/>
|
||||
<layoutColumns/>
|
||||
<style>TwoColumnsLeftToRight</style>
|
||||
</layoutSections>
|
||||
<layoutSections>
|
||||
<customLabel>false</customLabel>
|
||||
<detailHeading>false</detailHeading>
|
||||
<editHeading>true</editHeading>
|
||||
<label>Description Information</label>
|
||||
<layoutColumns/>
|
||||
<style>OneColumn</style>
|
||||
</layoutSections>
|
||||
<layoutSections>
|
||||
<customLabel>false</customLabel>
|
||||
<detailHeading>false</detailHeading>
|
||||
<editHeading>true</editHeading>
|
||||
<label>System Information</label>
|
||||
<layoutColumns>
|
||||
<layoutItems>
|
||||
<behavior>Readonly</behavior>
|
||||
<field>CreatedById</field>
|
||||
</layoutItems>
|
||||
</layoutColumns>
|
||||
<layoutColumns>
|
||||
<layoutItems>
|
||||
<behavior>Readonly</behavior>
|
||||
<field>LastModifiedById</field>
|
||||
</layoutItems>
|
||||
</layoutColumns>
|
||||
<style>TwoColumnsTopToBottom</style>
|
||||
</layoutSections>
|
||||
<layoutSections>
|
||||
<customLabel>false</customLabel>
|
||||
<detailHeading>true</detailHeading>
|
||||
<editHeading>false</editHeading>
|
||||
<label>Custom Links</label>
|
||||
<layoutColumns>
|
||||
<layoutItems>
|
||||
<customLink>GoogleMaps</customLink>
|
||||
</layoutItems>
|
||||
</layoutColumns>
|
||||
<layoutColumns/>
|
||||
<layoutColumns/>
|
||||
<style>CustomLinks</style>
|
||||
</layoutSections>
|
||||
<platformActionList>
|
||||
<actionListContext>Record</actionListContext>
|
||||
<platformActionListItems>
|
||||
<actionName>Edit</actionName>
|
||||
<actionType>StandardButton</actionType>
|
||||
<sortOrder>0</sortOrder>
|
||||
</platformActionListItems>
|
||||
<platformActionListItems>
|
||||
<actionName>Delete</actionName>
|
||||
<actionType>StandardButton</actionType>
|
||||
<sortOrder>1</sortOrder>
|
||||
</platformActionListItems>
|
||||
<platformActionListItems>
|
||||
<actionName>Clone</actionName>
|
||||
<actionType>StandardButton</actionType>
|
||||
<sortOrder>2</sortOrder>
|
||||
</platformActionListItems>
|
||||
<platformActionListItems>
|
||||
<actionName>Convert</actionName>
|
||||
<actionType>StandardButton</actionType>
|
||||
<sortOrder>3</sortOrder>
|
||||
</platformActionListItems>
|
||||
<platformActionListItems>
|
||||
<actionName>FeedItem.ContentNote</actionName>
|
||||
<actionType>QuickAction</actionType>
|
||||
<sortOrder>4</sortOrder>
|
||||
</platformActionListItems>
|
||||
<platformActionListItems>
|
||||
<actionName>LogACall</actionName>
|
||||
<actionType>QuickAction</actionType>
|
||||
<sortOrder>5</sortOrder>
|
||||
</platformActionListItems>
|
||||
<platformActionListItems>
|
||||
<actionName>NewTask</actionName>
|
||||
<actionType>QuickAction</actionType>
|
||||
<sortOrder>6</sortOrder>
|
||||
</platformActionListItems>
|
||||
<platformActionListItems>
|
||||
<actionName>NewEvent</actionName>
|
||||
<actionType>QuickAction</actionType>
|
||||
<sortOrder>7</sortOrder>
|
||||
</platformActionListItems>
|
||||
<platformActionListItems>
|
||||
<actionName>FeedItem.TextPost</actionName>
|
||||
<actionType>QuickAction</actionType>
|
||||
<sortOrder>8</sortOrder>
|
||||
</platformActionListItems>
|
||||
<platformActionListItems>
|
||||
<actionName>FeedItem.ContentPost</actionName>
|
||||
<actionType>QuickAction</actionType>
|
||||
<sortOrder>9</sortOrder>
|
||||
</platformActionListItems>
|
||||
<platformActionListItems>
|
||||
<actionName>SendEmail</actionName>
|
||||
<actionType>QuickAction</actionType>
|
||||
<sortOrder>10</sortOrder>
|
||||
</platformActionListItems>
|
||||
<platformActionListItems>
|
||||
<actionName>EmailHighlightAction</actionName>
|
||||
<actionType>ProductivityAction</actionType>
|
||||
<sortOrder>11</sortOrder>
|
||||
</platformActionListItems>
|
||||
</platformActionList>
|
||||
<quickActionList>
|
||||
<quickActionListItems>
|
||||
<quickActionName>FeedItem.TextPost</quickActionName>
|
||||
</quickActionListItems>
|
||||
<quickActionListItems>
|
||||
<quickActionName>FeedItem.ContentPost</quickActionName>
|
||||
</quickActionListItems>
|
||||
<quickActionListItems>
|
||||
<quickActionName>LogACall</quickActionName>
|
||||
</quickActionListItems>
|
||||
<quickActionListItems>
|
||||
<quickActionName>NewTask</quickActionName>
|
||||
</quickActionListItems>
|
||||
<quickActionListItems>
|
||||
<quickActionName>NewEvent</quickActionName>
|
||||
</quickActionListItems>
|
||||
<quickActionListItems>
|
||||
<quickActionName>FeedItem.LinkPost</quickActionName>
|
||||
</quickActionListItems>
|
||||
<quickActionListItems>
|
||||
<quickActionName>FeedItem.PollPost</quickActionName>
|
||||
</quickActionListItems>
|
||||
<quickActionListItems>
|
||||
<quickActionName>FeedItem.ContentNote</quickActionName>
|
||||
</quickActionListItems>
|
||||
<quickActionListItems>
|
||||
<quickActionName>SendEmail</quickActionName>
|
||||
</quickActionListItems>
|
||||
</quickActionList>
|
||||
<relatedContent>
|
||||
<relatedContentItems>
|
||||
<layoutItem>
|
||||
<behavior>Readonly</behavior>
|
||||
<field>CampaignId</field>
|
||||
</layoutItem>
|
||||
</relatedContentItems>
|
||||
</relatedContent>
|
||||
<relatedLists>
|
||||
<fields>CAMPAIGN.NAME</fields>
|
||||
<fields>CAMPAIGN.START_DATE</fields>
|
||||
<fields>CAMPAIGN.CAMPAIGN_TYPE</fields>
|
||||
<fields>CM.STATUS</fields>
|
||||
<fields>CM.RESPONDED</fields>
|
||||
<fields>CM.LAST_UPDATE</fields>
|
||||
<relatedList>RelatedCampaignList</relatedList>
|
||||
</relatedLists>
|
||||
<relatedLists>
|
||||
<relatedList>RelatedFileList</relatedList>
|
||||
</relatedLists>
|
||||
<relatedLists>
|
||||
<relatedList>RelatedContentNoteList</relatedList>
|
||||
</relatedLists>
|
||||
<relatedLists>
|
||||
<fields>TASK.SUBJECT</fields>
|
||||
<fields>ACTIVITY.TASK</fields>
|
||||
<fields>TASK.DUE_DATE</fields>
|
||||
<fields>TASK.STATUS</fields>
|
||||
<fields>CORE.USERS.FULL_NAME</fields>
|
||||
<fields>EVENT.LOCATION</fields>
|
||||
<relatedList>RelatedActivityList</relatedList>
|
||||
</relatedLists>
|
||||
<relatedLists>
|
||||
<fields>TASK.SUBJECT</fields>
|
||||
<fields>ACTIVITY.TASK</fields>
|
||||
<fields>TASK.DUE_DATE</fields>
|
||||
<fields>EVENT.LOCATION</fields>
|
||||
<fields>CORE.USERS.FULL_NAME</fields>
|
||||
<relatedList>RelatedHistoryList</relatedList>
|
||||
</relatedLists>
|
||||
<runAssignmentRulesDefault>false</runAssignmentRulesDefault>
|
||||
<showEmailCheckbox>false</showEmailCheckbox>
|
||||
<showHighlightsPanel>false</showHighlightsPanel>
|
||||
<showInteractionLogPanel>false</showInteractionLogPanel>
|
||||
<showRunAssignmentRulesCheckbox>true</showRunAssignmentRulesCheckbox>
|
||||
<showSubmitAndAttachButton>false</showSubmitAndAttachButton>
|
||||
<summaryLayout>
|
||||
<masterLabel>00ha500000BShn4</masterLabel>
|
||||
<sizeX>4</sizeX>
|
||||
<sizeY>0</sizeY>
|
||||
<summaryLayoutStyle>Default</summaryLayoutStyle>
|
||||
</summaryLayout>
|
||||
</Layout>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>Console_Account_ID__c</fullName>
|
||||
<description>The stable OpenCode Console user ID. Identifies the person, not their workspace.</description>
|
||||
<externalId>true</externalId>
|
||||
<label>Console Account ID</label>
|
||||
<length>255</length>
|
||||
<required>false</required>
|
||||
<type>Text</type>
|
||||
<unique>false</unique>
|
||||
</CustomField>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>Console_Signup_Date__c</fullName>
|
||||
<description>Creation time of the verified Console account, in UTC.</description>
|
||||
<label>Console Signup Date</label>
|
||||
<required>false</required>
|
||||
<type>DateTime</type>
|
||||
</CustomField>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>Has_Console_Account__c</fullName>
|
||||
<defaultValue>false</defaultValue>
|
||||
<description>True when this person has a verified OpenCode Console account. Existing records are not backfilled.</description>
|
||||
<label>Has Console Account</label>
|
||||
<type>Checkbox</type>
|
||||
</CustomField>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>Console_Account_ID__c</fullName>
|
||||
<description>The stable OpenCode Console user ID. Identifies the person, not their workspace.</description>
|
||||
<externalId>true</externalId>
|
||||
<label>Console Account ID</label>
|
||||
<length>255</length>
|
||||
<required>false</required>
|
||||
<type>Text</type>
|
||||
<unique>false</unique>
|
||||
</CustomField>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>Console_Signup_Date__c</fullName>
|
||||
<description>Creation time of the verified Console account, in UTC.</description>
|
||||
<label>Console Signup Date</label>
|
||||
<required>false</required>
|
||||
<type>DateTime</type>
|
||||
</CustomField>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>Has_Console_Account__c</fullName>
|
||||
<defaultValue>false</defaultValue>
|
||||
<description>True when this person has a verified OpenCode Console account. Existing records are not backfilled.</description>
|
||||
<label>Has Console Account</label>
|
||||
<type>Checkbox</type>
|
||||
</CustomField>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<description>Read and update Console account identity on Leads and Contacts.</description>
|
||||
<fieldPermissions><editable>true</editable><field>Lead.Has_Console_Account__c</field><readable>true</readable></fieldPermissions>
|
||||
<fieldPermissions><editable>true</editable><field>Lead.Console_Account_ID__c</field><readable>true</readable></fieldPermissions>
|
||||
<fieldPermissions><editable>true</editable><field>Lead.Console_Signup_Date__c</field><readable>true</readable></fieldPermissions>
|
||||
<fieldPermissions><editable>true</editable><field>Contact.Has_Console_Account__c</field><readable>true</readable></fieldPermissions>
|
||||
<fieldPermissions><editable>true</editable><field>Contact.Console_Account_ID__c</field><readable>true</readable></fieldPermissions>
|
||||
<fieldPermissions><editable>true</editable><field>Contact.Console_Signup_Date__c</field><readable>true</readable></fieldPermissions>
|
||||
<label>Console Account Sync</label>
|
||||
</PermissionSet>
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<StandardValueSet xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<sorted>false</sorted>
|
||||
<standardValue>
|
||||
<fullName>Advertisement</fullName>
|
||||
<default>false</default>
|
||||
<label>Advertisement</label>
|
||||
</standardValue>
|
||||
<standardValue>
|
||||
<fullName>Customer Event</fullName>
|
||||
<default>false</default>
|
||||
<label>Customer Event</label>
|
||||
</standardValue>
|
||||
<standardValue>
|
||||
<fullName>Employee Referral</fullName>
|
||||
<default>false</default>
|
||||
<label>Employee Referral</label>
|
||||
</standardValue>
|
||||
<standardValue>
|
||||
<fullName>External Referral</fullName>
|
||||
<default>false</default>
|
||||
<label>External Referral</label>
|
||||
</standardValue>
|
||||
<standardValue>
|
||||
<fullName>Google AdWords</fullName>
|
||||
<default>false</default>
|
||||
<label>Google AdWords</label>
|
||||
</standardValue>
|
||||
<standardValue>
|
||||
<fullName>Other</fullName>
|
||||
<default>false</default>
|
||||
<label>Other</label>
|
||||
</standardValue>
|
||||
<standardValue>
|
||||
<fullName>Partner</fullName>
|
||||
<default>false</default>
|
||||
<label>Partner</label>
|
||||
</standardValue>
|
||||
<standardValue>
|
||||
<fullName>Purchased List</fullName>
|
||||
<default>false</default>
|
||||
<label>Purchased List</label>
|
||||
</standardValue>
|
||||
<standardValue>
|
||||
<fullName>Trade Show</fullName>
|
||||
<default>false</default>
|
||||
<label>Trade Show</label>
|
||||
</standardValue>
|
||||
<standardValue>
|
||||
<fullName>Webinar</fullName>
|
||||
<default>false</default>
|
||||
<label>Webinar</label>
|
||||
</standardValue>
|
||||
<standardValue>
|
||||
<fullName>Website</fullName>
|
||||
<default>false</default>
|
||||
<label>Website</label>
|
||||
</standardValue>
|
||||
<standardValue>
|
||||
<fullName>enterprise website form</fullName>
|
||||
<default>false</default>
|
||||
<label>enterprise website form</label>
|
||||
</standardValue>
|
||||
<standardValue>
|
||||
<fullName>console signup</fullName>
|
||||
<default>false</default>
|
||||
<label>console signup</label>
|
||||
</standardValue>
|
||||
</StandardValueSet>
|
||||
6
salesforce/sfdx-project.json
Normal file
6
salesforce/sfdx-project.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"packageDirectories": [{ "path": "force-app", "default": true }],
|
||||
"namespace": "",
|
||||
"sfdcLoginUrl": "https://login.salesforce.com",
|
||||
"sourceApiVersion": "64.0"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue