Skip to content

feat(billing): add subscription end proration mode - #4553

Merged
turip merged 5 commits into
mainfrom
feat/cancel-proration-service-period
Jun 23, 2026
Merged

feat(billing): add subscription end proration mode#4553
turip merged 5 commits into
mainfrom
feat/cancel-proration-service-period

Conversation

@turip

@turip turip commented Jun 22, 2026

Copy link
Copy Markdown
Member

Summary

Adds a billing-profile setting for subscription-end proration behavior:

  • subscriptionEndProrationMode / subscription_end_proration_mode
  • bill_full_period preserves the legacy behavior for existing profiles
  • bill_actual_period is the default for new profiles

The setting controls how subscription-ending shortened service periods are billed for flat-fee subscription sync lines when product-catalog prorating is enabled.

Why

We had a bug/behavior where ServicePeriod < FullServicePeriod lines did not go through proration when they terminated with the subscription. That made terminal shortened periods bill the full period amount in some cancel/change flows.

This PR keeps the old behavior available for existing customers through a persisted billing-profile mode, while letting new profiles use the corrected behavior by default.

What changed

  • Added SubscriptionEndProrationMode to billing workflow invoicing config.
  • Added DB column subscription_end_proration_mode on billing_workflow_configs.
  • Migration writes existing rows as bill_full_period, then switches the column default to bill_actual_period.
  • Threaded the merged billing-profile setting into subscription sync target-state generation.
  • Updated API specs, generated clients, generated API types, and Ent code.
  • Added focused target-state and subscription-sync tests covering both modes.

Validation

  • make gen-api
  • make generate
  • atlas migrate --env local hash
  • go vet -tags=dynamic ./openmeter/billing ./openmeter/billing/httpdriver ./api/v3/handlers/billingprofiles ./openmeter/billing/worker/subscriptionsync/service ./openmeter/billing/worker/subscriptionsync/service/targetstate
  • go test -tags=dynamic ./openmeter/billing ./openmeter/billing/worker/subscriptionsync/service/targetstate ./api/v3/handlers/billingprofiles ./openmeter/billing/httpdriver
  • go test -tags=dynamic ./openmeter/billing/worker/subscriptionsync/service -run 'TestSubscriptionHandlerScenarios/TestInAdvanceFlatFeeCancelAtFirstBillingBoundary'
  • go test -tags=dynamic ./openmeter/billing/worker/subscriptionsync/service -run 'TestSubscriptionHandlerScenarios/(TestInAdvanceFlatFeeCancelAtFirstBillingBoundaryKeepsInitialProration|TestInArrearsProratingGathering)'
  • git diff --check

Summary by CodeRabbit

  • New Features
    • Added a configurable subscription-end proration mode for billing workflows (bill full period vs bill actual terminal service period), available via the billing workflow invoicing settings with a default of bill actual period.
  • Behavior Changes
    • Cancellation and gathering invoice line proration now follow the selected mode; profile updates preserve the existing mode when not explicitly provided.
  • Tests
    • Expanded coverage for in-advance cancellation at the first billing boundary across all supported modes, plus validation tests for the new setting.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c5424f29-fc33-41d8-90ca-104acf72776a

📥 Commits

Reviewing files that changed from the base of the PR and between a29ae8e and 5ae8a21.

📒 Files selected for processing (3)
  • test/app/stripe/appstripe.go
  • test/billing/invoice_test.go
  • test/customer/testenv.go

📝 Walkthrough

Walkthrough

Adds a new subscription_end_proration_mode configuration field throughout the billing stack, from API specs and core types through profile CRUD, database schema, and subscription sync logic. The shouldProrate() method is updated to condition proration behavior on this mode, allowing subscriptions cancelled at billing boundaries to be handled according to the configured mode (bill full period or actual period). Test coverage includes new in-advance and in-arrears cancellation scenarios verifying both proration modes.

Changes

Subscription end proration mode feature

