Initial commit: SDI SaaS Platform foundation
- Complete monorepo structure with pnpm workspaces - Prisma database schema with 20+ entities - NestJS API with 9 core modules - BullMQ orchestration worker - AWS and Azure provider adapters - Docker Compose infrastructure - Complete documentation
This commit is contained in:
5
apps/api/.env.example
Normal file
5
apps/api/.env.example
Normal file
@@ -0,0 +1,5 @@
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/sdi_saas?schema=public"
|
||||
CORS_ORIGIN=http://localhost:3001
|
||||
JWT_SECRET=your-secret-key-change-in-production
|
||||
26
apps/api/Dockerfile
Normal file
26
apps/api/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
FROM node:20-alpine AS base
|
||||
RUN npm install -g pnpm@8.15.0
|
||||
|
||||
FROM base AS dependencies
|
||||
WORKDIR /app
|
||||
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml* ./
|
||||
COPY apps/api/package.json ./apps/api/
|
||||
COPY packages/database/package.json ./packages/database/
|
||||
COPY packages/shared-types/package.json ./packages/shared-types/
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
FROM base AS build
|
||||
WORKDIR /app
|
||||
COPY --from=dependencies /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN pnpm --filter @sdi/database db:generate
|
||||
RUN pnpm --filter @sdi/api build
|
||||
|
||||
FROM base AS runtime
|
||||
WORKDIR /app
|
||||
COPY --from=dependencies /app/node_modules ./node_modules
|
||||
COPY --from=build /app/apps/api/dist ./apps/api/dist
|
||||
COPY --from=build /app/packages ./packages
|
||||
COPY apps/api/package.json ./apps/api/
|
||||
EXPOSE 3000
|
||||
CMD ["node", "apps/api/dist/main.js"]
|
||||
8
apps/api/nest-cli.json
Normal file
8
apps/api/nest-cli.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
50
apps/api/package.json
Normal file
50
apps/api/package.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "@sdi/api",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"dev": "nest start --watch",
|
||||
"start": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^10.3.0",
|
||||
"@nestjs/core": "^10.3.0",
|
||||
"@nestjs/platform-express": "^10.3.0",
|
||||
"@nestjs/config": "^3.1.1",
|
||||
"@nestjs/swagger": "^7.2.0",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@sdi/database": "workspace:*",
|
||||
"@sdi/shared-types": "workspace:*",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.3.0",
|
||||
"@nestjs/schematics": "^10.1.0",
|
||||
"@nestjs/testing": "^10.3.0",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/passport-jwt": "^4.0.0",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^6.19.0",
|
||||
"@typescript-eslint/parser": "^6.19.0",
|
||||
"eslint": "^8.56.0",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
32
apps/api/src/app.module.ts
Normal file
32
apps/api/src/app.module.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { DatabaseModule } from './database/database.module';
|
||||
import { AuthModule } from './modules/auth/auth.module';
|
||||
import { TenantsModule } from './modules/tenants/tenants.module';
|
||||
import { UsersModule } from './modules/users/users.module';
|
||||
import { ProvidersModule } from './modules/providers/providers.module';
|
||||
import { EndpointsModule } from './modules/endpoints/endpoints.module';
|
||||
import { QuotesModule } from './modules/quotes/quotes.module';
|
||||
import { OrdersModule } from './modules/orders/orders.module';
|
||||
import { ServicesModule } from './modules/services/services.module';
|
||||
import { AuditModule } from './modules/audit/audit.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
envFilePath: ['.env.local', '.env'],
|
||||
}),
|
||||
DatabaseModule,
|
||||
AuthModule,
|
||||
TenantsModule,
|
||||
UsersModule,
|
||||
ProvidersModule,
|
||||
EndpointsModule,
|
||||
QuotesModule,
|
||||
OrdersModule,
|
||||
ServicesModule,
|
||||
AuditModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
9
apps/api/src/database/database.module.ts
Normal file
9
apps/api/src/database/database.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { PrismaService } from './prisma.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [PrismaService],
|
||||
exports: [PrismaService],
|
||||
})
|
||||
export class DatabaseModule {}
|
||||
13
apps/api/src/database/prisma.service.ts
Normal file
13
apps/api/src/database/prisma.service.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||
import { PrismaClient } from '@sdi/database';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
||||
async onModuleInit() {
|
||||
await this.$connect();
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.$disconnect();
|
||||
}
|
||||
}
|
||||
43
apps/api/src/main.ts
Normal file
43
apps/api/src/main.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
// Global validation pipe
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transform: true,
|
||||
}),
|
||||
);
|
||||
|
||||
// CORS
|
||||
app.enableCors({
|
||||
origin: process.env.CORS_ORIGIN || '*',
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
// API prefix
|
||||
app.setGlobalPrefix('api/v1');
|
||||
|
||||
// Swagger documentation
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('SDI SaaS Platform API')
|
||||
.setDescription('Software-Defined Interconnection Platform API')
|
||||
.setVersion('1.0')
|
||||
.addBearerAuth()
|
||||
.build();
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
SwaggerModule.setup('api/docs', app, document);
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
await app.listen(port);
|
||||
console.log(`🚀 API server running on http://localhost:${port}`);
|
||||
console.log(`📚 API docs available at http://localhost:${port}/api/docs`);
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
8
apps/api/src/modules/audit/audit.module.ts
Normal file
8
apps/api/src/modules/audit/audit.module.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuditService } from './audit.service';
|
||||
|
||||
@Module({
|
||||
providers: [AuditService],
|
||||
exports: [AuditService],
|
||||
})
|
||||
export class AuditModule {}
|
||||
48
apps/api/src/modules/audit/audit.service.ts
Normal file
48
apps/api/src/modules/audit/audit.service.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../../database/prisma.service';
|
||||
|
||||
interface AuditLogData {
|
||||
aggregateType: string;
|
||||
aggregateId: string;
|
||||
eventType: string;
|
||||
actorId?: string;
|
||||
actorType?: string;
|
||||
payload: Record<string, any>;
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuditService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async log(data: AuditLogData) {
|
||||
return this.prisma.auditEvent.create({
|
||||
data: {
|
||||
aggregateType: data.aggregateType,
|
||||
aggregateId: data.aggregateId,
|
||||
eventType: data.eventType,
|
||||
actorId: data.actorId,
|
||||
actorType: data.actorType,
|
||||
payload: data.payload,
|
||||
ipAddress: data.ipAddress,
|
||||
userAgent: data.userAgent,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findByAggregate(aggregateType: string, aggregateId: string) {
|
||||
return this.prisma.auditEvent.findMany({
|
||||
where: { aggregateType, aggregateId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
async findByEventType(eventType: string, limit = 100) {
|
||||
return this.prisma.auditEvent.findMany({
|
||||
where: { eventType },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: limit,
|
||||
});
|
||||
}
|
||||
}
|
||||
4
apps/api/src/modules/auth/auth.module.ts
Normal file
4
apps/api/src/modules/auth/auth.module.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
@Module({})
|
||||
export class AuthModule {}
|
||||
28
apps/api/src/modules/endpoints/endpoints.controller.ts
Normal file
28
apps/api/src/modules/endpoints/endpoints.controller.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Controller, Get, Param, Query } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiQuery } from '@nestjs/swagger';
|
||||
import { EndpointsService } from './endpoints.service';
|
||||
|
||||
@ApiTags('endpoints')
|
||||
@Controller('endpoints')
|
||||
export class EndpointsController {
|
||||
constructor(private readonly endpointsService: EndpointsService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get all endpoints' })
|
||||
@ApiQuery({ name: 'providerId', required: false })
|
||||
@ApiQuery({ name: 'kind', required: false })
|
||||
@ApiQuery({ name: 'region', required: false })
|
||||
findAll(
|
||||
@Query('providerId') providerId?: string,
|
||||
@Query('kind') kind?: string,
|
||||
@Query('region') region?: string,
|
||||
) {
|
||||
return this.endpointsService.findAll({ providerId, kind, region });
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get endpoint by ID' })
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.endpointsService.findOne(id);
|
||||
}
|
||||
}
|
||||
10
apps/api/src/modules/endpoints/endpoints.module.ts
Normal file
10
apps/api/src/modules/endpoints/endpoints.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { EndpointsController } from './endpoints.controller';
|
||||
import { EndpointsService } from './endpoints.service';
|
||||
|
||||
@Module({
|
||||
controllers: [EndpointsController],
|
||||
providers: [EndpointsService],
|
||||
exports: [EndpointsService],
|
||||
})
|
||||
export class EndpointsModule {}
|
||||
26
apps/api/src/modules/endpoints/endpoints.service.ts
Normal file
26
apps/api/src/modules/endpoints/endpoints.service.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../../database/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class EndpointsService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async findAll(filters?: { providerId?: string; kind?: string; region?: string }) {
|
||||
return this.prisma.endpoint.findMany({
|
||||
where: {
|
||||
...(filters?.providerId && { providerId: filters.providerId }),
|
||||
...(filters?.kind && { kind: filters.kind }),
|
||||
...(filters?.region && { region: filters.region }),
|
||||
status: 'available',
|
||||
},
|
||||
include: { provider: true },
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
return this.prisma.endpoint.findUnique({
|
||||
where: { id },
|
||||
include: { provider: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
34
apps/api/src/modules/orders/dto/create-order.dto.ts
Normal file
34
apps/api/src/modules/orders/dto/create-order.dto.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { IsString, IsNotEmpty, IsInt, Min, IsOptional, IsUUID } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CreateOrderDto {
|
||||
@ApiProperty()
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
productOfferingId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
providerId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
sourceEndpointId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
targetEndpointId: string;
|
||||
|
||||
@ApiProperty({ example: 1000 })
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
bandwidthMbps: number;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
quoteId?: string;
|
||||
}
|
||||
42
apps/api/src/modules/orders/orders.controller.ts
Normal file
42
apps/api/src/modules/orders/orders.controller.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Controller, Get, Post, Body, Param, Query, Delete } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiQuery } from '@nestjs/swagger';
|
||||
import { OrdersService } from './orders.service';
|
||||
import { CreateOrderDto } from './dto/create-order.dto';
|
||||
|
||||
@ApiTags('orders')
|
||||
@Controller('orders')
|
||||
export class OrdersController {
|
||||
constructor(private readonly ordersService: OrdersService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create a new service order' })
|
||||
create(@Body() createOrderDto: CreateOrderDto) {
|
||||
// TODO: Extract tenantId and userId from JWT token
|
||||
const tenantId = 'mock-tenant-id';
|
||||
const userId = 'mock-user-id';
|
||||
return this.ordersService.create(tenantId, userId, createOrderDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get all orders for tenant' })
|
||||
@ApiQuery({ name: 'status', required: false })
|
||||
findAll(@Query('status') status?: string) {
|
||||
const tenantId = 'mock-tenant-id';
|
||||
return this.ordersService.findAll(tenantId, { status });
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get order by ID' })
|
||||
findOne(@Param('id') id: string) {
|
||||
const tenantId = 'mock-tenant-id';
|
||||
return this.ordersService.findOne(id, tenantId);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Cancel an order' })
|
||||
cancel(@Param('id') id: string) {
|
||||
const tenantId = 'mock-tenant-id';
|
||||
const userId = 'mock-user-id';
|
||||
return this.ordersService.cancel(id, tenantId, userId);
|
||||
}
|
||||
}
|
||||
12
apps/api/src/modules/orders/orders.module.ts
Normal file
12
apps/api/src/modules/orders/orders.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { OrdersController } from './orders.controller';
|
||||
import { OrdersService } from './orders.service';
|
||||
import { AuditModule } from '../audit/audit.module';
|
||||
|
||||
@Module({
|
||||
imports: [AuditModule],
|
||||
controllers: [OrdersController],
|
||||
providers: [OrdersService],
|
||||
exports: [OrdersService],
|
||||
})
|
||||
export class OrdersModule {}
|
||||
119
apps/api/src/modules/orders/orders.service.ts
Normal file
119
apps/api/src/modules/orders/orders.service.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { PrismaService } from '../../database/prisma.service';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import { CreateOrderDto } from './dto/create-order.dto';
|
||||
import { ServiceOrderStatus } from '@sdi/shared-types';
|
||||
|
||||
@Injectable()
|
||||
export class OrdersService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private auditService: AuditService,
|
||||
) {}
|
||||
|
||||
async create(tenantId: string, userId: string, data: CreateOrderDto) {
|
||||
// Validate endpoints exist
|
||||
const [sourceEndpoint, targetEndpoint] = await Promise.all([
|
||||
this.prisma.endpoint.findUnique({ where: { id: data.sourceEndpointId } }),
|
||||
this.prisma.endpoint.findUnique({ where: { id: data.targetEndpointId } }),
|
||||
]);
|
||||
|
||||
if (!sourceEndpoint || !targetEndpoint) {
|
||||
throw new BadRequestException('Invalid endpoint IDs');
|
||||
}
|
||||
|
||||
const order = await this.prisma.serviceOrder.create({
|
||||
data: {
|
||||
tenantId,
|
||||
userId,
|
||||
productOfferingId: data.productOfferingId,
|
||||
providerId: data.providerId,
|
||||
sourceEndpointId: data.sourceEndpointId,
|
||||
targetEndpointId: data.targetEndpointId,
|
||||
bandwidthMbps: data.bandwidthMbps,
|
||||
status: 'draft',
|
||||
quoteId: data.quoteId,
|
||||
},
|
||||
include: {
|
||||
sourceEndpoint: true,
|
||||
targetEndpoint: true,
|
||||
productOffering: true,
|
||||
},
|
||||
});
|
||||
|
||||
await this.auditService.log({
|
||||
aggregateType: 'order',
|
||||
aggregateId: order.id,
|
||||
eventType: 'order.created',
|
||||
actorId: userId,
|
||||
actorType: 'user',
|
||||
payload: { orderId: order.id, status: 'draft' },
|
||||
});
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
async findAll(tenantId: string, filters?: { status?: string }) {
|
||||
return this.prisma.serviceOrder.findMany({
|
||||
where: {
|
||||
tenantId,
|
||||
...(filters?.status && { status: filters.status }),
|
||||
},
|
||||
include: {
|
||||
sourceEndpoint: true,
|
||||
targetEndpoint: true,
|
||||
productOffering: true,
|
||||
service: true,
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: string, tenantId: string) {
|
||||
const order = await this.prisma.serviceOrder.findFirst({
|
||||
where: { id, tenantId },
|
||||
include: {
|
||||
sourceEndpoint: true,
|
||||
targetEndpoint: true,
|
||||
productOffering: true,
|
||||
provider: true,
|
||||
service: true,
|
||||
provisioningTasks: { orderBy: { createdAt: 'desc' } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
throw new NotFoundException(`Order ${id} not found`);
|
||||
}
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
async updateStatus(orderId: string, status: ServiceOrderStatus, userId?: string) {
|
||||
const order = await this.prisma.serviceOrder.update({
|
||||
where: { id: orderId },
|
||||
data: { status, updatedAt: new Date() },
|
||||
});
|
||||
|
||||
await this.auditService.log({
|
||||
aggregateType: 'order',
|
||||
aggregateId: orderId,
|
||||
eventType: 'order.status_changed',
|
||||
actorId: userId,
|
||||
actorType: userId ? 'user' : 'system',
|
||||
payload: { orderId, status },
|
||||
});
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
async cancel(id: string, tenantId: string, userId: string) {
|
||||
const order = await this.findOne(id, tenantId);
|
||||
|
||||
if (!['draft', 'submitted', 'quoted'].includes(order.status)) {
|
||||
throw new BadRequestException('Order cannot be cancelled in current status');
|
||||
}
|
||||
|
||||
return this.updateStatus(id, 'terminated', userId);
|
||||
}
|
||||
}
|
||||
21
apps/api/src/modules/providers/providers.controller.ts
Normal file
21
apps/api/src/modules/providers/providers.controller.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Controller, Get, Param } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
import { ProvidersService } from './providers.service';
|
||||
|
||||
@ApiTags('providers')
|
||||
@Controller('providers')
|
||||
export class ProvidersController {
|
||||
constructor(private readonly providersService: ProvidersService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get all providers' })
|
||||
findAll() {
|
||||
return this.providersService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get provider by ID' })
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.providersService.findOne(id);
|
||||
}
|
||||
}
|
||||
10
apps/api/src/modules/providers/providers.module.ts
Normal file
10
apps/api/src/modules/providers/providers.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ProvidersController } from './providers.controller';
|
||||
import { ProvidersService } from './providers.service';
|
||||
|
||||
@Module({
|
||||
controllers: [ProvidersController],
|
||||
providers: [ProvidersService],
|
||||
exports: [ProvidersService],
|
||||
})
|
||||
export class ProvidersModule {}
|
||||
28
apps/api/src/modules/providers/providers.service.ts
Normal file
28
apps/api/src/modules/providers/providers.service.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../../database/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class ProvidersService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async findAll() {
|
||||
return this.prisma.provider.findMany({
|
||||
include: {
|
||||
endpoints: true,
|
||||
productOfferings: true,
|
||||
_count: { select: { orders: true } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
return this.prisma.provider.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
endpoints: true,
|
||||
productOfferings: true,
|
||||
accounts: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
24
apps/api/src/modules/quotes/dto/create-quote.dto.ts
Normal file
24
apps/api/src/modules/quotes/dto/create-quote.dto.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { IsUUID, IsNotEmpty, IsInt, Min } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CreateQuoteDto {
|
||||
@ApiProperty()
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
productOfferingId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
sourceEndpointId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
targetEndpointId: string;
|
||||
|
||||
@ApiProperty({ example: 1000 })
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
bandwidthMbps: number;
|
||||
}
|
||||
31
apps/api/src/modules/quotes/quotes.controller.ts
Normal file
31
apps/api/src/modules/quotes/quotes.controller.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
import { QuotesService } from './quotes.service';
|
||||
import { CreateQuoteDto } from './dto/create-quote.dto';
|
||||
|
||||
@ApiTags('quotes')
|
||||
@Controller('quotes')
|
||||
export class QuotesController {
|
||||
constructor(private readonly quotesService: QuotesService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create a new quote' })
|
||||
create(@Body() createQuoteDto: CreateQuoteDto) {
|
||||
const tenantId = 'mock-tenant-id';
|
||||
return this.quotesService.create(tenantId, createQuoteDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get all quotes' })
|
||||
findAll() {
|
||||
const tenantId = 'mock-tenant-id';
|
||||
return this.quotesService.findAll(tenantId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get quote by ID' })
|
||||
findOne(@Param('id') id: string) {
|
||||
const tenantId = 'mock-tenant-id';
|
||||
return this.quotesService.findOne(id, tenantId);
|
||||
}
|
||||
}
|
||||
10
apps/api/src/modules/quotes/quotes.module.ts
Normal file
10
apps/api/src/modules/quotes/quotes.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { QuotesController } from './quotes.controller';
|
||||
import { QuotesService } from './quotes.service';
|
||||
|
||||
@Module({
|
||||
controllers: [QuotesController],
|
||||
providers: [QuotesService],
|
||||
exports: [QuotesService],
|
||||
})
|
||||
export class QuotesModule {}
|
||||
49
apps/api/src/modules/quotes/quotes.service.ts
Normal file
49
apps/api/src/modules/quotes/quotes.service.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../../database/prisma.service';
|
||||
import { CreateQuoteDto } from './dto/create-quote.dto';
|
||||
|
||||
@Injectable()
|
||||
export class QuotesService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async create(tenantId: string, data: CreateQuoteDto) {
|
||||
// Simple pricing calculation - in production, use pricing engine
|
||||
const monthlyRecurring = data.bandwidthMbps * 0.05; // $0.05 per Mbps
|
||||
const setupFee = 500; // Flat setup fee
|
||||
const validUntil = new Date();
|
||||
validUntil.setDate(validUntil.getDate() + 30); // Valid for 30 days
|
||||
|
||||
return this.prisma.quote.create({
|
||||
data: {
|
||||
tenantId,
|
||||
productOfferingId: data.productOfferingId,
|
||||
sourceEndpointId: data.sourceEndpointId,
|
||||
targetEndpointId: data.targetEndpointId,
|
||||
bandwidthMbps: data.bandwidthMbps,
|
||||
monthlyRecurring,
|
||||
setupFee,
|
||||
validUntil,
|
||||
status: 'valid',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findAll(tenantId: string) {
|
||||
return this.prisma.quote.findMany({
|
||||
where: { tenantId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: string, tenantId: string) {
|
||||
const quote = await this.prisma.quote.findFirst({
|
||||
where: { id, tenantId },
|
||||
});
|
||||
|
||||
if (!quote) {
|
||||
throw new NotFoundException(`Quote ${id} not found`);
|
||||
}
|
||||
|
||||
return quote;
|
||||
}
|
||||
}
|
||||
38
apps/api/src/modules/services/services.controller.ts
Normal file
38
apps/api/src/modules/services/services.controller.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Controller, Get, Post, Param, Query } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiQuery } from '@nestjs/swagger';
|
||||
import { ServicesService } from './services.service';
|
||||
|
||||
@ApiTags('services')
|
||||
@Controller('services')
|
||||
export class ServicesController {
|
||||
constructor(private readonly servicesService: ServicesService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get all services' })
|
||||
@ApiQuery({ name: 'status', required: false })
|
||||
findAll(@Query('status') status?: string) {
|
||||
const tenantId = 'mock-tenant-id';
|
||||
return this.servicesService.findAll(tenantId, { status });
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get service by ID' })
|
||||
findOne(@Param('id') id: string) {
|
||||
const tenantId = 'mock-tenant-id';
|
||||
return this.servicesService.findOne(id, tenantId);
|
||||
}
|
||||
|
||||
@Post(':id/suspend')
|
||||
@ApiOperation({ summary: 'Suspend a service' })
|
||||
suspend(@Param('id') id: string) {
|
||||
const tenantId = 'mock-tenant-id';
|
||||
return this.servicesService.suspend(id, tenantId);
|
||||
}
|
||||
|
||||
@Post(':id/terminate')
|
||||
@ApiOperation({ summary: 'Terminate a service' })
|
||||
terminate(@Param('id') id: string) {
|
||||
const tenantId = 'mock-tenant-id';
|
||||
return this.servicesService.terminate(id, tenantId);
|
||||
}
|
||||
}
|
||||
10
apps/api/src/modules/services/services.module.ts
Normal file
10
apps/api/src/modules/services/services.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ServicesController } from './services.controller';
|
||||
import { ServicesService } from './services.service';
|
||||
|
||||
@Module({
|
||||
controllers: [ServicesController],
|
||||
providers: [ServicesService],
|
||||
exports: [ServicesService],
|
||||
})
|
||||
export class ServicesModule {}
|
||||
76
apps/api/src/modules/services/services.service.ts
Normal file
76
apps/api/src/modules/services/services.service.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../../database/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class ServicesService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async findAll(tenantId: string, filters?: { status?: string }) {
|
||||
return this.prisma.service.findMany({
|
||||
where: {
|
||||
tenantId,
|
||||
...(filters?.status && { status: filters.status }),
|
||||
},
|
||||
include: {
|
||||
order: {
|
||||
include: {
|
||||
sourceEndpoint: true,
|
||||
targetEndpoint: true,
|
||||
productOffering: true,
|
||||
},
|
||||
},
|
||||
usageRecords: { take: 10, orderBy: { createdAt: 'desc' } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: string, tenantId: string) {
|
||||
const service = await this.prisma.service.findFirst({
|
||||
where: { id, tenantId },
|
||||
include: {
|
||||
order: {
|
||||
include: {
|
||||
sourceEndpoint: true,
|
||||
targetEndpoint: true,
|
||||
productOffering: true,
|
||||
provider: true,
|
||||
provisioningTasks: { orderBy: { createdAt: 'desc' } },
|
||||
},
|
||||
},
|
||||
usageRecords: { orderBy: { createdAt: 'desc' } },
|
||||
inventoryRecords: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!service) {
|
||||
throw new NotFoundException(`Service ${id} not found`);
|
||||
}
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
async suspend(id: string, tenantId: string) {
|
||||
const service = await this.findOne(id, tenantId);
|
||||
|
||||
return this.prisma.service.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'suspended',
|
||||
suspendedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async terminate(id: string, tenantId: string) {
|
||||
const service = await this.findOne(id, tenantId);
|
||||
|
||||
return this.prisma.service.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'terminated',
|
||||
terminatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
17
apps/api/src/modules/tenants/dto/create-tenant.dto.ts
Normal file
17
apps/api/src/modules/tenants/dto/create-tenant.dto.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { IsString, IsNotEmpty, Matches } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CreateTenantDto {
|
||||
@ApiProperty({ example: 'Acme Corporation' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ example: 'acme-corp' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Matches(/^[a-z0-9-]+$/, {
|
||||
message: 'Slug must contain only lowercase letters, numbers, and hyphens',
|
||||
})
|
||||
slug: string;
|
||||
}
|
||||
34
apps/api/src/modules/tenants/tenants.controller.ts
Normal file
34
apps/api/src/modules/tenants/tenants.controller.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Controller, Get, Post, Body, Param, Patch } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
import { TenantsService } from './tenants.service';
|
||||
import { CreateTenantDto } from './dto/create-tenant.dto';
|
||||
|
||||
@ApiTags('tenants')
|
||||
@Controller('tenants')
|
||||
export class TenantsController {
|
||||
constructor(private readonly tenantsService: TenantsService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create a new tenant' })
|
||||
create(@Body() createTenantDto: CreateTenantDto) {
|
||||
return this.tenantsService.create(createTenantDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get all tenants' })
|
||||
findAll() {
|
||||
return this.tenantsService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get tenant by ID' })
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.tenantsService.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@ApiOperation({ summary: 'Update tenant' })
|
||||
update(@Param('id') id: string, @Body() updateTenantDto: Partial<CreateTenantDto>) {
|
||||
return this.tenantsService.update(id, updateTenantDto);
|
||||
}
|
||||
}
|
||||
10
apps/api/src/modules/tenants/tenants.module.ts
Normal file
10
apps/api/src/modules/tenants/tenants.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TenantsController } from './tenants.controller';
|
||||
import { TenantsService } from './tenants.service';
|
||||
|
||||
@Module({
|
||||
controllers: [TenantsController],
|
||||
providers: [TenantsService],
|
||||
exports: [TenantsService],
|
||||
})
|
||||
export class TenantsModule {}
|
||||
52
apps/api/src/modules/tenants/tenants.service.ts
Normal file
52
apps/api/src/modules/tenants/tenants.service.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../../database/prisma.service';
|
||||
import { CreateTenantDto } from './dto/create-tenant.dto';
|
||||
|
||||
@Injectable()
|
||||
export class TenantsService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async create(data: CreateTenantDto) {
|
||||
return this.prisma.tenant.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
slug: data.slug,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findAll() {
|
||||
return this.prisma.tenant.findMany({
|
||||
include: {
|
||||
_count: {
|
||||
select: { users: true, orders: true, services: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
const tenant = await this.prisma.tenant.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
users: true,
|
||||
orders: { take: 10, orderBy: { createdAt: 'desc' } },
|
||||
services: { take: 10, orderBy: { createdAt: 'desc' } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!tenant) {
|
||||
throw new NotFoundException(`Tenant ${id} not found`);
|
||||
}
|
||||
|
||||
return tenant;
|
||||
}
|
||||
|
||||
async update(id: string, data: Partial<CreateTenantDto>) {
|
||||
return this.prisma.tenant.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
23
apps/api/src/modules/users/users.controller.ts
Normal file
23
apps/api/src/modules/users/users.controller.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Controller, Get, Param } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
@ApiTags('users')
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get all users' })
|
||||
findAll() {
|
||||
const tenantId = 'mock-tenant-id';
|
||||
return this.usersService.findAll(tenantId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get user by ID' })
|
||||
findOne(@Param('id') id: string) {
|
||||
const tenantId = 'mock-tenant-id';
|
||||
return this.usersService.findOne(id, tenantId);
|
||||
}
|
||||
}
|
||||
10
apps/api/src/modules/users/users.module.ts
Normal file
10
apps/api/src/modules/users/users.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UsersController } from './users.controller';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
@Module({
|
||||
controllers: [UsersController],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService],
|
||||
})
|
||||
export class UsersModule {}
|
||||
21
apps/api/src/modules/users/users.service.ts
Normal file
21
apps/api/src/modules/users/users.service.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../../database/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async findAll(tenantId: string) {
|
||||
return this.prisma.user.findMany({
|
||||
where: { tenantId },
|
||||
include: { roles: true },
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: string, tenantId: string) {
|
||||
return this.prisma.user.findFirst({
|
||||
where: { id, tenantId },
|
||||
include: { roles: true, orders: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
28
apps/api/tsconfig.json
Normal file
28
apps/api/tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": false,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"paths": {
|
||||
"@sdi/database": ["../../packages/database/src"],
|
||||
"@sdi/shared-types": ["../../packages/shared-types/src"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "test"]
|
||||
}
|
||||
Reference in New Issue
Block a user