/// pets 域响应 / 请求模型(接口契约冻结稿 openapi.yaml v1.4.0, /// 字段名与后端逐字一致;枚举取值严格校验,未知值抛 [FormatException] /// 以便契约漂移在测试期暴露而非静默吞掉)。 library; import 'package:patbond_flutter/core/models/patch_field.dart'; export 'package:patbond_flutter/core/models/patch_field.dart'; export 'package:patbond_flutter/core/models/cursor_page.dart'; /// 物种(创建即定,不可修改)。 enum PetSpecies { dog, cat, other; static PetSpecies fromJson(String value) => _enumFromJson(values, value, 'species'); } /// 性别。 enum PetSex { male, female, unknown; static PetSex fromJson(String value) => _enumFromJson(values, value, 'sex'); } /// 宠物状态(deleted 为内部软删态,接口永不返回)。 enum PetStatus { active, lost, deceased, archived; static PetStatus fromJson(String value) => _enumFromJson(values, value, 'status'); } /// 调用者对宠物的权限角色(客户端据此显隐写入口)。 enum PetRole { owner, caregiver, viewer; static PetRole fromJson(String value) => _enumFromJson(values, value, 'myRole'); } /// 体重记录来源。 enum WeightSource { manual, clinic, device; static WeightSource fromJson(String value) => _enumFromJson(values, value, 'source'); } /// 疫苗记录状态。 enum VaccinationStatus { scheduled, completed, cancelled; static VaccinationStatus fromJson(String value) => _enumFromJson(values, value, 'status'); } /// 健康事件六类类型。 enum HealthEventType { medical, feeding, deworming, grooming, measurement, note; static HealthEventType fromJson(String value) => _enumFromJson(values, value, 'eventType'); } /// 照护提醒四类类型。 enum CareReminderType { deworming, checkup, medication, other; static CareReminderType fromJson(String value) => _enumFromJson(values, value, 'reminderType'); } /// 照护提醒状态。 enum CareReminderStatus { pending, completed, dismissed; static CareReminderStatus fromJson(String value) => _enumFromJson(values, value, 'status'); } /// 摘要「下次接种」的取值来源。 enum NextVaccinationSource { planned, nextDue; static NextVaccinationSource fromJson(String value) => _enumFromJson(values, value, 'source'); } T _enumFromJson(List values, String raw, String field) { for (final value in values) { if (value.name == raw) return value; } throw FormatException('未知的 $field 取值:$raw'); } /// 日期字段(契约 format: date)序列化为 YYYY-MM-DD。 String dateToJson(DateTime date) { final y = date.year.toString().padLeft(4, '0'); final m = date.month.toString().padLeft(2, '0'); final d = date.day.toString().padLeft(2, '0'); return '$y-$m-$d'; } DateTime? _dateOrNull(Object? value) => value == null ? null : DateTime.parse(value as String); /// 宠物档案(列表 / 详情 / 创建 / 更新统一响应形态)。 /// breedId 与 customBreedName 恰有其一非空;breedDisplayName 随 breedId 存在。 class Pet { const Pet({ required this.id, required this.name, required this.species, required this.breedId, required this.breedDisplayName, required this.customBreedName, required this.sex, required this.birthDate, required this.birthDateEstimated, required this.personality, required this.microchipNo, required this.sterilizedOn, required this.status, required this.avatarUrl, required this.myRole, required this.createdAt, required this.updatedAt, required this.version, }); factory Pet.fromJson(Map json) { return Pet( id: json['id'] as String, name: json['name'] as String, species: PetSpecies.fromJson(json['species'] as String), breedId: json['breedId'] as String?, breedDisplayName: json['breedDisplayName'] as String?, customBreedName: json['customBreedName'] as String?, sex: PetSex.fromJson(json['sex'] as String), birthDate: _dateOrNull(json['birthDate']), birthDateEstimated: json['birthDateEstimated'] as bool, personality: json['personality'] as String?, microchipNo: json['microchipNo'] as String?, sterilizedOn: _dateOrNull(json['sterilizedOn']), status: PetStatus.fromJson(json['status'] as String), // 契约 v1.4.0 新增:时效性预签名 GET URL,每次响应现签; // 不得持久化、过期即重取。响应**不含 avatarAssetId**(只写不读)。 avatarUrl: json['avatarUrl'] as String?, myRole: PetRole.fromJson(json['myRole'] as String), createdAt: DateTime.parse(json['createdAt'] as String), updatedAt: DateTime.parse(json['updatedAt'] as String), version: json['version'] as int, ); } final String id; final String name; final PetSpecies species; final String? breedId; final String? breedDisplayName; final String? customBreedName; final PetSex sex; final DateTime? birthDate; final bool birthDateEstimated; final String? personality; final String? microchipNo; final DateTime? sterilizedOn; final PetStatus status; /// 头像预签名 GET URL;无头像 / asset 非 ready / 对象存储未配置均为 null。 /// 「有头像」等价于本字段非 null(契约不外露 assetId)。 final String? avatarUrl; final PetRole myRole; final DateTime createdAt; final DateTime updatedAt; final int version; } /// 创建宠物请求。breedId 与 customBreedName 二选一且互斥(400/40000 兜底在服务端)。 class CreatePetRequest { const CreatePetRequest({ required this.name, required this.species, required this.sex, this.breedId, this.customBreedName, this.birthDate, this.birthDateEstimated, this.personality, this.microchipNo, this.sterilizedOn, }); final String name; final PetSpecies species; final PetSex sex; final String? breedId; final String? customBreedName; final DateTime? birthDate; final bool? birthDateEstimated; final String? personality; final String? microchipNo; final DateTime? sterilizedOn; Map toJson() => { 'name': name, 'species': species.name, 'sex': sex.name, if (breedId != null) 'breedId': breedId, if (customBreedName != null) 'customBreedName': customBreedName, if (birthDate != null) 'birthDate': dateToJson(birthDate!), if (birthDateEstimated != null) 'birthDateEstimated': birthDateEstimated, if (personality != null) 'personality': personality, if (microchipNo != null) 'microchipNo': microchipNo, if (sterilizedOn != null) 'sterilizedOn': dateToJson(sterilizedOn!), }; } /// 更新宠物请求(部分更新:缺席字段不变;品种对整体替换;species 不可改; /// version 乐观锁必填)。 /// /// **两态与三态并存**:除 [avatarAssetId] 外的字段沿 M2 两态语义(缺省或 /// null 皆为「不改」,不支持清空回 null——它们的 CHECK 约束本就不允许空值); /// [avatarAssetId] 是本 schema **唯一的三态字段**(契约 v1.4.0),因为 /// 「删掉我设的那张头像」是一等公民操作,两态根本无法表达。 /// /// **权限随本次触及的字段变档**(ADR-022 + 服务端 `requiredLevel`): /// 只带 [avatarAssetId] 走 WRITE(owner + caregiver 均可);碰任一资料字段 /// 即 MANAGE(仅 owner);混合请求取更严的一半(防「夹带改名」)。 class UpdatePetRequest { const UpdatePetRequest({ required this.version, this.name, this.breedId, this.customBreedName, this.sex, this.birthDate, this.birthDateEstimated, this.personality, this.microchipNo, this.sterilizedOn, this.status, this.avatarAssetId = const PatchField.absent(), }); final int version; final String? name; final String? breedId; final String? customBreedName; final PetSex? sex; final DateTime? birthDate; final bool? birthDateEstimated; final String? personality; final String? microchipNo; final DateTime? sterilizedOn; final PetStatus? status; /// 宠物头像 asset(两步上传产物,`purpose` 须为 `pet_avatar`)。三态。 final PatchField avatarAssetId; Map toJson() { final json = { 'version': version, if (name != null) 'name': name, if (breedId != null) 'breedId': breedId, if (customBreedName != null) 'customBreedName': customBreedName, if (sex != null) 'sex': sex!.name, if (birthDate != null) 'birthDate': dateToJson(birthDate!), if (birthDateEstimated != null) 'birthDateEstimated': birthDateEstimated, if (personality != null) 'personality': personality, if (microchipNo != null) 'microchipNo': microchipNo, if (sterilizedOn != null) 'sterilizedOn': dateToJson(sterilizedOn!), if (status != null) 'status': status!.name, }; avatarAssetId.writeTo(json, 'avatarAssetId'); return json; } } /// 品种目录项(只读字典)。 class Breed { const Breed({ required this.id, required this.species, required this.code, required this.displayName, }); factory Breed.fromJson(Map json) { return Breed( id: json['id'] as String, species: PetSpecies.fromJson(json['species'] as String), code: json['code'] as String, displayName: json['displayName'] as String, ); } final String id; final PetSpecies species; final String code; final String displayName; } /// 体重记录(append-only,无乐观锁)。 class WeightRecord { const WeightRecord({ required this.id, required this.petId, required this.weightKg, required this.measuredAt, required this.source, required this.note, required this.createdAt, }); factory WeightRecord.fromJson(Map json) { return WeightRecord( id: json['id'] as String, petId: json['petId'] as String, // 服务端 numeric(6,2),整数值也可能以 int 下发。 weightKg: (json['weightKg'] as num).toDouble(), measuredAt: DateTime.parse(json['measuredAt'] as String), source: WeightSource.fromJson(json['source'] as String), note: json['note'] as String?, createdAt: DateTime.parse(json['createdAt'] as String), ); } final String id; final String petId; final double weightKg; final DateTime measuredAt; final WeightSource source; final String? note; final DateTime createdAt; } /// 添加体重记录请求。 class CreateWeightRequest { const CreateWeightRequest({ required this.weightKg, required this.measuredAt, this.source, this.note, }); final double weightKg; final DateTime measuredAt; final WeightSource? source; final String? note; Map toJson() => { 'weightKg': weightKg, 'measuredAt': measuredAt.toIso8601String(), if (source != null) 'source': source!.name, if (note != null) 'note': note, }; } /// 疫苗目录项(只读字典)。 class VaccineCatalogItem { const VaccineCatalogItem({ required this.id, required this.code, required this.name, required this.species, required this.description, }); factory VaccineCatalogItem.fromJson(Map json) { return VaccineCatalogItem( id: json['id'] as String, code: json['code'] as String, name: json['name'] as String, species: PetSpecies.fromJson(json['species'] as String), description: json['description'] as String?, ); } final String id; final String code; final String name; final PetSpecies species; final String? description; } /// 疫苗记录。vaccineName 由目录解出(列表页免二次查字典)。 class Vaccination { const Vaccination({ required this.id, required this.petId, required this.vaccineId, required this.vaccineName, required this.seriesKey, required this.doseNo, required this.doseLabel, required this.status, required this.plannedOn, required this.administeredOn, required this.nextDueOn, required this.manufacturer, required this.batchNo, required this.notes, required this.createdAt, required this.updatedAt, required this.version, }); factory Vaccination.fromJson(Map json) { return Vaccination( id: json['id'] as String, petId: json['petId'] as String, vaccineId: json['vaccineId'] as String, vaccineName: json['vaccineName'] as String, seriesKey: json['seriesKey'] as String, doseNo: json['doseNo'] as int, doseLabel: json['doseLabel'] as String?, status: VaccinationStatus.fromJson(json['status'] as String), plannedOn: _dateOrNull(json['plannedOn']), administeredOn: _dateOrNull(json['administeredOn']), nextDueOn: _dateOrNull(json['nextDueOn']), manufacturer: json['manufacturer'] as String?, batchNo: json['batchNo'] as String?, notes: json['notes'] as String?, createdAt: DateTime.parse(json['createdAt'] as String), updatedAt: DateTime.parse(json['updatedAt'] as String), version: json['version'] as int, ); } final String id; final String petId; final String vaccineId; final String vaccineName; final String seriesKey; final int doseNo; final String? doseLabel; final VaccinationStatus status; final DateTime? plannedOn; final DateTime? administeredOn; final DateTime? nextDueOn; final String? manufacturer; final String? batchNo; final String? notes; final DateTime createdAt; final DateTime updatedAt; final int version; } /// 创建疫苗记录请求(创建状态仅 scheduled / completed)。 class CreateVaccinationRequest { const CreateVaccinationRequest({ required this.vaccineId, required this.seriesKey, required this.doseNo, required this.status, this.doseLabel, this.plannedOn, this.administeredOn, this.nextDueOn, this.manufacturer, this.batchNo, this.notes, }); final String vaccineId; final String seriesKey; final int doseNo; /// 契约允许 scheduled / completed(cancelled 创建无业务意义,服务端 400)。 final VaccinationStatus status; final String? doseLabel; final DateTime? plannedOn; final DateTime? administeredOn; final DateTime? nextDueOn; final String? manufacturer; final String? batchNo; final String? notes; Map toJson() => { 'vaccineId': vaccineId, 'seriesKey': seriesKey, 'doseNo': doseNo, 'status': status.name, if (doseLabel != null) 'doseLabel': doseLabel, if (plannedOn != null) 'plannedOn': dateToJson(plannedOn!), if (administeredOn != null) 'administeredOn': dateToJson(administeredOn!), if (nextDueOn != null) 'nextDueOn': dateToJson(nextDueOn!), if (manufacturer != null) 'manufacturer': manufacturer, if (batchNo != null) 'batchNo': batchNo, if (notes != null) 'notes': notes, }; } /// 更新疫苗记录请求(vaccineId / seriesKey / doseNo 不可改;version 必填)。 class UpdateVaccinationRequest { const UpdateVaccinationRequest({ required this.version, this.status, this.plannedOn, this.administeredOn, this.nextDueOn, this.doseLabel, this.manufacturer, this.batchNo, this.notes, }); final int version; final VaccinationStatus? status; final DateTime? plannedOn; final DateTime? administeredOn; final DateTime? nextDueOn; final String? doseLabel; final String? manufacturer; final String? batchNo; final String? notes; Map toJson() => { 'version': version, if (status != null) 'status': status!.name, if (plannedOn != null) 'plannedOn': dateToJson(plannedOn!), if (administeredOn != null) 'administeredOn': dateToJson(administeredOn!), if (nextDueOn != null) 'nextDueOn': dateToJson(nextDueOn!), if (doseLabel != null) 'doseLabel': doseLabel, if (manufacturer != null) 'manufacturer': manufacturer, if (batchNo != null) 'batchNo': batchNo, if (notes != null) 'notes': notes, }; } /// 健康事件(六类;金额整数分传输,元/分换算留给 UI 层)。 class HealthEvent { const HealthEvent({ required this.id, required this.petId, required this.eventType, required this.occurredAt, required this.title, required this.notes, required this.amountCents, required this.createdByUserId, required this.createdAt, required this.updatedAt, required this.version, }); factory HealthEvent.fromJson(Map json) { return HealthEvent( id: json['id'] as String, petId: json['petId'] as String, eventType: HealthEventType.fromJson(json['eventType'] as String), occurredAt: DateTime.parse(json['occurredAt'] as String), title: json['title'] as String, notes: json['notes'] as String?, amountCents: json['amountCents'] as int?, createdByUserId: json['createdByUserId'] as String, createdAt: DateTime.parse(json['createdAt'] as String), updatedAt: DateTime.parse(json['updatedAt'] as String), version: json['version'] as int, ); } final String id; final String petId; final HealthEventType eventType; final DateTime occurredAt; final String title; final String? notes; /// 金额(整数分,非负,可空)。DTO 层保持整数分不换算。 final int? amountCents; final String createdByUserId; final DateTime createdAt; final DateTime updatedAt; final int version; } /// 添加健康事件请求(createdByUserId 取自 token,不收请求体)。 class CreateHealthEventRequest { const CreateHealthEventRequest({ required this.eventType, required this.occurredAt, required this.title, this.notes, this.amountCents, }); final HealthEventType eventType; final DateTime occurredAt; final String title; final String? notes; /// 整数分;提交小数服务端 400/40000(不做静默截断)。 final int? amountCents; Map toJson() => { 'eventType': eventType.name, 'occurredAt': occurredAt.toIso8601String(), 'title': title, if (notes != null) 'notes': notes, if (amountCents != null) 'amountCents': amountCents, }; } /// 更新健康事件请求(仅 title / notes / amountCents 可编辑;version 必填)。 class UpdateHealthEventRequest { const UpdateHealthEventRequest({ required this.version, this.title, this.notes, this.amountCents, }); final int version; final String? title; final String? notes; final int? amountCents; Map toJson() => { 'version': version, if (title != null) 'title': title, if (notes != null) 'notes': notes, if (amountCents != null) 'amountCents': amountCents, }; } /// 照护提醒(无 version 字段;completedAt 非空当且仅当 status=completed)。 class CareReminder { const CareReminder({ required this.id, required this.petId, required this.reminderType, required this.title, required this.dueAt, required this.status, required this.completedAt, required this.createdAt, required this.updatedAt, }); factory CareReminder.fromJson(Map json) { return CareReminder( id: json['id'] as String, petId: json['petId'] as String, reminderType: CareReminderType.fromJson(json['reminderType'] as String), title: json['title'] as String, dueAt: DateTime.parse(json['dueAt'] as String), status: CareReminderStatus.fromJson(json['status'] as String), completedAt: json['completedAt'] == null ? null : DateTime.parse(json['completedAt'] as String), createdAt: DateTime.parse(json['createdAt'] as String), updatedAt: DateTime.parse(json['updatedAt'] as String), ); } final String id; final String petId; final CareReminderType reminderType; final String title; final DateTime dueAt; final CareReminderStatus status; final DateTime? completedAt; final DateTime createdAt; final DateTime updatedAt; } /// 创建照护提醒请求(创建恒为 pending,不收 status)。 class CreateCareReminderRequest { const CreateCareReminderRequest({ required this.reminderType, required this.title, required this.dueAt, }); final CareReminderType reminderType; final String title; final DateTime dueAt; Map toJson() => { 'reminderType': reminderType.name, 'title': title, 'dueAt': dueAt.toIso8601String(), }; } /// 更新提醒状态请求(状态流转专用:仅 status + completedAt)。 class UpdateCareReminderRequest { const UpdateCareReminderRequest({required this.status, this.completedAt}); final CareReminderStatus status; /// status=completed 时必填;其余状态禁带(违反 422/42202)。 /// 由客户端提交(而非服务端 now()),允许补记实际完成时刻。 final DateTime? completedAt; Map toJson() => { 'status': status.name, if (completedAt != null) 'completedAt': completedAt!.toIso8601String(), }; } /// 摘要:最新体重(无记录 → 整体 null)。 class SummaryLatestWeight { const SummaryLatestWeight({required this.weightKg, required this.measuredAt}); factory SummaryLatestWeight.fromJson(Map json) { return SummaryLatestWeight( weightKg: (json['weightKg'] as num).toDouble(), measuredAt: DateTime.parse(json['measuredAt'] as String), ); } final double weightKg; final DateTime measuredAt; } /// 摘要:疫苗进度(totalDoses=0 → 整体 null,不是 0/0)。 class SummaryVaccinationProgress { const SummaryVaccinationProgress({ required this.completedDoses, required this.totalDoses, }); factory SummaryVaccinationProgress.fromJson(Map json) { return SummaryVaccinationProgress( completedDoses: json['completedDoses'] as int, totalDoses: json['totalDoses'] as int, ); } final int completedDoses; final int totalDoses; } /// 摘要:下次接种(候选集空 → 整体 null;dueOn 可为过去日期——逾期针仍是下一针)。 class SummaryNextVaccination { const SummaryNextVaccination({ required this.vaccinationId, required this.vaccineId, required this.vaccineName, required this.doseNo, required this.doseLabel, required this.dueOn, required this.source, }); factory SummaryNextVaccination.fromJson(Map json) { return SummaryNextVaccination( vaccinationId: json['vaccinationId'] as String, vaccineId: json['vaccineId'] as String, vaccineName: json['vaccineName'] as String, doseNo: json['doseNo'] as int, doseLabel: json['doseLabel'] as String?, dueOn: DateTime.parse(json['dueOn'] as String), source: NextVaccinationSource.fromJson(json['source'] as String), ); } final String vaccinationId; final String vaccineId; final String vaccineName; final int doseNo; final String? doseLabel; final DateTime dueOn; final NextVaccinationSource source; } /// 摘要:当月花费(恒非 null;无支出 amountCents=0;月边界随 tz 移动)。 class SummaryMonthlyExpense { const SummaryMonthlyExpense({ required this.month, required this.timezone, required this.amountCents, }); factory SummaryMonthlyExpense.fromJson(Map json) { return SummaryMonthlyExpense( month: json['month'] as String, timezone: json['timezone'] as String, amountCents: json['amountCents'] as int, ); } /// ISO year-month(如 2026-09)。 final String month; final String timezone; final int amountCents; } /// 档案聚合摘要(四项聚合实时计算;前三项无记录时为 null, /// monthlyExpense 恒非 null——契约 PetSummary null 语义)。 class PetSummary { const PetSummary({ required this.petId, required this.latestWeight, required this.vaccinationProgress, required this.nextVaccination, required this.monthlyExpense, }); factory PetSummary.fromJson(Map json) { return PetSummary( petId: json['petId'] as String, latestWeight: json['latestWeight'] == null ? null : SummaryLatestWeight.fromJson( json['latestWeight'] as Map, ), vaccinationProgress: json['vaccinationProgress'] == null ? null : SummaryVaccinationProgress.fromJson( json['vaccinationProgress'] as Map, ), nextVaccination: json['nextVaccination'] == null ? null : SummaryNextVaccination.fromJson( json['nextVaccination'] as Map, ), monthlyExpense: SummaryMonthlyExpense.fromJson( json['monthlyExpense'] as Map, ), ); } final String petId; final SummaryLatestWeight? latestWeight; final SummaryVaccinationProgress? vaccinationProgress; final SummaryNextVaccination? nextVaccination; final SummaryMonthlyExpense monthlyExpense; }