From 6aa77ee490d14edb7d9cc9a1312d072705b2541d Mon Sep 17 00:00:00 2001 From: Rohil Surana Date: Fri, 25 Sep 2026 15:27:37 +0530 Subject: [PATCH 1/3] feat(store): org tokens search skips soft-deleted rows --- .../store/postgres/org_tokens_repository.go | 24 +++++++++++++------ .../postgres/org_tokens_repository_test.go | 12 +++++----- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/internal/store/postgres/org_tokens_repository.go b/internal/store/postgres/org_tokens_repository.go index ad750177f1..caac514111 100644 --- a/internal/store/postgres/org_tokens_repository.go +++ b/internal/store/postgres/org_tokens_repository.go @@ -119,7 +119,12 @@ func (r OrgTokensRepository) prepareDataQuery(orgID string, rql *rql.Query) (str // we need to cast the user_id to text since it's stored as text in billing_transactions but the users.id is uuid. func (r OrgTokensRepository) buildBaseQuery(orgID string) *goqu.SelectDataset { - return dialect.From(TABLE_BILLING_TRANSACTIONS).Prepared(true). + liveUserOfTransaction := goqu.On( + goqu.L("CASE WHEN \"billing_transactions\".\"user_id\" IS NOT NULL AND \"billing_transactions\".\"user_id\" != '' THEN CAST(\"billing_transactions\".\"user_id\" AS uuid) = \"users\".\"id\" ELSE false END"), + live(TABLE_USERS), + ) + + return fromLive(TABLE_BILLING_TRANSACTIONS).Prepared(true). Select( goqu.I(TABLE_BILLING_TRANSACTIONS+"."+COLUMN_AMOUNT).As("token_amount"), goqu.I(TABLE_BILLING_TRANSACTIONS+"."+COLUMN_TYPE).As("token_type"), @@ -135,13 +140,18 @@ func (r OrgTokensRepository) buildBaseQuery(orgID string) *goqu.SelectDataset { goqu.T(TABLE_BILLING_CUSTOMERS), goqu.On(goqu.I(TABLE_BILLING_TRANSACTIONS+"."+COLUMN_ACCOUNT_ID).Eq(goqu.I(TABLE_BILLING_CUSTOMERS+".id"))), ). - LeftJoin( - goqu.T(TABLE_USERS), - goqu.On(goqu.L("CASE WHEN \"billing_transactions\".\"user_id\" IS NOT NULL AND \"billing_transactions\".\"user_id\" != '' THEN CAST(\"billing_transactions\".\"user_id\" AS uuid) = \"users\".\"id\" ELSE false END")), + InnerJoin( + goqu.T(TABLE_ORGANIZATIONS), + goqu.On(goqu.I(TABLE_BILLING_CUSTOMERS+"."+COLUMN_ORG_ID).Eq(goqu.I(TABLE_ORGANIZATIONS+"."+COLUMN_ID))), ). - Where(goqu.Ex{ - TABLE_BILLING_CUSTOMERS + "." + COLUMN_ORG_ID: orgID, - }) + LeftJoin(goqu.T(TABLE_USERS), liveUserOfTransaction). + Where( + goqu.Ex{ + TABLE_BILLING_CUSTOMERS + "." + COLUMN_ORG_ID: orgID, + }, + live(TABLE_BILLING_CUSTOMERS), + live(TABLE_ORGANIZATIONS), + ) } func (r OrgTokensRepository) addFilter(query *goqu.SelectDataset, filter rql.Filter) (*goqu.SelectDataset, error) { diff --git a/internal/store/postgres/org_tokens_repository_test.go b/internal/store/postgres/org_tokens_repository_test.go index 65475e481d..6493d1bb95 100644 --- a/internal/store/postgres/org_tokens_repository_test.go +++ b/internal/store/postgres/org_tokens_repository_test.go @@ -23,7 +23,7 @@ func TestOrgTokensRepository_prepareDataQuery(t *testing.T) { Limit: 10, Offset: 20, }, - wantSQL: `SELECT "billing_transactions"."amount" AS "token_amount", "billing_transactions"."type" AS "token_type", "billing_transactions"."source" AS "token_source", "billing_transactions"."description" AS "token_description", "billing_transactions"."user_id" AS "token_user_id", "users"."title" AS "user_title", "users"."avatar" AS "user_avatar", "billing_transactions"."created_at" AS "token_created_at", "billing_customers"."org_id" AS "org_id" FROM "billing_transactions" INNER JOIN "billing_customers" ON ("billing_transactions"."account_id" = "billing_customers"."id") LEFT JOIN "users" ON CASE WHEN "billing_transactions"."user_id" IS NOT NULL AND "billing_transactions"."user_id" != '' THEN CAST("billing_transactions"."user_id" AS uuid) = "users"."id" ELSE false END WHERE ("billing_customers"."org_id" = $1) LIMIT $2 OFFSET $3`, + wantSQL: `SELECT "billing_transactions"."amount" AS "token_amount", "billing_transactions"."type" AS "token_type", "billing_transactions"."source" AS "token_source", "billing_transactions"."description" AS "token_description", "billing_transactions"."user_id" AS "token_user_id", "users"."title" AS "user_title", "users"."avatar" AS "user_avatar", "billing_transactions"."created_at" AS "token_created_at", "billing_customers"."org_id" AS "org_id" FROM "billing_transactions" INNER JOIN "billing_customers" ON ("billing_transactions"."account_id" = "billing_customers"."id") INNER JOIN "organizations" ON ("billing_customers"."org_id" = "organizations"."id") LEFT JOIN "users" ON (CASE WHEN "billing_transactions"."user_id" IS NOT NULL AND "billing_transactions"."user_id" != '' THEN CAST("billing_transactions"."user_id" AS uuid) = "users"."id" ELSE false END AND ("users"."deleted_at" IS NULL)) WHERE (("billing_transactions"."deleted_at" IS NULL) AND ("billing_customers"."org_id" = $1) AND ("billing_customers"."deleted_at" IS NULL) AND ("organizations"."deleted_at" IS NULL)) LIMIT $2 OFFSET $3`, wantParams: []any{"org123", int64(10), int64(20)}, wantErr: false, }, @@ -41,7 +41,7 @@ func TestOrgTokensRepository_prepareDataQuery(t *testing.T) { Limit: 10, Offset: 30, }, - wantSQL: `SELECT "billing_transactions"."amount" AS "token_amount", "billing_transactions"."type" AS "token_type", "billing_transactions"."source" AS "token_source", "billing_transactions"."description" AS "token_description", "billing_transactions"."user_id" AS "token_user_id", "users"."title" AS "user_title", "users"."avatar" AS "user_avatar", "billing_transactions"."created_at" AS "token_created_at", "billing_customers"."org_id" AS "org_id" FROM "billing_transactions" INNER JOIN "billing_customers" ON ("billing_transactions"."account_id" = "billing_customers"."id") LEFT JOIN "users" ON CASE WHEN "billing_transactions"."user_id" IS NOT NULL AND "billing_transactions"."user_id" != '' THEN CAST("billing_transactions"."user_id" AS uuid) = "users"."id" ELSE false END WHERE (("billing_customers"."org_id" = $1) AND ("billing_transactions"."amount" >= $2)) LIMIT $3 OFFSET $4`, + wantSQL: `SELECT "billing_transactions"."amount" AS "token_amount", "billing_transactions"."type" AS "token_type", "billing_transactions"."source" AS "token_source", "billing_transactions"."description" AS "token_description", "billing_transactions"."user_id" AS "token_user_id", "users"."title" AS "user_title", "users"."avatar" AS "user_avatar", "billing_transactions"."created_at" AS "token_created_at", "billing_customers"."org_id" AS "org_id" FROM "billing_transactions" INNER JOIN "billing_customers" ON ("billing_transactions"."account_id" = "billing_customers"."id") INNER JOIN "organizations" ON ("billing_customers"."org_id" = "organizations"."id") LEFT JOIN "users" ON (CASE WHEN "billing_transactions"."user_id" IS NOT NULL AND "billing_transactions"."user_id" != '' THEN CAST("billing_transactions"."user_id" AS uuid) = "users"."id" ELSE false END AND ("users"."deleted_at" IS NULL)) WHERE (("billing_transactions"."deleted_at" IS NULL) AND ("billing_customers"."org_id" = $1) AND ("billing_customers"."deleted_at" IS NULL) AND ("organizations"."deleted_at" IS NULL) AND ("billing_transactions"."amount" >= $2)) LIMIT $3 OFFSET $4`, wantParams: []any{"org123", int64(1000), int64(10), int64(30)}, wantErr: false, }, @@ -60,7 +60,7 @@ func TestOrgTokensRepository_prepareDataQuery(t *testing.T) { Limit: 10, Offset: 40, }, - wantSQL: `SELECT "billing_transactions"."amount" AS "token_amount", "billing_transactions"."type" AS "token_type", "billing_transactions"."source" AS "token_source", "billing_transactions"."description" AS "token_description", "billing_transactions"."user_id" AS "token_user_id", "users"."title" AS "user_title", "users"."avatar" AS "user_avatar", "billing_transactions"."created_at" AS "token_created_at", "billing_customers"."org_id" AS "org_id" FROM "billing_transactions" INNER JOIN "billing_customers" ON ("billing_transactions"."account_id" = "billing_customers"."id") LEFT JOIN "users" ON CASE WHEN "billing_transactions"."user_id" IS NOT NULL AND "billing_transactions"."user_id" != '' THEN CAST("billing_transactions"."user_id" AS uuid) = "users"."id" ELSE false END WHERE (("billing_customers"."org_id" = $1) AND ("billing_transactions"."type" = $2) AND ((CAST("billing_transactions"."type" AS TEXT) ILIKE $3) OR (CAST("billing_transactions"."description" AS TEXT) ILIKE $4) OR (CAST("users"."title" AS TEXT) ILIKE $5) OR (CAST("billing_transactions"."amount" AS TEXT) ILIKE $6))) LIMIT $7 OFFSET $8`, + wantSQL: `SELECT "billing_transactions"."amount" AS "token_amount", "billing_transactions"."type" AS "token_type", "billing_transactions"."source" AS "token_source", "billing_transactions"."description" AS "token_description", "billing_transactions"."user_id" AS "token_user_id", "users"."title" AS "user_title", "users"."avatar" AS "user_avatar", "billing_transactions"."created_at" AS "token_created_at", "billing_customers"."org_id" AS "org_id" FROM "billing_transactions" INNER JOIN "billing_customers" ON ("billing_transactions"."account_id" = "billing_customers"."id") INNER JOIN "organizations" ON ("billing_customers"."org_id" = "organizations"."id") LEFT JOIN "users" ON (CASE WHEN "billing_transactions"."user_id" IS NOT NULL AND "billing_transactions"."user_id" != '' THEN CAST("billing_transactions"."user_id" AS uuid) = "users"."id" ELSE false END AND ("users"."deleted_at" IS NULL)) WHERE (("billing_transactions"."deleted_at" IS NULL) AND ("billing_customers"."org_id" = $1) AND ("billing_customers"."deleted_at" IS NULL) AND ("organizations"."deleted_at" IS NULL) AND ("billing_transactions"."type" = $2) AND ((CAST("billing_transactions"."type" AS TEXT) ILIKE $3) OR (CAST("billing_transactions"."description" AS TEXT) ILIKE $4) OR (CAST("users"."title" AS TEXT) ILIKE $5) OR (CAST("billing_transactions"."amount" AS TEXT) ILIKE $6))) LIMIT $7 OFFSET $8`, wantParams: []any{"org123", "credit", "%test%", "%test%", "%test%", "%test%", int64(10), int64(40)}, wantErr: false, }, @@ -84,7 +84,7 @@ func TestOrgTokensRepository_prepareDataQuery(t *testing.T) { Limit: 10, Offset: 50, }, - wantSQL: `SELECT "billing_transactions"."amount" AS "token_amount", "billing_transactions"."type" AS "token_type", "billing_transactions"."source" AS "token_source", "billing_transactions"."description" AS "token_description", "billing_transactions"."user_id" AS "token_user_id", "users"."title" AS "user_title", "users"."avatar" AS "user_avatar", "billing_transactions"."created_at" AS "token_created_at", "billing_customers"."org_id" AS "org_id" FROM "billing_transactions" INNER JOIN "billing_customers" ON ("billing_transactions"."account_id" = "billing_customers"."id") LEFT JOIN "users" ON CASE WHEN "billing_transactions"."user_id" IS NOT NULL AND "billing_transactions"."user_id" != '' THEN CAST("billing_transactions"."user_id" AS uuid) = "users"."id" ELSE false END WHERE (("billing_customers"."org_id" = $1) AND ("billing_transactions"."created_at" >= $2)) ORDER BY "billing_transactions"."created_at" DESC LIMIT $3 OFFSET $4`, + wantSQL: `SELECT "billing_transactions"."amount" AS "token_amount", "billing_transactions"."type" AS "token_type", "billing_transactions"."source" AS "token_source", "billing_transactions"."description" AS "token_description", "billing_transactions"."user_id" AS "token_user_id", "users"."title" AS "user_title", "users"."avatar" AS "user_avatar", "billing_transactions"."created_at" AS "token_created_at", "billing_customers"."org_id" AS "org_id" FROM "billing_transactions" INNER JOIN "billing_customers" ON ("billing_transactions"."account_id" = "billing_customers"."id") INNER JOIN "organizations" ON ("billing_customers"."org_id" = "organizations"."id") LEFT JOIN "users" ON (CASE WHEN "billing_transactions"."user_id" IS NOT NULL AND "billing_transactions"."user_id" != '' THEN CAST("billing_transactions"."user_id" AS uuid) = "users"."id" ELSE false END AND ("users"."deleted_at" IS NULL)) WHERE (("billing_transactions"."deleted_at" IS NULL) AND ("billing_customers"."org_id" = $1) AND ("billing_customers"."deleted_at" IS NULL) AND ("organizations"."deleted_at" IS NULL) AND ("billing_transactions"."created_at" >= $2)) ORDER BY "billing_transactions"."created_at" DESC LIMIT $3 OFFSET $4`, wantParams: []any{"org123", "2024-01-01T00:00:00Z", int64(10), int64(50)}, wantErr: false, }, @@ -105,7 +105,7 @@ func TestOrgTokensRepository_prepareDataQuery(t *testing.T) { Limit: 10, Offset: 25, }, - wantSQL: `SELECT "billing_transactions"."amount" AS "token_amount", "billing_transactions"."type" AS "token_type", "billing_transactions"."source" AS "token_source", "billing_transactions"."description" AS "token_description", "billing_transactions"."user_id" AS "token_user_id", "users"."title" AS "user_title", "users"."avatar" AS "user_avatar", "billing_transactions"."created_at" AS "token_created_at", "billing_customers"."org_id" AS "org_id" FROM "billing_transactions" INNER JOIN "billing_customers" ON ("billing_transactions"."account_id" = "billing_customers"."id") LEFT JOIN "users" ON CASE WHEN "billing_transactions"."user_id" IS NOT NULL AND "billing_transactions"."user_id" != '' THEN CAST("billing_transactions"."user_id" AS uuid) = "users"."id" ELSE false END WHERE ("billing_customers"."org_id" = $1) ORDER BY "billing_transactions"."type" DESC, "users"."title" ASC LIMIT $2 OFFSET $3`, + wantSQL: `SELECT "billing_transactions"."amount" AS "token_amount", "billing_transactions"."type" AS "token_type", "billing_transactions"."source" AS "token_source", "billing_transactions"."description" AS "token_description", "billing_transactions"."user_id" AS "token_user_id", "users"."title" AS "user_title", "users"."avatar" AS "user_avatar", "billing_transactions"."created_at" AS "token_created_at", "billing_customers"."org_id" AS "org_id" FROM "billing_transactions" INNER JOIN "billing_customers" ON ("billing_transactions"."account_id" = "billing_customers"."id") INNER JOIN "organizations" ON ("billing_customers"."org_id" = "organizations"."id") LEFT JOIN "users" ON (CASE WHEN "billing_transactions"."user_id" IS NOT NULL AND "billing_transactions"."user_id" != '' THEN CAST("billing_transactions"."user_id" AS uuid) = "users"."id" ELSE false END AND ("users"."deleted_at" IS NULL)) WHERE (("billing_transactions"."deleted_at" IS NULL) AND ("billing_customers"."org_id" = $1) AND ("billing_customers"."deleted_at" IS NULL) AND ("organizations"."deleted_at" IS NULL)) ORDER BY "billing_transactions"."type" DESC, "users"."title" ASC LIMIT $2 OFFSET $3`, wantParams: []any{"org123", int64(10), int64(25)}, wantErr: false, }, @@ -122,7 +122,7 @@ func TestOrgTokensRepository_prepareDataQuery(t *testing.T) { Limit: 10, Offset: 45, }, - wantSQL: `SELECT "billing_transactions"."amount" AS "token_amount", "billing_transactions"."type" AS "token_type", "billing_transactions"."source" AS "token_source", "billing_transactions"."description" AS "token_description", "billing_transactions"."user_id" AS "token_user_id", "users"."title" AS "user_title", "users"."avatar" AS "user_avatar", "billing_transactions"."created_at" AS "token_created_at", "billing_customers"."org_id" AS "org_id" FROM "billing_transactions" INNER JOIN "billing_customers" ON ("billing_transactions"."account_id" = "billing_customers"."id") LEFT JOIN "users" ON CASE WHEN "billing_transactions"."user_id" IS NOT NULL AND "billing_transactions"."user_id" != '' THEN CAST("billing_transactions"."user_id" AS uuid) = "users"."id" ELSE false END WHERE (("billing_customers"."org_id" = $1) AND (("billing_transactions"."description" IS NULL) OR ("billing_transactions"."description" = $2))) LIMIT $3 OFFSET $4`, + wantSQL: `SELECT "billing_transactions"."amount" AS "token_amount", "billing_transactions"."type" AS "token_type", "billing_transactions"."source" AS "token_source", "billing_transactions"."description" AS "token_description", "billing_transactions"."user_id" AS "token_user_id", "users"."title" AS "user_title", "users"."avatar" AS "user_avatar", "billing_transactions"."created_at" AS "token_created_at", "billing_customers"."org_id" AS "org_id" FROM "billing_transactions" INNER JOIN "billing_customers" ON ("billing_transactions"."account_id" = "billing_customers"."id") INNER JOIN "organizations" ON ("billing_customers"."org_id" = "organizations"."id") LEFT JOIN "users" ON (CASE WHEN "billing_transactions"."user_id" IS NOT NULL AND "billing_transactions"."user_id" != '' THEN CAST("billing_transactions"."user_id" AS uuid) = "users"."id" ELSE false END AND ("users"."deleted_at" IS NULL)) WHERE (("billing_transactions"."deleted_at" IS NULL) AND ("billing_customers"."org_id" = $1) AND ("billing_customers"."deleted_at" IS NULL) AND ("organizations"."deleted_at" IS NULL) AND (("billing_transactions"."description" IS NULL) OR ("billing_transactions"."description" = $2))) LIMIT $3 OFFSET $4`, wantParams: []any{"org123", "", int64(10), int64(45)}, wantErr: false, }, From 7bd0f5d087db19332be2041a7c0e302c9e1b5d91 Mon Sep 17 00:00:00 2001 From: Rohil Surana Date: Fri, 25 Sep 2026 15:27:37 +0530 Subject: [PATCH 2/3] test(store): cover org tokens search against a real postgres --- .../postgres/org_tokens_repository_pg_test.go | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 internal/store/postgres/org_tokens_repository_pg_test.go diff --git a/internal/store/postgres/org_tokens_repository_pg_test.go b/internal/store/postgres/org_tokens_repository_pg_test.go new file mode 100644 index 0000000000..10cc5cc0f1 --- /dev/null +++ b/internal/store/postgres/org_tokens_repository_pg_test.go @@ -0,0 +1,184 @@ +package postgres_test + +import ( + "context" + "fmt" + "io" + "log/slog" + "testing" + + "github.com/ory/dockertest" + "github.com/raystack/frontier/core/aggregates/orgtokens" + "github.com/raystack/frontier/internal/store/postgres" + "github.com/raystack/frontier/pkg/db" + "github.com/raystack/salt/rql" + "github.com/stretchr/testify/suite" +) + +type OrgTokensRepositoryPGTestSuite struct { + suite.Suite + ctx context.Context + client *db.Client + pool *dockertest.Pool + resource *dockertest.Resource + repository *postgres.OrgTokensRepository + orgID string +} + +func (s *OrgTokensRepositoryPGTestSuite) SetupSuite() { + var err error + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + s.client, s.pool, s.resource, err = newTestClient(logger) + if err != nil { + s.T().Fatal(err) + } + s.ctx = context.TODO() + s.repository = postgres.NewOrgTokensRepository(s.client) +} + +func (s *OrgTokensRepositoryPGTestSuite) TearDownSuite() { + if err := purgeDocker(s.pool, s.resource); err != nil { + s.T().Fatal(err) + } +} + +func (s *OrgTokensRepositoryPGTestSuite) SetupTest() { + s.exec(`INSERT INTO organizations (name, title) VALUES + ('ot-org', 'Org Tokens Org'), + ('ot-org-gone', 'Gone Org')`) + s.orgID = s.orgIDOf("ot-org") + + s.exec(`INSERT INTO billing_customers (org_id, provider_id, name, email) VALUES + ($1, 'ot-cust-live', 'Live Account', 'live@example.com'), + ($1, 'ot-cust-gone', 'Gone Account', 'gone@example.com'), + ($2, 'ot-cust-of-gone-org', 'Gone Org Account', 'goneorg@example.com')`, + s.orgID, s.orgIDOf("ot-org-gone")) + + s.exec(`INSERT INTO users (name, email, title) VALUES + ('ot-uma', 'ot-uma@example.com', 'Uma'), + ('ot-gary', 'ot-gary@example.com', 'Gary')`) + + s.exec(`INSERT INTO billing_transactions (account_id, type, source, amount, description, user_id) VALUES + ($1, 'credit', 'system', 100, 'Live top up', $2), + ($1, 'credit', 'system', 50, 'Top up by gone user', $3), + ($1, 'credit', 'system', 40, 'No user top up', NULL), + ($1, 'credit', 'system', 30, 'Empty user top up', ''), + ($1, 'credit', 'system', 25, 'Deleted top up', $2), + ($4, 'credit', 'system', 10, 'Gone account top up', $2), + ($5, 'credit', 'system', 5, 'Gone org top up', $2)`, + s.customerID("ot-cust-live"), s.userID("ot-uma"), s.userID("ot-gary"), + s.customerID("ot-cust-gone"), s.customerID("ot-cust-of-gone-org")) + + s.exec(`UPDATE billing_transactions SET deleted_at = now() WHERE description = 'Deleted top up'`) + s.exec(`UPDATE billing_customers SET deleted_at = now() WHERE provider_id = 'ot-cust-gone'`) + s.exec(`UPDATE organizations SET deleted_at = now() WHERE name = 'ot-org-gone'`) + s.exec(`UPDATE users SET deleted_at = now() WHERE name = 'ot-gary'`) +} + +func (s *OrgTokensRepositoryPGTestSuite) TearDownTest() { + queries := []string{} + for _, table := range []string{postgres.TABLE_BILLING_TRANSACTIONS, postgres.TABLE_BILLING_CUSTOMERS, + postgres.TABLE_USERS, postgres.TABLE_ORGANIZATIONS} { + queries = append(queries, fmt.Sprintf("TRUNCATE TABLE %s RESTART IDENTITY CASCADE", table)) + } + if err := execQueries(s.ctx, s.client, queries); err != nil { + s.T().Fatal(err) + } +} + +func (s *OrgTokensRepositoryPGTestSuite) exec(query string, args ...any) { + s.T().Helper() + execSQL(s.T(), s.ctx, s.client, query, args...) +} + +func (s *OrgTokensRepositoryPGTestSuite) scalar(query string, args ...any) string { + s.T().Helper() + return scalarSQL(s.T(), s.ctx, s.client, query, args...) +} + +func (s *OrgTokensRepositoryPGTestSuite) orgIDOf(name string) string { + s.T().Helper() + return s.scalar(`SELECT id FROM organizations WHERE name = $1`, name) +} + +func (s *OrgTokensRepositoryPGTestSuite) customerID(providerID string) string { + s.T().Helper() + return s.scalar(`SELECT id FROM billing_customers WHERE provider_id = $1`, providerID) +} + +func (s *OrgTokensRepositoryPGTestSuite) userID(name string) string { + s.T().Helper() + return s.scalar(`SELECT id FROM users WHERE name = $1`, name) +} + +func (s *OrgTokensRepositoryPGTestSuite) search(text string) []orgtokens.AggregatedToken { + s.T().Helper() + return s.searchOrg(s.orgID, text) +} + +func (s *OrgTokensRepositoryPGTestSuite) searchOrg(orgID, text string) []orgtokens.AggregatedToken { + s.T().Helper() + res, err := s.repository.Search(s.ctx, orgID, &rql.Query{ + Limit: 50, + Search: text, + Sort: []rql.Sort{{Name: "description", Order: "asc"}}, + }) + s.Require().NoError(err) + return res.Tokens +} + +func (s *OrgTokensRepositoryPGTestSuite) tokenWithDescription(description string) orgtokens.AggregatedToken { + s.T().Helper() + for _, token := range s.search("") { + if token.Description == description { + return token + } + } + s.T().Fatalf("no token with description %q", description) + return orgtokens.AggregatedToken{} +} + +func (s *OrgTokensRepositoryPGTestSuite) descriptions(tokens []orgtokens.AggregatedToken) []string { + s.T().Helper() + out := make([]string, 0, len(tokens)) + for _, t := range tokens { + out = append(out, t.Description) + } + return out +} + +func (s *OrgTokensRepositoryPGTestSuite) TestSkipsSoftDeletedTokensAndAccounts() { + s.Equal([]string{"Empty user top up", "Live top up", "No user top up", "Top up by gone user"}, + s.descriptions(s.search(""))) +} + +func (s *OrgTokensRepositoryPGTestSuite) TestSoftDeletedOrgHasNoTokens() { + s.Empty(s.descriptions(s.searchOrg(s.orgIDOf("ot-org-gone"), ""))) +} + +func (s *OrgTokensRepositoryPGTestSuite) TestTokensWithoutAUserAreStillListed() { + s.Empty(s.tokenWithDescription("No user top up").UserID) + s.Empty(s.tokenWithDescription("Empty user top up").UserID) +} + +func (s *OrgTokensRepositoryPGTestSuite) TestSoftDeletedUserKeepsTheTokenAndHidesTheName() { + live := s.tokenWithDescription("Live top up") + s.Equal(s.userID("ot-uma"), live.UserID) + s.Equal("Uma", live.UserTitle) + + gone := s.tokenWithDescription("Top up by gone user") + s.Equal(s.userID("ot-gary"), gone.UserID) + s.Empty(gone.UserTitle, "the user is soft-deleted") +} + +func (s *OrgTokensRepositoryPGTestSuite) TestSearchMatchesLiveRowsOnly() { + s.Equal([]string{"Live top up"}, s.descriptions(s.search("Live top"))) + s.Empty(s.descriptions(s.search("Deleted top up")), "the transaction is soft-deleted") + s.Empty(s.descriptions(s.search("Gone account")), "the billing account is soft-deleted") + s.Empty(s.descriptions(s.search("Gary")), "the user is soft-deleted") + s.Empty(s.descriptions(s.search("Gone org")), "the organization is soft-deleted") +} + +func TestOrgTokensRepositoryPG(t *testing.T) { + suite.Run(t, new(OrgTokensRepositoryPGTestSuite)) +} From e44bcd6678c2f500f15cf62a073b461938c76832 Mon Sep 17 00:00:00 2001 From: Rohil Surana Date: Mon, 28 Sep 2026 13:29:08 +0530 Subject: [PATCH 3/3] fix(store): drop a stale comment on the org tokens user join --- internal/store/postgres/org_tokens_repository.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/store/postgres/org_tokens_repository.go b/internal/store/postgres/org_tokens_repository.go index caac514111..104c6ea693 100644 --- a/internal/store/postgres/org_tokens_repository.go +++ b/internal/store/postgres/org_tokens_repository.go @@ -117,9 +117,8 @@ func (r OrgTokensRepository) prepareDataQuery(orgID string, rql *rql.Query) (str return query.Offset(uint(rql.Offset)).Limit(uint(rql.Limit)).ToSQL() } -// we need to cast the user_id to text since it's stored as text in billing_transactions but the users.id is uuid. func (r OrgTokensRepository) buildBaseQuery(orgID string) *goqu.SelectDataset { - liveUserOfTransaction := goqu.On( + liveUserOfTransactionUserID := goqu.On( goqu.L("CASE WHEN \"billing_transactions\".\"user_id\" IS NOT NULL AND \"billing_transactions\".\"user_id\" != '' THEN CAST(\"billing_transactions\".\"user_id\" AS uuid) = \"users\".\"id\" ELSE false END"), live(TABLE_USERS), ) @@ -144,7 +143,7 @@ func (r OrgTokensRepository) buildBaseQuery(orgID string) *goqu.SelectDataset { goqu.T(TABLE_ORGANIZATIONS), goqu.On(goqu.I(TABLE_BILLING_CUSTOMERS+"."+COLUMN_ORG_ID).Eq(goqu.I(TABLE_ORGANIZATIONS+"."+COLUMN_ID))), ). - LeftJoin(goqu.T(TABLE_USERS), liveUserOfTransaction). + LeftJoin(goqu.T(TABLE_USERS), liveUserOfTransactionUserID). Where( goqu.Ex{ TABLE_BILLING_CUSTOMERS + "." + COLUMN_ORG_ID: orgID,