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

153
services/README.md Normal file
View File

@@ -0,0 +1,153 @@
# Services Directory
**Last Updated**: 2025-01-27
**Purpose**: Backend microservices overview
## Overview
This directory contains all backend microservices for The Order platform. Each service is a self-contained, independently deployable unit following microservices architecture principles.
## Available Services
### Identity Service (`identity/`)
- **Purpose**: Digital identity, verifiable credentials, Entra VerifiedID
- **Port**: 4002
- **Features**: eIDAS/DID, credential issuance, identity verification
- **Documentation**: [Identity Service README](identity/README.md)
### Intake Service (`intake/`)
- **Purpose**: Document ingestion, OCR, classification
- **Port**: 4001
- **Features**: Document upload, OCR processing, classification, routing
- **Documentation**: [Intake Service README](intake/README.md)
### Finance Service (`finance/`)
- **Purpose**: Payments, ledgers, invoicing
- **Port**: 4003
- **Features**: Payment processing, ledger management, invoicing
- **Documentation**: [Finance Service README](finance/README.md)
### Dataroom Service (`dataroom/`)
- **Purpose**: Virtual data rooms, deal management
- **Port**: 4004
- **Features**: Secure VDR, deal rooms, document access control
- **Documentation**: [Dataroom Service README](dataroom/README.md)
### Legal Documents Service (`legal-documents/`)
- **Purpose**: Comprehensive document management
- **Port**: 4005
- **Features**: Templates, versioning, matter management, court filing, collaboration
- **Documentation**: [Legal Documents Service README](legal-documents/README.md)
### e-Residency Service (`eresidency/`)
- **Purpose**: Digital residency services
- **Port**: 4006
- **Features**: e-Residency management
- **Documentation**: [e-Residency Service README](eresidency/README.md)
## Service Architecture
All services follow a consistent architecture:
```
service/
├── src/
│ ├── index.ts # Entry point
│ ├── routes/ # API routes
│ ├── services/ # Business logic
│ └── types/ # TypeScript types
├── tests/ # Test files
├── k8s/ # Kubernetes manifests
├── Dockerfile # Container definition
├── package.json # Dependencies
└── README.md # Service documentation
```
## Common Patterns
### Health Checks
All services expose `/health` endpoint:
```bash
curl http://localhost:4002/health
```
### API Documentation
All services provide Swagger/OpenAPI docs:
```bash
# Access at /docs endpoint
http://localhost:4002/docs
```
### Metrics
All services expose Prometheus metrics:
```bash
# Access at /metrics endpoint
http://localhost:4002/metrics
```
## Development
### Running Services Locally
```bash
# Start all services
pnpm dev
# Start specific service
pnpm --filter @the-order/identity-service dev
```
### Building Services
```bash
# Build all services
pnpm build
# Build specific service
pnpm --filter @the-order/identity-service build
```
### Testing Services
```bash
# Test all services
pnpm test
# Test specific service
pnpm --filter @the-order/identity-service test
```
## Deployment
### Kubernetes
Each service has Kubernetes manifests in `services/{service}/k8s/`:
```bash
kubectl apply -f services/identity/k8s/deployment.yaml
```
### Docker
Each service has a Dockerfile:
```bash
docker build -t theorder/identity-service:latest -f services/identity/Dockerfile .
```
## Service Communication
Services communicate via:
- **HTTP/REST**: Synchronous communication
- **Message Queue**: Asynchronous communication (planned)
- **Shared Database**: Common data access
- **Event Bus**: Event-driven architecture (planned)
## Related Documentation
- [Architecture Documentation](../docs/architecture/)
- [Deployment Guides](../docs/deployment/)
- [Infrastructure Documentation](../infra/)
---
**Last Updated**: 2025-01-27

View File

@@ -0,0 +1,49 @@
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package.json pnpm-lock.yaml ./
COPY packages/*/package.json ./packages/*/
COPY services/dataroom/package.json ./services/dataroom/
# Install pnpm
RUN npm install -g pnpm
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy source code
COPY . .
# Build
RUN pnpm build --filter=@the-order/dataroom-service
FROM node:20-alpine AS runner
WORKDIR /app
# Copy package files
COPY package.json pnpm-lock.yaml ./
COPY packages/*/package.json ./packages/*/
COPY services/dataroom/package.json ./services/dataroom/
# Install pnpm
RUN npm install -g pnpm
# Install production dependencies
RUN pnpm install --frozen-lockfile --prod
# Copy built files
COPY --from=builder /app/services/dataroom/dist ./services/dataroom/dist
COPY --from=builder /app/packages ./packages
WORKDIR /app/services/dataroom
EXPOSE 4004
# Graceful shutdown signal handling
STOPSIGNAL SIGTERM
CMD ["node", "dist/index.js"]

