Skip to content
Merged
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
Binary file added eta-endpoint.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2,285 changes: 2,285 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

26 changes: 20 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
{
"name": "swiftchain-backend",
"version": "1.0.0",
"description": "Backend API for SwiftChain - Blockchain-Powered Logistics & Escrow Delivery Platform",
"main": "dist/server.js",
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"dev": "nodemon src/server.ts",
"build": "tsc",
"start": "node dist/server.js",
"dev": "nodemon --exec ts-node src/server.ts",
"seed": "ts-node src/seed.ts"
},
"dependencies": {
"axios": "^1.6.0",
"cors": "^2.8.5",
"dotenv": "^16.0.0",
"express": "^4.18.0",
"mongoose": "^7.0.0",
"winston": "^3.19.0"
},
"devDependencies": {
"@types/axios": "^0.14.0",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/node": "^20.0.0",
"@types/winston": "^2.4.4",
"nodemon": "^3.0.2",
"ts-node": "^10.9.0",
"typescript": "^5.0.0"
"lint": "eslint . --ext .ts",
"format": "prettier --write .",
"test": "jest",
Expand Down
48 changes: 29 additions & 19 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import express from 'express';
import mongoose from 'mongoose';
import cors from 'cors';
import dotenv from 'dotenv';
import routes from './routes';
import logger from './config/logger';

dotenv.config();

const app = express();
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/swiftchain';
import helmet from 'helmet';
import compression from 'compression';
import rateLimit from 'express-rate-limit';
Expand Down Expand Up @@ -36,37 +45,38 @@ app.use('/api', limiter);
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));

// Compression
app.use(compression());
app.use(cors());
app.use(express.json());

// Request logging middleware
app.use(requestLogger);
app.use('/api', routes);

// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({
status: 'success',
message: 'SwiftChain-Backend is running',
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
mongodb: mongoose.connection.readyState === 1 ? 'connected' : 'disconnected',
});
});

// API routes
app.use('/api/v1', routes);

