Files
honeyDueAPI/internal/dto/responses/residence.go
T
Trey T 12de5a230a
Backend CI / Test (push) Has been cancelled
Backend CI / Contract Tests (push) Has been cancelled
Backend CI / Lint (push) Has been cancelled
Backend CI / Secret Scanning (push) Has been cancelled
Backend CI / Build (push) Has been cancelled
i18n: backend-localized lookups, suggestions, and static data (10 languages)
- suggestion_service: fix scorer (stringList unmarshal accepts scalar|array;
  anchor scoring on base universal score so bool matches no longer tie); add
  localizeReasons for human-readable, Accept-Language-localized match reasons
- lookup_i18n: localize lookup display names, home-profile options, document
  types/categories via internal/i18n
- static_data_handler: per-locale seeded-data response (display_name, home
  profile options, document types/categories) with per-locale cache + ETag
- settings_handler: invalidate per-locale seeded-data cache on lookup change
  instead of pre-warming a single non-localized blob
- cache_service: per-locale seeded-data keys + ETag
- DTOs: add DisplayName fields (task/residence/contractor)
- translations: add suggestion.reason.* and lookup.* keys across all 10 langs
- cmd/api: extract startup helpers + tests

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 20:54:54 -05:00

271 lines
9.8 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 is the stable English identifier (clients match on this).
Name string `json:"name"`
// DisplayName is the localized label for the request's Accept-Language.
DisplayName string `json:"display_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,
}
}