fix(audit): residential/tenancies — M28 (unified stage validation), M29 (explicit-disable wins), L31 (active-tenancy warning), L32 (socket event + saveStages tx)

Updated tenancy-auto-create integration test to assert M29 (explicit disable
respected) instead of the old re-enable behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 13:18:28 +02:00
parent 7b74e2314b
commit e7fdf75a6c
9 changed files with 149 additions and 34 deletions

View File

@@ -5,11 +5,11 @@ import { withAuth, withRateLimit } from '@/lib/api/helpers';
import { parseBody } from '@/lib/api/route-helpers';
import { errorResponse } from '@/lib/errors';
import { assertResidentialModuleEnabled } from '@/lib/services/residential-module.service';
import { assertValidStage } from '@/lib/services/residential-stages.service';
import {
archiveResidentialInterest,
updateResidentialInterest,
} from '@/lib/services/residential.service';
import { PIPELINE_STAGES } from '@/lib/validators/residential';
/**
* Synchronous bulk endpoint for the residential interests list - mirrors
@@ -24,7 +24,13 @@ const bulkSchema = z.discriminatedUnion('action', [
z.object({
action: z.literal('change_stage'),
ids: z.array(z.string().min(1)).min(1).max(100),
pipelineStage: z.enum(PIPELINE_STAGES),
// Accept any non-empty string at the schema layer; membership against
// the port's live stage list (built-ins OR admin-customized) is
// enforced at runtime via `assertValidStage` inside the handler. A
// hardcoded enum here would 400 every valid custom stage after an
// admin renames/adds stages, while the per-row PATCH wrote arbitrary
// strings unchecked — this unifies both on one runtime check.
pipelineStage: z.string().min(1),
}),
z.object({
action: z.literal('archive'),
@@ -62,6 +68,18 @@ export const POST = withAuth(
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
// Validate the target stage once up-front against the port's live
// stage list so an invalid stage 400s the whole request instead of
// reporting every row as failed. `updateResidentialInterest` also
// re-checks per row (defense in depth for direct callers).
if (body.action === 'change_stage') {
try {
await assertValidStage(ctx.portId, body.pipelineStage);
} catch (error) {
return errorResponse(error);
}
}
const meta = {
userId: ctx.userId,
portId: ctx.portId,