1use std::fmt;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum Bank {
13 Bca,
14 Bni,
15 Bri,
16 Cimb,
17 Danamon,
18 Mandiri,
19 Permata,
20 Bnc,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub enum Wallet {
26 Dana,
27 Ovo,
28 ShopeePay,
29 LinkAja,
30 GoPay,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub enum Retail {
36 Alfamart,
37 Indomaret,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum Channel {
42 VirtualAccount(Bank),
44 Qris,
46 EWallet(Wallet),
47 Retail(Retail),
49}
50
51impl Channel {
52 pub fn code(&self) -> String {
54 match self {
55 Channel::VirtualAccount(bank) => format!("va_{}", lower(&format!("{bank:?}"))),
56 Channel::Qris => "qris".to_string(),
57 Channel::EWallet(wallet) => format!("ewallet_{}", lower(&format!("{wallet:?}"))),
58 Channel::Retail(store) => format!("retail_{}", lower(&format!("{store:?}"))),
59 }
60 }
61
62 pub fn parse(code: &str) -> Option<Channel> {
64 let code = code.trim().to_ascii_lowercase();
65 if code == "qris" {
66 return Some(Channel::Qris);
67 }
68 let (kind, name) = code.split_once('_')?;
69 match kind {
70 "va" => Some(Channel::VirtualAccount(match name {
71 "bca" => Bank::Bca,
72 "bni" => Bank::Bni,
73 "bri" => Bank::Bri,
74 "cimb" => Bank::Cimb,
75 "danamon" => Bank::Danamon,
76 "mandiri" => Bank::Mandiri,
77 "permata" => Bank::Permata,
78 "bnc" => Bank::Bnc,
79 _ => return None,
80 })),
81 "ewallet" => Some(Channel::EWallet(match name {
82 "dana" => Wallet::Dana,
83 "ovo" => Wallet::Ovo,
84 "shopeepay" => Wallet::ShopeePay,
85 "linkaja" => Wallet::LinkAja,
86 "gopay" => Wallet::GoPay,
87 _ => return None,
88 })),
89 "retail" => Some(Channel::Retail(match name {
90 "alfamart" => Retail::Alfamart,
91 "indomaret" => Retail::Indomaret,
92 _ => return None,
93 })),
94 _ => None,
95 }
96 }
97}
98
99fn lower(text: &str) -> String {
100 text.to_ascii_lowercase()
101}
102
103impl fmt::Display for Channel {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 f.write_str(&self.code())
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
116 fn every_channel_round_trips_through_its_code() {
117 let all = [
118 Channel::VirtualAccount(Bank::Bca),
119 Channel::VirtualAccount(Bank::Bni),
120 Channel::VirtualAccount(Bank::Bri),
121 Channel::VirtualAccount(Bank::Cimb),
122 Channel::VirtualAccount(Bank::Danamon),
123 Channel::VirtualAccount(Bank::Mandiri),
124 Channel::VirtualAccount(Bank::Permata),
125 Channel::VirtualAccount(Bank::Bnc),
126 Channel::Qris,
127 Channel::EWallet(Wallet::Dana),
128 Channel::EWallet(Wallet::Ovo),
129 Channel::EWallet(Wallet::ShopeePay),
130 Channel::EWallet(Wallet::LinkAja),
131 Channel::EWallet(Wallet::GoPay),
132 Channel::Retail(Retail::Alfamart),
133 Channel::Retail(Retail::Indomaret),
134 ];
135 for channel in all {
136 assert_eq!(Channel::parse(&channel.code()), Some(channel), "{channel}");
137 }
138 assert_eq!(Channel::VirtualAccount(Bank::Bca).code(), "va_bca");
139 assert_eq!(Channel::EWallet(Wallet::ShopeePay).code(), "ewallet_shopeepay");
140 }
141
142 #[test]
143 fn an_unknown_code_is_none_rather_than_a_default() {
144 assert_eq!(Channel::parse("va_unknownbank"), None);
145 assert_eq!(Channel::parse("cash"), None);
146 assert_eq!(Channel::parse(""), None);
147 }
148}