1use bitwarden_vault::CardBrand;
6use chrono::Month;
7use credential_exchange_format::{Credential, CreditCardCredential, EditableFieldYearMonth};
8use num_traits::FromPrimitive;
9
10use crate::{cxf::editable_field::create_field, Card, Field};
11
12impl From<Card> for Vec<Credential> {
13 fn from(value: Card) -> Self {
14 let expiry_date = match (value.exp_year, value.exp_month) {
15 (Some(year), Some(month)) => {
16 let year_parsed = year.parse().ok();
17 let numeric_month: Option<u32> = month.parse().ok();
18 let month_parsed = numeric_month.and_then(Month::from_u32);
19 match (year_parsed, month_parsed) {
20 (Some(year), Some(month)) => {
21 Some(EditableFieldYearMonth { year, month }.into())
22 }
23 _ => None,
24 }
25 }
26 _ => None,
27 };
28
29 vec![Credential::CreditCard(Box::new(CreditCardCredential {
30 number: value.number.map(|v| v.into()),
31 full_name: value.cardholder_name.map(|v| v.into()),
32 card_type: value.brand.map(|v| v.into()),
33 verification_number: value.code.map(|v| v.into()),
34 pin: None,
35 expiry_date,
36 valid_from: None,
37 }))]
38 }
39}
40
41impl From<&CreditCardCredential> for Card {
42 fn from(value: &CreditCardCredential) -> Self {
43 Card {
44 cardholder_name: value.full_name.clone().map(|v| v.into()),
45 exp_month: value
46 .expiry_date
47 .as_ref()
48 .map(|v| v.value.month.number_from_month().to_string()),
49 exp_year: value.expiry_date.as_ref().map(|v| v.value.year.to_string()),
50 code: value.verification_number.clone().map(|v| v.into()),
51 brand: value
52 .card_type
53 .as_ref()
54 .and_then(|brand| sanitize_brand(&brand.value.0)),
55 number: value.number.clone().map(|v| v.into()),
56 }
57 }
58}
59
60pub(super) fn to_card(credential: &CreditCardCredential) -> (Card, Vec<Field>) {
61 let card = credential.into();
62
63 let fields = [
64 credential
65 .pin
66 .as_ref()
67 .map(|v| create_field(v, Some("PIN"))),
68 credential
69 .valid_from
70 .as_ref()
71 .map(|v| create_field(v, Some("Valid From"))),
72 ]
73 .into_iter()
74 .flatten()
75 .collect();
76
77 (card, fields)
78}
79
80fn sanitize_brand(value: &str) -> Option<String> {
88 match value.to_lowercase().replace(" ", "").as_str() {
89 "visa" => Some(CardBrand::Visa),
90 "mastercard" => Some(CardBrand::Mastercard),
91 "amex" | "americanexpress" => Some(CardBrand::Amex),
92 "discover" => Some(CardBrand::Discover),
93 "dinersclub" => Some(CardBrand::DinersClub),
94 "jcb" => Some(CardBrand::Jcb),
95 "maestro" => Some(CardBrand::Maestro),
96 "unionpay" => Some(CardBrand::UnionPay),
97 "rupay" => Some(CardBrand::RuPay),
98 _ => None,
99 }
100 .and_then(|brand| serde_json::to_value(&brand).ok())
101 .and_then(|v| v.as_str().map(|s| s.to_string()))
102}
103
104#[cfg(test)]
105mod tests {
106 use bitwarden_vault::FieldType;
107 use chrono::Month;
108 use credential_exchange_format::EditableFieldYearMonth;
109
110 use super::*;
111
112 #[test]
113 fn test_sanitize_brand() {
114 assert_eq!(sanitize_brand("Visa"), Some("Visa".to_string()));
115 assert_eq!(sanitize_brand(" visa "), Some("Visa".to_string()));
116 assert_eq!(sanitize_brand("MasterCard"), Some("Mastercard".to_string()));
117 assert_eq!(sanitize_brand("amex"), Some("Amex".to_string()));
118 assert_eq!(sanitize_brand("American Express"), Some("Amex".to_string()));
119 assert_eq!(
120 sanitize_brand("DinersClub"),
121 Some("Diners Club".to_string())
122 );
123 assert_eq!(sanitize_brand("j c b"), Some("JCB".to_string()));
124 assert_eq!(sanitize_brand("Some unknown"), None);
125 }
126
127 #[test]
128 fn test_card_to_credentials() {
129 let card = Card {
130 cardholder_name: Some("John Doe".to_string()),
131 exp_month: Some("12".to_string()),
132 exp_year: Some("2025".to_string()),
133 code: Some("123".to_string()),
134 brand: Some("Visa".to_string()),
135 number: Some("4111111111111111".to_string()),
136 };
137
138 let credentials: Vec<Credential> = card.into();
139 assert_eq!(credentials.len(), 1);
140
141 if let Credential::CreditCard(credit_card) = &credentials[0] {
142 assert_eq!(credit_card.full_name.as_ref().unwrap().value.0, "John Doe");
143 assert_eq!(
144 credit_card.expiry_date.as_ref().unwrap().value,
145 EditableFieldYearMonth {
146 year: 2025,
147 month: Month::December
148 }
149 );
150 assert_eq!(
151 credit_card.verification_number.as_ref().unwrap().value.0,
152 "123".to_string()
153 );
154 assert_eq!(
155 credit_card.card_type.as_ref().unwrap().value.0,
156 "Visa".to_string()
157 );
158 assert_eq!(
159 credit_card.number.as_ref().unwrap().value.0,
160 "4111111111111111"
161 );
162 } else {
163 panic!("Expected CreditCardCredential");
164 }
165 }
166
167 #[test]
168 fn test_credit_card_credential_to_card() {
169 let credit_card = CreditCardCredential {
170 number: Some("4111111111111111".to_string().into()),
171 full_name: Some("John Doe".to_string().into()),
172 card_type: Some("Visa".to_string().into()),
173 verification_number: Some("123".to_string().into()),
174 pin: Some("4567".to_string().into()),
175 expiry_date: Some(
176 EditableFieldYearMonth {
177 year: 2025,
178 month: Month::December,
179 }
180 .into(),
181 ),
182 valid_from: Some(
183 EditableFieldYearMonth {
184 year: 2024,
185 month: Month::January,
186 }
187 .into(),
188 ),
189 };
190
191 let (card, fields) = to_card(&credit_card);
192 assert_eq!(card.cardholder_name, Some("John Doe".to_string()));
193 assert_eq!(card.exp_month, Some("12".to_string()));
194 assert_eq!(card.exp_year, Some("2025".to_string()));
195 assert_eq!(card.code, Some("123".to_string()));
196 assert_eq!(card.brand, Some("Visa".to_string()));
197 assert_eq!(card.number, Some("4111111111111111".to_string()));
198
199 assert_eq!(
200 fields,
201 vec![
202 Field {
203 name: Some("PIN".to_string()),
204 value: Some("4567".to_string()),
205 r#type: FieldType::Hidden as u8,
206 linked_id: None,
207 },
208 Field {
209 name: Some("Valid From".to_string()),
210 value: Some("2024-01".to_string()),
211 r#type: FieldType::Text as u8,
212 linked_id: None,
213 },
214 ]
215 )
216 }
217}