manicanldes-backend/src/operations/operations.store.ts

120 lines
3.4 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { DatabaseService } from '../database/database.service';
import { AccessStore } from '../identity/access.store';
import type { Principal } from '../identity/identity.types';
import type { PageInput } from '../identity/identity.schemas';
import { AppError } from '../common/errors/app-error';
import { recordAudit } from '../identity/audit';
@Injectable()
export class OperationsStore {
constructor(
private readonly db: DatabaseService,
private readonly access: AccessStore,
) {}
async summary(actor: Principal) {
const organizationId = actor.organizationId;
const now = new Date();
return this.db.$transaction(
async (tx) => ({
pendingOrders: await tx.order.count({
where: {
organizationId,
status: 'PENDING_PAYMENT',
expiresAt: { gt: now },
},
}),
expiredOrders: await tx.order.count({
where: {
organizationId,
status: 'PENDING_PAYMENT',
expiresAt: { lte: now },
},
}),
cancelledOrders: await tx.order.count({
where: { organizationId, status: 'CANCELLED' },
}),
unpricedActiveOrders: await tx.order.count({
where: {
organizationId,
status: 'PENDING_PAYMENT',
expiresAt: { gt: now },
pricing: null,
},
}),
pendingDeliveries: await tx.eventDelivery.count({
where: {
event: { organizationId },
deliveredAt: null,
attempts: { lt: 5 },
},
}),
failedDeliveries: await tx.eventDelivery.count({
where: {
event: { organizationId },
deliveredAt: null,
attempts: { gte: 5 },
},
}),
}),
{ isolationLevel: 'RepeatableRead' },
);
}
events(actor: Principal, page: PageInput) {
return this.db.commerceEvent.findMany({
where: { organizationId: actor.organizationId },
take: page.limit,
skip: page.offset,
orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],
select: {
id: true,
orderId: true,
kind: true,
createdAt: true,
delivery: {
select: {
attempts: true,
availableAt: true,
deliveredAt: true,
lastErrorCode: true,
},
},
},
});
}
retry(actor: Principal, id: string) {
return this.access.mutate(actor, 'notifications.retry', async (tx) => {
const event = await tx.commerceEvent.findFirst({
where: { id, organizationId: actor.organizationId },
});
if (!event) throw new AppError('EVENT_NOT_FOUND');
const updated = await tx.eventDelivery.updateMany({
where: {
eventId: id,
deliveredAt: null,
OR: [
{ leaseExpiresAt: null },
{ leaseExpiresAt: { lte: new Date() } },
],
},
data: {
attempts: 0,
availableAt: new Date(),
leaseToken: null,
leaseExpiresAt: null,
lastErrorCode: null,
},
});
if (!updated.count) throw new AppError('EVENT_NOT_RETRYABLE');
await recordAudit(
tx,
actor.organizationId,
actor.userId,
'notification.retry.requested',
id,
);
return { queued: true };
});
}
}