task-i
This commit is contained in:
313
src/features/foundation/document-artifact/server/service.ts
Normal file
313
src/features/foundation/document-artifact/server/service.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
import { and, desc, eq, inArray, isNull } from 'drizzle-orm';
|
||||
import { crmDocumentArtifacts, users } from '@/db/schema';
|
||||
import { db } from '@/lib/db';
|
||||
import { AuthError } from '@/lib/auth/session';
|
||||
import { auditAction } from '@/features/foundation/audit-log/service';
|
||||
import { getStorageProvider } from '@/features/foundation/storage/service';
|
||||
|
||||
export type DocumentArtifactRecord = {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
documentType: string;
|
||||
artifactType: string;
|
||||
templateVersionId: string | null;
|
||||
storageProvider: string;
|
||||
storageKey: string;
|
||||
fileName: string;
|
||||
contentType: string;
|
||||
fileSize: number | null;
|
||||
checksum: string | null;
|
||||
status: string;
|
||||
generatedBy: string;
|
||||
generatedByName: string | null;
|
||||
generatedAt: string;
|
||||
lockedAt: string | null;
|
||||
lockedBy: string | null;
|
||||
lockedByName: string | null;
|
||||
voidedAt: string | null;
|
||||
voidedBy: string | null;
|
||||
voidReason: string | null;
|
||||
metadata: unknown | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
};
|
||||
|
||||
function mapDocumentArtifactRecord(
|
||||
row: typeof crmDocumentArtifacts.$inferSelect,
|
||||
names?: { generatedByName?: string | null; lockedByName?: string | null }
|
||||
): DocumentArtifactRecord {
|
||||
return {
|
||||
id: row.id,
|
||||
organizationId: row.organizationId,
|
||||
entityType: row.entityType,
|
||||
entityId: row.entityId,
|
||||
documentType: row.documentType,
|
||||
artifactType: row.artifactType,
|
||||
templateVersionId: row.templateVersionId,
|
||||
storageProvider: row.storageProvider,
|
||||
storageKey: row.storageKey,
|
||||
fileName: row.fileName,
|
||||
contentType: row.contentType,
|
||||
fileSize: row.fileSize,
|
||||
checksum: row.checksum,
|
||||
status: row.status,
|
||||
generatedBy: row.generatedBy,
|
||||
generatedByName: names?.generatedByName ?? null,
|
||||
generatedAt: row.generatedAt.toISOString(),
|
||||
lockedAt: row.lockedAt?.toISOString() ?? null,
|
||||
lockedBy: row.lockedBy,
|
||||
lockedByName: names?.lockedByName ?? null,
|
||||
voidedAt: row.voidedAt?.toISOString() ?? null,
|
||||
voidedBy: row.voidedBy,
|
||||
voidReason: row.voidReason,
|
||||
metadata: row.metadata ?? null,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
deletedAt: row.deletedAt?.toISOString() ?? null
|
||||
};
|
||||
}
|
||||
|
||||
async function enrichArtifact(row: typeof crmDocumentArtifacts.$inferSelect) {
|
||||
const userIds = [row.generatedBy, row.lockedBy].filter(Boolean) as string[];
|
||||
const userRows = userIds.length
|
||||
? await db
|
||||
.select({ id: users.id, name: users.name })
|
||||
.from(users)
|
||||
.where(inArray(users.id, userIds))
|
||||
: [];
|
||||
const userMap = new Map(userRows.map((user) => [user.id, user.name]));
|
||||
|
||||
return mapDocumentArtifactRecord(row, {
|
||||
generatedByName: userMap.get(row.generatedBy) ?? null,
|
||||
lockedByName: row.lockedBy ? (userMap.get(row.lockedBy) ?? null) : null
|
||||
});
|
||||
}
|
||||
|
||||
export async function createArtifact(input: {
|
||||
organizationId: string;
|
||||
userId: string;
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
documentType: string;
|
||||
artifactType: 'preview' | 'approved_pdf' | 'export';
|
||||
templateVersionId?: string | null;
|
||||
storageProvider: string;
|
||||
storageKey: string;
|
||||
fileName: string;
|
||||
contentType: string;
|
||||
fileSize?: number | null;
|
||||
checksum?: string | null;
|
||||
metadata?: unknown;
|
||||
}) {
|
||||
const [created] = await db
|
||||
.insert(crmDocumentArtifacts)
|
||||
.values({
|
||||
id: crypto.randomUUID(),
|
||||
organizationId: input.organizationId,
|
||||
entityType: input.entityType,
|
||||
entityId: input.entityId,
|
||||
documentType: input.documentType,
|
||||
artifactType: input.artifactType,
|
||||
templateVersionId: input.templateVersionId ?? null,
|
||||
storageProvider: input.storageProvider,
|
||||
storageKey: input.storageKey,
|
||||
fileName: input.fileName,
|
||||
contentType: input.contentType,
|
||||
fileSize: input.fileSize ?? null,
|
||||
checksum: input.checksum ?? null,
|
||||
status: 'active',
|
||||
generatedBy: input.userId,
|
||||
generatedAt: new Date(),
|
||||
metadata: input.metadata ?? null
|
||||
})
|
||||
.returning();
|
||||
|
||||
await auditAction({
|
||||
organizationId: input.organizationId,
|
||||
userId: input.userId,
|
||||
entityType: 'crm_document_artifact',
|
||||
entityId: created.id,
|
||||
action: 'create',
|
||||
afterData: created
|
||||
});
|
||||
|
||||
return enrichArtifact(created);
|
||||
}
|
||||
|
||||
async function getArtifactRow(id: string, organizationId: string) {
|
||||
const [artifact] = await db
|
||||
.select()
|
||||
.from(crmDocumentArtifacts)
|
||||
.where(
|
||||
and(
|
||||
eq(crmDocumentArtifacts.id, id),
|
||||
eq(crmDocumentArtifacts.organizationId, organizationId),
|
||||
isNull(crmDocumentArtifacts.deletedAt)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!artifact) {
|
||||
throw new AuthError('Document artifact not found', 404);
|
||||
}
|
||||
|
||||
return artifact;
|
||||
}
|
||||
|
||||
export async function getArtifact(id: string, organizationId: string) {
|
||||
return enrichArtifact(await getArtifactRow(id, organizationId));
|
||||
}
|
||||
|
||||
export async function getActiveArtifactForEntity(input: {
|
||||
organizationId: string;
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
artifactType: string;
|
||||
}) {
|
||||
const artifacts = await db
|
||||
.select()
|
||||
.from(crmDocumentArtifacts)
|
||||
.where(
|
||||
and(
|
||||
eq(crmDocumentArtifacts.organizationId, input.organizationId),
|
||||
eq(crmDocumentArtifacts.entityType, input.entityType),
|
||||
eq(crmDocumentArtifacts.entityId, input.entityId),
|
||||
eq(crmDocumentArtifacts.artifactType, input.artifactType),
|
||||
isNull(crmDocumentArtifacts.deletedAt)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(crmDocumentArtifacts.createdAt));
|
||||
|
||||
const artifact = artifacts.find((row) => row.status === 'active' || row.status === 'locked');
|
||||
if (!artifact || artifact.status === 'voided' || artifact.status === 'deleted') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return enrichArtifact(artifact);
|
||||
}
|
||||
|
||||
export async function lockArtifact(input: {
|
||||
artifactId: string;
|
||||
organizationId: string;
|
||||
userId: string;
|
||||
}) {
|
||||
const row = await getArtifactRow(input.artifactId, input.organizationId);
|
||||
const [updated] = await db
|
||||
.update(crmDocumentArtifacts)
|
||||
.set({
|
||||
status: 'locked',
|
||||
lockedAt: row.lockedAt ?? new Date(),
|
||||
lockedBy: row.lockedBy ?? input.userId,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(crmDocumentArtifacts.id, input.artifactId))
|
||||
.returning();
|
||||
|
||||
await auditAction({
|
||||
organizationId: input.organizationId,
|
||||
userId: input.userId,
|
||||
entityType: 'crm_document_artifact',
|
||||
entityId: updated.id,
|
||||
action: 'lock',
|
||||
beforeData: row,
|
||||
afterData: updated
|
||||
});
|
||||
|
||||
return enrichArtifact(updated);
|
||||
}
|
||||
|
||||
export async function voidArtifact(input: {
|
||||
artifactId: string;
|
||||
organizationId: string;
|
||||
userId: string;
|
||||
reason: string;
|
||||
}) {
|
||||
const row = await getArtifactRow(input.artifactId, input.organizationId);
|
||||
const [updated] = await db
|
||||
.update(crmDocumentArtifacts)
|
||||
.set({
|
||||
status: 'voided',
|
||||
voidedAt: new Date(),
|
||||
voidedBy: input.userId,
|
||||
voidReason: input.reason.trim(),
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(crmDocumentArtifacts.id, input.artifactId))
|
||||
.returning();
|
||||
|
||||
await auditAction({
|
||||
organizationId: input.organizationId,
|
||||
userId: input.userId,
|
||||
entityType: 'crm_document_artifact',
|
||||
entityId: updated.id,
|
||||
action: 'void',
|
||||
beforeData: row,
|
||||
afterData: updated
|
||||
});
|
||||
|
||||
return enrichArtifact(updated);
|
||||
}
|
||||
|
||||
export async function deleteArtifactMetadataOnly(input: {
|
||||
artifactId: string;
|
||||
organizationId: string;
|
||||
}) {
|
||||
await db
|
||||
.update(crmDocumentArtifacts)
|
||||
.set({
|
||||
status: 'deleted',
|
||||
deletedAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(crmDocumentArtifacts.id, input.artifactId),
|
||||
eq(crmDocumentArtifacts.organizationId, input.organizationId)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function downloadArtifact(input: {
|
||||
artifactId: string;
|
||||
organizationId: string;
|
||||
userId?: string;
|
||||
auditDownload?: boolean;
|
||||
}) {
|
||||
const artifact = await getArtifactRow(input.artifactId, input.organizationId);
|
||||
|
||||
if (artifact.status === 'voided' || artifact.status === 'deleted') {
|
||||
throw new AuthError('Document artifact is not available for download', 409);
|
||||
}
|
||||
|
||||
const object = await getStorageProvider().getObject({
|
||||
key: artifact.storageKey
|
||||
});
|
||||
|
||||
if (input.auditDownload && input.userId) {
|
||||
await auditAction({
|
||||
organizationId: input.organizationId,
|
||||
userId: input.userId,
|
||||
entityType: 'crm_document_artifact',
|
||||
entityId: artifact.id,
|
||||
action: 'download',
|
||||
afterData: {
|
||||
storageKey: artifact.storageKey,
|
||||
fileName: artifact.fileName
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
artifact: await enrichArtifact(artifact),
|
||||
object: {
|
||||
...object,
|
||||
contentType: artifact.contentType,
|
||||
fileName: artifact.fileName,
|
||||
size: artifact.fileSize ?? object.size,
|
||||
checksum: artifact.checksum
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user