mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-09 13:11:54 +00:00
- migration : Asset.underContract + WorkOrder.periodKey avec unicité [assetId, periodKey] — l'idempotence de la génération est EN BASE (deux appels concurrents ne doublent jamais une grille) - génération : une grille par appareil sous contrat, périodicités ancrées sur la mise en service (mensuelles toujours dues), premier contrôle (toutes les tâches) pour un appareil sans historique, échéance fin de mois, événement GENERATED ; déclenchement manuel (cron au durcissement) - gabarits administrables (désactivé = exclu des générations suivantes) ; GET /preventive/status (générées, terminées, en retard, premiers contrôles) - compteurs : GET meters (2 compteurs, récents d'abord) ; relevé strictement croissant, refus motivé sinon - contrat +7 opérations (48) ; 50 tests verts (94,7 % / 78 %) ; smoke test prod : juillet généré (9 grilles, 8 premiers contrôles), 2e appel à zéro Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
78 lines
2.4 KiB
TypeScript
78 lines
2.4 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
Injectable,
|
|
NotFoundException,
|
|
} from '@nestjs/common';
|
|
import { METER_KINDS, type MeterReadingCreate, type MetersResponse } from '@siop/shared';
|
|
import type { AuthenticatedUser } from '../auth/current-user.decorator';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
|
|
@Injectable()
|
|
export class MetersService {
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
async list(assetId: string): Promise<MetersResponse> {
|
|
await this.assertAsset(assetId);
|
|
const meters = await this.prisma.meter.findMany({
|
|
where: { assetId },
|
|
include: {
|
|
readings: {
|
|
include: { readBy: true },
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 10,
|
|
},
|
|
},
|
|
});
|
|
// Les deux compteurs existent toujours dans la réponse, même vides
|
|
return {
|
|
meters: METER_KINDS.map((kind) => {
|
|
const meter = meters.find((m) => m.kind === kind);
|
|
return {
|
|
kind,
|
|
readings:
|
|
meter?.readings.map((r) => ({
|
|
id: r.id,
|
|
value: r.value,
|
|
readBy: r.readBy
|
|
? { id: r.readBy.id, displayName: r.readBy.displayName }
|
|
: null,
|
|
createdAt: r.createdAt.toISOString(),
|
|
})) ?? [],
|
|
};
|
|
}),
|
|
};
|
|
}
|
|
|
|
/** Invariant : un compteur ne redescend jamais. */
|
|
async addReading(
|
|
assetId: string,
|
|
dto: MeterReadingCreate,
|
|
user: AuthenticatedUser,
|
|
): Promise<MetersResponse> {
|
|
await this.assertAsset(assetId);
|
|
const meter = await this.prisma.meter.upsert({
|
|
where: { assetId_kind: { assetId, kind: dto.kind } },
|
|
update: {},
|
|
create: { assetId, kind: dto.kind },
|
|
});
|
|
const dernier = await this.prisma.meterReading.findFirst({
|
|
where: { meterId: meter.id },
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
if (dernier && dto.value <= dernier.value) {
|
|
throw new BadRequestException(
|
|
`Relevé refusé : ${dto.value} est inférieur ou égal au précédent (${dernier.value}) — un compteur ne redescend pas`,
|
|
);
|
|
}
|
|
await this.prisma.meterReading.create({
|
|
data: { meterId: meter.id, value: dto.value, readById: user.userId },
|
|
});
|
|
return this.list(assetId);
|
|
}
|
|
|
|
private async assertAsset(assetId: string): Promise<void> {
|
|
const asset = await this.prisma.asset.findUnique({ where: { id: assetId } });
|
|
if (!asset) throw new NotFoundException('Appareil inconnu');
|
|
}
|
|
}
|