yeaerly-cleanup #1160

Merged
philipp merged 5 commits from yeaerly-cleanup into staging 2025-11-21 10:35:22 +01:00
6 changed files with 291 additions and 222 deletions

View File

@@ -1,4 +1,9 @@
import { test, expect } from "@playwright/test"; import { test, expect, Page } from "@playwright/test";
import { resetDatabase, login } from "./helpers";
test.beforeEach(async () => {
await resetDatabase();
});
test("cox can create and delete trip", async ({ page }) => { test("cox can create and delete trip", async ({ page }) => {
await page.goto("/auth"); await page.goto("/auth");
@@ -16,22 +21,13 @@ test("cox can create and delete trip", async ({ page }) => {
await page.getByRole("spinbutton").fill("5"); await page.getByRole("spinbutton").fill("5");
await page.getByRole("button", { name: "Erstellen", exact: true }).click(); await page.getByRole("button", { name: "Erstellen", exact: true }).click();
await expect(page.locator("body")).toContainText("18:00 Uhr (cox) Details"); await expect(page.locator("body")).toContainText("18:00 Uhr (cox) Details");
await page.goto("/planned");
await page.getByRole('link', { name: 'Details' }).nth(1).click();
await page.getByRole("link", { name: "Termin löschen" }).click();
await expect(page.locator("body")).toContainText("Erfolgreich gelöscht!");
}); });
// TODO: group -> cox can create trips // TODO: group -> cox can create trips
// TODO: cox can help/register at trips/events // TODO: cox can help/register at trips/events
test.describe("cox can edit trips", () => { test.describe("cox can edit trips", () => {
let sharedPage: Page; async function createTrip(page: Page) {
test.beforeAll(async ({ browser }) => {
const page = await browser.newPage();
await page.goto("/auth"); await page.goto("/auth");
await page.getByPlaceholder("Name").click(); await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("cox"); await page.getByPlaceholder("Name").fill("cox");
@@ -46,151 +42,101 @@ test.describe("cox can edit trips", () => {
await page.locator("#sidebar #planned_starting_time").press("Tab"); await page.locator("#sidebar #planned_starting_time").press("Tab");
await page.getByRole("spinbutton").fill("5"); await page.getByRole("spinbutton").fill("5");
await page.getByRole("button", { name: "Erstellen", exact: true }).click(); await page.getByRole("button", { name: "Erstellen", exact: true }).click();
}
sharedPage = page; test("edit remarks", async ({ page }) => {
}); await createTrip(page);
test("edit remarks", async () => { await page.goto("/planned");
await sharedPage.goto("/planned"); await page.getByRole('link', { name: 'Details' }).nth(1).click();
await sharedPage.getByRole('link', { name: 'Details' }).nth(1).click(); await page.locator("#sidebar #notes").click();
await sharedPage.locator("#sidebar #notes").click(); await page.locator("#sidebar #notes").fill("Meine Anmerkung");
await sharedPage.locator("#sidebar #notes").fill("Meine Anmerkung"); await page.getByRole("button", { name: "Speichern" }).click();
await sharedPage.getByRole("button", { name: "Speichern" }).click(); await page.getByRole("link", { name: "Details" }).nth(1).click();
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click(); await expect(page.locator("#sidebar")).toContainText(
await expect(sharedPage.locator("#sidebar")).toContainText(
"Meine Anmerkung", "Meine Anmerkung",
); );
await sharedPage
.getByRole("button", { name: "Ausfahrt erstellen schließen" })
.click();
}); });
test("add and remove guest", async () => { test("add and remove guest", async ({ page }) => {
await sharedPage.goto("/planned"); await createTrip(page);
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await sharedPage.locator("#sidebar #user_note").click(); await page.goto("/planned");
await sharedPage.locator("#sidebar #user_note").fill("Mein Gast"); await page.getByRole("link", { name: "Details" }).nth(1).click();
await sharedPage.getByRole("button", { name: "Gast hinzufügen" }).click(); await page.locator("#sidebar #user_note").click();
await expect(sharedPage.locator("body")).toContainText( await page.locator("#sidebar #user_note").fill("Mein Gast");
await page.getByRole("button", { name: "Gast hinzufügen" }).click();
await expect(page.locator("body")).toContainText(
"Erfolgreich angemeldet!", "Erfolgreich angemeldet!",
); );
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click(); await page.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText( await expect(page.locator("#sidebar")).toContainText(
"Freie Plätze: 4", "Freie Plätze: 4",
); );
await expect(sharedPage.locator("#sidebar")).toContainText( await expect(page.locator("#sidebar")).toContainText(
"Mein Gast (Gast) Abmelden", "Mein Gast (Gast) Abmelden",
); );
await expect( await expect(
sharedPage.getByRole("link", { name: "Termin löschen" }), page.getByRole("link", { name: "Termin löschen" }),
).not.toBeVisible(); ).not.toBeVisible();
await sharedPage.getByRole("link", { name: "Abmelden" }).click(); await page.getByRole("link", { name: "Abmelden" }).click();
await expect(sharedPage.locator("body")).toContainText( await expect(page.locator("body")).toContainText(
"Erfolgreich abgemeldet!", "Erfolgreich abgemeldet!",
); );
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click(); await page.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText( await expect(page.locator("#sidebar")).toContainText(
"Freie Plätze: 5", "Freie Plätze: 5",
); );
await expect(sharedPage.locator("#sidebar")).toContainText( await expect(page.locator("#sidebar")).toContainText(
"Keine Ruderer angemeldet", "Keine Ruderer angemeldet",
); );
await expect( await expect(
sharedPage.getByRole("link", { name: "Termin löschen" }), page.getByRole("link", { name: "Termin löschen" }),
).toBeVisible(); ).toBeVisible();
await sharedPage
.getByRole("button", { name: "Ausfahrt erstellen schließen" })
.click();
}); });
test("change amount rower", async () => { test("change amount rower", async ({ page }) => {
await sharedPage.goto("/planned"); await createTrip(page);
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText( await page.goto("/planned");
await page.getByRole("link", { name: "Details" }).nth(1).click();
await expect(page.locator("#sidebar")).toContainText(
"Freie Plätze: 5", "Freie Plätze: 5",
); );
await sharedPage.getByRole("spinbutton").click(); await page.getByRole("spinbutton").click();
await sharedPage.getByRole("spinbutton").fill("3"); await page.getByRole("spinbutton").fill("3");
await sharedPage.getByRole("button", { name: "Speichern" }).click(); await page.getByRole("button", { name: "Speichern" }).click();
await expect(sharedPage.locator("body")).toContainText( await expect(page.locator("body")).toContainText(
"Ausfahrt erfolgreich aktualisiert.", "Ausfahrt erfolgreich aktualisiert.",
); );
}); });
test("call off trip", async () => { test("call off trip", async ({ page }) => {
await createTrip(page);
// Someone registers... // Someone registers...
await sharedPage.goto("/auth/logout"); await page.goto("/auth/logout");
await sharedPage.goto("/auth"); await page.waitForURL("/auth");
await sharedPage.getByPlaceholder("Name").click(); await login(page, "rower", "rower");
await sharedPage.getByPlaceholder("Name").fill("rower");
await sharedPage.getByPlaceholder("Name").press("Tab");
await sharedPage.getByPlaceholder("Passwort").fill("rower");
await sharedPage.getByPlaceholder("Passwort").press("Enter");
await sharedPage.goto("/planned");
await sharedPage.getByRole('link', { name: 'Mitrudern' }).nth(1).click();
await page.goto("/planned");
await page.getByRole('link', { name: 'Mitrudern' }).nth(1).click();
// Login as cox again // Login as cox again
await sharedPage.goto("/auth/logout"); await page.goto("/auth/logout");
await sharedPage.goto("/auth"); await page.waitForURL("/auth");
await sharedPage.getByPlaceholder("Name").click(); await login(page, "cox", "cox");
await sharedPage.getByPlaceholder("Name").fill("cox");
await sharedPage.getByPlaceholder("Name").press("Tab");
await sharedPage.getByPlaceholder("Passwort").fill("cox");
await sharedPage.getByPlaceholder("Passwort").press("Enter");
await sharedPage.goto("/planned"); await page.goto("/planned");
// Now cancel the trip
// ... now I can cancel trip await page.getByRole("link", { name: "Details" }).nth(1).click();
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click(); await page.getByRole("button", { name: "Ausfahrt absagen" }).click();
await sharedPage.getByRole("button", { name: "Ausfahrt absagen" }).click(); await expect(page.locator("body")).toContainText(
await expect(sharedPage.locator("body")).toContainText(
"Ausfahrt erfolgreich aktualisiert.", "Ausfahrt erfolgreich aktualisiert.",
); );
await expect(sharedPage.locator("body")).toContainText("(Absage cox)"); await expect(page.locator("body")).toContainText("(Absage cox)");
// Done with the test -> cancel the cancellation of the trip, otherwise the afterAll function below fails
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await sharedPage.getByRole("spinbutton").click();
await sharedPage.getByRole("spinbutton").fill("3");
await sharedPage.getByRole("button", { name: "Speichern" }).click();
// deregistering
await sharedPage.goto("/auth/logout");
await sharedPage.goto("/auth");
await sharedPage.getByPlaceholder("Name").click();
await sharedPage.getByPlaceholder("Name").fill("rower");
await sharedPage.getByPlaceholder("Name").press("Tab");
await sharedPage.getByPlaceholder("Passwort").fill("rower");
await sharedPage.getByPlaceholder("Passwort").press("Enter");
await sharedPage.goto("/planned");
await sharedPage.getByRole('link', { name: 'Abmelden' }).click();
// now cox can delete trip again in afterAll
await sharedPage.goto("/auth/logout");
await sharedPage.goto("/auth");
await sharedPage.getByPlaceholder("Name").click();
await sharedPage.getByPlaceholder("Name").fill("cox");
await sharedPage.getByPlaceholder("Name").press("Tab");
await sharedPage.getByPlaceholder("Passwort").fill("cox");
await sharedPage.getByPlaceholder("Passwort").press("Enter");
});
test.afterAll(async () => {
await sharedPage.goto("/planned");
await sharedPage.getByRole('link', { name: 'Details' }).nth(1).click();
await sharedPage.getByRole("link", { name: "Termin löschen" }).click();
await sharedPage.close();
}); });
// TODO: 'Immer anzeigen' (also verify the functionality), 'Gesperrt' + type // TODO: 'Immer anzeigen' (also verify the functionality), 'Gesperrt' + type

29
frontend/tests/helpers.ts Normal file
View File

@@ -0,0 +1,29 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import { Page } from '@playwright/test';
const execAsync = promisify(exec);
export async function resetDatabase(): Promise<void> {
await execAsync('cd .. && ./reset_test_data.sh');
}
export async function login(page: Page, username: string, password: string): Promise<void> {
// Clear cookies to ensure clean state
await page.context().clearCookies();
// Navigate to auth page and wait for it to fully load
await page.goto("/auth", { waitUntil: 'load' });
await page.waitForLoadState('networkidle');
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill(username);
await page.getByPlaceholder("Passwort").click();
await page.getByPlaceholder("Passwort").fill(password);
// Wait for navigation after form submission
await Promise.all([
page.waitForURL(/\/(planned|log|$)/, { timeout: 10000 }),
page.getByPlaceholder("Passwort").press("Enter")
]);
}

View File

@@ -1,4 +1,9 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { resetDatabase } from "./helpers";
test.beforeEach(async () => {
await resetDatabase();
});
test("Cox can start and cancel trip", async ({ page }, testInfo) => { test("Cox can start and cancel trip", async ({ page }, testInfo) => {
await page.goto("/auth"); await page.goto("/auth");
@@ -34,12 +39,6 @@ test("Cox can start and cancel trip", async ({ page }, testInfo) => {
"Ausfahrt erfolgreich hinzugefügt", "Ausfahrt erfolgreich hinzugefügt",
); );
await expect(page.locator("body")).toContainText("Joe"); await expect(page.locator("body")).toContainText("Joe");
await page.getByRole("link", { name: "Joe" }).click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole("link", { name: "Löschen" }).click();
}); });
test("Cox can start and finish trip", async ({ page }, testInfo) => { test("Cox can start and finish trip", async ({ page }, testInfo) => {
@@ -102,28 +101,6 @@ test("Cox can start and finish trip", async ({ page }, testInfo) => {
await expect(page.locator('body')).toContainText('(cox2)'); await expect(page.locator('body')).toContainText('(cox2)');
await expect(page.locator('body')).toContainText('Ottensheim (25 km)'); await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2'); await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
// Login as admin
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("main");
await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("admin");
await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show");
await page.getByRole('link', { name: 'Joe' }).nth(1).click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole('link', { name: 'Löschen' }).click();
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
}); });
test("Kiosk can start and cancel trip", async ({ page }, testInfo) => { test("Kiosk can start and cancel trip", async ({ page }, testInfo) => {
@@ -151,12 +128,6 @@ test("Kiosk can start and cancel trip", async ({ page }, testInfo) => {
"Ausfahrt erfolgreich hinzugefügt", "Ausfahrt erfolgreich hinzugefügt",
); );
await expect(page.locator("body")).toContainText("Joe"); await expect(page.locator("body")).toContainText("Joe");
await page.getByRole("link", { name: "Joe" }).click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole("link", { name: "Löschen" }).click();
}); });
test("Kiosk can start and finish trip", async ({ page }, testInfo) => { test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
@@ -210,29 +181,6 @@ test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
await expect(page.locator('body')).toContainText('Joe'); await expect(page.locator('body')).toContainText('Joe');
await expect(page.locator('body')).toContainText('Ottensheim (25 km)'); await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2'); await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
//Ausloggen...
await page.context().clearCookies();
await page.goto("/auth");
// Login as admin
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("main");
await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("admin");
await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show");
await page.getByRole('link', { name: 'Joe' }).nth(1).click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole('link', { name: 'Löschen' }).click();
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
}); });
test("Cox can start and finish trip with cox steering only", async ({ page }, testInfo) => { test("Cox can start and finish trip with cox steering only", async ({ page }, testInfo) => {
@@ -286,29 +234,6 @@ test("Cox can start and finish trip with cox steering only", async ({ page }, te
await page.goto('/log/show'); await page.goto('/log/show');
await expect(page.locator('body')).toContainText('cox_only_steering_boat'); await expect(page.locator('body')).toContainText('cox_only_steering_boat');
await expect(page.locator('body')).toContainText('Ottensheim (25 km)'); await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
// Login as admin
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("main");
await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("admin");
await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show");
await page.getByRole("link", { name: "cox_only_steering_boat" }).click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole('link', { name: 'Löschen' }).click();
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
}); });
test("Kiosk can start and finish trip in one stop", async ({ page }, testInfo) => { test("Kiosk can start and finish trip in one stop", async ({ page }, testInfo) => {
@@ -355,27 +280,4 @@ test("Kiosk can start and finish trip in one stop", async ({ page }, testInfo) =
await expect(page.locator('body')).toContainText('(cox2)'); await expect(page.locator('body')).toContainText('(cox2)');
await expect(page.locator('body')).toContainText('a (1 km)'); await expect(page.locator('body')).toContainText('a (1 km)');
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2'); await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
//Ausloggen...
await page.context().clearCookies();
await page.goto("/auth");
// Login as admin
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("main");
await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("admin");
await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show");
await page.getByRole('link', { name: 'Joe' }).nth(1).click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole('link', { name: 'Löschen' }).click();
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
}); });

19
reset_test_data.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -e
DB_FILE="db.sqlite"
# Clear all data and reseed
sqlite3 "$DB_FILE" << 'EOF'
PRAGMA writable_schema = 1;
DELETE FROM sqlite_sequence;
PRAGMA writable_schema = 0;
PRAGMA foreign_keys = OFF;
EOF
# Get all tables and delete from them
sqlite3 "$DB_FILE" "SELECT 'DELETE FROM ' || name || ';' FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';" | sqlite3 "$DB_FILE"
# Re-enable foreign keys and reseed
sqlite3 "$DB_FILE" "PRAGMA foreign_keys = ON;"
sqlite3 "$DB_FILE" < seeds.sql

View File

@@ -1,5 +1,6 @@
mod waterlevel; mod waterlevel;
mod weather; mod weather;
mod yearly_role_cleanup;
use std::time::Duration; use std::time::Duration;
@@ -13,7 +14,7 @@ pub fn schedule(db: &SqlitePool, config: &Config) {
let db = db.clone(); let db = db.clone();
let openweathermap_key = config.openweathermap_key.clone(); let openweathermap_key = config.openweathermap_key.clone();
tokio::task::spawn(async { tokio::task::spawn(async move {
if let Err(e) = waterlevel::update(&db).await { if let Err(e) = waterlevel::update(&db).await {
log::error!("Water level update error: {e}, trying again next time"); log::error!("Water level update error: {e}, trying again next time");
} }
@@ -24,8 +25,9 @@ pub fn schedule(db: &SqlitePool, config: &Config) {
let mut sched = JobScheduler::new(); let mut sched = JobScheduler::new();
// Every hour // Every hour
let db_for_hourly = db.clone();
sched.add(Job::new("0 0 * * * * *".parse().unwrap(), move || { sched.add(Job::new("0 0 * * * * *".parse().unwrap(), move || {
let db_clone = db.clone(); let db_clone = db_for_hourly.clone();
// Use block_in_place to run async code in the synchronous function; TODO: Make it // Use block_in_place to run async code in the synchronous function; TODO: Make it
// nicer one's rust (stable) support async closures // nicer one's rust (stable) support async closures
task::block_in_place(|| { task::block_in_place(|| {
@@ -40,6 +42,19 @@ pub fn schedule(db: &SqlitePool, config: &Config) {
}); });
})); }));
// January 1st at midnight - yearly role cleanup
let db_for_yearly = db.clone();
sched.add(Job::new("0 0 0 1 1 * *".parse().unwrap(), move || {
let db_clone = db_for_yearly.clone();
task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
if let Err(e) = yearly_role_cleanup::cleanup_roles(&db_clone).await {
log::error!("Yearly role cleanup error: {e}");
}
});
});
}));
let mut interval = time::interval(Duration::from_secs(60)); let mut interval = time::interval(Duration::from_secs(60));
loop { loop {
sched.tick(); sched.tick();

View File

@@ -0,0 +1,158 @@
use crate::model::{notification::Notification, role::Role};
use sqlx::SqlitePool;
pub async fn cleanup_roles(db: &SqlitePool) -> Result<(), String> {
log::info!("Starting yearly role cleanup...");
let mut tx = db.begin().await.map_err(|e| e.to_string())?;
// Find all roles to remove
let paid_role = Role::find_by_name_tx(&mut tx, "paid")
.await
.ok_or("Role 'paid' not found")?;
let schueler_role = Role::find_by_name_tx(&mut tx, "Schüler")
.await
.ok_or("Role 'Schüler' not found")?;
let student_role = Role::find_by_name_tx(&mut tx, "Student")
.await
.ok_or("Role 'Student' not found")?;
let no_einschreibgebuehr_role = Role::find_by_name_tx(&mut tx, "no-einschreibgebuehr")
.await
.ok_or("Role 'no-einschreibgebuehr' not found")?;
let half_rennrudern_role = Role::find_by_name_tx(&mut tx, "half-rennrudern")
.await
.ok_or("Role 'half-rennrudern' not found")?;
let participated_schnupperkurs_role =
Role::find_by_name_tx(&mut tx, "participated_schnupperkurs")
.await
.ok_or("Role 'participated_schnupperkurs' not found")?;
// Find scheckbuch role (needed to exclude users from "paid" removal -> they have still paid
// for the scheckbuch)
let scheckbuch_role = Role::find_by_name_tx(&mut tx, "scheckbuch")
.await
.ok_or("Role 'scheckbuch' not found")?;
// Remove "paid" role from all users EXCEPT those with scheckbuch role
let paid_removed = sqlx::query!(
"DELETE FROM user_role
WHERE role_id = ?
AND user_id NOT IN (
SELECT user_id FROM user_role WHERE role_id = ?
)",
paid_role.id,
scheckbuch_role.id
)
.execute(&mut *tx)
.await
.map_err(|e| e.to_string())?
.rows_affected();
// Remove other roles from all users
let schueler_removed =
sqlx::query!("DELETE FROM user_role WHERE role_id = ?", schueler_role.id)
.execute(&mut *tx)
.await
.map_err(|e| e.to_string())?
.rows_affected();
let student_removed = sqlx::query!("DELETE FROM user_role WHERE role_id = ?", student_role.id)
.execute(&mut *tx)
.await
.map_err(|e| e.to_string())?
.rows_affected();
let no_einschreibgebuehr_removed = sqlx::query!(
"DELETE FROM user_role WHERE role_id = ?",
no_einschreibgebuehr_role.id
)
.execute(&mut *tx)
.await
.map_err(|e| e.to_string())?
.rows_affected();
let half_rennrudern_removed = sqlx::query!(
"DELETE FROM user_role WHERE role_id = ?",
half_rennrudern_role.id
)
.execute(&mut *tx)
.await
.map_err(|e| e.to_string())?
.rows_affected();
let participated_schnupperkurs_removed = sqlx::query!(
"DELETE FROM user_role WHERE role_id = ?",
participated_schnupperkurs_role.id
)
.execute(&mut *tx)
.await
.map_err(|e| e.to_string())?
.rows_affected();
// Send notifications to admins and Vorstand
let admin_role = Role::find_by_name_tx(&mut tx, "admin")
.await
.ok_or("Role 'admin' not found")?;
let vorstand_role = Role::find_by_name_tx(&mut tx, "Vorstand")
.await
.ok_or("Role 'Vorstand' not found")?;
let notification_message_admin = format!(
"Jährliche Rollenbereinigung abgeschlossen. Die folgenden Rollen wurden entfernt: \
paid ({} Benutzer, außer Scheckbuch-Mitglieder), \
Schüler/Student ({}/{} Benutzer), \
no-einschreibgebuehr ({} Benutzer), \
half-rennrudern ({} Benutzer), \
participated_schnupperkurs ({} Benutzer). \
Die aktualisierten Gebühren können unter https://app.rudernlinz.at/admin/user/fees eingesehen werden.",
paid_removed,
schueler_removed,
student_removed,
no_einschreibgebuehr_removed,
half_rennrudern_removed,
participated_schnupperkurs_removed
);
let notification_message_vorstand = format!(
"Jährliche Rollenbereinigung abgeschlossen. \
Die aktualisierten Gebühren können unter https://app.rudernlinz.at/admin/user/fees eingesehen werden.",
);
// Notify admins
Notification::create_for_role_tx(
&mut tx,
&admin_role,
&notification_message_admin,
"Systembenachrichtigung",
Some("https://app.rudernlinz.at/admin/user/fees"),
None,
)
.await;
// Notify Vorstand
Notification::create_for_role_tx(
&mut tx,
&vorstand_role,
&notification_message_vorstand,
"Systembenachrichtigung",
Some("https://app.rudernlinz.at/admin/user/fees"),
None,
)
.await;
// Commit transaction
tx.commit().await.map_err(|e| e.to_string())?;
log::info!(
"Yearly role cleanup completed successfully: \
paid={}, Schüler={}, Student={}, no-einschreibgebuehr={}, \
half-rennrudern={}, participated_schnupperkurs={} removals",
paid_removed,
schueler_removed,
student_removed,
no_einschreibgebuehr_removed,
half_rennrudern_removed,
participated_schnupperkurs_removed
);
Ok(())
}