Layer / File(s) Summary
API and type contract definitions
api/spec/packages/aip/src/billing/profile.tsp, api/spec/packages/legacy/src/billing/profile.tsp, api/v3/api.gen.go, api/spec/packages/aip-client-javascript/src/index.ts, api/spec/packages/aip-client-javascript/src/models/schemas.ts, api/spec/packages/aip-client-javascript/src/models/types.ts
TypeSpec files define the new subscription_end_proration_mode enum with BillFullPeriod and BillActualPeriod modes on billing workflow invoicing settings; generated Go API adds enum constants and struct field; TypeScript client types and schemas are reordered and updated to include the new field with validation.
Core invoicing type and validation
openmeter/billing/profile.go, openmeter/billing/profile_test.go
Introduces SubscriptionEndProrationMode public type with constants and Validate() helper; adds the field to InvoicingConfig struct requiring it during validation.
Defaults, merge, and profile validation
openmeter/billing/defaults.go, openmeter/billing/profile.go
Sets default mode to BillActualPeriod; updates profile merge to preserve invoicing mode from existing profile; streamlines profile update validation to require namespace and skip base-profile field validation.
Profile service integration
openmeter/billing/service/profile.go
Profile update ensures SubscriptionEndProrationMode is populated from existing profile when empty, validates early, and wraps errors appropriately.
Database schema and persistence
openmeter/ent/schema/billing.go, tools/migrate/migrations/20260623101013_add_billing_profile_subscription_end_proration_mode.up.sql, openmeter/billing/adapter/profile.go
Ent schema adds the enum field with default; migration creates the column with initial and final defaults; adapter persists and loads the mode through workflow config create, update, and database mapping operations.
HTTP API mapping
api/v3/handlers/billingprofiles/convert.go, api/v3/handlers/billingprofiles/convert.gen.go, api/v3/handlers/billingprofiles/convert_test.go, openmeter/billing/httpdriver/profile.go, openmeter/billing/httpdriver/profile_test.go
HTTP v3 handler and httpdriver convert the proration mode between API and internal representations during workflow lifecycle operations, with defaulting applied appropriately for create vs update requests.
Subscription sync infrastructure
openmeter/billing/worker/subscriptionsync/service/reconcile.go, openmeter/billing/worker/subscriptionsync/service/sync.go
Reconciliation refactors buildSyncPlan to accept a bundled buildSyncPlanInput struct including proration mode; sync service fetches the mode from customer override and threads it through plan generation.
Target state construction
openmeter/billing/worker/subscriptionsync/service/targetstate/targetstate.go
Builder accepts proration mode in BuildInput and assigns it to each generated StateItem.
Conditional prorating logic
openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem.go, openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem_test.go
shouldProrate() now conditions on SubscriptionEndProrationMode: applies ActiveTo guard only in BillFullPeriod mode; new unit test verifies behavior across all mode values.
Test coverage and helpers
openmeter/billing/worker/subscriptionsync/service/sync_test.go, openmeter/billing/worker/subscriptionsync/service/sync_credittheninvoice_test.go, test/billing/invoice_test.go, test/billing/profile.go, test/billing/profile_test.go, test/subscription/framework_test.go, test/app/stripe/appstripe.go, test/customer/testenv.go
Updates in-arrears test expectations with adjusted amounts; adds three new in-advance flat-fee cancellation tests verifying both proration modes; updates all test helpers and workflow config setups to include the new field.
Documentation note
openmeter/subscription/billing.go
Updates comment clarifying subscription end proration mode configuration.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant HTTPHandler
  participant ProfileService
  participant Adapter
  participant Database
  participant SyncService
  participant TargetStateBuilder
  participant ProrationLogic

  Client->>HTTPHandler: Create/update workflow with subscription_end_proration_mode
  HTTPHandler->>ProfileService: Normalize and validate profile
  ProfileService->>Adapter: Persist workflow config
  Adapter->>Database: Save subscription_end_proration_mode
  SyncService->>SyncService: Fetch customer profile with mode
  SyncService->>TargetStateBuilder: buildSyncPlanInput with mode
  TargetStateBuilder->>TargetStateBuilder: Assign mode to StateItems
  ProrationLogic->>ProrationLogic: shouldProrate() checks mode for conditional behavior
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • openmeterio/openmeter#4184: Both PRs touch openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem.go's StateItem.shouldProrate cutoff logic—one switches the comparison to ServicePeriod.To, and the other changes the ActiveTo-vs-ServicePeriod.To rejection to be conditional on SubscriptionEndProrationMode.
  • openmeterio/openmeter#4364: This PR's addition of SubscriptionEndProrationMode-driven cancellation/proration behavior and credit-then-invoice test updates directly relate to that PR's new subscription sync test suite validating proration/cancellation outcomes.

Suggested labels

