diff --git a/openmeter/app/stripe/entity/app/calculator.go b/openmeter/app/stripe/entity/app/calculator.go index eff3c10b2..f7f3af130 100644 --- a/openmeter/app/stripe/entity/app/calculator.go +++ b/openmeter/app/stripe/entity/app/calculator.go @@ -4,6 +4,9 @@ import ( "fmt" "github.com/alpacahq/alpacadecimal" + "github.com/invopop/gobl/num" + "golang.org/x/text/language" + "golang.org/x/text/message" "github.com/openmeterio/openmeter/pkg/currencyx" ) @@ -17,6 +20,7 @@ func NewStripeCalculator(currency currencyx.Code) (StripeCalculator, error) { return StripeCalculator{ calculator: calculator, + printer: message.NewPrinter(language.English), multiplier: alpacadecimal.NewFromInt(10).Pow(alpacadecimal.NewFromInt(int64(calculator.Def.Subunits))), }, nil } @@ -24,6 +28,7 @@ func NewStripeCalculator(currency currencyx.Code) (StripeCalculator, error) { // StripeCalculator provides a currency calculator object. type StripeCalculator struct { calculator currencyx.Calculator + printer *message.Printer multiplier alpacadecimal.Decimal } @@ -31,3 +36,29 @@ type StripeCalculator struct { func (c StripeCalculator) RoundToAmount(amount alpacadecimal.Decimal) int64 { return amount.Mul(c.multiplier).Round(0).IntPart() } + +// FormatAmount formats the amount +func (c StripeCalculator) FormatAmount(amount alpacadecimal.Decimal) string { + if amount.IsInteger() { + return c.calculator.Def.FormatAmount(num.MakeAmount(amount.IntPart(), 0)) + } + + am, _ := amount.Float64() + return c.calculator.Def.FormatAmount(num.AmountFromFloat64(am, uint32(amount.NumDigits()))) +} + +// FormatQuantity formats the quantity to two decimal places. +// This should be only used to display the quantity not for calculations. +func (c StripeCalculator) FormatQuantity(quantity alpacadecimal.Decimal) string { + if quantity.IsInteger() { + return c.printer.Sprintf("%d", quantity.IntPart()) + } else { + f, _ := quantity.Float64() + return c.printer.Sprintf("%.2f", f) + } +} + +// IsInteger checks if the amount is an integer in the Stripe currency. +func (c StripeCalculator) IsInteger(amount alpacadecimal.Decimal) bool { + return amount.Mul(c.multiplier).IsInteger() +} diff --git a/openmeter/app/stripe/entity/app/invoice.go b/openmeter/app/stripe/entity/app/invoice.go index e13ce9998..0eccd0f33 100644 --- a/openmeter/app/stripe/entity/app/invoice.go +++ b/openmeter/app/stripe/entity/app/invoice.go @@ -5,6 +5,7 @@ import ( "fmt" "sort" + "github.com/alpacahq/alpacadecimal" "github.com/samber/lo" "github.com/stripe/stripe-go/v80" @@ -457,9 +458,8 @@ func getStripeUpdateLinesLineParams( // getStripeAddLinesLineParams returns the Stripe line item func getStripeAddLinesLineParams(line *billing.Line, calculator StripeCalculator) *stripe.InvoiceAddLinesLineParams { - name := getLineName(line) + description := getLineName(line) period := getPeriod(line) - amount := line.Totals.Amount // Handle usage based commitments like minimum spend @@ -469,9 +469,19 @@ func getStripeAddLinesLineParams(line *billing.Line, calculator StripeCalculator amount = line.Totals.ChargesTotal } + // If the line has a quantity we add the quantity and per unit amount to the description + if line.FlatFee.Quantity.GreaterThan(alpacadecimal.NewFromInt(1)) { + description = fmt.Sprintf( + "%s (%s x %s)", + description, + calculator.FormatQuantity(line.FlatFee.Quantity), + calculator.FormatAmount(line.FlatFee.PerUnitAmount), + ) + } + // Otherwise we add the calculated total with with quantity one return &stripe.InvoiceAddLinesLineParams{ - Description: lo.ToPtr(name), + Description: lo.ToPtr(description), Amount: lo.ToPtr(calculator.RoundToAmount(amount)), Period: period, Metadata: map[string]string{