From 8d16a41f906c3ca26a1287b55152fe27b15f7f9a Mon Sep 17 00:00:00 2001 From: alltheseas Date: Thu, 19 Feb 2026 20:44:05 -0600 Subject: [PATCH] Add active_membership_duration to account attributes Return active_membership_duration (seconds) in the attributes payload so the client can compute badge tiers locally. Keeps member_for_more_than_one_year for backward compat with shipped clients. Signed-off-by: elsat Co-Authored-By: Claude Opus 4.6 --- src/user_management.js | 4 +- test/router_config.test.js | 117 +++++++++++++++++++++++++++++++++---- 2 files changed, 108 insertions(+), 13 deletions(-) diff --git a/src/user_management.js b/src/user_management.js index bea0ddd..c5247e8 100644 --- a/src/user_management.js +++ b/src/user_management.js @@ -180,7 +180,8 @@ function get_account_info_payload(subscriber_number, account, authenticated = fa // We consider one year to be 360 days, to be a bit lenient with users who might have a few days of downtime in their subscription, and make sure everyone who roughly got a year of service gets the benefit during the announcement. const one_year_in_seconds = 360 * 24 * 60 * 60 // Performance optimization: We only calculate the total membership time if the account is active - const member_for_more_than_one_year = account_active ? total_active_membership_time(account) > one_year_in_seconds : false + const total_membership_time = account_active ? total_active_membership_time(account) : 0 + const member_for_more_than_one_year = total_membership_time > one_year_in_seconds return { @@ -192,6 +193,7 @@ function get_account_info_payload(subscriber_number, account, authenticated = fa testflight_url: (authenticated && account_active) ? process.env.TESTFLIGHT_URL : null, attributes: { member_for_more_than_one_year: member_for_more_than_one_year, + active_membership_duration: total_membership_time, } } } diff --git a/test/router_config.test.js b/test/router_config.test.js index ea262c6..2b2302c 100644 --- a/test/router_config.test.js +++ b/test/router_config.test.js @@ -3,6 +3,7 @@ const express = require('express'); const config_router = require('../src/router_config.js').config_router; const nostr = require('nostr'); const current_time = require('../src/utils.js').current_time; +const { get_account_info_payload } = require('../src/user_management.js'); const { supertest_client } = require('./controllers/utils.js'); const { v4: uuidv4 } = require('uuid') @@ -84,20 +85,112 @@ test('config_router - Account management routes', async (t) => { .get('/accounts/abc123') .expect(200); - const expectedData = { - pubkey: account_info.pubkey, - created_at: account_info.created_at, - subscriber_number: 1, - expiry: account_info.expiry, - active: true, - testflight_url: null, - attributes: { - member_for_more_than_one_year: false - } - }; - t.same(res.body, expectedData, 'Response should match expected value'); + t.equal(res.body.pubkey, account_info.pubkey) + t.equal(res.body.created_at, account_info.created_at) + t.equal(res.body.subscriber_number, 1) + t.equal(res.body.expiry, account_info.expiry) + t.equal(res.body.active, true) + t.equal(res.body.testflight_url, null) + t.equal(res.body.attributes.member_for_more_than_one_year, false) + // Legacy account with 30-day past + 30-day future expiry yields ~60 days of membership + const sixty_days = 60 * 24 * 60 * 60 + t.ok(res.body.attributes.active_membership_duration > sixty_days - 10, 'duration should be approximately 60 days') + t.ok(res.body.attributes.active_membership_duration < sixty_days + 10, 'duration should be approximately 60 days') t.end(); }); t.end(); }); + +test('get_account_info_payload - membership tenure attributes', async (t) => { + const one_year_in_seconds = 360 * 24 * 60 * 60 + const thirty_days_in_seconds = 60 * 60 * 24 * 30 + + t.test('new account returns duration and member_for_more_than_one_year false', async (t) => { + const account = { + pubkey: 'abc123', + created_at: current_time() - thirty_days_in_seconds, + expiry: current_time() + thirty_days_in_seconds, + transactions: [{ + type: 'iap', + id: '1', + start_date: current_time() - thirty_days_in_seconds, + end_date: current_time() + thirty_days_in_seconds, + purchased_date: current_time() - thirty_days_in_seconds, + duration: null + }] + } + const payload = get_account_info_payload(1, account) + t.equal(payload.attributes.member_for_more_than_one_year, false) + t.ok(payload.attributes.active_membership_duration > 0, 'duration should be positive for active account') + t.ok(payload.attributes.active_membership_duration < one_year_in_seconds, 'duration should be less than one year') + t.end() + }) + + t.test('account with > 3 years returns correct duration', async (t) => { + const total_duration = 3 * one_year_in_seconds + 1 + const account = { + pubkey: 'abc123', + created_at: current_time() - total_duration, + expiry: current_time() + thirty_days_in_seconds, + transactions: [{ + type: 'iap', + id: '1', + start_date: current_time() - total_duration, + end_date: current_time(), + purchased_date: current_time() - total_duration, + duration: null + }] + } + const payload = get_account_info_payload(1, account) + t.equal(payload.attributes.member_for_more_than_one_year, true) + t.ok(payload.attributes.active_membership_duration > 3 * one_year_in_seconds, 'duration should exceed three years') + t.end() + }) + + t.test('account with > 1 year but < 3 years returns correct duration', async (t) => { + const total_duration = one_year_in_seconds + 1 + const account = { + pubkey: 'abc123', + created_at: current_time() - total_duration, + expiry: current_time() + thirty_days_in_seconds, + transactions: [{ + type: 'iap', + id: '1', + start_date: current_time() - total_duration, + end_date: current_time(), + purchased_date: current_time() - total_duration, + duration: null + }] + } + const payload = get_account_info_payload(1, account) + t.equal(payload.attributes.member_for_more_than_one_year, true) + t.ok(payload.attributes.active_membership_duration > one_year_in_seconds) + t.ok(payload.attributes.active_membership_duration < 3 * one_year_in_seconds) + t.end() + }) + + t.test('inactive account returns zero duration', async (t) => { + const total_duration = 3 * one_year_in_seconds + 1 + const account = { + pubkey: 'abc123', + created_at: current_time() - total_duration, + expiry: current_time() - 1, // expired + transactions: [{ + type: 'iap', + id: '1', + start_date: current_time() - total_duration, + end_date: current_time() - 1, + purchased_date: current_time() - total_duration, + duration: null + }] + } + const payload = get_account_info_payload(1, account) + t.equal(payload.active, false) + t.equal(payload.attributes.member_for_more_than_one_year, false) + t.equal(payload.attributes.active_membership_duration, 0) + t.end() + }) + + t.end() +});