Files

259 lines
8.4 KiB
Go
Raw Permalink Normal View History

package httpapi
import (
"net/http"
"strconv"
"github.com/descrybe/descrybe-v2/apps/api/internal/billing"
"github.com/descrybe/descrybe-v2/apps/api/internal/sales"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
)
func (s *Server) salesSvc() *sales.Service {
if s.Sales != nil {
return s.Sales
}
s.Sales = &sales.Service{Pool: s.Pool}
return s.Sales
}
type salesContactBody struct {
Name string `json:"name"`
Email string `json:"email"`
CompanyName string `json:"company_name"`
Phone string `json:"phone"`
Message string `json:"message"`
EstimatedSKUs *int `json:"estimated_skus"`
Source string `json:"source"`
}
// handleSalesContact is public (CSRF required, session optional).
func (s *Server) handleSalesContact(w http.ResponseWriter, r *http.Request) {
var body salesContactBody
if err := DecodeJSON(r, &body); err != nil {
Error(w, http.StatusBadRequest, "invalid json")
return
}
in := sales.CreateLeadInput{
Name: body.Name,
Email: body.Email,
CompanyName: body.CompanyName,
Phone: body.Phone,
Message: body.Message,
EstimatedSKUs: body.EstimatedSKUs,
Source: body.Source,
}
if uid, ok := UserIDFromContext(r.Context()); ok && uid != uuid.Nil {
in.UserID = &uid
}
if cid, ok := CompanyIDFromContext(r.Context()); ok && cid != uuid.Nil {
in.CompanyID = &cid
}
lead, err := s.salesSvc().CreateLead(r.Context(), in)
if err != nil {
ClientOrLog(w, http.StatusBadRequest, "could not submit contact request", err, sales.ClientError)
return
}
JSON(w, http.StatusCreated, map[string]any{"lead": lead})
}
func (s *Server) handleAdminListSalesLeads(w http.ResponseWriter, r *http.Request) {
status := r.URL.Query().Get("status")
q := r.URL.Query().Get("q")
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
leads, total, err := s.salesSvc().ListLeads(r.Context(), status, q, limit, offset)
if err != nil {
ClientOrLog(w, http.StatusInternalServerError, "could not list sales leads", err, sales.ClientError)
return
}
JSON(w, http.StatusOK, map[string]any{"leads": leads, "total": total})
}
func (s *Server) handleAdminGetSalesLead(w http.ResponseWriter, r *http.Request) {
id, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
Error(w, http.StatusBadRequest, "invalid id")
return
}
lead, err := s.salesSvc().GetLead(r.Context(), id)
if err != nil {
ClientOrLog(w, http.StatusNotFound, "lead not found", err, sales.ClientError)
return
}
quotes, err := s.salesSvc().ListQuotesForLead(r.Context(), id)
if err != nil {
ClientOrLog(w, http.StatusInternalServerError, "could not list quotes", err, sales.ClientError)
return
}
JSON(w, http.StatusOK, map[string]any{"lead": lead, "quotes": quotes})
}
type adminUpdateSalesLeadBody struct {
Status *string `json:"status"`
CompanyID *uuid.UUID `json:"company_id"`
ClearCompany bool `json:"clear_company"`
AdminNotes *string `json:"admin_notes"`
}
func (s *Server) handleAdminUpdateSalesLead(w http.ResponseWriter, r *http.Request) {
id, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
Error(w, http.StatusBadRequest, "invalid id")
return
}
var body adminUpdateSalesLeadBody
if err := DecodeJSON(r, &body); err != nil {
Error(w, http.StatusBadRequest, "invalid json")
return
}
lead, err := s.salesSvc().UpdateLead(r.Context(), id, sales.UpdateLeadInput{
Status: body.Status,
CompanyID: body.CompanyID,
ClearCompany: body.ClearCompany,
AdminNotes: body.AdminNotes,
})
if err != nil {
ClientOrLog(w, http.StatusBadRequest, "could not update lead", err, sales.ClientError)
return
}
JSON(w, http.StatusOK, map[string]any{"lead": lead})
}
type adminCreateSalesQuoteBody struct {
CompanyID uuid.UUID `json:"company_id"`
PlanName string `json:"plan_name"`
MonthlyCredits int `json:"monthly_credits"`
MaxProducts *int `json:"max_products"`
Currency string `json:"currency"`
TotalAmountCents int `json:"total_amount_cents"`
InstallmentCount int `json:"installment_count"`
InstallmentInterval string `json:"installment_interval"`
TermMonths *int `json:"term_months"`
PrepareCheckout bool `json:"prepare_checkout"`
}
func (s *Server) handleAdminCreateSalesQuote(w http.ResponseWriter, r *http.Request) {
leadID, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
Error(w, http.StatusBadRequest, "invalid id")
return
}
var body adminCreateSalesQuoteBody
if err := DecodeJSON(r, &body); err != nil {
Error(w, http.StatusBadRequest, "invalid json")
return
}
var createdBy *uuid.UUID
if uid, ok := UserIDFromContext(r.Context()); ok && uid != uuid.Nil {
createdBy = &uid
}
quote, err := s.salesSvc().CreateQuote(r.Context(), leadID, sales.CreateQuoteInput{
CompanyID: body.CompanyID,
PlanName: body.PlanName,
MonthlyCredits: body.MonthlyCredits,
MaxProducts: body.MaxProducts,
Currency: body.Currency,
TotalAmountCents: body.TotalAmountCents,
InstallmentCount: body.InstallmentCount,
InstallmentInterval: body.InstallmentInterval,
TermMonths: body.TermMonths,
CreatedByUserID: createdBy,
})
if err != nil {
ClientOrLog(w, http.StatusBadRequest, "could not create quote", err, sales.ClientError)
return
}
if body.PrepareCheckout {
quote, err = s.prepareSalesQuoteCheckout(r, quote)
if err != nil {
ClientOrLog(w, http.StatusBadRequest, "quote created but checkout failed", err, func(e error) (string, bool) {
if msg, ok := sales.ClientError(e); ok {
return msg, true
}
return billing.ClientError(e)
})
return
}
}
JSON(w, http.StatusCreated, map[string]any{"quote": quote})
}
func (s *Server) handleAdminPrepareSalesQuoteCheckout(w http.ResponseWriter, r *http.Request) {
quoteID, err := uuid.Parse(chi.URLParam(r, "quoteID"))
if err != nil {
Error(w, http.StatusBadRequest, "invalid quote id")
return
}
quote, err := s.salesSvc().GetQuote(r.Context(), quoteID)
if err != nil {
ClientOrLog(w, http.StatusNotFound, "quote not found", err, sales.ClientError)
return
}
quote, err = s.prepareSalesQuoteCheckout(r, quote)
if err != nil {
ClientOrLog(w, http.StatusBadRequest, "could not prepare checkout", err, func(e error) (string, bool) {
if msg, ok := sales.ClientError(e); ok {
return msg, true
}
return billing.ClientError(e)
})
return
}
JSON(w, http.StatusOK, map[string]any{"quote": quote})
}
func (s *Server) handleAdminMarkSalesQuoteSent(w http.ResponseWriter, r *http.Request) {
quoteID, err := uuid.Parse(chi.URLParam(r, "quoteID"))
if err != nil {
Error(w, http.StatusBadRequest, "invalid quote id")
return
}
quote, err := s.salesSvc().MarkQuoteSent(r.Context(), quoteID)
if err != nil {
ClientOrLog(w, http.StatusBadRequest, "could not mark quote sent", err, sales.ClientError)
return
}
JSON(w, http.StatusOK, map[string]any{"quote": quote})
}
func (s *Server) prepareSalesQuoteCheckout(r *http.Request, quote sales.Quote) (sales.Quote, error) {
if quote.PlanID == nil || *quote.PlanID <= 0 {
return quote, sales.ErrQuoteNotReady
}
if quote.Status == "paid" || quote.Status == "canceled" {
return quote, sales.ErrQuoteNotReady
}
var companyName, billingEmail string
_ = s.Pool.QueryRow(r.Context(), `SELECT name FROM companies WHERE id = $1`, quote.CompanyID).Scan(&companyName)
lead, err := s.salesSvc().GetLead(r.Context(), quote.LeadID)
if err == nil {
billingEmail = lead.Email
}
res, err := s.stripeSvc().CreateSalesQuoteCheckout(r.Context(), billing.SalesQuoteCheckoutInput{
QuoteID: quote.ID,
CompanyID: quote.CompanyID,
PlanID: *quote.PlanID,
PlanName: quote.PlanName,
Email: billingEmail,
CompanyName: companyName,
Currency: quote.Currency,
TotalAmountCents: quote.TotalAmountCents,
InstallmentCount: quote.InstallmentCount,
InstallmentInterval: quote.InstallmentInterval,
InstallmentAmountCents: quote.InstallmentAmountCents,
})
if err != nil {
return quote, err
}
if res.Applied {
updated, getErr := s.salesSvc().GetQuote(r.Context(), quote.ID)
if getErr != nil {
return quote, getErr
}
return updated, nil
}
return s.salesSvc().MarkQuoteCheckoutReady(r.Context(), quote.ID, res.ProductID, res.PriceID, res.SessionID, res.URL)
}