Skip to content

Conversation

@chrisgacsal
Copy link
Collaborator

@chrisgacsal chrisgacsal commented Sep 11, 2025

Overview

  • always fetch active subsctiptions for Customer
  • return error on deleting Customer with active subscription(s)

Summary by CodeRabbit

  • New Features

    • Customer records now include a list of active subscription IDs.
    • Deletion attempts report which active subscriptions block deletion.
  • Improvements

    • Entitlement queries use a consistent time snapshot and always account for active subscriptions for more stable, predictable results.
  • Bug Fixes

    • Prevents deleting customers with active subscriptions to avoid accidental data loss.

@chrisgacsal chrisgacsal self-assigned this Sep 11, 2025
@chrisgacsal chrisgacsal requested a review from a team as a code owner September 11, 2025 08:32
@chrisgacsal chrisgacsal added the release-note/bug-fix Release note: Bug Fixes label Sep 11, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 11, 2025

📝 Walkthrough

Walkthrough

Expose a public WithActiveSubscriptions query helper; enforce preloaded Subjects and Subscriptions in entity mapping and add ActiveSubscriptionIDs to Customer; prevent deleting customers with active subscriptions; and unify entitlement queries to use a single captured timestamp and preload active subscriptions.

Changes

Cohort / File(s) Summary
Adapter query helper rename
openmeter/customer/adapter/customer.go
withSubscription renamed and exported as WithActiveSubscriptions; updated call sites (ListCustomers, GetCustomer, GetCustomerByUsageAttribution); behavior preserved.
Entity mapping: enforce preloaded edges & expose active subscriptions
openmeter/customer/adapter/entitymapping.go
Replace nil checks with Edges.SubjectsOrErr() and Edges.SubscriptionOrErr(); return explicit errors when edges not loaded; compute SubjectKey and ActiveSubscriptionIDs via filtered mapping; populate Customer.ActiveSubscriptionIDs.
Customer public struct update
openmeter/customer/customer.go
Added exported field ActiveSubscriptionIDs []string to Customer.
Customer errors for active subscriptions on delete
openmeter/customer/errors.go
Added ErrCodeDeletingCustomerWithActiveSubscriptions, ErrDeletingCustomerWithActiveSubscriptions, and constructor NewErrDeletingCustomerWithActiveSubscriptions(subscriptionIDs []string).
Service deletion precondition
openmeter/customer/service/customer.go
DeleteCustomer now checks len(cus.ActiveSubscriptionIDs) > 0 and returns a PreConditionFailed error wrapping NewErrDeletingCustomerWithActiveSubscriptions(...) to abort deletion.
Entitlement queries: single timestamp & preload active subscriptions
openmeter/entitlement/adapter/...
Capture a single now timestamp per operation (replace scattered clock.Now()), and preload active subscriptions using customeradapter.WithActiveSubscriptions(..., now) alongside subjects across entitlement queries and create/reload paths; adjust local variables and reloading to use composed queries with subjects and active subscriptions.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Pre-merge checks (3 passed)

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "fix: deletion of customer with active subscriptions" concisely and accurately summarizes the primary change—adding a guard that prevents deleting customers with active subscriptions and returning an explicit error—while following conventional commit style and containing no noisy or ambiguous language.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8f9ca28 and aef545d.

📒 Files selected for processing (2)
  • openmeter/customer/adapter/customer.go (4 hunks)
  • openmeter/entitlement/adapter/entitlement.go (15 hunks)
✅ Files skipped from review due to trivial changes (1)
  • openmeter/entitlement/adapter/entitlement.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • openmeter/customer/adapter/customer.go
⏰ 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: Code Generators
  • GitHub Check: Build
  • GitHub Check: Lint
  • GitHub Check: Analyze (go)
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/customer-delete

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

🧹 Nitpick comments (4)
openmeter/customer/adapter/customer.go (1)

