Skip to content

Commit dc22f73

Browse files
committed
fix(declaration): prefill step 5 from last declaration containing indicator 7 (#3246)
Previously, the catégories d'emploi prefill on step 5 only looked at year N-1. If the company skipped that year or submitted without indicator 7 (no job categories), nothing was prefilled even when earlier declarations had usable data. fetchPreviousYearJobCategories now walks all previous submitted declarations for the SIREN ordered by year DESC and returns the first one with job categories, regardless of how many years back.
1 parent 0008e82 commit dc22f73

3 files changed

Lines changed: 110 additions & 75 deletions

File tree

packages/app/src/server/api/routers/__tests__/declarationHelpers.test.ts

Lines changed: 74 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -199,51 +199,74 @@ describe("fetchAllCategories", () => {
199199
});
200200

201201
describe("fetchPreviousYearJobCategories", () => {
202-
it("returns null when no submitted declaration exists for previous year", async () => {
202+
type PreviousDeclarationRow = { id: string };
203+
type JobCategoryRow = {
204+
name: string;
205+
detail: string | null;
206+
source: string;
207+
categoryIndex: number;
208+
};
209+
210+
/**
211+
* Builds a mocked Drizzle `tx` where:
212+
* - the first `select()` chain (declarations query) resolves to
213+
* `previousDeclarations` after `.orderBy()`
214+
* - each subsequent `select()` chain (jobCategories query per declaration)
215+
* resolves to the next entry in `jobsPerDeclaration` after `.where()`
216+
*/
217+
const makeTx = (
218+
previousDeclarations: PreviousDeclarationRow[],
219+
jobsPerDeclaration: JobCategoryRow[][],
220+
) => {
221+
let selectCallCount = 0;
222+
const queuedJobs = [...jobsPerDeclaration];
223+
224+
const mockSelect = vi.fn().mockImplementation(() => {
225+
selectCallCount++;
226+
if (selectCallCount === 1) {
227+
const mockOrderBy = vi.fn().mockResolvedValue(previousDeclarations);
228+
const mockWhere = vi.fn().mockReturnValue({ orderBy: mockOrderBy });
229+
const mockFrom = vi.fn().mockReturnValue({ where: mockWhere });
230+
return { from: mockFrom };
231+
}
232+
const mockWhere = vi.fn().mockResolvedValue(queuedJobs.shift() ?? []);
233+
const mockFrom = vi.fn().mockReturnValue({ where: mockWhere });
234+
return { from: mockFrom };
235+
});
236+
237+
return { select: mockSelect } as never;
238+
};
239+
240+
it("returns null when no submitted previous declaration exists", async () => {
203241
const { fetchPreviousYearJobCategories } = await import(
204242
"../declarationHelpers"
205243
);
206244

207-
const mockLimit = vi.fn().mockResolvedValue([]);
208-
const mockWhere = vi.fn().mockReturnValue({ limit: mockLimit });
209-
const mockFrom = vi.fn().mockReturnValue({ where: mockWhere });
210-
const mockSelect = vi.fn().mockReturnValue({ from: mockFrom });
211-
212-
const tx = { select: mockSelect } as never;
245+
const tx = makeTx([], []);
213246

214247
const result = await fetchPreviousYearJobCategories(tx, "123456789", 2026);
215248

216249
expect(result).toBeNull();
217250
});
218251

219-
it("returns null when declaration exists but has no job categories", async () => {
252+
it("returns null when no previous declaration contains job categories", async () => {
220253
const { fetchPreviousYearJobCategories } = await import(
221254
"../declarationHelpers"
222255
);
223256

224-
let selectCallCount = 0;
225-
const mockLimit = vi.fn().mockResolvedValue([{ id: "decl-prev" }]);
226-
const mockWhere = vi.fn().mockImplementation(() => {
227-
selectCallCount++;
228-
if (selectCallCount === 1) return { limit: mockLimit };
229-
return Promise.resolve([]);
230-
});
231-
const mockFrom = vi.fn().mockReturnValue({ where: mockWhere });
232-
const mockSelect = vi.fn().mockReturnValue({ from: mockFrom });
233-
234-
const tx = { select: mockSelect } as never;
257+
const tx = makeTx([{ id: "decl-2025" }, { id: "decl-2024" }], [[], []]);
235258

236259
const result = await fetchPreviousYearJobCategories(tx, "123456789", 2026);
237260

238261
expect(result).toBeNull();
239262
});
240263

241-
it("returns categories sorted by index with source from previous year", async () => {
264+
it("returns categories from the most recent previous declaration", async () => {
242265
const { fetchPreviousYearJobCategories } = await import(
243266
"../declarationHelpers"
244267
);
245268

246-
const mockJobs = [
269+
const mostRecentJobs: JobCategoryRow[] = [
247270
{
248271
name: "Employés",
249272
detail: "Support",
@@ -258,17 +281,7 @@ describe("fetchPreviousYearJobCategories", () => {
258281
},
259282
];
260283

261-
let selectCallCount = 0;
262-
const mockLimit = vi.fn().mockResolvedValue([{ id: "decl-prev" }]);
263-
const mockWhere = vi.fn().mockImplementation(() => {
264-
selectCallCount++;
265-
if (selectCallCount === 1) return { limit: mockLimit };
266-
return Promise.resolve(mockJobs);
267-
});
268-
const mockFrom = vi.fn().mockReturnValue({ where: mockWhere });
269-
const mockSelect = vi.fn().mockReturnValue({ from: mockFrom });
270-
271-
const tx = { select: mockSelect } as never;
284+
const tx = makeTx([{ id: "decl-2025" }], [mostRecentJobs]);
272285

273286
const result = await fetchPreviousYearJobCategories(tx, "123456789", 2026);
274287

@@ -281,31 +294,48 @@ describe("fetchPreviousYearJobCategories", () => {
281294
});
282295
});
283296

284-
it("uses empty string for null detail", async () => {
297+
it("falls back to an earlier year when N-1 has no job categories", async () => {
285298
const { fetchPreviousYearJobCategories } = await import(
286299
"../declarationHelpers"
287300
);
288301

289-
const mockJobs = [
302+
const olderJobs: JobCategoryRow[] = [
290303
{
291304
name: "Cadres",
292-
detail: null,
305+
detail: "Managers",
293306
source: "autre",
294307
categoryIndex: 0,
295308
},
296309
];
297310

298-
let selectCallCount = 0;
299-
const mockLimit = vi.fn().mockResolvedValue([{ id: "decl-prev" }]);
300-
const mockWhere = vi.fn().mockImplementation(() => {
301-
selectCallCount++;
302-
if (selectCallCount === 1) return { limit: mockLimit };
303-
return Promise.resolve(mockJobs);
311+
const tx = makeTx(
312+
[{ id: "decl-2025" }, { id: "decl-2023" }],
313+
[[], olderJobs],
314+
);
315+
316+
const result = await fetchPreviousYearJobCategories(tx, "123456789", 2026);
317+
318+
expect(result).toEqual({
319+
source: "autre",
320+
categories: [{ name: "Cadres", detail: "Managers" }],
304321
});
305-
const mockFrom = vi.fn().mockReturnValue({ where: mockWhere });
306-
const mockSelect = vi.fn().mockReturnValue({ from: mockFrom });
322+
});
307323

308-
const tx = { select: mockSelect } as never;
324+
it("uses empty string for null detail", async () => {
325+
const { fetchPreviousYearJobCategories } = await import(
326+
"../declarationHelpers"
327+
);
328+
329+
const jobs: JobCategoryRow[] = [
330+
{
331+
name: "Cadres",
332+
detail: null,
333+
source: "autre",
334+
categoryIndex: 0,
335+
},
336+
];
337+
338+
const tx = makeTx([{ id: "decl-2025" }], [jobs]);
309339

310340
const result = await fetchPreviousYearJobCategories(tx, "123456789", 2026);
311341

packages/app/src/server/api/routers/declaration.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ export const declarationRouter = createTRPCRouter({
114114

115115
const gipPrefillData = gipRow[0] ? mapGipToFormData(gipRow[0]) : null;
116116

117-
// Fetch N-1 categories for automatic prefilling when step 5 is empty
117+
// Fetch job categories from the most recent previous declaration that
118+
// contains indicator 7, for automatic prefilling when step 5 is empty.
118119
const hasCurrentCategories = (result.jobCategories ?? []).length > 0;
119120
const previousYearCategories = hasCurrentCategories
120121
? null

packages/app/src/server/api/routers/declarationHelpers.ts

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { and, eq } from "drizzle-orm";
1+
import { and, desc, eq, lt } from "drizzle-orm";
22

33
import {
44
declarations,
@@ -97,44 +97,48 @@ export async function fetchPreviousYearJobCategories(
9797
siren: string,
9898
currentYear: number,
9999
) {
100-
const previousYear = currentYear - 1;
101-
102-
const [previousDeclaration] = await tx
100+
// Walk previous submitted declarations from most recent down and pick the
101+
// first one that actually contains indicator 7 (i.e. has job categories).
102+
// A company may skip year N-1 or submit without indicator 7, in which case
103+
// we fall back to earlier years.
104+
const previousDeclarations = await tx
103105
.select({ id: declarations.id })
104106
.from(declarations)
105107
.where(
106108
and(
107109
eq(declarations.siren, siren),
108-
eq(declarations.year, previousYear),
110+
lt(declarations.year, currentYear),
109111
eq(declarations.status, "submitted"),
110112
),
111113
)
112-
.limit(1);
113-
114-
if (!previousDeclaration) return null;
115-
116-
const jobs = await tx
117-
.select({
118-
name: jobCategories.name,
119-
detail: jobCategories.detail,
120-
source: jobCategories.source,
121-
categoryIndex: jobCategories.categoryIndex,
122-
})
123-
.from(jobCategories)
124-
.where(eq(jobCategories.declarationId, previousDeclaration.id));
125-
126-
if (jobs.length === 0) return null;
127-
128-
const sorted = [...jobs].sort((a, b) => a.categoryIndex - b.categoryIndex);
129-
const source = sorted[0]?.source ?? "";
114+
.orderBy(desc(declarations.year));
115+
116+
for (const { id } of previousDeclarations) {
117+
const jobs = await tx
118+
.select({
119+
name: jobCategories.name,
120+
detail: jobCategories.detail,
121+
source: jobCategories.source,
122+
categoryIndex: jobCategories.categoryIndex,
123+
})
124+
.from(jobCategories)
125+
.where(eq(jobCategories.declarationId, id));
126+
127+
if (jobs.length === 0) continue;
128+
129+
const sorted = [...jobs].sort((a, b) => a.categoryIndex - b.categoryIndex);
130+
const source = sorted[0]?.source ?? "";
131+
132+
return {
133+
source,
134+
categories: sorted.map((j) => ({
135+
name: j.name,
136+
detail: j.detail ?? "",
137+
})),
138+
};
139+
}
130140

131-
return {
132-
source,
133-
categories: sorted.map((j) => ({
134-
name: j.name,
135-
detail: j.detail ?? "",
136-
})),
137-
};
141+
return null;
138142
}
139143

140144
type JobCategoryRow = typeof jobCategories.$inferSelect;

0 commit comments

Comments
 (0)