-
-
Notifications
You must be signed in to change notification settings - Fork 262
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4346 from opencollective/fix/transactions-followup-2
Transactions Page Follow-Up
- Loading branch information
Showing
10 changed files
with
251 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { TransactionTypes } from '../../constants/transactions'; | ||
|
||
const isRoot = async (req): Promise<boolean> => { | ||
if (!req.remoteUser) { | ||
return false; | ||
} | ||
|
||
return req.remoteUser.isRoot(); | ||
}; | ||
|
||
const isPayerCollectiveAdmin = async (req, transaction): Promise<boolean> => { | ||
if (!req.remoteUser) { | ||
return false; | ||
} | ||
|
||
const collectiveId = transaction.type === 'DEBIT' ? transaction.CollectiveId : transaction.FromCollectiveId; | ||
|
||
if (req.remoteUser.isAdmin(collectiveId)) { | ||
return true; | ||
} else { | ||
const collective = await req.loaders.Collective.byId.load(collectiveId); | ||
return req.remoteUser.isAdmin(collective.ParentCollectiveId); | ||
} | ||
}; | ||
|
||
const isPayeeHostAdmin = async (req, transaction): Promise<boolean> => { | ||
if (!req.remoteUser) { | ||
return false; | ||
} | ||
const collective = await req.loaders.Collective.byId.load( | ||
transaction.type === 'CREDIT' ? transaction.CollectiveId : transaction.FromCollectiveId, | ||
); | ||
return req.remoteUser.isAdmin(collective.HostCollectiveId); | ||
}; | ||
|
||
/** | ||
* Returns true if the transaction meets at least one condition. | ||
* Always returns false for unauthenticated requests. | ||
*/ | ||
const remoteUserMeetsOneCondition = async (req, transaction, conditions): Promise<boolean> => { | ||
if (!req.remoteUser) { | ||
return false; | ||
} | ||
|
||
for (const condition of conditions) { | ||
if (await condition(req, transaction)) { | ||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
}; | ||
|
||
/** Checks if the user can see transaction's attachments (items URLs, attached files) */ | ||
export const canRefund = async (transaction, _, req): Promise<boolean> => { | ||
if (transaction.type !== TransactionTypes.CREDIT || transaction.OrderId === null) { | ||
return false; | ||
} | ||
return remoteUserMeetsOneCondition(req, transaction, [isRoot, isPayeeHostAdmin]); | ||
}; | ||
|
||
export const canDownloadInvoice = async (transaction, _, req): Promise<boolean> => { | ||
return remoteUserMeetsOneCondition(req, transaction, [isPayerCollectiveAdmin, isPayeeHostAdmin]); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { GraphQLInputObjectType, GraphQLInt, GraphQLString } from 'graphql'; | ||
|
||
import models from '../../../models'; | ||
import { NotFound } from '../../errors'; | ||
import { idDecode, IDENTIFIER_TYPES } from '../identifiers'; | ||
|
||
const TransactionReferenceInput = new GraphQLInputObjectType({ | ||
name: 'TransactionReferenceInput', | ||
fields: { | ||
id: { | ||
type: GraphQLString, | ||
description: 'The public id identifying the transaction (ie: dgm9bnk8-0437xqry-ejpvzeol-jdayw5re)', | ||
}, | ||
legacyId: { | ||
type: GraphQLInt, | ||
description: 'The internal id of the transaction (ie: 580)', | ||
}, | ||
}, | ||
}); | ||
|
||
const getDatabaseIdFromTransactionReference = (input: object): number => { | ||
if (input['id']) { | ||
return idDecode(input['id'], IDENTIFIER_TYPES.TRANSACTION); | ||
} else if (input['legacyId']) { | ||
return input['legacyId']; | ||
} else { | ||
return null; | ||
} | ||
}; | ||
|
||
/** | ||
* Retrieve an expense from an `ExpenseReferenceInput` | ||
*/ | ||
const fetchTransactionWithReference = async ( | ||
input: object, | ||
{ loaders = null, throwIfMissing = false } = {}, | ||
): Promise<any> => { | ||
const dbId = getDatabaseIdFromTransactionReference(input); | ||
let transaction = null; | ||
if (dbId) { | ||
transaction = await (loaders ? loaders.Transaction.byId.load(dbId) : models.Transaction.findByPk(dbId)); | ||
} | ||
|
||
if (!transaction && throwIfMissing) { | ||
throw new NotFound(); | ||
} | ||
|
||
return transaction; | ||
}; | ||
|
||
export { TransactionReferenceInput, fetchTransactionWithReference, getDatabaseIdFromTransactionReference }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { GraphQLNonNull } from 'graphql'; | ||
|
||
import { Unauthorized } from '../../errors'; | ||
import { refundTransaction as legacyRefundTransaction } from '../../v1/mutations/orders'; | ||
import { fetchTransactionWithReference, TransactionReferenceInput } from '../input/TransactionReferenceInput'; | ||
import { Transaction } from '../interface/Transaction'; | ||
|
||
const transactionMutations = { | ||
refundTransaction: { | ||
type: new GraphQLNonNull(Transaction), | ||
description: 'Refunds transaction', | ||
args: { | ||
transaction: { | ||
type: new GraphQLNonNull(TransactionReferenceInput), | ||
description: 'Reference of the transaction to refund', | ||
}, | ||
}, | ||
async resolve(_, args, req): Promise<typeof Transaction> { | ||
if (!req.remoteUser) { | ||
throw new Unauthorized(); | ||
} | ||
const transaction = await fetchTransactionWithReference(args.transaction); | ||
return legacyRefundTransaction(undefined, { id: transaction.id }, req); | ||
}, | ||
}, | ||
}; | ||
|
||
export default transactionMutations; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { expect } from 'chai'; | ||
|
||
import { roles } from '../../../../server/constants'; | ||
import { canDownloadInvoice, canRefund } from '../../../../server/graphql/common/transactions'; | ||
import { fakeCollective, fakeOrder, fakeTransaction, fakeUser } from '../../../test-helpers/fake-data'; | ||
import { makeRequest } from '../../../utils'; | ||
|
||
describe('server/graphql/common/transactions', () => { | ||
let collective, collectiveAdmin, hostAdmin, contributor, randomUser, transaction; | ||
let publicReq, randomUserReq, collectiveAdminReq, hostAdminReq, rootAdminReq, contributorReq; | ||
|
||
before(async () => { | ||
randomUser = await fakeUser(); | ||
collectiveAdmin = await fakeUser(); | ||
hostAdmin = await fakeUser(); | ||
const order = await fakeOrder(); | ||
const rootAdmin = await fakeUser(); | ||
contributor = await fakeUser(); | ||
collective = await fakeCollective(); | ||
transaction = await fakeTransaction({ | ||
CollectiveId: collective.id, | ||
FromCollectiveId: contributor.CollectiveId, | ||
amount: 100000, | ||
OrderId: order.id, | ||
}); | ||
console.log(transaction.OrderId); | ||
await collective.addUserWithRole(collectiveAdmin, 'ADMIN'); | ||
await collective.host.addUserWithRole(hostAdmin, 'ADMIN'); | ||
|
||
await collectiveAdmin.populateRoles(); | ||
await hostAdmin.populateRoles(); | ||
await rootAdmin.populateRoles(); | ||
|
||
rootAdmin.rolesByCollectiveId[1] = [roles.ADMIN]; | ||
|
||
publicReq = makeRequest(); | ||
randomUserReq = makeRequest(randomUser); | ||
collectiveAdminReq = makeRequest(collectiveAdmin); | ||
hostAdminReq = makeRequest(hostAdmin); | ||
rootAdminReq = makeRequest(rootAdmin); | ||
contributorReq = makeRequest(contributor); | ||
}); | ||
|
||
describe('canRefund', () => { | ||
it('can refund if root or host admin of the collective receiving the contribution', async () => { | ||
expect(await canRefund(transaction, undefined, publicReq)).to.be.false; | ||
expect(await canRefund(transaction, undefined, randomUserReq)).to.be.false; | ||
expect(await canRefund(transaction, undefined, collectiveAdminReq)).to.be.false; | ||
expect(await canRefund(transaction, undefined, contributorReq)).to.be.false; | ||
expect(await canRefund(transaction, undefined, hostAdminReq)).to.be.true; | ||
expect(await canRefund(transaction, undefined, rootAdminReq)).to.be.true; | ||
}); | ||
}); | ||
|
||
describe('canDownloadInvoice', () => { | ||
it('can download invoice if donator or host admin of the collective receiving the contribution', async () => { | ||
expect(await canDownloadInvoice(transaction, undefined, publicReq)).to.be.false; | ||
expect(await canDownloadInvoice(transaction, undefined, randomUserReq)).to.be.false; | ||
expect(await canDownloadInvoice(transaction, undefined, collectiveAdminReq)).to.be.false; | ||
expect(await canDownloadInvoice(transaction, undefined, contributorReq)).to.be.true; | ||
expect(await canDownloadInvoice(transaction, undefined, hostAdminReq)).to.be.true; | ||
expect(await canDownloadInvoice(transaction, undefined, rootAdminReq)).to.be.false; | ||
}); | ||
}); | ||
}); |