Skip to content

Conversation

@GAlexIHU
Copy link
Contributor

@GAlexIHU GAlexIHU commented Nov 4, 2025

Overview

Removes anything subject related from entitlements

  • backfills V1 APIs
  • drops columns in DB

⚠️ This changes linking from db.Subject to db.CustomerSubject which assumes our database is consistent ⚠️
(currently those two aren't hard linked in the db. if we want to be paranoid we can preemptively hard-link the two)

Changes how customers - subjects - entitlements interact, so its more in line with how it will work after multi-subjects. For the functional spec see /tests/customer/subject, but TLDR

  • we can have dangling subjects (not referenced by a customer)
  • subjects can be deleted even if the customer has entitlements
  • subjects can be deleted directly when referenced in CustomerUsageAttribution (instead of conflict error)

@GAlexIHU GAlexIHU added the release-note/feature Release note: Exciting New Features label Nov 4, 2025
@GAlexIHU GAlexIHU requested a review from a team as a code owner November 4, 2025 16:31
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 4, 2025

📝 Walkthrough

Walkthrough

This PR removes subject-based validation and tracking from the entitlement system, pivoting to a customer-centric model. It deletes validator hooks, eliminates subject fields from the entitlement schema, updates adapters and service logic, introduces an entitlement reset event v2 with customer attribution, and adjusts subject deletion behavior to clean up customer associations.

Changes

Cohort / File(s) Summary
Hook wiring cleanup
app/common/customer.go, app/common/subject.go, cmd/server/wire.go, cmd/server/wire_gen.go
Removed CustomerSubjectValidatorHook and SubjectEntitlementValidatorHook constructors, field declarations, and wire initialization logic; refactored wire-gen variable slot assignments.
Customer model & errors
openmeter/customer/customer.go, openmeter/customer/errors.go
Added AsCustomerMutate() method on Customer; renamed GetSubjectKey() to GetFirstSubjectKey() on CustomerUsageAttribution (returns first sorted key or error); removed ErrCustomerSubjectKeyNotSingular error.
Customer service hooks
openmeter/customer/service/hooks/subjectcustomer.go, openmeter/customer/service/hooks/subjectvalidator.go, openmeter/customer/service/hooks/subjectvalidator_test.go
Added PostDelete hook on SubjectCustomerHook to clean up customer usage attribution when subjects are deleted; removed entire SubjectValidatorHook component and its test suite.
Entitlement schema & edges
openmeter/ent/schema/entitlement.go, openmeter/ent/schema/subject.go
Removed subject_id and subject_key fields from Entitlement; dropped corresponding indexes and edge relationships; removed entitlements edge from Subject.
Entitlement adapter & queries
openmeter/entitlement/adapter/entitlement.go, openmeter/entitlement/adapter/entitlement_test.go
Removed WithSubject() from all entitlement queries, subject resolution during creation, and subject mapping in mapEntitlementEntity; removed subject package imports.
Entitlement balance worker
openmeter/entitlement/balanceworker/subject_customer.go, openmeter/entitlement/balanceworker/worker.go
Changed GetSubjectKey() to GetFirstSubjectKey() in customer usage attribution flow; added handler for EntitlementResetEventV2.
Entitlement model & events
openmeter/entitlement/entitlement.go, openmeter/entitlement/errors.go, openmeter/entitlement/events.go
Removed SubjectKey and Subject fields from GenericProperties; renamed AlreadyExistsError.SubjectKey to CustomerID; removed subject propagation from domain model conversion.
Entitlement reset event v2
openmeter/entitlement/metered/events.go, openmeter/entitlement/metered/reset.go
Introduced EntitlementResetEventV2 with CustomerID and CustomerUsageAttribution fields; updated reset event construction to use v2 payload with customer data instead of subject.
Entitlement driver & service
openmeter/entitlement/driver/parser.go, openmeter/entitlement/service/scheduling.go
Updated subject key retrieval to use GetFirstSubjectKey() with fallback to empty string; updated ScheduleEntitlement error payload to include CustomerID.
Subject service & hooks
openmeter/subject/adapter/subject.go, openmeter/subject/service/hooks/entitlementvalidator.go, openmeter/subject/service/service_test.go
Removed entitlement-aware deletion constraints and EntitlementValidatorHook component; updated Delete test to expect success when entitlements exist (as they now belong to customer, not subject).
Subscription validation
openmeter/subscription/service/servicevalidation.go, openmeter/subscription/testutils/service.go
Removed subject-existence validation checks for entitlements and metered billables; removed hook creation and registration in test utilities.
Test suite & environment setup
test/billing/suite.go, test/customer/testenv.go
Removed entitlement and customer subject validator hook creation; added SubjectCustomerHook and CustomerSubjectHook wiring to test environment.
Customer test coverage
test/customer/customer.go, test/customer/customer_test.go, test/customer/subject.go
Added subtests for subject creation alongside customers and subject deletion scenarios; introduced TestSubjectDeletion to verify usage attribution cleanup.
Entitlement test updates
test/entitlement/regression/scenario_test.go, test/notification/consumer_balance.go, test/notification/testenv.go, test/notification/consumer_balance.go
Updated GetSubjectKey() calls to GetFirstSubjectKey(); added Customer field to entitlement snapshot events; removed SubjectKey from test data; added TestCustomerID constant.
Database migrations
tools/migrate/migrations/20251104121422_remove-subject-from-entitlement.up.sql, tools/migrate/migrations/20251104121422_remove-subject-from-entitlement.down.sql
Added migration to drop subject_id and subject_key columns from entitlements table; reverse migration restores columns and constraints.

Sequence Diagram(s)

sequenceDiagram
    participant SubjectService as Subject Service
    participant SubjectDeleteHook as SubjectCustomer Hook
    participant CustomerService as Customer Service
    participant Entitlement as Entitlement Service

    Note over SubjectService: Subject Delete Called
    SubjectService->>SubjectDeleteHook: PostDelete (new)
    SubjectDeleteHook->>CustomerService: GetCustomerByUsageAttribution
    alt Customer Found
        SubjectDeleteHook->>CustomerService: UpdateCustomer (remove subject key)
        CustomerService-->>SubjectDeleteHook: Updated
    else Not Found or Deleted
        SubjectDeleteHook-->>SubjectService: Log & return nil
    end
    Note over SubjectService: Deletion proceeds<br/>(no entitlement checks)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Hook removal coherence: Multiple files uniformly removing validator hooks (CustomerSubjectValidatorHook, SubjectValidatorHook, EntitlementValidatorHook), making changes relatively consistent but requiring verification across multiple layers.
  • Schema impact: Entitlement schema changes (removing subject fields/edges/indexes) cascade through adapter, queries, and type definitions—need to verify completeness of removals.
  • Event payload migration: The shift from EntitlementResetEvent v1 to v2 with customer attribution requires tracing through event construction, publishing, and consumption paths.
  • Test coverage shifts: New test structure for subject deletion and customer association behavior should be verified against intent.
  • Database migration: Verify migration is safe (no data preservation needed, constraints properly removed).

Areas warranting extra attention:

  • Verify all WithSubject() calls removed from adapter queries and no subject references linger in mappers
  • Confirm EntitlementResetEventV2 publishing integration with existing handlers
  • Check that GetFirstSubjectKey() behavior (sorted first key vs. previous singular assumption) aligns with intended semantics
  • Validate subject deletion cleanup via PostDelete hook doesn't cause race conditions

Possibly related PRs

  • #3222 — Directly reverses the subject↔entitlement DB linking and validator hooks that were introduced in this PR.
  • #3291 — Removed the CustomerSubjectValidatorHook and SubjectValidatorHook wiring that this PR eliminates.
  • #3324 — Adds customer/subject validator hooks and DI wiring that this PR removes.

Suggested labels

area/entitlements, area/customers, breaking-change

Suggested reviewers

  • turip

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'feat(multi-subject): entitlement subjects' is overly vague. While it references the general area (multi-subject, entitlements), it doesn't clearly convey the main change: removing subject-related fields from entitlements and restructuring the subject-customer relationship. Consider a more specific title like 'refactor: remove subject fields from entitlements and restructure subject-customer relationship' to better convey the scope and nature of the changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/multi-subject/entitlement-subject

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

Caution

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

⚠️ Outside diff range comments (1)
openmeter/subject/service/service_test.go (1)

201-206: Verify customer UsageAttribution cleanup after subject deletion.

The comment on line 205 nicely explains the behavior change, but the test doesn't verify what happens to the customer's UsageAttribution after the subject is deleted. According to the AI summary, there's a PostDelete hook that should remove the subject key from the customer's usage attribution.

Consider adding assertions to verify:

  • The subject is soft-deleted
  • The customer's UsageAttribution.SubjectKeys no longer includes sub1.Key
  • The entitlement remains active and associated with the customer
 err = env.SubjectService.Delete(t.Context(), models.NamespacedID{
 	Namespace: sub1.Namespace,
 	ID:        sub1.Id,
 })
 require.NoErrorf(t, err, "We will not delete the entitlements as they belong to the customer not the subject")
+
+// Verify customer usage attribution was cleaned up
+updatedCus, err := env.CustomerService.GetCustomer(t.Context(), customer.GetCustomerInput{
+	Namespace: cus.Namespace,
+	ID:        cus.ID,
+})
+require.NoErrorf(t, err, "getting customer should not fail")
+assert.NotContainsf(t, updatedCus.UsageAttribution.SubjectKeys, sub1.Key, "subject key should be removed from customer usage attribution")
+
+// Verify subject is soft-deleted
+deletedSub, err := env.SubjectService.GetById(t.Context(), models.NamespacedID{
+	Namespace: sub1.Namespace,
+	ID:        sub1.Id,
+})
+require.NoErrorf(t, err, "getting deleted subject should not fail")
+assert.Truef(t, deletedSub.IsDeleted(), "subject should be soft-deleted")

As per coding guidelines: "Make sure the tests are comprehensive and cover the changes."

🧹 Nitpick comments (6)
openmeter/subject/service/service_test.go (1)

207-239: Clarify the nested test structure.

The nested "Delete" test is a bit confusing because the subject was already deleted at line 201. Calling Delete again at line 225 doesn't reflect typical usage, and the test structure makes it unclear what scenario is being verified.

Consider either:

  1. Renaming and restructuring to make the test intent clearer (e.g., "VerifySubjectRemainsSoftDeleted")
  2. Moving the soft-delete verification (lines 231-238) to immediately after the first deletion (after line 205)
  3. Removing this nested test if it's redundant

The current structure makes it look like you're testing deletion twice on the same subject, which might not be the intended scenario.

openmeter/entitlement/metered/reset.go (1)

56-66: Consider using clock abstraction for consistency.

The v2 event structure looks good! One small observation: line 65 uses time.Now() directly, but other parts of the codebase use a clock abstraction (like clock.Now() in parser.go). For consistency and testability, consider using the same clock abstraction here.

-		ResetRequestedAt:         time.Now(),
+		ResetRequestedAt:         clock.Now(),
openmeter/entitlement/driver/parser.go (2)

34-37: Silent error handling might mask issues.

When GetFirstSubjectKey() fails, the code defaults to an empty string. This might be confusing for debugging if a customer genuinely has no subject keys. Consider logging the error or checking if it's an expected "no keys" scenario versus an actual error.

 subjKey, err := metered.Customer.UsageAttribution.GetFirstSubjectKey()
 if err != nil {
+	// Log or handle expected vs unexpected errors
 	subjKey = ""
 }

34-37: Consider extracting repeated pattern.

The same GetFirstSubjectKey() error handling pattern appears in ToMetered, ToStatic, and ToBoolean. Consider extracting this to a helper function to reduce duplication and make any future changes easier.

func getSubjectKeyOrEmpty(ua customer.CustomerUsageAttribution) string {
	subjKey, err := ua.GetFirstSubjectKey()
	if err != nil {
		return ""
	}
	return subjKey
}

Then use: subjKey := getSubjectKeyOrEmpty(metered.Customer.UsageAttribution)

Also applies to: 76-79, 108-111

openmeter/customer/service/hooks/subjectcustomer.go (1)

62-133: Looks good overall! One thing to consider:

The implementation correctly handles the happy path and gracefully deals with not-found/deleted customers. However, there's a subtle edge case: if the subject key isn't actually in the customer's UsageAttribution.SubjectKeys, we'll still call UpdateCustomer with the same list (since lo.Filter would return unchanged data).

Consider adding a quick check before the update to skip the mutation if the subject key isn't present:

if !lo.Contains(cus.UsageAttribution.SubjectKeys, sub.Key) {
    span.AddEvent("subject key not in customer usage attribution")
    return nil
}

This would avoid unnecessary database operations and make the intent clearer. Otherwise, the error handling, tracing, and graceful nil returns are spot on!

openmeter/customer/customer.go (1)

220-230: Implementation looks good, but consider clarifying the deprecation note.

The method correctly returns the first (lexicographically smallest) subject key after sorting, and properly clones the slice to avoid mutations. However, the deprecation note might be a bit confusing since this appears to be the new standard way to get a subject key across the codebase (replacing GetSubjectKey()).

Consider updating the deprecation comment to better explain the future direction:

// Deprecated: This is a transitional method for backwards compatibility as the codebase 
// migrates from subject-based to customer-based entitlements. In the future, direct 
// subject key lookups should be avoided in favor of customer-centric patterns.

This would make it clearer that while the method is new, it's already marked for eventual removal.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e987734 and 2bfdaae.

⛔ Files ignored due to path filters (17)
  • openmeter/ent/db/client.go is excluded by !**/ent/db/**
  • openmeter/ent/db/entitlement.go is excluded by !**/ent/db/**
  • openmeter/ent/db/entitlement/entitlement.go is excluded by !**/ent/db/**
  • openmeter/ent/db/entitlement/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/entitlement_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/entitlement_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/entitlement_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/migrate/schema.go is excluded by !**/ent/db/**
  • openmeter/ent/db/mutation.go is excluded by !**/ent/db/**
  • openmeter/ent/db/runtime.go is excluded by !**/ent/db/**
  • openmeter/ent/db/subject.go is excluded by !**/ent/db/**
  • openmeter/ent/db/subject/subject.go is excluded by !**/ent/db/**
  • openmeter/ent/db/subject/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/subject_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/subject_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/subject_update.go is excluded by !**/ent/db/**
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (38)
  • app/common/customer.go (0 hunks)
  • app/common/subject.go (0 hunks)
  • cmd/server/wire.go (0 hunks)
  • cmd/server/wire_gen.go (2 hunks)
  • openmeter/customer/customer.go (3 hunks)
  • openmeter/customer/errors.go (0 hunks)
  • openmeter/customer/service/hooks/subjectcustomer.go (3 hunks)
  • openmeter/customer/service/hooks/subjectvalidator.go (0 hunks)
  • openmeter/customer/service/hooks/subjectvalidator_test.go (0 hunks)
  • openmeter/ent/schema/entitlement.go (0 hunks)
  • openmeter/ent/schema/subject.go (1 hunks)
  • openmeter/entitlement/adapter/entitlement.go (2 hunks)
  • openmeter/entitlement/adapter/entitlement_test.go (2 hunks)
  • openmeter/entitlement/balanceworker/subject_customer.go (1 hunks)
  • openmeter/entitlement/balanceworker/worker.go (1 hunks)
  • openmeter/entitlement/driver/parser.go (6 hunks)
  • openmeter/entitlement/entitlement.go (2 hunks)
  • openmeter/entitlement/errors.go (1 hunks)
  • openmeter/entitlement/events.go (0 hunks)
  • openmeter/entitlement/metered/events.go (3 hunks)
  • openmeter/entitlement/metered/reset.go (1 hunks)
  • openmeter/entitlement/service/scheduling.go (1 hunks)
  • openmeter/notification/consumer/entitlementbalancethreshold_test.go (0 hunks)
  • openmeter/subject/adapter/subject.go (0 hunks)
  • openmeter/subject/service/hooks/entitlementvalidator.go (0 hunks)
  • openmeter/subject/service/service_test.go (1 hunks)
  • openmeter/subscription/service/servicevalidation.go (1 hunks)
  • openmeter/subscription/testutils/service.go (0 hunks)
  • test/billing/suite.go (0 hunks)
  • test/customer/customer.go (3 hunks)
  • test/customer/customer_test.go (1 hunks)
  • test/customer/subject.go (1 hunks)
  • test/customer/testenv.go (4 hunks)
  • test/entitlement/regression/scenario_test.go (1 hunks)
  • test/notification/consumer_balance.go (2 hunks)
  • test/notification/testenv.go (1 hunks)
  • tools/migrate/migrations/20251104121422_remove-subject-from-entitlement.down.sql (1 hunks)
  • tools/migrate/migrations/20251104121422_remove-subject-from-entitlement.up.sql (1 hunks)
💤 Files with no reviewable changes (13)
  • openmeter/notification/consumer/entitlementbalancethreshold_test.go
  • openmeter/subscription/testutils/service.go
  • openmeter/entitlement/events.go
  • openmeter/subject/adapter/subject.go
  • app/common/subject.go
  • openmeter/customer/errors.go
  • app/common/customer.go
  • test/billing/suite.go
  • openmeter/ent/schema/entitlement.go
  • cmd/server/wire.go
  • openmeter/customer/service/hooks/subjectvalidator_test.go
  • openmeter/subject/service/hooks/entitlementvalidator.go
  • openmeter/customer/service/hooks/subjectvalidator.go
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

⚙️ CodeRabbit configuration file

**/*.go: In general when reviewing the Golang code make readability and maintainability a priority, even potentially suggest restructuring the code to improve them.

Performance should be a priority in critical code paths. Anything related to event ingestion, message processing, database operations (regardless of database) should be vetted for potential performance bottlenecks.

Files:

  • openmeter/entitlement/metered/reset.go
  • openmeter/entitlement/driver/parser.go
  • openmeter/customer/service/hooks/subjectcustomer.go
  • openmeter/subscription/service/servicevalidation.go
  • openmeter/entitlement/balanceworker/worker.go
  • openmeter/entitlement/service/scheduling.go
  • openmeter/entitlement/balanceworker/subject_customer.go
  • test/notification/consumer_balance.go
  • openmeter/entitlement/adapter/entitlement.go
  • openmeter/subject/service/service_test.go
  • openmeter/customer/customer.go
  • openmeter/entitlement/adapter/entitlement_test.go
  • test/notification/testenv.go
  • test/customer/customer_test.go
  • test/entitlement/regression/scenario_test.go
  • openmeter/entitlement/errors.go
  • openmeter/entitlement/entitlement.go
  • openmeter/ent/schema/subject.go
  • test/customer/testenv.go
  • test/customer/subject.go
  • test/customer/customer.go
  • cmd/server/wire_gen.go
  • openmeter/entitlement/metered/events.go
**/*_test.go

⚙️ CodeRabbit configuration file

**/*_test.go: Make sure the tests are comprehensive and cover the changes. Keep a strong focus on unit tests and in-code integration tests.
When appropriate, recommend e2e tests for critical changes.

Files:

  • openmeter/subject/service/service_test.go
  • openmeter/entitlement/adapter/entitlement_test.go
  • test/customer/customer_test.go
  • test/entitlement/regression/scenario_test.go
🧠 Learnings (6)
📓 Common learnings
Learnt from: chrisgacsal
Repo: openmeterio/openmeter PR: 3373
File: openmeter/subject/adapter/subject.go:62-65
Timestamp: 2025-09-12T09:37:57.052Z
Learning: In the OpenMeter subject module, soft-delete validation is handled at the service layer rather than the adapter layer, following a clean separation of concerns where business logic validation occurs at higher layers and the adapter focuses on data access operations.
Learnt from: chrisgacsal
Repo: openmeterio/openmeter PR: 3373
File: openmeter/subject/adapter/subject.go:62-65
Timestamp: 2025-09-12T09:37:57.052Z
Learning: In the OpenMeter subject module, soft-delete validation is handled at the service layer rather than the adapter layer, following a clean separation of concerns where business logic validation occurs at higher layers and the adapter focuses on data access operations.
📚 Learning: 2025-10-09T13:59:12.012Z
Learnt from: chrisgacsal
Repo: openmeterio/openmeter PR: 3486
File: openmeter/ingest/kafkaingest/serializer/serializer.go:105-107
Timestamp: 2025-10-09T13:59:12.012Z
Learning: In OpenMeter, the CloudEvents `subject` field is mandatory for the application's business logic, even though it's optional in the CloudEvents specification. The `ValidateKafkaPayloadToCloudEvent` function in `openmeter/ingest/kafkaingest/serializer/serializer.go` intentionally enforces this requirement.

Applied to files:

  • openmeter/entitlement/metered/reset.go
  • openmeter/entitlement/driver/parser.go
  • openmeter/subscription/service/servicevalidation.go
  • openmeter/entitlement/errors.go
  • openmeter/entitlement/entitlement.go
  • test/customer/testenv.go
  • openmeter/entitlement/metered/events.go
📚 Learning: 2025-03-07T12:17:43.129Z
Learnt from: GAlexIHU
Repo: openmeterio/openmeter PR: 2383
File: openmeter/entitlement/metered/lateevents_test.go:37-45
Timestamp: 2025-03-07T12:17:43.129Z
Learning: In the OpenMeter codebase, test files like `openmeter/entitlement/metered/lateevents_test.go` may use variables like `meterSlug` and `namespace` without explicit declarations visible in the same file. This appears to be an accepted pattern in their test structure.

Applied to files:

  • openmeter/entitlement/metered/reset.go
  • openmeter/entitlement/driver/parser.go
  • test/notification/consumer_balance.go
  • openmeter/entitlement/adapter/entitlement.go
  • openmeter/subject/service/service_test.go
  • openmeter/entitlement/adapter/entitlement_test.go
  • test/customer/testenv.go
  • test/customer/customer.go
📚 Learning: 2025-09-12T09:38:52.436Z
Learnt from: chrisgacsal
Repo: openmeterio/openmeter PR: 3373
File: openmeter/subject/adapter/subject.go:119-136
Timestamp: 2025-09-12T09:38:52.436Z
Learning: In OpenMeter subject adapter GetByIdOrKey method, ID-based lookups should return subjects even if soft-deleted, while Key-based lookups should be gated by DeletedAt filters. This is intentional design where IDs are treated as immutable references that can retrieve deleted entities, but Keys should only match active (non-deleted) subjects.

Applied to files:

  • openmeter/entitlement/adapter/entitlement_test.go
📚 Learning: 2025-04-20T11:15:07.499Z
Learnt from: chrisgacsal
Repo: openmeterio/openmeter PR: 2692
File: openmeter/productcatalog/plan/adapter/mapping.go:64-74
Timestamp: 2025-04-20T11:15:07.499Z
Learning: In the OpenMeter codebase, Ent's edge methods ending in "OrErr" (like AddonsOrErr()) only return NotLoadedError when the edge wasn't loaded, and cannot return DB errors. Simple err != nil checks are sufficient for these methods.

Applied to files:

  • openmeter/ent/schema/subject.go
📚 Learning: 2025-08-29T12:31:52.802Z
Learnt from: chrisgacsal
Repo: openmeterio/openmeter PR: 3291
File: app/common/customer.go:88-89
Timestamp: 2025-08-29T12:31:52.802Z
Learning: In Go projects using Google's wire dependency injection framework, named types (without =) should be used instead of type aliases (with =) to work around wire limitations. For example, use `type CustomerSubjectValidatorHook customerservicehooks.SubjectValidatorHook` instead of `type CustomerSubjectValidatorHook = customerservicehooks.SubjectValidatorHook` when wire is involved.

Applied to files:

  • test/customer/testenv.go
  • cmd/server/wire_gen.go
🧬 Code graph analysis (16)
openmeter/entitlement/metered/reset.go (2)
openmeter/entitlement/metered/events.go (1)
  • EntitlementResetEventV2 (73-81)
api/api.gen.go (2)
  • Customer (2222-2272)
  • CustomerUsageAttribution (2410-2413)
openmeter/entitlement/driver/parser.go (2)
openmeter/customer/customer.go (1)
  • Customer (41-53)
api/api.gen.go (1)
  • Customer (2222-2272)
openmeter/customer/service/hooks/subjectcustomer.go (3)
openmeter/customer/customer.go (4)
  • GetCustomerByUsageAttributionInput (233-239)
  • UpdateCustomerInput (330-333)
  • CustomerID (147-147)
  • CustomerMutate (112-122)
pkg/models/errors.go (1)
  • IsGenericNotFoundError (57-65)
pkg/clock/clock.go (1)
  • Now (14-21)
openmeter/entitlement/balanceworker/worker.go (6)
openmeter/watermill/grouphandler/grouphandler.go (1)
  • NewGroupEventHandler (29-31)
openmeter/entitlement/metered/events.go (1)
  • EntitlementResetEventV2 (73-81)
pkg/models/id.go (1)
  • NamespacedID (7-10)
openmeter/entitlement/balanceworker/entitlementhandler.go (3)
  • WithSource (49-53)
  • WithEventAt (55-59)
  • WithSourceOperation (61-65)
openmeter/event/metadata/resourcepath.go (2)
  • ComposeResourcePath (29-31)
  • EntityEntitlement (10-10)
openmeter/entitlement/snapshot/event.go (1)
  • ValueOperationReset (22-22)
openmeter/entitlement/service/scheduling.go (1)
openmeter/entitlement/errors.go (1)
  • AlreadyExistsError (9-13)
test/notification/consumer_balance.go (4)
api/api.gen.go (2)
  • Customer (2222-2272)
  • CustomerUsageAttribution (2410-2413)
openmeter/streaming/query_params.go (2)
  • Customer (76-78)
  • CustomerUsageAttribution (81-85)
pkg/models/model.go (2)
  • ManagedResource (23-31)
  • NamespacedModel (204-206)
test/notification/testenv.go (2)
  • TestCustomerID (38-38)
  • TestSubjectKey (36-36)
openmeter/entitlement/adapter/entitlement.go (3)
openmeter/ent/db/entitlement.go (2)
  • Entitlement (21-73)
  • Entitlement (153-176)
openmeter/ent/schema/entitlement.go (5)
  • Entitlement (20-22)
  • Entitlement (24-31)
  • Entitlement (33-71)
  • Entitlement (73-84)
  • Entitlement (86-111)
openmeter/ent/db/customer_query.go (1)
  • CustomerQuery (27-43)
openmeter/customer/customer.go (3)
api/api.gen.go (2)
  • Customer (2222-2272)
  • CustomerUsageAttribution (2410-2413)
openmeter/streaming/query_params.go (2)
  • Customer (76-78)
  • CustomerUsageAttribution (81-85)
pkg/models/errors.go (1)
  • NewGenericValidationError (138-140)
test/customer/customer_test.go (1)
test/customer/customer.go (1)
  • CustomerHandlerTestSuite (54-58)
openmeter/entitlement/errors.go (1)
openmeter/ent/db/entitlement/where.go (1)
  • CustomerID (110-112)
openmeter/entitlement/entitlement.go (2)
api/api.gen.go (1)
  • Customer (2222-2272)
openmeter/ent/db/customer.go (2)
  • Customer (20-64)
  • Customer (142-157)
test/customer/testenv.go (4)
openmeter/customer/service/hooks/subjectcustomer.go (2)
  • NewSubjectCustomerHook (165-186)
  • SubjectCustomerHookConfig (188-196)
app/common/subject.go (2)
  • NewSubjectCustomerHook (35-53)
  • Subject (18-21)
openmeter/subject/service/hooks/customersubject.go (2)
  • NewCustomerSubjectHook (81-96)
  • CustomerSubjectHookConfig (98-98)
openmeter/billing/service.go (1)
  • CustomerOverrideService (35-42)
test/customer/subject.go (6)
test/customer/customer.go (1)
  • CustomerHandlerTestSuite (54-58)
openmeter/customer/customer.go (7)
  • Customer (41-53)
  • CreateCustomerInput (312-315)
  • CustomerMutate (112-122)
  • CustomerUsageAttribution (200-202)
  • UpdateCustomerInput (330-333)
  • CustomerID (147-147)
  • GetCustomerInput (351-358)
pkg/models/key.go (1)
  • NamespacedKey (5-8)
pkg/models/id.go (1)
  • NamespacedID (7-10)
openmeter/productcatalog/feature/connector.go (1)
  • CreateFeatureInputs (19-26)
openmeter/entitlement/entitlement.go (1)
  • CreateEntitlementInputs (77-102)
test/customer/customer.go (4)
pkg/models/key.go (1)
  • NamespacedKey (5-8)
openmeter/customer/customer.go (3)
  • CreateCustomerInput (312-315)
  • CustomerMutate (112-122)
  • CustomerUsageAttribution (200-202)
openmeter/customer/errors.go (1)
  • IsSubjectKeyConflictError (88-96)
pkg/models/errors.go (1)
  • IsGenericConflictError (87-95)
cmd/server/wire_gen.go (1)
app/common/telemetry.go (2)
  • NewTelemetryServer (261-268)
  • TelemetryServer (259-259)
openmeter/entitlement/metered/events.go (2)
api/api.gen.go (2)
  • CustomerUsageAttribution (2410-2413)
  • Subject (6898-6932)
openmeter/event/metadata/resourcepath.go (2)
  • ComposeResourcePath (29-31)
  • EntityEntitlement (10-10)
⏰ 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). (7)
  • GitHub Check: Artifacts / Benthos Collector Container image
  • GitHub Check: Artifacts / Container image
  • GitHub Check: Test
  • GitHub Check: Migration Checks
  • GitHub Check: Lint
  • GitHub Check: Build
  • GitHub Check: Code Generators