// 404 handler
app.use('*', (req, res) => {
app.use((req, res) => {
res.status(404).json({
status: 'error',
message: `Cannot ${req.method} ${req.originalUrl}`,
success: false,
error: `Route ${req.path} not found`,
});
});

// Global error handler
app.use(errorHandler);
// Connect to MongoDB but don't start the server here
const connectDB = async () => {
try {
await mongoose.connect(MONGODB_URI);
logger.info('✅ Connected to MongoDB');
} catch (error) {
logger.error('❌ Failed to connect to MongoDB:', error);
process.exit(1);
}
};

// Database connection
connectDatabase();
// Call connectDB but don't listen
connectDB();

export default app;
34 changes: 34 additions & 0 deletions src/controllers/deliveryController.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,37 @@
import { Request, Response } from 'express';
import { deliveryService } from '../services/deliveryService';

class DeliveryController {
async getDeliveryETA(req: Request, res: Response): Promise<void> {
try {
const { id } = req.params;

if (!id) {
res.status(400).json({
success: false,
error: 'Delivery ID is required',
});
return;
}

const result = await deliveryService.calculateDeliveryETA({ deliveryId: id });

res.status(200).json({
success: true,
data: result,
message: 'ETA calculated successfully',
});
} catch (error: any) {
const statusCode = error.message?.includes('not found') ? 404 : 500;
res.status(statusCode).json({
success: false,
error: error.message || 'Failed to calculate ETA',
});
}
}
}

export const deliveryController = new DeliveryController();
import { Request, Response, NextFunction } from 'express';
import crypto from 'crypto';
import mongoose from 'mongoose';
Expand Down
99 changes: 37 additions & 62 deletions src/models/Delivery.ts
Original file line number Diff line number Diff line change
@@ -1,77 +1,52 @@
import mongoose, { Document, Schema } from 'mongoose';

export type DeliveryStatus =
| 'pending'
| 'assigned'
| 'in_transit'
| 'delivered'
| 'cancelled'
| 'disputed';

export interface ISender {
userId?: string;
name: string;
contact: string;
address: string;
}

export interface IRecipient {
name: string;
contact: string;
address: string;
}
import mongoose, { Schema, Document } from 'mongoose';

export interface IDelivery extends Document {
trackingId: string;
sender: ISender;
recipient: IRecipient;
packageDescription: string;
weight?: number;
estimatedValue?: number;
driverId?: string;
status: DeliveryStatus;
notes?: string;
deliveryId: string;
driverId: string;
userId: string;
pickupCoordinates: {
lat: number;
lng: number;
address: string;
};
dropoffCoordinates: {
lat: number;
lng: number;
address: string;
};
status: 'pending' | 'assigned' | 'in_progress' | 'completed' | 'cancelled';
distance?: number;
estimatedDuration?: number;
actualDuration?: number;
createdAt: Date;
updatedAt: Date;
}

const SenderSchema = new Schema<ISender>(
{
userId: { type: String },
name: { type: String, required: true },
contact: { type: String, required: true },
address: { type: String, required: true },
},
{ _id: false },
);

const RecipientSchema = new Schema<IRecipient>(
{
name: { type: String, required: true },
contact: { type: String, required: true },
address: { type: String, required: true },
},
{ _id: false },
);

const DeliverySchema = new Schema<IDelivery>(
{
trackingId: { type: String, required: true, unique: true, index: true },
sender: { type: SenderSchema, required: true },
recipient: { type: RecipientSchema, required: true },
packageDescription: { type: String, required: true },
weight: { type: Number },
estimatedValue: { type: Number },
driverId: { type: String, index: true },
deliveryId: { type: String, required: true, unique: true },
driverId: { type: String, required: true },
userId: { type: String, required: true },
pickupCoordinates: {
lat: { type: Number, required: true },
lng: { type: Number, required: true },
address: { type: String, required: true },
},
dropoffCoordinates: {
lat: { type: Number, required: true },
lng: { type: Number, required: true },
address: { type: String, required: true },
},
status: {
type: String,
enum: ['pending', 'assigned', 'in_transit', 'delivered', 'cancelled', 'disputed'],
enum: ['pending', 'assigned', 'in_progress', 'completed', 'cancelled'],
default: 'pending',
index: true,
},
notes: { type: String },
distance: { type: Number },
estimatedDuration: { type: Number },
actualDuration: { type: Number },
},
{ timestamps: true },
{ timestamps: true }
);

export default mongoose.model<IDelivery>('Delivery', DeliverySchema);
export const Delivery = mongoose.model<IDelivery>('Delivery', DeliverySchema);
9 changes: 2 additions & 7 deletions src/routes/deliveryRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import { Router } from 'express';
import { createDelivery } from '../controllers/deliveryController';
import { deliveryController } from '../controllers/deliveryController';

const router = Router();

/**
* @route POST /api/v1/deliveries
* @desc Create a new delivery and store its off-chain metadata
* @access Public (authentication to be layered on in a future issue)
*/
router.post('/', createDelivery);
router.get('/:id/eta', deliveryController.getDeliveryETA);

export default router;
10 changes: 2 additions & 8 deletions src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import { Router } from 'express';
import stellarRoutes from './stellar.routes';
import deliveryRoutes from './deliveryRoutes';

const router = Router();

// ─── Stellar / Soroban ──────────────────────────────────────────────────────
router.use('/stellar', stellarRoutes);

// ─── Future routes ──────────────────────────────────────────────────────────
// router.use('/auth', authRoutes);
// router.use('/users', userRoutes);
// router.use('/deliveries', deliveryRoutes);
router.use('/v1/deliveries', deliveryRoutes);

export default router;
41 changes: 41 additions & 0 deletions src/seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import mongoose from 'mongoose';
import dotenv from 'dotenv';
import { Delivery } from './models/Delivery';

dotenv.config();

const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/swiftchain';

const seedDeliveries = async () => {
try {
await mongoose.connect(MONGODB_URI);

const delivery = {
deliveryId: 'DEL-001',
driverId: 'DRV-001',
userId: 'USR-001',
pickupCoordinates: {
lat: 40.7128,
lng: -74.0060,
address: 'New York, NY',
},
dropoffCoordinates: {
lat: 40.7580,
lng: -73.9855,
address: 'Times Square, NY',
},
status: 'pending',
};

await Delivery.deleteMany({});
await Delivery.create(delivery);

console.log('✅ Test delivery created successfully');
process.exit(0);
} catch (error) {
console.error('❌ Failed to seed data:', error);
process.exit(1);
}
};

seedDeliveries();
14 changes: 4 additions & 10 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,11 @@ import { initializeSocketServer, shutdownSocketServer, TypedServer } from './soc

const PORT = env.PORT;

// Create an HTTP server so Socket.IO can share it with Express
const httpServer = http.createServer(app);

// Attach Socket.IO to the HTTP server
const io: TypedServer = initializeSocketServer(httpServer);

// Start listening
httpServer.listen(PORT, () => {
logger.info(`🚀 Server running on port ${PORT} in ${process.env.NODE_ENV} mode`);
// Start the server from here
const server = app.listen(PORT, () => {
logger.info(`🚀 Server running on port ${PORT} in ${process.env.NODE_ENV || 'development'} mode`);
logger.info(`📝 Health check: http://localhost:${PORT}/health`);
logger.info(`🔌 WebSocket server ready on port ${PORT}`);
logger.info(`📦 ETA endpoint: http://localhost:${PORT}/api/v1/deliveries/:id/eta`);
});

// ─── Graceful shutdown ────────────────────────────────────────────────────────
Expand Down
Loading