Skip to content

Commit 8a5bb46

Browse files
author
Just Chris
committed
feat: integrate libphonenumber for phone number and carrier detection
Replace hardcoded prefix logic with libphonenumber-for-php library for detecting countries and carriers from phone numbers. Add CarrierMapper service to map carrier names to FedaPay payment mode codes. - Add giggsey/libphonenumber-for-php dependency - Create CarrierMapper with carrier-to-FedaPay mode mapping - Refactor PhoneNumberService to use libphonenumber - Update MobileMoneyDetector with hybrid detection (prefix for Togo, libphonenumber for others) - Add Celtiis (sbin) support for Benin - Register phone services as singletons in BillingServiceProvider
1 parent ae583dc commit 8a5bb46

File tree

10 files changed

+531
-106
lines changed

10 files changed

+531
-106
lines changed

composer.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@
2828
],
2929
"require": {
3030
"php": "^8.4",
31-
"spatie/laravel-package-tools": "^1.16",
32-
"illuminate/contracts": "^11.0||^12.0",
3331
"fedapay/fedapay-php": "^0.4",
34-
"laravel/prompts": "^0.3"
32+
"giggsey/libphonenumber-for-php": "^9.0",
33+
"illuminate/contracts": "^11.0||^12.0",
34+
"laravel/prompts": "^0.3",
35+
"spatie/laravel-package-tools": "^1.16"
3536
},
3637
"require-dev": {
3738
"larastan/larastan": "^3.0",

peck.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
"jour",
1414
"mois",
1515
"unpause",
16-
"transactionable"
16+
"transactionable",
17+
"libphonenumber",
18+
"util"
1719
],
1820
"paths": []
1921
}

src/BillingServiceProvider.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44

55
namespace Ratoufa\Billing;
66

7+
use libphonenumber\PhoneNumberToCarrierMapper;
8+
use libphonenumber\PhoneNumberUtil;
79
use Ratoufa\Billing\Commands\InstallCommand;
810
use Ratoufa\Billing\Commands\ProcessRecurringPaymentsCommand;
911
use Ratoufa\Billing\Commands\ProcessScheduledPayoutsCommand;
12+
use Ratoufa\Billing\Services\CarrierMapper;
1013
use Ratoufa\Billing\Services\CommissionCalculator;
1114
use Ratoufa\Billing\Services\CommissionService;
1215
use Ratoufa\Billing\Services\InstantPayoutService;
16+
use Ratoufa\Billing\Services\PhoneNumberService;
1317
use Spatie\LaravelPackageTools\Package;
1418
use Spatie\LaravelPackageTools\PackageServiceProvider;
1519

@@ -32,9 +36,27 @@ public function packageRegistered(): void
3236

3337
$this->app->alias(BillingManager::class, 'billing');
3438

39+
$this->registerPhoneServices();
3540
$this->registerMarketplaceServices();
3641
}
3742

