Frontend: - Implementar máscara de entrada de telefone para números BR ((XX) XXXXX-XXXX). - Atualizar formulário de cadastro para enviar dados completos do perfil do candidato (endereço, formação, habilidades, etc.). - Corrigir problemas de idioma misto na página de Detalhes da Vaga e adicionar traduções faltantes. Backend: - Atualizar modelo de Usuário, Entidade e DTOs para incluir campos de perfil (Data de Nascimento, Endereço, Formação, etc.). - Atualizar UserRepository para persistir e recuperar os dados estendidos do usuário no PostgreSQL. - Atualizar RegisterCandidateUseCase para mapear campos de entrada para a entidade Usuário.
338 lines
9.5 KiB
Go
338 lines
9.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// PaymentCredentialsServiceInterface defines the contract for credentials
|
|
type PaymentCredentialsServiceInterface interface {
|
|
GetDecryptedKey(ctx context.Context, keyName string) (string, error)
|
|
}
|
|
|
|
// StripeClientInterface defines the contract for Stripe operations
|
|
type StripeClientInterface interface {
|
|
CreateCheckoutSession(secretKey string, req CreateCheckoutRequest) (string, string, error)
|
|
}
|
|
|
|
// PaymentHandler handles Stripe payment operations
|
|
type PaymentHandler struct {
|
|
credentialsService PaymentCredentialsServiceInterface
|
|
stripeClient StripeClientInterface
|
|
}
|
|
|
|
// NewPaymentHandler creates a new payment handler
|
|
func NewPaymentHandler(credentialsService PaymentCredentialsServiceInterface) *PaymentHandler {
|
|
return &PaymentHandler{
|
|
credentialsService: credentialsService,
|
|
stripeClient: &defaultStripeClient{},
|
|
}
|
|
}
|
|
|
|
// defaultStripeClient implements StripeClientInterface
|
|
type defaultStripeClient struct{}
|
|
|
|
func (c *defaultStripeClient) CreateCheckoutSession(secretKey string, req CreateCheckoutRequest) (string, string, error) {
|
|
return createStripeCheckoutSession(secretKey, req)
|
|
}
|
|
|
|
// CreateCheckoutRequest represents a checkout session request
|
|
type CreateCheckoutRequest struct {
|
|
JobID int `json:"jobId"`
|
|
PriceID string `json:"priceId"` // Stripe Price ID
|
|
SuccessURL string `json:"successUrl"` // URL after success
|
|
CancelURL string `json:"cancelUrl"` // URL after cancel
|
|
}
|
|
|
|
// CreateCheckoutResponse represents the checkout session response
|
|
type CreateCheckoutResponse struct {
|
|
SessionID string `json:"sessionId"`
|
|
CheckoutURL string `json:"checkoutUrl"`
|
|
}
|
|
|
|
// StripeConfig holds the configuration for Stripe
|
|
type StripeConfig struct {
|
|
SecretKey string `json:"secretKey"`
|
|
WebhookSecret string `json:"webhookSecret"`
|
|
PublishableKey string `json:"publishableKey"`
|
|
}
|
|
|
|
// CreateCheckout creates a Stripe checkout session for job posting payment
|
|
// @Summary Create checkout session
|
|
// @Description Create a Stripe checkout session for job posting payment
|
|
// @Tags Payments
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param request body CreateCheckoutRequest true "Checkout request"
|
|
// @Success 200 {object} CreateCheckoutResponse
|
|
// @Failure 400 {string} string "Bad Request"
|
|
// @Failure 500 {string} string "Internal Server Error"
|
|
// @Router /api/v1/payments/create-checkout [post]
|
|
func (h *PaymentHandler) CreateCheckout(w http.ResponseWriter, r *http.Request) {
|
|
var req CreateCheckoutRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if req.JobID == 0 || req.PriceID == "" {
|
|
http.Error(w, "JobID and PriceID are required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get Stripe config from encrypted vault
|
|
var config StripeConfig
|
|
payload, err := h.credentialsService.GetDecryptedKey(r.Context(), "stripe")
|
|
if err == nil {
|
|
json.Unmarshal([]byte(payload), &config)
|
|
}
|
|
|
|
// Fallback to Env if not found or empty (for migration/dev)
|
|
if config.SecretKey == "" {
|
|
config.SecretKey = os.Getenv("STRIPE_SECRET_KEY")
|
|
}
|
|
|
|
if config.SecretKey == "" {
|
|
http.Error(w, "Payment service not configured", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Create Stripe checkout session via API
|
|
sessionID, checkoutURL, err := h.stripeClient.CreateCheckoutSession(config.SecretKey, req)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Failed to create checkout session: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
response := CreateCheckoutResponse{
|
|
SessionID: sessionID,
|
|
CheckoutURL: checkoutURL,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(response)
|
|
}
|
|
|
|
// HandleWebhook processes Stripe webhook events
|
|
// @Summary Handle Stripe webhook
|
|
// @Description Process Stripe webhook events (payment success, failure, etc.)
|
|
// @Tags Payments
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Success 200 {string} string "OK"
|
|
// @Failure 400 {string} string "Bad Request"
|
|
// @Router /api/v1/payments/webhook [post]
|
|
func (h *PaymentHandler) HandleWebhook(w http.ResponseWriter, r *http.Request) {
|
|
// Get Stripe config from encrypted vault
|
|
var config StripeConfig
|
|
payload, err := h.credentialsService.GetDecryptedKey(r.Context(), "stripe")
|
|
if err == nil {
|
|
json.Unmarshal([]byte(payload), &config)
|
|
}
|
|
|
|
// Fallback to Env
|
|
if config.WebhookSecret == "" {
|
|
config.WebhookSecret = os.Getenv("STRIPE_WEBHOOK_SECRET")
|
|
}
|
|
|
|
if config.WebhookSecret == "" {
|
|
http.Error(w, "Webhook secret not configured", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Read body
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
http.Error(w, "Failed to read body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Verify signature
|
|
signature := r.Header.Get("Stripe-Signature")
|
|
if !verifyStripeSignature(body, signature, config.WebhookSecret) {
|
|
http.Error(w, "Invalid signature", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Parse event
|
|
var event map[string]interface{}
|
|
if err := json.Unmarshal(body, &event); err != nil {
|
|
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
eventType, ok := event["type"].(string)
|
|
if !ok {
|
|
http.Error(w, "Missing event type", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Handle event types
|
|
switch eventType {
|
|
case "checkout.session.completed":
|
|
h.handleCheckoutComplete(event)
|
|
case "payment_intent.succeeded":
|
|
h.handlePaymentSuccess(event)
|
|
case "payment_intent.payment_failed":
|
|
h.handlePaymentFailed(event)
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"received": true}`))
|
|
}
|
|
|
|
func (h *PaymentHandler) handleCheckoutComplete(event map[string]interface{}) {
|
|
// Extract session data and update job payment status
|
|
data, _ := event["data"].(map[string]interface{})
|
|
obj, _ := data["object"].(map[string]interface{})
|
|
|
|
sessionID, _ := obj["id"].(string)
|
|
metadata, _ := obj["metadata"].(map[string]interface{})
|
|
jobIDStr, _ := metadata["job_id"].(string)
|
|
|
|
if jobIDStr != "" && sessionID != "" {
|
|
// TODO: Update job_payments table to mark as completed
|
|
fmt.Printf("Payment completed for job %s, session %s\n", jobIDStr, sessionID)
|
|
}
|
|
}
|
|
|
|
func (h *PaymentHandler) handlePaymentSuccess(_ map[string]interface{}) {
|
|
// Payment succeeded
|
|
fmt.Println("Payment succeeded")
|
|
}
|
|
|
|
func (h *PaymentHandler) handlePaymentFailed(_ map[string]interface{}) {
|
|
// Payment failed
|
|
fmt.Println("Payment failed")
|
|
}
|
|
|
|
// GetPaymentStatus returns the status of a payment
|
|
// @Summary Get payment status
|
|
// @Description Get the status of a job posting payment
|
|
// @Tags Payments
|
|
// @Produce json
|
|
// @Param id path string true "Payment ID"
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 404 {string} string "Not Found"
|
|
// @Router /api/v1/payments/status/{id} [get]
|
|
func (h *PaymentHandler) GetPaymentStatus(w http.ResponseWriter, r *http.Request) {
|
|
paymentID := r.PathValue("id")
|
|
if paymentID == "" {
|
|
http.Error(w, "Payment ID is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// TODO: Query job_payments table for status
|
|
response := map[string]interface{}{
|
|
"id": paymentID,
|
|
"status": "pending",
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(response)
|
|
}
|
|
|
|
// Helper function to create Stripe checkout session via API
|
|
func createStripeCheckoutSession(secretKey string, req CreateCheckoutRequest) (string, string, error) {
|
|
client := &http.Client{}
|
|
|
|
// Build form data
|
|
data := fmt.Sprintf(
|
|
"mode=payment&success_url=%s&cancel_url=%s&line_items[0][price]=%s&line_items[0][quantity]=1&metadata[job_id]=%d",
|
|
req.SuccessURL, req.CancelURL, req.PriceID, req.JobID,
|
|
)
|
|
|
|
httpReq, err := http.NewRequest("POST", "https://api.stripe.com/v1/checkout/sessions",
|
|
io.NopCloser(io.Reader(nil)))
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
httpReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", secretKey))
|
|
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
httpReq.Body = io.NopCloser(strings.NewReader(data))
|
|
|
|
resp, err := client.Do(httpReq)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 400 {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return "", "", fmt.Errorf("Stripe API error: %s", string(body))
|
|
}
|
|
|
|
var result map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
sessionID, _ := result["id"].(string)
|
|
checkoutURL, _ := result["url"].(string)
|
|
|
|
return sessionID, checkoutURL, nil
|
|
}
|
|
|
|
// Verify Stripe webhook signature
|
|
func verifyStripeSignature(payload []byte, header, secret string) bool {
|
|
if header == "" {
|
|
return false
|
|
}
|
|
|
|
// Parse signature header
|
|
var timestamp string
|
|
var signatures []string
|
|
|
|
parts := splitHeader(header)
|
|
for _, p := range parts {
|
|
if len(p) > 2 && p[0] == 't' && p[1] == '=' {
|
|
timestamp = p[2:]
|
|
} else if len(p) > 3 && p[0] == 'v' && p[1] == '1' && p[2] == '=' {
|
|
signatures = append(signatures, p[3:])
|
|
}
|
|
}
|
|
|
|
if timestamp == "" || len(signatures) == 0 {
|
|
return false
|
|
}
|
|
|
|
// Check timestamp (5 min tolerance)
|
|
ts, err := strconv.ParseInt(timestamp, 10, 64)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if time.Now().Unix()-ts > 300 {
|
|
return false
|
|
}
|
|
|
|
// Compute expected signature
|
|
signedPayload := fmt.Sprintf("%s.%s", timestamp, string(payload))
|
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
mac.Write([]byte(signedPayload))
|
|
expectedSig := hex.EncodeToString(mac.Sum(nil))
|
|
|
|
// Compare signatures
|
|
for _, sig := range signatures {
|
|
if hmac.Equal([]byte(sig), []byte(expectedSig)) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func splitHeader(header string) []string {
|
|
return strings.Split(header, ",")
|
|
}
|