-
Notifications
You must be signed in to change notification settings - Fork 122
Fix/test cases billing #746
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
Fix/test cases billing #746
Conversation
WalkthroughA new repository method Changes
Estimated code review effortπ― 4 (Complex) | β±οΈ ~45 minutes
Pre-merge checks and finishing touchesβ Failed checks (1 warning)
β Passed checks (2 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.
Important
Looks good to me! π
Reviewed everything up to b2b5a01 in 38 seconds. Click for details.
- Reviewed
246lines of code in5files - Skipped
0files when reviewing. - Skipped posting
1draft comments. View those below. - Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with π or π to teach Ellipsis.
1. internal/testutil/inmemory_event_store.go:678
- Draft comment:
The methods FindUnprocessedEvents and FindUnprocessedEventsFromFeatureUsage are stubbed to return a 'not implemented' error. Be sure to add a comment/TODO explaining when a lightweight inβmemory simulation might be needed for tests that exercise these methods. - Reason this comment was not posted:
Comment was not on a location in the diff, so it can't be submitted as a review comment.
Workflow ID: wflow_1dEb9VZRDfYncqDJ
You can customize by changing your verbosity settings, reacting with π or π, replying to comments, or adding code review rules.
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
π§Ή Nitpick comments (1)
internal/service/billing.go (1)
1345-1349: LGTM! Clean refactoring from else-if to switch case.Converting the subscription entity type handling to a proper case statement is more idiomatic and improves code clarity. This aligns with the PR objective of fixing the test case issue.
Consider adding a default case to the switch for defensive programming:
case types.ENTITLEMENT_ENTITY_TYPE_SUBSCRIPTION: entityType = dto.EntitlementSourceEntityTypeSubscription // For subscription entitlements, entity_name can be left empty or set to subscription identifier // The entity_id is the subscription ID itself +default: + // Log unexpected entity type or handle as needed + s.Logger.Warnw("unexpected entitlement entity type, defaulting to plan", + "entity_type", ent.EntityType, + "entitlement_id", ent.ID) }
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
π Files selected for processing (5)
internal/domain/events/repository.go(1 hunks)internal/repository/clickhouse/event.go(1 hunks)internal/service/billing.go(1 hunks)internal/service/feature_usage_tracking.go(2 hunks)internal/testutil/inmemory_event_store.go(1 hunks)
π§° Additional context used
π§ Learnings (2)
π Learning: 2025-10-18T10:22:48.348Z
Learnt from: omkar273
Repo: flexprice/flexprice PR: 652
File: internal/service/billing.go:1454-1477
Timestamp: 2025-10-18T10:22:48.348Z
Learning: In `internal/service/billing.go`, the `GetCustomerEntitlements` function passes `subscriptions[0].ID` to `AggregateEntitlements` as a fallback subscription ID. This is acceptable because customers typically have a single subscription, and the subscription ID parameter is only used as fallback metadata in `EntitlementSource` objects, not for business logic or filtering.
Applied to files:
internal/service/billing.go
π Learning: 2025-10-17T21:53:42.505Z
Learnt from: omkar273
Repo: flexprice/flexprice PR: 652
File: internal/service/subscription.go:3495-3534
Timestamp: 2025-10-17T21:53:42.505Z
Learning: In `internal/service/subscription.go`, when adding multiple-type addons to a subscription, existing active line items are intentionally reused rather than creating new line items for each addon association. The addon occurrences are tracked via separate `AddonAssociation` records, but billing happens through the shared `SubscriptionLineItem`. This is the expected behavior and should not be flagged as an issue.
Applied to files:
internal/service/billing.go
𧬠Code graph analysis (5)
internal/testutil/inmemory_event_store.go (2)
internal/domain/events/model.go (2)
FindUnprocessedEventsParams(79-87)Event(13-45)internal/errors/errors.go (1)
ErrSystem(21-21)
internal/domain/events/repository.go (1)
internal/domain/events/model.go (2)
FindUnprocessedEventsParams(79-87)Event(13-45)
internal/service/billing.go (1)
internal/types/entitlement.go (1)
ENTITLEMENT_ENTITY_TYPE_SUBSCRIPTION(49-49)
internal/repository/clickhouse/event.go (5)
internal/domain/events/model.go (2)
FindUnprocessedEventsParams(79-87)Event(13-45)internal/repository/clickhouse/helper.go (4)
StartRepositorySpan(11-35)FinishSpan(38-42)SetSpanError(45-52)SetSpanSuccess(55-59)internal/types/context.go (2)
GetTenantID(33-38)GetEnvironmentID(54-59)internal/errors/builder.go (1)
WithError(26-28)internal/errors/errors.go (2)
ErrDatabase(20-20)ErrValidation(16-16)
internal/service/feature_usage_tracking.go (2)
internal/errors/errors.go (1)
ErrSystem(21-21)internal/errors/builder.go (1)
WithError(26-28)
β° 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: Run Tests and Lint
π Additional comments (4)
internal/domain/events/repository.go (1)
18-18: LGTM! Clean interface addition.The new method
FindUnprocessedEventsFromFeatureUsagefollows the existing pattern and clearly indicates its purpose through naming.internal/testutil/inmemory_event_store.go (1)
684-688: LGTM! Proper test stub implementation.The stub correctly satisfies the interface while clearly indicating the method is not implemented for the in-memory store.
internal/service/feature_usage_tracking.go (2)
144-169: LGTM! Improved variable scoping eliminates shadowing.The refactored approach declares
pubSubandtopiconce at the outer scope and conditionally assigns them based onisBackfill, eliminating potential variable shadowing issues. The nil check at line 151 ensures safe usage.
1907-1907: LGTM! Correct method selection for feature usage reprocessing.The switch to
FindUnprocessedEventsFromFeatureUsagealigns with the refactoring objective to use a feature-usage-specific unprocessed events query.
| // FindUnprocessedEvents finds events that haven't been processed yet | ||
| // Uses keyset pagination for better performance with large datasets | ||
| func (r *EventRepository) FindUnprocessedEventsFromFeatureUsage(ctx context.Context, params *events.FindUnprocessedEventsParams) ([]*events.Event, error) { | ||
| span := StartRepositorySpan(ctx, "event", "find_unprocessed_events", map[string]interface{}{ | ||
| "batch_size": params.BatchSize, | ||
| "external_customer_id": params.ExternalCustomerID, | ||
| }) | ||
| defer FinishSpan(span) | ||
|
|
||
| // Use ANTI JOIN for better performance with ClickHouse | ||
| // This avoids the need for subqueries in the WHERE clause | ||
| // Also using the primary key ORDER BY for efficiency | ||
| query := ` | ||
| SELECT | ||
| e.id, e.external_customer_id, e.customer_id, e.tenant_id, | ||
| e.event_name, e.timestamp, e.source, e.properties, | ||
| e.environment_id, e.ingested_at | ||
| FROM events e | ||
| ANTI JOIN ( | ||
| SELECT id, tenant_id, environment_id | ||
| FROM feature_usage | ||
| WHERE tenant_id = ? | ||
| AND environment_id = ? | ||
| ) AS p | ||
| ON e.id = p.id AND e.tenant_id = p.tenant_id AND e.environment_id = p.environment_id | ||
| WHERE e.tenant_id = ? | ||
| AND e.environment_id = ? | ||
| ` | ||
|
|
||
| args := []interface{}{ | ||
| types.GetTenantID(ctx), | ||
| types.GetEnvironmentID(ctx), | ||
| types.GetTenantID(ctx), | ||
| types.GetEnvironmentID(ctx), | ||
| } | ||
|
|
||
| // Add the last seen ID and timestamp for keyset pagination if provided | ||
| if params.LastID != "" && !params.LastTimestamp.IsZero() { | ||
| // Use keyset pagination for better performance | ||
| query += " AND (e.timestamp, e.id) < (?, ?)" | ||
| args = append(args, params.LastTimestamp, params.LastID) | ||
| } | ||
|
|
||
| // Add filters if provided | ||
| if params.ExternalCustomerID != "" { | ||
| query += " AND e.external_customer_id = ?" | ||
| args = append(args, params.ExternalCustomerID) | ||
| } | ||
|
|
||
| if params.EventName != "" { | ||
| query += " AND e.event_name = ?" | ||
| args = append(args, params.EventName) | ||
| } | ||
|
|
||
| if !params.StartTime.IsZero() { | ||
| query += " AND e.timestamp >= ?" | ||
| args = append(args, params.StartTime) | ||
| } | ||
|
|
||
| if !params.EndTime.IsZero() { | ||
| query += " AND e.timestamp <= ?" | ||
| args = append(args, params.EndTime) | ||
| } | ||
|
|
||
| // Add sorting for consistent keyset pagination | ||
| // Using the same fields we're filtering on for the keyset | ||
| query += " ORDER BY e.timestamp DESC, e.id DESC" | ||
|
|
||
| // Add batch size limit | ||
| if params.BatchSize > 0 { | ||
| query += " LIMIT ?" | ||
| args = append(args, params.BatchSize) | ||
| } else { | ||
| // Default to a reasonable batch size to avoid huge result sets | ||
| query += " LIMIT 100" | ||
| } | ||
|
|
||
| r.logger.Debugw("executing find unprocessed events query", | ||
| "query", query, | ||
| "external_customer_id", params.ExternalCustomerID, | ||
| "event_name", params.EventName, | ||
| "batch_size", params.BatchSize, | ||
| ) | ||
|
|
||
| // Execute the query | ||
| rows, err := r.store.GetConn().Query(ctx, query, args...) | ||
| if err != nil { | ||
| SetSpanError(span, err) | ||
| return nil, ierr.WithError(err). | ||
| WithHint("Failed to query unprocessed events"). | ||
| Mark(ierr.ErrDatabase) | ||
| } | ||
| defer rows.Close() | ||
|
|
||
| var eventsList []*events.Event | ||
| for rows.Next() { | ||
| var event events.Event | ||
| var propertiesJSON string | ||
|
|
||
| err := rows.Scan( | ||
| &event.ID, | ||
| &event.ExternalCustomerID, | ||
| &event.CustomerID, | ||
| &event.TenantID, | ||
| &event.EventName, | ||
| &event.Timestamp, | ||
| &event.Source, | ||
| &propertiesJSON, | ||
| &event.EnvironmentID, | ||
| &event.IngestedAt, | ||
| ) | ||
| if err != nil { | ||
| SetSpanError(span, err) | ||
| return nil, ierr.WithError(err). | ||
| WithHint("Failed to scan event"). | ||
| Mark(ierr.ErrDatabase) | ||
| } | ||
|
|
||
| if err := json.Unmarshal([]byte(propertiesJSON), &event.Properties); err != nil { | ||
| SetSpanError(span, err) | ||
| return nil, ierr.WithError(err). | ||
| WithHint("Failed to unmarshal event properties"). | ||
| Mark(ierr.ErrValidation) | ||
| } | ||
|
|
||
| eventsList = append(eventsList, &event) | ||
| } | ||
|
|
||
| SetSpanSuccess(span) | ||
| return eventsList, 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.
π οΈ Refactor suggestion | π Major
Extract common logic to eliminate ~130 lines of duplication.
The FindUnprocessedEventsFromFeatureUsage method is nearly identical to FindUnprocessedEvents (lines 688-818), with the only difference being the table name in the ANTI JOIN query (feature_usage vs events_processed). This creates significant maintenance burden as bug fixes must be applied to both methods.
Consider refactoring to a shared helper function:
// findUnprocessedEventsFromTable finds events that haven't been processed in the specified table
func (r *EventRepository) findUnprocessedEventsFromTable(
ctx context.Context,
params *events.FindUnprocessedEventsParams,
tableName string,
spanOperation string,
) ([]*events.Event, error) {
span := StartRepositorySpan(ctx, "event", spanOperation, map[string]interface{}{
"batch_size": params.BatchSize,
"external_customer_id": params.ExternalCustomerID,
"table": tableName,
})
defer FinishSpan(span)
query := fmt.Sprintf(`
SELECT
e.id, e.external_customer_id, e.customer_id, e.tenant_id,
e.event_name, e.timestamp, e.source, e.properties,
e.environment_id, e.ingested_at
FROM events e
ANTI JOIN (
SELECT id, tenant_id, environment_id
FROM %s
WHERE tenant_id = ?
AND environment_id = ?
) AS p
ON e.id = p.id AND e.tenant_id = p.tenant_id AND e.environment_id = p.environment_id
WHERE e.tenant_id = ?
AND e.environment_id = ?
`, tableName)
// ... rest of the common logic
}
func (r *EventRepository) FindUnprocessedEvents(ctx context.Context, params *events.FindUnprocessedEventsParams) ([]*events.Event, error) {
return r.findUnprocessedEventsFromTable(ctx, params, "events_processed", "find_unprocessed_events")
}
func (r *EventRepository) FindUnprocessedEventsFromFeatureUsage(ctx context.Context, params *events.FindUnprocessedEventsParams) ([]*events.Event, error) {
return r.findUnprocessedEventsFromTable(ctx, params, "feature_usage", "find_unprocessed_events_from_feature_usage")
}π€ Prompt for AI Agents
internal/repository/clickhouse/event.go lines 820-950: the method duplicates
~130 lines from FindUnprocessedEvents; extract the shared query construction,
arg assembly, query execution and row scanning into a helper (e.g.
findUnprocessedEventsFromTable) that accepts ctx, params, tableName and
spanOperation; build the ANTI JOIN query using fmt.Sprintf to inject tableName,
preserve all existing param handling (LastID/LastTimestamp, ExternalCustomerID,
EventName, StartTime, EndTime, ORDER BY, LIMIT) and logging/span behavior
(include table in span tags), then update FindUnprocessedEvents and
FindUnprocessedEventsFromFeatureUsage to simply call the helper with
"events_processed" and "feature_usage" respectively and return its result.
| // FindUnprocessedEvents finds events that haven't been processed yet | ||
| // Uses keyset pagination for better performance with large datasets | ||
| func (r *EventRepository) FindUnprocessedEventsFromFeatureUsage(ctx context.Context, params *events.FindUnprocessedEventsParams) ([]*events.Event, error) { | ||
| span := StartRepositorySpan(ctx, "event", "find_unprocessed_events", map[string]interface{}{ |
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.
Update span operation name to distinguish from the other method.
The span operation is named "find_unprocessed_events", identical to the existing FindUnprocessedEvents method at line 691. This makes it difficult to distinguish between the two operations in traces and monitoring.
Apply this diff:
- span := StartRepositorySpan(ctx, "event", "find_unprocessed_events", map[string]interface{}{
+ span := StartRepositorySpan(ctx, "event", "find_unprocessed_events_from_feature_usage", map[string]interface{}{π Committable suggestion
βΌοΈ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| span := StartRepositorySpan(ctx, "event", "find_unprocessed_events", map[string]interface{}{ | |
| span := StartRepositorySpan(ctx, "event", "find_unprocessed_events_from_feature_usage", map[string]interface{}{ |
π€ Prompt for AI Agents
In internal/repository/clickhouse/event.go around line 823, the
StartRepositorySpan call uses the operation name "find_unprocessed_events",
which duplicates the span name used by the other FindUnprocessedEvents method at
line 691; change the operation name to a distinct, descriptive string (for
example include method differentiator like "find_unprocessed_events_stream" or
"find_unprocessed_events_with_cursor" or mirror the current function name) so
traces/monitoring can distinguish the two operations, keeping the rest of the
span attributes unchanged.
π Description
π¨ Changes Made
β Checklist
Important
Fix test case in
billing.go, addFindUnprocessedEventsFromFeatureUsage()inevent.go, and refactorPublishEvent()infeature_usage_tracking.go.AggregateEntitlements()inbilling.goto usecaseinstead ofelse iffortypes.ENTITLEMENT_ENTITY_TYPE_SUBSCRIPTION.FindUnprocessedEventsFromFeatureUsage()toevent.gofor querying unprocessed events using ClickHouse.PublishEvent()infeature_usage_tracking.goto streamline backfill logic.FindUnprocessedEventsFromFeatureUsage()ininmemory_event_store.go.This description was created by
for b2b5a01. You can customize this summary. It will automatically update as commits are pushed.
Summary by CodeRabbit
New Features
Bug Fixes