View File

@@ -0,0 +1,49 @@
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package.json pnpm-lock.yaml ./
COPY packages/*/package.json ./packages/*/
COPY services/finance/package.json ./services/finance/
# Install pnpm
RUN npm install -g pnpm
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy source code
COPY . .
# Build
RUN pnpm build --filter=@the-order/finance-service
FROM node:20-alpine AS runner
WORKDIR /app
# Copy package files
COPY package.json pnpm-lock.yaml ./
COPY packages/*/package.json ./packages/*/
COPY services/finance/package.json ./services/finance/
# Install pnpm
RUN npm install -g pnpm
# Install production dependencies
RUN pnpm install --frozen-lockfile --prod
# Copy built files
COPY --from=builder /app/services/finance/dist ./services/finance/dist
COPY --from=builder /app/packages ./packages
WORKDIR /app/services/finance
EXPOSE 4003
# Graceful shutdown signal handling
STOPSIGNAL SIGTERM
CMD ["node", "dist/index.js"]

View File

@@ -0,0 +1,49 @@
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package.json pnpm-lock.yaml ./
COPY packages/*/package.json ./packages/*/
COPY services/identity/package.json ./services/identity/
# Install pnpm
RUN npm install -g pnpm
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy source code
COPY . .
# Build
RUN pnpm build --filter=@the-order/identity-service
FROM node:20-alpine AS runner
WORKDIR /app
# Copy package files
COPY package.json pnpm-lock.yaml ./
COPY packages/*/package.json ./packages/*/
COPY services/identity/package.json ./services/identity/
# Install pnpm
RUN npm install -g pnpm
# Install production dependencies
RUN pnpm install --frozen-lockfile --prod
# Copy built files
COPY --from=builder /app/services/identity/dist ./services/identity/dist
COPY --from=builder /app/packages ./packages
WORKDIR /app/services/identity
EXPOSE 4002
# Graceful shutdown signal handling
STOPSIGNAL SIGTERM
CMD ["node", "dist/index.js"]

View File

