feat(opportunities): add opportunities.service

This commit is contained in:
admin-valentin 2026-07-31 10:45:04 +00:00
parent 0243e78fea
commit 80fd3e0b4e

View file

@ -0,0 +1,92 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { and, desc, eq } from 'drizzle-orm';
import { db } from '../db/client';
import { opportunities } from '../db/schema';
import { AuditService } from '../audit/audit.service';
import { OutboxService } from '../events/outbox.service';
import type { AuthenticatedSession } from '../auth/tenant.guard';
import type { CreateOpportunityDto, UpdateOpportunityDto } from './dto';
@Injectable()
export class OpportunitiesService {
constructor(
private readonly outbox: OutboxService,
private readonly audit: AuditService,
) {}
async list(session: AuthenticatedSession) {
return db.query.opportunities.findMany({
where: eq(opportunities.tenantId, session.tenantId),
orderBy: desc(opportunities.createdAt),
});
}
async getById(session: AuthenticatedSession, id: string) {
const row = await db.query.opportunities.findFirst({
where: and(eq(opportunities.id, id), eq(opportunities.tenantId, session.tenantId)),
});
if (!row) throw new NotFoundException('Opportunity not found');
return row;
}
async create(session: AuthenticatedSession, dto: CreateOpportunityDto) {
return this.outbox.withTransaction(async (tx) => {
const [row] = await tx
.insert(opportunities)
.values({
tenantId: session.tenantId,
source: dto.source,
entityType: dto.entityType,
entityId: dto.entityId,
valueRangeMin: dto.valueRangeMin !== undefined ? String(Math.round(dto.valueRangeMin)) : undefined,
valueRangeMax: dto.valueRangeMax !== undefined ? String(Math.round(dto.valueRangeMax)) : undefined,
probability: dto.probability !== undefined ? String(dto.probability) : undefined,
nextAction: dto.nextAction,
expiresAt: dto.expiresAt ? new Date(dto.expiresAt) : undefined,
})
.returning();
await this.audit.record(tx, {
tenantId: session.tenantId,
actorId: session.userId,
action: 'opportunity.created',
resource: `opportunity:${row.id}`,
});
await this.outbox.record(tx, {
tenantId: session.tenantId,
workspaceId: session.workspaceId,
actorId: session.userId,
eventType: 'opportunity.created',
aggregateType: 'opportunity',
subjectId: row.id,
correlationId: session.correlationId,
payload: { source: row.source, entityType: row.entityType },
});
return row;
});
}
async update(session: AuthenticatedSession, id: string, dto: UpdateOpportunityDto) {
await this.getById(session, id);
return this.outbox.withTransaction(async (tx) => {
const [row] = await tx
.update(opportunities)
.set({
...(dto.probability !== undefined ? { probability: String(dto.probability) } : {}),
...(dto.nextAction !== undefined ? { nextAction: dto.nextAction } : {}),
...(dto.expiresAt !== undefined ? { expiresAt: new Date(dto.expiresAt) } : {}),
...(dto.valueRangeMin !== undefined ? { valueRangeMin: String(Math.round(dto.valueRangeMin)) } : {}),
...(dto.valueRangeMax !== undefined ? { valueRangeMax: String(Math.round(dto.valueRangeMax)) } : {}),
updatedAt: new Date(),
})
.where(and(eq(opportunities.id, id), eq(opportunities.tenantId, session.tenantId)))
.returning();
await this.audit.record(tx, {
tenantId: session.tenantId,
actorId: session.userId,
action: 'opportunity.updated',
resource: `opportunity:${id}`,
});
return row;
});
}
}