Skip to content

Commit

Permalink
Export data to excel (#391)
Browse files Browse the repository at this point in the history
* add export module
add methods supporting excel export
add types

* add small tweaks

* fix yarn.lock

* yarn lock

* update yarn lock

* update yarn lock

* Update yarn.lock

* Fix unit tests

* add ExportableData type

* Use prisma validator to create the input type

Co-authored-by: Andrey <[email protected]>
Co-authored-by: Ilko Kacharov <[email protected]>
  • Loading branch information
3 people authored Dec 11, 2022
1 parent 07ff323 commit dbc3a45
Show file tree
Hide file tree
Showing 15 changed files with 538 additions and 31 deletions.
2 changes: 2 additions & 0 deletions apps/api/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { OrganizerModule } from '../organizer/organizer.module'
import { DonationWishModule } from '../donation-wish/donation-wish.module'
import { ApiLoggerMiddleware } from './middleware/apilogger.middleware'
import { PaypalModule } from '../paypal/paypal.module'
import { ExportModule } from '../export/export.module'

@Module({
imports: [
Expand Down Expand Up @@ -90,6 +91,7 @@ import { PaypalModule } from '../paypal/paypal.module'
OrganizerModule,
DonationWishModule,
PaypalModule,
ExportModule,
],
controllers: [AppController],
providers: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ConfigService } from '@nestjs/config'
import { Test, TestingModule } from '@nestjs/testing'
import { CampaignService } from '../campaign/campaign.service'
import { DonationsService } from '../donations/donations.service'
import { ExportService } from '../export/export.service'
import { PersonService } from '../person/person.service'
import { PrismaService } from '../prisma/prisma.service'
import { S3Service } from '../s3/s3.service'
Expand Down Expand Up @@ -36,6 +37,7 @@ describe('BankTransactionsFileController', () => {
PersonService,
PrismaService,
S3Service,
ExportService,
],
}).compile()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import { CampaignService } from '../campaign/campaign.service'
import { ConfigService } from '@nestjs/config'
import { StripeModule } from '@golevelup/nestjs-stripe'
import { useFactoryService } from './helpers/use-factory-service'
import { ExportService } from '../export/export.service'

@Module({
imports: [
StripeModule.forRootAsync(StripeModule, {
inject: [ConfigService],
useFactory:useFactoryService.useFactory
useFactory: useFactoryService.useFactory,
}),
],
controllers: [BankTransactionsFileController],
Expand All @@ -27,6 +28,7 @@ import { useFactoryService } from './helpers/use-factory-service'
VaultService,
CampaignService,
DonationsService,
ExportService,
],
})
export class BankTransactionsFileModule {}
4 changes: 3 additions & 1 deletion apps/api/src/donations/donations.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Prisma,
} from '@prisma/client'
import { CampaignService } from '../campaign/campaign.service'
import { ExportService } from '../export/export.service'
import { PersonService } from '../person/person.service'
import { MockPrismaService, prismaMock } from '../prisma/prisma-client.mock'
import { VaultService } from '../vault/vault.service'
Expand Down Expand Up @@ -83,6 +84,7 @@ describe('DonationsController', () => {
useValue: stripeMock,
},
PersonService,
ExportService,
],
}).compile()

Expand Down Expand Up @@ -199,7 +201,7 @@ describe('DonationsController', () => {
}

const existingDonation = { ...mockDonation, status: DonationStatus.initial }
const expectedUpdatedDonation = {...existingDonation, status: DonationStatus.succeeded }
const expectedUpdatedDonation = { ...existingDonation, status: DonationStatus.succeeded }

prismaMock.donation.findFirst.mockResolvedValueOnce(existingDonation)
prismaMock.donation.update.mockResolvedValueOnce(expectedUpdatedDonation)
Expand Down
16 changes: 14 additions & 2 deletions apps/api/src/donations/donations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import {
Post,
UnauthorizedException,
Query,
Res,
} from '@nestjs/common'
import { ApiQuery } from '@nestjs/swagger'
import { Response } from 'express'

import { KeycloakTokenParsed } from '../auth/keycloak'
import { isAdmin, KeycloakTokenParsed } from '../auth/keycloak'
import { DonationsService } from './donations.service'
import { CreateSessionDto } from './dto/create-session.dto'
import { CreatePaymentDto } from './dto/create-payment.dto'
Expand All @@ -27,6 +29,17 @@ import { ApiTags } from '@nestjs/swagger'
export class DonationsController {
constructor(private readonly donationsService: DonationsService) {}

@Get('export-excel')
@Roles({
roles: [RealmViewSupporters.role, ViewSupporters.role],
mode: RoleMatchingMode.ANY,
})
async exportToExcel(@Res() res: Response, @AuthenticatedUser() user: KeycloakTokenParsed) {
if (isAdmin(user)) {
await this.donationsService.exportToExcel(res)
}
}

@Post('create-checkout-session')
@Public()
createCheckoutSession(@Body() sessionDto: CreateSessionDto) {
Expand Down Expand Up @@ -67,7 +80,6 @@ export class DonationsController {
@Query('status') status?: DonationStatus,
@Query() query?: PagingQueryDto,
) {
console.log(query)
return this.donationsService.listDonationsPublic(
campaignId,
status,
Expand Down
4 changes: 4 additions & 0 deletions apps/api/src/donations/donations.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ConfigService } from '@nestjs/config'
import { useFactoryService } from '../bank-transactions-file/helpers/use-factory-service'
import { CampaignModule } from '../campaign/campaign.module'
import { CampaignService } from '../campaign/campaign.service'
import { ExportService } from '../export/export.service'
import { PersonModule } from '../person/person.module'
import { PersonService } from '../person/person.service'
import { PrismaService } from '../prisma/prisma.service'
Expand All @@ -12,6 +13,7 @@ import { VaultService } from '../vault/vault.service'
import { DonationsController } from './donations.controller'
import { DonationsService } from './donations.service'
import { StripePaymentService } from './events/stripe-payment.service'
import { ExportModule } from './../export/export.module'

@Module({
imports: [
Expand All @@ -22,6 +24,7 @@ import { StripePaymentService } from './events/stripe-payment.service'
VaultModule,
CampaignModule,
PersonModule,
ExportModule,
],
controllers: [DonationsController],
providers: [
Expand All @@ -31,6 +34,7 @@ import { StripePaymentService } from './events/stripe-payment.service'
PrismaService,
VaultService,
PersonService,
ExportService,
],
})
export class DonationsModule {}
2 changes: 2 additions & 0 deletions apps/api/src/donations/donations.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { STRIPE_CLIENT_TOKEN } from '@golevelup/nestjs-stripe'
import { ConfigService } from '@nestjs/config'
import { Test, TestingModule } from '@nestjs/testing'
import { CampaignService } from '../campaign/campaign.service'
import { ExportService } from '../export/export.service'
import { PersonService } from '../person/person.service'
import { MockPrismaService } from '../prisma/prisma-client.mock'
import { VaultModule } from '../vault/vault.module'
Expand All @@ -25,6 +26,7 @@ describe('DonationsService', () => {
useValue: jest.fn(),
},
PersonService,
ExportService,
],
}).compile()

Expand Down
35 changes: 25 additions & 10 deletions apps/api/src/donations/donations.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ import {
PaymentProvider,
Prisma,
} from '@prisma/client'
import { Response } from 'express'
import { getTemplateByTable } from '../export/helpers/exportableData'

import { KeycloakTokenParsed } from '../auth/keycloak'
import { CampaignService } from '../campaign/campaign.service'
import { PrismaService } from '../prisma/prisma.service'
import { VaultService } from '../vault/vault.service'
import { ExportService } from '../export/export.service'
import { DonationMetadata } from './dontation-metadata.interface'
import { CreateBankPaymentDto } from './dto/create-bank-payment.dto'
import { CreatePaymentDto } from './dto/create-payment.dto'
Expand All @@ -23,6 +26,7 @@ import { UpdatePaymentDto } from './dto/update-payment.dto'
import { Person } from '../person/entities/person.entity'
import { CreateManyBankPaymentsDto } from './dto/create-many-bank-payments.dto'
import { DonationBaseDto, ListDonationsDto } from './dto/list-donations.dto'
import { donationWithPerson, DonationWithPerson } from './validators/donation.validator'

@Injectable()
export class DonationsService {
Expand All @@ -32,6 +36,7 @@ export class DonationsService {
private campaignService: CampaignService,
private prisma: PrismaService,
private vaultService: VaultService,
private exportService: ExportService,
) {}

async listPrices(type?: Stripe.PriceListParams.Type, active?: boolean): Promise<Stripe.Price[]> {
Expand Down Expand Up @@ -213,16 +218,12 @@ export class DonationsService {
status?: DonationStatus,
pageIndex?: number,
pageSize?: number,
): Promise<ListDonationsDto<Donation>> {
): Promise<ListDonationsDto<DonationWithPerson>> {
const data = await this.prisma.donation.findMany({
where: { status, targetVault: { campaign: { id: campaignId } } },
orderBy: [{ createdAt: 'desc' }],
include: {
person: { select: { firstName: true, lastName: true } },
targetVault: { select: { name: true } },
},
skip: pageIndex && pageSize ? pageIndex * pageSize : undefined,
take: pageSize ? pageSize : undefined,
...donationWithPerson,
})

const count = await this.prisma.donation.count({
Expand Down Expand Up @@ -296,7 +297,6 @@ export class DonationsService {
this.vaultService.incrementVaultAmount(donation.targetVaultId, donation.amount)
} catch (error) {
Logger.error('Error while importing bank donation. ', error)
throw error
}
}
}
Expand Down Expand Up @@ -349,9 +349,11 @@ export class DonationsService {
},
})

if (currentDonation.status !== DonationStatus.succeeded
&& updatePaymentDto.status === DonationStatus.succeeded
&& donation.status === DonationStatus.succeeded) {
if (
currentDonation.status !== DonationStatus.succeeded &&
updatePaymentDto.status === DonationStatus.succeeded &&
donation.status === DonationStatus.succeeded
) {
await this.vaultService.incrementVaultAmount(
currentDonation.targetVaultId,
currentDonation.amount,
Expand Down Expand Up @@ -412,4 +414,17 @@ export class DonationsService {
},
})
}

async exportToExcel(res: Response) {
const { items } = await this.listDonations()
const donationsMappedForExport = items.map((donation) => ({
...donation,
person: donation.person
? `${donation.person.firstName} ${donation.person.lastName}`
: 'Unknown',
}))
const donationExcelTemplate = getTemplateByTable('donations')

await this.exportService.exportToExcel(res, donationsMappedForExport, donationExcelTemplate)
}
}
20 changes: 20 additions & 0 deletions apps/api/src/donations/validators/donation.validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Prisma } from '@prisma/client'

export const donationWithPerson = Prisma.validator<Prisma.DonationFindManyArgs>()({
include: {
person: {
select: {
firstName: true,
lastName: true,
},
},
targetVault: {
select: {
name: true,
},
},
},
orderBy: [{ createdAt: 'desc' }],
})

export type DonationWithPerson = Prisma.DonationGetPayload<typeof donationWithPerson>
9 changes: 9 additions & 0 deletions apps/api/src/export/export.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common'
import { ExportService } from './export.service'
import { PrismaService } from '../prisma/prisma.service'

@Module({
providers: [ExportService, PrismaService],
exports: [ExportService],
})
export class ExportModule {}
30 changes: 30 additions & 0 deletions apps/api/src/export/export.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Injectable } from '@nestjs/common'
import { PrismaService } from '../prisma/prisma.service'
import { Response } from 'express'
import { createWorkbook } from './helpers/createWorkbook'
import { ExportableData } from './helpers/exportableData'
import { Template } from './helpers/exportableData'