🔇 Additional comments (34)
openmeter/subscription/service/servicevalidation.go (1)

33-38: LGTM! Comment renumbering looks good.

The step numbering has been updated correctly after removing the subject validation logic. The currency validation itself remains intact and properly handles the case where the customer has a currency set.

test/notification/testenv.go (1)

38-38: Nice addition! The constant fits right in.

The new TestCustomerID constant aligns well with the shift to a customer-centric model. The naming and value are consistent with the existing test constants, and placing it right after TestSubjectID makes sense given the relationship between customers and subjects.

test/notification/consumer_balance.go (1)

74-84: Nice update to the test fixture!

The Customer object construction looks good and properly aligns with the customer-centric model. The nested structure with ManagedResource and UsageAttribution matches the expected schema.

cmd/server/wire_gen.go (1)

550-602: Auto-generated changes look consistent.

The variable renumbering (v9 → v8) is expected since this file is auto-generated by Wire and reflects the removal of the validator hooks. The wiring structure remains sound.

openmeter/ent/schema/subject.go (1)

66-67: Verified: No code relies on the Subject→Entitlements edge.

The search results confirm the removal is safe. All entitlements edge usage found is on the Customer entity, not Subject. The QueryEntitlements(), WithEntitlements(), and EntitlementsOrErr() patterns show zero results for Subject, and nothing else attempts to navigate from Subject to Entitlements.