628-634: Tweak comment and avoid unnecessary plan eager-loading.

  • Comment says “subscription”; use “active subscriptions”.
  • Loading Plan for every call adds extra JOINs/rows; it’s only needed when filtering by plan key. Consider dropping WithPlan here to reduce query cost.

Apply:

-// withActiveSubscriptions returns a query with the subscription
+// withActiveSubscriptions returns a query with active subscriptions preloaded
 func withActiveSubscriptions(query *entdb.CustomerQuery, at time.Time) *entdb.CustomerQuery {
   return query.WithSubscription(func(query *entdb.SubscriptionQuery) {
     applyActiveSubscriptionFilter(query, at)
-    query.WithPlan()
+    // Note: do not eager-load plans here; plan JOIN is applied only when filtering by plan key.
   })
 }
openmeter/customer/adapter/entitymapping.go (2)

23-29: De-duplicate subject keys to avoid accidental duplicates.

While Ent should de-dup, defensive uniqueness here is cheap and prevents surprises from joins.

Apply:

   subjectKeys := lo.FilterMap(subjects, func(item *db.CustomerSubjects, _ int) (string, bool) {
     if item == nil {
       return "", false
     }
     return item.SubjectKey, true
   })
+  subjectKeys = lo.Uniq(subjectKeys)

40-47: De-duplicate subscription IDs as well.

Prevents duplicate IDs if multiple joins surface the same row.

Apply:

   subscriptionIDs := lo.FilterMap(subscriptions, func(item *db.Subscription, _ int) (string, bool) {
     if item == nil {
       return "", false
     }
     return item.ID, true
   })
+  subscriptionIDs = lo.Uniq(subscriptionIDs)
openmeter/customer/errors.go (1)

116-118: Attribute name clarity.

Consider using "subscription_ids" to mirror "subject_keys" naming and be explicit.

Apply:

-func NewErrDeletingCustomerWithActiveSubscriptions(subscriptionIDs []string) error {
-    return ErrDeletingCustomerWithActiveSubscriptions.WithAttr("subscriptions", subscriptionIDs)
-}
+func NewErrDeletingCustomerWithActiveSubscriptions(subscriptionIDs []string) error {
+    return ErrDeletingCustomerWithActiveSubscriptions.WithAttr("subscription_ids", subscriptionIDs)
+}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5197a3c and e0dd7d1.

📒 Files selected for processing (5)
  • openmeter/customer/adapter/customer.go (4 hunks)
  • openmeter/customer/adapter/entitymapping.go (3 hunks)
  • openmeter/customer/customer.go (1 hunks)
  • openmeter/customer/errors.go (1 hunks)
  • openmeter/customer/service/customer.go (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-04-20T11:15:07.499Z
Learnt from: chrisgacsal
PR: openmeterio/openmeter#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/customer/adapter/entitymapping.go
🧬 Code graph analysis (4)
openmeter/customer/errors.go (2)
pkg/models/errors.go (1)
  • ErrorCode (294-294)
pkg/models/validationissue.go (1)
  • NewValidationIssue (219-230)
openmeter/customer/service/customer.go (2)
pkg/models/errors.go (1)
  • NewGenericPreConditionFailedError (233-235)
openmeter/customer/errors.go (1)
  • NewErrDeletingCustomerWithActiveSubscriptions (116-118)
openmeter/customer/adapter/entitymapping.go (3)
openmeter/ent/db/ent.go (1)
  • IsNotLoaded (340-346)
openmeter/ent/db/predicate/predicate.go (2)
  • SubscriptionOrErr (267-275)
  • Subscription (264-264)
openmeter/ent/db/subscription.go (2)
  • Subscription (22-60)
  • Subscription (140-157)
openmeter/customer/adapter/customer.go (1)
openmeter/ent/db/customer_query.go (1)
  • CustomerQuery (27-43)
⏰ 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). (8)
  • GitHub Check: Quickstart
  • GitHub Check: E2E
  • GitHub Check: Code Generators
  • GitHub Check: Migration Checks
  • GitHub Check: Lint
  • GitHub Check: Test
  • GitHub Check: Build
  • GitHub Check: Analyze (go)
