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:
153
services/README.md
Normal file
153
services/README.md
Normal 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
|
||||
|
||||
49
services/dataroom/Dockerfile
Normal file
49
services/dataroom/Dockerfile
Normal 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"]
|
||||
|
||||
49
services/finance/Dockerfile
Normal file
49
services/finance/Dockerfile
Normal 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"]
|
||||
|
||||
49
services/identity/Dockerfile
Normal file
49
services/identity/Dockerfile
Normal 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"]
|
||||
|
||||
@@ -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);
|
||||
|
||||
49
services/intake/Dockerfile
Normal file
49
services/intake/Dockerfile
Normal 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"]
|
||||
|
||||
56
services/legal-documents/.github/workflows/ci.yml
vendored
Normal file
56
services/legal-documents/.github/workflows/ci.yml
vendored
Normal 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 .
|
||||
|
||||
46
services/legal-documents/Dockerfile
Normal file
46
services/legal-documents/Dockerfile
Normal 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"]
|
||||
|
||||
133
services/legal-documents/README.md
Normal file
133
services/legal-documents/README.md
Normal 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.
|
||||
|
||||
95
services/legal-documents/k8s/deployment.yaml
Normal file
95
services/legal-documents/k8s/deployment.yaml
Normal 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
|
||||
|
||||
32
services/legal-documents/package.json
Normal file
32
services/legal-documents/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
|
||||
162
services/legal-documents/src/index.ts
Normal file
162
services/legal-documents/src/index.ts
Normal 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;
|
||||
|
||||
222
services/legal-documents/src/routes/assembly-routes.ts
Normal file
222
services/legal-documents/src/routes/assembly-routes.ts
Normal 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),
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
61
services/legal-documents/src/routes/audit-routes.ts
Normal file
61
services/legal-documents/src/routes/audit-routes.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
|
||||
88
services/legal-documents/src/routes/clause-routes.ts
Normal file
88
services/legal-documents/src/routes/clause-routes.ts
Normal 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 });
|
||||
});
|
||||
}
|
||||
|
||||
196
services/legal-documents/src/routes/collaboration-routes.ts
Normal file
196
services/legal-documents/src/routes/collaboration-routes.ts
Normal 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);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
359
services/legal-documents/src/routes/document-routes.ts
Normal file
359
services/legal-documents/src/routes/document-routes.ts
Normal 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 });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
74
services/legal-documents/src/routes/filing-routes.ts
Normal file
74
services/legal-documents/src/routes/filing-routes.ts
Normal 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 });
|
||||
});
|
||||
}
|
||||
|
||||
356
services/legal-documents/src/routes/matter-routes.ts
Normal file
356
services/legal-documents/src/routes/matter-routes.ts
Normal 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 });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
107
services/legal-documents/src/routes/retention-routes.ts
Normal file
107
services/legal-documents/src/routes/retention-routes.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
|
||||
44
services/legal-documents/src/routes/search-routes.ts
Normal file
44
services/legal-documents/src/routes/search-routes.ts
Normal 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 });
|
||||
});
|
||||
}
|
||||
|
||||
34
services/legal-documents/src/routes/security-routes.ts
Normal file
34
services/legal-documents/src/routes/security-routes.ts
Normal 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' });
|
||||
});
|
||||
}
|
||||
|
||||
287
services/legal-documents/src/routes/template-routes.ts
Normal file
287
services/legal-documents/src/routes/template-routes.ts
Normal 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 });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
200
services/legal-documents/src/routes/version-routes.ts
Normal file
200
services/legal-documents/src/routes/version-routes.ts
Normal 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);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
76
services/legal-documents/src/routes/workflow-routes.ts
Normal file
76
services/legal-documents/src/routes/workflow-routes.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
|
||||
134
services/legal-documents/src/services/court-efiling.ts
Normal file
134
services/legal-documents/src/services/court-efiling.ts
Normal 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'],
|
||||
};
|
||||
}
|
||||
|
||||
147
services/legal-documents/src/services/document-analytics.ts
Normal file
147
services/legal-documents/src/services/document-analytics.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
|
||||
156
services/legal-documents/src/services/document-assembly.ts
Normal file
156
services/legal-documents/src/services/document-assembly.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
|
||||
172
services/legal-documents/src/services/document-export.ts
Normal file
172
services/legal-documents/src/services/document-export.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
126
services/legal-documents/src/services/document-optimization.ts
Normal file
126
services/legal-documents/src/services/document-optimization.ts
Normal 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();
|
||||
}
|
||||
|
||||
129
services/legal-documents/src/services/document-security.ts
Normal file
129
services/legal-documents/src/services/document-security.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
|
||||
108
services/legal-documents/src/services/e-signature.ts
Normal file
108
services/legal-documents/src/services/e-signature.ts
Normal 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
|
||||
}
|
||||
|
||||
131
services/legal-documents/src/services/real-time-collaboration.ts
Normal file
131
services/legal-documents/src/services/real-time-collaboration.ts
Normal 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) : [];
|
||||
}
|
||||
|
||||
119
services/legal-documents/src/services/workflow-engine.ts
Normal file
119
services/legal-documents/src/services/workflow-engine.ts
Normal 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);
|
||||
}
|
||||
|
||||
51
services/legal-documents/tests/document-templates.test.ts
Normal file
51
services/legal-documents/tests/document-templates.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
|
||||
120
services/legal-documents/tests/document-versions.test.ts
Normal file
120
services/legal-documents/tests/document-versions.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
|
||||
61
services/legal-documents/tests/legal-matters.test.ts
Normal file
61
services/legal-documents/tests/legal-matters.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
|
||||
13
services/legal-documents/tsconfig.json
Normal file
13
services/legal-documents/tsconfig.json
Normal 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"]
|
||||
}
|
||||
|
||||
14
services/legal-documents/vitest.config.ts
Normal file
14
services/legal-documents/vitest.config.ts
Normal 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'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user