Skip to content

Conversation

@turip
Copy link
Member

@turip turip commented Apr 8, 2025

Overview

The only logic change included is the addition of the Pricer interface, other than that the logic parts are copy paste from the old implementation to prevent anything from breaking.

This patch splits the monolithic ubp line into smaller pricers allowing to compose the types going forward.

The patch also fixes an issue, that UBP FlatFee lines were considered progressively billable (this is only an issue if the line is in arrears which is a rare use-case).

Summary by CodeRabbit

  • New Features

    • Introduced new pricing models, including dynamic, flat, unit-based, tiered, and package pricing options, enhancing billing flexibility.
    • Added support for progressive billing error handling, improving user notifications when unsupported.
    • Implemented comprehensive unit tests for various pricing calculations to ensure accuracy and reliability.
    • Added a new error variable for unsupported progressive billing scenarios.
  • Bug Fixes

    • Enhanced invoice generation to reflect updated billing line items accurately.
    • Improved error messaging and validation processes for more consistent billing outcomes.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Apr 8, 2025

📝 Walkthrough

Walkthrough

This pull request introduces several new pricing implementations and refactors within the billing module. Multiple pricer types—including dynamic, flat, graduated tiered, unit, volume tiered, and package pricers—are added with their corresponding tests. A common Pricer interface is defined along with progressive and non-progressive billing variants. Additionally, the usage-based line calculations are refactored to delegate pricing to the appropriate pricer, and invoice tests are updated to reflect changes in billing support. A new error for unsupported progressive billing is also introduced.

Changes

File(s) Change Summary
openmeter/billing/errors.go Added new error variable ErrInvoiceProgressiveBillingNotSupported to handle unsupported progressive billing scenarios.
openmeter/billing/service/lineservice/pricedynamic.go, pricedynamic_test.go Introduced dynamicPricer with a Calculate method for dynamic price conversion and integrated comprehensive tests.
openmeter/billing/service/lineservice/priceflat.go, priceflat_test.go Added flatPricer to convert usage-based prices to a flat rate with conditional handling for payment terms; tests cover various scenarios.
openmeter/billing/service/lineservice/pricegraduatedtiered.go, pricegraduatedtiered_test.go Implemented graduatedTieredPricer with tiered pricing logic, supporting types (e.g., tierRange, tierCallbackInput), validation, and associated tests.
openmeter/billing/service/lineservice/pricer.go, pricetiered.go Defined the Pricer interface (with Calculate and CanBeInvoicedAsOf methods) and added implementations including ProgressiveBillingPricer, NonProgressiveBillingPricer, and a tieredPricer that routes based on pricing mode.
openmeter/billing/service/lineservice/priceunit.go, priceunit_test.go Introduced unitPricer for unit-based pricing calculations that apply commitments, with tests validating various usage and spend scenarios.
openmeter/billing/service/lineservice/pricevolumetiered.go, pricevolumetiered_test.go Implemented volumeTieredPricer with logic to select the appropriate tier based on quantity and detailed tests for volume tiered pricing.
openmeter/billing/service/lineservice/pricepackage.go, pricepackage_test.go Added packagePricer to compute package pricing using a getNumberOfPackages method, accompanied by tests to verify package calculation functionality.
openmeter/billing/service/lineservice/usagebasedline.go, usagebasedline_test.go Refactored usage-based line calculations: renamed methods and introduced getPricer to delegate pricing via the Pricer interface; tests were updated by removing individual pricing test functions.
test/billing/invoice_test.go Updated TestUBPProgressiveInvoicing to modify the expected invoice lines count and error handling—switching from ErrInvoiceLinesNotBillable to ErrInvoiceProgressiveBillingNotSupported.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (1.64.8)

Error: you are using a configuration file for golangci-lint v2 with golangci-lint v1: please use golangci-lint v2
Failed executing command with error: you are using a configuration file for golangci-lint v2 with golangci-lint v1: please use golangci-lint v2

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai plan to trigger planning for file edits and PR creation.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@turip turip added release-note/bug-fix Release note: Bug Fixes area/billing labels Apr 8, 2025
@turip turip marked this pull request as ready for review April 8, 2025 08:50
@turip turip requested a review from a team as a code owner April 8, 2025 08:50
@turip turip requested a review from tothandras April 8, 2025 08:50
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (24)
openmeter/billing/service/lineservice/pricedynamic.go (1)

1-53: Overall implementation of the dynamic pricer looks solid.
The logic for calculating the period amount and applying commitments is clearly structured. One optional consideration is to guard against scenarios where dynamicPrice.Multiplier might be zero or negative (if that is possible) to avoid unintended zero or negative charges. Otherwise, the code appears correct.

test/billing/invoice_test.go (1)

2290-2291: Consider removing or converting debug statements.
s.DebugDumpInvoice can be helpful during development, but consider replacing it with structured logging or removing it if it’s no longer needed.

openmeter/billing/service/lineservice/pricetiered.go (1)

35-49: Consider extracting common conversion logic.

While the implementation is correct, there's duplicate code between Calculate and CanBeInvoicedAsOf for price conversion and switch logic. Consider extracting this common functionality to reduce duplication.

+func (p tieredPricer) convertAndRoute(l usageBasedLine, handler func(mode productcatalog.TieredPriceMode) (bool, error)) (bool, error) {
+	price, err := l.line.UsageBased.Price.AsTiered()
+	if err != nil {
+		return false, fmt.Errorf("converting price to tiered price: %w", err)
+	}
+
+	switch price.Mode {
+	case productcatalog.VolumeTieredPrice:
+		return handler(productcatalog.VolumeTieredPrice)
+	case productcatalog.GraduatedTieredPrice:
+		return handler(productcatalog.GraduatedTieredPrice)
+	default:
+		return false, fmt.Errorf("unsupported tiered price mode: %s", price.Mode)
+	}
+}
+
 func (p tieredPricer) CanBeInvoicedAsOf(l usageBasedLine, asOf time.Time) (bool, error) {
-	price, err := l.line.UsageBased.Price.AsTiered()
-	if err != nil {
-		return false, fmt.Errorf("converting price to tiered price: %w", err)
-	}
-
-	switch price.Mode {
-	case productcatalog.VolumeTieredPrice:
-		return p.volume.CanBeInvoicedAsOf(l, asOf)
-	case productcatalog.GraduatedTieredPrice:
-		return p.graduated.CanBeInvoicedAsOf(l, asOf)
-	default:
-		return false, fmt.Errorf("unsupported tiered price mode: %s", price.Mode)
-	}
+	return p.convertAndRoute(l, func(mode productcatalog.TieredPriceMode) (bool, error) {
+		if mode == productcatalog.VolumeTieredPrice {
+			return p.volume.CanBeInvoicedAsOf(l, asOf)
+		}
+		return p.graduated.CanBeInvoicedAsOf(l, asOf)
+	})
 }

Note: This suggestion requires adapting Calculate method as well but increases complexity for relatively small methods, so it's optional.

openmeter/billing/service/lineservice/priceflat.go (1)

17-47: Consider adding a comment for the default return case.

The implementation correctly handles in-advance payments for the first line in a period and non-advance payments for the last line. However, when neither condition is met, the method returns nil, nil without explanation.

Consider adding a comment before the final return statement to explain the intended behavior:

 	}

+	// For other cases (e.g., mid-period lines for in-advance payments or non-last lines for in-arrears payments),
+	// we don't generate any line items as they will be handled by either the first or last line in the period.
 	return nil, nil
openmeter/billing/service/lineservice/priceunit.go (1)

9-11: Consider adding a descriptive comment for the unitPricer struct

A brief comment explaining the purpose of this struct and how it relates to the ProgressiveBillingPricer would improve code clarity.

+// unitPricer implements the Pricer interface for unit-based pricing calculations.
+// It embeds ProgressiveBillingPricer to inherit progressive billing behavior.
 type unitPricer struct {
 	ProgressiveBillingPricer
 }
openmeter/billing/service/lineservice/pricepacakge_test.go (1)

1-2: Filename has a typo

There's a typo in the filename "pricepacakge_test.go" - it should be "pricepackage_test.go".

openmeter/billing/service/lineservice/pricevolumetiered.go (2)

27-31: Consider adding logging or supplementary details for early returns.

When !l.IsLastInPeriod(), the function returns (nil, nil) with a brief comment about progressive billing not being supported. It may improve maintainability to log or provide more context in the error message or comment to clarify that no billing occurs for partial periods.


66-84: Extract repeated discount logic for maintainability.

Lines 66-84 replicate discounting logic similar to lines 47-64. Consider extracting a helper function to implement ApplyDiscountForMaxSpend, to adhere to DRY and improve maintainability.

openmeter/billing/service/lineservice/pricepacakage.go (1)

60-68: Guard against zero or invalid package sizes.

If packagePrice.QuantityPerPackage is zero or invalid, qty.Div(packageSize) could lead to a runtime error or an unexpected result. Consider validating the package size to avoid division by zero.

openmeter/billing/service/lineservice/usagebasedline_test.go (1)

91-94: Expand test coverage for edge cases.

Currently, the test sets usage from tc.usage. Consider adding cases for negative usage or extremely large values to ensure robust error checking and behavior.

openmeter/billing/service/lineservice/usagebasedline.go (5)

94-97: Handling CanBeInvoicedAsOf errors
The error check pattern is consistent. You may consider adding context in the error log for traceability, although it's optional.


215-223: Sequential calls to compute and instantiate detailed lines
The two-step approach (calculate & then instantiate lines) is straightforward. Ensure intermediate results are not needed elsewhere to reduce complexity.


225-228: Reusing child line IDs
Good approach to maintain continuity in child line references. Renaming childrenWithIDReuse to a more descriptive term might improve clarity (e.g., reusedChildLines).


247-266: Switch-based pricer instantiation
The switch is fine. If price types grow, consider a map-based or factory-based approach for maintainability. Otherwise, no blocking issues.


268-281: Delegation to pricer
Cleanly delegates final calculation to the pricer. Logging the calculation outcome might aid debugging, but it’s optional.

openmeter/billing/service/lineservice/pricegraduatedtiered_test.go (2)

16-115: Test: TestTieredGraduatedCalculation
This suite comprehensively checks various mid- and last-in-period scenarios, ensuring correct usage of tiered pricing. Consider verifying edge cases with zero usage, negative usage (if ever possible), and extremely large usage for extra coverage.


147-221: Minimum spend scenarios
The tests confirm proper line creation for minimum spends with zero usage as well as partial usage. This coverage is strong. Make sure you also cover boundary conditions around exactly meeting the minimum amount.

openmeter/billing/service/lineservice/pricer.go (2)

15-20: PricerCalculateInput fields
Encapsulating usage-based line, pre-line usage, and line usage sets a clear structure for pricer computations. Ensure that conversion/truncation logic (if needed) is handled consistently.


30-38: ProgressiveBillingPricer
Allows invoicing if current time is on or after start time. This is consistent with typical partial billing logic. Consider clarifying whether partial usage queries need additional checks here, though.

openmeter/billing/service/lineservice/pricegraduatedtiered.go (5)

12-14: Validate embedding of ProgressiveBillingPricer.
Embedding ProgressiveBillingPricer here might expose methods or properties that aren’t strictly needed by graduatedTieredPricer. Ensure you want this “is-a” relationship rather than a “has-a.”


18-110: Revisit complex inline logic in Calculate method.
The Calculate method is quite comprehensive, directly handling multiple pricing scenarios (e.g., usage price, flat price, discount application, minimum spend). While it appears functional, consider splitting out some logic (e.g., discount logic or fee logic) to smaller helper functions to improve maintainability and testability.


113-120: Enhance struct documentation for tierRange.
Clarify the meaning of AtTierBoundary and whether FromQty is inclusive or exclusive in a doc comment. This will help future maintainers.


122-128: Document the tierCallbackInput fields.
Adding doc comments for fields like Quantity and PreviousTotalAmount would help clarify their roles during tier processing.


223-326: Fix minor spelling and consider modularizing callback flow.
Line 281 in the comment says “cummulative” instead of “cumulative.” Also, the tier callback flow is carefully integrated, but you might reduce complexity by modularizing repeated logic branches (e.g., repeated rounding calls).

Apply this diff to fix the spelling:

-// Now that we have the ranges, let's iterate over the ranges and calculate the cummulative total amount
+// Now that we have the ranges, let's iterate over the ranges and calculate the cumulative total amount
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0075285 and 57558ab.

