264 lines
11 KiB
Rust
264 lines
11 KiB
Rust
use crate::paheko;
|
|
use crate::paheko::AccountingYear;
|
|
use crate::{
|
|
Config, UserCache,
|
|
};
|
|
use crate::utils::{generate_id, normalize_first_name, normalize_last_name};
|
|
|
|
use anyhow::{Context, Result};
|
|
use chrono::prelude::{NaiveDate, DateTime, Utc};
|
|
use fully_pub::fully_pub;
|
|
use serde::{Serialize, Deserialize};
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
|
#[fully_pub]
|
|
enum MembershipMode {
|
|
#[serde(rename = "Individuel")]
|
|
Individual,
|
|
#[serde(rename = "Couple")]
|
|
Couple,
|
|
#[serde(rename = "Individuel bienfaiteur")]
|
|
BenefactorIndividual,
|
|
#[serde(rename = "Couple bienfaiteur")]
|
|
BenefactorCouple,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
#[fully_pub]
|
|
struct GeneralizedAnswer {
|
|
// TODO: users are unique via their first and last name, instead of emails
|
|
first_name: Option<String>,
|
|
last_name: String,
|
|
email: Option<String>,
|
|
|
|
phone: Option<String>,
|
|
address: String,
|
|
city: String,
|
|
postal_code: String,
|
|
country: String,
|
|
skills: Option<String>,
|
|
job: Option<String>,
|
|
birth_year: Option<u32>,
|
|
|
|
membership_mode: MembershipMode,
|
|
inception_time: DateTime<Utc>,
|
|
subscription_amount: f64,
|
|
donation_amount: f64,
|
|
reference: String,
|
|
|
|
linked_user_first_name: Option<String>
|
|
}
|
|
|
|
fn get_accounting_year_for_time<'a>(accounting_years: &'a Vec<AccountingYear>, time: &'a DateTime<Utc>) -> Option<&'a AccountingYear> {
|
|
let date_ref = time.date_naive().clone();
|
|
accounting_years.iter().find(|year| year.start_date <= date_ref && date_ref <= year.end_date)
|
|
}
|
|
|
|
pub async fn sync_paheko(
|
|
paheko_client: &paheko::AuthentifiedClient,
|
|
config: &Config,
|
|
user_cache: &mut UserCache,
|
|
answers: Vec<GeneralizedAnswer>,
|
|
debit_account_code: &str,
|
|
via_name: &str
|
|
) -> Result<()> {
|
|
// FIXME: search existing paheko users using the first name and last name, some ppl don't have
|
|
// emails
|
|
|
|
struct Stats {
|
|
subscriptions_created: u32,
|
|
users_created: u32
|
|
}
|
|
|
|
let mut stats = Stats { subscriptions_created: 0, users_created: 0 };
|
|
|
|
let mut pk_memberships: Vec<paheko::Membership> = vec![];
|
|
|
|
let accounting_years = paheko_client.get_accounting_years().await.context("Get acc years")?;
|
|
|
|
// 1. get summary of existing paheko users
|
|
let mut existing_users = paheko_client.get_users().await.context("Get users")?;
|
|
// 2. get summary of transactions for that year
|
|
// TODO: also get for the year n-1
|
|
let existing_transactions = paheko_client.get_transactions(1).await.context("Get transactions")?;
|
|
// 3. get summary of services_users for that year
|
|
let existing_subscriptions = paheko_client.get_service_subscriptions(&config.paheko_target_activity_name)
|
|
.await.context("Get existing paheko subscriptions to the target activity")?;
|
|
|
|
// query paheko to get top ids
|
|
// IMPORTANT: this mean that while the script is running, there must be NO mutations to the
|
|
// users and services_users table on the paheko side
|
|
let mut pk_next_user_id = paheko_client.get_next_id("users")
|
|
.await.context("Get paheko users next id")?;
|
|
let mut pk_next_user_service_id = paheko_client.get_next_id("services_users")
|
|
.await.context("Get paheko services_users next id")?;
|
|
|
|
for answer_inp in &answers {
|
|
let mut answer = answer_inp.clone();
|
|
answer.first_name = answer.first_name.map(normalize_first_name);
|
|
answer.last_name = normalize_last_name(answer.last_name);
|
|
|
|
eprintln!("Processing answer:");
|
|
eprintln!(" name: {} {}", &answer.last_name, answer.first_name.clone().unwrap_or("".to_string()));
|
|
eprintln!(" email: {:?}", answer.email);
|
|
|
|
// list of users involved in this answer
|
|
let mut pk_users_summaries: Vec<paheko::UserSummary> = vec![];
|
|
let mut pk_user_service_registrations: Vec<paheko::UserServiceRegistration> = vec![];
|
|
|
|
// check for existing user in paheko by email
|
|
// TODO: check user with fuzzing first name and last name
|
|
let existing_user_opt = existing_users
|
|
.iter().find(|user| user.first_name == answer.first_name && user.last_name == answer.last_name)
|
|
.cloned();
|
|
|
|
// check for existing transactions
|
|
if existing_transactions.iter().any(
|
|
|summary| summary.reference == answer.reference
|
|
) {
|
|
eprintln!(" skipped: existing transaction found");
|
|
continue;
|
|
}
|
|
|
|
// dbg!(&existing_subscriptions);
|
|
let pk_user_summary = match existing_user_opt.clone() {
|
|
Some(user) => {
|
|
eprintln!(" Found existing paheko user by name.");
|
|
user
|
|
},
|
|
None => {
|
|
// create paheko user
|
|
let c = paheko_client.create_user(
|
|
&answer, pk_next_user_id
|
|
).await.context("Expected to create paheko user")?;
|
|
eprintln!(" Created paheko user");
|
|
pk_next_user_id += 1;
|
|
existing_users.push(c.clone());
|
|
stats.users_created += 1;
|
|
c
|
|
}
|
|
};
|
|
pk_users_summaries.push(pk_user_summary.clone());
|
|
|
|
// check if the user is already subscribed to the target activity
|
|
if
|
|
existing_user_opt.is_some() &&
|
|
existing_subscriptions.iter()
|
|
.find(|membership| membership.users_ids
|
|
.iter().find(|i| **i == pk_user_summary.id).is_some()
|
|
)
|
|
.is_some()
|
|
{
|
|
eprintln!(" skipped: user is already subscribed to this activity");
|
|
continue;
|
|
}
|
|
|
|
let mut pk_membership = paheko::Membership {
|
|
id: generate_id(),
|
|
service_name: config.paheko_target_activity_name.clone(),
|
|
// FIXME: handle errors when mode is invalid
|
|
mode_name: serde_json::to_value(answer.membership_mode.clone())
|
|
.unwrap().as_str().unwrap().to_string(),
|
|
start_time: answer.inception_time,
|
|
end_time:
|
|
DateTime::<Utc>::from_naive_utc_and_offset(
|
|
NaiveDate::from_ymd_opt(2024, 12, 31).unwrap().and_hms_opt(23, 59, 59).unwrap(),
|
|
Utc
|
|
),
|
|
payed_amount: answer.subscription_amount,
|
|
users_ids: vec![pk_user_summary.id.clone()]
|
|
};
|
|
|
|
// add activity for first member
|
|
// TODO: check if activity already exists
|
|
let user_registration = paheko_client.register_user_to_service(
|
|
pk_users_summaries.get(0).unwrap(),
|
|
&pk_membership,
|
|
pk_next_user_service_id
|
|
).await.context("Expected to register user activity to paheko")?;
|
|
pk_user_service_registrations.push(user_registration);
|
|
pk_next_user_service_id += 1;
|
|
eprintln!(" Created paheko activity registration");
|
|
|
|
// then create optional linked user
|
|
if answer.membership_mode == MembershipMode::Couple {
|
|
let mut second_answer = answer.clone();
|
|
second_answer.email = None;
|
|
second_answer.phone = None;
|
|
second_answer.skills = None;
|
|
second_answer.job = None;
|
|
second_answer.birth_year = None;
|
|
|
|
// add first_name
|
|
match answer.linked_user_first_name {
|
|
Some(name) => {
|
|
second_answer.first_name = Some(name);
|
|
},
|
|
None => {
|
|
second_answer.first_name = None;
|
|
eprintln!("Warn: Got a user with Couple mode but no additional name given!")
|
|
}
|
|
}
|
|
|
|
if existing_user_opt.is_none() {
|
|
// only create the linked user in paheko, if the first user was also created
|
|
let second_pk_user_summary = paheko_client.create_user(&second_answer, pk_next_user_id)
|
|
.await.context("Expected to create second paheko user")?;
|
|
eprintln!(" Created conjoint paheko user");
|
|
pk_users_summaries.push(second_pk_user_summary.clone());
|
|
pk_next_user_id += 1;
|
|
|
|
// create activity of second user
|
|
let user_registration = paheko_client.register_user_to_service(
|
|
pk_users_summaries.get(1).unwrap(), // pass user, for the id
|
|
&pk_membership,
|
|
pk_next_user_service_id
|
|
).await.context("Registering service to second paheko server")?;
|
|
pk_user_service_registrations.push(user_registration);
|
|
pk_next_user_service_id += 1;
|
|
eprintln!(" Created paheko activity registration for conjoint user");
|
|
|
|
pk_membership.users_ids.push(second_pk_user_summary.id)
|
|
}
|
|
// FIXME: reuse a previous user
|
|
// TODO: get existing linked user from previous year
|
|
|
|
}
|
|
|
|
// add transaction
|
|
let transaction = paheko::SimpleTransaction {
|
|
accounting_year: match get_accounting_year_for_time(&accounting_years, &answer.inception_time) {
|
|
None => {
|
|
eprintln!("Cannot find an accounting year on paheko that include the inception date {:?} given", &answer.inception_time);
|
|
panic!();
|
|
},
|
|
Some(s) => s
|
|
}.id.clone(),
|
|
// TODO: make the label template configurable
|
|
label: format!("{} {:?} via {}", pk_membership.service_name, pk_membership.mode_name, via_name),
|
|
amount: pk_membership.payed_amount,
|
|
reference: answer.reference,
|
|
// TODO: make these field configurable
|
|
credit_account_code: "756".to_string(), // cotisations account
|
|
debit_account_code: debit_account_code.to_string(), // helloasso account
|
|
inception_time: answer.inception_time,
|
|
kind: paheko::TransactionKind::Revenue,
|
|
linked_users: pk_users_summaries.iter().map(|x| x.id.clone()).collect(),
|
|
// the linked_services, depend on a patch to paheko API code to work (see https://forge.lefuturiste.fr/mbess/paheko-fork/commit/a4fdd816112f51db23a2b02ac160b0513a5b09c5)
|
|
linked_services: pk_user_service_registrations.iter().map(|x| x.id.clone()).collect()
|
|
};
|
|
let _ = paheko_client.register_transaction(transaction)
|
|
.await.context("Expected to create new paheko transaction")?;
|
|
eprintln!(" Created paheko transaction");
|
|
|
|
stats.subscriptions_created += 1;
|
|
|
|
// TODO: handle donation amount
|
|
|
|
pk_memberships.push(pk_membership);
|
|
}
|
|
eprintln!("{via_name} sync done.");
|
|
eprintln!("{} subs created; {} users created", stats.subscriptions_created, stats.users_created);
|
|
Ok(())
|
|
}
|