Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ npm-debug.log*
# Keys
*.pem

# Test snapshots
*.snap
*.snap.new
**/__snapshots__/
**/snapshots/

# Temporary files
*.tmp
*.bak
*.orig

.claude
.lhci_reports/
bundle-reports/
Expand Down
87 changes: 87 additions & 0 deletions backend/src/routes/contracts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Router, type Request, type Response } from 'express';
import { z } from 'zod';
import {
generateContract,
amendContract,
getContract,
listContracts,
searchContracts,
getContractDiff,
type GenerateContractInput,
type AmendmentInput,
} from '../services/contracts.js';
import { asyncHandler } from '../middleware/errorHandler.js';

export const contractsRouter = Router();

const generateSchema = z.object({
projectId: z.string().min(1),
type: z.enum(['service', 'milestone', 'nda', 'custom']),
clientId: z.string().min(1),
freelancerId: z.string().min(1),
projectConfig: z.object({
name: z.string().min(1),
description: z.string().min(1),
budget: z.number().positive(),
currency: z.string().min(1),
milestones: z.array(
z.object({
title: z.string().min(1),
amount: z.number().positive(),
dueDate: z.string(),
}),
),
paymentTerms: z.string().min(1),
startDate: z.string(),
endDate: z.string().optional(),
}),
createdBy: z.string().min(1),
});

const amendSchema = z.object({
contractId: z.string().min(1),
changeDescription: z.string().min(1),
newClauses: z
.array(
z.object({
title: z.string().min(1),
body: z.string().min(1),
isRequired: z.boolean().optional(),
}),
)
.optional(),
removedClauseIds: z.array(z.string().min(1)).optional(),
createdBy: z.string().min(1),
});

contractsRouter.post('/generate', asyncHandler(async (req: Request, res: Response) => {
const input = generateSchema.parse(req.body) as GenerateContractInput;
const contract = generateContract(input);
res.status(201).json(contract);
}));

contractsRouter.post('/amend', asyncHandler(async (req: Request, res: Response) => {
const input = amendSchema.parse(req.body) as AmendmentInput;
const contract = amendContract(input);
res.json(contract);
}));

contractsRouter.get('/', asyncHandler(async (req: Request, res: Response) => {
const { projectId, status, type } = req.query as Record<string, string | undefined>;
const results = searchContracts({ projectId, status, type });
res.json({ contracts: results, total: results.length });
}));

contractsRouter.get('/:id', asyncHandler(async (req: Request, res: Response) => {
const contract = getContract(req.params.id);
if (!contract) return res.status(404).json({ error: 'Contract not found' });
res.json(contract);
}));

contractsRouter.get('/:id/diff', asyncHandler(async (req: Request, res: Response) => {
const from = Number(req.query.from);
const to = Number(req.query.to);
if (!from || !to) return res.status(400).json({ error: 'from and to query params are required' });
const diff = getContractDiff(from, to, req.params.id);
res.json(diff);
}));
29 changes: 29 additions & 0 deletions backend/src/routes/receipts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
searchReceipts,
archiveReceipts,
generateReceiptPdf,
verifyReceiptOnChain,
exportReceipts,
} from '../services/receipts.js';
import {
archiveReceiptSchema,
Expand Down Expand Up @@ -175,6 +177,33 @@ receiptsRouter.get(
})
);

receiptsRouter.get(
'/:tokenId/verify/onchain',
cacheControl({ maxAge: CacheTTL.SHORT }),
asyncHandler(async (req, res) => {
const receipt = getReceiptByTokenId(req.params.tokenId);
if (!receipt) throw new AppError(404, 'Receipt not found', 'NOT_FOUND');
const onChain = verifyReceiptOnChain(receipt);
res.json(onChain);
})
);

receiptsRouter.get(
'/export',
cacheControl({ maxAge: CacheTTL.SHORT }),
asyncHandler(async (req, res) => {
const format = (req.query.format as string) === 'csv' ? 'csv' : 'json';
const data = exportReceipts(format);
if (format === 'csv') {
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename="receipts.csv"');
res.send(data);
} else {
res.json(JSON.parse(data as string));
}
})
);

receiptsRouter.get(
'/:tokenId/pdf',
cacheControl({ maxAge: CacheTTL.LONG }),
Expand Down
63 changes: 63 additions & 0 deletions backend/src/routes/reputation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Router, type Request, type Response } from 'express';
import { z } from 'zod';
import {
getReputation,
listReputations,
getReputationSnapshot,
recordTransaction,
awardBadge,
recalculateAll,
detectGamingPattern,
} from '../services/reputation.js';
import { asyncHandler } from '../middleware/errorHandler.js';

export const reputationRouter = Router();

const recordTransactionSchema = z.object({
event: z.enum(['completed', 'late', 'disputed', 'quality_rated']),
weight: z.number().positive().optional(),
});

const awardBadgeSchema = z.object({
name: z.string().min(1),
description: z.string().min(1),
});

reputationRouter.get('/:userId', asyncHandler(async (req: Request, res: Response) => {
const reputation = getReputation(req.params.userId);
if (!reputation) return res.status(404).json({ error: 'Reputation not found' });
res.json(reputation);
}));

reputationRouter.get('/:userId/snapshot', asyncHandler(async (req: Request, res: Response) => {
const snapshot = getReputationSnapshot(req.params.userId);
if (!snapshot) return res.status(404).json({ error: 'Reputation not found' });
res.json(snapshot);
}));

reputationRouter.post('/:userId/transaction', asyncHandler(async (req: Request, res: Response) => {
const { event, weight } = recordTransactionSchema.parse(req.body);
const reputation = recordTransaction(req.params.userId, event, weight);
res.json(reputation);
}));

reputationRouter.post('/:userId/badges', asyncHandler(async (req: Request, res: Response) => {
const { name, description } = awardBadgeSchema.parse(req.body);
const reputation = awardBadge(req.params.userId, name, description);
res.json(reputation);
}));

reputationRouter.get('/', asyncHandler(async (_req: Request, res: Response) => {
const reputations = listReputations();
res.json({ reputations, total: reputations.length });
}));

reputationRouter.post('/recalculate', asyncHandler(async (_req: Request, res: Response) => {
recalculateAll();
res.json({ success: true, message: 'Recalculation started' });
}));

reputationRouter.get('/:userId/gaming-check', asyncHandler(async (req: Request, res: Response) => {
const result = detectGamingPattern(req.params.userId);
res.json(result);
}));
Loading