@@ -18,6 +18,8 @@ import {
authenticateDID,
requireRole,
registerCredentialRateLimit,
setupGracefulShutdown,
createConnectionDrainer,
} from '@the-order/shared';
import { IssueVCSchema, VerifyVCSchema } from '@the-order/schemas';
import { KMSClient } from '@the-order/crypto';
@@ -428,6 +430,13 @@ const start = async () => {
const port = env.PORT || 4002;
await server.listen({ port, host: '0.0.0.0' });
logger.info({ port }, 'Identity service listening');
// Setup graceful shutdown
const pool = getPool({ connectionString: env.DATABASE_URL });
setupGracefulShutdown(server, {
timeout: 30000,
onShutdown: createConnectionDrainer(pool),
});
} catch (err) {
logger.error({ err }, 'Failed to start server');
process.exit(1);

View File

@@ -0,0 +1,49 @@
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package.json pnpm-lock.yaml ./
COPY packages/*/package.json ./packages/*/
COPY services/intake/package.json ./services/intake/
# Install pnpm
RUN npm install -g pnpm
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy source code
COPY . .
# Build
RUN pnpm build --filter=@the-order/intake-service
FROM node:20-alpine AS runner
WORKDIR /app
# Copy package files
COPY package.json pnpm-lock.yaml ./
COPY packages/*/package.json ./packages/*/
COPY services/intake/package.json ./services/intake/
# Install pnpm
RUN npm install -g pnpm
# Install production dependencies
RUN pnpm install --frozen-lockfile --prod
# Copy built files
COPY --from=builder /app/services/intake/dist ./services/intake/dist
COPY --from=builder /app/packages ./packages
WORKDIR /app/services/intake
EXPOSE 4001
# Graceful shutdown signal handling
STOPSIGNAL SIGTERM
CMD ["node", "dist/index.js"]

View File

@@ -0,0 +1,56 @@
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm build --filter=@the-order/legal-documents
- run: pnpm test --filter=@the-order/legal-documents
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm lint --filter=@the-order/legal-documents
build:
runs-on: ubuntu-latest
needs: [test, lint]
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm build --filter=@the-order/legal-documents
- name: Build Docker image
run: |
docker build -t theorder/legal-documents-service:latest -f services/legal-documents/Dockerfile .

View File

@@ -0,0 +1,46 @@
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package.json pnpm-lock.yaml ./
COPY packages/*/package.json ./packages/*/
COPY services/legal-documents/package.json ./services/legal-documents/
# Install pnpm
RUN npm install -g pnpm
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy source code
COPY . .
# Build
RUN pnpm build --filter=@the-order/legal-documents
FROM node:20-alpine AS runner
WORKDIR /app
# Copy package files
COPY package.json pnpm-lock.yaml ./
COPY packages/*/package.json ./packages/*/
COPY services/legal-documents/package.json ./services/legal-documents/
# Install pnpm
RUN npm install -g pnpm
# Install production dependencies
RUN pnpm install --frozen-lockfile --prod
# Copy built files
COPY --from=builder /app/services/legal-documents/dist ./services/legal-documents/dist
COPY --from=builder /app/packages ./packages
WORKDIR /app/services/legal-documents
EXPOSE 4005
CMD ["node", "dist/index.js"]

View File

@@ -0,0 +1,133 @@
# Legal Documents Service
Comprehensive document management service for law firms and courts.
## Features
- **Document Management**: Full CRUD operations with versioning
- **Template System**: Template-based document generation
- **Legal Matters**: Matter management and document linking
- **Document Assembly**: Multi-clause document assembly
- **Collaboration**: Comments, annotations, and review workflows
- **Workflows**: Approval, signing, and filing workflows
- **Court Filings**: E-filing and deadline tracking
- **Audit Trail**: Comprehensive audit logging
- **Search**: Full-text search and discovery
- **Security**: Watermarking, encryption, access control
- **Retention**: Retention policies and disposal workflows
- **Clause Library**: Reusable clause management
## API Endpoints
### Documents
- `POST /documents` - Create document
- `GET /documents/:id` - Get document
- `GET /documents` - List documents
- `PATCH /documents/:id` - Update document
- `POST /documents/:id/checkout` - Checkout document
- `POST /documents/:id/checkin` - Checkin document
### Versions
- `GET /documents/:id/versions` - List versions
- `GET /documents/:id/versions/:version` - Get version
- `GET /documents/:id/versions/latest` - Get latest version
- `GET /documents/:id/versions/history` - Get version history
- `GET /documents/:id/versions/:v1/compare/:v2` - Compare versions
- `POST /documents/:id/versions/:version/restore` - Restore version
### Templates
- `POST /templates` - Create template
- `GET /templates/:id` - Get template
- `GET /templates` - List templates
- `PATCH /templates/:id` - Update template
- `POST /templates/:id/version` - Create template version
- `POST /templates/:id/render` - Render template
- `GET /templates/:id/variables` - Extract variables
### Matters
- `POST /matters` - Create matter
- `GET /matters/:id` - Get matter
- `GET /matters` - List matters
- `PATCH /matters/:id` - Update matter
- `POST /matters/:id/participants` - Add participant
- `GET /matters/:id/participants` - Get participants
- `POST /matters/:matter_id/documents/:document_id` - Link document
- `GET /matters/:id/documents` - Get matter documents
### Assembly
- `POST /assembly/generate` - Generate from template
- `POST /assembly/preview` - Preview template rendering
- `POST /assembly/from-clauses` - Assemble from clauses
### Collaboration
- `POST /documents/:id/comments` - Create comment
- `GET /documents/:id/comments` - Get comments
- `PATCH /comments/:id` - Update comment
- `POST /comments/:id/resolve` - Resolve comment
### Workflows
- `POST /workflows` - Create workflow
- `GET /workflows/:id` - Get workflow
- `GET /documents/:id/workflows` - Get document workflows
- `PATCH /workflows/:id/status` - Update workflow status
### Filings
- `POST /filings` - Create filing
- `GET /filings/:id` - Get filing
- `GET /filings` - List filings
- `GET /matters/:id/filing-deadlines` - Get deadlines
### Audit
- `GET /documents/:id/audit` - Get audit logs
- `GET /audit/search` - Search audit logs
- `GET /audit/statistics` - Get statistics
### Search
- `POST /search` - Search documents
- `GET /search/suggestions` - Get search suggestions
### Retention
- `POST /retention/policies` - Create policy
- `GET /retention/policies` - List policies
- `POST /documents/:id/retention` - Apply policy
- `GET /documents/:id/retention` - Get retention record
- `GET /retention/expired` - Get expired records
- `POST /documents/:id/dispose` - Dispose document
- `GET /retention/statistics` - Get statistics
### Clauses
- `POST /clauses` - Create clause
- `GET /clauses/:id` - Get clause
- `GET /clauses` - List clauses
- `POST /clauses/:id/render` - Render clause
## Development
```bash
# Install dependencies
pnpm install
# Run development server
pnpm dev
# Build
pnpm build
# Start production server
pnpm start
# Run tests
pnpm test
```
## Environment Variables
- `PORT` - Server port (default: 4005)
- `DATABASE_URL` - PostgreSQL connection string
- `NODE_ENV` - Environment (development/production)
- `SWAGGER_SERVER_URL` - Swagger server URL
## API Documentation
Swagger documentation available at `/docs` when running.

View File

@@ -0,0 +1,95 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: legal-documents-service
namespace: the-order
labels:
app: legal-documents-service
version: v1
spec:
replicas: 2
selector:
matchLabels:
app: legal-documents-service
template:
metadata:
labels:
app: legal-documents-service
version: v1
spec:
containers:
- name: legal-documents
image: theorder/legal-documents-service:latest
ports:
- containerPort: 4005
name: http
env:
- name: PORT
value: "4005"
- name: NODE_ENV
value: "production"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: legal-documents-secrets
key: database-url
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 4005
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 4005
initialDelaySeconds: 10
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: legal-documents-service
namespace: the-order
spec:
selector:
app: legal-documents-service
ports:
- port: 80
targetPort: 4005
protocol: TCP
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: legal-documents-service-hpa
namespace: the-order
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: legal-documents-service
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80

View File

@@ -0,0 +1,32 @@
{
"name": "@the-order/legal-documents",
"version": "0.1.0",
"description": "Legal document management service for law firms and courts",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"test": "vitest",
"lint": "eslint src --ext .ts",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@the-order/database": "workspace:*",
"@the-order/shared": "workspace:*",
"@the-order/schemas": "workspace:*",
"@the-order/storage": "workspace:*",
"fastify": "^4.24.3",
"@fastify/swagger": "^8.12.0",
"@fastify/swagger-ui": "^1.9.3",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^20.10.0",
"tsx": "^4.7.0",
"typescript": "^5.3.3",
"vitest": "^1.1.0"
}
}

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);
}

