All files / src/features/export export.service.ts

93.75% Statements 30/32
92.85% Branches 13/14
75% Functions 3/4
93.54% Lines 29/31

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100                15x     15x 15x       3x   3x       3x 3x   3x           3x     3x             3x 4x   4x   4x 1x     1x     3x             2x   1x   1x           3x   3x       4x         4x 1x     3x 1x     2x   4x                
import { Injectable, Logger, StreamableFile } from '@nestjs/common'
import archiver from 'archiver'
import { PrismaService } from '../../database/prisma.service'
import { Prisma } from '@prisma/client'
import { PdfService } from '../pdf/pdf.service'
 
@Injectable()
export class ExportService {
    private readonly logger = new Logger(ExportService.name)
 
    constructor(
        private readonly prisma: PrismaService,
        private readonly pdfService: PdfService,
    ) {}
 
    async exportReceipts(eventId?: string): Promise<StreamableFile> {
        this.logger.log(`Starting global receipt export for event: ${eventId || 'ALL'}...`)
 
        const archive = archiver('zip', {
            zlib: { level: 9 }, // Best compression
        })
 
        const where: Prisma.DonationWhereInput = { status: 'COMPLETED' }
        if (eventId) where.eventId = eventId
 
        const donations = await this.prisma.donation.findMany({
            where,
            orderBy: { createdAt: 'desc' },
            include: { event: { select: { slug: true } } },
        })
 
        this.logger.log(`Found ${donations.length} donations to process.`)
 
        // Error handling for the archive
        archive.on('error', (err) => {
            this.logger.error('Archiver error', err)
            throw err
        })
 
        // We process sequentially for MVP to avoid memory spikes with parallel PDF generation
        // A better approach for HUGE datasets would be a dedicated worker.
        for (const donation of donations) {
            try {
                // For tax receipts, we always use the real name, even if the donation is anonymous publicly.
                const donorName = donation.donorName || 'Supporter'
 
                if (!donation.event?.slug) {
                    this.logger.warn(
                        `Skipping receipt for donation ${donation.id}: Event slug not found`,
                    )
                    continue
                }
 
                const pdfBuffer = await this.pdfService.generateReceipt(donation.event.slug, {
                    amount: donation.amount.toNumber(), // Convert Decimal to number (cents)
                    donorName: donorName,
                    date: donation.createdAt,
                    transactionId: donation.id, // or donation.transactionId (stripe ID)
                })
 
                archive.append(pdfBuffer, { name: `Receipt-${donation.id}.pdf` })
            } catch (error) {
                this.logger.error(`Failed to generate PDF for donation ${donation.id}`, error)
                // We append a text file with error log instead of failing the whole export
                archive.append(`Error generating receipt: ${error.message}`, {
                    name: `ERROR-${donation.id}.txt`,
                })
            }
        }
 
        archive.finalize()
 
        return new StreamableFile(archive)
    }
 
    async getReceipt(donationId: string): Promise<Buffer> {
        const donation = await this.prisma.donation.findUnique({
            where: { id: donationId },
            include: { event: { select: { slug: true } } },
        })
 
        if (!donation) {
            throw new Error('Donation not found')
        }
 
        if (!donation.event?.slug) {
            throw new Error('Event context missing for donation')
        }
 
        const donorName = donation.donorName || 'Supporter'
 
        return this.pdfService.generateReceipt(donation.event.slug, {
            amount: donation.amount.toNumber(),
            donorName: donorName,
            date: donation.createdAt,
            transactionId: donation.id,
        })
    }
}