package handler import ( "encoding/json" "errors" "fmt" "io" "net/http" "os" "path/filepath" "strconv" "strings" "github.com/gofrs/uuid/v5" ) const uploadsDir = "./uploads" const maxFileSize = 10 << 20 // 10 MB // UploadDocument handles KYC/license document upload via multipart form. // Accepts: multipart/form-data with field "file" (PDF/JPG/PNG) and optional "document_type". func (h *Handler) UploadDocument(w http.ResponseWriter, r *http.Request) { usr, err := h.getUserFromContext(r.Context()) if err != nil { http.Error(w, "unauthorized", http.StatusUnauthorized) return } // Support both multipart (file upload) and JSON (URL reference) contentType := r.Header.Get("Content-Type") if strings.Contains(contentType, "multipart/form-data") { // --- Multipart file upload --- if err := r.ParseMultipartForm(maxFileSize); err != nil { http.Error(w, "file too large or invalid form (max 10MB)", http.StatusBadRequest) return } file, header, err := r.FormFile("file") if err != nil { http.Error(w, "field 'file' is required", http.StatusBadRequest) return } defer file.Close() ext := strings.ToLower(filepath.Ext(header.Filename)) allowed := map[string]bool{".pdf": true, ".jpg": true, ".jpeg": true, ".png": true} if !allowed[ext] { http.Error(w, "only PDF, JPG and PNG files are allowed", http.StatusBadRequest) return } docType := strings.ToUpper(r.FormValue("document_type")) if docType == "" { docType = "LICENSE" } targetCompanyID := usr.CompanyID if targetIDStr := r.FormValue("target_company_id"); targetIDStr != "" && usr.Superadmin { parsedID, err := uuid.FromString(targetIDStr) if err == nil { targetCompanyID = parsedID } } // Save file companyDir := filepath.Join(uploadsDir, targetCompanyID.String()) if err := os.MkdirAll(companyDir, 0755); err != nil { http.Error(w, "failed to create upload directory", http.StatusInternalServerError) return } filename := fmt.Sprintf("%s_%s%s", strings.ToLower(docType), targetCompanyID.String()[:8], ext) destPath := filepath.Join(companyDir, filename) dst, err := os.Create(destPath) if err != nil { http.Error(w, "failed to save file", http.StatusInternalServerError) return } defer dst.Close() if _, err := io.Copy(dst, file); err != nil { http.Error(w, "failed to write file", http.StatusInternalServerError) return } fileURL := fmt.Sprintf("/api/v1/files/%s/%s", targetCompanyID.String(), filename) doc, err := h.svc.UploadDocument(r.Context(), targetCompanyID, docType, fileURL) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(doc) return } // --- JSON fallback (URL reference) --- var req struct { Type string `json:"type"` URL string `json:"url"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } doc, err := h.svc.UploadDocument(r.Context(), usr.CompanyID, req.Type, req.URL) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(doc) } // GetDocuments lists company KYC docs. func (h *Handler) GetDocuments(w http.ResponseWriter, r *http.Request) { usr, err := h.getUserFromContext(r.Context()) if err != nil { http.Error(w, "unauthorized", http.StatusUnauthorized) return } docs, err := h.svc.GetCompanyDocuments(r.Context(), usr.CompanyID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(docs) } // ServeFile serves uploaded files from the local uploads directory. // Path: /api/v1/files/{company_id}/{filename} func (h *Handler) ServeFile(w http.ResponseWriter, r *http.Request) { parts := splitPath(r.URL.Path) if len(parts) < 2 { http.NotFound(w, r) return } companyID := parts[len(parts)-2] filename := parts[len(parts)-1] if strings.Contains(companyID, "..") || strings.Contains(filename, "..") { http.Error(w, "invalid path", http.StatusBadRequest) return } filePath := filepath.Join(uploadsDir, companyID, filename) http.ServeFile(w, r, filePath) } // GetLedger returns financial history. func (h *Handler) GetLedger(w http.ResponseWriter, r *http.Request) { usr, err := h.getUserFromContext(r.Context()) if err != nil { http.Error(w, "unauthorized", http.StatusUnauthorized) return } page, _ := strconv.Atoi(r.URL.Query().Get("page")) pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size")) res, err := h.svc.GetFormattedLedger(r.Context(), usr.CompanyID, page, pageSize) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(res) } // GetBalance returns current wallet balance. func (h *Handler) GetBalance(w http.ResponseWriter, r *http.Request) { usr, err := h.getUserFromContext(r.Context()) if err != nil { http.Error(w, "unauthorized", http.StatusUnauthorized) return } bal, err := h.svc.GetBalance(r.Context(), usr.CompanyID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]int64{"balance_cents": bal}) } // RequestWithdrawal initiates a payout. func (h *Handler) RequestWithdrawal(w http.ResponseWriter, r *http.Request) { usr, err := h.getUserFromContext(r.Context()) if err != nil { http.Error(w, "unauthorized", http.StatusUnauthorized) return } var req struct { AmountCents int64 `json:"amount_cents"` BankInfo string `json:"bank_info"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } wd, err := h.svc.RequestWithdrawal(r.Context(), usr.CompanyID, req.AmountCents, req.BankInfo) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(wd) } // ListWithdrawals shows history of payouts. func (h *Handler) ListWithdrawals(w http.ResponseWriter, r *http.Request) { usr, err := h.getUserFromContext(r.Context()) if err != nil { http.Error(w, "unauthorized", http.StatusUnauthorized) return } wds, err := h.svc.ListWithdrawals(r.Context(), usr.CompanyID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(wds) } // unused import guard var _ = errors.New