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: f64, // 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        let tantiemes = (ownership_percentage * building.total_tantiemes as f64) as i32;
159        current_layer.use_text(
160            format!("Tantièmes: {} sur {}", tantiemes, building.total_tantiemes),
161            10.0,
162            Mm(20.0),
163            Mm(y),
164            &font_bold,
165        );
166        y -= 6.0;
167
168        current_layer.use_text(
169            format!("Quote-part: {:.2}%", ownership_percentage * 100.0),
170            10.0,
171            Mm(20.0),
172            Mm(y),
173            &font_bold,
174        );
175        y -= 8.0;
176
177        // === ARTICLE 3: OWNER INFORMATION ===
178        current_layer.use_text(
179            "ARTICLE 3 - COPROPRIÉTAIRE".to_string(),
180            12.0,
181            Mm(20.0),
182            Mm(y),
183            &font_bold,
184        );
185        y -= 8.0;
186
187        let owner_name = format!("{} {}", owner.first_name, owner.last_name);
188
189        current_layer.use_text(format!("Nom: {}", owner_name), 10.0, Mm(20.0), Mm(y), &font);
190        y -= 6.0;
191
192        current_layer.use_text(
193            format!("Email: {}", owner.email),
194            10.0,
195            Mm(20.0),
196            Mm(y),
197            &font,
198        );
199        y -= 6.0;
200
201        if let Some(ref phone) = owner.phone {
202            current_layer.use_text(
203                format!("Téléphone: {}", phone),
204                10.0,
205                Mm(20.0),
206                Mm(y),
207                &font,
208            );
209            y -= 6.0;
210        }
211
212        current_layer.use_text(
213            format!(
214                "Date d'entrée en copropriété: {}",
215                ownership_start_date.format("%d/%m/%Y")
216            ),
217            10.0,
218            Mm(20.0),
219            Mm(y),
220            &font,
221        );
222        y -= 8.0;
223
224        // === ARTICLE 4: RIGHTS AND OBLIGATIONS ===
225        current_layer.use_text(
226            "ARTICLE 4 - DROITS ET OBLIGATIONS".to_string(),
227            12.0,
228            Mm(20.0),
229            Mm(y),
230            &font_bold,
231        );
232        y -= 8.0;
233
234        let rights_text = [
235            "Le copropriétaire dispose des droits suivants:",
236            "• Droit d'usage exclusif du lot ci-dessus désigné",
237            "• Droit de participation aux assemblées générales",
238            "• Droit de vote proportionnel à sa quote-part",
239            "• Droit d'accès aux parties communes",
240            "",
241            "Le copropriétaire est tenu aux obligations suivantes:",
242            "• Paiement des charges communes proportionnellement à sa quote-part",
243            "• Respect du règlement de copropriété",
244            "• Participation aux travaux votés en assemblée générale",
245            "• Entretien de son lot privatif",
246        ];
247
248        for line in rights_text.iter() {
249            if y < 80.0 {
250                break;
251            }
252            current_layer.use_text(line.to_string(), 9.0, Mm(20.0), Mm(y), &font);
253            y -= 5.0;
254        }
255        y -= 5.0;
256
257        // === ARTICLE 5: EXPENSES ===
258        current_layer.use_text(
259            "ARTICLE 5 - RÉPARTITION DES CHARGES".to_string(),
260            12.0,
261            Mm(20.0),
262            Mm(y),
263            &font_bold,
264        );
265        y -= 8.0;
266
267        current_layer.use_text(
268            format!(
269                "Les charges communes sont réparties selon la quote-part de {:.2}%",
270                ownership_percentage * 100.0
271            ),
272            10.0,
273            Mm(20.0),
274            Mm(y),
275            &font,
276        );
277        y -= 6.0;
278
279        current_layer.use_text(
280            "correspondant aux tantièmes du lot.".to_string(),
281            10.0,
282            Mm(20.0),
283            Mm(y),
284            &font,
285        );
286        y -= 10.0;
287
288        // === SIGNATURES ===
289        if y < 40.0 {
290            y = 40.0;
291        }
292
293        current_layer.use_text("SIGNATURES".to_string(), 12.0, Mm(20.0), Mm(y), &font_bold);
294        y -= 10.0;
295
296        current_layer.use_text(
297            "Le Syndic: ________________".to_string(),
298            10.0,
299            Mm(20.0),
300            Mm(y),
301            &font,
302        );
303
304        current_layer.use_text(
305            "Le Copropriétaire: ________________".to_string(),
306            10.0,
307            Mm(120.0),
308            Mm(y),
309            &font,
310        );
311        y -= 6.0;
312
313        current_layer.use_text(
314            "Date: ________________".to_string(),
315            10.0,
316            Mm(20.0),
317            Mm(y),
318            &font,
319        );
320
321        current_layer.use_text(
322            "Date: ________________".to_string(),
323            10.0,
324            Mm(120.0),
325            Mm(y),
326            &font,
327        );
328
329        // Save to bytes
330        let mut buffer = Vec::new();
331        doc.save(&mut BufWriter::new(&mut buffer))
332            .map_err(|e| e.to_string())?;
333
334        Ok(buffer)
335    }
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341    use uuid::Uuid;
342
343    #[test]
344    fn test_export_ownership_contract_pdf() {
345        let building = Building {
346            id: Uuid::new_v4(),
347            name: "Les Jardins de Bruxelles".to_string(),
348            address: "123 Avenue Louise".to_string(),
349            city: "Bruxelles".to_string(),
350            postal_code: "1000".to_string(),
351            country: "Belgium".to_string(),
352            total_units: 10,
353            total_tantiemes: 1000,
354            construction_year: Some(1990),
355            syndic_name: None,
356            syndic_email: None,
357            syndic_phone: None,
358            syndic_address: None,
359            syndic_office_hours: None,
360            syndic_emergency_contact: None,
361            slug: None,
362            organization_id: Uuid::new_v4(),
363            created_at: Utc::now(),
364            updated_at: Utc::now(),
365        };
366
367        let unit = Unit {
368            id: Uuid::new_v4(),
369            organization_id: building.organization_id,
370            building_id: building.id,
371            unit_number: "A1".to_string(),
372            unit_type: crate::domain::entities::UnitType::Apartment,
373            floor: Some(1),
374            surface_area: 75.5,
375            quota: 150.0,
376            owner_id: None,
377            created_at: Utc::now(),
378            updated_at: Utc::now(),
379        };
380
381        let owner = Owner {
382            id: Uuid::new_v4(),
383            organization_id: building.organization_id,
384            user_id: None,
385            first_name: "Jean".to_string(),
386            last_name: "Dupont".to_string(),
387            email: "jean@example.com".to_string(),
388            phone: Some("+32 2 123 45 67".to_string()),
389            address: "123 Rue de Test".to_string(),
390            city: "Bruxelles".to_string(),
391            postal_code: "1000".to_string(),
392            country: "Belgium".to_string(),
393            created_at: Utc::now(),
394            updated_at: Utc::now(),
395        };
396
397        let result = OwnershipContractExporter::export_to_pdf(
398            &building,
399            &unit,
400            &owner,
401            0.15,                                     // 15% ownership
402            Utc::now() - chrono::Duration::days(365), // Started 1 year ago
403        );
404
405        assert!(result.is_ok());
406        let pdf_bytes = result.unwrap();
407        assert!(!pdf_bytes.is_empty());
408        assert!(pdf_bytes.len() > 100);
409    }
410}