Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent removing payment method from processing orders and return payment method info even if deleted #10073

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions server/graphql/loaders/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,15 @@ export const loaders = req => {
}).then(results => sortResults(CollectiveIds, results, 'CollectiveId', [])),
);

context.loaders.PaymentMethod.byIdIncludeDeleted = new DataLoader(keys => {
return models.PaymentMethod.findAll({
where: {
id: keys,
},
paranoid: false,
}).then(results => sortResultsSimple(keys, results, pm => pm.id, []));
});
Comment on lines +713 to +720
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick reminder: this is only safe because we use the primary key ID in this query.
Hypothetically, we should not implement stuff like byCollectiveIdIncludedDeleted because our custom indexes exclude deleted records.


/** *** Order *****/
// Order - findByMembership
context.loaders.Order.findByMembership = new DataLoader(combinedKeys =>
Expand Down
14 changes: 12 additions & 2 deletions server/graphql/v1/mutations/paymentMethods.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,22 @@ export async function removePaymentMethod(paymentMethodId, req) {

// Block the removal if the payment method has subscriptions linked
const subscriptions = await paymentMethod.getOrders({
where: { status: { [Op.or]: [ORDER_STATUS.ACTIVE, ORDER_STATUS.ERROR] } },
where: {
[Op.or]: [
{
status: {
[Op.or]: [ORDER_STATUS.ACTIVE, ORDER_STATUS.ERROR],
},
['$Subscription.id$']: { [Op.ne]: null },
},
{ status: ORDER_STATUS.PROCESSING },
],
},
include: [
{
model: models.Subscription,
where: { isActive: true },
required: true,
required: false,
hdiniz marked this conversation as resolved.
Show resolved Hide resolved
},
],
});
Expand Down
2 changes: 1 addition & 1 deletion server/graphql/v2/object/Order.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ export const GraphQLOrder = new GraphQLObjectType({
type: GraphQLPaymentMethod,
resolve(order, _, req) {
if (order.PaymentMethodId) {
return req.loaders.PaymentMethod.byId.load(order.PaymentMethodId);
return req.loaders.PaymentMethod.byIdIncludeDeleted.load(order.PaymentMethodId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to expose deleted assets? What is the use case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When looking at past contributions / transactions. If the user removes the payment method, we still want to show info about it in the contribution list/drawers.

}
},
},
Expand Down
163 changes: 162 additions & 1 deletion test/server/graphql/v1/paymentMethods.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@ import { expect } from 'chai';
import gqlV1 from 'fake-tag';
import { stub } from 'sinon';

import OrderStatuses from '../../../../server/constants/order-status';
import roles from '../../../../server/constants/roles';
import models from '../../../../server/models';
import paypalAdaptive from '../../../../server/paymentProviders/paypal/adaptiveGateway';
import paypalMock from '../../../mocks/paypal';
import { fakeOrganization, fakeTransaction } from '../../../test-helpers/fake-data';
import {
fakeCollective,
fakeOrder,
fakeOrganization,
fakePaymentMethod,
fakeTransaction,
fakeUser,
} from '../../../test-helpers/fake-data';
import * as utils from '../../../utils';

let host, admin, user, collective, paypalPaymentMethod;
Expand Down Expand Up @@ -101,6 +109,159 @@ describe('server/graphql/v1/paymentMethods', () => {
}),
);

describe('removePaymentMethod', () => {
const removePaymentMethodQuery = gqlV1`
mutation RemovePaymentMethod($id: Int!) {
removePaymentMethod(id: $id) {
id
}
}
`;

it('removes payment method', async () => {
const user = await fakeUser();
const collective = await fakeCollective({ admin: user });
const pm = await fakePaymentMethod({
CollectiveId: collective.id,
});

await fakeOrder({
PaymentMethodId: pm.id,
});

const res = await utils.graphqlQuery(
removePaymentMethodQuery,
{
id: pm.id,
},
user,
);

expect(res.errors).to.not.exist;
await pm.reload({
paranoid: false,
});
expect(pm.isSoftDeleted()).to.be.true;
});

it('removes payment method from inactive subscription', async () => {
const user = await fakeUser();
const collective = await fakeCollective({ admin: user });
const pm = await fakePaymentMethod({
CollectiveId: collective.id,
});

const order = await fakeOrder(
{
PaymentMethodId: pm.id,
},
{ withSubscription: true },
);

await order.Subscription.update({ isActive: false });

const res = await utils.graphqlQuery(
removePaymentMethodQuery,
{
id: pm.id,
},
user,
);

expect(res.errors).to.not.exist;
await pm.reload({
paranoid: false,
});
expect(pm.isSoftDeleted()).to.be.true;
});

it('cant remove if is an active subscription payment method', async () => {
const user = await fakeUser();
const collective = await fakeCollective({ admin: user });
const pm = await fakePaymentMethod({
CollectiveId: collective.id,
});

await fakeOrder(
{
PaymentMethodId: pm.id,
},
{ withSubscription: true },
);

const res = await utils.graphqlQuery(
removePaymentMethodQuery,
{
id: pm.id,
},
user,
);

expect(res.errors).to.exist;
await pm.reload({
paranoid: false,
});
expect(pm.isSoftDeleted()).to.be.false;
});

it('cant remove if is an active subscription payment method with order processing', async () => {
const user = await fakeUser();
const collective = await fakeCollective({ admin: user });
const pm = await fakePaymentMethod({
CollectiveId: collective.id,
});

await fakeOrder(
{
PaymentMethodId: pm.id,
status: OrderStatuses.PROCESSING,
},
{ withSubscription: true },
);

const res = await utils.graphqlQuery(
removePaymentMethodQuery,
{
id: pm.id,
},
user,
);

expect(res.errors).to.exist;
await pm.reload({
paranoid: false,
});
expect(pm.isSoftDeleted()).to.be.false;
});

it('cant remove if is an payment method with order processing', async () => {
const user = await fakeUser();
const collective = await fakeCollective({ admin: user });
const pm = await fakePaymentMethod({
CollectiveId: collective.id,
});

await fakeOrder({
PaymentMethodId: pm.id,
status: OrderStatuses.PROCESSING,
});

const res = await utils.graphqlQuery(
removePaymentMethodQuery,
{
id: pm.id,
},
user,
);

expect(res.errors).to.exist;
await pm.reload({
paranoid: false,
});
expect(pm.isSoftDeleted()).to.be.false;
});
});

describe('oauth flow', () => {
// not implemented
});
Expand Down
Loading