tools/migrate/migrations/20251104121422_remove-subject-from-entitlement.up.sql (1)

1-2: All clear—the migration is safe to deploy.

I've verified your concerns and the good news: the codebase is ready. Here's what I found:

No remaining code references — The search across all Go files (excluding tests and generated code) turned up zero active uses of subject_key or subject_id in entitlements operations. No queries like QuerySubject() or WithSubject() on the Entitlements entity.

Down migration is solid — The down migration file exists and is comprehensive. It not only restores the columns with the correct types but also recreates the indexes and the foreign key constraint to subjects. It's production-ready for rollback if needed.

Schema already cleaned up — The current Entitlements schema definition has no references to subject columns—just the customer relationship. This confirms the code changes are complete and consistent.

The timing also makes sense: the migration is dated Nov 4, 2025, which aligns perfectly with OpenMeter's subject-based API removal date. You're good to ship this.

test/customer/customer_test.go (1)

63-71: LGTM! Nice test organization.

The new "Subject" test subgroup follows the existing pattern well and integrates cleanly with the test suite structure.

openmeter/entitlement/balanceworker/worker.go (1)

295-306: LGTM! Clean v2 handler addition.

The new EntitlementResetEventV2 handler mirrors the v1 handler nicely and maintains backward compatibility. The structure and parameter usage are consistent and correct.

