Skip to content

Conversation

@turip
Copy link
Member

@turip turip commented Jan 23, 2026

Overview

This PR is the first step of implementing Gathering Invoice and it's lines Gathering Lines. It changes the upcoming charge creation api to use the new type, while in the background we are still storing to the old database tables. Later changes will also change this behavior.

why? Right now we are using standard lines instead of Gathering Lines which is a validation nightmare: gathering lines have no totals, no quantities while std ones have. The only way to determine if a line is a std line or an upcoming charge is by the invoice.

Summary by CodeRabbit

  • New Features

    • Full support for gathering invoices: create, update, delete, fetch-by-ID, paginated listing, and a gathering-invoice created event.
    • API mapping to return gathering invoices with expanded lines and available actions.
  • Improvements

    • New gathering line/service-period model, deterministic line ordering, separate legacy vs. new calculation paths, and richer invoice expansions.
    • Utility additions: period helpers and metadata cloning.
  • Tests

    • Updated tests and fixtures across suites to cover gathering-invoice flows.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 23, 2026

📝 Walkthrough

Walkthrough

Adds a new GatheringInvoice domain and end-to-end support: public models and converters, adapter CRUD (Create/Update/Delete/Get/List) and line diff/upsert, service APIs for pending-lines and listing, HTTP mapping/events, invoice-calculation split (legacy vs gathering), and widespread test migrations from Standard* → Gathering*.

Changes

