intelwatch 1.3.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,6 +20,7 @@
20
20
  import { isPro, requirePro, getLimits, gatePro } from '../license.js';
21
21
 
22
22
  // Providers that require Pro license for any API call
23
+ // Note: 'pappers' is Pro-only, but 'annuaire-entreprises' is free — used as FR fallback
23
24
  const PRO_ONLY_PROVIDERS = new Set(['pappers', 'apollo', 'clearbit']);
24
25
 
25
26
  // ── TLD → Country mapping ────────────────────────────────────────────────────
@@ -65,6 +66,9 @@ const PROVIDER_MAP = {
65
66
  // All others → apollo for enrichment (extensible: add 'GB': 'companieshouse', etc.)
66
67
  };
67
68
 
69
+ // France-specific fallback chain: Pappers (deep, paid) → Annuaire Entreprises (basic, free)
70
+ const FR_FALLBACK_CHAIN = ['pappers', 'annuaire-entreprises'];
71
+
68
72
  // Fallback chain for international domains (tried in order, first available wins)
69
73
  const INTL_FALLBACK_CHAIN = ['apollo', 'clearbit', 'opencorporates'];
70
74
 
@@ -176,46 +180,93 @@ async function attemptFranceHandoff(intlProfileData, options = {}) {
176
180
 
177
181
  // French company detected on international TLD — get deep data from Pappers
178
182
  const pappersProvider = providers['pappers'];
179
- if (!pappersProvider || !pappersProvider.isAvailable()) {
180
- return {
181
- data: { ...intlProfileData, _handoff: 'pappers_unavailable' },
182
- handoff: false,
183
- };
183
+ if (pappersProvider && pappersProvider.isAvailable()) {
184
+ const companyName = extractCompanyName(intlProfileData);
185
+ if (!companyName) {
186
+ return {
187
+ data: { ...intlProfileData, _handoff: 'no_company_name' },
188
+ handoff: false,
189
+ };
190
+ }
191
+
192
+ try {
193
+ // Search Pappers by company name to find the SIREN
194
+ const searchResult = await pappersProvider.search(companyName, { count: 1 });
195
+ const topResult = searchResult?.results?.[0];
196
+
197
+ if (!topResult?.siren) {
198
+ return {
199
+ data: { ...intlProfileData, _handoff: 'pappers_no_match' },
200
+ handoff: false,
201
+ };
202
+ }
203
+
204
+ // Get full Pappers profile by SIREN
205
+ const isPreview = options.preview || false;
206
+ const pappersProfile = isPreview
207
+ ? await pappersProvider.getProfile(topResult.siren, { preview: true })
208
+ : await pappersProvider.getProfile(topResult.siren, { preview: false });
209
+
210
+ // Check for 401 → try Annuaire Entreprises fallback
211
+ if (pappersProfile.error && /401|unauthorized|forbidden|key/i.test(pappersProfile.error)) {
212
+ return await attemptAnnuaireFallback(intlProfileData, companyName);
213
+ }
214
+
215
+ const pappersData = pappersProfile?.data;
216
+ const merged = mergeWithPappers(intlProfileData, pappersData);
217
+ return { data: merged, handoff: true };
218
+ } catch {
219
+ return await attemptAnnuaireFallback(intlProfileData, extractCompanyName(intlProfileData));
220
+ }
184
221
  }
185
222
 
186
- const companyName = extractCompanyName(intlProfileData);
187
- if (!companyName) {
223
+ // Pappers unavailable → try Annuaire Entreprises fallback
224
+ return await attemptAnnuaireFallback(intlProfileData, extractCompanyName(intlProfileData));
225
+ }
226
+
227
+ /**
228
+ * Attempt France handoff via Annuaire Entreprises (free fallback).
229
+ * @param {object} intlProfileData
230
+ * @param {string|null} companyName
231
+ * @returns {Promise<{ data: object, handoff: boolean }>}
232
+ */
233
+ async function attemptAnnuaireFallback(intlProfileData, companyName) {
234
+ const annuaireProvider = providers['annuaire-entreprises'];
235
+ if (!annuaireProvider || !companyName) {
188
236
  return {
189
- data: { ...intlProfileData, _handoff: 'no_company_name' },
237
+ data: { ...intlProfileData, _handoff: companyName ? 'annuaire_unavailable' : 'no_company_name' },
190
238
  handoff: false,
191
239
  };
192
240
  }
193
241
 
194
242
  try {
195
- // Search Pappers by company name to find the SIREN
196
- const searchResult = await pappersProvider.search(companyName, { count: 1 });
243
+ const searchResult = await annuaireProvider.search(companyName, { count: 1 });
197
244
  const topResult = searchResult?.results?.[0];
198
245
 
199
246
  if (!topResult?.siren) {
200
247
  return {
201
- data: { ...intlProfileData, _handoff: 'pappers_no_match' },
248
+ data: { ...intlProfileData, _handoff: 'annuaire_no_match' },
202
249
  handoff: false,
203
250
  };
204
251
  }
205
252
 
206
- // Get full Pappers profile by SIREN
207
- const isPreview = options.preview || false;
208
- const pappersProfile = isPreview
209
- ? await pappersProvider.getProfile(topResult.siren, { preview: true })
210
- : await pappersProvider.getProfile(topResult.siren, { preview: false });
253
+ const annuaireProfile = await annuaireProvider.getProfile(topResult.siren, { preview: true });
254
+ const annuaireData = annuaireProfile?.data;
211
255
 
212
- const pappersData = pappersProfile?.data;
213
- const merged = mergeWithPappers(intlProfileData, pappersData);
256
+ if (!annuaireData) {
257
+ return {
258
+ data: { ...intlProfileData, _handoff: 'annuaire_no_data' },
259
+ handoff: false,
260
+ };
261
+ }
214
262
 
263
+ const merged = mergeWithPappers(intlProfileData, annuaireData);
264
+ merged.source = 'annuaire-entreprises+' + (intlProfileData.source || 'international');
265
+ merged._handoff = 'annuaire_fallback';
215
266
  return { data: merged, handoff: true };
216
267
  } catch {
217
268
  return {
218
- data: { ...intlProfileData, _handoff: 'pappers_error' },
269
+ data: { ...intlProfileData, _handoff: 'annuaire_error' },
219
270
  handoff: false,
220
271
  };
221
272
  }
@@ -259,13 +310,28 @@ export function detectCountry(domainOrUrl) {
259
310
 
260
311
  /**
261
312
  * Get the best provider for a domain/country.
313
+ * For France: tries Pappers first, falls back to Annuaire Entreprises (free).
314
+ * For international: uses the INTL_FALLBACK_CHAIN.
262
315
  * @param {string} domainOrUrl
263
316
  * @returns {{ provider: object|null, providerName: string, country: string }}
264
317
  */
265
318
  export function resolveProvider(domainOrUrl) {
266
319
  const country = detectCountry(domainOrUrl);
267
- const mapped = PROVIDER_MAP[country];
268
320
 
321
+ // France: try Pappers first, fallback to Annuaire Entreprises
322
+ if (country === 'FR') {
323
+ for (const name of FR_FALLBACK_CHAIN) {
324
+ const p = providers[name];
325
+ if (p && p.isAvailable()) {
326
+ return { provider: p, providerName: name, country };
327
+ }
328
+ }
329
+ // Last resort: annuaire-entreprises is always available, but be explicit
330
+ const fallback = providers['annuaire-entreprises'];
331
+ return { provider: fallback || null, providerName: 'annuaire-entreprises', country };
332
+ }
333
+
334
+ const mapped = PROVIDER_MAP[country];
269
335
  if (mapped && providers[mapped]) {
270
336
  return { provider: providers[mapped], providerName: mapped, country };
271
337
  }
@@ -291,33 +357,52 @@ export function resolveProvider(domainOrUrl) {
291
357
  * @param {object} options — { count, preview }
292
358
  */
293
359
  export async function searchCompany(query, domainOrUrl, options = {}) {
294
- // ── SIREN/SIRET direct routing → Pappers immediately ──
360
+ // ── SIREN/SIRET direct routing → France (Pappers Annuaire Entreprises fallback) ──
295
361
  if (isSirenOrSiret(query)) {
296
- const pappersP = providers['pappers'];
297
- const providerName = 'pappers';
298
362
  const country = 'FR';
299
363
 
300
- if (!isPro()) {
364
+ // Try Pappers first (if Pro + key available)
365
+ const pappersP = providers['pappers'];
366
+ if (isPro() && pappersP && pappersP.isAvailable()) {
367
+ const results = await pappersP.search(query, options);
368
+ // Check for 401 or auth errors → fallback
369
+ if (results.error && /401|unauthorized|forbidden|key/i.test(results.error)) {
370
+ // Pappers 401 — fall through to Annuaire Entreprises
371
+ } else {
372
+ return { ...results, provider: 'pappers', country, _routing: 'siren_direct' };
373
+ }
374
+ }
375
+
376
+ // Fallback: Annuaire Entreprises (free, always available)
377
+ const annuaireP = providers['annuaire-entreprises'];
378
+ if (annuaireP) {
379
+ const results = await annuaireP.search(query, options);
301
380
  return {
302
- results: [],
303
- provider: providerName,
381
+ ...results,
382
+ provider: 'annuaire-entreprises',
304
383
  country,
305
- error: `Business Data (${providerName}) requires an Intelwatch Pro license.`,
306
- licenseRequired: true,
384
+ _routing: 'siren_direct_fallback',
385
+ _fallbackNote: 'Pappers non disponible, données issues de l\'Annuaire Entreprises (data.gouv.fr)',
307
386
  };
308
387
  }
309
388
 
310
- if (!pappersP || !pappersP.isAvailable()) {
389
+ // Neither provider available
390
+ if (!isPro()) {
311
391
  return {
312
392
  results: [],
313
- provider: providerName,
393
+ provider: 'pappers',
314
394
  country,
315
- error: `${providerName} API key not configured.`,
395
+ error: `Business Data (pappers) requires an Intelwatch Pro license. Annuaire Entreprises fallback not registered.`,
396
+ licenseRequired: true,
316
397
  };
317
398
  }
318
399
 
319
- const results = await pappersP.search(query, options);
320
- return { ...results, provider: providerName, country, _routing: 'siren_direct' };
400
+ return {
401
+ results: [],
402
+ provider: 'pappers',
403
+ country,
404
+ error: `No France provider available. Pappers API key not configured and Annuaire Entreprises not registered.`,
405
+ };
321
406
  }
322
407
 
323
408
  const { provider, providerName, country } = resolveProvider(domainOrUrl);
@@ -363,45 +448,66 @@ export async function searchCompany(query, domainOrUrl, options = {}) {
363
448
  * @param {object} options — { preview }
364
449
  */
365
450
  export async function getCompanyProfile(identifier, domainOrUrl, options = {}) {
366
- // ── SIREN/SIRET direct routing → Pappers immediately ──
451
+ // ── SIREN/SIRET direct routing → France (Pappers Annuaire Entreprises fallback) ──
367
452
  if (isSirenOrSiret(identifier)) {
368
- const pappersP = providers['pappers'];
369
- const providerName = 'pappers';
370
453
  const country = 'FR';
371
454
  const tier = isPro() ? 'pro' : 'free';
372
455
  const isPreview = options.preview || !isPro();
373
456
 
374
- if (!isPro()) {
457
+ // Try Pappers first (if Pro + key available)
458
+ const pappersP = providers['pappers'];
459
+ if (isPro() && pappersP && pappersP.isAvailable()) {
460
+ const profile = await pappersP.getProfile(identifier, { ...options, preview: isPreview });
461
+ // Check for 401 or auth errors → fallback
462
+ if (profile.error && /401|unauthorized|forbidden|key/i.test(profile.error)) {
463
+ // Pappers 401 — fall through to Annuaire Entreprises
464
+ } else {
465
+ return {
466
+ ...profile,
467
+ provider: 'pappers',
468
+ country,
469
+ tier,
470
+ isPreview,
471
+ _routing: 'siren_direct',
472
+ };
473
+ }
474
+ }
475
+
476
+ // Fallback: Annuaire Entreprises (free, no license required)
477
+ const annuaireP = providers['annuaire-entreprises'];
478
+ if (annuaireP) {
479
+ const profile = await annuaireP.getProfile(identifier, { ...options, preview: true });
375
480
  return {
376
- data: null,
377
- provider: providerName,
481
+ ...profile,
482
+ provider: 'annuaire-entreprises',
378
483
  country,
379
- tier,
484
+ tier: 'free',
380
485
  isPreview: true,
381
- error: `Business Data (${providerName}) requires an Intelwatch Pro license.`,
382
- licenseRequired: true,
486
+ _routing: 'siren_direct_fallback',
487
+ _fallbackNote: profile.data?._fallbackNote || 'Pappers non disponible, profil issu de l\'Annuaire Entreprises (data.gouv.fr). Données financières limitées (CA, résultat net). UBO, BODACC et mandats croisés non disponibles.',
383
488
  };
384
489
  }
385
490
 
386
- if (!pappersP || !pappersP.isAvailable()) {
491
+ // Neither provider available
492
+ if (!isPro()) {
387
493
  return {
388
494
  data: null,
389
- provider: providerName,
495
+ provider: 'pappers',
390
496
  country,
391
497
  tier,
392
- isPreview,
393
- error: `${providerName} API key not configured.`,
498
+ isPreview: true,
499
+ error: `Business Data (pappers) requires an Intelwatch Pro license. Annuaire Entreprises fallback not registered.`,
500
+ licenseRequired: true,
394
501
  };
395
502
  }
396
503
 
397
- const profile = await pappersP.getProfile(identifier, { ...options, preview: isPreview });
398
504
  return {
399
- ...profile,
400
- provider: providerName,
505
+ data: null,
506
+ provider: 'pappers',
401
507
  country,
402
508
  tier,
403
509
  isPreview,
404
- _routing: 'siren_direct',
510
+ error: `No France provider available. Pappers API key not configured and Annuaire Entreprises not registered.`,
405
511
  };
406
512
  }
407
513