openmeter/entitlement/errors.go (1)

12-16: LGTM! Clean transition to customer-centric error.

The field rename from SubjectKey to CustomerID is consistent and the error message accurately reflects the new field. This aligns well with the customer-centric model.

openmeter/entitlement/balanceworker/subject_customer.go (1)

30-33: LGTM! Proper error handling here.

The update to GetFirstSubjectKey() is correct, and I appreciate that this code properly propagates the error instead of silently defaulting to empty string. This makes debugging easier if a customer has no subject keys.

openmeter/entitlement/service/scheduling.go (1)

99-99: LGTM! Consistent with error struct change.

The update to use CustomerID: conflict.Customer.ID correctly aligns with the AlreadyExistsError struct change. The Customer field should always be populated based on the entitlement construction flow, so this looks safe.

test/customer/testenv.go (4)

10-10: LGTM! Imports look good.

All new imports are properly utilized in the hook wiring below.

Also applies to: 13-13, 17-17, 28-28


159-169: LGTM! Subject customer hook is wired correctly.

The hook configuration includes all required dependencies and is properly registered with the subject service. Using a noop tracer is appropriate for the test environment.


171-180: LGTM! Customer subject hook is wired correctly.

The hook configuration and registration look good. Both hooks now establish the cross-service subject-customer lifecycle interactions needed for testing.


