Notification updates
This commit is contained in:
@@ -17,11 +17,11 @@ export class DocumentsController {
|
||||
|
||||
logger.info('Documents list requested', {
|
||||
operation: 'documents.list',
|
||||
user_id: userId,
|
||||
userId,
|
||||
filters: {
|
||||
vehicle_id: request.query.vehicleId,
|
||||
vehicleId: request.query.vehicleId,
|
||||
type: request.query.type,
|
||||
expires_before: request.query.expiresBefore,
|
||||
expiresBefore: request.query.expiresBefore,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -33,8 +33,8 @@ export class DocumentsController {
|
||||
|
||||
logger.info('Documents list retrieved', {
|
||||
operation: 'documents.list.success',
|
||||
user_id: userId,
|
||||
document_count: docs.length,
|
||||
userId,
|
||||
documentCount: docs.length,
|
||||
});
|
||||
|
||||
return reply.code(200).send(docs);
|
||||
@@ -46,26 +46,26 @@ export class DocumentsController {
|
||||
|
||||
logger.info('Document get requested', {
|
||||
operation: 'documents.get',
|
||||
user_id: userId,
|
||||
document_id: documentId,
|
||||
userId,
|
||||
documentId,
|
||||
});
|
||||
|
||||
const doc = await this.service.getDocument(userId, documentId);
|
||||
if (!doc) {
|
||||
logger.warn('Document not found', {
|
||||
operation: 'documents.get.not_found',
|
||||
user_id: userId,
|
||||
document_id: documentId,
|
||||
userId,
|
||||
documentId,
|
||||
});
|
||||
return reply.code(404).send({ error: 'Not Found' });
|
||||
}
|
||||
|
||||
logger.info('Document retrieved', {
|
||||
operation: 'documents.get.success',
|
||||
user_id: userId,
|
||||
document_id: documentId,
|
||||
vehicle_id: doc.vehicle_id,
|
||||
document_type: doc.document_type,
|
||||
userId,
|
||||
documentId,
|
||||
vehicleId: doc.vehicleId,
|
||||
documentType: doc.documentType,
|
||||
});
|
||||
|
||||
return reply.code(200).send(doc);
|
||||
@@ -76,9 +76,9 @@ export class DocumentsController {
|
||||
|
||||
logger.info('Document create requested', {
|
||||
operation: 'documents.create',
|
||||
user_id: userId,
|
||||
vehicle_id: request.body.vehicle_id,
|
||||
document_type: request.body.document_type,
|
||||
userId,
|
||||
vehicleId: request.body.vehicleId,
|
||||
documentType: request.body.documentType,
|
||||
title: request.body.title,
|
||||
});
|
||||
|
||||
@@ -86,10 +86,10 @@ export class DocumentsController {
|
||||
|
||||
logger.info('Document created', {
|
||||
operation: 'documents.create.success',
|
||||
user_id: userId,
|
||||
document_id: created.id,
|
||||
vehicle_id: created.vehicle_id,
|
||||
document_type: created.document_type,
|
||||
userId,
|
||||
documentId: created.id,
|
||||
vehicleId: created.vehicleId,
|
||||
documentType: created.documentType,
|
||||
title: created.title,
|
||||
});
|
||||
|
||||
@@ -102,26 +102,26 @@ export class DocumentsController {
|
||||
|
||||
logger.info('Document update requested', {
|
||||
operation: 'documents.update',
|
||||
user_id: userId,
|
||||
document_id: documentId,
|
||||
update_fields: Object.keys(request.body),
|
||||
userId,
|
||||
documentId,
|
||||
updateFields: Object.keys(request.body),
|
||||
});
|
||||
|
||||
const updated = await this.service.updateDocument(userId, documentId, request.body);
|
||||
if (!updated) {
|
||||
logger.warn('Document not found for update', {
|
||||
operation: 'documents.update.not_found',
|
||||
user_id: userId,
|
||||
document_id: documentId,
|
||||
userId,
|
||||
documentId,
|
||||
});
|
||||
return reply.code(404).send({ error: 'Not Found' });
|
||||
}
|
||||
|
||||
logger.info('Document updated', {
|
||||
operation: 'documents.update.success',
|
||||
user_id: userId,
|
||||
document_id: documentId,
|
||||
vehicle_id: updated.vehicle_id,
|
||||
userId,
|
||||
documentId,
|
||||
vehicleId: updated.vehicleId,
|
||||
title: updated.title,
|
||||
});
|
||||
|
||||
@@ -134,28 +134,28 @@ export class DocumentsController {
|
||||
|
||||
logger.info('Document delete requested', {
|
||||
operation: 'documents.delete',
|
||||
user_id: userId,
|
||||
document_id: documentId,
|
||||
userId,
|
||||
documentId,
|
||||
});
|
||||
|
||||
// If object exists, delete it from storage first
|
||||
const existing = await this.service.getDocument(userId, documentId);
|
||||
if (existing && existing.storage_bucket && existing.storage_key) {
|
||||
if (existing && existing.storageBucket && existing.storageKey) {
|
||||
const storage = getStorageService();
|
||||
try {
|
||||
await storage.deleteObject(existing.storage_bucket, existing.storage_key);
|
||||
await storage.deleteObject(existing.storageBucket, existing.storageKey);
|
||||
logger.info('Document file deleted from storage', {
|
||||
operation: 'documents.delete.storage_cleanup',
|
||||
user_id: userId,
|
||||
document_id: documentId,
|
||||
storage_key: existing.storage_key,
|
||||
userId,
|
||||
documentId,
|
||||
storageKey: existing.storageKey,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.warn('Failed to delete document file from storage', {
|
||||
operation: 'documents.delete.storage_cleanup_failed',
|
||||
user_id: userId,
|
||||
document_id: documentId,
|
||||
storage_key: existing.storage_key,
|
||||
userId,
|
||||
documentId,
|
||||
storageKey: existing.storageKey,
|
||||
error: e instanceof Error ? e.message : 'Unknown error',
|
||||
});
|
||||
// Non-fatal: proceed with soft delete
|
||||
@@ -166,10 +166,10 @@ export class DocumentsController {
|
||||
|
||||
logger.info('Document deleted', {
|
||||
operation: 'documents.delete.success',
|
||||
user_id: userId,
|
||||
document_id: documentId,
|
||||
vehicle_id: existing?.vehicle_id,
|
||||
had_file: !!(existing?.storage_key),
|
||||
userId,
|
||||
documentId,
|
||||
vehicleId: existing?.vehicleId,
|
||||
hadFile: !!(existing?.storageKey),
|
||||
});
|
||||
|
||||
return reply.code(204).send();
|
||||
@@ -181,16 +181,16 @@ export class DocumentsController {
|
||||
|
||||
logger.info('Document upload requested', {
|
||||
operation: 'documents.upload',
|
||||
user_id: userId,
|
||||
document_id: documentId,
|
||||
userId,
|
||||
documentId,
|
||||
});
|
||||
|
||||
const doc = await this.service.getDocument(userId, documentId);
|
||||
if (!doc) {
|
||||
logger.warn('Document not found for upload', {
|
||||
operation: 'documents.upload.not_found',
|
||||
user_id: userId,
|
||||
document_id: documentId,
|
||||
userId,
|
||||
documentId,
|
||||
});
|
||||
return reply.code(404).send({ error: 'Not Found' });
|
||||
}
|
||||
@@ -199,8 +199,8 @@ export class DocumentsController {
|
||||
if (!mp) {
|
||||
logger.warn('No file provided for upload', {
|
||||
operation: 'documents.upload.no_file',
|
||||
user_id: userId,
|
||||
document_id: documentId,
|
||||
userId,
|
||||
documentId,
|
||||
});
|
||||
return reply.code(400).send({ error: 'Bad Request', message: 'No file provided' });
|
||||
}
|
||||
@@ -216,10 +216,10 @@ export class DocumentsController {
|
||||
if (!contentType || !allowedTypes.has(contentType)) {
|
||||
logger.warn('Unsupported file type for upload (header validation)', {
|
||||
operation: 'documents.upload.unsupported_type',
|
||||
user_id: userId,
|
||||
document_id: documentId,
|
||||
content_type: contentType,
|
||||
file_name: mp.filename,
|
||||
userId,
|
||||
documentId,
|
||||
contentType,
|
||||
fileName: mp.filename,
|
||||
});
|
||||
return reply.code(415).send({
|
||||
error: 'Unsupported Media Type',
|
||||
@@ -248,10 +248,10 @@ export class DocumentsController {
|
||||
if (!detectedType) {
|
||||
logger.warn('Unable to detect file type from content', {
|
||||
operation: 'documents.upload.type_detection_failed',
|
||||
user_id: userId,
|
||||
document_id: documentId,
|
||||
content_type: contentType,
|
||||
file_name: mp.filename,
|
||||
userId,
|
||||
documentId,
|
||||
contentType,
|
||||
fileName: mp.filename,
|
||||
});
|
||||
return reply.code(415).send({
|
||||
error: 'Unsupported Media Type',
|
||||
@@ -264,11 +264,11 @@ export class DocumentsController {
|
||||
if (!allowedDetectedTypes || !allowedDetectedTypes.has(detectedType.mime)) {
|
||||
logger.warn('File content does not match Content-Type header', {
|
||||
operation: 'documents.upload.type_mismatch',
|
||||
user_id: userId,
|
||||
document_id: documentId,
|
||||
claimed_type: contentType,
|
||||
detected_type: detectedType.mime,
|
||||
file_name: mp.filename,
|
||||
userId,
|
||||
documentId,
|
||||
claimedType: contentType,
|
||||
detectedType: detectedType.mime,
|
||||
fileName: mp.filename,
|
||||
});
|
||||
return reply.code(415).send({
|
||||
error: 'Unsupported Media Type',
|
||||
@@ -310,29 +310,29 @@ export class DocumentsController {
|
||||
const bucket = 'documents';
|
||||
const version = 'v1';
|
||||
const unique = cryptoRandom();
|
||||
const key = `documents/${userId}/${doc.vehicle_id}/${doc.id}/${version}/${unique}.${ext}`;
|
||||
const key = `documents/${userId}/${doc.vehicleId}/${doc.id}/${version}/${unique}.${ext}`;
|
||||
|
||||
await storage.putObject(bucket, key, counter, contentType, { 'x-original-filename': originalName });
|
||||
|
||||
const updated = await this.service['repo'].updateStorageMeta(doc.id, userId, {
|
||||
storage_bucket: bucket,
|
||||
storage_key: key,
|
||||
file_name: originalName,
|
||||
content_type: contentType,
|
||||
file_size: counter.bytes,
|
||||
file_hash: null,
|
||||
storageBucket: bucket,
|
||||
storageKey: key,
|
||||
fileName: originalName,
|
||||
contentType: contentType,
|
||||
fileSize: counter.bytes,
|
||||
fileHash: null,
|
||||
});
|
||||
|
||||
logger.info('Document upload completed', {
|
||||
operation: 'documents.upload.success',
|
||||
user_id: userId,
|
||||
document_id: documentId,
|
||||
vehicle_id: doc.vehicle_id,
|
||||
file_name: originalName,
|
||||
content_type: contentType,
|
||||
detected_type: detectedType.mime,
|
||||
file_size: counter.bytes,
|
||||
storage_key: key,
|
||||
userId,
|
||||
documentId,
|
||||
vehicleId: doc.vehicleId,
|
||||
fileName: originalName,
|
||||
contentType,
|
||||
detectedType: detectedType.mime,
|
||||
fileSize: counter.bytes,
|
||||
storageKey: key,
|
||||
});
|
||||
|
||||
return reply.code(200).send(updated);
|
||||
@@ -344,18 +344,18 @@ export class DocumentsController {
|
||||
|
||||
logger.info('Document download requested', {
|
||||
operation: 'documents.download',
|
||||
user_id: userId,
|
||||
document_id: documentId,
|
||||
userId,
|
||||
documentId,
|
||||
});
|
||||
|
||||
const doc = await this.service.getDocument(userId, documentId);
|
||||
if (!doc || !doc.storage_bucket || !doc.storage_key) {
|
||||
if (!doc || !doc.storageBucket || !doc.storageKey) {
|
||||
logger.warn('Document or file not found for download', {
|
||||
operation: 'documents.download.not_found',
|
||||
user_id: userId,
|
||||
document_id: documentId,
|
||||
has_document: !!doc,
|
||||
has_storage_info: !!(doc?.storage_bucket && doc?.storage_key),
|
||||
userId,
|
||||
documentId,
|
||||
hasDocument: !!doc,
|
||||
hasStorageInfo: !!(doc?.storageBucket && doc?.storageKey),
|
||||
});
|
||||
return reply.code(404).send({ error: 'Not Found' });
|
||||
}
|
||||
@@ -363,10 +363,10 @@ export class DocumentsController {
|
||||
const storage = getStorageService();
|
||||
let head: Partial<import('../../../core/storage/storage.service').HeadObjectResult> = {};
|
||||
try {
|
||||
head = await storage.headObject(doc.storage_bucket, doc.storage_key);
|
||||
head = await storage.headObject(doc.storageBucket, doc.storageKey);
|
||||
} catch { /* ignore */ }
|
||||
const contentType = head.contentType || doc.content_type || 'application/octet-stream';
|
||||
const filename = doc.file_name || path.basename(doc.storage_key);
|
||||
const contentType = head.contentType || doc.contentType || 'application/octet-stream';
|
||||
const filename = doc.fileName || path.basename(doc.storageKey);
|
||||
const inlineTypes = new Set(['application/pdf', 'image/jpeg', 'image/png']);
|
||||
const disposition = inlineTypes.has(contentType) ? 'inline' : 'attachment';
|
||||
|
||||
@@ -375,16 +375,16 @@ export class DocumentsController {
|
||||
|
||||
logger.info('Document download initiated', {
|
||||
operation: 'documents.download.success',
|
||||
user_id: userId,
|
||||
document_id: documentId,
|
||||
vehicle_id: doc.vehicle_id,
|
||||
file_name: filename,
|
||||
content_type: contentType,
|
||||
disposition: disposition,
|
||||
file_size: head.size || doc.file_size,
|
||||
userId,
|
||||
documentId,
|
||||
vehicleId: doc.vehicleId,
|
||||
fileName: filename,
|
||||
contentType,
|
||||
disposition,
|
||||
fileSize: head.size || doc.fileSize,
|
||||
});
|
||||
|
||||
const stream = await storage.getObjectStream(doc.storage_bucket, doc.storage_key);
|
||||
const stream = await storage.getObjectStream(doc.storageBucket, doc.storageKey);
|
||||
return reply.send(stream);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,40 +5,74 @@ import type { DocumentRecord, DocumentType } from '../domain/documents.types';
|
||||
export class DocumentsRepository {
|
||||
constructor(private readonly db: Pool = pool) {}
|
||||
|
||||
// ========================
|
||||
// Row Mapper
|
||||
// ========================
|
||||
|
||||
private mapDocumentRecord(row: any): DocumentRecord {
|
||||
return {
|
||||
id: row.id,
|
||||
userId: row.user_id,
|
||||
vehicleId: row.vehicle_id,
|
||||
documentType: row.document_type,
|
||||
title: row.title,
|
||||
notes: row.notes,
|
||||
details: row.details,
|
||||
storageBucket: row.storage_bucket,
|
||||
storageKey: row.storage_key,
|
||||
fileName: row.file_name,
|
||||
contentType: row.content_type,
|
||||
fileSize: row.file_size,
|
||||
fileHash: row.file_hash,
|
||||
issuedDate: row.issued_date,
|
||||
expirationDate: row.expiration_date,
|
||||
emailNotifications: row.email_notifications,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
deletedAt: row.deleted_at
|
||||
};
|
||||
}
|
||||
|
||||
// ========================
|
||||
// CRUD Operations
|
||||
// ========================
|
||||
|
||||
async insert(doc: {
|
||||
id: string;
|
||||
user_id: string;
|
||||
vehicle_id: string;
|
||||
document_type: DocumentType;
|
||||
userId: string;
|
||||
vehicleId: string;
|
||||
documentType: DocumentType;
|
||||
title: string;
|
||||
notes?: string | null;
|
||||
details?: any;
|
||||
issued_date?: string | null;
|
||||
expiration_date?: string | null;
|
||||
issuedDate?: string | null;
|
||||
expirationDate?: string | null;
|
||||
emailNotifications?: boolean;
|
||||
}): Promise<DocumentRecord> {
|
||||
const res = await this.db.query(
|
||||
`INSERT INTO documents (
|
||||
id, user_id, vehicle_id, document_type, title, notes, details, issued_date, expiration_date
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
|
||||
id, user_id, vehicle_id, document_type, title, notes, details, issued_date, expiration_date, email_notifications
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
|
||||
RETURNING *`,
|
||||
[
|
||||
doc.id,
|
||||
doc.user_id,
|
||||
doc.vehicle_id,
|
||||
doc.document_type,
|
||||
doc.userId,
|
||||
doc.vehicleId,
|
||||
doc.documentType,
|
||||
doc.title,
|
||||
doc.notes ?? null,
|
||||
doc.details ?? null,
|
||||
doc.issued_date ?? null,
|
||||
doc.expiration_date ?? null,
|
||||
doc.issuedDate ?? null,
|
||||
doc.expirationDate ?? null,
|
||||
doc.emailNotifications ?? false,
|
||||
]
|
||||
);
|
||||
return res.rows[0] as DocumentRecord;
|
||||
return this.mapDocumentRecord(res.rows[0]);
|
||||
}
|
||||
|
||||
async findById(id: string, userId: string): Promise<DocumentRecord | null> {
|
||||
const res = await this.db.query(`SELECT * FROM documents WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL`, [id, userId]);
|
||||
return res.rows[0] || null;
|
||||
return res.rows[0] ? this.mapDocumentRecord(res.rows[0]) : null;
|
||||
}
|
||||
|
||||
async listByUser(userId: string, filters?: { vehicleId?: string; type?: DocumentType; expiresBefore?: string }): Promise<DocumentRecord[]> {
|
||||
@@ -50,31 +84,32 @@ export class DocumentsRepository {
|
||||
if (filters?.expiresBefore) { conds.push(`expiration_date <= $${i++}`); params.push(filters.expiresBefore); }
|
||||
const sql = `SELECT * FROM documents WHERE ${conds.join(' AND ')} ORDER BY created_at DESC`;
|
||||
const res = await this.db.query(sql, params);
|
||||
return res.rows as DocumentRecord[];
|
||||
return res.rows.map(row => this.mapDocumentRecord(row));
|
||||
}
|
||||
|
||||
async softDelete(id: string, userId: string): Promise<void> {
|
||||
await this.db.query(`UPDATE documents SET deleted_at = NOW() WHERE id = $1 AND user_id = $2`, [id, userId]);
|
||||
}
|
||||
|
||||
async updateMetadata(id: string, userId: string, patch: Partial<Pick<DocumentRecord, 'title'|'notes'|'details'|'issued_date'|'expiration_date'>>): Promise<DocumentRecord | null> {
|
||||
async updateMetadata(id: string, userId: string, patch: Partial<Pick<DocumentRecord, 'title'|'notes'|'details'|'issuedDate'|'expirationDate'|'emailNotifications'>>): Promise<DocumentRecord | null> {
|
||||
const fields: string[] = [];
|
||||
const params: any[] = [];
|
||||
let i = 1;
|
||||
if (patch.title !== undefined) { fields.push(`title = $${i++}`); params.push(patch.title); }
|
||||
if (patch.notes !== undefined) { fields.push(`notes = $${i++}`); params.push(patch.notes); }
|
||||
if (patch.details !== undefined) { fields.push(`details = $${i++}`); params.push(patch.details); }
|
||||
if (patch.issued_date !== undefined) { fields.push(`issued_date = $${i++}`); params.push(patch.issued_date); }
|
||||
if (patch.expiration_date !== undefined) { fields.push(`expiration_date = $${i++}`); params.push(patch.expiration_date); }
|
||||
if (patch.issuedDate !== undefined) { fields.push(`issued_date = $${i++}`); params.push(patch.issuedDate); }
|
||||
if (patch.expirationDate !== undefined) { fields.push(`expiration_date = $${i++}`); params.push(patch.expirationDate); }
|
||||
if (patch.emailNotifications !== undefined) { fields.push(`email_notifications = $${i++}`); params.push(patch.emailNotifications); }
|
||||
if (!fields.length) return this.findById(id, userId);
|
||||
params.push(id, userId);
|
||||
const sql = `UPDATE documents SET ${fields.join(', ')} WHERE id = $${i++} AND user_id = $${i++} AND deleted_at IS NULL RETURNING *`;
|
||||
const res = await this.db.query(sql, params);
|
||||
return res.rows[0] || null;
|
||||
return res.rows[0] ? this.mapDocumentRecord(res.rows[0]) : null;
|
||||
}
|
||||
|
||||
async updateStorageMeta(id: string, userId: string, meta: {
|
||||
storage_bucket: string; storage_key: string; file_name: string; content_type: string; file_size: number; file_hash?: string | null;
|
||||
storageBucket: string; storageKey: string; fileName: string; contentType: string; fileSize: number; fileHash?: string | null;
|
||||
}): Promise<DocumentRecord | null> {
|
||||
const res = await this.db.query(
|
||||
`UPDATE documents SET
|
||||
@@ -86,9 +121,9 @@ export class DocumentsRepository {
|
||||
file_hash = $6
|
||||
WHERE id = $7 AND user_id = $8 AND deleted_at IS NULL
|
||||
RETURNING *`,
|
||||
[meta.storage_bucket, meta.storage_key, meta.file_name, meta.content_type, meta.file_size, meta.file_hash ?? null, id, userId]
|
||||
[meta.storageBucket, meta.storageKey, meta.fileName, meta.contentType, meta.fileSize, meta.fileHash ?? null, id, userId]
|
||||
);
|
||||
return res.rows[0] || null;
|
||||
return res.rows[0] ? this.mapDocumentRecord(res.rows[0]) : null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,18 +7,19 @@ export class DocumentsService {
|
||||
private readonly repo = new DocumentsRepository(pool);
|
||||
|
||||
async createDocument(userId: string, body: CreateDocumentBody): Promise<DocumentRecord> {
|
||||
await this.assertVehicleOwnership(userId, body.vehicle_id);
|
||||
await this.assertVehicleOwnership(userId, body.vehicleId);
|
||||
const id = randomUUID();
|
||||
return this.repo.insert({
|
||||
id,
|
||||
user_id: userId,
|
||||
vehicle_id: body.vehicle_id,
|
||||
document_type: body.document_type as DocumentType,
|
||||
userId,
|
||||
vehicleId: body.vehicleId,
|
||||
documentType: body.documentType as DocumentType,
|
||||
title: body.title,
|
||||
notes: body.notes ?? null,
|
||||
details: body.details ?? null,
|
||||
issued_date: body.issued_date ?? null,
|
||||
expiration_date: body.expiration_date ?? null,
|
||||
issuedDate: body.issuedDate ?? null,
|
||||
expirationDate: body.expirationDate ?? null,
|
||||
emailNotifications: body.emailNotifications ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,35 +3,39 @@ import { z } from 'zod';
|
||||
export const DocumentTypeSchema = z.enum(['insurance', 'registration']);
|
||||
export type DocumentType = z.infer<typeof DocumentTypeSchema>;
|
||||
|
||||
// API response type (camelCase for frontend)
|
||||
export interface DocumentRecord {
|
||||
id: string;
|
||||
user_id: string;
|
||||
vehicle_id: string;
|
||||
document_type: DocumentType;
|
||||
userId: string;
|
||||
vehicleId: string;
|
||||
documentType: DocumentType;
|
||||
title: string;
|
||||
notes?: string | null;
|
||||
details?: Record<string, any> | null;
|
||||
storage_bucket?: string | null;
|
||||
storage_key?: string | null;
|
||||
file_name?: string | null;
|
||||
content_type?: string | null;
|
||||
file_size?: number | null;
|
||||
file_hash?: string | null;
|
||||
issued_date?: string | null;
|
||||
expiration_date?: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
deleted_at?: string | null;
|
||||
storageBucket?: string | null;
|
||||
storageKey?: string | null;
|
||||
fileName?: string | null;
|
||||
contentType?: string | null;
|
||||
fileSize?: number | null;
|
||||
fileHash?: string | null;
|
||||
issuedDate?: string | null;
|
||||
expirationDate?: string | null;
|
||||
emailNotifications?: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt?: string | null;
|
||||
}
|
||||
|
||||
// API request schemas (camelCase for frontend)
|
||||
export const CreateDocumentBodySchema = z.object({
|
||||
vehicle_id: z.string().uuid(),
|
||||
document_type: DocumentTypeSchema,
|
||||
vehicleId: z.string().uuid(),
|
||||
documentType: DocumentTypeSchema,
|
||||
title: z.string().min(1).max(200),
|
||||
notes: z.string().max(10000).optional(),
|
||||
details: z.record(z.any()).optional(),
|
||||
issued_date: z.string().optional(),
|
||||
expiration_date: z.string().optional(),
|
||||
issuedDate: z.string().optional(),
|
||||
expirationDate: z.string().optional(),
|
||||
emailNotifications: z.boolean().optional(),
|
||||
});
|
||||
export type CreateDocumentBody = z.infer<typeof CreateDocumentBodySchema>;
|
||||
|
||||
@@ -39,8 +43,9 @@ export const UpdateDocumentBodySchema = z.object({
|
||||
title: z.string().min(1).max(200).optional(),
|
||||
notes: z.string().max(10000).nullable().optional(),
|
||||
details: z.record(z.any()).optional(),
|
||||
issued_date: z.string().nullable().optional(),
|
||||
expiration_date: z.string().nullable().optional(),
|
||||
issuedDate: z.string().nullable().optional(),
|
||||
expirationDate: z.string().nullable().optional(),
|
||||
emailNotifications: z.boolean().optional(),
|
||||
});
|
||||
export type UpdateDocumentBody = z.infer<typeof UpdateDocumentBodySchema>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user