Skip to content

Conversation

@GAlexIHU
Copy link
Contributor

@GAlexIHU GAlexIHU commented Oct 16, 2025

Overview

Implements first-day-of-month-like invoicing scenarios. This is implemented via a new BillingProfile CollectionConfig setting. The current implementation will effectively result in possibly multiple invoices all "being issued" when the next anchor recurrence happens. (Later we can implement the "invoiced together" behavior as an improvement)

  • As a somewhat unrelated change, TypeSpec => OpenAPI doesn't generate property defaults correctly for union types (unsure why), so a patch script was added that overrides the generated schema without changing formatting.
  • There also seems to be some issue with formatting in api/spec & api/client/javascript packages, we should add formattingrules for these too

Summary by CodeRabbit

Release Notes

  • New Features

    • Added anchored billing collection alignment mode for more precise invoice scheduling based on specific anchor dates and intervals.
    • Enhanced invoice management with improved timing controls, including draft periods and collection date configuration.
    • Expanded subscription management capabilities with granular phase control and flexible timing options.
    • Added support for multiple billing applications and integrations.
  • Improvements

    • Strengthened validation for recurring billing periods using ISO 8601 duration format standards.

@GAlexIHU GAlexIHU requested a review from a team as a code owner October 16, 2025 15:57
@GAlexIHU GAlexIHU added the release-note/feature Release note: Exciting New Features label Oct 16, 2025
@GAlexIHU GAlexIHU requested review from chrisgacsal and turip October 16, 2025 15:58
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 16, 2025

Warning

Rate limit exceeded

@GAlexIHU has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 7 minutes and 46 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between b3abeed and 3184c87.

⛔ Files ignored due to path filters (11)
  • 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/client/python/openmeter/_types.py is excluded by !api/client/**
  • api/openapi.cloud.yaml is excluded by !**/openapi.cloud.yaml
  • api/openapi.yaml is excluded by !**/openapi.yaml
  • api/spec/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • go.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (33)
  • api/spec/package.json (3 hunks)
  • api/spec/src/billing/profile.tsp (3 hunks)
  • api/spec/src/types.tsp (1 hunks)
  • api/spec/tsconfig.json (1 hunks)
  • openmeter/billing/adapter/customeroverride.go (2 hunks)
  • openmeter/billing/adapter/invoice.go (1 hunks)
  • openmeter/billing/adapter/profile.go (5 hunks)
  • openmeter/billing/customeroverride.go (1 hunks)
  • openmeter/billing/httpdriver/invoice.go (1 hunks)
  • openmeter/billing/httpdriver/profile.go (5 hunks)
  • openmeter/billing/invoice.go (3 hunks)
  • openmeter/billing/profile.go (3 hunks)
  • openmeter/billing/service/invoice.go (1 hunks)
  • openmeter/billing/service/invoicecalc/collectionat.go (2 hunks)
  • openmeter/billing/service/invoicecalc/draftuntil.go (1 hunks)
  • openmeter/billing/service/invoicestate.go (1 hunks)
  • openmeter/billing/worker/subscription/sync_test.go (1 hunks)
  • openmeter/billing/workflow.go (1 hunks)
  • openmeter/ent/db/billingcustomeroverride.go (4 hunks)
  • openmeter/ent/db/billingcustomeroverride/billingcustomeroverride.go (3 hunks)
  • openmeter/ent/db/billingcustomeroverride/where.go (1 hunks)
  • openmeter/ent/db/billingcustomeroverride_create.go (6 hunks)
  • openmeter/ent/db/billingcustomeroverride_update.go (6 hunks)
  • openmeter/ent/db/billingworkflowconfig.go (4 hunks)
  • openmeter/ent/db/billingworkflowconfig/billingworkflowconfig.go (3 hunks)
  • openmeter/ent/db/billingworkflowconfig/where.go (1 hunks)
  • openmeter/ent/db/billingworkflowconfig_create.go (6 hunks)
  • openmeter/ent/db/billingworkflowconfig_update.go (6 hunks)
  • openmeter/ent/db/migrate/schema.go (4 hunks)
  • openmeter/ent/db/mutation.go (20 hunks)
  • openmeter/ent/db/runtime.go (1 hunks)
  • openmeter/ent/db/setorclear.go (2 hunks)
  • openmeter/ent/schema/billing.go (2 hunks)
📝 Walkthrough

Walkthrough

Introduces support for "anchored" billing alignment mode alongside the existing "subscription" mode. When enabled, invoices are collected on a fixed day-of-month anchor rather than subscription billing cadence. Changes span TypeSpec specifications, OpenAPI schemas, JavaScript client code generation, Go domain models, database ORM layer, migrations, and test coverage.

Changes

Cohort / File(s) Summary
TypeSpec Schemas
api/spec/src/types.tsp, api/spec/src/billing/profile.tsp
Introduces RecurringPeriodV2 model with interval and anchor fields, deprecates original RecurringPeriod. Reworks BillingWorkflowCollectionAlignment into a discriminated union supporting subscription and anchored variants. Adds AlignmentKindAnchored and AnchoredAlignmentDetail types with validation.
OpenAPI Specifications
api/openapi.yaml, api/openapi.cloud.yaml
Adds RecurringPeriodV2 schema and BillingWorkflowCollectionAlignmentAnchored model. Extends BillingCollectionAlignment enum with anchored value. Transforms BillingWorkflowCollectionAlignment from allOf to oneOf with discriminator. Expands Invoice, Subscription, and related schemas with new fields and detailed status tracking.
OpenAPI Tooling
api/spec/package.json, api/spec/scripts/update-collection-default.ts, api/spec/tsconfig.json
Adds fix:openapi-default script and TypeScript tooling. Introduces script to update default values in OpenAPI files for alignment configuration.
JavaScript Client Codegen
api/client/javascript/scripts/add-as-const.ts
Applies stylistic refactors (standardized quotes, semicolons, formatting) without functional changes.
JavaScript Client Schemas
api/client/javascript/src/client/schemas.ts
Exports new RecurringPeriodV2 type from OpenAPI components.
JavaScript Zod Validation
api/client/javascript/src/zod/index.ts
Adds regex validation constants for recurring period intervals (e.g., createBillingProfileBodyWorkflowCollectionAlignmentRecurringPeriodIntervalRegExpTwo) across multiple schema types, enabling ISO 8601 duration validation.
Go Billing Domain Models
openmeter/billing/profile.go, openmeter/billing/workflow.go, openmeter/billing/customeroverride.go
Introduces AlignmentKindAnchored constant and AnchoredAlignmentDetail type. Adds validation logic for anchored alignment details. Extends CollectionConfig and CollectionOverrideConfig structs with AnchoredAlignmentDetail field.
Go Invoice Creation & Calculation
openmeter/billing/invoice.go, openmeter/billing/adapter/invoice.go, openmeter/billing/adapter/profile.go, openmeter/billing/adapter/customeroverride.go
Adds DefaultCollectionAtForStandardInvoice() method. Modifies invoice creation to handle CollectionAt input field. Introduces mapping for anchored alignment details during profile creation and updates.
Go Collection Date Calculation
openmeter/billing/service/invoicecalc/collectionat.go, openmeter/billing/service/invoicecalc/draftuntil.go
Refactors collection date calculation to support anchored alignment by computing next first-day-of-month when AlignmentKindAnchored is active. Normalizes collectionAt to latest of value and CreatedAt.
Go Invoice Workflow Mapping
openmeter/billing/httpdriver/profile.go, openmeter/billing/httpdriver/invoice.go
Introduces mapAlignmentToAPI and mapWorkflowConfigSettingsToAPI helpers with error handling for mapping workflow configuration to API representation. Updates mapWorkflowConfigToAPI to return error.
Go Invoice State & Service
openmeter/billing/service/invoicestate.go, openmeter/billing/service/invoice.go
Updates collection date reference logic using DefaultCollectionAtForStandardInvoice(). Minor comment additions to invoice pending lines processing.
Database Schema Definition
openmeter/ent/schema/billing.go
Adds anchored_alignment_detail JSON field (jsonb) to BillingWorkflowConfig and BillingCustomerOverride entities.
Database Migrations
tools/migrate/migrations/20251014140637_first-day-of-month-billing.up.sql, tools/migrate/migrations/20251014140637_first-day-of-month-billing.down.sql
Adds anchored_alignment_detail nullable jsonb column to billing_workflow_configs and billing_customer_overrides tables.
BillingWorkflowConfig ORM
openmeter/ent/db/billingworkflowconfig.go, openmeter/ent/db/billingworkflowconfig/billingworkflowconfig.go, openmeter/ent/db/billingworkflowconfig/where.go, openmeter/ent/db/billingworkflowconfig_create.go, openmeter/ent/db/billingworkflowconfig_update.go
Extends ORM with AnchoredAlignmentDetail field. Adds SetAnchoredAlignmentDetail, ClearAnchoredAlignmentDetail, AnchoredAlignmentDetailIsNil, and AnchoredAlignmentDetailNotNil methods across create, update, and query builders.
BillingCustomerOverride ORM
openmeter/ent/db/billingcustomeroverride.go, openmeter/ent/db/billingcustomeroverride/billingcustomeroverride.go, openmeter/ent/db/billingcustomeroverride/where.go, openmeter/ent/db/billingcustomeroverride_create.go, openmeter/ent/db/billingcustomeroverride_update.go
Mirrors BillingWorkflowConfig changes with AnchoredAlignmentDetail field support across ORM methods.
ORM Mutation & Runtime
openmeter/ent/db/mutation.go, openmeter/ent/db/setorclear.go, openmeter/ent/db/runtime.go, openmeter/ent/db/migrate/schema.go
Adds mutation methods for AnchoredAlignmentDetail (SetAnchoredAlignmentDetail, ClearAnchoredAlignmentDetail, OldAnchoredAlignmentDetail, etc.) across multiple entity types. Extends schema descriptors with new field. Updates enum validators to accept anchored value. Adds SetOrClearAnchoredAlignmentDetail helpers.
Unit & Integration Tests
test/billing/collection_config_test.go, test/billing/collection_test.go, test/billing/profile_test.go, test/subscription/scenario_firstofmonth_test.go, openmeter/billing/worker/subscription/sync_test.go
Adds tests validating CollectionConfig validation, anchored alignment invoice collection behavior, profile persistence with anchored details, and subscription scenario with first-day-of-month anchor. Includes skipped test scaffold for future validation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Reasoning: Multiple layers of changes across specifications, code generation, domain logic, and database layer introduce new business logic for anchored billing alignment. Collection date calculation requires understanding timing semantics and calendar logic. However, ORM changes follow repetitive patterns, and overall structure remains modular. Moderate-to-high heterogeneity with dense logic in collection calculation and invoice/workflow mapping functions warrants careful review.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.22% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ 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 succinctly describes the primary feature added by this pull request, namely support for first-day-of-month billing, and follows conventional commit style without extraneous detail. It's focused, clear, and aligns with the PR objectives.

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

Caution

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

⚠️ Outside diff range comments (3)
openmeter/billing/service/invoicestate.go (1)

714-729: Possible nil dereference when CollectionAt is nil in snapshotQuantityAsNeeded.

When CollectionAt is nil, state transition still allows DraftCollecting. The second branch dereferences it and can panic.

Use DefaultCollectionAtForStandardInvoice() consistently to avoid nil deref.

