Files
API/test/feedback/reports.admin.service.test.ts
T
Paddy c2ddb11c4c Fix silent newsletter validation drops, surface skipped-sync visibility, consolidate duplicated helpers
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>
2026-08-15 17:45:37 +02:00

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([]);
});
});