📒 Files selected for processing (18)
  • openmeter/billing/errors.go (1 hunks)
  • openmeter/billing/service/lineservice/pricedynamic.go (1 hunks)
  • openmeter/billing/service/lineservice/pricedynamic_test.go (1 hunks)
  • openmeter/billing/service/lineservice/priceflat.go (1 hunks)
  • openmeter/billing/service/lineservice/priceflat_test.go (1 hunks)
  • openmeter/billing/service/lineservice/pricegraduatedtiered.go (1 hunks)
  • openmeter/billing/service/lineservice/pricegraduatedtiered_test.go (1 hunks)
  • openmeter/billing/service/lineservice/pricepacakage.go (1 hunks)
  • openmeter/billing/service/lineservice/pricepacakge_test.go (1 hunks)
  • openmeter/billing/service/lineservice/pricer.go (1 hunks)
  • openmeter/billing/service/lineservice/pricetiered.go (1 hunks)
  • openmeter/billing/service/lineservice/priceunit.go (1 hunks)
  • openmeter/billing/service/lineservice/priceunit_test.go (1 hunks)
  • openmeter/billing/service/lineservice/pricevolumetiered.go (1 hunks)
  • openmeter/billing/service/lineservice/pricevolumetiered_test.go (1 hunks)
  • openmeter/billing/service/lineservice/usagebasedline.go (3 hunks)
  • openmeter/billing/service/lineservice/usagebasedline_test.go (1 hunks)
  • test/billing/invoice_test.go (6 hunks)
🧰 Additional context used
🧬 Code Definitions (17)
openmeter/billing/errors.go (1)
openmeter/billing/validationissue.go (1)
  • NewValidationError (75-81)
openmeter/billing/service/lineservice/pricetiered.go (2)
openmeter/billing/service/lineservice/pricer.go (2)
  • Pricer (22-28)
  • PricerCalculateInput (15-20)
openmeter/productcatalog/price.go (2)
  • VolumeTieredPrice (448-448)
  • GraduatedTieredPrice (450-450)
test/billing/invoice_test.go (4)
pkg/slicesx/map.go (1)
  • Map (6-19)
openmeter/billing/invoiceline.go (2)
  • Line (278-291)
  • Period (82-85)
openmeter/billing/errors.go (1)
  • ErrInvoiceProgressiveBillingNotSupported (38-38)
api/client/go/client.gen.go (1)
  • Period (4325-4331)
openmeter/billing/service/lineservice/priceunit.go (4)
openmeter/billing/service/lineservice/pricer.go (4)
  • ProgressiveBillingPricer (30-30)
  • ProgressiveBillingPricer (32-38)
  • Pricer (22-28)
  • PricerCalculateInput (15-20)
openmeter/ent/db/billinginvoiceflatfeelineconfig/where.go (1)
  • PerUnitAmount (74-76)
openmeter/billing/service/lineservice/usagebasedline.go (2)
  • UnitPriceUsageChildUniqueReferenceID (27-27)
  • UnitPriceMinSpendChildUniqueReferenceID (28-28)
openmeter/productcatalog/price.go (2)
  • InArrearsPaymentTerm (21-21)
  • Commitments (687-693)
openmeter/billing/service/lineservice/pricepacakge_test.go (5)
openmeter/productcatalog/price.go (2)
  • NewPriceFrom (338-360)
  • Commitments (687-693)
openmeter/billing/service/lineservice/usagebasedline.go (2)
  • MinSpendChildUniqueReferenceID (21-21)
  • UsageChildUniqueReferenceID (20-20)
openmeter/billing/invoiceline.go (1)
  • FlatFeeCategoryCommitment (248-248)
openmeter/productcatalog/discount.go (1)
  • Discounts (346-346)
openmeter/billing/invoicelinediscount.go (5)
  • NewLineDiscounts (625-627)
  • AmountLineDiscount (100-108)
  • LineDiscountBase (62-69)
  • LineMaximumSpendReferenceID (21-21)
  • LineDiscountReasonMaximumSpend (27-27)
openmeter/billing/service/lineservice/priceunit_test.go (5)
openmeter/productcatalog/price.go (2)
  • NewPriceFrom (338-360)
  • Commitments (687-693)
openmeter/billing/service/lineservice/usagebasedline.go (2)
  • UnitPriceMinSpendChildUniqueReferenceID (28-28)
  • UnitPriceUsageChildUniqueReferenceID (27-27)
openmeter/billing/invoiceline.go (1)
  • FlatFeeCategoryCommitment (248-248)
openmeter/productcatalog/discount.go (1)
  • Discounts (346-346)
openmeter/billing/invoicelinediscount.go (6)
  • NewLineDiscounts (625-627)
  • NewLineDiscountFrom (334-361)
  • AmountLineDiscount (100-108)
  • LineDiscountBase (62-69)
  • LineMaximumSpendReferenceID (21-21)
  • LineDiscountReasonMaximumSpend (27-27)
openmeter/billing/service/lineservice/priceflat_test.go (2)
openmeter/productcatalog/price.go (3)
  • NewPriceFrom (338-360)
  • InAdvancePaymentTerm (20-20)
  • InArrearsPaymentTerm (21-21)
openmeter/billing/service/lineservice/usagebasedline.go (1)
  • FlatPriceChildUniqueReferenceID (25-25)
openmeter/billing/service/lineservice/priceflat.go (3)
openmeter/billing/service/lineservice/pricer.go (4)
  • NonProgressiveBillingPricer (40-40)
  • NonProgressiveBillingPricer (42-58)
  • Pricer (22-28)
  • PricerCalculateInput (15-20)
openmeter/productcatalog/price.go (2)
  • InAdvancePaymentTerm (20-20)
  • InArrearsPaymentTerm (21-21)
openmeter/billing/service/lineservice/usagebasedline.go (1)
  • FlatPriceChildUniqueReferenceID (25-25)
openmeter/billing/service/lineservice/pricedynamic.go (4)
openmeter/billing/service/lineservice/pricer.go (4)
  • ProgressiveBillingPricer (30-30)
  • ProgressiveBillingPricer (32-38)
  • Pricer (22-28)
  • PricerCalculateInput (15-20)
openmeter/ent/db/billinginvoiceflatfeelineconfig/where.go (1)
  • PerUnitAmount (74-76)
openmeter/billing/service/lineservice/usagebasedline.go (2)
  • UsageChildUniqueReferenceID (20-20)
  • MinSpendChildUniqueReferenceID (21-21)
openmeter/productcatalog/price.go (2)
  • InArrearsPaymentTerm (21-21)
  • Commitments (687-693)
openmeter/billing/service/lineservice/pricedynamic_test.go (5)
openmeter/productcatalog/price.go (2)
  • NewPriceFrom (338-360)
  • Commitments (687-693)
openmeter/billing/service/lineservice/usagebasedline.go (3)
  • DecimalOne (42-42)
  • MinSpendChildUniqueReferenceID (21-21)
  • UsageChildUniqueReferenceID (20-20)
openmeter/billing/invoiceline.go (1)
  • FlatFeeCategoryCommitment (248-248)
openmeter/productcatalog/discount.go (1)
  • Discounts (346-346)
openmeter/billing/invoicelinediscount.go (6)
  • NewLineDiscounts (625-627)
  • NewLineDiscountFrom (334-361)
  • AmountLineDiscount (100-108)
  • LineDiscountBase (62-69)
  • LineMaximumSpendReferenceID (21-21)
  • LineDiscountReasonMaximumSpend (27-27)
openmeter/billing/service/lineservice/pricevolumetiered.go (5)
openmeter/billing/service/lineservice/pricer.go (4)
  • NonProgressiveBillingPricer (40-40)
  • NonProgressiveBillingPricer (42-58)
  • Pricer (22-28)
  • PricerCalculateInput (15-20)
openmeter/productcatalog/price.go (1)
  • VolumeTieredPrice (448-448)
openmeter/billing/errors.go (2)
  • ErrInvoiceLineVolumeSplitNotSupported (45-45)
  • ErrInvoiceLineMissingOpenEndedTier (47-47)
openmeter/billing/service/lineservice/usagebasedline.go (3)
  • VolumeFlatPriceChildUniqueReferenceID (33-33)
  • VolumeUnitPriceChildUniqueReferenceID (34-34)
  • VolumeMinSpendChildUniqueReferenceID (35-35)
openmeter/billing/invoiceline.go (1)
  • FlatFeeCategoryCommitment (248-248)
openmeter/billing/service/lineservice/pricepacakage.go (3)
openmeter/billing/service/lineservice/pricer.go (4)
  • ProgressiveBillingPricer (30-30)
  • ProgressiveBillingPricer (32-38)
  • Pricer (22-28)
  • PricerCalculateInput (15-20)
openmeter/billing/service/lineservice/usagebasedline.go (3)
  • UsageChildUniqueReferenceID (20-20)
  • MinSpendChildUniqueReferenceID (21-21)
  • DecimalOne (42-42)
openmeter/productcatalog/price.go (1)
  • InArrearsPaymentTerm (21-21)
openmeter/billing/service/lineservice/pricevolumetiered_test.go (3)
openmeter/productcatalog/price.go (3)
  • PriceTierFlatPrice (651-654)
  • PriceTierUnitPrice (670-673)
  • NewPriceFrom (338-360)
openmeter/billing/service/lineservice/usagebasedline.go (3)
  • VolumeFlatPriceChildUniqueReferenceID (33-33)
  • VolumeUnitPriceChildUniqueReferenceID (34-34)
  • VolumeMinSpendChildUniqueReferenceID (35-35)
openmeter/billing/invoicelinediscount.go (3)
  • NewLineDiscounts (625-627)
  • NewLineDiscountFrom (334-361)
  • AmountLineDiscount (100-108)
openmeter/billing/service/lineservice/usagebasedline_test.go (3)
openmeter/ent/db/billinginvoicelinediscount/where.go (2)
  • Quantity (116-118)
  • PreLinePeriodQuantity (121-123)
openmeter/ent/db/billinginvoiceline/where.go (1)
  • Quantity (168-170)
openmeter/ent/db/billinginvoiceusagebasedlineconfig/where.go (1)
  • PreLinePeriodQuantity (78-80)
openmeter/billing/service/lineservice/pricegraduatedtiered_test.go (4)
openmeter/productcatalog/price.go (5)
  • PriceTierFlatPrice (651-654)
  • PriceTierUnitPrice (670-673)
  • NewPriceFrom (338-360)
  • GraduatedTieredPrice (450-450)
  • Commitments (687-693)
openmeter/billing/service/lineservice/usagebasedline.go (1)
  • GraduatedMinSpendChildUniqueReferenceID (39-39)
openmeter/billing/invoiceline.go (1)
  • FlatFeeCategoryCommitment (248-248)
openmeter/billing/invoicelinediscount.go (3)
  • NewLineDiscounts (625-627)
  • NewLineDiscountFrom (334-361)
  • AmountLineDiscount (100-108)
openmeter/billing/service/lineservice/usagebasedline.go (1)
openmeter/billing/service/lineservice/pricer.go (2)
  • PricerCalculateInput (15-20)
  • Pricer (22-28)
openmeter/billing/service/lineservice/pricegraduatedtiered.go (4)
openmeter/billing/service/lineservice/pricer.go (4)
  • ProgressiveBillingPricer (30-30)
  • ProgressiveBillingPricer (32-38)
  • Pricer (22-28)
  • PricerCalculateInput (15-20)
openmeter/productcatalog/price.go (2)
  • GraduatedTieredPrice (450-450)
  • InArrearsPaymentTerm (21-21)
openmeter/billing/service/lineservice/usagebasedline.go (3)
  • GraduatedTieredPriceUsageChildUniqueReferenceID (37-37)
  • GraduatedTieredFlatPriceChildUniqueReferenceID (38-38)
  • GraduatedMinSpendChildUniqueReferenceID (39-39)
openmeter/billing/invoiceline.go (1)
  • FlatFeeCategoryCommitment (248-248)
⏰ Context from checks skipped due to timeout of 90000ms (9)
  • GitHub Check: Test
  • GitHub Check: Quickstart
  • GitHub Check: Lint
  • GitHub Check: E2E
  • GitHub Check: Developer environment
  • GitHub Check: CI
  • GitHub Check: Commit hooks
  • GitHub Check: Build
  • GitHub Check: Analyze (go)
🔇 Additional comments (51)
openmeter/billing/errors.go (1)

35-42: New invoice-related error constants look consistent.
These additions align well with existing validation error patterns and naming schemes. It would be good to ensure they are covered by relevant unit or integration tests, but there are no immediate issues from a correctness or maintainability standpoint.

test/billing/invoice_test.go (8)

1811-1811: Confirmed correct line count check.
This test assertion correctly verifies the expected number of lines.


1819-1820: Helpful clarifying comment.
These new comments make it clearer which items are meant to be invoiced in arrears.


1951-1951: Appropriate error handling for unsupported progressive billing.
Switching the requirement to check ErrInvoiceProgressiveBillingNotSupported is consistent with the new logic introduced in the pricer.


1970-1970: Correct assertion verifying updated lines.
Ensuring that exactly two lines remain is consistent with scenarios where one line was removed or updated earlier in the test.


2108-2108: Good test coverage for multiple line items.
This assertion helps confirm that two lines appear in this scenario.


2116-2117: Additional clarifying comments regarding invoice lines in arrears.
No functional changes here, but these comments improve overall readability.


2303-2305: Clear explanation of non-prorated flat fees.
These comments provide a concise rationale for why certain lines do not receive a parent reference.


