feat: activity and transaction registration

This commit is contained in:
Matthieu Bessat 2023-12-28 02:23:34 +01:00
parent 1fcf35eaa7
commit 05eab87c3a
3 changed files with 163 additions and 57 deletions

View file

@ -296,7 +296,7 @@ struct OrderDetails {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[fully_pub] #[fully_pub]
struct FormAnswer { struct FormAnswer {
amount: u64, amount: u32,
#[serde(rename = "name")] #[serde(rename = "name")]
mode: MembershipMode, mode: MembershipMode,

View file

@ -4,7 +4,7 @@ mod helloasso;
use thiserror::Error; use thiserror::Error;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use chrono::prelude::{NaiveDate, DateTime, Utc, Datelike}; use chrono::prelude::{NaiveDate, NaiveDateTime, DateTime, Utc, Datelike};
use strum::Display; use strum::Display;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use std::collections::HashSet; use std::collections::HashSet;
@ -195,14 +195,14 @@ fn parse_and_get_birthday_year(raw_date: String) -> Option<u32> {
Some(d.year().try_into().ok()?) Some(d.year().try_into().ok()?)
} }
fn helloasso_to_paheko_membership(helloasso_membership: &helloasso::MembershipMode) -> paheko::MembershipMode { // fn helloasso_to_paheko_membership(helloasso_membership: &helloasso::MembershipMode) -> paheko::MembershipMode {
match helloasso_membership { // match helloasso_membership {
helloasso::MembershipMode::Couple => paheko::MembershipMode::Couple, // helloasso::MembershipMode::Couple => paheko::MembershipMode::Couple,
helloasso::MembershipMode::Individual => paheko::MembershipMode::Individual, // helloasso::MembershipMode::Individual => paheko::MembershipMode::Individual,
helloasso::MembershipMode::BenefactorCouple => paheko::MembershipMode::BenefactorCouple, // helloasso::MembershipMode::BenefactorCouple => paheko::MembershipMode::BenefactorCouple,
helloasso::MembershipMode::BenefactorIndividual => paheko::MembershipMode::BenefactorIndividual // helloasso::MembershipMode::BenefactorIndividual => paheko::MembershipMode::BenefactorIndividual
} // }
} // }
async fn launch_adapter() -> Result<()> { async fn launch_adapter() -> Result<()> {
dotenvy::dotenv()?; dotenvy::dotenv()?;
@ -243,8 +243,6 @@ async fn launch_adapter() -> Result<()> {
// get the list of payments associated // get the list of payments associated
// first step: output a list of PahekoUser with PahekoMembership // first step: output a list of PahekoUser with PahekoMembership
let pk_memberships: Vec<paheko::Membership> = vec![];
let mut pk_users: Vec<paheko::User> = vec![];
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())), // read_custom_field(&answer, HelloAssoCustomFieldType::Email).or(Some(answer.payer_user.email.clone())),
@ -268,9 +266,16 @@ async fn launch_adapter() -> Result<()> {
// TODO: before creating any users, get the current maximum id of the users table to predict // TODO: before creating any users, get the current maximum id of the users table to predict
// the next auto-incrementing id. // the next auto-incrementing id.
let mut pk_next_id = paheko_client.get_user_next_id().await.context("Get paheko users next id")?; 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 in answers { for answer in answers {
// 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![];
eprintln!("Processing answer:"); eprintln!("Processing answer:");
let email = choose_email(&answer); let email = choose_email(&answer);
@ -318,29 +323,48 @@ 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_id.clone() &pk_user, pk_next_user_id.clone()
).await.context("Expected to create paheko user")?; ).await.context("Expected to create paheko user")?;
eprintln!(" Created paheko user"); eprintln!(" Created paheko user");
pk_next_id += 1; pk_next_user_id += 1;
existing_users.push(c.clone()); existing_users.push(c.clone());
c c
} }
}; };
pk_users_summaries.push(pk_user_summary);
let mut pk_membership = paheko::Membership { let mut pk_membership = paheko::Membership {
id: generate_id(), id: generate_id(),
campaign: "".to_string(), campaign_name: "Cotisation 2023-2024".to_string(),
inception_time: Utc::now(), // FIXME: handle errors
mode: helloasso_to_paheko_membership(&answer.mode), mode_name: serde_json::to_value(answer.mode.clone())
.unwrap().as_str().unwrap().to_string(),
start_time: answer.order.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: f64::from(answer.amount)/100.0,
users: vec![pk_user.id.clone()], users: vec![pk_user.id.clone()],
external_references: paheko::ExternalReferences { external_references: paheko::ExternalReferences {
helloasso_ref: paheko::HelloassoReferences { helloasso_refs: paheko::HelloassoReferences {
answer_id: answer.id, answer_id: answer.id,
order_id: answer.order.id order_id: answer.order.id
} }
} }
}; };
// add activity for first member
let user_registration = paheko_client.register_user_to_service(
pk_users_summaries.iter().nth(0).unwrap(),
&pk_membership,
pk_next_user_service_id.clone()
).await.context("Registering user to paheko server")?;
pk_user_service_registrations.push(user_registration);
pk_next_user_service_id += 1;
// then create optional linked user // then create optional linked user
if answer.mode == helloasso::MembershipMode::Couple { if answer.mode == helloasso::MembershipMode::Couple {
let mut second_pk_user = pk_user.clone(); let mut second_pk_user = pk_user.clone();
@ -363,26 +387,46 @@ async fn launch_adapter() -> Result<()> {
} }
if existing_user_opt.is_none() { if existing_user_opt.is_none() {
let second_pk_user_summary = paheko_client.create_user(&second_pk_user, pk_next_id) let second_pk_user_summary = paheko_client.create_user(&second_pk_user, pk_next_user_id)
.await.context("Expected to create second paheko user")?; .await.context("Expected to create second paheko user")?;
eprintln!(" Created conjoint paheko user"); eprintln!(" Created conjoint paheko user");
pk_next_id += 1; pk_users_summaries.push(second_pk_user_summary);
pk_next_user_id += 1;
// create activity of second user
let user_registration = paheko_client.register_user_to_service(
pk_users_summaries.iter().nth(1).unwrap(),
&pk_membership,
pk_next_user_service_id.clone()
).await.context("Registering service to second paheko server")?;
pk_user_service_registrations.push(user_registration);
pk_next_user_service_id += 1;
// 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
pk_membership.users.push(second_pk_user.id.clone()); pk_membership.users.push(second_pk_user.id.clone());
pk_users.push(second_pk_user);
} }
// add activity // add transaction
paheko_client.register_user_to_service(&pk_user_summary).await.context("Registering user to paheko server")?; let transaction = paheko::SimpleTransaction {
label: "Adhésion Helloasso".to_string(),
amount: pk_membership.payed_amount,
reference: format!("HA/{}", pk_membership.external_references.helloasso_refs.answer_id),
credit_account_code: "756".to_string(), // cotisations account
debit_account_code: "512HA".to_string(), // helloasso account
inception_time: answer.order.inception_time,
kind: paheko::TransactionKind::Revenue,
linked_users: pk_users_summaries.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)
.await.context("Expected to create new paheko transaction");
pk_users.push(pk_user);
pk_memberships.push(pk_membership); pk_memberships.push(pk_membership);
} }
dbg!(&pk_users);
dbg!(&pk_memberships);
dbg!(&pk_users.len());
dbg!(&pk_memberships.len()); dbg!(&pk_memberships.len());
Ok(()) Ok(())