🔇 Additional comments (6)
openmeter/customer/adapter/customer.go (3)

38-38: Ensure eager-loading for mapping.

Good call to use withActiveSubscriptions; this satisfies CustomerFromDBEntity’s new edge requirement.


316-316: Eager-load subscriptions here as well.

LGTM; prevents “subscriptions must be loaded” errors on GetCustomer.


392-392: Keep active-subscription preload for attribution lookup.

LGTM; consistent with entity mapping expectations.

openmeter/customer/adapter/entitymapping.go (3)

14-22: Switch to OrErr for edge presence — good.

This enforces preloaded edges; error handling with db.IsNotLoaded is appropriate per prior learnings.


31-38: Subscription edge presence check — good.

Same pattern as subjects; keeps mapping strict and explicit.


78-79: Expose ActiveSubscriptionIDs — aligned with service precondition.

LGTM; mapping surfaces the IDs needed by the delete guard.

@chrisgacsal chrisgacsal marked this pull request as draft September 11, 2025 09:17
@chrisgacsal chrisgacsal marked this pull request as ready for review September 11, 2025 09:47
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: 1

🧹 Nitpick comments (4)
openmeter/customer/errors.go (1)

109-118: Error code addition looks good; align attribute name and add a guard test.

  • Suggest renaming the attribute key to “subscription_ids” to match existing snake_case keys like “subject_keys”.
  • Please add a unit test to lock the error code string and prevent regressions.

Apply within this file:

-func NewErrDeletingCustomerWithActiveSubscriptions(subscriptionIDs []string) error {
-	return ErrDeletingCustomerWithActiveSubscriptions.WithAttr("subscriptions", subscriptionIDs)
-}
+func NewErrDeletingCustomerWithActiveSubscriptions(subscriptionIDs []string) error {
+	return ErrDeletingCustomerWithActiveSubscriptions.WithAttr("subscription_ids", subscriptionIDs)
+}

Minimal test (same package):

package customer

import "testing"

func TestErrCodeDeletingCustomerWithActiveSubscriptions(t *testing.T) {
	const want = "deleting_customer_with_active_subscriptions"
	if string(ErrCodeDeletingCustomerWithActiveSubscriptions) != want {
		t.Fatalf("unexpected code: %q != %q", ErrCodeDeletingCustomerWithActiveSubscriptions, want)
	}
}
openmeter/entitlement/adapter/entitlement.go (3)

62-72: Use a single captured time in GetEntitlement to avoid skew.

