feat(transactions): add transactions.service
This commit is contained in:
parent
844dd81af2
commit
a80bd5080b
1 changed files with 129 additions and 0 deletions
129
src/transactions/transactions.service.ts
Normal file
129
src/transactions/transactions.service.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { and, desc, eq, isNull, sql } from 'drizzle-orm';
|
||||||
|
import { db } from '../db/client';
|
||||||
|
import { transactions } from '../db/schema';
|
||||||
|
import { AuditService } from '../audit/audit.service';
|
||||||
|
import { OutboxService } from '../events/outbox.service';
|
||||||
|
import type { AuthenticatedSession } from '../auth/tenant.guard';
|
||||||
|
import type { CreateTransactionDto, UpdateTransactionDto } from './dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TransactionsService {
|
||||||
|
constructor(
|
||||||
|
private readonly outbox: OutboxService,
|
||||||
|
private readonly audit: AuditService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async list(session: AuthenticatedSession, organizationId?: string) {
|
||||||
|
return db.query.transactions.findMany({
|
||||||
|
where: and(
|
||||||
|
eq(transactions.tenantId, session.tenantId),
|
||||||
|
isNull(transactions.deletedAt),
|
||||||
|
...(organizationId ? [eq(transactions.organizationId, organizationId)] : []),
|
||||||
|
),
|
||||||
|
orderBy: desc(transactions.transactionDate),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getById(session: AuthenticatedSession, id: string) {
|
||||||
|
const row = await db.query.transactions.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(transactions.id, id),
|
||||||
|
eq(transactions.tenantId, session.tenantId),
|
||||||
|
isNull(transactions.deletedAt),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
if (!row) throw new NotFoundException('Transaction not found');
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(session: AuthenticatedSession, dto: CreateTransactionDto) {
|
||||||
|
return this.outbox.withTransaction(async (tx) => {
|
||||||
|
const [row] = await tx
|
||||||
|
.insert(transactions)
|
||||||
|
.values({
|
||||||
|
tenantId: session.tenantId,
|
||||||
|
organizationId: dto.organizationId,
|
||||||
|
type: dto.type,
|
||||||
|
amountMinorUnits: String(dto.amountMinorUnits),
|
||||||
|
currency: dto.currency.toUpperCase(),
|
||||||
|
transactionDate: new Date(dto.transactionDate),
|
||||||
|
evidenceStatus: dto.evidenceStatus ?? 'missing',
|
||||||
|
source: dto.source,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
await this.audit.record(tx, {
|
||||||
|
tenantId: session.tenantId,
|
||||||
|
actorId: session.userId,
|
||||||
|
action: 'transaction.created',
|
||||||
|
resource: `transaction:${row.id}`,
|
||||||
|
});
|
||||||
|
await this.outbox.record(tx, {
|
||||||
|
tenantId: session.tenantId,
|
||||||
|
workspaceId: session.workspaceId,
|
||||||
|
actorId: session.userId,
|
||||||
|
eventType: 'transaction.created',
|
||||||
|
aggregateType: 'transaction',
|
||||||
|
subjectId: row.id,
|
||||||
|
correlationId: session.correlationId,
|
||||||
|
payload: { type: row.type, currency: row.currency, evidenceStatus: row.evidenceStatus },
|
||||||
|
});
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(session: AuthenticatedSession, id: string, dto: UpdateTransactionDto) {
|
||||||
|
await this.getById(session, id);
|
||||||
|
return this.outbox.withTransaction(async (tx) => {
|
||||||
|
const [row] = await tx
|
||||||
|
.update(transactions)
|
||||||
|
.set({
|
||||||
|
...(dto.evidenceStatus !== undefined ? { evidenceStatus: dto.evidenceStatus } : {}),
|
||||||
|
...(dto.type !== undefined ? { type: dto.type } : {}),
|
||||||
|
...(dto.source !== undefined ? { source: dto.source } : {}),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
version: sql`${transactions.version} + 1`,
|
||||||
|
})
|
||||||
|
.where(and(eq(transactions.id, id), eq(transactions.tenantId, session.tenantId)))
|
||||||
|
.returning();
|
||||||
|
await this.audit.record(tx, {
|
||||||
|
tenantId: session.tenantId,
|
||||||
|
actorId: session.userId,
|
||||||
|
action: dto.evidenceStatus !== undefined
|
||||||
|
? `transaction.evidence_updated:${dto.evidenceStatus}`
|
||||||
|
: 'transaction.updated',
|
||||||
|
resource: `transaction:${id}`,
|
||||||
|
});
|
||||||
|
if (dto.evidenceStatus !== undefined) {
|
||||||
|
await this.outbox.record(tx, {
|
||||||
|
tenantId: session.tenantId,
|
||||||
|
workspaceId: session.workspaceId,
|
||||||
|
actorId: session.userId,
|
||||||
|
eventType: 'transaction.evidence_updated',
|
||||||
|
aggregateType: 'transaction',
|
||||||
|
subjectId: id,
|
||||||
|
correlationId: session.correlationId,
|
||||||
|
payload: { evidenceStatus: dto.evidenceStatus },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async softDelete(session: AuthenticatedSession, id: string) {
|
||||||
|
await this.getById(session, id);
|
||||||
|
await this.outbox.withTransaction(async (tx) => {
|
||||||
|
await tx
|
||||||
|
.update(transactions)
|
||||||
|
.set({ deletedAt: new Date() })
|
||||||
|
.where(and(eq(transactions.id, id), eq(transactions.tenantId, session.tenantId)));
|
||||||
|
await this.audit.record(tx, {
|
||||||
|
tenantId: session.tenantId,
|
||||||
|
actorId: session.userId,
|
||||||
|
action: 'transaction.deleted',
|
||||||
|
resource: `transaction:${id}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return { deleted: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue