@@ -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 */
206224export 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