2320-2320: Straightforward iteration update.
This loop restricts processing to the relevant lines, reflecting the new logic surrounding usage-based lines. Looks good.

openmeter/billing/service/lineservice/pricetiered.go (2)

10-15: Good implementation of the router pattern for tiered pricing.

The tieredPricer struct elegantly routes pricing calculations to the appropriate pricer based on the pricing mode. This design aligns well with the PR's objective to break down the monolithic UBP line into smaller, more manageable components.


19-33: Clean implementation with proper error handling.

The Calculate method correctly:

  1. Converts the price to tiered format
  2. Routes to the appropriate pricer based on the mode
  3. Returns meaningful error messages for both conversion failures and unsupported modes

The router pattern implementation is consistent with the PR objectives of creating more modular pricing components.

openmeter/billing/service/lineservice/priceflat_test.go (6)

10-32: Good test coverage for flat price with no usage.

The test properly validates the expected behavior when a flat price is applied with no usage in an in-advance payment scenario. The test structure is clear and consistent with the overall testing approach.


34-54: Appropriate test for in-advance flat pricing with usage.

This test correctly verifies that usage doesn't affect flat pricing when payment term is in advance. The flat price is still applied regardless of usage amount, which is the expected behavior.


56-68: Good test for mid-period behavior with in-advance pricing.

This test verifies an important edge case: when using in-advance payment terms in the middle of a period, no detailed lines should be created. This aligns with the expectation that in-advance charges only apply to the first line of a period.


70-90: Proper handling of in-arrears flat pricing.

The test correctly validates the behavior for in-arrears flat pricing in a single period line scenario. This test case is particularly relevant to the PR objective of addressing the issue where UBP FlatFee lines were incorrectly classified as progressively billable.


92-104: Important edge case for in-arrears mid-period lines.

This test verifies that flat fees with in-arrears payment terms are not billed in mid-period lines, which is consistent with the expected behavior that they should only be billed in the last period.


106-126: Comprehensive test case for last-period in-arrears flat pricing.

This test completes the coverage by verifying that in-arrears flat prices are applied in the last period line. The comprehensive test suite effectively covers all the timing scenarios for flat pricing, which is important for the refactoring goals of this PR.

openmeter/billing/service/lineservice/pricedynamic_test.go (6)

12-24: Good baseline test for dynamic pricing with no usage.

The test correctly verifies that when there's no usage with a dynamic price (and no minimum spend), no detailed lines are created. This establishes the baseline behavior for dynamic pricing.


26-51: Proper verification of minimum spend commitments.

This test ensures that minimum spend is honored even when there's no usage, which is a critical aspect of commitment-based pricing. The test properly validates the creation of a line item for minimum spend with the correct payment term (in arrears) and the appropriate billing category.


53-95: Good tests for minimum spend timing logic.

These two tests comprehensively verify the timing aspects of minimum spend commitments:

  1. Mid-period lines should not bill minimum spend
  2. Only the last line in a period should bill minimum spend

This behavior is essential for preventing duplicate billing of commitments and ensures the PR objective of correctly handling billing timing.


97-138: Thorough testing of usage-based billing.

These tests properly verify that usage is billed correctly regardless of the line position within a period. The tests check that:

  1. The amount is calculated correctly using the multiplier
  2. The line is categorized as usage
  3. The payment term is in arrears

These tests ensure the core usage-based pricing functionality works as expected.


140-163: Good verification of maximum spend limit not reached.

This test ensures that when a maximum spend is set but not reached, the billing behaves normally with no discount applied. This is an important scenario to test as it validates that maximum spend limits don't affect normal billing when they aren't triggered.


165-201: Comprehensive test for maximum spend discount application.

This test thoroughly validates the complex scenario where maximum spend is hit:

  1. Pre-line period usage is tracked
  2. Line period usage is calculated
  3. The total exceeds the maximum spend limit
  4. A discount is applied with the correct amount and metadata

This test ensures the pricing module correctly implements maximum spend caps, which is a critical billing component.

openmeter/billing/service/lineservice/priceflat.go (2)

10-13: Clear indication of future enhancement for pro-rating support.

The TODO comment indicates awareness that full pro-rating support will require changing to ProgressiveBillingPricer. This aligns with the PR objective of addressing the issue where UBP FlatFee lines were incorrectly classified as progressively billable.


17-47: Good implementation of the flat pricing logic.

The Calculate method properly implements the two key scenarios for flat pricing:

  1. In-advance payments are only billed on the first line of a period
  2. In-arrears payments are only billed on the last line of a period

This correctly addresses the issue mentioned in the PR objectives where flat fee lines needed special handling.

openmeter/billing/service/lineservice/priceunit_test.go (1)

1-200: Great test coverage for unit price calculation!

The test cases comprehensively cover key pricing scenarios including:

  • Zero usage cases
  • Minimum spend handling across different line positions
  • Usage calculation with various quantities
  • Maximum spend thresholds and discount application

The test structure is clean and follows best practices with descriptive test names and clear expected outputs for each case.

openmeter/billing/service/lineservice/priceunit.go (1)

15-48: Clear implementation of unit price calculation logic

The Calculate method correctly:

  1. Extracts the unit price from the line configuration
  2. Handles usage-based pricing when quantities are present
  3. Properly calculates previous period billing
  4. Applies commitments (min/max spend) appropriately

The error handling is thorough with meaningful error messages.

openmeter/billing/service/lineservice/pricepacakge_test.go (2)

13-254: Comprehensive test suite for package pricing

The test cases are well-structured and cover essential pricing scenarios:

  • Empty usage cases
  • Minimum spend validation across different line positions
  • Various package quantity calculations
  • Maximum spend thresholds and discount application

The test cases properly validate that packages are calculated correctly based on usage quantities.


257-265: Good unit testing for the getNumberOfPackages method

The test verifies multiple edge cases for package quantity calculation:

  • Zero quantity (0 packages)
  • Partial package (1 package)
  • Exact package size (1 package)
  • Package overflow (2 packages)

This ensures the core package calculation logic works correctly.

openmeter/billing/service/lineservice/pricevolumetiered_test.go (5)

14-40: Well-structured test tiers definition

The test tiers are clearly defined with a mix of flat and unit pricing across different quantity thresholds:

  • Tier 1: Flat price of 100 for quantities up to 5
  • Tier 2: Flat price of 150 for quantities up to 10
  • Tier 3: Unit price of 10 for quantities up to 15
  • Tier 4: Unit price of 5 for all quantities above 15

This provides a good foundation for testing various tier calculation scenarios.


42-228: Thorough testing of volume-based tier selection

The tests effectively verify:

  • Correct tier selection based on usage quantity
  • Proper boundary condition handling at tier transitions
  • Appropriate billing format (flat vs unit price) based on tier type

All tests appropriately validate the expected behavior for each scenario.


232-346: Comprehensive minimum spend validation tests

The test cases verify that:

  • Minimum spend is honored when usage is below threshold
  • Additional commitment line is created when needed
  • Appropriate calculation of delta between actual usage and minimum spend

The tests cover various edge cases including exact minimum spend matches.


349-407: Solid maximum spend limit testing

The tests effectively verify:

  • Maximum spend is properly enforced
  • Discounts are correctly applied when usage exceeds maximum spend
  • Appropriate handling of maximum spend thresholds that don't align with tier boundaries

The discount calculation and application logic is thoroughly tested.


410-470: Good unit tests for tier selection logic

The TestFindTierForQuantity function effectively validates that:

  • Correct tier is selected for quantities within various tier ranges
  • Tier boundaries are handled properly (e.g., quantity exactly at the boundary)
  • Large quantities beyond defined thresholds select the final tier

This ensures the core tier selection logic works as expected.

openmeter/billing/service/lineservice/pricevolumetiered.go (1)

1-13: Well-structured initialization of volumeTieredPricer.

Indices and imports appear consistent, and embedding NonProgressiveBillingPricer aligns with the intended design.

openmeter/billing/service/lineservice/pricepacakage.go (1)

19-22: Good error handling.

Converting to a package price is clearly guarded by proper error handling, improving reliability.

openmeter/billing/service/lineservice/usagebasedline.go (4)

88-92: Use of getPricer() for progressive billing
No immediate issues. This approach cleanly separates the logic for pricer retrieval and allows for structured error handling.


99-102: Return nil, nil instead of raising an error
Returning nil, nil is appropriate if the calling code interprets “cannot be invoiced” as a non-error scenario. Confirm that upstream callers expect this behavior and handle it properly.


105-105: Refined comment
The inline comment is clear, no concerns.


235-245: Validation for required quantities
The check ensures quantity fields are set before pricer calculations. Consider returning a custom error type if you want more specific parsing upstream, but this is acceptable.

openmeter/billing/service/lineservice/pricegraduatedtiered_test.go (5)

1-14: File-level setup and imports
Test file is well-structured with relevant imports and packages. No issues spotted here.


117-146: Test: verifying multiple tier usage in mid period
Logic covers partial usage crossing tier boundaries. The approach looks robust. No further issues noted.


223-283: Maximum spend scenarios
Tests correctly validate discount application once usage crosses the maximum spend threshold. The discount item lines are well-structured. Great coverage of boundary cross.


286-547: Test: TestTieredPriceCalculator - tier calculations
Extensive coverage of tier-by-tier partial usage (and no usage) scenarios. The usage of callback mocks ensures logic is validated thoroughly. The dynamic tier boundary validations are clear.


549-593: Helper functions and mock callbacks
Good separation of concerns for finalizing amounts, introspecting tiers, and verifying callback logic. This ensures reliable debugging of tier transitions.

openmeter/billing/service/lineservice/pricer.go (3)

1-13: Added type definitions
The PricerCapabilities struct is straightforward; no concerns. The PricerCalculateInput struct is well-defined for usage-based lines.


22-28: Pricer interface
Defines the contract for progressive or non-progressive pricers, enabling flexible expansions. The minimal interface is appropriate.


40-58: NonProgressiveBillingPricer
The handling of parent lines as an error scenario clarifies the “no progressive billing” stance. This logic is correct and returns a specialized error as needed.

openmeter/billing/service/lineservice/pricegraduatedtiered.go (3)

1-2: File structure and naming look good.
The package declaration and blank line follow standard Go conventions. No issues here.


144-170: Robust validation checks.
The Validate() method thoroughly checks for malformed input (negative quantities, currency presence, price mode). This reduces unexpected runtime errors. Good job.


172-221: Keep an eye on boundary splits in splitTierRangeAtBoundary.
The logic for splitting ranges is intricate but appears consistent with your tiering approach. Add unit tests for edge cases where from == to or nearly so, ensuring boundary conditions behave correctly.

Would you like a script to locate or generate tests specifically addressing boundary splits?

@turip turip force-pushed the refactor/billing-pricer-interface branch from 8fe18e2 to b7c7703 Compare April 8, 2025 09:02
tothandras
tothandras previously approved these changes Apr 8, 2025
@turip turip enabled auto-merge (squash) April 8, 2025 09:04
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (12)
openmeter/billing/service/lineservice/pricevolumetiered.go (2)

33-35: Check error message consistency.
billing.ErrInvoiceLineVolumeSplitNotSupported references "graduated" in its message, but here it's used for volume-tiered pricing. Consider updating the error definition or message to avoid confusion.

- return nil, billing.ErrInvoiceLineVolumeSplitNotSupported
+ return nil, billing.ErrInvoiceLineVolumeSplitNotSupported // modify error message to reflect volume-tiered

48-65: Validate discount application order for flat and unit lines.
Both flat and unit price lines apply maximum spend discounts sequentially, possibly causing different discount amounts depending on line order. If the discount logic for multiple lines is intended to prioritize certain charges first, consider clarifying or enforcing an explicit order.

Also applies to: 67-85

openmeter/billing/service/lineservice/usagebasedline.go (1)

88-105: Ensure pricer compatibility with meter checks.
After pricer.CanBeInvoicedAsOf returns true, you further validate the meter. Confirm that any decision from the pricer is consistent with the meter logic (e.g., if the pricer says “invoice now” but the meter forbids it). Consider logging or clarifying any conflicting signals.

openmeter/billing/service/lineservice/usagebasedline_test.go (5)

1811-1811: Enhance test assertion messages.

While require.Len(s.T(), invoiceLines, 2) is good for correctness, adding a descriptive message parameter (e.g., "expected two invoice lines at mid-period") can ease debugging when the assertion fails.


1819-1820: Fix spelling in comment.

There is a small spelling issue: "arreas" should be "arrears".

Apply this diff to correct the spelling:

-// - the volume item as that must be invoiced in arreas
-// - the flat per usage item as that is invoiced in arreas (no pro-rating)
+// - the volume item as that must be invoiced in arrears
+// - the flat per usage item as that is invoiced in arrears (no pro-rating)

1970-1970: Optional: Add assertion message.

require.Len(s.T(), updatedInvoice.Lines.MustGet(), 2) could include a description (e.g., "updated invoice should carry two line items") for clarity.


2108-2108: Optional: Add assertion message.

