feat: comprehensive project structure improvements and Cloud for Sovereignty landing zone

- Add Cloud for Sovereignty landing zone architecture and deployment
- Implement complete legal document management system
- Reorganize documentation with improved navigation
- Add infrastructure improvements (Dockerfiles, K8s, monitoring)
- Add operational improvements (graceful shutdown, rate limiting, caching)
- Create comprehensive project structure documentation
- Add Azure deployment automation scripts
- Improve repository navigation and organization
This commit is contained in:
defiQUG
2025-11-13 09:32:55 -08:00
parent 92cc41d26d
commit 6a8582e54d
202 changed files with 22699 additions and 981 deletions

View File

@@ -0,0 +1,162 @@
/**
* Legal Documents Service
* Comprehensive document management for law firms and courts
*/
import Fastify, { type FastifyInstance } from 'fastify';
import fastifySwagger from '@fastify/swagger';
import fastifySwaggerUI from '@fastify/swagger-ui';
import {
errorHandler,
createLogger,
registerSecurityPlugins,
addCorrelationId,
addRequestLogging,
getEnv,
} from '@the-order/shared';
import { getPool } from '@the-order/database';
// Import route handlers
import { registerDocumentRoutes } from './routes/document-routes';
import { registerVersionRoutes } from './routes/version-routes';
import { registerTemplateRoutes } from './routes/template-routes';
import { registerMatterRoutes } from './routes/matter-routes';
import { registerAssemblyRoutes } from './routes/assembly-routes';
import { registerCollaborationRoutes } from './routes/collaboration-routes';
import { registerWorkflowRoutes } from './routes/workflow-routes';
import { registerFilingRoutes } from './routes/filing-routes';
import { registerAuditRoutes } from './routes/audit-routes';
import { registerSearchRoutes } from './routes/search-routes';
import { registerSecurityRoutes } from './routes/security-routes';
import { registerRetentionRoutes } from './routes/retention-routes';
import { registerClauseRoutes } from './routes/clause-routes';
const logger = createLogger('legal-documents-service');
const server: FastifyInstance = Fastify({
logger: logger as any,
requestIdLogLabel: 'requestId',
disableRequestLogging: false,
});
// Initialize database pool
const env = getEnv();
if (env.DATABASE_URL) {
getPool({ connectionString: env.DATABASE_URL });
}
// Initialize server
async function initializeServer(): Promise<void> {
// Register Swagger
const swaggerUrl =
env.SWAGGER_SERVER_URL ||
(env.NODE_ENV === 'development' ? 'http://localhost:4005' : undefined);
if (swaggerUrl) {
await server.register(fastifySwagger, {
openapi: {
info: {
title: 'Legal Documents Service API',
description: 'Comprehensive document management for law firms and courts',
version: '1.0.0',
},
servers: [
{
url: swaggerUrl,
description: env.NODE_ENV || 'Development server',
},
],
},
});
await server.register(fastifySwaggerUI, {
routePrefix: '/docs',
});
}
await registerSecurityPlugins(server as any);
addCorrelationId(server as any);
addRequestLogging(server as any);
server.setErrorHandler(errorHandler as any);
// Register all routes
await registerDocumentRoutes(server);
await registerVersionRoutes(server);
await registerTemplateRoutes(server);
await registerMatterRoutes(server);
await registerAssemblyRoutes(server);
await registerCollaborationRoutes(server);
await registerWorkflowRoutes(server);
await registerFilingRoutes(server);
await registerAuditRoutes(server);
await registerSearchRoutes(server);
await registerSecurityRoutes(server);
await registerRetentionRoutes(server);
await registerClauseRoutes(server);
}
// Health check
server.get(
'/health',
{
schema: {
description: 'Health check endpoint',
tags: ['health'],
response: {
200: {
type: 'object',
properties: {
status: { type: 'string' },
service: { type: 'string' },
timestamp: { type: 'string' },
},
},
},
},
},
async (request, reply) => {
return reply.send({
status: 'healthy',
service: 'legal-documents',
timestamp: new Date().toISOString(),
});
}
);
// Start server
async function start(): Promise<void> {
try {
await initializeServer();
const port = env.PORT ? parseInt(env.PORT, 10) : 4005;
const host = env.HOST || '0.0.0.0';
await server.listen({ port, host });
logger.info(`Legal Documents Service listening on ${host}:${port}`);
logger.info(`Swagger documentation available at http://${host}:${port}/docs`);
} catch (err) {
logger.error('Error starting server:', err);
process.exit(1);
}
}
// Handle graceful shutdown
process.on('SIGTERM', async () => {
logger.info('SIGTERM received, shutting down gracefully');
await server.close();
process.exit(0);
});
process.on('SIGINT', async () => {
logger.info('SIGINT received, shutting down gracefully');
await server.close();
process.exit(0);
});
if (require.main === module) {
start().catch((err) => {
logger.error('Fatal error:', err);
process.exit(1);
});
}
export default server;

View File

@@ -0,0 +1,222 @@
/**
* Document Assembly Routes
* Template-based document generation and assembly
*/
import { FastifyInstance } from 'fastify';
import {
getDocumentTemplate,
renderDocumentTemplate,
extractTemplateVariables,
getClause,
listClauses,
renderClause,
} from '@the-order/database';
import { authenticateJWT, requireRole } from '@the-order/shared';
import { createDocument, createDocumentVersion } from '@the-order/database';
export async function registerAssemblyRoutes(server: FastifyInstance): Promise<void> {
// Generate document from template
server.post(
'/assembly/generate',
{
preHandler: [authenticateJWT, requireRole('admin', 'attorney', 'legal_staff')],
schema: {
body: {
type: 'object',
required: ['template_id', 'variables', 'title'],
properties: {
template_id: { type: 'string' },
variables: { type: 'object' },
title: { type: 'string' },
type: { type: 'string', enum: ['legal', 'treaty', 'finance', 'history'] },
matter_id: { type: 'string' },
relationship_type: { type: 'string' },
save_document: { type: 'boolean' },
},
},
tags: ['assembly'],
},
},
async (request, reply) => {
const body = request.body as {
template_id: string;
variables: Record<string, unknown>;
title: string;
type?: string;
matter_id?: string;
relationship_type?: string;
save_document?: boolean;
};
const user = (request as any).user;
const template = await getDocumentTemplate(body.template_id);
if (!template) {
return reply.code(404).send({ error: 'Template not found' });
}
// Render template
const rendered = renderDocumentTemplate(template, body.variables);
if (body.save_document) {
// Create document from rendered template
const document = await createDocument({
title: body.title,
type: (body.type || 'legal') as 'legal' | 'treaty' | 'finance' | 'history',
content: rendered,
});
// Create initial version
await createDocumentVersion({
document_id: document.id,
version_number: 1,
version_label: 'Generated from Template',
content: rendered,
change_type: 'created',
created_by: user?.id,
change_summary: `Generated from template: ${template.name}`,
});
// Link to matter if provided
if (body.matter_id && body.relationship_type) {
const { linkDocumentToMatter } = await import('@the-order/database');
await linkDocumentToMatter(body.matter_id, document.id, body.relationship_type);
}
return reply.code(201).send({
document,
rendered,
template_used: template.name,
});
}
return reply.send({ rendered, template_used: template.name });
}
);
// Preview template rendering
server.post(
'/assembly/preview',
{
preHandler: [authenticateJWT],
schema: {
body: {
type: 'object',
required: ['template_id', 'variables'],
properties: {
template_id: { type: 'string' },
variables: { type: 'object' },
},
},
tags: ['assembly'],
},
},
async (request, reply) => {
const { template_id, variables } = request.body as {
template_id: string;
variables: Record<string, unknown>;
};
const template = await getDocumentTemplate(template_id);
if (!template) {
return reply.code(404).send({ error: 'Template not found' });
}
const rendered = renderDocumentTemplate(template, variables);
const variables_used = extractTemplateVariables(template.template_content);
return reply.send({
rendered,
variables_used,
template_name: template.name,
});
}
);
// Assemble document from multiple clauses
server.post(
'/assembly/from-clauses',
{
preHandler: [authenticateJWT, requireRole('admin', 'attorney', 'legal_staff')],
schema: {
body: {
type: 'object',
required: ['clause_ids', 'title'],
properties: {
clause_ids: { type: 'array', items: { type: 'string' } },
variables: { type: 'object' },
title: { type: 'string' },
type: { type: 'string' },
matter_id: { type: 'string' },
save_document: { type: 'boolean' },
},
},
tags: ['assembly'],
},
},
async (request, reply) => {
const body = request.body as {
clause_ids: string[];
variables?: Record<string, unknown>;
title: string;
type?: string;
matter_id?: string;
save_document?: boolean;
};
const user = (request as any).user;
// Fetch all clauses
const clauses = await Promise.all(
body.clause_ids.map((id) => getClause(id))
);
const missing = clauses.findIndex((c) => !c);
if (missing !== -1) {
return reply.code(404).send({
error: `Clause not found: ${body.clause_ids[missing]}`,
});
}
// Render and combine clauses
const rendered_parts = clauses.map((clause) =>
renderClause(clause!, body.variables || {})
);
const rendered = rendered_parts.join('\n\n');
if (body.save_document) {
const document = await createDocument({
title: body.title,
type: (body.type || 'legal') as 'legal' | 'treaty' | 'finance' | 'history',
content: rendered,
});
await createDocumentVersion({
document_id: document.id,
version_number: 1,
version_label: 'Assembled from Clauses',
content: rendered,
change_type: 'created',
created_by: user?.id,
change_summary: `Assembled from ${clauses.length} clauses`,
});
if (body.matter_id) {
const { linkDocumentToMatter } = await import('@the-order/database');
await linkDocumentToMatter(body.matter_id, document.id, 'draft');
}
return reply.code(201).send({
document,
rendered,
clauses_used: clauses.map((c) => c!.name),
});
}
return reply.send({
rendered,
clauses_used: clauses.map((c) => c!.name),
});
}
);
}

