shoc-frontend-new/src/api/media-upload-contracts.ts
2026-09-23 04:17:18 -03:00

130 lines
3.4 KiB
TypeScript

/** Shared SH-116 resumable media upload contract (dispatcher + vendor portal). */
export const MEDIA_UPLOAD_CHUNK_SIZE_BYTES = 5_242_880;
export const MEDIA_UPLOAD_SCANNING_STATE = "Scanning" as const;
export type UploadSurface = "WorkOrderTable" | "CompletionMedia" | "VendorPortal";
export type UploadCategory = "Before" | "After" | "Extra" | "Aveta";
export type MediaKind = "Photo" | "Video";
export type MediaUploadState =
"Created" | "Uploading" | "Scanning" | "Completed" | "Rejected" | "Canceled" | "Expired";
export interface MediaUploadLimits {
photoMaxBytes: number;
videoMaxBytes: number;
videoMaxSeconds: number;
maxPhotos: number;
maxVideos: number;
}
export interface MediaUploadRemaining {
photos: number;
videos: number;
}
export interface MediaUploadPolicy {
workOrderType: string;
canUpload: boolean;
limits: MediaUploadLimits;
remaining: MediaUploadRemaining;
}
export interface MediaUploadCreateDto {
idempotencyKey: string;
fileName: string;
contentType: string;
sizeBytes: number;
category?: UploadCategory;
surface: UploadSurface;
}
export interface MediaUploadSessionDto {
uploadId: string;
state: MediaUploadState;
chunkSizeBytes: number;
sizeBytes: number;
receivedChunks: number[];
expiresAt: string;
attachment?: { id: string | number };
}
export interface MediaChunkAckDto {
index: number;
sha256: string;
sizeBytes: number;
receivedChunks: number[];
}
export const MEDIA_UPLOAD_TERMINAL_STATES: ReadonlySet<MediaUploadState> = new Set([
"Completed",
"Rejected",
"Canceled",
"Expired",
]);
export function isTerminalMediaUploadState(state: MediaUploadState): boolean {
return MEDIA_UPLOAD_TERMINAL_STATES.has(state);
}
export function isUploadCategory(value: string | undefined): value is UploadCategory {
return value === "Before" || value === "After" || value === "Extra" || value === "Aveta";
}
/** Safe, stable user-facing messages. Never render raw server payloads. */
export function safeMediaUploadErrorMessage(error: Error): string {
if (error instanceof MediaUploadValidationError) {
return error.message;
}
if (error instanceof MediaUploadRejectedError) {
return "The file was rejected by the security scan and was not saved.";
}
if (error instanceof MediaUploadExpiredError) {
return "The upload session expired. Please try again.";
}
if (error instanceof MediaUploadScanTimeoutError) {
return "The upload is still processing. Check back shortly to see the result.";
}
if (error.name === "AbortError") {
return "Upload canceled.";
}
return "Upload failed. Check your connection and try again.";
}
export class MediaUploadValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "MediaUploadValidationError";
}
}
export class MediaUploadRejectedError extends Error {
constructor() {
super("The file was rejected by the security scan.");
this.name = "MediaUploadRejectedError";
}
}
export class MediaUploadExpiredError extends Error {
constructor() {
super("The upload session expired.");
this.name = "MediaUploadExpiredError";
}
}
export class MediaUploadCanceledError extends Error {
constructor() {
super("The upload session was canceled.");
this.name = "MediaUploadCanceledError";
}
}
export class MediaUploadScanTimeoutError extends Error {
constructor() {
super("The upload is still processing.");
this.name = "MediaUploadScanTimeoutError";
}
}