area/billing, release-note/feature

Suggested reviewers

  • tothandras
  • borbelyr-kong
  • GAlexIHU
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main feature being added: a subscription end proration mode setting for billing profiles.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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/cancel-proration-service-period

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.

GAlexIHU
GAlexIHU previously approved these changes Jun 22, 2026
@turip
turip marked this pull request as ready for review June 22, 2026 16:48
@turip
turip requested a review from a team as a code owner June 22, 2026 16:48
@greptile-apps

greptile-apps Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a subscription_end_proration_mode field to billing profiles, controlling whether terminal shortened flat-fee service periods are prorated (bill_actual_period) or billed in full (bill_full_period). The migration backfills existing rows with the legacy bill_full_period behavior and then sets the DB/Ent default to bill_actual_period for new profiles.

  • Migration strategy: adds NOT NULL column with bill_full_period default (backfills existing rows), then alters the column default to bill_actual_period so new profiles get the corrected behavior.
  • API / HTTP layer: CREATE calls default to bill_actual_period; UPDATE calls pass "" when the field is absent and the service backfills from the stored profile, preserving the existing setting.
  • Proration logic: shouldProrate() gains a mode switch; BillFullPeriod skips proration when the subscription ends at or before the service period boundary, while BillActualPeriod (and empty/unknown modes) fall through to the existing ProRatingConfig check.

Confidence Score: 5/5

Safe to merge — the migration correctly backfills existing rows with the legacy mode and all three API paths (create, update with field, update without field) produce the correct InvoicingConfig value before validation.

The migration two-step (ADD COLUMN with bill_full_period default, then ALTER DEFAULT to bill_actual_period) is correct and non-destructive. CREATE defaults are applied at the HTTP conversion layer; UPDATE backfills from the stored profile in the service before validation. The shouldProrate() switch is straightforward, tested at the unit level, and the integration test updates correctly reflect the new prorated-by-default behavior for new profiles.

No files require special attention. The migration, Ent schema, service backfill, and HTTP conversion layers are all consistent with each other.

Important Files Changed

Filename Overview
openmeter/billing/profile.go Adds SubscriptionEndProrationMode type, constants, Validate(), and wires it into InvoicingConfig; UpdateProfileInput.Validate() correctly strips full-profile validation (moved to service after backfilling).
openmeter/billing/service/profile.go Backfills SubscriptionEndProrationMode from the stored profile when absent on UPDATE, then calls full validation — correctly separating structural from domain validation.
tools/migrate/migrations/20260623101013_add_billing_profile_subscription_end_proration_mode.up.sql Two-step migration: ADD COLUMN with bill_full_period default (backfills existing rows), then ALTER DEFAULT to bill_actual_period for new rows — correct approach.
openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem.go shouldProrate() gains a mode switch: BillFullPeriod skips proration when subscription ends at/before service period boundary; BillActualPeriod case is intentionally empty (falls through to ProRatingConfig check); unknown/empty modes implicitly behave as BillActualPeriod.
openmeter/billing/httpdriver/profile.go CREATE path uses fromAPIBillingWorkflowCreate which defaults to BillActualPeriod; UPDATE path passes "" and the service backfills — both paths are correct.
api/v3/handlers/billingprofiles/convert.go Introduces FromAPIBillingWorkflowCreate (with default) and retains FromAPIBillingWorkflow (no default, service backfills); both paths handled correctly.
openmeter/billing/worker/subscriptionsync/service/sync.go Reads subscriptionEndProrationMode from the merged billing profile inside HasBillables guard; zero-value ("") for non-billable/nil-view subscriptions is safe since shouldProrate() is never reached in those paths.
openmeter/ent/schema/billing.go Adds subscription_end_proration_mode enum field with BillActualPeriod as the Go-level default, consistent with the migration's post-ALTER default.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[API Request: Create/Update Profile] --> B{CREATE or UPDATE?}
    B -->|CREATE| C[fromAPIBillingWorkflowCreate\ndefault = bill_actual_period]
    B -->|UPDATE| D[fromAPIBillingWorkflow\ndefault = empty string]
    C --> E[InvoicingConfig.SubscriptionEndProrationMode\n= bill_actual_period]
    D --> F{field present\nin request?}
    F -->|yes| G[use request value]
    F -->|no| H[service backfills from\nexisting DB profile]
    G --> I[targetState.Validate]
    H --> I
    E --> I
    I --> J[adapter writes to DB]

    K[Subscription Sync] --> L{subsView != nil &&\nHasBillables?}
    L -->|no| M[subscriptionEndProrationMode = empty]
    L -->|yes| N[read from MergedProfile]
    N --> O[buildSyncPlan]
    M --> O
    O --> P[targetstate.Builder]
    P --> Q[StateItem.shouldProrate]
    Q --> R{mode?}
    R -->|bill_full_period| S{ActiveTo <= ServicePeriod.To?}
    S -->|yes| T[no proration\nbill full amount]
    S -->|no| U{ProRatingConfig.Mode}
    R -->|bill_actual_period\nor empty/unknown| U
    U -->|ProratePrices| V[prorate]
    U -->|other| W[no proration]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[API Request: Create/Update Profile] --> B{CREATE or UPDATE?}
    B -->|CREATE| C[fromAPIBillingWorkflowCreate\ndefault = bill_actual_period]
    B -->|UPDATE| D[fromAPIBillingWorkflow\ndefault = empty string]
    C --> E[InvoicingConfig.SubscriptionEndProrationMode\n= bill_actual_period]
    D --> F{field present\nin request?}
    F -->|yes| G[use request value]
    F -->|no| H[service backfills from\nexisting DB profile]
    G --> I[targetState.Validate]
    H --> I
    E --> I
    I --> J[adapter writes to DB]

    K[Subscription Sync] --> L{subsView != nil &&\nHasBillables?}
    L -->|no| M[subscriptionEndProrationMode = empty]
    L -->|yes| N[read from MergedProfile]
    N --> O[buildSyncPlan]
    M --> O
    O --> P[targetstate.Builder]
    P --> Q[StateItem.shouldProrate]
    Q --> R{mode?}
    R -->|bill_full_period| S{ActiveTo <= ServicePeriod.To?}
    S -->|yes| T[no proration\nbill full amount]
    S -->|no| U{ProRatingConfig.Mode}
    R -->|bill_actual_period\nor empty/unknown| U
    U -->|ProratePrices| V[prorate]
    U -->|other| W[no proration]
