Skip to main content

koprogo_api/domain/services/
ownership_contract_exporter.rs

1use crate::domain::entities::{Building, Owner, Unit};
2use chrono::{DateTime, Utc};
3use printpdf::*;
4use std::io::BufWriter;
5
6/// Ownership Contract Exporter - Generates PDF for Contrat de Copropriété
7///
8/// Generates formal ownership contracts for unit purchases.
9pub struct OwnershipContractExporter;
10
11impl OwnershipContractExporter {
12    /// Export ownership contract to PDF bytes
13    ///
14    /// Generates a Contrat de Copropriété including:
15    /// - Building information
16    /// - Unit details (number, floor, area, tantièmes)
17    /// - Owner information
18    /// - Ownership start date
19    /// - Percentage owned
20    /// - Rights and obligations
21    /// - General assembly rules
22    /// - Expense allocation rules
23    pub fn export_to_pdf(
24        building: &Building,
25        unit: &Unit,
26        owner: &Owner,
27        ownership_percentage: rust_decimal::Decimal, // 0.0 to 1.0
28        ownership_start_date: DateTime<Utc>,
29    ) -> Result<Vec<u8>, String> {
30        // Create PDF document (A4: 210mm x 297mm)
31        let (doc, page1, layer1) =
32            PdfDocument::new("Contrat de Copropriété", Mm(210.0), Mm(297.0), "Layer 1");
33        let current_layer = doc.get_page(page1).get_layer(layer1);
34
35        // Load fonts
36        let font = doc
37            .add_builtin_font(BuiltinFont::Helvetica)
38            .map_err(|e| e.to_string())?;
39        let font_bold = doc
40            .add_builtin_font(BuiltinFont::HelveticaBold)
41            .map_err(|e| e.to_string())?;
42
43        let mut y = 270.0; // Start from top
44
45        // === HEADER ===
46        current_layer.use_text(
47            "CONTRAT DE COPROPRIÉTÉ".to_string(),
48            18.0,
49            Mm(20.0),
50            Mm(y),
51            &font_bold,
52        );
53        y -= 15.0;
54
55        current_layer.use_text(
56            format!("Date d'établissement: {}", Utc::now().format("%d/%m/%Y")),
57            10.0,
58            Mm(20.0),
59            Mm(y),
60            &font,
61        );
62        y -= 15.0;
63
64        // === ARTICLE 1: BUILDING INFORMATION ===
65        current_layer.use_text(
66            "ARTICLE 1 - IMMEUBLE CONCERNÉ".to_string(),
67            12.0,
68            Mm(20.0),
69            Mm(y),
70            &font_bold,
71        );
72        y -= 8.0;
73
74        current_layer.use_text(
75            format!("Dénomination: {}", building.name),
76            10.0,
77            Mm(20.0),
78            Mm(y),
79            &font,
80        );
81        y -= 6.0;
82
83        current_layer.use_text(
84            format!(
85                "Adresse: {}, {} {}, {}",
86                building.address, building.postal_code, building.city, building.country
87            ),
88            10.0,
89            Mm(20.0),
90            Mm(y),
91            &font,
92        );
93        y -= 6.0;
94
95        current_layer.use_text(
96            format!("Nombre total de lots: {}", building.total_units),
97            10.0,
98            Mm(20.0),
99            Mm(y),
100            &font,
101        );
102        y -= 6.0;
103
104        if let Some(year) = building.construction_year {
105            current_layer.use_text(
106                format!("Année de construction: {}", year),
107                10.0,
108                Mm(20.0),
109                Mm(y),
110                &font,
111            );
112            y -= 6.0;
113        }
114        y -= 8.0;
115
116        // === ARTICLE 2: UNIT DETAILS ===
117        current_layer.use_text(
118            "ARTICLE 2 - DESCRIPTION DU LOT".to_string(),
119            12.0,
120            Mm(20.0),
121            Mm(y),
122            &font_bold,
123        );
124        y -= 8.0;
125
126        current_layer.use_text(
127            format!("Numéro de lot: {}", unit.unit_number),
128            10.0,
129            Mm(20.0),
130            Mm(y),
131            &font,
132        );
133        y -= 6.0;
134
135        if let Some(floor) = unit.floor {
136            current_layer.use_text(format!("Étage: {}", floor), 10.0, Mm(20.0), Mm(y), &font);
137            y -= 6.0;
138        }
139
140        current_layer.use_text(
141            format!("Superficie: {:.2} m²", unit.surface_area),
142            10.0,
143            Mm(20.0),
144            Mm(y),
145            &font,
146        );
147        y -= 6.0;
148
149        current_layer.use_text(
150            format!("Type: {:?}", unit.unit_type),
151            10.0,
152            Mm(20.0),
153            Mm(y),
154            &font,
155        );
156        y -= 6.0;
157
158        use rust_decimal::prelude::ToPrimitive;
159        let tantiemes_dec =
160            ownership_percentage * rust_decimal::Decimal::from(building.total_tantiemes);
161        let tantiemes = tantiemes_dec.trunc().to_i32().unwrap_or(0);
162        current_layer.use_text(
163            format!("Tantièmes: {} sur {}", tantiemes, building.total_tantiemes),
164            10.0,
165            Mm(20.0),
166            Mm(y),
167            &font_bold,
168        );
169        y -= 6.0;
170
171        current_layer.use_text(
172            format!(
173                "Quote-part: {:.2}%",
174                ownership_percentage * rust_decimal_macros::dec!(100)
175            ),
176            10.0,
177            Mm(20.0),
178            Mm(y),
179            &font_bold,
180        );
181        y -= 8.0;
182
183        // === ARTICLE 3: OWNER INFORMATION ===
184        current_layer.use_text(
185            "ARTICLE 3 - COPROPRIÉTAIRE".to_string(),
186            12.0,
187            Mm(20.0),
188            Mm(y),
189            &font_bold,
190        );
191        y -= 8.0;
192
193        let owner_name = format!("{} {}", owner.first_name, owner.last_name);
194
195        current_layer.use_text(format!("Nom: {}", owner_name), 10.0, Mm(20.0), Mm(y), &font);
196        y -= 6.0;
197
198        current_layer.use_text(
199            format!("Email: {}", owner.email),
200            10.0,
201            Mm(20.0),
202            Mm(y),
203            &font,
204        );
205        y -= 6.0;
206
207        if let Some(ref phone) = owner.phone {
208            current_layer.use_text(
209                format!("Téléphone: {}", phone),
210                10.0,
211                Mm(20.0),
212                Mm(y),
213                &font,
214            );
215            y -= 6.0;
216        }
217
218        current_layer.use_text(
219            format!(
220                "Date d'entrée en copropriété: {}",
221                ownership_start_date.format("%d/%m/%Y")
222            ),
223            10.0,
224            Mm(20.0),
225            Mm(y),
226            &font,
227        );
228        y -= 8.0;
229
230        // === ARTICLE 4: RIGHTS AND OBLIGATIONS ===
231        current_layer.use_text(
232            "ARTICLE 4 - DROITS ET OBLIGATIONS".to_string(),
233            12.0,
234            Mm(20.0),
235            Mm(y),
236            &font_bold,
237        );
238        y -= 8.0;
239
240        let rights_text = [
241            "Le copropriétaire dispose des droits suivants:",
242            "• Droit d'usage exclusif du lot ci-dessus désigné",
243            "• Droit de participation aux assemblées générales",
244            "• Droit de vote proportionnel à sa quote-part",
245            "• Droit d'accès aux parties communes",
246            "",
247            "Le copropriétaire est tenu aux obligations suivantes:",
248            "• Paiement des charges communes proportionnellement à sa quote-part",
249            "• Respect du règlement de copropriété",
250            "• Participation aux travaux votés en assemblée générale",
251            "• Entretien de son lot privatif",
252        ];
253
254        for line in rights_text.iter() {
255            if y < 80.0 {
256                break;
257            }
258            current_layer.use_text(line.to_string(), 9.0, Mm(20.0), Mm(y), &font);
259            y -= 5.0;
260        }
261        y -= 5.0;
262
263        // === ARTICLE 5: EXPENSES ===
264        current_layer.use_text(
265            "ARTICLE 5 - RÉPARTITION DES CHARGES".to_string(),
266            12.0,
267            Mm(20.0),
268            Mm(y),
269            &font_bold,
270        );
271        y -= 8.0;
272
273        current_layer.use_text(
274            format!(
275                "Les charges communes sont réparties selon la quote-part de {:.2}%",
276                ownership_percentage * rust_decimal_macros::dec!(100)
277            ),
278            10.0,
279            Mm(20.0),
280            Mm(y),
281            &font,
282        );
283        y -= 6.0;
284
285        current_layer.use_text(
286            "correspondant aux tantièmes du lot.".to_string(),
287            10.0,
288            Mm(20.0),
289            Mm(y),
290            &font,
291        );
292        y -= 10.0;
293
294        // === SIGNATURES ===
295        if y < 40.0 {
296            y = 40.0;
297        }
298
299        current_layer.use_text("SIGNATURES".to_string(), 12.0, Mm(20.0), Mm(y), &font_bold);
300        y -= 10.0;
301
302        current_layer.use_text(
303            "Le Syndic: ________________".to_string(),
304            10.0,
305            Mm(20.0),
306            Mm(y),
307            &font,
308        );
309
310        current_layer.use_text(
311            "Le Copropriétaire: ________________".to_string(),
312            10.0,
313            Mm(120.0),
314            Mm(y),
315            &font,
316        );
317        y -= 6.0;
318
319        current_layer.use_text(
320            "Date: ________________".to_string(),
321            10.0,
322            Mm(20.0),
323            Mm(y),
324            &font,
325        );
326
327        current_layer.use_text(
328            "Date: ________________".to_string(),
329            10.0,
330            Mm(120.0),
331            Mm(y),
332            &font,
333        );
334
335        // Save to bytes
336        let mut buffer = Vec::new();
337        doc.save(&mut BufWriter::new(&mut buffer))
338            .map_err(|e| e.to_string())?;
339
340        Ok(buffer)
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347    use uuid::Uuid;
348
349    #[test]
350    fn test_export_ownership_contract_pdf() {
351        let building = Building {
352            id: Uuid::new_v4(),
353            name: "Les Jardins de Bruxelles".to_string(),
354            address: "123 Avenue Louise".to_string(),
355            city: "Bruxelles".to_string(),
356            postal_code: "1000".to_string(),
357            country: "Belgium".to_string(),
358            total_units: 10,
359            total_tantiemes: 1000,
360            construction_year: Some(1990),
361            syndic_name: None,
362            syndic_email: None,
363            syndic_phone: None,
364            syndic_address: None,
365            syndic_office_hours: None,
366            syndic_emergency_contact: None,
367            slug: None,
368            organization_id: Uuid::new_v4(),
369            created_at: Utc::now(),
370            updated_at: Utc::now(),
371        };
372
373        let unit = Unit {
374            id: Uuid::new_v4(),
375            organization_id: building.organization_id,
376            building_id: building.id,
377            unit_number: "A1".to_string(),
378            unit_type: crate::domain::entities::UnitType::Apartment,
379            floor: Some(1),
380            surface_area: 75.5,
381            quota: rust_decimal_macros::dec!(150),
382            owner_id: None,
383            created_at: Utc::now(),
384            updated_at: Utc::now(),
385        };
386
387        let owner = Owner {
388            id: Uuid::new_v4(),
389            organization_id: building.organization_id,
390            user_id: None,
391            first_name: "Jean".to_string(),
392            last_name: "Dupont".to_string(),
393            email: "jean@example.com".to_string(),
394            phone: Some("+32 2 123 45 67".to_string()),
395            address: "123 Rue de Test".to_string(),
396            city: "Bruxelles".to_string(),
397            postal_code: "1000".to_string(),
398            country: "Belgium".to_string(),
399            created_at: Utc::now(),
400            updated_at: Utc::now(),
401        };
402
403        let result = OwnershipContractExporter::export_to_pdf(
404            &building,
405            &unit,
406            &owner,
407            rust_decimal_macros::dec!(0.15),          // 15% ownership
408            Utc::now() - chrono::Duration::days(365), // Started 1 year ago
409        );
410
411        assert!(result.is_ok());
412        let pdf_bytes = result.unwrap();
413        assert!(!pdf_bytes.is_empty());
414        assert!(pdf_bytes.len() > 100);
415    }
416}