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

Check balance before issuing a refund #10468

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions server/graphql/common/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,14 @@ export async function refundTransaction(
}
}

// Check if the hosted collective has enough funds to refund the transaction
if (collective && collective.HostCollectiveId && transaction.type === 'CREDIT') {
const balanceInHostCurrency = await collective.getBalance({ currency: transaction.hostCurrency });
if (balanceInHostCurrency < transaction.amountInHostCurrency) {
throw new Forbidden('Not enough funds to refund this transaction');
}
}

// 2. Refund via payment method
// 3. Create new transactions with the refund value in our database
const result = await refundTransactionPayment(transaction, req.remoteUser, args.message);
Expand Down
2 changes: 1 addition & 1 deletion server/graphql/v2/mutation/TransactionMutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const transactionMutations = {
description: 'Reference of the transaction to refund',
},
},
async resolve(_: void, args, req: express.Request): Promise<typeof GraphQLTransaction> {
async resolve(_: void, args, req: express.Request): Promise<Transaction> {
checkRemoteUserCanUseTransactions(req);
const transaction = await fetchTransactionWithReference(args.transaction, { throwIfMissing: true });
return refundTransaction(transaction, req);
Expand Down
31 changes: 28 additions & 3 deletions test/server/graphql/v2/mutation/TransactionMutations.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { expect } from 'chai';
import gql from 'fake-tag';
import { pick } from 'lodash';
import nock from 'nock';
import { createSandbox } from 'sinon';
import Stripe from 'stripe';

import { SupportedCurrency } from '../../../../../server/constants/currencies';
import MemberRoles from '../../../../../server/constants/roles';
import { TransactionKind } from '../../../../../server/constants/transaction-kind';
import { TransactionTypes } from '../../../../../server/constants/transactions';
import * as TransactionMutationHelpers from '../../../../../server/graphql/common/transactions';
import emailLib from '../../../../../server/lib/email';
import { calcFee, executeOrder } from '../../../../../server/lib/payments';
import stripe, { convertFromStripeAmount, convertToStripeAmount, extractFees } from '../../../../../server/lib/stripe';
import models from '../../../../../server/models';
import stripeMocks from '../../../../mocks/stripe';
import { fakeCollective, fakeOrder, fakeUser, randStr } from '../../../../test-helpers/fake-data';
import { fakeCollective, fakeOrder, fakeTransaction, fakeUser, randStr } from '../../../../test-helpers/fake-data';
import { graphqlQueryV2 } from '../../../../utils';
import * as utils from '../../../../utils';

Expand Down Expand Up @@ -91,10 +93,12 @@ describe('server/graphql/v2/mutation/TransactionMutations', () => {
await hostAdminUser.populateRoles();
order1 = await fakeOrder({
CollectiveId: collective.id,
totalAmount: stripeMocks.balance.amount,
});
order1 = await order1.setPaymentMethod({ token: STRIPE_TOKEN });
order2 = await fakeOrder({
CollectiveId: collective.id,
totalAmount: stripeMocks.balance.amount,
});
order2 = await order2.setPaymentMethod({ token: STRIPE_TOKEN });
await models.ConnectedAccount.create({
Expand Down Expand Up @@ -173,6 +177,16 @@ describe('server/graphql/v2/mutation/TransactionMutations', () => {
expect(refund1.CreatedByUserId).to.equal(hostAdminUser.id);
expect(refund2.CreatedByUserId).to.equal(hostAdminUser.id);
});

it('error if the collective does not have enough funds', async () => {
const result = await graphqlQueryV2(
refundTransactionMutation,
{ transaction: { legacyId: transaction1.id } },
hostAdminUser,
);
const [{ message }] = result.errors;
expect(message).to.equal('Not enough funds to refund this transaction');
});
});

describe('rejectTransaction', () => {
Expand Down Expand Up @@ -210,6 +224,11 @@ describe('server/graphql/v2/mutation/TransactionMutations', () => {
});

it('rejects the transaction', async () => {
// Add funds to the collective
await fakeTransaction({
...pick(transaction1, ['CollectiveId', 'HostCollectiveId', 'amount', 'amountInHostCurrency', 'currency']),
kind: TransactionKind.ADDED_FUNDS,
});
const message = 'We do not want your contribution';
const result = await graphqlQueryV2(
rejectTransactionMutation,
Expand Down Expand Up @@ -359,6 +378,12 @@ describe('refundTransaction legacy tests', () => {
data: { charge, balanceTransaction },
};
const transaction = await models.Transaction.createFromContributionPayload(transactionPayload);
await fakeTransaction({
...pick(transaction, ['CollectiveId', 'HostCollectiveId']),
amount: 100000,
amountInHostCurrency: 100000,
kind: TransactionKind.ADDED_FUNDS,
});
return { user, host, collective, tier, paymentMethod, order, transaction };
}

Expand Down Expand Up @@ -407,7 +432,7 @@ describe('refundTransaction legacy tests', () => {
const { user, collective, host, transaction } = await setupTestObjects();

// Balance pre-refund
expect(await collective.getBalance()).to.eq(400000);
expect(await collective.getBalance()).to.eq(500000);

// When the above transaction is refunded
const result = await graphqlQueryV2(
Expand All @@ -432,7 +457,7 @@ describe('refundTransaction legacy tests', () => {
await snapshotTransactionsForRefund(allTransactions);

// Collective balance should go back to 0
expect(await collective.getBalance()).to.eq(0);
expect(await collective.getBalance()).to.eq(100000);

// And two new transactions should be created in the
// database. This only makes sense in an empty database. For
Expand Down
Loading