diff --git a/core/server/api/canary/members.js b/core/server/api/canary/members.js index 3c53c647ce..aa9cc724d3 100644 --- a/core/server/api/canary/members.js +++ b/core/server/api/canary/members.js @@ -40,7 +40,7 @@ module.exports = { permissions: true, validation: {}, async query(frame) { - frame.options.withRelated = ['labels', 'stripeSubscriptions', 'stripeSubscriptions.customer']; + frame.options.withRelated = ['labels', 'stripeSubscriptions', 'stripeSubscriptions.customer', 'stripeSubscriptions.stripePrice', 'stripeSubscriptions.stripePrice.stripeProduct']; const page = await membersService.api.members.list(frame.options); return page; @@ -65,7 +65,7 @@ module.exports = { }, permissions: true, async query(frame) { - const defaultWithRelated = ['labels', 'stripeSubscriptions', 'stripeSubscriptions.customer']; + const defaultWithRelated = ['labels', 'stripeSubscriptions', 'stripeSubscriptions.customer', 'stripeSubscriptions.stripePrice', 'stripeSubscriptions.stripePrice.stripeProduct']; if (!frame.options.withRelated) { frame.options.withRelated = defaultWithRelated; @@ -109,7 +109,7 @@ module.exports = { permissions: true, async query(frame) { let member; - frame.options.withRelated = ['stripeSubscriptions', 'stripeSubscriptions.customer']; + frame.options.withRelated = ['stripeSubscriptions', 'products', 'labels', 'stripeSubscriptions.stripePrice', 'stripeSubscriptions.stripePrice.stripeProduct']; try { if (!membersService.config.isStripeConnected() && (frame.data.members[0].stripe_customer_id || frame.data.members[0].comped)) { @@ -185,7 +185,7 @@ module.exports = { permissions: true, async query(frame) { try { - frame.options.withRelated = ['stripeSubscriptions', 'labels']; + frame.options.withRelated = ['stripeSubscriptions', 'products', 'labels', 'stripeSubscriptions.stripePrice', 'stripeSubscriptions.stripePrice.stripeProduct']; const member = await membersService.api.members.update(frame.data.members[0], frame.options); const hasCompedSubscription = !!member.related('stripeSubscriptions').find(sub => sub.get('plan_nickname') === 'Complimentary' && sub.get('status') === 'active'); @@ -255,7 +255,51 @@ module.exports = { } }); let model = await membersService.api.members.get({id: frame.options.id}, { - withRelated: ['labels', 'stripeSubscriptions', 'stripeSubscriptions.customer'] + withRelated: ['labels', 'products', 'stripeSubscriptions', 'stripeSubscriptions.customer', 'stripeSubscriptions.stripePrice', 'stripeSubscriptions.stripePrice.stripeProduct'] + }); + if (!model) { + throw new errors.NotFoundError({ + message: i18n.t('errors.api.members.memberNotFound') + }); + } + + return model; + } + }, + + createSubscription: { + statusCode: 200, + headers: {}, + options: [ + 'id' + ], + data: [ + 'stripe_price_id' + ], + validation: { + options: { + id: { + required: true + } + }, + data: { + stripe_price_id: { + required: true + } + } + }, + permissions: { + method: 'edit' + }, + async query(frame) { + await membersService.api.members.createSubscription({ + id: frame.options.id, + subscription: { + stripe_price_id: frame.data.stripe_price_id + } + }); + let model = await membersService.api.members.get({id: frame.options.id}, { + withRelated: ['labels', 'products', 'stripeSubscriptions', 'stripeSubscriptions.customer', 'stripeSubscriptions.stripePrice', 'stripeSubscriptions.stripePrice.stripeProduct'] }); if (!model) { throw new errors.NotFoundError({ diff --git a/core/server/api/canary/utils/serializers/output/members.js b/core/server/api/canary/utils/serializers/output/members.js index af9feb3cc6..eaf8382c37 100644 --- a/core/server/api/canary/utils/serializers/output/members.js +++ b/core/server/api/canary/utils/serializers/output/members.js @@ -10,6 +10,7 @@ module.exports = { edit: createSerializer('edit', singleMember), add: createSerializer('add', singleMember), editSubscription: createSerializer('editSubscription', singleMember), + createSubscription: createSerializer('createSubscription', singleMember), bulkDestroy: createSerializer('bulkDestroy', passthrough), exportCSV: createSerializer('exportCSV', exportCSV), @@ -197,11 +198,16 @@ function createSerializer(debugString, serialize) { * @prop {null|string} customer.name * @prop {string} customer.email * - * @prop {Object} plan - * @prop {string} plan.id - * @prop {string} plan.nickname - * @prop {number} plan.amount - * @prop {string} plan.currency + * @prop {Object} price + * @prop {string} price.id + * @prop {string} price.nickname + * @prop {number} price.amount + * @prop {string} price.interval + * @prop {string} price.currency + * + * @prop {Object} price.product + * @prop {string} price.product.id + * @prop {string} price.product.product_id */ /** diff --git a/core/server/models/stripe-customer-subscription.js b/core/server/models/stripe-customer-subscription.js index c39e66b507..fd9265f7aa 100644 --- a/core/server/models/stripe-customer-subscription.js +++ b/core/server/models/stripe-customer-subscription.js @@ -7,10 +7,14 @@ const StripeCustomerSubscription = ghostBookshelf.Model.extend({ return this.belongsTo('MemberStripeCustomer', 'customer_id', 'customer_id'); }, + stripePrice() { + return this.hasOne('StripePrice', 'stripe_price_id', 'stripe_price_id'); + }, + serialize(options) { const defaultSerializedObject = ghostBookshelf.Model.prototype.serialize.call(this, options); - return { + const serialized = { id: defaultSerializedObject.subscription_id, customer: { id: defaultSerializedObject.customer_id, @@ -32,6 +36,26 @@ const StripeCustomerSubscription = ghostBookshelf.Model.extend({ cancellation_reason: defaultSerializedObject.cancellation_reason, current_period_end: defaultSerializedObject.current_period_end }; + + if (defaultSerializedObject.stripePrice) { + serialized.price = { + id: defaultSerializedObject.stripePrice.stripe_price_id, + nickname: defaultSerializedObject.stripePrice.nickname, + amount: defaultSerializedObject.stripePrice.amount, + interval: defaultSerializedObject.stripePrice.interval, + currency: String.prototype.toUpperCase.call(defaultSerializedObject.stripePrice.currency) + }; + + if (defaultSerializedObject.stripePrice.stripeProduct) { + serialized.price.product = { + id: defaultSerializedObject.stripePrice.stripeProduct.stripe_product_id, + name: defaultSerializedObject.stripePrice.stripeProduct.name, + product_id: defaultSerializedObject.stripePrice.stripeProduct.product_id + }; + } + } + + return serialized; } }, { diff --git a/core/server/web/api/canary/admin/routes.js b/core/server/web/api/canary/admin/routes.js index f65f317433..e764b77f8b 100644 --- a/core/server/web/api/canary/admin/routes.js +++ b/core/server/web/api/canary/admin/routes.js @@ -121,6 +121,7 @@ module.exports = function apiRoutes() { router.put('/members/:id', mw.authAdminApi, http(apiCanary.members.edit)); router.del('/members/:id', mw.authAdminApi, http(apiCanary.members.destroy)); + router.post('/members/:id/subscriptions/', mw.authAdminApi, http(apiCanary.members.createSubscription)); router.put('/members/:id/subscriptions/:subscription_id', mw.authAdminApi, http(apiCanary.members.editSubscription)); router.get('/members/:id/signin_urls', mw.authAdminApi, http(apiCanary.memberSigninUrls.read)); diff --git a/test/api-acceptance/admin/members_spec.js b/test/api-acceptance/admin/members_spec.js index e3c7fd1916..1e0f015174 100644 --- a/test/api-acceptance/admin/members_spec.js +++ b/test/api-acceptance/admin/members_spec.js @@ -234,7 +234,7 @@ describe('Members API', function () { should.exist(jsonResponse2); should.exist(jsonResponse2.members); jsonResponse2.members.should.have.length(1); - localUtils.API.checkResponse(jsonResponse2.members[0], 'member', 'subscriptions'); + localUtils.API.checkResponse(jsonResponse2.members[0], 'member', ['subscriptions', 'products']); jsonResponse2.members[0].name.should.equal(memberChanged.name); jsonResponse2.members[0].email.should.equal(memberChanged.email); jsonResponse2.members[0].email.should.not.equal(memberToChange.email);