View File

@@ -0,0 +1,51 @@
/**
* Document Templates Tests
*/
import { describe, it, expect } from 'vitest';
import {
createDocumentTemplate,
getDocumentTemplate,
renderDocumentTemplate,
extractTemplateVariables,
} from '@the-order/database';
describe('Document Templates', () => {
it('should create a template', async () => {
const template = await createDocumentTemplate({
name: 'Test Contract',
template_content: 'This is a contract between {{party1}} and {{party2}}.',
category: 'contract',
});
expect(template).toBeDefined();
expect(template.name).toBe('Test Contract');
});
it('should render template with variables', async () => {
const template = await createDocumentTemplate({
name: 'Test Template',
template_content: 'Hello {{name}}, welcome to {{company}}.',
});
const rendered = renderDocumentTemplate(template, {
name: 'John Doe',
company: 'Acme Corp',
});
expect(rendered).toContain('John Doe');
expect(rendered).toContain('Acme Corp');
expect(rendered).not.toContain('{{name}}');
expect(rendered).not.toContain('{{company}}');
});
it('should extract variables from template', () => {
const template_content = 'Contract between {{party1}} and {{party2}} dated {{date}}';
const variables = extractTemplateVariables(template_content);
expect(variables).toContain('party1');
expect(variables).toContain('party2');
expect(variables).toContain('date');
});
});

View File

