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:
parent
e7a72e2ab1
commit
e4f60ee028
7 changed files with 147 additions and 32 deletions
|
@ -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"> –</span>
|
||||
<span class="gh-cp-membertier-pricelabel">{{sub.price.nickname}}</span><span class="gh-cp-membertier-renewal"> – </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}} />
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
39
ghost/admin/app/helpers/most-relevant-subscription.js
Normal file
39
ghost/admin/app/helpers/most-relevant-subscription.js
Normal 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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue