diff --git a/test/api/unit/libs/payments/apple.test.js b/test/api/unit/libs/payments/apple.test.js index 08cb34607e..1aefe2cb51 100644 --- a/test/api/unit/libs/payments/apple.test.js +++ b/test/api/unit/libs/payments/apple.test.js @@ -326,6 +326,7 @@ describe('Apple Payments', () => { it('errors when a user is already subscribed', async () => { payments.createSubscription.restore(); user = new User(); + await user.save(); await applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing); diff --git a/website/server/libs/payments/subscriptions.js b/website/server/libs/payments/subscriptions.js index 2052dc4c9a..80401f21de 100644 --- a/website/server/libs/payments/subscriptions.js +++ b/website/server/libs/payments/subscriptions.js @@ -11,6 +11,7 @@ import { // eslint-disable-line import/no-cycle model as Group, basicFields as basicGroupFields, } from '../../models/group'; +import { model as User } from '../../models/user'; // eslint-disable-line import/no-cycle import { NotAuthorized, NotFound, @@ -79,6 +80,22 @@ async function createSubscription (data) { let emailType = 'subscription-begins'; let recipientIsSubscribed = recipient.isSubscribed(); + if (data.user && !data.gift && !data.groupId) { + const unlockedUser = await User.findOneAndUpdate( + { + _id: data.user._id, + $or: [ + { _subSignature: 'NOT_RUNNING' }, + { _subSignature: { $exists: false } }, + ], + }, + { $set: { _subSignature: 'SUB_IN_PROGRESS' } }, + ); + if (!unlockedUser) { + throw new NotFound('User not found or subscription already processing.'); + } + } + // If we are buying a group subscription if (data.groupId) { const groupFields = basicGroupFields.concat(' purchased'); @@ -282,10 +299,6 @@ async function createSubscription (data) { } } - if (group) await group.save(); - if (data.user && data.user.isModified()) await data.user.save(); - if (data.gift) await data.gift.member.save(); - slack.sendSubscriptionNotification({ buyer: { id: data.user._id, @@ -302,6 +315,24 @@ async function createSubscription (data) { groupId, autoRenews, }); + + if (group) { + await group.save(); + } + if (data.user) { + if (data.user.isModified()) { + await data.user.save(); + } + if (!data.gift && !data.groupId) { + await User.findOneAndUpdate( + { _id: data.user._id }, + { $set: { _subSignature: 'NOT_RUNNING' } }, + ); + } + } + if (data.gift) { + await data.gift.member.save(); + } } // Cancels a subscription or group plan, setting termination to happen later diff --git a/website/server/models/user/schema.js b/website/server/models/user/schema.js index 3e0b4b84c0..79e58328a4 100644 --- a/website/server/models/user/schema.js +++ b/website/server/models/user/schema.js @@ -430,6 +430,8 @@ export default new Schema({ lastCron: { $type: Date, default: Date.now }, _cronSignature: { $type: String, default: 'NOT_RUNNING' }, // Private property used to avoid double cron + // Lock property to avoid double subscription. Not strictly private because we query on it + _subSignature: { $type: String, default: 'NOT_RUNNING' }, // {GROUP_ID: Boolean}, represents whether they have unseen chat messages newMessages: {