const assert = require('node:assert/strict');
const sinon = require('sinon');
const errors = require('@tryghost/errors');
const DomainEvents = require('@tryghost/domain-events');
const labs = require('../../../../../../../core/shared/labs');
const MemberRepository = require('../../../../../../../core/server/services/members/members-api/repositories/member-repository');
const {SubscriptionCreatedEvent, OfferRedemptionEvent} = require('../../../../../../../core/shared/events');

describe('MemberRepository', function () {
    let Automation;
    let Member;
    let MemberCancelEvent;
    let MemberEmailChangeEvent;
    let MemberNewsletter;
    let MemberPaidSubscriptionEvent;
    let MemberProductEvent;
    let MemberStatusEvent;
    let MemberSubscribeEvent;
    let mockOfferRedemption;
    let StripeCustomer;
    let StripeCustomerSubscription;
    let WelcomeEmailAutomationRun;
    let newslettersService;
    let offersAPI;
    let automationsApi;
    let productRepository;
    let stripeAPIService;
    let tokenService;
    let labsIsSet;

    /**
     * @param {Partial<Record<keyof ConstructorParameters<typeof MemberRepository>[0], any>>} overrides
     * @returns {MemberRepository}
     */
    const buildRepo = (overrides = {}) => new MemberRepository({
        Automation,
        Member,
        MemberCancelEvent,
        MemberEmailChangeEvent,
        MemberNewsletter,
        MemberPaidSubscriptionEvent,
        MemberProductEvent,
        MemberStatusEvent,
        MemberSubscribeEventModel: MemberSubscribeEvent,
        OfferRedemption: mockOfferRedemption,
        StripeCustomer,
        StripeCustomerSubscription,
        WelcomeEmailAutomationRun,
        newslettersService,
        offersAPI,
        automationsApi,
        productRepository,
        stripeAPIService,
        tokenService,
        ...overrides
    });

    beforeEach(function () {
        labsIsSet = sinon.stub(labs, 'isSet').returns(false);

        mockOfferRedemption = {
            add: sinon.stub().resolves(),
            findOne: sinon.stub().resolves(null)
        };

        Member = {
            transaction: sinon.stub().callsFake((callback) => {
                return callback({executionPromise: Promise.resolve()});
            }),
            add: sinon.stub().resolves({
                id: 'member_id_123',
                get: sinon.stub().callsFake((key) => {
                    const data = {
                        email: 'test@example.com',
                        name: 'Test Member',
                        status: 'free',
                        created_at: new Date()
                    };
                    return data[key];
                }),
                related: sinon.stub().callsFake((relation) => {
                    if (relation === 'products') {
                        return {models: []};
                    }
                    if (relation === 'newsletters') {
                        return {models: []};
                    }
                    return {models: []};
                }),
                toJSON: sinon.stub().returns({
                    id: 'member_id_123',
                    email: 'test@example.com',
                    name: 'Test Member',
                    status: 'free'
                })
            })
        };

        MemberNewsletter = {
            bulkDestroy: sinon.stub().resolves(),
            getFilteredCollectionQuery: sinon.stub().resolves([])
        };

        MemberCancelEvent = {
            add: sinon.stub().resolves()
        };

        MemberEmailChangeEvent = {
            add: sinon.stub().resolves()
        };

        MemberPaidSubscriptionEvent = {
            add: sinon.stub().resolves(),
            query: sinon.stub().returns({
                where: sinon.stub().returns({
                    delete: sinon.stub().returns({
                        transacting: sinon.stub().resolves()
                    })
                })
            })
        };

        MemberProductEvent = {
            add: sinon.stub().resolves()
        };

        WelcomeEmailAutomationRun = {
            add: sinon.stub().resolves()
        };

        MemberStatusEvent = {
            add: sinon.stub().resolves()
        };

        MemberSubscribeEvent = {
            add: sinon.stub().resolves()
        };

        StripeCustomer = {
            add: sinon.stub().resolves(),
            findOne: sinon.stub().resolves(null),
            upsert: sinon.stub().resolves()
        };

        StripeCustomerSubscription = {
            add: sinon.stub().resolves({
                get: sinon.stub().returns()
            }),
            edit: sinon.stub().resolves({
                get: sinon.stub().returns()
            }),
            findOne: sinon.stub().resolves(null),
            upsert: sinon.stub().resolves()
        };

        newslettersService = {
            getDefaultNewsletters: sinon.stub().resolves([]),
            getAll: sinon.stub().resolves([])
        };

        offersAPI = {
            ensureOfferForStripeCoupon: sinon.stub().resolves({
                id: 'offer_new'
            })
        };

        automationsApi = {
            trigger: sinon.stub().resolves()
        };

        productRepository = {
            get: sinon.stub().resolves({
                get: sinon.stub().returns(),
                toJSON: sinon.stub().returns({})
            }),
            update: sinon.stub().resolves({}),
            getDefaultProduct: sinon.stub().resolves(null)
        };

        stripeAPIService = {
            configured: false
        };

        tokenService = {
            decodeToken: sinon.stub().resolves({})
        };

        Automation = {
            findOne: sinon.stub().resolves({
                id: 'automation_id_free',
                get: sinon.stub().callsFake((key) => {
                    const data = {status: 'active'};
                    return data[key];
                }),
                related: sinon.stub().callsFake((relation) => {
                    assert.equal(relation, 'welcomeEmailAutomatedEmail');
                    return {
                        id: 'automated_email_id_free',
                        get: sinon.stub().callsFake((key) => {
                            const data = {lexical: '{"root":{}}'};
                            return data[key];
                        })
                    };
                })
            })
        };
    });

    afterEach(function () {
        sinon.restore();
        // MemberRepository's constructor subscribes to DomainEvents on every build.
        // These tests construct the repository many times over a shared static
        // EventEmitter, so clear listeners between tests to avoid a leak warning
        // and to stop this file's listeners leaking into other files (isolate:false).
        DomainEvents.ee.removeAllListeners();
    });

    describe('#isComplimentarySubscription', function () {
        it('Does not error when subscription.plan is null', function () {
            const repo = buildRepo();
            repo.isComplimentarySubscription({});
        });
    });

    describe('#resolveContextSource', function (){
        it('Maps context to source', function (){
            const repo = buildRepo();

            let source = repo._resolveContextSource({
                import: true
            });
            assert.equal(source, 'import');

            source = repo._resolveContextSource({
                importer: true
            });
            assert.equal(source, 'import');

            source = repo._resolveContextSource({
                user: true
            });
            assert.equal(source, 'admin');

            source = repo._resolveContextSource({
                user: true,
                api_key: true
            });
            assert.equal(source, 'api');

            source = repo._resolveContextSource({
                api_key: true
            });
            assert.equal(source, 'api');

            source = repo._resolveContextSource({
            });
            assert.equal(source, 'member');

            source = repo._resolveContextSource({
                generic_context: true
            });
            assert.equal(source, 'member');
        });
    });

    describe('setComplimentarySubscription', function () {
        beforeEach(function () {
            Member = {
                findOne: sinon.stub().resolves({
                    id: 'member_id_123',
                    related: () => {
                        return {
                            fetch: () => {
                                return {
                                    models: []
                                };
                            }
                        };
                    }
                })
            };
        });

        it('throws an error when there is no default product', async function () {
            const repo = buildRepo({
                Member,
                stripeAPIService: {
                    configured: true
                },
                productRepository,
                OfferRedemption: mockOfferRedemption
            });

            try {
                await repo.setComplimentarySubscription({
                    id: 'member_id_123'
                }, {
                    transacting: true
                });

                assert.fail('setComplimentarySubscription should have thrown');
            } catch (err) {
                assert.equal(err.message, 'Could not find Product "default"');
            }
        });

        it('uses the right options for fetching default product', async function () {
            productRepository = {
                getDefaultProduct: sinon.stub().resolves({
                    toJSON: () => {
                        return null;
                    }
                })
            };

            const repo = buildRepo({
                Member,
                stripeAPIService: {
                    configured: true
                },
                productRepository,
                OfferRedemption: mockOfferRedemption
            });

            try {
                await repo.setComplimentarySubscription({
                    id: 'member_id_123'
                }, {
                    transacting: true,
                    withRelated: ['labels']
                });

                assert.fail('setComplimentarySubscription should have thrown');
            } catch (err) {
                sinon.assert.calledWith(productRepository.getDefaultProduct, {withRelated: ['stripePrices'], transacting: true});
                assert.equal(err.message, 'Could not find Product "default"');
            }
        });

        it('ignores non-complimentary zero-value prices when creating a complimentary subscription', async function () {
            const member = {
                id: 'member_id_123',
                get: sinon.stub().withArgs('email').returns('member@example.com'),
                related: () => {
                    return {
                        fetch: sinon.stub().resolves({
                            models: []
                        })
                    };
                }
            };
            Member.findOne.resolves(member);

            const activeSubscriptionPrice = {
                stripe_price_id: 'price_active_subscription',
                nickname: 'Active Subscription',
                currency: 'usd',
                amount: 0
            };
            const complimentaryPrice = {
                stripe_price_id: 'price_complimentary',
                nickname: 'Complimentary',
                currency: 'usd',
                amount: 0
            };

            productRepository = {
                getDefaultProduct: sinon.stub().resolves({
                    toJSON: () => {
                        return {
                            id: 'product_id_123',
                            name: 'Default tier',
                            description: null,
                            stripePrices: [activeSubscriptionPrice]
                        };
                    }
                }),
                update: sinon.stub().resolves({
                    toJSON: () => {
                        return {
                            stripePrices: [
                                activeSubscriptionPrice,
                                complimentaryPrice
                            ]
                        };
                    }
                })
            };

            stripeAPIService = {
                configured: true,
                createCustomer: sinon.stub().resolves({
                    id: 'cus_123',
                    email: 'member@example.com',
                    name: null
                }),
                createSubscription: sinon.stub().resolves({
                    id: 'sub_123',
                    customer: 'cus_123'
                })
            };

            StripeCustomer = {
                upsert: sinon.stub().resolves()
            };

            const repo = buildRepo({
                Member,
                productRepository,
                OfferRedemption: mockOfferRedemption
            });
            sinon.stub(repo, 'linkSubscription').resolves();

            await repo.setComplimentarySubscription({
                id: 'member_id_123'
            }, {
                transacting: true
            });

            sinon.assert.calledOnce(productRepository.update);
            sinon.assert.calledWith(stripeAPIService.createSubscription, 'cus_123', 'price_complimentary');
            sinon.assert.neverCalledWith(stripeAPIService.createSubscription, 'cus_123', 'price_active_subscription');
        });
    });

    describe('newsletter subscriptions', function () {
        let existingNewsletters;

        beforeEach(async function () {
            existingNewsletters = [
                {
                    id: 'newsletter_id_123',
                    attributes: {
                        status: 'active'
                    },
                    get: sinon.stub().withArgs('status').returns('active')
                },
                {
                    id: 'newsletter_id_1234_archive',
                    attributes: {
                        status: 'archived'
                    },
                    get: sinon.stub().withArgs('status').returns('archived')
                }
            ];

            Member = {
                findOne: sinon.stub().resolves({
                    get: sinon.stub().returns('member_id_123'),
                    related: sinon.stub().withArgs('newsletters').returns({
                        models: existingNewsletters
                    }),
                    toJSON: sinon.stub().returns({})
                }),
                edit: sinon.stub().resolves({
                    attributes: {},
                    _previousAttributes: {}
                })
            };

            stripeAPIService = {
                configured: false
            };

            MemberSubscribeEvent = {
                add: sinon.stub().resolves()
            };
        });

        it('Does not create false archived newsletter events', async function () {
            const repo = buildRepo({
                Member,
                MemberProductEvent,
                productRepository,
                stripeAPIService,
                MemberSubscribeEventModel: MemberSubscribeEvent,
                OfferRedemption: mockOfferRedemption
            });

            await repo.update({
                email: 'test@email.com',
                newsletters: [{
                    id: 'newsletter_id_123'
                },
                {
                    id: 'newsletter_id_456'
                },
                {
                    id: 'newsletter_id_new'
                },
                {
                    id: 'newsletter_id_1234_archive'
                }]
            },{
                transacting: {
                    executionPromise: Promise.resolve()
                },
                context: {}
            });

            sinon.assert.calledTwice(MemberSubscribeEvent.add);
        });
    });

    describe('linkSubscription', function (){
        let subscriptionData;
        let subscriptionCreatedNotifySpy;
        let offerRedemptionNotifySpy;

        afterEach(function () {
            sinon.restore();
        });

        beforeEach(async function () {
            sinon.stub(MemberRepository.prototype, '_updateCurrentSubscription').resolves();
            subscriptionCreatedNotifySpy = sinon.spy();
            offerRedemptionNotifySpy = sinon.spy();

            subscriptionData = {
                id: 'sub_123',
                customer: 'cus_123',
                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
            };

            Member = {
                findOne: sinon.stub().resolves({
                    related: (relation) => {
                        return {
                            query: sinon.stub().returns({
                                fetchOne: sinon.stub().resolves({})
                            }),
                            toJSON: sinon.stub().returns(relation === 'products' ? [] : {}),
                            fetch: sinon.stub().resolves({
                                toJSON: sinon.stub().returns(relation === 'products' ? [] : {}),
                                models: []
                            })
                        };
                    },
                    toJSON: sinon.stub().returns({})
                }),
                edit: sinon.stub().resolves({
                    attributes: {},
                    _previousAttributes: {}
                })
            };
            MemberPaidSubscriptionEvent = {
                add: sinon.stub().resolves()
            };
            StripeCustomerSubscription = {
                add: sinon.stub().resolves({
                    get: sinon.stub().returns()
                }),
                edit: sinon.stub().resolves({
                    get: sinon.stub().returns()
                })
            };
            MemberProductEvent = {
                add: sinon.stub().resolves({})
            };

            stripeAPIService = {
                configured: true,
                getSubscription: sinon.stub().resolves(subscriptionData),
                getCustomer: sinon.stub().resolves({
                    id: 'cus_123',
                    invoice_settings: {
                        default_payment_method: null
                    },
                    subscriptions: {data: []}
                })
            };

            productRepository = {
                get: sinon.stub().resolves({
                    get: sinon.stub().returns(),
                    toJSON: sinon.stub().returns({})
                }),
                update: sinon.stub().resolves({})
            };

            offersAPI = {
                ensureOfferForStripeCoupon: sinon.stub().resolves({
                    id: 'offer_new'
                })
            };
        });

        it('dispatches paid subscription event', async function (){
            const repo = buildRepo({
                stripeAPIService,
                StripeCustomerSubscription,
                MemberPaidSubscriptionEvent,
                MemberProductEvent,
                productRepository,
                Member,
                OfferRedemption: mockOfferRedemption
            });

            sinon.stub(repo, 'getSubscriptionByStripeID').resolves(null);

            DomainEvents.subscribe(SubscriptionCreatedEvent, subscriptionCreatedNotifySpy);
            DomainEvents.subscribe(OfferRedemptionEvent, offerRedemptionNotifySpy);

            await repo.linkSubscription({
                subscription: subscriptionData
            }, {
                transacting: {
                    executionPromise: Promise.resolve()
                },
                context: {}
            });

            sinon.assert.calledOnce(subscriptionCreatedNotifySpy);
            sinon.assert.notCalled(offerRedemptionNotifySpy);
        });

        it('dispatches the offer redemption event for a new member starting a subscription', async function (){
            // When a new member starts a paid subscription, the subscription is created with the offer ID
            const repo = buildRepo({
                stripeAPIService,
                StripeCustomerSubscription,
                MemberPaidSubscriptionEvent,
                MemberProductEvent,
                productRepository,
                offersAPI,
                Member,
                OfferRedemption: mockOfferRedemption
            });

            // No existing subscription
            sinon.stub(repo, 'getSubscriptionByStripeID').resolves(null);

            DomainEvents.subscribe(SubscriptionCreatedEvent, subscriptionCreatedNotifySpy);
            DomainEvents.subscribe(OfferRedemptionEvent, offerRedemptionNotifySpy);

            await repo.linkSubscription({
                id: 'member_id_123',
                subscription: subscriptionData,
                offerId: 'offer_123'
            }, {
                transacting: {
                    executionPromise: Promise.resolve()
                },
                context: {}
            });

            sinon.assert.calledOnce(subscriptionCreatedNotifySpy);
            sinon.assert.calledWith(subscriptionCreatedNotifySpy, sinon.match((event) => {
                if (event.data.offerId === 'offer_123') {
                    return true;
                }
                return false;
            }));

            sinon.assert.called(offerRedemptionNotifySpy);
            sinon.assert.calledWith(offerRedemptionNotifySpy, sinon.match((event) => {
                if (event.data.offerId === 'offer_123') {
                    return true;
                }
                return false;
            }));
        });

        it('dispatches the offer redemption event for an existing member upgrading to a paid subscription', async function (){
            // When an existing free member upgrades to a paid subscription, the subscription is first created _without_ the offer id
            // Then it is updated with the offer id after the checkout.completed webhook is received
            const repo = buildRepo({
                stripeAPIService,
                StripeCustomerSubscription,
                MemberPaidSubscriptionEvent,
                MemberProductEvent,
                productRepository,
                Member,
                OfferRedemption: mockOfferRedemption
            });

            sinon.stub(repo, 'getSubscriptionByStripeID').resolves({
                get: sinon.stub().withArgs('offer_id').returns(null)
            });

            DomainEvents.subscribe(SubscriptionCreatedEvent, subscriptionCreatedNotifySpy);
            DomainEvents.subscribe(OfferRedemptionEvent, offerRedemptionNotifySpy);

            await repo.linkSubscription({
                id: 'member_id_123',
                subscription: subscriptionData,
                offerId: 'offer_123'
            }, {
                transacting: {
                    executionPromise: Promise.resolve()
                },
                context: {}
            });

            sinon.assert.notCalled(subscriptionCreatedNotifySpy);

            sinon.assert.called(offerRedemptionNotifySpy);
            sinon.assert.calledWith(offerRedemptionNotifySpy, sinon.match((event) => {
                if (event.data.offerId === 'offer_123') {
                    return true;
                }
                return false;
            }));
        });

        it('creates an offer from a Stripe coupon', async function () {
            offersAPI = {
                ensureOfferForStripeCoupon: sinon.stub().resolves({id: 'offer_new'})
            };

            const productRepositoryWithTier = {
                get: sinon.stub().resolves({
                    get: sinon.stub().callsFake((key) => {
                        if (key === 'id') {
                            return 'tier_1';
                        }
                        if (key === 'name') {
                            return 'Tier One';
                        }
                        return null;
                    }),
                    toJSON: sinon.stub().returns({})
                }),
                update: sinon.stub().resolves({})
            };

            const stripeCoupon = {
                id: 'coupon_abc',
                percent_off: 20,
                duration: 'forever'
            };

            const repo = buildRepo({
                stripeAPIService: {
                    ...stripeAPIService,
                    getSubscription: sinon.stub().resolves({
                        ...subscriptionData,
                        discount: {
                            coupon: stripeCoupon
                        }
                    })
                },
                StripeCustomerSubscription,
                MemberPaidSubscriptionEvent,
                MemberProductEvent,
                productRepository: productRepositoryWithTier,
                offersAPI,
                Member,
                OfferRedemption: mockOfferRedemption
            });

            sinon.stub(repo, 'getSubscriptionByStripeID').resolves(null);

            const transacting = {
                executionPromise: Promise.resolve()
            };

            await repo.linkSubscription({
                id: 'member_id_123',
                subscription: {...subscriptionData, discount: {coupon: {id: 'coupon_abc'}}}
            }, {
                transacting,
                context: {}
            });

            sinon.assert.calledOnce(offersAPI.ensureOfferForStripeCoupon);
            // Verify the coupon, cadence, tier, and options are passed correctly
            assert.deepEqual(offersAPI.ensureOfferForStripeCoupon.firstCall.args[0], stripeCoupon);
            assert.equal(offersAPI.ensureOfferForStripeCoupon.firstCall.args[1], 'month');
            assert.deepEqual(offersAPI.ensureOfferForStripeCoupon.firstCall.args[2], {id: 'tier_1', name: 'Tier One'});
            assert.equal(offersAPI.ensureOfferForStripeCoupon.firstCall.args[3].transacting, transacting);
            assert.equal(StripeCustomerSubscription.add.firstCall.args[0].offer_id, 'offer_new');
        });

        it('sets offer_id to null if Stripe coupon is known to be incompatible with Ghost offers', async function () {
            const invalidCouponError = new errors.ValidationError({
                message: 'Offer `duration` must be "once" or "forever" for the "yearly" cadence.',
                code: 'INVALID_YEARLY_DURATION'
            });

            offersAPI = {
                ensureOfferForStripeCoupon: sinon.stub().rejects(invalidCouponError)
            };

            const productRepositoryWithTier = {
                get: sinon.stub().resolves({
                    get: sinon.stub().callsFake((key) => {
                        if (key === 'id') {
                            return 'tier_1';
                        }
                        if (key === 'name') {
                            return 'Tier One';
                        }
                        return null;
                    }),
                    toJSON: sinon.stub().returns({})
                }),
                update: sinon.stub().resolves({})
            };

            const stripeCoupon = {
                id: 'coupon_invalid',
                percent_off: 20,
                duration: 'repeating',
                duration_in_months: 3
            };

            const repo = buildRepo({
                stripeAPIService: {
                    ...stripeAPIService,
                    getSubscription: sinon.stub().resolves({
                        ...subscriptionData,
                        discount: {
                            coupon: stripeCoupon
                        }
                    })
                },
                StripeCustomerSubscription,
                MemberPaidSubscriptionEvent,
                MemberProductEvent,
                productRepository: productRepositoryWithTier,
                offersAPI,
                Member,
                OfferRedemption: mockOfferRedemption
            });

            sinon.stub(repo, 'getSubscriptionByStripeID').resolves(null);

            const transacting = {
                executionPromise: Promise.resolve()
            };

            // Should not throw despite the offer validation failure
            await repo.linkSubscription({
                id: 'member_id_123',
                subscription: {...subscriptionData, discount: {coupon: {id: 'coupon_invalid'}}}
            }, {
                transacting,
                context: {}
            });

            // Verify ensureOfferForStripeCoupon was called
            sinon.assert.calledOnce(offersAPI.ensureOfferForStripeCoupon);

            // Verify subscription was still created, but without an offer_id
            sinon.assert.calledOnce(StripeCustomerSubscription.add);
            assert.equal(StripeCustomerSubscription.add.firstCall.args[0].offer_id, null);
        });

        it('throws other validation errors from ensureOfferForStripeCoupon', async function () {
            const otherValidationError = new errors.ValidationError({
                message: 'Some other validation error'
            });

            offersAPI = {
                ensureOfferForStripeCoupon: sinon.stub().rejects(otherValidationError)
            };

            const productRepositoryWithTier = {
                get: sinon.stub().resolves({
                    get: sinon.stub().callsFake((key) => {
                        if (key === 'id') {
                            return 'tier_1';
                        }
                        if (key === 'name') {
                            return 'Tier One';
                        }
                        return null;
                    }),
                    toJSON: sinon.stub().returns({})
                }),
                update: sinon.stub().resolves({})
            };

            const stripeCoupon = {
                id: 'coupon_abc',
                percent_off: 20,
                duration: 'forever'
            };

            const repo = buildRepo({
                stripeAPIService: {
                    ...stripeAPIService,
                    getSubscription: sinon.stub().resolves({
                        ...subscriptionData,
                        discount: {
                            coupon: stripeCoupon
                        }
                    })
                },
                StripeCustomerSubscription,
                MemberPaidSubscriptionEvent,
                MemberProductEvent,
                productRepository: productRepositoryWithTier,
                offersAPI,
                Member,
                OfferRedemption: mockOfferRedemption
            });

            sinon.stub(repo, 'getSubscriptionByStripeID').resolves(null);

            const transacting = {
                executionPromise: Promise.resolve()
            };

            // Should throw because it's not the INVALID_YEARLY_DURATION error
            await assert.rejects(
                repo.linkSubscription({
                    id: 'member_id_123',
                    subscription: {...subscriptionData, discount: {coupon: {id: 'coupon_abc'}}}
                }, {
                    transacting,
                    context: {}
                }),
                (err) => {
                    assert.equal(err.message, 'Some other validation error');
                    return true;
                }
            );

            // Verify ensureOfferForStripeCoupon was called
            sinon.assert.calledOnce(offersAPI.ensureOfferForStripeCoupon);

            // Verify subscription was NOT created because the error was rethrown
            sinon.assert.notCalled(StripeCustomerSubscription.add);
        });

        it('persists discount_start and discount_end for a new subscription', async function () {
            const discountStart = 1768940436;
            const discountEnd = 1784578836;

            const subscriptionWithDiscount = {
                ...subscriptionData,
                discount: {
                    id: 'di_1SrlPkB8iNXqDnZRX03bQHnQ',
                    coupon: {
                        id: 'H4H47PbG',
                        percent_off: 20,
                        duration: 'repeating',
                        duration_in_months: 6
                    },
                    start: discountStart,
                    end: discountEnd
                }
            };

            const repo = buildRepo({
                stripeAPIService: {
                    ...stripeAPIService,
                    getSubscription: sinon.stub().resolves(subscriptionWithDiscount)
                },
                StripeCustomerSubscription,
                MemberPaidSubscriptionEvent,
                MemberProductEvent,
                productRepository,
                offersAPI,
                Member,
                OfferRedemption: mockOfferRedemption
            });

            // No existing subscription
            sinon.stub(repo, 'getSubscriptionByStripeID').resolves(null);

            await repo.linkSubscription({
                subscription: subscriptionWithDiscount
            }, {
                transacting: {
                    executionPromise: Promise.resolve()
                },
                context: {}
            });

            // Verify discount_start and discount_end are set correctly
            sinon.assert.calledOnce(StripeCustomerSubscription.add);
            const addedSubscriptionData = StripeCustomerSubscription.add.firstCall.args[0];

            assert.ok(addedSubscriptionData.discount_start instanceof Date);
            assert.ok(addedSubscriptionData.discount_end instanceof Date);
            assert.equal(addedSubscriptionData.discount_start.getTime(), discountStart * 1000);
            assert.equal(addedSubscriptionData.discount_end.getTime(), discountEnd * 1000);
        });

        it('persists discount_start and discount_end for an existing subscription', async function () {
            const discountStart = 1768940436;
            const discountEnd = 1784578836;

            const subscriptionWithDiscount = {
                ...subscriptionData,
                discount: {
                    id: 'di_1SrlPkB8iNXqDnZRX03bQHnQ',
                    coupon: {
                        id: 'H4H47PbG',
                        percent_off: 20,
                        duration: 'repeating',
                        duration_in_months: 6
                    },
                    start: discountStart,
                    end: discountEnd
                }
            };

            const repo = buildRepo({
                stripeAPIService: {
                    ...stripeAPIService,
                    getSubscription: sinon.stub().resolves(subscriptionWithDiscount)
                },
                StripeCustomerSubscription,
                MemberPaidSubscriptionEvent,
                MemberProductEvent,
                productRepository,
                offersAPI,
                Member,
                OfferRedemption: mockOfferRedemption
            });

            // Existing subscription
            sinon.stub(repo, 'getSubscriptionByStripeID').resolves({
                get: sinon.stub().withArgs('offer_id').returns(null)
            });

            await repo.linkSubscription({
                subscription: subscriptionWithDiscount
            }, {
                transacting: {
                    executionPromise: Promise.resolve()
                },
                context: {}
            });

            // Verify edit was called (not add) since subscription exists
            sinon.assert.notCalled(StripeCustomerSubscription.add);
            sinon.assert.calledOnce(StripeCustomerSubscription.edit);

            const editedSubscriptionData = StripeCustomerSubscription.edit.firstCall.args[0];

            assert.ok(editedSubscriptionData.discount_start instanceof Date);
            assert.ok(editedSubscriptionData.discount_end instanceof Date);
            assert.equal(editedSubscriptionData.discount_start.getTime(), discountStart * 1000);
            assert.equal(editedSubscriptionData.discount_end.getTime(), discountEnd * 1000);
        });

        it('nullifies discount_start and discount_end when discount is removed from Stripe subscription', async function () {
            const repo = buildRepo({
                stripeAPIService,
                StripeCustomerSubscription,
                MemberPaidSubscriptionEvent,
                MemberProductEvent,
                productRepository,
                Member,
                OfferRedemption: mockOfferRedemption
            });

            sinon.stub(repo, 'getSubscriptionByStripeID').resolves(null);

            const subscriptionWithNoDiscount = {
                ...subscriptionData,
                discount: null
            };

            await repo.linkSubscription({
                subscription: subscriptionWithNoDiscount
            }, {
                transacting: {
                    executionPromise: Promise.resolve()
                },
                context: {}
            });

            sinon.assert.calledOnce(StripeCustomerSubscription.add);
            const addedSubscriptionData = StripeCustomerSubscription.add.firstCall.args[0];

            assert.equal(addedSubscriptionData.discount_start, null);
            assert.equal(addedSubscriptionData.discount_end, null);
        });

        it('falls back to customer default payment method when subscription has none', async function () {
            const repo = buildRepo({
                stripeAPIService: {
                    ...stripeAPIService,
                    getSubscription: sinon.stub().resolves({
                        ...subscriptionData,
                        default_payment_method: null
                    }),
                    getCustomer: sinon.stub().resolves({
                        id: 'cus_123',
                        invoice_settings: {
                            default_payment_method: 'pm_customer_default'
                        },
                        subscriptions: {data: []}
                    }),
                    getCardPaymentMethod: sinon.stub().resolves({
                        id: 'pm_customer_default',
                        type: 'card',
                        card: {
                            last4: '8888'
                        }
                    })
                },
                StripeCustomerSubscription,
                MemberPaidSubscriptionEvent,
                MemberProductEvent,
                productRepository,
                Member,
                OfferRedemption: mockOfferRedemption
            });

            sinon.stub(repo, 'getSubscriptionByStripeID').resolves(null);

            await repo.linkSubscription({
                subscription: subscriptionData
            }, {
                transacting: {
                    executionPromise: Promise.resolve()
                },
                context: {}
            });

            sinon.assert.calledOnce(StripeCustomerSubscription.add);
            const addedData = StripeCustomerSubscription.add.firstCall.args[0];
            assert.equal(addedData.default_payment_card_last4, '8888');
        });

        it('sets card last4 to null when neither subscription nor customer has a default payment method', async function () {
            const repo = buildRepo({
                stripeAPIService: {
                    ...stripeAPIService,
                    getSubscription: sinon.stub().resolves({
                        ...subscriptionData,
                        default_payment_method: null
                    }),
                    getCustomer: sinon.stub().resolves({
                        id: 'cus_123',
                        invoice_settings: {
                            default_payment_method: null
                        },
                        subscriptions: {data: []}
                    })
                },
                StripeCustomerSubscription,
                MemberPaidSubscriptionEvent,
                MemberProductEvent,
                productRepository,
                Member,
                OfferRedemption: mockOfferRedemption
            });

            sinon.stub(repo, 'getSubscriptionByStripeID').resolves(null);

            await repo.linkSubscription({
                subscription: subscriptionData
            }, {
                transacting: {
                    executionPromise: Promise.resolve()
                },
                context: {}
            });

            sinon.assert.calledOnce(StripeCustomerSubscription.add);
            const addedData = StripeCustomerSubscription.add.firstCall.args[0];
            assert.equal(addedData.default_payment_card_last4, null);
        });

        it('uses subscription default payment method when available', async function () {
            const repo = buildRepo({
                stripeAPIService: {
                    ...stripeAPIService,
                    getSubscription: sinon.stub().resolves({
                        ...subscriptionData,
                        default_payment_method: 'pm_sub_default'
                    }),
                    getCardPaymentMethod: sinon.stub().resolves({
                        id: 'pm_sub_default',
                        type: 'card',
                        card: {
                            last4: '4242'
                        }
                    })
                },
                StripeCustomerSubscription,
                MemberPaidSubscriptionEvent,
                MemberProductEvent,
                productRepository,
                Member,
                OfferRedemption: mockOfferRedemption
            });

            sinon.stub(repo, 'getSubscriptionByStripeID').resolves(null);

            await repo.linkSubscription({
                subscription: subscriptionData
            }, {
                transacting: {
                    executionPromise: Promise.resolve()
                },
                context: {}
            });

            sinon.assert.calledOnce(StripeCustomerSubscription.add);
            const addedData = StripeCustomerSubscription.add.firstCall.args[0];
            assert.equal(addedData.default_payment_card_last4, '4242');
        });

        it('handles expanded customer object on subscription when falling back', async function () {
            const repo = buildRepo({
                stripeAPIService: {
                    ...stripeAPIService,
                    getSubscription: sinon.stub().resolves({
                        ...subscriptionData,
                        customer: {id: 'cus_123', object: 'customer'},
                        default_payment_method: null
                    }),
                    getCustomer: sinon.stub().resolves({
                        id: 'cus_123',
                        invoice_settings: {
                            default_payment_method: {id: 'pm_expanded', type: 'card', card: {last4: '1234'}}
                        },
                        subscriptions: {data: []}
                    }),
                    getCardPaymentMethod: sinon.stub().resolves({
                        id: 'pm_expanded',
                        type: 'card',
                        card: {
                            last4: '1234'
                        }
                    })
                },
                StripeCustomerSubscription,
                MemberPaidSubscriptionEvent,
                MemberProductEvent,
                productRepository,
                Member,
                OfferRedemption: mockOfferRedemption
            });

            sinon.stub(repo, 'getSubscriptionByStripeID').resolves(null);

            await repo.linkSubscription({
                subscription: {...subscriptionData, customer: {id: 'cus_123', object: 'customer'}}
            }, {
                transacting: {
                    executionPromise: Promise.resolve()
                },
                context: {}
            });

            sinon.assert.calledOnce(StripeCustomerSubscription.add);
            const addedData = StripeCustomerSubscription.add.firstCall.args[0];
            assert.equal(addedData.default_payment_card_last4, '1234');
        });

        it('handles discounts with no end date (e.g. forever discounts)', async function () {
            const discountStart = 1768940436;

            const subscriptionWithForeverDiscount = {
                ...subscriptionData,
                discount: {
                    id: 'di_forever',
                    coupon: {
                        id: 'forever_coupon',
                        percent_off: 20,
                        duration: 'forever'
                    },
                    start: discountStart,
                    end: null
                }
            };

            const repo = buildRepo({
                stripeAPIService: {
                    ...stripeAPIService,
                    getSubscription: sinon.stub().resolves(subscriptionWithForeverDiscount)
                },
                StripeCustomerSubscription,
                MemberPaidSubscriptionEvent,
                MemberProductEvent,
                productRepository,
                offersAPI,
                Member,
                OfferRedemption: mockOfferRedemption
            });

            sinon.stub(repo, 'getSubscriptionByStripeID').resolves(null);

            await repo.linkSubscription({
                subscription: subscriptionWithForeverDiscount
            }, {
                transacting: {
                    executionPromise: Promise.resolve()
                },
                context: {}
            });

            sinon.assert.calledOnce(StripeCustomerSubscription.add);
            const addedSubscriptionData = StripeCustomerSubscription.add.firstCall.args[0];

            assert.ok(addedSubscriptionData.discount_start instanceof Date);
            assert.equal(addedSubscriptionData.discount_start.getTime(), discountStart * 1000);
            assert.equal(addedSubscriptionData.discount_end, null);
        });

        it('clears offer_id when existing subscription has no active trial and no Stripe discount', async function () {
            // Subscription has an old offer_id but the Stripe discount has expired (no discount in webhook data)
            // and there is no active trial — offer_id should be cleared (not preserved)
            const repo = buildRepo({
                stripeAPIService,
                StripeCustomerSubscription,
                MemberPaidSubscriptionEvent,
                MemberProductEvent,
                productRepository,
                Member,
                OfferRedemption: mockOfferRedemption
            });

            sinon.stub(repo, 'getSubscriptionByStripeID').resolves({
                id: 'sub_db_id',
                get: sinon.stub().callsFake((key) => {
                    if (key === 'offer_id') {
                        return 'old_offer_123';
                    }
                    if (key === 'trial_end_at') {
                        return null;
                    }
                    return null;
                })
            });

            DomainEvents.subscribe(OfferRedemptionEvent, offerRedemptionNotifySpy);

            await repo.linkSubscription({
                subscription: subscriptionData // no discount, so offer_id resolves to null
            }, {
                transacting: {
                    executionPromise: Promise.resolve()
                },
                context: {}
            });

            sinon.assert.calledOnce(StripeCustomerSubscription.edit);
            const editedData = StripeCustomerSubscription.edit.firstCall.args[0];

            // offer_id should NOT have been deleted — it should be null (clearing the stale value)
            assert.ok('offer_id' in editedData, 'offer_id should be present in the update data');
            assert.equal(editedData.offer_id, null);
        });

        it('preserves offer_id when existing subscription has an active trial', async function () {
            // Trial offers don't create Stripe discounts, so offer_id can't be resolved from Stripe.
            // When the trial is active, offer_id should be preserved (not cleared).
            const futureTrialEnd = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days from now

            const repo = buildRepo({
                stripeAPIService,
                StripeCustomerSubscription,
                MemberPaidSubscriptionEvent,
                MemberProductEvent,
                productRepository,
                Member,
                OfferRedemption: mockOfferRedemption
            });

            sinon.stub(repo, 'getSubscriptionByStripeID').resolves({
                id: 'sub_db_id',
                get: sinon.stub().callsFake((key) => {
                    if (key === 'offer_id') {
                        return 'trial_offer_123';
                    }
                    return null;
                })
            });

            stripeAPIService.getSubscription.resolves({
                ...subscriptionData,
                trial_start: (Date.now() / 1000) - (7 * 24 * 60 * 60),
                trial_end: futureTrialEnd.getTime() / 1000
            });

            await repo.linkSubscription({
                subscription: subscriptionData // no discount, so offer_id resolves to null
            }, {
                transacting: {
                    executionPromise: Promise.resolve()
                },
                context: {}
            });

            sinon.assert.calledOnce(StripeCustomerSubscription.edit);
            const editedData = StripeCustomerSubscription.edit.firstCall.args[0];

            // offer_id should have been deleted from the update data (preserving the existing value)
            assert.ok(!('offer_id' in editedData), 'offer_id should be deleted from update data to preserve existing value');
        });

        it('clears offer_id when existing subscription has an expired trial', async function () {
            // Trial is over — offer_id should be allowed to clear
            const pastTrialEnd = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // 30 days ago

            const repo = buildRepo({
                stripeAPIService,
                StripeCustomerSubscription,
                MemberPaidSubscriptionEvent,
                MemberProductEvent,
                productRepository,
                Member,
                OfferRedemption: mockOfferRedemption
            });

            sinon.stub(repo, 'getSubscriptionByStripeID').resolves({
                id: 'sub_db_id',
                get: sinon.stub().callsFake((key) => {
                    if (key === 'offer_id') {
                        return 'trial_offer_123';
                    }
                    return null;
                })
            });

            stripeAPIService.getSubscription.resolves({
                ...subscriptionData,
                trial_start: (pastTrialEnd.getTime() / 1000) - (7 * 24 * 60 * 60),
                trial_end: pastTrialEnd.getTime() / 1000
            });

            await repo.linkSubscription({
                subscription: subscriptionData // no discount
            }, {
                transacting: {
                    executionPromise: Promise.resolve()
                },
                context: {}
            });

            sinon.assert.calledOnce(StripeCustomerSubscription.edit);
            const editedData = StripeCustomerSubscription.edit.firstCall.args[0];

            // offer_id should NOT have been deleted — it should be null (clearing the stale value)
            assert.ok('offer_id' in editedData, 'offer_id should be present in the update data');
            assert.equal(editedData.offer_id, null);
        });

        it('dispatches OfferRedemptionEvent when offer_id changes from one offer to another', async function () {
            // A retention offer replaces an expired signup offer (old → new)
            // The event timestamp should use the Stripe discount start time, not the subscription created_at
            const discountStartUnix = Math.floor(Date.now() / 1000) - 60; // 1 minute ago
            const subscriptionWithDiscount = {
                ...subscriptionData,
                discount: {
                    coupon: {
                        id: 'coupon_retention',
                        percent_off: 20,
                        duration: 'once'
                    },
                    start: discountStartUnix,
                    end: null
                }
            };

            const repo = buildRepo({
                stripeAPIService: {
                    ...stripeAPIService,
                    getSubscription: sinon.stub().resolves(subscriptionWithDiscount)
                },
                StripeCustomerSubscription,
                MemberPaidSubscriptionEvent,
                MemberProductEvent,
                productRepository,
                offersAPI,
                Member,
                OfferRedemption: mockOfferRedemption
            });

            sinon.stub(repo, 'getSubscriptionByStripeID').resolves({
                id: 'sub_db_id',
                get: sinon.stub().callsFake((key) => {
                    if (key === 'offer_id') {
                        return 'old_offer_123';
                    }
                    if (key === 'trial_end_at') {
                        return null;
                    }
                    return null;
                })
            });

            DomainEvents.subscribe(OfferRedemptionEvent, offerRedemptionNotifySpy);

            await repo.linkSubscription({
                id: 'member_id_123',
                subscription: subscriptionWithDiscount,
                offerId: 'new_retention_offer_456'
            }, {
                transacting: {
                    executionPromise: Promise.resolve()
                },
                context: {}
            });

            sinon.assert.called(offerRedemptionNotifySpy);
            sinon.assert.calledWith(offerRedemptionNotifySpy, sinon.match((event) => {
                return event.data.offerId === 'new_retention_offer_456';
            }));

            // Timestamp should be the discount start, not the subscription created_at
            const event = offerRedemptionNotifySpy.firstCall.args[0];

            assert.equal(event.timestamp.getTime(), discountStartUnix * 1000);
        });

        it('dispatches OfferRedemptionEvent with created_at timestamp when no Stripe discount is present', async function () {
            // Trial offers don't have Stripe discounts — timestamp falls back to created_at
            const subCreatedAt = new Date('2025-06-15T00:00:00Z');

            const repo = buildRepo({
                stripeAPIService,
                StripeCustomerSubscription: {
                    ...StripeCustomerSubscription,
                    edit: sinon.stub().resolves({
                        id: 'sub_db_id',
                        get: sinon.stub().callsFake((key) => {
                            if (key === 'created_at') {
                                return subCreatedAt;
                            }
                            return null;
                        })
                    })
                },
                MemberPaidSubscriptionEvent,
                MemberProductEvent,
                productRepository,
                Member,
                OfferRedemption: mockOfferRedemption
            });

            sinon.stub(repo, 'getSubscriptionByStripeID').resolves({
                id: 'sub_db_id',
                get: sinon.stub().callsFake((key) => {
                    if (key === 'offer_id') {
                        return null;
                    }
                    if (key === 'trial_end_at') {
                        return null;
                    }
                    return null;
                })
            });

            DomainEvents.subscribe(OfferRedemptionEvent, offerRedemptionNotifySpy);

            await repo.linkSubscription({
                id: 'member_id_123',
                subscription: subscriptionData,
                offerId: 'trial_offer_789'
            }, {
                transacting: {
                    executionPromise: Promise.resolve()
                },
                context: {}
            });

            sinon.assert.called(offerRedemptionNotifySpy);

            const event = offerRedemptionNotifySpy.firstCall.args[0];

            assert.equal(event.data.offerId, 'trial_offer_789');
            assert.equal(event.timestamp.getTime(), subCreatedAt.getTime());
        });

        it('overwrites offer_id when new offer arrives via Stripe even with an active trial', async function () {
            // A subscription has an active trial AND a new discount arrives from Stripe
            // The new offer should overwrite the existing one (the trial guard only applies when offer_id is null)
            const futureTrialEnd = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);

            const subscriptionWithDiscount = {
                ...subscriptionData,
                trial_start: (Date.now() / 1000) - (7 * 24 * 60 * 60),
                trial_end: futureTrialEnd.getTime() / 1000,
                discount: {
                    coupon: {
                        id: 'coupon_new',
                        percent_off: 15,
                        duration: 'forever'
                    },
                    start: Date.now() / 1000,
                    end: null
                }
            };

            const repo = buildRepo({
                stripeAPIService: {
                    ...stripeAPIService,
                    getSubscription: sinon.stub().resolves(subscriptionWithDiscount)
                },
                StripeCustomerSubscription,
                MemberPaidSubscriptionEvent,
                MemberProductEvent,
                productRepository,
                offersAPI,
                Member,
                OfferRedemption: mockOfferRedemption
            });

            sinon.stub(repo, 'getSubscriptionByStripeID').resolves({
                id: 'sub_db_id',
                get: sinon.stub().callsFake((key) => {
                    if (key === 'offer_id') {
                        return 'old_trial_offer';
                    }
                    if (key === 'trial_end_at') {
                        return futureTrialEnd;
                    }
                    return null;
                })
            });

            DomainEvents.subscribe(OfferRedemptionEvent, offerRedemptionNotifySpy);

            await repo.linkSubscription({
                subscription: subscriptionWithDiscount
            }, {
                transacting: {
                    executionPromise: Promise.resolve()
                },
                context: {}
            });

            sinon.assert.calledOnce(StripeCustomerSubscription.edit);
            const editedData = StripeCustomerSubscription.edit.firstCall.args[0];

            // New offer should overwrite — offer_id should be present and set to the new offer
            assert.ok('offer_id' in editedData, 'offer_id should be present in the update data');
            assert.equal(editedData.offer_id, 'offer_new'); // from offersAPI.ensureOfferForStripeCoupon stub

            // Should dispatch redemption event for the new offer
            sinon.assert.called(offerRedemptionNotifySpy);
            sinon.assert.calledWith(offerRedemptionNotifySpy, sinon.match((event) => {
                return event.data.offerId === 'offer_new';
            }));
        });

        it('does not dispatch OfferRedemptionEvent when offer_id stays the same', async function () {
            // Same offer synced again via webhook — no new event
            const subscriptionWithDiscount = {
                ...subscriptionData,
                discount: {
                    coupon: {
                        id: 'coupon_abc',
                        percent_off: 20,
                        duration: 'forever'
                    },
                    start: Date.now() / 1000,
                    end: null
                }
            };

            const repo = buildRepo({
                stripeAPIService: {
                    ...stripeAPIService,
                    getSubscription: sinon.stub().resolves(subscriptionWithDiscount)
                },
                StripeCustomerSubscription,
                MemberPaidSubscriptionEvent,
                MemberProductEvent,
                productRepository,
                offersAPI,
                Member,
                OfferRedemption: mockOfferRedemption
            });

            sinon.stub(repo, 'getSubscriptionByStripeID').resolves({
                id: 'sub_db_id',
                get: sinon.stub().callsFake((key) => {
                    if (key === 'offer_id') {
                        return 'offer_new'; // same as what ensureOfferForStripeCoupon returns
                    }
                    if (key === 'trial_end_at') {
                        return null;
                    }
                    return null;
                })
            });

            DomainEvents.subscribe(OfferRedemptionEvent, offerRedemptionNotifySpy);

            await repo.linkSubscription({
                subscription: subscriptionWithDiscount
            }, {
                transacting: {
                    executionPromise: Promise.resolve()
                },
                context: {}
            });

            sinon.assert.notCalled(offerRedemptionNotifySpy);
        });
    });

    describe('create - automation integration', function () {
        it('triggers an automation event for free signup', async function () {
            const repo = buildRepo();
            await repo.create({email: 'test@example.com', name: 'Test Member'}, {});

            sinon.assert.calledOnceWithExactly(automationsApi.trigger, {
                event: 'member_sign_up',
                memberId: 'member_id_123',
                memberEmail: 'test@example.com',
                memberStatus: 'free'
            });
        });

        describe('legacy automations', function () {
            it('creates automation run for free member signup (free welcome email)', async function () {
                const repo = buildRepo({
                    Member,
                    WelcomeEmailAutomationRun,
                    MemberStatusEvent,
                    MemberSubscribeEventModel: MemberSubscribeEvent,
                    newslettersService,
                    Automation,
                    OfferRedemption: mockOfferRedemption
                });

                await repo.create({email: 'test@example.com', name: 'Test Member'}, {});

                sinon.assert.calledOnce(WelcomeEmailAutomationRun.add);
                const runCall = WelcomeEmailAutomationRun.add.firstCall.args[0];
                assert.equal(runCall.welcome_email_automation_id, 'automation_id_free');
                assert.equal(runCall.member_id, 'member_id_123');
                assert.equal(runCall.next_welcome_email_automated_email_id, 'automated_email_id_free');
                assert.ok(runCall.ready_at);
                assert.equal(runCall.step_started_at, null);
                assert.equal(runCall.step_attempts, 0);
                assert.equal(runCall.exit_reason, null);
            });

            it('does NOT create automation run when the automations labs flag is enabled', async function () {
                labsIsSet.withArgs('automations').returns(true);

                const repo = buildRepo({
                    Member,
                    WelcomeEmailAutomationRun,
                    MemberStatusEvent,
                    MemberSubscribeEventModel: MemberSubscribeEvent,
                    newslettersService,
                    Automation,
                    OfferRedemption: mockOfferRedemption
                });

                await repo.create({email: 'test@example.com', name: 'Test Member'}, {});

                sinon.assert.calledOnceWithExactly(automationsApi.trigger, {
                    event: 'member_sign_up',
                    memberId: 'member_id_123',
                    memberEmail: 'test@example.com',
                    memberStatus: 'free'
                });
                sinon.assert.notCalled(WelcomeEmailAutomationRun.add);
                sinon.assert.notCalled(Automation.findOne);
            });

            it('does not create automation run for disallowed sources', async function () {
                const repo = buildRepo({
                    Member,
                    WelcomeEmailAutomationRun,
                    MemberStatusEvent,
                    MemberSubscribeEventModel: MemberSubscribeEvent,
                    newslettersService,
                    Automation,
                    OfferRedemption: mockOfferRedemption
                });

                const disallowedSources = [
                    {name: 'import', context: {import: true}},
                    {name: 'admin', context: {user: true}},
                    {name: 'api', context: {api_key: true}}
                ];

                for (const source of disallowedSources) {
                    WelcomeEmailAutomationRun.add.resetHistory();
                    await repo.create({email: 'test@example.com', name: 'Test Member'}, {context: source.context});
                    sinon.assert.notCalled(WelcomeEmailAutomationRun.add);
                }
            });

            it('passes transaction to automation run creation', async function () {
                const repo = buildRepo({
                    Member,
                    WelcomeEmailAutomationRun,
                    MemberStatusEvent,
                    MemberSubscribeEventModel: MemberSubscribeEvent,
                    newslettersService,
                    Automation,
                    OfferRedemption: mockOfferRedemption
                });

                await repo.create({email: 'test@example.com', name: 'Test Member'}, {});

                const runOptions = WelcomeEmailAutomationRun.add.firstCall.args[1];
                assert.ok(runOptions.transacting);
            });

            it('does NOT create automation run when welcome email is inactive', async function () {
                Automation.findOne.resolves({
                    get: sinon.stub().callsFake((key) => {
                        const data = {status: 'inactive'};
                        return data[key];
                    }),
                    related: sinon.stub().callsFake((relation) => {
                        assert.equal(relation, 'welcomeEmailAutomatedEmail');
                        return {
                            get: sinon.stub().callsFake((key) => {
                                const data = {lexical: '{"root":{}}'};
                                return data[key];
                            })
                        };
                    })
                });

                const repo = buildRepo({
                    Member,
                    WelcomeEmailAutomationRun,
                    MemberStatusEvent,
                    MemberSubscribeEventModel: MemberSubscribeEvent,
                    newslettersService,
                    Automation,
                    OfferRedemption: mockOfferRedemption
                });

                await repo.create({email: 'test@example.com', name: 'Test Member'}, {});

                sinon.assert.notCalled(WelcomeEmailAutomationRun.add);
            });

            it('does NOT create automation run when member is signing up for a paid subscription (stripeCustomer is present)', async function () {
                const repo = buildRepo({
                    Member,
                    WelcomeEmailAutomationRun,
                    MemberStatusEvent,
                    MemberSubscribeEventModel: MemberSubscribeEvent,
                    newslettersService,
                    Automation,
                    OfferRedemption: mockOfferRedemption
                });

                // Stub linkSubscription to avoid needing all the stripe-related mocks
                sinon.stub(repo, 'linkSubscription').resolves();
                sinon.stub(repo, 'upsertCustomer').resolves();

                // Create a member with a stripeCustomer (i.e., signing up for paid subscription)
                await repo.create({
                    email: 'test@example.com',
                    name: 'Test Member',
                    stripeCustomer: {
                        id: 'cus_123',
                        name: 'Test Member',
                        email: 'test@example.com',
                        subscriptions: {
                            data: [{
                                id: 'sub_123',
                                customer: 'cus_123',
                                status: 'active'
                            }]
                        }
                    }
                }, {});

                // The free welcome email should NOT be sent when stripeCustomer is present
                sinon.assert.notCalled(WelcomeEmailAutomationRun.add);
                sinon.assert.notCalled(Automation.findOne);
                sinon.assert.notCalled(Member.transaction);
            });
        });
    });

    describe('linkSubscription - automation integration', function () {
        let subscriptionData;

        afterEach(function () {
            sinon.restore();
        });

        beforeEach(function () {
            sinon.stub(MemberRepository.prototype, '_updateCurrentSubscription').resolves();
            subscriptionData = {
                id: 'sub_123',
                customer: 'cus_123',
                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
            };

            Member = {
                findOne: sinon.stub().resolves({
                    id: 'member_id_123',
                    get: sinon.stub().callsFake((key) => {
                        const data = {
                            email: 'test@example.com',
                            name: 'Test Member'
                        };
                        return data[key];
                    }),
                    related: (relation) => {
                        return {
                            query: sinon.stub().returns({
                                fetchOne: sinon.stub().resolves({})
                            }),
                            toJSON: sinon.stub().returns(relation === 'products' ? [] : {}),
                            fetch: sinon.stub().resolves({
                                toJSON: sinon.stub().returns(relation === 'products' ? [] : {}),
                                models: []
                            })
                        };
                    },
                    toJSON: sinon.stub().returns({})
                }),
                edit: sinon.stub().resolves({
                    attributes: {},
                    _previousAttributes: {}
                })
            };

            WelcomeEmailAutomationRun = {
                add: sinon.stub().resolves()
            };

            MemberPaidSubscriptionEvent = {
                add: sinon.stub().resolves()
            };

            StripeCustomerSubscription = {
                add: sinon.stub().resolves({
                    id: 'stripe_sub_id_123',
                    get: sinon.stub().callsFake((key) => {
                        const data = {
                            created_at: new Date(),
                            status: 'active',
                            cancel_at_period_end: false
                        };
                        return data[key];
                    })
                }),
                edit: sinon.stub().resolves({
                    id: 'stripe_sub_id_123',
                    get: sinon.stub().callsFake((key) => {
                        const data = {
                            created_at: new Date(),
                            status: 'active',
                            cancel_at_period_end: false
                        };
                        return data[key];
                    })
                })
            };

            MemberProductEvent = {
                add: sinon.stub().resolves({})
            };

            MemberStatusEvent = {
                add: sinon.stub().resolves()
            };

            stripeAPIService = {
                configured: true,
                getSubscription: sinon.stub().resolves(subscriptionData),
                getCustomer: sinon.stub().resolves({
                    id: 'cus_123',
                    invoice_settings: {
                        default_payment_method: null
                    },
                    subscriptions: {data: []}
                })
            };

            productRepository = {
                get: sinon.stub().resolves({
                    get: sinon.stub().returns(),
                    toJSON: sinon.stub().returns({})
                }),
                update: sinon.stub().resolves({})
            };

            Automation = {
                findOne: sinon.stub().resolves({
                    id: 'automation_id_paid',
                    get: sinon.stub().callsFake((key) => {
                        const data = {status: 'active'};
                        return data[key];
                    }),
                    related: sinon.stub().callsFake((relation) => {
                        assert.equal(relation, 'welcomeEmailAutomatedEmail');
                        return {
                            id: 'automated_email_id_paid',
                            get: sinon.stub().callsFake((key) => {
                                const data = {lexical: '{"root":{}}'};
                                return data[key];
                            })
                        };
                    })
                })
            };
        });

        afterEach(function () {
            sinon.restore();
        });

        it('triggers an automation event for paid signup', async function () {
            Member.edit.resolves({
                attributes: {status: 'paid'},
                _previousAttributes: {status: 'free'},
                get: sinon.stub().callsFake((key) => {
                    const data = {status: 'paid'};
                    return data[key];
                })
            });

            const repo = buildRepo();
            sinon.stub(repo, 'getSubscriptionByStripeID').resolves(null);

            await repo.linkSubscription({
                id: 'member_id_123',
                subscription: subscriptionData
            }, {
                transacting: {
                    executionPromise: Promise.resolve()
                },
                context: {}
            });

            sinon.assert.calledOnceWithExactly(automationsApi.trigger, {
                event: 'member_sign_up',
                memberId: 'member_id_123',
                memberEmail: 'test@example.com',
                memberStatus: 'paid'
            });
        });

        describe('legacy automations', function () {
            it('creates automation run when member status changes to paid', async function () {
                Member.edit.resolves({
                    attributes: {status: 'paid'},
                    _previousAttributes: {status: 'free'},
                    get: sinon.stub().callsFake((key) => {
                        const data = {status: 'paid'};
                        return data[key];
                    })
                });

                const repo = buildRepo({
                    Member,
                    WelcomeEmailAutomationRun,
                    MemberPaidSubscriptionEvent,
                    StripeCustomerSubscription,
                    MemberProductEvent,
                    MemberStatusEvent,
                    stripeAPIService,
                    productRepository,
                    Automation,
                    OfferRedemption: mockOfferRedemption
                });

                sinon.stub(repo, 'getSubscriptionByStripeID').resolves(null);

                await repo.linkSubscription({
                    id: 'member_id_123',
                    subscription: subscriptionData
                }, {
                    transacting: {
                        executionPromise: Promise.resolve()
                    },
                    context: {}
                });

                sinon.assert.calledOnce(WelcomeEmailAutomationRun.add);
                const runCall = WelcomeEmailAutomationRun.add.firstCall.args[0];
                assert.equal(runCall.welcome_email_automation_id, 'automation_id_paid');
                assert.equal(runCall.member_id, 'member_id_123');
                assert.equal(runCall.next_welcome_email_automated_email_id, 'automated_email_id_paid');
                assert.ok(runCall.ready_at);
                assert.equal(runCall.step_started_at, null);
                assert.equal(runCall.step_attempts, 0);
                assert.equal(runCall.exit_reason, null);
            });

            it('does NOT create automation run when the automations labs flag is enabled', async function () {
                labsIsSet.withArgs('automations').returns(true);

                Member.edit.resolves({
                    attributes: {status: 'paid'},
                    _previousAttributes: {status: 'free'},
                    get: sinon.stub().callsFake((key) => {
                        const data = {status: 'paid'};
                        return data[key];
                    })
                });

                const repo = buildRepo({
                    Member,
                    WelcomeEmailAutomationRun,
                    MemberPaidSubscriptionEvent,
                    StripeCustomerSubscription,
                    MemberProductEvent,
                    MemberStatusEvent,
                    stripeAPIService,
                    productRepository,
                    Automation,
                    OfferRedemption: mockOfferRedemption
                });

                sinon.stub(repo, 'getSubscriptionByStripeID').resolves(null);

                await repo.linkSubscription({
                    id: 'member_id_123',
                    subscription: subscriptionData
                }, {
                    transacting: {
                        executionPromise: Promise.resolve()
                    },
                    context: {}
                });

                sinon.assert.calledOnceWithExactly(automationsApi.trigger, {
                    event: 'member_sign_up',
                    memberId: 'member_id_123',
                    memberEmail: 'test@example.com',
                    memberStatus: 'paid'
                });
                sinon.assert.notCalled(WelcomeEmailAutomationRun.add);
                sinon.assert.notCalled(Automation.findOne);
            });

            it('does NOT create automation run for disallowed sources', async function () {
                Member.edit.resolves({
                    attributes: {status: 'paid'},
                    _previousAttributes: {status: 'free'},
                    get: sinon.stub().callsFake((key) => {
                        const data = {status: 'paid'};
                        return data[key];
                    })
                });

                const repo = buildRepo({
                    Member,
                    WelcomeEmailAutomationRun,
                    MemberPaidSubscriptionEvent,
                    StripeCustomerSubscription,
                    MemberProductEvent,
                    MemberStatusEvent,
                    stripeAPIService,
                    productRepository,
                    Automation,
                    OfferRedemption: mockOfferRedemption
                });

                sinon.stub(repo, 'getSubscriptionByStripeID').resolves(null);

                const disallowedSources = [
                    {name: 'import', context: {import: true}},
                    {name: 'admin', context: {user: true}},
                    {name: 'api', context: {api_key: true}}
                ];

                for (const source of disallowedSources) {
                    WelcomeEmailAutomationRun.add.resetHistory();
                    await repo.linkSubscription({
                        id: 'member_id_123',
                        subscription: subscriptionData
                    }, {
                        transacting: {
                            executionPromise: Promise.resolve()
                        },
                        context: source.context
                    });
                    sinon.assert.notCalled(WelcomeEmailAutomationRun.add);
                }
            });

            it('does NOT create automation run when paid welcome email is inactive', async function () {
                Member.edit.resolves({
                    attributes: {status: 'paid'},
                    _previousAttributes: {status: 'free'},
                    get: sinon.stub().callsFake((key) => {
                        const data = {status: 'paid'};
                        return data[key];
                    })
                });

                Automation.findOne.resolves({
                    id: 'automation_id_paid',
                    get: sinon.stub().callsFake((key) => {
                        const data = {status: 'inactive'};
                        return data[key];
                    }),
                    related: sinon.stub().callsFake((relation) => {
                        assert.equal(relation, 'welcomeEmailAutomatedEmail');
                        return {
                            id: 'automated_email_id_paid',
                            get: sinon.stub().callsFake((key) => {
                                const data = {lexical: '{"root":{}}'};
                                return data[key];
                            })
                        };
                    })
                });

                const repo = buildRepo({
                    Member,
                    WelcomeEmailAutomationRun,
                    MemberPaidSubscriptionEvent,
                    StripeCustomerSubscription,
                    MemberProductEvent,
                    MemberStatusEvent,
                    stripeAPIService,
                    productRepository,
                    Automation,
                    OfferRedemption: mockOfferRedemption
                });

                sinon.stub(repo, 'getSubscriptionByStripeID').resolves(null);

                await repo.linkSubscription({
                    id: 'member_id_123',
                    subscription: subscriptionData
                }, {
                    transacting: {
                        executionPromise: Promise.resolve()
                    },
                    context: {}
                });

                sinon.assert.notCalled(WelcomeEmailAutomationRun.add);
            });

            it('does NOT create automation run when previous status was "gift" (already received paid welcome at redemption)', async function () {
                Member.edit.resolves({
                    attributes: {status: 'paid'},
                    _previousAttributes: {status: 'gift'},
                    get: sinon.stub().callsFake((key) => {
                        const data = {status: 'paid'};
                        return data[key];
                    })
                });

                const repo = buildRepo({
                    Member,
                    WelcomeEmailAutomationRun,
                    MemberPaidSubscriptionEvent,
                    StripeCustomerSubscription,
                    MemberProductEvent,
                    MemberStatusEvent,
                    stripeAPIService,
                    productRepository,
                    Automation,
                    OfferRedemption: mockOfferRedemption
                });

                sinon.stub(repo, 'getSubscriptionByStripeID').resolves(null);

                await repo.linkSubscription({
                    id: 'member_id_123',
                    subscription: subscriptionData
                }, {
                    transacting: {
                        executionPromise: Promise.resolve()
                    },
                    context: {}
                });

                sinon.assert.notCalled(WelcomeEmailAutomationRun.add);
            });
        });
    });

    describe('create - member status', function () {
        let memberAdd;

        beforeEach(function () {
            memberAdd = sinon.stub().resolves({
                id: 'member_id_status',
                get: sinon.stub().callsFake((key) => {
                    const data = {
                        email: 'test@example.com',
                        status: 'free',
                        created_at: new Date()
                    };
                    return data[key];
                }),
                related: sinon.stub().returns({models: []})
            });

            Member = {
                transaction: sinon.stub().callsFake((callback) => {
                    return callback({executionPromise: Promise.resolve()});
                }),
                add: memberAdd
            };

            MemberStatusEvent = {add: sinon.stub().resolves()};
            MemberSubscribeEvent = {add: sinon.stub().resolves()};

            newslettersService = {
                getDefaultNewsletters: sinon.stub().resolves([]),
                getAll: sinon.stub().resolves([])
            };

            Automation = {
                findOne: sinon.stub().resolves(null)
            };

            productRepository = {
                get: sinon.stub().resolves({
                    id: 'tier_1',
                    get: sinon.stub().withArgs('active').returns(true)
                })
            };
        });

        it('defaults status to "free" when no status and no products are provided', async function () {
            await buildRepo().create({email: 'test@example.com', name: 'Test Member'}, {});

            sinon.assert.calledOnce(memberAdd);
            assert.equal(memberAdd.firstCall.args[0].status, 'free');
        });

        it('defaults status to "comped" when no status is provided but a product is', async function () {
            await buildRepo().create({
                email: 'test@example.com',
                name: 'Test Member',
                products: [{id: 'tier_1'}]
            }, {});

            sinon.assert.calledOnce(memberAdd);
            assert.equal(memberAdd.firstCall.args[0].status, 'comped');
        });

        it('respects the status passed in data instead of overriding it', async function () {
            await buildRepo().create({
                email: 'test@example.com',
                name: 'Test Member',
                status: 'gift'
            }, {});

            sinon.assert.calledOnce(memberAdd);
            assert.equal(memberAdd.firstCall.args[0].status, 'gift');
        });

        it('respects the status passed in data even when a product is passed', async function () {
            await buildRepo().create({
                email: 'test@example.com',
                name: 'Test Member',
                status: 'gift',
                products: [{id: 'tier_1'}]
            }, {});

            sinon.assert.calledOnce(memberAdd);
            assert.equal(memberAdd.firstCall.args[0].status, 'gift');
        });

        it('rejects invalid status in create', async function () {
            const repo = buildRepo();

            try {
                await repo.create({
                    email: 'test@example.com',
                    name: 'Test Member',
                    status: 'not-a-real-status'
                }, {});

                assert.fail('Expected create to reject invalid status');
            } catch (err) {
                assert.equal(err instanceof errors.ValidationError, true);
                assert.equal(err.property, 'status');
                assert.equal(err.message, 'Invalid member status, must be one of free, paid, comped, gift');
            }

            sinon.assert.notCalled(Member.add);
            sinon.assert.notCalled(Member.transaction);
        });

        it('allows gift members to be created on an archived tier (gift was valid at purchase time)', async function () {
            productRepository.get = sinon.stub().resolves({
                id: 'tier_1',
                get: sinon.stub().withArgs('active').returns(false)
            });

            await buildRepo().create({
                email: 'test@example.com',
                name: 'Test Member',
                status: 'gift',
                products: [{id: 'tier_1'}]
            }, {});

            sinon.assert.calledOnce(memberAdd);
            assert.equal(memberAdd.firstCall.args[0].status, 'gift');
        });

        it('rejects non-gift members being created on an archived tier', async function () {
            productRepository.get = sinon.stub().resolves({
                id: 'tier_1',
                get: sinon.stub().withArgs('active').returns(false)
            });

            try {
                await buildRepo().create({
                    email: 'test@example.com',
                    name: 'Test Member',
                    products: [{id: 'tier_1'}]
                }, {});

                assert.fail('Expected create to reject archived tier for non-gift member');
            } catch (err) {
                assert.equal(err instanceof errors.BadRequestError, true);
                assert.equal(err.message, 'Cannot use archived Tiers');
            }

            sinon.assert.notCalled(memberAdd);
        });
    });

    describe('update - member status', function () {
        let memberEdit;
        let existingProducts;
        let existingSubscriptions;
        let initialMember;

        beforeEach(function () {
            memberEdit = sinon.stub().callsFake((data) => {
                return {
                    id: 'member_id_status',
                    updated_at: new Date(),
                    attributes: {
                        email: 'test@example.com',
                        status: data.status
                    },
                    _previousAttributes: {
                        email: 'test@example.com',
                        status: data.status
                    },
                    _changed: {},
                    get: sinon.stub().callsFake((key) => {
                        const memberData = {
                            email: 'test@example.com',
                            status: data.status
                        };
                        return memberData[key];
                    }),
                    related: sinon.stub().withArgs('stripeCustomers').returns({
                        fetch: sinon.stub().resolves(),
                        models: []
                    })
                };
            });

            existingProducts = [];
            existingSubscriptions = [];

            initialMember = {
                get: sinon.stub().withArgs('email').returns('test@example.com'),
                related: sinon.stub().callsFake((relation) => {
                    if (relation === 'products') {
                        return {models: existingProducts};
                    }

                    if (relation === 'stripeSubscriptions') {
                        return {models: existingSubscriptions};
                    }

                    if (relation === 'newsletters') {
                        return {models: []};
                    }

                    return {models: []};
                }),
                load: sinon.stub().resolves()
            };

            Member = {
                edit: memberEdit,
                findOne: sinon.stub().resolves(initialMember)
            };

            MemberProductEvent = {add: sinon.stub().resolves()};
            MemberStatusEvent = {add: sinon.stub().resolves()};
            MemberSubscribeEvent = {add: sinon.stub().resolves()};

            newslettersService = {
                getDefaultNewsletters: sinon.stub().resolves([]),
                getAll: sinon.stub().resolves([])
            };

            Automation = {
                findOne: sinon.stub().resolves(null)
            };

            productRepository = {
                get: sinon.stub().resolves({
                    id: 'tier_1',
                    get: sinon.stub().withArgs('active').returns(true)
                })
            };

            stripeAPIService = {
                configured: false
            };
        });

        it('rejects invalid status in update', async function () {
            const repo = buildRepo();

            try {
                await repo.update({
                    status: 'not-a-real-status'
                }, {
                    id: 'member_id_123'
                });

                assert.fail('Expected update to reject invalid status');
            } catch (err) {
                assert.equal(err instanceof errors.ValidationError, true);
                assert.equal(err.property, 'status');
                assert.equal(err.message, 'Invalid member status, must be one of free, paid, comped, gift');
            }

            sinon.assert.notCalled(Member.edit);
        });

        it('defaults status to "comped" when adding a product without an explicit status', async function () {
            const repo = buildRepo();
            stripeAPIService.configured = true;

            await repo.update({
                products: [{id: 'tier_1'}]
            }, {
                id: 'member_id_123'
            });

            sinon.assert.calledOnce(memberEdit);
            assert.equal(memberEdit.firstCall.args[0].status, 'comped');
        });

        it('respects an explicit status when adding a product', async function () {
            const repo = buildRepo();
            stripeAPIService.configured = true;

            await repo.update({
                status: 'gift',
                products: [{id: 'tier_1'}]
            }, {
                id: 'member_id_123'
            });

            sinon.assert.calledOnce(memberEdit);
            assert.equal(memberEdit.firstCall.args[0].status, 'gift');
        });

        it('defaults status to "free" when removing all products without active subscriptions', async function () {
            const repo = buildRepo();
            stripeAPIService.configured = true;
            existingProducts = [{id: 'tier_1'}];

            await repo.update({
                products: []
            }, {
                id: 'member_id_123'
            });

            sinon.assert.calledOnce(memberEdit);
            assert.equal(memberEdit.firstCall.args[0].status, 'free');
        });

        it('allows gift redemption on an archived tier (gift was valid at purchase time)', async function () {
            productRepository.get = sinon.stub().resolves({
                id: 'tier_1',
                get: sinon.stub().withArgs('active').returns(false)
            });
            stripeAPIService.configured = true;

            await buildRepo().update({
                status: 'gift',
                products: [{id: 'tier_1'}]
            }, {
                id: 'member_id_123'
            });

            sinon.assert.calledOnce(memberEdit);
            assert.equal(memberEdit.firstCall.args[0].status, 'gift');
        });

        it('rejects adding an archived tier to a non-gift member', async function () {
            productRepository.get = sinon.stub().resolves({
                id: 'tier_1',
                get: sinon.stub().withArgs('active').returns(false)
            });
            stripeAPIService.configured = true;

            try {
                await buildRepo().update({
                    products: [{id: 'tier_1'}]
                }, {
                    id: 'member_id_123'
                });

                assert.fail('Expected update to reject archived tier for non-gift member');
            } catch (err) {
                assert.equal(err instanceof errors.BadRequestError, true);
                assert.equal(err.message, 'Cannot use archived Tiers');
            }

            sinon.assert.notCalled(memberEdit);
        });
    });

    describe('destroy', function () {
        it('passes require: false to tolerate concurrent deletes', async function () {
            Member = {
                findOne: sinon.stub().resolves({
                    id: 'member_id_123',
                    related: () => ({
                        fetch: sinon.stub().resolves({models: []})
                    })
                }),
                destroy: sinon.stub().resolves()
            };

            const repo = buildRepo();

            await repo.destroy({id: 'member_id_123'}, {});

            sinon.assert.calledOnce(Member.destroy);
            const destroyOptions = Member.destroy.firstCall.args[1];
            assert.equal(destroyOptions.require, false);
        });

        it('returns early when member does not exist', async function () {
            Member = {
                findOne: sinon.stub().resolves(null),
                destroy: sinon.stub()
            };

            const repo = buildRepo();

            await repo.destroy({id: 'nonexistent'}, {});

            sinon.assert.notCalled(Member.destroy);
        });
    });
});
