Merge pull request 'reflect new fee structure' (#1029) from new-fee-structure into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m47s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m31s

Reviewed-on: #1029
This commit is contained in:
philipp 2025-05-17 08:14:04 +02:00
commit 7e10253e2e
7 changed files with 116 additions and 65 deletions

View File

@ -23,6 +23,9 @@ pub(crate) const UNTERSTUETZEND: i64 = 2500;
pub(crate) const FOERDERND: i64 = 8500; pub(crate) const FOERDERND: i64 = 8500;
pub(crate) const SCHECKBUCH: i64 = 3000; pub(crate) const SCHECKBUCH: i64 = 3000;
pub(crate) const EINSCHREIBGEBUEHR: i64 = 3000; pub(crate) const EINSCHREIBGEBUEHR: i64 = 3000;
pub(crate) const DUAL_MEMBERSHIP: i64 = 18000;
pub(crate) const TRIAL_ROWING: i64 = 12000;
pub(crate) const TRIAL_ROWING_REDUCED: i64 = 6000;
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct NonEmptyString(String); pub struct NonEmptyString(String);

View File

@ -1,9 +1,9 @@
use std::{error::Error, fs}; use std::{error::Error, fs};
use lettre::{ use lettre::{
Address, Message, SmtpTransport, Transport, message::{header::ContentType, Attachment, MultiPart, SinglePart},
message::{Attachment, MultiPart, SinglePart, header::ContentType},
transport::smtp::authentication::Credentials, transport::smtp::authentication::Credentials,
Address, Message, SmtpTransport, Transport,
}; };
use sqlx::{Sqlite, SqlitePool, Transaction}; use sqlx::{Sqlite, SqlitePool, Transaction};
@ -161,6 +161,11 @@ impl Mail {
continue; continue;
} }
} }
if user.has_role(db, "schnupperant").await {
continue;
}
if !user.has_role(db, "paid").await || test.is_some() { if !user.has_role(db, "paid").await || test.is_some() {
let mut is_family = false; let mut is_family = false;
let mut send_to = String::new(); let mut send_to = String::new();
@ -273,6 +278,11 @@ Der Vorstand");
continue; continue;
} }
} }
if user.has_role(db, "schnupperant").await {
continue;
}
if let Some(fee) = user.fee(db).await { if let Some(fee) = user.fee(db).await {
if !fee.paid || test.is_some() { if !fee.paid || test.is_some() {
let mut is_family = false; let mut is_family = false;

View File

@ -519,6 +519,15 @@ impl User {
Ok(()) Ok(())
} }
pub(crate) async fn remove_membership_pdf(&self, db: &SqlitePool, updated_by: &ManageUserUser) {
sqlx::query!(
"UPDATE user SET membership_pdf = null where id = ?",
self.id
)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a User of a valid id
}
pub(crate) async fn add_membership_pdf( pub(crate) async fn add_membership_pdf(
&self, &self,
db: &SqlitePool, db: &SqlitePool,

View File

@ -1,7 +1,8 @@
use super::User; use super::User;
use crate::{ use crate::{
BOAT_STORAGE, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO, FOERDERND, REGULAR, model::family::Family, BOAT_STORAGE, DUAL_MEMBERSHIP, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE,
RENNRUDERBEITRAG, STUDENT_OR_PUPIL, UNTERSTUETZEND, model::family::Family, FAMILY_TWO, FOERDERND, REGULAR, RENNRUDERBEITRAG, STUDENT_OR_PUPIL, TRIAL_ROWING,
TRIAL_ROWING_REDUCED, UNTERSTUETZEND,
}; };
use chrono::{Datelike, Local, NaiveDate}; use chrono::{Datelike, Local, NaiveDate};
use serde::Serialize; use serde::Serialize;
@ -68,6 +69,7 @@ impl User {
if !self.has_role(db, "Donau Linz").await if !self.has_role(db, "Donau Linz").await
&& !self.has_role(db, "Unterstützend").await && !self.has_role(db, "Unterstützend").await
&& !self.has_role(db, "Förderndes Mitglied").await && !self.has_role(db, "Förderndes Mitglied").await
&& !self.has_role(db, "schnupperant").await
{ {
return None; return None;
} }
@ -107,6 +109,7 @@ impl User {
if !self.has_role(db, "Donau Linz").await if !self.has_role(db, "Donau Linz").await
&& !self.has_role(db, "Unterstützend").await && !self.has_role(db, "Unterstützend").await
&& !self.has_role(db, "Förderndes Mitglied").await && !self.has_role(db, "Förderndes Mitglied").await
&& !self.has_role(db, "schnupperant").await
{ {
return fee; return fee;
} }
@ -126,8 +129,10 @@ impl User {
); );
} }
if !self.has_role(db, "schnupperant").await {
if let Some(member_since_date) = &self.member_since_date { if let Some(member_since_date) = &self.member_since_date {
if let Ok(member_since_date) = NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d") if let Ok(member_since_date) =
NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d")
{ {
if member_since_date.year() == Local::now().year() if member_since_date.year() == Local::now().year()
&& !self.has_role(db, "no-einschreibgebuehr").await && !self.has_role(db, "no-einschreibgebuehr").await
@ -136,6 +141,7 @@ impl User {
} }
} }
} }
}
let halfprice = if let Some(member_since_date) = &self.member_since_date { let halfprice = if let Some(member_since_date) = &self.member_since_date {
match NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d") { match NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d") {
@ -150,7 +156,13 @@ impl User {
false false
}; };
if self.has_role(db, "Unterstützend").await { if self.has_role(db, "schnupperant").await {
if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await {
fee.add("Schnupperkurs (reduziert)".into(), TRIAL_ROWING_REDUCED);
} else {
fee.add("Schnupperkurs".into(), TRIAL_ROWING);
}
} else if self.has_role(db, "Unterstützend").await {
fee.add("Unterstützendes Mitglied".into(), UNTERSTUETZEND); fee.add("Unterstützendes Mitglied".into(), UNTERSTUETZEND);
} else if self.has_role(db, "Förderndes Mitglied").await { } else if self.has_role(db, "Förderndes Mitglied").await {
fee.add("Förderndes Mitglied".into(), FOERDERND); fee.add("Förderndes Mitglied".into(), FOERDERND);
@ -163,6 +175,18 @@ impl User {
} }
} else if self.has_role(db, "Ehrenmitglied").await { } else if self.has_role(db, "Ehrenmitglied").await {
fee.add("Ehrenmitglied".into(), 0); fee.add("Ehrenmitglied".into(), 0);
} else if self.has_role(db, "dual_membership").await {
if halfprice {
fee.add(
"Doppelmitgliedschaft mit anderem österr. Ruderverein (Halbpreis)".into(),
DUAL_MEMBERSHIP / 2,
);
} else {
fee.add(
"Doppelmitgliedschaft mit anderem österr. Ruderverein".into(),
DUAL_MEMBERSHIP,
);
}
} else if halfprice { } else if halfprice {
fee.add("Mitgliedsbeitrag (Halbpreis)".into(), REGULAR / 2); fee.add("Mitgliedsbeitrag (Halbpreis)".into(), REGULAR / 2);
} else { } else {
@ -170,6 +194,19 @@ impl User {
} }
} }
if !self.has_role(db, "schnupperant").await
&& self.has_role(db, "participated_schnupperkurs").await
{
if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await {
fee.add(
"Anrechnung reduzierter Schnupperkurs".into(),
-TRIAL_ROWING_REDUCED,
);
} else {
fee.add("Anrechnung Schnupperkurs".into(), -TRIAL_ROWING);
}
}
fee fee
} }
} }

