feat: activity and transaction registration
This commit is contained in:
parent
1fcf35eaa7
commit
05eab87c3a
3 changed files with 163 additions and 57 deletions
|
@ -296,7 +296,7 @@ struct OrderDetails {
|
|||
#[serde(rename_all = "camelCase")]
|
||||
#[fully_pub]
|
||||
struct FormAnswer {
|
||||
amount: u64,
|
||||
amount: u32,
|
||||
|
||||
#[serde(rename = "name")]
|
||||
mode: MembershipMode,
|
||||
|
|
98
src/main.rs
98
src/main.rs
|
@ -4,7 +4,7 @@ mod helloasso;
|
|||
|
||||
use thiserror::Error;
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::prelude::{NaiveDate, DateTime, Utc, Datelike};
|
||||
use chrono::prelude::{NaiveDate, NaiveDateTime, DateTime, Utc, Datelike};
|
||||
use strum::Display;
|
||||
use serde::{Serialize, Deserialize};
|
||||
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()?)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
// 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<()> {
|
||||
dotenvy::dotenv()?;
|
||||
|
@ -243,8 +243,6 @@ async fn launch_adapter() -> Result<()> {
|
|||
// get the list of payments associated
|
||||
|
||||
// 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![];
|
||||
|
||||
// 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
|
||||
// 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 {
|
||||
// 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:");
|
||||
let email = choose_email(&answer);
|
||||
|
||||
|
@ -318,29 +323,48 @@ async fn launch_adapter() -> Result<()> {
|
|||
Some(user) => user,
|
||||
None => {
|
||||
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")?;
|
||||
eprintln!(" Created paheko user");
|
||||
pk_next_id += 1;
|
||||
pk_next_user_id += 1;
|
||||
existing_users.push(c.clone());
|
||||
c
|
||||
}
|
||||
};
|
||||
pk_users_summaries.push(pk_user_summary);
|
||||
|
||||
let mut pk_membership = paheko::Membership {
|
||||
id: generate_id(),
|
||||
campaign: "".to_string(),
|
||||
inception_time: Utc::now(),
|
||||
mode: helloasso_to_paheko_membership(&answer.mode),
|
||||
campaign_name: "Cotisation 2023-2024".to_string(),
|
||||
// FIXME: handle errors
|
||||
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()],
|
||||
external_references: paheko::ExternalReferences {
|
||||
helloasso_ref: paheko::HelloassoReferences {
|
||||
helloasso_refs: paheko::HelloassoReferences {
|
||||
answer_id: answer.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
|
||||
if answer.mode == helloasso::MembershipMode::Couple {
|
||||
let mut second_pk_user = pk_user.clone();
|
||||
|
@ -363,26 +387,46 @@ async fn launch_adapter() -> Result<()> {
|
|||
}
|
||||
|
||||
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")?;
|
||||
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
|
||||
|
||||
pk_membership.users.push(second_pk_user.id.clone());
|
||||
pk_users.push(second_pk_user);
|
||||
}
|
||||
|
||||
// add activity
|
||||
paheko_client.register_user_to_service(&pk_user_summary).await.context("Registering user to paheko server")?;
|
||||
// add transaction
|
||||
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);
|
||||
}
|
||||
dbg!(&pk_users);
|
||||
dbg!(&pk_memberships);
|
||||
dbg!(&pk_users.len());
|
||||
dbg!(&pk_memberships.len());
|
||||
|
||||
Ok(())
|
||||
|
|
120
src/paheko.rs
120
src/paheko.rs
|
@ -17,7 +17,7 @@ struct HelloassoReferences {
|
|||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[fully_pub]
|
||||
struct ExternalReferences {
|
||||
helloasso_ref: HelloassoReferences
|
||||
helloasso_refs: HelloassoReferences
|
||||
}
|
||||
|
||||
/// for now we include the custom fields into the paheko user
|
||||
|
@ -52,28 +52,48 @@ struct UserSummary {
|
|||
email: Option<String>
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
#[fully_pub]
|
||||
enum MembershipMode {
|
||||
Individual,
|
||||
Couple,
|
||||
BenefactorIndividual,
|
||||
BenefactorCouple,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[fully_pub]
|
||||
struct Membership {
|
||||
id: Id,
|
||||
users: Vec<Id>,
|
||||
campaign: String,
|
||||
mode: MembershipMode,
|
||||
inception_time: DateTime<Utc>,
|
||||
external_references: ExternalReferences
|
||||
campaign_name: String,
|
||||
mode_name: String,
|
||||
start_time: DateTime<Utc>,
|
||||
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)]
|
||||
#[fully_pub]
|
||||
|
@ -197,6 +217,12 @@ struct TransactionSummary {
|
|||
reference: String
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[fully_pub]
|
||||
struct UserServiceRegistration {
|
||||
id: Id
|
||||
}
|
||||
|
||||
impl AuthentifiedClient {
|
||||
pub fn new(base_url: Url, credentials: Credentials) -> Self {
|
||||
AuthentifiedClient {
|
||||
|
@ -237,21 +263,21 @@ impl AuthentifiedClient {
|
|||
Ok(serde_json::from_value(users_val.results)?)
|
||||
}
|
||||
|
||||
pub async fn get_user_next_id(&self) -> Result<u64> {
|
||||
let query: String = r#"
|
||||
SELECT id FROM users ORDER BY id DESC LIMIT 1
|
||||
"#.to_string();
|
||||
pub async fn get_next_id(&self, table_name: &str) -> Result<u64> {
|
||||
let query: String = format!(r#"
|
||||
SELECT id FROM {} ORDER BY id DESC LIMIT 1
|
||||
"#, 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)]
|
||||
struct UserIdEntry {
|
||||
struct Entry {
|
||||
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)
|
||||
|
@ -319,8 +345,8 @@ impl AuthentifiedClient {
|
|||
)
|
||||
}
|
||||
|
||||
pub async fn register_user_to_service(&self, user: &UserSummary)
|
||||
-> Result<()>
|
||||
pub async fn register_user_to_service(&self, user: &UserSummary, user_membership: &Membership, next_id: u64)
|
||||
-> Result<UserServiceRegistration>
|
||||
{
|
||||
// single-user import
|
||||
// create virtual file
|
||||
|
@ -333,11 +359,11 @@ impl AuthentifiedClient {
|
|||
csv_content.push_str(
|
||||
format!("{},{:?},{:?},{:?},{:?},{:?},{:?}\n",
|
||||
u.id,
|
||||
"Cotisation 2023-2024",
|
||||
"Physique Individuelle",
|
||||
"10/10/2023",
|
||||
"10/10/2025",
|
||||
"10",
|
||||
user_membership.campaign_name,
|
||||
user_membership.mode_name,
|
||||
user_membership.start_time.format("%d/%m/%Y").to_string(),
|
||||
user_membership.end_time.format("%d/%m/%Y").to_string(),
|
||||
format!("{}", user_membership.payed_amount),
|
||||
"Oui"
|
||||
).as_str());
|
||||
|
||||
|
@ -354,6 +380,42 @@ impl AuthentifiedClient {
|
|||
.multipart(form)
|
||||
.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 {
|
||||
return Err(APIClientError::InvalidStatusCode.into());
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue