Skip to content

Commit c15b594

Browse files
authored
Merge branch 'alpha' into fix/audit-cleanup-direct-db
2 parents 365ddc1 + 882d042 commit c15b594

5 files changed

Lines changed: 154 additions & 128 deletions

File tree

packages/app/src/e2e/helpers/db.ts

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -203,48 +203,59 @@ export async function deleteCseOpinions() {
203203
}
204204
}
205205

206+
const PREV_YEAR_DECL_ID_PREFIX = "e2e-prev-year-decl-";
207+
const PREV_YEAR_DECL_ID_SUFFIX = "-000000000000";
208+
const PREV_YEAR_JOB_CATEGORY_IDS = [
209+
"e2e-jobcat-1111-0000-000000000000",
210+
"e2e-jobcat-2222-0000-000000000000",
211+
"e2e-jobcat-3333-0000-000000000000",
212+
] as const;
213+
214+
function previousYearDeclId(yearsBack: number) {
215+
return `${PREV_YEAR_DECL_ID_PREFIX}${String(yearsBack).padStart(4, "0")}${PREV_YEAR_DECL_ID_SUFFIX}`;
216+
}
217+
206218
/**
207-
* Insert a submitted declaration for the previous year (N-1) with job categories,
208-
* so the "Reprendre les catégories de l'année précédente" button appears.
219+
* Insert a submitted declaration `yearsBack` years before the current year with
220+
* job categories (indicator 7). Default `yearsBack = 1` matches the original
221+
* N-1 seed so existing callers are unaffected.
209222
*/
210-
export async function insertPreviousYearDeclaration() {
223+
export async function insertPreviousYearDeclaration(yearsBack = 1) {
211224
const sql = createConnection();
212-
const yearResult =
213-
await sql`SELECT EXTRACT(YEAR FROM CURRENT_DATE)::int - 1 AS previous_year`;
214-
const previousYear = yearResult[0]?.previous_year as number;
215-
const declId = "e2e-prev-year-decl-0000-000000000000";
225+
const yearResult = await sql`
226+
SELECT EXTRACT(YEAR FROM CURRENT_DATE)::int - ${yearsBack} AS target_year
227+
`;
228+
const targetYear = yearResult[0]?.target_year as number;
229+
const declId = previousYearDeclId(yearsBack);
216230

217231
try {
218-
// Get test user id
219232
const users = await sql`
220233
SELECT user_id FROM app_user_company WHERE siren = ${TEST_SIREN} LIMIT 1
221234
`;
222235
const userId = users[0]?.user_id;
223236
if (!userId) return;
224237

225-
// Insert submitted declaration for previous year
226238
await sql`
227239
INSERT INTO app_declaration (id, siren, year, declarant_id, total_women, total_men, current_step, status, created_at, updated_at)
228-
VALUES (${declId}, ${TEST_SIREN}, ${previousYear}, ${userId}, 150, 200, 6, 'submitted', NOW(), NOW())
240+
VALUES (${declId}, ${TEST_SIREN}, ${targetYear}, ${userId}, 150, 200, 6, 'submitted', NOW(), NOW())
229241
ON CONFLICT DO NOTHING
230242
`;
231243

232-
// Insert 3 job categories
233244
const categories = [
234245
{
235-
id: "e2e-jobcat-1111-0000-000000000000",
246+
id: PREV_YEAR_JOB_CATEGORY_IDS[0],
236247
index: 0,
237248
name: "Cadres dirigeants",
238249
detail: "Directeurs et cadres supérieurs",
239250
},
240251
{
241-
id: "e2e-jobcat-2222-0000-000000000000",
252+
id: PREV_YEAR_JOB_CATEGORY_IDS[1],
242253
index: 1,
243254
name: "Ingénieurs et cadres",
244255
detail: "Ingénieurs, chefs de projet, managers",
245256
},
246257
{
247-
id: "e2e-jobcat-3333-0000-000000000000",
258+
id: PREV_YEAR_JOB_CATEGORY_IDS[2],
248259
index: 2,
249260
name: "Techniciens",
250261
detail: "Techniciens qualifiés",
@@ -263,18 +274,13 @@ export async function insertPreviousYearDeclaration() {
263274
}
264275
}
265276

266-
/** Remove the previous year test declaration and its job categories. */
267-
export async function deletePreviousYearDeclaration() {
277+
/** Remove the seeded previous-year declaration at `yearsBack` and its job categories. */
278+
export async function deletePreviousYearDeclaration(yearsBack = 1) {
268279
const sql = createConnection();
269-
const declId = "e2e-prev-year-decl-0000-000000000000";
280+
const declId = previousYearDeclId(yearsBack);
270281

271282
try {
272-
const jobIds = [
273-
"e2e-jobcat-1111-0000-000000000000",
274-
"e2e-jobcat-2222-0000-000000000000",
275-
"e2e-jobcat-3333-0000-000000000000",
276-
];
277-
await sql`DELETE FROM app_employee_category WHERE job_category_id = ANY(${jobIds})`;
283+
await sql`DELETE FROM app_employee_category WHERE job_category_id = ANY(${PREV_YEAR_JOB_CATEGORY_IDS})`;
278284
await sql`DELETE FROM app_job_category WHERE declaration_id = ${declId}`;
279285
await sql`DELETE FROM app_declaration WHERE id = ${declId}`;
280286
} finally {

packages/app/src/e2e/previous-year-categories.e2e.ts

Lines changed: 62 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,51 +14,73 @@ async function goToStep5(page: Page) {
1414
await expect(page.getByText("Étape 5 sur 6")).toBeVisible();
1515
}
1616

17+
async function assertStep5PrefilledWithSeedData(page: Page) {
18+
// Source pre-filled from the previous declaration
19+
await expect(
20+
page.getByRole("combobox", { name: /source utilisée/i }),
21+
).toHaveValue("convention-collective");
22+
23+
// 3 categories pre-filled
24+
await expect(page.getByText("Nombre de catégories : 3")).toBeVisible();
25+
await expect(
26+
page.getByRole("button", { name: /Cadres dirigeants/ }),
27+
).toBeVisible();
28+
await expect(
29+
page.getByRole("button", { name: /Ingénieurs et cadres/ }),
30+
).toBeVisible();
31+
await expect(page.getByRole("button", { name: /Techniciens/ })).toBeVisible();
32+
33+
// Detail pre-filled, numeric fields empty
34+
await page.getByRole("button", { name: /Cadres dirigeants/ }).click();
35+
await expect(page.locator("#cat-0-detail")).toHaveValue(
36+
"Directeurs et cadres supérieurs",
37+
);
38+
await expect(
39+
page.getByRole("textbox", { name: "Effectif femmes, catégorie 1" }),
40+
).toHaveValue("");
41+
await expect(
42+
page.getByRole("textbox", {
43+
name: "Salaire de base annuel femmes, catégorie 1",
44+
}),
45+
).toHaveValue("");
46+
}
47+
1748
test.describe("Previous year categories prefill", () => {
18-
test.beforeAll(async () => {
19-
await resetDeclarationToDraft();
20-
await deleteCurrentYearCategories();
21-
await insertPreviousYearDeclaration();
22-
});
49+
test.describe("N-1 declaration contains indicator 7", () => {
50+
test.beforeAll(async () => {
51+
await resetDeclarationToDraft();
52+
await deleteCurrentYearCategories();
53+
await insertPreviousYearDeclaration(1);
54+
});
2355

24-
test.afterAll(async () => {
25-
await deletePreviousYearDeclaration();
26-
});
56+
test.afterAll(async () => {
57+
await deletePreviousYearDeclaration(1);
58+
});
2759

28-
test("pre-fills N-1 category names, details and source with empty numeric fields", async ({
29-
page,
30-
}) => {
31-
await goToStep5(page);
60+
test("pre-fills category names, details and source with empty numeric fields", async ({
61+
page,
62+
}) => {
63+
await goToStep5(page);
64+
await assertStep5PrefilledWithSeedData(page);
65+
});
66+
});
3267

33-
// Source pre-filled from N-1
34-
await expect(
35-
page.getByRole("combobox", { name: /source utilisée/i }),
36-
).toHaveValue("convention-collective");
68+
test.describe("N-1 skipped, N-2 contains indicator 7", () => {
69+
test.beforeAll(async () => {
70+
await resetDeclarationToDraft();
71+
await deleteCurrentYearCategories();
72+
// Make sure no N-1 seed is left over from a previous run.
73+
await deletePreviousYearDeclaration(1);
74+
await insertPreviousYearDeclaration(2);
75+
});
3776

38-
// 3 categories pre-filled
39-
await expect(page.getByText("Nombre de catégories : 3")).toBeVisible();
40-
await expect(
41-
page.getByRole("button", { name: /Cadres dirigeants/ }),
42-
).toBeVisible();
43-
await expect(
44-
page.getByRole("button", { name: /Ingénieurs et cadres/ }),
45-
).toBeVisible();
46-
await expect(
47-
page.getByRole("button", { name: /Techniciens/ }),
48-
).toBeVisible();
77+
test.afterAll(async () => {
78+
await deletePreviousYearDeclaration(2);
79+
});
4980

50-
// Detail pre-filled, numeric fields empty
51-
await page.getByRole("button", { name: /Cadres dirigeants/ }).click();
52-
await expect(page.locator("#cat-0-detail")).toHaveValue(
53-
"Directeurs et cadres supérieurs",
54-
);
55-
await expect(
56-
page.getByRole("textbox", { name: "Effectif femmes, catégorie 1" }),
57-
).toHaveValue("");
58-
await expect(
59-
page.getByRole("textbox", {
60-
name: "Salaire de base annuel femmes, catégorie 1",
61-
}),
62-
).toHaveValue("");
81+
test("falls back to N-2 when N-1 is missing", async ({ page }) => {
82+
await goToStep5(page);
83+
await assertStep5PrefilledWithSeedData(page);
84+
});
6385
});
6486
});

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

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

201201
describe("fetchPreviousYearJobCategories", () => {
202-
it("returns null when no submitted declaration exists for previous year", async () => {
203-
const { fetchPreviousYearJobCategories } = await import(
204-
"../declarationHelpers"
205-
);
206-
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;
213-
214-
const result = await fetchPreviousYearJobCategories(tx, "123456789", 2026);
202+
type JobCategoryRow = {
203+
name: string;
204+
detail: string | null;
205+
source: string;
206+
categoryIndex: number;
207+
};
215208

216-
expect(result).toBeNull();
217-
});
209+
/**
210+
* Build a mocked `tx` with two select chains:
211+
* 1. declarations probe (innerJoin jobCategories, limit 1) → id or nothing
212+
* 2. jobCategories fetch for that id → rows
213+
*
214+
* Each chain resolves at its terminal call (`.limit()` / `.where()`), so
215+
* the mock is driven by the data the query is expected to return, not by
216+
* internal call-order counters.
217+
*/
218+
const makeTx = (
219+
declarationId: string | null,
220+
jobs: JobCategoryRow[] = [],
221+
) => {
222+
const declarationChain = {
223+
from: () => ({
224+
innerJoin: () => ({
225+
where: () => ({
226+
orderBy: () => ({
227+
limit: () =>
228+
Promise.resolve(declarationId ? [{ id: declarationId }] : []),
229+
}),
230+
}),
231+
}),
232+
}),
233+
};
234+
235+
const jobCategoriesChain = {
236+
from: () => ({
237+
where: () => Promise.resolve(jobs),
238+
}),
239+
};
240+
241+
const queue = [declarationChain, jobCategoriesChain];
242+
return { select: () => queue.shift() } as never;
243+
};
218244

219-
it("returns null when declaration exists but has no job categories", async () => {
245+
it("returns null when no previous declaration contains indicator 7", async () => {
220246
const { fetchPreviousYearJobCategories } = await import(
221247
"../declarationHelpers"
222248
);
223249

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;
235-
236-
const result = await fetchPreviousYearJobCategories(tx, "123456789", 2026);
250+
const result = await fetchPreviousYearJobCategories(
251+
makeTx(null),
252+
"123456789",
253+
2026,
254+
);
237255

238256
expect(result).toBeNull();
239257
});
240258

241-
it("returns categories sorted by index with source from previous year", async () => {
259+
it("returns categories from the most recent qualifying declaration", async () => {
242260
const { fetchPreviousYearJobCategories } = await import(
243261
"../declarationHelpers"
244262
);
245263

246-
const mockJobs = [
264+
const tx = makeTx("decl-2024", [
247265
{
248266
name: "Employés",
249267
detail: "Support",
@@ -256,19 +274,7 @@ describe("fetchPreviousYearJobCategories", () => {
256274
source: "convention-collective",
257275
categoryIndex: 0,
258276
},
259-
];
260-
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;
277+
]);
272278

273279
const result = await fetchPreviousYearJobCategories(tx, "123456789", 2026);
274280

@@ -286,26 +292,14 @@ describe("fetchPreviousYearJobCategories", () => {
286292
"../declarationHelpers"
287293
);
288294

289-
const mockJobs = [
295+
const tx = makeTx("decl-2024", [
290296
{
291297
name: "Cadres",
292298
detail: null,
293299
source: "autre",
294300
categoryIndex: 0,
295301
},
296-
];
297-
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);
304-
});
305-
const mockFrom = vi.fn().mockReturnValue({ where: mockWhere });
306-
const mockSelect = vi.fn().mockReturnValue({ from: mockFrom });
307-
308-
const tx = { select: mockSelect } as never;
302+
]);
309303

310304
const result = await fetchPreviousYearJobCategories(tx, "123456789", 2026);
311305

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

0 commit comments

Comments
 (0)