Skip to main content

koprogo_api/domain/services/
ownership_contract_exporter.rs

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