Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
72 lines
3.8 KiB
TypeScript
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;
|
|
}
|