Cohort / File(s) Summary
Core models & conversions
openmeter/billing/gatheringinvoice.go, openmeter/billing/stdinvoiceline.go, openmeter/billing/invoiceline.go
Introduce GatheringInvoice/GatheringLine types, ServicePeriod (timeutil.ClosedPeriod), validation/normalize/clone helpers, conversion from StandardLine→GatheringLine, and Period→ClosedPeriod helper.
Adapter interface
openmeter/billing/adapter.go
Add GatheringInvoiceAdapter and embed it into Adapter (Create/Update/Delete/Get/List signatures).
Adapter implementation
openmeter/billing/adapter/gatheringinvoice.go, openmeter/billing/adapter/gatheringlines.go
Implement full gathering-invoice lifecycle plus line diff/upsert, DB mappings, optional expansion, validation, and transactional wrappers.
Service layer
openmeter/billing/service.go, openmeter/billing/service/gatheringinvoice.go, openmeter/billing/service/stdinvoiceline.go, openmeter/billing/service/gatheringinvoicependinglines.go
Add GatheringInvoiceService, move pending-lines flow to gathering model, run adapter calls in transactions, publish gathering events, and refetch expanded lines when restoring/updating.
Invoice calculation
openmeter/billing/service/invoicecalc/*
Split calculators into legacy vs gathering paths, add GatheringCalculation type, separate collection/period functions, add gathering-specific discount upsert and update mocks.
HTTP mapping & input flow
openmeter/billing/httpdriver/gatheringinvoice.go, openmeter/billing/httpdriver/invoiceline.go, openmeter/billing/httpdriver/invoice_test.go
Add MapGatheringInvoiceToAPI, mapping for GatheringLine→api.InvoiceLine, mapCreateGatheringLineToEntity for request→GatheringLine, and update tests to use Gathering constructors.
Events & metadata
openmeter/billing/eventsgathering.go, openmeter/event/metadata/resourcepath.go
Add GatheringInvoiceCreatedEvent and EntityGatheringInvoice metadata constant and helpers.
Worker & sync
openmeter/billing/worker/subscriptionsync/...
Convert subscription-sync outputs to GatheringLine and call CreatePendingInvoiceLines with GatheringLine inputs.
Widespread tests & examples
test/**, test/app/**, test/customer/**
Mass migration in tests from StandardLine/StandardInvoice → GatheringLine/GatheringInvoice, Period→ServicePeriod (ClosedPeriod), replace NewFlatFeeLine→NewFlatFeeGatheringLine, and adjust price construction/assertions.
Utilities & stubs
pkg/timeutil/closedperiod.go, pkg/models/metadata.go, openmeter/server/server_test.go
Add ClosedPeriod.Truncate/Equal, Metadata.Clone(), and NoopBillingService stubs for pending-lines and ListGatheringInvoices.
Generated/derived changes
openmeter/billing/derived.gen.go
Adjust derived equality helpers to new metadata/discount/subscription shapes and update helper signatures.
Minor adapter/profile cleanup
openmeter/billing/adapter/invoice.go, openmeter/billing/adapter/profile.go
Remove redundant workflowConfig remapping and delete mapWorkflowConfigToDB helper.
Small API/document comment
api/spec/src/billing/invoices.tsp
Inserted comment in InvoicePendingLineCreateResponse noting invoice omission for V3 API (comment-only, no signature change).

Sequence Diagram(s)

sequenceDiagram
    rect rgba(200,220,255,0.5)
    participant Client as API Client
    participant Service as GatheringInvoice Service
    participant Adapter as Billing Adapter
    participant DB as Database
    participant EventBus as Event Bus
    end

    Client->>Service: CreatePendingInvoiceLines(ctx, input)
    Service->>Service: Validate input
    Service->>Adapter: CreateGatheringInvoice(ctx, input) (transaction)
    Adapter->>DB: Insert/Upsert BillingInvoice + BillingInvoiceLines
    Adapter->>Adapter: mapGatheringInvoiceFromDB(...)
    Adapter-->>Service: GatheringInvoice result
    Service->>EventBus: Publish GatheringInvoiceCreatedEvent
    Service-->>Client: CreatePendingInvoiceLinesResult {Lines, Invoice}
Loading
sequenceDiagram
    rect rgba(200,240,200,0.5)
    participant Client as API Client
    participant Service as GatheringInvoice Service
    participant Adapter as Billing Adapter
    participant DB as Database
    end

    Client->>Service: ListGatheringInvoices(ctx, input)
    Service->>Service: Validate input
    Service->>Adapter: ListGatheringInvoices(ctx, input) (transaction)
    Adapter->>DB: Query BillingInvoices (filters + pagination)
    Adapter->>DB: Fetch lines if expand requested
    Adapter->>Adapter: mapGatheringInvoiceFromDB for each row
    Adapter-->>Service: pagination.Result[GatheringInvoice]
    Service-->>Client: Paginated gathering invoices
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • tothandras
  • chrisgacsal
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: introducing gathering invoice implementation as a significant refactoring effort across the billing subsystem.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/start-implementing-gathering-invoice

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@turip turip force-pushed the refactor/start-implementing-gathering-invoice branch 3 times, most recently from e1b43c5 to 115ac9c Compare January 29, 2026 14:00
@turip turip force-pushed the refactor/start-implementing-gathering-invoice branch 2 times, most recently from 60744f2 to be3ecfe Compare February 3, 2026 13:31
@turip turip added release-note/misc Miscellaneous changes area/billing labels Feb 4, 2026
@turip turip force-pushed the refactor/start-implementing-gathering-invoice branch from 1951b9a to 607e22e Compare February 4, 2026 08:22
@turip turip marked this pull request as ready for review February 4, 2026 08:22
@turip turip requested a review from a team as a code owner February 4, 2026 08:22
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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openmeter/billing/httpdriver/invoiceline.go (1)

46-118: ⚠️ Potential issue | 🟠 Major

Guard against nil CreatePendingInvoiceLines results.
CreatePendingInvoiceLines can return nil (e.g., zero‑valued lines). The current flow dereferences res immediately, which would panic in that valid case. A quick nil guard before building the response would make this safe.

💡 Suggested guard
res, err := h.service.CreatePendingInvoiceLines(ctx, request)
if err != nil {
	return CreatePendingLineResponse{}, fmt.Errorf("failed to create invoice lines: %w", err)
}
+if res == nil {
+	return CreatePendingLineResponse{}, nil
+}

out := CreatePendingLineResponse{
	IsInvoiceNew: res.IsInvoiceNew,
}
🤖 Fix all issues with AI agents
In `@openmeter/billing/adapter/gatheringinvoice.go`:
- Around line 98-190: UpdateGatheringInvoice is unconditionally calling
SetCollectionAt(in.NextCollectionAt.In(time.UTC)) which will panic or write
year‑0001 when collection_at is NULL; mirror CreateGatheringInvoice and
mapGatheringInvoiceFromDB by preserving nullable behavior: guard the nullable
NextCollectionAt by either calling SetNillableCollectionAt(...) with nil when
in.NextCollectionAt is unset or only invoke SetCollectionAt(...) when the
NextCollectionAt value is present, and ensure the same pattern is applied in the
other affected block (the second occurrence noted around lines 353-406); update
references in UpdateGatheringInvoice, CreateGatheringInvoice and
mapGatheringInvoiceFromDB accordingly.

In `@openmeter/billing/gatheringinvoice.go`:
- Around line 343-361: UpcomingChargeBase.Clone currently does a shallow copy
with out := i which leaves the Metadata map shared; update
UpcomingChargeBase.Clone to deep-copy the Metadata field (e.g., use maps.Clone
or equivalent) after copying annotations and before returning so the cloned
UpcomingChargeBase has its own map instance; keep the existing cloning of
Annotations (i.Annotations.Clone()), TaxConfig, and Subscription logic intact
and apply maps.Clone to i.Metadata into out.Metadata to prevent mutations on the
clone from affecting the original.

In `@openmeter/billing/service/invoicecalc/period.go`:
- Around line 10-12: The top-of-file doc comment still references the old
function name; update it to match the renamed function
CalculateStandardInvoiceServicePeriod by changing the comment from
"CalculateStandardInvoicePeriod calculates the period of the invoice based on
the lines." to a correct description that starts with
CalculateStandardInvoiceServicePeriod (e.g.,
"CalculateStandardInvoiceServicePeriod calculates the service period of the
invoice based on the lines."), ensuring the comment noun and verb agree with the
new function name and purpose.
🧹 Nitpick comments (5)
api/spec/src/billing/invoices.tsp (1)

709-709: Avoid TODO comments in API specs—use deprecation or external tracking.

This TODO comment will likely leak into generated client SDKs and documentation, creating confusion for API consumers. Since the invoice field is still present and functional, consider one of these alternatives:

  1. If deprecating soon: Use the @deprecated decorator with a message:

    `@deprecated`("This field will be removed in a future API version")
    `@visibility`(Lifecycle.Read)
    invoice: Invoice;
  2. If this is longer-term: Remove the comment from the spec and track it in your issue management system instead.

  3. If V3 is a distinct API version: Consider creating version-specific response models when that work begins.

As per coding guidelines, the API should be easy to understand for users and multiple codegen toolchains depend on this spec.

openmeter/billing/service/stdinvoiceline.go (1)

60-63: Consider returning an empty result instead of nil.

When len(input.Lines) == 0, returning (nil, nil) means callers must handle a nil result. Returning an empty CreatePendingInvoiceLinesResult (e.g., with an empty Lines slice) might be more ergonomic and prevent potential nil pointer issues downstream.

💡 Suggested change
	return transcationForInvoiceManipulation(ctx, s, input.Customer, func(ctx context.Context) (*billing.CreatePendingInvoiceLinesResult, error) {
		if len(input.Lines) == 0 {
-			return nil, nil
+			return &billing.CreatePendingInvoiceLinesResult{
+				Lines: []billing.UpcomingCharge{},
+			}, nil
		}
openmeter/billing/gatheringinvoice.go (1)

426-436: Default or validate line.Namespace during pending-line validation.
Line 426 only injects currency; if callers omit Namespace (common in tests), UpcomingChargeBase.Validate will fail with “namespace required.” Consider defaulting from Customer.Namespace and rejecting mismatches.

♻️ Suggested tweak
 	for id, line := range c.Lines {
 		// Note: this is for validation purposes, as Line is copied, we are not altering the struct itself
 		line.Currency = c.Currency
+		if line.Namespace == "" {
+			line.Namespace = c.Customer.Namespace
+		} else if line.Namespace != c.Customer.Namespace {
+			errs = append(errs, fmt.Errorf("line.%d: namespace must match customer namespace", id))
+		}
 
 		if err := line.Validate(); err != nil {
 			errs = append(errs, fmt.Errorf("line.%d: %w", id, err))
 		}
openmeter/billing/service/gatheringinvoicependinglines.go (1)

610-639: Optional: validate GatheringInvoice in input.Validate for symmetry.
It would surface invalid/empty invoices earlier and make error sources clearer.

♻️ Suggested validation addition
 func (in createStandardInvoiceFromUpcomingChargesInput) Validate() error {
 	var errs []error
 
+	if err := in.GatheringInvoice.Validate(); err != nil {
+		errs = append(errs, fmt.Errorf("gathering invoice: %w", err))
+	}
+
 	if err := in.Customer.Validate(); err != nil {
 		errs = append(errs, fmt.Errorf("customer: %w", err))
 	}
openmeter/billing/httpdriver/gatheringinvoice.go (1)

17-60: Consider populating full customer details in the API invoice.
invoiceCustomer only sets key + name right now, so customerId (and possibly billing address / usage attribution) will be empty if clients expect parity with standard invoice responses. If that’s unintentional, wiring those fields from customer would avoid surprises.

@turip turip requested a review from tothandras February 4, 2026 10:08
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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openmeter/billing/gatheringinvoice.go (1)

413-449: ⚠️ Potential issue | 🟠 Major

Currency defaulting in validation doesn’t persist.
line.Currency = c.Currency only updates the loop copy, so the slice entries can still have empty currency. If callers rely on defaulting, this leaves invalid data downstream.

🔧 Proposed fix
-	for id, line := range c.Lines {
-		// Note: this is for validation purposes, as Line is copied, we are not altering the struct itself
-		line.Currency = c.Currency
+	for id := range c.Lines {
+		// Persist default currency on the actual slice element
+		c.Lines[id].Currency = c.Currency
+		line := c.Lines[id]
🤖 Fix all issues with AI agents
In `@openmeter/billing/adapter/gatheringinvoice.go`:
- Around line 141-145: The branch handling NextCollectionAt is inverted: instead
of calling updateQuery.SetCollectionAt when in.NextCollectionAt.IsZero() is
true, swap the branches so that when in.NextCollectionAt.IsZero() you call
updateQuery.ClearCollectionAt(), and when it's non-zero you call
updateQuery.SetCollectionAt(in.NextCollectionAt.In(time.UTC)); update the logic
around in.NextCollectionAt.IsZero(), keeping the same calls to SetCollectionAt
and ClearCollectionAt.

In `@openmeter/billing/adapter/gatheringlines.go`:
- Around line 232-242: The SubscriptionReference construction uses lo.FromPtr on
SubscriptionBillingPeriodFrom/To without nil checks which can produce zero
times; update the logic in gatheringlines.go where dbLine is mapped (the block
that sets line.Subscription and constructs billing.SubscriptionReference) to
validate that dbLine.SubscriptionBillingPeriodFrom and
SubscriptionBillingPeriodTo are non-nil before calling lo.FromPtr — if either is
nil, either skip setting Subscription.BillingPeriod (or set it to nil) or
surface/log an error/return so you don't silently create an invalid
ClosedPeriod; ensure you reference SubscriptionReference,
SubscriptionBillingPeriodFrom, SubscriptionBillingPeriodTo, and the mapping
block for the change.

In `@openmeter/billing/gatheringinvoice.go`:
- Around line 65-134: The Clone method currently does a shallow copy (clone :=
g) which leaves the Metadata map (on GatheringInvoice/GatheringInvoiceBase)
aliased between original and clone; update GatheringInvoice.Clone to deep-copy
the Metadata map (allocate a new map and copy each key/value) into the clone
before returning so mutations on the clone won't affect the original (this will
also fix WithoutDBState since it calls Clone); reference GatheringInvoice.Clone,
GatheringInvoice (and the Metadata field on GatheringInvoiceBase) when making
the change.
🧹 Nitpick comments (5)
openmeter/billing/gatheringinvoice.go (1)

24-63: Consider validating CustomerID / Number here too (if required invariants).
If these fields are meant to be mandatory on a GatheringInvoiceBase, adding checks would keep model validation consistent and avoid partially‑initialized instances slipping through.

openmeter/billing/stdinvoiceline.go (1)

189-221: Consider cloning metadata/annotations to avoid aliasing.
ToGatheringLineBase currently passes through the original maps, so later mutations on the gathering line could leak back into the StandardLine. A defensive copy keeps the conversion isolated.

🔧 Suggested tweak
 func (i StandardLine) ToGatheringLineBase() (GatheringLineBase, error) {
 	if i.UsageBased == nil {
 		return GatheringLineBase{}, errors.New("usage based line is required")
 	}
 
 	if i.UsageBased.Price == nil {
 		return GatheringLineBase{}, errors.New("usage based line price is required")
 	}
+
+	metadata, err := models.Metadata(i.Metadata).Clone()
+	if err != nil {
+		return GatheringLineBase{}, fmt.Errorf("cloning metadata: %w", err)
+	}
+
+	annotations, err := i.Annotations.Clone()
+	if err != nil {
+		return GatheringLineBase{}, fmt.Errorf("cloning annotations: %w", err)
+	}
 
 	return GatheringLineBase{
 		ManagedResource: i.ManagedResource,
-		Metadata:        i.Metadata,
-		Annotations:     i.Annotations,
+		Metadata:        metadata,
+		Annotations:     annotations,
test/billing/collection_test.go (1)

93-93: Minor inconsistency in price construction pattern.

Here you're using lo.FromPtr(productcatalog.NewPriceFrom(...)) which is good. However, at lines 345 and 518, the code uses *productcatalog.NewPriceFrom(...) (direct dereference). Both work, but lo.FromPtr is safer if NewPriceFrom could ever return nil. Consider standardizing on one approach for consistency.

📝 Suggested standardization
- Price: *productcatalog.NewPriceFrom(productcatalog.UnitPrice{Amount: alpacadecimal.NewFromFloat(1)}),
+ Price: lo.FromPtr(productcatalog.NewPriceFrom(productcatalog.UnitPrice{Amount: alpacadecimal.NewFromFloat(1)})),
openmeter/billing/adapter/gatheringinvoice.go (1)

151-160: Hardcoded placeholder values for supplier fields.

Setting SupplierName to "UNSET" and SupplierAddressCountry to "XX" as hardcoded placeholders works as a temporary hack, but these should ideally be documented or have a constant defined. The comments mention "Hack until we split the invoices table" which is helpful.

Consider defining constants for these placeholder values to make them easier to find and update later:

const (
    gatheringInvoicePlaceholderSupplierName    = "UNSET"
    gatheringInvoicePlaceholderCountry         = "XX"
)
openmeter/billing/service/stdinvoiceline.go (1)

24-24: Tracking the TODO.

Good to see the TODO comment for future organization. Moving this to gatheringinvoice.go would help keep the gathering invoice logic together.

Would you like me to open an issue to track moving this function to gatheringinvoice.go?

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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openmeter/billing/httpdriver/invoiceline.go (1)

86-113: ⚠️ Potential issue | 🟠 Major

Avoid post‑create failures that can cause duplicate pending lines.
CreatePendingInvoiceLines is a non‑idempotent write. If GetCustomerOverride (or subsequent mapping) fails afterward, the handler returns an error even though lines were persisted, so a client retry can duplicate data. Fetching the override before the write (or treating mapping failure as best‑effort) keeps the operation atomic from the caller’s view.

🧭 One way to make the flow safer
-	res, err := h.service.CreatePendingInvoiceLines(ctx, request)
-	if err != nil {
-		return CreatePendingLineResponse{}, fmt.Errorf("failed to create invoice lines: %w", err)
-	}
-
-	if res == nil {
-		return CreatePendingLineResponse{}, fmt.Errorf("create pending invoice lines result is nil")
-	}
-
-	out := CreatePendingLineResponse{
-		IsInvoiceNew: res.IsInvoiceNew,
-	}
-
-	mergedProfile, err := h.service.GetCustomerOverride(ctx, billing.GetCustomerOverrideInput{
+	mergedProfile, err := h.service.GetCustomerOverride(ctx, billing.GetCustomerOverrideInput{
 		Customer: request.Customer,
 		Expand: billing.CustomerOverrideExpand{
 			Customer: true,
 			Apps:     true,
 		},
 	})
 	if err != nil {
 		return CreatePendingLineResponse{}, fmt.Errorf("failed to get customer override: %w", err)
 	}
+
+	res, err := h.service.CreatePendingInvoiceLines(ctx, request)
+	if err != nil {
+		return CreatePendingLineResponse{}, fmt.Errorf("failed to create invoice lines: %w", err)
+	}
+
+	if res == nil {
+		return CreatePendingLineResponse{}, fmt.Errorf("create pending invoice lines result is nil")
+	}
+
+	out := CreatePendingLineResponse{
+		IsInvoiceNew: res.IsInvoiceNew,
+	}
🤖 Fix all issues with AI agents
In `@openmeter/billing/gatheringinvoice.go`:
- Around line 280-331: Add a validation in GatheringLineBase.Validate to require
FeatureKey when the price is not flat: detect non‑flat prices using the same
predicate used in UsageBasedLine.Validate (e.g., Price.IsFlat() returning false
or the Price billing model check used there) and if the price is non‑flat ensure
i.FeatureKey is non‑nil/non‑empty, appending an error like "feature key is
required for non‑flat price" to errs; reference GatheringLineBase.Validate,
Price (and its IsFlat or billing model check) and the FeatureKey field to locate
where to add this guard.

In `@openmeter/billing/stdinvoiceline.go`:
- Around line 189-228: The ToGatheringLineBase method currently clones Metadata
and Annotations but returns TaxConfig, Subscription, and RateCardDiscounts by
reference which can cause later mutations to bleed back; update
ToGatheringLineBase to produce deep copies of i.TaxConfig, i.Subscription and
i.RateCardDiscounts (e.g., call existing Clone/DeepCopy helpers or create
shallow-copy constructors if no helpers exist) and use those copies in the
returned GatheringLineBase (handle nils and any clone errors similarly to
Annotations.Clone and propagate errors where appropriate).
🧹 Nitpick comments (1)
openmeter/billing/gatheringinvoice.go (1)

39-63: Consider validating CustomerID and Number in GatheringInvoiceBase.Validate.
CreateGatheringInvoiceAdapterInput.Validate already requires both, so mirroring the checks here keeps invariants consistent and prevents partially‑populated invoices from slipping through.

🔧 Possible validation add‑on
 	if g.Namespace == "" {
 		errs = append(errs, errors.New("namespace is required"))
 	}
+
+	if g.Number == "" {
+		errs = append(errs, errors.New("number is required"))
+	}
+
+	if g.CustomerID == "" {
+		errs = append(errs, errors.New("customerID is required"))
+	}

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

🤖 Fix all issues with AI agents
In `@openmeter/billing/adapter/gatheringinvoice.go`:
- Around line 269-289: The validator must block attempts to update a record that
is not already a gathering invoice: in validateUpdateGatheringInvoiceRequest,
add a check that the existing invoice's type/status indicates it is a gathering
invoice (e.g., ensure existing.Type == billing.InvoiceTypeGathering or
existing.Status == billing.InvoiceStatusGathering depending on your model) and
return a billing.ValidationError (message like "cannot update non-gathering
invoice") if it is not; keep the existing Currency, Type, and CustomerID checks
but perform this new guard early using the validateUpdateGatheringInvoiceRequest
function and the existing variable.

@turip turip merged commit 590ed8a into main Feb 4, 2026
25 of 26 checks passed
@turip turip deleted the refactor/start-implementing-gathering-invoice branch February 4, 2026 15:57
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