Capture clock.Now() once and reuse it for preloads and predicates to ensure a consistent snapshot.

 func (a *entitlementDBAdapter) GetEntitlement(ctx context.Context, entitlementID models.NamespacedID) (*entitlement.Entitlement, error) {
   return entutils.TransactingRepo(
     ctx,
     a,
     func(ctx context.Context, repo *entitlementDBAdapter) (*entitlement.Entitlement, error) {
+      snap := clock.Now().UTC()
       res, err := withAllUsageResets(repo.db.Entitlement.Query(), []string{entitlementID.Namespace}).
         WithCustomer(func(q *db.CustomerQuery) {
-          customeradapter.WithSubjects(q, clock.Now())
-          customeradapter.WithActiveSubscriptions(q, clock.Now())
+          customeradapter.WithSubjects(q, snap)
+          customeradapter.WithActiveSubscriptions(q, snap)
         }).
         WithSubject().
         Where(
           db_entitlement.ID(entitlementID.ID),
           db_entitlement.Namespace(entitlementID.Namespace),
-          db_entitlement.Or(db_entitlement.DeletedAtGT(clock.Now()), db_entitlement.DeletedAtIsNil()),
+          db_entitlement.Or(db_entitlement.DeletedAtGT(snap), db_entitlement.DeletedAtIsNil()),
         ).
         First(ctx)

321-326: Capture ‘now’ once per loop iteration in ListEntitlementsAffectedByIngestEvents.

Multiple clock.Now() calls within the same iteration can drift; capture once and reuse for predicates and preloads.

 for _, pair := range eventFilters {
-  entities, err := repo.db.Entitlement.Query().
+  snap := clock.Now().UTC()
+  entities, err := repo.db.Entitlement.Query().
     Where(
       db_entitlement.Namespace(pair.Namespace),
       db_entitlement.HasCustomerWith(
         customerdb.Namespace(pair.Namespace),
-        customerNotDeletedAt(clock.Now()),
+        customerNotDeletedAt(snap),
         customerdb.HasSubjectsWith(
           customersubjectsdb.SubjectKey(pair.EventSubject),
           customersubjectsdb.DeletedAtIsNil(),
         ),
       ),
       db_entitlement.HasFeatureWith(db_feature.MeterSlugIn(pair.MeterSlugs...)),
     ).
     WithFeature().
     WithCustomer(
       func(q *db.CustomerQuery) {
-        customeradapter.WithSubjects(q, clock.Now())
-        customeradapter.WithActiveSubscriptions(q, clock.Now())
+        customeradapter.WithSubjects(q, snap)
+        customeradapter.WithActiveSubscriptions(q, snap)
       },
     ).

227-229: Consider avoiding Plan edge preloading when only IDs are needed.

entitymapping maps only subscription IDs; preloading Plan in WithActiveSubscriptions adds I/O and memory. If Plan isn’t used on these paths, consider a lighter helper (e.g., WithActiveSubscriptionIDs) or make Plan preload optional.

Also applies to: 360-362, 808-809, 954-955

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e0dd7d1 and 8f9ca28.

📒 Files selected for processing (6)
  • openmeter/customer/adapter/customer.go (4 hunks)
  • openmeter/customer/adapter/entitymapping.go (3 hunks)
  • openmeter/customer/customer.go (1 hunks)
  • openmeter/customer/errors.go (1 hunks)
  • openmeter/customer/service/customer.go (1 hunks)
  • openmeter/entitlement/adapter/entitlement.go (15 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • openmeter/customer/customer.go
  • openmeter/customer/adapter/entitymapping.go
  • openmeter/customer/adapter/customer.go
  • openmeter/customer/service/customer.go
🧰 Additional context used
🧬 Code graph analysis (2)
openmeter/entitlement/adapter/entitlement.go (4)
openmeter/customer/adapter/customer.go (2)
  • WithActiveSubscriptions (629-634)
  • WithSubjects (602-626)
pkg/clock/clock.go (1)
  • Now (14-21)
openmeter/ent/db/customer_query.go (1)
  • CustomerQuery (27-43)
openmeter/customer/adapter/entitymapping.go (1)
  • CustomerFromDBEntity (13-98)
openmeter/customer/errors.go (2)
pkg/models/errors.go (1)
  • ErrorCode (294-294)
pkg/models/validationissue.go (1)
  • NewValidationIssue (219-230)
⏰ 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 (4)
openmeter/entitlement/adapter/entitlement.go (4)

133-144: Consistent time snapshot and edge preloading — nice.

Capturing a single UTC time and reusing it across customer resolution and the post-create reload ensures deterministic results. Looks good.

Also applies to: 224-229


398-406: LGTM: unified ‘now’ for listing path.

Time is captured once and reused across filters and preloads. Good.


716-730: LGTM: snapshot time reused across edge preloads in upsert path.

Consistent and deterministic.


803-816: LGTM: consistent ‘now’ usage in scanners and scheduled queries.

Filters and preloads share the same timestamp.

Also applies to: 952-971

GAlexIHU
GAlexIHU previously approved these changes Sep 11, 2025
@chrisgacsal chrisgacsal merged commit cd605a4 into main Sep 11, 2025
21 checks passed
@chrisgacsal chrisgacsal deleted the fix/customer-delete branch September 11, 2025 10:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/bug-fix Release note: Bug Fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants