Integration: Stubs von L2/L3/L4/L5 gegen echte Services getauscht
- Aufträge: createWorkOrder/transitionWorkOrder/getCompletionBlockers aus L2 - Dokumente: storeFile aus L1; neu services/documents/read.ts (readStoredBytes, readDocumentBytes mit Prüfsummenprüfung); L2-Upload nutzt zentrale Ablage - Dubletten aus L1 (findDuplicateCustomers(ctx)); Objekt-Kandidaten als imports/site-candidates.ts; Dateityp-Erkennung mobil als field/mime.ts - Objekt-Historie mobil als Adapter auf L1 getSiteHistory, PDF über /api/v1/reports/:id/pdf - L4-Upload-Idempotenz: fester Upload-Lineage nach storeFile, Race → Soft-Delete + Replay - PDF-Worker-Kontext: document:write zum Ablegen des Berichts-PDF - Import: doppeltes Work-Order-Audit entfernt; bestätigte Aufträge starten planned - Dateilinks in Auftragsdetail auf /files/[documentId] - proxy: /api/v1 ohne Session → 401 JSON statt Login-Redirect - ARCHITEKTUR §2: Objekt-Historie für Feldrollen (freigegebene Einsätze aller Teams) Gate: tsc, lint, build, 42/42 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,8 +4,8 @@ import { dispatchJob } from "@/server/jobs/dispatch";
|
||||
import { JOB_QUEUES } from "@/server/jobs/queues";
|
||||
import { ServiceError, type ServiceCtx } from "@/server/services/context";
|
||||
import { audit, isUniqueViolation, requireFieldOrder } from "./common";
|
||||
// TODO(merge documents contract §4.3): replace with "@/server/services/documents/store"
|
||||
import { sniffMime, storeFile } from "./stubs/documents-store";
|
||||
import { storeFile } from "@/server/services/documents/store";
|
||||
import { sniffMime } from "./mime";
|
||||
|
||||
/**
|
||||
* Binary uploads of the mobile app (ARCHITEKTUR §4.6: POST /api/v1/uploads → documentId).
|
||||
@@ -52,19 +52,24 @@ export async function storeFieldUpload(
|
||||
const expectedKind = meta.kind === "photo" ? "image" : "audio";
|
||||
if (!sniffed || sniffed.kind !== expectedKind) throw new ServiceError("invalid", `file is not a valid ${expectedKind}`);
|
||||
|
||||
// The central store treats `lineageId` as "new version of an existing document", so the file is
|
||||
// stored as a fresh document and the deterministic upload lineage (idempotency key per device
|
||||
// clientId) is attached afterwards. A concurrent upload with the same clientId loses the unique
|
||||
// (lineageId, version) race: its document is soft-deleted and the winner is returned.
|
||||
const created = await storeFile(ctx, {
|
||||
bytes: file.bytes,
|
||||
fileName: file.name || (meta.kind === "photo" ? "foto.jpg" : "sprachnotiz.webm"),
|
||||
declaredMime: file.type || sniffed.mime,
|
||||
category: meta.kind,
|
||||
visibility: "team",
|
||||
links: { workOrderId: wo.id, siteId: wo.siteId, customerId: wo.customerId },
|
||||
});
|
||||
let doc;
|
||||
try {
|
||||
doc = await storeFile(ctx, {
|
||||
bytes: file.bytes,
|
||||
fileName: file.name || (meta.kind === "photo" ? "foto.jpg" : "sprachnotiz.webm"),
|
||||
declaredMime: file.type || sniffed.mime,
|
||||
category: meta.kind,
|
||||
visibility: "team",
|
||||
links: { workOrderId: wo.id, siteId: wo.siteId, customerId: wo.customerId },
|
||||
lineageId,
|
||||
});
|
||||
doc = await ctx.db.document.update({ where: { id: created.id }, data: { lineageId } });
|
||||
} catch (err) {
|
||||
if (isUniqueViolation(err)) {
|
||||
await ctx.db.document.update({ where: { id: created.id }, data: { deletedAt: new Date() } });
|
||||
const raced = await replay();
|
||||
if (raced) return raced;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user