feat(Deployment): Integrate Ansible deployment via PHP deployment pipeline
- Create AnsibleDeployStage using framework's Process module for secure command execution - Integrate AnsibleDeployStage into DeploymentPipelineCommands for production deployments - Add force_deploy flag support in Ansible playbook to override stale locks - Use PHP deployment module as orchestrator (php console.php deploy:production) - Fix ErrorAggregationInitializer to use Environment class instead of $_ENV superglobal Architecture: - BuildStage → AnsibleDeployStage → HealthCheckStage for production - Process module provides timeout, error handling, and output capture - Ansible playbook supports rollback via rollback-git-based.yml - Zero-downtime deployments with health checks
This commit is contained in:
438
docs/whatsapp-notification-channel.md
Normal file
438
docs/whatsapp-notification-channel.md
Normal file
@@ -0,0 +1,438 @@
|
||||
# WhatsApp Notification Channel
|
||||
|
||||
Dokumentation für den WhatsApp Business API Notification Channel im Custom PHP Framework.
|
||||
|
||||
## Übersicht
|
||||
|
||||
Der WhatsApp Channel ermöglicht das Versenden von Notifications über die WhatsApp Business API. Es werden sowohl Textnachrichten als auch Template-basierte Nachrichten unterstützt.
|
||||
|
||||
## Features
|
||||
|
||||
✅ **Text Messages**: Einfache Textnachrichten mit Markdown-Formatierung
|
||||
✅ **Template Messages**: WhatsApp-approved Message Templates mit Parametern
|
||||
✅ **Action Buttons**: Support für Action URLs und Labels
|
||||
✅ **Type Safety**: Framework-konforme Value Objects für alle Identifier
|
||||
✅ **HttpClient Integration**: Nutzung des Framework's HttpClient Moduls
|
||||
✅ **Error Handling**: Umfassende Exception-Behandlung mit WhatsAppApiException
|
||||
|
||||
## Architektur
|
||||
|
||||
```
|
||||
WhatsAppChannel (NotificationChannelInterface)
|
||||
↓
|
||||
WhatsAppClient (API Communication)
|
||||
↓
|
||||
HttpClient (Framework's HTTP Module)
|
||||
↓
|
||||
WhatsApp Business API
|
||||
```
|
||||
|
||||
## Installation & Setup
|
||||
|
||||
### 1. WhatsApp Business Account einrichten
|
||||
|
||||
1. Erstelle einen WhatsApp Business Account bei Facebook
|
||||
2. Registriere deine Business Phone Number
|
||||
3. Generiere einen Access Token
|
||||
4. Notiere deine Phone Number ID und Business Account ID
|
||||
|
||||
**URLs**:
|
||||
- WhatsApp Business Dashboard: https://business.facebook.com/settings/whatsapp-business-accounts
|
||||
- Meta for Developers: https://developers.facebook.com/
|
||||
|
||||
### 2. Konfiguration
|
||||
|
||||
Die Konfiguration erfolgt aktuell hardcoded in `WhatsAppConfig::createDefault()`:
|
||||
|
||||
```php
|
||||
use App\Framework\Notification\Channels\WhatsApp\WhatsAppConfig;
|
||||
|
||||
$config = WhatsAppConfig::createDefault();
|
||||
// Oder manuell:
|
||||
$config = new WhatsAppConfig(
|
||||
accessToken: 'YOUR_ACCESS_TOKEN',
|
||||
phoneNumberId: 'YOUR_PHONE_NUMBER_ID',
|
||||
businessAccountId: WhatsAppBusinessAccountId::fromString('YOUR_BUSINESS_ACCOUNT_ID'),
|
||||
apiVersion: 'v18.0'
|
||||
);
|
||||
```
|
||||
|
||||
## Verwendung
|
||||
|
||||
### Basic Text Message
|
||||
|
||||
```php
|
||||
use App\Framework\Core\ValueObjects\PhoneNumber;
|
||||
use App\Framework\Notification\Notification;
|
||||
use App\Framework\Notification\ValueObjects\NotificationChannel;
|
||||
use App\Framework\Notification\ValueObjects\SystemNotificationType;
|
||||
|
||||
// Create notification
|
||||
$notification = Notification::create(
|
||||
recipientId: 'user_123',
|
||||
type: SystemNotificationType::SYSTEM_ALERT(),
|
||||
title: 'Important Update',
|
||||
body: 'Your order has been shipped!',
|
||||
NotificationChannel::WHATSAPP
|
||||
);
|
||||
|
||||
// Send via dispatcher
|
||||
$result = $notificationDispatcher->send($notification);
|
||||
|
||||
if ($result->isSuccessful()) {
|
||||
echo "WhatsApp message sent: {$result->getMetadata()['message_id']}";
|
||||
}
|
||||
```
|
||||
|
||||
### Template Message
|
||||
|
||||
WhatsApp erfordert pre-approved Templates für Marketing und Notifications.
|
||||
|
||||
```php
|
||||
$notification = Notification::create(
|
||||
recipientId: 'user_123',
|
||||
type: SystemNotificationType::ORDER_CONFIRMATION(),
|
||||
title: 'Order Confirmation',
|
||||
body: 'Template will be used',
|
||||
NotificationChannel::WHATSAPP
|
||||
)->withData([
|
||||
'whatsapp_template_id' => 'order_confirmation',
|
||||
'whatsapp_language' => 'en_US',
|
||||
'whatsapp_template_params' => [
|
||||
'John Doe', // Customer name
|
||||
'ORD-12345', // Order number
|
||||
'€99.99' // Total amount
|
||||
]
|
||||
]);
|
||||
|
||||
$result = $notificationDispatcher->send($notification);
|
||||
```
|
||||
|
||||
### With Action Button
|
||||
|
||||
```php
|
||||
$notification = Notification::create(
|
||||
recipientId: 'user_123',
|
||||
type: SystemNotificationType::PAYMENT_REMINDER(),
|
||||
title: 'Payment Due',
|
||||
body: 'Your invoice is ready for payment.',
|
||||
NotificationChannel::WHATSAPP
|
||||
)->withAction(
|
||||
url: 'https://example.com/invoices/123',
|
||||
label: 'View Invoice'
|
||||
);
|
||||
|
||||
// Message will include: "👉 View Invoice: https://example.com/invoices/123"
|
||||
```
|
||||
|
||||
## Phone Number Resolver
|
||||
|
||||
Implementiere `UserPhoneNumberResolver` für deine Anwendung:
|
||||
|
||||
```php
|
||||
use App\Framework\Core\ValueObjects\PhoneNumber;
|
||||
use App\Framework\Notification\Channels\WhatsApp\UserPhoneNumberResolver;
|
||||
|
||||
final readonly class DatabaseUserPhoneNumberResolver implements UserPhoneNumberResolver
|
||||
{
|
||||
public function __construct(
|
||||
private UserRepository $userRepository
|
||||
) {}
|
||||
|
||||
public function resolvePhoneNumber(string $userId): ?PhoneNumber
|
||||
{
|
||||
$user = $this->userRepository->find($userId);
|
||||
|
||||
if ($user === null || $user->phoneNumber === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return PhoneNumber::fromString($user->phoneNumber);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// Invalid phone number format
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Value Objects
|
||||
|
||||
### PhoneNumber
|
||||
|
||||
```php
|
||||
use App\Framework\Core\ValueObjects\PhoneNumber;
|
||||
|
||||
// E.164 format required: +[country code][number]
|
||||
$phone = PhoneNumber::fromString('+4917612345678');
|
||||
|
||||
// Or from parts
|
||||
$phone = PhoneNumber::fromInternational('49', '17612345678');
|
||||
|
||||
// Methods
|
||||
$phone->toString(); // +4917612345678
|
||||
$phone->toDisplayFormat(); // +49 176 126 456 78
|
||||
$phone->getCountryCode(); // 49
|
||||
$phone->getSubscriberNumber(); // 17612345678
|
||||
```
|
||||
|
||||
### WhatsAppTemplateId
|
||||
|
||||
```php
|
||||
use App\Framework\Notification\Channels\WhatsApp\ValueObjects\WhatsAppTemplateId;
|
||||
|
||||
// Template names must be lowercase alphanumeric with underscores
|
||||
$templateId = WhatsAppTemplateId::fromString('order_confirmation');
|
||||
$templateId = WhatsAppTemplateId::fromString('hello_world');
|
||||
|
||||
// ❌ Invalid
|
||||
$templateId = WhatsAppTemplateId::fromString('OrderConfirmation'); // Uppercase not allowed
|
||||
$templateId = WhatsAppTemplateId::fromString('order-confirmation'); // Hyphen not allowed
|
||||
```
|
||||
|
||||
### WhatsAppBusinessAccountId
|
||||
|
||||
```php
|
||||
use App\Framework\Notification\Channels\WhatsApp\ValueObjects\WhatsAppBusinessAccountId;
|
||||
|
||||
$accountId = WhatsAppBusinessAccountId::fromString('123456789012345');
|
||||
```
|
||||
|
||||
### WhatsAppMessageId
|
||||
|
||||
```php
|
||||
use App\Framework\Notification\Channels\WhatsApp\ValueObjects\WhatsAppMessageId;
|
||||
|
||||
// Returned from API after sending
|
||||
$messageId = WhatsAppMessageId::fromString('wamid.HBgNNDkxNzYxMjM0NTY3OBUCABIYFjNFQjBDNzE4RjAzMEE1NzQxODZEMDIA');
|
||||
```
|
||||
|
||||
## WhatsApp Templates
|
||||
|
||||
### Template Erstellen
|
||||
|
||||
1. Gehe zu WhatsApp Business Manager
|
||||
2. Navigiere zu "Message Templates"
|
||||
3. Erstelle ein neues Template
|
||||
4. Warte auf Approval (kann 24-48h dauern)
|
||||
|
||||
### Template Beispiel
|
||||
|
||||
**Template Name**: `order_confirmation`
|
||||
**Language**: English (US)
|
||||
**Category**: Transactional
|
||||
**Body**:
|
||||
```
|
||||
Hello {{1}},
|
||||
|
||||
Your order {{2}} has been confirmed!
|
||||
Total amount: {{3}}
|
||||
|
||||
Thank you for your purchase.
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```php
|
||||
$notification->withData([
|
||||
'whatsapp_template_id' => 'order_confirmation',
|
||||
'whatsapp_language' => 'en_US',
|
||||
'whatsapp_template_params' => [
|
||||
'John Doe', // {{1}}
|
||||
'ORD-12345', // {{2}}
|
||||
'€99.99' // {{3}}
|
||||
]
|
||||
]);
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Test Script
|
||||
|
||||
```bash
|
||||
docker exec php php tests/debug/test-whatsapp-notification.php
|
||||
```
|
||||
|
||||
**Wichtig**: Ersetze die Test-Telefonnummer in der Datei mit deiner eigenen WhatsApp-Nummer!
|
||||
|
||||
### Unit Test
|
||||
|
||||
```php
|
||||
use App\Framework\Notification\Channels\WhatsAppChannel;
|
||||
use App\Framework\Notification\Notification;
|
||||
|
||||
it('sends WhatsApp notification successfully', function () {
|
||||
$mockClient = Mockery::mock(WhatsAppClient::class);
|
||||
$mockResolver = Mockery::mock(UserPhoneNumberResolver::class);
|
||||
|
||||
$mockResolver->shouldReceive('resolvePhoneNumber')
|
||||
->with('user_123')
|
||||
->andReturn(PhoneNumber::fromString('+4917612345678'));
|
||||
|
||||
$mockClient->shouldReceive('sendTextMessage')
|
||||
->once()
|
||||
->andReturn(new WhatsAppResponse(
|
||||
success: true,
|
||||
messageId: WhatsAppMessageId::fromString('wamid_test_123')
|
||||
));
|
||||
|
||||
$channel = new WhatsAppChannel($mockClient, $mockResolver);
|
||||
|
||||
$notification = Notification::create(
|
||||
recipientId: 'user_123',
|
||||
type: SystemNotificationType::SYSTEM_ALERT(),
|
||||
title: 'Test',
|
||||
body: 'Test message',
|
||||
NotificationChannel::WHATSAPP
|
||||
);
|
||||
|
||||
$result = $channel->send($notification);
|
||||
|
||||
expect($result->isSuccessful())->toBeTrue();
|
||||
});
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### WhatsAppApiException
|
||||
|
||||
```php
|
||||
use App\Framework\Notification\Channels\WhatsApp\WhatsAppApiException;
|
||||
|
||||
try {
|
||||
$response = $whatsappClient->sendTextMessage($phoneNumber, $message);
|
||||
} catch (WhatsAppApiException $e) {
|
||||
// API returned an error
|
||||
$httpCode = $e->getHttpStatusCode();
|
||||
$message = $e->getMessage();
|
||||
|
||||
// Log or handle error
|
||||
$logger->error('WhatsApp API error', [
|
||||
'http_code' => $httpCode,
|
||||
'message' => $message
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
### Common Errors
|
||||
|
||||
| Error Code | Beschreibung | Lösung |
|
||||
|------------|--------------|--------|
|
||||
| 100 | Invalid parameter | Prüfe Parameter (phone number format, template ID) |
|
||||
| 131009 | Parameter value not valid | Template parameters stimmen nicht mit Template überein |
|
||||
| 131026 | Message undeliverable | Empfänger hat WhatsApp nicht oder blockiert Business Account |
|
||||
| 131047 | Re-engagement message | User muss zuerst Business Account kontaktieren |
|
||||
| 190 | Access token expired | Generiere neuen Access Token |
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Phone Number Validation
|
||||
|
||||
```php
|
||||
// ✅ Validate before using
|
||||
try {
|
||||
$phone = PhoneNumber::fromString($userInput);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// Handle invalid phone number
|
||||
return 'Invalid phone number format';
|
||||
}
|
||||
|
||||
// ❌ Don't assume format
|
||||
$phone = PhoneNumber::fromString($_POST['phone']); // Can throw exception
|
||||
```
|
||||
|
||||
### 2. Template Usage
|
||||
|
||||
```php
|
||||
// ✅ Use templates for marketing/promotional content
|
||||
$notification->withData([
|
||||
'whatsapp_template_id' => 'weekly_newsletter',
|
||||
'whatsapp_language' => 'en_US'
|
||||
]);
|
||||
|
||||
// ✅ Use text messages for immediate transactional updates
|
||||
$notification = Notification::create(
|
||||
recipientId: 'user_123',
|
||||
type: SystemNotificationType::SYSTEM_ALERT(),
|
||||
title: 'Server Alert',
|
||||
body: 'Critical: Database connection lost!',
|
||||
NotificationChannel::WHATSAPP
|
||||
);
|
||||
```
|
||||
|
||||
### 3. Rate Limiting
|
||||
|
||||
WhatsApp hat Rate Limits pro Business Account:
|
||||
- **Tier 1** (default): 1,000 unique contacts/24h
|
||||
- **Tier 2**: 10,000 unique contacts/24h
|
||||
- **Tier 3**: 100,000 unique contacts/24h
|
||||
|
||||
```php
|
||||
// Implement rate limiting
|
||||
if ($this->rateLimiter->tooManyAttempts("whatsapp:{$userId}", 5, 3600)) {
|
||||
throw new RateLimitException('Too many WhatsApp messages sent');
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Opt-In Requirement
|
||||
|
||||
WhatsApp erfordert **explicit Opt-In** von Usern:
|
||||
|
||||
```php
|
||||
// Check user consent before sending
|
||||
if (!$user->hasWhatsAppOptIn()) {
|
||||
return ChannelResult::failure(
|
||||
channel: NotificationChannel::WHATSAPP,
|
||||
errorMessage: 'User has not opted in to WhatsApp notifications'
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Message nicht zugestellt
|
||||
|
||||
**Checklist**:
|
||||
- [ ] Phone Number ist in E.164 Format (`+4917612345678`)
|
||||
- [ ] Empfänger hat WhatsApp installiert
|
||||
- [ ] Empfänger hat Business Account nicht blockiert
|
||||
- [ ] Access Token ist gültig
|
||||
- [ ] Template ist approved (für Template Messages)
|
||||
- [ ] Rate Limits nicht überschritten
|
||||
|
||||
### Template Errors
|
||||
|
||||
**Problem**: "Parameter value not valid"
|
||||
**Lösung**: Anzahl der Parameter muss exakt mit Template übereinstimmen
|
||||
|
||||
```php
|
||||
// Template hat 3 placeholders: {{1}}, {{2}}, {{3}}
|
||||
// ✅ Correct
|
||||
'whatsapp_template_params' => ['Param1', 'Param2', 'Param3']
|
||||
|
||||
// ❌ Wrong - zu wenige Parameter
|
||||
'whatsapp_template_params' => ['Param1', 'Param2']
|
||||
```
|
||||
|
||||
### Access Token Expired
|
||||
|
||||
**Problem**: Error 190 - Access token has expired
|
||||
**Lösung**: Generiere neuen Access Token im Facebook Business Manager
|
||||
|
||||
## Weiterführende Ressourcen
|
||||
|
||||
- **WhatsApp Business API Docs**: https://developers.facebook.com/docs/whatsapp/cloud-api
|
||||
- **Message Templates**: https://developers.facebook.com/docs/whatsapp/business-management-api/message-templates
|
||||
- **Error Codes**: https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes
|
||||
- **E.164 Format**: https://en.wikipedia.org/wiki/E.164
|
||||
|
||||
## Framework Integration
|
||||
|
||||
Der WhatsApp Channel folgt allen Framework-Patterns:
|
||||
|
||||
✅ **Readonly Classes**: Alle VOs und Configs sind `final readonly`
|
||||
✅ **Value Objects**: Keine Primitive Obsession (PhoneNumber, TemplateId, etc.)
|
||||
✅ **No Inheritance**: Composition über Inheritance
|
||||
✅ **Type Safety**: Strikte Typisierung für alle Parameter
|
||||
✅ **Framework Compliance**: Integration mit HttpClient, Notification System
|
||||
✅ **Explicit Dependencies**: Constructor Injection, keine Service Locators
|
||||
Reference in New Issue
Block a user