Skip to content

feat(api): credit grants handler#4075

Open
tothandras wants to merge 2 commits intomainfrom
feat/api-v3-credit-grants
Open

feat(api): credit grants handler#4075
tothandras wants to merge 2 commits intomainfrom
feat/api-v3-credit-grants

Conversation

@tothandras
Copy link
Copy Markdown
Contributor

@tothandras tothandras commented Apr 2, 2026

Summary by CodeRabbit

  • New Features

    • Added POST endpoint to create customer credit adjustments and a new credit-adjustment payload.
    • Added endpoints to create, list, and retrieve customer credit grants.
  • Bug Fixes

    • Removed the void-credit-grant operation from the public API.
    • Removed effective/expiration timing fields from credit grant payloads and reduced available grant availability options.

@tothandras tothandras added the release-note/feature Release note: Exciting New Features label Apr 2, 2026
@tothandras tothandras requested a review from a team as a code owner April 2, 2026 16:56
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

📝 Walkthrough

Walkthrough

Adds a CreditAdjustment model and POST /openmeter/customers/{customerId}/credits/adjustments endpoint, implements credit-grant domain/service and handlers (create/get/list), removes the void-credit-grant operation and grant timing fields, and wires the CreditGrantService through the app and router.

Changes

Cohort / File(s) Summary
TypeSpec API & Routes
api/spec/packages/aip/src/customers/credits/adjustment.tsp, api/spec/packages/aip/src/customers/credits/grant.tsp, api/spec/packages/aip/src/customers/credits/operations.tsp, api/spec/packages/aip/src/konnect.tsp, api/spec/packages/aip/src/openmeter.tsp
Added CreditAdjustment model (currency, amount). Removed grant timing fields (effective_at, expires_after, expires_at). Added CustomerCreditAdjustmentsOperations and endpoint route; commented out voidGrant route.
Generated API code & types
api/v3/api.gen.go, api/types/entitlement.go
Generated changes: BillingCreditAdjustment and create-adjustment request types added; grant timing fields removed from BillingCreditGrant; VoidCreditGrant endpoint removed; entitlement JSON type constants deleted.
API Handlers & Converters
api/v3/handlers/customers/credits/convert.go, api/v3/handlers/customers/credits/create_grant.go, api/v3/handlers/customers/credits/get_grant.go, api/v3/handlers/customers/credits/list_grants.go, api/v3/handlers/customers/credits/get_balance.go, api/v3/handlers/customers/credits/handler.go
Added handlers for Create/Get/List credit grants, converter utilities (including ConvertMetadataToLabels, convertBalance), updated balance handler to use convertBalance, and wired creditGrantService into handler constructor.
Domain service: creditgrant
openmeter/billing/creditgrant/service.go, openmeter/billing/creditgrant/service/service.go
Introduced creditgrant.Service interface and implementation: Create/Get/List, input types (CreateInput/GetInput/ListInput), FundingMethod, PurchaseTerms, validation, and mapping to credit-purchase intents/settlements.
Wiring & DI
app/common/creditgrant.go, cmd/server/wire.go, cmd/server/wire_gen.go, cmd/server/main.go
Added Wire provider and NewCreditGrantService constructor; threaded CreditGrantService into application initialization and main router config.
Server/router updates
openmeter/server/router/router.go, openmeter/server/server.go, api/v3/server/routes.go, api/v3/server/server.go
Added CreditGrantService to router/server configs; registered CreateCreditAdjustment route; removed VoidCreditGrant route and associated handler wiring.
Ledger & adapter changes
openmeter/ledger/chargeadapter/creditpurchase.go
Added settlementCostBasis helper and refactored cost-basis extraction for credit-purchase flows; updated error wraps and transaction templates.
Other
api/v3/handlers/customers/credits/convert.go (conversions added), api/v3/handlers/customers/credits/* (many new files)
Conversion helpers and handler wiring for grants and balances; introduced exported ConvertMetadataToLabels.

Sequence Diagram

sequenceDiagram
    participant Client
    participant API_Handler as CreateCreditAdjustment<br/>(Handler)
    participant CreditGrantSvc as CreditGrantService
    participant BillingRegistry as BillingRegistry<br/>(CreditPurchase)
    participant Storage as Charge Storage

    Client->>API_Handler: POST /customers/{id}/credits/adjustments (currency, amount,...)
    API_Handler->>API_Handler: validate request, resolve namespace
    API_Handler->>CreditGrantSvc: Create(CreateInput)
    CreditGrantSvc->>CreditGrantSvc: validate input, verify customer
    CreditGrantSvc->>BillingRegistry: Create CreditPurchase.Intent
    BillingRegistry->>Storage: persist charge
    Storage-->>BillingRegistry: stored charge record
    BillingRegistry-->>CreditGrantSvc: creditpurchase.Charge
    CreditGrantSvc-->>API_Handler: creditpurchase.Charge
    API_Handler->>API_Handler: convert to API model
    API_Handler-->>Client: 201 Created (BillingCreditAdjustment)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

area/api

Suggested reviewers

  • GAlexIHU
  • turip
🚥 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 'feat(api): credit grants handler' clearly and concisely summarizes the main change—adding credit grants handler functionality to the API. It's specific, follows conventional commit format, and accurately reflects the primary purpose of this changeset.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/api-v3-credit-grants

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.

Copy link
Copy Markdown
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: 6

Caution

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

⚠️ Outside diff range comments (1)
api/v3/api.gen.go (1)

3147-3212: ⚠️ Potential issue | 🟡 Minor

The grant lifecycle docs are now out of sync with the public schema.

This request no longer exposes timing fields, but the generated BillingCreditGrantStatus docs still describe pending / active / expired in terms of effective_at and expires_at. That makes the published contract confusing for SDK consumers. Please fix the source spec comments and regenerate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v3/api.gen.go` around lines 3147 - 3212, The generated docs for
BillingCreditGrantStatus are out-of-date because CreateCreditGrantRequest no
longer exposes timing fields (effective_at, expires_at); update the source
OpenAPI/spec comments for BillingCreditGrantStatus (and any related schema
comments) to remove references to effective_at/expires_at and instead describe
the new lifecycle semantics (e.g., how pending/active/expired are determined
without those fields), then regenerate api.gen.go so the comment block above
type BillingCreditGrantStatus and any references in
CreateCreditGrantRequest/Purchase/TaxConfig match the current public schema.
🧹 Nitpick comments (5)
openmeter/billing/creditgrant/service.go (1)

97-105: Avoid silently accepting unused purchase terms

On Line 97-Line 105, Purchase is allowed even when FundingMethod is none, but those fields are later ignored. I’d recommend rejecting that combination to keep API behavior explicit and predictable.

Suggested validation tweak
 if i.FundingMethod != FundingMethodNone && i.Purchase == nil {
 	errs = append(errs, errors.New("purchase terms are required for funded grants"))
 }
+
+if i.FundingMethod == FundingMethodNone && i.Purchase != nil {
+	errs = append(errs, errors.New("purchase terms must be empty when funding method is none"))
+}
 
 if i.Purchase != nil {
 	if err := i.Purchase.Currency.Validate(); err != nil {
 		errs = append(errs, fmt.Errorf("purchase currency: %w", err))
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/creditgrant/service.go` around lines 97 - 105, The
validation currently allows i.Purchase when i.FundingMethod == FundingMethodNone
and later ignores those fields; update the validation in the same function
(where i is validated) to reject the combination by appending an error when
i.FundingMethod == FundingMethodNone && i.Purchase != nil (e.g., "purchase terms
not allowed for non-funded grants"), and keep the existing currency validation
but guard it so it only runs when i.Purchase != nil and i.FundingMethod !=
FundingMethodNone to avoid validating ignored fields.
openmeter/server/router/router.go (1)

138-248: Add an explicit credits-enabled guard for CreditGrantService

Since CreditGrantService is now part of Config, it’d be safer to enforce: if credits are enabled, the service must be non-nil. That makes failures deterministic at startup instead of later in request handling.

Based on learnings Ensure credits.enabled setting is explicitly guarded at multiple layers: ledger-backed customer credit handlers in api/v3/server, customer ledger hooks, and namespace/default-account provisioning; these are wired separately and must each stay disabled when credits are off

Suggested guard in config validation
 func (c Config) Validate() error {
+	if c.Credits.Enabled && c.CreditGrantService == nil {
+		return errors.New("credit grant service is required when credits are enabled")
+	}
+
 	if c.NamespaceManager == nil {
 		return errors.New("namespace manager is required")
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/server/router/router.go` around lines 138 - 248, Config.Validate
must explicitly guard CreditGrantService when credits are enabled: add a check
in Config.Validate that returns an error if the credits.enabled flag is true and
c.CreditGrantService is nil. Implement this by reading your config flag (e.g., a
boolean field like c.CreditsEnabled or a nested c.Credits.Enabled) and if it is
true and CreditGrantService == nil, return errors.New("credit grant service is
required when credits are enabled"); keep the check near the other
connector/service validations in the Validate() method.
api/spec/packages/aip/src/customers/credits/adjustment.tsp (1)

16-23: Clarify adjustment sign semantics in the field docs

Tiny docs nit: on Line 16 and Line 22, “granted” reads like only positive grants. For an adjustment endpoint, consider wording like “adjusted amount” and (if true) explicitly state positive vs negative behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/spec/packages/aip/src/customers/credits/adjustment.tsp` around lines 16 -
23, Update the field JSDoc to clarify sign semantics: change "The currency of
the granted credits." (currency: Currencies.CurrencyCode) to indicate it's the
currency for the adjustment (e.g., "The currency of the adjusted credits."), and
replace "Granted credit amount." on the adjustment amount field with wording
like "Adjusted amount; positive values grant credits, negative values deduct
credits." Ensure you update only the documentation comments for the relevant
fields (currency and the adjustment amount) in the adjustment.tsp model.
api/spec/packages/aip/src/customers/credits/grant.tsp (1)

258-279: Consider removing (not commenting) deprecated contract fields.

Keeping full commented-out schema blocks in the TypeSpec source makes the contract harder to read and maintain. A short changelog note/PR reference is usually cleaner than inline commented definitions.

As per coding guidelines: "Review the TypeSpec code for conformity with TypeSpec best practices."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/spec/packages/aip/src/customers/credits/grant.tsp` around lines 258 -
279, Remove the commented-out deprecated schema fields (effective_at,
expires_after, expires_at) from the TypeSpec source so the contract is not
cluttered with dead code; delete the entire commented blocks that reference
those symbols and, if needed, add a short changelog/PR note near the top of the
file or in the PR description documenting the removal rather than leaving inline
commented definitions.
api/spec/packages/aip/src/customers/credits/operations.tsp (1)

81-98: Can we remove the old void operation instead of parking it as comments?

Keeping a full removed endpoint in the TypeSpec source gets stale fast and makes the contract a bit harder to trust. I’d just delete it and let git/history preserve the old shape.

As per coding guidelines, "Define API specifications using TypeSpec in api/spec/ as the source of truth for API contracts, then regenerate OpenAPI and SDKs with make gen-api."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/spec/packages/aip/src/customers/credits/operations.tsp` around lines 81 -
98, Remove the commented-out legacy endpoint block for
CustomerCreditGrantVoidOperations (the commented interface and its voidGrant
method including `@post/`@operationId/@summary annotations) from the TypeSpec file
so the API spec no longer contains stale commented endpoints; rely on git
history to retain the previous shape and then run the usual generation step
(make gen-api) to update derived artifacts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/v3/handlers/customers/credits/convert.go`:
- Around line 52-60: The convertGrantStatus function currently maps only
"created" and "active" and defaults everything else to
api.BillingCreditGrantStatusActive; update
convertGrantStatus(creditpurchase.Charge) to explicitly map all known domain
statuses (at minimum "active", "created", and "voided") to their corresponding
api.BillingCreditGrantStatus values and stop silently defaulting unknown strings
to Active — change the function to return (api.BillingCreditGrantStatus, error)
(or otherwise return a clearly invalid/unknown enum value) and return an error
for any unrecognized charge.Status so callers fail closed instead of
misreporting unknown states.
- Around line 244-256: The parsed per_unit_cost_basis must be validated as >0;
update validation to reject zero/negative values by adding an IsPositive() check
after alpacadecimal.NewFromString(...) and setting purchase.PerUnitCostBasis (or
centralize it in creditgrant.CreateInput.Validate()); specifically, in the
handler around the block that parses body.Purchase.PerUnitCostBasis ensure you
call costBasis.IsPositive() (or equivalent) and return an error like
"per_unit_cost_basis must be positive" if false, or alternatively add the same
positive check inside creditgrant.CreateInput.Validate() so CreateInput and
purchase.PerUnitCostBasis cannot be zero/negative.

In `@api/v3/handlers/customers/credits/get_grant.go`:
- Around line 17-20: GetCreditGrantParams currently includes CustomerID but that
value isn't used: update the handler that builds creditgrant.GetInput (and
related lookups used by GetCreditGrant handler) to pass args.CustomerID into the
service query or, after fetching the grant via creditgrant.GetInput (which
currently uses Namespace + ChargeID), add an ownership check comparing the
returned grant's CustomerID to args.CustomerID and return a not-found/forbidden
error if they differ; specifically modify where creditgrant.GetInput is
constructed and where the service returns the grant to either include CustomerID
in the input or validate ownership before returning in the GetCreditGrant
handler (also apply the same fix to the other similar handler referenced around
lines 32-35).

In `@api/v3/server/routes.go`:
- Around line 372-374: The CreateCreditAdjustment handler currently delegates to
unimplemented.CreateCreditAdjustment, exposing a 501 for the public API; either
remove/hide the route from the published spec or implement the handler now:
replace the unimplemented call in Server.CreateCreditAdjustment with the real
logic (e.g., parse request body, validate customerId, call your service/repo
method such as s.Service.CreateCreditAdjustment or s.AdjustmentStore.Create,
handle errors and write appropriate HTTP responses), or if staged, remove the
public registration so the contract matches implementation.
- Around line 337-347: The grant route currently only checks
s.customersCreditsHandler but can still panic if the CreditGrantService is nil;
update the guard in ListCreditGrants (and the other grant handlers around the
same area) to require both s.customersCreditsHandler != nil AND
s.CreditGrantService != nil before calling
customersCreditsHandler.ListCreditGrants().With(...).ServeHTTP; if either is
nil, call unimplemented.ListCreditGrants(w, r, customerId, params) (or the
corresponding unimplemented handler) and return. Ensure you apply the same
pattern to the other grant routes referenced (the blocks around lines 349–358
and 360–370) so grant-specific handlers are gated separately from balance
handlers.

In `@openmeter/billing/creditgrant/service/service.go`:
- Around line 126-128: The current call to s.chargesService.ListCharges(ctx,
listInput) ignores the FundingMethod, Status, and Currency filters; add a
fail-fast guard before invoking ListCharges that validates listInput (check the
fields named FundingMethod, Status, and Currency on the request/listInput struct
used by service.go) and return a clear error when any of these filters are set
(non-nil or non-empty) so callers are not silently given unfiltered results;
keep the TODO comment and replace the guard with real filtering logic once
ListCharges supports those filters.

---

Outside diff comments:
In `@api/v3/api.gen.go`:
- Around line 3147-3212: The generated docs for BillingCreditGrantStatus are
out-of-date because CreateCreditGrantRequest no longer exposes timing fields
(effective_at, expires_at); update the source OpenAPI/spec comments for
BillingCreditGrantStatus (and any related schema comments) to remove references
to effective_at/expires_at and instead describe the new lifecycle semantics
(e.g., how pending/active/expired are determined without those fields), then
regenerate api.gen.go so the comment block above type BillingCreditGrantStatus
and any references in CreateCreditGrantRequest/Purchase/TaxConfig match the
current public schema.

---

Nitpick comments:
In `@api/spec/packages/aip/src/customers/credits/adjustment.tsp`:
- Around line 16-23: Update the field JSDoc to clarify sign semantics: change
"The currency of the granted credits." (currency: Currencies.CurrencyCode) to
indicate it's the currency for the adjustment (e.g., "The currency of the
adjusted credits."), and replace "Granted credit amount." on the adjustment
amount field with wording like "Adjusted amount; positive values grant credits,
negative values deduct credits." Ensure you update only the documentation
comments for the relevant fields (currency and the adjustment amount) in the
adjustment.tsp model.

In `@api/spec/packages/aip/src/customers/credits/grant.tsp`:
- Around line 258-279: Remove the commented-out deprecated schema fields
(effective_at, expires_after, expires_at) from the TypeSpec source so the
contract is not cluttered with dead code; delete the entire commented blocks
that reference those symbols and, if needed, add a short changelog/PR note near
the top of the file or in the PR description documenting the removal rather than
leaving inline commented definitions.

In `@api/spec/packages/aip/src/customers/credits/operations.tsp`:
- Around line 81-98: Remove the commented-out legacy endpoint block for
CustomerCreditGrantVoidOperations (the commented interface and its voidGrant
method including `@post/`@operationId/@summary annotations) from the TypeSpec file
so the API spec no longer contains stale commented endpoints; rely on git
history to retain the previous shape and then run the usual generation step
(make gen-api) to update derived artifacts.

In `@openmeter/billing/creditgrant/service.go`:
- Around line 97-105: The validation currently allows i.Purchase when
i.FundingMethod == FundingMethodNone and later ignores those fields; update the
validation in the same function (where i is validated) to reject the combination
by appending an error when i.FundingMethod == FundingMethodNone && i.Purchase !=
nil (e.g., "purchase terms not allowed for non-funded grants"), and keep the
existing currency validation but guard it so it only runs when i.Purchase != nil
and i.FundingMethod != FundingMethodNone to avoid validating ignored fields.

In `@openmeter/server/router/router.go`:
- Around line 138-248: Config.Validate must explicitly guard CreditGrantService
when credits are enabled: add a check in Config.Validate that returns an error
if the credits.enabled flag is true and c.CreditGrantService is nil. Implement
this by reading your config flag (e.g., a boolean field like c.CreditsEnabled or
a nested c.Credits.Enabled) and if it is true and CreditGrantService == nil,
return errors.New("credit grant service is required when credits are enabled");
keep the check near the other connector/service validations in the Validate()
method.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 943aa1d2-1be4-43f8-8813-fcd427e160b4

📥 Commits

Reviewing files that changed from the base of the PR and between fe83424 and 66486a7.

⛔ Files ignored due to path filters (1)
  • api/v3/openapi.yaml is excluded by !**/openapi.yaml