View file

@ -17,7 +17,7 @@ struct HelloassoReferences {
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
#[fully_pub] #[fully_pub]
struct ExternalReferences { struct ExternalReferences {
helloasso_ref: HelloassoReferences helloasso_refs: HelloassoReferences
} }
/// for now we include the custom fields into the paheko user /// for now we include the custom fields into the paheko user
@ -52,28 +52,48 @@ struct UserSummary {
email: Option<String> email: Option<String>
} }
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[fully_pub]
enum MembershipMode {
Individual,
Couple,
BenefactorIndividual,
BenefactorCouple,
}
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
#[fully_pub] #[fully_pub]
struct Membership { struct Membership {
id: Id, id: Id,
users: Vec<Id>, users: Vec<Id>,
campaign: String, campaign_name: String,
mode: MembershipMode, mode_name: String,
inception_time: DateTime<Utc>, start_time: DateTime<Utc>,
external_references: ExternalReferences end_time: DateTime<Utc>,
payed_amount: f64,
external_references: ExternalReferences,
} }
#[derive(Debug, Clone)]
#[fully_pub]
enum TransactionKind {
Expense,
Revenue
}
impl Into<String> for TransactionKind {
fn into(self) -> String {
match self {
TransactionKind::Expense => "EXPENSE".to_string(),
TransactionKind::Revenue => "REVENUE".to_string()
}
}
}
#[derive(Debug, Clone)]
#[fully_pub]
struct SimpleTransaction {
label: String,
kind: TransactionKind,
inception_time: DateTime<Utc>,
amount: f64,
credit_account_code: String,
debit_account_code: String,
reference: String,
linked_users: Vec<Id>,
linked_services: Vec<Id>
}
#[derive(Debug)] #[derive(Debug)]
#[fully_pub] #[fully_pub]
@ -197,6 +217,12 @@ struct TransactionSummary {
reference: String reference: String
} }
#[derive(Debug)]
#[fully_pub]
struct UserServiceRegistration {
id: Id
}
impl AuthentifiedClient { impl AuthentifiedClient {
pub fn new(base_url: Url, credentials: Credentials) -> Self { pub fn new(base_url: Url, credentials: Credentials) -> Self {
AuthentifiedClient { AuthentifiedClient {
@ -237,21 +263,21 @@ impl AuthentifiedClient {
Ok(serde_json::from_value(users_val.results)?) Ok(serde_json::from_value(users_val.results)?)
} }
pub async fn get_user_next_id(&self) -> Result<u64> { pub async fn get_next_id(&self, table_name: &str) -> Result<u64> {
let query: String = r#" let query: String = format!(r#"
SELECT id FROM users ORDER BY id DESC LIMIT 1 SELECT id FROM {} ORDER BY id DESC LIMIT 1
"#.to_string(); "#, table_name).to_string();
let users_id_val = self.sql_query(query).await.context("Fetching users")?; let data = self.sql_query(query).await.context("Fetching next id from table")?;
#[derive(Deserialize)] #[derive(Deserialize)]
struct UserIdEntry { struct Entry {
id: u64 id: u64
} }
let users_ids: Vec<UserIdEntry> = serde_json::from_value(users_id_val.results)?; let ids: Vec<Entry> = serde_json::from_value(data.results)?;
Ok(users_ids.iter().nth(0).map(|x| x.id).unwrap_or(1)+1) Ok(ids.iter().nth(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)
@ -319,8 +345,8 @@ impl AuthentifiedClient {
) )
} }
pub async fn register_user_to_service(&self, user: &UserSummary) pub async fn register_user_to_service(&self, user: &UserSummary, user_membership: &Membership, next_id: u64)
-> Result<()> -> Result<UserServiceRegistration>
{ {
// single-user import // single-user import
// create virtual file // create virtual file
@ -333,11 +359,11 @@ impl AuthentifiedClient {
csv_content.push_str( csv_content.push_str(
format!("{},{:?},{:?},{:?},{:?},{:?},{:?}\n", format!("{},{:?},{:?},{:?},{:?},{:?},{:?}\n",
u.id, u.id,
"Cotisation 2023-2024", user_membership.campaign_name,
"Physique Individuelle", user_membership.mode_name,
"10/10/2023", user_membership.start_time.format("%d/%m/%Y").to_string(),
"10/10/2025", user_membership.end_time.format("%d/%m/%Y").to_string(),
"10", format!("{}", user_membership.payed_amount),
"Oui" "Oui"
).as_str()); ).as_str());
@ -354,6 +380,42 @@ impl AuthentifiedClient {
.multipart(form) .multipart(form)
.send().await?; .send().await?;
if res.status() != 200 {
return Err(APIClientError::InvalidStatusCode.into());
}
Ok(UserServiceRegistration {
id: Id(next_id)
})
}
pub async fn register_transaction(&self, transaction: SimpleTransaction)
-> Result<()>
{
use reqwest::multipart::Form;
let mut form = Form::new()
.text("id_year", "1")
.text("label", transaction.label)
.text("date", transaction.inception_time.format("%d/%m/%Y").to_string())
.text("type", Into::<String>::into(transaction.kind))
.text("amount", format!("{}", transaction.amount))
.text("debit", transaction.debit_account_code)
.text("credit", transaction.credit_account_code)
.text("reference", transaction.reference)
;
for linked_id in transaction.linked_users {
form = form.text("linked_users[]", format!("{}", linked_id.0));
}
for linked_id in transaction.linked_services {
form = form.text("linked_services[]", format!("{}", linked_id.0));
}
let res = self.client
.post(self.base_url.join("accounting/transaction")?)
.multipart(form)
.send().await?;
if res.status() != 200 { if res.status() != 200 {
return Err(APIClientError::InvalidStatusCode.into()); return Err(APIClientError::InvalidStatusCode.into());
} }