Monorepo skeleton, Prisma schema, core domain and services with tests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zabudko-ruandClaude Opus 5.5 committed 2026-10-17 19:45:51 +04:00
commit 45d7512d2b
64 files changed
+9483

No files matched your search

+210
View File
@@ -0,0 +1,210 @@
import type { ParamsSchema } from './params.ts';
import type { Pricing } from './pricing.ts';
import type { SlotConfig } from './slots.ts';
import type { CategoryCode, ProviderRole, ServiceFlow } from './types.ts';
/**
* Состав каталога из PRD (раздел 2) — «только эти, ничего не добавлять».
* Цены здесь — заглушки для seed; реальные задаёт владелец в админке.
* `stage` — этап, в котором услуга становится доступной для заказа.
*/
export interface CatalogService {
code: string;
category: CategoryCode;
flow: ServiceFlow;
providerRole: ProviderRole;
stage: 1 | 2 | 3;
isOnline: boolean;
pricing: Pricing;
paramsSchema: ParamsSchema;
slotConfig: SlotConfig;
checklist: { code: string; requiresPhoto?: boolean }[];
}
const DAY_WINDOW: SlotConfig = { dayStartMinute: 9 * 60, dayEndMinute: 20 * 60, stepMinutes: 60, leadHours: 3, horizonDays: 14 };
const fixed = (price: number, minutes: number): Pricing => ({ kind: 'fixed', price, minutes });
export const CATALOG: CatalogService[] = [
{
code: 'cleaning',
category: 'HOME',
flow: 'CLEANING',
providerRole: 'CLEANER',
stage: 1,
isOnline: false,
pricing: {
kind: 'rooms',
variantParam: 'cleaningType',
roomsParam: 'rooms',
tiers: {
regular: { base: 15000, perRoom: 4000, baseMinutes: 120, perRoomMinutes: 45 },
general: { base: 25000, perRoom: 7000, baseMinutes: 210, perRoomMinutes: 75 },
renovation: { base: 35000, perRoom: 10000, baseMinutes: 300, perRoomMinutes: 90 },
},
},
paramsSchema: {
fields: [
{ key: 'cleaningType', type: 'select', options: ['regular', 'general', 'renovation'], default: 'regular', required: true },
{ key: 'rooms', type: 'int', min: 1, max: 6, default: 2, required: true },
],
},
slotConfig: DAY_WINDOW,
checklist: [
{ code: 'kitchen' },
{ code: 'bathroom' },
{ code: 'rooms' },
{ code: 'floors' },
{ code: 'photoReport', requiresPhoto: true },
],
},
{
code: 'repair',
category: 'HOME',
flow: 'REPAIR',
providerRole: 'HANDYMAN',
stage: 2,
isOnline: false,
pricing: fixed(8000, 90),
paramsSchema: {
fields: [
{ key: 'repairType', type: 'select', options: ['plumbing', 'electrical', 'minor'], required: true },
{ key: 'problem', type: 'text', maxLength: 1000, required: true },
],
},
slotConfig: DAY_WINDOW,
checklist: [{ code: 'diagnostics' }, { code: 'estimate' }, { code: 'repair' }, { code: 'photo', requiresPhoto: true }],
},
{
code: 'transfer',
category: 'HOME',
flow: 'TRANSFER',
providerRole: 'DRIVER',
// решение владельца: трансфер отложен, сценарий и state machine готовы
stage: 2,
isOnline: false,
pricing: fixed(10000, 60),
paramsSchema: {
fields: [
{ key: 'direction', type: 'select', options: ['fromAirport', 'toAirport'], required: true },
{ key: 'flight', type: 'text', maxLength: 16, required: true },
{ key: 'passengers', type: 'int', min: 1, max: 7, default: 1, required: true },
{ key: 'luggage', type: 'int', min: 0, max: 10, default: 1, required: true },
],
},
slotConfig: { dayStartMinute: 0, dayEndMinute: 1440, stepMinutes: 15, leadHours: 6, horizonDays: 30 },
checklist: [{ code: 'luggage' }, { code: 'address' }],
},
{
code: 'social_card',
category: 'DOCS',
flow: 'DOCUMENTS',
providerRole: 'DOCS_AGENT',
stage: 2,
isOnline: false,
pricing: fixed(12000, 60),
paramsSchema: { fields: [{ key: 'forWhom', type: 'select', options: ['self', 'family'], required: true }] },
slotConfig: DAY_WINDOW,
checklist: [],
},
{
code: 'passport_translation',
category: 'DOCS',
flow: 'DOCUMENTS',
providerRole: 'DOCS_AGENT',
stage: 2,
isOnline: false,
pricing: fixed(9000, 60),
paramsSchema: { fields: [{ key: 'forWhom', type: 'select', options: ['self', 'family'], required: true }] },
slotConfig: DAY_WINDOW,
checklist: [],
},
{
code: 'residence_permit',
category: 'DOCS',
flow: 'DOCUMENTS',
providerRole: 'DOCS_AGENT',
stage: 2,
isOnline: false,
pricing: fixed(45000, 60),
paramsSchema: { fields: [{ key: 'forWhom', type: 'select', options: ['self', 'family'], required: true }] },
slotConfig: DAY_WINDOW,
checklist: [],
},
{
code: 'lang_armenian',
category: 'LEARNING',
flow: 'LANGUAGE',
providerRole: 'TEACHER',
stage: 2,
isOnline: true,
pricing: fixed(40000, 60),
paramsSchema: {
fields: [
{ key: 'level', type: 'select', options: ['A1', 'A2', 'B1', 'B2'], required: true },
{ key: 'format', type: 'select', options: ['online', 'offline'], required: true },
{ key: 'lessons', type: 'select', options: ['4', '8', '12'], required: true },
],
},
slotConfig: DAY_WINDOW,
checklist: [{ code: 'review' }, { code: 'newTopic' }, { code: 'homework' }],
},
{
code: 'lang_english',
category: 'LEARNING',
flow: 'LANGUAGE',
providerRole: 'TEACHER',
stage: 2,
isOnline: true,
pricing: fixed(40000, 60),
paramsSchema: {
fields: [
{ key: 'level', type: 'select', options: ['A1', 'A2', 'B1', 'B2', 'C1'], required: true },
{ key: 'format', type: 'select', options: ['online', 'offline'], required: true },
{ key: 'lessons', type: 'select', options: ['4', '8', '12'], required: true },
],
},
slotConfig: DAY_WINDOW,
checklist: [{ code: 'review' }, { code: 'newTopic' }, { code: 'homework' }],
},
{
code: 'career',
category: 'LEARNING',
flow: 'CAREER',
providerRole: 'CAREER_CONSULTANT',
stage: 2,
isOnline: true,
pricing: fixed(20000, 60),
paramsSchema: { fields: [{ key: 'topic', type: 'select', options: ['resume', 'jobSearch'], required: true }] },
slotConfig: DAY_WINDOW,
checklist: [{ code: 'resume' }, { code: 'plan' }, { code: 'materials' }],
},
];
/** Скидка Barev+ по направлениям — заглушки */
export const DEFAULT_CATEGORY_DISCOUNTS: Record<CategoryCode, number> = { HOME: 10, DOCS: 10, LEARNING: 15, PERKS: 0 };
/** 12 административных районов Еревана; названия — i18n districts.<code> */
export const YEREVAN_DISTRICTS = [
'ajapnyak',
'arabkir',
'avan',
'davtashen',
'erebuni',
'kanaker_zeytun',
'kentron',
'malatia_sebastia',
'nork_marash',
'nor_nork',
'nubarashen',
'shengavit',
] as const;
/** Роль исполнителя → какие сценарии он ведёт (для бота исполнителей) */
export const ROLE_FLOWS: Record<ProviderRole, ServiceFlow[]> = {
CLEANER: ['CLEANING'],
HANDYMAN: ['REPAIR'],
DRIVER: ['TRANSFER'],
TEACHER: ['LANGUAGE'],
CAREER_CONSULTANT: ['CAREER'],
DOCS_AGENT: ['DOCUMENTS'],
};