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

@ -143,20 +143,24 @@
{{else}}
{{#if (or (eq sub.price.nickname "Monthly") (eq sub.price.nickname "Yearly"))}}
{{else}}
<span class="gh-cp-membertier-pricelabel">{{sub.price.nickname}}</span><span class="gh-cp-membertier-renewal"> &ndash;</span>
<span class="gh-cp-membertier-pricelabel">{{sub.price.nickname}}</span><span class="gh-cp-membertier-renewal"> &ndash; </span>
{{/if}}
{{/if}}
{{#if sub.hasEnded}}
<span class="gh-cp-membertier-renewal">Ended {{sub.validUntil}}</span>
{{else if sub.willEndSoon}}
<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}}
<span class="gh-cp-membertier-renewal">Ends {{sub.trialUntil}}</span>
{{#if sub.isComplimentary}}
{{#if sub.compExpiry}}
<span class="gh-cp-membertier-renewal">Expires {{sub.compExpiry}}</span>
{{/if}}
{{else}}
<span class="gh-cp-membertier-renewal">Renews {{sub.validUntil}}</span>
{{#if sub.hasEnded}}
<span class="gh-cp-membertier-renewal">Ended {{sub.validUntil}}</span>
{{else if sub.willEndSoon}}
<span class="gh-cp-membertier-renewal">Has access until {{sub.validUntil}}</span>
{{else if sub.trialUntil}}
<span class="gh-cp-membertier-renewal">Ends {{sub.trialUntil}}</span>
{{else}}
<span class="gh-cp-membertier-renewal">Renews {{sub.validUntil}}</span>
{{/if}}
{{/if}}
</div>
<Member::SubscriptionDetailBox @sub={{sub}} @index={{index}} />

View file

@ -1,15 +1,15 @@
import {DATE_RELATION_OPTIONS} from './relation-options';
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 = {
label: 'Next billing date',
name: 'subscriptions.current_period_end',
valueType: 'date',
columnLabel: 'Next billing date',
label: 'Next billing date',
name: 'subscriptions.current_period_end',
valueType: 'date',
columnLabel: 'Next billing date',
relationOptions: DATE_RELATION_OPTIONS,
getColumnValue: (member, filter) => {
const subscription = mostRecentlyUpdated(member.subscriptions);
const subscription = mostRelevantSubscription(member.subscriptions);
return getDateColumnValue(subscription?.current_period_end, filter);
}
};

View file

@ -1,11 +1,11 @@
import {MATCH_RELATION_OPTIONS} from './relation-options';
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 = {
label: 'Billing period',
name: 'subscriptions.plan_interval',
columnLabel: 'Billing period',
label: 'Billing period',
name: 'subscriptions.plan_interval',
columnLabel: 'Billing period',
relationOptions: MATCH_RELATION_OPTIONS,
valueType: 'options',
options: [
@ -13,7 +13,7 @@ export const PLAN_INTERVAL_FILTER = {
{label: 'Yearly', name: 'year'}
],
getColumnValue: (member) => {
const subscription = mostRecentlyUpdated(member.subscriptions);
const subscription = mostRelevantSubscription(member.subscriptions);
if (!subscription) {
return null;
}

View file

@ -1,15 +1,15 @@
import {DATE_RELATION_OPTIONS} from './relation-options';
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 = {
label: 'Paid start date',
name: 'subscriptions.start_date',
valueType: 'date',
columnLabel: 'Paid start date',
label: 'Paid start date',
name: 'subscriptions.start_date',
valueType: 'date',
columnLabel: 'Paid start date',
relationOptions: DATE_RELATION_OPTIONS,
getColumnValue: (member, filter) => {
const subscription = mostRecentlyUpdated(member.subscriptions);
const subscription = mostRelevantSubscription(member.subscriptions);
return getDateColumnValue(subscription?.start_date, filter);
}
};

View file

@ -1,11 +1,11 @@
import {MATCH_RELATION_OPTIONS} from './relation-options';
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 = {
label: 'Stripe subscription status',
name: 'subscriptions.status',
columnLabel: 'Subscription Status',
label: 'Stripe subscription status',
name: 'subscriptions.status',
columnLabel: 'Subscription Status',
relationOptions: MATCH_RELATION_OPTIONS,
valueType: 'options',
options: [
@ -18,7 +18,7 @@ export const SUBSCRIPTION_STATUS_FILTER = {
{label: 'Incomplete - Expired', name: 'incomplete_expired'}
],
getColumnValue: (member) => {
const subscription = mostRecentlyUpdated(member.subscriptions);
const subscription = mostRelevantSubscription(member.subscriptions);
if (!subscription) {
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);
});
});