@Injectable()
export class ExportService {
constructor(private prisma: PrismaService) {}

exportToExcel = async (res: Response, data: ExportableData, template: Template) => {
if (!data.length) res.status(404).end('No data to export')

try {
const workbook = createWorkbook(data, template)

res.set({
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'Content-Disposition': 'attachment;',
})

workbook.xlsx.write(res).then(() => {
res.status(200).end()
})
} catch (err) {
throw new Error(err)
}
}
}
42 changes: 42 additions & 0 deletions apps/api/src/export/helpers/createWorkbook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import excelJs from 'exceljs'
import { ExportableData } from './exportableData'
import { Template } from './exportableData'

const applySheetDataToRows = (sheet, data: ExportableData) => {
data.forEach((el) => {
sheet.addRow(el)
})
}

const handleHeadersRowStyle = (header, style) => {
header.font = style?.font
header.alignment = style?.alignment
header.height = style?.height
}

const handleBodyRowsStyle = (sheet, style) => {
let rowIndex = 2
for (rowIndex; rowIndex <= sheet.rowCount; rowIndex++) {
const currentRow = sheet.getRow(rowIndex)
currentRow.alignment = style?.alignment
}
}

export const createWorkbook = (data: ExportableData, template: Template) => {
const workbook = new excelJs.Workbook()
template.sheets.forEach((sheet) => {
const { title, columns, style } = sheet

const currentSheet = workbook.addWorksheet(title)
currentSheet.columns = columns

applySheetDataToRows(currentSheet, data)

const headerRow = currentSheet.getRow(1)
handleHeadersRowStyle(headerRow, style.header)

handleBodyRowsStyle(currentSheet, style.body)
})

return workbook
}
Loading

0 comments on commit dbc3a45

Please sign in to comment.