feat(helloasso): add dry run flag

This commit is contained in:
Matthieu Bessat 2024-01-21 18:10:13 +01:00
parent 700ba180b6
commit f8a5aac75f
5 changed files with 96 additions and 24 deletions

View file

@ -97,8 +97,7 @@ fn get_proxy_from_url(proxy_url: &Option<String>) -> Result<Option<reqwest::Prox
}) })
} }
async fn launch_adapter(source: SourceType, config: &Config) -> Result<()> { async fn launch_adapter(source: SourceType, config: &Config, dry_run: bool) -> Result<()> {
let mut user_cache = load_user_cache().context("Failed to load user cache")?; let mut user_cache = load_user_cache().context("Failed to load user cache")?;
if !&config.paheko_base_url.ends_with('/') { if !&config.paheko_base_url.ends_with('/') {
@ -114,11 +113,11 @@ async fn launch_adapter(source: SourceType, config: &Config) -> Result<()> {
client_id: config.paheko_client_id.clone(), client_id: config.paheko_client_id.clone(),
client_secret: config.paheko_client_secret.clone() client_secret: config.paheko_client_secret.clone()
}; };
let paheko_client: paheko::AuthentifiedClient = paheko_client.login(paheko_credentials).await?; let paheko_client = paheko_client.login(paheko_credentials).await.context("Paheko login")?;
match source { match source {
SourceType::Csv => sync_csv::sync_csv(&paheko_client, config, &mut user_cache).await?, SourceType::Csv => sync_csv::sync_csv(&paheko_client, config, &mut user_cache, dry_run).await?,
SourceType::Helloasso => sync_helloasso::sync_helloasso(&paheko_client, config, &mut user_cache).await? SourceType::Helloasso => sync_helloasso::sync_helloasso(&paheko_client, config, &mut user_cache, dry_run).await?
} }
Ok(()) Ok(())
@ -134,7 +133,11 @@ struct App {
/// output debug info /// output debug info
#[argh(switch, short = 'i')] #[argh(switch, short = 'i')]
info: bool env_info: bool,
/// dry run
#[argh(switch, short = 'd')]
dry_run: bool
} }
enum SourceType { enum SourceType {
@ -148,7 +151,7 @@ async fn main() {
dotenvy::dotenv().expect("Could not load dot env file"); dotenvy::dotenv().expect("Could not load dot env file");
let config: Config = envy::from_env().expect("Failed to load env vars"); let config: Config = envy::from_env().expect("Failed to load env vars");
if app.info { if app.env_info {
dbg!(config); dbg!(config);
return; return;
} }
@ -161,7 +164,7 @@ async fn main() {
return; return;
} }
}; };
let res = launch_adapter(source, &config).await; let res = launch_adapter(source, &config, app.dry_run).await;
match res { match res {
Err(err) => { Err(err) => {
eprintln!("Program failed, details bellow"); eprintln!("Program failed, details bellow");

View file

@ -318,7 +318,8 @@ impl AuthentifiedClient {
{ {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct Row { struct Row {
_id: u64, #[allow(dead_code)]
id: u64,
label: String, label: String,
reference: String, reference: String,
#[serde(deserialize_with = "deserialize_json_list")] #[serde(deserialize_with = "deserialize_json_list")]

View file

@ -33,7 +33,12 @@ fn process_price(value: String) -> f64 {
// read csv from stdin // read csv from stdin
pub async fn sync_csv(paheko_client: &paheko::AuthentifiedClient, config: &Config, _user_cache: &mut UserCache) -> Result<()> { pub async fn sync_csv(
paheko_client: &paheko::AuthentifiedClient,
config: &Config,
_user_cache: &mut UserCache,
_dry_run: bool
) -> Result<()> {
// raw row record directly from CSV // raw row record directly from CSV
#[derive(Debug, serde::Deserialize)] #[derive(Debug, serde::Deserialize)]
struct AnswerRecord { struct AnswerRecord {

View file

@ -2,15 +2,54 @@ use crate::helloasso;
use crate::paheko; use crate::paheko;
use crate::{ use crate::{
Config, UserCache, Config, UserCache,
get_proxy_from_url, get_auth_client_from_cache, get_proxy_from_url, write_user_cache
}; };
use crate::utils::{parse_and_get_birthday_year, parse_normalize_phone, normalize_str}; use crate::utils::{parse_and_get_birthday_year, parse_normalize_phone, normalize_str};
use crate::sync_paheko::{GeneralizedAnswer, sync_paheko}; use crate::sync_paheko::{GeneralizedAnswer, sync_paheko};
use anyhow::Result; use anyhow::{Context, Result};
use url::Url; use url::Url;
use email_address::EmailAddress; use email_address::EmailAddress;
/// Get authentified HelloAsso client
async fn get_auth_client_from_cache(
user_cache: &mut UserCache,
ha_client: &mut helloasso::Client,
login_payload: helloasso::LoginPayload
) -> Result<helloasso::AuthentifiedClient> {
// TODO: find a better way to have the logic implemented
async fn login(
user_cache: &mut UserCache,
ha_client: &mut helloasso::Client,
login_payload: helloasso::LoginPayload
) -> Result<helloasso::AuthentifiedClient> {
let auth_client = ha_client.login(login_payload).await.context("Failed to login to HelloAsso")?;
user_cache.helloasso_session = Some(auth_client.session.clone());
write_user_cache(user_cache).expect("Unable to write user cache");
eprintln!("HelloAsso: Logged in and wrote token to cache");
Ok(auth_client)
}
eprintln!("Init HA client");
match &user_cache.helloasso_session {
Some(cached_session) => {
let auth_client = ha_client.authentified_client(cached_session.clone());
if !auth_client.verify_auth().await? {
println!("HelloAsso: Need to relog, token invalid");
return login(user_cache, ha_client, login_payload).await
}
eprintln!("HelloAsso: Used anterior token");
Ok(auth_client)
},
None => {
eprintln!("HelloAsso: First time login");
login(user_cache, ha_client, login_payload).await
}
}
}
/// rust how to access inner enum value /// rust how to access inner enum value
#[derive(Debug, PartialEq, Clone, Copy)] #[derive(Debug, PartialEq, Clone, Copy)]
enum HelloassoCustomFieldType { enum HelloassoCustomFieldType {
@ -52,7 +91,14 @@ fn read_custom_field(form_answer: &helloasso::FormAnswer, custom_field: Helloass
} }
pub async fn sync_helloasso(paheko_client: &paheko::AuthentifiedClient, config: &Config, user_cache: &mut UserCache) -> Result<()> { pub async fn sync_helloasso(
paheko_client: &paheko::AuthentifiedClient,
config: &Config,
user_cache: &mut UserCache,
dry_run: bool
) -> Result<()> {
eprintln!("wow");
let mut ha_client: helloasso::Client = helloasso::Client::new(helloasso::ClientConfig { let mut ha_client: helloasso::Client = helloasso::Client::new(helloasso::ClientConfig {
base_url: Url::parse("https://api.helloasso.com/v5/") base_url: Url::parse("https://api.helloasso.com/v5/")
.expect("Expected valid helloasso API base URL"), .expect("Expected valid helloasso API base URL"),
@ -84,6 +130,7 @@ pub async fn sync_helloasso(paheko_client: &paheko::AuthentifiedClient, config:
.or(Some(answer.payer_user.email.clone())) .or(Some(answer.payer_user.email.clone()))
} }
let mut kept_original_answers: Vec<helloasso::FormAnswer> = vec![];
let mut generalized_answers: Vec<GeneralizedAnswer> = vec![]; let mut generalized_answers: Vec<GeneralizedAnswer> = vec![];
for answer in answers { for answer in answers {
let email = choose_email(&answer); let email = choose_email(&answer);
@ -132,10 +179,26 @@ pub async fn sync_helloasso(paheko_client: &paheko::AuthentifiedClient, config:
generalized_answer.email = None; generalized_answer.email = None;
generalized_answer.phone = None; generalized_answer.phone = None;
} }
if answer.id == 64756581 {
if let Some(wrong_first_name) = generalized_answer.first_name {
let compos: Vec<&str> = wrong_first_name.split(" et ").collect();
generalized_answer.first_name = Some(compos.get(0).unwrap().to_string());
generalized_answer.linked_user_first_name = Some(compos.get(1).unwrap().to_string());
}
}
generalized_answers.push(generalized_answer); generalized_answers.push(generalized_answer);
kept_original_answers.push(answer);
} }
println!("Generated GeneralizedAnswers"); println!("Generated GeneralizedAnswers");
if dry_run {
eprintln!("Will stop because dry run mode is enabled.");
list_answers(kept_original_answers, generalized_answers);
return Ok(());
}
sync_paheko( sync_paheko(
paheko_client, paheko_client,
config, config,
@ -145,4 +208,12 @@ pub async fn sync_helloasso(paheko_client: &paheko::AuthentifiedClient, config:
).await ).await
} }
fn list_answers(original_answers: Vec<helloasso::FormAnswer>, generalized_answers: Vec<GeneralizedAnswer>) {
for (ha_answer, ga) in std::iter::zip(original_answers, generalized_answers) {
println!(
"{} {:?} {:?} {:?} {:?}",
ha_answer.id, ga.first_name, ga.last_name,
ga.email, ga.linked_user_first_name
);
}
}

View file

@ -81,8 +81,7 @@ pub async fn sync_paheko(
// 1. get summary of existing paheko users // 1. get summary of existing paheko users
let mut existing_users = paheko_client.get_users().await.context("Get users")?; let mut existing_users = paheko_client.get_users().await.context("Get users")?;
// 2. get summary of transactions for that year // 2. get summary of transactions for the years we will use
// TODO: also get for the year n-1
let existing_transactions = paheko_client.get_transactions(&config.paheko_accounting_years_ids) let existing_transactions = paheko_client.get_transactions(&config.paheko_accounting_years_ids)
.await.context("Get transactions")?; .await.context("Get transactions")?;
// 3. get summary of services_users for that year // 3. get summary of services_users for that year
@ -116,7 +115,6 @@ pub async fn sync_paheko(
.filter(|t| t.reference == answer.reference) .filter(|t| t.reference == answer.reference)
.collect(); .collect();
// dbg!(&existing_subscriptions);
// check for existing user in paheko by email // check for existing user in paheko by email
// TODO: check user with fuzzing first name and last name // TODO: check user with fuzzing first name and last name
let existing_user_opt = existing_users let existing_user_opt = existing_users
@ -166,7 +164,6 @@ pub async fn sync_paheko(
eprintln!(" User is already subscribed to this activity"); eprintln!(" User is already subscribed to this activity");
} else { } else {
// add activity for first member // add activity for first member
// TODO: check if activity already exists
let user_registration = paheko_client.register_user_to_service( let user_registration = paheko_client.register_user_to_service(
pk_users_summaries.get(0).unwrap(), pk_users_summaries.get(0).unwrap(),
&pk_membership, &pk_membership,
@ -191,7 +188,7 @@ pub async fn sync_paheko(
// add first_name // add first_name
match answer.linked_user_first_name { match answer.linked_user_first_name {
Some(name) => { Some(name) => {
second_answer.first_name = Some(name); second_answer.first_name = Some(normalize_first_name(name));
}, },
None => { None => {
second_answer.first_name = None; second_answer.first_name = None;
@ -237,13 +234,11 @@ pub async fn sync_paheko(
label: format!("{} {:?} via {}", pk_membership.service_name, pk_membership.mode_name, via_name), label: format!("{} {:?} via {}", pk_membership.service_name, pk_membership.mode_name, via_name),
amount: pk_membership.payed_amount, amount: pk_membership.payed_amount,
reference: answer.reference.clone(), reference: answer.reference.clone(),
// TODO: make these field configurable
credit_account_code: "756".to_string(), // cotisations account credit_account_code: "756".to_string(), // cotisations account
debit_account_code: debit_account_code.to_string(), // helloasso account debit_account_code: debit_account_code.to_string(), // helloasso account
inception_time: answer.inception_time, inception_time: answer.inception_time,
kind: paheko::TransactionKind::Revenue, kind: paheko::TransactionKind::Revenue,
linked_users: pk_users_summaries.iter().map(|x| x.id.clone()).collect(), 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_subscriptions: pk_user_service_registrations.iter().map(|x| x.id.clone()).collect() linked_subscriptions: pk_user_service_registrations.iter().map(|x| x.id.clone()).collect()
}; };
paheko_client.register_transaction(transaction) paheko_client.register_transaction(transaction)
@ -286,9 +281,6 @@ pub async fn sync_paheko(
} }
} }
// TODO: handle donation amount
pk_memberships.push(pk_membership); pk_memberships.push(pk_membership);
} }
eprintln!("{via_name} sync done."); eprintln!("{via_name} sync done.");