44
55namespace Ratoufa \Billing \Providers \FedaPay ;
66
7+ use Ratoufa \Billing \Services \CarrierMapper ;
8+ use Ratoufa \Billing \Services \PhoneNumberService ;
9+
710/**
811 * Detects mobile money provider from phone number.
12+ *
13+ * Uses libphonenumber for carrier detection with fallback to prefix-based detection.
914 */
10- final class MobileMoneyDetector
15+ final readonly class MobileMoneyDetector
1116{
1217 /**
1318 * Mobile money modes supported for USSD Push payments.
19+ * Used as fallback when libphonenumber cannot detect the carrier.
1420 *
1521 * @var array<string, array{country: string, name: string, prefixes: array<int, string>}>
1622 */
1723 public const array MODES = [
1824 // Benin
1925 'mtn_open ' => ['country ' => 'bj ' , 'name ' => 'MTN Mobile Money ' , 'prefixes ' => ['229 ' ]],
2026 'moov ' => ['country ' => 'bj ' , 'name ' => 'Moov Money ' , 'prefixes ' => ['229 ' ]],
27+ 'sbin ' => ['country ' => 'bj ' , 'name ' => 'Celtiis ' , 'prefixes ' => ['229 ' ]],
2128
2229 // Togo
2330 'moov_tg ' => ['country ' => 'tg ' , 'name ' => 'Moov Money Togo ' , 'prefixes ' => ['22896 ' , '22897 ' , '22898 ' , '22899 ' ]],
@@ -36,28 +43,45 @@ final class MobileMoneyDetector
3643 'mtn_open_gn ' => ['country ' => 'gn ' , 'name ' => 'MTN Guinée ' , 'prefixes ' => ['224 ' ]],
3744 ];
3845
46+ public function __construct (
47+ private PhoneNumberService $ phoneService ,
48+ private CarrierMapper $ carrierMapper ,
49+ ) {}
50+
51+ /**
52+ * Create a new instance with default dependencies.
53+ */
54+ public static function create (): self
55+ {
56+ return new self (
57+ PhoneNumberService::create (),
58+ new CarrierMapper (),
59+ );
60+ }
61+
3962 /**
4063 * Detect the mobile money mode from a phone number.
4164 */
4265 public function detect (string $ phoneNumber ): ?string
4366 {
44- $ phone = $ this ->normalizePhone ($ phoneNumber );
45-
46- // Check against known prefixes
47- foreach (self ::MODES as $ mode => $ config ) {
48- foreach ($ config ['prefixes ' ] as $ prefix ) {
49- if (str_starts_with ($ phone , $ prefix )) {
50- return $ mode ;
51- }
67+ // For Togo numbers, use prefix-based detection first (more accurate)
68+ $ country = $ this ->phoneService ->detectCountry ($ phoneNumber );
69+ if (mb_strtolower ($ country ) === 'tg ' ) {
70+ $ mode = $ this ->detectWithPrefixes ($ phoneNumber );
71+ if ($ mode !== null ) {
72+ return $ mode ;
5273 }
5374 }
5475
55- // Special detection for Togo (228)
56- if (str_starts_with ($ phone , '228 ' )) {
57- return $ this ->detectTogoProvider ($ phone );
76+ // Try libphonenumber
77+ $ mode = $ this ->detectWithLibphonenumber ($ phoneNumber );
78+
79+ // Fallback to prefix-based detection for other countries
80+ if ($ mode === null ) {
81+ return $ this ->detectWithPrefixes ($ phoneNumber );
5882 }
5983
60- return null ;
84+ return $ mode ;
6185 }
6286
6387 /**
@@ -75,12 +99,13 @@ public function supportsDirectPayment(string $phoneNumber): bool
7599 */
76100 public function getAvailableModes (?string $ country = null ): array
77101 {
78- $ modes = [];
102+ if ($ country !== null ) {
103+ return $ this ->carrierMapper ->getModesForCountry ($ country );
104+ }
79105
106+ $ modes = [];
80107 foreach (self ::MODES as $ mode => $ config ) {
81- if ($ country === null || $ config ['country ' ] === mb_strtolower ($ country )) {
82- $ modes [$ mode ] = $ config ['name ' ];
83- }
108+ $ modes [$ mode ] = $ config ['name ' ];
84109 }
85110
86111 return $ modes ;
@@ -101,17 +126,66 @@ public function getTogoModes(): array
101126 */
102127 public function getModeName (string $ mode ): ?string
103128 {
104- return self ::MODES [$ mode ]['name ' ] ?? null ;
129+ return $ this -> carrierMapper -> getModeName ( $ mode ) ?? self ::MODES [$ mode ]['name ' ] ?? null ;
105130 }
106131
107132 /**
108133 * Validate if a mode is supported.
109134 */
110135 public function isValidMode (string $ mode ): bool
111136 {
137+ if ($ this ->carrierMapper ->isValidMode ($ mode )) {
138+ return true ;
139+ }
140+
112141 return isset (self ::MODES [$ mode ]);
113142 }
114143
144+ /**
145+ * Detect using libphonenumber carrier detection.
146+ */
147+ private function detectWithLibphonenumber (string $ phoneNumber ): ?string
148+ {
149+ $ carrier = $ this ->phoneService ->detectCarrier ($ phoneNumber );
150+
151+ if ($ carrier === null ) {
152+ return null ;
153+ }
154+
155+ $ country = $ this ->phoneService ->detectCountry ($ phoneNumber );
156+
157+ return $ this ->carrierMapper ->getFedaPayMode ($ carrier , $ country );
158+ }
159+
160+ /**
161+ * Detect using prefix-based matching (fallback).
162+ */
163+ private function detectWithPrefixes (string $ phoneNumber ): ?string
164+ {
165+ $ phone = $ this ->normalizePhone ($ phoneNumber );
166+
167+ // Check against known prefixes
168+ foreach (self ::MODES as $ mode => $ config ) {
169+ foreach ($ config ['prefixes ' ] as $ prefix ) {
170+ if (str_starts_with ($ phone , $ prefix )) {
171+ // For countries with multiple operators, use specific logic
172+ if ($ config ['country ' ] === 'tg ' ) {
173+ return $ this ->detectTogoProvider ($ phone );
174+ }
175+
176+ return $ mode ;
177+ }
178+ }
179+ }
180+
181+ // Special detection for Togo (228)
182+ if (str_starts_with ($ phone , '228 ' )) {
183+ return $ this ->detectTogoProvider ($ phone );
184+ }
185+
186+ return null ;
187+ }
188+
115189 /**
116190 * Detect Togo-specific provider from phone number.
117191 */
0 commit comments