1use crate::domain::entities::{Building, Expense, Owner, Unit};
2use crate::domain::services::pdf_writer::{
3 builtin_font, new_document, save_document, PdfPageBuilder,
4};
5use chrono::{DateTime, Utc};
6use printpdf::BuiltinFont;
7use rust_decimal::Decimal;
8
9pub struct OwnerStatementExporter;
15
16#[derive(Debug, Clone)]
17pub struct UnitWithOwnership {
18 pub unit: Unit,
19 pub ownership_percentage: Decimal, }
21
22impl OwnerStatementExporter {
23 pub fn export_to_pdf(
33 owner: &Owner,
34 building: &Building,
35 units: &[UnitWithOwnership],
36 expenses: &[Expense],
37 start_date: DateTime<Utc>,
38 end_date: DateTime<Utc>,
39 ) -> Result<Vec<u8>, String> {
40 let doc = new_document("Relevé de Charges");
42 let mut current_layer = PdfPageBuilder::new();
43
44 let font = builtin_font(BuiltinFont::Helvetica);
46 let font_bold = builtin_font(BuiltinFont::HelveticaBold);
47
48 let mut y = 270.0; current_layer.text("RELEVÉ DE CHARGES".to_string(), 18.0, 20.0, y, &font_bold);
52 y -= 15.0;
53
54 current_layer.text(
56 format!("Copropriété: {}", building.name),
57 12.0,
58 20.0,
59 y,
60 &font_bold,
61 );
62 y -= 7.0;
63
64 current_layer.text(
65 format!("Adresse: {}", building.address),
66 10.0,
67 20.0,
68 y,
69 &font,
70 );
71 y -= 10.0;
72
73 let period = format!(
75 "Période: du {} au {}",
76 start_date.format("%d/%m/%Y"),
77 end_date.format("%d/%m/%Y")
78 );
79 current_layer.text(period, 10.0, 20.0, y, &font);
80 y -= 10.0;
81
82 current_layer.text("COPROPRIÉTAIRE".to_string(), 14.0, 20.0, y, &font_bold);
84 y -= 8.0;
85
86 current_layer.text(
87 format!("{} {}", owner.first_name, owner.last_name),
88 11.0,
89 20.0,
90 y,
91 &font,
92 );
93 y -= 6.0;
94
95 current_layer.text(format!("Email: {}", owner.email), 10.0, 20.0, y, &font);
96 y -= 6.0;
97
98 if let Some(ref phone) = owner.phone {
99 current_layer.text(format!("Téléphone: {}", phone), 10.0, 20.0, y, &font);
100 y -= 6.0;
101 }
102 y -= 5.0;
103
104 current_layer.text("LOTS DÉTENUS".to_string(), 14.0, 20.0, y, &font_bold);
106 y -= 8.0;
107
108 current_layer.text("Lot", 10.0, 20.0, y, &font_bold);
109 current_layer.text("Étage", 10.0, 60.0, y, &font_bold);
110 current_layer.text("Surface", 10.0, 90.0, y, &font_bold);
111 current_layer.text("Quote-part", 10.0, 130.0, y, &font_bold);
112 y -= 6.0;
113
114 for unit_info in units {
115 if y < 100.0 {
116 break;
118 }
119
120 current_layer.text(unit_info.unit.unit_number.as_str(), 9.0, 20.0, y, &font);
121
122 if let Some(floor) = unit_info.unit.floor {
123 current_layer.text(floor.to_string(), 9.0, 60.0, y, &font);
124 }
125
126 current_layer.text(
127 format!("{:.2} m²", unit_info.unit.surface_area),
128 9.0,
129 90.0,
130 y,
131 &font,
132 );
133
134 current_layer.text(
135 format!(
136 "{:.2}%",
137 unit_info.ownership_percentage * rust_decimal_macros::dec!(100)
138 ),
139 9.0,
140 130.0,
141 y,
142 &font,
143 );
144 y -= 5.0;
145 }
146 y -= 8.0;
147
148 current_layer.text("DÉTAIL DES CHARGES".to_string(), 14.0, 20.0, y, &font_bold);
150 y -= 8.0;
151
152 current_layer.text("Date", 10.0, 20.0, y, &font_bold);
153 current_layer.text("Description", 10.0, 50.0, y, &font_bold);
154 current_layer.text("Montant", 10.0, 140.0, y, &font_bold);
155 current_layer.text("Statut", 10.0, 170.0, y, &font_bold);
156 y -= 6.0;
157
158 let mut total_amount = Decimal::ZERO;
159 let mut total_paid = Decimal::ZERO;
160
161 for expense in expenses {
162 if y < 50.0 {
163 break;
165 }
166
167 current_layer.text(
168 expense.expense_date.format("%d/%m/%Y").to_string(),
169 9.0,
170 20.0,
171 y,
172 &font,
173 );
174
175 let description = if expense.description.len() > 30 {
176 format!("{}...", &expense.description[..30])
177 } else {
178 expense.description.clone()
179 };
180 current_layer.text(description, 9.0, 50.0, y, &font);
181
182 current_layer.text(format!("{:.2} €", expense.amount), 9.0, 140.0, y, &font);
183
184 let status = if expense.is_paid() {
185 "Payée"
186 } else {
187 "En attente"
188 };
189 current_layer.text(status.to_string(), 9.0, 170.0, y, &font);
190
191 total_amount += expense.amount;
192 if expense.is_paid() {
193 total_paid += expense.amount;
194 }
195
196 y -= 5.0;
197 }
198 y -= 10.0;
199
200 current_layer.text("RÉCAPITULATIF".to_string(), 14.0, 20.0, y, &font_bold);
202 y -= 8.0;
203
204 current_layer.text(
205 format!("Total des charges: {:.2} €", total_amount),
206 11.0,
207 20.0,
208 y,
209 &font,
210 );
211 y -= 6.0;
212
213 current_layer.text(
214 format!("Montant payé: {:.2} €", total_paid),
215 11.0,
216 20.0,
217 y,
218 &font,
219 );
220 y -= 6.0;
221
222 let amount_due = total_amount - total_paid;
223 current_layer.text(
224 format!("Montant dû: {:.2} €", amount_due),
225 12.0,
226 20.0,
227 y,
228 &font_bold,
229 );
230 y -= 10.0;
231
232 if amount_due > Decimal::ZERO {
234 current_layer.text(
235 "Modalités de paiement:".to_string(),
236 10.0,
237 20.0,
238 y,
239 &font_bold,
240 );
241 y -= 6.0;
242
243 current_layer.text(
244 "Merci d'effectuer votre paiement par virement bancaire".to_string(),
245 9.0,
246 20.0,
247 y,
248 &font,
249 );
250 y -= 5.0;
251
252 current_layer.text(
253 "avec la référence suivante en communication.".to_string(),
254 9.0,
255 20.0,
256 y,
257 &font,
258 );
259 }
260
261 let page = current_layer.into_page(210.0, 297.0);
263 Ok(save_document(doc, page))
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use crate::domain::entities::ExpenseCategory;
271 use uuid::Uuid;
272
273 #[test]
274 fn test_export_owner_statement_pdf() {
275 let owner = Owner {
276 id: Uuid::new_v4(),
277 organization_id: Uuid::new_v4(),
278 user_id: None,
279 first_name: "Jean".to_string(),
280 last_name: "Dupont".to_string(),
281 email: "jean@example.com".to_string(),
282 phone: Some("+32 2 123 45 67".to_string()),
283 address: "123 Rue de Test".to_string(),
284 city: "Bruxelles".to_string(),
285 postal_code: "1000".to_string(),
286 country: "Belgium".to_string(),
287 created_at: Utc::now(),
288 updated_at: Utc::now(),
289 };
290
291 let building = Building {
292 id: Uuid::new_v4(),
293 name: "Les Jardins de Bruxelles".to_string(),
294 address: "123 Avenue Louise".to_string(),
295 city: "Bruxelles".to_string(),
296 postal_code: "1000".to_string(),
297 country: "Belgium".to_string(),
298 total_units: 10,
299 total_tantiemes: 1000,
300 construction_year: Some(1990),
301 syndic_name: None,
302 syndic_email: None,
303 syndic_phone: None,
304 syndic_address: None,
305 syndic_office_hours: None,
306 syndic_emergency_contact: None,
307 slug: None,
308 acp_id: Uuid::new_v4(),
309 created_at: Utc::now(),
310 updated_at: Utc::now(),
311 };
312
313 let unit = Unit {
314 id: Uuid::new_v4(),
315 acp_id: building.acp_id,
316 building_id: building.id,
317 unit_number: "A1".to_string(),
318 unit_type: crate::domain::entities::UnitType::Apartment,
319 floor: Some(1),
320 surface_area: 75.5,
321 quota: rust_decimal_macros::dec!(150),
322 owner_id: None,
323 created_at: Utc::now(),
324 updated_at: Utc::now(),
325 };
326
327 let units = vec![UnitWithOwnership {
328 unit,
329 ownership_percentage: rust_decimal_macros::dec!(0.15), }];
331
332 let expenses = vec![Expense {
333 id: Uuid::new_v4(),
334 acp_id: Uuid::new_v4(),
335 building_id: building.id,
336 organization_id: owner.organization_id,
337 description: "Entretien ascenseur".to_string(),
338 amount: rust_decimal_macros::dec!(150),
339 amount_excl_vat: Some(rust_decimal_macros::dec!(123.97)),
340 vat_rate: Some(rust_decimal_macros::dec!(21)),
341 vat_amount: Some(rust_decimal_macros::dec!(26.03)),
342 amount_incl_vat: Some(rust_decimal_macros::dec!(150)),
343 expense_date: Utc::now(),
344 invoice_date: None,
345 due_date: None,
346 paid_date: None,
347 category: ExpenseCategory::Maintenance,
348 approval_status: crate::domain::entities::ApprovalStatus::Approved,
349 submitted_at: None,
350 approved_by: None,
351 approved_at: None,
352 rejection_reason: None,
353 payment_status: crate::domain::entities::PaymentStatus::Pending,
354 supplier: None,
355 invoice_number: Some("INV-001".to_string()),
356 account_code: None,
357 created_at: Utc::now(),
358 updated_at: Utc::now(),
359 contractor_report_id: None,
360 }];
361
362 let result = OwnerStatementExporter::export_to_pdf(
363 &owner,
364 &building,
365 &units,
366 &expenses,
367 Utc::now() - chrono::Duration::days(30),
368 Utc::now(),
369 );
370
371 assert!(result.is_ok());
372 let pdf_bytes = result.unwrap();
373 assert!(!pdf_bytes.is_empty());
374 assert!(pdf_bytes.len() > 100);
375 }
376}