207-209: LGTM! Noop service is appropriate for tests.

The noop implementation correctly embeds the interface, providing zero-value implementations for all methods. This is the right approach for a test environment that doesn't need actual billing interactions.

test/entitlement/regression/scenario_test.go (1)

631-631: LGTM! Method rename is correct.

The update from GetSubjectKey() to GetFirstSubjectKey() aligns with the API changes across the codebase. Since this test creates a customer with a single subject key, the "first" semantics (returning the smallest sorted key) won't affect the test behavior.

openmeter/entitlement/adapter/entitlement_test.go (2)

250-250: LGTM! Subject-related assertion removed.

The removal of subject-key assertions aligns with the broader PR changes to drop subject fields from entitlements. The test still properly validates the important fields after the upsert operation.


497-498: LGTM! Method rename is correct.

The switch to GetFirstSubjectKey() is consistent with the API changes. Error handling is properly in place, and the usage in subsequent assertions is appropriate.

openmeter/customer/service/hooks/subjectcustomer.go (1)

13-13: LGTM! Necessary imports.

Both imports are properly utilized in the new PostDelete method.

Also applies to: 26-26

test/customer/customer.go (4)

110-122: LGTM! Nice test addition.

Verifying that subjects are created alongside customers is a good practice, especially with the new hook wiring. The subtest structure keeps things organized.


125-141: LGTM! Nice refactoring.

Wrapping these conflict tests in subtests makes the test structure much clearer and easier to follow. The test logic itself remains solid.

Also applies to: 144-161, 164-181


207-207: LGTM! Clear test data.

The new subject key name is descriptive and follows the existing pattern.


245-271: LGTM! Excellent test coverage.

These subtests properly verify the subject lifecycle during customer updates: new subjects are created, and old subjects are left dangling rather than deleted. This is important behavior to test explicitly!

test/customer/subject.go (3)

1-65: LGTM! Great test setup.

The first subtest nicely covers the dangling subject scenario: create a customer with a subject, remove it from usage attribution, then delete the subject. This is an important edge case to test!


67-109: LGTM! Core hook behavior tested.

This subtest validates the main PostDelete hook behavior: when a subject is deleted while still in a customer's usage attribution, the customer is automatically updated to remove that subject key. The assertion properly checks for an empty list.


111-171: LGTM! Important edge case covered.

This test addresses a potentially tricky scenario: a customer with active entitlements but no remaining subjects after deletion. The test name clearly communicates the expected behavior (no error), which is helpful for future maintainers.

openmeter/entitlement/metered/events.go (3)

7-7: LGTM! Clean deprecation path.

The import is necessary for the v2 event, and the deprecation comment clearly guides users to the new version.

