package licensing import ( "context" "encoding/json" "fmt" "net/http" "time" "github.com/google/uuid" "github.com/rs/zerolog/log" ) // EnterpriseValidator handles validation of enterprise licenses via KACHING type EnterpriseValidator struct { kachingEndpoint string client *http.Client cache *LicenseCache } // LicenseFeatures represents the features available in a license type LicenseFeatures struct { SpecKitMethodology bool `json:"spec_kit_methodology"` CustomTemplates bool `json:"custom_templates"` AdvancedAnalytics bool `json:"advanced_analytics"` WorkflowQuota int `json:"workflow_quota"` PrioritySupport bool `json:"priority_support"` Additional map[string]interface{} `json:"additional,omitempty"` } // LicenseInfo contains validated license information type LicenseInfo struct { LicenseID uuid.UUID `json:"license_id"` OrgID uuid.UUID `json:"org_id"` DeploymentID uuid.UUID `json:"deployment_id"` PlanID string `json:"plan_id"` // community, professional, enterprise Features LicenseFeatures `json:"features"` ValidFrom time.Time `json:"valid_from"` ValidTo time.Time `json:"valid_to"` SeatsLimit *int `json:"seats_limit,omitempty"` NodesLimit *int `json:"nodes_limit,omitempty"` IsValid bool `json:"is_valid"` ValidationTime time.Time `json:"validation_time"` } // ValidationRequest sent to KACHING for license validation type ValidationRequest struct { DeploymentID uuid.UUID `json:"deployment_id"` Feature string `json:"feature"` // e.g., "spec_kit_methodology" Context Context `json:"context"` } // Context provides additional information for license validation type Context struct { ProjectID string `json:"project_id,omitempty"` IssueID string `json:"issue_id,omitempty"` CouncilID string `json:"council_id,omitempty"` RequestedBy string `json:"requested_by,omitempty"` } // ValidationResponse from KACHING type ValidationResponse struct { Valid bool `json:"valid"` License *LicenseInfo `json:"license,omitempty"` Reason string `json:"reason,omitempty"` UsageInfo *UsageInfo `json:"usage_info,omitempty"` Suggestions []Suggestion `json:"suggestions,omitempty"` } // UsageInfo provides current usage statistics type UsageInfo struct { CurrentMonth struct { SpecKitWorkflows int `json:"spec_kit_workflows"` Quota int `json:"quota"` Remaining int `json:"remaining"` } `json:"current_month"` PreviousMonth struct { SpecKitWorkflows int `json:"spec_kit_workflows"` } `json:"previous_month"` } // Suggestion for license upgrades type Suggestion struct { Type string `json:"type"` // upgrade_tier, enable_feature Title string `json:"title"` Description string `json:"description"` TargetPlan string `json:"target_plan,omitempty"` Benefits map[string]string `json:"benefits,omitempty"` } // NewEnterpriseValidator creates a new enterprise license validator func NewEnterpriseValidator(kachingEndpoint string) *EnterpriseValidator { return &EnterpriseValidator{ kachingEndpoint: kachingEndpoint, client: &http.Client{ Timeout: 10 * time.Second, }, cache: NewLicenseCache(5 * time.Minute), // 5-minute cache TTL } } // ValidateSpecKitAccess validates if a deployment has access to spec-kit features func (v *EnterpriseValidator) ValidateSpecKitAccess( ctx context.Context, deploymentID uuid.UUID, context Context, ) (*ValidationResponse, error) { startTime := time.Now() log.Info(). Str("deployment_id", deploymentID.String()). Str("feature", "spec_kit_methodology"). Msg("Validating spec-kit access") // Check cache first if cached := v.cache.Get(deploymentID, "spec_kit_methodology"); cached != nil { log.Debug(). Str("deployment_id", deploymentID.String()). Msg("Using cached license validation") return cached, nil } // Prepare validation request request := ValidationRequest{ DeploymentID: deploymentID, Feature: "spec_kit_methodology", Context: context, } response, err := v.callKachingValidation(ctx, request) if err != nil { log.Error(). Err(err). Str("deployment_id", deploymentID.String()). Msg("Failed to validate license with KACHING") return nil, fmt.Errorf("license validation failed: %w", err) } // Cache successful responses if response.Valid { v.cache.Set(deploymentID, "spec_kit_methodology", response) } duration := time.Since(startTime).Milliseconds() log.Info(). Str("deployment_id", deploymentID.String()). Bool("valid", response.Valid). Int64("duration_ms", duration). Msg("License validation completed") return response, nil } // ValidateWorkflowQuota checks if deployment has remaining spec-kit workflow quota func (v *EnterpriseValidator) ValidateWorkflowQuota( ctx context.Context, deploymentID uuid.UUID, context Context, ) (*ValidationResponse, error) { // First validate basic access response, err := v.ValidateSpecKitAccess(ctx, deploymentID, context) if err != nil { return nil, err } if !response.Valid { return response, nil } // Check quota specifically if response.UsageInfo != nil { remaining := response.UsageInfo.CurrentMonth.Remaining if remaining <= 0 { response.Valid = false response.Reason = "Monthly spec-kit workflow quota exceeded" // Add upgrade suggestion if quota exceeded if response.License != nil && response.License.PlanID == "professional" { response.Suggestions = append(response.Suggestions, Suggestion{ Type: "upgrade_tier", Title: "Upgrade to Enterprise", Description: "Get unlimited spec-kit workflows with Enterprise tier", TargetPlan: "enterprise", Benefits: map[string]string{ "workflows": "Unlimited spec-kit workflows", "templates": "Custom template library access", "support": "24/7 priority support", }, }) } } } return response, nil } // GetLicenseInfo retrieves complete license information for a deployment func (v *EnterpriseValidator) GetLicenseInfo( ctx context.Context, deploymentID uuid.UUID, ) (*LicenseInfo, error) { response, err := v.ValidateSpecKitAccess(ctx, deploymentID, Context{}) if err != nil { return nil, err } return response.License, nil } // IsEnterpriseFeatureEnabled checks if a specific enterprise feature is enabled func (v *EnterpriseValidator) IsEnterpriseFeatureEnabled( ctx context.Context, deploymentID uuid.UUID, feature string, ) (bool, error) { request := ValidationRequest{ DeploymentID: deploymentID, Feature: feature, Context: Context{}, } response, err := v.callKachingValidation(ctx, request) if err != nil { return false, err } return response.Valid, nil } // callKachingValidation makes HTTP request to KACHING validation endpoint func (v *EnterpriseValidator) callKachingValidation( ctx context.Context, request ValidationRequest, ) (*ValidationResponse, error) { // Prepare HTTP request requestBody, err := json.Marshal(request) if err != nil { return nil, fmt.Errorf("failed to marshal request: %w", err) } url := fmt.Sprintf("%s/v1/license/validate", v.kachingEndpoint) req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(requestBody)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", "WHOOSH/1.0") // Make request resp, err := v.client.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() // Handle different response codes switch resp.StatusCode { case http.StatusOK: var response ValidationResponse if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } return &response, nil case http.StatusUnauthorized: return &ValidationResponse{ Valid: false, Reason: "Invalid or expired license", }, nil case http.StatusTooManyRequests: return &ValidationResponse{ Valid: false, Reason: "Rate limit exceeded", }, nil case http.StatusServiceUnavailable: // KACHING service unavailable - fallback to cached or basic validation log.Warn(). Str("deployment_id", request.DeploymentID.String()). Msg("KACHING service unavailable, falling back to basic validation") return v.fallbackValidation(request.DeploymentID) default: return nil, fmt.Errorf("unexpected response status: %d", resp.StatusCode) } } // fallbackValidation provides basic validation when KACHING is unavailable func (v *EnterpriseValidator) fallbackValidation(deploymentID uuid.UUID) (*ValidationResponse, error) { // Check cache for any recent validation if cached := v.cache.Get(deploymentID, "spec_kit_methodology"); cached != nil { log.Info(). Str("deployment_id", deploymentID.String()). Msg("Using cached license data for fallback validation") return cached, nil } // Default to basic access for community features return &ValidationResponse{ Valid: false, // Spec-kit is enterprise only Reason: "License service unavailable - spec-kit requires enterprise license", Suggestions: []Suggestion{ { Type: "contact_support", Title: "Contact Support", Description: "License service is temporarily unavailable. Contact support for assistance.", }, }, }, nil } // TrackWorkflowUsage reports spec-kit workflow usage to KACHING for billing func (v *EnterpriseValidator) TrackWorkflowUsage( ctx context.Context, deploymentID uuid.UUID, workflowType string, metadata map[string]interface{}, ) error { usageEvent := map[string]interface{}{ "deployment_id": deploymentID, "event_type": "spec_kit_workflow_executed", "workflow_type": workflowType, "timestamp": time.Now().UTC(), "metadata": metadata, } eventData, err := json.Marshal(usageEvent) if err != nil { return fmt.Errorf("failed to marshal usage event: %w", err) } url := fmt.Sprintf("%s/v1/usage/track", v.kachingEndpoint) req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(eventData)) if err != nil { return fmt.Errorf("failed to create usage tracking request: %w", err) } req.Header.Set("Content-Type", "application/json") resp, err := v.client.Do(req) if err != nil { // Log error but don't fail the workflow for usage tracking issues log.Error(). Err(err). Str("deployment_id", deploymentID.String()). Str("workflow_type", workflowType). Msg("Failed to track workflow usage") return nil } defer resp.Body.Close() if resp.StatusCode >= 400 { log.Error(). Int("status_code", resp.StatusCode). Str("deployment_id", deploymentID.String()). Msg("Usage tracking request failed") } return nil }