📒 Files selected for processing (23)
  • api/spec/packages/aip/src/customers/credits/adjustment.tsp
  • api/spec/packages/aip/src/customers/credits/grant.tsp
  • api/spec/packages/aip/src/customers/credits/operations.tsp
  • api/spec/packages/aip/src/konnect.tsp
  • api/spec/packages/aip/src/openmeter.tsp
  • api/types/entitlement.go
  • api/v3/api.gen.go
  • api/v3/handlers/customers/credits/convert.go
  • api/v3/handlers/customers/credits/create_grant.go
  • api/v3/handlers/customers/credits/get_balance.go
  • api/v3/handlers/customers/credits/get_grant.go
  • api/v3/handlers/customers/credits/handler.go
  • api/v3/handlers/customers/credits/list_grants.go
  • api/v3/server/routes.go
  • api/v3/server/server.go
  • app/common/creditgrant.go
  • cmd/server/main.go
  • cmd/server/wire.go
  • cmd/server/wire_gen.go
  • openmeter/billing/creditgrant/service.go
  • openmeter/billing/creditgrant/service/service.go
  • openmeter/server/router/router.go
  • openmeter/server/server.go
💤 Files with no reviewable changes (1)
  • api/types/entitlement.go

Comment on lines +17 to +20
GetCreditGrantParams struct {
CustomerID api.ULID
CreditGrantID api.ULID
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Please scope this lookup by customerId.

args.CustomerID is never used, and creditgrant.GetInput only carries Namespace + ChargeID (openmeter/billing/creditgrant/service.go:116-133). Right now the same grant can be fetched through any customer path inside the namespace, so the customer-scoped route isn’t actually enforced. Please either thread CustomerID into the service lookup or verify ownership before returning the grant.

Also applies to: 32-35

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v3/handlers/customers/credits/get_grant.go` around lines 17 - 20,
GetCreditGrantParams currently includes CustomerID but that value isn't used:
update the handler that builds creditgrant.GetInput (and related lookups used by
GetCreditGrant handler) to pass args.CustomerID into the service query or, after
fetching the grant via creditgrant.GetInput (which currently uses Namespace +
ChargeID), add an ownership check comparing the returned grant's CustomerID to
args.CustomerID and return a not-found/forbidden error if they differ;
specifically modify where creditgrant.GetInput is constructed and where the
service returns the grant to either include CustomerID in the input or validate
ownership before returning in the GetCreditGrant handler (also apply the same
fix to the other similar handler referenced around lines 32-35).

Comment on lines +372 to 374
func (s *Server) CreateCreditAdjustment(w http.ResponseWriter, r *http.Request, customerId api.ULID) {
unimplemented.CreateCreditAdjustment(w, r, customerId)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

CreateCreditAdjustment is publicly exposed but always returns 501.

Line 373 hardwires this endpoint to unimplemented, so the new API contract is advertised without functional behavior. If this is intentionally staged, consider hiding it from the public spec until wiring lands, or wire the handler in this PR.

As per coding guidelines: "The declared API should be accurate, in parity with the actual implementation, and easy to understand for the user."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v3/server/routes.go` around lines 372 - 374, The CreateCreditAdjustment
handler currently delegates to unimplemented.CreateCreditAdjustment, exposing a
501 for the public API; either remove/hide the route from the published spec or
implement the handler now: replace the unimplemented call in
Server.CreateCreditAdjustment with the real logic (e.g., parse request body,
validate customerId, call your service/repo method such as
s.Service.CreateCreditAdjustment or s.AdjustmentStore.Create, handle errors and
write appropriate HTTP responses), or if staged, remove the public registration
so the contract matches implementation.

Comment on lines +126 to +128
// TODO: apply FundingMethod, Status, Currency filters once the charges list supports them

result, err := s.chargesService.ListCharges(ctx, listInput)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Filters are accepted but ignored in list results

Line 126-Line 128 currently drops FundingMethod, Status, and Currency filters. That means clients can send valid filters and still get unfiltered results, which is a behavior bug.

If upstream filtering support isn’t ready yet, it’s safer to fail fast when any of these filters are set (instead of silently ignoring them).

Fail-fast guard until filtering is wired
 func (s *service) List(ctx context.Context, input creditgrant.ListInput) (pagination.Result[creditpurchase.Charge], error) {
 	if err := input.Validate(); err != nil {
 		return pagination.Result[creditpurchase.Charge]{}, fmt.Errorf("invalid input: %w", err)
 	}
+
+	if input.FundingMethod != nil || input.Status != nil || input.Currency != nil {
+		return pagination.Result[creditpurchase.Charge]{}, fmt.Errorf("list filters are not supported yet")
+	}
 
 	listInput := charges.ListChargesInput{
 		Page:        input.Page,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// TODO: apply FundingMethod, Status, Currency filters once the charges list supports them
result, err := s.chargesService.ListCharges(ctx, listInput)
func (s *service) List(ctx context.Context, input creditgrant.ListInput) (pagination.Result[creditpurchase.Charge], error) {
if err := input.Validate(); err != nil {
return pagination.Result[creditpurchase.Charge]{}, fmt.Errorf("invalid input: %w", err)
}
if input.FundingMethod != nil || input.Status != nil || input.Currency != nil {
return pagination.Result[creditpurchase.Charge]{}, fmt.Errorf("list filters are not supported yet")
}
// TODO: apply FundingMethod, Status, Currency filters once the charges list supports them
result, err := s.chargesService.ListCharges(ctx, listInput)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/creditgrant/service/service.go` around lines 126 - 128, The
current call to s.chargesService.ListCharges(ctx, listInput) ignores the
FundingMethod, Status, and Currency filters; add a fail-fast guard before
invoking ListCharges that validates listInput (check the fields named
FundingMethod, Status, and Currency on the request/listInput struct used by
service.go) and return a clear error when any of these filters are set (non-nil
or non-empty) so callers are not silently given unfiltered results; keep the
TODO comment and replace the guard with real filtering logic once ListCharges
supports those filters.

Copy link
Copy Markdown
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

Caution

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

⚠️ Outside diff range comments (1)
api/v3/api.gen.go (1)

310-323: ⚠️ Potential issue | 🟡 Minor

Enum docs and actual values are out of sync

Nice cleanup on the enum values, but the type docs still mention removed values (on_authorization, on_settlement) at Line 1809 and Line 1810. That can mislead SDK/API consumers.

Please fix this in the OpenAPI/TypeSpec source and regenerate this file (rather than editing this file directly).

As per coding guidelines, all generated files must have // Code generated by X, DO NOT EDIT. headers at the top; never edit files bearing this header.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v3/api.gen.go` around lines 310 - 323, The generated enum docs for
BillingCreditAvailabilityPolicy are stale—remove the references to the removed
values ("on_authorization", "on_settlement") from the OpenAPI/TypeSpec source
where BillingCreditAvailabilityPolicy is defined, ensure the enum definition
only contains the current member (BillingCreditAvailabilityPolicyOnCreation) and
update its description accordingly, then regenerate api.gen.go so the docblock
and enum values stay in sync and the file contains the required "// Code
generated by ..." header; do not edit api.gen.go directly.
🧹 Nitpick comments (2)
api/v3/handlers/customers/credits/convert.go (1)

134-143: Handle all payment.Status values explicitly for robustness.

The code does sensibly default unknown payment statuses to Pending, but you might want to add payment.StatusOpen (or similar) to the switch if it exists in the domain. Currently:

  • Authorized → Authorized
  • Settled → Settled
  • Everything else → Pending

If other statuses exist (e.g., StatusFailed, StatusOpen), they'd silently become Pending.

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

144-186: ListInput validation looks good, but optional filters are silently accepted.

The validation properly checks required fields and validates optional filters when present. However, I noticed from reviewing the service implementation that FundingMethod, Status, and Currency filters pass validation here but are currently ignored downstream (see the TODO at line 137 in service/service.go).

Since validation passes but the filters have no effect, clients might not realize their filters aren't being applied. Consider either:

  1. Rejecting unsupported filters in validation until implemented
  2. Documenting this limitation clearly

This is a behavior quirk rather than a correctness bug, so flagging for awareness.

Optional: Fail fast on unsupported filters
 func (i ListInput) Validate() error {
 	var errs []error

 	if i.Namespace == "" {
 		errs = append(errs, errors.New("namespace is required"))
 	}

 	if i.CustomerID == "" {
 		errs = append(errs, errors.New("customer ID is required"))
 	}

+	// Filters are validated but not yet supported downstream
+	if i.FundingMethod != nil || i.Status != nil || i.Currency != nil {
+		errs = append(errs, errors.New("list filters are not yet supported"))
+	}
+
-	if i.FundingMethod != nil {
-		if err := i.FundingMethod.Validate(); err != nil {
-			errs = append(errs, err)
-		}
-	}
-
-	if i.Status != nil {
-		if err := i.Status.Validate(); err != nil {
-			errs = append(errs, err)
-		}
-	}
-
-	if i.Currency != nil {
-		if err := i.Currency.Validate(); err != nil {
-			errs = append(errs, fmt.Errorf("currency: %w", err))
-		}
-	}

 	return models.NewNillableGenericValidationError(errors.Join(errs...))
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/creditgrant/service.go` around lines 144 - 186,
ListInput.Validate currently accepts optional filters (FundingMethod, Status,
Currency) but those filters are ignored downstream; to fail fast, update
ListInput.Validate (the Validate method on the ListInput type) to reject
unsupported filters by appending validation errors when FundingMethod, Status,
or Currency are non-nil (e.g., errors.New("funding method filter not
supported"), errors.New("status filter not supported"), errors.New("currency
filter not supported")), then return them via
models.NewNillableGenericValidationError(errors.Join(errs...)); this ensures
callers get a clear validation error instead of silently ignored filters.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/v3/api.gen.go`:
- Around line 8830-8832: Add the same guard used by other credit endpoints to
the CreateCreditAdjustment handler: detect if s.customersCreditsHandler == nil
and if so call unimplemented.CreateCreditAdjustment(w, r, customerId) and
return; modify the CreateCreditAdjustment implementation (the handler that
currently unconditionally calls unimplemented.CreateCreditAdjustment) to perform
this nil-check before delegating to s.customersCreditsHandler so the
credits.enabled feature flag gates the endpoint consistently.

---

Outside diff comments:
In `@api/v3/api.gen.go`:
- Around line 310-323: The generated enum docs for
BillingCreditAvailabilityPolicy are stale—remove the references to the removed
values ("on_authorization", "on_settlement") from the OpenAPI/TypeSpec source
where BillingCreditAvailabilityPolicy is defined, ensure the enum definition
only contains the current member (BillingCreditAvailabilityPolicyOnCreation) and
update its description accordingly, then regenerate api.gen.go so the docblock
and enum values stay in sync and the file contains the required "// Code
generated by ..." header; do not edit api.gen.go directly.

---

Nitpick comments:
In `@openmeter/billing/creditgrant/service.go`:
- Around line 144-186: ListInput.Validate currently accepts optional filters
(FundingMethod, Status, Currency) but those filters are ignored downstream; to
fail fast, update ListInput.Validate (the Validate method on the ListInput type)
to reject unsupported filters by appending validation errors when FundingMethod,
Status, or Currency are non-nil (e.g., errors.New("funding method filter not
supported"), errors.New("status filter not supported"), errors.New("currency
filter not supported")), then return them via
models.NewNillableGenericValidationError(errors.Join(errs...)); this ensures
callers get a clear validation error instead of silently ignored filters.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a311604d-4393-4a84-9acf-9febf3eb8a31

📥 Commits

Reviewing files that changed from the base of the PR and between 66486a7 and d384629.

⛔ Files ignored due to path filters (1)
  • api/v3/openapi.yaml is excluded by !**/openapi.yaml
📒 Files selected for processing (8)
  • api/spec/packages/aip/src/customers/credits/grant.tsp
  • api/v3/api.gen.go
  • api/v3/handlers/customers/credits/convert.go
  • api/v3/handlers/customers/credits/get_grant.go
  • api/v3/server/routes.go
  • openmeter/billing/creditgrant/service.go
  • openmeter/billing/creditgrant/service/service.go
  • openmeter/ledger/chargeadapter/creditpurchase.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • api/spec/packages/aip/src/customers/credits/grant.tsp
  • api/v3/handlers/customers/credits/get_grant.go

Comment on lines +8830 to +8832
r.Group(func(r chi.Router) {
r.Post(options.BaseURL+"/openmeter/customers/{customerId}/credits/adjustments", wrapper.CreateCreditAdjustment)
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that CreateCreditAdjustment is protected by credits-enabled checks
# in non-generated server/handler layers.

rg -n -C4 --type=go '\bCreateCreditAdjustment\b'

echo "---- searching for credits feature flags/guards ----"
rg -n -C4 --type=go 'credits\.enabled|CreditsEnabled|EnableCredits|credit.*enabled'

Repository: openmeterio/openmeter

Length of output: 11784


🏁 Script executed:

rg -n -C6 --type=go '\bGetCustomerCreditBalance\b|\bListCreditGrants\b|\bCreateCreditGrant\b' api/v3/server/routes.go

Repository: openmeterio/openmeter

Length of output: 1761


🏁 Script executed:

rg -n -C10 --type=go 'credits.*enabled|Credits.*Enabled' api/v3/server/routes.go | head -50

Repository: openmeterio/openmeter

Length of output: 47


🏁 Script executed:

# Look for how credit-related routes are mounted and check for any middleware
rg -n -B5 -A5 --type=go 'openmeter/customers.*credits' api/v3/api.gen.go | head -80

Repository: openmeterio/openmeter

Length of output: 4505


🏁 Script executed:

rg -n --type=go 'customersCreditsHandler' api/v3/server/*.go | head -30

Repository: openmeterio/openmeter

Length of output: 1445


🏁 Script executed:

rg -n --type=go 'customersCreditsHandler.*=' | grep -v test

Repository: openmeterio/openmeter

Length of output: 661


🏁 Script executed:

# Check the server initialization to see how credits handler is wired
fd -e go 'server' api/v3 | xargs grep -l 'NewServer\|customersCreditsHandler' | head -5

Repository: openmeterio/openmeter

Length of output: 88


🏁 Script executed:

# Check the condition around customersCreditsHandler initialization
sed -n '215,230p' api/v3/server/server.go

Repository: openmeterio/openmeter

Length of output: 2060


🏁 Script executed:

# Check if CreateCreditAdjustment has the nil check that other credit handlers have
sed -n '372,375p' api/v3/server/routes.go

Repository: openmeterio/openmeter

Length of output: 226


🏁 Script executed:

# Let's see more context around where customersCreditsHandler is initialized
sed -n '210,260p' api/v3/server/server.go

Repository: openmeterio/openmeter

Length of output: 3520


Add the credits.enabled guard to CreateCreditAdjustment like other credit handlers

CreateCreditAdjustment at api/v3/server/routes.go:372 unconditionally calls unimplemented.CreateCreditAdjustment(), unlike other credit endpoints (GetCustomerCreditBalance, ListCreditGrants, etc.) that first check if s.customersCreditsHandler == nil. The customersCreditsHandler is only initialized when config.Credits.Enabled is true, so this nil-check is the defensive guard that gates the feature.

Add the same pattern:

if s.customersCreditsHandler == nil {
    unimplemented.CreateCreditAdjustment(w, r, customerId)
    return
}

This ensures consistency across all credit operations and maintains the multi-layer guarding described in your implementation approach.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v3/api.gen.go` around lines 8830 - 8832, Add the same guard used by other
credit endpoints to the CreateCreditAdjustment handler: detect if
s.customersCreditsHandler == nil and if so call
unimplemented.CreateCreditAdjustment(w, r, customerId) and return; modify the
CreateCreditAdjustment implementation (the handler that currently
unconditionally calls unimplemented.CreateCreditAdjustment) to perform this
nil-check before delegating to s.customersCreditsHandler so the credits.enabled
feature flag gates the endpoint consistently.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/feature Release note: Exciting New Features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants