801 lines
35 KiB
TypeScript
801 lines
35 KiB
TypeScript
import { Contact } from '../entities/crm/contact';
|
||
import { Company } from '../entities/crm/company';
|
||
import { Deal, DealStage } from '../entities/crm/deal';
|
||
import { Order, OrderStatus, PaymentStatus, FulfillmentStatus } from '../entities/commerce/order';
|
||
import { Product, ProductCategory } from '../entities/commerce/product';
|
||
import { Warehouse } from '../entities/inventory/warehouse';
|
||
import { StockItem, StockMovement } from '../services/contracts/inventory';
|
||
import { Account, CashFlowTransaction } from '../services/contracts/finance';
|
||
import { Payment, Shipment } from '../services/contracts/commerce';
|
||
import { SYSTEM_ENTITY_METADATA } from '../entities/metadata/registry';
|
||
import { AuditEntry } from '../services/contracts/audit';
|
||
import {
|
||
putRecord,
|
||
putStoreItem,
|
||
countRecords,
|
||
resetDatabase,
|
||
} from '../services/storage/indexedDb';
|
||
import { DemoClock } from './demoClock';
|
||
|
||
function relDate(daysOffset: number, hoursOffset = 0): string {
|
||
const base = DemoClock.getNow().getTime();
|
||
const target = new Date(base + daysOffset * 86400000 + hoursOffset * 3600000);
|
||
return target.toISOString();
|
||
}
|
||
|
||
function relDateStr(daysOffset: number): string {
|
||
return relDate(daysOffset).slice(0, 10);
|
||
}
|
||
|
||
// ----------------------------------------------------
|
||
// 1. COMPANIES (12)
|
||
// ----------------------------------------------------
|
||
export function generateSeedCompanies(): Company[] {
|
||
const raw = [
|
||
{ name: 'ООО «СеверТорг»', inn: '7701234567', roles: ['client', 'partner'] as const, city: 'Москва' },
|
||
{ name: 'АО «ТехноЛаб»', inn: '7812987654', roles: ['vendor'] as const, city: 'Санкт-Петербург' },
|
||
{ name: 'ГК «Айсберг Логистик»', inn: '5405112233', roles: ['partner'] as const, city: 'Новосибирск' },
|
||
{ name: 'ООО «Кофе Эксперт»', inn: '7722445566', roles: ['vendor', 'client'] as const, city: 'Москва' },
|
||
{ name: 'ИП «Михайлов Автодеталь»', inn: '6677889900', roles: ['client'] as const, city: 'Екатеринбург' },
|
||
{ name: 'ООО «Бьюти Групп»', inn: '7733557799', roles: ['client'] as const, city: 'Москва' },
|
||
{ name: 'АО «Ресторанный Синдикат»', inn: '7801334455', roles: ['client', 'partner'] as const, city: 'Санкт-Петербург' },
|
||
{ name: 'ООО «ПакТрейд Упаковка»', inn: '5001223344', roles: ['vendor'] as const, city: 'Мытищи' },
|
||
{ name: 'ООО «СтройСервисМонтаж»', inn: '7744112233', roles: ['subcontractor'] as const, city: 'Москва' },
|
||
{ name: 'ИП «Семенова Студио»', inn: '2311445566', roles: ['client'] as const, city: 'Краснодар' },
|
||
{ name: 'ООО «Гурман Продукт»', inn: '7715667788', roles: ['vendor'] as const, city: 'Москва' },
|
||
{ name: 'АО «ФармаХолдинг»', inn: '7819998877', roles: ['client'] as const, city: 'Санкт-Петербург' },
|
||
];
|
||
|
||
return raw.map((c, i) => ({
|
||
id: `comp_seed_${i + 1}`,
|
||
name: c.name,
|
||
legalName: c.name,
|
||
inn: c.inn,
|
||
kpp: '770101001',
|
||
roles: [...c.roles],
|
||
phone: `+7 (495) ${100 + i * 15}-${10 + i * 2}-${20 + i * 3}`,
|
||
email: `contact@${c.name.toLowerCase().replace(/[^a-z0-9]/g, '') || 'company'}.ru`,
|
||
website: `https://${c.name.toLowerCase().replace(/[^a-z0-9]/g, '') || 'company'}.example.com`,
|
||
address: `г. ${c.city}, ул. Деловая, д. ${i + 10}`,
|
||
status: 'active',
|
||
audit: {
|
||
createdAt: relDate(-30 + i),
|
||
createdById: 'usr_sys_01',
|
||
updatedAt: relDate(-2),
|
||
updatedById: 'usr_sys_01',
|
||
version: 1,
|
||
},
|
||
}));
|
||
}
|
||
|
||
// ----------------------------------------------------
|
||
// 2. CONTACTS (32)
|
||
// ----------------------------------------------------
|
||
export function generateSeedContacts(companies: Company[]): Contact[] {
|
||
const names = [
|
||
['Алексей', 'Морозов', 'Владимирович'],
|
||
['Елена', 'Соколова', 'Сергеевна'],
|
||
['Михаил', 'Романов', 'Николаевич'],
|
||
['Анна', 'Кузнецова', 'Игоревна'],
|
||
['Дмитрий', 'Смирнов', 'Алексеевич'],
|
||
['Ольга', 'Попова', 'Дмитриевна'],
|
||
['Сергей', 'Васильев', 'Петрович'],
|
||
['Татьяна', 'Новикова', 'Олеговна'],
|
||
['Иван', 'Федоров', 'Сергеевич'],
|
||
['Наталья', 'Морозова', 'Андреевна'],
|
||
['Артем', 'Волков', 'Михайлович'],
|
||
['Мария', 'Алексеева', 'Юрьевна'],
|
||
['Виктор', 'Лебедев', 'Борисович'],
|
||
['Юлия', 'Семенова', 'Викторовна'],
|
||
['Павел', 'Егоров', 'Анатольевич'],
|
||
['Ирина', 'Павлова', 'Константиновна'],
|
||
['Роман', 'Козлов', 'Евгеньевич'],
|
||
['Екатерина', 'Степанова', 'Валерьевна'],
|
||
['Денис', 'Николаев', 'Григорьевич'],
|
||
['Светлана', 'Орлова', 'Романовна'],
|
||
['Константин', 'Андреев', 'Сергеевич'],
|
||
['Алина', 'Макарова', 'Ильинична'],
|
||
['Максим', 'Захаров', 'Витальевич'],
|
||
['Ксения', 'Зайцева', 'Тимуровна'],
|
||
['Владимир', 'Соловьев', 'Максимович'],
|
||
['Дарья', 'Борисова', 'Денисовна'],
|
||
['Андрей', 'Яковлев', 'Станиславович'],
|
||
['Виктория', 'Григорьева', 'Аркадьевна'],
|
||
['Илья', 'Романов', 'Вадимович'],
|
||
['Полина', 'Воробьева', 'Антоновна'],
|
||
['Григорий', 'Сергеев', 'Олегович'],
|
||
['Александра', 'Кузьмина', 'Федоровна'],
|
||
];
|
||
|
||
return names.map(([first, last, middle], i) => {
|
||
const comp = companies[i % companies.length];
|
||
const role = i % 4 === 0 ? 'partner' : i % 3 === 0 ? 'vendor' : 'client';
|
||
return {
|
||
id: `cnt_seed_${i + 1}`,
|
||
firstName: first,
|
||
lastName: last,
|
||
middleName: middle,
|
||
roles: [role],
|
||
companyId: i % 2 === 0 ? comp.id : undefined,
|
||
jobTitle: i % 2 === 0 ? 'Руководитель направления' : 'Частный клиент',
|
||
email: `${last.toLowerCase()}.${first.toLowerCase()}@example.com`,
|
||
phone: `+7 (9${10 + (i % 80)}) ${200 + i * 5}-${10 + (i % 80)}-${20 + (i % 70)}`,
|
||
telegram: `@${last.toLowerCase()}_${i + 1}`,
|
||
status: 'active',
|
||
notes: `Клиентский профиль #${i + 1}. Приоритетное обслуживание.`,
|
||
audit: {
|
||
createdAt: relDate(-25 + i * 0.5),
|
||
createdById: 'usr_sys_01',
|
||
updatedAt: relDate(-1),
|
||
updatedById: 'usr_sys_01',
|
||
version: 1,
|
||
},
|
||
};
|
||
});
|
||
}
|
||
|
||
// ----------------------------------------------------
|
||
// 3. CATEGORIES & PRODUCTS (45 across 6 categories)
|
||
// ----------------------------------------------------
|
||
export function generateSeedCategories(): ProductCategory[] {
|
||
const cats = [
|
||
{ id: 'cat_beverages', name: 'Кофе и чайное сырье', code: 'BEV' },
|
||
{ id: 'cat_food', name: 'Кулинария и десерты', code: 'FOOD' },
|
||
{ id: 'cat_equip', name: 'Кофейное оборудование', code: 'EQUIP' },
|
||
{ id: 'cat_auto', name: 'Автохимия и запчасти', code: 'AUTO' },
|
||
{ id: 'cat_beauty', name: 'Косметика и уход', code: 'BEAUTY' },
|
||
{ id: 'cat_services', name: 'Сервисные работы и ТО', code: 'SRV' },
|
||
];
|
||
|
||
return cats.map((c) => ({
|
||
id: c.id,
|
||
name: c.name,
|
||
code: c.code,
|
||
audit: {
|
||
createdAt: relDate(-60),
|
||
createdById: 'usr_sys_01',
|
||
updatedAt: relDate(-10),
|
||
updatedById: 'usr_sys_01',
|
||
version: 1,
|
||
},
|
||
}));
|
||
}
|
||
|
||
export function generateSeedProducts(categories: ProductCategory[]): Product[] {
|
||
const products: Product[] = [];
|
||
const baseSkus = [
|
||
// Beverages (8)
|
||
{ cat: 'cat_beverages', name: 'Зерновой кофе Espresso Bar 1кг', price: 185000, cost: 95000, unit: 'кг' },
|
||
{ cat: 'cat_beverages', name: 'Зерновой кофе Ethiopia Yirgacheffe 1кг', price: 245000, cost: 130000, unit: 'кг' },
|
||
{ cat: 'cat_beverages', name: 'Зерновой кофе Colombia Supremo 1кг', price: 210000, cost: 110000, unit: 'кг' },
|
||
{ cat: 'cat_beverages', name: 'Чай листовой Ассам Мокалбари 500г', price: 120000, cost: 60000, unit: 'уп' },
|
||
{ cat: 'cat_beverages', name: 'Чай зеленый Сенча Премиум 500г', price: 135000, cost: 70000, unit: 'уп' },
|
||
{ cat: 'cat_beverages', name: 'Сироп Карамель Соленая 1л', price: 89000, cost: 42000, unit: 'бут' },
|
||
{ cat: 'cat_beverages', name: 'Сироп Ваниль Мадагаскар 1л', price: 89000, cost: 42000, unit: 'бут' },
|
||
{ cat: 'cat_beverages', name: 'Какао-порошок Barry Callebaut 1кг', price: 160000, cost: 90000, unit: 'кг' },
|
||
|
||
// Food (8)
|
||
{ cat: 'cat_food', name: 'Круассан классический на сливочном масле', price: 22000, cost: 9000, unit: 'шт' },
|
||
{ cat: 'cat_food', name: 'Круассан миндальный с кремом', price: 29000, cost: 12000, unit: 'шт' },
|
||
{ cat: 'cat_food', name: 'Сэндвич с лососем и авокадо', price: 42000, cost: 21000, unit: 'шт' },
|
||
{ cat: 'cat_food', name: 'Сэндвич с индейкой и песто', price: 36000, cost: 17000, unit: 'шт' },
|
||
{ cat: 'cat_food', name: 'Чизкейк Нью-Йорк классический', price: 34000, cost: 14000, unit: 'порц' },
|
||
{ cat: 'cat_food', name: 'Торт Морковный со сливочным кремом', price: 32000, cost: 13000, unit: 'порц' },
|
||
{ cat: 'cat_food', name: 'Гранола с греческим йогуртом и ягодами', price: 28000, cost: 11000, unit: 'порц' },
|
||
{ cat: 'cat_food', name: 'Печенье Кукис Шоколадное 80г', price: 15000, cost: 6000, unit: 'шт' },
|
||
|
||
// Equipment (7)
|
||
{ cat: 'cat_equip', name: 'Кофемашина La Marzocco Linea PB 2GR', price: 98000000, cost: 72000000, unit: 'шт' },
|
||
{ cat: 'cat_equip', name: 'Кофемашина Nuova Simonelli Appia Life 2GR', price: 45000000, cost: 34000000, unit: 'шт' },
|
||
{ cat: 'cat_equip', name: 'Кофемолка Mahlkonig E65S GBW', price: 23000000, cost: 17500000, unit: 'шт' },
|
||
{ cat: 'cat_equip', name: 'Водоумягчитель DVA 12л', price: 1450000, cost: 850000, unit: 'шт' },
|
||
{ cat: 'cat_equip', name: 'Темпер динамометрический 58.5мм', price: 95000, cost: 45000, unit: 'шт' },
|
||
{ cat: 'cat_equip', name: 'Питчер молочный Motta 500мл', price: 38000, cost: 18000, unit: 'шт' },
|
||
{ cat: 'cat_equip', name: 'Весы бариста Acaia Lunar', price: 280000, cost: 190000, unit: 'шт' },
|
||
|
||
// Auto (8)
|
||
{ cat: 'cat_auto', name: 'Моторное масло синтетическое 5W-40 4л', price: 450000, cost: 270000, unit: 'канистра' },
|
||
{ cat: 'cat_auto', name: 'Моторное масло 0W-20 Eco 4л', price: 520000, cost: 310000, unit: 'канистра' },
|
||
{ cat: 'cat_auto', name: 'Масляный фильтр Mann-Filter W712', price: 85000, cost: 42000, unit: 'шт' },
|
||
{ cat: 'cat_auto', name: 'Воздушный фильтр C25004', price: 95000, cost: 48000, unit: 'шт' },
|
||
{ cat: 'cat_auto', name: 'Колодки тормозные передние керамик', price: 420000, cost: 230000, unit: 'компл' },
|
||
{ cat: 'cat_auto', name: 'Тормозная жидкость DOT-4 1л', price: 75000, cost: 35000, unit: 'бут' },
|
||
{ cat: 'cat_auto', name: 'Очиститель тормозов аэрозоль 500мл', price: 45000, cost: 20000, unit: 'баллон' },
|
||
{ cat: 'cat_auto', name: 'Антифриз готовый красный G12+ 5кг', price: 160000, cost: 85000, unit: 'канистра' },
|
||
|
||
// Beauty (7)
|
||
{ cat: 'cat_beauty', name: 'Шампунь восстанавливающий кератин 1л', price: 320000, cost: 160000, unit: 'бут' },
|
||
{ cat: 'cat_beauty', name: 'Маска глубокого увлажнения волос 500мл', price: 280000, cost: 135000, unit: 'банка' },
|
||
{ cat: 'cat_beauty', name: 'Масло аргановое для кончиков волос 100мл', price: 210000, cost: 95000, unit: 'флакон' },
|
||
{ cat: 'cat_beauty', name: 'Крем питательный для рук с маслом ши 75мл', price: 85000, cost: 38000, unit: 'тюбик' },
|
||
{ cat: 'cat_beauty', name: 'Гель-лак премиум оттенок Nude 15мл', price: 65000, cost: 28000, unit: 'флакон' },
|
||
{ cat: 'cat_beauty', name: 'Сыворотка для лица с гиалуроновой кислотой 30мл', price: 390000, cost: 180000, unit: 'флакон' },
|
||
{ cat: 'cat_beauty', name: 'Набор одноразовых пилок для маникюра (50 шт)', price: 95000, cost: 40000, unit: 'уп' },
|
||
|
||
// Services (7)
|
||
{ cat: 'cat_services', name: 'Регламентное ТО кофемашины с декальцинацией', price: 650000, cost: 200000, unit: 'усл', isSrv: true },
|
||
{ cat: 'cat_services', name: 'Комплексная замена масла и фильтров автомобиля', price: 250000, cost: 80000, unit: 'усл', isSrv: true },
|
||
{ cat: 'cat_services', name: 'Диагностика ходовой части автомобиля на вибростенде', price: 180000, cost: 50000, unit: 'усл', isSrv: true },
|
||
{ cat: 'cat_services', name: 'Стрижка женская модельная и укладка', price: 350000, cost: 100000, unit: 'усл', isSrv: true },
|
||
{ cat: 'cat_services', name: 'Маникюр аппаратный с покрытием гель-лак', price: 280000, cost: 80000, unit: 'усл', isSrv: true },
|
||
{ cat: 'cat_services', name: 'Настройка и калибровка кофемолки bar-flow', price: 300000, cost: 90000, unit: 'усл', isSrv: true },
|
||
{ cat: 'cat_services', name: 'Выездной аудит торговой точки и склада', price: 1200000, cost: 400000, unit: 'усл', isSrv: true },
|
||
];
|
||
|
||
baseSkus.forEach((b, i) => {
|
||
const isService = !!b.isSrv;
|
||
const cat = categories.find((c) => c.id === b.cat);
|
||
products.push({
|
||
id: `prd_seed_${i + 1}`,
|
||
sku: `${cat?.code || 'GEN'}-${1000 + i}`,
|
||
name: b.name,
|
||
categoryId: b.cat,
|
||
categoryName: cat?.name || 'Общая',
|
||
barcode: `4600000${100000 + i}`,
|
||
price: { minor: b.price, currency: 'RUB' },
|
||
costPrice: { minor: b.cost, currency: 'RUB' },
|
||
unit: b.unit,
|
||
isService,
|
||
trackInventory: !isService,
|
||
minStockAlert: isService ? 0 : 10,
|
||
status: 'active',
|
||
audit: {
|
||
createdAt: relDate(-45 + (i % 20)),
|
||
createdById: 'usr_sys_01',
|
||
updatedAt: relDate(-2),
|
||
updatedById: 'usr_sys_01',
|
||
version: 1,
|
||
},
|
||
});
|
||
});
|
||
|
||
return products;
|
||
}
|
||
|
||
// ----------------------------------------------------
|
||
// 4. WAREHOUSES (3)
|
||
// ----------------------------------------------------
|
||
export function generateSeedWarehouses(): Warehouse[] {
|
||
return [
|
||
{
|
||
id: 'wh_main',
|
||
name: 'Центральный распределительный склад (Москва-Юг)',
|
||
code: 'WH-MAIN',
|
||
type: 'central',
|
||
address: 'г. Москва, Варшавское шоссе, д. 148',
|
||
isDefault: true,
|
||
status: 'active',
|
||
responsibleUserId: 'usr_wh_01',
|
||
audit: {
|
||
createdAt: relDate(-100),
|
||
createdById: 'usr_sys_01',
|
||
updatedAt: relDate(-5),
|
||
updatedById: 'usr_sys_01',
|
||
version: 1,
|
||
},
|
||
},
|
||
{
|
||
id: 'wh_northwest',
|
||
name: 'Региональный склад (Санкт-Петербург СЗ)',
|
||
code: 'WH-NW',
|
||
type: 'store',
|
||
address: 'г. Санкт-Петербург, Выборгское шоссе, д. 32',
|
||
isDefault: false,
|
||
status: 'active',
|
||
responsibleUserId: 'usr_wh_02',
|
||
audit: {
|
||
createdAt: relDate(-80),
|
||
createdById: 'usr_sys_01',
|
||
updatedAt: relDate(-5),
|
||
updatedById: 'usr_sys_01',
|
||
version: 1,
|
||
},
|
||
},
|
||
{
|
||
id: 'wh_showroom',
|
||
name: 'Склад-шоурум (Флагманский магазин)',
|
||
code: 'WH-SHOWROOM',
|
||
type: 'store',
|
||
address: 'г. Москва, ул. Большая Дмитровка, д. 7',
|
||
isDefault: false,
|
||
status: 'active',
|
||
responsibleUserId: 'usr_wh_03',
|
||
audit: {
|
||
createdAt: relDate(-60),
|
||
createdById: 'usr_sys_01',
|
||
updatedAt: relDate(-5),
|
||
updatedById: 'usr_sys_01',
|
||
version: 1,
|
||
},
|
||
},
|
||
];
|
||
}
|
||
|
||
// ----------------------------------------------------
|
||
// 5. DEALS (25 across pipeline stages)
|
||
// ----------------------------------------------------
|
||
export function generateSeedDeals(contacts: Contact[], companies: Company[]): Deal[] {
|
||
const stages: DealStage[] = ['discovery', 'proposal', 'negotiation', 'contract', 'won', 'lost'];
|
||
const titles = [
|
||
'Поставка кофемашин для сети ресторанов',
|
||
'Годовой сервисный контракт на автопарк',
|
||
'Оптовая партия премиального зернового кофе',
|
||
'Оснащение спа-салона уходовой косметикой',
|
||
'Комплекс ТО и замена масел для таксопарка',
|
||
'Поставка оборудования для 3 новых кофеен',
|
||
'Партия сиропов и чайных смесей для бара',
|
||
'Договор на поставку автохимии и фильтров',
|
||
'Корпоративные подарочные наборы кофе и чая',
|
||
'Обновление кухонного и барного инвентаря',
|
||
'Запуск франшизной точки: полное оснащение',
|
||
'Поставка косметики для сети барбершопов',
|
||
'Договор планового технического обслуживания',
|
||
'Спецзаказ на партию динамических темперов',
|
||
'Поставка расходных материалов для шиномонтажа',
|
||
'Кейтеринг и мобильная кофейня на выставку',
|
||
'Автоматизация рабочих мест и кассовой зоны',
|
||
'Поставка профессиональных весов и посуды',
|
||
'Партия синтетических масел 0W-20 для СТО',
|
||
'Поставка десертов и выпечки для кофе-поинтов',
|
||
'Сертификация и аудит оборудования фабрики',
|
||
'Поставка средств для бережного ухода за кожей',
|
||
'Замена группы прокладок и клапанов 12 машин',
|
||
'Снабжение автосервиса тормозными колодками',
|
||
'Партия крафтового какао для кондитерской',
|
||
];
|
||
|
||
return titles.map((title, i) => {
|
||
const stage = stages[i % stages.length];
|
||
const amountMinor = 15000000 + ((i * 37) % 85) * 1000000; // between 150,000 and 1,000,000 RUB
|
||
const contact = contacts[i % contacts.length];
|
||
const company = companies[i % companies.length];
|
||
const prob = stage === 'won' ? 100 : stage === 'lost' ? 0 : stage === 'contract' ? 85 : stage === 'negotiation' ? 65 : 40;
|
||
|
||
return {
|
||
id: `deal_seed_${i + 1}`,
|
||
title,
|
||
pipelineId: 'pipe_b2b',
|
||
stage,
|
||
amount: { minor: amountMinor, currency: 'RUB' },
|
||
probability: prob,
|
||
expectedCloseDate: relDateStr(5 + (i % 25)),
|
||
contactId: contact.id,
|
||
companyId: company.id,
|
||
lostReason: stage === 'lost' ? 'Бюджет проекта перенесен на следующий квартал' : undefined,
|
||
audit: {
|
||
createdAt: relDate(-20 + (i % 15)),
|
||
createdById: 'usr_sys_01',
|
||
updatedAt: relDate(-1),
|
||
updatedById: 'usr_sys_01',
|
||
version: 1,
|
||
},
|
||
};
|
||
});
|
||
}
|
||
|
||
// ----------------------------------------------------
|
||
// 6. ORDERS (28 with complete items, payments, shipments)
|
||
// ----------------------------------------------------
|
||
export function generateSeedOrders(
|
||
contacts: Contact[],
|
||
products: Product[],
|
||
warehouses: Warehouse[]
|
||
): { orders: Order[]; payments: Payment[]; shipments: Shipment[] } {
|
||
const statuses: OrderStatus[] = [
|
||
'delivered',
|
||
'delivered',
|
||
'completed',
|
||
'completed',
|
||
'shipped',
|
||
'ready',
|
||
'confirmed',
|
||
'processing',
|
||
'draft',
|
||
'cancelled',
|
||
];
|
||
|
||
const orders: Order[] = [];
|
||
const payments: Payment[] = [];
|
||
const shipments: Shipment[] = [];
|
||
|
||
for (let i = 0; i < 28; i++) {
|
||
const status = statuses[i % statuses.length];
|
||
const contact = contacts[i % contacts.length];
|
||
const warehouse = warehouses[i % warehouses.length];
|
||
const orderNumber = `ORD-2026-${String(1001 + i).padStart(4, '0')}`;
|
||
const orderId = `ord_seed_${i + 1}`;
|
||
|
||
// Select 2-4 products for order items
|
||
const itemCount = 2 + (i % 3);
|
||
const orderItems = [];
|
||
let subtotalMinor = 0;
|
||
|
||
for (let j = 0; j < itemCount; j++) {
|
||
const p = products[(i * 3 + j) % products.length];
|
||
const qty = 1 + ((i + j) % 4);
|
||
const totalItemMinor = p.price.minor * qty;
|
||
subtotalMinor += totalItemMinor;
|
||
|
||
orderItems.push({
|
||
id: `oi_seed_${i + 1}_${j + 1}`,
|
||
productId: p.id,
|
||
sku: p.sku,
|
||
name: p.name,
|
||
quantity: qty,
|
||
unitPrice: p.price,
|
||
discountMinor: 0,
|
||
totalPrice: { minor: totalItemMinor, currency: 'RUB' },
|
||
});
|
||
}
|
||
|
||
const discountMinor = i % 5 === 0 ? Math.round(subtotalMinor * 0.05) : 0;
|
||
const totalMinor = subtotalMinor - discountMinor;
|
||
const isPaid = status === 'delivered' || status === 'completed' || status === 'shipped' || i % 3 === 0;
|
||
const isShipped = status === 'delivered' || status === 'completed' || status === 'shipped';
|
||
|
||
const payStatus: PaymentStatus = isPaid ? 'paid' : status === 'draft' ? 'pending' : 'pending';
|
||
const fulfillStatus: FulfillmentStatus = isShipped ? 'fulfilled' : status === 'ready' ? 'partially_fulfilled' : 'unfulfilled';
|
||
|
||
const order: Order = {
|
||
id: orderId,
|
||
orderNumber,
|
||
customerType: 'contact',
|
||
customerId: contact.id,
|
||
customerName: `${contact.firstName} ${contact.lastName}`,
|
||
status,
|
||
paymentStatus: payStatus,
|
||
fulfillmentStatus: fulfillStatus,
|
||
items: orderItems,
|
||
subtotal: { minor: subtotalMinor, currency: 'RUB' },
|
||
discountMinor,
|
||
taxMinor: Math.round(totalMinor * 0.2), // 20% VAT included
|
||
shippingMinor: 0,
|
||
total: { minor: totalMinor, currency: 'RUB' },
|
||
warehouseId: warehouse.id,
|
||
channel: i % 4 === 0 ? 'pos' : i % 3 === 0 ? 'web' : 'b2b',
|
||
shippingAddress: `г. Москва, ул. Клиентская, д. ${i + 1}`,
|
||
audit: {
|
||
createdAt: relDate(-18 + i * 0.6),
|
||
createdById: 'usr_sys_01',
|
||
updatedAt: relDate(-1),
|
||
updatedById: 'usr_sys_01',
|
||
version: 1,
|
||
},
|
||
};
|
||
orders.push(order);
|
||
|
||
// Linked Payment
|
||
if (isPaid) {
|
||
payments.push({
|
||
id: `pmt_seed_${i + 1}`,
|
||
orderId,
|
||
orderNumber,
|
||
amount: { minor: totalMinor, currency: 'RUB' },
|
||
method: i % 2 === 0 ? 'bank_transfer' : 'card',
|
||
status: 'completed',
|
||
transactionId: `TXN-${100000 + i * 17}`,
|
||
paidAt: relDate(-17 + i * 0.6),
|
||
audit: {
|
||
createdAt: relDate(-17 + i * 0.6),
|
||
createdById: 'usr_sys_01',
|
||
updatedAt: relDate(-17 + i * 0.6),
|
||
updatedById: 'usr_sys_01',
|
||
version: 1,
|
||
},
|
||
});
|
||
}
|
||
|
||
// Linked Shipment
|
||
if (isShipped) {
|
||
shipments.push({
|
||
id: `shp_seed_${i + 1}`,
|
||
orderId,
|
||
orderNumber,
|
||
warehouseId: warehouse.id,
|
||
status: status === 'delivered' || status === 'completed' ? 'delivered' : 'shipped',
|
||
trackingNumber: `KRONA-TRK-${70000 + i * 11}`,
|
||
carrier: 'Krona Экспресс Доставка',
|
||
dispatchedAt: relDate(-15 + i * 0.6),
|
||
deliveredAt: status === 'delivered' || status === 'completed' ? relDate(-14 + i * 0.6) : undefined,
|
||
items: orderItems.map((oi) => ({
|
||
productId: oi.productId,
|
||
sku: oi.sku,
|
||
name: oi.name,
|
||
quantity: oi.quantity,
|
||
})),
|
||
audit: {
|
||
createdAt: relDate(-15 + i * 0.6),
|
||
createdById: 'usr_sys_01',
|
||
updatedAt: relDate(-1),
|
||
updatedById: 'usr_sys_01',
|
||
version: 1,
|
||
},
|
||
});
|
||
}
|
||
}
|
||
|
||
return { orders, payments, shipments };
|
||
}
|
||
|
||
// ----------------------------------------------------
|
||
// 7. STOCK LEDGER & MOVEMENTS
|
||
// ----------------------------------------------------
|
||
export function generateSeedStock(products: Product[], warehouses: Warehouse[]): {
|
||
stockItems: StockItem[];
|
||
movements: StockMovement[];
|
||
} {
|
||
const stockItems: StockItem[] = [];
|
||
const movements: StockMovement[] = [];
|
||
|
||
warehouses.forEach((wh) => {
|
||
products.filter((p) => !p.isService).forEach((p, pIdx) => {
|
||
const onHand = 30 + ((pIdx * 7) % 150);
|
||
const reserved = (pIdx % 5) * 3;
|
||
const available = onHand - reserved;
|
||
|
||
stockItems.push({
|
||
id: `stk_${wh.id}_${p.id}`,
|
||
warehouseId: wh.id,
|
||
warehouseName: wh.name,
|
||
productId: p.id,
|
||
sku: p.sku,
|
||
productName: p.name,
|
||
onHand,
|
||
reserved,
|
||
available,
|
||
unit: p.unit,
|
||
minThreshold: p.minStockAlert,
|
||
audit: {
|
||
createdAt: relDate(-40),
|
||
createdById: 'usr_sys_01',
|
||
updatedAt: relDate(-2),
|
||
updatedById: 'usr_sys_01',
|
||
version: 1,
|
||
},
|
||
});
|
||
|
||
// Add a couple recent movements
|
||
if (pIdx % 3 === 0) {
|
||
movements.push({
|
||
id: `mov_${wh.id}_${p.id}_1`,
|
||
type: 'receipt',
|
||
productId: p.id,
|
||
productName: p.name,
|
||
sku: p.sku,
|
||
targetWarehouseId: wh.id,
|
||
quantity: 50,
|
||
referenceNumber: `GR-${1000 + pIdx}`,
|
||
notes: 'Плановое поступление от поставщика',
|
||
timestamp: relDate(-10 + (pIdx % 5)),
|
||
audit: {
|
||
createdAt: relDate(-10 + (pIdx % 5)),
|
||
createdById: 'usr_sys_01',
|
||
updatedAt: relDate(-10 + (pIdx % 5)),
|
||
updatedById: 'usr_sys_01',
|
||
version: 1,
|
||
},
|
||
});
|
||
}
|
||
});
|
||
});
|
||
|
||
return { stockItems, movements };
|
||
}
|
||
|
||
// ----------------------------------------------------
|
||
// 8. FINANCE ACCOUNTS & CASH-FLOW
|
||
// ----------------------------------------------------
|
||
export function generateSeedFinance(payments: Payment[]): {
|
||
accounts: Account[];
|
||
transactions: CashFlowTransaction[];
|
||
} {
|
||
const accounts: Account[] = [
|
||
{
|
||
id: 'acc_main_bank',
|
||
name: 'Основной расчетный счет (ПАО Сбербанк)',
|
||
type: 'bank',
|
||
balance: { minor: 458000000, currency: 'RUB' }, // 4,580,000.00 RUB
|
||
currency: 'RUB',
|
||
accountNumber: '40702810938000012345',
|
||
bankName: 'ПАО СБЕРБАНК',
|
||
isDefault: true,
|
||
status: 'active',
|
||
audit: {
|
||
createdAt: relDate(-100),
|
||
createdById: 'usr_sys_01',
|
||
updatedAt: relDate(-1),
|
||
updatedById: 'usr_sys_01',
|
||
version: 1,
|
||
},
|
||
},
|
||
{
|
||
id: 'acc_cash_desk',
|
||
name: 'Касса наличных (Главный офис)',
|
||
type: 'cash_desk',
|
||
balance: { minor: 34500000, currency: 'RUB' }, // 345,000.00 RUB
|
||
currency: 'RUB',
|
||
isDefault: false,
|
||
status: 'active',
|
||
audit: {
|
||
createdAt: relDate(-100),
|
||
createdById: 'usr_sys_01',
|
||
updatedAt: relDate(-1),
|
||
updatedById: 'usr_sys_01',
|
||
version: 1,
|
||
},
|
||
},
|
||
{
|
||
id: 'acc_acquiring',
|
||
name: 'Эквайринг Т-Банк',
|
||
type: 'acquiring',
|
||
balance: { minor: 189000000, currency: 'RUB' }, // 1,890,000.00 RUB
|
||
currency: 'RUB',
|
||
isDefault: false,
|
||
status: 'active',
|
||
audit: {
|
||
createdAt: relDate(-100),
|
||
createdById: 'usr_sys_01',
|
||
updatedAt: relDate(-1),
|
||
updatedById: 'usr_sys_01',
|
||
version: 1,
|
||
},
|
||
},
|
||
];
|
||
|
||
const transactions: CashFlowTransaction[] = [];
|
||
|
||
// Inflow transactions matching order payments
|
||
payments.forEach((pmt, i) => {
|
||
transactions.push({
|
||
id: `tx_seed_${i + 1}`,
|
||
transactionNumber: `CFT-2026-${String(1001 + i).padStart(4, '0')}`,
|
||
type: 'inflow',
|
||
category: 'sales_revenue',
|
||
amount: pmt.amount,
|
||
accountId: pmt.method === 'cash' ? 'acc_cash_desk' : 'acc_main_bank',
|
||
accountName: pmt.method === 'cash' ? 'Касса наличных' : 'Основной расчетный счет',
|
||
referenceType: 'order_payment',
|
||
referenceId: pmt.id,
|
||
description: `Поступление оплаты по заказу №${pmt.orderNumber}`,
|
||
timestamp: pmt.paidAt,
|
||
audit: {
|
||
createdAt: pmt.paidAt,
|
||
createdById: 'usr_sys_01',
|
||
updatedAt: pmt.paidAt,
|
||
updatedById: 'usr_sys_01',
|
||
version: 1,
|
||
},
|
||
});
|
||
});
|
||
|
||
// A few regular operating outflows (rent, payroll, supplier payments)
|
||
const outflows = [
|
||
{ cat: 'rent' as const, name: 'Аренда помещений склада и офиса за текущий месяц', amt: 38000000, daysAgo: 10 },
|
||
{ cat: 'payroll' as const, name: 'Выплата аванса сотрудникам', amt: 65000000, daysAgo: 15 },
|
||
{ cat: 'supplier_payment' as const, name: 'Оплата партии сырья поставщику ООО «Кофе Эксперт»', amt: 48500000, daysAgo: 7 },
|
||
{ cat: 'taxes' as const, name: 'Уплата квартальных налоговых платежей (УСН/НДС)', amt: 29000000, daysAgo: 12 },
|
||
{ cat: 'utilities' as const, name: 'Оплата электроэнергии и коммунальных услуг', amt: 8500000, daysAgo: 4 },
|
||
];
|
||
|
||
outflows.forEach((o, idx) => {
|
||
const timestamp = relDate(-o.daysAgo);
|
||
transactions.push({
|
||
id: `tx_outflow_${idx + 1}`,
|
||
transactionNumber: `CFT-OUT-${2001 + idx}`,
|
||
type: 'outflow',
|
||
category: o.cat,
|
||
amount: { minor: o.amt, currency: 'RUB' },
|
||
accountId: 'acc_main_bank',
|
||
accountName: 'Основной расчетный счет',
|
||
referenceType: 'manual',
|
||
referenceId: `ref_outflow_${idx + 1}`,
|
||
description: o.name,
|
||
timestamp,
|
||
audit: {
|
||
createdAt: timestamp,
|
||
createdById: 'usr_sys_01',
|
||
updatedAt: timestamp,
|
||
updatedById: 'usr_sys_01',
|
||
version: 1,
|
||
},
|
||
});
|
||
});
|
||
|
||
return { accounts, transactions };
|
||
}
|
||
|
||
// ----------------------------------------------------
|
||
// 9. AUDIT LOG INITIAL SEED
|
||
// ----------------------------------------------------
|
||
export function generateSeedAuditLogs(): AuditEntry[] {
|
||
return [
|
||
{
|
||
id: `aud_seed_1`,
|
||
entityType: 'system',
|
||
entityId: 'seed_init',
|
||
action: 'create',
|
||
userId: 'usr_sys_01',
|
||
userName: 'Системный администратор',
|
||
timestamp: relDate(-30),
|
||
summary: 'Инициализация демонстрационного контура Krona Universal Business OS',
|
||
},
|
||
{
|
||
id: `aud_seed_2`,
|
||
entityType: 'warehouse',
|
||
entityId: 'wh_main',
|
||
action: 'create',
|
||
userId: 'usr_sys_01',
|
||
userName: 'Администратор склада',
|
||
timestamp: relDate(-25),
|
||
summary: 'Создан центральный склад «Москва-Юг» с адресным хранением',
|
||
},
|
||
{
|
||
id: `aud_seed_3`,
|
||
entityType: 'company',
|
||
entityId: 'comp_seed_1',
|
||
action: 'create',
|
||
userId: 'usr_sales_01',
|
||
userName: 'Менеджер по продажам',
|
||
timestamp: relDate(-20),
|
||
summary: 'Зарегистрирован ключевой корпоративный клиент ООО «СеверТорг»',
|
||
},
|
||
];
|
||
}
|
||
|
||
// ----------------------------------------------------
|
||
// SEEDING AND RESET
|
||
// ----------------------------------------------------
|
||
|
||
export async function seedInitialData(force = false): Promise<boolean> {
|
||
const count = await countRecords('contact');
|
||
if (count > 0 && !force) {
|
||
return false;
|
||
}
|
||
|
||
if (force) {
|
||
await resetDatabase();
|
||
}
|
||
|
||
const companies = generateSeedCompanies();
|
||
const contacts = generateSeedContacts(companies);
|
||
const categories = generateSeedCategories();
|
||
const products = generateSeedProducts(categories);
|
||
const warehouses = generateSeedWarehouses();
|
||
const deals = generateSeedDeals(contacts, companies);
|
||
const { orders, payments, shipments } = generateSeedOrders(contacts, products, warehouses);
|
||
const { stockItems, movements } = generateSeedStock(products, warehouses);
|
||
const { accounts, transactions } = generateSeedFinance(payments);
|
||
const auditLogs = generateSeedAuditLogs();
|
||
|
||
// Save into unified records_data store with entityType
|
||
for (const item of companies) await putRecord({ ...item, entityType: 'company' });
|
||
for (const item of contacts) await putRecord({ ...item, entityType: 'contact' });
|
||
for (const item of categories) await putRecord({ ...item, entityType: 'product_category' });
|
||
for (const item of products) await putRecord({ ...item, entityType: 'product' });
|
||
for (const item of warehouses) await putRecord({ ...item, entityType: 'warehouse' });
|
||
for (const item of deals) await putRecord({ ...item, entityType: 'deal' });
|
||
for (const item of orders) await putRecord({ ...item, entityType: 'order' });
|
||
for (const item of payments) await putRecord({ ...item, entityType: 'payment' });
|
||
for (const item of shipments) await putRecord({ ...item, entityType: 'shipment' });
|
||
for (const item of stockItems) await putRecord({ ...item, entityType: 'stock_item' });
|
||
for (const item of movements) await putRecord({ ...item, entityType: 'stock_movement' });
|
||
for (const item of accounts) await putRecord({ ...item, entityType: 'account' });
|
||
for (const item of transactions) await putRecord({ ...item, entityType: 'cash_flow_transaction' });
|
||
|
||
// Save audit logs
|
||
for (const item of auditLogs) await putStoreItem('audit_log', item);
|
||
|
||
// Save metadata
|
||
for (const [, meta] of Object.entries(SYSTEM_ENTITY_METADATA)) {
|
||
await putStoreItem('entities_meta', meta);
|
||
for (const field of meta.fields) {
|
||
await putStoreItem('fields_meta', { ...field, entityCode: meta.code });
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
export async function resetToInitialSeed(): Promise<void> {
|
||
await seedInitialData(true);
|
||
}
|