# Known Issue: Compound Autoname Ordering (RESOLVED 2026-04-18)

**Discovered**: 2026-04-18 during local runtime verification of PR #1140.
**Severity**: HIGH (compound autoname path silently skipped for all Quote/Contract/Order).
**Blast radius**: Any object with a compound Name format using cross-record tokens (`{Account.X}-...-{00}`).
**Status**: RESOLVED 2026-04-18 (follow-up PR #1163, #1174, and runtime fixes in commit 8dbff3ed).

## Resolution

Option A was applied: `enrichQuoteFromOpportunity` now threads a skip-set of autonumber-owned field names (see `backend/internal/features/records/quote_create.go:autoNumberFieldSkipSet`), so engine-managed fields remain blank through enrichment and the engine wins at rendering. The same PRs also (a) introduce a `per_prefix` scope so Quote/Order/Contract Name + Account.Account_ID__c reset per `{country-prefix-YYMMDD}` bucket — matching the Apex-trigger convention (`Account_Country_Code__c + '-OF-' + YYMMDD + NN` for Order, etc.), and (b) correct Order.Name's stale `-ORD-` literal to `-OF-`.

Local runtime verification evidence (2026-04-18 on docker TiDB):
- Quote.Name: `SG-QT-260418-01` (cross-record Account.BillingCountry + engine-rendered).
- Contract.Name: `SG-CSA-260418-01` (fixed compound format).
- QuoteNumber: `00000002` (standard counter, incremental).

---

## Symptom

Setup:
1. `_System_Field.auto_number_format` seeded for `Quote.Name` to `'{Account.BillingCountry}-QT-{YY}{MM}{DD}-{00}'`
2. Account with `BillingCountry='JP'` + linked Opportunity exist
3. `POST /services/data/v63.0/sobjects/Quote {"OpportunityId": "<opp-id>"}` (no Name)

Expected:
- `Quote.Name = "JP-QT-260418-01"` (engine renders compound format)
- `Quote.QuoteNumber = "00000001"` (engine renders standard autonumber)

Actual:
- `Quote.QuoteNumber = "00000001"` ✓
- `Quote.Name = "Opp1"` ✗ (= linked Opportunity.Name; **engine never fired for Name**)

## Root cause

`backend/internal/features/records/persistence_insert.go:63-72` — **before** the engine runs, `enrichQuoteFromOpportunity` (line 64) pre-fills Quote.Name from Opportunity.Name:

```go
if strings.EqualFold(objectName, quoteObjectAPIName) && !shouldSkipQuoteBootstrap(ctx) {
    data, sourceOpportunityID, err = ps.enrichQuoteFromOpportunity(ctx, data, currentUser)
    ...
}
```

Later at `persistence_insert.go:142` the engine runs:

```go
if err := ps.assignAutoNumbers(txCtx, objectName, data); err != nil { ... }
```

Inside `assignAutoNumbers` (persistence_service.go:881-884), engine correctly honors `keep_existing` semantics (PRD §3.3):

```go
// Respect keep_existing semantics: do not overwrite a non-blank value
// that the caller or a prior listener set.
if existing, ok := data[fm.FieldAPIName]; ok && !isBlankValue(existing) {
    continue
}
```

Because enrichment ran first and set `data["Name"] = "Opp1"`, engine sees a non-blank existing value and skips autoname rendering entirely.

## Why PR-1a didn't catch this

The deleted `CompoundAutoNumberListener` ran in a different lifecycle phase (BeforeCreate after enrichment but with explicit override logic). When the listener was removed in Wave 4, the PR's wiring to `assignAutoNumbers` silently inherited the `keep_existing` rule without considering this Quote-specific enrichment step.

Unit tests for the engine (parser/renderer/cross-record) pass because they isolate the engine. Integration tests in `reserve_integration_test.go` also pass because they don't exercise the full Quote-enrichment pipeline. The gap only reveals itself with a full backend + REST POST flow — exactly the "agent-browser server-side engine case" we flagged as `PENDING` in `evidence/autonumber-v2/runtime-verify-pending.md`.

## Fix proposals (pick one)

### Option A — narrow enrichment (RECOMMENDED)

Modify `enrichQuoteFromOpportunity` (and any equivalent Order/Contract enricher) to skip fields whose `_System_Field.auto_number_format` is non-empty. Engine remains authoritative for autoname / autonumber.

```go
// Inside enrichQuoteFromOpportunity's field copy loop:
if fieldHasAutoNumberFormat(ctx, ps.metadataService, quoteObjectAPIName, fieldName) {
    continue
}
```

**Pro**: minimal change, respects autonumber engine as SSoT.
**Con**: admin fix; if user passes explicit `Name: "My Quote"` in POST, enrichment wouldn't clobber that either (neutral — engine's keep_existing handles it).

### Option B — engine forces overwrite for string+format fields

Relax the engine's `keep_existing` semantics so `string`-typed fields with `auto_number_format` always overwrite. Only true user-typed `autonumber` fields respect keep_existing.

**Pro**: single-point fix in `persistence_service.go:883`.
**Con**: loses the "admin can pre-set a Name in POST" affordance. Semantic change to PRD §3.3.

### Option C — reorder pipeline (engine first, enrichment second)

Move `assignAutoNumbers` before `enrichQuoteFromOpportunity`, have enrichment skip already-populated fields.

**Pro**: conceptually cleanest.
**Con**: engine reads cross-record values (Account.BillingCountry) that enrichment might mutate; risk of pulling stale snapshots. Larger blast radius.

## Recommended plan

**Option A.** Add a helper `persistence_insert.go` private fn:

```go
func (ps *PersistenceService) fieldHasAutoNumberFormat(ctx context.Context, objectName, fieldName string) bool {
    if ps.autoNumberMeta == nil { return false }
    fields := ps.autoNumberMeta.LookupByObject(objectName)
    for _, f := range fields {
        if strings.EqualFold(f.FieldAPIName, fieldName) {
            return true
        }
    }
    return false
}
```

Then inside `enrichQuoteFromOpportunity`, `enrichOrderFrom…`, etc., guard each copy:

```go
if ps.fieldHasAutoNumberFormat(ctx, "Quote", "Name") {
    // engine owns this field; skip enrichment copy
} else {
    data["Name"] = opp.Name
}
```

Or a batch guard that filters the source-enrichment map to only fields WITHOUT auto_number_format before the merge.

## Test plan for the fix

Integration test `backend/internal/features/records/persistence_insert_autoname_test.go`:

```go
func TestQuoteAutoname_EngineWinsOverOpportunityEnrichment(t *testing.T) {
    // Setup: Quote.Name has auto_number_format "{Account.BillingCountry}-QT-{YY}{MM}{DD}-{00}"
    // Account BillingCountry=JP, Opportunity.Name="Some Opp"
    // POST Quote {OpportunityId}
    // Expect Quote.Name ~ "JP-QT-<YYMMDD>-01", NOT "Some Opp"
}
```

Plus an agent-browser evidence pass once the fix lands.

## Related

- Original PR-1a: #1140 (draft)
- Original regression flag (Wave 4 report): "Quote multi-chain country-code fallback" — same root cause class
- `evidence/autonumber-v2/runtime-verify-pending.md`: this section was explicitly marked PENDING because full-stack engine cases require live DB + backend
