Files
OnBudget/lib/src/features/user/application/user_seeder.dart
T
2026-06-28 00:39:09 +03:00

470 lines
16 KiB
Dart

import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../../core/database/converters/enum_converters.dart';
import '../../accounts/application/account_providers.dart';
import '../../accounts/domain/entities/account.dart';
import '../../accounts/domain/repositories/account_repository.dart';
import '../../categories/application/category_providers.dart';
import '../../categories/domain/entities/category.dart';
import '../../categories/domain/repositories/category_repository.dart';
import '../../categories/presentation/widgets/category_icon.dart';
import '../../transactions/application/transaction_providers.dart';
import '../../transactions/domain/repositories/transaction_repository.dart';
part 'user_seeder.g.dart';
@Riverpod(keepAlive: true)
UserSeeder userSeeder(Ref ref) => UserSeeder(
accountRepo: ref.watch(accountRepositoryProvider),
categoryRepo: ref.watch(categoryRepositoryProvider),
txRepo: ref.watch(transactionRepositoryProvider),
);
/// Засевает базовый набор данных при создании нового пользователя:
/// несколько счетов и категорий. Демо-транзакции автоматически НЕ создаются —
/// их можно добавить вручную через [seedDemoTransactionsForUser] (кнопка в профиле).
class UserSeeder {
UserSeeder({
required this.accountRepo,
required this.categoryRepo,
required this.txRepo,
});
final AccountRepository accountRepo;
final CategoryRepository categoryRepo;
final TransactionRepository txRepo;
Future<void> seedForNewUser(String userId) async {
await _seedAccounts(userId);
await _seedCategories(userId);
}
/// Добавляет демо-транзакции для существующего пользователя.
/// Использует имеющиеся счета/категории; создаёт недостающие.
Future<void> seedDemoTransactionsForUser(String userId) async {
final existingAccounts = await accountRepo.watchByUser(userId).first;
final existingCategories = await categoryRepo.watchByUser(userId).first;
Account? card = existingAccounts
.where((a) => a.type == AccountType.card && !a.archived)
.firstOrNull;
Account? cash = existingAccounts
.where((a) => a.type == AccountType.cash && !a.archived)
.firstOrNull;
Account? savings = existingAccounts
.where((a) => a.type == AccountType.savings && !a.archived)
.firstOrNull;
if (card == null || cash == null || savings == null) {
final seeded = await _seedAccounts(userId);
card ??= seeded.card;
cash ??= seeded.cash;
savings ??= seeded.savings;
}
Category? findCat(List<Category> cats, String name, CategoryType type) =>
cats
.where((c) => c.name == name && c.type == type && !c.archived)
.firstOrNull;
Category? food =
findCat(existingCategories, 'Продукты', CategoryType.expense);
Category? rent =
findCat(existingCategories, 'Жильё', CategoryType.expense);
Category? transport =
findCat(existingCategories, 'Транспорт', CategoryType.expense);
Category? cafe =
findCat(existingCategories, 'Кафе', CategoryType.expense);
Category? entertainment =
findCat(existingCategories, 'Досуг', CategoryType.expense);
Category? salary =
findCat(existingCategories, 'Зарплата', CategoryType.income);
if (food == null ||
rent == null ||
transport == null ||
cafe == null ||
entertainment == null ||
salary == null) {
final seeded = await _seedCategories(userId);
food ??= seeded.food;
rent ??= seeded.rent;
transport ??= seeded.transport;
cafe ??= seeded.cafe;
entertainment ??= seeded.entertainment;
salary ??= seeded.salary;
}
await _seedDemoTransactions(
userId,
_SeededAccounts(card: card, cash: cash, savings: savings),
_SeededCategories(
food: food,
rent: rent,
transport: transport,
cafe: cafe,
entertainment: entertainment,
salary: salary,
),
);
}
Future<_SeededAccounts> _seedAccounts(String userId) async {
final card = await accountRepo.create(
userId: userId,
name: 'Карта',
type: AccountType.card,
currency: 'RUB',
initialBalance: 14250000,
colorValue: _argb(0x8AA6A0),
);
final cash = await accountRepo.create(
userId: userId,
name: 'Наличные',
type: AccountType.cash,
currency: 'RUB',
initialBalance: 1282000,
colorValue: _argb(0xC89A86),
);
final savings = await accountRepo.create(
userId: userId,
name: 'Копилка',
type: AccountType.savings,
currency: 'RUB',
initialBalance: 2900000,
colorValue: _argb(0xB3A589),
);
return _SeededAccounts(card: card, cash: cash, savings: savings);
}
Future<_SeededCategories> _seedCategories(String userId) async {
final food = await categoryRepo.create(
userId: userId,
name: 'Продукты',
type: CategoryType.expense,
iconCode: icoCategoryFood,
colorValue: _argb(0x8AA6A0),
);
final rent = await categoryRepo.create(
userId: userId,
name: 'Жильё',
type: CategoryType.expense,
iconCode: icoCategoryHouse,
colorValue: _argb(0xC89A86),
);
final transport = await categoryRepo.create(
userId: userId,
name: 'Транспорт',
type: CategoryType.expense,
iconCode: icoCategoryTransport,
colorValue: _argb(0xB3A589),
);
final cafe = await categoryRepo.create(
userId: userId,
name: 'Кафе',
type: CategoryType.expense,
iconCode: icoCategoryCafe,
colorValue: _argb(0x9FB38A),
);
final entertainment = await categoryRepo.create(
userId: userId,
name: 'Досуг',
type: CategoryType.expense,
iconCode: icoCategoryEntertainment,
colorValue: _argb(0xA99CB9),
);
final salary = await categoryRepo.create(
userId: userId,
name: 'Зарплата',
type: CategoryType.income,
iconCode: icoCategoryOther,
colorValue: _argb(0xB8B5AC),
);
return _SeededCategories(
food: food,
rent: rent,
transport: transport,
cafe: cafe,
entertainment: entertainment,
salary: salary,
);
}
Future<void> _seedDemoTransactions(
String userId,
_SeededAccounts a,
_SeededCategories c,
) async {
final now = DateTime.now();
DateTime atToday(int h, int m) =>
DateTime(now.year, now.month, now.day, h, m);
DateTime atYesterday(int h, int m) =>
atToday(h, m).subtract(const Duration(days: 1));
DateTime daysAgo(int d, [int h = 12, int m = 0]) =>
DateTime(now.year, now.month, now.day, h, m)
.subtract(Duration(days: d));
// now.month - 1 == 0 корректно даёт декабрь прошлого года (Dart нормализует)
DateTime inPrevMonth(int day, [int h = 12, int m = 0]) =>
DateTime(now.year, now.month - 1, day, h, m);
final demo = <_Demo>[
// ── Май (текущий месяц) ────────────────────────────────────────────────
_Demo(
accountId: a.card.id,
categoryId: c.food.id,
type: TransactionType.expense,
amount: 234000,
date: atToday(19, 42),
merchant: 'Лента',
extraInfo: 'Чек №481523',
obligation: SpendingObligation.required),
_Demo(
accountId: a.card.id,
categoryId: c.cafe.id,
type: TransactionType.expense,
amount: 48000,
date: atToday(9, 15),
merchant: 'Кофе Хауз',
extraInfo: 'Двойной эспрессо + круассан',
obligation: SpendingObligation.optional,
impulse: SpendingImpulse.impulsive),
_Demo(
accountId: a.card.id,
categoryId: c.transport.id,
type: TransactionType.expense,
amount: 6200,
date: atToday(8, 50),
merchant: 'Метро',
obligation: SpendingObligation.required),
_Demo(
accountId: a.cash.id,
categoryId: c.food.id,
type: TransactionType.expense,
amount: 112000,
date: atYesterday(21, 8),
merchant: 'Перекрёсток',
extraInfo: 'Чек №209847',
obligation: SpendingObligation.required),
_Demo(
accountId: a.card.id,
categoryId: c.entertainment.id,
type: TransactionType.expense,
amount: 65000,
date: atYesterday(19, 30),
merchant: 'Кинотеатр',
extraInfo: '2 билета · зал IMAX',
obligation: SpendingObligation.unnecessary,
impulse: SpendingImpulse.impulsive),
_Demo(
accountId: a.card.id,
transferToAccountId: a.cash.id,
type: TransactionType.transfer,
amount: 200000,
date: daysAgo(2, 16, 30),
merchant: 'Снятие наличных'),
_Demo(
accountId: a.card.id,
categoryId: c.rent.id,
type: TransactionType.expense,
amount: 3200000,
date: daysAgo(3, 12, 0),
merchant: 'Аренда квартиры',
extraInfo: 'Май 2026 · ул. Ленина 12',
obligation: SpendingObligation.required),
_Demo(
accountId: a.card.id,
categoryId: c.salary.id,
type: TransactionType.income,
amount: 9500000,
date: daysAgo(4, 11, 0),
merchant: 'Зарплата',
extraInfo: 'Аванс за апрель'),
_Demo(
accountId: a.card.id,
categoryId: c.transport.id,
type: TransactionType.expense,
amount: 34000,
date: daysAgo(4, 18, 30),
merchant: 'Яндекс Такси',
obligation: SpendingObligation.optional,
impulse: SpendingImpulse.considered),
_Demo(
accountId: a.cash.id,
categoryId: c.cafe.id,
type: TransactionType.expense,
amount: 72000,
date: daysAgo(5, 14, 0),
merchant: 'Шоколадница',
extraInfo: 'Обед с коллегами',
obligation: SpendingObligation.optional,
impulse: SpendingImpulse.impulsive),
_Demo(
accountId: a.card.id,
categoryId: c.food.id,
type: TransactionType.expense,
amount: 89000,
date: daysAgo(5, 9, 20),
merchant: 'Магнит',
obligation: SpendingObligation.required),
_Demo(
accountId: a.card.id,
transferToAccountId: a.savings.id,
type: TransactionType.transfer,
amount: 500000,
date: daysAgo(7, 11, 0),
merchant: 'Пополнение копилки'),
// ── Апрель ────────────────────────────────────────────────────────────
_Demo(
accountId: a.card.id,
categoryId: c.entertainment.id,
type: TransactionType.expense,
amount: 120000,
date: inPrevMonth(27, 20, 0),
merchant: 'Концерт',
extraInfo: '2 билета',
obligation: SpendingObligation.unnecessary,
impulse: SpendingImpulse.impulsive),
_Demo(
accountId: a.cash.id,
categoryId: c.food.id,
type: TransactionType.expense,
amount: 88000,
date: inPrevMonth(25, 11, 10),
merchant: 'Перекрёсток',
obligation: SpendingObligation.required),
_Demo(
accountId: a.card.id,
categoryId: c.transport.id,
type: TransactionType.expense,
amount: 38000,
date: inPrevMonth(22, 19, 0),
merchant: 'Яндекс Такси',
obligation: SpendingObligation.optional,
impulse: SpendingImpulse.considered),
_Demo(
accountId: a.card.id,
categoryId: c.food.id,
type: TransactionType.expense,
amount: 195000,
date: inPrevMonth(19, 17, 40),
merchant: 'Лента',
obligation: SpendingObligation.required),
_Demo(
accountId: a.card.id,
transferToAccountId: a.cash.id,
type: TransactionType.transfer,
amount: 150000,
date: inPrevMonth(17, 10, 0),
merchant: 'Снятие наличных'),
_Demo(
accountId: a.card.id,
categoryId: c.transport.id,
type: TransactionType.expense,
amount: 6200,
date: inPrevMonth(14, 8, 45),
merchant: 'Метро',
obligation: SpendingObligation.required),
_Demo(
accountId: a.cash.id,
categoryId: c.cafe.id,
type: TransactionType.expense,
amount: 55000,
date: inPrevMonth(12, 13, 30),
merchant: 'Шоколадница',
obligation: SpendingObligation.optional,
impulse: SpendingImpulse.impulsive),
_Demo(
accountId: a.card.id,
transferToAccountId: a.savings.id,
type: TransactionType.transfer,
amount: 500000,
date: inPrevMonth(11, 12, 0),
merchant: 'Пополнение копилки'),
_Demo(
accountId: a.card.id,
categoryId: c.salary.id,
type: TransactionType.income,
amount: 9500000,
date: inPrevMonth(10, 11, 0),
merchant: 'Зарплата',
extraInfo: 'Оклад за март'),
_Demo(
accountId: a.card.id,
categoryId: c.rent.id,
type: TransactionType.expense,
amount: 3200000,
date: inPrevMonth(5, 12, 0),
merchant: 'Аренда квартиры',
extraInfo: 'Апрель 2026 · ул. Ленина 12',
obligation: SpendingObligation.required),
];
for (final d in demo) {
await txRepo.create(
userId: userId,
accountId: d.accountId,
categoryId: d.categoryId,
type: d.type,
amount: d.amount,
date: d.date,
merchant: d.merchant,
extraInfo: d.extraInfo,
transferToAccountId: d.transferToAccountId,
obligation: d.obligation,
impulse: d.impulse,
);
}
}
}
int _argb(int rgb) => 0xFF000000 | rgb;
class _SeededAccounts {
_SeededAccounts({required this.card, required this.cash, required this.savings});
final Account card;
final Account cash;
final Account savings;
}
class _SeededCategories {
_SeededCategories({
required this.food,
required this.rent,
required this.transport,
required this.cafe,
required this.entertainment,
required this.salary,
});
final Category food;
final Category rent;
final Category transport;
final Category cafe;
final Category entertainment;
final Category salary;
}
class _Demo {
_Demo({
required this.accountId,
this.categoryId,
required this.type,
required this.amount,
required this.date,
this.merchant,
this.extraInfo,
this.transferToAccountId,
this.obligation,
this.impulse,
});
final String accountId;
final String? categoryId;
final TransactionType type;
final int amount;
final DateTime date;
final String? merchant;
final String? extraInfo;
final String? transferToAccountId;
final SpendingObligation? obligation;
final SpendingImpulse? impulse;
}