Add multi-image support for task completions and documents
- Add TaskCompletionImage and DocumentImage models with one-to-many relationships
- Update admin panel to display images for completions and documents
- Add image arrays to API request/response DTOs
- Update repositories with Preload("Images") for eager loading
- Fix seed SQL execution to use raw SQL instead of prepared statements
- Fix table names in seed file (admin_users, push_notifications_*)
- Add comprehensive seed test data with 34 completion images and 24 document images
- Add subscription limitations admin feature with toggle
- Update admin sidebar with limitations link
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -68,26 +68,52 @@ func (s *SubscriptionService) GetSubscriptionStatus(userID uint) (*SubscriptionS
|
||||
return nil, err
|
||||
}
|
||||
|
||||
limits, err := s.subscriptionRepo.GetTierLimits(sub.Tier)
|
||||
// Get all tier limits and build a map
|
||||
allLimits, err := s.subscriptionRepo.GetAllTierLimits()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get current usage if limitations are enabled
|
||||
var usage *UsageResponse
|
||||
if settings.EnableLimitations {
|
||||
usage, err = s.getUserUsage(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitsMap := make(map[string]*TierLimitsClientResponse)
|
||||
for _, l := range allLimits {
|
||||
limitsMap[string(l.Tier)] = NewTierLimitsClientResponse(&l)
|
||||
}
|
||||
|
||||
return &SubscriptionStatusResponse{
|
||||
Subscription: NewSubscriptionResponse(sub),
|
||||
Limits: NewTierLimitsResponse(limits),
|
||||
Usage: usage,
|
||||
// Ensure both free and pro exist with defaults if missing
|
||||
if _, ok := limitsMap["free"]; !ok {
|
||||
defaults := models.GetDefaultFreeLimits()
|
||||
limitsMap["free"] = NewTierLimitsClientResponse(&defaults)
|
||||
}
|
||||
if _, ok := limitsMap["pro"]; !ok {
|
||||
defaults := models.GetDefaultProLimits()
|
||||
limitsMap["pro"] = NewTierLimitsClientResponse(&defaults)
|
||||
}
|
||||
|
||||
// Get current usage
|
||||
usage, err := s.getUserUsage(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build flattened response (KMM expects subscription fields at top level)
|
||||
resp := &SubscriptionStatusResponse{
|
||||
AutoRenew: sub.AutoRenew,
|
||||
Limits: limitsMap,
|
||||
Usage: usage,
|
||||
LimitationsEnabled: settings.EnableLimitations,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Format dates if present
|
||||
if sub.SubscribedAt != nil {
|
||||
t := sub.SubscribedAt.Format("2006-01-02T15:04:05Z")
|
||||
resp.SubscribedAt = &t
|
||||
}
|
||||
if sub.ExpiresAt != nil {
|
||||
t := sub.ExpiresAt.Format("2006-01-02T15:04:05Z")
|
||||
resp.ExpiresAt = &t
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// getUserUsage calculates current usage for a user
|
||||
@@ -121,10 +147,10 @@ func (s *SubscriptionService) getUserUsage(userID uint) (*UsageResponse, error)
|
||||
}
|
||||
|
||||
return &UsageResponse{
|
||||
Properties: propertiesCount,
|
||||
Tasks: tasksCount,
|
||||
Contractors: contractorsCount,
|
||||
Documents: documentsCount,
|
||||
PropertiesCount: propertiesCount,
|
||||
TasksCount: tasksCount,
|
||||
ContractorsCount: contractorsCount,
|
||||
DocumentsCount: documentsCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -162,19 +188,19 @@ func (s *SubscriptionService) CheckLimit(userID uint, limitType string) error {
|
||||
|
||||
switch limitType {
|
||||
case "properties":
|
||||
if limits.PropertiesLimit != nil && usage.Properties >= int64(*limits.PropertiesLimit) {
|
||||
if limits.PropertiesLimit != nil && usage.PropertiesCount >= int64(*limits.PropertiesLimit) {
|
||||
return ErrPropertiesLimitExceeded
|
||||
}
|
||||
case "tasks":
|
||||
if limits.TasksLimit != nil && usage.Tasks >= int64(*limits.TasksLimit) {
|
||||
if limits.TasksLimit != nil && usage.TasksCount >= int64(*limits.TasksLimit) {
|
||||
return ErrTasksLimitExceeded
|
||||
}
|
||||
case "contractors":
|
||||
if limits.ContractorsLimit != nil && usage.Contractors >= int64(*limits.ContractorsLimit) {
|
||||
if limits.ContractorsLimit != nil && usage.ContractorsCount >= int64(*limits.ContractorsLimit) {
|
||||
return ErrContractorsLimitExceeded
|
||||
}
|
||||
case "documents":
|
||||
if limits.DocumentsLimit != nil && usage.Documents >= int64(*limits.DocumentsLimit) {
|
||||
if limits.DocumentsLimit != nil && usage.DocumentsCount >= int64(*limits.DocumentsLimit) {
|
||||
return ErrDocumentsLimitExceeded
|
||||
}
|
||||
}
|
||||
@@ -194,6 +220,21 @@ func (s *SubscriptionService) GetUpgradeTrigger(key string) (*UpgradeTriggerResp
|
||||
return NewUpgradeTriggerResponse(trigger), nil
|
||||
}
|
||||
|
||||
// GetAllUpgradeTriggers gets all active upgrade triggers as a map keyed by trigger_key
|
||||
// KMM client expects Map<String, UpgradeTriggerData>
|
||||
func (s *SubscriptionService) GetAllUpgradeTriggers() (map[string]*UpgradeTriggerDataResponse, error) {
|
||||
triggers, err := s.subscriptionRepo.GetAllUpgradeTriggers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string]*UpgradeTriggerDataResponse)
|
||||
for _, t := range triggers {
|
||||
result[t.TriggerKey] = NewUpgradeTriggerDataResponse(&t)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetFeatureBenefits gets all feature benefits
|
||||
func (s *SubscriptionService) GetFeatureBenefits() ([]FeatureBenefitResponse, error) {
|
||||
benefits, err := s.subscriptionRepo.GetFeatureBenefits()
|
||||
@@ -331,23 +372,47 @@ func NewTierLimitsResponse(l *models.TierLimits) *TierLimitsResponse {
|
||||
}
|
||||
}
|
||||
|
||||
// UsageResponse represents current usage
|
||||
// UsageResponse represents current usage (KMM client expects _count suffix)
|
||||
type UsageResponse struct {
|
||||
Properties int64 `json:"properties"`
|
||||
Tasks int64 `json:"tasks"`
|
||||
Contractors int64 `json:"contractors"`
|
||||
Documents int64 `json:"documents"`
|
||||
PropertiesCount int64 `json:"properties_count"`
|
||||
TasksCount int64 `json:"tasks_count"`
|
||||
ContractorsCount int64 `json:"contractors_count"`
|
||||
DocumentsCount int64 `json:"documents_count"`
|
||||
}
|
||||
|
||||
// TierLimitsClientResponse represents tier limits for mobile client (simple field names)
|
||||
type TierLimitsClientResponse struct {
|
||||
Properties *int `json:"properties"`
|
||||
Tasks *int `json:"tasks"`
|
||||
Contractors *int `json:"contractors"`
|
||||
Documents *int `json:"documents"`
|
||||
}
|
||||
|
||||
// NewTierLimitsClientResponse creates a TierLimitsClientResponse from a model
|
||||
func NewTierLimitsClientResponse(l *models.TierLimits) *TierLimitsClientResponse {
|
||||
return &TierLimitsClientResponse{
|
||||
Properties: l.PropertiesLimit,
|
||||
Tasks: l.TasksLimit,
|
||||
Contractors: l.ContractorsLimit,
|
||||
Documents: l.DocumentsLimit,
|
||||
}
|
||||
}
|
||||
|
||||
// SubscriptionStatusResponse represents full subscription status
|
||||
// Fields are flattened to match KMM client expectations
|
||||
type SubscriptionStatusResponse struct {
|
||||
Subscription *SubscriptionResponse `json:"subscription"`
|
||||
Limits *TierLimitsResponse `json:"limits"`
|
||||
Usage *UsageResponse `json:"usage,omitempty"`
|
||||
LimitationsEnabled bool `json:"limitations_enabled"`
|
||||
// Flattened subscription fields (KMM expects these at top level)
|
||||
SubscribedAt *string `json:"subscribed_at"`
|
||||
ExpiresAt *string `json:"expires_at"`
|
||||
AutoRenew bool `json:"auto_renew"`
|
||||
|
||||
// Other fields
|
||||
Usage *UsageResponse `json:"usage"`
|
||||
Limits map[string]*TierLimitsClientResponse `json:"limits"`
|
||||
LimitationsEnabled bool `json:"limitations_enabled"`
|
||||
}
|
||||
|
||||
// UpgradeTriggerResponse represents an upgrade trigger
|
||||
// UpgradeTriggerResponse represents an upgrade trigger (includes trigger_key)
|
||||
type UpgradeTriggerResponse struct {
|
||||
TriggerKey string `json:"trigger_key"`
|
||||
Title string `json:"title"`
|
||||
@@ -367,6 +432,29 @@ func NewUpgradeTriggerResponse(t *models.UpgradeTrigger) *UpgradeTriggerResponse
|
||||
}
|
||||
}
|
||||
|
||||
// UpgradeTriggerDataResponse represents trigger data for map values (no trigger_key)
|
||||
// Matches KMM UpgradeTriggerData model
|
||||
type UpgradeTriggerDataResponse struct {
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
PromoHTML *string `json:"promo_html"`
|
||||
ButtonText string `json:"button_text"`
|
||||
}
|
||||
|
||||
// NewUpgradeTriggerDataResponse creates an UpgradeTriggerDataResponse from a model
|
||||
func NewUpgradeTriggerDataResponse(t *models.UpgradeTrigger) *UpgradeTriggerDataResponse {
|
||||
var promoHTML *string
|
||||
if t.PromoHTML != "" {
|
||||
promoHTML = &t.PromoHTML
|
||||
}
|
||||
return &UpgradeTriggerDataResponse{
|
||||
Title: t.Title,
|
||||
Message: t.Message,
|
||||
PromoHTML: promoHTML,
|
||||
ButtonText: t.ButtonText,
|
||||
}
|
||||
}
|
||||
|
||||
// FeatureBenefitResponse represents a feature benefit
|
||||
type FeatureBenefitResponse struct {
|
||||
FeatureName string `json:"feature_name"`
|
||||
|
||||
Reference in New Issue
Block a user