class ReminderPayload { final String eventId; final String title; final DateTime startAt; final DateTime? endAt; final String timezone; final String? location; final String? notes; final String? color; final ReminderPayloadMode mode; final List aggregateIds; final int? fireTimeBucket; final int version; const ReminderPayload({ required this.eventId, required this.title, required this.startAt, required this.timezone, this.endAt, this.location, this.notes, this.color, this.mode = ReminderPayloadMode.single, this.aggregateIds = const [], this.fireTimeBucket, this.version = 1, }); ReminderPayload copyWith({ String? eventId, String? title, DateTime? startAt, DateTime? endAt, String? timezone, String? location, String? notes, String? color, ReminderPayloadMode? mode, List? aggregateIds, int? fireTimeBucket, int? version, }) { return ReminderPayload( eventId: eventId ?? this.eventId, title: title ?? this.title, startAt: startAt ?? this.startAt, endAt: endAt ?? this.endAt, timezone: timezone ?? this.timezone, location: location ?? this.location, notes: notes ?? this.notes, color: color ?? this.color, mode: mode ?? this.mode, aggregateIds: aggregateIds ?? this.aggregateIds, fireTimeBucket: fireTimeBucket ?? this.fireTimeBucket, version: version ?? this.version, ); } Map toJson() { return { 'eventId': eventId, 'title': title, 'startAt': startAt.toIso8601String(), 'endAt': endAt?.toIso8601String(), 'timezone': timezone, 'location': location, 'notes': notes, 'color': color, 'mode': mode.value, 'aggregateIds': aggregateIds, 'fireTimeBucket': fireTimeBucket, 'version': version, }; } factory ReminderPayload.fromJson(Map json) { final eventId = (json['eventId'] as String?) ?? ''; if (eventId.isEmpty) { throw const FormatException('eventId is required'); } final startAtRaw = json['startAt'] as String?; if (startAtRaw == null || startAtRaw.isEmpty) { throw const FormatException('startAt is required'); } final parsedStartAt = DateTime.parse(startAtRaw); final mode = ReminderPayloadMode.fromValue( (json['mode'] as String?) ?? 'single', ); final aggregateIds = (json['aggregateIds'] as List? ?? const []) .map((item) => item.toString()) .toList(); if (mode == ReminderPayloadMode.aggregate && aggregateIds.length < 2) { throw const FormatException('aggregateIds must contain at least 2 items'); } return ReminderPayload( eventId: eventId, title: (json['title'] as String?) ?? '', startAt: parsedStartAt, endAt: json['endAt'] != null ? DateTime.parse(json['endAt'] as String) : null, timezone: (json['timezone'] as String?) ?? 'UTC', location: json['location'] as String?, notes: json['notes'] as String?, color: json['color'] as String?, mode: mode, aggregateIds: aggregateIds, fireTimeBucket: json['fireTimeBucket'] as int?, version: (json['version'] as int?) ?? 1, ); } @override bool operator ==(Object other) { if (identical(this, other)) { return true; } return other is ReminderPayload && other.eventId == eventId && other.title == title && other.startAt == startAt && other.endAt == endAt && other.timezone == timezone && other.location == location && other.notes == notes && other.color == color && other.mode == mode && _listEquals(other.aggregateIds, aggregateIds) && other.fireTimeBucket == fireTimeBucket && other.version == version; } @override int get hashCode { return Object.hash( eventId, title, startAt, endAt, timezone, location, notes, color, mode, Object.hashAll(aggregateIds), fireTimeBucket, version, ); } } enum ReminderPayloadMode { single('single'), aggregate('aggregate'); const ReminderPayloadMode(this.value); final String value; static ReminderPayloadMode fromValue(String raw) { return ReminderPayloadMode.values.firstWhere( (item) => item.value == raw, orElse: () => ReminderPayloadMode.single, ); } } bool _listEquals(List left, List right) { if (left.length != right.length) { return false; } for (var i = 0; i < left.length; i++) { if (left[i] != right[i]) { return false; } } return true; }