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 }; }); } }