feat: add TTR simulator and calculation engine, enhance member model with TTR adjustment constant
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 59s

This commit is contained in:
Torsten Schulz (local)
2026-10-05 15:18:10 +02:00
parent 6fa54163f6
commit 6d982add75
21 changed files with 466 additions and 27 deletions

View File

@@ -59,6 +59,8 @@ class ClubService {
{
model: User,
as: 'Users',
where: { isHidden: false },
required: false,
through: {
attributes: []
}
@@ -224,6 +226,8 @@ class ClubService {
{
model: User,
as: 'user',
where: { isHidden: false },
required: true,
}
]
});

View File

@@ -1,4 +1,5 @@
import nodemailer from 'nodemailer';
import User from '../models/User.js';
let transporter = null;
@@ -38,7 +39,32 @@ function getEmailTransportStatus() {
};
}
function normalizeRecipientList(recipients) {
const values = Array.isArray(recipients) ? recipients : [recipients];
return values
.flatMap((value) => String(value || '').split(','))
.map((value) => value.trim().toLowerCase())
.filter(Boolean);
}
async function sendMail(mailOptions) {
const recipients = normalizeRecipientList(mailOptions.to);
if (recipients.length) {
const suppressedUsers = await User.findAll({
where: { email: recipients, suppressEmails: true },
attributes: ['email'],
});
const suppressedEmails = new Set(suppressedUsers.map((user) => String(user.email).toLowerCase()));
const allowedRecipients = recipients.filter((email) => !suppressedEmails.has(email));
if (!allowedRecipients.length) {
console.info('[email] Delivery suppressed for hidden test account');
return { suppressed: true, accepted: [], rejected: [] };
}
mailOptions = { ...mailOptions, to: allowedRecipients.join(', ') };
}
return getTransporter().sendMail({
from: getDefaultFrom(),
...mailOptions,

View File

@@ -1,4 +1,5 @@
import UserClub from "../models/UserClub.js";
import User from "../models/User.js";
import Club from "../models/Club.js";
import { checkAccess, getUserByToken, hasUserClubAccess } from "../utils/userUtils.js";
import Member from "../models/Member.js";
@@ -221,6 +222,17 @@ class MemberService {
if (!showAll || showAll === 'false') {
where['active'] = true;
}
const hiddenUsers = await User.findAll({
where: { isHidden: true },
attributes: ['id'],
});
const hiddenUserIds = hiddenUsers.map((user) => user.id);
if (hiddenUserIds.length) {
where[Op.or] = [
{ userId: null },
{ userId: { [Op.notIn]: hiddenUserIds } },
];
}
const MemberContact = (await import('../models/MemberContact.js')).default;
try {
const members = await Member.findAll({

View File

@@ -474,7 +474,13 @@ class PermissionService {
const userClubs = await UserClub.findAll({
where: { clubId },
include: [{ model: User, as: 'user', attributes: ['id', 'email'] }],
include: [{
model: User,
as: 'user',
attributes: ['id', 'email'],
where: { isHidden: false },
required: true,
}],
order: [[{ model: User, as: 'user' }, 'email', 'ASC']],
});