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
69 changes: 43 additions & 26 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';
import { EmailModule } from './email/email.module';
import { NewsletterModule } from './newsletter/newsletter.module';
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './auth/guards/jwt.guard';
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';

import { BadgesModule } from './badges/badges.module';
import { InternetSpeedModule } from './internet-speed/internet-speed.module';
import { LibraryService } from './library/library.service';
import { LibraryModule } from './library/library-module.module';
import { BusinessesModule } from './businesses/businesses.module';
import { PollsModule } from './polls/polls.module';
import { LeaveModule } from './leave/leave.module';
import { InternalNotesModule } from './internal-notes/internal-notes.module';
import { SystemStatsModule } from './system-stats/system-stats.module';
import { ServiceVendorVisitModule } from './service-vendor-visit/service-vendor-visit.module';
import { BackupsModule } from './backups/backups.module';
import { EnvironmentMonitorModule } from './environment-monitor/environment-monitor.module';
import { LeaderboardModule } from './leaderboard/leaderboard.module';
import { BroadcastModule } from './broadcast/broadcast.module';
import { ScheduleModule } from '@nestjs/schedule';
import { MonitoringModule } from './monitoring/monitoring.module';
import { SettingsModule } from './settings/settings.module';
import { ParkingModule } from './parking/parking.module';
import { SurveysModule } from './surveys/surveys.module';
import { PartnersModule } from './partners/partners.module';
import { DonationsModule } from 'donations/donations.module';
import { InventoryMovementsModule } from './inventory-movements/inventory-movements.module';

@Module({
imports: [
Expand Down Expand Up @@ -44,23 +58,26 @@ import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
configService.get<string>('DATABASE_SSL') === 'true' ||
(host ? host.includes('neon.tech') : false);

return {
type: 'postgres',
database: configService.get('DATABASE_NAME'),
password: configService.get('DATABASE_PASSWORD'),
username: configService.get('DATABASE_USERNAME'),
port: +configService.get('DATABASE_PORT'),
host,
autoLoadEntities: true,
synchronize: true,
ssl: sslRequired ? { rejectUnauthorized: false } : false,
};
},
}),
AuthModule,
UsersModule,
EmailModule,
NewsletterModule,
BadgesModule,
InternetSpeedModule,
LibraryModule,
BusinessesModule,
PollsModule,
LeaveModule,
InternalNotesModule,
SystemStatsModule,
ServiceVendorVisitModule,
BackupsModule,
EnvironmentMonitorModule,
LeaderboardModule,
BroadcastModule,
MonitoringModule,
SettingsModule,
ParkingModule,
SurveysModule,
PartnersModule,
DonationsModule,
InventoryMovementsModule
],
controllers: [AppController],
providers: [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsInt, IsNotEmpty, IsUUID, Min } from 'class-validator';
import { MovementType } from '../entities/inventory-movement.entity';

export class CreateInventoryMovementDto {
@ApiProperty()
@IsUUID()
itemId: string;

@ApiProperty({ enum: MovementType })
@IsEnum(MovementType)
type: MovementType;

@ApiProperty()
@IsInt()
@Min(1)
quantity: number;

@ApiProperty()
@IsNotEmpty()
initiatedBy: string;
}
17 changes: 17 additions & 0 deletions backend/src/inventory-movements/entities/inventory-item.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
import { InventoryMovement } from './inventory-movement.entity';

