feat(catalog) : M6 — StorageType référentiel plat + seed migration (drop storage_type_site)
La disponibilité « type de stockage par site » relèvera de la future entité Stockage (site + type), pas du référentiel. On retire donc la jointure M2M storage_type_site et le filtrage du multi-select par site (RG-6.06 revue) : - migration : DROP storage_type_site + seed idempotent des 10 types (prod-safe, ON CONFLICT) ; - StorageType : référentiel plat (plus de relation sites) ; - Product : suppression du Assert\Callback de disponibilité par site ; - provider/repository : /storage_types renvoie tous les types (plus de ?siteId[]) ; - front : useStorageTypeOptions charge tout dans loadReferentials, setSites sans cascade/purge ; - fixture, ColumnCommentsCatalog, tests et spec-back M6 alignés.
This commit is contained in:
@@ -29,23 +29,13 @@ vi.stubGlobal('useI18n', () => ({
|
||||
params ? `${key}::${JSON.stringify(params)}` : key,
|
||||
}))
|
||||
|
||||
/** Reponse Hydra des types de stockage selon les sites demandes. */
|
||||
function storageMembersForSites(siteIds: string[]): { member: Array<{ '@id': string, label: string }> } {
|
||||
// Site 1 → types 9 et 5 ; site 2 → type 7. Permet de tester la cascade.
|
||||
const byId: Record<string, Array<{ '@id': string, label: string }>> = {
|
||||
'1': [
|
||||
{ '@id': '/api/storage_types/9', label: 'Tas' },
|
||||
{ '@id': '/api/storage_types/5', label: 'Cellule' },
|
||||
],
|
||||
'2': [{ '@id': '/api/storage_types/7', label: 'Cuve mélasse' }],
|
||||
}
|
||||
const seen = new Map<string, { '@id': string, label: string }>()
|
||||
for (const id of siteIds) {
|
||||
for (const m of byId[id] ?? []) {
|
||||
seen.set(m['@id'], m)
|
||||
}
|
||||
}
|
||||
return { member: [...seen.values()] }
|
||||
/** Referentiel PLAT des types de stockage (renvoye tel quel, sans filtre site). */
|
||||
const STORAGE_TYPES = {
|
||||
member: [
|
||||
{ '@id': '/api/storage_types/9', label: 'Tas' },
|
||||
{ '@id': '/api/storage_types/5', label: 'Cellule' },
|
||||
{ '@id': '/api/storage_types/7', label: 'Cuve mélasse' },
|
||||
],
|
||||
}
|
||||
|
||||
describe('useProductForm', () => {
|
||||
@@ -56,8 +46,9 @@ describe('useProductForm', () => {
|
||||
mockToastSuccess.mockReset()
|
||||
mockToastError.mockReset()
|
||||
|
||||
// Routage des GET par url (referentiels + cascade stockage).
|
||||
mockGet.mockImplementation((url: string, query: Record<string, unknown> = {}) => {
|
||||
// Routage des GET par url (referentiels). Le stockage est un referentiel
|
||||
// plat : meme reponse quelle que soit la requete.
|
||||
mockGet.mockImplementation((url: string) => {
|
||||
if (url === '/sites') {
|
||||
return Promise.resolve({ member: [{ '@id': '/api/sites/1', name: 'Chatellerault' }] })
|
||||
}
|
||||
@@ -65,8 +56,7 @@ describe('useProductForm', () => {
|
||||
return Promise.resolve({ member: [{ '@id': '/api/categories/12', name: 'Céréales' }] })
|
||||
}
|
||||
if (url === '/storage_types') {
|
||||
const raw = (query['siteId[]'] ?? []) as string[]
|
||||
return Promise.resolve(storageMembersForSites(raw))
|
||||
return Promise.resolve(STORAGE_TYPES)
|
||||
}
|
||||
return Promise.resolve({ member: [] })
|
||||
})
|
||||
@@ -97,44 +87,36 @@ describe('useProductForm', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('RG-6.06 — cascade Site → Type de stockage', () => {
|
||||
it('charge les types de stockage filtres par les sites selectionnes', async () => {
|
||||
const { storageTypeOptions, setSites } = useProductForm()
|
||||
await setSites(['/api/sites/1'])
|
||||
describe('RG-6.06 — types de stockage (referentiel plat)', () => {
|
||||
it('loadReferentials charge TOUS les types de stockage, sans filtre site', async () => {
|
||||
const { storageTypeOptions, loadReferentials } = useProductForm()
|
||||
await loadReferentials()
|
||||
|
||||
expect(mockGet).toHaveBeenCalledWith(
|
||||
'/storage_types',
|
||||
expect.objectContaining({ 'siteId[]': ['1'], pagination: 'false' }),
|
||||
expect.any(Object),
|
||||
)
|
||||
const storageCall = mockGet.mock.calls.find(c => c[0] === '/storage_types')
|
||||
expect(storageCall).toBeDefined()
|
||||
// Aucun filtre siteId envoye (referentiel plat).
|
||||
expect(storageCall?.[1]).not.toHaveProperty('siteId[]')
|
||||
expect(storageTypeOptions.value.map(o => o.value)).toEqual([
|
||||
'/api/storage_types/9',
|
||||
'/api/storage_types/5',
|
||||
'/api/storage_types/7',
|
||||
])
|
||||
})
|
||||
|
||||
it('retire de la selection les types devenus indisponibles', async () => {
|
||||
const { form, setStorageTypes, setSites } = useProductForm()
|
||||
it('setSites met a jour les sites sans recharger le stockage ni purger la selection', async () => {
|
||||
const { form, setSites, setStorageTypes, loadReferentials } = useProductForm()
|
||||
await loadReferentials()
|
||||
const storageCallsBefore = mockGet.mock.calls.filter(c => c[0] === '/storage_types').length
|
||||
|
||||
// Selection initiale sur le site 1 (types 9 et 5).
|
||||
await setSites(['/api/sites/1'])
|
||||
setStorageTypes(['/api/storage_types/9', '/api/storage_types/5'])
|
||||
|
||||
// Bascule vers le site 2 (type 7 seul) : 9 et 5 ne sont plus dispo.
|
||||
await setSites(['/api/sites/2'])
|
||||
expect(form.storageTypeIris).toEqual([])
|
||||
})
|
||||
|
||||
it('vide options + selection quand plus aucun site n\'est selectionne', async () => {
|
||||
const { form, storageTypeOptions, setStorageTypes, setSites } = useProductForm()
|
||||
await setSites(['/api/sites/1'])
|
||||
setStorageTypes(['/api/storage_types/9'])
|
||||
setSites(['/api/sites/1'])
|
||||
|
||||
await setSites([])
|
||||
expect(storageTypeOptions.value).toEqual([])
|
||||
expect(form.storageTypeIris).toEqual([])
|
||||
// Pas d'appel /storage_types inutile sans site.
|
||||
expect(mockGet).not.toHaveBeenCalledWith('/storage_types', expect.objectContaining({ 'siteId[]': [] }), expect.any(Object))
|
||||
expect(form.siteIris).toEqual(['/api/sites/1'])
|
||||
// Selection conservee : plus de cascade ni de purge par site.
|
||||
expect(form.storageTypeIris).toEqual(['/api/storage_types/9'])
|
||||
// setSites ne declenche aucun nouvel appel /storage_types.
|
||||
const storageCallsAfter = mockGet.mock.calls.filter(c => c[0] === '/storage_types').length
|
||||
expect(storageCallsAfter).toBe(storageCallsBefore)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -235,8 +217,8 @@ describe('useProductForm', () => {
|
||||
createdAt: '', updatedAt: '',
|
||||
}
|
||||
|
||||
it('pre-remplit le formulaire depuis le produit (relations en IRI) + charge le stockage', async () => {
|
||||
const { form, prefill, storageTypeOptions } = useProductForm()
|
||||
it('pre-remplit le formulaire depuis le produit (relations en IRI)', async () => {
|
||||
const { form, prefill } = useProductForm()
|
||||
await prefill(PRODUCT)
|
||||
|
||||
expect(form.code).toBe('BLE-01')
|
||||
@@ -246,13 +228,6 @@ describe('useProductForm', () => {
|
||||
expect(form.siteIris).toEqual(['/api/sites/1'])
|
||||
expect(form.storageTypeIris).toEqual(['/api/storage_types/9'])
|
||||
expect(form.manufactured).toBe(true)
|
||||
// Cascade : options de stockage chargees pour le site du produit.
|
||||
expect(mockGet).toHaveBeenCalledWith(
|
||||
'/storage_types',
|
||||
expect.objectContaining({ 'siteId[]': ['1'] }),
|
||||
expect.any(Object),
|
||||
)
|
||||
expect(storageTypeOptions.value.map(o => o.value)).toContain('/api/storage_types/9')
|
||||
})
|
||||
|
||||
it('soumet un PATCH /products/{id} apres prefill (RG-6.08)', async () => {
|
||||
|
||||
Reference in New Issue
Block a user