Skip to content
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
60 changes: 57 additions & 3 deletions server/lib/pdf.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import config from 'config';
import { get } from 'lodash';
import { get, uniq } from 'lodash';
import moment from 'moment';

import { TransactionKind } from '../constants/transaction-kind';
import models, { Op } from '../models';
import { USTaxFormType } from '../models/LegalDocument';

Expand Down Expand Up @@ -59,10 +60,44 @@ export const getConsolidatedInvoicesData = async fromCollective => {
}

const transactions = await models.Transaction.findAll({
attributes: ['createdAt', 'HostCollectiveId'],
attributes: ['createdAt', 'HostCollectiveId', 'kind', 'TransactionGroup'],
where,
});

const platformTipGroups = transactions
.filter(t => t.kind === TransactionKind.PLATFORM_TIP)
.map(t => t.TransactionGroup);
let contributionByGroup = {};
const contributionHostsById = {};
if (platformTipGroups.length) {
const contributions = await models.Transaction.findAll({
attributes: ['HostCollectiveId', 'TransactionGroup'],
where: {
TransactionGroup: platformTipGroups,
kind: TransactionKind.CONTRIBUTION,
type: 'CREDIT',
},
});

contributionByGroup = contributions.reduce((result, contribution) => {
result[contribution.TransactionGroup] = contribution;
return result;
}, {});

// Pre-fetch the contribution hosts in a single query (with their settings) to avoid an
// N+1 lookup when checking the single-receipt opt-in for each platform tip below.
const contributionHostIds = uniq(contributions.map(c => c.HostCollectiveId).filter(Boolean));
if (contributionHostIds.length) {
const contributionHosts = await models.Collective.findAll({
attributes: ['id', 'settings'],
where: { id: contributionHostIds },
});
for (const host of contributionHosts) {
contributionHostsById[host.id] = host;
}
}
}

const hostsById = {};
const invoicesByKey: Record<
string,
Expand All @@ -76,6 +111,21 @@ export const getConsolidatedInvoicesData = async fromCollective => {
}
> = {};
for (const transaction of transactions) {
if (transaction.kind === TransactionKind.PLATFORM_TIP) {
// For hosts that opted in to single contributor receipts, platform tips are rendered as line
// items on the host-issued contribution receipt. Keep legacy standalone OFiTech tip receipts
// for all other hosts until the setting is fully rolled out.
const relatedContribution = contributionByGroup[transaction.TransactionGroup];
if (relatedContribution?.HostCollectiveId) {
const contributionHost = contributionHostsById[relatedContribution.HostCollectiveId];
if (contributionHost && get(contributionHost, 'settings.singleReceiptPlatformTip') === true) {
// Do not add this platform tip to `invoicesByKey`, otherwise the dashboard would still
// show a separate OFiTech receipt alongside the host-issued contribution receipt.
continue;
}
}
Comment thread
znarf marked this conversation as resolved.
}

const HostCollectiveId = transaction.HostCollectiveId;
if (!HostCollectiveId) {
continue;
Expand All @@ -86,12 +136,16 @@ export const getConsolidatedInvoicesData = async fromCollective => {
attributes: ['id', 'slug'],
});
}
const host = hostsById[HostCollectiveId];
if (!host) {
continue;
}

const createdAt = new Date(transaction.createdAt);
const year = createdAt.getFullYear();
const month = createdAt.getMonth() + 1;
const monthToDigit = month < 10 ? `0${month}` : `${month}`;
const slug = `${year}${monthToDigit}.${hostsById[HostCollectiveId].slug}.${fromCollective.slug}`;
const slug = `${year}${monthToDigit}.${host.slug}.${fromCollective.slug}`;
const totalTransactions = invoicesByKey[slug] ? invoicesByKey[slug].totalTransactions + 1 : 1;

invoicesByKey[slug] = {
Expand Down
27 changes: 26 additions & 1 deletion test/server/graphql/common/transactions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { expect } from 'chai';
import { useFakeTimers } from 'sinon';

import { roles } from '../../../../server/constants';
import PlatformConstants from '../../../../server/constants/platform';
import { TransactionKind } from '../../../../server/constants/transaction-kind';
import { canDownloadInvoice, canRefund, canReject } from '../../../../server/graphql/common/transactions';
import {
Expand All @@ -12,7 +13,7 @@ import {
fakeUser,
fakeUserToken,
} from '../../../test-helpers/fake-data';
import { makeRequest } from '../../../utils';
import { getOrCreatePlatformAccount, makeRequest } from '../../../utils';

describe('server/graphql/common/transactions', () => {
let collective,
Expand All @@ -24,6 +25,7 @@ describe('server/graphql/common/transactions', () => {
contributor,
randomUser,
transaction,
platformTipTransaction,
refundTransaction,
manualPaymentTransaction;

Expand Down Expand Up @@ -62,6 +64,21 @@ describe('server/graphql/common/transactions', () => {
OrderId: order.id,
PaymentMethodId: creditCard.id,
});
// Platform tips are collected by, and hosted by, the platform account itself
// (see Transaction.createPlatformTipTransactions), not the contribution's host.
await getOrCreatePlatformAccount();
platformTipTransaction = await fakeTransaction({
type: 'CREDIT',
description: 'Financial contribution to the Open Collective Platform',
CollectiveId: PlatformConstants.PlatformCollectiveId,
FromCollectiveId: contributor.CollectiveId,
HostCollectiveId: PlatformConstants.PlatformCollectiveId,
TransactionGroup: transaction.TransactionGroup,
kind: TransactionKind.PLATFORM_TIP,
amount: 1000,
OrderId: order.id,
PaymentMethodId: creditCard.id,
});
refundTransaction = await fakeTransaction({
description: 'Refund of Contribution',
FromCollectiveId: collective.id,
Expand Down Expand Up @@ -224,5 +241,13 @@ describe('server/graphql/common/transactions', () => {
expect(await canDownloadInvoice(transaction, undefined, contributorOAuthReq)).to.be.false;
expect(await canDownloadInvoice(refundTransaction, undefined, contributorOAuthReq)).to.be.false;
});

it('lets the contributor download platform tip receipts', async () => {
// The contributor is the payer of the platform tip, so they can download its receipt.
expect(await canDownloadInvoice(platformTipTransaction, undefined, contributorReq)).to.be.true;
// The platform tip is hosted by the platform account, not the contribution's fiscal host,
// so an admin of that host has no access to the standalone platform tip receipt.
expect(await canDownloadInvoice(platformTipTransaction, undefined, hostAdminReq)).to.be.false;
});
});
});
Loading