Skip to content

Guide : creer un nouveau profil OPCO

Vue d'ensemble

Creer un profil pour un nouvel OPCO consiste a :

  1. Creer le dossier du profil
  2. Identifier les particularites de l'API
  3. Definir les transforms partages
  4. Definir chaque route
  5. Assembler le profil
  6. Tester
  7. Enregistrer dans le systeme

Etape 1 : creer le dossier

src/profiles/opco-xxx/
  index.ts
  shared-transforms.ts
  get-dossiers.ts
  post-dossiers.ts
  ...

Convention : 1 fichier par route, un fichier pour les transforms partages.

Etape 2 : identifier les particularites

Avant de coder, tester l'API cible pour decouvrir ses divergences :

  • Formats de date : ISO datetime ? YYYY-MM-DD ? DD/MM/YYYY ?
  • Casse des champs : noms en majuscules ? camelCase ? snake_case ?
  • Valeurs null : l'API accepte null ? plante ? ignore ?
  • Enums : typos ? valeurs non documentees ? nommage incoherent ?
  • Structure reponse : paginee ? wrappee ? array brut ?
  • Types : string attendu pour des nombres ? boolean en 0/1 ?
  • Multipart : quels endpoints ? quel nom de champ metadata ?

Comparer les reponses reelles avec la spec (quand elle existe). Le swagger n'est pas fiable -- tester en vrai.

Etape 3 : transforms partages

Definir les transforms reutilisables dans shared-transforms.ts :

typescript
import { t } from '../field-transform.js'

// Exemple : OPCO XXX veut les dates en DD/MM/YYYY
export const frenchDate = t.edit((v) => {
  const [y, m, d] = String(v).split('-')
  return `${d}/${m}/${y}`
})

// Exemple : OPCO XXX a ses propres codes d'etat
export const ETAT_MAPPING: Record<string, string> = {
  'ACCEPTED': 'ENGAGE',
  'PENDING': 'EN_COURS_INSTRUCTION',
}

Etape 4 : definir les routes

Un fichier par route. Importer depuis ../dsl.js et depuis ./shared-transforms.js.

typescript
// post-dossiers.ts
import { defineRoute } from '../dsl.js'
import { frenchDate, ETAT_MAPPING } from './shared-transforms.js'

export const opcoXxxPostDossiers = defineRoute('/dossiers', 'POST', {
  dest: '/api/v2/dossiers',
  rateLimit: { maxPerMinute: 30 },
  requestTransform: (t) => {
    t.transform('contrat.dateDebut', frenchDate)
    t.transform('contrat.dateFin', frenchDate)
    t.stripNulls()
  },
  responseTransform: (t) => {
    t.mapDeep('statut', ETAT_MAPPING)
  },
})

Pour les endpoints multipart :

typescript
export const opcoXxxPostFactures = defineRoute('/factures', 'POST', {
  dest: {
    endpoint: '/api/v2/factures',
    format: 'multipart',
    multipart: { metadata: 'donnees' },
  },
})

Pour les routes composites (multi-etapes) :

typescript
import { defineCompositeRoute } from '../dsl.js'

export const opcoXxxPostDossierComplet = defineCompositeRoute(
  '/dossiers', 'POST',
  [
    {
      dest: '/api/v2/dossiers',
      responseTransform: (t) => { t.next('id') },
    },
    {
      dest: '/api/v2/conventions',
      requestTransform: (t) => {
        t.transform('dossierId', t.previous())
      },
    },
  ],
)

Etape 5 : assembler le profil

typescript
// index.ts
import { defineProfile } from '../dsl.js'
import { opcoXxxPostDossiers } from './post-dossiers.js'
// ... autres routes

export default defineProfile('opco-xxx', {
  sync: { batchSize: 100 },
  poll: { intervalMs: 600_000 },
  routes: [
    opcoXxxPostDossiers,
    // ... autres routes
  ],
})

Etape 6 : tester

Tests unitaires des transforms

Tester les FieldTransform isoles :

typescript
import { frenchDate } from '../opco-xxx/shared-transforms.js'

it('convertit YYYY-MM-DD en DD/MM/YYYY', () => {
  expect(frenchDate.apply('2024-01-15')).toBe('15/01/2024')
})

Tests d'integration des routes

Tester l'aller-retour defineRoute + applyOps :

typescript
import { opcoXxxPostDossiers } from '../opco-xxx/post-dossiers.js'
import { applyOps } from '../engine.js'

it('transforme la requete correctement', () => {
  const input = { contrat: { dateDebut: '2024-01-15', dateFin: null } }
  const result = applyOps(input, opcoXxxPostDossiers.requestOps)
  expect(result.contrat.dateDebut).toBe('15/01/2024')
  expect('dateFin' in result.contrat).toBe(false) // stripNulls
})

Tester contre l'API reelle

Utiliser le profil mock (opco-mock/) comme modele pour les tests d'integration. Comparer les reponses reelles avec les reponses attendues.

Etape 7 : enregistrer

Le profil est charge automatiquement par le ProfileAdapter. Creer l'instance dans la configuration de l'environnement en passant les credentials :

typescript
import profile from './profiles/opco-xxx/index.js'
import { ProfileAdapter } from './profiles/adapter.js'

const adapter = new ProfileAdapter(profile, {
  clientId: env.OPCO_XXX_CLIENT_ID,
  clientSecret: env.OPCO_XXX_CLIENT_SECRET,
  tokenUrl: 'https://api.opco-xxx.fr/oauth/token',
  baseUrl: 'https://api.opco-xxx.fr',
})

Checklist

  • [ ] Dossier src/profiles/opco-xxx/ cree
  • [ ] shared-transforms.ts avec les transforms reutilisables
  • [ ] 1 fichier par route
  • [ ] index.ts assemble le profil avec defineProfile
  • [ ] Tests unitaires des transforms
  • [ ] Tests d'integration des routes
  • [ ] Test contre l'API reelle (ou mock)
  • [ ] Pas de duplication (utiliser shared-transforms.ts)
  • [ ] stripNulls() si l'API ne tolere pas les null