Loading

Reviews (4): Last reviewed commit: "fix(billing): default proration mode in ..." | Re-trigger Greptile

@GAlexIHU GAlexIHU changed the title Fix subscription cancel proration feat(billing): add subscription end proration mode Jun 23, 2026
@GAlexIHU GAlexIHU added the release-note/ignore Ignore this change when generating release notes label Jun 23, 2026
GAlexIHU
GAlexIHU previously approved these changes Jun 23, 2026
@GAlexIHU
GAlexIHU force-pushed the feat/cancel-proration-service-period branch from e8da2a2 to fccf8e2 Compare June 23, 2026 12:57

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (1)
openmeter/billing/profile.go (1)

111-116: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Aggregate validation issues in the touched Validate() methods.

Nice addition overall, but these methods still short-circuit on first error. Please collect all failures into var errs []error and return models.NewNillableGenericValidationError(errors.Join(errs...)) so callers get the full validation set in one response.

Suggested pattern
func (i UpdateProfileInput) Validate() error {
-   if i.ID == "" {
-       return errors.New("id is required")
-   }
-   if i.Namespace == "" {
-       return errors.New("namespace is required")
-   }
-   if i.AppReferences != nil {
-       return errors.New("apps cannot be updated")
-   }
-   return nil
+   var errs []error
+   if i.ID == "" {
+       errs = append(errs, errors.New("id is required"))
+   }
+   if i.Namespace == "" {
+       errs = append(errs, errors.New("namespace is required"))
+   }
+   if i.AppReferences != nil {
+       errs = append(errs, errors.New("apps cannot be updated"))
+   }
+   return models.NewNillableGenericValidationError(errors.Join(errs...))
}

As per coding guidelines, "For Go Validate() error methods, prefer collecting all validation issues into var errs []error and returning models.NewNillableGenericValidationError(errors.Join(errs...)) instead of returning on the first invalid field".