View File

@ -1,8 +1,7 @@
use super::{ManageUserUser, User}; use super::{ManageUserUser, User};
use crate::{ use crate::{
NonEmptyString,
model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role}, model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role},
special_user, special_user, NonEmptyString,
}; };
use chrono::NaiveDate; use chrono::NaiveDate;
use rocket::{async_trait, fs::TempFile, tokio::io::AsyncReadExt}; use rocket::{async_trait, fs::TempFile, tokio::io::AsyncReadExt};
@ -93,6 +92,8 @@ Beim nächsten Treffen im Verein, erinnere jemand vom Vorstand (https://rudernli
Damit du dich noch mehr verbunden fühlst (:-)), haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz' eingerichtet. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter. Damit du dich noch mehr verbunden fühlst (:-)), haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz' eingerichtet. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter.
Falls du deinen Mitgliedsbeitrag noch nicht bezahlt hast, erledige dies bitte demnächst. Den genauen Betrag und einen QR Code, den du mit deiner Bankapp scannen kannst findest du unter https://app.rudernlinz.at/planned
Wir freuen uns darauf, dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln! Wir freuen uns darauf, dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln!
Riemen- & Dollenbruch Riemen- & Dollenbruch

View File

@ -4,9 +4,9 @@ use super::scheckbuch::ScheckbuchUser;
use super::schnupperinterest::SchnupperInterestUser; use super::schnupperinterest::SchnupperInterestUser;
use super::unterstuetzend::UnterstuetzendUser; use super::unterstuetzend::UnterstuetzendUser;
use super::{ManageUserUser, User}; use super::{ManageUserUser, User};
use crate::NonEmptyString;
use crate::model::activity::ActivityBuilder; use crate::model::activity::ActivityBuilder;
use crate::model::role::Role; use crate::model::role::Role;
use crate::NonEmptyString;
use crate::{ use crate::{
model::{mail::Mail, notification::Notification}, model::{mail::Mail, notification::Notification},
special_user, special_user,
@ -65,20 +65,32 @@ impl SchnupperantUser {
.await?; .await?;
// Change roles // Change roles
let regular = Role::find_by_name(db, "Donau Linz").await.unwrap(); let paid = Role::find_by_name(db, "paid").await.unwrap();
if self.user.remove_role(db, changed_by, &paid).await.is_err() {
self.remove_membership_pdf(db, changed_by).await;
return Err("Kann noch kein normales Mitglied werden, da die Schnupperkurs-Gebühr noch nicht bezahlt wurde.".into());
}
let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap(); let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap();
self.user.remove_role(db, changed_by, &scheckbook).await?; self.user.remove_role(db, changed_by, &scheckbook).await?;
let regular = Role::find_by_name(db, "Donau Linz").await.unwrap();
self.user.add_role(db, changed_by, &regular).await?; self.user.add_role(db, changed_by, &regular).await?;
let participated_schnupperkurs = Role::find_by_name(db, "participated_schnupperkurs")
.await
.unwrap();
self.user
.add_role(db, changed_by, &participated_schnupperkurs)
.await?;
// Notify // Notify
let regular = RegularUser::new(db, &self.user).await.unwrap(); let regular = RegularUser::new(db, &self.user).await.unwrap();
regular.send_welcome_mail_to_user(db, smtp_pw).await?; regular.send_welcome_mail_to_user(db, smtp_pw).await?;
Notification::create_for_steering_people( Notification::create_for_steering_people(
db, db,
&format!( &format!(
"Liebe Steuerberechtigte, {} nahm an unserem Schnupperkurs teil und ist nun seit {} ein neues reguläres Mitglied. 🎉", "Liebe Steuerberechtigte, {} nahm an unserem Schnupperkurs teil und ist nun seit {member_since} ein neues reguläres Mitglied. 🎉",
self.name, self.name
self.member_since_date.clone().unwrap()
), ),
"Neues Vereinsmitglied", "Neues Vereinsmitglied",
None, None,
@ -203,6 +215,11 @@ impl SchnupperantUser {
.await?; .await?;
// Change roles // Change roles
let paid = Role::find_by_name(db, "paid").await.unwrap();
if self.user.remove_role(db, changed_by, &paid).await.is_err() {
self.remove_membership_pdf(db, changed_by).await;
return Err("Kann noch kein normales Mitglied werden, da die Schnupperkurs-Gebühr noch nicht bezahlt wurde.".into());
}
let unterstuetzend = Role::find_by_name(db, "Unterstützend").await.unwrap(); let unterstuetzend = Role::find_by_name(db, "Unterstützend").await.unwrap();
let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap(); let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap();
self.user.remove_role(db, changed_by, &scheckbook).await?; self.user.remove_role(db, changed_by, &scheckbook).await?;
@ -267,6 +284,11 @@ impl SchnupperantUser {
.await?; .await?;
// Change roles // Change roles
let paid = Role::find_by_name(db, "paid").await.unwrap();
if self.user.remove_role(db, changed_by, &paid).await.is_err() {
self.remove_membership_pdf(db, changed_by).await;
return Err("Kann noch kein normales Mitglied werden, da die Schnupperkurs-Gebühr noch nicht bezahlt wurde.".into());
}
let unterstuetzend = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap(); let unterstuetzend = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap();
let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap(); let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap();
self.user.remove_role(db, changed_by, &scheckbook).await?; self.user.remove_role(db, changed_by, &scheckbook).await?;
@ -307,7 +329,7 @@ impl SchnupperantUser {
// TODO: make private // TODO: make private
pub(crate) async fn notify(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> { pub(crate) async fn notify(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> {
self.notify_coxes_about_new_scheckbuch(db).await; self.notify_coxes_about_new_schnupperant(db).await;
self.send_welcome_mail_to_user(db, smtp_pw).await?; self.send_welcome_mail_to_user(db, smtp_pw).await?;
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
@ -335,19 +357,27 @@ impl SchnupperantUser {
mail, mail,
"ASKÖ Ruderverein Donau Linz | Anmeldung Schnupperkurs", "ASKÖ Ruderverein Donau Linz | Anmeldung Schnupperkurs",
format!( format!(
"Hallo {0}, "Hallo {0},
es freut uns sehr, dich bei unserem Schnupperkurs willkommen heißen zu dürfen. Detaillierte Informationen folgen noch, ich werde sie dir ein paar Tage vor dem Termin zusenden. es freut uns sehr, dich bei unserem Schnupperkurs willkommen heißen zu dürfen.
Bitte überweise die {1} auf unser Bankkonto (IBAN: AT58 2032 0321 0072 9256) und gib beim Verwendungszweck 'Schnupperkurs {0}' an.
Detaillierte Informationen folgen noch, du wirst sie ein paar Tage vor dem Termin bekommen (wenn das Wetter/Wasserstand/... abschätzbar ist).
Riemen- & Dollenbruch, Riemen- & Dollenbruch,
ASKÖ Ruderverein Donau Linz", self.name), ASKÖ Ruderverein Donau Linz",
self.name,
self.fee(db).await.unwrap().sum_in_cents/100
),
smtp_pw, smtp_pw,
).await?; )
.await?;
Ok(()) Ok(())
} }
async fn notify_coxes_about_new_scheckbuch(&self, db: &SqlitePool) { async fn notify_coxes_about_new_schnupperant(&self, db: &SqlitePool) {
if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await { if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await {
Notification::create_for_role( Notification::create_for_role(
db, db,

View File

@ -5,44 +5,5 @@ INSERT INTO user(name) VALUES('Philipp');
INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Philipp'),(SELECT id FROM role where name = 'Donau Linz')); INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Philipp'),(SELECT id FROM role where name = 'Donau Linz'));
-- Step 1: Create a new table without the 'notes' column insert into role(name, cluster, formatted_name) values('dual_membership', 'financial', 'Doppelmitgliedschaft mit anderem österr. Ruderverein');
CREATE TABLE "user_new" ( insert into role(name, hide_in_lists) values('participated_schnupperkurs', true);
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" text NOT NULL UNIQUE,
"pw" text,
"deleted" boolean NOT NULL DEFAULT FALSE,
"last_access" DATETIME,
"dob" text,
"weight" text,
"sex" text,
"dirty_thirty" text,
"dirty_dozen" text,
"member_since_date" text,
"birthdate" text,
"mail" text,
"nickname" text,
"phone" text,
"address" text,
"family_id" INTEGER REFERENCES family(id),
"membership_pdf" BLOB,
"user_token" TEXT NOT NULL DEFAULT (lower(hex(randomblob(16))))
);
-- Step 2: Copy data from the old table to the new one (excluding 'notes')
INSERT INTO user_new (
id, name, pw, deleted, last_access, dob, weight, sex,
dirty_thirty, dirty_dozen, member_since_date, birthdate,
mail, nickname, phone, address, family_id, membership_pdf, user_token
)
SELECT
id, name, pw, deleted, last_access, dob, weight, sex,
dirty_thirty, dirty_dozen, member_since_date, birthdate,
mail, nickname, phone, address, family_id, membership_pdf, user_token
FROM user;
-- Step 3: Drop the old table
DROP TABLE user;
-- Step 4: Rename the new table to the original name
ALTER TABLE user_new RENAME TO user;