Files
BarevClub/packages/services/src/clientRequests.ts
T

72 lines
3.8 KiB
TypeScript

import type { ClientRequestKind, ClientRequestStatus, SubscriptionPlan } from '@barev/db';
import { DomainError, type Ctx } from './context.ts';
const DAY = 24 * 3600_000;
/**
* Заявка из приложения оператору: подписка (онлайн-оплаты пока нет — оператор договаривается и включает)
* или «перезвоните мне» по услуге, которую ещё нельзя заказать с выбором времени.
* Повторная заявка на ту же подписку или услугу, пока первая открыта, не создаётся.
*/
export async function createClientRequest(
ctx: Ctx,
clientId: string,
input: { kind: ClientRequestKind; plan?: SubscriptionPlan; serviceCode?: string; comment?: string },
) {
if (input.kind === 'SUBSCRIPTION' && !input.plan) throw new DomainError('INVALID_PARAMS');
if (input.kind === 'SERVICE') {
if (!input.serviceCode) throw new DomainError('INVALID_PARAMS');
const service = await ctx.db.service.findUnique({ where: { code: input.serviceCode } });
if (!service) throw new DomainError('NOT_FOUND');
}
const open = await ctx.db.clientRequest.findFirst({
where: { clientId, status: 'NEW', kind: input.kind, plan: input.plan ?? null, serviceCode: input.serviceCode ?? null },
});
if (open) return open;
const request = await ctx.db.clientRequest.create({
data: { clientId, kind: input.kind, plan: input.plan, serviceCode: input.serviceCode, comment: input.comment?.trim() || undefined },
});
await ctx.notify.emit({ type: 'clientRequest.created', requestId: request.id });
return request;
}
/** Открытые заявки клиента — приложение показывает «заявка отправлена» */
export function openClientRequests(ctx: Ctx, clientId: string) {
return ctx.db.clientRequest.findMany({ where: { clientId, status: 'NEW' }, orderBy: { createdAt: 'desc' } });
}
export function listClientRequests(ctx: Ctx, input: { status?: ClientRequestStatus }) {
return ctx.db.clientRequest.findMany({
where: input.status ? { status: input.status } : {},
include: { client: { include: { user: { select: { firstName: true, lastName: true, username: true, phone: true, locale: true } } } } },
orderBy: { createdAt: 'desc' },
take: 200,
});
}
export async function updateClientRequest(ctx: Ctx, input: { id: string; status?: ClientRequestStatus; adminNote?: string }) {
return ctx.db.clientRequest.update({
where: { id: input.id },
data: { status: input.status, adminNote: input.adminNote === undefined ? undefined : input.adminNote.trim() || null },
});
}
/**
* Включить подписку вручную на N дней. Тот же план продлевается с даты окончания;
* переход на другой план начинается сразу (Elite важнее Barev+, пока обе активны).
*/
export async function grantSubscription(ctx: Ctx, input: { clientId: string; plan: SubscriptionPlan; days: number; requestId?: string }) {
const now = ctx.now();
const current = await ctx.db.subscription.findFirst({
where: { clientId: input.clientId, plan: input.plan, status: 'ACTIVE', endsAt: { gt: now } },
orderBy: { endsAt: 'desc' },
});
const startsAt = current?.endsAt ?? now;
const sub = await ctx.db.subscription.create({
data: { clientId: input.clientId, plan: input.plan, source: 'MANUAL', startsAt, endsAt: new Date(startsAt.getTime() + input.days * DAY) },
});
if (input.requestId) await ctx.db.clientRequest.update({ where: { id: input.requestId }, data: { status: 'DONE' } });
await ctx.notify.emit({ type: 'subscription.activated', subscriptionId: sub.id });
return sub;
}