Also applies to: 132-138, 525-533

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openmeter/billing/profile.go` around lines 111 - 116, The Validate() method
currently returns immediately on the first validation error, preventing callers
from seeing all validation issues at once. Refactor the method to collect all
validation failures into a var errs []error slice, appending errors from each
validation check (such as SubscriptionEndProrationMode.Validate()) instead of
returning early, then return
models.NewNillableGenericValidationError(errors.Join(errs...)) at the end to
provide the complete set of validation issues. Apply this same pattern to all
other Validate() methods mentioned in the comment.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@openmeter/billing/profile.go`:
- Around line 111-116: The Validate() method currently returns immediately on
the first validation error, preventing callers from seeing all validation issues
at once. Refactor the method to collect all validation failures into a var errs
[]error slice, appending errors from each validation check (such as
SubscriptionEndProrationMode.Validate()) instead of returning early, then return
models.NewNillableGenericValidationError(errors.Join(errs...)) at the end to
provide the complete set of validation issues. Apply this same pattern to all
other Validate() methods mentioned in the comment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 367acfa0-64c3-4691-9444-b39cf0787953

📥 Commits

Reviewing files that changed from the base of the PR and between 72235b7 and fccf8e2.

⛔ Files ignored due to path filters (18)
  • api/client/go/client.gen.go is excluded by !api/client/**
  • api/client/javascript/src/client/schemas.ts is excluded by !api/client/**
  • api/client/javascript/src/zod/index.ts is excluded by !api/client/**
  • api/client/python/openmeter/_generated/models/__init__.py is excluded by !**/_generated/**, !api/client/**
  • api/client/python/openmeter/_generated/models/_enums.py is excluded by !**/_generated/**, !api/client/**
  • api/client/python/openmeter/_generated/models/_models.py is excluded by !**/_generated/**, !api/client/**
  • api/openapi.cloud.yaml is excluded by !**/openapi.cloud.yaml
  • api/openapi.yaml is excluded by !**/openapi.yaml
  • api/v3/openapi.yaml is excluded by !**/openapi.yaml
  • openmeter/ent/db/billingworkflowconfig.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billingworkflowconfig/billingworkflowconfig.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billingworkflowconfig/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billingworkflowconfig_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billingworkflowconfig_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/migrate/schema.go is excluded by !**/ent/db/**
  • openmeter/ent/db/mutation.go is excluded by !**/ent/db/**
  • openmeter/ent/db/runtime.go is excluded by !**/ent/db/**
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (23)
  • api/api.gen.go
  • api/spec/packages/aip-client-javascript/src/index.ts
  • api/spec/packages/aip-client-javascript/src/models/schemas.ts
  • api/spec/packages/aip-client-javascript/src/models/types.ts
  • api/spec/packages/aip/src/billing/profile.tsp
  • api/spec/packages/legacy/src/billing/profile.tsp
  • api/v3/api.gen.go
  • api/v3/handlers/billingprofiles/convert.go
  • openmeter/billing/adapter/profile.go
  • openmeter/billing/defaults.go
  • openmeter/billing/httpdriver/profile.go
  • openmeter/billing/profile.go
  • openmeter/billing/service/profile.go
  • openmeter/billing/worker/subscriptionsync/service/reconcile.go
  • openmeter/billing/worker/subscriptionsync/service/sync.go
  • openmeter/billing/worker/subscriptionsync/service/sync_test.go
  • openmeter/billing/worker/subscriptionsync/service/targetstate/targetstate.go
  • openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem.go
  • openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem_test.go
  • openmeter/ent/schema/billing.go
  • openmeter/subscription/billing.go
  • tools/migrate/migrations/20260623101013_add_billing_profile_subscription_end_proration_mode.down.sql
  • tools/migrate/migrations/20260623101013_add_billing_profile_subscription_end_proration_mode.up.sql
✅ Files skipped from review due to trivial changes (5)
  • tools/migrate/migrations/20260623101013_add_billing_profile_subscription_end_proration_mode.down.sql
  • openmeter/subscription/billing.go
  • openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem_test.go
  • api/v3/api.gen.go
  • api/spec/packages/aip-client-javascript/src/index.ts

Comment thread openmeter/billing/httpdriver/profile.go Outdated
Comment thread api/v3/handlers/billingprofiles/convert.go Outdated

@coderabbitai coderabbitai Bot left a comment

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.

Caution

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

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

360-365: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don’t overwrite explicit proration-mode updates during merge.

This merge behavior keeps the mode from p and can prevent an update payload from ever changing SubscriptionEndProrationMode, which breaks the new configurability.

💡 Proposed fix
- merged.WorkflowConfig.Invoicing.SubscriptionEndProrationMode = p.WorkflowConfig.Invoicing.SubscriptionEndProrationMode
+ if o.WorkflowConfig.Invoicing.SubscriptionEndProrationMode == "" {
+ 	merged.WorkflowConfig.Invoicing.SubscriptionEndProrationMode = p.WorkflowConfig.Invoicing.SubscriptionEndProrationMode
+ }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openmeter/billing/profile.go` around lines 360 - 365, The
SubscriptionEndProrationMode field assignment does not follow the same override
pattern as the other Invoicing fields (AutoAdvance, DraftPeriod, DueAfter,
ProgressiveBilling). Currently it directly uses the value from
p.WorkflowConfig.Invoicing.SubscriptionEndProrationMode without checking for an
explicit override in o.Invoicing.SubscriptionEndProrationMode first. Change the
SubscriptionEndProrationMode assignment to use the same lo.FromPtrOr pattern
used for the other fields to properly respect explicit updates while falling
back to the default WorkflowConfig value when no override is provided.
🧹 Nitpick comments (2)
openmeter/billing/profile.go (1)

96-120: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Aggregate validation errors in Validate() methods instead of failing fast.

Both updated Validate() paths still return on first invalid field. Please align with the repo pattern so callers receive the full validation set in one response.

💡 Pattern to apply
func (c *InvoicingConfig) Validate() error {
+	var errs []error
+
-	if c.DraftPeriod.IsNegative() && c.AutoAdvance {
-		return fmt.Errorf("draft period must be greater or equal to 0")
-	}
+	if c.DraftPeriod.IsNegative() && c.AutoAdvance {
+		errs = append(errs, fmt.Errorf("draft period must be greater or equal to 0"))
+	}

-	if c.DueAfter.IsNegative() {
-		return fmt.Errorf("due after must be greater or equal to 0")
-	}
+	if c.DueAfter.IsNegative() {
+		errs = append(errs, fmt.Errorf("due after must be greater or equal to 0"))
+	}

	if c.DefaultTaxConfig != nil {
		if err := c.DefaultTaxConfig.Validate(); err != nil {
-			return fmt.Errorf("invalid tax behavior: %w", err)
+			errs = append(errs, fmt.Errorf("invalid tax behavior: %w", err))
		}
	}

	if c.SubscriptionEndProrationMode == "" {
-		return errors.New("subscription end proration mode is required")
+		errs = append(errs, errors.New("subscription end proration mode is required"))
+	} else if err := c.SubscriptionEndProrationMode.Validate(); err != nil {
+		errs = append(errs, fmt.Errorf("invalid subscription end proration mode: %w", err))
	}
-
-	if err := c.SubscriptionEndProrationMode.Validate(); err != nil {
-		return fmt.Errorf("invalid subscription end proration mode: %w", err)
-	}
-
-	return nil
+	return models.NewNillableGenericValidationError(errors.Join(errs...))
}

As per coding guidelines, “For Go Validate() error methods, prefer collecting all validation issues into var errs []error and returning models.NewNillableGenericValidationError(errors.Join(errs...)) instead of returning on the first invalid field`.

Also applies to: 516-530

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openmeter/billing/profile.go` around lines 96 - 120, The Validate() method on
the InvoicingConfig type currently returns immediately upon encountering the
first validation error instead of collecting all validation issues. Refactor the
method by declaring a var errs []error slice at the start, then replace all
return statements (for DraftPeriod, AutoAdvance, DueAfter, DefaultTaxConfig, and
SubscriptionEndProrationMode validations) with errs = append(errs, ...) to
collect the errors, and finally return
models.NewNillableGenericValidationError(errors.Join(errs...)) at the end of the
method to provide callers with the complete set of validation issues in a single
response.

Source: Coding guidelines

openmeter/billing/profile_test.go (1)

13-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Nice enum coverage—please add one focused merge/update behavior test too.

This table verifies mode validation well, but it doesn’t assert the changed merge/update semantics for this setting. A small case for “explicit update value overrides existing mode” (and empty update preserves existing) would guard the new plumbing end-to-end.

As per path instructions, “Make sure the tests are comprehensive and cover the changes. Keep a strong focus on unit tests and in-code integration tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openmeter/billing/profile_test.go` around lines 13 - 59, The
TestInvoicingConfigValidateSubscriptionEndProrationMode test only validates enum
values but doesn't cover the merge/update behavior for
SubscriptionEndProrationMode. Add new test cases to verify that when updating a
configuration with an explicit mode value, it properly overrides an existing
mode, and when the update contains an empty mode value, the existing mode is
preserved. These tests should demonstrate the end-to-end update plumbing works
correctly for this setting by creating a base configuration with a mode,
applying an update with either a new explicit mode or an empty mode, and
asserting the resulting configuration reflects the expected merge behavior.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@openmeter/billing/profile.go`:
- Around line 360-365: The SubscriptionEndProrationMode field assignment does
not follow the same override pattern as the other Invoicing fields (AutoAdvance,
DraftPeriod, DueAfter, ProgressiveBilling). Currently it directly uses the value
from p.WorkflowConfig.Invoicing.SubscriptionEndProrationMode without checking
for an explicit override in o.Invoicing.SubscriptionEndProrationMode first.
Change the SubscriptionEndProrationMode assignment to use the same lo.FromPtrOr
pattern used for the other fields to properly respect explicit updates while
falling back to the default WorkflowConfig value when no override is provided.

---

Nitpick comments:
In `@openmeter/billing/profile_test.go`:
- Around line 13-59: The TestInvoicingConfigValidateSubscriptionEndProrationMode
test only validates enum values but doesn't cover the merge/update behavior for
SubscriptionEndProrationMode. Add new test cases to verify that when updating a
configuration with an explicit mode value, it properly overrides an existing
mode, and when the update contains an empty mode value, the existing mode is
preserved. These tests should demonstrate the end-to-end update plumbing works
correctly for this setting by creating a base configuration with a mode,
applying an update with either a new explicit mode or an empty mode, and
asserting the resulting configuration reflects the expected merge behavior.

In `@openmeter/billing/profile.go`:
- Around line 96-120: The Validate() method on the InvoicingConfig type
currently returns immediately upon encountering the first validation error
instead of collecting all validation issues. Refactor the method by declaring a
var errs []error slice at the start, then replace all return statements (for
DraftPeriod, AutoAdvance, DueAfter, DefaultTaxConfig, and
SubscriptionEndProrationMode validations) with errs = append(errs, ...) to
collect the errors, and finally return
models.NewNillableGenericValidationError(errors.Join(errs...)) at the end of the
method to provide callers with the complete set of validation issues in a single
response.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3a17e306-e4df-4579-9028-46cccbf6f216

📥 Commits

Reviewing files that changed from the base of the PR and between fccf8e2 and b717c85.

📒 Files selected for processing (16)
  • api/v3/handlers/billingprofiles/convert.go
  • openmeter/billing/adapter/profile.go
  • openmeter/billing/httpdriver/profile.go
  • openmeter/billing/profile.go
  • openmeter/billing/profile_test.go
  • openmeter/billing/service/profile.go
  • openmeter/billing/worker/subscriptionsync/service/sync.go
  • openmeter/billing/worker/subscriptionsync/service/sync_credittheninvoice_test.go
  • openmeter/billing/worker/subscriptionsync/service/sync_test.go
  • openmeter/billing/worker/subscriptionsync/service/targetstate/targetstate.go
  • openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem.go
  • openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem_test.go
  • test/billing/invoice_test.go
  • test/billing/profile.go
  • test/billing/profile_test.go
  • test/subscription/framework_test.go
💤 Files with no reviewable changes (2)
  • openmeter/billing/service/profile.go
  • openmeter/billing/adapter/profile.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • openmeter/billing/worker/subscriptionsync/service/targetstate/targetstate.go
  • api/v3/handlers/billingprofiles/convert.go
  • openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem_test.go
  • openmeter/billing/worker/subscriptionsync/service/sync.go
  • openmeter/billing/httpdriver/profile.go
  • openmeter/billing/worker/subscriptionsync/service/sync_test.go
  • openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem.go

@turip
turip enabled auto-merge (squash) June 23, 2026 13:31
@greptile-apps

greptile-apps Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Greptile encountered an error while reviewing this PR. Please reach out to support@greptile.com for assistance.

@GAlexIHU
GAlexIHU self-requested a review June 23, 2026 14:41
@turip
turip merged commit facf3b0 into main Jun 23, 2026
27 of 28 checks passed
@turip
turip deleted the feat/cancel-proration-service-period branch June 23, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/ignore Ignore this change when generating release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants