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")]
|
#[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,
|
||||||
|
|
98
src/main.rs
98
src/main.rs
|
@ -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(())
|
||||||
|
|
120
src/paheko.rs
120
src/paheko.rs
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue