1use bitwarden_vault::CardBrand;
6use chrono::Month;
7use credential_exchange_format::{Credential, CreditCardCredential, EditableFieldYearMonth};
8use num_traits::FromPrimitive;
9
10use crate::{Card, Field, cxf::editable_field::create_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 let expiry_date = value
44 .expiry_date
45 .as_ref()
46 .and_then(|v| v.value.as_expected().ok());
47 Card {
48 cardholder_name: value.full_name.clone().map(Into::into),
49 exp_month: expiry_date.map(|v| v.month.number_from_month().to_string()),
50 exp_year: expiry_date.map(|v| v.year.to_string()),
51 code: value.verification_number.clone().map(Into::into),
52 brand: value
53 .card_type
54 .clone()
55 .and_then(|b| sanitize_brand(b.into())),
56 number: value.number.clone().map(Into::into),
57 }
58 }
59}
60
61pub(super) fn to_card(credential: &CreditCardCredential) -> (Card, Vec<Field>) {
62 let card = credential.into();
63
64 let fields = [
65 credential
66 .pin
67 .as_ref()
68 .map(|v| create_field(v, Some("PIN"))),
69 credential
70 .valid_from
71 .as_ref()
72 .map(|v| create_field(v, Some("Valid From"))),
73 ]
74 .into_iter()
75 .flatten()
76 .collect();
77
78 (card, fields)
79}
80
81fn sanitize_brand(value: String) -> Option<String> {
89 match value.to_lowercase().replace(" ", "").as_str() {
90 "visa" => Some(CardBrand::Visa),
91 "mastercard" => Some(CardBrand::Mastercard),
92 "amex" | "americanexpress" => Some(CardBrand::Amex),
93 "discover" => Some(CardBrand::Discover),
94 "dinersclub" => Some(CardBrand::DinersClub),
95 "jcb" => Some(CardBrand::Jcb),
96 "maestro" => Some(CardBrand::Maestro),
97 "unionpay" => Some(CardBrand::UnionPay),
98 "rupay" => Some(CardBrand::RuPay),
99 _ => None,
100 }
101 .and_then(|brand| serde_json::to_value(&brand).ok())
102 .and_then(|v| v.as_str().map(|s| s.to_string()))
103}
104
105#[cfg(test)]
106mod tests {
107 use bitwarden_vault::FieldType;
108 use chrono::Month;
109 use credential_exchange_format::EditableFieldYearMonth;
110
111 use super::*;
112
113 #[test]
114 fn test_sanitize_brand() {
115 assert_eq!(sanitize_brand("Visa".into()), Some("Visa".to_string()));
116 assert_eq!(sanitize_brand(" visa ".into()), Some("Visa".to_string()));
117 assert_eq!(
118 sanitize_brand("MasterCard".into()),
119 Some("Mastercard".to_string())
120 );
121 assert_eq!(sanitize_brand("amex".into()), Some("Amex".to_string()));
122 assert_eq!(
123 sanitize_brand("American Express".into()),
124 Some("Amex".to_string())
125 );
126 assert_eq!(
127 sanitize_brand("DinersClub".into()),
128 Some("Diners Club".to_string())
129 );
130 assert_eq!(sanitize_brand("j c b".into()), Some("JCB".to_string()));
131 assert_eq!(sanitize_brand("Some unknown".into()), None);
132 }
133
134 #[test]
135 fn test_card_to_credentials() {
136 let card = Card {
137 cardholder_name: Some("John Doe".to_string()),
138 exp_month: Some("12".to_string()),
139 exp_year: Some("2025".to_string()),
140 code: Some("123".to_string()),
141 brand: Some("Visa".to_string()),
142 number: Some("4111111111111111".to_string()),
143 };
144
145 let credentials: Vec<Credential> = card.into();
146 assert_eq!(credentials.len(), 1);
147
148 if let Credential::CreditCard(credit_card) = &credentials[0] {
149 assert_eq!(
150 credit_card
151 .full_name
152 .as_ref()
153 .unwrap()
154 .value
155 .as_expected()
156 .unwrap()
157 .0,
158 "John Doe"
159 );
160 assert_eq!(
161 credit_card
162 .expiry_date
163 .as_ref()
164 .unwrap()
165 .value
166 .as_expected(),
167 Ok(&EditableFieldYearMonth {
168 year: 2025,
169 month: Month::December
170 })
171 );
172 assert_eq!(
173 credit_card
174 .verification_number
175 .as_ref()
176 .unwrap()
177 .value
178 .as_expected()
179 .unwrap()
180 .0,
181 "123".to_string()
182 );
183 assert_eq!(
184 credit_card
185 .card_type
186 .as_ref()
187 .unwrap()
188 .value
189 .as_expected()
190 .unwrap()
191 .0,
192 "Visa".to_string()
193 );
194 assert_eq!(
195 credit_card
196 .number
197 .as_ref()
198 .unwrap()
199 .value
200 .as_expected()
201 .unwrap()
202 .0,
203 "4111111111111111"
204 );
205 } else {
206 panic!("Expected CreditCardCredential");
207 }
208 }
209
210 #[test]
212 fn test_credit_card_credential_to_card_mismatched_card_type() {
213 let credit_card: CreditCardCredential = serde_json::from_value(serde_json::json!({
214 "cardType": {
215 "fieldType": "concealed-string",
216 "value": "visa"
217 }
218 }))
219 .unwrap();
220
221 let (card, _) = to_card(&credit_card);
222 assert_eq!(card.brand.unwrap(), "Visa");
223 }
224
225 #[test]
226 fn test_credit_card_credential_to_card() {
227 let credit_card = CreditCardCredential {
228 number: Some("4111111111111111".to_string().into()),
229 full_name: Some("John Doe".to_string().into()),
230 card_type: Some("Visa".to_string().into()),
231 verification_number: Some("123".to_string().into()),
232 pin: Some("4567".to_string().into()),
233 expiry_date: Some(
234 EditableFieldYearMonth {
235 year: 2025,
236 month: Month::December,
237 }
238 .into(),
239 ),
240 valid_from: Some(
241 EditableFieldYearMonth {
242 year: 2024,
243 month: Month::January,
244 }
245 .into(),
246 ),
247 };
248
249 let (card, fields) = to_card(&credit_card);
250 assert_eq!(card.cardholder_name, Some("John Doe".to_string()));
251 assert_eq!(card.exp_month, Some("12".to_string()));
252 assert_eq!(card.exp_year, Some("2025".to_string()));
253 assert_eq!(card.code, Some("123".to_string()));
254 assert_eq!(card.brand, Some("Visa".to_string()));
255 assert_eq!(card.number, Some("4111111111111111".to_string()));
256
257 assert_eq!(
258 fields,
259 vec![
260 Field {
261 name: Some("PIN".to_string()),
262 value: Some("4567".to_string()),
263 r#type: FieldType::Hidden as u8,
264 linked_id: None,
265 },
266 Field {
267 name: Some("Valid From".to_string()),
268 value: Some("2024-01".to_string()),
269 r#type: FieldType::Text as u8,
270 linked_id: None,
271 },
272 ]
273 )
274 }
275}