Frontend Restrictions on Integration Positions
Question
Section titled “Question”Can users modify custom_security for positions from integration sources (Yodlee, etc.)?
Answer: YES, with conditions
Section titled “Answer: YES, with conditions”Key Logic
Section titled “Key Logic”The frontend determines edit permissions based on account.source:
| Field | Manual Account (source = null) | Linked Account (source != null) |
|---|---|---|
| Quantity | Editable | Read-only |
| Price | Editable (custom security only) | Read-only |
| Value | Editable (generic only) | Read-only |
| Cost Basis | Editable | Editable (except Cash) |
| Asset Classification | Editable | Editable ✅ |
Asset Classification Button Visibility
Section titled “Asset Classification Button Visibility”Conditions (line 508-519):
{PositionModel.isAssetClassification(record, securities) && AccountModel.isAssetClassification(account) && ( <AssetClassificationDrawerButton .../> )}PositionModel.isAssetClassification()
Section titled “PositionModel.isAssetClassification()”File: frontend/app/src/application/shared/resources/position.model.ts:31-39
public static isAssetClassification( position: RecursivePartial<IPositionPayload>, securities: ISecurityPayload[],): boolean { return ( position.securityId === null || // Standalone position (including UNRECOGNIZED from integration) securities.map(({ id }) => id).includes(position.securityId as string) // Unknown securities (!?E, !?F) );}AccountModel.isAssetClassification()
Section titled “AccountModel.isAssetClassification()”File: frontend/app/src/application/shared/resources/account.model.ts:402-409
public static isAssetClassification<T extends IVirtualAnnuityAccount>( account: T,): boolean { return ( account.type === 'investment' && account.investmentAccount?.allocationMode === 'auto' );}Critical: No check for account.source! Integration accounts can edit asset classification.
Row Expandable Logic
Section titled “Row Expandable Logic”File: investment-position-list.component.tsx:313-327
const rowExpandable = (record) => { // 1. Morningstar asset classification - always expandable if (checkIfHasMorningstarAssetClassification(account, record)) { return true; }
// 2. Asset classification (Unknown/standalone) - always expandable if ( PositionModel.isAssetClassification(record, securities) && AccountModel.isAssetClassification(account) ) { return true; // ✅ Integration UNRECOGNIZED positions can expand }
// 3. Other cases return ( _.isNil(account.source) || // Manual account (hasCostBasis && !checkIfPositionIsCash(record)) // Linked with cost basis );};For Your Specific Case
Section titled “For Your Specific Case”Your Yodlee integration position:
isManual: false(integration account)securityId: null(UNRECOGNIZED, standalone position)providerAccountId: 29116571(Yodlee)
Can the user modify asset classification?
✅ YES, if:
account.investmentAccount.allocationMode === 'auto'- Position will be expandable (line 317-321)
- Asset Classification Drawer button will appear
What Can Be Modified
Section titled “What Can Be Modified”For integration UNRECOGNIZED positions, users can modify:
| Property | Can Modify | Preserved on Sync |
|---|---|---|
custom_security.type | ❌ | No - overwritten by integration |
custom_security.symbol | ❌ | No - overwritten by integration |
custom_security.name | ❌ | No - overwritten by integration |
custom_security.categoryId | ✅ | Yes - preserved |
custom_security.percentagesByCategoryId | ✅ | Yes - preserved |
custom_security.id (linked security) | ✅ | Yes - preserved |
costBasis | ✅ | Yes - preserved |
Backend Sync Behavior
Section titled “Backend Sync Behavior”File: backend/packages/libs/integrations-core/src/Models/HoldingTrait.php:115-126
$custom_security = array_filter(($position->custom_security ?? []) + [ Security::COLUMN_SYMBOL => $this->symbol, Security::COLUMN_NAME => $this->name, Security::COLUMN_TYPE => $security_type,]);
// Custom security ID and type cannot co-existif (array_key_exists(Security::COLUMN_ID, $custom_security)) { unset($custom_security[Security::COLUMN_TYPE]);}
$position->custom_security = $custom_security;The + operator is left-associative (existing values take precedence):
- Existing
categoryId,percentagesByCategoryId,id→ preserved - New
symbol,name,type→ overwrite if not present
Special Case: Position Becomes Recognized
Section titled “Special Case: Position Becomes Recognized”File: HoldingTrait.php:283-286
If the integration later matches this position to a security:
if ($custom_security !== [] && !Security::isUnknown($this->matched_security_id)) { // Custom security must be removed if the position were to be securitized to anything other than unknown $custom_security = [];}All user customizations will be lost when position transitions from UNRECOGNIZED to RECOGNIZED.
Conclusion
Section titled “Conclusion”Current Behavior
Section titled “Current Behavior”✅ Frontend allows editing asset classification for integration UNRECOGNIZED positions
✅ Backend preserves categoryId and percentagesByCategoryId on sync
❌ Backend overwrites type, symbol, name on sync
❌ All customizations lost if position becomes recognized
Recommendation
Section titled “Recommendation”Consider adding a user warning in the UI:
⚠️ This holding is from an aggregated account. Asset classification will be preserved,but may be removed if the holding is later recognized by the data provider.Related
Section titled “Related”- ENGR-11553 Investigation - Root cause of 500 error
- Backend Sync Logic (if exists)