Skip to content

Commit fb7f8ea

Browse files
Merge pull request #485 from Breedar/assetsService
created the entire assets service methods
2 parents f3a9db6 + 3fa70ab commit fb7f8ea

File tree

1 file changed

+398
-0
lines changed

1 file changed

+398
-0
lines changed
Lines changed: 398 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,398 @@
1+
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
2+
import { InjectRepository } from '@nestjs/typeorm';
3+
import { Repository } from 'typeorm';
4+
import { ConfigService } from '@nestjs/config';
5+
import { Asset } from './asset.entity';
6+
import { AssetHistory } from './asset-history.entity';
7+
import { AssetNote } from './asset-note.entity';
8+
import { Maintenance, MaintenanceStatus } from './maintenance.entity';
9+
import { AssetDocument } from './asset-document.entity';
10+
import { CreateAssetDto } from './dto/create-asset.dto';
11+
import { UpdateAssetDto } from './dto/update-asset.dto';
12+
import { AssetFiltersDto } from './dto/asset-filters.dto';
13+
import { TransferAssetDto } from './dto/transfer-asset.dto';
14+
import { UpdateStatusDto } from './dto/update-status.dto';
15+
import { CreateNoteDto } from './dto/create-note.dto';
16+
import { CreateMaintenanceDto } from './dto/create-maintenance.dto';
17+
import { UpdateMaintenanceDto } from './dto/update-maintenance.dto';
18+
import { CreateDocumentDto } from './dto/create-document.dto';
19+
import { AssetStatus, AssetHistoryAction, StellarStatus } from './enums';
20+
import { DepartmentsService } from '../departments/departments.service';
21+
import { CategoriesService } from '../categories/categories.service';
22+
import { UsersService } from '../users/users.service';
23+
import { StellarService } from '../stellar/stellar.service';
24+
import { User } from '../users/user.entity';
25+
26+
@Injectable()
27+
export class AssetsService {
28+
private readonly logger = new Logger(AssetsService.name);
29+
30+
constructor(
31+
@InjectRepository(Asset)
32+
private readonly assetsRepo: Repository<Asset>,
33+
@InjectRepository(AssetHistory)
34+
private readonly historyRepo: Repository<AssetHistory>,
35+
@InjectRepository(AssetNote)
36+
private readonly notesRepo: Repository<AssetNote>,
37+
@InjectRepository(Maintenance)
38+
private readonly maintenanceRepo: Repository<Maintenance>,
39+
@InjectRepository(AssetDocument)
40+
private readonly documentsRepo: Repository<AssetDocument>,
41+
private readonly departmentsService: DepartmentsService,
42+
private readonly categoriesService: CategoriesService,
43+
private readonly usersService: UsersService,
44+
private readonly configService: ConfigService,
45+
private readonly stellarService: StellarService,
46+
) {}
47+
48+
async findAll(filters: AssetFiltersDto): Promise<{ data: Asset[]; total: number; page: number; limit: number }> {
49+
const { search, status, condition, categoryId, departmentId, page = 1, limit = 20 } = filters;
50+
51+
const qb = this.assetsRepo
52+
.createQueryBuilder('asset')
53+
.leftJoinAndSelect('asset.category', 'category')
54+
.leftJoinAndSelect('asset.department', 'department')
55+
.leftJoinAndSelect('asset.assignedTo', 'assignedTo')
56+
.leftJoinAndSelect('asset.createdBy', 'createdBy')
57+
.leftJoinAndSelect('asset.updatedBy', 'updatedBy');
58+
59+
if (search) {
60+
qb.andWhere(
61+
'(asset.name ILIKE :search OR asset.assetId ILIKE :search OR asset.serialNumber ILIKE :search OR asset.manufacturer ILIKE :search OR asset.model ILIKE :search)',
62+
{ search: `%${search}%` },
63+
);
64+
}
65+
if (status) qb.andWhere('asset.status = :status', { status });
66+
if (condition) qb.andWhere('asset.condition = :condition', { condition });
67+
if (categoryId) qb.andWhere('category.id = :categoryId', { categoryId });
68+
if (departmentId) qb.andWhere('department.id = :departmentId', { departmentId });
69+
70+
qb.orderBy('asset.createdAt', 'DESC')
71+
.skip((page - 1) * limit)
72+
.take(limit);
73+
74+
const [data, total] = await qb.getManyAndCount();
75+
return { data, total, page, limit };
76+
}
77+
78+
async findOne(id: string): Promise<Asset> {
79+
const asset = await this.assetsRepo
80+
.createQueryBuilder('asset')
81+
.leftJoinAndSelect('asset.category', 'category')
82+
.leftJoinAndSelect('asset.department', 'department')
83+
.leftJoinAndSelect('asset.assignedTo', 'assignedTo')
84+
.leftJoinAndSelect('asset.createdBy', 'createdBy')
85+
.leftJoinAndSelect('asset.updatedBy', 'updatedBy')
86+
.where('asset.id = :id', { id })
87+
.getOne();
88+
89+
if (!asset) throw new NotFoundException('Asset not found');
90+
return asset;
91+
}
92+
93+
async create(dto: CreateAssetDto, currentUser: User): Promise<Asset> {
94+
const category = await this.categoriesService.findOne(dto.categoryId);
95+
const department = await this.departmentsService.findOne(dto.departmentId);
96+
const assignedTo = dto.assignedToId
97+
? await this.usersService.findById(dto.assignedToId)
98+
: null;
99+
100+
const assetId = await this.generateAssetId();
101+
102+
const asset = this.assetsRepo.create({
103+
assetId,
104+
name: dto.name,
105+
description: dto.description ?? null,
106+
category,
107+
department,
108+
assignedTo,
109+
serialNumber: dto.serialNumber ?? null,
110+
purchaseDate: dto.purchaseDate ? new Date(dto.purchaseDate) : null,
111+
purchasePrice: dto.purchasePrice ?? null,
112+
currentValue: dto.currentValue ?? null,
113+
warrantyExpiration: dto.warrantyExpiration ? new Date(dto.warrantyExpiration) : null,
114+
status: dto.status ?? AssetStatus.ACTIVE,
115+
condition: dto.condition,
116+
location: dto.location ?? null,
117+
manufacturer: dto.manufacturer ?? null,
118+
model: dto.model ?? null,
119+
tags: dto.tags ?? null,
120+
notes: dto.notes ?? null,
121+
createdBy: currentUser,
122+
updatedBy: currentUser,
123+
});
124+
125+
const saved = await this.assetsRepo.save(asset);
126+
127+
await this.logHistory(saved, AssetHistoryAction.CREATED, 'Asset registered', null, null, currentUser);
128+
129+
// Derive on-chain ID deterministically and mark PENDING (only if Stellar enabled)
130+
if (this.stellarService.isEnabled) {
131+
const { hex: stellarAssetId } = this.stellarService.deriveAssetId(saved.id);
132+
await this.assetsRepo.update(saved.id, {
133+
stellarAssetId,
134+
stellarStatus: StellarStatus.PENDING,
135+
});
136+
137+
// Fire-and-forget: non-blocking
138+
this.registerOnChain(saved).catch((err: Error) => {
139+
this.logger.error(
140+
`On-chain registration failed for asset ${saved.assetId}: ${err.message}`,
141+
err.stack,
142+
);
143+
});
144+
}
145+
146+
return this.findOne(saved.id);
147+
}
148+
149+
async update(id: string, dto: UpdateAssetDto, currentUser: User): Promise<Asset> {
150+
const asset = await this.findOne(id);
151+
const before = { ...asset };
152+
153+
if (dto.categoryId) asset.category = await this.categoriesService.findOne(dto.categoryId);
154+
if (dto.departmentId) asset.department = await this.departmentsService.findOne(dto.departmentId);
155+
if (dto.assignedToId !== undefined) {
156+
asset.assignedTo = dto.assignedToId ? await this.usersService.findById(dto.assignedToId) : null;
157+
}
158+
159+
const fields: (keyof UpdateAssetDto)[] = [
160+
'name', 'description', 'serialNumber', 'purchasePrice', 'currentValue',
161+
'status', 'condition', 'location', 'manufacturer', 'model', 'tags', 'notes',
162+
];
163+
for (const field of fields) {
164+
if (dto[field] !== undefined) {
165+
(asset as unknown as Record<string, unknown>)[field] = dto[field] as unknown;
166+
}
167+
}
168+
if (dto.purchaseDate !== undefined) asset.purchaseDate = dto.purchaseDate ? new Date(dto.purchaseDate) : null;
169+
if (dto.warrantyExpiration !== undefined) asset.warrantyExpiration = dto.warrantyExpiration ? new Date(dto.warrantyExpiration) : null;
170+
171+
asset.updatedBy = currentUser;
172+
173+
await this.assetsRepo.save(asset);
174+
await this.logHistory(asset, AssetHistoryAction.UPDATED, 'Asset updated', before as unknown as Record<string, unknown>, dto as unknown as Record<string, unknown>, currentUser);
175+
176+
return this.findOne(id);
177+
}
178+
179+
async updateStatus(id: string, dto: UpdateStatusDto, currentUser: User): Promise<Asset> {
180+
const asset = await this.findOne(id);
181+
const prevStatus = asset.status;
182+
183+
asset.status = dto.status;
184+
asset.updatedBy = currentUser;
185+
await this.assetsRepo.save(asset);
186+
187+
await this.logHistory(
188+
asset,
189+
AssetHistoryAction.STATUS_CHANGED,
190+
`Status changed from ${prevStatus} to ${dto.status}`,
191+
{ status: prevStatus },
192+
{ status: dto.status },
193+
currentUser,
194+
);
195+
196+
return this.findOne(id);
197+
}
198+
199+
async transfer(id: string, dto: TransferAssetDto, currentUser: User): Promise<Asset> {
200+
const asset = await this.findOne(id);
201+
const prevDept = asset.department?.name;
202+
203+
asset.department = await this.departmentsService.findOne(dto.departmentId);
204+
asset.assignedTo = dto.assignedToId ? await this.usersService.findById(dto.assignedToId) : null;
205+
if (dto.location !== undefined) asset.location = dto.location ?? null;
206+
asset.status = AssetStatus.ASSIGNED;
207+
asset.updatedBy = currentUser;
208+
209+
await this.assetsRepo.save(asset);
210+
211+
await this.logHistory(
212+
asset,
213+
AssetHistoryAction.TRANSFERRED,
214+
`Asset transferred from ${prevDept} to ${asset.department.name}${dto.notes ? '. ' + dto.notes : ''}`,
215+
{ departmentId: prevDept },
216+
{ departmentId: asset.department.name },
217+
currentUser,
218+
);
219+
220+
return this.findOne(id);
221+
}
222+
223+
async remove(id: string): Promise<void> {
224+
const asset = await this.findOne(id);
225+
await this.assetsRepo.remove(asset);
226+
}
227+
228+
async getHistory(assetId: string): Promise<AssetHistory[]> {
229+
await this.findOne(assetId);
230+
return this.historyRepo.find({
231+
where: { assetId },
232+
relations: ['performedBy'],
233+
order: { createdAt: 'DESC' },
234+
});
235+
}
236+
237+
// ── Notes ─────────────────────────────────────────────────────
238+
239+
async getNotes(assetId: string): Promise<AssetNote[]> {
240+
await this.findOne(assetId);
241+
return this.notesRepo.find({
242+
where: { assetId },
243+
order: { createdAt: 'DESC' },
244+
});
245+
}
246+
247+
async createNote(assetId: string, dto: CreateNoteDto, currentUser: User): Promise<AssetNote> {
248+
await this.findOne(assetId);
249+
const note = this.notesRepo.create({
250+
assetId,
251+
content: dto.content,
252+
createdBy: currentUser,
253+
});
254+
const saved = await this.notesRepo.save(note);
255+
await this.logHistory(
256+
{ id: assetId } as Asset,
257+
AssetHistoryAction.NOTE_ADDED,
258+
'Note added',
259+
null,
260+
{ content: dto.content },
261+
currentUser,
262+
);
263+
return saved;
264+
}
265+
266+
async deleteNote(assetId: string, noteId: string): Promise<void> {
267+
const note = await this.notesRepo.findOne({ where: { id: noteId, assetId } });
268+
if (!note) throw new NotFoundException('Note not found');
269+
await this.notesRepo.remove(note);
270+
}
271+
272+
// ── Maintenance ───────────────────────────────────────────────
273+
274+
async getMaintenance(assetId: string): Promise<Maintenance[]> {
275+
await this.findOne(assetId);
276+
return this.maintenanceRepo.find({
277+
where: { assetId },
278+
order: { scheduledDate: 'DESC' },
279+
});
280+
}
281+
282+
async createMaintenance(assetId: string, dto: CreateMaintenanceDto, currentUser: User): Promise<Maintenance> {
283+
await this.findOne(assetId);
284+
const record = this.maintenanceRepo.create({
285+
assetId,
286+
type: dto.type,
287+
description: dto.description,
288+
scheduledDate: new Date(dto.scheduledDate),
289+
cost: dto.cost ?? null,
290+
notes: dto.notes ?? null,
291+
performedBy: currentUser,
292+
});
293+
const saved = await this.maintenanceRepo.save(record);
294+
await this.logHistory(
295+
{ id: assetId } as Asset,
296+
AssetHistoryAction.MAINTENANCE,
297+
`Maintenance scheduled: ${dto.description}`,
298+
null,
299+
{ type: dto.type, scheduledDate: dto.scheduledDate },
300+
currentUser,
301+
);
302+
return saved;
303+
}
304+
305+
async updateMaintenance(assetId: string, maintenanceId: string, dto: UpdateMaintenanceDto): Promise<Maintenance> {
306+
const record = await this.maintenanceRepo.findOne({ where: { id: maintenanceId, assetId } });
307+
if (!record) throw new NotFoundException('Maintenance record not found');
308+
309+
if (dto.status !== undefined) record.status = dto.status;
310+
if (dto.completedDate !== undefined) record.completedDate = dto.completedDate ? new Date(dto.completedDate) : null;
311+
if (dto.cost !== undefined) record.cost = dto.cost ?? null;
312+
if (dto.notes !== undefined) record.notes = dto.notes ?? null;
313+
314+
if (dto.status === MaintenanceStatus.COMPLETED && !record.completedDate) {
315+
record.completedDate = new Date();
316+
}
317+
318+
return this.maintenanceRepo.save(record);
319+
}
320+
321+
// ── Documents ─────────────────────────────────────────────────
322+
323+
async getDocuments(assetId: string): Promise<AssetDocument[]> {
324+
await this.findOne(assetId);
325+
return this.documentsRepo.find({
326+
where: { assetId },
327+
order: { createdAt: 'DESC' },
328+
});
329+
}
330+
331+
async addDocument(assetId: string, dto: CreateDocumentDto, currentUser: User): Promise<AssetDocument> {
332+
await this.findOne(assetId);
333+
const doc = this.documentsRepo.create({
334+
assetId,
335+
name: dto.name,
336+
url: dto.url,
337+
type: dto.type ?? 'application/octet-stream',
338+
size: dto.size ?? null,
339+
uploadedBy: currentUser,
340+
});
341+
const saved = await this.documentsRepo.save(doc);
342+
await this.logHistory(
343+
{ id: assetId } as Asset,
344+
AssetHistoryAction.DOCUMENT_UPLOADED,
345+
`Document added: ${dto.name}`,
346+
null,
347+
{ name: dto.name, url: dto.url },
348+
currentUser,
349+
);
350+
return saved;
351+
}
352+
353+
async deleteDocument(assetId: string, documentId: string): Promise<void> {
354+
const doc = await this.documentsRepo.findOne({ where: { id: documentId, assetId } });
355+
if (!doc) throw new NotFoundException('Document not found');
356+
await this.documentsRepo.remove(doc);
357+
}
358+
359+
private async registerOnChain(asset: Asset): Promise<void> {
360+
try {
361+
const txHash = await this.stellarService.registerAsset(asset);
362+
await this.assetsRepo.update(asset.id, {
363+
stellarTxHash: txHash,
364+
stellarStatus: StellarStatus.CONFIRMED,
365+
});
366+
this.logger.log(`Asset ${asset.assetId} anchored on Stellar. Tx: ${txHash}`);
367+
} catch (err) {
368+
await this.assetsRepo.update(asset.id, { stellarStatus: StellarStatus.FAILED });
369+
throw err;
370+
}
371+
}
372+
373+
private async generateAssetId(): Promise<string> {
374+
const prefix = this.configService.get<string>('ASSET_ID_PREFIX', 'AST');
375+
const count = await this.assetsRepo.count();
376+
const start = Number(this.configService.get('ASSET_ID_START', 1000));
377+
return `${prefix}-${start + count + 1}`;
378+
}
379+
380+
private async logHistory(
381+
asset: Asset,
382+
action: AssetHistoryAction,
383+
description: string,
384+
previousValue: Record<string, unknown> | null,
385+
newValue: Record<string, unknown> | null,
386+
performedBy: User,
387+
): Promise<void> {
388+
const entry = this.historyRepo.create({
389+
assetId: asset.id,
390+
action,
391+
description,
392+
previousValue,
393+
newValue,
394+
performedBy,
395+
});
396+
await this.historyRepo.save(entry);
397+
}
398+
}

0 commit comments

Comments
 (0)