Skip to content

Commit 3870828

Browse files
committed
test: customer's billing profile id filter to cover the unpinned billing profiles
1 parent dc07897 commit 3870828

1 file changed

Lines changed: 155 additions & 0 deletions

File tree

openmeter/customer/adapter/customer_test.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import (
55
"time"
66

77
"github.com/oklog/ulid/v2"
8+
"github.com/samber/lo"
89
"github.com/stretchr/testify/assert"
910
"github.com/stretchr/testify/require"
1011

1112
"github.com/openmeterio/openmeter/openmeter/app"
13+
"github.com/openmeterio/openmeter/openmeter/billing"
1214
"github.com/openmeterio/openmeter/openmeter/customer"
1315
customeradapter "github.com/openmeterio/openmeter/openmeter/customer/adapter"
1416
entdb "github.com/openmeterio/openmeter/openmeter/ent/db"
@@ -19,7 +21,10 @@ import (
1921
customersubjectsdb "github.com/openmeterio/openmeter/openmeter/ent/db/customersubjects"
2022
"github.com/openmeterio/openmeter/openmeter/testutils"
2123
"github.com/openmeterio/openmeter/pkg/clock"
24+
"github.com/openmeterio/openmeter/pkg/datetime"
25+
"github.com/openmeterio/openmeter/pkg/filter"
2226
"github.com/openmeterio/openmeter/pkg/models"
27+
"github.com/openmeterio/openmeter/pkg/pagination"
2328
)
2429

2530
type fixture struct {
@@ -358,6 +363,156 @@ func TestDeleteCustomer(t *testing.T) {
358363
})
359364
}
360365

366+
func TestListCustomersBillingProfileIDFilter(t *testing.T) {
367+
// The BillingProfileID filter targets the customer's *effective* billing
368+
// profile = COALESCE(override.billing_profile_id, namespace_default_profile.id).
369+
// A customer is "pinned" when it has a live override with a non-null
370+
// billing_profile_id; it is "unpinned" (resolves to the default) when it has
371+
// no live override OR an override with a NULL billing_profile_id.
372+
373+
env := newTestEnv(t)
374+
ns := ulid.Make().String()
375+
376+
appID := env.seedApp(ns)
377+
defaultProfileID := env.seedBillingProfile(ns, appID, true)
378+
otherProfileID := env.seedBillingProfile(ns, appID, false)
379+
380+
// Customers covering every override state.
381+
pinnedToDefault := env.seedCustomer(ns, "pinned-to-default")
382+
env.seedBillingCustomerOverride(ns, pinnedToDefault, &defaultProfileID)
383+
384+
pinnedToOther := env.seedCustomer(ns, "pinned-to-other")
385+
env.seedBillingCustomerOverride(ns, pinnedToOther, &otherProfileID)
386+
387+
nullOverride := env.seedCustomer(ns, "null-override")
388+
env.seedBillingCustomerOverride(ns, nullOverride, nil)
389+
390+
noOverride := env.seedCustomer(ns, "no-override")
391+
392+
t.Run("DefaultProfileIDReturnsPinnedAndUnpinned", func(t *testing.T) {
393+
// given:
394+
// - one default and one non-default billing profile in the namespace
395+
// - customers pinned to default, pinned to other, with a null override,
396+
// and with no override at all
397+
// when:
398+
// - filtering by the default profile id
399+
// then:
400+
// - customers explicitly pinned to the default are returned, plus the
401+
// unpinned customers (null override and no override) that fall back to
402+
// the default; the customer pinned to a different profile is excluded.
403+
got := env.listCustomerIDsByBillingProfileID(t, ns, defaultProfileID)
404+
405+
require.ElementsMatch(t, []string{pinnedToDefault, nullOverride, noOverride}, got)
406+
})
407+
408+
t.Run("NonDefaultProfileIDReturnsOnlyExplicitlyPinned", func(t *testing.T) {
409+
// given:
410+
// - the same customer set as above
411+
// when:
412+
// - filtering by a non-default profile id
413+
// then:
414+
// - only the customer explicitly pinned to that profile is returned; the
415+
// unpinned (fallback-to-default) customers are excluded.
416+
got := env.listCustomerIDsByBillingProfileID(t, ns, otherProfileID)
417+
418+
require.ElementsMatch(t, []string{pinnedToOther}, got)
419+
})
420+
}
421+
422+
// listCustomerIDsByBillingProfileID runs ListCustomers with an eq filter on the
423+
// billing profile id and returns the matched customer ids.
424+
func (e *testEnv) listCustomerIDsByBillingProfileID(t *testing.T, ns, profileID string) []string {
425+
t.Helper()
426+
427+
res, err := e.adapter.ListCustomers(t.Context(), customer.ListCustomersInput{
428+
Namespace: ns,
429+
Page: pagination.Page{PageNumber: 1, PageSize: 1000},
430+
BillingProfileID: &filter.FilterULID{
431+
FilterString: filter.FilterString{Eq: lo.ToPtr(profileID)},
432+
},
433+
})
434+
require.NoError(t, err, "listing customers must not fail")
435+
436+
return lo.Map(res.Items, func(c customer.Customer, _ int) string { return c.ID })
437+
}
438+
439+
// seedApp creates a bare App row usable as the tax/invoicing/payment app for a
440+
// billing profile and returns its id.
441+
func (e *testEnv) seedApp(namespace string) string {
442+
e.t.Helper()
443+
444+
appRow, err := e.db.App.Create().
445+
SetNamespace(namespace).
446+
SetName("billing-app").
447+
SetType(app.AppTypeStripe).
448+
SetStatus(app.AppStatusReady).
449+
Save(e.t.Context())
450+
require.NoError(e.t, err, "seeding billing app must not fail")
451+
452+
return appRow.ID
453+
}
454+
455+
// seedBillingProfile creates a billing profile (optionally the namespace default)
456+
// wired to the given app for tax/invoicing/payment plus its required workflow
457+
// config, and returns the profile id.
458+
func (e *testEnv) seedBillingProfile(namespace, appID string, isDefault bool) string {
459+
e.t.Helper()
460+
ctx := e.t.Context()
461+
462+
wc, err := e.db.BillingWorkflowConfig.Create().
463+
SetNamespace(namespace).
464+
SetCollectionAlignment(billing.AlignmentKindSubscription).
465+
SetLineCollectionPeriod(datetime.ISODurationString("P1D")).
466+
SetInvoiceAutoAdvance(true).
467+
SetInvoiceDraftPeriod(datetime.ISODurationString("P1D")).
468+
SetInvoiceDueAfter(datetime.ISODurationString("P1D")).
469+
SetInvoiceCollectionMethod(billing.CollectionMethodChargeAutomatically).
470+
SetInvoiceProgressiveBilling(false).
471+
Save(ctx)
472+
require.NoError(e.t, err, "seeding billing workflow config must not fail")
473+
474+
profile, err := e.db.BillingProfile.Create().
475+
SetNamespace(namespace).
476+
SetName("billing-profile").
477+
SetWorkflowConfig(wc).
478+
SetTaxAppID(appID).
479+
SetInvoicingAppID(appID).
480+
SetPaymentAppID(appID).
481+
SetSupplierName("test-supplier").
482+
SetDefault(isDefault).
483+
Save(ctx)
484+
require.NoError(e.t, err, "seeding billing profile must not fail")
485+
486+
return profile.ID
487+
}
488+
489+
// seedCustomer creates a bare customer in the namespace and returns its id.
490+
func (e *testEnv) seedCustomer(namespace, name string) string {
491+
e.t.Helper()
492+
493+
cust, err := e.db.Customer.Create().
494+
SetNamespace(namespace).
495+
SetName(name).
496+
Save(e.t.Context())
497+
require.NoError(e.t, err, "seeding customer must not fail")
498+
499+
return cust.ID
500+
}
501+
502+
// seedBillingCustomerOverride creates a billing customer override for the
503+
// customer. A nil profileID stores a NULL billing_profile_id (the customer is
504+
// unpinned but still has an override row).
505+
func (e *testEnv) seedBillingCustomerOverride(namespace, customerID string, profileID *string) {
506+
e.t.Helper()
507+
508+
_, err := e.db.BillingCustomerOverride.Create().
509+
SetNamespace(namespace).
510+
SetCustomerID(customerID).
511+
SetNillableBillingProfileID(profileID).
512+
Save(e.t.Context())
513+
require.NoError(e.t, err, "seeding billing customer override must not fail")
514+
}
515+
361516
// --- assertion helpers ---
362517

363518
func assertCustomerDeletedAt(t *testing.T, db *entdb.Client, ns, id string, want time.Time) {

0 commit comments

Comments
 (0)