fix(transport) : upload décharge différé à l'enregistrement/validation (évite les orphelins) (ERP-171)
This commit is contained in:
@@ -350,6 +350,52 @@ describe('useCarrierForm — champs conditionnels (ERP-166)', () => {
|
|||||||
form.main.dischargeDocumentIri = '/api/uploaded_documents/7'
|
form.main.dischargeDocumentIri = '/api/uploaded_documents/7'
|
||||||
expect(form.buildMainPayload()).toMatchObject({ dischargeDocument: '/api/uploaded_documents/7' })
|
expect(form.buildMainPayload()).toMatchObject({ dischargeDocument: '/api/uploaded_documents/7' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('RG-4.02 upload différé : selectDischarge ne POST pas ; submitMain upload PUIS crée', async () => {
|
||||||
|
mockPost.mockReset()
|
||||||
|
// 1er POST = /uploaded_documents (renvoie l'IRI) ; 2e = /carriers (création).
|
||||||
|
mockPost
|
||||||
|
.mockResolvedValueOnce({ '@id': '/api/uploaded_documents/7' })
|
||||||
|
.mockResolvedValueOnce({ id: 12, name: 'ACME', certificationType: 'AUTRE' })
|
||||||
|
|
||||||
|
const form = useCarrierForm()
|
||||||
|
form.main.name = 'Acme'
|
||||||
|
form.main.certificationType = 'AUTRE'
|
||||||
|
|
||||||
|
// Sélection du fichier : aucun appel réseau (upload différé à l'enregistrement).
|
||||||
|
form.selectDischarge(new File(['x'], 'decharge.pdf', { type: 'application/pdf' }))
|
||||||
|
expect(mockPost).not.toHaveBeenCalled()
|
||||||
|
// La validation est satisfaite par le fichier en attente (pas encore d'IRI).
|
||||||
|
expect(form.mainErrors.errors.dischargeDocument).toBeUndefined()
|
||||||
|
|
||||||
|
const created = await form.submitMain()
|
||||||
|
expect(created).toBe(true)
|
||||||
|
|
||||||
|
// 1er appel : upload multipart ; 2e : création carrier avec l'IRI résolu.
|
||||||
|
expect(mockPost.mock.calls[0][0]).toBe('/uploaded_documents')
|
||||||
|
expect(mockPost.mock.calls[1][0]).toBe('/carriers')
|
||||||
|
expect(mockPost.mock.calls[1][1]).toMatchObject({ dischargeDocument: '/api/uploaded_documents/7' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('RG-4.02 upload différé : un 422 MIME bloque la création (message inline, pas de POST /carriers)', async () => {
|
||||||
|
mockPost.mockReset()
|
||||||
|
// Le POST /uploaded_documents échoue (MIME hors whitelist) → 422.
|
||||||
|
mockPost.mockRejectedValueOnce(Object.assign(new Error('422'), {
|
||||||
|
data: { 'hydra:description': 'Type de fichier non autorisé.' },
|
||||||
|
}))
|
||||||
|
|
||||||
|
const form = useCarrierForm()
|
||||||
|
form.main.name = 'Acme'
|
||||||
|
form.main.certificationType = 'AUTRE'
|
||||||
|
form.selectDischarge(new File(['x'], 'malware.exe', { type: 'application/x-msdownload' }))
|
||||||
|
|
||||||
|
const created = await form.submitMain()
|
||||||
|
expect(created).toBe(false)
|
||||||
|
// Message back affiché inline sous le champ ; aucune création de carrier.
|
||||||
|
expect(form.mainErrors.errors.dischargeDocument).toBe('Type de fichier non autorisé.')
|
||||||
|
expect(mockPost).toHaveBeenCalledTimes(1)
|
||||||
|
expect(mockPost.mock.calls[0][0]).toBe('/uploaded_documents')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('useCarrierForm — copie QUALIMAT (ERP-166)', () => {
|
describe('useCarrierForm — copie QUALIMAT (ERP-166)', () => {
|
||||||
|
|||||||
@@ -68,7 +68,9 @@ export function useCarrierForm() {
|
|||||||
const mainErrors = useFormErrors()
|
const mainErrors = useFormErrors()
|
||||||
|
|
||||||
// Upload de la décharge (RG-4.02) — infra partagée /api/uploaded_documents.
|
// Upload de la décharge (RG-4.02) — infra partagée /api/uploaded_documents.
|
||||||
|
// L'upload est DIFFÉRÉ : le fichier choisi attend ici jusqu'à l'enregistrement.
|
||||||
const { uploading: dischargeUploading, upload: uploadFile } = useUpload()
|
const { uploading: dischargeUploading, upload: uploadFile } = useUpload()
|
||||||
|
const pendingDischargeFile = ref<File | null>(null)
|
||||||
|
|
||||||
// ── État du transporteur créé ─────────────────────────────────────────────
|
// ── État du transporteur créé ─────────────────────────────────────────────
|
||||||
const carrierId = ref<number | null>(null)
|
const carrierId = ref<number | null>(null)
|
||||||
@@ -144,8 +146,9 @@ export function useCarrierForm() {
|
|||||||
valid = false
|
valid = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// RG-4.02 : décharge obligatoire si certification AUTRE.
|
// RG-4.02 : décharge obligatoire si certification AUTRE — satisfaite par un
|
||||||
if (main.certificationType === 'AUTRE' && !main.dischargeDocumentIri) {
|
// IRI déjà posé OU un fichier en attente d'upload (différé à l'enregistrement).
|
||||||
|
if (main.certificationType === 'AUTRE' && !main.dischargeDocumentIri && !pendingDischargeFile.value) {
|
||||||
mainErrors.setError('dischargeDocument', t('transport.carriers.form.errors.dischargeRequired'))
|
mainErrors.setError('dischargeDocument', t('transport.carriers.form.errors.dischargeRequired'))
|
||||||
valid = false
|
valid = false
|
||||||
}
|
}
|
||||||
@@ -170,20 +173,41 @@ export function useCarrierForm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upload de la décharge (RG-4.02) déclenché par `@file-selected` du champ
|
* Sélection de la décharge (RG-4.02) via `@file-selected` : le fichier est mis
|
||||||
* Décharge : envoie le fichier, pose l'IRI résultant sur le brouillon. Au 422
|
* EN ATTENTE, l'upload réel est DIFFÉRÉ à l'enregistrement (`submitMain` /
|
||||||
* (MIME hors whitelist / taille), le message back s'affiche sous le champ
|
* `updateMain`). Évite les binaires orphelins si l'utilisateur abandonne le
|
||||||
* (pas de toast) ; l'IRI est remis à null pour bloquer la validation.
|
* formulaire après avoir choisi un fichier.
|
||||||
*/
|
*/
|
||||||
async function uploadDischarge(file: File): Promise<void> {
|
function selectDischarge(file: File): void {
|
||||||
mainErrors.clearError('dischargeDocument')
|
mainErrors.clearError('dischargeDocument')
|
||||||
|
pendingDischargeFile.value = file
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Annulation du choix de décharge : oublie le fichier en attente et l'IRI. */
|
||||||
|
function clearDischarge(): void {
|
||||||
|
pendingDischargeFile.value = null
|
||||||
|
main.dischargeDocumentIri = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Résout l'upload différé au moment de l'enregistrement : s'il y a un fichier
|
||||||
|
* en attente, l'envoie (POST /uploaded_documents) et pose l'IRI sur le
|
||||||
|
* brouillon. Retourne false au 422 (MIME / taille → message inline) pour
|
||||||
|
* interrompre la sauvegarde du transporteur. Pas de fichier en attente → no-op.
|
||||||
|
*/
|
||||||
|
async function resolveDischargeUpload(): Promise<boolean> {
|
||||||
|
if (!pendingDischargeFile.value) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
main.dischargeDocumentIri = await uploadFile(file)
|
main.dischargeDocumentIri = await uploadFile(pendingDischargeFile.value)
|
||||||
|
pendingDischargeFile.value = null
|
||||||
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
main.dischargeDocumentIri = null
|
|
||||||
const message = extractApiErrorMessage((error as { data?: unknown })?.data)
|
const message = extractApiErrorMessage((error as { data?: unknown })?.data)
|
||||||
|| t('transport.carriers.form.errors.uploadFailed')
|
|| t('transport.carriers.form.errors.uploadFailed')
|
||||||
mainErrors.setError('dischargeDocument', message)
|
mainErrors.setError('dischargeDocument', message)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,6 +273,9 @@ export function useCarrierForm() {
|
|||||||
|
|
||||||
mainSubmitting.value = true
|
mainSubmitting.value = true
|
||||||
try {
|
try {
|
||||||
|
// Upload différé de la décharge : envoyé seulement maintenant (au Valider).
|
||||||
|
if (!(await resolveDischargeUpload())) return false
|
||||||
|
|
||||||
const created = await api.post<CarrierMainResponse>('/carriers', buildMainPayload(), {
|
const created = await api.post<CarrierMainResponse>('/carriers', buildMainPayload(), {
|
||||||
headers: { Accept: 'application/ld+json' },
|
headers: { Accept: 'application/ld+json' },
|
||||||
toast: false,
|
toast: false,
|
||||||
@@ -299,6 +326,9 @@ export function useCarrierForm() {
|
|||||||
|
|
||||||
mainSubmitting.value = true
|
mainSubmitting.value = true
|
||||||
try {
|
try {
|
||||||
|
// Upload différé de la décharge : envoyé seulement maintenant (à l'Enregistrer).
|
||||||
|
if (!(await resolveDischargeUpload())) return false
|
||||||
|
|
||||||
const updated = await api.patch<CarrierMainResponse>(
|
const updated = await api.patch<CarrierMainResponse>(
|
||||||
`/carriers/${carrierId.value}`,
|
`/carriers/${carrierId.value}`,
|
||||||
buildMainPayload(),
|
buildMainPayload(),
|
||||||
@@ -791,7 +821,8 @@ export function useCarrierForm() {
|
|||||||
removePrice,
|
removePrice,
|
||||||
submitPrices,
|
submitPrices,
|
||||||
// actions
|
// actions
|
||||||
uploadDischarge,
|
selectDischarge,
|
||||||
|
clearDischarge,
|
||||||
validateMainFront,
|
validateMainFront,
|
||||||
buildMainPayload,
|
buildMainPayload,
|
||||||
submitMain,
|
submitMain,
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
:clearable="true"
|
:clearable="true"
|
||||||
:error="mainErrors.errors.dischargeDocument"
|
:error="mainErrors.errors.dischargeDocument"
|
||||||
@update:model-value="(v: string) => dischargeFileName = v"
|
@update:model-value="(v: string) => dischargeFileName = v"
|
||||||
@file-selected="uploadDischarge"
|
@file-selected="selectDischarge"
|
||||||
@clear="onClearDischarge"
|
@clear="onClearDischarge"
|
||||||
/>
|
/>
|
||||||
<div v-else class="hidden xl:block"></div>
|
<div v-else class="hidden xl:block"></div>
|
||||||
@@ -236,7 +236,8 @@ const {
|
|||||||
tabSubmitting,
|
tabSubmitting,
|
||||||
mainErrors,
|
mainErrors,
|
||||||
dischargeUploading,
|
dischargeUploading,
|
||||||
uploadDischarge,
|
selectDischarge,
|
||||||
|
clearDischarge,
|
||||||
isLiot,
|
isLiot,
|
||||||
certificationReadonly,
|
certificationReadonly,
|
||||||
showCharteredFields,
|
showCharteredFields,
|
||||||
@@ -335,9 +336,9 @@ function apiErrorMessage(err: unknown): string {
|
|||||||
// chargement d'un transporteur ayant déjà une décharge).
|
// chargement d'un transporteur ayant déjà une décharge).
|
||||||
const dischargeFileName = ref('')
|
const dischargeFileName = ref('')
|
||||||
|
|
||||||
/** Vidage du champ Décharge : retire l'IRI et le nom affiché. */
|
/** Vidage du champ Décharge : oublie le fichier en attente / l'IRI + le nom affiché. */
|
||||||
function onClearDischarge(): void {
|
function onClearDischarge(): void {
|
||||||
main.dischargeDocumentIri = null
|
clearDischarge()
|
||||||
dischargeFileName.value = ''
|
dischargeFileName.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,8 +53,8 @@
|
|||||||
<!-- Colonne 3 RÉSERVÉE à la Décharge (RG-4.02 : visible et obligatoire
|
<!-- Colonne 3 RÉSERVÉE à la Décharge (RG-4.02 : visible et obligatoire
|
||||||
si certification AUTRE). Si elle n'apparaît pas, on garde la colonne
|
si certification AUTRE). Si elle n'apparaît pas, on garde la colonne
|
||||||
vide (xl) pour qu'« Affréter » reste en colonne 4 de la ligne 1.
|
vide (xl) pour qu'« Affréter » reste en colonne 4 de la ligne 1.
|
||||||
L'upload réel (File → IRI via useUpload, ERP-171) résout le
|
Upload DIFFÉRÉ (ERP-171) : le fichier choisi est mis en attente
|
||||||
fichier en IRI posé sur main.dischargeDocumentIri. -->
|
et envoyé seulement à la validation du formulaire. -->
|
||||||
<MalioInputUpload
|
<MalioInputUpload
|
||||||
v-if="showDischarge"
|
v-if="showDischarge"
|
||||||
:model-value="dischargeFileName"
|
:model-value="dischargeFileName"
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
:clearable="true"
|
:clearable="true"
|
||||||
:error="mainErrors.errors.dischargeDocument"
|
:error="mainErrors.errors.dischargeDocument"
|
||||||
@update:model-value="(v: string) => dischargeFileName = v"
|
@update:model-value="(v: string) => dischargeFileName = v"
|
||||||
@file-selected="uploadDischarge"
|
@file-selected="selectDischarge"
|
||||||
@clear="onClearDischarge"
|
@clear="onClearDischarge"
|
||||||
/>
|
/>
|
||||||
<div v-else class="hidden xl:block"></div>
|
<div v-else class="hidden xl:block"></div>
|
||||||
@@ -401,7 +401,8 @@ const {
|
|||||||
tabSubmitting,
|
tabSubmitting,
|
||||||
mainErrors,
|
mainErrors,
|
||||||
dischargeUploading,
|
dischargeUploading,
|
||||||
uploadDischarge,
|
selectDischarge,
|
||||||
|
clearDischarge,
|
||||||
isLiot,
|
isLiot,
|
||||||
isQualimat,
|
isQualimat,
|
||||||
certificationReadonly,
|
certificationReadonly,
|
||||||
@@ -436,9 +437,9 @@ const {
|
|||||||
// Nom de fichier affiché dans le champ Décharge (alimenté à la sélection).
|
// Nom de fichier affiché dans le champ Décharge (alimenté à la sélection).
|
||||||
const dischargeFileName = ref('')
|
const dischargeFileName = ref('')
|
||||||
|
|
||||||
/** Vidage du champ Décharge : retire l'IRI et le nom affiché. */
|
/** Vidage du champ Décharge : oublie le fichier en attente / l'IRI + le nom affiché. */
|
||||||
function onClearDischarge(): void {
|
function onClearDischarge(): void {
|
||||||
main.dischargeDocumentIri = null
|
clearDischarge()
|
||||||
dischargeFileName.value = ''
|
dischargeFileName.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user