async linkSubscription(data, options = {}) {
        if (!this._stripeAPIService.configured) {
            throw new errors.BadRequestError({message: tpl(messages.noStripeConnection, {action: 'link Stripe Subscription'})});
        }

        if (!options.transacting) {
            return this._Member.transaction((transacting) => {
                return this.linkSubscription(data, {
                    ...options,
                    transacting
                });
            });
        }

        if (!options.batch_id) {
            options.batch_id = ObjectId().toHexString();
        }

        const memberModel = await this._Member.findOne({
            id: data.id
        }, {...options, forUpdate: true});

        const memberStripeCustomerModel = await memberModel.related('stripeCustomers').query({
            where: {
                customer_id: data.subscription.customer
            }
        }).fetchOne(options);

        if (!memberStripeCustomerModel) {
            // Maybe just link the customer?
            throw new errors.NotFoundError({message: tpl(messages.subscriptionNotFound)});
        }

        const stripeSubscriptionData = await this._stripeAPIService.getSubscription(data.subscription.id);
        let paymentMethodId;
        if (!stripeSubscriptionData.default_payment_method) {
            paymentMethodId = null;
        } else if (typeof stripeSubscriptionData.default_payment_method === 'string') {
            paymentMethodId = stripeSubscriptionData.default_payment_method;
        } else {
            paymentMethodId = stripeSubscriptionData.default_payment_method.id;
        }
        const stripePaymentMethodData = paymentMethodId ? await this._stripeAPIService.getCardPaymentMethod(paymentMethodId) : null;

        const stripeCustomerSubscriptionModel = await this.getSubscriptionByStripeID(stripeSubscriptionData.id, {...options, forUpdate: true});

        const subscriptionPriceData = _.get(stripeSubscriptionData, 'items.data[0].price');
        let ghostProduct;
        try {
            ghostProduct = await this._productRepository.get({stripe_product_id: subscriptionPriceData.product}, options);
            // Use first Ghost product as default product in case of missing link
            if (!ghostProduct) {
                ghostProduct = await this._productRepository.getDefaultProduct({
                    forUpdate: true,
                    ...options
                });
            }

            // Link Stripe Product & Price to Ghost Product
            if (ghostProduct) {
                await this._productRepository.update({
                    id: ghostProduct.get('id'),
                    name: ghostProduct.get('name'),
                    stripe_prices: [
                        {
                            stripe_price_id: subscriptionPriceData.id,
                            stripe_product_id: subscriptionPriceData.product,
                            active: subscriptionPriceData.active,
                            nickname: subscriptionPriceData.nickname,
                            currency: subscriptionPriceData.currency,
                            amount: subscriptionPriceData.unit_amount,
                            type: subscriptionPriceData.type,
                            interval: (subscriptionPriceData.recurring && subscriptionPriceData.recurring.interval) || null
                        }
                    ]
                }, options);
            } else {
                // Log error if no Ghost products found
                logging.error(`There was an error linking subscription - ${stripeSubscriptionData.id}, no Products exist.`);
            }
        } catch (e) {
            logging.error(`Failed to handle prices and product for - ${stripeSubscriptionData.id}.`);
            logging.error(e);
        }

        let stripeCouponId = stripeSubscriptionData.discount && stripeSubscriptionData.discount.coupon ? stripeSubscriptionData.discount.coupon.id : null;

        // For trial offers, offer id is passed from metadata as there is no stripe coupon
        let offerId = data.offerId || null;
        let offer = null;

        if (stripeCouponId) {
            // Get the offer from our database
            offer = await this._offerRepository.getByStripeCouponId(stripeCouponId, {transacting: options.transacting});
            if (offer) {
                offerId = offer.id;
            } else {
                logging.error(`Received an unknown stripe coupon id (${stripeCouponId}) for subscription - ${stripeSubscriptionData.id}.`);
            }
        } else if (offerId) {
            offer = await this._offerRepository.getById(offerId, {transacting: options.transacting});
        }

        const subscriptionData = {
            customer_id: stripeSubscriptionData.customer,
            subscription_id: stripeSubscriptionData.id,
            status: stripeSubscriptionData.status,
            cancel_at_period_end: stripeSubscriptionData.cancel_at_period_end,
            cancellation_reason: this.getCancellationReason(stripeSubscriptionData),
            current_period_end: new Date(stripeSubscriptionData.current_period_end * 1000),
            start_date: new Date(stripeSubscriptionData.start_date * 1000),
            default_payment_card_last4: stripePaymentMethodData && stripePaymentMethodData.card && stripePaymentMethodData.card.last4 || null,
            stripe_price_id: subscriptionPriceData.id,
            plan_id: subscriptionPriceData.id,
            // trial start and end are returned as Stripe timestamps and need coversion
            trial_start_at: stripeSubscriptionData.trial_start ? new Date(stripeSubscriptionData.trial_start * 1000) : null,
            trial_end_at: stripeSubscriptionData.trial_end ? new Date(stripeSubscriptionData.trial_end * 1000) : null,
            // NOTE: Defaulting to interval as migration to nullable field
            // turned out to be much bigger problem.
            // Ideally, would need nickname field to be nullable on the DB level
            // condition can be simplified once this is done
            plan_nickname: subscriptionPriceData.nickname || _.get(subscriptionPriceData, 'recurring.interval'),
            plan_interval: _.get(subscriptionPriceData, 'recurring.interval', ''),
            plan_amount: subscriptionPriceData.unit_amount,
            plan_currency: subscriptionPriceData.currency,
            mrr: this.getMRR({
                interval: _.get(subscriptionPriceData, 'recurring.interval', ''),
                amount: subscriptionPriceData.unit_amount,
                status: stripeSubscriptionData.status,
                canceled: stripeSubscriptionData.cancel_at_period_end,
                discount: stripeSubscriptionData.discount
            }),
            offer_id: offerId
        };

        const getStatus = (modelToCheck) => {
            const status = modelToCheck.get('status');
            const canceled = modelToCheck.get('cancel_at_period_end');

            if (status === 'canceled') {
                return 'expired';
            }

            if (canceled) {
                return 'canceled';
            }

            if (this.isActiveSubscriptionStatus(status)) {
                return 'active';
            }

            return 'inactive';
        };
        let eventData = {};

        const stripeCustomerSubscriptionModelShouldBeDeleted = stripeSubscriptionData.metadata && !!stripeSubscriptionData.metadata.ghost_migrated_to && stripeSubscriptionData.status === 'canceled';
        if (stripeCustomerSubscriptionModelShouldBeDeleted) {
            logging.warn(`Subscription ${subscriptionData.subscription_id} is marked for deletion, skipping linking.`);

            if (stripeCustomerSubscriptionModel) {
                // Delete all paid subscription events manually for this subscription
                // This is the only related event without a foreign key constraint
                await this._MemberPaidSubscriptionEvent.query().where('subscription_id', stripeCustomerSubscriptionModel.id).delete().transacting(options.transacting);

                // Delete the subscription in the database because we don't want to show it in the UI or in our data calculations
                await stripeCustomerSubscriptionModel.destroy(options);
            }
        } else if (stripeCustomerSubscriptionModel) {
            // CASE: Offer is already mapped against sub, don't overwrite it with NULL
            // Needed for trial offers, which don't have a stripe coupon/discount attached to sub
            if (!subscriptionData.offer_id) {
                delete subscriptionData.offer_id;
            }
            const updatedStripeCustomerSubscriptionModel = await this._StripeCustomerSubscription.edit(subscriptionData, {
                ...options,
                id: stripeCustomerSubscriptionModel.id
            });

            // CASE: Existing free member subscribes to a paid tier with an offer
            // Stripe doesn't send the discount/offer info in the subscription.created event
            // So we need to record the offer redemption event upon updating the subscription here
            if (stripeCustomerSubscriptionModel.get('offer_id') === null && subscriptionData.offer_id) {
                const offerRedemptionEvent = OfferRedemptionEvent.create({
                    memberId: memberModel.id,
                    offerId: subscriptionData.offer_id,
                    subscriptionId: updatedStripeCustomerSubscriptionModel.id
                }, updatedStripeCustomerSubscriptionModel.get('created_at'));
                this.dispatchEvent(offerRedemptionEvent, options);
            }

            if (stripeCustomerSubscriptionModel.get('mrr') !== updatedStripeCustomerSubscriptionModel.get('mrr') || stripeCustomerSubscriptionModel.get('plan_id') !== updatedStripeCustomerSubscriptionModel.get('plan_id') || stripeCustomerSubscriptionModel.get('status') !== updatedStripeCustomerSubscriptionModel.get('status') || stripeCustomerSubscriptionModel.get('cancel_at_period_end') !== updatedStripeCustomerSubscriptionModel.get('cancel_at_period_end')) {
                const originalMrrDelta = stripeCustomerSubscriptionModel.get('mrr');
                const updatedMrrDelta = updatedStripeCustomerSubscriptionModel.get('mrr');

                const getEventType = (originalStatus, updatedStatus) => {
                    if (originalStatus === updatedStatus) {
                        return 'updated';
                    }

                    if (originalStatus === 'canceled' && updatedStatus === 'active') {
                        return 'reactivated';
                    }

                    return updatedStatus;
                };

                const originalStatus = getStatus(stripeCustomerSubscriptionModel);
                const updatedStatus = getStatus(updatedStripeCustomerSubscriptionModel);
                const eventType = getEventType(originalStatus, updatedStatus);

                const mrrDelta = updatedMrrDelta - originalMrrDelta;

                await this._MemberPaidSubscriptionEvent.add({
                    member_id: memberModel.id,
                    source: 'stripe',
                    type: eventType,
                    subscription_id: updatedStripeCustomerSubscriptionModel.id,
                    from_plan: stripeCustomerSubscriptionModel.get('plan_id'),
                    to_plan: updatedStripeCustomerSubscriptionModel.get('status') === 'canceled' ? null : updatedStripeCustomerSubscriptionModel.get('plan_id'),
                    currency: subscriptionPriceData.currency,
                    mrr_delta: mrrDelta
                }, options);

                // Did we activate this subscription?
                // This happens when an incomplete subscription is completed
                // This always happens during the 3D secure flow, so it is important to catch
                if (originalStatus !== 'active' && updatedStatus === 'active') {
                    const context = options?.context || {};
                    const source = this._resolveContextSource(context);

                    const subscriptionActivatedEvent = SubscriptionActivatedEvent.create({
                        source,
                        tierId: ghostProduct?.get('id'),
                        memberId: memberModel.id,
                        subscriptionId: updatedStripeCustomerSubscriptionModel.get('id'),
                        offerId: offerId,
                        batchId: options.batch_id
                    });
                    this.dispatchEvent(subscriptionActivatedEvent, options);
                }

                // Dispatch cancellation event, i.e. send paid cancellation staff notification, if:
                // 1. The subscription has been set to cancel at period end, by the member in Portal, status 'canceled'
                // 2. The subscription has been immediately canceled (e.g. due to multiple failed payments), status 'expired'
                if (this.isActiveSubscriptionStatus(originalStatus) && (updatedStatus === 'canceled' || updatedStatus === 'expired')) {
                    const context = options?.context || {};
                    const source = this._resolveContextSource(context);
                    const cancelNow = updatedStatus === 'expired';
                    const canceledAt = new Date(stripeSubscriptionData.canceled_at * 1000);
                    const expiryAt = cancelNow ? canceledAt : updatedStripeCustomerSubscriptionModel.get('current_period_end');

                    const subscriptionCancelledEvent = SubscriptionCancelledEvent.create({
                        source,
                        tierId: ghostProduct?.get('id'),
                        memberId: memberModel.id,
                        subscriptionId: updatedStripeCustomerSubscriptionModel.get('id'),
                        cancelNow,
                        canceledAt,
                        expiryAt
                    });

                    this.dispatchEvent(subscriptionCancelledEvent, options);
                }
            }
        } else {
            eventData.created_at = new Date(stripeSubscriptionData.start_date * 1000);
            const newStripeCustomerSubscriptionModel = await this._StripeCustomerSubscription.add(subscriptionData, options);
            await this._MemberPaidSubscriptionEvent.add({
                member_id: memberModel.id,
                subscription_id: newStripeCustomerSubscriptionModel.id,
                type: 'created',
                source: 'stripe',
                from_plan: null,
                to_plan: subscriptionPriceData.id,
                currency: subscriptionPriceData.currency,
                mrr_delta: newStripeCustomerSubscriptionModel.get('mrr'),
                ...eventData
            }, options);

            const context = options?.context || {};
            const source = this._resolveContextSource(context);
            const attribution = {
                id: data.attribution?.id ?? stripeSubscriptionData.metadata?.attribution_id ?? null,
                url: data.attribution?.url ?? stripeSubscriptionData.metadata?.attribution_url ?? null,
                type: data.attribution?.type ?? stripeSubscriptionData.metadata?.attribution_type ?? null,
                referrerSource: data.attribution?.referrerSource ?? stripeSubscriptionData.metadata?.referrer_source ?? null,
                referrerMedium: data.attribution?.referrerMedium ?? stripeSubscriptionData.metadata?.referrer_medium ?? null,
                referrerUrl: data.attribution?.referrerUrl ?? stripeSubscriptionData.metadata?.referrer_url ?? null
            };

            const subscriptionCreatedEvent = SubscriptionCreatedEvent.create({
                source,
                tierId: ghostProduct?.get('id'),
                memberId: memberModel.id,
                subscriptionId: newStripeCustomerSubscriptionModel.get('id'),
                offerId: offerId,
                attribution: attribution,
                batchId: options.batch_id
            });

            this.dispatchEvent(subscriptionCreatedEvent, options);

            if (offerId) {
                const offerRedemptionEvent = OfferRedemptionEvent.create({
                    memberId: memberModel.id,
                    offerId: offerId,
                    subscriptionId: newStripeCustomerSubscriptionModel.get('id')
                });
                this.dispatchEvent(offerRedemptionEvent, options);
            }

            if (getStatus(newStripeCustomerSubscriptionModel) === 'active') {
                const subscriptionActivatedEvent = SubscriptionActivatedEvent.create({
                    source,
                    tierId: ghostProduct?.get('id'),
                    memberId: memberModel.id,
                    subscriptionId: newStripeCustomerSubscriptionModel.get('id'),
                    offerId: offerId,
                    attribution: attribution,
                    batchId: options.batch_id
                });
                this.dispatchEvent(subscriptionActivatedEvent, options);
            }
        }

        let memberProducts = (await memberModel.related('products').fetch(options)).toJSON();
        const oldMemberProducts = memberModel.related('products').toJSON();
        let status = memberProducts.length === 0 ? 'free' : 'comped';
        if (!stripeCustomerSubscriptionModelShouldBeDeleted && this.isActiveSubscriptionStatus(stripeSubscriptionData.status)) {
            if (this.isComplimentarySubscription(stripeSubscriptionData)) {
                status = 'comped';
            } else {
                status = 'paid';
            }

            if (stripeCustomerSubscriptionModel) {
                // We might need to...
                // 1. delete the previous product from the linked member products (in case an existing subscription changed product/price)
                // 2. fix the list of products linked to a member (an existing subscription doesn't have a linked product to this member)

                const subscriptions = await memberModel.related('stripeSubscriptions').fetch(options);

                const previousProduct = await this._productRepository.get({
                    stripe_price_id: stripeCustomerSubscriptionModel.get('stripe_price_id')
                }, options);

                if (previousProduct) {
                    let activeSubscriptionForPreviousProduct = false;

                    for (const subscriptionModel of subscriptions.models) {
                        if (this.isActiveSubscriptionStatus(subscriptionModel.get('status')) && subscriptionModel.id !== stripeCustomerSubscriptionModel.id) {
                            try {
                                const subscriptionProduct = await this._productRepository.get({stripe_price_id: subscriptionModel.get('stripe_price_id')}, options);
                                if (subscriptionProduct && previousProduct && subscriptionProduct.id === previousProduct.id) {
                                    activeSubscriptionForPreviousProduct = true;
                                }

                                if (subscriptionProduct && !memberProducts.find(p => p.id === subscriptionProduct.id)) {
                                    // Due to a bug in the past it is possible that this subscription's product wasn't added to the member products
                                    // So we need to add it again
                                    memberProducts.push(subscriptionProduct.toJSON());
                                }
                            } catch (e) {
                                logging.error(`Failed to attach products to member - ${data.id}`);
                                logging.error(e);
                            }
                        }
                    }

                    if (!activeSubscriptionForPreviousProduct) {
                        // We can safely remove the product from this member because it doesn't have any other remaining active subscription for it
                        memberProducts = memberProducts.filter((product) => {
                            return product.id !== previousProduct.id;
                        });
                    }
                }
            }

            if (ghostProduct) {
                // Note: we add the product here
                // We don't override the products because in an edge case a member can have multiple subscriptions
                // We'll need to keep all the products related to those subscriptions to avoid creating other issues
                memberProducts.push(ghostProduct.toJSON());
            }
        } else {
            const subscriptions = await memberModel.related('stripeSubscriptions').fetch(options);
            let activeSubscriptionForGhostProduct = false;
            for (const subscriptionModel of subscriptions.models) {
                if (this.isActiveSubscriptionStatus(subscriptionModel.get('status'))) {
                    status = 'paid';
                    try {
                        const subscriptionProduct = await this._productRepository.get({stripe_price_id: subscriptionModel.get('stripe_price_id')}, options);
                        if (subscriptionProduct && ghostProduct && subscriptionProduct.id === ghostProduct.id) {
                            activeSubscriptionForGhostProduct = true;
                        }

                        if (subscriptionProduct && !memberProducts.find(p => p.id === subscriptionProduct.id)) {
                            // Due to a bug in the past it is possible that this subscription's product wasn't added to the member products
                            // So we need to add it again
                            memberProducts.push(subscriptionProduct.toJSON());
                        }
                    } catch (e) {
                        logging.error(`Failed to attach products to member - ${data.id}`);
                        logging.error(e);
                    }
                }
            }

            if (!activeSubscriptionForGhostProduct) {
                // We don't have an active subscription for this product anymore, so we can safely unlink it from the member
                memberProducts = memberProducts.filter((product) => {
                    return product.id !== ghostProduct.id;
                });
            }

            if (memberProducts.length === 0) {
                // If all products were removed, set the status back to 'free'
                status = 'free';
            }
        }

        let updatedMember;
        try {
            // Remove duplicate products from the list
            memberProducts = _.uniqBy(memberProducts, function (e) {
                return e.id;
            });
            // Edit member with updated products assoicated
            updatedMember = await this._Member.edit({status: status, products: memberProducts}, {...options, id: data.id});
        } catch (e) {
            logging.error(`Failed to update member - ${data.id} - with related products`);
            logging.error(e);
            updatedMember = await this._Member.edit({status: status}, {...options, id: data.id});
        }

        const newMemberProductIds = memberProducts.map(product => product.id);
        const oldMemberProductIds = oldMemberProducts.map(product => product.id);

        const productsToAdd = _.differenceWith(newMemberProductIds, oldMemberProductIds);
        const productsToRemove = _.differenceWith(oldMemberProductIds, newMemberProductIds);

        for (const productToAdd of productsToAdd) {
            await this._MemberProductEvent.add({
                member_id: memberModel.id,
                product_id: productToAdd,
                action: 'added'
            }, options);
        }

        for (const productToRemove of productsToRemove) {
            await this._MemberProductEvent.add({
                member_id: memberModel.id,
                product_id: productToRemove,
                action: 'removed'
            }, options);
        }

        if (updatedMember.attributes.status !== updatedMember._previousAttributes.status) {
            await this._MemberStatusEvent.add({
                member_id: data.id,
                from_status: updatedMember._previousAttributes.status,
                to_status: updatedMember.get('status'),
                ...eventData
            }, options);
        }
    }
@param {Object} data
@param {String} data.id - member ID
@param {Object} data.subscription
@param {String} data.offerId
@param {import('@tryghost/member-attribution/lib/Attribution').AttributionResource} [data.attribution]
@param {*} options
@returns