Also applies to: 18-18


73-91: LGTM! V2 event structure looks good.

The new event properly replaces subject-based fields with customer-centric ones (CustomerID and CustomerUsageAttribution). The versioning is clear, and interface implementation is correctly declared.


93-130: LGTM! Event methods implemented correctly.

The EventName(), EventMetadata(), and Validate() methods are all properly implemented:

  • Metadata subject path now references the customer instead of subject key (line 100)
  • Validation covers all required fields including the nested CustomerUsageAttribution
  • Error messages are clear and consistent
openmeter/customer/customer.go (2)

6-6: LGTM! Necessary import.

The slices import is used in GetFirstSubjectKey() for cloning and sorting operations.


55-68: LGTM! Handy conversion helper.

The AsCustomerMutate() method provides a clean way to convert a Customer to a CustomerMutate, mapping all the relevant fields. This simplifies mutation workflows and reduces boilerplate.

openmeter/entitlement/adapter/entitlement.go (3)

318-320: Nice work on the customer-centric model! 🎯

The query properly loads customers with their subjects via the customer adapter, which is exactly right for the new model. The eager loading pattern is consistent with the rest of the file and should perform well.


830-832: Consistent pattern maintained! ✓

The same customer-with-subjects loading pattern is applied here, keeping things nice and uniform across the codebase.


