package e2e import ( "context" "net/http" "testing" "time" "github.com/brianvoe/gofakeit/v6" cloudevents "github.com/cloudevents/sdk-go/v2/event" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" api "github.com/openmeterio/openmeter/api/client/go" ) var ( MultiSubjectFeatureKey = "multi_subject_feature_1" MultiSubjectMeteredFeatureKey = "multi_subject_metered_feature_1" MultiSubjectPlanKey = "multi_subject_plan" MultiSubjectMeterSlug = "multi_subject_meter" ) func TestMultiSubject(t *testing.T) { client := initClient(t) ctx, cancel := context.WithCancel(context.Background()) defer cancel() var customerId string subjectKeys := []string{"multi_subject_1", "multi_subject_2"} // Let's set up a customer with multiple subjects t.Run("Should create a customer with multiple subjects", func(t *testing.T) { customerAPIRes, err := client.CreateCustomerWithResponse(ctx, api.CreateCustomerJSONRequestBody{ Name: "Test Customer Multi Subject", Currency: lo.ToPtr(api.CurrencyCode("USD")), Description: lo.ToPtr("Test Customer Description"), PrimaryEmail: lo.ToPtr("customer-multi-subject@mail.com"), BillingAddress: &api.Address{ City: lo.ToPtr("City"), Country: lo.ToPtr("US"), Line1: lo.ToPtr("Line 1"), Line2: lo.ToPtr("Line 2"), State: lo.ToPtr("State"), PhoneNumber: lo.ToPtr("1234567890"), PostalCode: lo.ToPtr("12345"), }, UsageAttribution: &api.CustomerUsageAttribution{ SubjectKeys: subjectKeys, }, }) require.Nil(t, err) require.Equal(t, 201, customerAPIRes.StatusCode(), "received the following body: %s", customerAPIRes.Body) customer1 := customerAPIRes.JSON201 require.NotNil(t, customer1) require.Len(t, customer1.UsageAttribution.SubjectKeys, 2) customerId = customer1.Id require.NotEmpty(t, customerId) }) var planId string // Now let's set up a plan (with no billables for simplicity) t.Run("Should be able to set up a plan as normal", func(t *testing.T) { // Now, let's create dedicated features for the plan featureAPIRes, err := client.CreateFeatureWithResponse(ctx, api.CreateFeatureJSONRequestBody{ Key: MultiSubjectFeatureKey, Name: "Test Plan Feature", }) require.Nil(t, err) feature := featureAPIRes.JSON201 require.NotNil(t, feature) // And let's create a dedicated metered feature meteredFeatureAPIRes, err := client.CreateFeatureWithResponse(ctx, api.CreateFeatureJSONRequestBody{ Key: MultiSubjectMeteredFeatureKey, Name: "Test Plan Metered Feature", MeterSlug: lo.ToPtr(MultiSubjectMeterSlug), }) require.Nil(t, err) meteredFeature := meteredFeatureAPIRes.JSON201 require.NotNil(t, meteredFeature) et := &api.RateCardEntitlement{} err = et.FromRateCardBooleanEntitlement(api.RateCardBooleanEntitlement{ Type: api.RateCardBooleanEntitlementType("boolean"), }) require.Nil(t, err) met := &api.RateCardEntitlement{} err = met.FromRateCardMeteredEntitlement(api.RateCardMeteredEntitlement{ Type: api.RateCardMeteredEntitlementType("metered"), IssueAfterReset: lo.ToPtr(100.0), }) require.Nil(t, err) // Lets build a PlanCreate input p1RC1 := api.RateCard{} err = p1RC1.FromRateCardFlatFee(api.RateCardFlatFee{ Name: "Simple Feature", Description: lo.ToPtr("Just a ratecard with an entitlement"), Key: MultiSubjectFeatureKey, FeatureKey: lo.ToPtr(MultiSubjectFeatureKey), TaxConfig: &api.TaxConfig{ Stripe: &api.StripeTaxConfig{ Code: "txcd_10000000", }, }, EntitlementTemplate: et, BillingCadence: nil, Type: api.RateCardFlatFeeType("flat"), }) require.Nil(t, err) p1RC2 := api.RateCard{} err = p1RC2.FromRateCardFlatFee(api.RateCardFlatFee{ Name: "Metered Feature", Description: lo.ToPtr("Has a monthly recurring price to grant access to a feature"), Key: MultiSubjectMeteredFeatureKey, FeatureKey: lo.ToPtr(MultiSubjectMeteredFeatureKey), EntitlementTemplate: met, TaxConfig: &api.TaxConfig{ Stripe: &api.StripeTaxConfig{ Code: "txcd_10000000", }, }, Price: &api.FlatPriceWithPaymentTerm{ Amount: "1000", PaymentTerm: lo.ToPtr(api.PricePaymentTerm("in_advance")), Type: api.FlatPriceWithPaymentTermType("flat"), }, BillingCadence: lo.ToPtr("P1M"), Type: api.RateCardFlatFeeType("flat"), }) require.Nil(t, err) planCreate := api.PlanCreate{ Currency: api.CurrencyCode("USD"), Name: "Test Plan", Description: lo.ToPtr("Test Plan Description"), Key: MultiSubjectPlanKey, BillingCadence: "P1M", Alignment: &api.Alignment{ BillablesMustAlign: lo.ToPtr(true), }, Phases: []api.PlanPhase{ { Name: "Test Plan Phase 1", Key: "test_plan_phase_1", Description: lo.ToPtr("Test Plan Phase 1 Description"), Duration: nil, RateCards: []api.RateCard{p1RC1, p1RC2}, }, }, } planAPIRes, err := client.CreatePlanWithResponse(ctx, planCreate) require.Nil(t, err) require.Equal(t, 201, planAPIRes.StatusCode()) plan := planAPIRes.JSON201 require.NotNil(t, plan, "received the following body: %s", planAPIRes.Body) assert.Equal(t, MultiSubjectPlanKey, plan.Key) require.NotNil(t, plan.Version) assert.Equal(t, 1, plan.Version) require.NotNil(t, plan.Id) planId = plan.Id require.NotEmpty(t, planId) apiRes, err := client.PublishPlanWithResponse(ctx, planId) require.Nil(t, err) assert.Equal(t, 200, apiRes.StatusCode(), "received the following body: %s", apiRes.Body) body := apiRes.JSON200 require.NotNil(t, body) }) t.Run("Should be able to create a subscription for customer with multiple subjects", func(t *testing.T) { immediate := &api.SubscriptionTiming{} require.NoError(t, immediate.FromSubscriptionTimingEnum(api.SubscriptionTimingEnumImmediate)) req := api.CreateSubscriptionJSONRequestBody{} require.NoError(t, req.FromPlanSubscriptionCreate(api.PlanSubscriptionCreate{ CustomerId: lo.ToPtr(customerId), Plan: api.PlanReferenceInput{ Key: MultiSubjectPlanKey, Version: lo.ToPtr(1), }, Timing: immediate, Name: lo.ToPtr("Test Subscription Multi Subject"), })) apiRes, err := client.CreateSubscriptionWithResponse(ctx, req) require.Nil(t, err) require.Equal(t, 201, apiRes.StatusCode(), "received the following body: %s", apiRes.Body) }) t.Run("Should be able to check entitlement access through each subject", func(t *testing.T) { for _, subjectKey := range subjectKeys { apiRes, err := client.GetEntitlementValueWithResponse(ctx, subjectKey, MultiSubjectFeatureKey, nil) require.Nil(t, err) require.Equal(t, 200, apiRes.StatusCode(), "received the following body: %s", apiRes.Body) body := apiRes.JSON200 require.NotNil(t, body) require.True(t, body.HasAccess) } }) t.Run("Should aggregate usage across all subjects", func(t *testing.T) { // Let's ingest usage for each subject // Make clickhouse's job easier by sending events within a fix time range now := time.Now() for _, subjectKey := range subjectKeys { timestamp := gofakeit.DateRange(now, now.Add(time.Second*2)) ev := cloudevents.New() ev.SetID(gofakeit.UUID()) ev.SetSource("my-app") ev.SetType("multi_subject") ev.SetSubject(subjectKey) ev.SetTime(timestamp) resp, err := client.IngestEventWithResponse(context.Background(), ev) require.NoError(t, err) require.Equal(t, http.StatusNoContent, resp.StatusCode()) } // Wait for events to be processed assert.EventuallyWithT(t, func(t *assert.CollectT) { resp, err := client.QueryMeterWithResponse(context.Background(), MultiSubjectMeterSlug, nil) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode()) require.Len(t, resp.JSON200.Data, 1) assert.Equal(t, float64(len(subjectKeys)), resp.JSON200.Data[0].Value) }, time.Minute, time.Second) entResp, err := client.GetCustomerEntitlementValueV2WithResponse(ctx, customerId, MultiSubjectMeteredFeatureKey, nil) require.NoError(t, err) require.Equal(t, http.StatusOK, entResp.StatusCode()) ent := entResp.JSON200 require.NotNil(t, ent) require.True(t, ent.HasAccess) require.NotNil(t, ent.Usage) require.Equal(t, float64(len(subjectKeys)), *ent.Usage) require.NotNil(t, ent.Balance) require.NotNil(t, ent.GrantBalances) require.GreaterOrEqual(t, len(*ent.GrantBalances), 1) var grantBalancesSum float64 for _, grantBalance := range *ent.GrantBalances { grantBalancesSum += grantBalance } assert.Equal(t, *ent.Balance, grantBalancesSum) }) }