Adding a small description, like "should have exactly two invoice lines at this stage", would improve test clarity here as well.


2290-2291: Consider removing or gating debugging statements.

s.DebugDumpInvoice("end of period invoice", out[0]) is helpful for diagnostics but might clutter logs in production. If feasible, consider wrapping it with a debug flag or removing it once the code is stable.

openmeter/billing/service/lineservice/pricegraduatedtiered.go (4)

226-327: Consider breaking down the complex tiered price calculator method

The tieredPriceCalculator method is quite long and handles multiple responsibilities. Consider breaking it down into smaller, more focused methods to improve readability and maintainability.

For example, you could extract the range creation logic (lines 232-280) and the range processing logic (lines 282-318) into separate methods:

-func (p graduatedTieredPricer) tieredPriceCalculator(in tieredPriceCalculatorInput) error {
+func (p graduatedTieredPricer) tieredPriceCalculator(in tieredPriceCalculatorInput) error {
     // Note: this is not the most efficient algorithm, but it is at least pseudo-readable
     if err := in.Validate(); err != nil {
         return err
     }
+
+    qtyRanges := p.createTierRanges(in)
+    
+    if in.IntrospectRangesFn != nil {
+        in.IntrospectRangesFn(qtyRanges)
+    }
+
+    total, err := p.processTierRanges(in, qtyRanges)
+    if err != nil {
+        return err
+    }
+
+    if in.FinalizerFn != nil {
+        if err := in.FinalizerFn(total); err != nil {
+            return err
+        }
+    }
+
+    return nil
+}
+
+func (p graduatedTieredPricer) createTierRanges(in tieredPriceCalculatorInput) []tierRange {
     // Let's break up the tiers and the input data into a sequence of periods, for easier processing
     // Invariant of the qtyRanges:
     // - Non overlapping ranges
     // - The ranges are sorted by the from quantity
     // - There is always one range for which range.From == in.FromQty
     // - There is always one range for which range.ToQty == in.ToQty
     qtyRanges := make([]tierRange, 0, len(in.TieredPrice.Tiers)+2)

     previousTierQty := alpacadecimal.Zero
     for idx, tier := range in.TieredPrice.WithSortedTiers().Tiers {
         if previousTierQty.GreaterThanOrEqual(in.ToQty) {
             // We already have enough data to bill for this tiered price
             break
         }

         // Given that the previous tier's max qty was less than then in.ToQty, toQty will fall into the
         // open ended tier, so we can safely use it as the upper bound
         tierUpperBound := in.ToQty
         if tier.UpToAmount != nil {
             tierUpperBound = *tier.UpToAmount
         }

         input := tierRange{
             Tier:           tier,
             TierIndex:      idx,
             AtTierBoundary: true,
             FromQty:        previousTierQty,
             ToQty:          tierUpperBound,
         }

         qtyRanges = append(qtyRanges, p.splitTierRangeAtBoundary(in.FromQty, in.ToQty, input)...)

         previousTierQty = tierUpperBound
     }

     if in.ToQty.Equal(alpacadecimal.Zero) {
         // We need to add the first range, in case there's a flat price component
         qtyRanges = append(qtyRanges, tierRange{
             Tier:           in.TieredPrice.Tiers[0],
             TierIndex:      0,
             AtTierBoundary: true,
             FromQty:        alpacadecimal.Zero,
             ToQty:          alpacadecimal.Zero,
         })
     }
+
+    return qtyRanges
+}
+
+func (p graduatedTieredPricer) processTierRanges(in tieredPriceCalculatorInput, qtyRanges []tierRange) (alpacadecimal.Decimal, error) {
     // Now that we have the ranges, let's iterate over the ranges and calculate the cummulative total amount
     // and call the callback for each in-scope range
     total := alpacadecimal.Zero
     shouldEmitCallbacks := false
     for _, qtyRange := range qtyRanges {
         if qtyRange.FromQty.Equal(in.FromQty) {
             shouldEmitCallbacks = true
         }

         if shouldEmitCallbacks && in.TierCallbackFn != nil {
             err := in.TierCallbackFn(tierCallbackInput{
                 Tier:                qtyRange.Tier,
                 TierIndex:           qtyRange.TierIndex,
                 Quantity:            qtyRange.ToQty.Sub(qtyRange.FromQty),
                 PreviousTotalAmount: total,
                 AtTierBoundary:      qtyRange.AtTierBoundary,
             })
             if err != nil {
-                return err
+                return alpacadecimal.Zero, err
             }
         }

         // Let's update totals
         if qtyRange.Tier.FlatPrice != nil && qtyRange.AtTierBoundary {
             total = total.Add(in.Currency.RoundToPrecision(qtyRange.Tier.FlatPrice.Amount))
         }

         if qtyRange.Tier.UnitPrice != nil {
             total = total.Add(in.Currency.RoundToPrecision(qtyRange.ToQty.Sub(qtyRange.FromQty).Mul(qtyRange.Tier.UnitPrice.Amount)))
         }

         // We should only calculate totals up to in.ToQty (given tiers are open-ended we cannot have a full upper bound
         // either ways)
         if qtyRange.ToQty.GreaterThanOrEqual(in.ToQty) {
             break
         }
     }
-
-    if in.FinalizerFn != nil {
-        if err := in.FinalizerFn(total); err != nil {
-            return err
-        }
-    }
-
-    return nil
+    return total, nil
}

31-106: Consider returning detailed lines from tieredPriceCalculator instead of using callbacks

The current approach using callback functions to build the out slice makes the code harder to follow. Consider refactoring to have the tieredPriceCalculator return the detailed lines directly.

This would result in cleaner, more straightforward code and improve testability.


29-29: Consider increasing the initial capacity of the output slice

The current allocation assumes at most one line per tier, but you may add more (unit price, flat price, and minimum spend). Consider increasing the capacity estimation to avoid reallocations.

-out := make(newDetailedLinesInput, 0, len(price.Tiers))
+out := make(newDetailedLinesInput, 0, len(price.Tiers)*2+1) // Account for unit price, flat price per tier, plus min spend

173-222: Splitting tier ranges looks good but needs more comments

The splitTierRangeAtBoundary method is complex but appears correct. Consider adding more detailed comments to explain the splitting logic, especially the conditions that trigger the splits.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57558ab and b7c7703.

📒 Files selected for processing (18)
  • openmeter/billing/errors.go (1 hunks)
  • openmeter/billing/service/lineservice/pricedynamic.go (1 hunks)
  • openmeter/billing/service/lineservice/pricedynamic_test.go (1 hunks)
  • openmeter/billing/service/lineservice/priceflat.go (1 hunks)
  • openmeter/billing/service/lineservice/priceflat_test.go (1 hunks)
  • openmeter/billing/service/lineservice/pricegraduatedtiered.go (1 hunks)
  • openmeter/billing/service/lineservice/pricegraduatedtiered_test.go (1 hunks)
  • openmeter/billing/service/lineservice/pricepackage.go (1 hunks)
  • openmeter/billing/service/lineservice/pricepackage_test.go (1 hunks)
  • openmeter/billing/service/lineservice/pricer.go (1 hunks)
  • openmeter/billing/service/lineservice/pricetiered.go (1 hunks)
  • openmeter/billing/service/lineservice/priceunit.go (1 hunks)
  • openmeter/billing/service/lineservice/priceunit_test.go (1 hunks)
  • openmeter/billing/service/lineservice/pricevolumetiered.go (1 hunks)
  • openmeter/billing/service/lineservice/pricevolumetiered_test.go (1 hunks)
  • openmeter/billing/service/lineservice/usagebasedline.go (3 hunks)
  • openmeter/billing/service/lineservice/usagebasedline_test.go (1 hunks)
  • test/billing/invoice_test.go (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • openmeter/billing/errors.go
  • openmeter/billing/service/lineservice/priceflat.go
  • openmeter/billing/service/lineservice/pricedynamic.go
  • openmeter/billing/service/lineservice/priceunit.go
  • openmeter/billing/service/lineservice/priceflat_test.go
  • openmeter/billing/service/lineservice/pricetiered.go
  • openmeter/billing/service/lineservice/pricegraduatedtiered_test.go
🧰 Additional context used
🧬 Code Definitions (10)
openmeter/billing/service/lineservice/pricedynamic_test.go (5)
openmeter/productcatalog/price.go (1)
  • NewPriceFrom (338-360)
openmeter/billing/service/lineservice/usagebasedline.go (3)
  • DecimalOne (42-42)
  • MinSpendChildUniqueReferenceID (21-21)
  • UsageChildUniqueReferenceID (20-20)
openmeter/billing/invoiceline.go (1)
  • FlatFeeCategoryCommitment (248-248)
openmeter/productcatalog/discount.go (1)
  • Discounts (346-346)
openmeter/billing/invoicelinediscount.go (6)
  • NewLineDiscounts (625-627)
  • NewLineDiscountFrom (334-361)
  • AmountLineDiscount (100-108)
  • LineDiscountBase (62-69)
  • LineMaximumSpendReferenceID (21-21)
  • LineDiscountReasonMaximumSpend (27-27)
openmeter/billing/service/lineservice/pricepackage_test.go (5)
openmeter/productcatalog/price.go (1)
  • NewPriceFrom (338-360)
openmeter/billing/service/lineservice/usagebasedline.go (2)
  • MinSpendChildUniqueReferenceID (21-21)
  • UsageChildUniqueReferenceID (20-20)
openmeter/billing/invoiceline.go (1)
  • FlatFeeCategoryCommitment (248-248)
openmeter/productcatalog/discount.go (1)
  • Discounts (346-346)
openmeter/billing/invoicelinediscount.go (6)
  • NewLineDiscounts (625-627)
  • NewLineDiscountFrom (334-361)
  • AmountLineDiscount (100-108)
  • LineDiscountBase (62-69)
  • LineMaximumSpendReferenceID (21-21)
  • LineDiscountReasonMaximumSpend (27-27)
openmeter/billing/service/lineservice/pricevolumetiered_test.go (4)
openmeter/productcatalog/price.go (4)
  • PriceTierFlatPrice (651-654)
  • PriceTierUnitPrice (670-673)
  • NewPriceFrom (338-360)
  • Commitments (687-693)
openmeter/billing/service/lineservice/usagebasedline.go (3)
  • VolumeFlatPriceChildUniqueReferenceID (33-33)
  • VolumeUnitPriceChildUniqueReferenceID (34-34)
  • VolumeMinSpendChildUniqueReferenceID (35-35)
openmeter/billing/invoiceline.go (1)
  • FlatFeeCategoryCommitment (248-248)
openmeter/billing/invoicelinediscount.go (3)
  • NewLineDiscounts (625-627)
  • AmountLineDiscount (100-108)
  • LineDiscountBase (62-69)
openmeter/billing/service/lineservice/priceunit_test.go (5)
openmeter/productcatalog/price.go (2)
  • NewPriceFrom (338-360)
  • Commitments (687-693)
openmeter/billing/service/lineservice/usagebasedline.go (2)
  • UnitPriceMinSpendChildUniqueReferenceID (28-28)
  • UnitPriceUsageChildUniqueReferenceID (27-27)
openmeter/billing/invoiceline.go (1)
  • FlatFeeCategoryCommitment (248-248)
openmeter/productcatalog/discount.go (1)
  • Discounts (346-346)
openmeter/billing/invoicelinediscount.go (6)
  • NewLineDiscounts (625-627)
  • NewLineDiscountFrom (334-361)
  • AmountLineDiscount (100-108)
  • LineDiscountBase (62-69)
  • LineMaximumSpendReferenceID (21-21)
  • LineDiscountReasonMaximumSpend (27-27)
openmeter/billing/service/lineservice/pricepackage.go (3)
openmeter/billing/service/lineservice/pricer.go (2)
  • Pricer (23-29)
  • PricerCalculateInput (16-21)
openmeter/billing/service/lineservice/usagebasedline.go (3)
  • UsageChildUniqueReferenceID (20-20)
  • MinSpendChildUniqueReferenceID (21-21)
  • DecimalOne (42-42)
openmeter/productcatalog/price.go (2)
  • InArrearsPaymentTerm (21-21)
  • Commitments (687-693)
openmeter/billing/service/lineservice/pricegraduatedtiered.go (4)
openmeter/billing/service/lineservice/pricer.go (4)
  • ProgressiveBillingPricer (31-31)
  • ProgressiveBillingPricer (33-39)
  • Pricer (23-29)
  • PricerCalculateInput (16-21)
openmeter/productcatalog/price.go (1)
  • GraduatedTieredPrice (450-450)
openmeter/billing/service/lineservice/usagebasedline.go (3)
  • GraduatedTieredPriceUsageChildUniqueReferenceID (37-37)
  • GraduatedTieredFlatPriceChildUniqueReferenceID (38-38)
  • GraduatedMinSpendChildUniqueReferenceID (39-39)
openmeter/billing/invoiceline.go (1)
  • FlatFeeCategoryCommitment (248-248)
openmeter/billing/service/lineservice/usagebasedline.go (1)
openmeter/billing/service/lineservice/pricer.go (2)
  • PricerCalculateInput (16-21)
  • Pricer (23-29)
openmeter/billing/service/lineservice/usagebasedline_test.go (3)
openmeter/ent/db/billinginvoicelinediscount/where.go (2)
  • Quantity (116-118)
  • PreLinePeriodQuantity (121-123)
openmeter/ent/db/billinginvoiceline/where.go (1)
  • Quantity (168-170)
openmeter/ent/db/billinginvoiceusagebasedlineconfig/where.go (1)
  • PreLinePeriodQuantity (78-80)
openmeter/billing/service/lineservice/pricevolumetiered.go (5)
openmeter/billing/service/lineservice/pricer.go (4)
  • NonProgressiveBillingPricer (41-41)
  • NonProgressiveBillingPricer (43-59)
  • Pricer (23-29)
  • PricerCalculateInput (16-21)
openmeter/productcatalog/price.go (1)
  • VolumeTieredPrice (448-448)
openmeter/billing/errors.go (2)
  • ErrInvoiceLineVolumeSplitNotSupported (45-45)
  • ErrInvoiceLineMissingOpenEndedTier (47-47)
openmeter/billing/service/lineservice/usagebasedline.go (3)
  • VolumeFlatPriceChildUniqueReferenceID (33-33)
  • VolumeUnitPriceChildUniqueReferenceID (34-34)
  • VolumeMinSpendChildUniqueReferenceID (35-35)
openmeter/billing/invoiceline.go (1)
  • FlatFeeCategoryCommitment (248-248)
openmeter/billing/service/lineservice/pricer.go (2)
openmeter/ent/db/billinginvoiceline/where.go (1)
  • ParentLineID (142-144)
openmeter/billing/errors.go (2)
  • ValidationError (116-116)
  • ErrInvoiceProgressiveBillingNotSupported (38-38)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: E2E
  • GitHub Check: CI
  • GitHub Check: Developer environment
  • GitHub Check: Analyze (go)
🔇 Additional comments (70)
openmeter/billing/service/lineservice/pricedynamic_test.go (8)

14-25: Test case properly handles no usage scenario

The test correctly verifies that when there is no usage, the expected output is an empty detailed lines input, which is an important base case for the dynamic pricing logic.


28-52: Minimum spend commitment correctly enforced

This test validates that the minimum spend is honored even when there is no usage, which is a critical business rule. The expected output correctly includes a line item for the minimum spend with appropriate reference ID and payment term.


55-69: Mid-period behavior with minimum spend is validated

The test correctly verifies that minimum spend is not billed for mid-period lines, which aligns with the expected billing behavior where minimum spend commitments are only evaluated at the end of the period.


72-96: Last-in-period handling of minimum spend is verified

This test ensures that minimum spend is properly applied for the last line in a period, even with no usage, which is an important edge case for commitment billing.


99-118: Usage billing accurately tested

The test verifies that usage is calculated correctly with the multiplier applied. The expected per unit amount of 13.33 matches the calculation of 10 * 1.33333, demonstrating proper rounding behavior.


120-139: Mid-period usage billing verified

This test ensures that usage is billed correctly regardless of line position, which is important for consistent billing across split periods.


142-164: Maximum spend limit respected when not reached

The test validates that when a maximum spend is set but not hit, billing proceeds normally without any discounts applied.


166-201: Maximum spend discount correctly applied

This test comprehensively verifies the complex scenario where maximum spend is hit and a discount is applied. The calculation considers both the current period usage and pre-period usage to determine when the maximum spend cap is reached.

openmeter/billing/service/lineservice/priceunit_test.go (8)

13-25: LGTM: Unit price calculation tests cover essential scenarios

This test function follows the same pattern as the dynamic pricing tests, ensuring consistent testing across different pricing models, and properly handles the no usage case.


28-52: Minimum spend validation for unit prices

Test properly verifies that minimum spend commitment is honored even when there's no usage, which is an important business rule for unit pricing.


55-69: Mid-period line behavior with minimum spend

The test correctly verifies that minimum spend is not billed for mid-period lines, ensuring proper billing behavior for split billing periods.


72-96: Last-in-period behavior with minimum spend

This test confirms that minimum spend is properly applied for the last line in a period, which is essential for validating end-of-period commitment calculations.


99-118: Unit price usage billing verified

Test correctly validates that when usage is present, it's multiplied by the unit price (100 * 10 = 1000) and billed properly. The quantity field reflects the actual usage amount.


120-139: Mid-period usage billing

This test ensures that usage is billed consistently in mid-period line modes, which is important for split billing periods.


142-164: Maximum spend validation when not reached

Test correctly verifies that when maximum spend is set but not hit, billing proceeds normally without discounts.


166-200: Maximum spend discount properly calculated

This test validates that when maximum spend is exceeded, the appropriate discount is applied. The calculation properly accounts for both current and previous line period usage to determine when the cap is reached.

openmeter/billing/service/lineservice/pricepackage_test.go (11)

14-27: LGTM: Package pricing tests for no usage scenario

Test correctly verifies the base case where there's no usage, resulting in no detailed line items.


30-55: Package pricing with minimum spend commitment

Test properly validates that minimum spend commitment is honored for package pricing when there's no usage, following the same pattern as other pricing types.


58-73: Mid-period behavior with minimum spend

This test ensures that minimum spend is not billed for mid-period lines in package pricing, consistent with other pricing types.


76-101: Last-in-period with minimum spend

Test verifies that minimum spend is properly applied for the last line in a period, maintaining consistency with other pricing models.


104-124: Basic package usage billing

This test validates that when usage exactly matches the package size (10), one package is billed at the correct amount (15).


126-146: Mid-line package billing

Test confirms that package pricing works properly for mid-period line modes with full package usage.


148-168: Partial package usage billing

This test validates an important edge case where usage (5) is less than the package size (10), verifying that a full package is still billed even with partial usage.


170-192: Package billing with prior usage

Test ensures that package billing correctly accounts for previous usage in the period, which is important for maintaining consistent package billing across split periods.


195-218: Maximum spend handling when not reached

This test verifies that maximum spend limits don't affect package pricing when the limit isn't reached.


220-255: Maximum spend discount correctly applied

This test comprehensively validates the complex scenario where package usage exceeds the maximum spend limit, ensuring the proper discount is applied to cap the total cost.


258-266: Package calculation unit test

This test thoroughly verifies the getNumberOfPackages helper function with various edge cases, including zero quantity, values below package size, exactly at package size, and values requiring multiple packages.

openmeter/billing/service/lineservice/pricevolumetiered_test.go (17)

14-41: LGTM: Comprehensive test tier definition

The test tiers are well-defined to cover multiple scenarios: flat pricing in early tiers and unit pricing in later tiers with various thresholds.


43-55: Mid-period tiered volume test

This test verifies that no billing occurs for mid-period lines, which is consistent with the expected behavior for tiered volume pricing.


57-77: Last-in-period with no usage

The test correctly validates that when there's no usage in the last line of a period, the flat price for the first tier is applied.


79-97: Unit price in first tier

This test verifies that when only unit pricing is defined (no flat price) and there's no usage, no billing occurs, which is the expected behavior.


99-119: Tier 1 mid usage

The test confirms that usage within the first tier is properly billed using the flat price for that tier.


121-141: Tier 1 at upper boundary

This test validates that usage exactly at the upper boundary of tier 1 (5 units) is still billed using tier 1's flat price.


143-163: Tier 3 near upper boundary

Test verifies that usage near the upper boundary of tier 3 is billed correctly using the unit price for that tier.


165-185: Tier 3 at exact boundary

This test validates the edge case where usage is exactly at the upper boundary of tier 3, ensuring it's still billed in tier 3.


187-207: Tier 4 just over boundary

Test confirms that usage just above the tier 3 boundary (16 units) is correctly billed using tier 4's unit price.


209-229: Tier 4 with high usage

This test validates that high usage volumes are properly billed using the appropriate tier (tier 4 in this case).


233-264: Minimum spend with tiered pricing

Test verifies that minimum spend commitments are properly applied with tiered pricing, including calculating the difference between tier cost and minimum commitment.


266-289: Usage exceeding minimum spend

This test ensures that when usage exceeds the minimum spend commitment, no additional minimum spend line is added.


291-322: Usage below minimum spend

The test validates that when usage is below the minimum spend commitment, an additional line is added to meet the commitment amount.


324-347: Usage exactly at minimum spend

This test verifies the edge case where usage exactly matches the minimum spend commitment, ensuring no additional minimum spend line is added.


350-373: Maximum spend at tier boundary

The test validates that when maximum spend exactly matches a tier boundary, pricing is capped appropriately without discounts.


375-408: Maximum spend not aligned with tier boundary

This test verifies the complex case where maximum spend is not aligned with tier boundaries, ensuring the appropriate discount is applied to cap the total cost.


411-471: Helper function for tier identification

This test thoroughly validates the findTierForQuantity helper function across multiple scenarios, including tier boundaries and values in different ranges, ensuring the correct tier is identified for various usage amounts.

openmeter/billing/service/lineservice/pricevolumetiered.go (2)

28-31: Clarify partial billing logic.
Returning nil, nil when !l.IsLastInPeriod() means partial usage in mid-period is neither billed nor tracked, which might be acceptable for non-progressive billing strategies. However, confirm this aligns with your business requirements so that no usage is lost inadvertently.


112-125: Ensure infinite tier coverage is properly enforced.
By design, the last tier should have UpToAmount == nil. If it's missing, an error is returned. This seems correct for volume pricing. However, if there's a scenario in which the last tier incorrectly includes an upper bound, no tier might match large quantities. Ensure the product catalog always includes an open-ended final tier.

openmeter/billing/service/lineservice/usagebasedline.go (3)

235-245: Initialization of pricer input looks solid.
The straightforward check for nil quantities before building PricerCalculateInput ensures no hidden nil-pointer issues.


247-265: Confirm usage of volumeTieredPricer.
Although pricevolumetiered.go defines volumeTieredPricer, this getPricer() switch does not reference it. If volume-tiered pricing is expected to appear as a distinct type from standard tiered pricing, you may need an explicit case. Otherwise, ensure tieredPricer indeed covers volume-tiered logic.


268-280: Centralized calculation approach is clean.
This method delegates the entire logic to the pricer’s Calculate function, improving modularity and clarity.

openmeter/billing/service/lineservice/usagebasedline_test.go (4)

91-95: Consider validating tc.usage before assignment.

Here, you're directly assigning the usage-based quantities from tc.usage before invoking calculateDetailedLines(). It might be prudent to verify that tc.usage is not nil or contains valid data to avoid unexpected nil pointer dereferences. If there's any chance that tc.usage can be invalid, consider adding defensive checks or explicit test cases for zero/negative usage scenarios.


1951-1951: Good coverage check for unsupported progress billing.

Verifying that ErrInvoiceProgressiveBillingNotSupported is returned ensures the new validation logic works as intended.


2303-2305: Comment clarity acknowledged.

Your comment clarifies why there's no proration for flat prices. The subsequent line assignment to flatPerUsage aligns with the explanation. This is consistent and readable.


2320-2320: Loop usage looks good.

Iterating over [flatPerUnit, tieredGraduated] is straightforward for the final checks. The usage inside the loop appears consistent with your billing logic.

openmeter/billing/service/lineservice/pricer.go (6)

1-9: File structure and imports look clean.

The new file sets a clear boundary for pricer logic. The imports are minimal and relevant, with no obvious redundancies.


11-14: PricerCapabilities is succinct and purposeful.

Exposing a boolean flag for progressive billing is straightforward. The naming is direct, avoiding confusion about usage.


16-21: PricerCalculateInput naming is clear.

Structuring the line’s pre-period and line-period quantities alongside the embedded usageBasedLine clarifies input data for pricers.


23-29: Interface design is concise and future-proof.

Defining Calculate and CanBeInvoicedAsOf sets a clear contract. This clean separation of responsibilities should aid maintainability.


31-39: Progressive pricer logic partially implemented.

CanBeInvoicedAsOf correctly blocks invoicing if asOf precedes the line’s period start. However, note that the Calculate method is not shown here. If it's implemented elsewhere, ensure integration is tested. Otherwise, consider adding an explicit implementation or a stub to prevent runtime errors.


41-59: Non-progressive pricer logic is consistent with new billing constraints.

We see the correct error return for parent lines indicating progressive billing is unsupported. As with the progressive pricer, ensure the Calculate method is fully implemented or stubbed in the appropriate location to satisfy the Pricer interface fully.

openmeter/billing/service/lineservice/pricegraduatedtiered.go (5)

13-17: Good implementation of the Pricer interface

The code correctly defines the graduatedTieredPricer struct and confirms it implements the Pricer interface with a compile-time check, which is a great practice to ensure interface compliance.


19-112: The Calculate method implementation looks correct

The overall flow of the Calculate method is appropriate. It properly converts the price to a tiered price, validates the price mode, and delegates to the tiered price calculator.


145-171: Thorough validation of input parameters

The validation method is well-implemented with meaningful error messages for each validation check.


66-68:

Details

❓ Verification inconclusive

Verify the flat price billing logic

The comment states "Flat price is always billed for the whole tier when we are crossing the tier boundary", but the code checks both in.AtTierBoundary and shouldFirstFlatLineBeBilled. Please verify if this is the intended behavior, as it may lead to unexpected results in edge cases.


🏁 Script executed:

#!/bin/bash
# Search for other implementations or tests of flat price billing logic
rg -A 5 "flat[_ ]?price.*bill" --type go

Length of output: 657


Verify Flat Price Billing Logic

The current implementation in openmeter/billing/service/lineservice/pricegraduatedtiered.go (lines 66–68) checks for both in.AtTierBoundary and shouldFirstFlatLineBeBilled along with the existence of in.Tier.FlatPrice. Test cases in pricegraduatedtiered_test.go indicate that this combined condition is intentional to ensure that the flat price is billed only once when the tier boundary is crossed. However, the inline comment stating "Flat price is always billed for the whole tier when we are crossing the tier boundary" may be misleading since the billing only occurs if shouldFirstFlatLineBeBilled is true.

  • Verify that the additional shouldFirstFlatLineBeBilled flag is indeed meant to gate the flat price billing.
  • If this behavior is intended, consider updating the comment to clarify that billing applies only when both conditions are met (i.e., to prevent duplicate billing in edge cases).

267-276:

Details

❓ Verification inconclusive

Special handling for zero quantity

This special case handles a scenario where in.ToQty is zero. It's important to verify this is the correct behavior and that it's properly tested, especially since this appears to be the only place that explicitly handles zero quantities.


🏁 Script executed:

#!/bin/bash
# Search for tests covering zero quantity scenarios
rg -A 10 "Zero|zero.*quantity" --type go

Length of output: 88608


Review Zero Quantity Handling in Price Graduated Tiered Logic

The special handling for when in.ToQty is zero (lines 267–276 in openmeter/billing/service/lineservice/pricegraduatedtiered.go) remains the only explicit treatment of a zero quantity in this module. A search for tests using the “Zero” keyword returned many hits across the codebase (including tests like TestEmptyInvoiceGenerationZeroUsage and TestEmptyInvoiceGenerationZeroPrice in the billing tests), but none appear to directly target this exact behavior.

  • Action Required:
    Please verify that the scenario where in.ToQty is zero is explicitly covered—either by dedicating a test for this edge case or by confirming that existing tests indeed exercise this code path.
test/billing/invoice_test.go (6)

1811-1812: Updated test expectations for progressive invoicing

This change reduces the expected number of invoice lines from 3 to 2, correctly reflecting that the flat per usage item (which is in arrears) should no longer be included in progressive billing.


1819-1820: Improved test documentation for billing behavior

This comment clearly explains why the flat per usage item is excluded from progressive billing - because it's invoiced in arrears without pro-rating, which aligns with the PR objective of correctly classifying UBP FlatFee lines.


1820-1825: Enhanced assertion logic for invoice line validation

Changed from NotContains to ElementsMatch for a more precise verification of exactly which lines should be present in the invoice, improving the test's reliability.


1951-1952: Updated error verification for progressive billing support

The error checking now correctly uses the new ErrInvoiceProgressiveBillingNotSupported error instead of the previous ErrInvoiceLinesNotBillable, properly reflecting the new validation behavior for progressive billing.


1970-1971: Consistent line count assertion for updated invoice

Updated the expected line count to 2 to maintain consistency with the earlier change to line count expectations.


2290-2291: Added debug output for improved test diagnostics

Added debug dump of the invoice to aid in troubleshooting and understanding the test behavior.

@turip turip force-pushed the refactor/billing-pricer-interface branch from b7c7703 to 3d893b9 Compare April 8, 2025 10:13
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🔭 Outside diff range comments (1)
openmeter/billing/service/lineservice/usagebasedline.go (1)

215-245: 💡 Verification agent

🧩 Analysis chain

Validate potential negative usage.
Currently, asPricerCalculateInput only checks if usage is nil, not if it’s negative. If negative usage is impossible, consider asserting it in your validation. Otherwise, handle it carefully (e.g., convert to zero or return an error).

Run the following script to see if negative usage can come from other code paths that call SnapshotQuantity:


🏁 Script executed:

#!/bin/bash
# Searching codebase for suspicious usage assignments
rg -A 3 "UsageBased" | rg -i "quantity.*="

Length of output: 1261


Action Required: Validate and Handle Negative Usage Values

The function asPricerCalculateInput in openmeter/billing/service/lineservice/usagebasedline.go currently only checks for nil values for the usage-based quantities. However, based on our search, there’s no evident mechanism preventing negative values from being assigned (e.g., via lo.ToPtr(usage.LinePeriodQty)). If negative usage is not a valid state, please enhance the validation to assert non-negativity (or otherwise handle negative values, such as converting them to zero or returning an error). For instance, consider updating the function to:

  • Verify that both *l.line.UsageBased.Quantity and *l.line.UsageBased.PreLinePeriodQuantity are non-negative.
  • Include unit tests to confirm that negative values are either rejected or handled as intended.
♻️ Duplicate comments (2)
openmeter/billing/service/lineservice/pricepackage.go (2)

34-46: Guard against negative “toBeBilledPackages.”
If postLinePeriodPackages < preLinePeriodPackages, then toBeBilledPackages can become negative, potentially creating a negative quantity line entry, which may be disallowed. Consider returning an error or capping the value at zero if usage legitimately decreases.


61-69: Prevent division by zero in getNumberOfPackages.
No checks guard packageSize from being zero or negative, which can lead to a panic. As done in earlier pricer implementations, add a guard clause to handle zero or negative packageSize accordingly.

🧹 Nitpick comments (10)
openmeter/billing/service/lineservice/priceflat.go (2)

14-14: Consider linking the TODO with an issue or future plan.
The comment at line 14 indicates an upcoming need to handle progressive billing logic. Having a clear reference or issue ID ensures that the future refactor won’t be overlooked.

Would you like me to open a follow-up issue to track this enhancement?


20-24: Append context to the error message.
When wrapping the error at line 23, consider appending some metadata (e.g., line ID, underlying price data) to aid in troubleshooting.

-return nil, fmt.Errorf("converting price to flat price: %w", err)
+return nil, fmt.Errorf("converting price to flat price (%s, line: %s): %w",
+     l.line.Name, l.line.ID, err)
openmeter/billing/service/lineservice/pricepackage_test.go (1)

258-266: Consider boundary tests for negative usage or extremely large usage.
While the existing tests are thorough for typical usage ranges, additional tests covering edge cases (e.g., accidental negative usage, extremely large usage) could bolster your confidence in the pricing logic.

openmeter/billing/service/lineservice/pricevolumetiered.go (3)

28-31: Confirm unsupported progressive billing for volume-tiered pricing.
Returning no error but also no lines when IsLastInPeriod() is false may be confusing to future maintainers. Document this clearly in related docstrings or comments to clarify why partial billing is disregarded.


33-36: Raise an explicit error or logging warning for partial usage.
Instead of silently returning an error only if preLinePeriodQty is non-zero, consider alerting that volume-tiered pricing does not support partial usage if the code ever attempts it.


107-124: Handle multiple tiers if future requirements expand.
Currently, findTierForQuantity returns only a single tier. If future product requirements necessitate multi-tier splits within a single period, you might need a more advanced approach.

openmeter/billing/service/lineservice/usagebasedline.go (1)

88-103: Consider adding debug logs or comments for pricer eligibility.
When pricer.CanBeInvoicedAsOf returns !canBeInvoiced, the code silently skips invoicing with a nil, nil return. Adding logs or clarifying comments could help future maintainers understand why invoicing was skipped.

openmeter/billing/service/lineservice/usagebasedline_test.go (1)

91-99: Add coverage for edge usage values.
The test sets positive usage but does not verify behavior with zero or negative usage. If such edge cases can occur, add test scenarios to ensure calculateDetailedLines handles them consistently.

test/billing/invoice_test.go (1)

1819-1820: Correct spelling of "arreas" → "arrears"
There is a minor misspelling in these comments.

-// - the volume item as that must be invoiced in arreas
-// - the flat per usage item as that is invoiced in arreas (no pro-rating)
+// - the volume item as that must be invoiced in arrears
+// - the flat per usage item as that is invoiced in arrears (no pro-rating)
openmeter/billing/service/lineservice/pricegraduatedtiered.go (1)

174-224: Splitting tier ranges at boundaries
Splitting the usage range at tier boundaries helps produce clear sub-ranges for partial usage. The approach is correct but keep an eye on large usage inputs—extensive splitting might be less efficient.

Potentially consider an early-exit if usage surpasses an upper limit.

+// Example early exit check:
+if from >= in.ToQty {
+    return append(res, pendingLine)
+}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b7c7703 and 3d893b9.

📒 Files selected for processing (18)
  • openmeter/billing/errors.go (1 hunks)
  • openmeter/billing/service/lineservice/pricedynamic.go (1 hunks)
  • openmeter/billing/service/lineservice/pricedynamic_test.go (1 hunks)
  • openmeter/billing/service/lineservice/priceflat.go (1 hunks)
  • openmeter/billing/service/lineservice/priceflat_test.go (1 hunks)
  • openmeter/billing/service/lineservice/pricegraduatedtiered.go (1 hunks)
  • openmeter/billing/service/lineservice/pricegraduatedtiered_test.go (1 hunks)
  • openmeter/billing/service/lineservice/pricepackage.go (1 hunks)
  • openmeter/billing/service/lineservice/pricepackage_test.go (1 hunks)
  • openmeter/billing/service/lineservice/pricer.go (1 hunks)
  • openmeter/billing/service/lineservice/pricetiered.go (1 hunks)
  • openmeter/billing/service/lineservice/priceunit.go (1 hunks)
  • openmeter/billing/service/lineservice/priceunit_test.go (1 hunks)
  • openmeter/billing/service/lineservice/pricevolumetiered.go (1 hunks)
  • openmeter/billing/service/lineservice/pricevolumetiered_test.go (1 hunks)
  • openmeter/billing/service/lineservice/usagebasedline.go (3 hunks)
  • openmeter/billing/service/lineservice/usagebasedline_test.go (1 hunks)
  • test/billing/invoice_test.go (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (9)
  • openmeter/billing/service/lineservice/priceflat_test.go
  • openmeter/billing/errors.go
  • openmeter/billing/service/lineservice/priceunit.go
  • openmeter/billing/service/lineservice/pricedynamic.go
  • openmeter/billing/service/lineservice/priceunit_test.go
  • openmeter/billing/service/lineservice/pricedynamic_test.go
  • openmeter/billing/service/lineservice/pricetiered.go
  • openmeter/billing/service/lineservice/pricevolumetiered_test.go
  • openmeter/billing/service/lineservice/pricer.go
🧰 Additional context used
🧬 Code Definitions (8)
openmeter/billing/service/lineservice/pricepackage.go (3)
openmeter/billing/service/lineservice/pricer.go (2)
  • Pricer (23-29)
  • PricerCalculateInput (16-21)
openmeter/billing/service/lineservice/usagebasedline.go (3)
  • UsageChildUniqueReferenceID (20-20)
  • MinSpendChildUniqueReferenceID (21-21)
  • DecimalOne (42-42)
openmeter/productcatalog/price.go (1)
  • InArrearsPaymentTerm (21-21)
openmeter/billing/service/lineservice/pricevolumetiered.go (5)
openmeter/billing/service/lineservice/pricer.go (4)
  • NonProgressiveBillingPricer (41-41)
  • NonProgressiveBillingPricer (43-59)
  • Pricer (23-29)
  • PricerCalculateInput (16-21)
openmeter/productcatalog/price.go (2)
  • VolumeTieredPrice (448-448)
  • InArrearsPaymentTerm (21-21)
openmeter/billing/errors.go (2)
  • ErrInvoiceLineVolumeSplitNotSupported (45-45)
  • ErrInvoiceLineMissingOpenEndedTier (47-47)
openmeter/billing/service/lineservice/usagebasedline.go (3)
  • VolumeFlatPriceChildUniqueReferenceID (33-33)
  • VolumeUnitPriceChildUniqueReferenceID (34-34)
  • VolumeMinSpendChildUniqueReferenceID (35-35)
openmeter/billing/invoiceline.go (1)
  • FlatFeeCategoryCommitment (248-248)
openmeter/billing/service/lineservice/pricegraduatedtiered.go (4)
openmeter/billing/service/lineservice/pricer.go (4)
  • ProgressiveBillingPricer (31-31)
  • ProgressiveBillingPricer (33-39)
  • Pricer (23-29)
  • PricerCalculateInput (16-21)
openmeter/productcatalog/price.go (2)
  • GraduatedTieredPrice (450-450)
  • InArrearsPaymentTerm (21-21)
openmeter/billing/service/lineservice/usagebasedline.go (3)
  • GraduatedTieredPriceUsageChildUniqueReferenceID (37-37)
  • GraduatedTieredFlatPriceChildUniqueReferenceID (38-38)
  • GraduatedMinSpendChildUniqueReferenceID (39-39)
openmeter/billing/invoiceline.go (1)
  • FlatFeeCategoryCommitment (248-248)
openmeter/billing/service/lineservice/priceflat.go (4)
openmeter/billing/service/lineservice/pricer.go (4)
  • NonProgressiveBillingPricer (41-41)
  • NonProgressiveBillingPricer (43-59)
  • Pricer (23-29)
  • PricerCalculateInput (16-21)
openmeter/productcatalog/price.go (3)
  • PaymentTermType (24-24)
  • InAdvancePaymentTerm (20-20)
  • InArrearsPaymentTerm (21-21)
openmeter/billing/errors.go (1)
  • ValidationError (116-116)
openmeter/billing/service/lineservice/usagebasedline.go (1)
  • FlatPriceChildUniqueReferenceID (25-25)
openmeter/billing/service/lineservice/usagebasedline.go (1)
openmeter/billing/service/lineservice/pricer.go (2)
  • PricerCalculateInput (16-21)
  • Pricer (23-29)
openmeter/billing/service/lineservice/pricepackage_test.go (5)
openmeter/productcatalog/price.go (2)
  • NewPriceFrom (338-360)
  • Commitments (687-693)
openmeter/billing/service/lineservice/usagebasedline.go (2)
  • MinSpendChildUniqueReferenceID (21-21)
  • UsageChildUniqueReferenceID (20-20)
openmeter/billing/invoiceline.go (1)
  • FlatFeeCategoryCommitment (248-248)
openmeter/productcatalog/discount.go (1)
  • Discounts (346-346)
openmeter/billing/invoicelinediscount.go (6)
  • NewLineDiscounts (625-627)
  • NewLineDiscountFrom (334-361)
  • AmountLineDiscount (100-108)
  • LineDiscountBase (62-69)
  • LineMaximumSpendReferenceID (21-21)
  • LineDiscountReasonMaximumSpend (27-27)
openmeter/billing/service/lineservice/pricegraduatedtiered_test.go (4)
openmeter/productcatalog/price.go (4)
  • PriceTierFlatPrice (651-654)
  • PriceTierUnitPrice (670-673)
  • GraduatedTieredPrice (450-450)
  • Commitments (687-693)
openmeter/billing/service/lineservice/usagebasedline.go (1)
  • GraduatedMinSpendChildUniqueReferenceID (39-39)
openmeter/billing/invoiceline.go (1)
  • FlatFeeCategoryCommitment (248-248)
openmeter/billing/invoicelinediscount.go (3)
  • NewLineDiscounts (625-627)
  • NewLineDiscountFrom (334-361)
  • AmountLineDiscount (100-108)
openmeter/billing/service/lineservice/usagebasedline_test.go (3)
openmeter/ent/db/billinginvoicelinediscount/where.go (2)
  • Quantity (116-118)
  • PreLinePeriodQuantity (121-123)
openmeter/ent/db/billinginvoiceline/where.go (1)
  • Quantity (168-170)
openmeter/ent/db/billinginvoiceusagebasedlineconfig/where.go (1)
  • PreLinePeriodQuantity (78-80)
⏰ Context from checks skipped due to timeout of 90000ms (11)
  • GitHub Check: Artifacts / Container image
  • GitHub Check: Lint
  • GitHub Check: CI
  • GitHub Check: Quickstart
  • GitHub Check: E2E
  • GitHub Check: Developer environment
  • GitHub Check: Commit hooks
  • GitHub Check: Build
  • GitHub Check: Test
  • GitHub Check: Analyze (go)
  • GitHub Check: Migration Checks
🔇 Additional comments (18)
openmeter/billing/service/lineservice/priceflat.go (1)

57-57:

Details

❓ Verification inconclusive

Clarify return of nil lines for non-first and non-last periods.
Returning (nil, nil) is valid if the code intentionally avoids generating lines for partial or middle-of-the-period usage. Confirm whether this design meets long-term requirements for partial billing cycles.


🏁 Script executed:

#!/bin/bash
# Check references to partial-billing usage with `flatPricer` across the codebase.
rg -A 5 '[Ff]latPricer' 

Length of output: 1633


Clarify and Document the Partial Billing Cycle Behavior

The flatPricer.Calculate method currently returns (nil, nil) for non-first and non-last periods, indicating that no billing lines are generated for intermediate periods. This appears to be intentional—especially given the TODO comment suggesting the future introduction of a ProgressiveBillingPricer to support full pro-rating. Please update the inline documentation to explicitly state that this behavior is by design under the current model and note the potential future requirements for partial billing cycles.

openmeter/billing/service/lineservice/pricepackage_test.go (1)

14-255: Robust test coverage for package pricing scenarios.
These sub-tests comprehensively validate various usage and commitment scenarios, including zero usage, min spend, max spend, and partial usage. The naming and structure of these tests make them easy to read and maintain.

openmeter/billing/service/lineservice/usagebasedline.go (1)

247-280: Looks good for pricer selection and calculation flow.
The getPricer and calculateDetailedLines functions correctly delegate logic to the new pricer implementations and handle errors cleanly.

test/billing/invoice_test.go (6)

1811-1811: Confirm reduced line count expectation
This test now expects 2 lines instead of the previously higher count. Ensure all code paths align with this updated logic and that other tests or references are consistent with the refined invoice line structure.


1951-1951: Use of new error constant for unsupported progressive billing
This aligns correctly with the updated logic for progressive billing.


1971-1971: Reduced line count check
Similar to the prior assertion, ensure the new expected line count of 2 is fully tested and validated against the new logic.


2108-2108: Consistent line count check
Confirm that the invoice creation logic consistently produces 2 lines under this flow.


2291-2292: Debugging test output and updated line count
The additional debug dump can help trace flows at the end of the billing period. The assertion for 4 lines appears consistent with the extended scenario coverage.


2303-2305: Clarifying comment on pro-ration
These remarks clarify that flat prices are retained without partial splits. Looks consistent with the intended design.

openmeter/billing/service/lineservice/pricegraduatedtiered_test.go (4)

1-15: Package and imports
All necessary test dependencies (testify, lo, require, etc.) are correctly imported, and the package naming follows standard conventions.


17-116: Graduated tier test scenarios
The test cases comprehensively cover several usage patterns, checking both no usage and multi-tier usage. This is good coverage for graduated tiered pricing. Ensure minimal spend, maximum spend, or discount scenarios are also tested in these segments.


224-284: Maximum spend discount checks
These tests validate overage discount logic when usage surpasses the configured spend threshold. The approach to add line discounts looks correct and ensures no over-billing.


287-549: Tier callback mechanism & finalizer
The callback-based approach for tier boundaries, along with finalizer checks, is well-structured. It promotes clear testing of boundary conditions and accumulative totals. Keep verifying edge cases (like negative or extremely large quantities) if relevant.

openmeter/billing/service/lineservice/pricegraduatedtiered.go (5)

1-15: Struct definition and base pricer embedding
Embedding ProgressiveBillingPricer in graduatedTieredPricer is an organized approach. It keeps high-level progressive billing logic accessible while focusing the graduated tier logic here.


19-27: Validation on tiered price mode
The check ensures the price’s mode is indeed GraduatedTieredPrice. This guards against accidental usage of other pricing modes.


31-61: Callback-based tier processing
The callback function building each tier’s detailed line, along with maximum spend handling, is both readable and extensible. Continue to ensure concurrency safety if multi-threaded usage is anticipated.


63-106: Minimum spend finalizer
Applying the minimum spend as a final step when the total is below a threshold is well-handled. This logic is crucial for ensuring customers meet their committed spend.


225-326: Tiered price calculator
Iterating through sorted tiers, aggregating sub-ranges, and applying the finalizer is clean. The break condition at line 317 prevents unnecessary processing, which benefits performance.

@turip turip force-pushed the refactor/billing-pricer-interface branch from 3d893b9 to 8448c9c Compare April 8, 2025 10:23
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
openmeter/billing/service/lineservice/priceflat.go (5)

13-16: Ensure proper documentation for struct

The flatPricer struct lacks godoc-style documentation explaining its purpose and usage. Consider adding comments that describe what this struct does and how it fits into the billing system.

+// flatPricer implements the Pricer interface for flat pricing calculations.
+// It currently uses NonProgressiveBillingPricer as its base implementation.
 type flatPricer struct {
 	// TODO[later]: when we introduce full pro-rating support this should be ProgressiveBillingPricer
 	NonProgressiveBillingPricer
 }

20-24: Add godoc-style documentation for the Calculate method

The Calculate method lacks proper documentation explaining its purpose, parameters, and return values.

+// Calculate determines the detailed lines for a flat price based on the payment term.
+// It returns appropriate detailed line inputs or an error if the price cannot be calculated.
 func (p flatPricer) Calculate(l PricerCalculateInput) (newDetailedLinesInput, error) {
 	flatPrice, err := l.line.UsageBased.Price.AsFlat()
 	if err != nil {
 		return nil, fmt.Errorf("converting price to flat price: %w", err)
 	}

26-32: Consider using a cleaner approach for payment term validation

The payment term validation could be simplified by directly checking against the valid terms instead of using slices.Contains.

-	if !slices.Contains(
-		[]productcatalog.PaymentTermType{productcatalog.InAdvancePaymentTerm, productcatalog.InArrearsPaymentTerm},
-		flatPrice.PaymentTerm) {
+	if flatPrice.PaymentTerm != productcatalog.InAdvancePaymentTerm && 
+	   flatPrice.PaymentTerm != productcatalog.InArrearsPaymentTerm {
 		return nil, billing.ValidationError{
 			Err: fmt.Errorf("flat price payment term %s is not supported", flatPrice.PaymentTerm),
 		}
 	}

34-55: Consider refactoring duplicated code in switch statement

There's significant duplication between the two case blocks. You could extract the common parts to reduce repetition.

-	switch {
-	case flatPrice.PaymentTerm == productcatalog.InAdvancePaymentTerm && l.IsFirstInPeriod():
-		return newDetailedLinesInput{
-			{
-				Name:                   l.line.Name,
-				Quantity:               alpacadecimal.NewFromFloat(1),
-				PerUnitAmount:          flatPrice.Amount,
-				ChildUniqueReferenceID: FlatPriceChildUniqueReferenceID,
-				PaymentTerm:            productcatalog.InAdvancePaymentTerm,
-			},
-		}, nil
-	case flatPrice.PaymentTerm == productcatalog.InArrearsPaymentTerm && l.IsLastInPeriod():
-		return newDetailedLinesInput{
-			{
-				Name:                   l.line.Name,
-				Quantity:               alpacadecimal.NewFromFloat(1),
-				PerUnitAmount:          flatPrice.Amount,
-				ChildUniqueReferenceID: FlatPriceChildUniqueReferenceID,
-				PaymentTerm:            productcatalog.InArrearsPaymentTerm,
-			},
-		}, nil
-	}
+	var shouldCharge bool
+	var paymentTerm productcatalog.PaymentTermType
+	
+	switch flatPrice.PaymentTerm {
+	case productcatalog.InAdvancePaymentTerm:
+		shouldCharge = l.IsFirstInPeriod()
+		paymentTerm = productcatalog.InAdvancePaymentTerm
+	case productcatalog.InArrearsPaymentTerm:
+		shouldCharge = l.IsLastInPeriod()
+		paymentTerm = productcatalog.InArrearsPaymentTerm
+	}
+	
+	if shouldCharge {
+		return newDetailedLinesInput{
+			{
+				Name:                   l.line.Name,
+				Quantity:               alpacadecimal.NewFromFloat(1),
+				PerUnitAmount:          flatPrice.Amount,
+				ChildUniqueReferenceID: FlatPriceChildUniqueReferenceID,
+				PaymentTerm:            paymentTerm,
+			},
+		}, nil
+	}

14-14: Consider adding a ticket reference to the TODO comment

The TODO comment would be more useful with a reference to a ticket or issue number for tracking.

-	// TODO[later]: when we introduce full pro-rating support this should be ProgressiveBillingPricer
+	// TODO[later,TICKET-XYZ]: when we introduce full pro-rating support this should be ProgressiveBillingPricer
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3d893b9 and 8448c9c.

📒 Files selected for processing (1)
  • openmeter/billing/service/lineservice/priceflat.go (1 hunks)
🧰 Additional context used
🧬 Code Definitions (1)
openmeter/billing/service/lineservice/priceflat.go (4)
openmeter/billing/service/lineservice/pricer.go (4)
  • NonProgressiveBillingPricer (41-41)
  • NonProgressiveBillingPricer (43-59)
  • Pricer (23-29)
  • PricerCalculateInput (16-21)
openmeter/productcatalog/price.go (3)
  • PaymentTermType (24-24)
  • InAdvancePaymentTerm (20-20)
  • InArrearsPaymentTerm (21-21)
openmeter/billing/errors.go (1)
  • ValidationError (116-116)
openmeter/billing/service/lineservice/usagebasedline.go (1)
  • FlatPriceChildUniqueReferenceID (25-25)
⏰ Context from checks skipped due to timeout of 90000ms (11)
  • GitHub Check: E2E
  • GitHub Check: CI
  • GitHub Check: Developer environment
  • GitHub Check: Quickstart
  • GitHub Check: Test
  • GitHub Check: Migration Checks
  • GitHub Check: Lint
  • GitHub Check: Commit hooks
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Build
  • GitHub Check: Analyze (go)
🔇 Additional comments (1)
openmeter/billing/service/lineservice/priceflat.go (1)

18-18: Good usage of interface compliance check

Using a compile-time check to ensure that flatPricer implements the Pricer interface is a good practice.

turip added 4 commits April 8, 2025 12:30
This patch splits the monolithic ubp line into smaller pricers
allowing to compose the types going forward.

The patch also fixes an issue, that UBP FlatFee lines were considered
progressively billable (this is only an issue if the line is in arrears)
which is a rare use-case.
@turip turip force-pushed the refactor/billing-pricer-interface branch from 8448c9c to 74689c5 Compare April 8, 2025 10:30
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (5)
openmeter/billing/service/lineservice/pricepackage_test.go (1)

258-266: Duplicate comment: Ensure checks against zero or negative packageSize
This line references the getNumberOfPackages function, which can potentially divide by zero if packageSize is zero or negative. A similar comment was shared in a past review.

openmeter/billing/service/lineservice/pricepackage.go (2)

34-36: Duplicate comment: Guard against negative toBeBilledPackages
If postLinePeriodPackages < preLinePeriodPackages, then toBeBilledPackages becomes negative. Confirm whether usage can legitimately decrease and, if so, handle negative results appropriately. This was raised in a previous review.


61-69: Duplicate comment: Prevent division by zero in getNumberOfPackages
No check exists for packageSize being zero or negative, which could lead to division by zero. Consider adding appropriate validation as previously suggested.

openmeter/billing/service/lineservice/pricegraduatedtiered.go (2)

19-112: Missing billedAmount update after adding flat price.

When adding the flat price line at lines 66–85, the code appends the new flat line item but does not update billedAmount accordingly. This risks undercounting total spend when both unit price and flat price exist in the same tier.


224-328: Add safeguard for empty tiers.

As highlighted in a past review, ensure you handle the scenario where in.TieredPrice.Tiers might be empty to avoid potential panics or unexpected behavior.

🧹 Nitpick comments (3)
openmeter/billing/service/lineservice/pricepackage_test.go (1)

14-256: Consider adding negative usage scenario to broaden test coverage
These tests appear robust and thorough for typical usage patterns. However, it's unclear how the system should respond to negative usage values or other edge cases (e.g., usage decreasing between lines). Adding such scenarios could help ensure full coverage.

openmeter/billing/service/lineservice/usagebasedline.go (1)

88-92: Consider wrapping the error for additional context.

When getPricer() fails, returning the raw error might obscure the source. Consider wrapping it (e.g., fmt.Errorf("progressive billing: %w", err)) to improve traceability.

openmeter/billing/service/lineservice/pricegraduatedtiered.go (1)

173-222: Range-splitting logic is clear.

The function neatly partitions quantity ranges at the needed boundaries for tier calculations. Recommended to ensure thorough unit tests reflect off-by-one or boundary conditions.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8448c9c and 74689c5.

📒 Files selected for processing (18)
  • openmeter/billing/errors.go (1 hunks)
  • openmeter/billing/service/lineservice/pricedynamic.go (1 hunks)
  • openmeter/billing/service/lineservice/pricedynamic_test.go (1 hunks)
  • openmeter/billing/service/lineservice/priceflat.go (1 hunks)
  • openmeter/billing/service/lineservice/priceflat_test.go (1 hunks)
  • openmeter/billing/service/lineservice/pricegraduatedtiered.go (1 hunks)
  • openmeter/billing/service/lineservice/pricegraduatedtiered_test.go (1 hunks)
  • openmeter/billing/service/lineservice/pricepackage.go (1 hunks)
  • openmeter/billing/service/lineservice/pricepackage_test.go (1 hunks)
  • openmeter/billing/service/lineservice/pricer.go (1 hunks)
  • openmeter/billing/service/lineservice/pricetiered.go (1 hunks)
  • openmeter/billing/service/lineservice/priceunit.go (1 hunks)
  • openmeter/billing/service/lineservice/priceunit_test.go (1 hunks)
  • openmeter/billing/service/lineservice/pricevolumetiered.go (1 hunks)
  • openmeter/billing/service/lineservice/pricevolumetiered_test.go (1 hunks)
  • openmeter/billing/service/lineservice/usagebasedline.go (3 hunks)
  • openmeter/billing/service/lineservice/usagebasedline_test.go (1 hunks)
  • test/billing/invoice_test.go (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (12)
  • openmeter/billing/errors.go
  • openmeter/billing/service/lineservice/pricedynamic.go
  • openmeter/billing/service/lineservice/priceunit_test.go
  • openmeter/billing/service/lineservice/pricedynamic_test.go
  • openmeter/billing/service/lineservice/priceunit.go
  • openmeter/billing/service/lineservice/priceflat.go
  • openmeter/billing/service/lineservice/pricetiered.go
  • openmeter/billing/service/lineservice/pricegraduatedtiered_test.go
  • openmeter/billing/service/lineservice/pricevolumetiered_test.go
  • openmeter/billing/service/lineservice/pricer.go
  • openmeter/billing/service/lineservice/priceflat_test.go
  • test/billing/invoice_test.go
🧰 Additional context used
🧬 Code Definitions (6)
openmeter/billing/service/lineservice/pricepackage_test.go (5)
openmeter/productcatalog/price.go (2)
  • NewPriceFrom (338-360)
  • Commitments (687-693)
openmeter/billing/service/lineservice/usagebasedline.go (2)
  • MinSpendChildUniqueReferenceID (21-21)
  • UsageChildUniqueReferenceID (20-20)
openmeter/billing/invoiceline.go (1)
  • FlatFeeCategoryCommitment (248-248)
openmeter/productcatalog/discount.go (1)
  • Discounts (346-346)
openmeter/billing/invoicelinediscount.go (6)
  • NewLineDiscounts (625-627)
  • NewLineDiscountFrom (334-361)
  • AmountLineDiscount (100-108)
  • LineDiscountBase (62-69)
  • LineMaximumSpendReferenceID (21-21)
  • LineDiscountReasonMaximumSpend (27-27)
openmeter/billing/service/lineservice/pricevolumetiered.go (5)
openmeter/billing/service/lineservice/pricer.go (4)
  • NonProgressiveBillingPricer (41-41)
  • NonProgressiveBillingPricer (43-59)
  • Pricer (23-29)
  • PricerCalculateInput (16-21)
openmeter/productcatalog/price.go (2)
  • VolumeTieredPrice (448-448)
  • InArrearsPaymentTerm (21-21)
openmeter/billing/errors.go (2)
  • ErrInvoiceLineVolumeSplitNotSupported (45-45)
  • ErrInvoiceLineMissingOpenEndedTier (47-47)
openmeter/billing/service/lineservice/usagebasedline.go (3)
  • VolumeFlatPriceChildUniqueReferenceID (33-33)
  • VolumeUnitPriceChildUniqueReferenceID (34-34)
  • VolumeMinSpendChildUniqueReferenceID (35-35)
openmeter/billing/invoiceline.go (1)
  • FlatFeeCategoryCommitment (248-248)
openmeter/billing/service/lineservice/pricepackage.go (3)
openmeter/billing/service/lineservice/pricer.go (2)
  • Pricer (23-29)
  • PricerCalculateInput (16-21)
openmeter/billing/service/lineservice/usagebasedline.go (3)
  • UsageChildUniqueReferenceID (20-20)
  • MinSpendChildUniqueReferenceID (21-21)
  • DecimalOne (42-42)
openmeter/productcatalog/price.go (2)
  • InArrearsPaymentTerm (21-21)
  • Commitments (687-693)
openmeter/billing/service/lineservice/usagebasedline_test.go (3)
openmeter/ent/db/billinginvoicelinediscount/where.go (2)
  • Quantity (116-118)
  • PreLinePeriodQuantity (121-123)
openmeter/ent/db/billinginvoiceline/where.go (1)
  • Quantity (168-170)
openmeter/ent/db/billinginvoiceusagebasedlineconfig/where.go (1)
  • PreLinePeriodQuantity (78-80)
openmeter/billing/service/lineservice/pricegraduatedtiered.go (4)
openmeter/billing/service/lineservice/pricer.go (4)
  • ProgressiveBillingPricer (31-31)
  • ProgressiveBillingPricer (33-39)
  • Pricer (23-29)
  • PricerCalculateInput (16-21)
openmeter/productcatalog/price.go (2)
  • GraduatedTieredPrice (450-450)
  • InArrearsPaymentTerm (21-21)
openmeter/billing/service/lineservice/usagebasedline.go (3)
  • GraduatedTieredPriceUsageChildUniqueReferenceID (37-37)
  • GraduatedTieredFlatPriceChildUniqueReferenceID (38-38)
  • GraduatedMinSpendChildUniqueReferenceID (39-39)
openmeter/billing/invoiceline.go (1)
  • FlatFeeCategoryCommitment (248-248)
openmeter/billing/service/lineservice/usagebasedline.go (2)
openmeter/billing/service/lineservice/pricer.go (2)
  • PricerCalculateInput (16-21)
  • Pricer (23-29)
openmeter/productcatalog/price.go (6)
  • Price (80-87)
  • FlatPriceType (41-41)
  • UnitPriceType (42-42)
  • TieredPriceType (43-43)
  • PackagePriceType (45-45)
  • DynamicPriceType (44-44)
⏰ Context from checks skipped due to timeout of 90000ms (11)
  • GitHub Check: Quickstart
  • GitHub Check: E2E
  • GitHub Check: CI
  • GitHub Check: Developer environment
  • GitHub Check: Test
  • GitHub Check: Commit hooks
  • GitHub Check: Migration Checks
  • GitHub Check: Lint
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Build
  • GitHub Check: Analyze (go)
🔇 Additional comments (14)
openmeter/billing/service/lineservice/pricevolumetiered.go (2)

18-31: Logic for skipping volume-tiered billing on non-last lines is correct
No concerns found. This design aligns with the notion that volume-tiered pricing is billed once at the end of the period.


112-124: Consider additional tests for missing or improperly sorted tiers
The code expects the last tier to have an infinite upper bound (nil). If a price definition lacks this final open-ended tier, the function returns an error. Ensure the test suite covers this scenario thoroughly.

openmeter/billing/service/lineservice/usagebasedline.go (6)

94-102: Behavior on non-invoiceable scenario looks correct.

If the pricer explicitly indicates the line is not invoiceable, returning nil, nil is a clean way to skip further processing.


105-105: Comment matches logic.

This comment clarifies the intent to confirm progressive billing support in downstream logic. No concerns here.


215-215: Invocation of your new calculateDetailedLines() method is properly placed.

This is a straightforward call right after snapshotting. Looks consistent with the surrounding flow.


235-246: Good use of a dedicated helper to build pricer input.

Your asPricerCalculateInput method ensures consistency by validating and returning the required parameters. This fosters clarity and reduces duplication.


247-264: Clean switch-based logic for pricer retrieval.

This approach is easy to extend if more pricing models are introduced. Error handling for unsupported types is also appropriate.


268-280: Modular approach enhances maintainability.

Delegating the calculation to the pricer captures the new architecture’s intent. This function is concise and clear.

openmeter/billing/service/lineservice/usagebasedline_test.go (1)

91-95: Properly assigning usage before calculation.

Directly setting Quantity and PreLinePeriodQuantity on the line clarifies test setup and ensures test logic accurately simulates real usage data.

openmeter/billing/service/lineservice/pricegraduatedtiered.go (5)

1-15: File-level overview.

Introducing a graduated tiered pricer under the lineservice package is consistent with your new modular pricing architecture. The initial definitions (imports, struct, interface conformance) appear clear.


114-121: Straightforward tierRange struct.

Holds the tier metadata and bounding quantities for partial usage in progressive billing. Looks accurate and meets the needed data shape.


123-129: tierCallbackInput captures crucial tier context.

This struct is well-formed for passing partial usage and prior total to the callback function, promoting clarity in tier-based calculations.


131-143: Well-defined tieredPriceCalculatorInput.

Validation fields (e.g. FromQty, ToQty, Currency) and callback wrappers ensure structured data flow. No issues noted.


145-171: Robust validation.

Checks mode, currency, and quantity boundaries. This guards well against invalid tiered pricing setups.

@turip turip merged commit b638ec5 into main Apr 8, 2025
27 checks passed
@turip turip deleted the refactor/billing-pricer-interface branch April 8, 2025 10:46
@coderabbitai coderabbitai bot mentioned this pull request Apr 10, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants