---
status: proposed
source_prd: docs/design/lead-convert-phase1-prd-input-trace.md
source_prd_note: PRD draft was supplied from gitignored personal-workspace; the committed source_prd records the reviewed Phase 1 inputs.
scope: Lead Convert Phase 1 only
related:
  - backend/internal/bootstrap/system_tables.json
  - backend/internal/bootstrap/standard_actions.json
  - backend/internal/bootstrap/standard_field_mappings.json
  - backend/internal/bootstrap/standard_flows/Std_ConvertLead.flow.json
  - backend/internal/bootstrap/standard_flows/Std_SyncOpportunityContactRole.flow.json
  - backend/internal/bootstrap/standard_flows/Std_RelinkLeadChildren.flow.json
  - backend/internal/features/automation/field_mapping_applier.go
  - backend/internal/features/automation/field_default_resolver.go
  - backend/internal/interfaces/rest/standard_action_dispatcher.go
  - backend/internal/controlplane/tenants/pingcap
---

# Lead Convert Phase 1 Metadata-Driven Design

## Purpose

This document turns the Phase 1 Lead Convert PRD into an implementation design
that preserves the NexusCRM metadata-driven boundary:

- platform code is the generic engine
- standard core metadata is safe for a greenfield org
- PingCAP-specific behavior is tenant or org metadata
- user business records remain data

The design intentionally does not add Lead-specific Go code. Any implementation
that needs a Lead-only branch in `backend/internal/features/**`,
`backend/internal/interfaces/**`, or frontend platform code must stop and be
re-scoped.

## Authoritative End State

Phase 1 ships a Salesforce-compatible standard action named `convertLead` that
is executed through the generic standard-action dispatcher and the generic Flow
runtime.

A fresh org with only public standard core metadata must be able to:

1. create or reuse an Account
2. create or reuse a Contact
3. create, reuse, or skip an Opportunity according to action input guards
4. stamp the Lead converted anchors
5. copy standard Lead fields by `_System_ObjectFieldMapping`
6. set standard defaults through `_System_ActionFieldDefault`, flow
   input assignments, and schema defaults
7. reparent CampaignMember records and open Tasks
8. create a primary OpportunityContactRole without asserting a tenant Role
   value
9. reject writes to converted Leads through `_System_ValidationRule`

PingCAP-specific field mappings, picklist values, permission-set names, and
status-timestamp side effects are not part of the standard core. Steady-state
PingCAP behavior is applied only through the PingCAP tenant overlay; an
org-owned data-spec is reserved for one-time remediation of historical rows.

## PRD Interpretation Rules

The PRD is accepted only with these metadata-driven interpretations:

- any top-level statement that mentions OCR `Role = "Decision_Maker"` is a
  PingCAP overlay requirement, not a standard-core requirement
- `Department__c -> Department` is a PingCAP overlay row and must not be added
  to `standard_field_mappings.json`
- `Lead.Description -> {Account, Contact, Opportunity}.Description` is a
  NexusCRM standard-core design choice to preserve descriptive text
  symmetrically across converted target records; it is not claimed as a
  Salesforce-documented mapping row
- grants to Sales, SDR, Sales Ops, or other named org roles are tenant/admin
  operations, not public seed changes
- Phase 1 does not claim deterministic `triggerOrder`
- unknown request inputs are rejected, not silently ignored

## Current Backend Baseline

The current main branch already has most generic substrate needed for Phase 1:

| Area | Current fact | Phase 1 implication |
|------|--------------|---------------------|
| Standard action registry | `convertLead` is seeded in `standard_actions.json` with `ConvertLeads`, atomic execution, input schema, input guard, and `CloseDate` default | Use existing dispatcher path; keep `accountId` optional so the standard action contract supports create-or-reuse Account |
| Dispatcher input validation | `_System_ActionSchema` drives required inputs and rejects unknown fields with `INVALID_FIELD` | PRD must stay fail-closed; no silent-ignore code change |
| Flow execution | `Std_ConvertLead` exists and invokes OCR and relink subflows | Patch flow metadata only, not Go |
| Field mapping | `_System_ObjectFieldMapping` rows are applied in the generic recordCreate pipeline; the records package already has fill-blank semantics over the same table for insert inheritance | Add standard rows and tenant rows as metadata; reused Account/Contact blank-field merge must reuse or extend an existing generic mapping surface before adding any new substrate |
| Action defaults | dispatcher preloads `_System_ActionFieldDefault` on every request; staging verified on 2026-04-24 that the action context propagates through subflows | OCR Role default can be tenant metadata; the implementation PR must add an e2e-api regression guard for subflow default propagation |
| Validation rules | `CreateValidationRule` invalidates metadata cache; record update path evaluates rules with prior values | Post-convert read-only can be a metadata rule |
| Lead status timestamp | #1852 moved the old `Date_Status_Changed__c` behavior into `backend/internal/controlplane/tenants/pingcap` | Do not reintroduce runtime hardcoding |
| triggerOrder | flow loader does not guarantee same-object trigger order | Phase 1 cannot depend on trigger order |

## Repo Evidence Checked

- `backend/internal/bootstrap/standard_actions.json:12` defines the
  `convertLead` action input contract.
- `backend/internal/bootstrap/standard_flows/Std_ConvertLead.flow.json:129`
  routes Account reuse vs create from `accountId` presence.
- `backend/internal/bootstrap/standard_flows/Std_ConvertLead.flow.json:146`
  routes Contact reuse vs create from `contactId` presence.
- `backend/internal/bootstrap/standard_flows/Std_ConvertLead.flow.json:163`
  routes Opportunity skip, reuse, and create.
- `shared/pkg/models/salesforce_flow.go:287` defines the existing Flow
  `FieldMapping` reference on `recordCreate`.
- `backend/internal/features/automation/executor_record_ops.go:24` and
  `backend/internal/features/automation/executor_record_ops.go:108` apply
  `_System_ObjectFieldMapping` rows to `recordCreate`.
- `backend/internal/features/records/insert_inheritance.go:14` defines an
  existing fill-blank applier over `_System_ObjectFieldMapping`.
- `backend/internal/features/permissions/service.go:240` is the canonical
  object CRUD helper:
  `PermissionService.CheckObjectPermissionWithUser(ctx, objectAPIName, operation, user)`.
- `backend/internal/features/records/persistence_insert.go:34` applies the
  `PermCreate` gate through `prepareOperation`.
- `backend/internal/features/records/persistence_service.go:482` applies the
  `PermEdit` gate through `prepareOperation`; line 487 also shows why the
  implementation must not rely on a `SystemContext`/CRUD bypass path for Lead
  Convert.
- `backend/internal/features/automation/executor_record_ops.go:85` routes
  Flow `recordCreate` through `PersistenceService.Insert`; lines 274 and 393
  route Flow `recordUpdate` through `PersistenceService.Update`.
- `docs/design/flow/lead-convert-flow-native-design.md:98` records the prior
  design decision that reused Opportunity preserves existing campaign
  attribution.
- `backend/internal/bootstrap/standard_flows/Std_ConvertLead.flow.json:415`
  invokes `Std_SyncOpportunityContactRole`; line 428 invokes
  `Std_RelinkLeadChildren`.
- `backend/internal/bootstrap/standard_flows/Std_SyncOpportunityContactRole.flow.json:72`
  demotes other primary OCR rows; line 102 creates the primary OCR row.
- `backend/internal/bootstrap/standard_flows/Std_RelinkLeadChildren.flow.json:79`
  updates CampaignMember rows; lines 95 and 111 update open Task rows.

## Salesforce Evidence Checked

- Salesforce Help,
  [Convert Qualified Leads](https://help.salesforce.com/s/articleView?id=sales.leads_convert.htm&language=en_US&type=5):
  standard Lead Convert supports existing-or-new Account, existing-or-new
  Contact, new-or-existing Opportunity, Opportunity skip, and the permission
  baseline of `ConvertLeads` plus Create and Edit on Lead, Account, and
  Contact; Opportunity permissions are required for Opportunity creation or
  reuse according to the chosen Opportunity path.
- Salesforce Help,
  [What happens when I convert leads?](https://help.salesforce.com/s/articleView?id=sales.faq_leads_what_happens_when.htm&language=en_US&type=5):
  conversion creates or associates Account, Contact, and optionally Opportunity
  records; existing Account/Contact data is not overwritten; CampaignMember and
  activity records are attached to the resulting converted records as conversion
  effects.
- Salesforce Help,
  [How are lead fields mapped to other fields during conversion?](https://help.salesforce.com/s/articleView?id=sales.faq_leads_how_are_lead.htm&language=en_US&type=5):
  standard lead fields transfer to standard target fields, custom lead fields use
  admin-authored Lead Field Mapping, and created records use target-object
  default picklist values and record types where applicable.
- Salesforce Help,
  [Lead Conversion Field Mapping table](https://help.salesforce.com/s/articleView?id=sales.faq_leads_how_are_lead.htm&language=en_US&type=5):
  standard Lead Owner maps to Account Owner, Contact Owner, and Opportunity
  Owner; standard Account mappings include Company to Account Name, Lead Source
  to Account Source, Rating to Rating, address fields to Billing address fields,
  and business profile fields such as Phone, Website, Industry, Annual Revenue,
  and Number of Employees.
- Salesforce Help,
  [Guidelines for Mapping Custom Lead Fields for Lead Conversion](https://help.salesforce.com/s/articleView?id=sf.customize_mapleads_guidelines.htm&language=en_US&type=5):
  custom Lead conversion mappings are admin metadata and can target Account,
  Contact, and Opportunity fields subject to type/length/FLS constraints.

## Scope Split

### Track A - Standard Core

Track A is the only public repo implementation slice. It must pass in a
greenfield org without the PingCAP tenant overlay.

Allowed changes:

- `backend/internal/bootstrap/standard_actions.json`
- `backend/internal/bootstrap/standard_field_mappings.json`
- `backend/internal/bootstrap/standard_flows/Std_ConvertLead.flow.json`
- standard validation-rule seed path, implemented through metadata APIs or a
  generic seed mechanism
- frontend Convert modal and action discovery code, as long as it uses metadata
  and permissions rather than profile names
- focused tests for the standard core behavior

Disallowed in Track A:

- `Department__c`
- `Decision_Maker`
- PingCAP, AliCloud, HubSpot, region/legal-entity names, or TiDB used as a
  tenant business/product literal
- Sales, SDR, Sales Ops profile or permission-set names in public seed or
  runtime code
- any `if object == "Lead"` runtime branch in generic engines
- any code branch that special-cases a tenant field, picklist value, or
  permission-set name

### Track B - PingCAP Tenant Overlay

Track B is a private tenant-overlay deployment. It is not a blocker for
standard-core signoff unless the release explicitly includes PingCAP overlay
validation. Steady-state PingCAP rows live under
`backend/internal/controlplane/tenants/pingcap/**`; `data-specs/` is only for
one-time remediation of already-deployed legacy rows and must not be the
authoritative home for re-applied tenant metadata.

Allowed changes:

- idempotent tenant bootstrap rows for `_System_ObjectFieldMapping`
- tenant `_System_ActionFieldDefault` rows
- tenant Flow metadata under `backend/internal/controlplane/tenants/pingcap/**`
- org permission assignment runbook or tenant deploy script

Track B must never be referenced by Track A code.

### Track C - Frontend Modal

The frontend may implement the Convert modal in Phase 1, but it must use
metadata-driven discovery:

- show the Convert button from active standard-action metadata and permission
  checks
- show skip-Opportunity controls only when the `doNotCreateOpportunity` input
  guard says the user has the required permission
- do not branch on profile id, profile name, Sales, SDR, or System Administrator
  labels
- surface backend errors by status code rather than duplicating business rules
  in TypeScript

Frontend convenience checks for Account/Contact/Opportunity consistency are
allowed as pre-submit UX mirrors. The backend remains authoritative.

The Phase 1 frontend rollout may expose only the existing-Account path: users
search for and select an Account, and the modal does not yet offer a "New
Account" branch. This is a UI rollout limitation only. It must not narrow the
standard action schema or the Flow/API contract; backend/API tests still cover
the `accountId`-absent create-Account path so the frontend can add that branch
later without a standard-action contract change.

## Salesforce Parity And Phase 1 UI Delta

Salesforce baseline:

- Lead Convert supports creating or reusing Account.
- Lead Convert supports creating or reusing Contact.
- Lead Convert supports creating, reusing, or skipping Opportunity.
- Lead field mappings fill empty fields when converting into existing Account
  and Contact records.
- Reused Opportunity keeps its existing fields and campaign attribution unless a
  separately verified Salesforce behavior or explicit NexusCRM extension changes
  that contract.

NexusCRM Phase 1 backend/API contract:

- keeps the same create-or-reuse Account, Contact, and Opportunity product
  contract
- keeps custom field and tenant behavior in metadata rather than platform code
- implements existing Account/Contact mapped-field behavior as generic
  fill-blank merge
- preserves reused Opportunity values and campaign attribution by default

Declared Phase 1 UI delta:

- the frontend Convert modal initially exposes only Account search/selection
- the modal does not initially expose the "New Account" branch

Declared Phase 1 permission delta:

- Salesforce's published strict-parity baseline requires `ConvertLeads` plus
  Create and Edit on Lead, Account, and Contact, with Opportunity permission
  requirements depending on whether an Opportunity is created or reused.
- NexusCRM Phase 1 uses request-path guards: Lead converted-anchor stamping
  requires `Lead.Edit`; create branches require target-object Create; reuse
  fill-blank branches require target-object Edit; Opportunity Create/Edit is
  required only when the Opportunity branch is not skipped.
- Surface classification: strict-parity delta. A user who has `Account.Create`
  but not `Account.Edit`, or `Contact.Create` but not `Contact.Edit`, can pass a
  NexusCRM create path that Salesforce would block.
- Justification: Phase 1 authorizes the exact Flow DML graph through metadata
  guards and `PersistenceService.Insert/Update`, avoiding a broader preflight
  deny that would block valid create-only or edit-only metadata paths in this
  implementation.
- Next-tenant impact: tenants migrating from Salesforce must review permission
  sets because NexusCRM Phase 1 can be more granular than Salesforce on Account
  and Contact Create/Edit pairing.
- Verification: dispatcher and flow/API tests must prove each Object CRUD gate
  is read from `_System_ActionInputGuard`/ObjectPermissions and that create-only
  and reuse-only paths are accepted or rejected according to request inputs, not
  profile names or tenant permission-set labels.

Declared Phase 1 standard-core field mapping choice:

- `Lead.Description` propagates to Account, Contact, and Opportunity
  `Description` to preserve user-entered descriptive text symmetrically across
  converted target records. The currently cited Salesforce Lead Conversion
  Field Mapping table does not list Description, so this is a NexusCRM
  standard-core design choice rather than a Salesforce-documented mapping row.
- Surface classification: strict-parity field-mapping delta. Salesforce
  migrators should expect this row unless they disable or override the metadata.
- The behavior remains metadata-driven because it is represented by
  `_System_ObjectFieldMapping` rows that an admin can inspect, disable, or
  override.

Justification:

- the backend/API contract and Flow metadata keep the Salesforce-compatible
  product form
- limiting the current UI surface lets the frontend ship the Account-selection
  path before the New Account modal workflow is finished
- later enabling "New Account" in the frontend should be a UI rollout, not a
  standard action schema or backend contract change

Next-tenant impact:

- API clients and backend tests can use either create or reuse Account
- tenants that need a complete Salesforce-like UI must wait for the frontend
  "New Account" branch or provide their own UI, but they do not need a new
  backend action contract
- tenants that want reused Opportunity field merge need either Salesforce
  evidence for that behavior or a separately declared NexusCRM extension
- tenants migrating from Salesforce must review the standard-core Description
  mapping and can remove or override it through `_System_ObjectFieldMapping`
  metadata if they need stricter Salesforce parity

Verification:

- backend/API tests must cover `accountId` absent -> Account is created
- backend/API tests must cover `accountId` present -> Account is reused
- backend/API tests must cover reused Account and Contact fill-blank mapping
  behavior
- backend/API tests must cover reused Opportunity preserving existing fields and
  campaign attribution unless an explicit action input changes a field
- bootstrap tests must verify the standard-core Description mapping rows by key
  for Account, Contact, and Opportunity, so the declared delta is visible as
  metadata rather than implied by row counts
- frontend tests must cover that the Phase 1 modal only exposes existing
  Account selection

## Standard Core Design

### 1. Action Schema

Keep `convertLead.inputs.accountId` optional in `standard_actions.json`:

```json
{ "apiName": "accountId", "dataType": "reference", "referenceTo": "Account" }
```

Reason:

- Salesforce Lead Convert supports relating the converted Contact to either an
  existing Account or an Account created during conversion.
- The current flow already has `ResolveAccount -> GetAccountById/CreateAccount`
  branches, so the standard action contract should preserve that product shape.
- The Phase 1 frontend can temporarily expose only Account selection, but that
  belongs in Track C UI scope, not in `_System_ActionSchema`.

Rejected options:

- deleting the `CreateAccount` branch from the flow in Phase 1. This would
  weaken the longer-term architecture and force unnecessary flow churn when
  Account creation returns in Phase 2.
- making `accountId` required to match the current frontend rollout. That would
  shrink the standard product contract instead of documenting a UI limitation.

### 2. Existing-Record Merge Semantics

Lead field mappings apply on both create and reuse paths:

- when Account, Contact, or Opportunity is created, mapping rows populate the
  new record before flow assignments and defaults
- when Account or Contact is reused, mapping rows fill blank target fields only
  and must not overwrite existing non-blank target values
- when Opportunity is reused, Phase 1 preserves existing Opportunity fields and
  existing campaign attribution by default; it does not claim mapped-field merge
  for reused Opportunity without Salesforce evidence
- explicit action inputs may still override when the contract names that
  behavior, for example `overwriteLeadSource`; those overrides are separate from
  default mapped-field merge behavior

This must stay generic. The implementation must first try to reuse or extend
the existing Flow `FieldMapping` surface and the existing fill-blank semantics
already implemented over `_System_ObjectFieldMapping` for insert inheritance. A
new independent mapping/update primitive is not allowed unless the
implementation PR provides R3 evidence showing why those existing mechanisms
are insufficient. Any accepted mechanism must consume `_System_ObjectFieldMapping`
rows for `ActionType = "convert"` so admins can change Account/Contact mappings
without a code change and reuse paths remain consistent with create paths.

### 3. Generic Implementation Mechanisms

The implementation PR must make the generic mechanism explicit before changing
runtime behavior.

Object-permission action guards:

- Extend `_System_ActionInputGuard` or equivalent action-guard metadata, not
  `Std_ConvertLead` Go, so a guard row can declare a condition, a required
  object, and a required operation. Phase 1 uses this shape for conditional
  paths such as `Input.opportunityId` requiring `Opportunity.Edit`.
- Runtime evaluates object CRUD with the existing
  `PermissionService.CheckObjectPermissionWithUser(ctx, objectAPIName, operation, user)`
  helper.
- `RequiredPermission` remains for system/custom permissions such as
  `SkipOpportunityOnConvert`; Object CRUD must not be modeled as a fake custom
  permission like `EditOpportunity`.
- Standard object names may appear in standard metadata rows, never in generic
  Go conditionals.

Reuse-path field mappings:

- Extend the existing Flow mapping surface instead of adding a second mapping
  registry: `recordUpdate` may accept `FlowFieldMappingRef` plus a generic
  merge mode such as `fillBlankOnly`.
- Reuse or extract the existing `_System_ObjectFieldMapping` fill-blank
  semantics from insert inheritance where practical.
- Mapping rows remain `_System_ObjectFieldMapping(ActionType = "convert")`; no
  new table, hardcoded field allowlist, or Lead-specific field map is allowed.
- If that extension is infeasible, the implementation PR must provide R3
  evidence before adding any new substrate.

Action-owned internal access:

Reuse-tier decision:

- Tier 1, column on `_System_StandardAction`: rejected because internal-access
  scopes are one-to-many per action and must remain queryable, auditable, and
  independently active/inactive by object and operation.
- Tier 2, extend `_System_ActionFieldDefault`: rejected because action field
  defaults model per-field value assignment, while internal-access scope models a
  per-`(object, operation)` permission exception. A discriminator would merge
  two different semantic shapes into one table.
- Tier 3, hardcode action/object exceptions in Go: rejected by the R7
  no-SObject-name-switch invariant and by the admin-config test.
- Tier 4, new child SObject on `_System_StandardAction`: adopted because it
  keeps the policy as metadata while preserving one fact, one owner, and one
  read path.

Extend standard-action metadata with this child table, not an opaque JSON blob
on `_System_StandardAction`:

  ```text
  _System_StandardActionInternalAccessScope
    StandardActionId  reference(_System_StandardAction)
    ObjectApiName     string
    Operation         picklist(Read, Create, Edit, Delete)
    IsActive          boolean
    Reason            string
  ```

- Rows in this table are L1 standard core, seeded in the platform binary, and
  not admin-editable. `IsActive` is a pack lifecycle flag, not a tenant runtime
  toggle.
- The standard action dispatcher loads active rows for the current action and
  stamps them onto the action execution context before invoking the Flow graph.
  The implementation PR must prove that this stamped context reaches every
  `QueryWithFilter` and `PersistenceService.Insert/Update/Delete` call made by
  synchronous dispatch, asynchronous dispatch if present, subflows, and nested
  Flow record steps.
- Query and persistence paths consult the scoped context through the generic
  helper
  `IsScopedInternalAccess(ctx context.Context, objectAPIName string, op constants.Permission) bool`
  before object CRUD/FLS/RLS evaluation. The helper is read-only, reads the
  current standard action ID from the stamped context rather than from a
  hardcoded action name, and returns true only when an active
  `_System_StandardActionInternalAccessScope` row matches the current action,
  object, and operation. A true return allows the caller to bypass user
  Object CRUD/FLS/RLS for that declared access only.
- This is the mechanism Phase 1 uses for conversion-owned OCR sync and child
  relink reads and writes after the user-visible
  Lead/Account/Contact/Opportunity
  permission gates pass.
- Standard core seeds exactly these Phase 1 rows:

  ```text
  sa_convertlead, OpportunityContactRole, Read
  sa_convertlead, OpportunityContactRole, Create
  sa_convertlead, OpportunityContactRole, Edit
  sa_convertlead, CampaignMember, Read
  sa_convertlead, CampaignMember, Edit
  sa_convertlead, Task, Read
  sa_convertlead, Task, Edit
  ```

- The scoped context must still route through generic query and
  `PersistenceService.Insert/Update` paths so filtering, validation rules, audit
  fields, flow side effects, and the transaction boundary remain intact.
- The generic Flow executor must not run the whole `Std_ConvertLead` graph under
  broad `SystemContext` or a global `BypassesCRUDandFLS()` context.
- Standard object names appear only in `_System_StandardActionInternalAccessScope`
  seed rows. Generic Go must not branch on action name, object name, or Flow
  developer name for this behavior.
- Requiring end users to hold Task, CampaignMember, or OCR CRUD for standard
  Lead Convert would be a declared Salesforce product delta and is not the
  Phase 1 design.

### 4. Standard Field Mappings

Patch `standard_field_mappings.json` with standard-core rows only.

Lead to Account final standard-core rows: 15.

| Mapping | Status | Evidence basis |
|---------|--------|----------------|
| Company to Name with `defaultValue = "Unknown"` | retain | canonical Lead Company to Account Name conversion |
| Phone | retain | standard Lead Conversion Field Mapping table |
| Website | retain | standard Lead Conversion Field Mapping table |
| Industry | retain | standard Lead Conversion Field Mapping table |
| AnnualRevenue | retain | standard Lead Conversion Field Mapping table |
| NumberOfEmployees | retain | standard Lead Conversion Field Mapping table |
| Description | retain | NexusCRM standard-core design choice; preserve descriptive text symmetrically with Contact and Opportunity |
| Rating | retain | standard Lead Conversion Field Mapping table |
| Street to BillingStreet | retain | standard Lead address to Account billing address mapping |
| City to BillingCity | retain | standard Lead address to Account billing address mapping |
| State to BillingState | retain | standard Lead address to Account billing address mapping |
| PostalCode to BillingPostalCode | retain | standard Lead address to Account billing address mapping |
| Country to BillingCountry | retain | standard Lead address to Account billing address mapping |
| OwnerId | add | Salesforce owner parity: converted Account is owned by Lead owner |
| LeadSource to AccountSource | add | standard Lead Source to Account Source conversion |

The `OwnerId` row closes the Salesforce parity gap that a new Account created
from Lead Convert is owned by the Lead owner, not by whichever user happens to
execute the conversion. The `LeadSource -> AccountSource` row requires
standard-core metadata to include or verify the target `Account.AccountSource`
standard field before the mapping row is seeded. `Description` remains standard
core here and on Contact and Opportunity as the declared NexusCRM standard-core
design choice above, not as a Salesforce-documented mapping row.

Lead to Contact final standard-core rows: 26.

Rows already seeded:

- FirstName
- LastName with `defaultValue = "Unknown"`
- Email
- Phone
- MobilePhone
- Title
- LeadSource
- Description
- Street to MailingStreet
- City to MailingCity
- State to MailingState
- PostalCode to MailingPostalCode
- Country to MailingCountry

Rows to add:

- Salutation
- HasOptedOutOfEmail
- DoNotCall
- EmailBouncedDate
- EmailBouncedReason
- OwnerId
- Jigsaw
- JigsawContactId
- StateCode to MailingStateCode
- CountryCode to MailingCountryCode
- Latitude to MailingLatitude
- Longitude to MailingLongitude
- GeocodeAccuracy to MailingGeocodeAccuracy

Lead to Opportunity final standard-core rows: 3.

Rows:

- LeadSource
- Description
- OwnerId

`Lead.Description -> Opportunity.Description` remains standard core under the
same declared NexusCRM design choice as Account and Contact Description
propagation. Any future change to Description propagation must update this
design first and apply the choice consistently to every target object.

### 5. Primary Campaign Source

Patch `Std_ConvertLead.flow.json`, not Go code:

1. add `GetPrimaryCampaignMember` recordLookup
2. filter by `LeadId = input.leadId`
3. sort by `CreatedDate DESC`
4. limit 1
5. assign `$primaryCM.CampaignId` to `CreateOpportunity.CampaignId`
6. route the new-opportunity path through the lookup before `CreateOpportunity`

No exclusion rules, second-newest fallback, or attribution-loss events belong in
Phase 1. Those are follow-up metadata rules after the core path is proven.

### 6. OpportunityContactRole

Keep `Std_SyncOpportunityContactRole.flow.json` tenant-neutral:

- write OpportunityId
- write ContactId
- write IsPrimary = true
- demote other primary rows on the same Opportunity
- preserve an existing pair's Role when promoting it

Do not add `Role = "Decision_Maker"` to the standard subflow.

If an org wants a default Role value, seed:

```text
_System_ActionFieldDefault
  StandardActionId = sa_convertlead
  TargetObject     = OpportunityContactRole
  FieldAPIName     = Role
  ValueFormula     = '<org picklist value>'
  Priority         = 0
  IsActive         = true
```

For string defaults, `ValueFormula` must include the single quotes because the
resolver treats unquoted text as a field reference or formula expression:

```text
ValueFormula = "'Decision_Maker'"   # string literal
ValueFormula = "TODAY() + 30"       # formula expression
ValueFormula = "Decision_Maker"     # invalid for a literal picklist value
```

This was verified on staging on 2026-04-24. The runtime mechanism is:

- `standard_action_dispatcher.go` calls `loadFieldDefaults(action.ID)` per
  request, so tenant-overlay updates do not need the validation-rule cache
  invalidation path
- the dispatcher stamps those defaults on `ctx` with
  `WithActionFieldDefaults`
- `executor_actions.go` derives `childCtx` with `withFlowCall(ctx, ...)` before
  `ExecuteFlow`, so the action defaults cover parent flow and subflows

The implementation PR must add an e2e-api regression guard under
`tests/e2e-api/cases/lead_convert/` that seeds an action-scope default for
`OpportunityContactRole.Role`, runs `convertLead`, and asserts the OCR created
inside `Std_SyncOpportunityContactRole` receives the metadata value. If a future
executor refactor breaks that case, Track B must use another tenant-owned
metadata mechanism, such as a tenant subflow override or tenant BeforeSave
flow. Track A must still not embed `Role = "Decision_Maker"` in standard core.

### 7. Converted Lead Read-Only

Create standard metadata:

```text
ObjectApiName: Lead
Name: Lead_Post_Convert_Readonly
Active: true
Condition: PRIORVALUE(IsConverted) = TRUE
ErrorMessage: This lead has already been converted. No further changes are allowed.
```

The row must be created through `MetadataService.CreateValidationRule`, for
example the metadata validation-rule endpoint, so metadata caches are
invalidated. Direct generic sObject insertion into `_System_ValidationRule` is
not enough unless the deploy step also invalidates or refreshes the cache.

The rule intentionally allows the conversion update itself because the prior
value of `IsConverted` is false during that transaction.

### 8. Permissions

Standard core seeds only permission primitives:

- required permission: `ConvertLeads`
- custom permission: `SkipOpportunityOnConvert`
- input guard: `doNotCreateOpportunity == true` requires
  `SkipOpportunityOnConvert`

NexusCRM Phase 1 enforces the declared request-path permission delta above:

| Path | Required permission |
|------|---------------------|
| Lead update for converted anchors such as `IsConverted`, `ConvertedDate`, `ConvertedAccountId`, `ConvertedContactId`, and `ConvertedOpportunityId` | `ConvertLeads` system permission plus `Lead.Edit` |
| Create Account | `Account.Create` |
| Reuse Account fill-blank merge | `Account.Edit` |
| Create Contact | `Contact.Create` |
| Reuse Contact fill-blank merge | `Contact.Edit` |
| Create Opportunity | `Opportunity.Create` |
| Reuse Opportunity through `opportunityId` | `Opportunity.Edit` |

This is intentionally narrower than Salesforce's published Create+Edit
baseline for Lead, Account, and Contact. The delta is allowed only because it is
declared in "Salesforce Parity And Phase 1 UI Delta" and verified by
request-path tests; removing the delta later requires tightening these metadata
guards and updating the tests in the same PR.

Each Object CRUD requirement must be enforced through the generic
ObjectPermissions/CRUD helper
`PermissionService.CheckObjectPermissionWithUser(ctx, objectAPIName, operation, user)`,
never through Lead-specific Go code, profile id, profile name, or tenant
permission-set name matching. Create and reuse-path field writes must route
through `PersistenceService.Insert/Update` so the existing `prepareOperation`
CRUD gate applies. A new mapping/update path that bypasses this gate is not
allowed unless the implementation PR provides R3 evidence that
`prepareOperation` is insufficient, together with executable tests proving the
new path enforces the same CRUD guarantees.

Standard core must not seed grants to Sales, SDR, Sales Ops, or any named
tenant permission set. Grants are tenant/admin metadata.

### 9. Conversion Bookkeeping Access

Salesforce Lead Convert does not expose separate user CRUD requirements for
the bookkeeping reads and writes that conversion performs after the chosen Account,
Contact, and Opportunity path is authorized. Phase 1 treats these as
conversion-owned internal access:

- `Std_SyncOpportunityContactRole` creates or updates
  OpportunityContactRole rows for the converted Contact and Opportunity.
- `Std_RelinkLeadChildren` relinks CampaignMember rows from Lead to Contact.
- `Std_RelinkLeadChildren` relinks open Task `WhoId` to the converted Contact
  and open Task `WhatId` to the resolved Opportunity or Account.

These reads and writes must be declared and enforced through a generic
standard-action internal-access scope, not through Lead-specific Go. Once the §8
baseline gates pass, the scoped internal-access context may bypass user Object
CRUD/FLS/RLS only for the declared bookkeeping objects and operations inside the
`Std_ConvertLead` execution graph. The runtime must still call the generic query
path and `PersistenceService.Insert/Update`; direct SQL, whole-flow
`SystemContext`, or an unscoped `BypassesCRUDandFLS()` context is not allowed.

This design commits to the `_System_StandardActionInternalAccessScope`
substrate defined in §3. Track A must implement that substrate before OCR sync
or child relink bookkeeping can ship. If a later implementation wants a
different substrate or wants to declare the user-CRUD/RLS requirement as a
NexusCRM product delta, it must update this design in a separate review before
runtime work proceeds.

## PingCAP Tenant Overlay Design

The PingCAP overlay must be idempotent and safe to run after Track A.
Its steady-state source of truth is
`backend/internal/controlplane/tenants/pingcap/**`; a `data-specs/` runbook may
repair historical rows once but must not remain the reapply path for the tenant
defaults or mappings.

Required overlay operations:

1. verify or upsert 96 custom Lead Convert mapping rows with `IsCustomized=1`
2. insert or verify `Lead.Department__c -> Contact.Department` with
   `IsCustomized=1`
3. insert or verify `_System_ActionFieldDefault` for OCR Role =
   `'Decision_Maker'`; the value is a string literal and must keep the single
   quotes in `ValueFormula`
4. keep any additional PingCAP-only Lead-to-Opportunity mappings as tenant
   metadata only
5. verify the #1852 tenant flow behavior for `Date_Status_Changed__c` if the
   PingCAP deployment requires the timestamp side effect during convert
6. apply permission grants to org-specific permission sets outside public seed

The overlay must not assume it is starting from an empty database. Current
environments may already contain many customized mapping rows, so tenant
bootstrap must insert or verify by key rather than blindly inserting duplicates
or overwriting admin-owned rows.

## Runtime Sequence

The Phase 1 standard action executes as:

```text
POST /actions/standard/convertLead
  -> load _System_StandardAction
  -> enforce ConvertLeads
  -> validate _System_ActionSchema
     - accountId optional
     - unknown inputs rejected
  -> evaluate input guards
     - doNotCreateOpportunity requires SkipOpportunityOnConvert
  -> preload _System_ActionFieldDefault rows
  -> ExecuteFlow(Std_ConvertLead) in one transaction
     -> GetLead
     -> reject already converted
     -> if accountId is present: GetAccountById and validate existing Account
     -> if accountId is absent: create Account from Lead mappings through
        PersistenceService.Insert, requiring Account.Create
     -> fill blank mapped fields when reusing Account through
        PersistenceService.Update, requiring Account.Edit
     -> create or reuse Contact
        -> new Contact requires Contact.Create through PersistenceService.Insert
        -> fill blank mapped fields when reusing Contact through
           PersistenceService.Update, requiring Contact.Edit
     -> create, reuse, or skip Opportunity
        -> if opportunityId is present: require Opportunity.Edit through the
           generic ObjectPermissions helper and validate existing Opportunity
        -> if reused Opportunity: preserve existing fields and CampaignId except
           explicit action-input updates
        -> if new Opportunity: lookup newest CampaignMember, set CampaignId,
           and require Opportunity.Create through PersistenceService.Insert
     -> sync OCR if Opportunity exists using declared action-owned
        internal-access scope
     -> relink CampaignMembers and open Tasks using declared action-owned
        internal-access scope
     -> stamp converted anchors on Lead through PersistenceService.Update,
        requiring Lead.Edit
```

The flow must not call external I/O or async email hooks inside the atomic
transaction.

## triggerOrder Policy

Phase 1 does not implement or depend on deterministic `triggerOrder` execution.
The positive Phase 1 guarantee is narrower: trigger execution order for multiple
same-object flows is implementation-defined, and any Phase 1 standard or tenant
flow must be correct regardless of relative order.

If migrated tenant BeforeSave flows interact with Lead Convert, the migration
owner must prove one of these before signoff:

- the flows are order-independent
- the dependent logic is merged into one flow
- a separate generic flow-runtime enhancement implements trigger-order sorting

Any implementation that sorts by flow name, Lead-specific names, or tenant
business rule is not acceptable.

## Unknown Input Policy

Unknown inputs remain fail-closed:

- request keys not present in `_System_ActionSchema` return `INVALID_FIELD`
- the dispatcher must not silently ignore extra fields

This is metadata-driven because the schema rows define the contract. It is also
safer for callers because typos fail immediately.

## Testing Strategy

### Standard Core Tests

Required coverage:

- bootstrap test that `convertLead.accountId` is optional
- flow/API test that omitting `accountId` creates Account through standard
  metadata
- flow/API tests that reused Account and Contact receive mapped Lead values only
  for blank target fields
- bootstrap test that standard Lead to Contact row count is 26 and contains no
  `__c` source fields
- bootstrap test that standard Lead to Account row count is 15 and includes
  `OwnerId -> OwnerId` plus `LeadSource -> AccountSource`
- bootstrap test that standard `Account.AccountSource` metadata exists before
  the Lead-to-Account `LeadSource -> AccountSource` mapping is seeded
- bootstrap test that standard Lead to Opportunity rows are exactly
  `{LeadSource -> LeadSource, Description -> Description, OwnerId -> OwnerId}`
- bootstrap test that standard internal-access scope rows exist only for
  `Std_ConvertLead` bookkeeping objects and operations:
  OpportunityContactRole Read/Create/Edit, CampaignMember Read/Edit, and
  Task Read/Edit
- bootstrap test that `Std_SyncOpportunityContactRole` does not set Role
- flow test for newest CampaignMember to new Opportunity CampaignId
- flow test for no CampaignMember to null CampaignId
- flow/API test: create-Account path fails without `Account.Create`
- flow/API test: reuse-Account fill-blank merge fails without `Account.Edit`
- flow/API test: create-Contact path fails without `Contact.Create`
- flow/API test: reuse-Contact fill-blank merge fails without `Contact.Edit`
- flow/API test: create-Opportunity path fails without `Opportunity.Create`
- flow/API test that the `opportunityId` reuse path fails without generic
  Opportunity Edit permission and succeeds when that ObjectPermissions/CRUD
  grant exists
- flow/API test: Lead converted-anchor stamping fails without `Lead.Edit`
- negative test: each permission gate above is satisfied or denied only by
  ObjectPermissions; switching `ProfileId` with the same ObjectPermissions does
  not change the verdict
- flow/API test: a user with the §8 baseline permissions but without explicit
  Task, CampaignMember, or OpportunityContactRole CRUD can still complete Lead
  Convert and observe OCR sync plus child relink effects
- negative test: action-owned internal-access scope applies only to declared
  objects and operations within `Std_ConvertLead`; an undeclared object or the
  same Flow outside the standard-action context does not inherit the scoped
  bypass
- flow/API test that the stamped internal-access scope reaches synchronous
  standard-action dispatch, subflows, and nested Flow record steps before any
  scoped bookkeeping read or write bypasses user Object CRUD/FLS/RLS
- flow test for existing Opportunity preserving CampaignId and existing fields
  except explicit action-input updates
- flow test for owner inheritance through mappings
- validation-rule test that convert succeeds but post-convert update fails
- dispatcher test that unknown inputs return `INVALID_FIELD`
- fresh-org e2e/API scene for AC1-AC24 without the PingCAP overlay

### PingCAP Overlay Tests

Required coverage:

- tenant overlay verification that `Department__c -> Department` exists and is
  `IsCustomized=1`
- tenant overlay verification that OCR Role default row exists and is
  `IsCustomized=1` or otherwise admin-owned
- custom mapping verification by key, not row count alone
- AC-T5 verification that the 9 PingCAP-specific Lead to Opportunity extension
  mapping rows exist as customized metadata and propagate by key
- #1852 regression scene for `Date_Status_Changed__c` if Convert is expected
  to trigger that side effect in PingCAP

### Metadata-Driven Static Checks

Before implementation signoff, review newly added code and public seed rather
than the whole repository, because the repository already contains legitimate
TiDB infrastructure references and historical test fixtures:

```bash
git diff origin/main...HEAD -- \
  backend/internal/bootstrap \
  backend/internal/features \
  backend/internal/interfaces \
  frontend/src |
rg -n 'Department__c|Decision_Maker|PingCAP|AliCloud|HubSpot|Sales Ops|SDR|TiDB'
```

Expected result:

- no new hits in platform code or public standard seed for Phase 1 tenant
  behavior, except generic database-infrastructure uses of TiDB that are
  manually justified and unrelated to tenant business semantics
- tenant-specific hits allowed under
  `backend/internal/controlplane/tenants/pingcap/**`
- test fixtures and docs are reviewed manually

## Acceptance Gates

### Gate 1 - Standard Core

Run without PingCAP overlay. Must pass:

- AC1-AC24 from the PRD standard-core section
- create-or-reuse Account as standard backend/API contract
- current Phase 1 frontend rollout may pass with existing-Account-only UI, but
  that must be documented as a UI limitation and not as action schema
- two-tenant test: no custom PingCAP fields or picklist values required
- admin-config test: field mappings, Role default, skip-Opp permission, and
  post-convert read-only are all metadata changes
- create paths require `{Account, Contact, Opportunity}.Create`; reuse paths
  require corresponding `.Edit`; Lead converted-anchor stamping requires
  `Lead.Edit`. All gates must use generic ObjectPermissions/CRUD helpers, not
  profile or tenant-name branching.
- conversion bookkeeping reads and writes for OCR, CampaignMember, and Task do
  not add end-user CRUD/RLS requirements beyond the §8 baseline; any scoped
  bypass is declared through `_System_StandardActionInternalAccessScope` and
  still routes through generic query and `PersistenceService` paths

### Gate 2 - PingCAP Overlay

Run only after applying tenant metadata. Must pass:

- PingCAP custom mapping propagation
- `Department__c -> Department`
- AC-T5 custom Lead to Opportunity extension mapping propagation
- OCR Role default
- any PingCAP status-timestamp expectations

Gate 2 failure must not invalidate Gate 1 unless the failure exposes a generic
engine bug.

## Stop Conditions

Pause implementation and re-scope if any of these happen:

- a Phase 1 requirement needs a Lead-specific Go branch
- public standard seed must contain a custom `__c` field to pass standard-core
  tests
- public standard seed must contain a tenant picklist value such as OCR Role
- frontend needs profile-name branching for behavior
- Phase 1 cannot pass without deterministic `triggerOrder`
- validation-rule seeding cannot be done through a cache-safe metadata path
- PingCAP overlay data must be applied to make the greenfield standard-core
  scenario pass

## Review Checklist

Every implementation PR should answer:

1. Business hardcoding: no tenant field, picklist value, profile name, tenant
   table, or tenant product string was added to platform code or public
   standard seed.
2. Salesforce metadata-driven compliance: admins can change field mappings,
   Role defaults, permission grants, validation-rule activation, and tenant
   side effects without a Go or TypeScript change.
3. Refactor discipline: if the PR touches shared records, metadata, flow, or
   permission state, include the R1-R10 evidence required by the refactor
   discipline rules; otherwise state that the trigger does not apply.
4. Data-spec need: confirm the implementation only changes standard metadata
   rows and requires no historical business-record re-projection. If any live
   `_System_ObjectFieldMapping` rows need one-time correction outside normal
   seed reconciliation, provide a `data-specs/` runbook instead of embedding
   cleanup in runtime code.

## Implementation Order

1. Standard-core metadata seed patch:
   - `accountId` optional
   - standard field mapping additions
   - standard `Account.AccountSource` field included or verified before adding
     the Lead-to-Account AccountSource mapping
   - `OwnerId -> OwnerId` and `LeadSource -> AccountSource` added to
     Lead-to-Account
   - `Description -> Opportunity.Description` retained as standard core
2. extend an existing generic mapping surface for reused Account/Contact
   fill-blank merge, or document R3 evidence before adding any new substrate
3. add `_System_StandardActionInternalAccessScope` metadata and runtime context
   support for OCR sync and child relink bookkeeping reads and writes
4. `Std_ConvertLead` flow patch for Account create/reuse, reused-record merge,
   CampaignMember lookup, and CampaignId
5. validation-rule seed path
6. standard-core backend tests
7. frontend modal using metadata discovery and existing-Account-only Phase 1 UI
   rollout; this may start in parallel with backend work once the action schema
   and metadata discovery contract are stable
8. PingCAP tenant overlay rows
9. tenant overlay tests
10. fresh-org plus PingCAP-overlay evidence capture

Do not start Track B as a substitute for Track A. Track A must pass alone.
