8376236a3c
- httpExternal : client dedie aux API publiques externes (URL absolue, sans cookie de session, timeout), seul point d'entree autorise pour un $fetch externe (regle frontend n°4). - useAddressAutocomplete : implementation BAN (api-adresse.data.gouv.fr), recherche ville (type=municipality) et adresse, mapping GeoJSON, throw en cas d'erreur/timeout (mode degrade cote composant). La recherche d'adresse n'impose pas type=housenumber (sinon 0 resultat tant qu'aucun numero n'est saisi) — spec-front mise a jour en consequence. - Tests Vitest : httpExternal, useAddressAutocomplete, et cas limites supplementaires pour formatPhoneFR.
118 lines
4.5 KiB
TypeScript
118 lines
4.5 KiB
TypeScript
import { httpExternal } from '~/shared/utils/httpExternal'
|
|
|
|
// Autocompletion d'adresse branchee sur la Base Adresse Nationale (BAN),
|
|
// `api-adresse.data.gouv.fr` — service public francais, gratuit, CORS ouvert.
|
|
//
|
|
// Appel HTTP DIRECT depuis le front (pas de proxy back), conformement a la spec
|
|
// M1 (§ API adresse postale). On passe par `httpExternal` et NON `useApi()` :
|
|
// la BAN est un domaine externe, sans cookie de session ni enveloppe Hydra.
|
|
//
|
|
// Contrat (fige) :
|
|
// searchCity(postalCode) -> liste { city, postalCode }
|
|
// searchAddress(query, cp?) -> liste { label, street, postalCode, city }
|
|
// En cas d'erreur/timeout, la methode THROW une AddressAutocompleteUnavailableError.
|
|
// Le composant consommateur catch, affiche un toast d'avertissement et bascule
|
|
// en saisie libre (MalioInputText).
|
|
|
|
/** URL de l'endpoint de recherche BAN. */
|
|
const BAN_SEARCH_URL = 'https://api-adresse.data.gouv.fr/search/'
|
|
|
|
/** Une suggestion de ville renvoyee a partir d'un code postal. */
|
|
export interface CitySuggestion {
|
|
city: string
|
|
postalCode: string
|
|
}
|
|
|
|
/** Une suggestion d'adresse complete (saisie assistee du champ « Adresse »). */
|
|
export interface AddressSuggestion {
|
|
label: string
|
|
street: string
|
|
postalCode: string
|
|
city: string
|
|
}
|
|
|
|
export interface AddressAutocomplete {
|
|
searchCity(postalCode: string): Promise<CitySuggestion[]>
|
|
searchAddress(query: string, postalCode?: string): Promise<AddressSuggestion[]>
|
|
}
|
|
|
|
/** Erreur signalant que le service d'autocompletion BAN n'est pas disponible. */
|
|
export class AddressAutocompleteUnavailableError extends Error {
|
|
constructor() {
|
|
// Message technique (non affiche tel quel) : le composant remonte son
|
|
// propre libelle i18n. Sert au debug / aux logs uniquement.
|
|
super('Address autocomplete (BAN) is not available.')
|
|
this.name = 'AddressAutocompleteUnavailableError'
|
|
}
|
|
}
|
|
|
|
/** Proprietes d'une « feature » GeoJSON renvoyee par la BAN (champs utilises). */
|
|
interface BanFeatureProperties {
|
|
label?: string
|
|
name?: string
|
|
street?: string
|
|
postcode?: string
|
|
city?: string
|
|
}
|
|
|
|
/** Reponse GeoJSON FeatureCollection de la BAN. */
|
|
interface BanResponse {
|
|
features?: { properties?: BanFeatureProperties }[]
|
|
}
|
|
|
|
export function useAddressAutocomplete(): AddressAutocomplete {
|
|
return {
|
|
async searchCity(postalCode: string): Promise<CitySuggestion[]> {
|
|
let res: BanResponse
|
|
try {
|
|
res = await httpExternal<BanResponse>(BAN_SEARCH_URL, {
|
|
query: { q: postalCode, type: 'municipality' },
|
|
})
|
|
} catch {
|
|
// Reseau coupe, 5xx, timeout... -> mode degrade cote composant.
|
|
throw new AddressAutocompleteUnavailableError()
|
|
}
|
|
|
|
return (res.features ?? []).map((feature) => {
|
|
const props = feature.properties ?? {}
|
|
return {
|
|
city: props.city ?? props.name ?? '',
|
|
postalCode: props.postcode ?? '',
|
|
}
|
|
})
|
|
},
|
|
|
|
async searchAddress(query: string, postalCode?: string): Promise<AddressSuggestion[]> {
|
|
// IMPORTANT : pas de `type=housenumber` ici. La BAN ne renvoie un
|
|
// resultat de ce type qu'une fois un numero saisi → une recherche par
|
|
// nom de rue (« boulevard du port ») renverrait 0 resultat pendant
|
|
// toute la frappe. Sans filtre `type`, la BAN classe rues + numeros
|
|
// par pertinence (comportement d'autocompletion attendu).
|
|
// On n'ajoute `postcode` que s'il est fourni (sinon recherche large).
|
|
const banQuery: Record<string, string> = { q: query }
|
|
if (postalCode) {
|
|
banQuery.postcode = postalCode
|
|
}
|
|
|
|
let res: BanResponse
|
|
try {
|
|
res = await httpExternal<BanResponse>(BAN_SEARCH_URL, { query: banQuery })
|
|
} catch {
|
|
throw new AddressAutocompleteUnavailableError()
|
|
}
|
|
|
|
return (res.features ?? []).map((feature) => {
|
|
const props = feature.properties ?? {}
|
|
return {
|
|
label: props.label ?? '',
|
|
// `name` porte la ligne d'adresse complete (numero + voie) ;
|
|
// `street` ne contient que la voie. On privilegie `name`.
|
|
street: props.name ?? props.street ?? '',
|
|
postalCode: props.postcode ?? '',
|
|
city: props.city ?? '',
|
|
}
|
|
})
|
|
},
|
|
}
|
|
}
|