Rebrand from Casera/MyCrib to honeyDue

Total rebrand across KMM project:
- Kotlin package: com.example.casera -> com.tt.honeyDue (dirs + declarations)
- Gradle: rootProject.name, namespace, applicationId
- Android: manifest, strings.xml (all languages), widget resources
- iOS: pbxproj bundle IDs, Info.plist, entitlements, xcconfig
- iOS directories: Casera/ -> HoneyDue/, CaseraTests/ -> HoneyDueTests/, etc.
- Swift source: all class/struct/enum renames
- Deep links: casera:// -> honeydue://, .casera -> .honeydue
- App icons replaced with honeyDue honeycomb icon
- Domains: casera.treytartt.com -> honeyDue.treytartt.com
- Bundle IDs: com.tt.casera -> com.tt.honeyDue
- Database table names preserved

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-03-07 06:33:57 -06:00
parent 9c574c4343
commit 1e2adf7660
450 changed files with 1730 additions and 1788 deletions
@@ -0,0 +1,70 @@
package com.tt.honeyDue.models
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
/**
* Shared encoder/decoder for `.honeydue` payloads across Android and iOS.
*
* This keeps package JSON shape in one place while each platform owns
* native share-sheet presentation details.
*/
object honeyDueShareCodec {
private val json = Json {
prettyPrint = true
ignoreUnknownKeys = true
encodeDefaults = true
}
fun encodeContractorPackage(contractor: Contractor, exportedBy: String? = null): String {
return encodeSharedContractor(contractor.toSharedContractor(exportedBy))
}
fun encodeSharedContractor(sharedContractor: SharedContractor): String {
return json.encodeToString(SharedContractor.serializer(), sharedContractor)
}
fun encodeSharedResidence(sharedResidence: SharedResidence): String {
return json.encodeToString(SharedResidence.serializer(), sharedResidence)
}
fun decodeSharedContractorOrNull(jsonContent: String): SharedContractor? {
return try {
json.decodeFromString(SharedContractor.serializer(), jsonContent)
} catch (_: Exception) {
null
}
}
fun decodeSharedResidenceOrNull(jsonContent: String): SharedResidence? {
return try {
json.decodeFromString(SharedResidence.serializer(), jsonContent)
} catch (_: Exception) {
null
}
}
fun createContractorImportRequestOrNull(
jsonContent: String,
availableSpecialties: List<ContractorSpecialty>
): ContractorCreateRequest? {
val shared = decodeSharedContractorOrNull(jsonContent) ?: return null
val specialtyIds = shared.resolveSpecialtyIds(availableSpecialties)
return shared.toCreateRequest(specialtyIds)
}
fun extractResidenceShareCodeOrNull(jsonContent: String): String? {
return decodeSharedResidenceOrNull(jsonContent)?.shareCode
}
/**
* Build a filesystem-safe package filename with `.honeydue` extension.
*/
fun safeShareFileName(displayName: String): String {
val safeName = displayName
.replace(" ", "_")
.replace("/", "-")
.take(50)
return "$safeName.honeydue"
}
}
@@ -0,0 +1,100 @@
package com.tt.honeyDue.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ContractorUser(
val id: Int,
val username: String,
@SerialName("first_name") val firstName: String? = null,
@SerialName("last_name") val lastName: String? = null
)
@Serializable
data class Contractor(
val id: Int,
@SerialName("residence_id") val residenceId: Int? = null,
@SerialName("created_by_id") val createdById: Int,
@SerialName("added_by") val addedBy: Int,
@SerialName("created_by") val createdBy: ContractorUser? = null,
val name: String,
val company: String? = null,
val phone: String? = null,
val email: String? = null,
val website: String? = null,
val notes: String? = null,
@SerialName("street_address") val streetAddress: String? = null,
val city: String? = null,
@SerialName("state_province") val stateProvince: String? = null,
@SerialName("postal_code") val postalCode: String? = null,
val specialties: List<ContractorSpecialty> = emptyList(),
val rating: Double? = null,
@SerialName("is_favorite") val isFavorite: Boolean = false,
@SerialName("is_active") val isActive: Boolean = true,
@SerialName("task_count") val taskCount: Int = 0,
@SerialName("created_at") val createdAt: String,
@SerialName("updated_at") val updatedAt: String
)
@Serializable
data class ContractorCreateRequest(
val name: String,
@SerialName("residence_id") val residenceId: Int? = null,
val company: String? = null,
val phone: String? = null,
val email: String? = null,
val website: String? = null,
@SerialName("street_address") val streetAddress: String? = null,
val city: String? = null,
@SerialName("state_province") val stateProvince: String? = null,
@SerialName("postal_code") val postalCode: String? = null,
val rating: Double? = null,
@SerialName("is_favorite") val isFavorite: Boolean = false,
val notes: String? = null,
@SerialName("specialty_ids") val specialtyIds: List<Int>? = null
)
@Serializable
data class ContractorUpdateRequest(
val name: String? = null,
@SerialName("residence_id") val residenceId: Int? = null,
val company: String? = null,
val phone: String? = null,
val email: String? = null,
val website: String? = null,
@SerialName("street_address") val streetAddress: String? = null,
val city: String? = null,
@SerialName("state_province") val stateProvince: String? = null,
@SerialName("postal_code") val postalCode: String? = null,
val rating: Double? = null,
@SerialName("is_favorite") val isFavorite: Boolean? = null,
val notes: String? = null,
@SerialName("specialty_ids") val specialtyIds: List<Int>? = null
)
@Serializable
data class ContractorSummary(
val id: Int,
@SerialName("residence_id") val residenceId: Int? = null,
val name: String,
val company: String? = null,
val phone: String? = null,
val specialties: List<ContractorSpecialty> = emptyList(),
val rating: Double? = null,
@SerialName("is_favorite") val isFavorite: Boolean = false,
@SerialName("task_count") val taskCount: Int = 0
)
// Extension to convert full Contractor to ContractorSummary
fun Contractor.toSummary() = ContractorSummary(
id = id,
residenceId = residenceId,
name = name,
company = company,
phone = phone,
specialties = specialties,
rating = rating,
isFavorite = isFavorite,
taskCount = taskCount
)
@@ -0,0 +1,218 @@
package com.tt.honeyDue.models
import com.tt.honeyDue.data.DataManager
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* User reference for task-related responses - matching Go API TaskUserResponse
*/
@Serializable
data class TaskUserResponse(
val id: Int,
val username: String,
val email: String,
@SerialName("first_name") val firstName: String = "",
@SerialName("last_name") val lastName: String = ""
) {
val displayName: String
get() = when {
firstName.isNotBlank() && lastName.isNotBlank() -> "$firstName $lastName"
firstName.isNotBlank() -> firstName
lastName.isNotBlank() -> lastName
else -> username
}
}
/**
* Task response matching Go API TaskResponse
*/
@Serializable
data class TaskResponse(
val id: Int,
@SerialName("residence_id") val residenceId: Int,
@SerialName("created_by_id") val createdById: Int,
@SerialName("created_by") val createdBy: TaskUserResponse? = null,
@SerialName("assigned_to_id") val assignedToId: Int? = null,
@SerialName("assigned_to") val assignedTo: TaskUserResponse? = null,
val title: String,
val description: String = "",
@SerialName("category_id") val categoryId: Int? = null,
val category: TaskCategory? = null,
@SerialName("priority_id") val priorityId: Int? = null,
val priority: TaskPriority? = null,
@SerialName("in_progress") val inProgress: Boolean = false,
@SerialName("frequency_id") val frequencyId: Int? = null,
val frequency: TaskFrequency? = null,
@SerialName("custom_interval_days") val customIntervalDays: Int? = null, // For "Custom" frequency, user-specified days
@SerialName("due_date") val dueDate: String? = null,
@SerialName("next_due_date") val nextDueDate: String? = null, // For recurring tasks, updated after each completion
@SerialName("estimated_cost") val estimatedCost: Double? = null,
@SerialName("actual_cost") val actualCost: Double? = null,
@SerialName("contractor_id") val contractorId: Int? = null,
@SerialName("is_cancelled") val isCancelled: Boolean = false,
@SerialName("is_archived") val isArchived: Boolean = false,
@SerialName("parent_task_id") val parentTaskId: Int? = null,
@SerialName("completion_count") val completionCount: Int = 0,
@SerialName("kanban_column") val kanbanColumn: String? = null, // Which kanban column this task belongs to
val completions: List<TaskCompletionResponse> = emptyList(),
@SerialName("created_at") val createdAt: String,
@SerialName("updated_at") val updatedAt: String
) {
// Helper for backwards compatibility with old code
val archived: Boolean get() = isArchived
// Aliases for backwards compatibility with UI code expecting old field names
val residence: Int get() = residenceId
// Helper to get created by username
val createdByUsername: String get() = createdBy?.displayName ?: ""
// Computed properties for UI compatibility - resolves from cache if not embedded in response
// This allows the API to skip Preloading these lookups for performance
val categoryName: String? get() = category?.name ?: DataManager.getTaskCategory(categoryId)?.name
val categoryDescription: String? get() = category?.description ?: DataManager.getTaskCategory(categoryId)?.description
val frequencyName: String? get() = frequency?.name ?: DataManager.getTaskFrequency(frequencyId)?.name
val frequencyDisplayName: String? get() = frequency?.displayName ?: DataManager.getTaskFrequency(frequencyId)?.displayName
val frequencyDaySpan: Int? get() = frequency?.days ?: DataManager.getTaskFrequency(frequencyId)?.days
val priorityName: String? get() = priority?.name ?: DataManager.getTaskPriority(priorityId)?.name
val priorityDisplayName: String? get() = priority?.displayName ?: DataManager.getTaskPriority(priorityId)?.displayName
// Effective due date: use nextDueDate if set (for recurring tasks), otherwise use dueDate
val effectiveDueDate: String? get() = nextDueDate ?: dueDate
// Fields that don't exist in Go API - return null/default
val nextScheduledDate: String? get() = nextDueDate // Now we have this from the API
val showCompletedButton: Boolean get() = true // Always show complete button since status is now just in_progress boolean
val daySpan: Int? get() = frequency?.days ?: DataManager.getTaskFrequency(frequencyId)?.days
val notifyDays: Int? get() = null // Not in Go API
}
/**
* Task completion response matching Go API TaskCompletionResponse
*/
@Serializable
data class TaskCompletionResponse(
val id: Int,
@SerialName("task_id") val taskId: Int,
@SerialName("completed_by") val completedBy: TaskUserResponse? = null,
@SerialName("completed_at") val completedAt: String,
val notes: String = "",
@SerialName("actual_cost") val actualCost: Double? = null,
val rating: Int? = null,
val images: List<TaskCompletionImage> = emptyList(),
@SerialName("created_at") val createdAt: String,
@SerialName("task") val updatedTask: TaskResponse? = null // Updated task after completion (for UI kanban update)
) {
// Helper for backwards compatibility
val completionDate: String get() = completedAt
val completedByName: String? get() = completedBy?.displayName
// Backwards compatibility for UI that expects these fields
val task: Int get() = taskId
val contractor: Int? get() = null // Not in Go API - would need to be fetched separately
val contractorDetails: ContractorMinimal? get() = null // Not in Go API
}
/**
* Task create request matching Go API CreateTaskRequest
*/
@Serializable
data class TaskCreateRequest(
@SerialName("residence_id") val residenceId: Int,
val title: String,
val description: String? = null,
@SerialName("category_id") val categoryId: Int? = null,
@SerialName("priority_id") val priorityId: Int? = null,
@SerialName("in_progress") val inProgress: Boolean = false,
@SerialName("frequency_id") val frequencyId: Int? = null,
@SerialName("custom_interval_days") val customIntervalDays: Int? = null, // For "Custom" frequency
@SerialName("assigned_to_id") val assignedToId: Int? = null,
@SerialName("due_date") val dueDate: String? = null,
@SerialName("estimated_cost") val estimatedCost: Double? = null,
@SerialName("contractor_id") val contractorId: Int? = null
)
/**
* Task update request matching Go API UpdateTaskRequest
* All fields are optional - only provided fields will be updated.
*/
@Serializable
data class TaskUpdateRequest(
val title: String? = null,
val description: String? = null,
@SerialName("category_id") val categoryId: Int? = null,
@SerialName("priority_id") val priorityId: Int? = null,
@SerialName("in_progress") val inProgress: Boolean? = null,
@SerialName("frequency_id") val frequencyId: Int? = null,
@SerialName("custom_interval_days") val customIntervalDays: Int? = null, // For "Custom" frequency
@SerialName("assigned_to_id") val assignedToId: Int? = null,
@SerialName("due_date") val dueDate: String? = null,
@SerialName("estimated_cost") val estimatedCost: Double? = null,
@SerialName("actual_cost") val actualCost: Double? = null,
@SerialName("contractor_id") val contractorId: Int? = null
)
/**
* Task action response (for cancel, archive, mark in progress, etc.)
*/
@Serializable
data class TaskActionResponse(
val message: String,
val task: TaskResponse
)
/**
* Kanban column response matching Go API KanbanColumnResponse
*/
@Serializable
data class TaskColumn(
val name: String,
@SerialName("display_name") val displayName: String,
@SerialName("button_types") val buttonTypes: List<String>,
val icons: Map<String, String>,
val color: String,
val tasks: List<TaskResponse>,
val count: Int
)
/**
* Kanban board response matching Go API KanbanBoardResponse
* NOTE: Summary statistics are calculated client-side from kanban data
*/
@Serializable
data class TaskColumnsResponse(
val columns: List<TaskColumn>,
@SerialName("days_threshold") val daysThreshold: Int,
@SerialName("residence_id") val residenceId: String
)
/**
* Task patch request for partial updates (status changes, archive/unarchive)
*/
@Serializable
data class TaskPatchRequest(
@SerialName("in_progress") val inProgress: Boolean? = null,
@SerialName("is_archived") val archived: Boolean? = null,
@SerialName("is_cancelled") val cancelled: Boolean? = null
)
/**
* Task completion image model
*/
@Serializable
data class TaskCompletionImage(
val id: Int,
@SerialName("image_url") val imageUrl: String,
@SerialName("media_url") val mediaUrl: String? = null, // Authenticated endpoint: /api/media/completion-image/{id}
val caption: String? = null,
@SerialName("uploaded_at") val uploadedAt: String? = null
) {
// Alias for backwards compatibility
val image: String get() = imageUrl
}
// Type aliases for backwards compatibility with existing code
typealias CustomTask = TaskResponse
typealias TaskDetail = TaskResponse
typealias TaskCompletion = TaskCompletionResponse
@@ -0,0 +1,161 @@
package com.tt.honeyDue.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class WarrantyStatus(
@SerialName("status_text") val statusText: String,
@SerialName("status_color") val statusColor: String,
@SerialName("is_expiring_soon") val isExpiringSoon: Boolean
)
@Serializable
data class DocumentUser(
val id: Int,
val username: String = "",
@SerialName("first_name") val firstName: String = "",
@SerialName("last_name") val lastName: String = ""
)
@Serializable
data class DocumentImage(
val id: Int? = null,
@SerialName("image_url") val imageUrl: String,
@SerialName("media_url") val mediaUrl: String? = null, // Authenticated endpoint: /api/media/document-image/{id}
val caption: String? = null,
@SerialName("uploaded_at") val uploadedAt: String? = null
)
@Serializable
data class DocumentActionResponse(
val message: String,
val document: Document
)
@Serializable
data class Document(
val id: Int? = null,
val title: String,
@SerialName("document_type") val documentType: String,
val description: String? = null,
@SerialName("file_url") val fileUrl: String? = null, // URL to the file
@SerialName("media_url") val mediaUrl: String? = null, // Authenticated endpoint: /api/media/document/{id}
@SerialName("file_name") val fileName: String? = null,
@SerialName("file_size") val fileSize: Int? = null,
@SerialName("mime_type") val mimeType: String? = null,
// Warranty-specific fields
@SerialName("model_number") val modelNumber: String? = null,
@SerialName("serial_number") val serialNumber: String? = null,
val vendor: String? = null,
@SerialName("purchase_date") val purchaseDate: String? = null,
@SerialName("purchase_price") val purchasePrice: String? = null,
@SerialName("expiry_date") val expiryDate: String? = null,
// Relationships
@SerialName("residence_id") val residenceId: Int? = null,
val residence: Int,
@SerialName("created_by_id") val createdById: Int? = null,
@SerialName("created_by") val createdBy: DocumentUser? = null,
@SerialName("task_id") val taskId: Int? = null,
// Images
val images: List<DocumentImage> = emptyList(),
// Status
@SerialName("is_active") val isActive: Boolean = true,
@SerialName("created_at") val createdAt: String? = null,
@SerialName("updated_at") val updatedAt: String? = null,
// Client-side convenience fields (not from backend, kept for UI compatibility)
// These fields are populated client-side or kept optional so deserialization doesn't fail
val category: String? = null,
val tags: String? = null,
val notes: String? = null,
@SerialName("item_name") val itemName: String? = null,
val provider: String? = null,
@SerialName("provider_contact") val providerContact: String? = null,
@SerialName("claim_phone") val claimPhone: String? = null,
@SerialName("claim_email") val claimEmail: String? = null,
@SerialName("claim_website") val claimWebsite: String? = null,
@SerialName("start_date") val startDate: String? = null,
@SerialName("days_until_expiration") val daysUntilExpiration: Int? = null,
@SerialName("warranty_status") val warrantyStatus: WarrantyStatus? = null
) {
// Backward-compatible alias: endDate maps to expiryDate
val endDate: String? get() = expiryDate
}
@Serializable
data class DocumentCreateRequest(
val title: String,
@SerialName("document_type") val documentType: String,
val description: String? = null,
// Note: file will be handled separately as multipart/form-data
// Warranty-specific fields
@SerialName("model_number") val modelNumber: String? = null,
@SerialName("serial_number") val serialNumber: String? = null,
val vendor: String? = null,
@SerialName("purchase_date") val purchaseDate: String? = null,
@SerialName("purchase_price") val purchasePrice: String? = null,
@SerialName("expiry_date") val expiryDate: String? = null,
// Relationships
@SerialName("residence_id") val residenceId: Int,
@SerialName("task_id") val taskId: Int? = null,
// Images
@SerialName("image_urls") val imageUrls: List<String>? = null
)
@Serializable
data class DocumentUpdateRequest(
val title: String? = null,
@SerialName("document_type") val documentType: String? = null,
val description: String? = null,
// Warranty-specific fields
@SerialName("model_number") val modelNumber: String? = null,
@SerialName("serial_number") val serialNumber: String? = null,
val vendor: String? = null,
@SerialName("purchase_date") val purchaseDate: String? = null,
@SerialName("purchase_price") val purchasePrice: String? = null,
@SerialName("expiry_date") val expiryDate: String? = null,
// Relationships
@SerialName("task_id") val taskId: Int? = null
)
// Removed: DocumentListResponse - no longer using paginated responses
// API now returns List<Document> directly
// Document type choices
enum class DocumentType(val value: String, val displayName: String) {
WARRANTY("warranty", "Warranty"),
MANUAL("manual", "User Manual"),
RECEIPT("receipt", "Receipt/Invoice"),
INSPECTION("inspection", "Inspection Report"),
PERMIT("permit", "Permit"),
DEED("deed", "Deed/Title"),
INSURANCE("insurance", "Insurance"),
CONTRACT("contract", "Contract"),
PHOTO("photo", "Photo"),
OTHER("other", "Other");
companion object {
fun fromValue(value: String): DocumentType {
return values().find { it.value == value } ?: OTHER
}
}
}
// Document/Warranty category choices
enum class DocumentCategory(val value: String, val displayName: String) {
APPLIANCE("appliance", "Appliance"),
HVAC("hvac", "HVAC"),
PLUMBING("plumbing", "Plumbing"),
ELECTRICAL("electrical", "Electrical"),
ROOFING("roofing", "Roofing"),
STRUCTURAL("structural", "Structural"),
LANDSCAPING("landscaping", "Landscaping"),
GENERAL("general", "General"),
OTHER("other", "Other");
companion object {
fun fromValue(value: String): DocumentCategory {
return values().find { it.value == value } ?: OTHER
}
}
}
@@ -0,0 +1,17 @@
package com.tt.honeyDue.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Error response model that handles the backend's error format.
* The backend returns: {"error": "message"}
* All fields except 'error' are optional for backwards compatibility.
*/
@Serializable
data class ErrorResponse(
val error: String,
val detail: String? = null,
@SerialName("status_code") val statusCode: Int? = null,
val errors: Map<String, List<String>>? = null
)
@@ -0,0 +1,140 @@
package com.tt.honeyDue.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Residence type lookup - matching Go API
* Note: Go API returns arrays directly, no wrapper
*/
@Serializable
data class ResidenceType(
val id: Int,
val name: String
)
/**
* Task frequency lookup - matching Go API TaskFrequencyResponse
*/
@Serializable
data class TaskFrequency(
val id: Int,
val name: String,
val days: Int? = null,
@SerialName("display_order") val displayOrder: Int = 0
) {
// Helper for display
val displayName: String
get() = name
}
/**
* Task priority lookup - matching Go API TaskPriorityResponse
*/
@Serializable
data class TaskPriority(
val id: Int,
val name: String,
val level: Int = 0,
val color: String = "",
@SerialName("display_order") val displayOrder: Int = 0
) {
// Helper for display
val displayName: String
get() = name
}
/**
* Task category lookup - matching Go API TaskCategoryResponse
*/
@Serializable
data class TaskCategory(
val id: Int,
val name: String,
val description: String = "",
val icon: String = "",
val color: String = "",
@SerialName("display_order") val displayOrder: Int = 0
)
/**
* Contractor specialty lookup
*/
@Serializable
data class ContractorSpecialty(
val id: Int,
val name: String,
val description: String? = null,
val icon: String? = null,
@SerialName("display_order") val displayOrder: Int = 0
)
/**
* Minimal contractor info for task references
*/
@Serializable
data class ContractorMinimal(
val id: Int,
val name: String,
val company: String? = null
)
/**
* Static data response - all lookups in one call
* Note: This may need adjustment based on Go API implementation
*/
@Serializable
data class StaticDataResponse(
@SerialName("residence_types") val residenceTypes: List<ResidenceType>,
@SerialName("task_frequencies") val taskFrequencies: List<TaskFrequency>,
@SerialName("task_priorities") val taskPriorities: List<TaskPriority>,
@SerialName("task_categories") val taskCategories: List<TaskCategory>,
@SerialName("contractor_specialties") val contractorSpecialties: List<ContractorSpecialty>
)
/**
* Unified seeded data response - all lookups + task templates in one call
* Supports ETag-based conditional fetching for efficient caching
*/
@Serializable
data class SeededDataResponse(
@SerialName("residence_types") val residenceTypes: List<ResidenceType>,
@SerialName("task_categories") val taskCategories: List<TaskCategory>,
@SerialName("task_priorities") val taskPriorities: List<TaskPriority>,
@SerialName("task_frequencies") val taskFrequencies: List<TaskFrequency>,
@SerialName("contractor_specialties") val contractorSpecialties: List<ContractorSpecialty>,
@SerialName("task_templates") val taskTemplates: TaskTemplatesGroupedResponse
)
// Legacy wrapper responses for backward compatibility
// These can be removed once all code is migrated to use arrays directly
@Serializable
data class ResidenceTypeResponse(
val count: Int,
val results: List<ResidenceType>
)
@Serializable
data class TaskFrequencyResponse(
val count: Int,
val results: List<TaskFrequency>
)
@Serializable
data class TaskPriorityResponse(
val count: Int,
val results: List<TaskPriority>
)
@Serializable
data class TaskCategoryResponse(
val count: Int,
val results: List<TaskCategory>
)
@Serializable
data class ContractorSpecialtyResponse(
val count: Int,
val results: List<ContractorSpecialty>
)
@@ -0,0 +1,120 @@
package com.tt.honeyDue.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class DeviceRegistrationRequest(
@SerialName("device_id")
val deviceId: String,
@SerialName("registration_id")
val registrationId: String,
val platform: String, // "android" or "ios"
val name: String? = null
)
@Serializable
data class DeviceRegistrationResponse(
val id: Int,
val name: String? = null,
@SerialName("device_id")
val deviceId: String,
@SerialName("registration_id")
val registrationId: String,
val platform: String,
val active: Boolean,
@SerialName("date_created")
val dateCreated: String
)
@Serializable
data class NotificationPreference(
@SerialName("task_due_soon")
val taskDueSoon: Boolean = true,
@SerialName("task_overdue")
val taskOverdue: Boolean = true,
@SerialName("task_completed")
val taskCompleted: Boolean = true,
@SerialName("task_assigned")
val taskAssigned: Boolean = true,
@SerialName("residence_shared")
val residenceShared: Boolean = true,
@SerialName("warranty_expiring")
val warrantyExpiring: Boolean = true,
@SerialName("daily_digest")
val dailyDigest: Boolean = true,
// Email preferences
@SerialName("email_task_completed")
val emailTaskCompleted: Boolean = true,
// Custom notification times (UTC hour 0-23, null means use system default)
@SerialName("task_due_soon_hour")
val taskDueSoonHour: Int? = null,
@SerialName("task_overdue_hour")
val taskOverdueHour: Int? = null,
@SerialName("warranty_expiring_hour")
val warrantyExpiringHour: Int? = null,
@SerialName("daily_digest_hour")
val dailyDigestHour: Int? = null
)
@Serializable
data class UpdateNotificationPreferencesRequest(
@SerialName("task_due_soon")
val taskDueSoon: Boolean? = null,
@SerialName("task_overdue")
val taskOverdue: Boolean? = null,
@SerialName("task_completed")
val taskCompleted: Boolean? = null,
@SerialName("task_assigned")
val taskAssigned: Boolean? = null,
@SerialName("residence_shared")
val residenceShared: Boolean? = null,
@SerialName("warranty_expiring")
val warrantyExpiring: Boolean? = null,
@SerialName("daily_digest")
val dailyDigest: Boolean? = null,
// Email preferences
@SerialName("email_task_completed")
val emailTaskCompleted: Boolean? = null,
// Custom notification times (UTC hour 0-23)
@SerialName("task_due_soon_hour")
val taskDueSoonHour: Int? = null,
@SerialName("task_overdue_hour")
val taskOverdueHour: Int? = null,
@SerialName("warranty_expiring_hour")
val warrantyExpiringHour: Int? = null,
@SerialName("daily_digest_hour")
val dailyDigestHour: Int? = null
)
@Serializable
data class Notification(
val id: Int,
@SerialName("notification_type")
val notificationType: String,
val title: String,
val body: String,
val data: Map<String, String> = emptyMap(),
val sent: Boolean,
@SerialName("sent_at")
val sentAt: String? = null,
val read: Boolean,
@SerialName("read_at")
val readAt: String? = null,
@SerialName("created_at")
val createdAt: String,
@SerialName("task_id")
val taskId: Int? = null
)
@Serializable
data class NotificationListResponse(
val count: Int,
val results: List<Notification>
)
@Serializable
data class UnreadCountResponse(
@SerialName("unread_count")
val unreadCount: Int
)
@@ -0,0 +1,269 @@
package com.tt.honeyDue.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* User reference for residence responses - matching Go API ResidenceUserResponse
*/
@Serializable
data class ResidenceUserResponse(
val id: Int,
val username: String,
val email: String,
@SerialName("first_name") val firstName: String = "",
@SerialName("last_name") val lastName: String = ""
) {
val displayName: String
get() = when {
firstName.isNotBlank() && lastName.isNotBlank() -> "$firstName $lastName"
firstName.isNotBlank() -> firstName
lastName.isNotBlank() -> lastName
else -> username
}
}
/**
* Residence response matching Go API ResidenceResponse
*/
@Serializable
data class ResidenceResponse(
val id: Int,
@SerialName("owner_id") val ownerId: Int,
val owner: ResidenceUserResponse? = null,
val users: List<ResidenceUserResponse> = emptyList(),
val name: String,
@SerialName("property_type_id") val propertyTypeId: Int? = null,
@SerialName("property_type") val propertyType: ResidenceType? = null,
@SerialName("street_address") val streetAddress: String = "",
@SerialName("apartment_unit") val apartmentUnit: String = "",
val city: String = "",
@SerialName("state_province") val stateProvince: String = "",
@SerialName("postal_code") val postalCode: String = "",
val country: String = "",
val bedrooms: Int? = null,
val bathrooms: Double? = null,
@SerialName("square_footage") val squareFootage: Int? = null,
@SerialName("lot_size") val lotSize: Double? = null,
@SerialName("year_built") val yearBuilt: Int? = null,
val description: String = "",
@SerialName("purchase_date") val purchaseDate: String? = null,
@SerialName("purchase_price") val purchasePrice: Double? = null,
@SerialName("is_primary") val isPrimary: Boolean = false,
@SerialName("is_active") val isActive: Boolean = true,
@SerialName("overdue_count") val overdueCount: Int = 0,
@SerialName("created_at") val createdAt: String,
@SerialName("updated_at") val updatedAt: String
) {
// Helper to get owner username
val ownerUsername: String get() = owner?.displayName ?: ""
// Helper to get property type name
val propertyTypeName: String? get() = propertyType?.name
// Backwards compatibility for UI code
// Note: isPrimaryOwner requires comparing with current user - can't be computed here
// UI components should check ownerId == currentUserId instead
// Stub task summary for UI compatibility (Go API doesn't return this per-residence)
val taskSummary: ResidenceTaskSummary get() = ResidenceTaskSummary()
// Stub summary for UI compatibility
val summary: ResidenceSummaryResponse get() = ResidenceSummaryResponse(id = id, name = name)
}
/**
* Residence create request matching Go API CreateResidenceRequest
*/
@Serializable
data class ResidenceCreateRequest(
val name: String,
@SerialName("property_type_id") val propertyTypeId: Int? = null,
@SerialName("street_address") val streetAddress: String? = null,
@SerialName("apartment_unit") val apartmentUnit: String? = null,
val city: String? = null,
@SerialName("state_province") val stateProvince: String? = null,
@SerialName("postal_code") val postalCode: String? = null,
val country: String? = null,
val bedrooms: Int? = null,
val bathrooms: Double? = null,
@SerialName("square_footage") val squareFootage: Int? = null,
@SerialName("lot_size") val lotSize: Double? = null,
@SerialName("year_built") val yearBuilt: Int? = null,
val description: String? = null,
@SerialName("purchase_date") val purchaseDate: String? = null,
@SerialName("purchase_price") val purchasePrice: Double? = null,
@SerialName("is_primary") val isPrimary: Boolean? = null
)
/**
* Residence update request matching Go API UpdateResidenceRequest
*/
@Serializable
data class ResidenceUpdateRequest(
val name: String? = null,
@SerialName("property_type_id") val propertyTypeId: Int? = null,
@SerialName("street_address") val streetAddress: String? = null,
@SerialName("apartment_unit") val apartmentUnit: String? = null,
val city: String? = null,
@SerialName("state_province") val stateProvince: String? = null,
@SerialName("postal_code") val postalCode: String? = null,
val country: String? = null,
val bedrooms: Int? = null,
val bathrooms: Double? = null,
@SerialName("square_footage") val squareFootage: Int? = null,
@SerialName("lot_size") val lotSize: Double? = null,
@SerialName("year_built") val yearBuilt: Int? = null,
val description: String? = null,
@SerialName("purchase_date") val purchaseDate: String? = null,
@SerialName("purchase_price") val purchasePrice: Double? = null,
@SerialName("is_primary") val isPrimary: Boolean? = null
)
/**
* Share code response matching Go API ShareCodeResponse
*/
@Serializable
data class ShareCodeResponse(
val id: Int,
val code: String,
@SerialName("residence_id") val residenceId: Int,
@SerialName("created_by_id") val createdById: Int,
@SerialName("is_active") val isActive: Boolean,
@SerialName("expires_at") val expiresAt: String?,
@SerialName("created_at") val createdAt: String
)
/**
* Generate share code request
*/
@Serializable
data class GenerateShareCodeRequest(
@SerialName("expires_in_hours") val expiresInHours: Int? = null
)
/**
* Generate share code response matching Go API GenerateShareCodeResponse
*/
@Serializable
data class GenerateShareCodeResponse(
val message: String,
@SerialName("share_code") val shareCode: ShareCodeResponse
)
/**
* Join residence request
*/
@Serializable
data class JoinResidenceRequest(
val code: String
)
/**
* Join residence response matching Go API JoinResidenceResponse
*/
@Serializable
data class JoinResidenceResponse(
val message: String,
val residence: ResidenceResponse,
val summary: TotalSummary
)
/**
* Total summary for dashboard display
*/
@Serializable
data class TotalSummary(
@SerialName("total_residences") val totalResidences: Int = 0,
@SerialName("total_tasks") val totalTasks: Int = 0,
@SerialName("total_pending") val totalPending: Int = 0,
@SerialName("total_overdue") val totalOverdue: Int = 0,
@SerialName("tasks_due_next_week") val tasksDueNextWeek: Int = 0,
@SerialName("tasks_due_next_month") val tasksDueNextMonth: Int = 0
)
/**
* Generic wrapper for CRUD responses that include TotalSummary.
* Used for Task and TaskCompletion operations to eliminate extra API calls
* for updating dashboard stats.
*
* Usage examples:
* - WithSummaryResponse<TaskResponse> for task CRUD
* - WithSummaryResponse<TaskCompletionResponse> for completion CRUD
* - WithSummaryResponse<String> for delete operations (data = "task deleted")
*/
@Serializable
data class WithSummaryResponse<T>(
val data: T,
val summary: TotalSummary
)
/**
* My residences response - list of user's residences
* NOTE: Summary statistics are calculated client-side from kanban data
*/
@Serializable
data class MyResidencesResponse(
val residences: List<ResidenceResponse>
)
/**
* Residence summary response for dashboard
*/
@Serializable
data class ResidenceSummaryResponse(
val id: Int = 0,
val name: String = "",
@SerialName("task_count") val taskCount: Int = 0,
@SerialName("pending_count") val pendingCount: Int = 0,
@SerialName("overdue_count") val overdueCount: Int = 0
)
/**
* Task category summary for residence
*/
@Serializable
data class TaskCategorySummary(
val name: String,
@SerialName("display_name") val displayName: String,
val icons: TaskCategoryIcons,
val color: String,
val count: Int
)
/**
* Icons for task category (Android/iOS)
*/
@Serializable
data class TaskCategoryIcons(
val android: String = "",
val ios: String = ""
)
/**
* Task summary per residence (for UI backwards compatibility)
*/
@Serializable
data class ResidenceTaskSummary(
val categories: List<TaskCategorySummary> = emptyList()
)
/**
* Residence users response - API returns a flat list of all users with access
*/
typealias ResidenceUsersResponse = List<ResidenceUserResponse>
/**
* Remove user response
*/
@Serializable
data class RemoveUserResponse(
val message: String
)
// Type aliases for backwards compatibility with existing code
typealias Residence = ResidenceResponse
typealias ResidenceShareCode = ShareCodeResponse
typealias ResidenceUser = ResidenceUserResponse
typealias TaskSummary = ResidenceTaskSummary
typealias TaskColumnCategory = TaskCategorySummary
@@ -0,0 +1,173 @@
package com.tt.honeyDue.models
import kotlin.time.Clock
import kotlin.time.ExperimentalTime
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive
/**
* Package type identifiers for .honeydue files
*/
object honeyDuePackageType {
const val CONTRACTOR = "contractor"
const val RESIDENCE = "residence"
}
/**
* Data model for .honeydue file format used to share contractors between users.
* Contains only the data needed to recreate a contractor, without server-specific IDs.
*/
@Serializable
data class SharedContractor(
/** File format version for future compatibility */
val version: Int = 1,
/** Package type discriminator */
val type: String = honeyDuePackageType.CONTRACTOR,
val name: String,
val company: String? = null,
val phone: String? = null,
val email: String? = null,
val website: String? = null,
val notes: String? = null,
@SerialName("street_address")
val streetAddress: String? = null,
val city: String? = null,
@SerialName("state_province")
val stateProvince: String? = null,
@SerialName("postal_code")
val postalCode: String? = null,
/** Specialty names (not IDs) for cross-account compatibility */
@SerialName("specialty_names")
val specialtyNames: List<String> = emptyList(),
val rating: Double? = null,
@SerialName("is_favorite")
val isFavorite: Boolean = false,
/** ISO8601 timestamp when the contractor was exported */
@SerialName("exported_at")
val exportedAt: String? = null,
/** Username of the person who exported the contractor */
@SerialName("exported_by")
val exportedBy: String? = null
)
/**
* Data model for .honeydue file format used to share residences between users.
* Contains the share code needed to join the residence.
*/
@Serializable
data class SharedResidence(
/** File format version for future compatibility */
val version: Int = 1,
/** Package type discriminator */
val type: String = honeyDuePackageType.RESIDENCE,
/** The share code for joining the residence */
@SerialName("share_code")
val shareCode: String,
/** Name of the residence being shared */
@SerialName("residence_name")
val residenceName: String,
/** Email of the person sharing the residence */
@SerialName("shared_by")
val sharedBy: String? = null,
/** ISO8601 timestamp when the code expires */
@SerialName("expires_at")
val expiresAt: String? = null,
/** ISO8601 timestamp when the package was created */
@SerialName("exported_at")
val exportedAt: String? = null,
/** Username of the person who created the package */
@SerialName("exported_by")
val exportedBy: String? = null
)
/**
* Detect the type of a .honeydue package from its JSON content.
* Returns null if the type cannot be determined.
*/
fun detecthoneyDuePackageType(jsonContent: String): String? {
return try {
val json = Json { ignoreUnknownKeys = true }
val jsonObject = json.decodeFromString<JsonObject>(jsonContent)
jsonObject["type"]?.jsonPrimitive?.content ?: honeyDuePackageType.CONTRACTOR // Default for backward compatibility
} catch (e: Exception) {
null
}
}
/**
* Convert a full Contractor to SharedContractor for export.
*/
@OptIn(ExperimentalTime::class)
fun Contractor.toSharedContractor(exportedBy: String? = null): SharedContractor {
return SharedContractor(
version = 1,
type = honeyDuePackageType.CONTRACTOR,
name = name,
company = company,
phone = phone,
email = email,
website = website,
notes = notes,
streetAddress = streetAddress,
city = city,
stateProvince = stateProvince,
postalCode = postalCode,
specialtyNames = specialties.map { it.name },
rating = rating,
isFavorite = isFavorite,
exportedAt = Clock.System.now().toString(),
exportedBy = exportedBy
)
}
/**
* Convert SharedContractor to ContractorCreateRequest for import.
* @param specialtyIds The resolved specialty IDs from the importing account's lookup data
*/
fun SharedContractor.toCreateRequest(specialtyIds: List<Int>): ContractorCreateRequest {
return ContractorCreateRequest(
name = name,
residenceId = null, // Imported contractors have no residence association
company = company,
phone = phone,
email = email,
website = website,
streetAddress = streetAddress,
city = city,
stateProvince = stateProvince,
postalCode = postalCode,
rating = rating,
isFavorite = isFavorite,
notes = notes,
specialtyIds = specialtyIds.ifEmpty { null }
)
}
/**
* Resolve specialty names to IDs using the available specialties in the importing account.
* Case-insensitive matching.
*/
fun SharedContractor.resolveSpecialtyIds(availableSpecialties: List<ContractorSpecialty>): List<Int> {
return specialtyNames.mapNotNull { name ->
availableSpecialties.find { specialty ->
specialty.name.equals(name, ignoreCase = true)
}?.id
}
}
@@ -0,0 +1,101 @@
package com.tt.honeyDue.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class SubscriptionStatus(
val tier: String = "free",
@SerialName("is_active") val isActive: Boolean = false,
@SerialName("subscribed_at") val subscribedAt: String? = null,
@SerialName("expires_at") val expiresAt: String? = null,
@SerialName("auto_renew") val autoRenew: Boolean = true,
val usage: UsageStats,
val limits: Map<String, TierLimits>, // {"free": {...}, "pro": {...}}
@SerialName("limitations_enabled") val limitationsEnabled: Boolean = false, // Master toggle
@SerialName("trial_start") val trialStart: String? = null,
@SerialName("trial_end") val trialEnd: String? = null,
@SerialName("trial_active") val trialActive: Boolean = false,
@SerialName("subscription_source") val subscriptionSource: String? = null,
)
@Serializable
data class UsageStats(
@SerialName("properties_count") val propertiesCount: Int,
@SerialName("tasks_count") val tasksCount: Int,
@SerialName("contractors_count") val contractorsCount: Int,
@SerialName("documents_count") val documentsCount: Int
)
@Serializable
data class TierLimits(
val properties: Int? = null, // null = unlimited
val tasks: Int? = null,
val contractors: Int? = null,
val documents: Int? = null
)
@Serializable
data class UpgradeTriggerData(
val title: String,
val message: String,
@SerialName("promo_html") val promoHtml: String? = null,
@SerialName("button_text") val buttonText: String
)
@Serializable
data class FeatureBenefit(
@SerialName("feature_name") val featureName: String,
@SerialName("free_tier_text") val freeTierText: String,
@SerialName("pro_tier_text") val proTierText: String
)
@Serializable
data class Promotion(
@SerialName("promotion_id") val promotionId: String,
val title: String,
val message: String,
val link: String? = null
)
@Serializable
data class ReceiptVerificationRequest(
@SerialName("receipt_data") val receiptData: String
)
@Serializable
data class PurchaseVerificationRequest(
@SerialName("purchase_token") val purchaseToken: String,
@SerialName("product_id") val productId: String
)
/**
* Nested subscription info returned by backend purchase/restore endpoints.
*/
@Serializable
data class VerificationSubscriptionInfo(
val tier: String = "",
@SerialName("subscribed_at") val subscribedAt: String? = null,
@SerialName("expires_at") val expiresAt: String? = null,
@SerialName("auto_renew") val autoRenew: Boolean = true,
@SerialName("cancelled_at") val cancelledAt: String? = null,
val platform: String = "",
@SerialName("is_active") val isActive: Boolean = false,
@SerialName("is_pro") val isPro: Boolean = false
)
/**
* Response from backend purchase/restore endpoints.
* Backend returns: { "message": "...", "subscription": { "tier": "pro", ... } }
*/
@Serializable
data class VerificationResponse(
val message: String = "",
val subscription: VerificationSubscriptionInfo? = null
) {
/** Backward-compatible: success when subscription is present */
val success: Boolean get() = subscription != null
/** Backward-compatible: tier extracted from nested subscription */
val tier: String? get() = subscription?.tier
}
@@ -0,0 +1,18 @@
package com.tt.honeyDue.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Task completion create request matching Go API CreateTaskCompletionRequest
*/
@Serializable
data class TaskCompletionCreateRequest(
@SerialName("task_id") val taskId: Int,
@SerialName("completed_at") val completedAt: String? = null, // Defaults to now on server
val notes: String? = null,
@SerialName("actual_cost") val actualCost: Double? = null,
val rating: Int? = null, // 1-5 star rating
@SerialName("image_urls") val imageUrls: List<String>? = null // Multiple image URLs
)
@@ -0,0 +1,56 @@
package com.tt.honeyDue.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Represents a task template fetched from the backend API.
* Users can select these when adding a new task to auto-fill form fields.
*/
@Serializable
data class TaskTemplate(
val id: Int,
val title: String,
val description: String = "",
@SerialName("category_id") val categoryId: Int? = null,
val category: TaskCategory? = null,
@SerialName("frequency_id") val frequencyId: Int? = null,
val frequency: TaskFrequency? = null,
@SerialName("icon_ios") val iconIos: String = "",
@SerialName("icon_android") val iconAndroid: String = "",
val tags: List<String> = emptyList(),
@SerialName("display_order") val displayOrder: Int = 0,
@SerialName("is_active") val isActive: Boolean = true
) {
/**
* Human-readable frequency display
*/
val frequencyDisplay: String
get() = frequency?.displayName ?: "One time"
/**
* Category name for display
*/
val categoryName: String
get() = category?.name ?: "Uncategorized"
}
/**
* Response for grouped templates by category
*/
@Serializable
data class TaskTemplateCategoryGroup(
@SerialName("category_name") val categoryName: String,
@SerialName("category_id") val categoryId: Int? = null,
val templates: List<TaskTemplate>,
val count: Int
)
/**
* Response for all templates grouped by category
*/
@Serializable
data class TaskTemplatesGroupedResponse(
val categories: List<TaskTemplateCategoryGroup>,
@SerialName("total_count") val totalCount: Int
)
@@ -0,0 +1,208 @@
package com.tt.honeyDue.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* User model matching Go API UserResponse/CurrentUserResponse
*/
@Serializable
data class User(
val id: Int,
val username: String,
val email: String,
@SerialName("first_name") val firstName: String = "",
@SerialName("last_name") val lastName: String = "",
@SerialName("is_active") val isActive: Boolean = true,
@SerialName("date_joined") val dateJoined: String,
@SerialName("last_login") val lastLogin: String? = null,
// Profile is included in CurrentUserResponse (/auth/me)
val profile: UserProfile? = null,
// Verified is returned directly in LoginResponse, and also in profile for CurrentUserResponse
@SerialName("verified") private val _verified: Boolean = false
) {
// Computed property for display name
val displayName: String
get() = when {
firstName.isNotBlank() && lastName.isNotBlank() -> "$firstName $lastName"
firstName.isNotBlank() -> firstName
lastName.isNotBlank() -> lastName
else -> username
}
// Check if user is verified - from direct field (login) OR from profile (currentUser)
val isVerified: Boolean
get() = _verified || (profile?.verified ?: false)
// Alias for backwards compatibility
val verified: Boolean
get() = isVerified
}
/**
* User profile model matching Go API UserProfileResponse
*/
@Serializable
data class UserProfile(
val id: Int,
@SerialName("user_id") val userId: Int,
val verified: Boolean = false,
val bio: String = "",
@SerialName("phone_number") val phoneNumber: String = "",
@SerialName("date_of_birth") val dateOfBirth: String? = null,
@SerialName("profile_picture") val profilePicture: String = ""
)
/**
* Register request matching Go API
*/
@Serializable
data class RegisterRequest(
val username: String,
val email: String,
val password: String,
@SerialName("first_name") val firstName: String? = null,
@SerialName("last_name") val lastName: String? = null
)
/**
* Login request matching Go API
*/
@Serializable
data class LoginRequest(
val username: String,
val password: String
)
/**
* Auth response for login - matching Go API LoginResponse
*/
@Serializable
data class AuthResponse(
val token: String,
val user: User
)
/**
* Auth response for registration - matching Go API RegisterResponse
*/
@Serializable
data class RegisterResponse(
val token: String,
val user: User,
val message: String = ""
)
/**
* Verify email request
*/
@Serializable
data class VerifyEmailRequest(
val code: String
)
/**
* Verify email response
*/
@Serializable
data class VerifyEmailResponse(
val message: String,
val verified: Boolean
)
/**
* Update profile request
*/
@Serializable
data class UpdateProfileRequest(
@SerialName("first_name") val firstName: String? = null,
@SerialName("last_name") val lastName: String? = null,
val email: String? = null
)
// Password Reset Models
@Serializable
data class ForgotPasswordRequest(
val email: String
)
@Serializable
data class ForgotPasswordResponse(
val message: String
)
@Serializable
data class VerifyResetCodeRequest(
val email: String,
val code: String
)
@Serializable
data class VerifyResetCodeResponse(
val message: String,
@SerialName("reset_token") val resetToken: String
)
@Serializable
data class ResetPasswordRequest(
@SerialName("reset_token") val resetToken: String,
@SerialName("new_password") val newPassword: String
)
@Serializable
data class ResetPasswordResponse(
val message: String
)
/**
* Generic message response used by many endpoints
*/
@Serializable
data class MessageResponse(
val message: String
)
// Apple Sign In Models
/**
* Apple Sign In request matching Go API
*/
@Serializable
data class AppleSignInRequest(
@SerialName("id_token") val idToken: String,
@SerialName("user_id") val userId: String,
val email: String? = null,
@SerialName("first_name") val firstName: String? = null,
@SerialName("last_name") val lastName: String? = null
)
/**
* Apple Sign In response matching Go API
*/
@Serializable
data class AppleSignInResponse(
val token: String,
val user: User,
@SerialName("is_new_user") val isNewUser: Boolean
)
// Google Sign In Models
/**
* Google Sign In request matching Go API
*/
@Serializable
data class GoogleSignInRequest(
@SerialName("id_token") val idToken: String
)
/**
* Google Sign In response matching Go API
*/
@Serializable
data class GoogleSignInResponse(
val token: String,
val user: User,
@SerialName("is_new_user") val isNewUser: Boolean
)