refactor: cleaning up
This commit is contained in:
parent
05eab87c3a
commit
0f15e64ba2
4 changed files with 76 additions and 118 deletions
|
@ -19,10 +19,6 @@ struct WebSession {
|
||||||
jwt: String
|
jwt: String
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum LoginError {
|
|
||||||
TransportFailure(reqwest::Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
#[fully_pub]
|
#[fully_pub]
|
||||||
struct Client {
|
struct Client {
|
||||||
|
@ -54,6 +50,8 @@ impl Client {
|
||||||
fn get_base_client_builder() -> reqwest::ClientBuilder {
|
fn get_base_client_builder() -> reqwest::ClientBuilder {
|
||||||
let mut default_headers = reqwest::header::HeaderMap::new();
|
let mut default_headers = reqwest::header::HeaderMap::new();
|
||||||
default_headers.insert("Accept", "application/json".parse().unwrap());
|
default_headers.insert("Accept", "application/json".parse().unwrap());
|
||||||
|
// decoy user agent
|
||||||
|
default_headers.insert("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/112.0".parse().unwrap());
|
||||||
|
|
||||||
let proxy = reqwest::Proxy::https("https://localhost:8999").unwrap();
|
let proxy = reqwest::Proxy::https("https://localhost:8999").unwrap();
|
||||||
reqwest::Client::builder()
|
reqwest::Client::builder()
|
||||||
|
@ -92,14 +90,14 @@ impl Client {
|
||||||
}
|
}
|
||||||
let value = String::from(value_raw.to_str().unwrap());
|
let value = String::from(value_raw.to_str().unwrap());
|
||||||
if value.starts_with("tm5-HelloAsso") {
|
if value.starts_with("tm5-HelloAsso") {
|
||||||
let jwt = value.split("tm5-HelloAsso=").nth(1)?.split(";").nth(0)?.trim().to_string();
|
let jwt = value.split("tm5-HelloAsso=").nth(1)?.split(';').next()?.trim().to_string();
|
||||||
return Some(jwt);
|
return Some(jwt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
let jwt = get_jwt_from_cookies_headers(&res.headers())
|
let jwt = get_jwt_from_cookies_headers(res.headers())
|
||||||
.context("Failed to find or parse JWT from login response")?;
|
.context("Failed to find or parse JWT from login response")?;
|
||||||
|
|
||||||
let session = WebSession { jwt };
|
let session = WebSession { jwt };
|
||||||
|
@ -166,14 +164,6 @@ struct UserDetails {
|
||||||
last_name: String
|
last_name: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// #[derive(Debug, Serialize, Deserialize)]
|
|
||||||
// #[serde(rename_all = "camelCase")]
|
|
||||||
// struct OrderDetails {
|
|
||||||
// date:
|
|
||||||
// form_
|
|
||||||
// }
|
|
||||||
|
|
||||||
impl AuthentifiedClient {
|
impl AuthentifiedClient {
|
||||||
/// each time we need to change the token, we will need to rebuild the client
|
/// each time we need to change the token, we will need to rebuild the client
|
||||||
pub fn new(base_url: Url, session: WebSession) -> Self {
|
pub fn new(base_url: Url, session: WebSession) -> Self {
|
||||||
|
@ -194,7 +184,7 @@ impl AuthentifiedClient {
|
||||||
let res = self.client
|
let res = self.client
|
||||||
.get(self.base_url.join("agg/user")?)
|
.get(self.base_url.join("agg/user")?)
|
||||||
.send().await?;
|
.send().await?;
|
||||||
return Ok(res.status() == 200);
|
Ok(res.status() == 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_user_details(&self) -> Result<serde_json::Value> {
|
pub async fn get_user_details(&self) -> Result<serde_json::Value> {
|
||||||
|
@ -247,7 +237,7 @@ impl AuthentifiedClient {
|
||||||
return Err(anyhow!("Unexpected json value in data bundle"));
|
return Err(anyhow!("Unexpected json value in data bundle"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if page_items.len() == 0 {
|
if page_items.is_empty() {
|
||||||
return Ok(data);
|
return Ok(data);
|
||||||
}
|
}
|
||||||
data.extend(page_items);
|
data.extend(page_items);
|
||||||
|
@ -319,7 +309,7 @@ impl Organization {
|
||||||
Ok(details)
|
Ok(details)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_form_answers(&self, form_slug: String) -> Result<Vec<FormAnswer>> {
|
pub async fn get_form_answers(&self, form_slug: &str) -> Result<Vec<FormAnswer>> {
|
||||||
let data = self.client.fetch_with_pagination(
|
let data = self.client.fetch_with_pagination(
|
||||||
format!("organizations/{}/forms/Membership/{}/participants?withDetails=true", self.slug, form_slug)
|
format!("organizations/{}/forms/Membership/{}/participants?withDetails=true", self.slug, form_slug)
|
||||||
).await?;
|
).await?;
|
||||||
|
|
150
src/main.rs
150
src/main.rs
|
@ -4,16 +4,14 @@ mod helloasso;
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use chrono::prelude::{NaiveDate, NaiveDateTime, DateTime, Utc, Datelike};
|
use chrono::prelude::{NaiveDate, DateTime, Utc, Datelike};
|
||||||
use strum::Display;
|
use strum::Display;
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use std::collections::HashSet;
|
|
||||||
use phonenumber;
|
|
||||||
use utils::generate_id;
|
use utils::generate_id;
|
||||||
use paheko::UserSummary;
|
|
||||||
|
|
||||||
/// permanent config to store long-term config
|
/// permanent config to store long-term config
|
||||||
/// used to ingest env settings
|
/// used to ingest env settings
|
||||||
|
/// config loaded from env variables
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
struct Config {
|
struct Config {
|
||||||
helloasso_email: String,
|
helloasso_email: String,
|
||||||
|
@ -23,12 +21,6 @@ struct Config {
|
||||||
paheko_client_secret: String,
|
paheko_client_secret: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
static APP_USER_AGENT: &str = concat!(
|
|
||||||
env!("CARGO_PKG_NAME"),
|
|
||||||
"/",
|
|
||||||
env!("CARGO_PKG_VERSION"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// start user cache management
|
// start user cache management
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
|
@ -51,16 +43,16 @@ enum LoadError {
|
||||||
|
|
||||||
fn write_user_cache(cache: &UserCache) -> Result<(), LoadError> {
|
fn write_user_cache(cache: &UserCache) -> Result<(), LoadError> {
|
||||||
let xdg_dirs = xdg::BaseDirectories::with_prefix(env!("CARGO_PKG_NAME"))
|
let xdg_dirs = xdg::BaseDirectories::with_prefix(env!("CARGO_PKG_NAME"))
|
||||||
.map_err(|e| { LoadError::XDG })?;
|
.map_err(|_e| { LoadError::XDG })?;
|
||||||
let user_cache_path = xdg_dirs.place_cache_file("session.json").map_err(|e| { LoadError::FailedToCreate })?;
|
let user_cache_path = xdg_dirs.place_cache_file("session.json").map_err(|_e| { LoadError::FailedToCreate })?;
|
||||||
let encoded_cache = serde_json::to_string(&cache).map_err(|e| { LoadError::FailedToEncode })?;
|
let encoded_cache = serde_json::to_string(&cache).map_err(|_e| { LoadError::FailedToEncode })?;
|
||||||
fs::write(&user_cache_path, encoded_cache.as_str()).map_err(|e| { LoadError::FailedToWrite })?;
|
fs::write(user_cache_path, encoded_cache.as_str()).map_err(|_e| { LoadError::FailedToWrite })?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_user_cache() -> Result<UserCache, LoadError> {
|
fn load_user_cache() -> Result<UserCache, LoadError> {
|
||||||
let xdg_dirs = xdg::BaseDirectories::with_prefix(env!("CARGO_PKG_NAME"))
|
let xdg_dirs = xdg::BaseDirectories::with_prefix(env!("CARGO_PKG_NAME"))
|
||||||
.map_err(|e| { LoadError::XDG })?;
|
.map_err(|_e| { LoadError::XDG })?;
|
||||||
let user_cache_path = xdg_dirs.get_cache_file("session.json");
|
let user_cache_path = xdg_dirs.get_cache_file("session.json");
|
||||||
|
|
||||||
if !user_cache_path.exists() {
|
if !user_cache_path.exists() {
|
||||||
|
@ -70,23 +62,11 @@ fn load_user_cache() -> Result<UserCache, LoadError> {
|
||||||
write_user_cache(&default_cache)?;
|
write_user_cache(&default_cache)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let session_content = fs::read_to_string(user_cache_path).map_err(|e| { LoadError::Fs })?;
|
let session_content = fs::read_to_string(user_cache_path).map_err(|_e| { LoadError::Fs })?;
|
||||||
let cache: UserCache = serde_json::from_str(&session_content).map_err(|e| { LoadError::FailedToParse })?;
|
let cache: UserCache = serde_json::from_str(&session_content).map_err(|_e| { LoadError::FailedToParse })?;
|
||||||
|
|
||||||
Ok(cache)
|
Ok(cache)
|
||||||
}
|
}
|
||||||
// todo:
|
|
||||||
// - make pagination working
|
|
||||||
// - create paheko client
|
|
||||||
// - get current paheko membership
|
|
||||||
// - function to convert participants to paheko members
|
|
||||||
// - clean up names and things
|
|
||||||
// - map custom fields with the right thing
|
|
||||||
// - handle linked users
|
|
||||||
|
|
||||||
fn get_paheko_membership_from_ha_answers() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: find a better way to have the logic implemented
|
// TODO: find a better way to have the logic implemented
|
||||||
async fn get_auth_client_from_cache(
|
async fn get_auth_client_from_cache(
|
||||||
|
@ -115,16 +95,16 @@ async fn get_auth_client_from_cache(
|
||||||
|
|
||||||
if !auth_client.verify_auth().await? {
|
if !auth_client.verify_auth().await? {
|
||||||
println!("Need to relog, token invalid");
|
println!("Need to relog, token invalid");
|
||||||
return Ok(login(user_cache, ha_client, login_payload).await?)
|
return login(user_cache, ha_client, login_payload).await
|
||||||
}
|
}
|
||||||
println!("Used anterior token");
|
println!("Used anterior token");
|
||||||
return Ok(auth_client);
|
Ok(auth_client)
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
println!("First time login");
|
println!("First time login");
|
||||||
return Ok(login(user_cache, ha_client, login_payload).await?);
|
login(user_cache, ha_client, login_payload).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -165,7 +145,7 @@ fn read_custom_field(form_answer: &helloasso::FormAnswer, custom_field: Helloass
|
||||||
// FIXME: compute the type directly at deserialization with serde
|
// FIXME: compute the type directly at deserialization with serde
|
||||||
form_answer.custom_fields.iter()
|
form_answer.custom_fields.iter()
|
||||||
.find(|f| HelloassoCustomFieldType::try_from(f.name.as_str()) == Ok(custom_field))
|
.find(|f| HelloassoCustomFieldType::try_from(f.name.as_str()) == Ok(custom_field))
|
||||||
.and_then(|cf| Some(cf.answer.clone()))
|
.map(|cf| cf.answer.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_normalize_phone(phone_number_opt: Option<String>) -> Option<String> {
|
fn parse_normalize_phone(phone_number_opt: Option<String>) -> Option<String> {
|
||||||
|
@ -190,20 +170,11 @@ fn normalize_str(subject: String) -> String {
|
||||||
|
|
||||||
/// remove year precision to comply with GDPR eu rules
|
/// remove year precision to comply with GDPR eu rules
|
||||||
fn parse_and_get_birthday_year(raw_date: String) -> Option<u32> {
|
fn parse_and_get_birthday_year(raw_date: String) -> Option<u32> {
|
||||||
let d_res = NaiveDate::parse_from_str(&raw_date.trim(), "%d/%m/%Y");
|
let d_res = NaiveDate::parse_from_str(raw_date.trim(), "%d/%m/%Y");
|
||||||
let d = d_res.ok()?;
|
let d = d_res.ok()?;
|
||||||
Some(d.year().try_into().ok()?)
|
d.year().try_into().ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
// fn helloasso_to_paheko_membership(helloasso_membership: &helloasso::MembershipMode) -> paheko::MembershipMode {
|
|
||||||
// match helloasso_membership {
|
|
||||||
// helloasso::MembershipMode::Couple => paheko::MembershipMode::Couple,
|
|
||||||
// helloasso::MembershipMode::Individual => paheko::MembershipMode::Individual,
|
|
||||||
// helloasso::MembershipMode::BenefactorCouple => paheko::MembershipMode::BenefactorCouple,
|
|
||||||
// helloasso::MembershipMode::BenefactorIndividual => paheko::MembershipMode::BenefactorIndividual
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
async fn launch_adapter() -> Result<()> {
|
async fn launch_adapter() -> Result<()> {
|
||||||
dotenvy::dotenv()?;
|
dotenvy::dotenv()?;
|
||||||
|
|
||||||
|
@ -228,27 +199,23 @@ async fn launch_adapter() -> Result<()> {
|
||||||
};
|
};
|
||||||
let auth_client: helloasso::AuthentifiedClient = get_auth_client_from_cache(&mut user_cache, &mut ha_client, login_payload).await?;
|
let auth_client: helloasso::AuthentifiedClient = get_auth_client_from_cache(&mut user_cache, &mut ha_client, login_payload).await?;
|
||||||
|
|
||||||
// dbg!(auth_client.get_user_details().await?);
|
// FIXME: make it configurable
|
||||||
|
let ha_org_slug = "l-etoile-de-bethleem-association-des-amis-de-la-chapelle-de-bethleem-d-aubevoye";
|
||||||
|
// FIXME: make it configurable
|
||||||
|
let pk_target_campaign_name = "Cotisation 2023-2024";
|
||||||
|
let ha_form_name = "2023-2024";
|
||||||
|
|
||||||
let slug = "l-etoile-de-bethleem-association-des-amis-de-la-chapelle-de-bethleem-d-aubevoye";
|
let org: helloasso::Organization = auth_client.organization(ha_org_slug);
|
||||||
|
let answers = org.get_form_answers(ha_form_name).await?;
|
||||||
let org: helloasso::Organization = auth_client.organization(slug);
|
|
||||||
// dbg!(org.get_details().await?);
|
|
||||||
let answers = org.get_form_answers("2023-2024".to_string()).await?;
|
|
||||||
|
|
||||||
// dbg!(&answers);
|
// dbg!(&answers);
|
||||||
println!("Got {} answers to the membership form. Processing...", &answers.len());
|
println!("Got {} answers to the membership form. Processing...", &answers.len());
|
||||||
|
|
||||||
// first, request the current list of membership in paheko that were created with helloasso
|
|
||||||
// get the list of payments associated
|
|
||||||
|
|
||||||
// first step: output a list of PahekoUser with PahekoMembership
|
|
||||||
let mut pk_memberships: Vec<paheko::Membership> = vec![];
|
let mut pk_memberships: Vec<paheko::Membership> = vec![];
|
||||||
|
|
||||||
// read_custom_field(&answer, HelloAssoCustomFieldType::Email).or(Some(answer.payer_user.email.clone())),
|
|
||||||
use email_address::*;
|
use email_address::*;
|
||||||
fn choose_email(answer: &helloasso::FormAnswer) -> Option<String> {
|
fn choose_email(answer: &helloasso::FormAnswer) -> Option<String> {
|
||||||
read_custom_field(&answer, HelloassoCustomFieldType::Email)
|
read_custom_field(answer, HelloassoCustomFieldType::Email)
|
||||||
.and_then(|x| {
|
.and_then(|x| {
|
||||||
if !EmailAddress::is_valid(&x) {
|
if !EmailAddress::is_valid(&x) {
|
||||||
None
|
None
|
||||||
|
@ -259,28 +226,28 @@ async fn launch_adapter() -> Result<()> {
|
||||||
.or(Some(answer.payer_user.email.clone()))
|
.or(Some(answer.payer_user.email.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// get summary of 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")?;
|
||||||
// get summary of transactions for that year
|
// 2. get summary of transactions for that year
|
||||||
let existing_transactions = paheko_client.get_transactions(1).await.context("Get transactions")?;
|
let existing_transactions = paheko_client.get_transactions(1).await.context("Get transactions")?;
|
||||||
|
|
||||||
// TODO: before creating any users, get the current maximum id of the users table to predict
|
// query paheko to get top ids
|
||||||
// the next auto-incrementing id.
|
// IMPORTANT: this mean that while the script is running, there must be NO mutations to the
|
||||||
let mut pk_next_user_id = paheko_client.get_next_id(&"users")
|
// 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")?;
|
.await.context("Get paheko users next id")?;
|
||||||
let mut pk_next_user_service_id = paheko_client.get_next_id(&"services_users")
|
let mut pk_next_user_service_id = paheko_client.get_next_id("services_users")
|
||||||
.await.context("Get paheko services_users next id")?;
|
.await.context("Get paheko services_users next id")?;
|
||||||
|
|
||||||
for answer in answers {
|
for answer in answers {
|
||||||
|
eprintln!("Processing answer:");
|
||||||
|
let email = choose_email(&answer);
|
||||||
|
eprintln!(" email: {:?}", email);
|
||||||
|
|
||||||
// list of users involved in this answer
|
// list of users involved in this answer
|
||||||
let mut pk_users_summaries: Vec<paheko::UserSummary> = vec![];
|
let mut pk_users_summaries: Vec<paheko::UserSummary> = vec![];
|
||||||
let mut pk_user_service_registrations: Vec<paheko::UserServiceRegistration> = vec![];
|
let mut pk_user_service_registrations: Vec<paheko::UserServiceRegistration> = vec![];
|
||||||
|
|
||||||
eprintln!("Processing answer:");
|
|
||||||
let email = choose_email(&answer);
|
|
||||||
|
|
||||||
eprintln!(" email: {:?}", email);
|
|
||||||
|
|
||||||
let mut pk_user = paheko::User {
|
let mut pk_user = paheko::User {
|
||||||
id: utils::Id(0),
|
id: utils::Id(0),
|
||||||
first_name: Some(normalize_str(answer.user.first_name.clone())),
|
first_name: Some(normalize_str(answer.user.first_name.clone())),
|
||||||
|
@ -290,12 +257,13 @@ async fn launch_adapter() -> Result<()> {
|
||||||
skills: read_custom_field(&answer, HelloassoCustomFieldType::Skills).map(normalize_str),
|
skills: read_custom_field(&answer, HelloassoCustomFieldType::Skills).map(normalize_str),
|
||||||
address: read_custom_field(&answer, HelloassoCustomFieldType::Address)
|
address: read_custom_field(&answer, HelloassoCustomFieldType::Address)
|
||||||
.map(normalize_str)
|
.map(normalize_str)
|
||||||
.expect("to have address"),
|
.expect("Expected ha answer to have address"),
|
||||||
postal_code: read_custom_field(&answer, HelloassoCustomFieldType::PostalCode).expect("to have postal code"),
|
postal_code: read_custom_field(&answer, HelloassoCustomFieldType::PostalCode)
|
||||||
|
.expect("Expected ha answer to have postalcode"),
|
||||||
city: read_custom_field(&answer, HelloassoCustomFieldType::City)
|
city: read_custom_field(&answer, HelloassoCustomFieldType::City)
|
||||||
.map(normalize_str)
|
.map(normalize_str)
|
||||||
.expect("to have city"),
|
.expect("Expected ha answer to have city"),
|
||||||
country: answer.payer_user.country.clone().trim()[..=1].to_string(), // ISO 3166-1 alpha-2
|
country: answer.payer_user.country.clone().trim()[..=1].to_string(), // we expect country code ISO 3166-1 alpha-2
|
||||||
job: read_custom_field(&answer, HelloassoCustomFieldType::Job).map(normalize_str),
|
job: read_custom_field(&answer, HelloassoCustomFieldType::Job).map(normalize_str),
|
||||||
birth_year: read_custom_field(&answer, HelloassoCustomFieldType::Birthday).and_then(parse_and_get_birthday_year),
|
birth_year: read_custom_field(&answer, HelloassoCustomFieldType::Birthday).and_then(parse_and_get_birthday_year),
|
||||||
register_time: answer.order.inception_time,
|
register_time: answer.order.inception_time,
|
||||||
|
@ -309,10 +277,10 @@ async fn launch_adapter() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for existing transactions
|
// check for existing transactions
|
||||||
if let Some(_) = existing_transactions.iter().find(
|
if existing_transactions.iter().any(
|
||||||
|summary| summary.reference == format!("HA/{}", answer.id)
|
|summary| summary.reference == format!("HA/{}", answer.id)
|
||||||
) {
|
) {
|
||||||
eprintln!(" Skipped: existing transaction found");
|
eprintln!(" skipped: existing transaction found");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -323,7 +291,7 @@ async fn launch_adapter() -> Result<()> {
|
||||||
Some(user) => user,
|
Some(user) => user,
|
||||||
None => {
|
None => {
|
||||||
let c = paheko_client.create_user(
|
let c = paheko_client.create_user(
|
||||||
&pk_user, pk_next_user_id.clone()
|
&pk_user, pk_next_user_id
|
||||||
).await.context("Expected to create paheko user")?;
|
).await.context("Expected to create paheko user")?;
|
||||||
eprintln!(" Created paheko user");
|
eprintln!(" Created paheko user");
|
||||||
pk_next_user_id += 1;
|
pk_next_user_id += 1;
|
||||||
|
@ -335,7 +303,7 @@ async fn launch_adapter() -> Result<()> {
|
||||||
|
|
||||||
let mut pk_membership = paheko::Membership {
|
let mut pk_membership = paheko::Membership {
|
||||||
id: generate_id(),
|
id: generate_id(),
|
||||||
campaign_name: "Cotisation 2023-2024".to_string(),
|
campaign_name: pk_target_campaign_name.to_string(),
|
||||||
// FIXME: handle errors
|
// FIXME: handle errors
|
||||||
mode_name: serde_json::to_value(answer.mode.clone())
|
mode_name: serde_json::to_value(answer.mode.clone())
|
||||||
.unwrap().as_str().unwrap().to_string(),
|
.unwrap().as_str().unwrap().to_string(),
|
||||||
|
@ -357,13 +325,13 @@ async fn launch_adapter() -> Result<()> {
|
||||||
|
|
||||||
// add activity for first member
|
// add activity for first member
|
||||||
let user_registration = paheko_client.register_user_to_service(
|
let user_registration = paheko_client.register_user_to_service(
|
||||||
pk_users_summaries.iter().nth(0).unwrap(),
|
pk_users_summaries.get(0).unwrap(),
|
||||||
&pk_membership,
|
&pk_membership,
|
||||||
pk_next_user_service_id.clone()
|
pk_next_user_service_id
|
||||||
).await.context("Registering user to paheko server")?;
|
).await.context("Expected to register user activity to paheko")?;
|
||||||
pk_user_service_registrations.push(user_registration);
|
pk_user_service_registrations.push(user_registration);
|
||||||
pk_next_user_service_id += 1;
|
pk_next_user_service_id += 1;
|
||||||
|
eprintln!(" Created paheko activity registration");
|
||||||
|
|
||||||
// then create optional linked user
|
// then create optional linked user
|
||||||
if answer.mode == helloasso::MembershipMode::Couple {
|
if answer.mode == helloasso::MembershipMode::Couple {
|
||||||
|
@ -395,15 +363,13 @@ async fn launch_adapter() -> Result<()> {
|
||||||
|
|
||||||
// create activity of second user
|
// create activity of second user
|
||||||
let user_registration = paheko_client.register_user_to_service(
|
let user_registration = paheko_client.register_user_to_service(
|
||||||
pk_users_summaries.iter().nth(1).unwrap(),
|
pk_users_summaries.get(1).unwrap(),
|
||||||
&pk_membership,
|
&pk_membership,
|
||||||
pk_next_user_service_id.clone()
|
pk_next_user_service_id
|
||||||
).await.context("Registering service to second paheko server")?;
|
).await.context("Registering service to second paheko server")?;
|
||||||
pk_user_service_registrations.push(user_registration);
|
pk_user_service_registrations.push(user_registration);
|
||||||
pk_next_user_service_id += 1;
|
pk_next_user_service_id += 1;
|
||||||
|
eprintln!(" Created paheko activity registration for conjoint user");
|
||||||
// FIXME: follow the ids of the services registrations, to be able to later
|
|
||||||
// reference that user service
|
|
||||||
}
|
}
|
||||||
// TODO: get existing linked user from previous year
|
// TODO: get existing linked user from previous year
|
||||||
|
|
||||||
|
@ -412,28 +378,34 @@ async fn launch_adapter() -> Result<()> {
|
||||||
|
|
||||||
// add transaction
|
// add transaction
|
||||||
let transaction = paheko::SimpleTransaction {
|
let transaction = paheko::SimpleTransaction {
|
||||||
label: "Adhésion Helloasso".to_string(),
|
// TODO: make the label template configurable
|
||||||
|
label: format!("Adhésion {:?} via HelloAsso", pk_membership.mode_name),
|
||||||
amount: pk_membership.payed_amount,
|
amount: pk_membership.payed_amount,
|
||||||
reference: format!("HA/{}", pk_membership.external_references.helloasso_refs.answer_id),
|
reference: format!("HA/{}", pk_membership.external_references.helloasso_refs.answer_id),
|
||||||
|
// TODO: make these field configurable
|
||||||
credit_account_code: "756".to_string(), // cotisations account
|
credit_account_code: "756".to_string(), // cotisations account
|
||||||
debit_account_code: "512HA".to_string(), // helloasso account
|
debit_account_code: "512HA".to_string(), // helloasso account
|
||||||
inception_time: answer.order.inception_time,
|
inception_time: answer.order.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(),
|
||||||
|
// this depend on a patch to paheko API code to work
|
||||||
linked_services: pk_user_service_registrations.iter().map(|x| x.id.clone()).collect()
|
linked_services: pk_user_service_registrations.iter().map(|x| x.id.clone()).collect()
|
||||||
};
|
};
|
||||||
let _ = paheko_client.register_transaction(transaction)
|
let _ = paheko_client.register_transaction(transaction)
|
||||||
.await.context("Expected to create new paheko transaction");
|
.await.context("Expected to create new paheko transaction");
|
||||||
|
eprintln!(" Created paheko transaction");
|
||||||
|
|
||||||
pk_memberships.push(pk_membership);
|
pk_memberships.push(pk_membership);
|
||||||
}
|
}
|
||||||
dbg!(&pk_memberships.len());
|
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("Done.");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let res = launch_adapter().await;
|
// TODO: add argument parser to have handle config file
|
||||||
dbg!(res);
|
let _res = launch_adapter().await;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ use url::Url;
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use fully_pub::fully_pub;
|
use fully_pub::fully_pub;
|
||||||
use crate::utils::Id;
|
use crate::utils::Id;
|
||||||
use chrono::prelude::{NaiveDate, DateTime, Utc, Datelike};
|
use chrono::prelude::{DateTime, Utc};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
@ -72,9 +72,9 @@ enum TransactionKind {
|
||||||
Revenue
|
Revenue
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<String> for TransactionKind {
|
impl From<TransactionKind> for String {
|
||||||
fn into(self) -> String {
|
fn from(val: TransactionKind) -> Self {
|
||||||
match self {
|
match val {
|
||||||
TransactionKind::Expense => "EXPENSE".to_string(),
|
TransactionKind::Expense => "EXPENSE".to_string(),
|
||||||
TransactionKind::Revenue => "REVENUE".to_string()
|
TransactionKind::Revenue => "REVENUE".to_string()
|
||||||
}
|
}
|
||||||
|
@ -250,7 +250,7 @@ impl AuthentifiedClient {
|
||||||
dbg!(res);
|
dbg!(res);
|
||||||
return Err(APIClientError::InvalidStatusCode.into());
|
return Err(APIClientError::InvalidStatusCode.into());
|
||||||
}
|
}
|
||||||
Ok(res.json().await.context("Sql query")?)
|
res.json().await.context("Sql query")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_users(&self) -> Result<Vec<UserSummary>> {
|
pub async fn get_users(&self) -> Result<Vec<UserSummary>> {
|
||||||
|
@ -277,7 +277,7 @@ impl AuthentifiedClient {
|
||||||
|
|
||||||
let ids: Vec<Entry> = serde_json::from_value(data.results)?;
|
let ids: Vec<Entry> = serde_json::from_value(data.results)?;
|
||||||
|
|
||||||
Ok(ids.iter().nth(0).map(|x| x.id).unwrap_or(1)+1)
|
Ok(ids.get(0).map(|x| x.id).unwrap_or(1)+1)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_transactions(&self, id_year: u32)
|
pub async fn get_transactions(&self, id_year: u32)
|
||||||
|
@ -355,7 +355,7 @@ impl AuthentifiedClient {
|
||||||
let mut csv_content: String = String::new();
|
let mut csv_content: String = String::new();
|
||||||
csv_content.push_str(
|
csv_content.push_str(
|
||||||
r#""Numéro de membre","Activité","Tarif","Date d'inscription","Date d'expiration","Montant à régler","Payé ?""#);
|
r#""Numéro de membre","Activité","Tarif","Date d'inscription","Date d'expiration","Montant à régler","Payé ?""#);
|
||||||
csv_content.push_str("\n");
|
csv_content.push('\n');
|
||||||
csv_content.push_str(
|
csv_content.push_str(
|
||||||
format!("{},{:?},{:?},{:?},{:?},{:?},{:?}\n",
|
format!("{},{:?},{:?},{:?},{:?},{:?},{:?}\n",
|
||||||
u.id,
|
u.id,
|
||||||
|
|
|
@ -6,11 +6,7 @@ use chrono::prelude::{DateTime, Utc};
|
||||||
/// ID
|
/// ID
|
||||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct Id(pub u64);
|
pub struct Id(pub u64);
|
||||||
impl Id {
|
|
||||||
pub fn to_string(&self) -> String {
|
|
||||||
format!("{:x}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Into<String> for Id {
|
impl Into<String> for Id {
|
||||||
fn into(self) -> String {
|
fn into(self) -> String {
|
||||||
format!("{:x}", self.0)
|
format!("{:x}", self.0)
|
||||||
|
|
Loading…
Reference in a new issue