-
Notifications
You must be signed in to change notification settings - Fork 127
feat(multi-subject): entitlement subjects #3576
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
base: main
Are you sure you want to change the base?
Conversation
📝 WalkthroughWalkthroughThis 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
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)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Areas warranting extra attention:
Possibly related PRs
Suggested labels
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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: 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
UsageAttributionafter the subject is deleted. According to the AI summary, there's aPostDeletehook 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.SubjectKeysno longer includessub1.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. CallingDeleteagain at line 225 doesn't reflect typical usage, and the test structure makes it unclear what scenario is being verified.Consider either:
- Renaming and restructuring to make the test intent clearer (e.g., "VerifySubjectRemainsSoftDeleted")
- Moving the soft-delete verification (lines 231-238) to immediately after the first deletion (after line 205)
- 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 (likeclock.Now()inparser.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 inToMetered,ToStatic, andToBoolean. 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 callUpdateCustomerwith the same list (sincelo.Filterwould 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
⛔ Files ignored due to path filters (17)
openmeter/ent/db/client.gois excluded by!**/ent/db/**openmeter/ent/db/entitlement.gois excluded by!**/ent/db/**openmeter/ent/db/entitlement/entitlement.gois excluded by!**/ent/db/**openmeter/ent/db/entitlement/where.gois excluded by!**/ent/db/**openmeter/ent/db/entitlement_create.gois excluded by!**/ent/db/**openmeter/ent/db/entitlement_query.gois excluded by!**/ent/db/**openmeter/ent/db/entitlement_update.gois excluded by!**/ent/db/**openmeter/ent/db/migrate/schema.gois excluded by!**/ent/db/**openmeter/ent/db/mutation.gois excluded by!**/ent/db/**openmeter/ent/db/runtime.gois excluded by!**/ent/db/**openmeter/ent/db/subject.gois excluded by!**/ent/db/**openmeter/ent/db/subject/subject.gois excluded by!**/ent/db/**openmeter/ent/db/subject/where.gois excluded by!**/ent/db/**openmeter/ent/db/subject_create.gois excluded by!**/ent/db/**openmeter/ent/db/subject_query.gois excluded by!**/ent/db/**openmeter/ent/db/subject_update.gois excluded by!**/ent/db/**tools/migrate/migrations/atlas.sumis 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.goopenmeter/entitlement/driver/parser.goopenmeter/customer/service/hooks/subjectcustomer.goopenmeter/subscription/service/servicevalidation.goopenmeter/entitlement/balanceworker/worker.goopenmeter/entitlement/service/scheduling.goopenmeter/entitlement/balanceworker/subject_customer.gotest/notification/consumer_balance.goopenmeter/entitlement/adapter/entitlement.goopenmeter/subject/service/service_test.goopenmeter/customer/customer.goopenmeter/entitlement/adapter/entitlement_test.gotest/notification/testenv.gotest/customer/customer_test.gotest/entitlement/regression/scenario_test.goopenmeter/entitlement/errors.goopenmeter/entitlement/entitlement.goopenmeter/ent/schema/subject.gotest/customer/testenv.gotest/customer/subject.gotest/customer/customer.gocmd/server/wire_gen.goopenmeter/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.goopenmeter/entitlement/adapter/entitlement_test.gotest/customer/customer_test.gotest/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.goopenmeter/entitlement/driver/parser.goopenmeter/subscription/service/servicevalidation.goopenmeter/entitlement/errors.goopenmeter/entitlement/entitlement.gotest/customer/testenv.goopenmeter/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.goopenmeter/entitlement/driver/parser.gotest/notification/consumer_balance.goopenmeter/entitlement/adapter/entitlement.goopenmeter/subject/service/service_test.goopenmeter/entitlement/adapter/entitlement_test.gotest/customer/testenv.gotest/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.gocmd/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
TestCustomerIDconstant 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 afterTestSubjectIDmakes 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
ManagedResourceandUsageAttributionmatches 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
Customerentity, notSubject. TheQueryEntitlements(),WithEntitlements(), andEntitlementsOrErr()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_keyorsubject_idin entitlements operations. No queries likeQuerySubject()orWithSubject()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
EntitlementResetEventV2handler 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
SubjectKeytoCustomerIDis 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.IDcorrectly aligns with theAlreadyExistsErrorstruct change. TheCustomerfield 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()toGetFirstSubjectKey()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 (
CustomerIDandCustomerUsageAttribution). The versioning is clear, and interface implementation is correctly declared.
93-130: LGTM! Event methods implemented correctly.The
EventName(),EventMetadata(), andValidate()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
slicesimport is used inGetFirstSubjectKey()for cloning and sorting operations.
55-68: LGTM! Handy conversion helper.The
AsCustomerMutate()method provides a clean way to convert aCustomerto aCustomerMutate, 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 upGood 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-linkand20251104121422_remove-subject-from-entitlement) actually handled this with a deliberate backfill strategy.The migration backfills
customer_idfrom thecustomer_subjectsmapping by joining on namespace and subject_key, then adds a foreign key constraint fromentitlements.customer_idtocustomers.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.
| 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 | ||
| } |
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.
🧩 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
CreateEntitlementmethod converts database models to domain objects but doesn't load theCustomerrelationship. 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
GenericPropertiesinitialization.
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; | |||
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.
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:
- Adding default values for the new columns
- Documenting that down migration requires an empty table
- 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.
Overview
Removes anything subject related from entitlements
db.Subjecttodb.CustomerSubjectwhich 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