56-81: The DB consistency assumption here is solid – the migrations back it up

Good catch on thinking about data consistency! Your original concern was totally valid during development, but here's what happened: The recent migration chain (especially 20250821121421_entitlement-customer-link and 20251104121422_remove-subject-from-entitlement) actually handled this with a deliberate backfill strategy.

The migration backfills customer_id from the customer_subjects mapping by joining on namespace and subject_key, then adds a foreign key constraint from entitlements.customer_id to customers.id. Since we just crossed the November 1st consolidation deadline (today's Nov 4th), the final cleanup migration already ran, removing the old subject columns.

So the code's assumption that customer-subject data is consistent and properly linked? That's baked in by the migration strategy. The FK constraint ensures integrity going forward. You're all good here.

Comment on lines 364 to 371
func (e GenericProperties) Validate() error {
// TODO: there are no clear validation requirements now but lets implement the interface
if e.Customer == nil {
return fmt.Errorf("customer is required")
}

return nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify all callers populate Customer before validation.

The new validation requiring Customer to be non-nil is a good safety check for the customer-centric model. However, this is a breaking change that could cause runtime errors if any code path creates entitlements without setting the Customer field.

Run the following to check entitlement creation and update paths:


🏁 Script executed:

#!/bin/bash
# Find all places where GenericProperties or Entitlement structs are created
# to verify Customer field is set

# Search for struct literal initialization
ast-grep --pattern $'entitlement.GenericProperties{
  $$$
}'

ast-grep --pattern $'entitlement.Entitlement{
  $$$
}'

# Search for entitlement creation functions that might not set Customer
rg -nP --type=go -C3 'CreateEntitlement|NewEntitlement' -g '!*_test.go' -g '!*_gen.go'

Length of output: 108850


Critical breaking change: Customer field not populated in entitlement adapter.

The new validation will cause runtime errors. Found incomplete implementations:

  • openmeter/entitlement/adapter/entitlement.go:471-491 — Main issue. The CreateEntitlement method converts database models to domain objects but doesn't load the Customer relationship. When this entitlement passes through validation, it will fail.

  • openmeter/server/server_test.go:712,716,720,724,728,748 — Mock implementations return bare &entitlement.Entitlement{} structs without Customer set. These will fail validation if exercised outside test contexts.

  • openmeter/notification/consumer/entitlementbalancethreshold_test.go:517,597 — Test fixtures with incomplete GenericProperties initialization.

The adapter conversion is the production blocker—you'll need to ensure the Customer relationship is loaded from the database when reconstructing entitlements.

🤖 Prompt for AI Agents
In openmeter/entitlement/adapter/entitlement.go around lines 471-491, the
CreateEntitlement conversion reconstructs domain Entitlement from DB models but
does not populate the Customer relationship, causing GenericProperties.Validate
to fail; fix by loading the Customer association from the database (e.g., use
ORM Preload/Join or explicit query for the related customer) when fetching the
entitlement or when converting the model so GenericProperties.Customer is set
(and handle nil safely), then update affected tests/fixtures (server_test.go
mocks and notification test fixtures) to return/construct entitlements with a
populated Customer to avoid false failures.

@@ -0,0 +1,9 @@
-- reverse: modify "entitlements" table
ALTER TABLE "entitlements" ADD COLUMN "subject_id" character(26) NOT NULL, ADD COLUMN "subject_key" character varying NOT NULL;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential migration failure on rollback.

Adding NOT NULL columns without default values to an existing table could fail if any entitlement records exist. Since the up migration removes these columns, any rollback attempt with existing data would hit constraint violations.

Consider either:

  1. Adding default values for the new columns
  2. Documenting that down migration requires an empty table
  3. Implementing a data backfill strategy if the records are expected to be preserved

Would you like me to help generate a safer down migration script?

🤖 Prompt for AI Agents
In
tools/migrate/migrations/20251104121422_remove-subject-from-entitlement.down.sql
around lines 2 to 2, the down migration adds NOT NULL columns without defaults
which will fail if rows exist; change the migration to safely add the columns as
NULLable or with a sensible DEFAULT (or add them NULL, backfill values, then
ALTER TABLE ... SET NOT NULL), or explicitly document/require that the
entitlements table is empty before running the down migration; implement one of
these options so rollback won't violate NOT NULL constraints.

Base automatically changed from chore/remove-entitlement-event-v1 to main November 5, 2025 05:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/feature Release note: Exciting New Features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants