Files
honeyDueAPI/internal/dto/responses/residence.go
Trey T cb7080c460 Smart onboarding: residence home profile + suggestion engine
14 new optional residence fields (heating, cooling, water heater, roof,
pool, sprinkler, septic, fireplace, garage, basement, attic, exterior,
flooring, landscaping) with JSONB conditions on templates.

Suggestion engine scores templates against home profile: string match
+0.25, bool +0.3, property type +0.15, universal base 0.3. Graceful
degradation from minimal to full profile info.

GET /api/tasks/suggestions/?residence_id=X returns ranked templates.
54 template conditions across 44 templates in seed data.
8 suggestion service tests.
2026-03-30 09:02:03 -05:00

268 lines
9.6 KiB
Go

package responses
import (
"time"
"github.com/shopspring/decimal"
"github.com/treytartt/honeydue-api/internal/models"
)
// ResidenceTypeResponse represents a residence type in the API response
type ResidenceTypeResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
}
// ResidenceUserResponse represents a user with access to a residence
type ResidenceUserResponse struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
// ResidenceResponse represents a residence in the API response
type ResidenceResponse struct {
ID uint `json:"id"`
OwnerID uint `json:"owner_id"`
Owner *ResidenceUserResponse `json:"owner,omitempty"`
Users []ResidenceUserResponse `json:"users,omitempty"`
Name string `json:"name"`
PropertyTypeID *uint `json:"property_type_id"`
PropertyType *ResidenceTypeResponse `json:"property_type,omitempty"`
StreetAddress string `json:"street_address"`
ApartmentUnit string `json:"apartment_unit"`
City string `json:"city"`
StateProvince string `json:"state_province"`
PostalCode string `json:"postal_code"`
Country string `json:"country"`
Bedrooms *int `json:"bedrooms"`
Bathrooms *decimal.Decimal `json:"bathrooms"`
SquareFootage *int `json:"square_footage"`
LotSize *decimal.Decimal `json:"lot_size"`
YearBuilt *int `json:"year_built"`
Description string `json:"description"`
PurchaseDate *time.Time `json:"purchase_date"`
PurchasePrice *decimal.Decimal `json:"purchase_price"`
// Home Profile
HeatingType *string `json:"heating_type"`
CoolingType *string `json:"cooling_type"`
WaterHeaterType *string `json:"water_heater_type"`
RoofType *string `json:"roof_type"`
HasPool bool `json:"has_pool"`
HasSprinklerSystem bool `json:"has_sprinkler_system"`
HasSeptic bool `json:"has_septic"`
HasFireplace bool `json:"has_fireplace"`
HasGarage bool `json:"has_garage"`
HasBasement bool `json:"has_basement"`
HasAttic bool `json:"has_attic"`
ExteriorType *string `json:"exterior_type"`
FlooringPrimary *string `json:"flooring_primary"`
LandscapingType *string `json:"landscaping_type"`
IsPrimary bool `json:"is_primary"`
IsActive bool `json:"is_active"`
OverdueCount int `json:"overdue_count"`
CompletionSummary *CompletionSummary `json:"completion_summary,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TotalSummary represents summary statistics for all residences
type TotalSummary struct {
TotalResidences int `json:"total_residences"`
TotalTasks int `json:"total_tasks"`
TotalPending int `json:"total_pending"`
TotalOverdue int `json:"total_overdue"`
TasksDueNextWeek int `json:"tasks_due_next_week"`
TasksDueNextMonth int `json:"tasks_due_next_month"`
}
// MyResidencesResponse represents the response for my-residences endpoint
// NOTE: Summary statistics are calculated client-side from kanban data
type MyResidencesResponse struct {
Residences []ResidenceResponse `json:"residences"`
}
// ResidenceWithSummaryResponse wraps ResidenceResponse with TotalSummary for CRUD operations
type ResidenceWithSummaryResponse struct {
Data ResidenceResponse `json:"data"`
Summary TotalSummary `json:"summary"`
}
// ResidenceDeleteWithSummaryResponse for delete operations
type ResidenceDeleteWithSummaryResponse struct {
Data string `json:"data"`
Summary TotalSummary `json:"summary"`
}
// ShareCodeResponse represents a share code in the API response
type ShareCodeResponse struct {
ID uint `json:"id"`
Code string `json:"code"`
ResidenceID uint `json:"residence_id"`
CreatedByID uint `json:"created_by_id"`
IsActive bool `json:"is_active"`
ExpiresAt *time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
}
// JoinResidenceResponse represents the response after joining a residence
type JoinResidenceResponse struct {
Message string `json:"message"`
Residence ResidenceResponse `json:"residence"`
Summary TotalSummary `json:"summary"`
}
// GenerateShareCodeResponse represents the response after generating a share code
type GenerateShareCodeResponse struct {
Message string `json:"message"`
ShareCode ShareCodeResponse `json:"share_code"`
}
// SharePackageResponse represents the response for generating a share package
// This contains the share code plus metadata for the .honeydue file
type SharePackageResponse struct {
ShareCode string `json:"share_code"`
ResidenceName string `json:"residence_name"`
SharedBy string `json:"shared_by"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
}
// ColumnCompletionCount represents completions from a specific kanban column
type ColumnCompletionCount struct {
Column string `json:"column"`
Color string `json:"color"`
Count int `json:"count"`
}
// MonthlyCompletionSummary represents completions for a single month
type MonthlyCompletionSummary struct {
Month string `json:"month"` // "2025-04" format
Completions []ColumnCompletionCount `json:"completions"`
Total int `json:"total"`
Overflow int `json:"overflow"` // completions beyond the display cap
}
// CompletionSummary represents task completion data for the honeycomb grid
type CompletionSummary struct {
TotalAllTime int `json:"total_all_time"`
TotalLast12Months int `json:"total_last_12_months"`
Months []MonthlyCompletionSummary `json:"months"`
}
// === Factory Functions ===
// NewResidenceUserResponse creates a ResidenceUserResponse from a User model
func NewResidenceUserResponse(user *models.User) *ResidenceUserResponse {
if user == nil {
return nil
}
return &ResidenceUserResponse{
ID: user.ID,
Username: user.Username,
Email: user.Email,
FirstName: user.FirstName,
LastName: user.LastName,
}
}
// NewResidenceTypeResponse creates a ResidenceTypeResponse from a ResidenceType model
func NewResidenceTypeResponse(rt *models.ResidenceType) *ResidenceTypeResponse {
if rt == nil {
return nil
}
return &ResidenceTypeResponse{
ID: rt.ID,
Name: rt.Name,
}
}
// NewResidenceResponse creates a ResidenceResponse from a Residence model
func NewResidenceResponse(residence *models.Residence) ResidenceResponse {
resp := ResidenceResponse{
ID: residence.ID,
OwnerID: residence.OwnerID,
Name: residence.Name,
PropertyTypeID: residence.PropertyTypeID,
StreetAddress: residence.StreetAddress,
ApartmentUnit: residence.ApartmentUnit,
City: residence.City,
StateProvince: residence.StateProvince,
PostalCode: residence.PostalCode,
Country: residence.Country,
Bedrooms: residence.Bedrooms,
Bathrooms: residence.Bathrooms,
SquareFootage: residence.SquareFootage,
LotSize: residence.LotSize,
YearBuilt: residence.YearBuilt,
Description: residence.Description,
PurchaseDate: residence.PurchaseDate,
PurchasePrice: residence.PurchasePrice,
HeatingType: residence.HeatingType,
CoolingType: residence.CoolingType,
WaterHeaterType: residence.WaterHeaterType,
RoofType: residence.RoofType,
HasPool: residence.HasPool,
HasSprinklerSystem: residence.HasSprinklerSystem,
HasSeptic: residence.HasSeptic,
HasFireplace: residence.HasFireplace,
HasGarage: residence.HasGarage,
HasBasement: residence.HasBasement,
HasAttic: residence.HasAttic,
ExteriorType: residence.ExteriorType,
FlooringPrimary: residence.FlooringPrimary,
LandscapingType: residence.LandscapingType,
IsPrimary: residence.IsPrimary,
IsActive: residence.IsActive,
CreatedAt: residence.CreatedAt,
UpdatedAt: residence.UpdatedAt,
}
// Include owner if loaded
if residence.Owner.ID != 0 {
resp.Owner = NewResidenceUserResponse(&residence.Owner)
}
// Include property type if loaded
if residence.PropertyType != nil {
resp.PropertyType = NewResidenceTypeResponse(residence.PropertyType)
}
// Include shared users if loaded
if len(residence.Users) > 0 {
resp.Users = make([]ResidenceUserResponse, len(residence.Users))
for i, user := range residence.Users {
resp.Users[i] = *NewResidenceUserResponse(&user)
}
} else {
resp.Users = []ResidenceUserResponse{}
}
return resp
}
// NewResidenceListResponse creates a list of residence responses
func NewResidenceListResponse(residences []models.Residence) []ResidenceResponse {
results := make([]ResidenceResponse, len(residences))
for i, r := range residences {
results[i] = NewResidenceResponse(&r)
}
return results
}
// NewShareCodeResponse creates a ShareCodeResponse from a ResidenceShareCode model
func NewShareCodeResponse(sc *models.ResidenceShareCode) ShareCodeResponse {
return ShareCodeResponse{
ID: sc.ID,
Code: sc.Code,
ResidenceID: sc.ResidenceID,
CreatedByID: sc.CreatedByID,
IsActive: sc.IsActive,
ExpiresAt: sc.ExpiresAt,
CreatedAt: sc.CreatedAt,
}
}