mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-27 10:42:45 +03:00
Updated MRR events & stored current subscription MRR
refs https://github.com/TryGhost/Team/issues/1456 refs https://github.com/TryGhost/Team/issues/1454 refs https://github.com/TryGhost/Team/issues/1302 refs https://github.com/TryGhost/Team/issues/1453 This includes changes to store MRR on subscriptions, as well as to store events for both cancellation and expiration of subscriptions. Cancellation events will cause a change in MRR when the `dashboardV5` flag is enabled. Co-authored-by: Simon Backx <simon@ghost.org>
This commit is contained in:
parent
3381dae1e6
commit
baf9c1b61b
@ -83,7 +83,7 @@
|
||||
"@tryghost/logging": "2.1.5",
|
||||
"@tryghost/magic-link": "1.0.21",
|
||||
"@tryghost/member-events": "0.4.1",
|
||||
"@tryghost/members-api": "5.6.1",
|
||||
"@tryghost/members-api": "5.7.1",
|
||||
"@tryghost/members-events-service": "0.3.3",
|
||||
"@tryghost/members-importer": "0.5.7",
|
||||
"@tryghost/members-offers": "0.10.9",
|
||||
|
@ -716,7 +716,7 @@ exports[`Members API Can create a member with an existing paid subscription 2: [
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "1769",
|
||||
"content-length": "1849",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
|
||||
|
@ -17,6 +17,14 @@ async function assertMemberEvents({eventType, memberId, asserts}) {
|
||||
assert.equal(events.length, asserts.length, `Only ${asserts.length} ${eventType} should have been added.`);
|
||||
}
|
||||
|
||||
async function assertSubscription(subscriptionId, asserts) {
|
||||
// eslint-disable-next-line dot-notation
|
||||
const subscription = await models['StripeCustomerSubscription'].where('subscription_id', subscriptionId).fetch({require: true});
|
||||
|
||||
// We use the native toJSON to prevent calling the overriden serialize method
|
||||
models.Base.Model.prototype.serialize.call(subscription).should.match(asserts);
|
||||
}
|
||||
|
||||
async function getPaidProduct() {
|
||||
return await Product.findOne({type: 'paid'});
|
||||
}
|
||||
@ -427,7 +435,7 @@ describe('Members API', function () {
|
||||
active: true,
|
||||
nickname: 'Complimentary',
|
||||
unit_amount: 0,
|
||||
currency: 'USD',
|
||||
currency: 'usd',
|
||||
type: 'recurring',
|
||||
recurring: {
|
||||
interval: 'year'
|
||||
@ -723,7 +731,7 @@ describe('Members API', function () {
|
||||
active: true,
|
||||
nickname: 'Complimentary',
|
||||
unit_amount: 0,
|
||||
currency: 'USD',
|
||||
currency: 'usd',
|
||||
type: 'recurring',
|
||||
recurring: {
|
||||
interval: 'year'
|
||||
@ -842,11 +850,11 @@ describe('Members API', function () {
|
||||
it('Can create a member with an existing paid subscription', async function () {
|
||||
const fakePrice = {
|
||||
id: 'price_1',
|
||||
product: '',
|
||||
product: 'product_1234',
|
||||
active: true,
|
||||
nickname: 'Paid',
|
||||
unit_amount: 1200,
|
||||
currency: 'USD',
|
||||
currency: 'usd',
|
||||
type: 'recurring',
|
||||
recurring: {
|
||||
interval: 'year'
|
||||
@ -864,6 +872,7 @@ describe('Members API', function () {
|
||||
plan: fakePrice,
|
||||
items: {
|
||||
data: [{
|
||||
id: 'item_123',
|
||||
price: fakePrice
|
||||
}]
|
||||
}
|
||||
@ -926,6 +935,8 @@ describe('Members API', function () {
|
||||
|
||||
const newMember = body.members[0];
|
||||
assert.equal(newMember.status, 'paid', 'The created member should have the paid status');
|
||||
assert.equal(newMember.subscriptions.length, 1, 'The member should have a single subscription');
|
||||
assert.equal(newMember.subscriptions[0].id, fakeSubscription.id, 'The returned subscription should have an ID assigned');
|
||||
|
||||
await assertMemberEvents({
|
||||
eventType: 'MemberStatusEvent',
|
||||
@ -959,6 +970,16 @@ describe('Members API', function () {
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await assertSubscription(fakeSubscription.id, {
|
||||
subscription_id: fakeSubscription.id,
|
||||
status: 'active',
|
||||
cancel_at_period_end: false,
|
||||
plan_amount: 1200,
|
||||
plan_interval: 'year',
|
||||
plan_currency: 'usd',
|
||||
mrr: 100
|
||||
});
|
||||
});
|
||||
|
||||
it('Can edit by id', async function () {
|
||||
|
@ -1,8 +1,8 @@
|
||||
const crypto = require('crypto');
|
||||
const assert = require('assert');
|
||||
const nock = require('nock');
|
||||
const should = require('should');
|
||||
const stripe = require('stripe');
|
||||
const {MemberStatusEvent} = require('../../../core/server/models/member-status-event');
|
||||
const {Product} = require('../../../core/server/models/product');
|
||||
const {agentProvider, mockManager, fixtureManager} = require('../../utils/e2e-framework');
|
||||
const models = require('../../../core/server/models');
|
||||
@ -10,16 +10,28 @@ const models = require('../../../core/server/models');
|
||||
let membersAgent;
|
||||
let adminAgent;
|
||||
|
||||
function createStripeID(prefix) {
|
||||
return `${prefix}_${crypto.randomBytes(16).toString('hex')}`;
|
||||
}
|
||||
|
||||
async function getPaidProduct() {
|
||||
return await Product.findOne({type: 'paid'});
|
||||
}
|
||||
|
||||
async function assertMemberEvents({eventType, memberId, asserts}) {
|
||||
const events = await models[eventType].where('member_id', memberId).fetchAll();
|
||||
events.map(e => e.toJSON()).should.match(asserts);
|
||||
const events = (await models[eventType].where('member_id', memberId).fetchAll()).toJSON();
|
||||
events.should.match(asserts);
|
||||
assert.equal(events.length, asserts.length, `Only ${asserts.length} ${eventType} should have been added.`);
|
||||
}
|
||||
|
||||
async function assertSubscription(subscriptionId, asserts) {
|
||||
// eslint-disable-next-line dot-notation
|
||||
const subscription = await models['StripeCustomerSubscription'].where('subscription_id', subscriptionId).fetch({require: true});
|
||||
|
||||
// We use the native toJSON to prevent calling the overriden serialize method
|
||||
models.Base.Model.prototype.serialize.call(subscription).should.match(asserts);
|
||||
}
|
||||
|
||||
describe('Members API', function () {
|
||||
before(async function () {
|
||||
const agents = await agentProvider.getAgentsForMembers();
|
||||
@ -170,12 +182,263 @@ describe('Members API', function () {
|
||||
.expectStatus(200);
|
||||
});
|
||||
|
||||
describe('Handling cancelled subscriptions', function () {
|
||||
describe('With the dashboardV5 flag', function () {
|
||||
beforeEach(function () {
|
||||
mockManager.mockLabsEnabled('dashboardV5');
|
||||
});
|
||||
it('Handles cancellation of paid subscriptions correctly', async function () {
|
||||
const customer_id = createStripeID('cust');
|
||||
const subscription_id = createStripeID('sub');
|
||||
|
||||
// Create a new subscription in Stripe
|
||||
set(subscription, {
|
||||
id: subscription_id,
|
||||
customer: customer_id,
|
||||
status: 'active',
|
||||
items: {
|
||||
type: 'list',
|
||||
data: [{
|
||||
id: 'item_123',
|
||||
price: {
|
||||
id: 'price_123',
|
||||
product: 'product_123',
|
||||
active: true,
|
||||
nickname: 'Monthly',
|
||||
currency: 'USD',
|
||||
recurring: {
|
||||
interval: 'month'
|
||||
},
|
||||
unit_amount: 500,
|
||||
type: 'recurring'
|
||||
}
|
||||
}]
|
||||
},
|
||||
start_date: Date.now() / 1000,
|
||||
current_period_end: Date.now() / 1000 + (60 * 60 * 24 * 31),
|
||||
cancel_at_period_end: false
|
||||
});
|
||||
|
||||
// Create a new customer in Stripe
|
||||
set(customer, {
|
||||
id: customer_id,
|
||||
name: 'Test Member',
|
||||
email: 'expired-paid-test@email.com',
|
||||
subscriptions: {
|
||||
type: 'list',
|
||||
data: [subscription]
|
||||
}
|
||||
});
|
||||
|
||||
// Make sure this customer has a corresponding member in the database
|
||||
// And all the subscriptions are setup correctly
|
||||
const initialMember = await createMemberFromStripe();
|
||||
assert.equal(initialMember.status, 'paid', 'The member initial status should be paid');
|
||||
assert.equal(initialMember.products.length, 1, 'The member should have one product');
|
||||
should(initialMember.subscriptions).match([
|
||||
{
|
||||
status: 'active'
|
||||
}
|
||||
]);
|
||||
|
||||
// Cancel the previously created subscription in Stripe
|
||||
set(subscription, {
|
||||
...subscription,
|
||||
cancel_at_period_end: true
|
||||
});
|
||||
|
||||
// Send the webhook call to anounce the cancelation
|
||||
const webhookPayload = JSON.stringify({
|
||||
type: 'customer.subscription.updated',
|
||||
data: {
|
||||
object: subscription
|
||||
}
|
||||
});
|
||||
const webhookSignature = stripe.webhooks.generateTestHeaderString({
|
||||
payload: webhookPayload,
|
||||
secret: process.env.WEBHOOK_SECRET
|
||||
});
|
||||
|
||||
await membersAgent.post('/webhooks/stripe/')
|
||||
.body(webhookPayload)
|
||||
.header('stripe-signature', webhookSignature)
|
||||
.expectStatus(200);
|
||||
|
||||
// Check status has been updated to 'free' after cancelling
|
||||
const {body: body2} = await adminAgent.get('/members/' + initialMember.id + '/');
|
||||
assert.equal(body2.members.length, 1, 'The member does not exist');
|
||||
const updatedMember = body2.members[0];
|
||||
assert.equal(updatedMember.status, 'paid');
|
||||
assert.equal(updatedMember.products.length, 1, 'The member should have products');
|
||||
should(updatedMember.subscriptions).match([
|
||||
{
|
||||
cancel_at_period_end: true
|
||||
}
|
||||
]);
|
||||
|
||||
// Check the status events for this newly created member (should be NULL -> paid only)
|
||||
await assertMemberEvents({
|
||||
eventType: 'MemberStatusEvent',
|
||||
memberId: updatedMember.id,
|
||||
asserts: [
|
||||
{
|
||||
from_status: null,
|
||||
to_status: 'free'
|
||||
},
|
||||
{
|
||||
from_status: 'free',
|
||||
to_status: 'paid'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await assertMemberEvents({
|
||||
eventType: 'MemberPaidSubscriptionEvent',
|
||||
memberId: updatedMember.id,
|
||||
asserts: [
|
||||
{
|
||||
type: 'created',
|
||||
mrr_delta: 500
|
||||
},
|
||||
{
|
||||
type: 'canceled',
|
||||
mrr_delta: -500
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Without the dashboardV5 flag', function () {
|
||||
it('Handles cancellation of paid subscriptions correctly', async function () {
|
||||
const customer_id = createStripeID('cust');
|
||||
const subscription_id = createStripeID('sub');
|
||||
|
||||
// Create a new subscription in Stripe
|
||||
set(subscription, {
|
||||
id: subscription_id,
|
||||
customer: customer_id,
|
||||
status: 'active',
|
||||
items: {
|
||||
type: 'list',
|
||||
data: [{
|
||||
id: 'item_123',
|
||||
price: {
|
||||
id: 'price_123',
|
||||
product: 'product_123',
|
||||
active: true,
|
||||
nickname: 'Monthly',
|
||||
currency: 'USD',
|
||||
recurring: {
|
||||
interval: 'month'
|
||||
},
|
||||
unit_amount: 500,
|
||||
type: 'recurring'
|
||||
}
|
||||
}]
|
||||
},
|
||||
start_date: Date.now() / 1000,
|
||||
current_period_end: Date.now() / 1000 + (60 * 60 * 24 * 31),
|
||||
cancel_at_period_end: false
|
||||
});
|
||||
|
||||
// Create a new customer in Stripe
|
||||
set(customer, {
|
||||
id: customer_id,
|
||||
name: 'Test Member',
|
||||
email: 'cancelled-paid-test-no-flag@email.com',
|
||||
subscriptions: {
|
||||
type: 'list',
|
||||
data: [subscription]
|
||||
}
|
||||
});
|
||||
|
||||
// Make sure this customer has a corresponding member in the database
|
||||
// And all the subscriptions are setup correctly
|
||||
const initialMember = await createMemberFromStripe();
|
||||
assert.equal(initialMember.status, 'paid', 'The member initial status should be paid');
|
||||
assert.equal(initialMember.products.length, 1, 'The member should have one product');
|
||||
should(initialMember.subscriptions).match([
|
||||
{
|
||||
status: 'active'
|
||||
}
|
||||
]);
|
||||
|
||||
// Cancel the previously created subscription in Stripe
|
||||
set(subscription, {
|
||||
...subscription,
|
||||
cancel_at_period_end: true
|
||||
});
|
||||
|
||||
// Send the webhook call to anounce the cancelation
|
||||
const webhookPayload = JSON.stringify({
|
||||
type: 'customer.subscription.updated',
|
||||
data: {
|
||||
object: subscription
|
||||
}
|
||||
});
|
||||
const webhookSignature = stripe.webhooks.generateTestHeaderString({
|
||||
payload: webhookPayload,
|
||||
secret: process.env.WEBHOOK_SECRET
|
||||
});
|
||||
|
||||
await membersAgent.post('/webhooks/stripe/')
|
||||
.body(webhookPayload)
|
||||
.header('stripe-signature', webhookSignature)
|
||||
.expectStatus(200);
|
||||
|
||||
// Check status has been updated to 'free' after cancelling
|
||||
const {body: body2} = await adminAgent.get('/members/' + initialMember.id + '/');
|
||||
assert.equal(body2.members.length, 1, 'The member does not exist');
|
||||
const updatedMember = body2.members[0];
|
||||
assert.equal(updatedMember.status, 'paid');
|
||||
assert.equal(updatedMember.products.length, 1, 'The member should have products');
|
||||
should(updatedMember.subscriptions).match([
|
||||
{
|
||||
cancel_at_period_end: true
|
||||
}
|
||||
]);
|
||||
|
||||
// Check the status events for this newly created member (should be NULL -> paid only)
|
||||
await assertMemberEvents({
|
||||
eventType: 'MemberStatusEvent',
|
||||
memberId: updatedMember.id,
|
||||
asserts: [
|
||||
{
|
||||
from_status: null,
|
||||
to_status: 'free'
|
||||
},
|
||||
{
|
||||
from_status: 'free',
|
||||
to_status: 'paid'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await assertMemberEvents({
|
||||
eventType: 'MemberPaidSubscriptionEvent',
|
||||
memberId: updatedMember.id,
|
||||
asserts: [
|
||||
{
|
||||
type: 'created',
|
||||
mrr_delta: 500
|
||||
},
|
||||
{
|
||||
type: 'canceled',
|
||||
mrr_delta: 0
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Handling the end of subscriptions', function () {
|
||||
let canceledPaidMember;
|
||||
|
||||
it('Handles cancellation of paid subscriptions correctly', async function () {
|
||||
const customer_id = 'cust_3432';
|
||||
const subscription_id = 'sub_3432';
|
||||
const customer_id = createStripeID('cust');
|
||||
const subscription_id = createStripeID('sub');
|
||||
|
||||
// Create a new subscription in Stripe
|
||||
set(subscription, {
|
||||
@ -191,7 +454,7 @@ describe('Members API', function () {
|
||||
product: 'product_123',
|
||||
active: true,
|
||||
nickname: 'Monthly',
|
||||
currency: 'USD',
|
||||
currency: 'usd',
|
||||
recurring: {
|
||||
interval: 'month'
|
||||
},
|
||||
@ -227,6 +490,17 @@ describe('Members API', function () {
|
||||
}
|
||||
]);
|
||||
|
||||
// Check whether MRR and status has been set
|
||||
await assertSubscription(initialMember.subscriptions[0].id, {
|
||||
subscription_id: subscription.id,
|
||||
status: 'active',
|
||||
cancel_at_period_end: false,
|
||||
plan_amount: 500,
|
||||
plan_interval: 'month',
|
||||
plan_currency: 'usd',
|
||||
mrr: 500
|
||||
});
|
||||
|
||||
// Cancel the previously created subscription in Stripe
|
||||
set(subscription, {
|
||||
...subscription,
|
||||
@ -262,8 +536,19 @@ describe('Members API', function () {
|
||||
}
|
||||
]);
|
||||
|
||||
// Check whether MRR and status has been set
|
||||
await assertSubscription(initialMember.subscriptions[0].id, {
|
||||
subscription_id: subscription.id,
|
||||
status: 'canceled',
|
||||
cancel_at_period_end: false,
|
||||
plan_amount: 500,
|
||||
plan_interval: 'month',
|
||||
plan_currency: 'usd',
|
||||
mrr: 0
|
||||
});
|
||||
|
||||
// Check the status events for this newly created member (should be NULL -> paid only)
|
||||
assertMemberEvents({
|
||||
await assertMemberEvents({
|
||||
eventType: 'MemberStatusEvent',
|
||||
memberId: updatedMember.id,
|
||||
asserts: [
|
||||
@ -282,14 +567,16 @@ describe('Members API', function () {
|
||||
]
|
||||
});
|
||||
|
||||
assertMemberEvents({
|
||||
await assertMemberEvents({
|
||||
eventType: 'MemberPaidSubscriptionEvent',
|
||||
memberId: updatedMember.id,
|
||||
asserts: [
|
||||
{
|
||||
type: 'created',
|
||||
mrr_delta: 500
|
||||
},
|
||||
{
|
||||
type: 'expired',
|
||||
mrr_delta: -500
|
||||
}
|
||||
]
|
||||
@ -329,7 +616,7 @@ describe('Members API', function () {
|
||||
assert.equal(updatedMember.subscriptions.length, 2, 'The member should have two subscriptions');
|
||||
|
||||
// Check the status events for this newly created member (should be NULL -> paid only)
|
||||
assertMemberEvents({
|
||||
await assertMemberEvents({
|
||||
eventType: 'MemberStatusEvent',
|
||||
memberId: updatedMember.id,
|
||||
asserts: [
|
||||
@ -352,7 +639,7 @@ describe('Members API', function () {
|
||||
]
|
||||
});
|
||||
|
||||
assertMemberEvents({
|
||||
await assertMemberEvents({
|
||||
eventType: 'MemberPaidSubscriptionEvent',
|
||||
memberId: updatedMember.id,
|
||||
asserts: [
|
||||
@ -367,15 +654,15 @@ describe('Members API', function () {
|
||||
});
|
||||
|
||||
it('Handles cancellation of old fashioned comped subscriptions correctly', async function () {
|
||||
const customer_id = 'cust_3433';
|
||||
const subscription_id = 'sub_3433';
|
||||
const customer_id = createStripeID('cust');
|
||||
const subscription_id = createStripeID('sub');
|
||||
|
||||
const price = {
|
||||
id: 'price_123',
|
||||
product: 'product_123',
|
||||
active: true,
|
||||
nickname: 'Complimentary',
|
||||
currency: 'USD',
|
||||
currency: 'usd',
|
||||
recurring: {
|
||||
interval: 'month'
|
||||
},
|
||||
@ -459,7 +746,7 @@ describe('Members API', function () {
|
||||
]);
|
||||
|
||||
// Check the status events for this newly created member (should be NULL -> paid only)
|
||||
assertMemberEvents({
|
||||
await assertMemberEvents({
|
||||
eventType: 'MemberStatusEvent',
|
||||
memberId: updatedMember.id,
|
||||
asserts: [
|
||||
@ -478,10 +765,16 @@ describe('Members API', function () {
|
||||
]
|
||||
});
|
||||
|
||||
assertMemberEvents({
|
||||
await assertMemberEvents({
|
||||
eventType: 'MemberPaidSubscriptionEvent',
|
||||
memberId: updatedMember.id,
|
||||
asserts: []
|
||||
asserts: [{
|
||||
type: 'created',
|
||||
mrr_delta: 0
|
||||
}, {
|
||||
type: 'expired',
|
||||
mrr_delta: 0
|
||||
}]
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -503,7 +796,7 @@ describe('Members API', function () {
|
||||
product: 'product_123',
|
||||
active: true,
|
||||
nickname: 'Monthly',
|
||||
currency: 'USD',
|
||||
currency: 'usd',
|
||||
recurring: {
|
||||
interval: 'month'
|
||||
},
|
||||
@ -513,7 +806,7 @@ describe('Members API', function () {
|
||||
}]
|
||||
},
|
||||
start_date: beforeNow / 1000,
|
||||
current_period_end: beforeNow / 1000 + (60 * 60 * 24 * 31),
|
||||
current_period_end: Math.floor(beforeNow / 1000) + (60 * 60 * 24 * 31),
|
||||
cancel_at_period_end: false
|
||||
});
|
||||
});
|
||||
@ -567,8 +860,20 @@ describe('Members API', function () {
|
||||
to: 'checkout-webhook-test@email.com'
|
||||
});
|
||||
|
||||
// Check whether MRR and status has been set
|
||||
await assertSubscription(member.subscriptions[0].id, {
|
||||
subscription_id: subscription.id,
|
||||
status: 'active',
|
||||
cancel_at_period_end: false,
|
||||
plan_amount: 500,
|
||||
plan_interval: 'month',
|
||||
plan_currency: 'usd',
|
||||
current_period_end: new Date(Math.floor(beforeNow / 1000) * 1000 + (60 * 60 * 24 * 31 * 1000)),
|
||||
mrr: 500
|
||||
});
|
||||
|
||||
// Check the status events for this newly created member (should be NULL -> paid only)
|
||||
assertMemberEvents({
|
||||
await assertMemberEvents({
|
||||
eventType: 'MemberStatusEvent',
|
||||
memberId: member.id,
|
||||
asserts: [
|
||||
@ -585,7 +890,7 @@ describe('Members API', function () {
|
||||
]
|
||||
});
|
||||
|
||||
assertMemberEvents({
|
||||
await assertMemberEvents({
|
||||
eventType: 'MemberPaidSubscriptionEvent',
|
||||
memberId: member.id,
|
||||
asserts: [
|
||||
@ -614,7 +919,7 @@ describe('Members API', function () {
|
||||
product: 'product_456',
|
||||
active: true,
|
||||
nickname: 'Monthly',
|
||||
currency: 'USD',
|
||||
currency: 'usd',
|
||||
recurring: {
|
||||
interval: 'month'
|
||||
},
|
||||
|
@ -2009,10 +2009,10 @@
|
||||
"@tryghost/domain-events" "^0.1.9"
|
||||
"@tryghost/member-events" "^0.4.1"
|
||||
|
||||
"@tryghost/members-api@5.6.1":
|
||||
version "5.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-5.6.1.tgz#5d04716701e813c3bfbc1be8f016a412859e1313"
|
||||
integrity sha512-Wamvr4rwl9ENt+Ue7o5nnBWuRlhXPYc5yoXn/2D87B2S1hMKhk+tu7zQ/0Hjb4ounZ9ypKR4Lp/KdC4RO5lPsQ==
|
||||
"@tryghost/members-api@5.7.1":
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-5.7.1.tgz#021db2c2092ad8320b93a76021761aa582c55f94"
|
||||
integrity sha512-zYrm+eP2ttvR+oetlNY1qyJGatmzDtxEp+t5cPq6cIBJdoCKL2Nm8BI9mbo4JEEpRmobJn78tD+IlxrEXtWF9A==
|
||||
dependencies:
|
||||
"@nexes/nql" "^0.6.0"
|
||||
"@tryghost/debug" "^0.1.2"
|
||||
|
Loading…
Reference in New Issue
Block a user