Fixed images being removed, fixed v4/v5 wrong status on uploadDocument

This commit is contained in:
SepehrYahyaee
2026-09-16 12:50:12 +03:30
parent dc30518a7f
commit fee0bcb5be
4 changed files with 305 additions and 12 deletions

View File

@@ -0,0 +1,113 @@
/**
* Regression: concurrent capturePart writes must not clobber sibling slots.
*
* capturePartV2 used to `$set` the entire `media.damagedParts` array from a
* stale read. Parallel uploads (common while Fanavaran attachment submit keeps
* the HTTP request open) made the last writer win — Fanavaran still saw each
* file on disk and could return errors, while Mongo was missing captures.
*
* Required strategy: per-index `$set` (`media.damagedParts.N`), matching
* `media.carAngles.<key>`.
*/
describe("capture-part media.damagedParts write strategies", () => {
type Row = { path?: string; fileName?: string; name?: string };
type Claim = { media: { damagedParts: Row[] } };
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
/** Legacy (buggy) strategy: replace the entire array from a stale read. */
async function writeWholeArray(
store: { claim: Claim },
index: number,
capture: Row,
readDelayMs: number,
) {
const snapshot = structuredClone(store.claim);
await sleep(readDelayMs);
const next = snapshot.media.damagedParts.map((row) => ({ ...row }));
while (next.length <= index) next.push({});
next[index] = { ...next[index], ...capture };
store.claim = {
...store.claim,
media: { ...store.claim.media, damagedParts: next },
};
}
/** Required strategy: set only the target index (Mongo $set media.damagedParts.N). */
async function writeSingleIndex(
store: { claim: Claim },
index: number,
capture: Row,
readDelayMs: number,
) {
await sleep(readDelayMs);
const next = store.claim.media.damagedParts.map((row) => ({ ...row }));
while (next.length <= index) next.push({});
next[index] = { ...next[index], ...capture };
store.claim.media.damagedParts[index] = next[index];
}
it("documents that whole-array replace loses a concurrent capture", async () => {
const store: { claim: Claim } = {
claim: {
media: {
damagedParts: [{ name: "hood" }, { name: "front_bumper" }],
},
},
};
await Promise.all([
writeWholeArray(
store,
0,
{ path: "files/claim-captures/hood.jpg", fileName: "hood.jpg" },
30,
),
writeWholeArray(
store,
1,
{
path: "files/claim-captures/bumper.jpg",
fileName: "bumper.jpg",
},
10,
),
]);
expect(store.claim.media.damagedParts.map((r) => r.path).filter(Boolean))
.toHaveLength(1);
});
it("keeps both concurrent captures with per-index writes", async () => {
const store: { claim: Claim } = {
claim: {
media: {
damagedParts: [{ name: "hood" }, { name: "front_bumper" }],
},
},
};
await Promise.all([
writeSingleIndex(
store,
0,
{ path: "files/claim-captures/hood.jpg", fileName: "hood.jpg" },
30,
),
writeSingleIndex(
store,
1,
{
path: "files/claim-captures/bumper.jpg",
fileName: "bumper.jpg",
},
10,
),
]);
expect(store.claim.media.damagedParts.map((r) => r.path)).toEqual([
"files/claim-captures/hood.jpg",
"files/claim-captures/bumper.jpg",
]);
});
});

View File

@@ -6452,14 +6452,22 @@ export class ClaimRequestManagementService {
);
}
// Schedule retry for attachments
await this.scheduleFanavaranRetry(
claimCaseId,
"attachments",
() => this.autoSubmitFanavaranAttachment(claimCaseId, file, options),
logPrefix,
{ error, clientKey },
);
// Schedule retry for attachments — never let retry bookkeeping fail the
// caller: local media/docs are already persisted before this submit.
try {
await this.scheduleFanavaranRetry(
claimCaseId,
"attachments",
() => this.autoSubmitFanavaranAttachment(claimCaseId, file, options),
logPrefix,
{ error, clientKey },
);
} catch (retryScheduleError) {
this.logger.error(
`${logPrefix} Failed to schedule Fanavaran attachment retry`,
retryScheduleError,
);
}
return {
attempted: true,
@@ -10911,7 +10919,14 @@ export class ClaimRequestManagementService {
...nextMedia[idx],
...captureData,
};
updateData["media.damagedParts"] = nextMedia;
// Per-index $set avoids lost updates when clients upload multiple parts
// in parallel (whole-array replace raced with Fanavaran-awaited requests).
// Legacy object maps still need a full write to migrate to array shape.
if (!Array.isArray(claimCase.media?.damagedParts)) {
updateData["media.damagedParts"] = nextMedia;
} else {
updateData[`media.damagedParts.${idx}`] = nextMedia[idx];
}
if (
isResendCapture &&
nextSelected.length !== selectedBeforeNorm.length
@@ -10920,10 +10935,14 @@ export class ClaimRequestManagementService {
}
}
const updatedClaim = await this.claimCaseDbService.findByIdAndUpdate(
await this.claimCaseDbService.findByIdAndUpdate(
claimRequestId,
updateData,
);
// Re-read so capture-progress sees sibling concurrent part/angle writes.
const updatedClaim =
(await this.claimCaseDbService.findById(claimRequestId)) ??
claimCase;
if (isResendCapture) {
await this.tryFinalizeExpertResendAfterUserAction(