Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
package billing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SyncCreditPackResult is one pack after Stripe Product/Price ensure.
|
||||
type SyncCreditPackResult struct {
|
||||
PackID string `json:"pack_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
PriceID string `json:"price_id"`
|
||||
Created bool `json:"created"`
|
||||
Credits int `json:"credits"`
|
||||
PriceUSD int `json:"price_usd"`
|
||||
}
|
||||
|
||||
// SyncCreditPackProducts creates/updates Stripe Products + one-time Prices for
|
||||
// DefaultCreditPacks. Packs are additional one-time products (Checkout mode=payment),
|
||||
// not subscription add-ons. Metadata descrybe_pack=<id> identifies each product.
|
||||
func (s *StripeService) SyncCreditPackProducts(ctx context.Context) ([]SyncCreditPackResult, error) {
|
||||
ctx, cfg, err := s.bindCfg(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cfg.MockMode() || strings.TrimSpace(cfg.SecretKey) == "" {
|
||||
return nil, ErrStripeNotConfigured
|
||||
}
|
||||
|
||||
out := make([]SyncCreditPackResult, 0, len(DefaultCreditPacks()))
|
||||
for _, pack := range DefaultCreditPacks() {
|
||||
productID, createdProduct, err := s.ensureCreditPackProduct(ctx, pack)
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("pack %s product: %w", pack.ID, err)
|
||||
}
|
||||
priceID, createdPrice, err := s.ensureCreditPackPrice(ctx, productID, pack)
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("pack %s price: %w", pack.ID, err)
|
||||
}
|
||||
out = append(out, SyncCreditPackResult{
|
||||
PackID: pack.ID,
|
||||
ProductID: productID,
|
||||
PriceID: priceID,
|
||||
Created: createdProduct || createdPrice,
|
||||
Credits: pack.Credits,
|
||||
PriceUSD: pack.PriceUSD,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *StripeService) ensureCreditPackProduct(ctx context.Context, pack CreditPack) (productID string, created bool, err error) {
|
||||
q := url.QueryEscape(fmt.Sprintf("active:'true' AND metadata['descrybe_pack']:'%s'", pack.ID))
|
||||
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 "", false, err
|
||||
}
|
||||
if len(search.Data) > 0 && strings.TrimSpace(search.Data[0].ID) != "" {
|
||||
return search.Data[0].ID, false, nil
|
||||
}
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("name", "Descrybe AI credits — "+pack.Name)
|
||||
form.Set("description", pack.Description)
|
||||
form.Set("metadata[descrybe_pack]", pack.ID)
|
||||
form.Set("metadata[credits]", strconv.Itoa(pack.Credits))
|
||||
form.Set("metadata[ai_products]", strconv.Itoa(pack.AIProducts))
|
||||
form.Set("metadata[price_usd]", strconv.Itoa(pack.PriceUSD))
|
||||
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 "", false, err
|
||||
}
|
||||
if strings.TrimSpace(product.ID) == "" {
|
||||
return "", false, fmt.Errorf("stripe product missing id")
|
||||
}
|
||||
return product.ID, true, nil
|
||||
}
|
||||
|
||||
func (s *StripeService) ensureCreditPackPrice(ctx context.Context, productID string, pack CreditPack) (priceID string, created bool, err error) {
|
||||
// Reuse an active one-time price on this product that matches unit amount.
|
||||
wantCents := pack.PriceUSD * 100
|
||||
var list struct {
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
UnitAmount int64 `json:"unit_amount"`
|
||||
Currency string `json:"currency"`
|
||||
Type string `json:"type"`
|
||||
Active bool `json:"active"`
|
||||
} `json:"data"`
|
||||
}
|
||||
endpoint := "https://api.stripe.com/v1/prices?product=" + url.QueryEscape(productID) + "&active=true&limit=20"
|
||||
if err := s.stripeGET(ctx, endpoint, &list); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
for _, p := range list.Data {
|
||||
if p.Active && p.Type == "one_time" && strings.EqualFold(p.Currency, "usd") && int(p.UnitAmount) == wantCents {
|
||||
return p.ID, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("product", productID)
|
||||
form.Set("currency", "usd")
|
||||
form.Set("unit_amount", strconv.Itoa(wantCents))
|
||||
form.Set("metadata[descrybe_pack]", pack.ID)
|
||||
form.Set("metadata[credits]", strconv.Itoa(pack.Credits))
|
||||
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 "", false, err
|
||||
}
|
||||
if strings.TrimSpace(price.ID) == "" {
|
||||
return "", false, fmt.Errorf("stripe price missing id")
|
||||
}
|
||||
return price.ID, true, nil
|
||||
}
|
||||
Reference in New Issue
Block a user