mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-23 23:18:10 +02:00
Merge pull request #34 from R0m1k3/claude/affectionate-cori-9pitju
Fournisseur : repli sur l'API FF pour les articles absents de la base miroir
This commit is contained in:
3 files changed
+150
-5
No files matched your search
+17
-5
@@ -7,7 +7,7 @@ use super::api::{avec_cle_ff, get_ff_api_base};
|
|||||||
use super::cache::get_fournisseurs_cached;
|
use super::cache::get_fournisseurs_cached;
|
||||||
use super::js::{get, js_number, js_number_to_string, message_erreur};
|
use super::js::{get, js_number, js_number_to_string, message_erreur};
|
||||||
use super::pg::{
|
use super::pg::{
|
||||||
pg_get_ca_by_fournisseur, pg_get_ca_by_nomenclature, pg_get_commandes_auto, pg_get_dashboard_data, pg_get_hit_parade,
|
completer_fournisseurs, pg_get_ca_by_fournisseur, pg_get_ca_by_nomenclature, pg_get_commandes_auto, pg_get_dashboard_data, pg_get_hit_parade,
|
||||||
pg_get_sans_vente_6_mois, pg_get_stock_negatif, pg_get_stock_sans_vente, PgCommandeAutoRow,
|
pg_get_sans_vente_6_mois, pg_get_stock_negatif, pg_get_stock_sans_vente, PgCommandeAutoRow,
|
||||||
};
|
};
|
||||||
use crate::error::{AppError, AppResult};
|
use crate::error::{AppError, AppResult};
|
||||||
@@ -115,7 +115,9 @@ pub async fn page_hit_parade(ctx: Ctx) -> AppResult<Value> {
|
|||||||
|
|
||||||
let (d, f) = (date_debut.clone(), date_fin.clone());
|
let (d, f) = (date_debut.clone(), date_fin.clone());
|
||||||
match cached(&format!("hit-parade:{date_debut}:{date_fin}"), CacheOpts::default(), move || async move {
|
match cached(&format!("hit-parade:{date_debut}:{date_fin}"), CacheOpts::default(), move || async move {
|
||||||
pg_get_hit_parade(&d, &f).await
|
let mut rows = pg_get_hit_parade(&d, &f).await?;
|
||||||
|
completer_fournisseurs(&mut rows, |r| (r.codein.as_str(), &mut r.fournisseur)).await;
|
||||||
|
anyhow::Ok(rows)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -142,10 +144,20 @@ pub async fn page_stock_negatif(ctx: Ctx) -> AppResult<Value> {
|
|||||||
let (k1, k2, k3) =
|
let (k1, k2, k3) =
|
||||||
(format!("stock-negatif:{magasin}"), format!("stock-sans-vente:{magasin}"), format!("stock-sans-vente-6-mois:{magasin}"));
|
(format!("stock-negatif:{magasin}"), format!("stock-sans-vente:{magasin}"), format!("stock-sans-vente-6-mois:{magasin}"));
|
||||||
let res = tokio::try_join!(
|
let res = tokio::try_join!(
|
||||||
cached(&k1, CacheOpts::default(), move || async move { pg_get_stock_negatif(s1.as_deref()).await }),
|
cached(&k1, CacheOpts::default(), move || async move {
|
||||||
cached(&k2, CacheOpts::default(), move || async move { pg_get_stock_sans_vente(s2.as_deref()).await }),
|
let mut rows = pg_get_stock_negatif(s1.as_deref()).await?;
|
||||||
|
completer_fournisseurs(&mut rows, |r| (r.codein.as_str(), &mut r.fournisseur)).await;
|
||||||
|
anyhow::Ok(rows)
|
||||||
|
}),
|
||||||
|
cached(&k2, CacheOpts::default(), move || async move {
|
||||||
|
let mut rows = pg_get_stock_sans_vente(s2.as_deref()).await?;
|
||||||
|
completer_fournisseurs(&mut rows, |r| (r.codein.as_str(), &mut r.fournisseur)).await;
|
||||||
|
anyhow::Ok(rows)
|
||||||
|
}),
|
||||||
cached(&k3, CacheOpts::default(), move || async move {
|
cached(&k3, CacheOpts::default(), move || async move {
|
||||||
pg_get_sans_vente_6_mois(s3.as_deref()).await
|
let mut rows = pg_get_sans_vente_6_mois(s3.as_deref()).await?;
|
||||||
|
completer_fournisseurs(&mut rows, |r| (r.codein.as_str(), &mut r.fournisseur)).await;
|
||||||
|
anyhow::Ok(rows)
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
match res {
|
match res {
|
||||||
|
|||||||
@@ -851,6 +851,103 @@ async fn enrichir_article_dashboard(base: &str, codein: &str, out: &mut Enrichis
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Fournisseur manquant — repli sur l'API FF en direct
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Libellé posé par les requêtes quand aucun fournisseur n'est trouvé.
|
||||||
|
pub const SANS_FOURNISSEUR: &str = "Sans fournisseur";
|
||||||
|
|
||||||
|
/// Au plus autant d'articles interrogés par appel (les premiers de la liste,
|
||||||
|
/// donc les plus gros CA du Hit Parade) : chacun coûte 3 appels à l'API FF.
|
||||||
|
const MAX_FOURNISSEURS_API: usize = 200;
|
||||||
|
|
||||||
|
/// Réponses de l'API déjà obtenues, y compris « aucun fournisseur », pour ne
|
||||||
|
/// pas réinterroger l'API à chaque page ni à chaque onglet.
|
||||||
|
type MemoFournisseurs = HashMap<String, (std::time::Instant, Option<String>)>;
|
||||||
|
static FOURNISSEURS_API: LazyLock<parking_lot::Mutex<MemoFournisseurs>> = LazyLock::new(Default::default);
|
||||||
|
|
||||||
|
/// Fournisseur d'articles que la base miroir ne rattache à aucun fournisseur :
|
||||||
|
/// aucune ligne `artfou1`, typiquement un article créé depuis la dernière
|
||||||
|
/// recopie de FF (codeins 503996, 504022…, sans référence ni fournisseur).
|
||||||
|
/// L'API FF répond en direct : fournisseur de la dernière entrée en stock,
|
||||||
|
/// sinon celui du référentiel (même règle que le tableau de bord).
|
||||||
|
/// Les codeins sans réponse sont absents de la map.
|
||||||
|
pub async fn ff_fournisseurs_par_codein(codeins: &[String]) -> HashMap<String, String> {
|
||||||
|
let ttl = crate::ff_cache::default_ttl();
|
||||||
|
let mut out = HashMap::new();
|
||||||
|
let mut a_lire = vec![];
|
||||||
|
{
|
||||||
|
let memo = FOURNISSEURS_API.lock();
|
||||||
|
for c in codeins {
|
||||||
|
match memo.get(c) {
|
||||||
|
Some((t, f)) if t.elapsed() < ttl => out.extend(f.clone().map(|f| (c.clone(), f))),
|
||||||
|
_ if a_lire.len() < MAX_FOURNISSEURS_API => a_lire.push(c.clone()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if a_lire.is_empty() {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
let base = get_ff_api_base().await;
|
||||||
|
let lus: Vec<(String, Option<String>)> = futures::stream::iter(a_lire)
|
||||||
|
.map(|codein| {
|
||||||
|
let base = base.clone();
|
||||||
|
async move {
|
||||||
|
let mut e = EnrichissementDashboard::default();
|
||||||
|
if let Err(err) = enrichir_article_dashboard(&base, &codein, &mut e).await {
|
||||||
|
tracing::error!("[pg-ff] fournisseur API pour {codein} : {}", message_erreur(&err));
|
||||||
|
}
|
||||||
|
(codein, e.fournisseur.filter(|f| !f.is_empty()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.buffer_unordered(12)
|
||||||
|
.collect()
|
||||||
|
.await;
|
||||||
|
tracing::info!(
|
||||||
|
"[pg-ff] fournisseur via l'API FF : {}/{} article(s) résolu(s)",
|
||||||
|
lus.iter().filter(|(_, f)| f.is_some()).count(),
|
||||||
|
lus.len()
|
||||||
|
);
|
||||||
|
let mut memo = FOURNISSEURS_API.lock();
|
||||||
|
let maintenant = std::time::Instant::now();
|
||||||
|
for (codein, f) in lus {
|
||||||
|
if let Some(f) = &f {
|
||||||
|
out.insert(codein.clone(), f.clone());
|
||||||
|
}
|
||||||
|
memo.insert(codein, (maintenant, f));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complète, via l'API FF, le fournisseur des lignes restées en
|
||||||
|
/// « Sans fournisseur » après la requête SQL. `champs` donne le codein et le
|
||||||
|
/// fournisseur d'une ligne.
|
||||||
|
pub async fn completer_fournisseurs<T>(rows: &mut [T], champs: fn(&mut T) -> (&str, &mut String)) {
|
||||||
|
let mut vus = HashSet::new();
|
||||||
|
let manquants: Vec<String> = rows
|
||||||
|
.iter_mut()
|
||||||
|
.filter_map(|r| {
|
||||||
|
let (codein, fou) = champs(r);
|
||||||
|
(fou == SANS_FOURNISSEUR && !codein.trim().is_empty()).then(|| codein.trim().to_string())
|
||||||
|
})
|
||||||
|
.filter(|c| vus.insert(c.clone()))
|
||||||
|
.collect();
|
||||||
|
if manquants.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let trouves = ff_fournisseurs_par_codein(&manquants).await;
|
||||||
|
for r in rows.iter_mut() {
|
||||||
|
let (codein, fou) = champs(r);
|
||||||
|
if let Some(f) = trouves.get(codein.trim()) {
|
||||||
|
if fou == SANS_FOURNISSEUR {
|
||||||
|
*fou = f.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Retourne toutes les données nécessaires pour le dashboard quotidien.
|
/// Retourne toutes les données nécessaires pour le dashboard quotidien.
|
||||||
/// 100% via l'API REST api.ffnancy.fr — aucune requête PostgreSQL directe.
|
/// 100% via l'API REST api.ffnancy.fr — aucune requête PostgreSQL directe.
|
||||||
/// N-1 : même jour de semaine (52 semaines = 364 jours avant hier).
|
/// N-1 : même jour de semaine (52 semaines = 364 jours avant hier).
|
||||||
|
|||||||
@@ -101,4 +101,40 @@ async fn article_sans_fournisseur_principal() {
|
|||||||
assert_eq!(catalogue["CC100001"].fournisseur, "ALPHA");
|
assert_eq!(catalogue["CC100001"].fournisseur, "ALPHA");
|
||||||
assert_eq!(catalogue["CC100003"].fournisseur, "Sans fournisseur");
|
assert_eq!(catalogue["CC100003"].fournisseur, "Sans fournisseur");
|
||||||
assert!(catalogue["CC504202"].codefou.is_empty());
|
assert!(catalogue["CC504202"].codefou.is_empty());
|
||||||
|
|
||||||
|
// Article absent de la base miroir (100003) : l'API FF en direct le connaît.
|
||||||
|
std::env::set_var("FF_API_BASE_URL", fausse_api().await);
|
||||||
|
super::api::reset_ff_api_base_cache();
|
||||||
|
let mut hp = hp;
|
||||||
|
completer_fournisseurs(&mut hp, |r| (r.codein.as_str(), &mut r.fournisseur)).await;
|
||||||
|
for r in &hp {
|
||||||
|
let voulu = if r.codein == "100003" { "FOURNISSEUR API" } else { attendu(&r.codein) };
|
||||||
|
assert_eq!(r.fournisseur, voulu, "hit parade complété {}", r.codein);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fausse API FF : seul l'article 4 (codein 100003) y a un fournisseur.
|
||||||
|
async fn fausse_api() -> String {
|
||||||
|
use axum::extract::{Path, Query};
|
||||||
|
use axum::routing::get;
|
||||||
|
use axum::Json;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
async fn articles(Query(q): Query<HashMap<String, String>>) -> Json<Value> {
|
||||||
|
match q.get("codein").map(|c| c.as_str()) {
|
||||||
|
Some("100003") => Json(json!({ "articles": [{ "no_id": 4, "codein": "100003" }] })),
|
||||||
|
_ => Json(json!({ "articles": [] })),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn referentiel(Path(_id): Path<String>) -> Json<Value> {
|
||||||
|
Json(json!({ "stock": [], "fournisseurs": [{ "codefou": "F9", "nom_fou": " FOURNISSEUR API ", "preference": 0 }] }))
|
||||||
|
}
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.route("/api/articles", get(articles))
|
||||||
|
.route("/api/articles/{id}/referentiel", get(referentiel))
|
||||||
|
.route("/api/articles/{id}/dernier-fournisseur", get(|| async { Json(json!({ "fournisseurs_par_site": [] })) }));
|
||||||
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let addr = listener.local_addr().unwrap();
|
||||||
|
tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
|
||||||
|
format!("http://{addr}")
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user