@Entity('inventory_items')
export class InventoryItem {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
name: string;

@Column({ default: 0 })
stock: number;

@OneToMany(() => InventoryMovement, movement => movement.item)
movements: InventoryMovement[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn } from 'typeorm';
import { InventoryItem } from './inventory-item.entity';

export enum MovementType {
IN = 'IN',
OUT = 'OUT',
}

@Entity('inventory_movements')
export class InventoryMovement {
@PrimaryGeneratedColumn('uuid')
id: string;

@ManyToOne(() => InventoryItem, item => item.movements, { eager: true })
item: InventoryItem;

@Column({ type: 'enum', enum: MovementType })
type: MovementType;

@Column('int')
quantity: number;

@CreateDateColumn()
date: Date;

@Column()
initiatedBy: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { InventoryMovementsController } from './inventory-movements.controller';

describe('InventoryMovementsController', () => {
let controller: InventoryMovementsController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [InventoryMovementsController],
}).compile();

controller = module.get<InventoryMovementsController>(InventoryMovementsController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
24 changes: 24 additions & 0 deletions backend/src/inventory-movements/inventory-movements.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Controller, Post, Body, Get, Query } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { InventoryMovementsService } from './inventory-movements.service';
import { CreateInventoryMovementDto } from './dto/create-inventory-movement.dto';
import { InventoryMovement } from './entities/inventory-movement.entity';

@ApiTags('Inventory Movements')
@Controller('inventory-movements')
export class InventoryMovementsController {
constructor(private readonly service: InventoryMovementsService) {}

@Post()
@ApiOperation({ summary: 'Record stock IN/OUT movement' })
@ApiResponse({ status: 201, type: InventoryMovement })
create(@Body() dto: CreateInventoryMovementDto) {
return this.service.createMovement(dto);
}

@Get()
@ApiOperation({ summary: 'Get inventory movements (optionally by itemId)' })
findAll(@Query('itemId') itemId?: string) {
return this.service.findMovements(itemId);
}
}
13 changes: 13 additions & 0 deletions backend/src/inventory-movements/inventory-movements.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { InventoryMovementsController } from './inventory-movements.controller';
import { InventoryMovementsService } from './inventory-movements.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { InventoryItem } from './entities/inventory-item.entity';
import { InventoryMovement } from './entities/inventory-movement.entity';

@Module({
imports: [TypeOrmModule.forFeature([InventoryItem, InventoryMovement])],
controllers: [InventoryMovementsController],
providers: [InventoryMovementsService]
})
export class InventoryMovementsModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { InventoryMovementsService } from './inventory-movements.service';

describe('InventoryMovementsService', () => {
let service: InventoryMovementsService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [InventoryMovementsService],
}).compile();

service = module.get<InventoryMovementsService>(InventoryMovementsService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
51 changes: 51 additions & 0 deletions backend/src/inventory-movements/inventory-movements.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Injectable, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { InventoryItem } from './entities/inventory-item.entity';
import { InventoryMovement, MovementType } from './entities/inventory-movement.entity';
import { CreateInventoryMovementDto } from './dto/create-inventory-movement.dto';

@Injectable()
export class InventoryMovementsService {
constructor(
@InjectRepository(InventoryItem)
private readonly itemRepo: Repository<InventoryItem>,
@InjectRepository(InventoryMovement)
private readonly movementRepo: Repository<InventoryMovement>,
private readonly dataSource: DataSource,
) {}

async createMovement(dto: CreateInventoryMovementDto): Promise<InventoryMovement> {
return await this.dataSource.transaction(async manager => {
const item = await manager.findOne(InventoryItem, { where: { id: dto.itemId } });
if (!item) throw new BadRequestException('Item not found');

if (dto.type === MovementType.OUT && item.stock < dto.quantity) {
throw new BadRequestException('Insufficient stock');
}

// update stock
item.stock += dto.type === MovementType.IN ? dto.quantity : -dto.quantity;
await manager.save(item);

// record movement
const movement = manager.create(InventoryMovement, {
item,
type: dto.type,
quantity: dto.quantity,
initiatedBy: dto.initiatedBy,
});
return manager.save(movement);
});
}

async findMovements(itemId?: string): Promise<InventoryMovement[]> {
if (itemId) {
return this.movementRepo.find({
where: { item: { id: itemId } },
order: { date: 'DESC' },
});
}
return this.movementRepo.find({ order: { date: 'DESC' } });
}
}