feat: Domain Entities — Task, PetState, User
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
enum PetType { dog, cat, capybara }
|
||||
|
||||
enum PetMood { happy, neutral, tired, excited }
|
||||
|
||||
class PetState {
|
||||
PetState({
|
||||
this.energyToday,
|
||||
this.energyDate,
|
||||
this.streakDays = 0,
|
||||
this.totalTasksDone = 0,
|
||||
this.unlockedItems = const [],
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
final int? energyToday;
|
||||
final DateTime? energyDate;
|
||||
final int streakDays;
|
||||
final int totalTasksDone;
|
||||
final List<String> unlockedItems;
|
||||
final DateTime updatedAt;
|
||||
|
||||
PetMood get mood {
|
||||
if (energyToday == null) return PetMood.neutral;
|
||||
if (streakDays >= 7 && energyToday! >= 2) return PetMood.excited;
|
||||
if (energyToday! >= 2) return PetMood.happy;
|
||||
return PetMood.tired;
|
||||
}
|
||||
|
||||
PetState copyWith({
|
||||
int? Function()? energyToday,
|
||||
DateTime? Function()? energyDate,
|
||||
int? streakDays,
|
||||
int? totalTasksDone,
|
||||
List<String>? unlockedItems,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return PetState(
|
||||
energyToday: energyToday != null ? energyToday() : this.energyToday,
|
||||
energyDate: energyDate != null ? energyDate() : this.energyDate,
|
||||
streakDays: streakDays ?? this.streakDays,
|
||||
totalTasksDone: totalTasksDone ?? this.totalTasksDone,
|
||||
unlockedItems: unlockedItems ?? this.unlockedItems,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is PetState &&
|
||||
energyToday == other.energyToday &&
|
||||
energyDate == other.energyDate &&
|
||||
streakDays == other.streakDays &&
|
||||
totalTasksDone == other.totalTasksDone &&
|
||||
updatedAt == other.updatedAt;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
energyToday,
|
||||
energyDate,
|
||||
streakDays,
|
||||
totalTasksDone,
|
||||
updatedAt,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'PetState(energy: $energyToday, streak: $streakDays, mood: $mood)';
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
enum EnergyLevel {
|
||||
low(1),
|
||||
medium(2),
|
||||
high(3);
|
||||
|
||||
const EnergyLevel(this.value);
|
||||
final int value;
|
||||
}
|
||||
|
||||
enum TaskStatus { pending, done, snoozed, someday, archived }
|
||||
|
||||
class Task {
|
||||
Task({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.title,
|
||||
this.description,
|
||||
this.energyLevel = EnergyLevel.medium,
|
||||
this.status = TaskStatus.pending,
|
||||
this.isSelfCare = false,
|
||||
this.deadline,
|
||||
this.photoPath,
|
||||
this.snoozeUntil,
|
||||
this.sortOrder = 0,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.syncedAt,
|
||||
this.localId,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String userId;
|
||||
final String title;
|
||||
final String? description;
|
||||
final EnergyLevel energyLevel;
|
||||
final TaskStatus status;
|
||||
final bool isSelfCare;
|
||||
final DateTime? deadline;
|
||||
final String? photoPath;
|
||||
final DateTime? snoozeUntil;
|
||||
final int sortOrder;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final DateTime? syncedAt;
|
||||
final String? localId;
|
||||
|
||||
bool get isDone => status == TaskStatus.done;
|
||||
bool get isPending => status == TaskStatus.pending;
|
||||
bool get isSnoozed => status == TaskStatus.snoozed;
|
||||
bool get isOverdue =>
|
||||
deadline != null && deadline!.isBefore(DateTime.now()) && isPending;
|
||||
bool get isSynced => syncedAt != null;
|
||||
|
||||
Task copyWith({
|
||||
String? id,
|
||||
String? userId,
|
||||
String? title,
|
||||
String? Function()? description,
|
||||
EnergyLevel? energyLevel,
|
||||
TaskStatus? status,
|
||||
bool? isSelfCare,
|
||||
DateTime? Function()? deadline,
|
||||
String? Function()? photoPath,
|
||||
DateTime? Function()? snoozeUntil,
|
||||
int? sortOrder,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
DateTime? Function()? syncedAt,
|
||||
String? Function()? localId,
|
||||
}) {
|
||||
return Task(
|
||||
id: id ?? this.id,
|
||||
userId: userId ?? this.userId,
|
||||
title: title ?? this.title,
|
||||
description: description != null ? description() : this.description,
|
||||
energyLevel: energyLevel ?? this.energyLevel,
|
||||
status: status ?? this.status,
|
||||
isSelfCare: isSelfCare ?? this.isSelfCare,
|
||||
deadline: deadline != null ? deadline() : this.deadline,
|
||||
photoPath: photoPath != null ? photoPath() : this.photoPath,
|
||||
snoozeUntil: snoozeUntil != null ? snoozeUntil() : this.snoozeUntil,
|
||||
sortOrder: sortOrder ?? this.sortOrder,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
syncedAt: syncedAt != null ? syncedAt() : this.syncedAt,
|
||||
localId: localId != null ? localId() : this.localId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) || other is Task && id == other.id;
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
|
||||
@override
|
||||
String toString() => 'Task(id: $id, title: $title, status: $status)';
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:kittie_mobile/domain/entities/pet_state.dart';
|
||||
|
||||
class User {
|
||||
User({
|
||||
required this.id,
|
||||
required this.deviceId,
|
||||
this.name = 'Friend',
|
||||
this.petType = PetType.cat,
|
||||
this.petName = 'Yoshi',
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String deviceId;
|
||||
final String name;
|
||||
final PetType petType;
|
||||
final String petName;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
User copyWith({
|
||||
String? id,
|
||||
String? deviceId,
|
||||
String? name,
|
||||
PetType? petType,
|
||||
String? petName,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return User(
|
||||
id: id ?? this.id,
|
||||
deviceId: deviceId ?? this.deviceId,
|
||||
name: name ?? this.name,
|
||||
petType: petType ?? this.petType,
|
||||
petName: petName ?? this.petName,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) || other is User && id == other.id;
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
|
||||
@override
|
||||
String toString() => 'User(id: $id, name: $name, pet: $petName)';
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
class KittieDateUtils {
|
||||
KittieDateUtils._();
|
||||
|
||||
static bool isToday(DateTime? date) {
|
||||
if (date == null) return false;
|
||||
final now = DateTime.now();
|
||||
return date.year == now.year &&
|
||||
date.month == now.month &&
|
||||
date.day == now.day;
|
||||
}
|
||||
|
||||
static bool isTomorrow(DateTime? date) {
|
||||
if (date == null) return false;
|
||||
final tomorrow = DateTime.now().add(const Duration(days: 1));
|
||||
return date.year == tomorrow.year &&
|
||||
date.month == tomorrow.month &&
|
||||
date.day == tomorrow.day;
|
||||
}
|
||||
|
||||
static bool isOverdue(DateTime? date) {
|
||||
if (date == null) return false;
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
return date.isBefore(today);
|
||||
}
|
||||
|
||||
static String formatRelativeDate(DateTime? date) {
|
||||
if (date == null) return '';
|
||||
if (isToday(date)) return 'Dnes';
|
||||
if (isTomorrow(date)) return 'Zitra';
|
||||
|
||||
final yesterday = DateTime.now().subtract(const Duration(days: 1));
|
||||
if (date.year == yesterday.year &&
|
||||
date.month == yesterday.month &&
|
||||
date.day == yesterday.day) {
|
||||
return 'Vcera';
|
||||
}
|
||||
|
||||
return '${date.day}. ${_monthName(date.month)}';
|
||||
}
|
||||
|
||||
static String getWeekdayLabel(int weekday) {
|
||||
const labels = ['Po', 'Ut', 'St', 'Ct', 'Pa', 'So', 'Ne'];
|
||||
return labels[(weekday - 1) % 7];
|
||||
}
|
||||
|
||||
static List<bool> getStreakDays(DateTime startOfWeek, {DateTime? now}) {
|
||||
final today = now ?? DateTime.now();
|
||||
final todayDate = DateTime(today.year, today.month, today.day);
|
||||
final start = DateTime(
|
||||
startOfWeek.year,
|
||||
startOfWeek.month,
|
||||
startOfWeek.day,
|
||||
);
|
||||
|
||||
return List.generate(7, (i) {
|
||||
final day = start.add(Duration(days: i));
|
||||
return !day.isAfter(todayDate);
|
||||
});
|
||||
}
|
||||
|
||||
static String _monthName(int month) {
|
||||
const months = [
|
||||
'ledna',
|
||||
'unora',
|
||||
'brezna',
|
||||
'dubna',
|
||||
'kvetna',
|
||||
'cervna',
|
||||
'cervence',
|
||||
'srpna',
|
||||
'zari',
|
||||
'rijna',
|
||||
'listopadu',
|
||||
'prosince',
|
||||
];
|
||||
return months[month - 1];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kittie_mobile/domain/entities/pet_state.dart';
|
||||
|
||||
void main() {
|
||||
final now = DateTime(2025, 6, 15, 10, 0);
|
||||
|
||||
PetState createState({
|
||||
int? energyToday,
|
||||
int streakDays = 0,
|
||||
int totalTasksDone = 0,
|
||||
}) {
|
||||
return PetState(
|
||||
energyToday: energyToday,
|
||||
streakDays: streakDays,
|
||||
totalTasksDone: totalTasksDone,
|
||||
updatedAt: now,
|
||||
);
|
||||
}
|
||||
|
||||
group('PetState defaults', () {
|
||||
test('has correct defaults', () {
|
||||
final state = createState();
|
||||
expect(state.energyToday, isNull);
|
||||
expect(state.streakDays, 0);
|
||||
expect(state.totalTasksDone, 0);
|
||||
expect(state.unlockedItems, isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('PetState mood algorithm', () {
|
||||
test('returns neutral when energy is null', () {
|
||||
final state = createState(energyToday: null);
|
||||
expect(state.mood, PetMood.neutral);
|
||||
});
|
||||
|
||||
test('returns excited when streak >= 7 and energy >= 2', () {
|
||||
final state = createState(energyToday: 2, streakDays: 7);
|
||||
expect(state.mood, PetMood.excited);
|
||||
});
|
||||
|
||||
test('returns excited when streak >= 7 and energy is 3', () {
|
||||
final state = createState(energyToday: 3, streakDays: 10);
|
||||
expect(state.mood, PetMood.excited);
|
||||
});
|
||||
|
||||
test('returns happy when energy >= 2 and streak < 7', () {
|
||||
final state = createState(energyToday: 2, streakDays: 3);
|
||||
expect(state.mood, PetMood.happy);
|
||||
});
|
||||
|
||||
test('returns happy when energy is 3 and streak < 7', () {
|
||||
final state = createState(energyToday: 3, streakDays: 0);
|
||||
expect(state.mood, PetMood.happy);
|
||||
});
|
||||
|
||||
test('returns tired when energy is 1', () {
|
||||
final state = createState(energyToday: 1, streakDays: 0);
|
||||
expect(state.mood, PetMood.tired);
|
||||
});
|
||||
|
||||
test('returns tired when energy is 1 even with high streak', () {
|
||||
final state = createState(energyToday: 1, streakDays: 10);
|
||||
expect(state.mood, PetMood.tired);
|
||||
});
|
||||
});
|
||||
|
||||
group('PetState copyWith', () {
|
||||
test('copies with new energy', () {
|
||||
final state = createState();
|
||||
final copy = state.copyWith(energyToday: () => 3);
|
||||
expect(copy.energyToday, 3);
|
||||
expect(copy.streakDays, 0);
|
||||
});
|
||||
|
||||
test('copies energy to null', () {
|
||||
final state = createState(energyToday: 2);
|
||||
final copy = state.copyWith(energyToday: () => null);
|
||||
expect(copy.energyToday, isNull);
|
||||
});
|
||||
|
||||
test('preserves energy when not specified', () {
|
||||
final state = createState(energyToday: 2);
|
||||
final copy = state.copyWith(streakDays: 5);
|
||||
expect(copy.energyToday, 2);
|
||||
});
|
||||
|
||||
test('copies with new streak', () {
|
||||
final state = createState();
|
||||
final copy = state.copyWith(streakDays: 5);
|
||||
expect(copy.streakDays, 5);
|
||||
});
|
||||
|
||||
test('copies with unlocked items', () {
|
||||
final state = createState();
|
||||
final copy = state.copyWith(unlockedItems: ['hat', 'scarf']);
|
||||
expect(copy.unlockedItems, ['hat', 'scarf']);
|
||||
});
|
||||
});
|
||||
|
||||
group('PetState equality', () {
|
||||
test('equal states are equal', () {
|
||||
final a = createState(energyToday: 2, streakDays: 3);
|
||||
final b = createState(energyToday: 2, streakDays: 3);
|
||||
expect(a, equals(b));
|
||||
expect(a.hashCode, b.hashCode);
|
||||
});
|
||||
|
||||
test('different states are not equal', () {
|
||||
final a = createState(energyToday: 2);
|
||||
final b = createState(energyToday: 3);
|
||||
expect(a, isNot(equals(b)));
|
||||
});
|
||||
});
|
||||
|
||||
group('PetState toString', () {
|
||||
test('includes energy, streak, and mood', () {
|
||||
final state = createState(energyToday: 2, streakDays: 3);
|
||||
expect(state.toString(), contains('2'));
|
||||
expect(state.toString(), contains('3'));
|
||||
expect(state.toString(), contains('happy'));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kittie_mobile/domain/entities/task.dart';
|
||||
|
||||
void main() {
|
||||
final now = DateTime(2025, 6, 15, 10, 0);
|
||||
|
||||
Task createTask({
|
||||
String id = 'task-1',
|
||||
TaskStatus status = TaskStatus.pending,
|
||||
DateTime? deadline,
|
||||
DateTime? syncedAt,
|
||||
}) {
|
||||
return Task(
|
||||
id: id,
|
||||
userId: 'user-1',
|
||||
title: 'Test task',
|
||||
status: status,
|
||||
deadline: deadline,
|
||||
syncedAt: syncedAt,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
);
|
||||
}
|
||||
|
||||
group('EnergyLevel', () {
|
||||
test('has correct int values', () {
|
||||
expect(EnergyLevel.low.value, 1);
|
||||
expect(EnergyLevel.medium.value, 2);
|
||||
expect(EnergyLevel.high.value, 3);
|
||||
});
|
||||
});
|
||||
|
||||
group('Task defaults', () {
|
||||
test('uses medium energy and pending status by default', () {
|
||||
final task = createTask();
|
||||
expect(task.energyLevel, EnergyLevel.medium);
|
||||
expect(task.status, TaskStatus.pending);
|
||||
expect(task.isSelfCare, false);
|
||||
expect(task.sortOrder, 0);
|
||||
});
|
||||
});
|
||||
|
||||
group('Task computed getters', () {
|
||||
test('isDone returns true when status is done', () {
|
||||
final task = createTask(status: TaskStatus.done);
|
||||
expect(task.isDone, true);
|
||||
expect(task.isPending, false);
|
||||
});
|
||||
|
||||
test('isPending returns true when status is pending', () {
|
||||
final task = createTask();
|
||||
expect(task.isPending, true);
|
||||
expect(task.isDone, false);
|
||||
});
|
||||
|
||||
test('isSnoozed returns true when status is snoozed', () {
|
||||
final task = createTask(status: TaskStatus.snoozed);
|
||||
expect(task.isSnoozed, true);
|
||||
});
|
||||
|
||||
test('isOverdue returns true when deadline is past and status is pending',
|
||||
() {
|
||||
final task = createTask(
|
||||
deadline: DateTime.now().subtract(const Duration(days: 1)),
|
||||
);
|
||||
expect(task.isOverdue, true);
|
||||
});
|
||||
|
||||
test('isOverdue returns false when deadline is in the future', () {
|
||||
final task = createTask(
|
||||
deadline: DateTime.now().add(const Duration(days: 1)),
|
||||
);
|
||||
expect(task.isOverdue, false);
|
||||
});
|
||||
|
||||
test('isOverdue returns false when status is done', () {
|
||||
final task = createTask(
|
||||
status: TaskStatus.done,
|
||||
deadline: DateTime.now().subtract(const Duration(days: 1)),
|
||||
);
|
||||
expect(task.isOverdue, false);
|
||||
});
|
||||
|
||||
test('isOverdue returns false when deadline is null', () {
|
||||
final task = createTask();
|
||||
expect(task.isOverdue, false);
|
||||
});
|
||||
|
||||
test('isSynced returns true when syncedAt is set', () {
|
||||
final task = createTask(syncedAt: now);
|
||||
expect(task.isSynced, true);
|
||||
});
|
||||
|
||||
test('isSynced returns false when syncedAt is null', () {
|
||||
final task = createTask();
|
||||
expect(task.isSynced, false);
|
||||
});
|
||||
});
|
||||
|
||||
group('Task copyWith', () {
|
||||
test('copies with new title', () {
|
||||
final task = createTask();
|
||||
final copy = task.copyWith(title: 'New title');
|
||||
expect(copy.title, 'New title');
|
||||
expect(copy.id, task.id);
|
||||
});
|
||||
|
||||
test('copies nullable field to null', () {
|
||||
final task = createTask(deadline: now);
|
||||
final copy = task.copyWith(deadline: () => null);
|
||||
expect(copy.deadline, isNull);
|
||||
});
|
||||
|
||||
test('preserves nullable field when not specified', () {
|
||||
final task = createTask(deadline: now);
|
||||
final copy = task.copyWith(title: 'Changed');
|
||||
expect(copy.deadline, now);
|
||||
});
|
||||
|
||||
test('copies with new status', () {
|
||||
final task = createTask();
|
||||
final copy = task.copyWith(status: TaskStatus.done);
|
||||
expect(copy.isDone, true);
|
||||
});
|
||||
});
|
||||
|
||||
group('Task equality', () {
|
||||
test('tasks with same id are equal', () {
|
||||
final a = createTask(id: 'same');
|
||||
final b = createTask(id: 'same');
|
||||
expect(a, equals(b));
|
||||
expect(a.hashCode, b.hashCode);
|
||||
});
|
||||
|
||||
test('tasks with different ids are not equal', () {
|
||||
final a = createTask(id: 'a');
|
||||
final b = createTask(id: 'b');
|
||||
expect(a, isNot(equals(b)));
|
||||
});
|
||||
});
|
||||
|
||||
group('Task toString', () {
|
||||
test('includes id, title, and status', () {
|
||||
final task = createTask();
|
||||
expect(task.toString(), contains('task-1'));
|
||||
expect(task.toString(), contains('Test task'));
|
||||
expect(task.toString(), contains('pending'));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kittie_mobile/domain/entities/pet_state.dart';
|
||||
import 'package:kittie_mobile/domain/entities/user.dart';
|
||||
|
||||
void main() {
|
||||
final now = DateTime(2025, 6, 15, 10, 0);
|
||||
|
||||
User createUser({
|
||||
String id = 'user-1',
|
||||
String name = 'Friend',
|
||||
PetType petType = PetType.cat,
|
||||
String petName = 'Yoshi',
|
||||
}) {
|
||||
return User(
|
||||
id: id,
|
||||
deviceId: 'device-1',
|
||||
name: name,
|
||||
petType: petType,
|
||||
petName: petName,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
);
|
||||
}
|
||||
|
||||
group('User defaults', () {
|
||||
test('has correct defaults', () {
|
||||
final user = User(
|
||||
id: 'user-1',
|
||||
deviceId: 'device-1',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
);
|
||||
expect(user.name, 'Friend');
|
||||
expect(user.petType, PetType.cat);
|
||||
expect(user.petName, 'Yoshi');
|
||||
});
|
||||
});
|
||||
|
||||
group('User copyWith', () {
|
||||
test('copies with new name', () {
|
||||
final user = createUser();
|
||||
final copy = user.copyWith(name: 'Alice');
|
||||
expect(copy.name, 'Alice');
|
||||
expect(copy.id, user.id);
|
||||
expect(copy.petName, user.petName);
|
||||
});
|
||||
|
||||
test('copies with new pet type and name', () {
|
||||
final user = createUser();
|
||||
final copy = user.copyWith(petType: PetType.dog, petName: 'Buddy');
|
||||
expect(copy.petType, PetType.dog);
|
||||
expect(copy.petName, 'Buddy');
|
||||
});
|
||||
|
||||
test('preserves all fields when no arguments given', () {
|
||||
final user = createUser(name: 'Bob', petName: 'Luna');
|
||||
final copy = user.copyWith();
|
||||
expect(copy.name, 'Bob');
|
||||
expect(copy.petName, 'Luna');
|
||||
expect(copy.id, user.id);
|
||||
});
|
||||
});
|
||||
|
||||
group('User equality', () {
|
||||
test('users with same id are equal', () {
|
||||
final a = createUser(id: 'same');
|
||||
final b = createUser(id: 'same', name: 'Different');
|
||||
expect(a, equals(b));
|
||||
expect(a.hashCode, b.hashCode);
|
||||
});
|
||||
|
||||
test('users with different ids are not equal', () {
|
||||
final a = createUser(id: 'a');
|
||||
final b = createUser(id: 'b');
|
||||
expect(a, isNot(equals(b)));
|
||||
});
|
||||
});
|
||||
|
||||
group('User toString', () {
|
||||
test('includes id, name, and pet name', () {
|
||||
final user = createUser();
|
||||
expect(user.toString(), contains('user-1'));
|
||||
expect(user.toString(), contains('Friend'));
|
||||
expect(user.toString(), contains('Yoshi'));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user