View File

@@ -0,0 +1,61 @@
/**
* Document Audit Routes
* Audit logs and compliance reporting
*/
import { FastifyInstance } from 'fastify';
import {
getDocumentAuditLogs,
searchDocumentAuditLogs,
getAuditStatistics,
} from '@the-order/database';
import { authenticateJWT, requireRole } from '@the-order/shared';
export async function registerAuditRoutes(server: FastifyInstance): Promise<void> {
server.get('/documents/:id/audit', {
preHandler: [authenticateJWT, requireRole('admin', 'attorney')],
schema: {
params: { type: 'object', properties: { id: { type: 'string' } } },
querystring: {
type: 'object',
properties: { action: { type: 'string' }, start_date: { type: 'string' }, end_date: { type: 'string' } },
},
tags: ['audit'],
},
}, async (request, reply) => {
const { id } = request.params as { id: string };
const query = request.query as any;
const logs = await getDocumentAuditLogs(id, query);
return reply.send({ logs });
});
server.get('/audit/search', {
preHandler: [authenticateJWT, requireRole('admin')],
schema: {
querystring: {
type: 'object',
properties: {
document_id: { type: 'string' },
action: { type: 'string' },
performed_by: { type: 'string' },
start_date: { type: 'string' },
end_date: { type: 'string' },
},
},
tags: ['audit'],
},
}, async (request, reply) => {
const query = request.query as any;
const result = await searchDocumentAuditLogs(query);
return reply.send(result);
});
server.get('/audit/statistics', {
preHandler: [authenticateJWT, requireRole('admin')],
schema: { tags: ['audit'] },
}, async (request, reply) => {
const stats = await getAuditStatistics();
return reply.send(stats);
});
}

View File

@@ -0,0 +1,88 @@
/**
* Clause Library Routes
* Reusable clause management
*/
import { FastifyInstance } from 'fastify';
import {
createClause,
getClause,
getClauseByName,
listClauses,
updateClause,
createClauseVersion,
renderClause,
getClauseUsageStats,
} from '@the-order/database';
import { authenticateJWT, requireRole } from '@the-order/shared';
export async function registerClauseRoutes(server: FastifyInstance): Promise<void> {
server.post('/clauses', {
preHandler: [authenticateJWT, requireRole('admin', 'attorney')],
schema: {
body: {
type: 'object',
required: ['name', 'clause_text'],
properties: {
name: { type: 'string' },
title: { type: 'string' },
clause_text: { type: 'string' },
category: { type: 'string' },
tags: { type: 'array', items: { type: 'string' } },
},
},
tags: ['clauses'],
},
}, async (request, reply) => {
const body = request.body as any;
const user = (request as any).user;
const clause = await createClause({ ...body, created_by: user?.id });
return reply.code(201).send(clause);
});
server.get('/clauses/:id', {
preHandler: [authenticateJWT],
schema: { params: { type: 'object', properties: { id: { type: 'string' } } }, tags: ['clauses'] },
}, async (request, reply) => {
const { id } = request.params as { id: string };
const clause = await getClause(id);
if (!clause) return reply.code(404).send({ error: 'Clause not found' });
return reply.send(clause);
});
server.get('/clauses', {
preHandler: [authenticateJWT],
schema: {
querystring: {
type: 'object',
properties: {
category: { type: 'string' },
search: { type: 'string' },
tags: { type: 'array', items: { type: 'string' } },
},
},
tags: ['clauses'],
},
}, async (request, reply) => {
const query = request.query as any;
const clauses = await listClauses(query);
return reply.send({ clauses });
});
server.post('/clauses/:id/render', {
preHandler: [authenticateJWT],
schema: {
params: { type: 'object', properties: { id: { type: 'string' } } },
body: { type: 'object', required: ['variables'], properties: { variables: { type: 'object' } } },
tags: ['clauses'],
},
}, async (request, reply) => {
const { id } = request.params as { id: string };
const { variables } = request.body as { variables: Record<string, unknown> };
const clause = await getClause(id);
if (!clause) return reply.code(404).send({ error: 'Clause not found' });
const rendered = renderClause(clause, variables);
return reply.send({ rendered });
});
}

View File

