From c9d0ccfdef028a44f593d3c05f3e1efaec762b04 Mon Sep 17 00:00:00 2001 From: Dennis Nemec Date: Fri, 25 Sep 2026 16:13:13 +0200 Subject: [PATCH] fix(sync): Lieferadresse nicht mehr feldweise mischen, Adresszusatz statt Land MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sync nimmt die Lieferadresse als Ganzes: mit eigener LieferAdressId alle Felder aus ihr, sonst alle aus der Belegadresse. Das bisherige COALESCE je Feld hat leere Felder mit Werten des Bestellers gefüllt (z. B. dessen Ortsteil an eine fremde Anschrift gehängt). - ERP-Freitext `Adressen.Land` (praktisch Ortsteil/Etage/Hinweis) wird zum Adresszusatz `Address.addition`; `country` kommt jetzt aus dem Länderstamm (`LandID` → `Laender.Land`). Deutschland-Kürzel und reine Landnamen sind kein Zusatz. - Migration 0034: `customers.address_addition`, `deliveries.snap_addition`; Bestandstext aus `country` wandert in den Zusatz. - Lieferbericht und Beleg-Ansicht: Land nur außerhalb Deutschlands, Zusatz als eigene Zeile „Zusatz: …". Co-Authored-By: Claude Opus 5.5 --- crates/api/src/routes/belege_view.rs | 9 +- crates/application/src/dto/delivery_report.rs | 3 + crates/domain/src/common.rs | 69 ++++++++++++++ crates/domain/src/lib.rs | 2 +- .../src/erp/mssql_delivery_source.rs | 72 ++++++++++++--- .../delivery_completion_repository.rs | 4 +- .../src/persistence/delivery_repository.rs | 5 +- .../src/persistence/tour_repository.rs | 23 +++-- crates/infrastructure/src/report/renderer.rs | 9 ++ .../infrastructure/src/report/repository.rs | 92 ++++++------------- migrations/0034_address_addition.sql | 32 +++++++ 11 files changed, 231 insertions(+), 89 deletions(-) create mode 100644 migrations/0034_address_addition.sql diff --git a/crates/api/src/routes/belege_view.rs b/crates/api/src/routes/belege_view.rs index cb8591e..37e801b 100644 --- a/crates/api/src/routes/belege_view.rs +++ b/crates/api/src/routes/belege_view.rs @@ -301,8 +301,13 @@ fn address_lines(a: &Address) -> Vec { if !city.is_empty() { lines.push(city); } - if !a.country.trim().is_empty() { - lines.push(a.country.clone()); + // Land nur außerhalb Deutschlands; der Adresszusatz (Ortsteil, Etage, + // Hinweis) als eigene, gekennzeichnete Zeile. + if let Some(country) = a.foreign_country() { + lines.push(country.to_string()); + } + if let Some(addition) = a.addition.as_deref().filter(|z| !z.trim().is_empty()) { + lines.push(format!("Zusatz: {addition}")); } lines } diff --git a/crates/application/src/dto/delivery_report.rs b/crates/application/src/dto/delivery_report.rs index 7751ba6..fd2e686 100644 --- a/crates/application/src/dto/delivery_report.rs +++ b/crates/application/src/dto/delivery_report.rs @@ -30,6 +30,8 @@ pub struct DeliveryReportData { pub customer_name: String, /// Lieferadresse (Snapshot am Beleg), einzeilig. pub address: String, + /// Adresszusatz der Lieferadresse (Ortsteil, Etage, Hinweis). + pub address_addition: Option, /// Nur gesetzt, wenn Empfänger oder Lieferadresse vom Besteller /// abweichen (ERP-`LieferAdressId`). Dann ist `customer_*` der Besteller /// und `address` die abweichende Lieferadresse. @@ -60,6 +62,7 @@ pub struct ReportRecipient { pub phones: Vec, /// Anschrift des Bestellers (Kundenstamm), einzeilig. pub orderer_address: String, + pub orderer_address_addition: Option, pub orderer_phones: Vec, } diff --git a/crates/domain/src/common.rs b/crates/domain/src/common.rs index a741b18..f3b07c8 100644 --- a/crates/domain/src/common.rs +++ b/crates/domain/src/common.rs @@ -16,5 +16,74 @@ pub struct Address { pub house_number: String, pub postal_code: String, pub city: String, + /// Land laut ERP-Länderstamm (`Adressen.LandID` → `Laender.Land`), z. B. + /// „Deutschland" oder „Belgien". Leer, wenn im ERP kein Land verknüpft ist. pub country: String, + /// Freier Adresszusatz. Kommt aus dem ERP-Textfeld `Adressen.Land`, das + /// die Sachbearbeitung praktisch für Ortsteil, Etage oder Hinweise nutzt + /// (z. B. „Holzheim", „3. Etage", „Parterre"). `None`, wenn leer. + #[serde(default)] + pub addition: Option, +} + +impl Address { + /// Einzeilige Anschrift „Straße Nr, PLZ Ort" — das Land nur, wenn es + /// nicht Deutschland ist. Der Adresszusatz gehört nicht dazu (eigene + /// Zeile in der Anzeige). + pub fn one_line(&self) -> String { + let line1 = join_non_empty(&[&self.street, &self.house_number], " "); + let line2 = join_non_empty(&[&self.postal_code, &self.city], " "); + let country = self.foreign_country().unwrap_or_default(); + join_non_empty(&[&line1, &line2, country], ", ") + } + + /// Land für die Anzeige: `None` für Deutschland (Heimatmarkt) oder leer. + pub fn foreign_country(&self) -> Option<&str> { + let c = self.country.trim(); + (!c.is_empty() && !is_germany(c)).then_some(c) + } +} + +/// `true` für die im ERP vorkommenden Schreibweisen von Deutschland. +pub fn is_germany(value: &str) -> bool { + matches!( + value.trim().to_lowercase().as_str(), + "d" | "de" | "deu" | "deutschland" | "germany" | "brd" + ) +} + +fn join_non_empty(parts: &[&str], sep: &str) -> String { + parts + .iter() + .map(|p| p.trim()) + .filter(|p| !p.is_empty()) + .collect::>() + .join(sep) +} + +#[cfg(test)] +mod address_tests { + use super::*; + + fn address(country: &str) -> Address { + Address { + street: "Am Sandberg".into(), + house_number: "15".into(), + postal_code: "41472".into(), + city: "Neuss".into(), + country: country.into(), + addition: Some("Holzheim".into()), + } + } + + #[test] + fn deutschland_und_zusatz_stehen_nicht_in_der_zeile() { + assert_eq!(address("Deutschland").one_line(), "Am Sandberg 15, 41472 Neuss"); + assert_eq!(address("").one_line(), "Am Sandberg 15, 41472 Neuss"); + } + + #[test] + fn ausland_wird_angehaengt() { + assert_eq!(address("Belgien").one_line(), "Am Sandberg 15, 41472 Neuss, Belgien"); + } } diff --git a/crates/domain/src/lib.rs b/crates/domain/src/lib.rs index 9936659..6135bbb 100644 --- a/crates/domain/src/lib.rs +++ b/crates/domain/src/lib.rs @@ -33,7 +33,7 @@ pub use account::Account; pub use article::Article; pub use audit::{AuditAction, ScanAuditEntry}; pub use car::Car; -pub use common::Address; +pub use common::{is_germany, Address}; pub use contact::{ContactChannel, ContactKind, ContactRole, ContactSource}; pub use customer::{Customer, CustomerContact}; pub use delivery::{ diff --git a/crates/infrastructure/src/erp/mssql_delivery_source.rs b/crates/infrastructure/src/erp/mssql_delivery_source.rs index b64f3af..24754f1 100644 --- a/crates/infrastructure/src/erp/mssql_delivery_source.rs +++ b/crates/infrastructure/src/erp/mssql_delivery_source.rs @@ -23,7 +23,7 @@ use holzleitner_application::dto::{ }; use holzleitner_application::error::ApplicationError; use holzleitner_application::ports::ErpDeliverySource; -use holzleitner_domain::{Address, ContactKind, ContactRole}; +use holzleitner_domain::{is_germany, Address, ContactKind, ContactRole}; /// Verbindungsparameter zur ERPframe-MSSQL. #[derive(Debug, Clone)] @@ -141,12 +141,18 @@ SELECT adr.Hausnummer AS custHouseNumber, LTRIM(RTRIM(adr.PLZ)) AS custPostalCode, adr.Ort AS custCity, - adr.Land AS custCountry, - COALESCE(dadr.Strasse, adr.Strasse) AS delivStreet, - COALESCE(dadr.Hausnummer, adr.Hausnummer) AS delivHouseNumber, - LTRIM(RTRIM(COALESCE(dadr.PLZ, adr.PLZ))) AS delivPostalCode, - COALESCE(dadr.Ort, adr.Ort) AS delivCity, - COALESCE(dadr.Land, adr.Land) AS delivCountry, + LTRIM(RTRIM(hland.Land)) AS custCountry, + adr.Land AS custAddition, + -- Lieferadresse als Ganzes: gibt es eine eigene, kommen ALLE Felder aus + -- ihr, sonst alle aus der Belegadresse. Kein Feld-für-Feld-COALESCE — + -- das hat leere Felder der Lieferadresse mit Werten des Bestellers + -- aufgefüllt (z. B. dessen Ortsteil an eine fremde Anschrift gehängt). + CASE WHEN dadr.ROW_ID IS NOT NULL THEN dadr.Strasse ELSE adr.Strasse END AS delivStreet, + CASE WHEN dadr.ROW_ID IS NOT NULL THEN dadr.Hausnummer ELSE adr.Hausnummer END AS delivHouseNumber, + LTRIM(RTRIM(CASE WHEN dadr.ROW_ID IS NOT NULL THEN dadr.PLZ ELSE adr.PLZ END)) AS delivPostalCode, + CASE WHEN dadr.ROW_ID IS NOT NULL THEN dadr.Ort ELSE adr.Ort END AS delivCity, + LTRIM(RTRIM(CASE WHEN dadr.ROW_ID IS NOT NULL THEN dland.Land ELSE hland.Land END)) AS delivCountry, + CASE WHEN dadr.ROW_ID IS NOT NULL THEN dadr.Land ELSE adr.Land END AS delivAddition, LTRIM(RTRIM(CAST(bk._Uhrzeit_Txt AS varchar(200)))) AS desiredTime, LTRIM(RTRIM(CAST(bk.Kopftext AS varchar(MAX)))) AS specialAgreements, CAST(ISNULL(bk._Anz_Bestellung, 0) AS FLOAT) AS prepaidAmount, @@ -174,6 +180,8 @@ SELECT FROM Belegkopf bk JOIN Adressen adr ON bk.AdressId = adr.ROW_ID LEFT JOIN Adressen dadr ON bk.LieferAdressId = dadr.ROW_ID +LEFT JOIN Laender hland ON hland.ROW_ID = adr.LandID +LEFT JOIN Laender dland ON dland.ROW_ID = dadr.LandID LEFT JOIN Adressen radr ON bk.RechnungsAdressId = radr.ROW_ID LEFT JOIN Adressen kadr ON bk.AnsprechpartnerId = kadr.ROW_ID JOIN Belegzeilen bz ON bk.row_id = bz.ParentID @@ -215,12 +223,18 @@ SELECT adr.Hausnummer AS custHouseNumber, LTRIM(RTRIM(adr.PLZ)) AS custPostalCode, adr.Ort AS custCity, - adr.Land AS custCountry, - COALESCE(dadr.Strasse, adr.Strasse) AS delivStreet, - COALESCE(dadr.Hausnummer, adr.Hausnummer) AS delivHouseNumber, - LTRIM(RTRIM(COALESCE(dadr.PLZ, adr.PLZ))) AS delivPostalCode, - COALESCE(dadr.Ort, adr.Ort) AS delivCity, - COALESCE(dadr.Land, adr.Land) AS delivCountry, + LTRIM(RTRIM(hland.Land)) AS custCountry, + adr.Land AS custAddition, + -- Lieferadresse als Ganzes: gibt es eine eigene, kommen ALLE Felder aus + -- ihr, sonst alle aus der Belegadresse. Kein Feld-für-Feld-COALESCE — + -- das hat leere Felder der Lieferadresse mit Werten des Bestellers + -- aufgefüllt (z. B. dessen Ortsteil an eine fremde Anschrift gehängt). + CASE WHEN dadr.ROW_ID IS NOT NULL THEN dadr.Strasse ELSE adr.Strasse END AS delivStreet, + CASE WHEN dadr.ROW_ID IS NOT NULL THEN dadr.Hausnummer ELSE adr.Hausnummer END AS delivHouseNumber, + LTRIM(RTRIM(CASE WHEN dadr.ROW_ID IS NOT NULL THEN dadr.PLZ ELSE adr.PLZ END)) AS delivPostalCode, + CASE WHEN dadr.ROW_ID IS NOT NULL THEN dadr.Ort ELSE adr.Ort END AS delivCity, + LTRIM(RTRIM(CASE WHEN dadr.ROW_ID IS NOT NULL THEN dland.Land ELSE hland.Land END)) AS delivCountry, + CASE WHEN dadr.ROW_ID IS NOT NULL THEN dadr.Land ELSE adr.Land END AS delivAddition, LTRIM(RTRIM(CAST(bk._Uhrzeit_Txt AS varchar(200)))) AS desiredTime, LTRIM(RTRIM(CAST(bk.Kopftext AS varchar(MAX)))) AS specialAgreements, CAST(ISNULL(bk._Anz_Bestellung, 0) AS FLOAT) AS prepaidAmount, @@ -243,6 +257,8 @@ SELECT FROM Belegkopf bk JOIN Adressen adr ON bk.AdressId = adr.ROW_ID LEFT JOIN Adressen dadr ON bk.LieferAdressId = dadr.ROW_ID +LEFT JOIN Laender hland ON hland.ROW_ID = adr.LandID +LEFT JOIN Laender dland ON dland.ROW_ID = dadr.LandID LEFT JOIN Adressen radr ON bk.RechnungsAdressId = radr.ROW_ID LEFT JOIN Adressen kadr ON bk.AnsprechpartnerId = kadr.ROW_ID JOIN Belegzeilen bz ON bk.row_id = bz.ParentID @@ -412,6 +428,14 @@ fn read_all_contact_sources(row: &tiberius::Row) -> Vec { .collect() } +/// Adresszusatz aus dem ERP-Freitextfeld `Adressen.Land`. Das Feld wird +/// praktisch für Ortsteil/Etage/Hinweise genutzt; steht dort nur das Land +/// (Deutschland-Kürzel oder derselbe Name wie im Länderstamm), ist es kein +/// Zusatz. +fn address_addition(free_text: Option, country: &str) -> Option { + free_text.filter(|t| !is_germany(t) && !t.eq_ignore_ascii_case(country.trim())) +} + fn map_row(row: &tiberius::Row) -> ErpRow { ErpRow { driver_personalnummer: i64c(row, "driverPersonalnummer"), @@ -427,6 +451,7 @@ fn map_row(row: &tiberius::Row) -> ErpRow { postal_code: s(row, "custPostalCode"), city: s(row, "custCity"), country: s(row, "custCountry"), + addition: address_addition(opt_s(row, "custAddition"), &s(row, "custCountry")), }, deliv: Address { street: s(row, "delivStreet"), @@ -434,6 +459,7 @@ fn map_row(row: &tiberius::Row) -> ErpRow { postal_code: s(row, "delivPostalCode"), city: s(row, "delivCity"), country: s(row, "delivCountry"), + addition: address_addition(opt_s(row, "delivAddition"), &s(row, "delivCountry")), }, desired_time: opt_s(row, "desiredTime"), special_agreements: opt_s(row, "specialAgreements"), @@ -584,3 +610,23 @@ impl ErpDeliverySource for MssqlErpDeliverySource { Ok(tours) } } + +#[cfg(test)] +mod address_addition_tests { + use super::address_addition; + + #[test] + fn ortsteil_und_hinweise_sind_zusatz() { + assert_eq!(address_addition(Some("Holzheim".into()), "Deutschland").as_deref(), Some("Holzheim")); + assert_eq!(address_addition(Some("3 Etage".into()), "").as_deref(), Some("3 Etage")); + } + + #[test] + fn reines_land_ist_kein_zusatz() { + for t in ["D", "DEU", "Deutschland"] { + assert_eq!(address_addition(Some(t.into()), ""), None, "{t}"); + } + assert_eq!(address_addition(Some("belgien".into()), "Belgien"), None); + assert_eq!(address_addition(None, "Deutschland"), None); + } +} diff --git a/crates/infrastructure/src/persistence/delivery_completion_repository.rs b/crates/infrastructure/src/persistence/delivery_completion_repository.rs index 08c4e6f..24d8e8f 100644 --- a/crates/infrastructure/src/persistence/delivery_completion_repository.rs +++ b/crates/infrastructure/src/persistence/delivery_completion_repository.rs @@ -48,6 +48,7 @@ struct DeliveryRow { snap_postal_code: String, snap_city: String, snap_country: String, + snap_addition: Option, assigned_car_id: Option, desired_time: Option, special_agreements: Option, @@ -68,7 +69,7 @@ async fn lock_delivery( r#" SELECT id, tour_id, erp_belegart_id, erp_belegnummer, customer_id, - snap_street, snap_house_number, snap_postal_code, snap_city, snap_country, + snap_street, snap_house_number, snap_postal_code, snap_city, snap_country, snap_addition, assigned_car_id, desired_time, special_agreements, state, prepaid_amount, payment_method_id FROM deliveries @@ -109,6 +110,7 @@ fn build_delivery(row: DeliveryRow, state: DeliveryState, state_reason: Option, assigned_car_id: Option, desired_time: Option, special_agreements: Option, @@ -112,7 +113,7 @@ async fn lock_delivery( r#" SELECT id, tour_id, erp_belegart_id, erp_belegnummer, customer_id, - snap_street, snap_house_number, snap_postal_code, snap_city, snap_country, + snap_street, snap_house_number, snap_postal_code, snap_city, snap_country, snap_addition, assigned_car_id, desired_time, special_agreements, state, prepaid_amount, payment_method_id FROM deliveries @@ -193,6 +194,7 @@ impl DeliveryRepository for PgDeliveryRepository { postal_code: row.snap_postal_code, city: row.snap_city, country: row.snap_country, + addition: row.snap_addition, }, assigned_car_id: row.assigned_car_id, contact_person_ids, @@ -250,6 +252,7 @@ impl DeliveryRepository for PgDeliveryRepository { postal_code: row.snap_postal_code, city: row.snap_city, country: row.snap_country, + addition: row.snap_addition, }, assigned_car_id: car_id, contact_person_ids, diff --git a/crates/infrastructure/src/persistence/tour_repository.rs b/crates/infrastructure/src/persistence/tour_repository.rs index 4688cd8..2242fbf 100644 --- a/crates/infrastructure/src/persistence/tour_repository.rs +++ b/crates/infrastructure/src/persistence/tour_repository.rs @@ -75,6 +75,7 @@ struct DeliveryRow { snap_postal_code: String, snap_city: String, snap_country: String, + snap_addition: Option, assigned_car_id: Option, desired_time: Option, special_agreements: Option, @@ -113,6 +114,7 @@ struct CustomerRow { postal_code: String, city: String, country: String, + address_addition: Option, } #[derive(sqlx::FromRow)] @@ -251,6 +253,7 @@ fn map_customer(row: CustomerRow) -> Customer { postal_code: row.postal_code, city: row.city, country: row.country, + addition: row.address_addition, }, } } @@ -413,6 +416,7 @@ fn map_delivery( postal_code: row.snap_postal_code, city: row.snap_city, country: row.snap_country, + addition: row.snap_addition, }, assigned_car_id: row.assigned_car_id, contact_person_ids, @@ -490,6 +494,7 @@ impl TourRepository for PgTourRepository { SELECT id, tour_id, erp_belegart_id, erp_belegnummer, customer_id, snap_street, snap_house_number, snap_postal_code, snap_city, snap_country, + snap_addition, assigned_car_id, desired_time, special_agreements, state, state_reason, sort_order, prepaid_amount, payment_method_id @@ -578,7 +583,8 @@ impl TourRepository for PgTourRepository { let customer_ids_vec: Vec = customer_ids.into_iter().collect(); let customers = sqlx::query_as::<_, CustomerRow>( r#" - SELECT id, erp_customer_id, name, street, house_number, postal_code, city, country + SELECT id, erp_customer_id, name, street, house_number, postal_code, city, country, + address_addition FROM customers WHERE id = ANY($1) ORDER BY name @@ -1105,15 +1111,17 @@ async fn upsert_customer( let id: Uuid = sqlx::query_scalar( r#" INSERT INTO customers ( - erp_customer_id, name, street, house_number, postal_code, city, country - ) VALUES ($1, $2, $3, $4, $5, $6, $7) + erp_customer_id, name, street, house_number, postal_code, city, country, + address_addition + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (erp_customer_id) DO UPDATE SET name = EXCLUDED.name, street = EXCLUDED.street, house_number = EXCLUDED.house_number, postal_code = EXCLUDED.postal_code, city = EXCLUDED.city, - country = EXCLUDED.country + country = EXCLUDED.country, + address_addition = EXCLUDED.address_addition RETURNING id "#, ) @@ -1124,6 +1132,7 @@ async fn upsert_customer( .bind(&delivery.customer_address.postal_code) .bind(&delivery.customer_address.city) .bind(&delivery.customer_address.country) + .bind(delivery.customer_address.addition.as_deref()) .fetch_one(&mut **tx) .await .map_err(db)?; @@ -1163,8 +1172,8 @@ async fn upsert_delivery( erp_belegnummer, customer_id, snap_street, snap_house_number, snap_postal_code, snap_city, snap_country, sort_order, desired_time, special_agreements, - prepaid_amount, payment_method_id - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) + prepaid_amount, payment_method_id, snap_addition + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) ON CONFLICT (erp_belegart_id, erp_belegnummer) DO UPDATE SET tour_id = EXCLUDED.tour_id, erp_belegart_code = EXCLUDED.erp_belegart_code, @@ -1175,6 +1184,7 @@ async fn upsert_delivery( snap_postal_code = EXCLUDED.snap_postal_code, snap_city = EXCLUDED.snap_city, snap_country = EXCLUDED.snap_country, + snap_addition = EXCLUDED.snap_addition, sort_order = EXCLUDED.sort_order, desired_time = EXCLUDED.desired_time, special_agreements = EXCLUDED.special_agreements, @@ -1199,6 +1209,7 @@ async fn upsert_delivery( .bind(delivery.special_agreements.as_deref()) .bind(delivery.prepaid_amount) .bind(payment_method_id) + .bind(delivery.delivery_address.addition.as_deref()) .fetch_one(&mut **tx) .await .map_err(db)?; diff --git a/crates/infrastructure/src/report/renderer.rs b/crates/infrastructure/src/report/renderer.rs index 62abe4d..30cee33 100644 --- a/crates/infrastructure/src/report/renderer.rs +++ b/crates/infrastructure/src/report/renderer.rs @@ -482,11 +482,17 @@ impl DeliveryReportRenderer for PdfDeliveryReportRenderer { ); p.kv("Besteller", &format!("{} (Nr. {})", d.customer_name, d.customer_number)); p.kv("Adresse Besteller", &r.orderer_address); + if let Some(z) = &r.orderer_address_addition { + p.kv("Zusatz Besteller", z); + } if !r.orderer_phones.is_empty() { p.kv("Tel. Besteller", &r.orderer_phones.join(" · ")); } p.kv("Empfänger", &r.name); p.kv("Lieferadresse", &d.address); + if let Some(z) = &d.address_addition { + p.kv("Zusatz", z); + } if !r.phones.is_empty() { p.kv("Tel. Empfänger", &r.phones.join(" · ")); } @@ -494,6 +500,9 @@ impl DeliveryReportRenderer for PdfDeliveryReportRenderer { None => { p.kv("Kunde", &format!("{} (Nr. {})", d.customer_name, d.customer_number)); p.kv("Adresse", &d.address); + if let Some(z) = &d.address_addition { + p.kv("Zusatz", z); + } } } p.kv("Wunschzeit", opt(&d.desired_time)); diff --git a/crates/infrastructure/src/report/repository.rs b/crates/infrastructure/src/report/repository.rs index 650b8d2..d62b542 100644 --- a/crates/infrastructure/src/report/repository.rs +++ b/crates/infrastructure/src/report/repository.rs @@ -15,6 +15,7 @@ use holzleitner_application::dto::{ }; use holzleitner_application::error::ApplicationError; use holzleitner_application::ports::DeliveryReportRepository; +use holzleitner_domain::Address; pub struct PgDeliveryReportRepository { pool: PgPool, @@ -49,11 +50,13 @@ struct HeadRow { cust_postal_code: String, cust_city: String, cust_country: String, + cust_addition: Option, snap_street: Option, snap_house_number: Option, snap_postal_code: Option, snap_city: Option, snap_country: Option, + snap_addition: Option, desired_time: Option, special_agreements: Option, prepaid_amount: f64, @@ -185,32 +188,6 @@ struct AttachmentRow { uploaded_by: i64, } -fn one_line_address( - street: Option, - house: Option, - plz: Option, - city: Option, - country: Option, -) -> String { - let line1 = [street, house] - .into_iter() - .flatten() - .filter(|s| !s.trim().is_empty()) - .collect::>() - .join(" "); - let line2 = [plz, city] - .into_iter() - .flatten() - .filter(|s| !s.trim().is_empty()) - .collect::>() - .join(" "); - [line1, line2, country.unwrap_or_default()] - .into_iter() - .filter(|s| !s.trim().is_empty()) - .collect::>() - .join(", ") -} - #[async_trait] impl DeliveryReportRepository for PgDeliveryReportRepository { async fn load( @@ -229,9 +206,9 @@ impl DeliveryReportRepository for PgDeliveryReportRepository { c.erp_customer_id, c.name AS customer_name, c.street AS cust_street, c.house_number AS cust_house_number, c.postal_code AS cust_postal_code, c.city AS cust_city, - c.country AS cust_country, + c.country AS cust_country, c.address_addition AS cust_addition, d.snap_street, d.snap_house_number, d.snap_postal_code, - d.snap_city, d.snap_country, + d.snap_city, d.snap_country, d.snap_addition, d.desired_time, d.special_agreements, d.prepaid_amount FROM deliveries d JOIN tours t ON t.id = d.tour_id @@ -432,19 +409,21 @@ impl DeliveryReportRepository for PgDeliveryReportRepository { }) .unwrap_or(0); - let snap = AddressParts { + let snap = Address { street: head.snap_street.unwrap_or_default(), house_number: head.snap_house_number.unwrap_or_default(), postal_code: head.snap_postal_code.unwrap_or_default(), city: head.snap_city.unwrap_or_default(), country: head.snap_country.unwrap_or_default(), + addition: head.snap_addition, }; - let orderer = AddressParts { + let orderer = Address { street: head.cust_street, house_number: head.cust_house_number, postal_code: head.cust_postal_code, city: head.cust_city, country: head.cust_country, + addition: head.cust_addition, }; let different_recipient = different_recipient( &head.customer_name, @@ -454,6 +433,7 @@ impl DeliveryReportRepository for PgDeliveryReportRepository { sources.iter().find(|s| s.role == "delivery"), ); let address = snap.one_line(); + let address_addition = snap.addition.clone(); Ok(Some(DeliveryReportData { generated_at: Utc::now(), @@ -470,6 +450,7 @@ impl DeliveryReportRepository for PgDeliveryReportRepository { customer_number: head.erp_customer_id, customer_name: head.customer_name, address, + address_addition, different_recipient, desired_time: head.desired_time, special_agreements: head.special_agreements, @@ -597,34 +578,13 @@ impl DeliveryReportRepository for PgDeliveryReportRepository { } } -/// Anschrift in Einzelteilen, wie sie am Kunden bzw. als Beleg-Snapshot liegt. -struct AddressParts { - street: String, - house_number: String, - postal_code: String, - city: String, - country: String, -} - -impl AddressParts { - fn one_line(&self) -> String { - one_line_address( - Some(self.street.clone()), - Some(self.house_number.clone()), - Some(self.postal_code.clone()), - Some(self.city.clone()), - Some(self.country.clone()), - ) - } - - /// Gleiche Anschrift, unabhängig von Groß-/Kleinschreibung und - /// Leerzeichen. Das Land zählt nicht (im ERP oft uneinheitlich gepflegt). - fn same_place(&self, other: &Self) -> bool { - normalized(&self.street) == normalized(&other.street) - && normalized(&self.house_number) == normalized(&other.house_number) - && normalized(&self.postal_code) == normalized(&other.postal_code) - && normalized(&self.city) == normalized(&other.city) - } +/// Gleiche Anschrift, unabhängig von Groß-/Kleinschreibung und Leerzeichen. +/// Land und Zusatz zählen nicht (im ERP uneinheitlich gepflegt). +fn same_place(a: &Address, b: &Address) -> bool { + normalized(&a.street) == normalized(&b.street) + && normalized(&a.house_number) == normalized(&b.house_number) + && normalized(&a.postal_code) == normalized(&b.postal_code) + && normalized(&a.city) == normalized(&b.city) } fn normalized(value: &str) -> String { @@ -637,8 +597,8 @@ fn normalized(value: &str) -> String { /// Kundennamen). fn different_recipient( customer_name: &str, - orderer: &AddressParts, - delivery_address: &AddressParts, + orderer: &Address, + delivery_address: &Address, header: Option<&ContactSourceRow>, delivery: Option<&ContactSourceRow>, ) -> Option { @@ -647,7 +607,7 @@ fn different_recipient( .unwrap_or_else(|| customer_name.to_string()); let recipient_name = delivery.and_then(ContactSourceRow::full_name); - let address_differs = !orderer.same_place(delivery_address); + let address_differs = !same_place(orderer, delivery_address); let name_differs = recipient_name .as_deref() .is_some_and(|n| normalized(n) != normalized(&orderer_name)); @@ -659,6 +619,7 @@ fn different_recipient( name: recipient_name.unwrap_or_else(|| customer_name.to_string()), phones: delivery.map(|s| s.phones.clone()).unwrap_or_default(), orderer_address: orderer.one_line(), + orderer_address_addition: orderer.addition.clone(), orderer_phones: header.map(|s| s.phones.clone()).unwrap_or_default(), }) } @@ -667,13 +628,14 @@ fn different_recipient( mod different_recipient_tests { use super::*; - fn addr(street: &str, city: &str) -> AddressParts { - AddressParts { + fn addr(street: &str, city: &str) -> Address { + Address { street: street.into(), house_number: "1".into(), postal_code: "41460".into(), city: city.into(), - country: "DE".into(), + country: "Deutschland".into(), + addition: None, } } @@ -714,7 +676,7 @@ mod different_recipient_tests { ) .expect("abweichend"); assert_eq!(r.name, "Musterfrau Erika"); - assert_eq!(r.orderer_address, "Am Sandberg 1, 41460 Neuss, DE"); + assert_eq!(r.orderer_address, "Am Sandberg 1, 41460 Neuss"); assert_eq!(r.phones, vec!["0151 1".to_string()]); } diff --git a/migrations/0034_address_addition.sql b/migrations/0034_address_addition.sql new file mode 100644 index 0000000..5d30546 --- /dev/null +++ b/migrations/0034_address_addition.sql @@ -0,0 +1,32 @@ +-- Adresszusatz getrennt vom Land. +-- +-- Das ERP-Textfeld `Adressen.Land` wird von der Sachbearbeitung praktisch +-- nie für das Land genutzt, sondern für Ortsteil, Etage oder Hinweise +-- („Holzheim", „3 Etage", „Parterre"). Bisher landete es in `country` und +-- erschien im Lieferbericht als vermeintliches Land. Ab jetzt: +-- * `*_addition` = dieser freie Zusatz, +-- * `country` = echtes Land aus dem ERP-Länderstamm (`LandID`). +-- +-- Bestandsdaten: der bisherige `country`-Text war immer dieses Freitextfeld +-- → in den Zusatz verschieben. Reine Deutschland-Kürzel („D", „DEU", …) +-- sind kein Zusatz und werden verworfen. Der nächste Sync setzt `country` +-- wieder korrekt aus dem Länderstamm. + +ALTER TABLE customers ADD COLUMN address_addition TEXT; +ALTER TABLE deliveries ADD COLUMN snap_addition TEXT; + +UPDATE customers + SET address_addition = CASE + WHEN lower(btrim(country)) IN ('', 'd', 'de', 'deu', 'deutschland', 'germany', 'brd') + THEN NULL + ELSE btrim(country) + END, + country = ''; + +UPDATE deliveries + SET snap_addition = CASE + WHEN lower(btrim(snap_country)) IN ('', 'd', 'de', 'deu', 'deutschland', 'germany', 'brd') + THEN NULL + ELSE btrim(snap_country) + END, + snap_country = '';