independencia del provider

This commit is contained in:
2026-07-09 16:55:39 +02:00
parent 61a43ef481
commit 6400485b6d
5 changed files with 164 additions and 17 deletions
+67 -7
View File
@@ -163,6 +163,10 @@ type eaCustomer struct {
Notes string `json:"notes"`
}
type eaProvider struct {
ID flexInt `json:"id"`
}
type eaAppointment struct {
Start string `json:"start"`
End string `json:"end"`
@@ -181,7 +185,6 @@ type bookRequest struct {
Email string `json:"email"`
Telefono string `json:"telefono"`
Mensaje string `json:"mensaje"`
ProviderID int `json:"providerId"`
ServiceIDs []int `json:"serviceIds"`
Date string `json:"date"` // YYYY-MM-DD
Time string `json:"time"` // HH:MM
@@ -195,8 +198,9 @@ var (
// handleBook orquesta la reserva contra EasyAppointments:
// 1. valida la entrada,
// 2. lee los servicios de EA (fuente de verdad de nombre/duración),
// 3. busca o crea el cliente por email,
// 4. crea UNA cita con el servicio de mayor duración y el bloque total en notas.
// 3. resuelve el provider (server-side, ver providerID),
// 4. busca o crea el cliente por email,
// 5. crea UNA cita con el servicio de mayor duración y el bloque total en notas.
func handleBook(ea *eaClient) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
@@ -216,7 +220,7 @@ func handleBook(ea *eaClient) http.HandlerFunc {
req.Mensaje = strings.TrimSpace(req.Mensaje)
if req.Nombre == "" || req.Apellidos == "" || req.Email == "" || req.Telefono == "" ||
len(req.ServiceIDs) == 0 || req.ProviderID <= 0 ||
len(req.ServiceIDs) == 0 ||
!reDate.MatchString(req.Date) || !reTime.MatchString(req.Time) {
writeJSONError(w, http.StatusBadRequest, "datos_incompletos")
return
@@ -284,7 +288,15 @@ func handleBook(ea *eaClient) http.HandlerFunc {
}
notes := strings.Join(notesLines, "\n")
// 5. cliente: buscar por email o crear (en EA).
// 5. provider resuelto server-side: el navegador no elige contra quién reserva.
providerID, err := ea.providerID()
if err != nil {
log.Printf("book: error resolviendo provider: %v", err)
writeJSONError(w, http.StatusBadGateway, "proveedor_no_disponible")
return
}
// 6. cliente: buscar por email o crear (en EA).
customerID, err := ea.findOrCreateCustomer(req)
if err != nil {
log.Printf("book: error con cliente: %v", err)
@@ -292,12 +304,12 @@ func handleBook(ea *eaClient) http.HandlerFunc {
return
}
// 6. crear la cita (en EA).
// 7. crear la cita (en EA).
appt := eaAppointment{
Start: start.Format(layout),
End: end.Format(layout),
ServiceID: int(primary.ID),
ProviderID: req.ProviderID,
ProviderID: providerID,
CustomerID: customerID,
Notes: notes,
Status: "booked",
@@ -314,6 +326,50 @@ func handleBook(ea *eaClient) http.HandlerFunc {
}
}
// providerID resuelve el id del provider contra el que se reserva. Con
// EA_PROVIDER_ID definido se usa ese valor; si no, se pregunta a EA y se toma
// el primero (el salón tiene un único provider). Así el id no va hardcodeado
// y cada entorno (desarrollo, producción) resuelve el suyo.
func (c *eaClient) providerID() (int, error) {
if v := os.Getenv("EA_PROVIDER_ID"); v != "" {
n, err := strconv.Atoi(v)
if err != nil || n <= 0 {
return 0, fmt.Errorf("EA_PROVIDER_ID inválido: %q", v)
}
return n, nil
}
var provs []eaProvider
if err := c.do(http.MethodGet, "/providers", nil, &provs); err != nil {
return 0, err
}
if len(provs) == 0 {
return 0, fmt.Errorf("EA no tiene ningún provider")
}
if len(provs) > 1 {
log.Printf("aviso: EA tiene %d providers; se usa el primero (id %d). Fija EA_PROVIDER_ID para elegir otro.", len(provs), int(provs[0].ID))
}
return int(provs[0].ID), nil
}
// handleConfig expone al frontend la configuración mínima que necesita para
// consultar disponibilidad por el proxy: solo el id del provider.
func handleConfig(ea *eaClient) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSONError(w, http.StatusMethodNotAllowed, "method_not_allowed")
return
}
id, err := ea.providerID()
if err != nil {
log.Printf("config: error resolviendo provider: %v", err)
writeJSONError(w, http.StatusBadGateway, "proveedor_no_disponible")
return
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"providerId": %d}`, id)
}
}
// findOrCreateCustomer busca un cliente por email en EA y, si no existe, lo crea.
func (c *eaClient) findOrCreateCustomer(req bookRequest) (int, error) {
var found []eaCustomer
@@ -408,6 +464,10 @@ func main() {
// Reservas: orquestadas server-side contra EasyAppointments.
http.HandleFunc("/api/book", handleBook(ea))
// Config para el frontend: el id del provider se resuelve aquí (EA o
// EA_PROVIDER_ID), nunca va hardcodeado en el HTML.
http.HandleFunc("/api/config", handleConfig(ea))
http.Handle("/", http.FileServer(http.FS(staticFS)))
addr := fmt.Sprintf(":%d", *port)