Files
zemailnator/app/Services/Payments/Providers/ActivationKeyProvider.php
idevakk 0d33c57b32 feat(notifications): implement comprehensive telegram notifications for payment providers
- Add NotifyMe trait with centralized Telegram bot integration
  - Implement webhook notifications for Polar, OxaPay, and ActivationKey providers
  - Add subscription lifecycle notifications (create, activate, cancel, pause, resume)
  - Enhance Polar webhook processing with user context and error handling
  - Fix subscription.updated and subscription.canceled webhook column errors
  - Add idempotent webhook processing to prevent duplicate handling
2025-12-08 09:25:19 -08:00

586 lines
20 KiB
PHP

<?php
namespace App\Services\Payments\Providers;
use App\Contracts\Payments\PaymentProviderContract;
use App\Models\ActivationKey;
use App\Models\Plan;
use App\Models\Subscription;
use App\Models\User;
use App\NotifyMe;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class ActivationKeyProvider implements PaymentProviderContract
{
use NotifyMe;
protected array $config;
public function __construct(array $config = [])
{
$this->config = array_merge([
'key_prefix' => 'AK-',
'key_length' => 32,
'expiration_days' => null, // null means no expiration
'success_url' => route('payment.success'),
'cancel_url' => route('payment.cancel'),
], $config);
}
public function getName(): string
{
return 'activation_key';
}
public function isActive(): bool
{
return true; // Activation keys are always available
}
public function createSubscription(User $user, Plan $plan, array $options = []): array
{
try {
DB::beginTransaction();
// Generate a unique activation key
$activationKey = $this->generateUniqueActivationKey();
// Create activation key record
$keyRecord = ActivationKey::create([
'user_id' => $user->id,
'activation_key' => $activationKey,
'price_id' => $plan->id,
'is_activated' => false,
]);
// Create subscription record
$subscription = Subscription::create([
'user_id' => $user->id,
'plan_id' => $plan->id,
'type' => 'activation_key',
'stripe_id' => 'ak_'.$keyRecord->id.'_'.uniqid(more_entropy: true), // Use activation key ID + unique ID for compatibility
'stripe_status' => 'pending_activation',
'provider' => $this->getName(),
'provider_subscription_id' => $keyRecord->id,
'status' => 'pending_activation',
'starts_at' => null,
'ends_at' => null,
'provider_data' => [
'activation_key' => $activationKey,
'key_id' => $keyRecord->id,
'created_at' => now()->toISOString(),
],
]);
DB::commit();
// Notify activation key generated
$this->notifyActivationKeyGenerated($user, $plan, $activationKey);
return [
'provider_subscription_id' => $keyRecord->id,
'status' => 'pending_activation',
'activation_key' => $activationKey,
'plan_name' => $plan->name,
'plan_price' => $plan->price,
'type' => 'activation_key',
'message' => 'Activation key generated. User needs to redeem the key to activate the subscription.',
];
} catch (\Exception $e) {
DB::rollBack();
Log::error('Activation key subscription creation failed', [
'user_id' => $user->id,
'plan_id' => $plan->id,
'error' => $e->getMessage(),
]);
throw $e;
}
}
public function cancelSubscription(Subscription $subscription, string $reason = ''): bool
{
try {
// For activation keys, we don't actually cancel since it's a one-time activation
// We can deactivate the subscription if needed
$subscription->update([
'status' => 'cancelled',
'cancelled_at' => now(),
'cancellation_reason' => $reason,
]);
// Notify subscription cancelled
$this->notifySubscriptionCancelled($subscription, $reason ?: 'Manual cancellation');
return true;
} catch (\Exception $e) {
Log::error('Activation key subscription cancellation failed', [
'subscription_id' => $subscription->id,
'error' => $e->getMessage(),
]);
throw $e;
}
}
public function updateSubscription(Subscription $subscription, Plan $newPlan): array
{
try {
// Activation keys don't support plan updates
// User would need a new activation key for a different plan
throw new \Exception('Activation keys do not support plan updates');
} catch (\Exception $e) {
Log::error('Activation key subscription update failed', [
'subscription_id' => $subscription->id,
'new_plan_id' => $newPlan->id,
'error' => $e->getMessage(),
]);
throw $e;
}
}
public function pauseSubscription(Subscription $subscription): bool
{
// Activation keys can't be paused
return false;
}
public function resumeSubscription(Subscription $subscription): bool
{
// Activation keys can't be paused, so can't be resumed
return false;
}
public function getSubscriptionDetails(string $providerSubscriptionId): array
{
try {
$activationKey = ActivationKey::findOrFail($providerSubscriptionId);
return [
'id' => $activationKey->id,
'activation_key' => $activationKey->activation_key,
'user_id' => $activationKey->user_id,
'price_id' => $activationKey->price_id,
'is_activated' => $activationKey->is_activated,
'created_at' => $activationKey->created_at->toISOString(),
'updated_at' => $activationKey->updated_at->toISOString(),
];
} catch (\Exception $e) {
Log::error('Activation key subscription details retrieval failed', [
'subscription_id' => $providerSubscriptionId,
'error' => $e->getMessage(),
]);
throw $e;
}
}
public function createCheckoutSession(User $user, Plan $plan, array $options = []): array
{
return $this->createSubscription($user, $plan, $options);
}
public function createCustomerPortalSession(User $user): array
{
return [
'portal_url' => route('dashboard'),
'message' => 'Activation keys are managed through your dashboard',
];
}
public function processWebhook(Request $request): array
{
// Activation keys don't have webhooks
return [
'event_type' => 'not_applicable',
'processed' => false,
'data' => [],
];
}
public function validateWebhook(Request $request): bool
{
// No webhooks to validate
return false;
}
public function getConfiguration(): array
{
return $this->config;
}
public function syncSubscriptionStatus(Subscription $subscription): array
{
return $this->getSubscriptionDetails($subscription->provider_subscription_id);
}
public function getPaymentMethodDetails(string $paymentMethodId): array
{
try {
$activationKey = ActivationKey::findOrFail($paymentMethodId);
return [
'id' => $activationKey->id,
'type' => 'activation_key',
'activation_key' => $activationKey->activation_key,
'is_activated' => $activationKey->is_activated,
'created_at' => $activationKey->created_at->toISOString(),
];
} catch (\Exception $e) {
Log::error('Activation key payment method details retrieval failed', [
'payment_method_id' => $paymentMethodId,
'error' => $e->getMessage(),
]);
throw $e;
}
}
public function processRefund(string $paymentId, float $amount, string $reason = ''): array
{
// Activation keys are not refundable
throw new \Exception('Activation keys are not refundable');
}
public function getTransactionHistory(User $user, array $filters = []): array
{
try {
$query = ActivationKey::where('user_id', $user->id);
// Apply filters
if (isset($filters['status'])) {
if ($filters['status'] === 'activated') {
$query->where('is_activated', true);
} elseif ($filters['status'] === 'unactivated') {
$query->where('is_activated', false);
}
}
if (isset($filters['date_from'])) {
$query->where('created_at', '>=', $filters['date_from']);
}
if (isset($filters['date_to'])) {
$query->where('created_at', '<=', $filters['date_to']);
}
$activationKeys = $query->orderBy('created_at', 'desc')->get();
$transactions = [];
foreach ($activationKeys as $key) {
$transactions[] = [
'id' => $key->id,
'activation_key' => $key->activation_key,
'plan_id' => $key->price_id,
'is_activated' => $key->is_activated,
'created_at' => $key->created_at->toISOString(),
'updated_at' => $key->updated_at->toISOString(),
];
}
return $transactions;
} catch (\Exception $e) {
Log::error('Activation key transaction history retrieval failed', [
'user_id' => $user->id,
'error' => $e->getMessage(),
]);
throw $e;
}
}
public function calculateFees(float $amount): array
{
// Activation keys have no fees
return [
'fixed_fee' => 0,
'percentage_fee' => 0,
'total_fee' => 0,
'net_amount' => $amount,
];
}
public function getSupportedCurrencies(): array
{
return ['USD']; // Activation keys are currency-agnostic
}
public function supportsRecurring(): bool
{
return false; // Activation keys are one-time
}
public function supportsOneTime(): bool
{
return true;
}
// Helper methods
protected function generateUniqueActivationKey(): string
{
do {
$key = $this->config['key_prefix'].strtoupper(Str::random($this->config['key_length']));
} while (ActivationKey::where('activation_key', $key)->exists());
return $key;
}
// Public method for redeeming activation keys
public function redeemActivationKey(string $activationKey, User $user): array
{
try {
DB::beginTransaction();
$keyRecord = ActivationKey::where('activation_key', $activationKey)
->where('is_activated', false)
->firstOrFail();
// Mark key as activated and assign to user
$keyRecord->update([
'user_id' => $user->id,
'is_activated' => true,
]);
// Find the plan associated with this activation key
$plan = Plan::where('pricing_id', $keyRecord->price_id)->first();
if (! $plan) {
throw new \Exception('No plan found for activation key with pricing_id: '.$keyRecord->price_id);
}
// Calculate subscription end date based on plan billing cycle
$endsAt = null;
if ($plan->billing_cycle_days && $plan->billing_cycle_days > 0) {
$endsAt = now()->addDays($plan->billing_cycle_days);
}
$subscription = Subscription::create([
'user_id' => $user->id,
'plan_id' => $plan->id,
'type' => 'default',
'stripe_id' => 'ak_'.$keyRecord->id.'_'.uniqid('', true),
'stripe_status' => 'active',
'provider' => $this->getName(),
'provider_subscription_id' => $keyRecord->id,
'status' => 'active',
'starts_at' => now(),
'ends_at' => $endsAt,
'provider_data' => [
'activation_key' => $activationKey,
'key_id' => $keyRecord->id,
'redeemed_at' => now()->toISOString(),
'plan_details' => [
'name' => $plan->name,
'price' => $plan->price,
'billing_cycle_days' => $plan->billing_cycle_days,
'billing_cycle_display' => $plan->getBillingCycleDisplay(),
'plan_tier' => $plan->planTier ? $plan->planTier->name : null,
'features' => $plan->getFeaturesWithLimits(),
],
'provider_info' => [
'name' => $this->getName(),
'version' => '1.0',
'processed_at' => now()->toISOString(),
],
],
]);
DB::commit();
// Notify activation key redeemed and subscription activated
$this->notifyActivationKeyRedeemed($user, $plan, $activationKey);
$this->notifySubscriptionActivated($user, $plan, $subscription);
return [
'success' => true,
'subscription_id' => $subscription->id,
'plan_name' => $plan->name,
'message' => 'Activation key redeemed successfully',
];
} catch (\Exception $e) {
DB::rollBack();
Log::error('Activation key redemption failed', [
'activation_key' => $activationKey,
'user_id' => $user->id,
'error' => $e->getMessage(),
]);
throw $e;
}
}
// Additional interface methods
public function getSubscriptionMetadata(Subscription $subscription): array
{
$keyDetails = $this->getSubscriptionDetails($subscription->provider_subscription_id);
return $keyDetails + [
'redeemed_at' => $subscription->provider_data['redeemed_at'] ?? null,
];
}
public function updateSubscriptionMetadata(Subscription $subscription, array $metadata): bool
{
try {
$subscription->update([
'provider_data' => array_merge($subscription->provider_data ?? [], $metadata),
]);
return true;
} catch (\Exception $e) {
Log::error('Failed to update activation key subscription metadata', [
'subscription_id' => $subscription->id,
'error' => $e->getMessage(),
]);
return false;
}
}
public function startTrial(Subscription $subscription, int $trialDays): bool
{
// Activation keys don't support trials
return false;
}
public function applyCoupon(Subscription $subscription, string $couponCode): array
{
// Activation keys don't support coupons
throw new \Exception('Coupons not supported for activation keys');
}
public function removeCoupon(Subscription $subscription): bool
{
return false; // No coupons to remove
}
public function getUpcomingInvoice(Subscription $subscription): array
{
// Activation keys don't have invoices
return [
'amount_due' => 0,
'currency' => 'USD',
'next_payment_date' => null,
];
}
public function retryFailedPayment(Subscription $subscription): array
{
// No payments to retry for activation keys
return $this->syncSubscriptionStatus($subscription);
}
public function canModifySubscription(Subscription $subscription): bool
{
try {
$details = $this->getSubscriptionDetails($subscription->provider_subscription_id);
return ! $details['is_activated']; // Can only modify before activation
} catch (\Exception $e) {
return false;
}
}
public function getCancellationTerms(Subscription $subscription): array
{
return [
'immediate_cancellation' => true,
'refund_policy' => 'non_refundable',
'cancellation_effective' => 'immediately',
'billing_cycle_proration' => false,
];
}
public function exportSubscriptionData(Subscription $subscription): array
{
return [
'provider' => 'activation_key',
'provider_subscription_id' => $subscription->provider_subscription_id,
'data' => $subscription->provider_data,
];
}
public function importSubscriptionData(User $user, array $subscriptionData): array
{
throw new \Exception('Import to activation keys not implemented');
}
// Notification methods
protected function notifyActivationKeyGenerated(User $user, Plan $plan, string $activationKey): void
{
$message = "🔑 ACTIVATION KEY GENERATED\n".
"👤 User: {$user->name} ({$user->email})\n".
"📋 Plan: {$plan->name}\n".
'💰 Price: $'.number_format($plan->price, 2)."\n".
"🏪 Provider: Activation Key\n".
"🔑 Key: {$activationKey}\n".
'📅 Generated: '.now()->format('Y-m-d H:i:s');
$this->sendTelegramNotification($message);
}
protected function notifyActivationKeyRedeemed(User $user, Plan $plan, string $activationKey): void
{
$message = "✅ ACTIVATION KEY REDEEMED\n".
"👤 User: {$user->name} ({$user->email})\n".
"📋 Plan: {$plan->name}\n".
"🏪 Provider: Activation Key\n".
"🔑 Key: {$activationKey}\n".
'📅 Redeemed: '.now()->format('Y-m-d H:i:s');
$this->sendTelegramNotification($message);
}
protected function notifySubscriptionActivated(User $user, Plan $plan, Subscription $subscription): void
{
$message = "🎉 SUBSCRIPTION ACTIVATED\n".
"👤 User: {$user->name} ({$user->email})\n".
"📋 Plan: {$plan->name}\n".
"🏪 Provider: Activation Key\n".
"🔄 Subscription ID: {$subscription->id}\n".
'📅 Activated: '.now()->format('Y-m-d H:i:s');
$this->sendTelegramNotification($message);
}
protected function notifyProviderError(string $operation, string $error, array $context = []): void
{
$contextStr = '';
if (! empty($context)) {
$contextStr = '📝 Details: '.json_encode(array_slice($context, 0, 3, true), JSON_UNESCAPED_SLASHES)."\n";
if (count($context) > 3) {
$contextStr .= '📝 Additional: '.(count($context) - 3).' more items'."\n";
}
}
$message = "🚨 PROVIDER ERROR\n".
"🏪 Provider: Activation Key\n".
"📡 Operation: {$operation}\n".
"💥 Error: {$error}\n".
$contextStr.
'⏰ Time: '.now()->format('Y-m-d H:i:s');
$this->sendTelegramNotification($message);
}
protected function notifySubscriptionCancelled(Subscription $subscription, string $reason): void
{
$user = $subscription->user;
$plan = $subscription->plan;
$message = "❌ SUBSCRIPTION CANCELLED\n".
"👤 User: {$user->name} ({$user->email})\n".
"📋 Plan: {$plan->name}\n".
"🏪 Provider: Activation Key\n".
"💭 Reason: {$reason}\n".
"🆔 Subscription ID: {$subscription->id}\n".
($subscription->ends_at ? '📅 Effective: '.$subscription->ends_at->format('Y-m-d')."\n" : '').
'⏰ Cancelled: '.now()->format('Y-m-d H:i:s');
$this->sendTelegramNotification($message);
}
}