@@ -0,0 +1,196 @@
/**
* Document Collaboration Routes
* Comments, annotations, and review features
*/
import { FastifyInstance } from 'fastify';
import {
createDocumentComment,
getDocumentComments,
getThreadedDocumentComments,
updateDocumentComment,
resolveDocumentComment,
getDocumentCommentStatistics,
} from '@the-order/database';
import { authenticateJWT, requireRole } from '@the-order/shared';
import { createDocumentAuditLog } from '@the-order/database';
export async function registerCollaborationRoutes(server: FastifyInstance): Promise<void> {
// Create comment
server.post(
'/documents/:id/comments',
{
preHandler: [authenticateJWT],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
body: {
type: 'object',
required: ['comment_text'],
properties: {
version_id: { type: 'string' },
parent_comment_id: { type: 'string' },
comment_text: { type: 'string' },
comment_type: { type: 'string', enum: ['comment', 'suggestion', 'question', 'resolution'] },
page_number: { type: 'number' },
x_position: { type: 'number' },
y_position: { type: 'number' },
highlight_text: { type: 'string' },
},
},
tags: ['collaboration'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const body = request.body as any;
const user = (request as any).user;
const comment = await createDocumentComment({
document_id: id,
...body,
author_id: user.id,
});
await createDocumentAuditLog({
document_id: id,
version_id: body.version_id,
action: 'commented',
performed_by: user.id,
});
return reply.code(201).send(comment);
}
);
// Get comments for document
server.get(
'/documents/:id/comments',
{
preHandler: [authenticateJWT],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
querystring: {
type: 'object',
properties: {
version_id: { type: 'string' },
include_resolved: { type: 'boolean' },
threaded: { type: 'boolean' },
},
},
tags: ['collaboration'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const query = request.query as {
version_id?: string;
include_resolved?: boolean;
threaded?: boolean;
};
if (query.threaded) {
const comments = await getThreadedDocumentComments(id, query.version_id);
return reply.send({ comments });
} else {
const comments = await getDocumentComments(
id,
query.version_id,
query.include_resolved || false
);
return reply.send({ comments });
}
}
);
// Update comment
server.patch(
'/comments/:id',
{
preHandler: [authenticateJWT],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
body: {
type: 'object',
properties: {
comment_text: { type: 'string' },
status: { type: 'string', enum: ['open', 'resolved', 'dismissed'] },
},
},
tags: ['collaboration'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const body = request.body as any;
const comment = await updateDocumentComment(id, body);
if (!comment) {
return reply.code(404).send({ error: 'Comment not found' });
}
return reply.send(comment);
}
);
// Resolve comment
server.post(
'/comments/:id/resolve',
{
preHandler: [authenticateJWT],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
tags: ['collaboration'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const user = (request as any).user;
const comment = await resolveDocumentComment(id, user.id);
if (!comment) {
return reply.code(404).send({ error: 'Comment not found' });
}
return reply.send(comment);
}
);
// Get comment statistics
server.get(
'/documents/:id/comments/statistics',
{
preHandler: [authenticateJWT],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
tags: ['collaboration'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const stats = await getDocumentCommentStatistics(id);
return reply.send(stats);
}
);
}

View File

@@ -0,0 +1,359 @@
/**
* Document Management Routes
* CRUD operations for documents with versioning
*/
import { FastifyInstance } from 'fastify';
import {
createDocument,
updateDocument,
getDocumentById,
listDocuments,
} from '@the-order/database';
import { authenticateJWT, requireRole } from '@the-order/shared';
import {
createDocumentVersion,
getDocumentVersions,
getLatestDocumentVersion,
} from '@the-order/database';
import { createDocumentAuditLog } from '@the-order/database';
import { checkoutDocument, checkinDocument, getDocumentCheckout } from '@the-order/database';
export async function registerDocumentRoutes(server: FastifyInstance): Promise<void> {
// Create document
server.post(
'/documents',
{
preHandler: [authenticateJWT, requireRole('admin', 'attorney', 'legal_staff')],
schema: {
body: {
type: 'object',
required: ['title', 'type'],
properties: {
title: { type: 'string' },
type: { type: 'string', enum: ['legal', 'treaty', 'finance', 'history'] },
content: { type: 'string' },
fileUrl: { type: 'string' },
matter_id: { type: 'string' },
relationship_type: { type: 'string' },
},
},
tags: ['documents'],
},
},
async (request, reply) => {
const body = request.body as {
title: string;
type: string;
content?: string;
fileUrl?: string;
matter_id?: string;
relationship_type?: string;
};
const user = (request as any).user;
// Create document
const document = await createDocument({
title: body.title,
type: body.type as 'legal' | 'treaty' | 'finance' | 'history',
content: body.content,
fileUrl: body.fileUrl,
});
// Create initial version
await createDocumentVersion({
document_id: document.id,
version_number: 1,
version_label: 'Initial Version',
content: body.content,
file_url: body.fileUrl,
change_type: 'created',
created_by: user?.id,
});
// Link to matter if provided
if (body.matter_id && body.relationship_type) {
const { linkDocumentToMatter } = await import('@the-order/database');
await linkDocumentToMatter(
body.matter_id,
document.id,
body.relationship_type
);
}
// Audit log
await createDocumentAuditLog({
document_id: document.id,
action: 'created',
performed_by: user?.id,
ip_address: request.ip,
user_agent: request.headers['user-agent'],
});
return reply.code(201).send(document);
}
);
// Get document by ID
server.get(
'/documents/:id',
{
preHandler: [authenticateJWT],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
tags: ['documents'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const user = (request as any).user;
const document = await getDocumentById(id);
if (!document) {
return reply.code(404).send({ error: 'Document not found' });
}
// Audit log
await createDocumentAuditLog({
document_id: id,
action: 'viewed',
performed_by: user?.id,
ip_address: request.ip,
user_agent: request.headers['user-agent'],
});
return reply.send(document);
}
);
// List documents
server.get(
'/documents',
{
preHandler: [authenticateJWT],
schema: {
querystring: {
type: 'object',
properties: {
type: { type: 'string' },
matter_id: { type: 'string' },
limit: { type: 'number' },
offset: { type: 'number' },
search: { type: 'string' },
},
},
tags: ['documents'],
},
},
async (request, reply) => {
const query = request.query as {
type?: string;
matter_id?: string;
limit?: number;
offset?: number;
search?: string;
};
const documents = await listDocuments(
query.type as any,
query.limit || 100,
query.offset || 0
);
return reply.send({ documents, total: documents.length });
}
);
// Update document
server.patch(
'/documents/:id',
{
preHandler: [authenticateJWT, requireRole('admin', 'attorney', 'legal_staff')],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
body: {
type: 'object',
properties: {
title: { type: 'string' },
content: { type: 'string' },
fileUrl: { type: 'string' },
change_summary: { type: 'string' },
},
},
tags: ['documents'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const body = request.body as {
title?: string;
content?: string;
fileUrl?: string;
change_summary?: string;
};
const user = (request as any).user;
// Check if document is checked out
const checkout = await getDocumentCheckout(id);
if (checkout && checkout.checked_out_by !== user?.id) {
return reply.code(409).send({
error: 'Document is checked out by another user',
checked_out_by: checkout.checked_out_by,
});
}
const document = await updateDocument(id, {
title: body.title,
content: body.content,
fileUrl: body.fileUrl,
});
if (!document) {
return reply.code(404).send({ error: 'Document not found' });
}
// Create new version
const latestVersion = await getLatestDocumentVersion(id);
await createDocumentVersion({
document_id: id,
version_number: (latestVersion?.version_number || 0) + 1,
content: body.content,
file_url: body.fileUrl,
change_summary: body.change_summary,
change_type: 'modified',
created_by: user?.id,
});
// Audit log
await createDocumentAuditLog({
document_id: id,
action: 'modified',
performed_by: user?.id,
ip_address: request.ip,
user_agent: request.headers['user-agent'],
details: { change_summary: body.change_summary },
});
return reply.send(document);
}
);
// Checkout document
server.post(
'/documents/:id/checkout',
{
preHandler: [authenticateJWT, requireRole('admin', 'attorney', 'legal_staff')],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
body: {
type: 'object',
properties: {
duration_hours: { type: 'number' },
notes: { type: 'string' },
},
},
tags: ['documents'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const body = request.body as { duration_hours?: number; notes?: string };
const user = (request as any).user;
try {
const checkout = await checkoutDocument({
document_id: id,
checked_out_by: user.id,
duration_hours: body.duration_hours,
notes: body.notes,
});
await createDocumentAuditLog({
document_id: id,
action: 'checked_out',
performed_by: user.id,
});
return reply.send(checkout);
} catch (error: any) {
return reply.code(409).send({ error: error.message });
}
}
);
// Checkin document
server.post(
'/documents/:id/checkin',
{
preHandler: [authenticateJWT, requireRole('admin', 'attorney', 'legal_staff')],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
tags: ['documents'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const user = (request as any).user;
try {
const checkedIn = await checkinDocument(id, user.id);
if (!checkedIn) {
return reply.code(404).send({ error: 'Document is not checked out' });
}
await createDocumentAuditLog({
document_id: id,
action: 'checked_in',
performed_by: user.id,
});
return reply.send({ success: true });
} catch (error: any) {
return reply.code(409).send({ error: error.message });
}
}
);
// Get document checkout status
server.get(
'/documents/:id/checkout',
{
preHandler: [authenticateJWT],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
tags: ['documents'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const checkout = await getDocumentCheckout(id);
return reply.send(checkout || { checked_out: false });
}
);
}

View File

@@ -0,0 +1,74 @@
/**
* Court Filing Routes
* E-filing and court document management
*/
import { FastifyInstance } from 'fastify';
import {
createCourtFiling,
getCourtFiling,
listCourtFilings,
updateFilingStatus,
getFilingDeadlines,
} from '@the-order/database';
import { authenticateJWT, requireRole } from '@the-order/shared';
export async function registerFilingRoutes(server: FastifyInstance): Promise<void> {
server.post('/filings', {
preHandler: [authenticateJWT, requireRole('admin', 'attorney')],
schema: {
body: {
type: 'object',
required: ['document_id', 'court_name'],
properties: {
document_id: { type: 'string' },
matter_id: { type: 'string' },
court_name: { type: 'string' },
case_number: { type: 'string' },
filing_type: { type: 'string' },
},
},
tags: ['filings'],
},
}, async (request, reply) => {
const body = request.body as any;
const user = (request as any).user;
const filing = await createCourtFiling({ ...body, filed_by: user?.id });
return reply.code(201).send(filing);
});
server.get('/filings/:id', {
preHandler: [authenticateJWT],
schema: { params: { type: 'object', properties: { id: { type: 'string' } } }, tags: ['filings'] },
}, async (request, reply) => {
const { id } = request.params as { id: string };
const filing = await getCourtFiling(id);
if (!filing) return reply.code(404).send({ error: 'Filing not found' });
return reply.send(filing);
});
server.get('/filings', {
preHandler: [authenticateJWT],
schema: {
querystring: {
type: 'object',
properties: { matter_id: { type: 'string' }, status: { type: 'string' } },
},
tags: ['filings'],
},
}, async (request, reply) => {
const query = request.query as any;
const filings = await listCourtFilings(query);
return reply.send({ filings });
});
server.get('/matters/:id/filing-deadlines', {
preHandler: [authenticateJWT],
schema: { params: { type: 'object', properties: { id: { type: 'string' } } }, tags: ['filings'] },
}, async (request, reply) => {
const { id } = request.params as { id: string };
const deadlines = await getFilingDeadlines(id);
return reply.send({ deadlines });
});
}

View File

@@ -0,0 +1,356 @@
/**
* Legal Matter Routes
* Matter management and matter-document relationships
*/
import { FastifyInstance } from 'fastify';
import {
createLegalMatter,
getLegalMatter,
getLegalMatterByNumber,
listLegalMatters,
updateLegalMatter,
addMatterParticipant,
getMatterParticipants,
linkDocumentToMatter,
getMatterDocuments,
getDocumentMatters,
} from '@the-order/database';
import { authenticateJWT, requireRole } from '@the-order/shared';
import { createDocumentAuditLog } from '@the-order/database';
export async function registerMatterRoutes(server: FastifyInstance): Promise<void> {
// Create matter
server.post(
'/matters',
{
preHandler: [authenticateJWT, requireRole('admin', 'attorney')],
schema: {
body: {
type: 'object',
required: ['matter_number', 'title'],
properties: {
matter_number: { type: 'string' },
title: { type: 'string' },
description: { type: 'string' },
matter_type: { type: 'string' },
status: { type: 'string' },
priority: { type: 'string' },
client_id: { type: 'string' },
responsible_attorney_id: { type: 'string' },
practice_area: { type: 'string' },
jurisdiction: { type: 'string' },
court_name: { type: 'string' },
case_number: { type: 'string' },
opened_date: { type: 'string' },
billing_code: { type: 'string' },
metadata: { type: 'object' },
},
},
tags: ['matters'],
},
},
async (request, reply) => {
const body = request.body as any;
const user = (request as any).user;
const matter = await createLegalMatter({
...body,
created_by: user?.id,
});
return reply.code(201).send(matter);
}
);
// Get matter by ID
server.get(
'/matters/:id',
{
preHandler: [authenticateJWT],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
tags: ['matters'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const matter = await getLegalMatter(id);
if (!matter) {
return reply.code(404).send({ error: 'Matter not found' });
}
return reply.send(matter);
}
);
// Get matter by number
server.get(
'/matters/number/:number',
{
preHandler: [authenticateJWT],
schema: {
params: {
type: 'object',
properties: {
number: { type: 'string' },
},
},
tags: ['matters'],
},
},
async (request, reply) => {
const { number } = request.params as { number: string };
const matter = await getLegalMatterByNumber(number);
if (!matter) {
return reply.code(404).send({ error: 'Matter not found' });
}
return reply.send(matter);
}
);
// List matters
server.get(
'/matters',
{
preHandler: [authenticateJWT],
schema: {
querystring: {
type: 'object',
properties: {
status: { type: 'string' },
matter_type: { type: 'string' },
client_id: { type: 'string' },
responsible_attorney_id: { type: 'string' },
practice_area: { type: 'string' },
search: { type: 'string' },
limit: { type: 'number' },
offset: { type: 'number' },
},
},
tags: ['matters'],
},
},
async (request, reply) => {
const query = request.query as any;
const matters = await listLegalMatters(
{
status: query.status,
matter_type: query.matter_type,
client_id: query.client_id,
responsible_attorney_id: query.responsible_attorney_id,
practice_area: query.practice_area,
search: query.search,
},
query.limit || 100,
query.offset || 0
);
return reply.send({ matters, total: matters.length });
}
);
// Update matter
server.patch(
'/matters/:id',
{
preHandler: [authenticateJWT, requireRole('admin', 'attorney')],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
body: {
type: 'object',
properties: {
title: { type: 'string' },
description: { type: 'string' },
status: { type: 'string' },
priority: { type: 'string' },
responsible_attorney_id: { type: 'string' },
closed_date: { type: 'string' },
},
},
tags: ['matters'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const body = request.body as any;
const matter = await updateLegalMatter(id, body);
if (!matter) {
return reply.code(404).send({ error: 'Matter not found' });
}
return reply.send(matter);
}
);
// Add participant to matter
server.post(
'/matters/:id/participants',
{
preHandler: [authenticateJWT, requireRole('admin', 'attorney')],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
body: {
type: 'object',
required: ['role'],
properties: {
user_id: { type: 'string' },
role: { type: 'string' },
organization_name: { type: 'string' },
email: { type: 'string' },
phone: { type: 'string' },
is_primary: { type: 'boolean' },
access_level: { type: 'string' },
notes: { type: 'string' },
},
},
tags: ['matters'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const body = request.body as any;
const participant = await addMatterParticipant(id, body);
return reply.code(201).send(participant);
}
);
// Get matter participants
server.get(
'/matters/:id/participants',
{
preHandler: [authenticateJWT],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
tags: ['matters'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const participants = await getMatterParticipants(id);
return reply.send({ participants });
}
);
// Link document to matter
server.post(
'/matters/:matter_id/documents/:document_id',
{
preHandler: [authenticateJWT, requireRole('admin', 'attorney', 'legal_staff')],
schema: {
params: {
type: 'object',
properties: {
matter_id: { type: 'string' },
document_id: { type: 'string' },
},
},
body: {
type: 'object',
required: ['relationship_type'],
properties: {
relationship_type: { type: 'string' },
folder_path: { type: 'string' },
is_primary: { type: 'boolean' },
display_order: { type: 'number' },
notes: { type: 'string' },
},
},
tags: ['matters'],
},
},
async (request, reply) => {
const { matter_id, document_id } = request.params as {
matter_id: string;
document_id: string;
};
const body = request.body as any;
const user = (request as any).user;
const link = await linkDocumentToMatter(matter_id, document_id, body.relationship_type, {
folder_path: body.folder_path,
is_primary: body.is_primary,
display_order: body.display_order,
notes: body.notes,
});
await createDocumentAuditLog({
document_id,
matter_id,
action: 'linked_to_matter',
performed_by: user?.id,
});
return reply.code(201).send(link);
}
);
// Get matter documents
server.get(
'/matters/:id/documents',
{
preHandler: [authenticateJWT],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
querystring: {
type: 'object',
properties: {
relationship_type: { type: 'string' },
},
},
tags: ['matters'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const { relationship_type } = request.query as { relationship_type?: string };
const documents = await getMatterDocuments(id, relationship_type);
return reply.send({ documents });
}
);
// Get document matters
server.get(
'/documents/:id/matters',
{
preHandler: [authenticateJWT],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
tags: ['documents'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const matters = await getDocumentMatters(id);
return reply.send({ matters });
}
);
}

View File

@@ -0,0 +1,107 @@
/**
* Document Retention Routes
* Retention policies and disposal workflows
*/
import { FastifyInstance } from 'fastify';
import {
createRetentionPolicy,
getRetentionPolicy,
listRetentionPolicies,
applyRetentionPolicy,
getDocumentRetentionRecord,
getExpiredRetentionRecords,
disposeDocument,
extendRetention,
placeOnLegalHold,
getRetentionStatistics,
} from '@the-order/database';
import { authenticateJWT, requireRole } from '@the-order/shared';
export async function registerRetentionRoutes(server: FastifyInstance): Promise<void> {
server.post('/retention/policies', {
preHandler: [authenticateJWT, requireRole('admin')],
schema: {
body: {
type: 'object',
required: ['name', 'retention_period_years'],
properties: {
name: { type: 'string' },
description: { type: 'string' },
retention_period_years: { type: 'number' },
retention_trigger: { type: 'string' },
disposal_action: { type: 'string' },
},
},
tags: ['retention'],
},
}, async (request, reply) => {
const body = request.body as any;
const user = (request as any).user;
const policy = await createRetentionPolicy({ ...body, created_by: user?.id });
return reply.code(201).send(policy);
});
server.get('/retention/policies', {
preHandler: [authenticateJWT],
schema: { tags: ['retention'] },
}, async (request, reply) => {
const policies = await listRetentionPolicies();
return reply.send({ policies });
});
server.post('/documents/:id/retention', {
preHandler: [authenticateJWT, requireRole('admin')],
schema: {
params: { type: 'object', properties: { id: { type: 'string' } } },
body: { type: 'object', required: ['policy_id'], properties: { policy_id: { type: 'string' } } },
tags: ['retention'],
},
}, async (request, reply) => {
const { id } = request.params as { id: string };
const { policy_id } = request.body as { policy_id: string };
const record = await applyRetentionPolicy(id, policy_id);
return reply.send(record);
});
server.get('/documents/:id/retention', {
preHandler: [authenticateJWT],
schema: { params: { type: 'object', properties: { id: { type: 'string' } } }, tags: ['retention'] },
}, async (request, reply) => {
const { id } = request.params as { id: string };
const record = await getDocumentRetentionRecord(id);
return reply.send(record || {});
});
server.get('/retention/expired', {
preHandler: [authenticateJWT, requireRole('admin')],
schema: { tags: ['retention'] },
}, async (request, reply) => {
const records = await getExpiredRetentionRecords();
return reply.send({ records });
});
server.post('/documents/:id/dispose', {
preHandler: [authenticateJWT, requireRole('admin')],
schema: {
params: { type: 'object', properties: { id: { type: 'string' } } },
body: { type: 'object', properties: { notes: { type: 'string' } } },
tags: ['retention'],
},
}, async (request, reply) => {
const { id } = request.params as { id: string };
const { notes } = request.body as { notes?: string };
const user = (request as any).user;
const record = await disposeDocument(id, user.id, notes);
return reply.send(record);
});
server.get('/retention/statistics', {
preHandler: [authenticateJWT, requireRole('admin')],
schema: { tags: ['retention'] },
}, async (request, reply) => {
const stats = await getRetentionStatistics();
return reply.send(stats);
});
}

View File

@@ -0,0 +1,44 @@
/**
* Document Search Routes
* Full-text search and advanced filtering
*/
import { FastifyInstance } from 'fastify';
import { searchDocuments, getSearchSuggestions } from '@the-order/database';
import { authenticateJWT } from '@the-order/shared';
export async function registerSearchRoutes(server: FastifyInstance): Promise<void> {
server.post('/search', {
preHandler: [authenticateJWT],
schema: {
body: {
type: 'object',
required: ['query'],
properties: {
query: { type: 'string' },
filters: { type: 'object' },
limit: { type: 'number' },
offset: { type: 'number' },
},
},
tags: ['search'],
},
}, async (request, reply) => {
const body = request.body as any;
const results = await searchDocuments(body);
return reply.send(results);
});
server.get('/search/suggestions', {
preHandler: [authenticateJWT],
schema: {
querystring: { type: 'object', properties: { q: { type: 'string' } } },
tags: ['search'],
},
}, async (request, reply) => {
const { q } = request.query as { q?: string };
const suggestions = await getSearchSuggestions(q || '');
return reply.send({ suggestions });
});
}

View File

@@ -0,0 +1,34 @@
/**
* Document Security Routes
* Watermarking, encryption, access control
*/
import { FastifyInstance } from 'fastify';
import { authenticateJWT, requireRole } from '@the-order/shared';
export async function registerSecurityRoutes(server: FastifyInstance): Promise<void> {
server.post('/documents/:id/watermark', {
preHandler: [authenticateJWT, requireRole('admin', 'attorney')],
schema: {
params: { type: 'object', properties: { id: { type: 'string' } } },
body: { type: 'object', properties: { text: { type: 'string' } } },
tags: ['security'],
},
}, async (request, reply) => {
// TODO: Implement watermarking
return reply.send({ message: 'Watermarking not yet implemented' });
});
server.post('/documents/:id/redact', {
preHandler: [authenticateJWT, requireRole('admin', 'attorney')],
schema: {
params: { type: 'object', properties: { id: { type: 'string' } } },
body: { type: 'object', properties: { redactions: { type: 'array' } } },
tags: ['security'],
},
}, async (request, reply) => {
// TODO: Implement redaction
return reply.send({ message: 'Redaction not yet implemented' });
});
}

View File

@@ -0,0 +1,287 @@
/**
* Document Template Routes
* Template management and rendering
*/
import { FastifyInstance } from 'fastify';
import {
createDocumentTemplate,
getDocumentTemplate,
getDocumentTemplateByName,
listDocumentTemplates,
updateDocumentTemplate,
createTemplateVersion,
renderDocumentTemplate,
extractTemplateVariables,
} from '@the-order/database';
import { authenticateJWT, requireRole } from '@the-order/shared';
export async function registerTemplateRoutes(server: FastifyInstance): Promise<void> {
// Create template
server.post(
'/templates',
{
preHandler: [authenticateJWT, requireRole('admin', 'attorney', 'legal_staff')],
schema: {
body: {
type: 'object',
required: ['name', 'template_content'],
properties: {
name: { type: 'string' },
description: { type: 'string' },
category: { type: 'string' },
subcategory: { type: 'string' },
template_content: { type: 'string' },
variables: { type: 'object' },
metadata: { type: 'object' },
is_public: { type: 'boolean' },
tags: { type: 'array', items: { type: 'string' } },
},
},
tags: ['templates'],
},
},
async (request, reply) => {
const body = request.body as any;
const user = (request as any).user;
const template = await createDocumentTemplate({
name: body.name,
description: body.description,
category: body.category,
subcategory: body.subcategory,
template_content: body.template_content,
variables: body.variables,
metadata: body.metadata,
is_public: body.is_public || false,
tags: body.tags,
created_by: user?.id,
});
return reply.code(201).send(template);
}
);
// Get template by ID
server.get(
'/templates/:id',
{
preHandler: [authenticateJWT],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
tags: ['templates'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const template = await getDocumentTemplate(id);
if (!template) {
return reply.code(404).send({ error: 'Template not found' });
}
return reply.send(template);
}
);
// Get template by name
server.get(
'/templates/name/:name',
{
preHandler: [authenticateJWT],
schema: {
params: {
type: 'object',
properties: {
name: { type: 'string' },
},
},
querystring: {
type: 'object',
properties: {
version: { type: 'number' },
},
},
tags: ['templates'],
},
},
async (request, reply) => {
const { name } = request.params as { name: string };
const { version } = request.query as { version?: number };
const template = await getDocumentTemplateByName(name, version);
if (!template) {
return reply.code(404).send({ error: 'Template not found' });
}
return reply.send(template);
}
);
// List templates
server.get(
'/templates',
{
preHandler: [authenticateJWT],
schema: {
querystring: {
type: 'object',
properties: {
category: { type: 'string' },
is_active: { type: 'boolean' },
is_public: { type: 'boolean' },
search: { type: 'string' },
limit: { type: 'number' },
offset: { type: 'number' },
},
},
tags: ['templates'],
},
},
async (request, reply) => {
const query = request.query as any;
const templates = await listDocumentTemplates(
{
category: query.category,
is_active: query.is_active,
is_public: query.is_public,
search: query.search,
},
query.limit || 100,
query.offset || 0
);
return reply.send({ templates });
}
);
// Update template
server.patch(
'/templates/:id',
{
preHandler: [authenticateJWT, requireRole('admin', 'attorney')],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
body: {
type: 'object',
properties: {
description: { type: 'string' },
template_content: { type: 'string' },
variables: { type: 'object' },
is_active: { type: 'boolean' },
tags: { type: 'array', items: { type: 'string' } },
},
},
tags: ['templates'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const body = request.body as any;
const template = await updateDocumentTemplate(id, body);
if (!template) {
return reply.code(404).send({ error: 'Template not found' });
}
return reply.send(template);
}
);
// Create template version
server.post(
'/templates/:id/version',
{
preHandler: [authenticateJWT, requireRole('admin', 'attorney')],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
body: {
type: 'object',
properties: {
template_content: { type: 'string' },
description: { type: 'string' },
},
},
tags: ['templates'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const body = request.body as any;
const template = await createTemplateVersion(id, body);
return reply.send(template);
}
);
// Render template
server.post(
'/templates/:id/render',
{
preHandler: [authenticateJWT],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
body: {
type: 'object',
required: ['variables'],
properties: {
variables: { type: 'object' },
},
},
tags: ['templates'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const { variables } = request.body as { variables: Record<string, unknown> };
const template = await getDocumentTemplate(id);
if (!template) {
return reply.code(404).send({ error: 'Template not found' });
}
const rendered = renderDocumentTemplate(template, variables);
return reply.send({ rendered });
}
);
// Extract variables from template
server.get(
'/templates/:id/variables',
{
preHandler: [authenticateJWT],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
tags: ['templates'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const template = await getDocumentTemplate(id);
if (!template) {
return reply.code(404).send({ error: 'Template not found' });
}
const variables = extractTemplateVariables(template.template_content);
return reply.send({ variables });
}
);
}

View File

@@ -0,0 +1,200 @@
/**
* Document Version Routes
* Version management and revision history
*/
import { FastifyInstance } from 'fastify';
import {
getDocumentVersions,
getDocumentVersionByNumber,
getLatestDocumentVersion,
compareDocumentVersions,
restoreDocumentVersion,
getDocumentVersionHistory,
} from '@the-order/database';
import { authenticateJWT, requireRole } from '@the-order/shared';
import { createDocumentAuditLog } from '@the-order/database';
export async function registerVersionRoutes(server: FastifyInstance): Promise<void> {
// Get all versions for a document
server.get(
'/documents/:id/versions',
{
preHandler: [authenticateJWT],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
tags: ['versions'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const versions = await getDocumentVersions(id);
return reply.send({ versions });
}
);
// Get specific version
server.get(
'/documents/:id/versions/:version',
{
preHandler: [authenticateJWT],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
version: { type: 'number' },
},
},
tags: ['versions'],
},
},
async (request, reply) => {
const { id, version } = request.params as { id: string; version: string };
const versionNumber = parseInt(version, 10);
const docVersion = await getDocumentVersionByNumber(id, versionNumber);
if (!docVersion) {
return reply.code(404).send({ error: 'Version not found' });
}
return reply.send(docVersion);
}
);
// Get latest version
server.get(
'/documents/:id/versions/latest',
{
preHandler: [authenticateJWT],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
tags: ['versions'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const version = await getLatestDocumentVersion(id);
if (!version) {
return reply.code(404).send({ error: 'No versions found' });
}
return reply.send(version);
}
);
// Get version history with user info
server.get(
'/documents/:id/versions/history',
{
preHandler: [authenticateJWT],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
tags: ['versions'],
},
},
async (request, reply) => {
const { id } = request.params as { id: string };
const history = await getDocumentVersionHistory(id);
return reply.send({ history });
}
);
// Compare two versions
server.get(
'/documents/:id/versions/:v1/compare/:v2',
{
preHandler: [authenticateJWT],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
v1: { type: 'string' },
v2: { type: 'string' },
},
},
tags: ['versions'],
},
},
async (request, reply) => {
const { id, v1, v2 } = request.params as { id: string; v1: string; v2: string };
const version1 = await getDocumentVersionByNumber(id, parseInt(v1, 10));
const version2 = await getDocumentVersionByNumber(id, parseInt(v2, 10));
if (!version1 || !version2) {
return reply.code(404).send({ error: 'One or both versions not found' });
}
const comparison = await compareDocumentVersions(version1.id, version2.id);
return reply.send(comparison);
}
);
// Restore document to a previous version
server.post(
'/documents/:id/versions/:version/restore',
{
preHandler: [authenticateJWT, requireRole('admin', 'attorney')],
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
version: { type: 'number' },
},
},
body: {
type: 'object',
properties: {
change_summary: { type: 'string' },
},
},
tags: ['versions'],
},
},
async (request, reply) => {
const { id, version } = request.params as { id: string; version: string };
const body = request.body as { change_summary?: string };
const user = (request as any).user;
const targetVersion = await getDocumentVersionByNumber(id, parseInt(version, 10));
if (!targetVersion) {
return reply.code(404).send({ error: 'Version not found' });
}
const restoredVersion = await restoreDocumentVersion(
id,
targetVersion.id,
user.id,
body.change_summary
);
await createDocumentAuditLog({
document_id: id,
version_id: restoredVersion.id,
action: 'version_restored',
performed_by: user.id,
details: { restored_from_version: targetVersion.version_number },
});
return reply.send(restoredVersion);
}
);
}

View File

@@ -0,0 +1,76 @@
/**
* Document Workflow Routes
* Approval, signing, and review workflows
*/
import { FastifyInstance } from 'fastify';
import {
createDocumentWorkflow,
getDocumentWorkflow,
listDocumentWorkflows,
updateWorkflowStatus,
assignWorkflowStep,
completeWorkflowStep,
} from '@the-order/database';
import { authenticateJWT, requireRole } from '@the-order/shared';
export async function registerWorkflowRoutes(server: FastifyInstance): Promise<void> {
server.post('/workflows', {
preHandler: [authenticateJWT, requireRole('admin', 'attorney')],
schema: {
body: {
type: 'object',
required: ['document_id', 'workflow_type'],
properties: {
document_id: { type: 'string' },
workflow_type: { type: 'string' },
steps: { type: 'array' },
},
},
tags: ['workflows'],
},
}, async (request, reply) => {
const body = request.body as any;
const user = (request as any).user;
const workflow = await createDocumentWorkflow({
...body,
created_by: user?.id,
});
return reply.code(201).send(workflow);
});
server.get('/workflows/:id', {
preHandler: [authenticateJWT],
schema: { params: { type: 'object', properties: { id: { type: 'string' } } }, tags: ['workflows'] },
}, async (request, reply) => {
const { id } = request.params as { id: string };
const workflow = await getDocumentWorkflow(id);
if (!workflow) return reply.code(404).send({ error: 'Workflow not found' });
return reply.send(workflow);
});
server.get('/documents/:id/workflows', {
preHandler: [authenticateJWT],
schema: { params: { type: 'object', properties: { id: { type: 'string' } } }, tags: ['workflows'] },
}, async (request, reply) => {
const { id } = request.params as { id: string };
const workflows = await listDocumentWorkflows(id);
return reply.send({ workflows });
});
server.patch('/workflows/:id/status', {
preHandler: [authenticateJWT, requireRole('admin', 'attorney')],
schema: {
params: { type: 'object', properties: { id: { type: 'string' } } },
body: { type: 'object', required: ['status'], properties: { status: { type: 'string' } } },
tags: ['workflows'],
},
}, async (request, reply) => {
const { id } = request.params as { id: string };
const { status } = request.body as { status: string };
const workflow = await updateWorkflowStatus(id, status);
if (!workflow) return reply.code(404).send({ error: 'Workflow not found' });
return reply.send(workflow);
});
}

View File

@@ -0,0 +1,134 @@
/**
* Court E-Filing Service
* Handles electronic filing with court systems
*/
import { getCourtFiling, updateFilingStatus } from '@the-order/database';
import { getDocumentById } from '@the-order/database';
export interface EFileOptions {
filing_id: string;
court_system: 'federal' | 'state' | 'municipal' | 'administrative';
credentials?: {
username: string;
password: string;
api_key?: string;
};
}
export interface EFileResult {
success: boolean;
filing_reference?: string;
confirmation_number?: string;
error?: string;
}
/**
* Submit filing to court e-filing system
* Note: This is a placeholder - actual implementation would integrate with
* specific court e-filing systems (e.g., CM/ECF, File & Serve, etc.)
*/
export async function submitEFiling(options: EFileOptions): Promise<EFileResult> {
const filing = await getCourtFiling(options.filing_id);
if (!filing) {
throw new Error('Filing not found');
}
const document = await getDocumentById(filing.document_id);
if (!document) {
throw new Error('Document not found');
}
// TODO: Integrate with actual court e-filing system
// Example integration points:
// - Federal: CM/ECF (Case Management/Electronic Case Files)
// - State: Various state-specific systems
// - Municipal: Local court systems
// Placeholder implementation
try {
// Simulate API call to court system
// const response = await courtEFilingAPI.submit({
// court: filing.court_name,
// case_number: filing.case_number,
// document: document.file_url,
// filing_type: filing.filing_type,
// credentials: options.credentials,
// });
// For now, simulate success
const filing_reference = `EF-${Date.now()}`;
const confirmation_number = `CONF-${Math.random().toString(36).substr(2, 9).toUpperCase()}`;
await updateFilingStatus(options.filing_id, 'submitted', {
filing_reference,
filing_confirmation: confirmation_number,
submitted_at: new Date(),
});
return {
success: true,
filing_reference,
confirmation_number,
};
} catch (error: any) {
await updateFilingStatus(options.filing_id, 'rejected', {
rejection_reason: error.message,
});
return {
success: false,
error: error.message,
};
}
}
/**
* Check filing status with court system
*/
export async function checkFilingStatus(filing_id: string): Promise<{
status: string;
court_status?: string;
last_checked: Date;
}> {
const filing = await getCourtFiling(filing_id);
if (!filing) {
throw new Error('Filing not found');
}
// TODO: Query court system for current status
// const courtStatus = await courtEFilingAPI.getStatus(filing.filing_reference);
return {
status: filing.status,
court_status: 'pending', // Would come from court system
last_checked: new Date(),
};
}
/**
* Get court system configuration
*/
export interface CourtSystemConfig {
court_name: string;
system_type: 'federal' | 'state' | 'municipal' | 'administrative';
api_endpoint?: string;
requires_credentials: boolean;
supported_filing_types: string[];
}
export async function getCourtSystemConfig(
court_name: string
): Promise<CourtSystemConfig | null> {
// TODO: Query database or configuration for court system details
// This would typically be stored in a court_systems table
// Placeholder
return {
court_name,
system_type: 'state',
requires_credentials: true,
supported_filing_types: ['pleading', 'motion', 'brief', 'exhibit'],
};
}

View File

@@ -0,0 +1,147 @@
/**
* Document Analytics Service
* Provides analytics and insights for documents
*/
import {
getDocumentById,
getDocumentAuditLogs,
getDocumentVersions,
getDocumentComments,
} from '@the-order/database';
import { getLegalMatter, getMatterDocuments } from '@the-order/database';
export interface DocumentAnalytics {
document_id: string;
views: number;
downloads: number;
edits: number;
comments: number;
versions: number;
last_accessed?: Date;
most_active_users: Array<{ user_id: string; action_count: number }>;
access_timeline: Array<{ date: string; count: number }>;
}
/**
* Get document analytics
*/
export async function getDocumentAnalytics(document_id: string): Promise<DocumentAnalytics> {
const document = await getDocumentById(document_id);
if (!document) {
throw new Error('Document not found');
}
const auditLogs = await getDocumentAuditLogs(document_id);
const versions = await getDocumentVersions(document_id);
const comments = await getDocumentComments(document_id);
const views = auditLogs.filter((log) => log.action === 'viewed').length;
const downloads = auditLogs.filter((log) => log.action === 'downloaded').length;
const edits = auditLogs.filter((log) => log.action === 'modified').length;
// Get most active users
const userActivity: Record<string, number> = {};
auditLogs.forEach((log) => {
if (log.performed_by) {
userActivity[log.performed_by] = (userActivity[log.performed_by] || 0) + 1;
}
});
const most_active_users = Object.entries(userActivity)
.map(([user_id, action_count]) => ({ user_id, action_count }))
.sort((a, b) => b.action_count - a.action_count)
.slice(0, 10);
// Get access timeline (last 30 days)
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const recentLogs = auditLogs.filter(
(log) => new Date(log.performed_at) >= thirtyDaysAgo
);
const timelineMap: Record<string, number> = {};
recentLogs.forEach((log) => {
const date = new Date(log.performed_at).toISOString().split('T')[0];
timelineMap[date] = (timelineMap[date] || 0) + 1;
});
const access_timeline = Object.entries(timelineMap)
.map(([date, count]) => ({ date, count }))
.sort((a, b) => a.date.localeCompare(b.date));
const last_accessed = auditLogs[0]?.performed_at;
return {
document_id,
views,
downloads,
edits,
comments: comments.length,
versions: versions.length,
last_accessed,
most_active_users,
access_timeline,
};
}
/**
* Get matter analytics
*/
export interface MatterAnalytics {
matter_id: string;
total_documents: number;
total_actions: number;
document_types: Record<string, number>;
activity_timeline: Array<{ date: string; count: number }>;
}
export async function getMatterAnalytics(matter_id: string): Promise<MatterAnalytics> {
const matter = await getLegalMatter(matter_id);
if (!matter) {
throw new Error('Matter not found');
}
const documents = await getMatterDocuments(matter_id);
// Get all audit logs for matter documents
const allAuditLogs = [];
for (const doc of documents) {
const logs = await getDocumentAuditLogs(doc.id);
allAuditLogs.push(...logs);
}
// Count document types
const document_types: Record<string, number> = {};
documents.forEach((doc) => {
document_types[doc.type] = (document_types[doc.type] || 0) + 1;
});
// Get activity timeline
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const recentLogs = allAuditLogs.filter(
(log) => new Date(log.performed_at) >= thirtyDaysAgo
);
const timelineMap: Record<string, number> = {};
recentLogs.forEach((log) => {
const date = new Date(log.performed_at).toISOString().split('T')[0];
timelineMap[date] = (timelineMap[date] || 0) + 1;
});
const activity_timeline = Object.entries(timelineMap)
.map(([date, count]) => ({ date, count }))
.sort((a, b) => a.date.localeCompare(b.date));
return {
matter_id,
total_documents: documents.length,
total_actions: allAuditLogs.length,
document_types,
activity_timeline,
};
}

View File

@@ -0,0 +1,156 @@
/**
* Document Assembly Service
* Handles template-based document generation and clause assembly
*/
import {
getDocumentTemplate,
renderDocumentTemplate,
extractTemplateVariables,
getClause,
listClauses,
renderClause,
createDocument,
createDocumentVersion,
} from '@the-order/database';
export interface AssemblyOptions {
template_id?: string;
clause_ids?: string[];
variables: Record<string, unknown>;
title: string;
type?: 'legal' | 'treaty' | 'finance' | 'history';
matter_id?: string;
save_document?: boolean;
}
export interface AssemblyResult {
rendered: string;
document_id?: string;
template_used?: string;
clauses_used?: string[];
}
/**
* Assemble document from template
*/
export async function assembleFromTemplate(
options: AssemblyOptions
): Promise<AssemblyResult> {
if (!options.template_id) {
throw new Error('Template ID is required');
}
const template = await getDocumentTemplate(options.template_id);
if (!template) {
throw new Error(`Template ${options.template_id} not found`);
}
// Render template with variables
const rendered = renderDocumentTemplate(template, options.variables);
if (options.save_document) {
const document = await createDocument({
title: options.title,
type: options.type || 'legal',
content: rendered,
});
await createDocumentVersion({
document_id: document.id,
version_number: 1,
version_label: 'Generated from Template',
content: rendered,
change_type: 'created',
change_summary: `Generated from template: ${template.name}`,
});
return {
rendered,
document_id: document.id,
template_used: template.name,
};
}
return {
rendered,
template_used: template.name,
};
}
/**
* Assemble document from clauses
*/
export async function assembleFromClauses(
options: AssemblyOptions
): Promise<AssemblyResult> {
if (!options.clause_ids || options.clause_ids.length === 0) {
throw new Error('At least one clause ID is required');
}
// Fetch all clauses
const clauses = await Promise.all(
options.clause_ids.map((id) => getClause(id))
);
const missing = clauses.findIndex((c) => !c);
if (missing !== -1) {
throw new Error(`Clause not found: ${options.clause_ids[missing]}`);
}
// Render and combine clauses
const rendered_parts = clauses.map((clause) =>
renderClause(clause!, options.variables)
);
const rendered = rendered_parts.join('\n\n');
if (options.save_document) {
const document = await createDocument({
title: options.title,
type: options.type || 'legal',
content: rendered,
});
await createDocumentVersion({
document_id: document.id,
version_number: 1,
version_label: 'Assembled from Clauses',
content: rendered,
change_type: 'created',
change_summary: `Assembled from ${clauses.length} clauses`,
});
return {
rendered,
document_id: document.id,
clauses_used: clauses.map((c) => c!.name),
};
}
return {
rendered,
clauses_used: clauses.map((c) => c!.name),
};
}
/**
* Preview template rendering
*/
export async function previewTemplate(
template_id: string,
variables: Record<string, unknown>
): Promise<{ rendered: string; variables_used: string[] }> {
const template = await getDocumentTemplate(template_id);
if (!template) {
throw new Error(`Template ${template_id} not found`);
}
const rendered = renderDocumentTemplate(template, variables);
const variables_used = extractTemplateVariables(template.template_content);
return {
rendered,
variables_used,
};
}

View File

@@ -0,0 +1,172 @@
/**
* Document Export Service
* Handles document export and reporting
*/
import { getDocumentById, getDocumentVersions, getDocumentAuditLogs } from '@the-order/database';
import { getLegalMatter, getMatterDocuments } from '@the-order/database';
export interface ExportOptions {
format: 'pdf' | 'docx' | 'txt' | 'json';
include_versions?: boolean;
include_audit_log?: boolean;
include_metadata?: boolean;
}
/**
* Export document
*/
export async function exportDocument(
document_id: string,
options: ExportOptions
): Promise<{ content: Buffer | string; mime_type: string; filename: string }> {
const document = await getDocumentById(document_id);
if (!document) {
throw new Error('Document not found');
}
let content: Buffer | string = '';
let mime_type = '';
let filename = '';
if (options.format === 'json') {
const exportData: any = {
document: {
id: document.id,
title: document.title,
type: document.type,
content: document.content,
created_at: document.created_at,
updated_at: document.updated_at,
},
};
if (options.include_versions) {
const versions = await getDocumentVersions(document_id);
exportData.versions = versions;
}
if (options.include_audit_log) {
const auditLogs = await getDocumentAuditLogs(document_id);
exportData.audit_log = auditLogs;
}
if (options.include_metadata) {
exportData.metadata = {
classification: document.classification,
extracted_data: document.extracted_data,
};
}
content = JSON.stringify(exportData, null, 2);
mime_type = 'application/json';
filename = `${document.title.replace(/[^a-z0-9]/gi, '_')}.json`;
} else if (options.format === 'txt') {
content = document.content || '';
mime_type = 'text/plain';
filename = `${document.title.replace(/[^a-z0-9]/gi, '_')}.txt`;
} else if (options.format === 'pdf') {
// TODO: Implement PDF generation using a library like pdfkit or puppeteer
content = Buffer.from('PDF generation not yet implemented');
mime_type = 'application/pdf';
filename = `${document.title.replace(/[^a-z0-9]/gi, '_')}.pdf`;
} else if (options.format === 'docx') {
// TODO: Implement DOCX generation using a library like docx
content = Buffer.from('DOCX generation not yet implemented');
mime_type = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
filename = `${document.title.replace(/[^a-z0-9]/gi, '_')}.docx`;
}
return { content, mime_type, filename };
}
/**
* Export matter with all documents
*/
export async function exportMatter(
matter_id: string,
options: ExportOptions
): Promise<{ content: Buffer | string; mime_type: string; filename: string }> {
const matter = await getLegalMatter(matter_id);
if (!matter) {
throw new Error('Matter not found');
}
const documents = await getMatterDocuments(matter_id);
const exportData: any = {
matter: {
id: matter.id,
matter_number: matter.matter_number,
title: matter.title,
description: matter.description,
status: matter.status,
},
documents: await Promise.all(
documents.map(async (doc) => {
const docData: any = {
id: doc.id,
title: doc.title,
type: doc.type,
};
if (options.include_versions) {
const versions = await getDocumentVersions(doc.id);
docData.versions = versions;
}
return docData;
})
),
};
const content = JSON.stringify(exportData, null, 2);
const mime_type = 'application/json';
const filename = `${matter.matter_number.replace(/[^a-z0-9]/gi, '_')}_export.json`;
return { content, mime_type, filename };
}
/**
* Generate compliance report
*/
export interface ComplianceReport {
document_id: string;
total_actions: number;
access_log: any[];
retention_status?: any;
audit_summary: any;
}
export async function generateComplianceReport(
document_id: string
): Promise<ComplianceReport> {
const document = await getDocumentById(document_id);
if (!document) {
throw new Error('Document not found');
}
const auditLogs = await getDocumentAuditLogs(document_id);
// Get access log (viewed, downloaded, etc.)
const accessLog = auditLogs.filter(
(log) => ['viewed', 'downloaded', 'exported', 'printed'].includes(log.action)
);
// Get retention status if applicable
const { getDocumentRetentionRecord } = await import('@the-order/database');
const retentionRecord = await getDocumentRetentionRecord(document_id);
return {
document_id,
total_actions: auditLogs.length,
access_log: accessLog,
retention_status: retentionRecord,
audit_summary: {
created: auditLogs.find((log) => log.action === 'created'),
last_accessed: accessLog[0],
total_accesses: accessLog.length,
},
};
}

View File

@@ -0,0 +1,126 @@
/**
* Document Optimization Service
* Handles performance optimization and caching
*/
import { getDocumentById, listDocuments } from '@the-order/database';
import { getDocumentVersions } from '@the-order/database';
// Simple in-memory cache (in production, use Redis)
const documentCache = new Map<string, { data: any; timestamp: number }>();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
/**
* Get document with caching
*/
export async function getDocumentCached(document_id: string): Promise<any> {
const cached = documentCache.get(document_id);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.data;
}
const document = await getDocumentById(document_id);
if (document) {
documentCache.set(document_id, { data: document, timestamp: Date.now() });
}
return document;
}
/**
* Invalidate document cache
*/
export function invalidateDocumentCache(document_id: string): void {
documentCache.delete(document_id);
}
/**
* Batch load documents
*/
export async function batchLoadDocuments(
document_ids: string[]
): Promise<Map<string, any>> {
const results = new Map<string, any>();
// Check cache first
const uncached: string[] = [];
for (const id of document_ids) {
const cached = documentCache.get(id);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
results.set(id, cached.data);
} else {
uncached.push(id);
}
}
// Load uncached documents
if (uncached.length > 0) {
// In a real implementation, you'd use a batch query
for (const id of uncached) {
const doc = await getDocumentById(id);
if (doc) {
results.set(id, doc);
documentCache.set(id, { data: doc, timestamp: Date.now() });
}
}
}
return results;
}
/**
* Optimize document query with pagination
*/
export interface PaginatedDocuments {
documents: any[];
total: number;
page: number;
page_size: number;
has_more: boolean;
}
export async function getPaginatedDocuments(
page = 1,
page_size = 50,
filters?: { type?: string; matter_id?: string }
): Promise<PaginatedDocuments> {
const offset = (page - 1) * page_size;
// In a real implementation, you'd use a more efficient query
const allDocuments = await listDocuments(filters?.type, 10000, 0);
const filtered = filters?.matter_id
? allDocuments // Would filter by matter_id in real query
: allDocuments;
const total = filtered.length;
const documents = filtered.slice(offset, offset + page_size);
return {
documents,
total,
page,
page_size,
has_more: offset + page_size < total,
};
}
/**
* Preload document versions
*/
export async function preloadDocumentVersions(document_id: string): Promise<void> {
// Preload versions into cache
const versions = await getDocumentVersions(document_id);
// Cache versions (simplified - in production, use proper caching)
documentCache.set(`versions:${document_id}`, {
data: versions,
timestamp: Date.now(),
});
}
/**
* Clear all caches (for testing/maintenance)
*/
export function clearAllCaches(): void {
documentCache.clear();
}

View File

@@ -0,0 +1,129 @@
/**
* Document Security Service
* Handles watermarking, encryption, and access control
*/
import { getDocumentById, updateDocument } from '@the-order/database';
import { createDocumentAuditLog } from '@the-order/database';
/**
* Add watermark to document
* Note: Actual watermarking would require PDF processing library
*/
export async function addWatermark(
document_id: string,
watermark_text: string,
user_id: string
): Promise<void> {
const document = await getDocumentById(document_id);
if (!document) {
throw new Error('Document not found');
}
// TODO: Implement actual PDF watermarking
// This would typically use a library like pdf-lib or pdfkit
// For now, we'll just log the action
await createDocumentAuditLog({
document_id,
action: 'watermarked',
performed_by: user_id,
details: { watermark_text },
});
// In a real implementation, you would:
// 1. Download the document file
// 2. Apply watermark using PDF library
// 3. Upload watermarked version
// 4. Update document record with new file URL
}
/**
* Redact sensitive information from document
*/
export interface Redaction {
page: number;
x: number;
y: number;
width: number;
height: number;
reason?: string;
}
export async function redactDocument(
document_id: string,
redactions: Redaction[],
user_id: string
): Promise<void> {
const document = await getDocumentById(document_id);
if (!document) {
throw new Error('Document not found');
}
// TODO: Implement actual PDF redaction
// This would typically use a library like pdf-lib
await createDocumentAuditLog({
document_id,
action: 'redacted',
performed_by: user_id,
details: { redaction_count: redactions.length, redactions },
});
// In a real implementation, you would:
// 1. Download the document file
// 2. Apply redactions using PDF library
// 3. Upload redacted version
// 4. Create new document version
}
/**
* Encrypt document
*/
export async function encryptDocument(
document_id: string,
encryption_key: string,
user_id: string
): Promise<void> {
const document = await getDocumentById(document_id);
if (!document) {
throw new Error('Document not found');
}
// TODO: Implement actual encryption
// This would typically encrypt the file content
await createDocumentAuditLog({
document_id,
action: 'encrypted',
performed_by: user_id,
});
await updateDocument(document_id, {
// Store encryption metadata
extracted_data: { encrypted: true, encrypted_at: new Date().toISOString() },
});
}
/**
* Decrypt document
*/
export async function decryptDocument(
document_id: string,
decryption_key: string,
user_id: string
): Promise<void> {
const document = await getDocumentById(document_id);
if (!document) {
throw new Error('Document not found');
}
// TODO: Implement actual decryption
await createDocumentAuditLog({
document_id,
action: 'decrypted',
performed_by: user_id,
});
}

View File

@@ -0,0 +1,108 @@
/**
* E-Signature Service
* Handles electronic signature integration
*/
import { getDocumentById, createDocumentVersion } from '@the-order/database';
import { createDocumentAuditLog } from '@the-order/database';
export interface SignatureRequest {
document_id: string;
signers: Array<{
email: string;
name: string;
role?: string;
order?: number;
}>;
message?: string;
expires_in_days?: number;
}
export interface SignatureStatus {
request_id: string;
status: 'pending' | 'signed' | 'declined' | 'expired';
signers: Array<{
email: string;
status: 'pending' | 'signed' | 'declined';
signed_at?: Date;
}>;
completed_at?: Date;
}
/**
* Create signature request
* Note: This is a placeholder - actual implementation would integrate with
* DocuSign, Adobe Sign, or another e-signature provider
*/
export async function createSignatureRequest(
request: SignatureRequest,
user_id: string
): Promise<{ request_id: string; signature_url: string }> {
const document = await getDocumentById(request.document_id);
if (!document) {
throw new Error('Document not found');
}
// TODO: Integrate with e-signature provider
// Example with DocuSign:
// const envelope = await docusignClient.createEnvelope({
// document: document.file_url,
// signers: request.signers,
// message: request.message,
// });
const request_id = `sig_${Date.now()}`;
await createDocumentAuditLog({
document_id: request.document_id,
action: 'signature_requested',
performed_by: user_id,
details: {
request_id,
signers: request.signers,
},
});
// In a real implementation, you would:
// 1. Create envelope with e-signature provider
// 2. Store request_id and envelope_id in database
// 3. Return signature URL for first signer
return {
request_id,
signature_url: `https://sign.example.com/sign/${request_id}`, // Placeholder
};
}
/**
* Get signature status
*/
export async function getSignatureStatus(
request_id: string
): Promise<SignatureStatus> {
// TODO: Query e-signature provider for status
// const envelope = await docusignClient.getEnvelope(request_id);
// Placeholder response
return {
request_id,
status: 'pending',
signers: [],
};
}
/**
* Handle signature webhook
* This would be called by the e-signature provider when a document is signed
*/
export async function handleSignatureWebhook(
webhook_data: any
): Promise<void> {
// TODO: Process webhook from e-signature provider
// 1. Verify webhook signature
// 2. Extract document_id and signer info
// 3. Update signature status
// 4. Create new document version with signed copy
// 5. Notify relevant users
}

View File

@@ -0,0 +1,131 @@
/**
* Real-Time Collaboration Service
* Handles WebSocket-based real-time collaboration
*/
import { Server as SocketIOServer } from 'socket.io';
import { createDocumentComment, getDocumentComments } from '@the-order/database';
import { createDocumentAuditLog } from '@the-order/database';
export interface CollaborationRoom {
document_id: string;
users: Set<string>;
}
const rooms = new Map<string, CollaborationRoom>();
/**
* Initialize collaboration server
*/
export function initializeCollaborationServer(io: SocketIOServer): void {
io.on('connection', (socket) => {
console.log('User connected:', socket.id);
// Join document room
socket.on('join-document', async (data: { document_id: string; user_id: string }) => {
const { document_id, user_id } = data;
socket.join(`document:${document_id}`);
if (!rooms.has(document_id)) {
rooms.set(document_id, { document_id, users: new Set() });
}
const room = rooms.get(document_id)!;
room.users.add(user_id);
// Notify others
socket.to(`document:${document_id}`).emit('user-joined', { user_id });
// Send current users
socket.emit('users-in-room', Array.from(room.users));
// Load existing comments
const comments = await getDocumentComments(document_id);
socket.emit('comments-loaded', { comments });
});
// Leave document room
socket.on('leave-document', (data: { document_id: string; user_id: string }) => {
const { document_id, user_id } = data;
socket.leave(`document:${document_id}`);
const room = rooms.get(document_id);
if (room) {
room.users.delete(user_id);
socket.to(`document:${document_id}`).emit('user-left', { user_id });
}
});
// Add comment
socket.on('add-comment', async (data: {
document_id: string;
comment_text: string;
user_id: string;
version_id?: string;
}) => {
const { document_id, comment_text, user_id, version_id } = data;
try {
const comment = await createDocumentComment({
document_id,
version_id,
comment_text,
author_id: user_id,
});
// Broadcast to all users in room
io.to(`document:${document_id}`).emit('comment-added', { comment });
await createDocumentAuditLog({
document_id,
version_id,
action: 'commented',
performed_by: user_id,
});
} catch (error) {
socket.emit('error', { message: 'Failed to add comment' });
}
});
// Cursor position (for real-time editing)
socket.on('cursor-move', (data: {
document_id: string;
user_id: string;
position: { line: number; column: number };
}) => {
const { document_id, user_id, position } = data;
socket.to(`document:${document_id}`).emit('cursor-update', { user_id, position });
});
// Text change (for real-time editing)
socket.on('text-change', (data: {
document_id: string;
user_id: string;
change: any; // Operational transform change
}) => {
const { document_id, user_id, change } = data;
socket.to(`document:${document_id}`).emit('text-updated', { user_id, change });
});
// Disconnect
socket.on('disconnect', () => {
console.log('User disconnected:', socket.id);
// Clean up rooms
for (const [docId, room] of rooms.entries()) {
// Note: In a real implementation, you'd track socket.id to user_id mapping
// For now, this is simplified
}
});
});
}
/**
* Get users in document room
*/
export function getUsersInDocument(document_id: string): string[] {
const room = rooms.get(document_id);
return room ? Array.from(room.users) : [];
}

View File

@@ -0,0 +1,119 @@
/**
* Workflow Engine Service
* Handles document workflow execution and step management
*/
import {
createDocumentWorkflow,
getDocumentWorkflow,
getWorkflowSteps,
updateWorkflowStatus,
updateWorkflowStepStatus,
getPendingWorkflowsForUser,
getWorkflowProgress,
} from '@the-order/database';
import { createDocumentAuditLog } from '@the-order/database';
export interface WorkflowStepResult {
step_id: string;
status: 'approved' | 'rejected';
comments?: string;
}
/**
* Execute workflow step
*/
export async function executeWorkflowStep(
step_id: string,
result: WorkflowStepResult,
user_id: string
): Promise<void> {
const step = await updateWorkflowStepStatus(step_id, result.status, result.comments);
if (!step) {
throw new Error('Workflow step not found');
}
// Get workflow to check if all steps are complete
const workflow = await getDocumentWorkflow(step.workflow_id);
if (!workflow) {
throw new Error('Workflow not found');
}
const allSteps = await getWorkflowSteps(workflow.id);
const allComplete = allSteps.every(
(s) => s.status === 'approved' || s.status === 'rejected' || s.status === 'skipped'
);
if (allComplete) {
// Determine overall workflow status
const hasRejected = allSteps.some((s) => s.status === 'rejected');
const finalStatus = hasRejected ? 'rejected' : 'completed';
await updateWorkflowStatus(workflow.id, finalStatus);
await createDocumentAuditLog({
document_id: workflow.document_id,
action: finalStatus === 'completed' ? 'approved' : 'rejected',
performed_by: user_id,
details: { workflow_id: workflow.id, workflow_type: workflow.workflow_type },
});
} else {
// Move to next pending step
const nextStep = allSteps.find((s) => s.status === 'pending');
if (nextStep) {
await updateWorkflowStepStatus(nextStep.id, 'in_progress');
}
}
}
/**
* Get user's pending workflows
*/
export async function getUserPendingWorkflows(
user_id: string,
role?: string
): Promise<Array<{
step: any;
workflow: any;
document: any;
}>> {
const steps = await getPendingWorkflowsForUser(user_id, role);
// Enrich with workflow and document info
const enriched = await Promise.all(
steps.map(async (step) => {
const workflow = await getDocumentWorkflow(step.workflow_id);
if (!workflow) return null;
const { getDocumentById } = await import('@the-order/database');
const document = await getDocumentById(workflow.document_id);
return {
step,
workflow,
document,
};
})
);
return enriched.filter((e) => e !== null) as any[];
}
/**
* Get workflow progress
*/
export async function getWorkflowProgressForDocument(
document_id: string
): Promise<any> {
const { getDocumentWorkflows } = await import('@the-order/database');
const workflows = await getDocumentWorkflows(document_id);
if (workflows.length === 0) {
return null;
}
// Get progress for the most recent workflow
const latestWorkflow = workflows[0];
return getWorkflowProgress(latestWorkflow.id);
}