Skip to content

Commit 883734e

Browse files
committed
fix(admin-settings): Paris TZ for active year + GIP skip reason in result
Why: addresses revu-bot review on PR #3279. - `getActiveCampaignYear` compared a system-TZ date (UTC in containers) to Paris-stored `date` columns, so the active year could flip for ~2h each night. Now pins `timeZone: "Europe/Paris"` on `toLocaleDateString`. - `importGipCsvToDb` silently skipped the `gipPublicationDate` write on invalid horodatage or missing `campaign_deadline` row. The return value now carries `gipPublicationDate`, `gipPublicationDateUpdated` and an optional `gipPublicationDateSkipReason`, so both the tRPC mutation and the `/api/gip-mds/import` cron response surface why the date was not stored.
1 parent c79b6b1 commit 883734e

2 files changed

Lines changed: 99 additions & 45 deletions

File tree

packages/app/src/server/db/getGlobalSettings.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ export const GLOBAL_SETTINGS_ID = 1;
1919
*/
2020
export const getActiveCampaignYear = cache(async (): Promise<number> => {
2121
// `.date` columns are stored as "YYYY-MM-DD" in Europe/Paris civil time, so
22-
// we compare them against the local calendar date. `toISOString()` would
23-
// yield a UTC date and flip the result across midnight for anyone not on
24-
// UTC (e.g. Paris in summer is UTC+2 → "tomorrow" for ~2h each night).
25-
// `sv-SE` locale renders dates as ISO "YYYY-MM-DD" in local time.
26-
const today = new Date().toLocaleDateString("sv-SE");
22+
// we compare them against the Paris calendar date. We force the timezone
23+
// explicitly because production containers run in UTC — relying on the
24+
// system locale would flip the result across midnight for ~2h each night
25+
// (Paris in summer is UTC+2). `sv-SE` renders dates as ISO "YYYY-MM-DD".
26+
const today = new Date().toLocaleDateString("sv-SE", {
27+
timeZone: "Europe/Paris",
28+
});
2729

2830
const rows = await db
2931
.select({ year: campaignDeadlines.year })

packages/app/src/server/services/gipMds.ts

Lines changed: 92 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -197,21 +197,44 @@ async function fetchCompanyInfoBatch(
197197
return results;
198198
}
199199

200+
/**
201+
* Reason why the SUIT `horodatage` was not stored on `campaign_deadline`.
202+
* Surfaced in the import result so callers (audit log, cron response) have a
203+
* paper trail for partial imports.
204+
*/
205+
export type GipPublicationSkipReason =
206+
| "invalid_horodatage"
207+
| "no_campaign_deadline_row";
208+
209+
export type GipImportResult = {
210+
year: number;
211+
rowCount: number;
212+
gipPublicationDate: string | null;
213+
gipPublicationDateUpdated: boolean;
214+
gipPublicationDateSkipReason?: GipPublicationSkipReason;
215+
};
216+
200217
/**
201218
* Import GIP MDS CSV content into the database.
202219
* Parses the CSV and upserts all rows for the given year.
203220
* Creates missing companies before inserting GIP data.
204-
* Returns the number of rows imported.
221+
* Returns the row count and whether the `gipPublicationDate` was stored so
222+
* callers can audit partial imports.
205223
*/
206224
export async function importGipCsvToDb(
207225
db: DB,
208226
csvContent: string,
209-
): Promise<{ year: number; rowCount: number }> {
227+
): Promise<GipImportResult> {
210228
const { metadata, rows } = parseGipCsv(csvContent);
211229
const year = yearFromPeriodEnd(metadata.periodEnd);
212230

213231
if (rows.length === 0) {
214-
return { year, rowCount: 0 };
232+
return {
233+
year,
234+
rowCount: 0,
235+
gipPublicationDate: null,
236+
gipPublicationDateUpdated: false,
237+
};
215238
}
216239

217240
// Ensure all referenced companies exist (outside transaction to avoid long locks on Weez calls)
@@ -220,45 +243,74 @@ export async function importGipCsvToDb(
220243
];
221244
await ensureCompaniesExist(db, uniqueSirens);
222245

223-
await db.transaction(async (tx) => {
224-
// Delete existing data for this year before inserting
225-
await tx.delete(gipMdsData).where(eq(gipMdsData.year, year));
226-
227-
// Insert all rows with metadata
228-
await tx.insert(gipMdsData).values(
229-
rows.map((row) => ({
230-
...row,
231-
year,
232-
periodStart: metadata.periodStart,
233-
periodEnd: metadata.periodEnd,
234-
siren: row.siren ?? "",
235-
})),
236-
);
237-
238-
// Record the SUIT `horodatage` as the GIP publication date — but only
239-
// on an EXISTING `campaign_deadline` row. We never synthesise a row
240-
// with placeholder deadlines here: admins must configure the campaign
241-
// first (decl1/decl2 deadlines are NOT NULL and those values must be
242-
// decided by a human, not made up by the import).
243-
if (!ISO_DATE_RE.test(metadata.publicationDate)) {
244-
console.warn(
245-
`[gip-mds/import] Ignoring invalid horodatage "${metadata.publicationDate}" for year ${year}`,
246+
const publicationOutcome = await db.transaction(
247+
async (
248+
tx,
249+
): Promise<
250+
Pick<
251+
GipImportResult,
252+
| "gipPublicationDate"
253+
| "gipPublicationDateUpdated"
254+
| "gipPublicationDateSkipReason"
255+
>
256+
> => {
257+
// Delete existing data for this year before inserting
258+
await tx.delete(gipMdsData).where(eq(gipMdsData.year, year));
259+
260+
// Insert all rows with metadata
261+
await tx.insert(gipMdsData).values(
262+
rows.map((row) => ({
263+
...row,
264+
year,
265+
periodStart: metadata.periodStart,
266+
periodEnd: metadata.periodEnd,
267+
siren: row.siren ?? "",
268+
})),
246269
);
247-
return;
248-
}
249270

250-
const updated = await tx
251-
.update(campaignDeadlines)
252-
.set({ gipPublicationDate: metadata.publicationDate })
253-
.where(eq(campaignDeadlines.year, year))
254-
.returning({ year: campaignDeadlines.year });
271+
// Record the SUIT `horodatage` as the GIP publication date — but only
272+
// on an EXISTING `campaign_deadline` row. We never synthesise a row
273+
// with placeholder deadlines here: admins must configure the campaign
274+
// first (decl1/decl2 deadlines are NOT NULL and those values must be
275+
// decided by a human, not made up by the import).
276+
if (!ISO_DATE_RE.test(metadata.publicationDate)) {
277+
console.warn(
278+
`[gip-mds/import] Ignoring invalid horodatage "${metadata.publicationDate}" for year ${year}`,
279+
);
280+
return {
281+
gipPublicationDate: null,
282+
gipPublicationDateUpdated: false,
283+
gipPublicationDateSkipReason: "invalid_horodatage",
284+
};
285+
}
255286

256-
if (updated.length === 0) {
257-
console.warn(
258-
`[gip-mds/import] No campaign_deadline row for year ${year} — gipPublicationDate not stored. Ask an admin to configure deadlines first.`,
259-
);
260-
}
261-
});
287+
const updated = await tx
288+
.update(campaignDeadlines)
289+
.set({ gipPublicationDate: metadata.publicationDate })
290+
.where(eq(campaignDeadlines.year, year))
291+
.returning({ year: campaignDeadlines.year });
292+
293+
if (updated.length === 0) {
294+
console.warn(
295+
`[gip-mds/import] No campaign_deadline row for year ${year} — gipPublicationDate not stored. Ask an admin to configure deadlines first.`,
296+
);
297+
return {
298+
gipPublicationDate: metadata.publicationDate,
299+
gipPublicationDateUpdated: false,
300+
gipPublicationDateSkipReason: "no_campaign_deadline_row",
301+
};
302+
}
262303

263-
return { year, rowCount: rows.length };
304+
return {
305+
gipPublicationDate: metadata.publicationDate,
306+
gipPublicationDateUpdated: true,
307+
};
308+
},
309+
);
310+
311+
return {
312+
year,
313+
rowCount: rows.length,
314+
...publicationOutcome,
315+
};
264316
}

0 commit comments

Comments
 (0)