package admin import ( "crussell/db" "encoding/json" "fmt" "log" "log/slog" "net/http" "net/url" "strconv" "strings" ) type BusinessSettings struct { BusinessName string `json:"business_name"` BusinessAddress string `json:"business_address"` BusinessPhone *string `json:"business_phone,omitempty"` BusinessEmail *string `json:"business_email,omitempty"` VATRegistrationNumber *string `json:"vat_registration_number,omitempty"` IsVATRegistered bool `json:"is_vat_registered"` DefaultVATRate float64 `json:"default_vat_rate"` CurrencyCode string `json:"currency_code"` WebsiteURL *string `json:"website_url,omitempty"` GiftCardExpiryMonths int `json:"gift_card_expiry_months"` VoucherType string `json:"voucher_type"` } func validateURL(rawURL string) error { parsed, err := url.Parse(rawURL) if err != nil { return err } if parsed.Scheme != "http" && parsed.Scheme != "https" { return fmt.Errorf("must start with http:// or https://") } if parsed.Host == "" { return fmt.Errorf("host is required") } return nil } // PublicBusinessInfo contains the non-sensitive subset of business settings // that is safe to expose to unauthenticated or non-admin users. type PublicBusinessInfo struct { BusinessName string `json:"business_name"` BusinessAddress string `json:"business_address"` BusinessPhone *string `json:"business_phone,omitempty"` BusinessEmail *string `json:"business_email,omitempty"` IsVATRegistered bool `json:"is_vat_registered"` VATRegistrationNumber *string `json:"vat_registration_number,omitempty"` DefaultVATRate float64 `json:"default_vat_rate"` } func GetPublicBusinessInfo(w http.ResponseWriter, r *http.Request) { var info PublicBusinessInfo err := db.Conn.QueryRow(r.Context(), ` SELECT business_name, business_address, business_phone, business_email, is_vat_registered, vat_registration_number, default_vat_rate FROM business_settings LIMIT 1 `).Scan( &info.BusinessName, &info.BusinessAddress, &info.BusinessPhone, &info.BusinessEmail, &info.IsVATRegistered, &info.VATRegistrationNumber, &info.DefaultVATRate, ) if err != nil { log.Printf("Failed to fetch public business info: %v", err) http.Error(w, "Failed to fetch business info", http.StatusInternalServerError) return } if err := json.NewEncoder(w).Encode(info); err != nil { log.Printf("Failed to encode JSON response: %v", err) } } func GetBusinessSettings(w http.ResponseWriter, r *http.Request) { var s BusinessSettings err := db.Conn.QueryRow(r.Context(), ` SELECT business_name, business_address, business_phone, business_email, vat_registration_number, is_vat_registered, default_vat_rate, currency_code, website_url, gift_card_expiry_months, voucher_type FROM business_settings LIMIT 1 `).Scan( &s.BusinessName, &s.BusinessAddress, &s.BusinessPhone, &s.BusinessEmail, &s.VATRegistrationNumber, &s.IsVATRegistered, &s.DefaultVATRate, &s.CurrencyCode, &s.WebsiteURL, &s.GiftCardExpiryMonths, &s.VoucherType, ) if err != nil { log.Printf("Failed to fetch business settings: %v", err) http.Error(w, "Failed to fetch settings", http.StatusInternalServerError) return } if err := json.NewEncoder(w).Encode(s); err != nil { log.Printf("Failed to encode JSON response: %v", err) } } type UpdateBusinessSettingsRequest struct { BusinessName *string `json:"business_name,omitempty"` BusinessAddress *string `json:"business_address,omitempty"` BusinessPhone *string `json:"business_phone,omitempty"` BusinessEmail *string `json:"business_email,omitempty"` VATRegistrationNumber *string `json:"vat_registration_number,omitempty"` IsVATRegistered *bool `json:"is_vat_registered,omitempty"` DefaultVATRate *float64 `json:"default_vat_rate,omitempty"` WebsiteURL *string `json:"website_url,omitempty"` GiftCardExpiryMonths *int `json:"gift_card_expiry_months,omitempty"` VoucherType *string `json:"voucher_type,omitempty"` } func UpdateBusinessSettings(w http.ResponseWriter, r *http.Request) { var req UpdateBusinessSettingsRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.BusinessName != nil && (len(*req.BusinessName) == 0 || len(*req.BusinessName) > 255) { http.Error(w, "business_name must be between 1 and 255 characters", http.StatusBadRequest) return } if req.BusinessAddress != nil && len(*req.BusinessAddress) == 0 { http.Error(w, "business_address must not be empty", http.StatusBadRequest) return } if req.BusinessPhone != nil && len(*req.BusinessPhone) > 20 { http.Error(w, "business_phone must be 20 characters or fewer", http.StatusBadRequest) return } if req.BusinessEmail != nil && len(*req.BusinessEmail) > 254 { http.Error(w, "business_email must be 254 characters or fewer", http.StatusBadRequest) return } if req.VATRegistrationNumber != nil && *req.VATRegistrationNumber != "" { v := *req.VATRegistrationNumber if len(v) < 11 || len(v) > 14 { http.Error(w, "vat_registration_number must be 11 characters (GB + 9 digits) or 14 characters (GB + 12 digits)", http.StatusBadRequest) return } if v[:2] != "GB" { http.Error(w, "vat_registration_number must start with 'GB'", http.StatusBadRequest) return } digits := v[2:] if len(digits) != 9 && len(digits) != 12 { http.Error(w, "vat_registration_number must have 9 or 12 digits after 'GB'", http.StatusBadRequest) return } for _, c := range digits { if c < '0' || c > '9' { http.Error(w, "vat_registration_number must contain only digits after 'GB'", http.StatusBadRequest) return } } } if req.WebsiteURL != nil && *req.WebsiteURL != "" { if err := validateURL(*req.WebsiteURL); err != nil { http.Error(w, "website_url: "+err.Error(), http.StatusBadRequest) return } } if req.VoucherType != nil && *req.VoucherType != "SPV" && *req.VoucherType != "MPV" { http.Error(w, "voucher_type must be 'SPV' or 'MPV'", http.StatusBadRequest) return } if req.GiftCardExpiryMonths != nil && *req.GiftCardExpiryMonths < 1 { http.Error(w, "gift_card_expiry_months must be at least 1", http.StatusBadRequest) return } if req.DefaultVATRate != nil && (*req.DefaultVATRate < 0 || *req.DefaultVATRate > 100) { http.Error(w, "default_vat_rate must be between 0 and 100", http.StatusBadRequest) return } setClauses := []string{} args := []any{} argIdx := 1 if req.BusinessName != nil { setClauses = append(setClauses, "business_name = $"+strconv.Itoa(argIdx)) args = append(args, *req.BusinessName) argIdx++ } if req.BusinessAddress != nil { setClauses = append(setClauses, "business_address = $"+strconv.Itoa(argIdx)) args = append(args, *req.BusinessAddress) argIdx++ } if req.BusinessPhone != nil { setClauses = append(setClauses, "business_phone = $"+strconv.Itoa(argIdx)) args = append(args, *req.BusinessPhone) argIdx++ } if req.BusinessEmail != nil { setClauses = append(setClauses, "business_email = $"+strconv.Itoa(argIdx)) args = append(args, *req.BusinessEmail) argIdx++ } if req.VATRegistrationNumber != nil { setClauses = append(setClauses, "vat_registration_number = $"+strconv.Itoa(argIdx)) args = append(args, *req.VATRegistrationNumber) argIdx++ } if req.IsVATRegistered != nil { setClauses = append(setClauses, "is_vat_registered = $"+strconv.Itoa(argIdx)) args = append(args, *req.IsVATRegistered) argIdx++ } if req.DefaultVATRate != nil { setClauses = append(setClauses, "default_vat_rate = $"+strconv.Itoa(argIdx)) args = append(args, *req.DefaultVATRate) argIdx++ } if req.WebsiteURL != nil { setClauses = append(setClauses, "website_url = $"+strconv.Itoa(argIdx)) args = append(args, *req.WebsiteURL) argIdx++ } if req.GiftCardExpiryMonths != nil { setClauses = append(setClauses, "gift_card_expiry_months = $"+strconv.Itoa(argIdx)) args = append(args, *req.GiftCardExpiryMonths) argIdx++ } if req.VoucherType != nil { setClauses = append(setClauses, "voucher_type = $"+strconv.Itoa(argIdx)) args = append(args, *req.VoucherType) } if len(setClauses) == 0 { http.Error(w, "No fields to update", http.StatusBadRequest) return } var query strings.Builder query.WriteString("UPDATE business_settings SET ") for i, clause := range setClauses { if i > 0 { query.WriteString(", ") } query.WriteString(clause) } tx, err := db.Conn.Begin(r.Context()) if err != nil { log.Printf("Failed to begin transaction: %v", err) http.Error(w, "Failed to update settings", http.StatusInternalServerError) return } defer func() { if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { slog.Error("failed to rollback transaction", "err", err) } }() _, err = tx.Exec(r.Context(), query.String(), args...) if err != nil { log.Printf("Failed to update business settings: %v", err) http.Error(w, "Failed to update settings", http.StatusInternalServerError) return } if req.IsVATRegistered != nil && *req.IsVATRegistered { vatRate := 20.0 if req.DefaultVATRate != nil { vatRate = *req.DefaultVATRate } var payUpdated, tillUpdated int var totalVAT, totalNet float64 // Use '2000-01-01' to cover all historical payments. Under UK VAT law, // the effective registration date may be backdated — this ensures all // past completed payments and till sales get VAT applied retroactively. vatErr := tx.QueryRow(r.Context(), "SELECT payments_updated, till_sales_updated, total_vat_calculated, total_net_calculated FROM enable_vat_registration('2000-01-01'::date, $1, $2)", vatRate, req.VATRegistrationNumber, ).Scan(&payUpdated, &tillUpdated, &totalVAT, &totalNet) if vatErr != nil { log.Printf("Failed to enable VAT retroactively: %v", vatErr) http.Error(w, "Business settings saved but failed to retroactively apply VAT to past payments. The database function enable_vat_registration returned an error. Please try again or contact support.", http.StatusInternalServerError) return } log.Printf("VAT enabled retroactively: %d payments (+£%.2f VAT), %d till sales updated", payUpdated, totalVAT, tillUpdated) } if err := tx.Commit(r.Context()); err != nil { log.Printf("Failed to commit settings update: %v", err) http.Error(w, "Failed to save settings", http.StatusInternalServerError) return } GetBusinessSettings(w, r) }