Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions frontend/src/__tests__/plans.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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' }));
});

Expand Down Expand Up @@ -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();
});

// -------------------------------------------------------------------------
Expand Down
19 changes: 6 additions & 13 deletions frontend/src/plans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1343,22 +1343,15 @@ export async function savePlan(e: Event): Promise<void> {
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();
Expand Down
Loading