diff --git a/server/graphql/v2/mutation/VendorMutations.ts b/server/graphql/v2/mutation/VendorMutations.ts index 63ca0bfa66d..413e43de0ed 100644 --- a/server/graphql/v2/mutation/VendorMutations.ts +++ b/server/graphql/v2/mutation/VendorMutations.ts @@ -8,7 +8,8 @@ import { v4 as uuid } from 'uuid'; import ActivityTypes from '../../../constants/activities'; import { CollectiveType } from '../../../constants/collectives'; import { getDiffBetweenInstances } from '../../../lib/data'; -import models, { Activity, LegalDocument } from '../../../models'; +import models, { Activity, LegalDocument, Op } from '../../../models'; +import { ExpenseStatus } from '../../../models/Expense'; import { checkRemoteUserCanUseHost } from '../../common/scope-check'; import { BadRequest, NotFound, Unauthorized, ValidationFailed } from '../../errors'; import { idDecode, IDENTIFIER_TYPES } from '../identifiers'; @@ -193,13 +194,14 @@ const vendorMutations = { } if (args.vendor.payoutMethod) { + let payoutMethod; const existingPayoutMethods = await vendor.getPayoutMethods({ where: { isSaved: true } }); if (!args.vendor.payoutMethod.id) { if (!isEmpty(existingPayoutMethods)) { existingPayoutMethods.map(pm => pm.update({ isSaved: false })); } - await models.PayoutMethod.create({ + payoutMethod = await models.PayoutMethod.create({ ...pick(args.vendor.payoutMethod, ['name', 'data', 'type']), CollectiveId: vendor.id, CreatedByUserId: req.remoteUser.id, @@ -207,10 +209,24 @@ const vendorMutations = { }); } else { const payoutMethodId = idDecode(args.vendor.payoutMethod.id, IDENTIFIER_TYPES.PAYOUT_METHOD); + payoutMethod = await models.PayoutMethod.findByPk(payoutMethodId); await Promise.all( existingPayoutMethods.filter(pm => pm.id !== payoutMethodId).map(pm => pm.update({ isSaved: false })), ); } + + // Since vendors can only have a single payout method, we update all expenses to use the new one + await models.Expense.update( + { PayoutMethodId: payoutMethod.id }, + { + where: { + FromCollectiveId: vendor.id, + status: { + [Op.in]: [ExpenseStatus.APPROVED, ExpenseStatus.DRAFT, ExpenseStatus.ERROR, ExpenseStatus.PENDING], + }, + }, + }, + ); } return vendor; }, diff --git a/test/server/graphql/v2/mutation/VendorMutations.test.ts b/test/server/graphql/v2/mutation/VendorMutations.test.ts index 859ffb09bf5..0279727cb4c 100644 --- a/test/server/graphql/v2/mutation/VendorMutations.test.ts +++ b/test/server/graphql/v2/mutation/VendorMutations.test.ts @@ -4,7 +4,14 @@ import gql from 'fake-tag'; import { CollectiveType } from '../../../../../server/constants/collectives'; import models from '../../../../../server/models'; import { LEGAL_DOCUMENT_TYPE } from '../../../../../server/models/LegalDocument'; -import { fakeCollective, fakeHost, fakeTransaction, fakeUser } from '../../../../test-helpers/fake-data'; +import { + fakeCollective, + fakeExpense, + fakeHost, + fakePayoutMethod, + fakeTransaction, + fakeUser, +} from '../../../../test-helpers/fake-data'; import { getMockFileUpload, graphqlQueryV2 } from '../../../../utils'; describe('server/graphql/v2/mutation/VendorMutations', () => { @@ -228,6 +235,55 @@ describe('server/graphql/v2/mutation/VendorMutations', () => { const location = await vendor.getLocation(); expect(location.address).to.equal('Zorg Avenue, 1'); }); + + it('invalidates existing Payout Method and updates existing Expenses', async () => { + const vendor = await fakeCollective({ + type: CollectiveType.VENDOR, + ParentCollectiveId: host.id, + data: { vendorInfo: vendorData.vendorInfo }, + }); + const existingPayoutMethod = await fakePayoutMethod({ CollectiveId: vendor.id, isSaved: true }); + const existingExpense = await fakeExpense({ + FromCollectiveId: vendor.id, + PayoutMethodId: existingPayoutMethod.id, + status: 'PENDING', + }); + const existingPaidExpense = await fakeExpense({ + FromCollectiveId: vendor.id, + PayoutMethodId: existingPayoutMethod.id, + status: 'PAID', + }); + const newVendorData = { + legacyId: vendor.id, + payoutMethod: { + type: 'PAYPAL', + name: 'Zorg Inc', + data: { email: 'zorg@zorg.com' }, + isSaved: true, + }, + }; + const result = await graphqlQueryV2( + editVendorMutation, + { + vendor: newVendorData, + }, + hostAdminUser, + ); + result.errors && console.error(result.errors); + expect(result.errors).to.not.exist; + + await existingPayoutMethod.reload(); + expect(existingPayoutMethod.isSaved).to.be.false; + + await existingExpense.reload(); + expect(existingExpense.PayoutMethodId).to.not.equal(existingPayoutMethod.id); + + await existingPaidExpense.reload(); + expect(existingPaidExpense.PayoutMethodId).to.equal(existingPayoutMethod.id); + + await models.PayoutMethod.destroy({ where: { CollectiveId: vendor.id }, force: true }); + await models.Expense.destroy({ where: { FromCollectiveId: vendor.id }, force: true }); + }); }); describe('deleteVendor', () => {