Skip to content

Commit b70d40e

Browse files
authored
feat(api): add v3 customer and subscription list filters (#4336)
1 parent cae0526 commit b70d40e

17 files changed

Lines changed: 433 additions & 298 deletions

File tree

api/spec/packages/aip/src/customers/operations.tsp

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,18 @@ namespace Customers;
1919
*/
2020
@friendlyName("ListCustomersParamsFilter")
2121
model ListCustomersParamsFilter {
22-
/**
23-
* Filter customers by key.
24-
*/
22+
#suppress "@openmeter/api-spec-aip/doc-decorator" "filter field"
2523
key?: Common.StringFieldFilter;
26-
27-
/**
28-
* Filter customers by name.
29-
*/
24+
#suppress "@openmeter/api-spec-aip/doc-decorator" "filter field"
3025
name?: Common.StringFieldFilter;
31-
32-
/**
33-
* Filter customers by primary email.
34-
*/
26+
#suppress "@openmeter/api-spec-aip/doc-decorator" "filter field"
3527
primary_email?: Common.StringFieldFilter;
28+
#suppress "@openmeter/api-spec-aip/doc-decorator" "filter field"
29+
usage_attribution_subject_key?: Common.StringFieldFilter;
30+
#suppress "@openmeter/api-spec-aip/doc-decorator" "filter field"
31+
plan_key?: Common.StringFieldFilter;
32+
#suppress "@openmeter/api-spec-aip/doc-decorator" "filter field"
33+
billing_profile_id?: Common.ULIDFieldFilter;
3634
}
3735

3836
interface CustomersOperations {

api/spec/packages/aip/src/subscriptions/operations.tsp

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,17 @@ model SubscriptionChangeResponse {
5353
next: Subscription;
5454
}
5555

56+
/**
57+
* Filter options for listing subscriptions.
58+
*/
59+
@friendlyName("ListSubscriptionsParamsFilter")
60+
model ListSubscriptionsParamsFilter {
61+
#suppress "@openmeter/api-spec-aip/doc-decorator" "filter field"
62+
customer_id?: Common.ULIDFieldFilter;
63+
#suppress "@openmeter/api-spec-aip/doc-decorator" "filter field"
64+
plan_key?: Common.StringFieldFilter;
65+
}
66+
5667
interface SubscriptionsOperations {
5768
@post
5869
@operationId("create-subscription")
@@ -73,12 +84,7 @@ interface SubscriptionsOperations {
7384
* Filter subscriptions.
7485
*/
7586
@query(#{ style: "deepObject", explode: true })
76-
filter?: {
77-
/**
78-
* Filter subscriptions by customer ID.
79-
*/
80-
customer_id?: Shared.ULID;
81-
},
87+
filter?: ListSubscriptionsParamsFilter,
8288
): Shared.PagePaginatedResponse<Subscription> | Common.NotFound | Common.ErrorResponses;
8389

8490
@get

api/v3/api.gen.go

Lines changed: 230 additions & 208 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v3/handlers/customers/list.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,27 @@ func (h *handler) ListCustomers() ListCustomersHandler {
9898
})
9999
}
100100
req.PrimaryEmail = primaryEmail
101+
usageAttributionSubjectKey, err := filters.FromAPIFilterString(params.Filter.UsageAttributionSubjectKey)
102+
if err != nil {
103+
return ListCustomersRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{
104+
{Field: "filter[usage_attribution_subject_key]", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery},
105+
})
106+
}
107+
req.UsageAttributionSubjectKey = usageAttributionSubjectKey
108+
planKey, err := filters.FromAPIFilterString(params.Filter.PlanKey)
109+
if err != nil {
110+
return ListCustomersRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{
111+
{Field: "filter[plan_key]", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery},
112+
})
113+
}
114+
req.PlanKey = planKey
115+
billingProfileID, err := filters.FromAPIFilterULID(params.Filter.BillingProfileId)
116+
if err != nil {
117+
return ListCustomersRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{
118+
{Field: "filter[billing_profile_id]", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery},
119+
})
120+
}
121+
req.BillingProfileID = billingProfileID
101122
}
102123

103124
return req, nil

api/v3/handlers/subscriptions/list.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
api "github.com/openmeterio/openmeter/api/v3"
1010
"github.com/openmeterio/openmeter/api/v3/apierrors"
11+
"github.com/openmeterio/openmeter/api/v3/filters"
1112
"github.com/openmeterio/openmeter/api/v3/response"
1213
"github.com/openmeterio/openmeter/openmeter/subscription"
1314
"github.com/openmeterio/openmeter/pkg/framework/commonhttp"
@@ -58,11 +59,20 @@ func (h *handler) ListSubscriptions() ListSubscriptionsHandler {
5859

5960
// Filters
6061
if params.Filter != nil {
61-
// Filter by customer ID
62-
if params.Filter.CustomerId != nil {
63-
// Add the customer ID filter to the request
64-
req.CustomerIDs = []string{*params.Filter.CustomerId}
62+
customerID, err := filters.FromAPIFilterULID(params.Filter.CustomerId)
63+
if err != nil {
64+
return ListSubscriptionsRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{
65+
{Field: "filter[customer_id]", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery},
66+
})
6567
}
68+
req.CustomerID = customerID
69+
planKey, err := filters.FromAPIFilterString(params.Filter.PlanKey)
70+
if err != nil {
71+
return ListSubscriptionsRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{
72+
{Field: "filter[plan_key]", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery},
73+
})
74+
}
75+
req.PlanKey = planKey
6676
}
6777

6878
return req, nil

api/v3/openapi.yaml

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2488,13 +2488,7 @@ paths:
24882488
required: false
24892489
description: Filter subscriptions.
24902490
schema:
2491-
type: object
2492-
properties:
2493-
customer_id:
2494-
allOf:
2495-
- $ref: '#/components/schemas/ULID'
2496-
description: Filter subscriptions by customer ID.
2497-
additionalProperties: false
2491+
$ref: '#/components/schemas/ListSubscriptionsParamsFilter'
24982492
style: deepObject
24992493
responses:
25002494
'200':
@@ -7833,17 +7827,17 @@ components:
78337827
type: object
78347828
properties:
78357829
key:
7836-
allOf:
7837-
- $ref: '#/components/schemas/StringFieldFilter'
7838-
description: Filter customers by key.
7830+
$ref: '#/components/schemas/StringFieldFilter'
78397831
name:
7840-
allOf:
7841-
- $ref: '#/components/schemas/StringFieldFilter'
7842-
description: Filter customers by name.
7832+
$ref: '#/components/schemas/StringFieldFilter'
78437833
primary_email:
7844-
allOf:
7845-
- $ref: '#/components/schemas/StringFieldFilter'
7846-
description: Filter customers by primary email.
7834+
$ref: '#/components/schemas/StringFieldFilter'
7835+
usage_attribution_subject_key:
7836+
$ref: '#/components/schemas/StringFieldFilter'
7837+
plan_key:
7838+
$ref: '#/components/schemas/StringFieldFilter'
7839+
billing_profile_id:
7840+
$ref: '#/components/schemas/ULIDFieldFilter'
78477841
additionalProperties: false
78487842
description: Filter options for listing customers.
78497843
ListEventsParamsFilter:
@@ -7945,6 +7939,15 @@ components:
79457939
$ref: '#/components/schemas/StringFieldFilterExact'
79467940
additionalProperties: false
79477941
description: Filter options for listing plans.
7942+
ListSubscriptionsParamsFilter:
7943+
type: object
7944+
properties:
7945+
customer_id:
7946+
$ref: '#/components/schemas/ULIDFieldFilter'
7947+
plan_key:
7948+
$ref: '#/components/schemas/StringFieldFilter'
7949+
additionalProperties: false
7950+
description: Filter options for listing subscriptions.
79487951
Meter:
79497952
type: object
79507953
required:

openmeter/billing/validators/customer/customer.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/openmeterio/openmeter/openmeter/billing/worker/subscriptionsync"
1111
"github.com/openmeterio/openmeter/openmeter/customer"
1212
"github.com/openmeterio/openmeter/openmeter/subscription"
13+
"github.com/openmeterio/openmeter/pkg/filter"
1314
)
1415

1516
var _ customer.RequestValidator = (*Validator)(nil)
@@ -46,8 +47,8 @@ func (v *Validator) ValidateDeleteCustomer(ctx context.Context, input customer.D
4647

4748
// Let's sync any subscriptions pending for this customer
4849
subs, err := v.subscriptionService.List(ctx, subscription.ListSubscriptionsInput{
49-
Namespaces: []string{input.Namespace},
50-
CustomerIDs: []string{input.ID},
50+
Namespaces: []string{input.Namespace},
51+
CustomerID: &filter.FilterULID{FilterString: filter.FilterString{Eq: &input.ID}},
5152
})
5253
if err != nil {
5354
return err

openmeter/billing/worker/subscriptionsync/reconciler/reconciler.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/openmeterio/openmeter/openmeter/customer"
1414
"github.com/openmeterio/openmeter/openmeter/subscription"
1515
"github.com/openmeterio/openmeter/pkg/clock"
16+
"github.com/openmeterio/openmeter/pkg/filter"
1617
"github.com/openmeterio/openmeter/pkg/models"
1718
"github.com/openmeterio/openmeter/pkg/pagination"
1819
"github.com/openmeterio/openmeter/pkg/timeutil"
@@ -102,8 +103,8 @@ func (r *Reconciler) ListSubscriptions(ctx context.Context, in ReconcilerListSub
102103

103104
for {
104105
subscriptions, err := r.subscriptionService.List(ctx, subscription.ListSubscriptionsInput{
105-
Namespaces: in.Namespaces,
106-
CustomerIDs: in.Customers,
106+
Namespaces: in.Namespaces,
107+
CustomerID: &filter.FilterULID{FilterString: filter.FilterString{In: &in.Customers}},
107108
ActiveInPeriod: &timeutil.StartBoundedPeriod{
108109
From: clock.Now().Add(-in.Lookback),
109110
To: lo.ToPtr(clock.Now()),

openmeter/customer/adapter/customer.go

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
entdb "github.com/openmeterio/openmeter/openmeter/ent/db"
1414
appcustomerdb "github.com/openmeterio/openmeter/openmeter/ent/db/appcustomer"
1515
appstripecustomerdb "github.com/openmeterio/openmeter/openmeter/ent/db/appstripecustomer"
16+
billingcustomeroverridedb "github.com/openmeterio/openmeter/openmeter/ent/db/billingcustomeroverride"
1617
customerdb "github.com/openmeterio/openmeter/openmeter/ent/db/customer"
1718
customersubjectsdb "github.com/openmeterio/openmeter/openmeter/ent/db/customersubjects"
1819
plandb "github.com/openmeterio/openmeter/openmeter/ent/db/plan"
@@ -56,18 +57,35 @@ func (a *adapter) ListCustomers(ctx context.Context, input customer.ListCustomer
5657
query = filter.ApplyToQuery(query, input.Name, customerdb.FieldName)
5758
query = filter.ApplyToQuery(query, input.PrimaryEmail, customerdb.FieldPrimaryEmail)
5859

59-
if input.Subject != nil {
60-
query = query.Where(customerdb.HasSubjectsWith(
61-
customersubjectsdb.SubjectKeyContainsFold(*input.Subject),
62-
customersubjectsdb.Or(
63-
customersubjectsdb.DeletedAtIsNil(),
64-
customersubjectsdb.DeletedAtGTE(now),
65-
),
66-
))
60+
if input.PlanKey != nil {
61+
applyActiveSubscriptionFilterWithPlanKey(query, now, input.PlanKey)
62+
}
63+
64+
if input.UsageAttributionSubjectKey != nil {
65+
if p := filter.SelectPredicate[predicate.CustomerSubjects](
66+
filter.Filter(*input.UsageAttributionSubjectKey),
67+
customersubjectsdb.FieldSubjectKey,
68+
); p != nil {
69+
query = query.Where(customerdb.HasSubjectsWith(
70+
*p,
71+
customersubjectsdb.Or(
72+
customersubjectsdb.DeletedAtIsNil(),
73+
customersubjectsdb.DeletedAtGTE(now),
74+
),
75+
))
76+
}
6777
}
6878

69-
if input.PlanKey != nil {
70-
applyActiveSubscriptionFilterWithPlanKey(query, now, *input.PlanKey)
79+
if input.BillingProfileID != nil {
80+
if p := filter.SelectPredicate[predicate.BillingCustomerOverride](
81+
filter.Filter(*input.BillingProfileID),
82+
billingcustomeroverridedb.FieldBillingProfileID,
83+
); p != nil {
84+
query = query.Where(customerdb.HasBillingCustomerOverrideWith(
85+
*p,
86+
billingcustomeroverridedb.DeletedAtIsNil(),
87+
))
88+
}
7189
}
7290

7391
if len(input.CustomerIDs) > 0 {
@@ -751,12 +769,14 @@ func applyActiveSubscriptionFilter(query *entdb.SubscriptionQuery, at time.Time)
751769
query.Where(activeSubscriptionFilter(at)...)
752770
}
753771

754-
func applyActiveSubscriptionFilterWithPlanKey(query *entdb.CustomerQuery, at time.Time, planKey string) {
755-
predicates := activeSubscriptionFilter(at)
772+
func applyActiveSubscriptionFilterWithPlanKey(query *entdb.CustomerQuery, at time.Time, planKey *filter.FilterString) {
773+
p := filter.SelectPredicate[predicate.Plan](filter.Filter(*planKey), plandb.FieldKey)
774+
if p == nil {
775+
return
776+
}
756777

757-
predicates = append(predicates, subscriptiondb.HasPlanWith(
758-
plandb.Key(planKey),
759-
))
778+
predicates := activeSubscriptionFilter(at)
779+
predicates = append(predicates, subscriptiondb.HasPlanWith(*p))
760780

761781
query.Where(
762782
customerdb.HasSubscriptionWith(predicates...),

openmeter/customer/customer.go

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -304,12 +304,13 @@ type ListCustomersInput struct {
304304
Order sortx.Order
305305

306306
// Filters
307-
Key *filter.FilterString
308-
Name *filter.FilterString
309-
PrimaryEmail *filter.FilterString
310-
Subject *string
311-
PlanKey *string
312-
CustomerIDs []string
307+
Key *filter.FilterString
308+
Name *filter.FilterString
309+
PrimaryEmail *filter.FilterString
310+
UsageAttributionSubjectKey *filter.FilterString
311+
PlanKey *filter.FilterString
312+
CustomerIDs []string
313+
BillingProfileID *filter.FilterULID
313314

314315
// Expand
315316
Expands Expands
@@ -344,6 +345,24 @@ func (i ListCustomersInput) Validate() error {
344345
}
345346
}
346347

348+
if i.UsageAttributionSubjectKey != nil {
349+
if err := i.UsageAttributionSubjectKey.Validate(); err != nil {
350+
errs = append(errs, models.NewGenericValidationError(fmt.Errorf("invalid usage attribution subject key filter: %w", err)))
351+
}
352+
}
353+
354+
if i.PlanKey != nil {
355+
if err := i.PlanKey.Validate(); err != nil {
356+
errs = append(errs, models.NewGenericValidationError(fmt.Errorf("invalid plan key filter: %w", err)))
357+
}
358+
}
359+
360+
if i.BillingProfileID != nil {
361+
if err := i.BillingProfileID.Validate(); err != nil {
362+
errs = append(errs, models.NewGenericValidationError(fmt.Errorf("invalid billing profile id filter: %w", err)))
363+
}
364+
}
365+
347366
return models.NewNillableGenericValidationError(errors.Join(errs...))
348367
}
349368

0 commit comments

Comments
 (0)