From 836d78fb9ad3113ca3f631b7e6964d590ea930d0 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Tue, 7 Oct 2025 23:21:06 -0400 Subject: [PATCH] fix: GET /users/:id returns monthly_supporter_badge only if user prefers to send it (#479) --- lib/controllers/v2/users_controller.js | 2 +- lib/models/user.js | 28 ++++++-- openapi/schema/response/private_user.js | 1 + openapi/schema/response/user.js | 2 +- schema/fixtures.js | 93 +++++++++++++++++++++++++ test/integration/v2/users.js | 66 ++++++++++++++++++ 6 files changed, 183 insertions(+), 9 deletions(-) diff --git a/lib/controllers/v2/users_controller.js b/lib/controllers/v2/users_controller.js index ad73417b..d8153ad5 100644 --- a/lib/controllers/v2/users_controller.js +++ b/lib/controllers/v2/users_controller.js @@ -30,7 +30,7 @@ const show = async req => { "journal_posts_count", "last_active", "login", - "monthly_supporter", + "monthly_supporter_badge", "name", "observations_count", "orcid", diff --git a/lib/models/user.js b/lib/models/user.js index fc8a74a5..20895564 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -94,13 +94,27 @@ const User = class User extends Model { const query = squel.select( ) .from( User.tableName ) .where( "id = ?", id ); - if ( dbFields.indexOf( "monthly_supporter" ) >= 0 ) { - _.pull( dbFields, "monthly_supporter" ); - query.field( - "( donorbox_plan_type = 'monthly' AND donorbox_plan_status = 'active' ) OR " - + "( fundraiseup_plan_frequency = 'monthly' AND fundraiseup_plan_status = 'active' )", - "monthly_supporter" - ); + if ( dbFields.indexOf( "monthly_supporter_badge" ) >= 0 ) { + _.pull( dbFields, "monthly_supporter_badge" ); + const prefsQuery = squel.select( ) + .field( "value" ) + .from( "preferences" ) + .where( "owner_id = ? AND owner_type = 'User' AND name = 'prefers_monthly_supporter_badge'", id ); + const { rows: results } = await pgClient.replica.query( prefsQuery.toString( ) ); + const userWantsToShowBadge = _.isEmpty( results ) || _.isEmpty( results[0] ) + ? _.find( PREFS, { name: "monthly_supporter_badge" } ).default + : results[0].value === "t"; + if ( userWantsToShowBadge ) { + // CASE clause ensures true/false return vals, else the expression can evaluate to null. + query.field( + "CASE WHEN (( donorbox_plan_type = 'monthly' AND donorbox_plan_status = 'active' ) OR " + + "( fundraiseup_plan_frequency = 'monthly' AND fundraiseup_plan_status = 'active' )) " + + "THEN true ELSE false END", + "monthly_supporter_badge" + ); + } else { + query.field( "false", "monthly_supporter_badge" ); + } } if ( fields.indexOf( "site" ) ) { _.pull( dbFields, "site" ); diff --git a/openapi/schema/response/private_user.js b/openapi/schema/response/private_user.js index 6b5494b4..ee525dc3 100644 --- a/openapi/schema/response/private_user.js +++ b/openapi/schema/response/private_user.js @@ -13,6 +13,7 @@ module.exports = user.append( { email: Joi.string( ).valid( null ), locale: Joi.string( ).valid( null ), login: Joi.string( ), + monthly_supporter: Joi.boolean( ).valid( null ), muted_user_ids: Joi.array( ).items( Joi.number( ).integer( ) ).valid( null ), diff --git a/openapi/schema/response/user.js b/openapi/schema/response/user.js index e4611489..d2c64fde 100644 --- a/openapi/schema/response/user.js +++ b/openapi/schema/response/user.js @@ -18,7 +18,7 @@ module.exports = Joi.object( ).keys( { annotated_observations_count: Joi.number( ).integer( ), last_active: Joi.date( ).iso( ), login: Joi.string( ), - monthly_supporter: Joi.boolean( ).valid( null ), + monthly_supporter_badge: Joi.boolean( ).valid( null ), name: Joi.string( ).valid( null ), observations_count: Joi.number( ).integer( ), orcid: Joi.string( ).valid( null ), diff --git a/schema/fixtures.js b/schema/fixtures.js index d75370e9..88c20bfb 100644 --- a/schema/fixtures.js +++ b/schema/fixtures.js @@ -2025,6 +2025,38 @@ "email": "user2023092503@gmail.com", "last_ip": "192.168.0.3", "suspended": false + }, + { + "id": 2025100708, + "login": "user2025100708", + "name": "User2025100708 is an active donorbox monthly donor but shy about it", + "email": "user2025100708@gmail.com", + "last_ip": "192.168.0.3", + "suspended": false + }, + { + "id": 2025100709, + "login": "user2025100709", + "name": "User2025100709 is an inactive donorbox monthly donor and boastful about it", + "email": "user2025100709@gmail.com", + "last_ip": "192.168.0.3", + "suspended": false + }, + { + "id": 2025100710, + "login": "user2025100710", + "name": "User2025100710 is an active fundraiseup monthly donor and boastful about it", + "email": "user2025100710@gmail.com", + "last_ip": "192.168.0.3", + "suspended": false + }, + { + "id": 2025100711, + "login": "user2025100711", + "name": "User2025100711 is an active fundraiseup nonmonthly donor and boastful about it", + "email": "user2025100711@gmail.com", + "last_ip": "192.168.0.3", + "suspended": false } ] }, @@ -3068,6 +3100,27 @@ "owner_type": "Project", "owner_id": 2005, "value": "t" + }, + { + "id": 2025100700, + "name": "prefers_monthly_supporter_badge", + "owner_id": 2025100709, + "owner_type": "User", + "value": "t" + }, + { + "id": 2025100701, + "name": "prefers_monthly_supporter_badge", + "owner_id": 2025100710, + "owner_type": "User", + "value": "t" + }, + { + "id": 2025100702, + "name": "prefers_monthly_supporter_badge", + "owner_id": 2025100711, + "owner_type": "User", + "value": "t" } ], "project_observations": [ @@ -3739,6 +3792,46 @@ "login": "user2024071702", "name": "User2024071702 with 2023 donation", "created_at": "2020-01-01 00:00:00" + }, + { + "id": 2025100708, + "login": "user2025100708", + "name": "User2025100708 is an active donorbox monthly donor but shy about it", + "created_at": "2025-10-07 00:00:00", + "updated_at": "2025-10-07 00:00:00", + "last_active": "2025-10-07", + "donorbox_plan_type": "monthly", + "donorbox_plan_status": "active" + }, + { + "id": 2025100709, + "login": "user2025100709", + "name": "User2025100709 is an inactive donorbox monthly donor and boastful about it", + "created_at": "2025-10-07 00:00:00", + "updated_at": "2025-10-07 00:00:00", + "last_active": "2025-10-07", + "donorbox_plan_type": "monthly", + "donorbox_plan_status": "inactive" + }, + { + "id": 2025100710, + "login": "user2025100710", + "name": "User2025100710 is an active fundraiseup monthly donor and boastful about it", + "created_at": "2025-10-07 00:00:00", + "updated_at": "2025-10-07 00:00:00", + "last_active": "2025-10-07", + "fundraiseup_plan_frequency": "monthly", + "fundraiseup_plan_status": "active" + }, + { + "id": 2025100711, + "login": "user2025100711", + "name": "User2025100711 is an active fundraiseup nonmonthly donor and boastful about it", + "created_at": "2025-10-07 00:00:00", + "updated_at": "2025-10-07 00:00:00", + "last_active": "2025-10-07", + "fundraiseup_plan_frequency": "yearly", + "fundraiseup_plan_status": "active" } ], "user_blocks": [ diff --git a/test/integration/v2/users.js b/test/integration/v2/users.js index fde18189..36973145 100644 --- a/test/integration/v2/users.js +++ b/test/integration/v2/users.js @@ -67,6 +67,72 @@ describe( "Users", ( ) => { } ).expect( "Content-Type", /json/ ) .expect( 200, done ); } ); + + describe( "monthly_supporter_badge property", ( ) => { + it( "is false if user does not prefer to show it", function ( done ) { + request( this.app ).get( "/v2/users/2025100708?fields=all" ) + .expect( res => { + const user = res.body.results[0]; + expect( res.body.page ).to.eq( 1 ); + expect( res.body.per_page ).to.eq( 1 ); + expect( res.body.total_results ).to.eq( 1 ); + expect( res.body.results.length ).to.eq( 1 ); + expect( user.id ).to.eq( 2025100708 ); + expect( user ).to.have.property( "monthly_supporter_badge" ); + expect( user.monthly_supporter_badge ).to.be.false; + } ) + .expect( "Content-Type", /json/ ) + .expect( 200, done ); + } ); + + it( "is false if user is an inactive donor", function ( done ) { + request( this.app ).get( "/v2/users/2025100709?fields=all" ) + .expect( res => { + const user = res.body.results[0]; + expect( res.body.page ).to.eq( 1 ); + expect( res.body.per_page ).to.eq( 1 ); + expect( res.body.total_results ).to.eq( 1 ); + expect( res.body.results.length ).to.eq( 1 ); + expect( user.id ).to.eq( 2025100709 ); + expect( user ).to.have.property( "monthly_supporter_badge" ); + expect( user.monthly_supporter_badge ).to.be.false; + } ) + .expect( "Content-Type", /json/ ) + .expect( 200, done ); + } ); + + it( "is true if user is an active monthly donor and prefers to show it", function ( done ) { + request( this.app ).get( "/v2/users/2025100710?fields=all" ) + .expect( res => { + const user = res.body.results[0]; + expect( res.body.page ).to.eq( 1 ); + expect( res.body.per_page ).to.eq( 1 ); + expect( res.body.total_results ).to.eq( 1 ); + expect( res.body.results.length ).to.eq( 1 ); + expect( user.id ).to.eq( 2025100710 ); + expect( user ).to.have.property( "monthly_supporter_badge" ); + expect( user.monthly_supporter_badge ).to.be.true; + } ) + .expect( "Content-Type", /json/ ) + .expect( 200, done ); + } ); + + it( "is false if user is an active yearly donor, not monthly", function ( done ) { + request( this.app ).get( "/v2/users/2025100711?fields=all" ) + .expect( res => { + const user = res.body.results[0]; + expect( res.body.page ).to.eq( 1 ); + expect( res.body.per_page ).to.eq( 1 ); + expect( res.body.total_results ).to.eq( 1 ); + expect( res.body.results.length ).to.eq( 1 ); + expect( user.id ).to.eq( 2025100711 ); + expect( user ).to.have.property( "monthly_supporter_badge" ); + expect( user.monthly_supporter_badge ).to.be.false; + } ) + .expect( "Content-Type", /json/ ) + .expect( 200, done ); + } ); + } ); } ); describe( "autocomplete", ( ) => {