package billing import ( "context" "errors" "fmt" "net/http" "net/url" "strconv" "strings" "time" "github.com/google/uuid" ) // SalesQuoteCheckoutInput drives Checkout for an admin-prepared custom deal. type SalesQuoteCheckoutInput struct { QuoteID uuid.UUID CompanyID uuid.UUID PlanID int64 PlanName string Email string CompanyName string Currency string TotalAmountCents int InstallmentCount int InstallmentInterval string // month | quarter | year InstallmentAmountCents int } // SalesQuoteCheckoutResult is returned to admin after preparing Checkout for a quote. type SalesQuoteCheckoutResult struct { URL string `json:"url"` Mock bool `json:"mock"` Applied bool `json:"applied,omitempty"` Message string `json:"message,omitempty"` ProductID string `json:"product_id,omitempty"` PriceID string `json:"price_id,omitempty"` SessionID string `json:"session_id,omitempty"` } // CreateSalesQuoteCheckout creates a Stripe Price + Checkout Session for a sales quote. // // ASSUMPTION (installments): // - installment_count == 1 → Checkout mode=payment (one-time Price). // - installment_count > 1 → Checkout mode=subscription with a recurring Price equal to // installment_amount_cents; subscription_data.cancel_at ends billing after N intervals // (month / quarter=3 months / year). Stripe collects the first installment at Checkout; // later invoices are charged automatically on the subscription. func (s *StripeService) CreateSalesQuoteCheckout(ctx context.Context, in SalesQuoteCheckoutInput) (SalesQuoteCheckoutResult, error) { if in.QuoteID == uuid.Nil || in.CompanyID == uuid.Nil || in.PlanID <= 0 { return SalesQuoteCheckoutResult{}, fmt.Errorf("%w: quote identifiers", ErrStripePlanUnsupported) } if in.InstallmentAmountCents <= 0 || in.TotalAmountCents <= 0 { return SalesQuoteCheckoutResult{}, fmt.Errorf("%w: amounts", ErrStripePlanUnsupported) } count := in.InstallmentCount if count <= 0 { count = 1 } interval := strings.ToLower(strings.TrimSpace(in.InstallmentInterval)) if interval == "" { interval = "month" } currency := strings.ToLower(strings.TrimSpace(in.Currency)) if currency == "" { currency = "usd" } ctx, cfg, err := s.bindCfg(ctx) if err != nil { return SalesQuoteCheckoutResult{}, err } web := strings.TrimRight(cfg.WebOrigin, "/") if web == "" { web = "http://localhost:5174" } if cfg.AllowMockPurchase() { if err := s.applySalesQuotePurchase(ctx, in.CompanyID, in.PlanID, in.QuoteID, "cus_mock_"+in.CompanyID.String()[:8], "sub_mock_"+uuid.NewString()[:8], "price_mock_quote"); err != nil { return SalesQuoteCheckoutResult{}, err } return SalesQuoteCheckoutResult{ URL: web + "/billing?checkout=success&mock=1&sales_quote=" + url.QueryEscape(in.QuoteID.String()), Mock: true, Applied: true, Message: "Mock mode: custom sales quote plan assigned without Stripe.", }, nil } if cfg.MockMode() { return SalesQuoteCheckoutResult{}, ErrStripeNotConfigured } productID, err := s.ensureSalesQuoteProduct(ctx, in) if err != nil { return SalesQuoteCheckoutResult{}, err } priceID, err := s.createSalesQuotePrice(ctx, productID, in, count == 1) if err != nil { return SalesQuoteCheckoutResult{}, err } customerID, err := s.ensureCustomer(ctx, in.CompanyID, in.Email, in.CompanyName) if err != nil { return SalesQuoteCheckoutResult{}, err } form := url.Values{} form.Set("success_url", web+"/billing?checkout=success&sales_quote="+url.QueryEscape(in.QuoteID.String())) form.Set("cancel_url", web+"/billing?checkout=cancel&sales_quote="+url.QueryEscape(in.QuoteID.String())) form.Set("client_reference_id", in.CompanyID.String()) form.Set("metadata[company_id]", in.CompanyID.String()) form.Set("metadata[kind]", "sales_quote") form.Set("metadata[quote_id]", in.QuoteID.String()) form.Set("metadata[plan_id]", strconv.FormatInt(in.PlanID, 10)) form.Set("metadata[plan]", strings.ToLower(strings.TrimSpace(in.PlanName))) form.Set("metadata[installment_count]", strconv.Itoa(count)) form.Set("metadata[installment_interval]", interval) form.Set("line_items[0][price]", priceID) form.Set("line_items[0][quantity]", "1") form.Set("allow_promotion_codes", "true") if customerID != "" { form.Set("customer", customerID) } else if in.Email != "" { form.Set("customer_email", in.Email) } if count == 1 { form.Set("mode", "payment") form.Set("payment_intent_data[metadata][company_id]", in.CompanyID.String()) form.Set("payment_intent_data[metadata][kind]", "sales_quote") form.Set("payment_intent_data[metadata][quote_id]", in.QuoteID.String()) form.Set("payment_intent_data[metadata][plan_id]", strconv.FormatInt(in.PlanID, 10)) } else { form.Set("mode", "subscription") form.Set("subscription_data[metadata][company_id]", in.CompanyID.String()) form.Set("subscription_data[metadata][kind]", "sales_quote") form.Set("subscription_data[metadata][quote_id]", in.QuoteID.String()) form.Set("subscription_data[metadata][plan_id]", strconv.FormatInt(in.PlanID, 10)) form.Set("subscription_data[metadata][plan]", strings.ToLower(strings.TrimSpace(in.PlanName))) cancelAt, err := salesQuoteCancelAt(time.Now().UTC(), count, interval) if err != nil { return SalesQuoteCheckoutResult{}, err } form.Set("subscription_data[cancel_at]", strconv.FormatInt(cancelAt.Unix(), 10)) } var sess struct { ID string `json:"id"` URL string `json:"url"` } if err := s.stripeForm(ctx, http.MethodPost, "https://api.stripe.com/v1/checkout/sessions", form, &sess); err != nil { return SalesQuoteCheckoutResult{}, err } if sess.URL == "" { return SalesQuoteCheckoutResult{}, errors.New("stripe checkout session missing url") } return SalesQuoteCheckoutResult{ URL: sess.URL, Mock: false, ProductID: productID, PriceID: priceID, SessionID: sess.ID, }, nil } func (s *StripeService) ensureSalesQuoteProduct(ctx context.Context, in SalesQuoteCheckoutInput) (string, error) { metaKey := in.QuoteID.String() q := url.QueryEscape(fmt.Sprintf("active:'true' AND metadata['descrybe_sales_quote']:'%s'", metaKey)) var search struct { Data []struct { ID string `json:"id"` } `json:"data"` } if err := s.stripeGET(ctx, "https://api.stripe.com/v1/products/search?query="+q+"&limit=1", &search); err != nil { return "", err } if len(search.Data) > 0 && strings.TrimSpace(search.Data[0].ID) != "" { return search.Data[0].ID, nil } form := url.Values{} name := strings.TrimSpace(in.PlanName) if name == "" { name = "Custom Descrybe plan" } form.Set("name", "Descrybe — "+name) form.Set("description", fmt.Sprintf("Sales quote %s (%d installments)", in.QuoteID.String(), in.InstallmentCount)) form.Set("metadata[descrybe_sales_quote]", metaKey) form.Set("metadata[company_id]", in.CompanyID.String()) form.Set("metadata[plan_id]", strconv.FormatInt(in.PlanID, 10)) var product struct { ID string `json:"id"` } if err := s.stripeForm(ctx, http.MethodPost, "https://api.stripe.com/v1/products", form, &product); err != nil { return "", err } if strings.TrimSpace(product.ID) == "" { return "", fmt.Errorf("stripe product missing id") } return product.ID, nil } func (s *StripeService) createSalesQuotePrice(ctx context.Context, productID string, in SalesQuoteCheckoutInput, oneTime bool) (string, error) { form := url.Values{} form.Set("product", productID) form.Set("currency", strings.ToLower(strings.TrimSpace(in.Currency))) if form.Get("currency") == "" { form.Set("currency", "usd") } form.Set("unit_amount", strconv.Itoa(in.InstallmentAmountCents)) form.Set("metadata[descrybe_sales_quote]", in.QuoteID.String()) form.Set("metadata[plan_id]", strconv.FormatInt(in.PlanID, 10)) if oneTime { // default type one_time } else { stripeInterval, intervalCount, err := stripeRecurringFromInstallment(in.InstallmentInterval) if err != nil { return "", err } form.Set("recurring[interval]", stripeInterval) if intervalCount > 1 { form.Set("recurring[interval_count]", strconv.Itoa(intervalCount)) } } var price struct { ID string `json:"id"` } if err := s.stripeForm(ctx, http.MethodPost, "https://api.stripe.com/v1/prices", form, &price); err != nil { return "", err } if strings.TrimSpace(price.ID) == "" { return "", fmt.Errorf("stripe price missing id") } return price.ID, nil } func stripeRecurringFromInstallment(interval string) (stripeInterval string, intervalCount int, err error) { switch strings.ToLower(strings.TrimSpace(interval)) { case "month", "": return "month", 1, nil case "quarter": return "month", 3, nil case "year": return "year", 1, nil default: return "", 0, fmt.Errorf("%w: installment interval %q", ErrStripePlanUnsupported, interval) } } func salesQuoteCancelAt(now time.Time, count int, interval string) (time.Time, error) { if count < 1 { return time.Time{}, fmt.Errorf("%w: installment count", ErrStripePlanUnsupported) } switch strings.ToLower(strings.TrimSpace(interval)) { case "month", "": return now.AddDate(0, count, 0), nil case "quarter": return now.AddDate(0, count*3, 0), nil case "year": return now.AddDate(count, 0, 0), nil default: return time.Time{}, fmt.Errorf("%w: installment interval %q", ErrStripePlanUnsupported, interval) } } func (s *StripeService) applySalesQuotePurchase(ctx context.Context, companyID uuid.UUID, planID int64, quoteID uuid.UUID, customerID, subscriptionID, priceID string) error { if s.Billing == nil { return errors.New("billing service not configured") } if planID <= 0 { return fmt.Errorf("%w: plan_id", ErrStripePlanUnsupported) } if err := s.Billing.AssignPlan(ctx, companyID, planID, false, 0); err != nil { return err } if customerID != "" { _, _ = s.Pool.Exec(ctx, ` UPDATE companies SET stripe_customer_id = $2, updated_at = now() WHERE id = $1`, companyID, customerID) } _, _ = s.Pool.Exec(ctx, ` UPDATE company_plans SET stripe_subscription_id = NULLIF($2, ''), stripe_price_id = NULLIF($3, ''), updated_at = now() WHERE company_id = $1 AND is_active = true`, companyID, subscriptionID, priceID) _ = s.setSubscriptionStatusNote(ctx, companyID, "active") if quoteID != uuid.Nil { _, err := s.Pool.Exec(ctx, ` UPDATE sales_quotes SET status = 'paid', paid_at = COALESCE(paid_at, now()), updated_at = now() WHERE id = $1 AND status <> 'canceled'`, quoteID) if err != nil { return err } _, _ = s.Pool.Exec(ctx, ` UPDATE sales_leads SET status = 'won', updated_at = now() WHERE id = (SELECT lead_id FROM sales_quotes WHERE id = $1) AND status <> 'closed'`, quoteID) } return nil }