c2ddb11c4c
Three fixes from the earlier review, plus cleanup: - submissions.service.ts: a newsletter opt-in present but failing validation (e.g. malformed email) was silently dropped with no signal to the client - the rest of the submission saved, but the visitor had no way to know their newsletter signup didn't go through. Added newsletterDropped to the submit response so the frontend can tell them. - reports.admin.service.ts: the newsletter summary tracked total/sent/pending/failed but silently omitted SKIPPED (stub-mode) signups from any bucket - every current signup showed total>0 with every bucket reading 0, indistinguishable from "we don't know what happened". Added a skipped count. - Consolidated two things duplicated across the module: sendServerError (reimplemented ~11 times, three of those as identical local copies of the same function) into feedback.errors.ts, and formatDatetime/ toMysqlDatetime (the same local-time formatting logic under two names, in csv.service.ts and events.admin.service.ts respectively) into feedback.dates.ts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
110 lines
5.1 KiB
TypeScript
110 lines
5.1 KiB
TypeScript
import {aggregateReport} from '../../src/models/feedback/admin/reports.admin.service';
|
|
import {AnswerRow} from '../../src/models/feedback/admin/reports.admin.interface';
|
|
|
|
const eventMeta = {eventId: 1, name: 'Sommerkonzert', eventDate: '2026-08-01', feedbackDeadline: '2026-08-15T23:59:59'};
|
|
const emptyNewsletter = {total: 0, sent: 0, pending: 0, failed: 0, skipped: 0};
|
|
|
|
const row = (overrides: Partial<AnswerRow>): AnswerRow => ({
|
|
submissionId: 1,
|
|
submittedAt: '2026-08-02T10:00:00.000Z',
|
|
questionId: 1,
|
|
questionLabel: 'Q',
|
|
questionType: 'FREE_TEXT',
|
|
songId: null,
|
|
songTitle: null,
|
|
rating: null,
|
|
textAnswer: null,
|
|
...overrides
|
|
});
|
|
|
|
describe('aggregateReport - song picks', () => {
|
|
it('counts votes per song and sorts by votes descending', () => {
|
|
const answers: AnswerRow[] = [
|
|
row({submissionId: 1, questionId: 5, questionLabel: 'Lieblingsstück?', questionType: 'SONG_PICK', songId: 10, songTitle: 'Abendlied'}),
|
|
row({submissionId: 2, questionId: 5, questionLabel: 'Lieblingsstück?', questionType: 'SONG_PICK', songId: 10, songTitle: 'Abendlied'}),
|
|
row({submissionId: 3, questionId: 5, questionLabel: 'Lieblingsstück?', questionType: 'SONG_PICK', songId: 11, songTitle: 'Morgenlied'})
|
|
];
|
|
const report = aggregateReport(eventMeta, {totalSubmissions: 3, firstSubmissionAt: null, lastSubmissionAt: null}, answers, 0, emptyNewsletter);
|
|
expect(report.songPicks).toHaveLength(1);
|
|
expect(report.songPicks[0].totalVotes).toBe(3);
|
|
expect(report.songPicks[0].results).toEqual([
|
|
{songId: 10, title: 'Abendlied', votes: 2},
|
|
{songId: 11, title: 'Morgenlied', votes: 1}
|
|
]);
|
|
});
|
|
|
|
it('keeps separate SONG_PICK questions in separate groups', () => {
|
|
const answers: AnswerRow[] = [
|
|
row({questionId: 5, questionLabel: 'Frage A', questionType: 'SONG_PICK', songId: 10, songTitle: 'Abendlied'}),
|
|
row({questionId: 6, questionLabel: 'Frage B', questionType: 'SONG_PICK', songId: 11, songTitle: 'Morgenlied'})
|
|
];
|
|
const report = aggregateReport(eventMeta, {totalSubmissions: 2, firstSubmissionAt: null, lastSubmissionAt: null}, answers, 0, emptyNewsletter);
|
|
expect(report.songPicks).toHaveLength(2);
|
|
});
|
|
});
|
|
|
|
describe('aggregateReport - song ratings', () => {
|
|
it('averages ratings per song, rounded to one decimal, sorted descending', () => {
|
|
const answers: AnswerRow[] = [
|
|
row({questionId: 7, questionLabel: 'Bewertung', questionType: 'SONG_RATING', songId: 10, songTitle: 'Abendlied', rating: 5}),
|
|
row({questionId: 7, questionLabel: 'Bewertung', questionType: 'SONG_RATING', songId: 10, songTitle: 'Abendlied', rating: 4}),
|
|
row({questionId: 7, questionLabel: 'Bewertung', questionType: 'SONG_RATING', songId: 11, songTitle: 'Morgenlied', rating: 3})
|
|
];
|
|
const report = aggregateReport(eventMeta, {totalSubmissions: 2, firstSubmissionAt: null, lastSubmissionAt: null}, answers, 0, emptyNewsletter);
|
|
expect(report.songRatings[0].results).toEqual([
|
|
{songId: 10, title: 'Abendlied', average: 4.5, count: 2},
|
|
{songId: 11, title: 'Morgenlied', average: 3, count: 1}
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('aggregateReport - free text', () => {
|
|
it('sorts newest first and caps at 500 with hasMore', () => {
|
|
const answers: AnswerRow[] = Array.from({length: 501}, (_, i) =>
|
|
row({
|
|
submissionId: i,
|
|
questionId: 9,
|
|
questionLabel: 'Sonstiges',
|
|
questionType: 'FREE_TEXT',
|
|
textAnswer: `Antwort ${i}`,
|
|
submittedAt: new Date(2026, 0, 1, 0, 0, i).toISOString()
|
|
})
|
|
);
|
|
const report = aggregateReport(eventMeta, {totalSubmissions: 501, firstSubmissionAt: null, lastSubmissionAt: null}, answers, 0, emptyNewsletter);
|
|
expect(report.freeText[0].responses).toHaveLength(500);
|
|
expect(report.freeText[0].hasMore).toBe(true);
|
|
expect(report.freeText[0].responses[0].text).toBe('Antwort 500');
|
|
});
|
|
|
|
it('does not set hasMore when at or under the cap', () => {
|
|
const answers: AnswerRow[] = [row({questionId: 9, questionLabel: 'Sonstiges', questionType: 'FREE_TEXT', textAnswer: 'Danke!'})];
|
|
const report = aggregateReport(eventMeta, {totalSubmissions: 1, firstSubmissionAt: null, lastSubmissionAt: null}, answers, 0, emptyNewsletter);
|
|
expect(report.freeText[0].hasMore).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('aggregateReport - top-level fields', () => {
|
|
it('passes through submission stats, guest book count, and newsletter counts unchanged', () => {
|
|
const report = aggregateReport(
|
|
eventMeta,
|
|
{totalSubmissions: 42, firstSubmissionAt: '2026-08-02T10:00:00.000Z', lastSubmissionAt: '2026-08-10T18:00:00.000Z'},
|
|
[],
|
|
7,
|
|
{total: 10, sent: 6, pending: 2, failed: 1, skipped: 1}
|
|
);
|
|
expect(report.totalSubmissions).toBe(42);
|
|
expect(report.firstSubmissionAt).toBe('2026-08-02T10:00:00.000Z');
|
|
expect(report.lastSubmissionAt).toBe('2026-08-10T18:00:00.000Z');
|
|
expect(report.guestBookCount).toBe(7);
|
|
expect(report.newsletter).toEqual({total: 10, sent: 6, pending: 2, failed: 1, skipped: 1});
|
|
expect(report.event).toEqual(eventMeta);
|
|
});
|
|
|
|
it('produces empty arrays for an event with no submissions', () => {
|
|
const report = aggregateReport(eventMeta, {totalSubmissions: 0, firstSubmissionAt: null, lastSubmissionAt: null}, [], 0, emptyNewsletter);
|
|
expect(report.songPicks).toEqual([]);
|
|
expect(report.songRatings).toEqual([]);
|
|
expect(report.freeText).toEqual([]);
|
|
});
|
|
});
|