feat(opportunities): add opportunities.service
This commit is contained in:
parent
0243e78fea
commit
80fd3e0b4e
1 changed files with 92 additions and 0 deletions
92
src/opportunities/opportunities.service.ts
Normal file
92
src/opportunities/opportunities.service.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue