-
Notifications
You must be signed in to change notification settings - Fork 143
feat: first day of month billing #3519
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
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 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. ⛔ Files ignored due to path filters (11)
📒 Files selected for processing (33)
📝 WalkthroughWalkthroughIntroduces 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
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)
✅ Passed checks (2 passed)
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. Comment |
There was a problem hiding this 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 idempotentGood 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:fixopenmeter/ent/schema/billing.go (1)
126-131: Anchored detail storage type looks right; confirm null/clear semanticsUsing 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 migrationsOptional: 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 validatei.Type. Consider either:
- Adding a type check and returning an error if not a standard invoice
- Using a more generic name like
DefaultCollectionAt()orCollectionAtOrCreatedAt()openmeter/ent/db/billingcustomeroverride.go (1)
305-307: String() prints nil anchored detail; align with other pointer fieldsFor 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 errorWrap 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 timezoneCurrent logic always uses “next first day of next month” in
collectionAt.Location(), ignoringAnchoredAlignmentDetail(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
beforeFirstOfMonthis 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
threeDaysBeforeNextAnchororlateJuneAnchorfor 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 mapWorkflowConfigSettingsToAPImapWorkflowConfigSettingsToAPI 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
⛔ Files ignored due to path filters (3)
api/spec/pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlgo.sumis excluded by!**/*.sumtools/migrate/migrations/atlas.sumis 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 testsThe 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 changeIndices 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 consistentConstant, 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.goproduces a clear error message for this case.openmeter/ent/db/billingcustomeroverride.go (1)
174-181: JSON unmarshalling pattern looks correctUnmarshalling 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 consistentEnums 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
SetOrClearAnchoredAlignmentDetailmethods are correctly implemented and follow the existing SetOrClear pattern. The type differences are appropriate:
BillingCustomerOverrideuses**billing.AnchoredAlignmentDetailOptionto support three states: not set, explicitly cleared, or set with a value (useful for overrides)BillingWorkflowConfiguses**billing.AnchoredAlignmentDetailfor simpler nullable field semanticsNote: 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
AlignmentKindSubscriptionpersist the alignment correctlyAnchoredAlignmentDetailis nil when not using anchored alignment modeThis 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
AnchoredAlignmentDetailfollows 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
AnchoredAlignmentDetailthroughout 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 usagego.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 informationAccording to official TypeSpec documentation,
#deprecatedis the correct directive for marking models as deprecated. The@deprecateddecorator 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_detailas Optional, so bothSet(nil)(JSON null) andClear()(SQL clear) yield null—useClearAnchoredAlignmentDetail()for intent clarity.api/client/javascript/src/client/schemas.ts (4)
3207-3220: LGTM! Anchored alignment union member properly added.The
BillingWorkflowCollectionAlignmentAnchoredschema 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
RecurringPeriodis superseded byRecurringPeriodV2.
11503-11504: LGTM! New types properly exported.The exports for
BillingWorkflowCollectionAlignmentAnchoredandRecurringPeriodV2follow the established pattern and make the new types available to API consumers.Also applies to: 11928-11928
9531-9535: ****The exclusion of
intervalISOfromRecurringPeriodV2is intentional and properly documented. The OpenAPI specs explicitly defineRecurringPeriodV2withoutintervalISO, and theintervalfield was enhanced with the description: "Heuristically maps ISO durations to enum values or returns the ISO duration." This is the intended replacement for the separateintervalISOfield in the deprecatedRecurringPeriod. Since no TypeScript/JavaScript code in the codebase referencesintervalISO, the change is safe and represents a clean deprecation path.openmeter/billing/httpdriver/profile.go (1)
425-428: Good: precompute workflow mapping with error handlingPre-mapping and propagating errors improves clarity and robustness. LGTM.
openmeter/billing/adapter/customeroverride.go (2)
41-51: Create path: anchored detail mapping LGTMConverts mo.Option to domain option correctly and persists via ent.
424-431: DB→API mo.Option mapping LGTMNil-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 itSetting 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 LGTMDiscriminated 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
2129ef2 to
4dcb3a1
Compare
cee8b2b to
3184c87
Compare
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)Summary by CodeRabbit
Release Notes
New Features
Improvements