0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-24 23:48:13 -05:00

Fixed member subscription status when multiple subs (#19530)

fixes PROD-325
- if a member has multiple subscriptions, show the status of the active
subscription
- if a member has multiple active subscriptins, show the status of the
subscription with the latest current_period_end date
This commit is contained in:
Sag 2024-01-24 12:16:26 +01:00 committed by GitHub
parent e7a72e2ab1
commit e4f60ee028
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 147 additions and 32 deletions

View file

@ -147,17 +147,21 @@
{{/if}} {{/if}}
{{/if}} {{/if}}
{{#if sub.isComplimentary}}
{{#if sub.compExpiry}}
<span class="gh-cp-membertier-renewal">Expires {{sub.compExpiry}}</span>
{{/if}}
{{else}}
{{#if sub.hasEnded}} {{#if sub.hasEnded}}
<span class="gh-cp-membertier-renewal">Ended {{sub.validUntil}}</span> <span class="gh-cp-membertier-renewal">Ended {{sub.validUntil}}</span>
{{else if sub.willEndSoon}} {{else if sub.willEndSoon}}
<span class="gh-cp-membertier-renewal">Has access until {{sub.validUntil}}</span> <span class="gh-cp-membertier-renewal">Has access until {{sub.validUntil}}</span>
{{else if sub.compExpiry}}
<span class="gh-cp-membertier-renewal">Expires {{sub.compExpiry}}</span>
{{else if sub.trialUntil}} {{else if sub.trialUntil}}
<span class="gh-cp-membertier-renewal">Ends {{sub.trialUntil}}</span> <span class="gh-cp-membertier-renewal">Ends {{sub.trialUntil}}</span>
{{else}} {{else}}
<span class="gh-cp-membertier-renewal">Renews {{sub.validUntil}}</span> <span class="gh-cp-membertier-renewal">Renews {{sub.validUntil}}</span>
{{/if}} {{/if}}
{{/if}}
</div> </div>
<Member::SubscriptionDetailBox @sub={{sub}} @index={{index}} /> <Member::SubscriptionDetailBox @sub={{sub}} @index={{index}} />
</div> </div>

View file

@ -1,6 +1,6 @@
import {DATE_RELATION_OPTIONS} from './relation-options'; import {DATE_RELATION_OPTIONS} from './relation-options';
import {getDateColumnValue} from './columns/date-column'; import {getDateColumnValue} from './columns/date-column';
import {mostRecentlyUpdated} from 'ghost-admin/helpers/most-recently-updated'; import {mostRelevantSubscription} from 'ghost-admin/helpers/most-relevant-subscription';
export const NEXT_BILLING_DATE_FILTER = { export const NEXT_BILLING_DATE_FILTER = {
label: 'Next billing date', label: 'Next billing date',
@ -9,7 +9,7 @@ export const NEXT_BILLING_DATE_FILTER = {
columnLabel: 'Next billing date', columnLabel: 'Next billing date',
relationOptions: DATE_RELATION_OPTIONS, relationOptions: DATE_RELATION_OPTIONS,
getColumnValue: (member, filter) => { getColumnValue: (member, filter) => {
const subscription = mostRecentlyUpdated(member.subscriptions); const subscription = mostRelevantSubscription(member.subscriptions);
return getDateColumnValue(subscription?.current_period_end, filter); return getDateColumnValue(subscription?.current_period_end, filter);
} }
}; };

View file

@ -1,6 +1,6 @@
import {MATCH_RELATION_OPTIONS} from './relation-options'; import {MATCH_RELATION_OPTIONS} from './relation-options';
import {capitalizeFirstLetter} from 'ghost-admin/helpers/capitalize-first-letter'; import {capitalizeFirstLetter} from 'ghost-admin/helpers/capitalize-first-letter';
import {mostRecentlyUpdated} from 'ghost-admin/helpers/most-recently-updated'; import {mostRelevantSubscription} from 'ghost-admin/helpers/most-relevant-subscription';
export const PLAN_INTERVAL_FILTER = { export const PLAN_INTERVAL_FILTER = {
label: 'Billing period', label: 'Billing period',
@ -13,7 +13,7 @@ export const PLAN_INTERVAL_FILTER = {
{label: 'Yearly', name: 'year'} {label: 'Yearly', name: 'year'}
], ],
getColumnValue: (member) => { getColumnValue: (member) => {
const subscription = mostRecentlyUpdated(member.subscriptions); const subscription = mostRelevantSubscription(member.subscriptions);
if (!subscription) { if (!subscription) {
return null; return null;
} }

View file

@ -1,6 +1,6 @@
import {DATE_RELATION_OPTIONS} from './relation-options'; import {DATE_RELATION_OPTIONS} from './relation-options';
import {getDateColumnValue} from './columns/date-column'; import {getDateColumnValue} from './columns/date-column';
import {mostRecentlyUpdated} from 'ghost-admin/helpers/most-recently-updated'; import {mostRelevantSubscription} from 'ghost-admin/helpers/most-relevant-subscription';
export const SUBSCRIPTION_START_DATE_FILTER = { export const SUBSCRIPTION_START_DATE_FILTER = {
label: 'Paid start date', label: 'Paid start date',
@ -9,7 +9,7 @@ export const SUBSCRIPTION_START_DATE_FILTER = {
columnLabel: 'Paid start date', columnLabel: 'Paid start date',
relationOptions: DATE_RELATION_OPTIONS, relationOptions: DATE_RELATION_OPTIONS,
getColumnValue: (member, filter) => { getColumnValue: (member, filter) => {
const subscription = mostRecentlyUpdated(member.subscriptions); const subscription = mostRelevantSubscription(member.subscriptions);
return getDateColumnValue(subscription?.start_date, filter); return getDateColumnValue(subscription?.start_date, filter);
} }
}; };

View file

@ -1,6 +1,6 @@
import {MATCH_RELATION_OPTIONS} from './relation-options'; import {MATCH_RELATION_OPTIONS} from './relation-options';
import {capitalizeFirstLetter} from 'ghost-admin/helpers/capitalize-first-letter'; import {capitalizeFirstLetter} from 'ghost-admin/helpers/capitalize-first-letter';
import {mostRecentlyUpdated} from 'ghost-admin/helpers/most-recently-updated'; import {mostRelevantSubscription} from 'ghost-admin/helpers/most-relevant-subscription';
export const SUBSCRIPTION_STATUS_FILTER = { export const SUBSCRIPTION_STATUS_FILTER = {
label: 'Stripe subscription status', label: 'Stripe subscription status',
@ -18,7 +18,7 @@ export const SUBSCRIPTION_STATUS_FILTER = {
{label: 'Incomplete - Expired', name: 'incomplete_expired'} {label: 'Incomplete - Expired', name: 'incomplete_expired'}
], ],
getColumnValue: (member) => { getColumnValue: (member) => {
const subscription = mostRecentlyUpdated(member.subscriptions); const subscription = mostRelevantSubscription(member.subscriptions);
if (!subscription) { if (!subscription) {
return null; return null;
} }

View file

@ -0,0 +1,39 @@
import moment from 'moment-timezone';
import {helper} from '@ember/component/helper';
export function mostRelevantSubscription(subs) {
// Ignore comped subscriptions (without id)
const items = [...(subs || []).filter(sub => !!sub.id)];
// Find active subscription if any, then sort by latest current_period_end if needed
items.sort((a, b) => {
const isActiveA = ['active', 'trialing', 'unpaid', 'past_due'].includes(a.status);
const isActiveB = ['active', 'trialing', 'unpaid', 'past_due'].includes(b.status);
// Sort by status, active first
if (isActiveA && !isActiveB) {
return -1;
} else if (!isActiveA && isActiveB) {
return 1;
}
// Sort by current_period_end, latest first
const endDateA = moment(a.current_period_end);
const endDateB = moment(b.current_period_end);
if (!endDateA.isValid()) {
return 1;
} else if (!endDateB.isValid()) {
return -1;
}
return endDateB.valueOf() - endDateA.valueOf();
});
return items[0] || null;
}
export default helper(function ([items = []]) {
return mostRelevantSubscription(items);
});

View file

@ -0,0 +1,72 @@
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {mostRelevantSubscription} from 'ghost-admin/helpers/most-relevant-subscription';
describe('Unit: Helper: most-relevant-subscription', function () {
it('returns active subscriptions first', function () {
const active = {id: 'a', status: 'active', current_period_end: '2022-03-04 16:10'};
const canceled = {id: 'b', status: 'canceled', current_period_end: '2022-03-04 16:10'};
const subs = [active, canceled];
expect(mostRelevantSubscription(subs)).to.equal(active);
});
it('returns the subscription with the latest current_period_end', function () {
const older = {id: 'a', status: 'active', current_period_end: '2022-03-04 16:10'};
const latest = {id: 'b', status: 'active', current_period_end: '2022-03-04 16:20'};
const subs = [older, latest];
expect(mostRelevantSubscription(subs)).to.equal(latest);
});
it('ignores comped subscriptions', function () {
const normal = {id: 'a', status: 'active', current_period_end: '2022-03-04 16:10'};
const comped = {id: null, status: 'active', current_period_end: '2022-03-04 16:20'};
const subs = [normal, comped];
expect(mostRelevantSubscription(subs)).to.equal(normal);
});
it('handles null or invalid dates', function () {
const a = {id: 'a', status: 'active', current_period_end: '2022-03-04 16:10'};
const b = {id: 'b', status: 'active', current_period_end: '2022-03-04 16:20'};
const c = {id: 'c', status: 'active', current_period_end: ''};
const d = {id: 'd', status: 'active', current_period_end: null};
const e = {id: 'e', status: 'active', current_period_end: 'string'};
const subs = [a, b, c, d, e];
expect(mostRelevantSubscription(subs)).to.equal(b);
});
it('handles a single-element array', function () {
const a = {id: 'a', current_period_end: '2022-02-22'};
expect(mostRelevantSubscription([a])).to.equal(a);
});
it('handles null', function () {
expect(mostRelevantSubscription(null)).to.equal(null);
});
it('handles empty array', function () {
expect(mostRelevantSubscription([])).to.equal(null);
});
it('does not modify original array', function () {
const a = {id: 'a', status: 'active', current_period_end: '2022-03-04 16:10'};
const b = {id: 'b', status: 'canceled', current_period_end: '2022-03-04 16:10'};
const c = {id: null, status: 'active', current_period_end: '2022-03-04 16:10'};
const subs = [a, b, c];
mostRelevantSubscription(subs);
expect(subs[0]).to.equal(a);
expect(subs[1]).to.equal(b);
expect(subs[2]).to.equal(c);
});
});