Skip to main content

amlich_api/
dto.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize, Default)]
4pub struct DateQuery {
5    pub day: i32,
6    pub month: i32,
7    pub year: i32,
8    pub timezone: Option<f64>,
9    pub ruleset_id: Option<String>,
10    pub event_kind: Option<String>,
11    #[serde(default)]
12    pub enabled_pack_ids: Vec<String>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct SolarDto {
17    pub day: i32,
18    pub month: i32,
19    pub year: i32,
20    pub day_of_week: usize,
21    pub day_of_week_name: String,
22    pub date_string: String,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct LunarDto {
27    pub day: i32,
28    pub month: i32,
29    pub year: i32,
30    pub is_leap_month: bool,
31    pub date_string: String,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct NguHanhDto {
36    pub can: String,
37    pub chi: String,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct CanChiDto {
42    pub can_index: usize,
43    pub chi_index: usize,
44    pub can: String,
45    pub chi: String,
46    pub full: String,
47    pub con_giap: String,
48    pub ngu_hanh: NguHanhDto,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct CanChiInfoDto {
53    pub day: CanChiDto,
54    pub month: CanChiDto,
55    pub year: CanChiDto,
56    pub full: String,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct TietKhiDto {
61    pub index: usize,
62    pub name: String,
63    pub description: String,
64    pub longitude: i32,
65    pub current_longitude: f64,
66    pub season: String,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct HourInfoDto {
71    pub hour_index: usize,
72    pub hour_chi: String,
73    pub time_range: String,
74    pub star: String,
75    pub is_good: bool,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct GioHoangDaoDto {
80    pub day_chi: String,
81    pub good_hour_count: usize,
82    pub good_hours: Vec<HourInfoDto>,
83    pub all_hours: Vec<HourInfoDto>,
84    pub summary: String,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct DayElementDto {
89    pub na_am: String,
90    pub element: String,
91    pub can_element: String,
92    pub chi_element: String,
93    pub evidence: Option<RuleEvidenceDto>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct DayConflictDto {
98    pub opposing_chi: String,
99    pub opposing_con_giap: String,
100    pub tuoi_xung: Vec<String>,
101    pub sat_huong: String,
102    pub evidence: Option<RuleEvidenceDto>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct TravelDirectionDto {
107    pub xuat_hanh_huong: String,
108    pub tai_than: String,
109    pub hy_than: String,
110    pub evidence: Option<RuleEvidenceDto>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct RuleEvidenceDto {
115    pub source_id: String,
116    pub method: String,
117    pub profile: String,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct DayStarDto {
122    pub system: String,
123    pub index: usize,
124    pub name: String,
125    pub quality: String,
126    pub evidence: Option<RuleEvidenceDto>,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct StarRuleEvidenceDto {
131    pub name: String,
132    pub quality: String,
133    pub category: String,
134    pub source_id: String,
135    pub method: String,
136    pub profile: String,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct DayStarsDto {
141    pub cat_tinh: Vec<String>,
142    pub sat_tinh: Vec<String>,
143    pub day_star: Option<DayStarDto>,
144    pub star_system: Option<String>,
145    pub evidence: Option<RuleEvidenceDto>,
146    pub matched_rules: Vec<StarRuleEvidenceDto>,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct DayDeityDto {
151    pub name: String,
152    pub classification: String,
153    pub evidence: Option<RuleEvidenceDto>,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct XungHopDto {
158    pub luc_xung: String,
159    pub tam_hop: Vec<String>,
160    pub tu_hanh_xung: Vec<String>,
161    pub liu_he: Option<String>,
162    pub xiang_hai: Option<String>,
163    pub xiang_xing: Option<Vec<String>>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct TangCanDto {
168    pub main: String,
169    pub central: String,
170    pub residual: String,
171    pub strength: [u8; 3],
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct TrucDto {
176    pub index: usize,
177    pub name: String,
178    pub quality: String,
179    pub evidence: Option<RuleEvidenceDto>,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct DayTenGodsDto {
184    /// Ten Gods relation from day stem to year stem
185    pub to_year_stem: Option<ThapThanResultDto>,
186    /// Ten Gods relation from day stem to self (day stem to day stem)
187    pub to_self: Option<ThapThanResultDto>,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct ThapThanResultDto {
192    pub label: String,
193    pub relation: String,
194    pub same_polarity: bool,
195    pub evidence: RuleEvidenceDto,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct DayTabooDto {
200    pub rule_id: String,
201    pub name: String,
202    pub severity: String,
203    pub reason: String,
204    pub evidence: Option<RuleEvidenceDto>,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct DayFortuneDto {
209    pub ruleset_id: String,
210    pub ruleset_version: String,
211    pub profile: String,
212    pub day_element: DayElementDto,
213    pub conflict: DayConflictDto,
214    pub travel: TravelDirectionDto,
215    pub stars: DayStarsDto,
216    pub day_deity: Option<DayDeityDto>,
217    pub taboos: Vec<DayTabooDto>,
218    pub xung_hop: XungHopDto,
219    pub truc: TrucDto,
220    pub tang_can: Option<TangCanDto>,
221    /// Ten Gods relations for predefined targets (populated when day stem available)
222    pub ten_gods: Option<DayTenGodsDto>,
223    /// Kua (Tu Mến) result (populated only when birth year and gender provided)
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub tu_menh: Option<KuaResultDto>,
226}
227
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
229#[serde(rename_all = "snake_case")]
230pub enum RecommendationScopeDto {
231    GeneralDay,
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
235#[serde(rename_all = "snake_case")]
236pub enum RecommendationBucketDto {
237    Nen,
238    CoThe,
239    Tranh,
240    KyManh,
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
244#[serde(rename_all = "snake_case")]
245pub enum RecommendationSeverityDto {
246    Primary,
247    Supporting,
248    Override,
249}
250
251#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
252#[serde(rename_all = "snake_case")]
253pub enum RecommendationEvidenceSourceDto {
254    DayGuidance,
255    Truc,
256    Stars,
257    DayDeity,
258    Taboo,
259    XungHop,
260    TietKhi,
261    GioHoangDao,
262    Travel,
263    ProductRule,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct ActivityLabelDto {
268    pub vi: String,
269    pub en: String,
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct RecommendationEvidenceDto {
274    pub source: RecommendationEvidenceSourceDto,
275    pub code: String,
276    pub note: String,
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct RecommendationReasonDto {
281    pub rule_id: String,
282    pub severity: RecommendationSeverityDto,
283    pub summary_vi: String,
284    pub summary_en: String,
285    pub evidence: RecommendationEvidenceDto,
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct SynthesizedRecommendationDto {
290    pub activity_id: String,
291    pub label: ActivityLabelDto,
292    pub bucket: RecommendationBucketDto,
293    #[serde(default)]
294    pub reasons: Vec<RecommendationReasonDto>,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct DailyRecommendationsDto {
299    pub ruleset_id: String,
300    pub ruleset_version: String,
301    pub profile: String,
302    pub scope: RecommendationScopeDto,
303    pub version: String,
304    pub summary_vi: String,
305    pub summary_en: String,
306    #[serde(default)]
307    pub active_packs: Vec<ActiveRecommendationPackDto>,
308    #[serde(default)]
309    pub activities: Vec<SynthesizedRecommendationDto>,
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct ActiveRecommendationPackDto {
314    pub pack_id: String,
315    pub version: String,
316    pub source_family: String,
317    pub mode: String,
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct RulesetDefaultsDto {
322    pub tz_offset: f64,
323    #[serde(default, skip_serializing_if = "Option::is_none")]
324    pub meridian: Option<String>,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct RulesetSourceNoteDto {
329    pub family: String,
330    pub source_id: String,
331    pub note: String,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct RulesetCatalogEntryDto {
336    pub id: String,
337    pub canonical_id: String,
338    pub version: String,
339    pub region: String,
340    pub profile: String,
341    pub schema_version: String,
342    pub is_default: bool,
343    #[serde(default)]
344    pub aliases: Vec<String>,
345    pub defaults: RulesetDefaultsDto,
346    #[serde(default)]
347    pub source_notes: Vec<RulesetSourceNoteDto>,
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct RecommendationPackCatalogEntryDto {
352    pub pack_id: String,
353    pub request_field: String,
354    pub version: String,
355    pub source_family: String,
356    pub mode: String,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct KuaResultDto {
361    pub kua: u8,
362    pub group: String,
363    pub favorable_directions: Vec<String>,
364    pub unfavorable_directions: Vec<String>,
365    pub convention: ConventionMetadataDto,
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct ConventionMetadataDto {
370    pub year_basis: String,
371    pub kua_five_resolution: String,
372    pub gender_encoding: String,
373}
374
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct DayInfoDto {
377    pub ruleset_id: String,
378    pub ruleset_version: String,
379    pub profile: String,
380    pub solar: SolarDto,
381    pub lunar: LunarDto,
382    pub jd: i32,
383    pub canchi: CanChiInfoDto,
384    pub tiet_khi: TietKhiDto,
385    pub gio_hoang_dao: GioHoangDaoDto,
386    pub day_fortune: Option<DayFortuneDto>,
387    pub daily_recommendations: DailyRecommendationsDto,
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub contextual_recommendations: Option<DailyRecommendationsDto>,
390}
391
392#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct HolidayDto {
394    pub name: String,
395    pub description: String,
396    pub solar_day: i32,
397    pub solar_month: i32,
398    pub solar_year: i32,
399    pub lunar_day: Option<i32>,
400    pub lunar_month: Option<i32>,
401    pub lunar_year: Option<i32>,
402    pub is_solar: bool,
403    pub category: String,
404    pub is_major: bool,
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct LocalizedTextDto {
409    pub vi: String,
410    pub en: String,
411}
412
413#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct LocalizedListDto {
415    pub vi: Vec<String>,
416    pub en: Vec<String>,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize)]
420pub struct FoodInsightDto {
421    pub name: LocalizedTextDto,
422    pub description: LocalizedTextDto,
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize)]
426pub struct TabooInsightDto {
427    pub action: LocalizedTextDto,
428    pub reason: LocalizedTextDto,
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize)]
432pub struct ProverbInsightDto {
433    pub text: String,
434    pub meaning: LocalizedTextDto,
435}
436
437#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct RegionsInsightDto {
439    pub north: LocalizedTextDto,
440    pub central: LocalizedTextDto,
441    pub south: LocalizedTextDto,
442}
443
444#[derive(Debug, Clone, Serialize, Deserialize)]
445pub struct FestivalInsightDto {
446    pub names: LocalizedListDto,
447    pub origin: Option<LocalizedTextDto>,
448    pub activities: Option<LocalizedListDto>,
449    pub food: Vec<FoodInsightDto>,
450    pub taboos: Vec<TabooInsightDto>,
451    pub proverbs: Vec<ProverbInsightDto>,
452    pub regions: Option<RegionsInsightDto>,
453    pub category: String,
454    pub is_major: bool,
455}
456
457#[derive(Debug, Clone, Serialize, Deserialize)]
458pub struct HolidayInsightDto {
459    pub names: LocalizedListDto,
460    pub origin: Option<LocalizedTextDto>,
461    pub significance: Option<LocalizedTextDto>,
462    pub activities: Option<LocalizedListDto>,
463    pub traditions: Option<LocalizedListDto>,
464    pub food: Vec<FoodInsightDto>,
465    pub taboos: Vec<TabooInsightDto>,
466    pub proverbs: Vec<ProverbInsightDto>,
467    pub regions: Option<RegionsInsightDto>,
468    pub category: String,
469    pub is_major: bool,
470}
471
472#[derive(Debug, Clone, Serialize, Deserialize)]
473pub struct UpcomingEventDto {
474    pub name: String,
475    pub days_left: i32,
476    pub is_lunar: bool,
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize)]
480pub struct ElementInsightDto {
481    pub key: String,
482    pub name: LocalizedTextDto,
483    pub nature: LocalizedTextDto,
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize)]
487pub struct CanInsightDto {
488    pub name: String,
489    pub element: String,
490    pub meaning: LocalizedTextDto,
491    pub nature: LocalizedTextDto,
492}
493
494#[derive(Debug, Clone, Serialize, Deserialize)]
495pub struct ChiInsightDto {
496    pub name: String,
497    pub animal: LocalizedTextDto,
498    pub element: String,
499    pub meaning: LocalizedTextDto,
500    pub hours: String,
501}
502
503#[derive(Debug, Clone, Serialize, Deserialize)]
504pub struct CanChiInsightDto {
505    pub can: CanInsightDto,
506    pub chi: ChiInsightDto,
507    pub element: Option<ElementInsightDto>,
508}
509
510#[derive(Debug, Clone, Serialize, Deserialize)]
511pub struct DayGuidanceDto {
512    pub good_for: LocalizedListDto,
513    pub avoid_for: LocalizedListDto,
514}
515
516#[derive(Debug, Clone, Serialize, Deserialize)]
517pub struct TietKhiInsightDto {
518    pub id: String,
519    pub name: LocalizedTextDto,
520    pub longitude: i32,
521    pub meaning: LocalizedTextDto,
522    pub astronomy: LocalizedTextDto,
523    pub agriculture: LocalizedListDto,
524    pub health: LocalizedListDto,
525    pub weather: LocalizedTextDto,
526}
527
528#[derive(Debug, Clone, Serialize, Deserialize)]
529pub struct NaAmInsightDto {
530    pub na_am: String,
531    pub element: String,
532    pub meaning: LocalizedTextDto,
533}
534
535#[derive(Debug, Clone, Serialize, Deserialize)]
536pub struct TrucInsightDto {
537    pub name: String,
538    pub quality: String,
539    pub meaning: LocalizedTextDto,
540    pub good_for: LocalizedListDto,
541    pub avoid_for: LocalizedListDto,
542}
543
544#[derive(Debug, Clone, Serialize, Deserialize)]
545pub struct DayDeityInsightDto {
546    pub name: String,
547    pub classification: String,
548    pub classification_meaning: LocalizedTextDto,
549    pub deity_meaning: Option<LocalizedTextDto>,
550}
551
552#[derive(Debug, Clone, Serialize, Deserialize)]
553pub struct StarsInsightDto {
554    pub cat_tinh: Vec<String>,
555    pub sat_tinh: Vec<String>,
556    pub day_star: Option<String>,
557    pub day_star_quality: Option<String>,
558}
559
560#[derive(Debug, Clone, Serialize, Deserialize)]
561pub struct TabooInsightItemDto {
562    pub name: String,
563    pub severity: String,
564    pub reason: String,
565}
566
567#[derive(Debug, Clone, Serialize, Deserialize)]
568pub struct TravelInsightDto {
569    pub xuat_hanh_huong: String,
570    pub tai_than: String,
571    pub hy_than: String,
572}
573
574#[derive(Debug, Clone, Serialize, Deserialize)]
575pub struct XungHopInsightDto {
576    pub luc_xung: String,
577    pub tam_hop: Vec<String>,
578    pub liu_he: Option<String>,
579    pub xiang_hai: Option<String>,
580}
581
582#[derive(Debug, Clone, Serialize, Deserialize)]
583pub struct TangCanInsightDto {
584    pub main: String,
585    pub central: String,
586    pub residual: String,
587    pub strength: [u8; 3],
588}
589
590#[derive(Debug, Clone, Serialize, Deserialize)]
591pub struct TenGodsEntryInsightDto {
592    pub label: String,
593    pub name: LocalizedTextDto,
594    pub meaning: LocalizedTextDto,
595    pub relation: String,
596    pub same_polarity: bool,
597}
598
599#[derive(Debug, Clone, Serialize, Deserialize)]
600pub struct TenGodsInsightDto {
601    pub to_year_stem: Option<TenGodsEntryInsightDto>,
602    pub to_self: Option<TenGodsEntryInsightDto>,
603}
604
605#[derive(Debug, Clone, Serialize, Deserialize)]
606pub struct HourInsightEntryDto {
607    pub chi: String,
608    pub time_range: String,
609    pub star: String,
610}
611
612#[derive(Debug, Clone, Serialize, Deserialize)]
613pub struct HoursInsightDto {
614    pub good_hour_count: usize,
615    pub good_hours: Vec<HourInsightEntryDto>,
616}
617
618#[derive(Debug, Clone, Serialize, Deserialize)]
619pub struct TuMenhInsightDto {
620    pub kua: u8,
621    pub group: String,
622    pub trigram: LocalizedTextDto,
623    pub direction: LocalizedTextDto,
624    pub meaning: LocalizedTextDto,
625    pub group_meaning: LocalizedTextDto,
626    pub favorable_directions: Vec<String>,
627    pub unfavorable_directions: Vec<String>,
628}
629
630#[derive(Debug, Clone, Serialize, Deserialize)]
631pub struct DaiVanPillarInsightDto {
632    pub index: usize,
633    pub can_chi: String,
634    pub start_age: f64,
635    pub end_age: f64,
636    pub element: String,
637    pub element_meaning: LocalizedTextDto,
638}
639
640#[derive(Debug, Clone, Serialize, Deserialize)]
641pub struct DaiVanInsightDto {
642    pub direction: String,
643    pub direction_meaning: LocalizedTextDto,
644    pub start_age: String,
645    pub current_pillar: Option<DaiVanPillarInsightDto>,
646    pub all_pillars: Vec<DaiVanPillarInsightDto>,
647    pub phases_meaning: LocalizedTextDto,
648}
649
650#[derive(Debug, Clone, Serialize, Deserialize)]
651pub struct DayInsightDto {
652    pub solar: SolarDto,
653    pub lunar: LunarDto,
654    #[serde(skip_serializing_if = "Option::is_none")]
655    pub festival: Option<FestivalInsightDto>,
656    #[serde(skip_serializing_if = "Option::is_none")]
657    pub holiday: Option<HolidayInsightDto>,
658    #[serde(skip_serializing_if = "Option::is_none")]
659    pub canchi: Option<CanChiInsightDto>,
660    #[serde(skip_serializing_if = "Option::is_none")]
661    pub day_guidance: Option<DayGuidanceDto>,
662    #[serde(skip_serializing_if = "Option::is_none")]
663    pub tiet_khi: Option<TietKhiInsightDto>,
664    #[serde(skip_serializing_if = "Option::is_none")]
665    pub na_am: Option<NaAmInsightDto>,
666    #[serde(skip_serializing_if = "Option::is_none")]
667    pub truc: Option<TrucInsightDto>,
668    #[serde(skip_serializing_if = "Option::is_none")]
669    pub day_deity: Option<DayDeityInsightDto>,
670    #[serde(skip_serializing_if = "Option::is_none")]
671    pub stars: Option<StarsInsightDto>,
672    #[serde(skip_serializing_if = "Option::is_none")]
673    pub taboos: Option<Vec<TabooInsightItemDto>>,
674    #[serde(skip_serializing_if = "Option::is_none")]
675    pub travel: Option<TravelInsightDto>,
676    #[serde(skip_serializing_if = "Option::is_none")]
677    pub xung_hop: Option<XungHopInsightDto>,
678    #[serde(skip_serializing_if = "Option::is_none")]
679    pub tang_can: Option<TangCanInsightDto>,
680    #[serde(skip_serializing_if = "Option::is_none")]
681    pub ten_gods: Option<TenGodsInsightDto>,
682    #[serde(skip_serializing_if = "Option::is_none")]
683    pub hours: Option<HoursInsightDto>,
684    #[serde(skip_serializing_if = "Option::is_none")]
685    pub tu_menh: Option<TuMenhInsightDto>,
686    #[serde(skip_serializing_if = "Option::is_none")]
687    pub dai_van: Option<DaiVanInsightDto>,
688}
689
690/// Na Am lookup result with cycle index and evidence metadata
691#[derive(Debug, Clone, Serialize, Deserialize)]
692pub struct NaAmLookupResultDto {
693    /// 1-based cycle index (1-60)
694    pub cycle_index: u8,
695    /// Heavenly stem (Vietnamese name)
696    pub can: String,
697    /// Earthly branch (Vietnamese name)
698    pub chi: String,
699    /// Na Am value (e.g., "Hải Trung Kim")
700    pub na_am: String,
701    /// Five element (last word of na_am)
702    pub element: String,
703    /// Evidence: source identifier
704    pub source_id: String,
705    /// Evidence: computation method
706    pub method: String,
707    /// Evidence: ruleset profile
708    pub profile: String,
709}
710
711/// Na Am lookup error with deterministic error type and human-readable message
712#[derive(Debug, Clone, Serialize, Deserialize)]
713pub struct NaAmErrorDto {
714    /// Error type: "invalid_cycle_index" | "invalid_stem_branch_pair" | "unknown_stem" | "unknown_branch"
715    pub error: String,
716    /// Human-readable error description
717    pub message: String,
718}
719
720/// Na Am API response with success or error variant
721#[derive(Debug, Clone, Serialize, Deserialize)]
722pub enum NaAmResponseDto {
723    Success(NaAmLookupResultDto),
724    Error(NaAmErrorDto),
725}
726
727#[cfg(test)]
728mod tests {
729    use super::*;
730
731    #[test]
732    fn day_fortune_dto_has_optional_ten_gods_and_tu_menh_fields() {
733        // Test 1: DTO has matching optional ten_gods and tu_menh fields
734        let dto = DayFortuneDto {
735            ruleset_id: "test".to_string(),
736            ruleset_version: "v1".to_string(),
737            profile: "baseline".to_string(),
738            day_element: DayElementDto {
739                na_am: "test".to_string(),
740                element: "Kim".to_string(),
741                can_element: "Mộc".to_string(),
742                chi_element: "Thổ".to_string(),
743                evidence: None,
744            },
745            conflict: DayConflictDto {
746                opposing_chi: "Tuất".to_string(),
747                opposing_con_giap: "Tuất (Chó)".to_string(),
748                tuoi_xung: vec![],
749                sat_huong: "Nam".to_string(),
750                evidence: None,
751            },
752            travel: TravelDirectionDto {
753                xuat_hanh_huong: "Đông Nam".to_string(),
754                tai_than: "Tây Nam".to_string(),
755                hy_than: "Đông Bắc".to_string(),
756                evidence: None,
757            },
758            stars: DayStarsDto {
759                cat_tinh: vec![],
760                sat_tinh: vec![],
761                day_star: None,
762                star_system: None,
763                evidence: None,
764                matched_rules: vec![],
765            },
766            day_deity: None,
767            taboos: vec![],
768            xung_hop: XungHopDto {
769                luc_xung: "Tuất".to_string(),
770                tam_hop: vec![],
771                tu_hanh_xung: vec![],
772                liu_he: None,
773                xiang_hai: None,
774                xiang_xing: None,
775            },
776            truc: TrucDto {
777                index: 0,
778                name: "Kiến".to_string(),
779                quality: "cat".to_string(),
780                evidence: None,
781            },
782            tang_can: None,
783            ten_gods: None,
784            tu_menh: None,
785        };
786
787        // Verify fields exist and are optional
788        let _ = dto.ten_gods;
789        let _ = dto.tu_menh;
790    }
791
792    #[test]
793    fn day_ten_gods_dto_serializes_with_snake_case() {
794        // Test 3: JSON serialization matches expected stable field names (snake_case)
795        let ten_gods = DayTenGodsDto {
796            to_year_stem: None,
797            to_self: None,
798        };
799
800        let json = serde_json::to_string(&ten_gods).expect("serialize");
801        // Verify snake_case field names
802        assert!(json.contains("\"to_year_stem\""));
803        assert!(json.contains("\"to_self\""));
804    }
805}