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

Get Transactions from Ledger #1578

Merged
merged 10 commits into from
Dec 12, 2018
Merged
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 config/custom-environment-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,13 @@
"recaptcha": {
"siteKey": "RECAPTCHA_SITE_KEY",
"secretKey": "RECAPTCHA_SECRET_KEY"
},
"ledger": {
"url": "LEDGER_URL",
"transactionUrl": "LEDGER_TRANSACTION_URL"
},
"ledgerQueue": {
"url": "LEDGER_QUEUE_URL",
"transactionQueue": "TRANSACTION_QUEUE_NAME"
}
}
8 changes: 8 additions & 0 deletions config/development.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,13 @@
"github": {
"clientID": "cdc163e5f5695e16787b",
"clientSecret": "1cc8e3a615af3a8ca47cd1f15adcfcdd1e880db4"
},
"ledger": {
"url": "http://localhost:3070",
"transactionUrl": "http://localhost:3070/transactions"
},
"ledgerQueue": {
"url": "amqp://localhost",
"transactionQueue": "transactions"
}
}
127 changes: 107 additions & 20 deletions server/graphql/v1/TransactionInterface.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { get } from 'lodash';
import models from '../../../server/models';
import {
GraphQLInt,
GraphQLFloat,
Expand Down Expand Up @@ -65,7 +67,12 @@ const TransactionFields = () => {
refundTransaction: {
type: TransactionInterfaceType,
resolve(transaction) {
return transaction.getRefundTransaction();
// If it's a sequelize model transaction, it means it has the method getRefundTransaction
// otherwise we just null
if (transaction && transaction.getRefundTransaction) {
return transaction.getRefundTransaction();
}
return null;
},
},
uuid: {
Expand All @@ -74,7 +81,12 @@ const TransactionFields = () => {
if (!req.remoteUser) {
return null;
}
return transaction.getDetailsForUser(req.remoteUser);
// If it's a sequelize model transaction, it means it has the method getDetailsForUser
// otherwise we return transaction.uuid
if (transaction && transaction.getDetailsForUser) {
return transaction.getDetailsForUser(req.remoteUser);
}
return transaction.uuid;
},
},
type: {
Expand Down Expand Up @@ -140,32 +152,77 @@ const TransactionFields = () => {
},
host: {
type: UserCollectiveType,
resolve(transaction) {
return transaction.getHostCollective();
async resolve(transaction) {
if (transaction && transaction.getHostCollective) {
return transaction.getHostCollective();
}
const FromCollectiveId = transaction.fromCollective.id;
const CollectiveId = transaction.collective.id;
let HostCollectiveId = transaction.HostCollectiveId;
// if the transaction is from the perspective of the fromCollective
if (!HostCollectiveId) {
const fromCollective = await models.Collective.findById(FromCollectiveId);
HostCollectiveId = await fromCollective.getHostCollectiveId();
// if fromCollective has no host, we try the collective
if (!HostCollectiveId) {
const collective = await models.Collective.findById(CollectiveId);
HostCollectiveId = await collective.getHostCollectiveId();
}
}
return models.Collective.findById(HostCollectiveId);
},
},
createdByUser: {
type: UserType,
resolve(transaction) {
return transaction.getCreatedByUser();
// If it's a sequelize model transaction, it means it has the method getCreatedByUser
// otherwise we return null
if (transaction && transaction.getCreatedByUser) {
return transaction.getCreatedByUser();
}
return null;
},
},
fromCollective: {
type: CollectiveInterfaceType,
resolve(transaction) {
return transaction.getFromCollective();
// If it's a sequelize model transaction, it means it has the method getFromCollective
// otherwise we check whether transaction has 'fromCollective.id', if not we return null
if (transaction && transaction.getFromCollective) {
return transaction.getFromCollective();
}
if (get(transaction, 'fromCollective.id')) {
return models.Collective.findById(get(transaction, 'fromCollective.id'));
}
return null;
},
},
usingVirtualCardFromCollective: {
type: CollectiveInterfaceType,
resolve(transaction) {
return transaction.getVirtualCardEmitterCollective();
// If it's a sequelize model transaction, it means it has the method getVirtualCardEmitterCollective
// otherwise we find the collective by id if transactions has UsingVirtualCardFromCollectiveId, if not we return null
if (transaction && transaction.getVirtualCardEmitterCollective) {
return transaction.getVirtualCardEmitterCollective();
}
if (transaction && transaction.UsingVirtualCardFromCollectiveId) {
return models.Collective.findById(transaction.UsingVirtualCardFromCollectiveId);
}
return null;
},
},
collective: {
type: CollectiveInterfaceType,
resolve(transaction) {
return transaction.getCollective();
// If it's a sequelize model transaction, it means it has the method getCollective
// otherwise we check whether transaction has 'collective.id', if not we return null
if (transaction && transaction.getCollective) {
return transaction.getCollective();
}
if (get(transaction, 'collective.id')) {
return models.Collective.findById(get(transaction, 'collective.id'));
}
return null;
},
},
createdAt: {
Expand All @@ -183,9 +240,10 @@ const TransactionFields = () => {
paymentMethod: {
type: PaymentMethodType,
resolve(transaction, args, req) {
if (!transaction.PaymentMethodId) return null;
const paymentMethodId = transaction.PaymentMethodId || get(transaction, 'paymentMethod.id');
if (!paymentMethodId) return null;
// TODO: put behind a login check
return req.loaders.paymentMethods.findById.load(transaction.PaymentMethodId);
return req.loaders.paymentMethods.findById.load(paymentMethodId);
},
},
};
Expand All @@ -200,25 +258,42 @@ export const TransactionExpenseType = new GraphQLObjectType({
description: {
type: GraphQLString,
resolve(transaction) {
return transaction.description || transaction.getExpense().then(expense => expense && expense.description);
// If it's a sequelize model transaction, it means it has the method getExpense
// otherwise we return transaction.description , if not then return null
const expense = transaction.getExpense
? transaction.getExpense().then(expense => expense && expense.description)
: null;
return transaction.description || expense;
},
},
privateMessage: {
type: GraphQLString,
resolve(transaction, args, req) {
return transaction.getExpenseForViewer(req.remoteUser).then(expense => expense && expense.privateMessage);
// If it's a sequelize model transaction, it means it has the method getExpenseFromViewer
// otherwise we return null
return transaction.getExpenseForViewer
? transaction.getExpenseForViewer(req.remoteUser).then(expense => expense && expense.privateMessage)
: null;
},
},
category: {
type: GraphQLString,
resolve(transaction, args, req) {
return transaction.getExpenseForViewer(req.remoteUser).then(expense => expense && expense.category);
// If it's a sequelize model transaction, it means it has the method getExpenseFromViewer
// otherwise we return null
return transaction.getExpenseForViewer
? transaction.getExpenseForViewer(req.remoteUser).then(expense => expense && expense.category)
: null;
},
},
attachment: {
type: GraphQLString,
resolve(transaction, args, req) {
return transaction.getExpenseForViewer(req.remoteUser).then(expense => expense && expense.attachment);
// If it's a sequelize model transaction, it means it has the method getExpenseFromViewer
// otherwise we return null
return transaction.getExpenseForViewer
? transaction.getExpenseForViewer(req.remoteUser).then(expense => expense && expense.attachment)
: null;
},
},
};
Expand All @@ -235,32 +310,44 @@ export const TransactionOrderType = new GraphQLObjectType({
description: {
type: GraphQLString,
resolve(transaction) {
return transaction.description || transaction.getOrder().then(order => order && order.description);
// If it's a sequelize model transaction, it means it has the method getOrder
// otherwise we return either transaction.description or null
const getOrder = transaction.getOrder
? transaction.getOrder().then(order => order && order.description)
: null;
return transaction.description || getOrder;
},
},
privateMessage: {
type: GraphQLString,
resolve(transaction) {
// TODO: Put behind a login check
return transaction.getOrder().then(order => order && order.privateMessage);
// If it's a sequelize model transaction, it means it has the method getOrder
// otherwise we return null
return transaction.getOrder ? transaction.getOrder().then(order => order && order.privateMessage) : null;
Copy link
Member

Choose a reason for hiding this comment

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

Why do we add all this defensive code to check if methods are available?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TransactionInterface is supposed to be expecting a Sequelize Model but when we parse the transactions coming from the ledger, those transactions are just object(with some fields like the Sequelize model) but they don't have their methods. That's why the validation is necessary.

Copy link
Member

Choose a reason for hiding this comment

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

Can you explain me why it's ok to send null there when there is no method? Aren't we expecting a value?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've checked every return null case for those validations. I don't think we're waiting these specific values where we return null/undefined. But also in all other scenarios we have, we are using the sequelize models and the validations I added won't never be catched other than the transactionsFromLedger

},
},
publicMessage: {
type: GraphQLString,
resolve(transaction) {
return transaction.getOrder().then(order => order && order.publicMessage);
// If it's a sequelize model transaction, it means it has the method getOrder
// otherwise we return null
return transaction.getOrder ? transaction.getOrder().then(order => order && order.publicMessage) : null;
},
},
order: {
type: OrderType,
resolve(transaction) {
return transaction.getOrder();
// If it's a sequelize model transaction, it means it has the method getOrder
// otherwise we return null
return transaction.getOrder ? transaction.getOrder() : null;
},
},
subscription: {
type: SubscriptionType,
resolve(transaction) {
return transaction.getOrder().then(order => order && order.getSubscription());
// If it's a sequelize model transaction, it means it has the method getOrder
// otherwise we return null
return transaction.getOrder ? transaction.getOrder().then(order => order && order.getSubscription()) : null;
},
},
};
Expand Down
45 changes: 36 additions & 9 deletions server/graphql/v1/queries.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import Promise from 'bluebird';
import { find, get, uniq } from 'lodash';

import algolia from '../../lib/algolia';
import errors from '../../lib/errors';
import { parseToBoolean } from '../../lib/utils';
import { fetchLedgerTransactionsGroupedByLegacyIds, parseLedgerTransactions } from '../../lib/ledger';

import { GraphQLList, GraphQLNonNull, GraphQLString, GraphQLInt, GraphQLBoolean } from 'graphql';

Expand Down Expand Up @@ -317,24 +318,50 @@ const queries = {
dateTo: { type: GraphQLString },
/** @deprecated since 2018-11-29: Virtual cards now included by default when necessary */
includeVirtualCards: { type: GraphQLBoolean },
fetchDataFromLedger: { type: GraphQLBoolean }, // flag to go with either api or ledger transactions
includeHostedCollectivesTransactions: {
type: GraphQLBoolean,
} /** flag to determine
whether we should include the transactions of the collectives of that host(if it's a host collective) */,
},
async resolve(_, args) {
let fetchDataFromLedger = parseToBoolean(process.env.GET_TRANSACTIONS_FROM_LEDGER);
if (args.hasOwnProperty('fetchDataFromLedger')) {
fetchDataFromLedger = args.fetchDataFromLedger;
}
// Load collective
const { CollectiveId, collectiveSlug } = args;
if (!CollectiveId && !collectiveSlug) throw new Error('You must specify a collective ID or a Slug');
const where = CollectiveId ? { id: CollectiveId } : { slug: collectiveSlug };
const collective = await models.Collective.findOne({ where });
if (!collective) throw new Error('This collective does not exist');

// Load transactions
return collective.getTransactions({
order: [['createdAt', 'DESC']],
type: args.type,
limit: args.limit,
offset: args.offset,
startDate: args.dateFrom,
endDate: args.dateTo,
// returns transactions straight from the api
if (!fetchDataFromLedger) {
return collective.getTransactions({
order: [['createdAt', 'DESC']],
type: args.type,
limit: args.limit,
offset: args.offset,
startDate: args.dateFrom,
endDate: args.dateTo,
});
}
// otherwise returns data from the ledger
const ledgerTransactions = await fetchLedgerTransactionsGroupedByLegacyIds(args);
const apiTransactions = await models.Transaction.findAll({
attributes: [
'id',
'uuid', // because the stored invoice pdf in aws uses the uuid as reference
'UsingVirtualCardFromCollectiveId', // because virtual cards will only work for wallets and we're skipping wallets in the transactions details for now
'HostCollectiveId', // because we're skipping wallets and using host on transactions details for now
'RefundTransactionId', // because the ledger refundTransactionId refers to the ledger id and not the legacy one
],
where: {
id: Object.keys(ledgerTransactions),
},
});
return parseLedgerTransactions(args.CollectiveId, ledgerTransactions, apiTransactions);
},
},

Expand Down
Loading