@@ -0,0 +1,120 @@
/**
* Document Versions Tests
*/
import { describe, it, expect, beforeEach } from 'vitest';
import {
createDocumentVersion,
getDocumentVersions,
getLatestDocumentVersion,
compareDocumentVersions,
restoreDocumentVersion,
} from '@the-order/database';
describe('Document Versions', () => {
const testDocumentId = 'test-doc-id';
beforeEach(async () => {
// Setup test data
});
it('should create a new document version', async () => {
const version = await createDocumentVersion({
document_id: testDocumentId,
version_number: 1,
content: 'Initial content',
change_type: 'created',
});
expect(version).toBeDefined();
expect(version.version_number).toBe(1);
expect(version.content).toBe('Initial content');
});
it('should list all versions for a document', async () => {
await createDocumentVersion({
document_id: testDocumentId,
version_number: 1,
content: 'Version 1',
change_type: 'created',
});
await createDocumentVersion({
document_id: testDocumentId,
version_number: 2,
content: 'Version 2',
change_type: 'modified',
});
const versions = await getDocumentVersions(testDocumentId);
expect(versions.length).toBeGreaterThanOrEqual(2);
expect(versions[0]?.version_number).toBeGreaterThanOrEqual(versions[1]?.version_number || 0);
});
it('should get the latest version', async () => {
await createDocumentVersion({
document_id: testDocumentId,
version_number: 1,
content: 'Version 1',
change_type: 'created',
});
await createDocumentVersion({
document_id: testDocumentId,
version_number: 2,
content: 'Version 2',
change_type: 'modified',
});
const latest = await getLatestDocumentVersion(testDocumentId);
expect(latest?.version_number).toBe(2);
});
it('should compare two versions', async () => {
const v1 = await createDocumentVersion({
document_id: testDocumentId,
version_number: 1,
content: 'Original content',
change_type: 'created',
});
const v2 = await createDocumentVersion({
document_id: testDocumentId,
version_number: 2,
content: 'Modified content',
change_type: 'modified',
});
const comparison = await compareDocumentVersions(v1.id, v2.id);
expect(comparison).toBeDefined();
expect(comparison?.differences.length).toBeGreaterThan(0);
});
it('should restore a previous version', async () => {
const original = await createDocumentVersion({
document_id: testDocumentId,
version_number: 1,
content: 'Original content',
change_type: 'created',
});
await createDocumentVersion({
document_id: testDocumentId,
version_number: 2,
content: 'Modified content',
change_type: 'modified',
});
const restored = await restoreDocumentVersion(
testDocumentId,
original.id,
'test-user-id',
'Restored from version 1'
);
expect(restored).toBeDefined();
expect(restored.version_number).toBe(3);
expect(restored.content).toBe('Original content');
});
});

View File

@@ -0,0 +1,61 @@
/**
* Legal Matters Tests
*/
import { describe, it, expect } from 'vitest';
import {
createLegalMatter,
getLegalMatter,
addMatterParticipant,
linkDocumentToMatter,
getMatterDocuments,
} from '@the-order/database';
describe('Legal Matters', () => {
it('should create a legal matter', async () => {
const matter = await createLegalMatter({
matter_number: 'MAT-2024-001',
title: 'Test Matter',
status: 'open',
});
expect(matter).toBeDefined();
expect(matter.matter_number).toBe('MAT-2024-001');
expect(matter.status).toBe('open');
});
it('should add participant to matter', async () => {
const matter = await createLegalMatter({
matter_number: 'MAT-2024-002',
title: 'Test Matter',
});
const participant = await addMatterParticipant(matter.id, {
user_id: 'test-user-id',
role: 'lead_counsel',
});
expect(participant).toBeDefined();
expect(participant.role).toBe('lead_counsel');
});
it('should link document to matter', async () => {
const matter = await createLegalMatter({
matter_number: 'MAT-2024-003',
title: 'Test Matter',
});
const { createDocument } = await import('@the-order/database');
const document = await createDocument({
title: 'Test Document',
type: 'legal',
});
const link = await linkDocumentToMatter(matter.id, document.id, 'primary_evidence');
expect(link).toBeDefined();
expect(link.matter_id).toBe(matter.id);
expect(link.document_id).toBe(document.id);
});
});

View File

@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"composite": true,
"declaration": true,
"declarationMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}

View File

@@ -0,0 +1,14 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/', 'dist/', '**/*.test.ts'],
},
},
});