webmoney/backend/app/Services/PayPalService.php
marcoitaloesp-ai 3a336eb692
feat: Admin user management + SaaS limits tested v1.51.0
- Add UserManagementController@store for creating users
- Add POST /api/admin/users endpoint
- Support user types: Free, Pro, Admin
- Auto-create 100-year subscription for Pro/Admin users
- Add user creation modal to Users.jsx
- Complete SaaS limit testing:
  - Free user limits: 1 account, 10 categories, 3 budgets, 100 tx
  - Middleware blocks correctly at limits
  - Error messages are user-friendly
  - Usage stats API working correctly
- Update SAAS_STATUS.md with test results
- Bump version to 1.51.0
2025-12-17 15:22:01 +00:00

370 lines
12 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use App\Models\Plan;
class PayPalService
{
private ?string $clientId;
private ?string $clientSecret;
private string $baseUrl;
private bool $sandbox;
public function __construct()
{
$this->clientId = config('services.paypal.client_id');
$this->clientSecret = config('services.paypal.client_secret');
$this->sandbox = config('services.paypal.mode', 'sandbox') === 'sandbox';
$this->baseUrl = $this->sandbox
? 'https://api-m.sandbox.paypal.com'
: 'https://api-m.paypal.com';
}
/**
* Get PayPal access token (cached for 8 hours)
*/
public function getAccessToken(): ?string
{
if (!$this->isConfigured()) {
Log::warning('PayPal not configured');
return null;
}
return Cache::remember('paypal_access_token', 28800, function () {
try {
$response = Http::withBasicAuth($this->clientId, $this->clientSecret)
->asForm()
->post("{$this->baseUrl}/v1/oauth2/token", [
'grant_type' => 'client_credentials',
]);
if ($response->successful()) {
return $response->json('access_token');
}
Log::error('PayPal auth failed', ['response' => $response->json()]);
return null;
} catch (\Exception $e) {
Log::error('PayPal auth exception', ['error' => $e->getMessage()]);
return null;
}
});
}
/**
* Create a PayPal Product (required before creating plans)
*/
public function createProduct(): ?array
{
$token = $this->getAccessToken();
if (!$token) return null;
try {
$response = Http::withToken($token)
->post("{$this->baseUrl}/v1/catalogs/products", [
'name' => 'WEBMoney Pro',
'description' => 'WEBMoney - Control de Finanzas Personales',
'type' => 'SERVICE',
'category' => 'SOFTWARE',
'home_url' => config('app.url'),
]);
if ($response->successful()) {
return $response->json();
}
Log::error('PayPal create product failed', ['response' => $response->json()]);
return null;
} catch (\Exception $e) {
Log::error('PayPal create product exception', ['error' => $e->getMessage()]);
return null;
}
}
/**
* Create a PayPal billing plan
*/
public function createBillingPlan(Plan $plan, string $productId): ?array
{
$token = $this->getAccessToken();
if (!$token) return null;
$billingCycles = [];
// Trial period (if plan has trial)
if ($plan->trial_days > 0) {
$billingCycles[] = [
'frequency' => [
'interval_unit' => 'DAY',
'interval_count' => $plan->trial_days,
],
'tenure_type' => 'TRIAL',
'sequence' => 1,
'total_cycles' => 1,
'pricing_scheme' => [
'fixed_price' => [
'value' => '0',
'currency_code' => $plan->currency,
],
],
];
}
// Regular billing cycle
$billingCycles[] = [
'frequency' => [
'interval_unit' => $plan->billing_period === 'annual' ? 'YEAR' : 'MONTH',
'interval_count' => 1,
],
'tenure_type' => 'REGULAR',
'sequence' => $plan->trial_days > 0 ? 2 : 1,
'total_cycles' => 0, // Infinite
'pricing_scheme' => [
'fixed_price' => [
'value' => (string) $plan->price,
'currency_code' => $plan->currency,
],
],
];
try {
$response = Http::withToken($token)
->post("{$this->baseUrl}/v1/billing/plans", [
'product_id' => $productId,
'name' => $plan->name,
'description' => $plan->description ?? "Suscripción {$plan->name}",
'status' => 'ACTIVE',
'billing_cycles' => $billingCycles,
'payment_preferences' => [
'auto_bill_outstanding' => true,
'setup_fee' => [
'value' => '0',
'currency_code' => $plan->currency,
],
'setup_fee_failure_action' => 'CONTINUE',
'payment_failure_threshold' => 3,
],
]);
if ($response->successful()) {
$data = $response->json();
// Update plan with PayPal plan ID
$plan->update(['paypal_plan_id' => $data['id']]);
return $data;
}
Log::error('PayPal create plan failed', ['response' => $response->json()]);
return null;
} catch (\Exception $e) {
Log::error('PayPal create plan exception', ['error' => $e->getMessage()]);
return null;
}
}
/**
* Create a subscription for a user
*/
public function createSubscription(Plan $plan, string $returnUrl, string $cancelUrl): ?array
{
$token = $this->getAccessToken();
if (!$token) return null;
if (!$plan->paypal_plan_id) {
Log::error('Plan has no PayPal plan ID', ['plan_id' => $plan->id]);
return null;
}
try {
$response = Http::withToken($token)
->post("{$this->baseUrl}/v1/billing/subscriptions", [
'plan_id' => $plan->paypal_plan_id,
'application_context' => [
'brand_name' => 'WEBMoney',
'locale' => 'es-ES',
'shipping_preference' => 'NO_SHIPPING',
'user_action' => 'SUBSCRIBE_NOW',
'payment_method' => [
'payer_selected' => 'PAYPAL',
'payee_preferred' => 'IMMEDIATE_PAYMENT_REQUIRED',
],
'return_url' => $returnUrl,
'cancel_url' => $cancelUrl,
],
]);
if ($response->successful()) {
return $response->json();
}
Log::error('PayPal create subscription failed', ['response' => $response->json()]);
return null;
} catch (\Exception $e) {
Log::error('PayPal create subscription exception', ['error' => $e->getMessage()]);
return null;
}
}
/**
* Get subscription details
*/
public function getSubscription(string $subscriptionId): ?array
{
$token = $this->getAccessToken();
if (!$token) return null;
try {
$response = Http::withToken($token)
->get("{$this->baseUrl}/v1/billing/subscriptions/{$subscriptionId}");
if ($response->successful()) {
return $response->json();
}
Log::error('PayPal get subscription failed', ['response' => $response->json()]);
return null;
} catch (\Exception $e) {
Log::error('PayPal get subscription exception', ['error' => $e->getMessage()]);
return null;
}
}
/**
* Cancel a subscription
*/
public function cancelSubscription(string $subscriptionId, string $reason = 'User requested cancellation'): bool
{
$token = $this->getAccessToken();
if (!$token) return false;
try {
$response = Http::withToken($token)
->post("{$this->baseUrl}/v1/billing/subscriptions/{$subscriptionId}/cancel", [
'reason' => $reason,
]);
if ($response->status() === 204) {
return true;
}
Log::error('PayPal cancel subscription failed', ['response' => $response->json()]);
return false;
} catch (\Exception $e) {
Log::error('PayPal cancel subscription exception', ['error' => $e->getMessage()]);
return false;
}
}
/**
* Suspend a subscription
*/
public function suspendSubscription(string $subscriptionId, string $reason = 'Suspended by system'): bool
{
$token = $this->getAccessToken();
if (!$token) return false;
try {
$response = Http::withToken($token)
->post("{$this->baseUrl}/v1/billing/subscriptions/{$subscriptionId}/suspend", [
'reason' => $reason,
]);
if ($response->status() === 204) {
return true;
}
Log::error('PayPal suspend subscription failed', ['response' => $response->json()]);
return false;
} catch (\Exception $e) {
Log::error('PayPal suspend subscription exception', ['error' => $e->getMessage()]);
return false;
}
}
/**
* Verify webhook signature
*/
public function verifyWebhookSignature(array $headers, string $body, string $webhookId): bool
{
$token = $this->getAccessToken();
if (!$token) return false;
try {
$response = Http::withToken($token)
->post("{$this->baseUrl}/v1/notifications/verify-webhook-signature", [
'auth_algo' => $headers['PAYPAL-AUTH-ALGO'] ?? '',
'cert_url' => $headers['PAYPAL-CERT-URL'] ?? '',
'transmission_id' => $headers['PAYPAL-TRANSMISSION-ID'] ?? '',
'transmission_sig' => $headers['PAYPAL-TRANSMISSION-SIG'] ?? '',
'transmission_time' => $headers['PAYPAL-TRANSMISSION-TIME'] ?? '',
'webhook_id' => $webhookId,
'webhook_event' => json_decode($body, true),
]);
if ($response->successful()) {
return $response->json('verification_status') === 'SUCCESS';
}
return false;
} catch (\Exception $e) {
Log::error('PayPal verify webhook exception', ['error' => $e->getMessage()]);
return false;
}
}
/**
* Get transactions for a subscription
*/
public function getSubscriptionTransactions(string $subscriptionId, string $startTime, string $endTime): ?array
{
$token = $this->getAccessToken();
if (!$token) return null;
try {
$response = Http::withToken($token)
->get("{$this->baseUrl}/v1/billing/subscriptions/{$subscriptionId}/transactions", [
'start_time' => $startTime,
'end_time' => $endTime,
]);
if ($response->successful()) {
return $response->json();
}
return null;
} catch (\Exception $e) {
Log::error('PayPal get transactions exception', ['error' => $e->getMessage()]);
return null;
}
}
/**
* Check if PayPal is configured
*/
public function isConfigured(): bool
{
return !empty($this->clientId) && !empty($this->clientSecret);
}
/**
* Get client ID for frontend
*/
public function getClientId(): ?string
{
return $this->clientId;
}
/**
* Check if in sandbox mode
*/
public function isSandbox(): bool
{
return $this->sandbox;
}
}