-	// We don't have the snapshot and the collection date is in the future
-	if m.Invoice.QuantitySnapshotedAt == nil && clock.Now().Before(*m.Invoice.CollectionAt) {
+	// We don't have the snapshot and the collection date is in the future
+	if m.Invoice.QuantitySnapshotedAt == nil && clock.Now().Before(m.Invoice.DefaultCollectionAtForStandardInvoice()) {
 		return nil
 	}
openmeter/billing/httpdriver/profile.go (1)

349-406: Parse inbound collection alignment in fromAPIBillingWorkflow
fromAPIBillingWorkflow currently ignores api.BillingWorkflow.Collection.Alignment and always falls back to the default. Add logic to inspect i.Collection.Alignment, use AsBillingWorkflowCollectionAlignmentAnchored to parse the ISO interval and anchor into AnchoredAlignmentDetail (setting AlignmentKindAnchored), or AsBillingWorkflowCollectionAlignmentSubscription to set AlignmentKindSubscription. This ensures clients can configure anchored or subscription-based alignment.

openmeter/billing/adapter/customeroverride.go (1)

91-104: Persist AnchoredAlignmentDetail in update path
Insert a conditional SetOrClearAnchoredAlignmentDetail(input.Collection.AnchoredAlignmentDetail) call before Save() in the BillingCustomerOverride.Update chain to ensure AnchoredAlignmentDetail is persisted; adjust to the actual ent builder method names.

🧹 Nitpick comments (18)
api/spec/Makefile (1)

10-10: Ensure the OpenAPI patch is formatted and idempotent

Good call running the patch after copying. Add a formatting pass to avoid noisy diffs and ensure stable outputs on repeated runs.

Apply:

 pnpm fix:openapi-default
+pnpm lint:fix
openmeter/ent/schema/billing.go (1)

126-131: Anchored detail storage type looks right; confirm null/clear semantics

Using a pointer JSON type with Optional() is fine. If you need explicit SetNillable*/Clear* builder methods or to persist NULL distinctly, consider adding Nillable().

Do we require ent-level nullability methods here, or are domain validators ensuring presence only when collection_alignment = anchored?

tools/migrate/migrations/20251014140637_first-day-of-month-billing.down.sql (1)

1-4: Use IF EXISTS for safer down migrations

Optional: guard drops to avoid failures on partially applied states.

-ALTER TABLE "billing_workflow_configs" DROP COLUMN "anchored_alignment_detail";
+ALTER TABLE "billing_workflow_configs" DROP COLUMN IF EXISTS "anchored_alignment_detail";
-ALTER TABLE "billing_customer_overrides" DROP COLUMN "anchored_alignment_detail";
+ALTER TABLE "billing_customer_overrides" DROP COLUMN IF EXISTS "anchored_alignment_detail";
openmeter/billing/httpdriver/invoice.go (1)

569-572: Include invoice ID in error for better diagnostics.

Add the invoice ID to the error to aid tracing.

-	if err != nil {
-		return api.Invoice{}, fmt.Errorf("failed to map workflow config to API: %w", err)
-	}
+	if err != nil {
+		return api.Invoice{}, fmt.Errorf("failed to map workflow config to API [invoice=%s]: %w", invoice.ID, err)
+	}
openmeter/billing/service/invoice.go (1)

329-331: Resolve TODO around collectionAt for anchored billing in pending-lines flow.

Clarify whether collectionAt is set by the calculator/state machine on creation for anchored alignment. If yes, remove or replace the TODO with a brief note; if not, compute it before persisting to avoid inconsistent states.

api/spec/package.json (1)

9-12: Prettier scripts — good; consider enforcing in CI/workspace.

Scripts look good. Consider adding a workspace-level format/lint step (e.g., pnpm -r run lint) in CI to keep spec and generated clients consistent.

openmeter/billing/worker/subscription/sync_test.go (1)

4635-4749: Fix time/price expectations and anchor alignment in skipped test.

Issues:

  • Jumps from 2025-10 to 2024-01 mid-test.
  • Expected amount 6 but plan price is 5.
  • Period/InvoiceAt should align to next month start (2025-11-01) for a 2025-10-15 start with 1st-of-month anchor.

Suggested adjustments:

-	clock.FreezeTime(s.mustParseTime("2024-01-20T00:00:00Z")) // This will be the present
+	clock.FreezeTime(s.mustParseTime("2025-10-20T00:00:00Z")) // Present after start; next anchor is 2025-11-01

@@
-			Price: mo.Some(productcatalog.NewPriceFrom(productcatalog.FlatPrice{
-				Amount:      alpacadecimal.NewFromFloat(6),
+			Price: mo.Some(productcatalog.NewPriceFrom(productcatalog.FlatPrice{
+				Amount:      alpacadecimal.NewFromFloat(5),
 				PaymentTerm: productcatalog.InAdvancePaymentTerm,
 			})),
 			Periods: []billing.Period{
 				{
-					Start: s.mustParseTime("2024-02-01T00:00:00Z"),
-					End:   s.mustParseTime("2024-03-01T00:00:00Z"),
+					Start: s.mustParseTime("2025-11-01T00:00:00Z"),
+					End:   s.mustParseTime("2025-12-01T00:00:00Z"),
 				},
 			},
-			InvoiceAt: mo.Some([]time.Time{s.mustParseTime("2024-02-01T00:00:00Z")}),
+			InvoiceAt: mo.Some([]time.Time{s.mustParseTime("2025-11-01T00:00:00Z")}),

Keep it skipped until anchor logic stabilizes, then unskip.

test/billing/collection_test.go (1)

419-419: Duplicate clock reset cleanup.

Line 419 already defers clock.ResetTime(), making the second defer at line 474 redundant.

Apply this diff to remove the redundant defer:

 	// Let's advance time
 	clock.SetTime(periodEnd.Add(time.Hour * 1))
-	defer clock.ResetTime()

Also applies to: 473-474

openmeter/billing/invoice.go (1)

331-337: Method name implies type checking that isn't performed.

DefaultCollectionAtForStandardInvoice() suggests it should only be called for standard invoices, but it doesn't validate i.Type. Consider either:

  1. Adding a type check and returning an error if not a standard invoice
  2. Using a more generic name like DefaultCollectionAt() or CollectionAtOrCreatedAt()
openmeter/ent/db/billingcustomeroverride.go (1)

305-307: String() prints nil anchored detail; align with other pointer fields

For consistency with other pointer fields, gate printing on non-nil to avoid anchored_alignment_detail=<nil> in output.

-	builder.WriteString("anchored_alignment_detail=")
-	builder.WriteString(fmt.Sprintf("%v", _m.AnchoredAlignmentDetail))
-	builder.WriteString(", ")
+	if v := _m.AnchoredAlignmentDetail; v != nil {
+		builder.WriteString("anchored_alignment_detail=")
+		builder.WriteString(fmt.Sprintf("%v", v))
+		builder.WriteString(", ")
+	}
openmeter/billing/workflow.go (3)

44-46: Preserve underlying validation error

Wrap the error instead of discarding it.

-	if err := c.Alignment.Validate(); err != nil {
-		return fmt.Errorf("invalid alignment: %s", c.Alignment)
-	}
+	if err := c.Alignment.Validate(); err != nil {
+		return fmt.Errorf("invalid alignment %q: %w", c.Alignment, err)
+	}

54-62: Clarify anchored detail presence rule and bubble validation

  • Message at Line 56-57 contradicts the condition. Make it explicit that detail is only allowed for anchored alignment.
  • Validation chaining looks good otherwise.
-		if c.Alignment != AlignmentKindAnchored {
-			return fmt.Errorf("anchored alignment detail must be set when alignment is anchored")
-		}
+		if c.Alignment != AlignmentKindAnchored {
+			return fmt.Errorf("anchored alignment detail is only allowed when alignment is %q", AlignmentKindAnchored)
+		}

64-66: Message vs check mismatch

IsPositive() enforces > 0, but the message says "greater or equal to 0". Align the message (or relax the check).

-	return fmt.Errorf("item collection period must be greater or equal to 0")
+	return fmt.Errorf("item collection period must be greater than 0")
openmeter/billing/service/invoicecalc/collectionat.go (1)

59-63: Anchored alignment should use provided anchor/interval and timezone

Current logic always uses “next first day of next month” in collectionAt.Location(), ignoring AnchoredAlignmentDetail (anchor timestamp, interval, tz). This can misalign when a different anchor day/time or timezone is configured.

  • Use i.Workflow.Config.Collection.AnchoredAlignmentDetail.Anchor.Location() (if present) for tz.
  • Compute next anchor from the configured anchor and interval (e.g., “P1M” anchored on day N, time HH:MM).
  • Only fall back to first-of-next-month if no anchored detail provided.

Would you like a patch implementing next-anchor computation from AnchoredAlignmentDetail?

Also applies to: 71-73

test/subscription/scenario_firstofmonth_test.go (1)

397-407: Clarify the variable naming for billing anchor.

The variable beforeFirstOfMonth is set to "2025-06-28T00:00:00Z", which is 3 days before the first of the next month (July 1st), not before the first of the current month. This naming could be misleading to future readers of the test.

Consider renaming to threeDaysBeforeNextAnchor or lateJuneAnchor for clarity, or add a comment explaining why this specific date is used and how it relates to the anchored alignment being tested.

Apply this diff to improve clarity:

 	firstOfNextMonth := testutils.GetRFC3339Time(t, "2025-07-01T00:00:00Z")
-	beforeFirstOfMonth := testutils.GetRFC3339Time(t, "2025-06-28T00:00:00Z")
+	threeDaysBeforeNextAnchor := testutils.GetRFC3339Time(t, "2025-06-28T00:00:00Z") // Billing anchor set before the next collection anchor to test anchored alignment
 	s, err := tDeps.pcSubscriptionService.Create(ctx, pcsubscription.CreateSubscriptionRequest{
 		WorkflowInput: subscriptionworkflow.CreateSubscriptionWorkflowInput{
 			Namespace:  namespace,
 			CustomerID: c.ID,
 			ChangeSubscriptionWorkflowInput: subscriptionworkflow.ChangeSubscriptionWorkflowInput{
 				Timing: subscription.Timing{Custom: &startOfSub},
 				Name:   "Anchored Sub",
 			},
-			BillingAnchor: &beforeFirstOfMonth,
+			BillingAnchor: &threeDaysBeforeNextAnchor,
 		},
openmeter/billing/httpdriver/profile.go (3)

567-605: Anchored interval mapping: verify branch and simplify error returns

  • Verify that FromRecurringPeriodInterval0 is the correct branch for ISO strings produced by c.AnchoredAlignmentDetail.Interval.ISOString(); otherwise intervals may serialize incorrectly.
  • Prefer returning nil on error instead of an empty alignment instance.

Apply:

-       ); err != nil {
-           return &api.BillingWorkflowCollectionAlignment{}, fmt.Errorf("failed to map alignment to API: %w", err)
+       ); err != nil {
+           return nil, fmt.Errorf("failed to map alignment to API: %w", err)
        }
...
-       if c.AnchoredAlignmentDetail == nil {
-           return &api.BillingWorkflowCollectionAlignment{}, fmt.Errorf("anchored alignment detail is not set")
+       if c.AnchoredAlignmentDetail == nil {
+           return nil, fmt.Errorf("anchored alignment detail is not set")
        }
...
-       if err := interval.FromRecurringPeriodInterval0(c.AnchoredAlignmentDetail.Interval.ISOString().String()); err != nil {
-           return &api.BillingWorkflowCollectionAlignment{}, fmt.Errorf("failed to map interval to API: %w", err)
+       if err := interval.FromRecurringPeriodInterval0(c.AnchoredAlignmentDetail.Interval.ISOString().String()); err != nil {
+           return nil, fmt.Errorf("failed to map interval to API: %w", err)
        }
...
-       ); err != nil {
-           return &api.BillingWorkflowCollectionAlignment{}, fmt.Errorf("failed to map alignment to API: %w", err)
+       ); err != nil {
+           return nil, fmt.Errorf("failed to map alignment to API: %w", err)
        }
...
-   default:
-       return &api.BillingWorkflowCollectionAlignment{}, fmt.Errorf("invalid alignment: %s", c.Alignment)
+   default:
+       return nil, fmt.Errorf("invalid alignment: %s", c.Alignment)
    }

638-667: Duplication: consolidate mapWorkflowConfigSettingsToAPI

mapWorkflowConfigSettingsToAPI duplicates mapWorkflowConfigToAPI. Delegate to the existing helper to reduce drift.

Apply:

-func mapWorkflowConfigSettingsToAPI(c billing.WorkflowConfig) (api.BillingWorkflow, error) {
-    apiAlignment, err := mapAlignmentToAPI(c.Collection)
-    if err != nil {
-        return api.BillingWorkflow{}, fmt.Errorf("failed to map alignment to API: %w", err)
-    }
-
-    return api.BillingWorkflow{
-        Collection: &api.BillingWorkflowCollectionSettings{
-            Alignment: apiAlignment,
-            Interval:  lo.EmptyableToPtr(c.Collection.Interval.String()),
-        },
-        Invoicing: &api.BillingWorkflowInvoicingSettings{
-            AutoAdvance:        lo.ToPtr(c.Invoicing.AutoAdvance),
-            DraftPeriod:        lo.EmptyableToPtr(c.Invoicing.DraftPeriod.String()),
-            DueAfter:           lo.EmptyableToPtr(c.Invoicing.DueAfter.String()),
-            ProgressiveBilling: lo.ToPtr(c.Invoicing.ProgressiveBilling),
-            DefaultTaxConfig:   mapTaxConfigToAPI(c.Invoicing.DefaultTaxConfig),
-        },
-        Payment: &api.BillingWorkflowPaymentSettings{
-            CollectionMethod: (*api.CollectionMethod)(lo.EmptyableToPtr(string(c.Payment.CollectionMethod))),
-        },
-        Tax: &api.BillingWorkflowTaxSettings{
-            Enabled:  lo.ToPtr(c.Tax.Enabled),
-            Enforced: lo.ToPtr(c.Tax.Enforced),
-        },
-    }, nil
-}
+func mapWorkflowConfigSettingsToAPI(c billing.WorkflowConfig) (api.BillingWorkflow, error) {
+    return mapWorkflowConfigToAPI(c)
+}

227-227: Nit: operation name pluralization

"UpdateBillingProfiles" should likely be singular ("UpdateBillingProfile") for consistency.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 594f2d6 and b3abeed.

⛔ Files ignored due to path filters (3)
  • api/spec/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • go.sum is excluded by !**/*.sum
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum
📒 Files selected for processing (46)
  • api/client/javascript/scripts/add-as-const.ts (2 hunks)
  • api/client/javascript/src/client/schemas.ts (6 hunks)
  • api/client/javascript/src/zod/index.ts (4 hunks)
  • api/openapi.cloud.yaml (3 hunks)
  • api/openapi.yaml (4 hunks)
  • api/spec/Makefile (1 hunks)
  • api/spec/package.json (2 hunks)
  • api/spec/scripts/update-collection-default.ts (1 hunks)
  • api/spec/src/billing/profile.tsp (3 hunks)
  • api/spec/src/types.tsp (1 hunks)
  • api/spec/tsconfig.json (1 hunks)
  • openmeter/billing/adapter/customeroverride.go (3 hunks)
  • openmeter/billing/adapter/invoice.go (1 hunks)
  • openmeter/billing/adapter/profile.go (5 hunks)
  • openmeter/billing/customeroverride.go (2 hunks)
  • openmeter/billing/httpdriver/invoice.go (1 hunks)
  • openmeter/billing/httpdriver/profile.go (5 hunks)
  • openmeter/billing/invoice.go (3 hunks)
  • openmeter/billing/profile.go (3 hunks)
  • openmeter/billing/service/invoice.go (1 hunks)
  • openmeter/billing/service/invoicecalc/collectionat.go (2 hunks)
  • openmeter/billing/service/invoicecalc/draftuntil.go (1 hunks)
  • openmeter/billing/service/invoicestate.go (1 hunks)
  • openmeter/billing/worker/subscription/sync_test.go (1 hunks)
  • openmeter/billing/workflow.go (1 hunks)
  • openmeter/ent/db/billingcustomeroverride.go (4 hunks)
  • openmeter/ent/db/billingcustomeroverride/billingcustomeroverride.go (3 hunks)
  • openmeter/ent/db/billingcustomeroverride/where.go (1 hunks)
  • openmeter/ent/db/billingcustomeroverride_create.go (5 hunks)
  • openmeter/ent/db/billingcustomeroverride_update.go (4 hunks)
  • openmeter/ent/db/billingworkflowconfig.go (4 hunks)
  • openmeter/ent/db/billingworkflowconfig/billingworkflowconfig.go (3 hunks)
  • openmeter/ent/db/billingworkflowconfig/where.go (1 hunks)
  • openmeter/ent/db/billingworkflowconfig_create.go (6 hunks)
  • openmeter/ent/db/billingworkflowconfig_update.go (6 hunks)
  • openmeter/ent/db/migrate/schema.go (4 hunks)
  • openmeter/ent/db/mutation.go (20 hunks)
  • openmeter/ent/db/runtime.go (1 hunks)
  • openmeter/ent/db/setorclear.go (2 hunks)
  • openmeter/ent/schema/billing.go (2 hunks)
  • test/billing/collection_config_test.go (1 hunks)
  • test/billing/collection_test.go (1 hunks)
  • test/billing/profile_test.go (3 hunks)
  • test/subscription/scenario_firstofmonth_test.go (1 hunks)
  • tools/migrate/migrations/20251014140637_first-day-of-month-billing.down.sql (1 hunks)
  • tools/migrate/migrations/20251014140637_first-day-of-month-billing.up.sql (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (32)
api/client/javascript/src/client/schemas.ts (2)
api/api.gen.go (2)
  • BillingWorkflowCollectionAlignmentAnchored (1580-1586)
  • RecurringPeriodV2 (6447-6453)
api/client/go/client.gen.go (2)
  • BillingWorkflowCollectionAlignmentAnchored (1412-1418)
  • RecurringPeriodV2 (5924-5930)
openmeter/ent/schema/billing.go (1)
openmeter/billing/profile.go (2)
  • AnchoredAlignmentDetail (62-65)
  • AnchoredAlignmentDetailOption (57-60)
openmeter/billing/worker/subscription/sync_test.go (9)
pkg/clock/clock.go (3)
  • FreezeTime (33-36)
  • UnFreeze (38-40)
  • Now (14-21)
openmeter/productcatalog/plan/service.go (1)
  • CreatePlanInput (105-110)
pkg/models/model.go (1)
  • NamespacedModel (204-206)
openmeter/productcatalog/plan.go (2)
  • Plan (40-45)
  • PlanMeta (197-223)
openmeter/productcatalog/pro_rating.go (2)
  • ProRatingConfig (23-29)
  • ProRatingModeProratePrices (11-11)
openmeter/productcatalog/ratecard.go (3)
  • RateCards (517-517)
  • UsageBasedRateCard (382-388)
  • RateCardMeta (54-85)
openmeter/productcatalog/price.go (4)
  • Price (86-93)
  • NewPriceFrom (369-391)
  • FlatPrice (420-427)
  • InAdvancePaymentTerm (20-20)
openmeter/subscription/workflow/service.go (2)
  • CreateSubscriptionWorkflowInput (23-29)
  • ChangeSubscriptionWorkflowInput (31-38)
openmeter/subscription/timing.go (1)
  • Timing (14-17)
openmeter/billing/invoice.go (2)
openmeter/ent/db/billinginvoice/where.go (1)
  • CollectionAt (321-323)
api/api.gen.go (1)
  • InvoiceStatusGathering (466-466)
openmeter/ent/db/billingworkflowconfig/where.go (6)
openmeter/ent/db/billingcustomeroverride/where.go (2)
  • AnchoredAlignmentDetailIsNil (504-506)
  • AnchoredAlignmentDetailNotNil (509-511)
openmeter/ent/db/billingworkflowconfig.go (2)
  • BillingWorkflowConfig (22-60)
  • BillingWorkflowConfig (96-113)
openmeter/ent/schema/billing.go (5)
  • BillingWorkflowConfig (108-110)
  • BillingWorkflowConfig (112-118)
  • BillingWorkflowConfig (120-154)
  • BillingWorkflowConfig (156-160)
  • BillingWorkflowConfig (162-169)
openmeter/ent/db/predicate/predicate.go (1)
  • BillingWorkflowConfig (150-150)
openmeter/ent/db/billingcustomeroverride/billingcustomeroverride.go (1)
  • FieldAnchoredAlignmentDetail (34-34)
openmeter/ent/db/billingworkflowconfig/billingworkflowconfig.go (1)
  • FieldAnchoredAlignmentDetail (30-30)
openmeter/ent/db/billingcustomeroverride/where.go (4)
openmeter/ent/db/billingworkflowconfig/where.go (2)
  • AnchoredAlignmentDetailIsNil (354-356)
  • AnchoredAlignmentDetailNotNil (359-361)
openmeter/ent/db/billingcustomeroverride.go (2)
  • BillingCustomerOverride (22-60)
  • BillingCustomerOverride (96-113)
openmeter/ent/db/predicate/predicate.go (1)
  • BillingCustomerOverride (62-62)
openmeter/ent/db/billingcustomeroverride/billingcustomeroverride.go (1)
  • FieldAnchoredAlignmentDetail (34-34)
openmeter/billing/httpdriver/invoice.go (1)
api/api.gen.go (2)
  • Invoice (3838-3952)
  • InvoiceWorkflowSettings (4849-4861)
openmeter/billing/customeroverride.go (2)
openmeter/billing/profile.go (2)
  • AlignmentKind (21-21)
  • AnchoredAlignmentDetail (62-65)
pkg/datetime/duration.go (1)
  • ISODuration (14-16)
test/subscription/scenario_firstofmonth_test.go (14)
openmeter/testutils/time.go (1)
  • GetRFC3339Time (8-15)
pkg/clock/clock.go (1)
  • SetTime (23-27)
openmeter/productcatalog/plan/service.go (2)
  • CreatePlanInput (105-110)
  • PublishPlanInput (353-358)
openmeter/productcatalog/plan.go (2)
  • Plan (40-45)
  • PlanMeta (197-223)
pkg/datetime/testutils.go (1)
  • MustParseDuration (38-45)
openmeter/productcatalog/ratecard.go (3)
  • RateCards (517-517)
  • UsageBasedRateCard (382-388)
  • RateCardMeta (54-85)
openmeter/productcatalog/price.go (2)
  • NewPriceFrom (369-391)
  • InArrearsPaymentTerm (21-21)
openmeter/billing/profile.go (2)
  • AlignmentKindAnchored (32-32)
  • AnchoredAlignmentDetail (62-65)
openmeter/customer/customer.go (2)
  • CreateCustomerInput (234-237)
  • CustomerMutate (74-84)
openmeter/billing/invoice.go (6)
  • CustomerUsageAttribution (663-663)
  • ListInvoicesInput (826-867)
  • InvoiceExpandAll (232-236)
  • Invoice (339-350)
  • InvoiceStatusGathering (75-75)
  • InvoiceStatusDraftWaitingForCollection (79-79)
openmeter/subscription/workflow/service.go (2)
  • CreateSubscriptionWorkflowInput (23-29)
  • ChangeSubscriptionWorkflowInput (31-38)
openmeter/subscription/timing.go (1)
  • Timing (14-17)
openmeter/ent/db/billinginvoice/where.go (1)
  • CollectionAt (321-323)
openmeter/ent/db/billinginvoiceline/where.go (1)
  • InvoiceAt (163-165)
openmeter/billing/adapter/invoice.go (1)
openmeter/ent/db/billinginvoice/where.go (1)
  • CollectionAt (321-323)
openmeter/billing/workflow.go (2)
openmeter/billing/profile.go (3)
  • AlignmentKind (21-21)
  • AnchoredAlignmentDetail (62-65)
  • AlignmentKindAnchored (32-32)
pkg/datetime/duration.go (1)
  • ISODuration (14-16)
openmeter/ent/db/billingworkflowconfig/billingworkflowconfig.go (1)
openmeter/ent/db/billingcustomeroverride/billingcustomeroverride.go (1)
  • FieldAnchoredAlignmentDetail (34-34)
openmeter/billing/service/invoicecalc/collectionat.go (1)
openmeter/billing/profile.go (2)
  • AlignmentKindSubscription (28-28)
  • AlignmentKindAnchored (32-32)
openmeter/ent/db/billingworkflowconfig_update.go (4)
openmeter/billing/profile.go (1)
  • AnchoredAlignmentDetail (62-65)
pkg/models/validator.go (1)
  • Validate (16-26)
openmeter/ent/db/ent.go (1)
  • ValidationError (260-263)
openmeter/ent/db/billingworkflowconfig/billingworkflowconfig.go (1)
  • FieldAnchoredAlignmentDetail (30-30)
openmeter/ent/db/billingcustomeroverride/billingcustomeroverride.go (1)
openmeter/ent/db/billingworkflowconfig/billingworkflowconfig.go (1)
  • FieldAnchoredAlignmentDetail (30-30)
openmeter/ent/db/billingcustomeroverride.go (2)
openmeter/billing/profile.go (2)
  • AnchoredAlignmentDetail (62-65)
  • AnchoredAlignmentDetailOption (57-60)
openmeter/ent/db/billingcustomeroverride/billingcustomeroverride.go (2)
  • FieldAnchoredAlignmentDetail (34-34)
  • FieldInvoiceDefaultTaxConfig (48-48)
test/billing/collection_config_test.go (3)
openmeter/billing/workflow.go (1)
  • CollectionConfig (37-41)
openmeter/billing/profile.go (2)
  • AlignmentKindSubscription (28-28)
  • AnchoredAlignmentDetail (62-65)
pkg/datetime/testutils.go (1)
  • MustParseDuration (38-45)
openmeter/billing/service/invoicestate.go (1)
api/api.gen.go (1)
  • Invoice (3838-3952)
openmeter/billing/adapter/profile.go (3)
openmeter/ent/db/billingworkflowconfig.go (2)
  • BillingWorkflowConfig (22-60)
  • BillingWorkflowConfig (96-113)
openmeter/ent/schema/billing.go (5)
  • BillingWorkflowConfig (108-110)
  • BillingWorkflowConfig (112-118)
  • BillingWorkflowConfig (120-154)
  • BillingWorkflowConfig (156-160)
  • BillingWorkflowConfig (162-169)
openmeter/billing/profile.go (1)
  • AnchoredAlignmentDetail (62-65)
test/billing/collection_test.go (10)
pkg/clock/clock.go (1)
  • SetTime (23-27)
test/billing/suite.go (1)
  • WithBillingProfileEditFn (533-537)
openmeter/billing/profile.go (3)
  • CreateProfileInput (330-340)
  • AlignmentKindAnchored (32-32)
  • AnchoredAlignmentDetail (62-65)
pkg/datetime/durationstring.go (1)
  • ISODurationString (8-8)
openmeter/customer/customer.go (2)
  • CreateCustomerInput (234-237)
  • CustomerMutate (74-84)
pkg/models/model.go (3)
  • Address (220-228)
  • CountryCode (231-231)
  • ManagedResource (23-31)
openmeter/billing/invoice.go (2)
  • CustomerUsageAttribution (663-663)
  • InvoicePendingLinesInput (974-983)
openmeter/billing/invoiceline.go (7)
  • CreatePendingInvoiceLinesInput (871-876)
  • Line (311-325)
  • LineBase (128-157)
  • Period (83-86)
  • ManuallyManagedLine (69-69)
  • InvoiceLineTypeUsageBased (35-35)
  • UsageBasedLine (804-817)
openmeter/productcatalog/price.go (1)
  • NewPriceFrom (369-391)
openmeter/ent/db/billinginvoice/where.go (1)
  • CollectionAt (321-323)
openmeter/ent/db/billingworkflowconfig_create.go (4)
openmeter/billing/profile.go (1)
  • AnchoredAlignmentDetail (62-65)
pkg/models/validator.go (1)
  • Validate (16-26)
openmeter/ent/db/ent.go (1)
  • ValidationError (260-263)
openmeter/ent/db/billingworkflowconfig/billingworkflowconfig.go (1)
  • FieldAnchoredAlignmentDetail (30-30)
openmeter/ent/db/billingcustomeroverride_update.go (2)
openmeter/billing/profile.go (2)
  • AnchoredAlignmentDetailOption (57-60)
  • AnchoredAlignmentDetail (62-65)
openmeter/ent/db/billingcustomeroverride/billingcustomeroverride.go (1)
  • FieldAnchoredAlignmentDetail (34-34)
openmeter/ent/db/runtime.go (1)
openmeter/ent/db/billingworkflowconfig/billingworkflowconfig.go (1)
  • DefaultTaxEnabled (111-111)
openmeter/billing/profile.go (3)
pkg/models/validator.go (1)
  • Validate (16-26)
pkg/datetime/duration.go (1)
  • ISODuration (14-16)
openmeter/billing/workflow.go (1)
  • WorkflowConfig (9-14)
openmeter/ent/db/setorclear.go (3)
openmeter/ent/db/billingcustomeroverride_update.go (2)
  • BillingCustomerOverrideUpdate (23-27)
  • BillingCustomerOverrideUpdateOne (445-450)
openmeter/billing/profile.go (2)
  • AnchoredAlignmentDetailOption (57-60)
  • AnchoredAlignmentDetail (62-65)
openmeter/ent/db/billingworkflowconfig_update.go (2)
  • BillingWorkflowConfigUpdate (24-28)
  • BillingWorkflowConfigUpdateOne (467-472)
openmeter/ent/db/billingcustomeroverride_create.go (3)
openmeter/billing/profile.go (2)
  • AnchoredAlignmentDetailOption (57-60)
  • AnchoredAlignmentDetail (62-65)
openmeter/ent/db/billingcustomeroverride/billingcustomeroverride.go (1)
  • FieldAnchoredAlignmentDetail (34-34)
openmeter/ent/db/billingworkflowconfig/billingworkflowconfig.go (1)
  • FieldAnchoredAlignmentDetail (30-30)
openmeter/billing/adapter/customeroverride.go (1)
openmeter/billing/profile.go (2)
  • AnchoredAlignmentDetailOption (57-60)
  • AnchoredAlignmentDetail (62-65)
openmeter/ent/db/migrate/schema.go (2)
openmeter/ent/db/billingcustomeroverride/billingcustomeroverride.go (1)
  • Columns (72-89)
openmeter/ent/db/billingworkflowconfig/billingworkflowconfig.go (1)
  • Columns (72-89)
openmeter/ent/db/mutation.go (3)
openmeter/billing/profile.go (2)
  • AnchoredAlignmentDetailOption (57-60)
  • AnchoredAlignmentDetail (62-65)
openmeter/ent/db/billingcustomeroverride/billingcustomeroverride.go (1)
  • FieldAnchoredAlignmentDetail (34-34)
openmeter/ent/db/billingworkflowconfig/billingworkflowconfig.go (1)
  • FieldAnchoredAlignmentDetail (30-30)
openmeter/billing/httpdriver/profile.go (3)
openmeter/billing/workflow.go (2)
  • WorkflowConfig (9-14)
  • CollectionConfig (37-41)
api/api.gen.go (11)
  • BillingWorkflowCollectionAlignment (1574-1576)
  • Alignment (1040-1045)
  • BillingWorkflowCollectionAlignmentSubscription (1593-1596)
  • BillingWorkflowCollectionAlignmentSubscriptionType (1599-1599)
  • RecurringPeriodInterval (6435-6437)
  • BillingWorkflowCollectionAlignmentAnchored (1580-1586)
  • BillingWorkflowCollectionAlignmentAnchoredType (1589-1589)
  • RecurringPeriod (6414-6423)
  • RecurringPeriodV2 (6447-6453)
  • BillingWorkflow (1553-1565)
  • BillingWorkflowCollectionSettings (1602-1611)
openmeter/billing/profile.go (3)
  • AlignmentKindSubscription (28-28)
  • AlignmentKindAnchored (32-32)
  • AnchoredAlignmentDetail (62-65)
test/billing/profile_test.go (3)
openmeter/billing/profile.go (7)
  • AlignmentKindAnchored (32-32)
  • AnchoredAlignmentDetail (62-65)
  • AlignmentKindSubscription (28-28)
  • GetProfileInput (417-420)
  • Profile (230-235)
  • ProfileID (176-176)
  • ProfileExpandAll (397-399)
openmeter/ent/db/usagereset/where.go (2)
  • Anchor (100-102)
  • Namespace (70-72)
pkg/clock/clock.go (1)
  • Now (14-21)
openmeter/ent/db/billingworkflowconfig.go (3)
openmeter/billing/profile.go (1)
  • AnchoredAlignmentDetail (62-65)
openmeter/ent/db/billingcustomeroverride/billingcustomeroverride.go (1)
  • FieldAnchoredAlignmentDetail (34-34)
openmeter/ent/db/billingworkflowconfig/billingworkflowconfig.go (2)
  • FieldAnchoredAlignmentDetail (30-30)
  • FieldInvoiceDefaultTaxSettings (44-44)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (go)
🔇 Additional comments (33)
openmeter/ent/schema/billing.go (1)

208-213: Override uses Option type correctly for tri‑state; verify mapping tests

The Option wrapper enables unset vs cleared vs value. Ensure adapters/tests assert:

  • nil column = no override
  • present JSON with HasValue=false = explicit clear
  • HasValue=true with Value set = override value
openmeter/ent/db/runtime.go (1)

794-800: Descriptor indices updated correctly after schema change

Indices now match the field order (tax_enabled at 9, tax_enforced at 10).

openmeter/ent/db/billingcustomeroverride/billingcustomeroverride.go (1)

33-35: Anchored alignment wiring looks consistent

Constant, column list, and enum validator updates align with the new alignment mode.

Also applies to: 81-82, 117-118

tools/migrate/migrations/20251014140637_first-day-of-month-billing.up.sql (1)

1-4: Anchored alignment columns added — looks good; confirm down migration and ORM wiring.

Schema change matches the new ent constant FieldAnchoredAlignmentDetail. Please verify:

  • A matching down migration exists for both tables.
  • Ent model scan/assign for anchored_alignment_detail is in place (JSON unmarshal).
openmeter/billing/service/invoicecalc/draftuntil.go (1)

17-18: DraftUntil normalization — LGTM.

Ensures draftUntil never precedes CreatedAt.

api/spec/package.json (1)

21-33: Dev deps OK; align Node types with CI Node version.

@types/node 24.x and TS 5.6 are fine; ensure CI/node runtime matches to avoid type/lib mismatches.

openmeter/ent/db/billingworkflowconfig/billingworkflowconfig.go (1)

29-31: Ent wiring for anchored alignment — aligned; confirm JSON mapping generated.

Constants/columns and enum accept "anchored" — good. Ensure the generated struct includes AnchoredAlignmentDetail and assignValues unmarshals JSONB correctly (as indicated in the PR).

Also applies to: 79-79, 121-121

test/billing/collection_config_test.go (1)

29-31: Verify validation error message clarity.

The error message "anchored alignment detail must be set when alignment is anchored" is confusing in this context where alignment is NOT anchored. A clearer message would be: "anchored alignment detail must only be set when alignment is anchored" or "anchored alignment detail is not allowed for subscription alignment".

Please verify that the validation logic in openmeter/billing/workflow.go produces a clear error message for this case.

openmeter/ent/db/billingcustomeroverride.go (1)

174-181: JSON unmarshalling pattern looks correct

Unmarshalling into &AnchoredAlignmentDetail (a **T) correctly allocates on non-null JSON. No issues.

openmeter/ent/db/migrate/schema.go (1)

498-500: Schema additions for anchored mode look consistent

Enums include "anchored" and JSONB columns are added for details. Indices/FKs updated accordingly. LGTM.

Please confirm tools/migrate SQL migrations match these shapes (column order, nullability, enum values) to avoid drift at deploy time.

Also applies to: 1244-1246

test/subscription/scenario_firstofmonth_test.go (1)

323-453: LGTM! Well-structured test for anchored alignment.

The test effectively validates the anchored alignment feature by:

  • Creating a subscription mid-month with a billing anchor set before the next collection anchor
  • Cancelling the subscription early
  • Verifying that lines remain on the gathering invoice until the next anchored period (July 1st)
  • Asserting that each line's InvoiceAt timestamp is before the invoice's CollectionAt timestamp

This provides good coverage for the "first-of-month" billing scenario where collection happens on a fixed anchor regardless of subscription timing.

openmeter/ent/db/setorclear.go (1)

437-449: LGTM! Generated code follows established patterns.

The new SetOrClearAnchoredAlignmentDetail methods are correctly implemented and follow the existing SetOrClear pattern. The type differences are appropriate:

  • BillingCustomerOverride uses **billing.AnchoredAlignmentDetailOption to support three states: not set, explicitly cleared, or set with a value (useful for overrides)
  • BillingWorkflowConfig uses **billing.AnchoredAlignmentDetail for simpler nullable field semantics

Note: This is generated code (as indicated by line 1), so manual changes should be made to the generator templates if modifications are needed.

Also applies to: 1809-1821

test/billing/profile_test.go (2)

312-314: LGTM! Proper timestamp normalization.

Good practice to truncate and convert to UTC before comparison. This eliminates issues with:

  • Monotonic time readings (removed by Truncate)
  • Timezone differences (normalized by UTC())
  • Sub-nanosecond precision differences

481-504: LGTM! Good test coverage for subscription alignment.

This test validates that:

  • Profiles with AlignmentKindSubscription persist the alignment correctly
  • AnchoredAlignmentDetail is nil when not using anchored alignment mode

This complements the existing test coverage and ensures the two alignment modes work correctly.

openmeter/ent/db/billingcustomeroverride_update.go (1)

101-111: LGTM! Generated code correctly implements persistence.

The setter, clearer, and persistence logic for AnchoredAlignmentDetail follows the established patterns for other fields in this generated file. The field is stored as JSON in the database and properly handled during both create and update operations.

Also applies to: 518-528, 355-360, 802-807

openmeter/billing/adapter/profile.go (1)

97-99: LGTM! Consistent adapter mappings for anchored alignment.

The changes correctly handle AnchoredAlignmentDetail throughout the profile adapter:

  • Create path (lines 97-99): Conditionally sets the field only when non-nil, avoiding unnecessary DB writes
  • Update path (line 372): Always sets the field (allows clearing if nil)
  • Mapping functions (lines 439, 468-470): Bidirectional mapping between domain and DB models

The implementation is consistent with how other optional fields are handled in this adapter.

Also applies to: 372-372, 439-439, 468-470

openmeter/billing/profile.go (3)

56-61: AnchoredAlignmentDetailOption is actively used—no removal needed.

The type is heavily referenced throughout the codebase: it's used in the ent schema definition (openmeter/ent/schema/billing.go:208), generated extensively in the ent database layer (openmeter/ent/db/), and actively instantiated in application code (openmeter/billing/adapter/customeroverride.go:41-47). This confirms it's necessary and not dead code.


265-280: CollectionConfig.Validate enforces AnchoredAlignmentDetail presence for anchored alignment
The Validate method in billing/workflow.go errors if AlignmentKindAnchored is set without AnchoredAlignmentDetail (and vice versa), including running the detail’s Validate().


41-47: No change needed for slices.Contains usage

go.mod declares Go 1.24.1 and the toolchain is Go 1.25.1, both ≥1.21.

api/spec/src/types.tsp (1)

252-268: The code is already correct—review comment is based on outdated information

According to official TypeSpec documentation, #deprecated is the correct directive for marking models as deprecated. The @deprecated decorator was actually deprecated and removed in favor of #deprecated. The current code at line 252 uses the correct syntax and does not require changes.

Likely an incorrect or invalid review comment.

openmeter/ent/db/billingworkflowconfig_update.go (1)

76-86: AnchoredAlignmentDetail mutation and validation LGTM; prefer Clear for null intent

  • Set/Clear methods and JSON wiring are correct; Validate() is nil-safe.
  • Schema marks anchored_alignment_detail as Optional, so both Set(nil) (JSON null) and Clear() (SQL clear) yield null—use ClearAnchoredAlignmentDetail() for intent clarity.
api/client/javascript/src/client/schemas.ts (4)

3207-3220: LGTM! Anchored alignment union member properly added.

The BillingWorkflowCollectionAlignmentAnchored schema is correctly added to the union type with proper type discrimination. The structure aligns with the Go implementation shown in the relevant code snippets.


9510-9510: LGTM! Proper deprecation of legacy schema.

The deprecation marker correctly signals that RecurringPeriod is superseded by RecurringPeriodV2.


11503-11504: LGTM! New types properly exported.

The exports for BillingWorkflowCollectionAlignmentAnchored and RecurringPeriodV2 follow the established pattern and make the new types available to API consumers.

Also applies to: 11928-11928


9531-9535: ****

The exclusion of intervalISO from RecurringPeriodV2 is intentional and properly documented. The OpenAPI specs explicitly define RecurringPeriodV2 without intervalISO, and the interval field was enhanced with the description: "Heuristically maps ISO durations to enum values or returns the ISO duration." This is the intended replacement for the separate intervalISO field in the deprecated RecurringPeriod. Since no TypeScript/JavaScript code in the codebase references intervalISO, the change is safe and represents a clean deprecation path.

openmeter/billing/httpdriver/profile.go (1)

425-428: Good: precompute workflow mapping with error handling

Pre-mapping and propagating errors improves clarity and robustness. LGTM.

openmeter/billing/adapter/customeroverride.go (2)

41-51: Create path: anchored detail mapping LGTM

Converts mo.Option to domain option correctly and persists via ent.


424-431: DB→API mo.Option mapping LGTM

Nil-safe and preserves HasValue semantics via mo.Some/None.

api/spec/src/billing/profile.tsp (2)

324-324: Default for discriminated union: ensure OpenAPI/codegen preserves it

Setting alignment default via constant is correct in TypeSpec, but OpenAPI generators often drop defaults on unions. Confirm your patch script ensures the default appears in the emitted schema and client generators honor it.

Also applies to: 356-359


367-372: Anchored alignment union and models LGTM

Discriminated union, anchored model, and subscription type updates look consistent.

Also applies to: 373-390, 402-402

openmeter/ent/db/billingcustomeroverride_create.go (1)

113-117: Ent mutations for AnchoredAlignmentDetail LGTM; confirm JSON encoding

  • Create and upsert methods for anchored detail are wired correctly.
  • Ensure the JSON codec for billing.AnchoredAlignmentDetailOption is registered/serializable as expected by ent (no custom Marshaler needed). If not, add proper (Un)MarshalJSON.

Also applies to: 383-386, 567-584, 845-864, 1314-1334

openmeter/ent/db/billingworkflowconfig_create.go (1)

85-90: WorkflowConfig anchored detail support LGTM; validate presence when anchored

  • Create/upsert wiring and Validate() hook are good.
  • Add a higher-level validation: when collection_alignment == anchored, anchored_alignment_detail must be present; otherwise reject config. Implement in domain/service layer (preferably), not in generated ent.

Also applies to: 302-306, 397-400, 563-580, 798-818, 1222-1242

openmeter/ent/db/mutation.go (1)

10916-10916: LGTM: Mechanical mutation implementation is correct.

All field registration, accessor, and mutator logic follows ent's standard patterns:

  • Field count updated to 15
  • All method switches properly route to the new field's getters/setters/clearers
  • Type assertions match the declared field types
  • Cleared-field tracking is correct

The implementation is mechanically sound, assuming the type difference between the two mutations is intentional (see previous comment).

Also applies to: 10938-10940, 10984-10985, 11023-11024, 11097-11103, 11192-11194, 11239-11241, 11292-11294, 28807-28807, 28823-28825, 28871-28872, 28910-28911, 28974-28980, 29077-29079, 29100-29102, 29129-29131

@GAlexIHU GAlexIHU force-pushed the feat/invoicing/first-day-of-month branch from 2129ef2 to 4dcb3a1 Compare October 31, 2025 16:10
@GAlexIHU GAlexIHU requested a review from turip October 31, 2025 16:55
@GAlexIHU GAlexIHU force-pushed the feat/invoicing/first-day-of-month branch from cee8b2b to 3184c87 Compare November 3, 2025 14:04
@GAlexIHU GAlexIHU merged commit da70866 into main Nov 3, 2025
26 checks passed
@GAlexIHU GAlexIHU deleted the feat/invoicing/first-day-of-month branch November 3, 2025 14:14
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.

4 participants