43+
private function registerPhoneServices(): void
44+
{
45+
$this->app->singleton(PhoneNumberUtil::class, fn (): PhoneNumberUtil => PhoneNumberUtil::getInstance());
46+
47+
$this->app->singleton(
48+
PhoneNumberToCarrierMapper::class,
49+
fn (): PhoneNumberToCarrierMapper => PhoneNumberToCarrierMapper::getInstance()
50+
);
51+
52+
$this->app->singleton(CarrierMapper::class);
53+
54+
$this->app->singleton(PhoneNumberService::class, fn ($app): PhoneNumberService => new PhoneNumberService(
55+
$app->make(PhoneNumberUtil::class),
56+
$app->make(PhoneNumberToCarrierMapper::class),
57+
));
58+
}
59+
3860
private function registerMarketplaceServices(): void
3961
{
4062
$this->app->singleton(CommissionCalculator::class);

src/Models/Subscription.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ public function valid(): bool
127127
if ($this->active()) {
128128
return true;
129129
}
130+
130131
if ($this->onTrial()) {
131132
return true;
132133
}

src/Providers/FedaPay/FedaPayProvider.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ public function __construct(array $config)
3535
{
3636
parent::__construct($config);
3737

38-
$this->mobileMoneyDetector = new MobileMoneyDetector;
39-
$this->phoneFormatter = new PhoneNumberFormatter;
38+
$this->mobileMoneyDetector = MobileMoneyDetector::create();
39+
$this->phoneFormatter = PhoneNumberFormatter::create();
4040
}
4141

4242
public function name(): string

src/Providers/FedaPay/MobileMoneyDetector.php

Lines changed: 92 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,27 @@
44

55
namespace 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
*/

src/Providers/FedaPay/PhoneNumberFormatter.php

Lines changed: 19 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,26 @@
44

55
namespace Ratoufa\Billing\Providers\FedaPay;
66

7+
use Ratoufa\Billing\Services\PhoneNumberService;
8+
79
/**
810
* Formats phone numbers for FedaPay API.
11+
*
12+
* Wrapper around PhoneNumberService for FedaPay-specific formatting.
913
*/
10-
final class PhoneNumberFormatter
14+
final readonly class PhoneNumberFormatter
1115
{
16+
public function __construct(
17+
private PhoneNumberService $phoneService,
18+
) {}
19+
1220
/**
13-
* Country code mappings.
14-
*
15-
* @var array<int|string, string>
21+
* Create a new instance with default dependencies.
1622
*/
17-
private const array COUNTRY_CODES = [
18-
229 => 'bj', // Benin
19-
226 => 'bf', // Burkina Faso
20-
225 => 'ci', // Côte d'Ivoire
21-
223 => 'ml', // Mali
22-
227 => 'ne', // Niger
23-
221 => 'sn', // Senegal
24-
228 => 'tg', // Togo
25-
224 => 'gn', // Guinea
26-
];
23+
public static function create(): self
24+
{
25+
return new self(PhoneNumberService::create());
26+
}
2727

2828
/**
2929
* Format a phone number for FedaPay API.
@@ -32,57 +32,32 @@ final class PhoneNumberFormatter
3232
*/
3333
public function format(?string $phone): ?array
3434
{
35-
if ($phone === null || $phone === '') {
36-
return null;
37-
}
38-
39-
$phone = $this->normalize($phone);
40-
$country = $this->detectCountry($phone);
41-
42-
return [
43-
'number' => $this->ensurePrefix($phone),
44-
'country' => $country,
45-
];
35+
return $this->phoneService->format($phone);
4636
}
4737

4838
/**
4939
* Normalize a phone number.
5040
*/
5141
public function normalize(string $phone): string
5242
{
53-
$normalized = preg_replace('/[^0-9+]/', '', $phone);
54-
55-
return $normalized ?? '';
43+
return $this->phoneService->normalize($phone);
5644
}
5745

5846
/**
5947
* Detect country from phone number.
6048
*/
6149
public function detectCountry(string $phone, string $default = 'tg'): string
6250
{
63-
$phone = $this->normalize($phone);
64-
$phone = mb_ltrim($phone, '+');
65-
66-
foreach (self::COUNTRY_CODES as $code => $country) {
67-
if (str_starts_with($phone, (string) $code)) {
68-
return $country;
69-
}
70-
}
51+
$this->phoneService->setDefaultCountry($default);
7152

72-
return $default;
53+
return $this->phoneService->detectCountry($phone);
7354
}
7455

7556
/**
7657
* Ensure phone number has + prefix.
7758
*/
7859
public function ensurePrefix(string $phone): string
7960
{
80-
$phone = $this->normalize($phone);
81-
82-
if (! str_starts_with($phone, '+')) {
83-
return '+'.$phone;
84-
}
85-
86-
return $phone;
61+
return $this->phoneService->ensurePrefix($phone);
8762
}
8863
}

0 commit comments

Comments
 (0)