diff --git a/frontend/src/__tests__/plans.test.ts b/frontend/src/__tests__/plans.test.ts index 244efce70..d35465c18 100644 --- a/frontend/src/__tests__/plans.test.ts +++ b/frontend/src/__tests__/plans.test.ts @@ -1137,11 +1137,14 @@ describe('Plans Module', () => { await savePlan(event); expect(api.createPlan).toHaveBeenCalled(); + // PR #861: no follow-up setPlanAccounts on the create path. + expect(api.setPlanAccounts).not.toHaveBeenCalled(); expect(mockShowToast).toHaveBeenCalledWith(expect.objectContaining({ message: 'Plan created successfully' })); }); test('updates existing plan when plan ID present', async () => { (api.updatePlan as jest.Mock).mockResolvedValue({}); + (api.setPlanAccounts as jest.Mock).mockResolvedValue(undefined); (api.getPlans as jest.Mock).mockResolvedValue({ plans: [] }); (api.getPlannedPurchases as jest.Mock).mockResolvedValue({ purchases: [] }); (document.getElementById('plan-id') as HTMLInputElement).value = 'plan-123'; @@ -1150,6 +1153,8 @@ describe('Plans Module', () => { await savePlan(event); expect(api.updatePlan).toHaveBeenCalledWith('plan-123', expect.any(Object)); + // PR #861: update path still calls setPlanAccounts (no atomic backend write on PUT). + expect(api.setPlanAccounts).toHaveBeenCalledWith('plan-123', expect.any(Array)); expect(mockShowToast).toHaveBeenCalledWith(expect.objectContaining({ message: 'Plan updated successfully' })); }); @@ -1425,6 +1430,11 @@ describe('Plans Module', () => { '22222222-2222-2222-2222-222222222222', ], })); + // Regression guard for PR #861: the create path must NOT call + // setPlanAccounts -- the backend already persists plan_accounts + // atomically from target_accounts in the POST body (handler_plans.go + // createPlan). A follow-up PUT would be a redundant double-write. + expect(api.setPlanAccounts).not.toHaveBeenCalled(); }); // ------------------------------------------------------------------------- diff --git a/frontend/src/plans.ts b/frontend/src/plans.ts index 7d8f3d18c..369f99078 100644 --- a/frontend/src/plans.ts +++ b/frontend/src/plans.ts @@ -1343,22 +1343,15 @@ export async function savePlan(e: Event): Promise { plan.target_accounts = accountIds; try { - let savedPlanId = planId; if (planId) { await api.updatePlan(planId, plan as unknown as api.CreatePlanRequest); + // Update flow: push the selected account list via the dedicated endpoint. + await api.setPlanAccounts(planId, accountIds); } else { - const created = await api.createPlan(plan as unknown as api.CreatePlanRequest) as unknown as { id: string }; - savedPlanId = created.id; - } - - // On update, the create path's atomic plan_accounts insert doesn't fire - // (we only POST /plans on create). For updates we still need to push the - // selected account list via the dedicated endpoint. On create, we also - // re-push here so that subsequent reselection is reflected even if the - // backend later opens an "atomic-create-only" path that diverges from - // PUT semantics — same call already handles dedupe via DELETE+INSERT. - if (savedPlanId) { - await api.setPlanAccounts(savedPlanId, accountIds); + await api.createPlan(plan as unknown as api.CreatePlanRequest); + // Create flow: backend inserted plan_accounts atomically from + // target_accounts in the POST body (see internal/api/handler_plans.go + // createPlan). No follow-up account-write needed. } closePlanModal();