1use chrono::{DateTime, NaiveDate, Utc};
9use rust_decimal::Decimal;
10use serde::{Deserialize, Serialize};
11use uuid::Uuid;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
15#[serde(rename_all = "snake_case")]
16pub enum RelatedPartyType {
17 #[default]
19 Subsidiary,
20 Associate,
22 JointVenture,
24 KeyManagement,
27 CloseFamily,
29 ShareholderSignificant,
31 CommonDirector,
33 Other,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
39#[serde(rename_all = "snake_case")]
40pub enum RelationshipBasis {
41 #[default]
43 Ownership,
44 Control,
46 SignificantInfluence,
48 KeyManagementPersonnel,
50 CloseFamily,
52 Other,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
58#[serde(rename_all = "snake_case")]
59pub enum IdentificationSource {
60 #[default]
62 ManagementDisclosure,
63 AuditorInquiry,
65 PublicRecords,
67 BankConfirmation,
69 LegalReview,
71 WhistleblowerTip,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
77#[serde(rename_all = "snake_case")]
78pub enum RptTransactionType {
79 #[default]
81 Sale,
82 Purchase,
84 Lease,
86 Loan,
88 Guarantee,
90 ManagementFee,
92 Dividend,
94 Transfer,
96 ServiceAgreement,
98 LicenseRoyalty,
100 CapitalContribution,
102 Other,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct RelatedParty {
109 pub party_id: Uuid,
111 pub party_ref: String,
113 pub engagement_id: Uuid,
115
116 pub party_name: String,
119 pub party_type: RelatedPartyType,
121 pub relationship_basis: RelationshipBasis,
123
124 pub ownership_percentage: Option<f64>,
127 pub board_representation: bool,
129 pub key_management: bool,
131
132 pub disclosed_in_financials: bool,
135 pub disclosure_adequate: Option<bool>,
137
138 pub identified_by: IdentificationSource,
141
142 #[serde(with = "crate::serde_timestamp::utc")]
144 pub created_at: DateTime<Utc>,
145 #[serde(with = "crate::serde_timestamp::utc")]
146 pub updated_at: DateTime<Utc>,
147}
148
149impl RelatedParty {
150 pub fn new(
152 engagement_id: Uuid,
153 party_name: impl Into<String>,
154 party_type: RelatedPartyType,
155 relationship_basis: RelationshipBasis,
156 ) -> Self {
157 let now = Utc::now();
158 let id = Uuid::new_v4();
159 let party_ref = format!("RP-{}", &id.simple().to_string()[..8]);
160 Self {
161 party_id: id,
162 party_ref,
163 engagement_id,
164 party_name: party_name.into(),
165 party_type,
166 relationship_basis,
167 ownership_percentage: None,
168 board_representation: false,
169 key_management: false,
170 disclosed_in_financials: true,
171 disclosure_adequate: None,
172 identified_by: IdentificationSource::ManagementDisclosure,
173 created_at: now,
174 updated_at: now,
175 }
176 }
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct RelatedPartyTransaction {
182 pub transaction_id: Uuid,
184 pub transaction_ref: String,
186 pub engagement_id: Uuid,
188 pub related_party_id: Uuid,
190
191 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub journal_entry_id: Option<String>,
196 pub transaction_type: RptTransactionType,
198 pub description: String,
200 pub amount: Decimal,
202 pub currency: String,
204 pub transaction_date: NaiveDate,
206 pub terms_description: String,
208
209 pub arms_length: Option<bool>,
212 pub arms_length_evidence: Option<String>,
214 pub business_rationale: Option<String>,
216 pub approved_by: Option<String>,
218
219 pub disclosed_in_financials: bool,
222 pub disclosure_adequate: Option<bool>,
224
225 pub management_override_risk: bool,
228
229 #[serde(with = "crate::serde_timestamp::utc")]
231 pub created_at: DateTime<Utc>,
232 #[serde(with = "crate::serde_timestamp::utc")]
233 pub updated_at: DateTime<Utc>,
234}
235
236impl RelatedPartyTransaction {
237 #[allow(clippy::too_many_arguments)]
239 pub fn new(
240 engagement_id: Uuid,
241 related_party_id: Uuid,
242 transaction_type: RptTransactionType,
243 description: impl Into<String>,
244 amount: Decimal,
245 currency: impl Into<String>,
246 transaction_date: NaiveDate,
247 ) -> Self {
248 let now = Utc::now();
249 let id = Uuid::new_v4();
250 let transaction_ref = format!("RPT-{}", &id.simple().to_string()[..8]);
251 Self {
252 transaction_id: id,
253 transaction_ref,
254 engagement_id,
255 related_party_id,
256 journal_entry_id: None,
257 transaction_type,
258 description: description.into(),
259 amount,
260 currency: currency.into(),
261 transaction_date,
262 terms_description: String::new(),
263 arms_length: None,
264 arms_length_evidence: None,
265 business_rationale: None,
266 approved_by: None,
267 disclosed_in_financials: true,
268 disclosure_adequate: None,
269 management_override_risk: false,
270 created_at: now,
271 updated_at: now,
272 }
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279 use rust_decimal_macros::dec;
280
281 fn sample_date(year: i32, month: u32, day: u32) -> NaiveDate {
282 NaiveDate::from_ymd_opt(year, month, day).unwrap()
283 }
284
285 #[test]
286 fn test_new_related_party() {
287 let eng = Uuid::new_v4();
288 let rp = RelatedParty::new(
289 eng,
290 "Acme Holdings Ltd",
291 RelatedPartyType::Subsidiary,
292 RelationshipBasis::Ownership,
293 );
294
295 assert_eq!(rp.engagement_id, eng);
296 assert_eq!(rp.party_name, "Acme Holdings Ltd");
297 assert_eq!(rp.party_type, RelatedPartyType::Subsidiary);
298 assert_eq!(rp.relationship_basis, RelationshipBasis::Ownership);
299 assert!(rp.disclosed_in_financials);
300 assert!(!rp.board_representation);
301 assert!(!rp.key_management);
302 assert!(rp.ownership_percentage.is_none());
303 assert!(rp.disclosure_adequate.is_none());
304 assert_eq!(rp.identified_by, IdentificationSource::ManagementDisclosure);
305 assert!(rp.party_ref.starts_with("RP-"));
306 assert_eq!(rp.party_ref.len(), 11); }
308
309 #[test]
310 fn test_new_rpt() {
311 let eng = Uuid::new_v4();
312 let party = Uuid::new_v4();
313 let rpt = RelatedPartyTransaction::new(
314 eng,
315 party,
316 RptTransactionType::ManagementFee,
317 "Annual management fee for shared services",
318 dec!(250_000),
319 "USD",
320 sample_date(2025, 6, 30),
321 );
322
323 assert_eq!(rpt.engagement_id, eng);
324 assert_eq!(rpt.related_party_id, party);
325 assert_eq!(rpt.transaction_type, RptTransactionType::ManagementFee);
326 assert_eq!(rpt.amount, dec!(250_000));
327 assert_eq!(rpt.currency, "USD");
328 assert!(rpt.disclosed_in_financials);
329 assert!(!rpt.management_override_risk);
330 assert!(rpt.arms_length.is_none());
331 assert!(rpt.terms_description.is_empty());
332 assert!(rpt.transaction_ref.starts_with("RPT-"));
333 assert_eq!(rpt.transaction_ref.len(), 12); }
335
336 #[test]
337 fn test_related_party_type_serde() {
338 let variants = [
339 RelatedPartyType::Subsidiary,
340 RelatedPartyType::Associate,
341 RelatedPartyType::JointVenture,
342 RelatedPartyType::KeyManagement,
343 RelatedPartyType::CloseFamily,
344 RelatedPartyType::ShareholderSignificant,
345 RelatedPartyType::CommonDirector,
346 RelatedPartyType::Other,
347 ];
348 for v in variants {
349 let json = serde_json::to_string(&v).unwrap();
350 let rt: RelatedPartyType = serde_json::from_str(&json).unwrap();
351 assert_eq!(v, rt);
352 }
353 assert_eq!(
354 serde_json::to_string(&RelatedPartyType::JointVenture).unwrap(),
355 "\"joint_venture\""
356 );
357 assert_eq!(
358 serde_json::to_string(&RelatedPartyType::ShareholderSignificant).unwrap(),
359 "\"shareholder_significant\""
360 );
361 assert_eq!(
362 serde_json::to_string(&RelatedPartyType::CommonDirector).unwrap(),
363 "\"common_director\""
364 );
365 }
366
367 #[test]
368 fn test_relationship_basis_serde() {
369 let variants = [
370 RelationshipBasis::Ownership,
371 RelationshipBasis::Control,
372 RelationshipBasis::SignificantInfluence,
373 RelationshipBasis::KeyManagementPersonnel,
374 RelationshipBasis::CloseFamily,
375 RelationshipBasis::Other,
376 ];
377 for v in variants {
378 let json = serde_json::to_string(&v).unwrap();
379 let rt: RelationshipBasis = serde_json::from_str(&json).unwrap();
380 assert_eq!(v, rt);
381 }
382 assert_eq!(
383 serde_json::to_string(&RelationshipBasis::SignificantInfluence).unwrap(),
384 "\"significant_influence\""
385 );
386 assert_eq!(
387 serde_json::to_string(&RelationshipBasis::KeyManagementPersonnel).unwrap(),
388 "\"key_management_personnel\""
389 );
390 }
391
392 #[test]
393 fn test_identification_source_serde() {
394 let variants = [
395 IdentificationSource::ManagementDisclosure,
396 IdentificationSource::AuditorInquiry,
397 IdentificationSource::PublicRecords,
398 IdentificationSource::BankConfirmation,
399 IdentificationSource::LegalReview,
400 IdentificationSource::WhistleblowerTip,
401 ];
402 for v in variants {
403 let json = serde_json::to_string(&v).unwrap();
404 let rt: IdentificationSource = serde_json::from_str(&json).unwrap();
405 assert_eq!(v, rt);
406 }
407 assert_eq!(
408 serde_json::to_string(&IdentificationSource::ManagementDisclosure).unwrap(),
409 "\"management_disclosure\""
410 );
411 assert_eq!(
412 serde_json::to_string(&IdentificationSource::WhistleblowerTip).unwrap(),
413 "\"whistleblower_tip\""
414 );
415 }
416
417 #[test]
418 fn test_rpt_transaction_type_serde() {
419 let variants = [
421 RptTransactionType::Sale,
422 RptTransactionType::Purchase,
423 RptTransactionType::Lease,
424 RptTransactionType::Loan,
425 RptTransactionType::Guarantee,
426 RptTransactionType::ManagementFee,
427 RptTransactionType::Dividend,
428 RptTransactionType::Transfer,
429 RptTransactionType::ServiceAgreement,
430 RptTransactionType::LicenseRoyalty,
431 RptTransactionType::CapitalContribution,
432 RptTransactionType::Other,
433 ];
434 for v in variants {
435 let json = serde_json::to_string(&v).unwrap();
436 let rt: RptTransactionType = serde_json::from_str(&json).unwrap();
437 assert_eq!(v, rt);
438 }
439 assert_eq!(
440 serde_json::to_string(&RptTransactionType::Sale).unwrap(),
441 "\"sale\""
442 );
443 assert_eq!(
444 serde_json::to_string(&RptTransactionType::ManagementFee).unwrap(),
445 "\"management_fee\""
446 );
447 assert_eq!(
448 serde_json::to_string(&RptTransactionType::ServiceAgreement).unwrap(),
449 "\"service_agreement\""
450 );
451 assert_eq!(
452 serde_json::to_string(&RptTransactionType::LicenseRoyalty).unwrap(),
453 "\"license_royalty\""
454 );
455 assert_eq!(
456 serde_json::to_string(&RptTransactionType::CapitalContribution).unwrap(),
457 "\"capital_contribution\""
458 );
459 }
460
461 #[test]
462 fn test_rpt_all_12_transaction_types() {
463 let eng = Uuid::new_v4();
464 let party = Uuid::new_v4();
465 let date = sample_date(2025, 1, 15);
466
467 let all_types = [
468 RptTransactionType::Sale,
469 RptTransactionType::Purchase,
470 RptTransactionType::Lease,
471 RptTransactionType::Loan,
472 RptTransactionType::Guarantee,
473 RptTransactionType::ManagementFee,
474 RptTransactionType::Dividend,
475 RptTransactionType::Transfer,
476 RptTransactionType::ServiceAgreement,
477 RptTransactionType::LicenseRoyalty,
478 RptTransactionType::CapitalContribution,
479 RptTransactionType::Other,
480 ];
481
482 assert_eq!(
483 all_types.len(),
484 12,
485 "must have exactly 12 transaction types"
486 );
487
488 for txn_type in all_types {
489 let rpt = RelatedPartyTransaction::new(
490 eng,
491 party,
492 txn_type,
493 "Test transaction",
494 dec!(1_000),
495 "USD",
496 date,
497 );
498 let json = serde_json::to_string(&rpt.transaction_type).unwrap();
500 let rt: RptTransactionType = serde_json::from_str(&json).unwrap();
501 assert_eq!(rpt.transaction_type, rt);
502 }
503 }
504
505 #[test]
506 fn test_management_override_risk_default() {
507 let eng = Uuid::new_v4();
508 let party = Uuid::new_v4();
509 let rpt = RelatedPartyTransaction::new(
510 eng,
511 party,
512 RptTransactionType::Loan,
513 "Intercompany loan",
514 dec!(1_000_000),
515 "GBP",
516 sample_date(2025, 3, 31),
517 );
518 assert!(!rpt.management_override_risk);
520 }
521}