120 lines
3.4 KiB
TypeScript
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 };
|
|
});
|
|
}
|
|
}
|