switched to paddle inline checkout, removed paypal and most of stripe. added product sync between app and paddle.
This commit is contained in:
@@ -4,8 +4,8 @@ namespace App\Services\Checkout;
|
||||
|
||||
use App\Mail\PurchaseConfirmation;
|
||||
use App\Mail\Welcome;
|
||||
use App\Models\CheckoutSession;
|
||||
use App\Models\AbandonedCheckout;
|
||||
use App\Models\CheckoutSession;
|
||||
use App\Models\Package;
|
||||
use App\Models\PackagePurchase;
|
||||
use App\Models\Tenant;
|
||||
@@ -47,10 +47,19 @@ class CheckoutAssignmentService
|
||||
return;
|
||||
}
|
||||
|
||||
$metadata = $session->provider_metadata ?? [];
|
||||
|
||||
$providerReference = $options['provider_reference']
|
||||
?? $metadata['paddle_transaction_id'] ?? null
|
||||
?? $metadata['paddle_checkout_id'] ?? null
|
||||
?? $session->stripe_payment_intent_id
|
||||
?? $session->paypal_order_id
|
||||
?? 'free';
|
||||
?? CheckoutSession::PROVIDER_FREE;
|
||||
|
||||
$providerName = $options['provider']
|
||||
?? $session->provider
|
||||
?? ($metadata['paddle_transaction_id'] ?? $metadata['paddle_checkout_id'] ? CheckoutSession::PROVIDER_PADDLE : null)
|
||||
?? ($session->stripe_payment_intent_id ? CheckoutSession::PROVIDER_STRIPE : null)
|
||||
?? CheckoutSession::PROVIDER_FREE;
|
||||
|
||||
$purchase = PackagePurchase::updateOrCreate(
|
||||
[
|
||||
@@ -59,6 +68,7 @@ class CheckoutAssignmentService
|
||||
'provider_id' => $providerReference,
|
||||
],
|
||||
[
|
||||
'provider' => $providerName,
|
||||
'price' => $session->amount_total,
|
||||
'type' => $package->type === 'reseller' ? 'reseller_subscription' : 'endcustomer_event',
|
||||
'purchased_at' => now(),
|
||||
@@ -121,6 +131,7 @@ class CheckoutAssignmentService
|
||||
if (! $user->tenant_id) {
|
||||
$user->forceFill(['tenant_id' => $user->tenant->getKey()])->save();
|
||||
}
|
||||
|
||||
return $user->tenant;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@ class CheckoutPaymentService
|
||||
public function __construct(
|
||||
private readonly CheckoutSessionService $sessions,
|
||||
private readonly CheckoutAssignmentService $assignment,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function initialiseStripe(CheckoutSession $session, array $payload = []): array
|
||||
{
|
||||
@@ -40,32 +39,6 @@ class CheckoutPaymentService
|
||||
return $session;
|
||||
}
|
||||
|
||||
public function initialisePayPal(CheckoutSession $session, array $payload = []): array
|
||||
{
|
||||
if ($session->provider !== CheckoutSession::PROVIDER_PAYPAL) {
|
||||
$this->sessions->selectProvider($session, CheckoutSession::PROVIDER_PAYPAL);
|
||||
}
|
||||
|
||||
// TODO: integrate PayPal Orders API and return order id + approval link
|
||||
return [
|
||||
'session_id' => $session->id,
|
||||
'status' => $session->status,
|
||||
'message' => 'PayPal integration pending implementation.',
|
||||
];
|
||||
}
|
||||
|
||||
public function capturePayPal(CheckoutSession $session, array $payload = []): CheckoutSession
|
||||
{
|
||||
if ($session->provider !== CheckoutSession::PROVIDER_PAYPAL) {
|
||||
throw new LogicException('Cannot capture PayPal payment on a non-PayPal session.');
|
||||
}
|
||||
|
||||
// TODO: call PayPal capture endpoint and persist order/subscription identifiers
|
||||
$this->sessions->markProcessing($session);
|
||||
|
||||
return $session;
|
||||
}
|
||||
|
||||
public function finaliseFree(CheckoutSession $session): CheckoutSession
|
||||
{
|
||||
if ($session->provider !== CheckoutSession::PROVIDER_FREE) {
|
||||
@@ -85,4 +58,4 @@ class CheckoutPaymentService
|
||||
|
||||
return $session;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ class CheckoutSessionService
|
||||
return $existing;
|
||||
}
|
||||
|
||||
$session = new CheckoutSession();
|
||||
$session = new CheckoutSession;
|
||||
$session->id = (string) Str::uuid();
|
||||
$session->status = CheckoutSession::STATUS_DRAFT;
|
||||
$session->provider = CheckoutSession::PROVIDER_NONE;
|
||||
@@ -69,8 +69,8 @@ class CheckoutSessionService
|
||||
$session->stripe_payment_intent_id = null;
|
||||
$session->stripe_customer_id = null;
|
||||
$session->stripe_subscription_id = null;
|
||||
$session->paypal_order_id = null;
|
||||
$session->paypal_subscription_id = null;
|
||||
$session->paddle_checkout_id = null;
|
||||
$session->paddle_transaction_id = null;
|
||||
$session->provider_metadata = [];
|
||||
$session->failure_reason = null;
|
||||
$session->expires_at = now()->addMinutes($this->sessionTtlMinutes);
|
||||
@@ -85,7 +85,11 @@ class CheckoutSessionService
|
||||
{
|
||||
$provider = strtolower($provider);
|
||||
|
||||
if (! in_array($provider, [CheckoutSession::PROVIDER_STRIPE, CheckoutSession::PROVIDER_PAYPAL, CheckoutSession::PROVIDER_FREE], true)) {
|
||||
if (! in_array($provider, [
|
||||
CheckoutSession::PROVIDER_STRIPE,
|
||||
CheckoutSession::PROVIDER_PADDLE,
|
||||
CheckoutSession::PROVIDER_FREE,
|
||||
], true)) {
|
||||
throw new RuntimeException("Unsupported checkout provider [{$provider}]");
|
||||
}
|
||||
|
||||
@@ -101,7 +105,7 @@ class CheckoutSessionService
|
||||
return $session;
|
||||
}
|
||||
|
||||
public function markRequiresCustomerAction(CheckoutSession $session, string $reason = null): CheckoutSession
|
||||
public function markRequiresCustomerAction(CheckoutSession $session, ?string $reason = null): CheckoutSession
|
||||
{
|
||||
$session->status = CheckoutSession::STATUS_REQUIRES_CUSTOMER_ACTION;
|
||||
$session->failure_reason = $reason;
|
||||
@@ -213,4 +217,4 @@ class CheckoutSessionService
|
||||
->orderByDesc('created_at')
|
||||
->first();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,23 @@
|
||||
namespace App\Services\Checkout;
|
||||
|
||||
use App\Models\CheckoutSession;
|
||||
use App\Models\Package;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\TenantPackage;
|
||||
use App\Services\Paddle\PaddleSubscriptionService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CheckoutWebhookService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CheckoutSessionService $sessions,
|
||||
private readonly CheckoutAssignmentService $assignment,
|
||||
) {
|
||||
}
|
||||
private readonly PaddleSubscriptionService $paddleSubscriptions,
|
||||
) {}
|
||||
|
||||
public function handleStripeEvent(array $event): bool
|
||||
{
|
||||
@@ -72,29 +78,37 @@ class CheckoutWebhookService
|
||||
}
|
||||
}
|
||||
|
||||
public function handlePayPalEvent(array $event): bool
|
||||
public function handlePaddleEvent(array $event): bool
|
||||
{
|
||||
$eventType = $event['event_type'] ?? null;
|
||||
$resource = $event['resource'] ?? [];
|
||||
$data = $event['data'] ?? [];
|
||||
|
||||
if (! $eventType || ! is_array($resource)) {
|
||||
if (! $eventType || ! is_array($data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$orderId = $resource['order_id'] ?? $resource['id'] ?? null;
|
||||
if (Str::startsWith($eventType, 'subscription.')) {
|
||||
return $this->handlePaddleSubscriptionEvent($eventType, $data);
|
||||
}
|
||||
|
||||
$session = $this->locatePayPalSession($resource, $orderId);
|
||||
$session = $this->locatePaddleSession($data);
|
||||
|
||||
if (! $session) {
|
||||
Log::info('[CheckoutWebhook] Paddle session not resolved', [
|
||||
'event_type' => $eventType,
|
||||
'transaction_id' => $data['id'] ?? null,
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$lockKey = "checkout:webhook:paypal:".($orderId ?: $session->id);
|
||||
$transactionId = $data['id'] ?? $data['transaction_id'] ?? null;
|
||||
$lockKey = 'checkout:webhook:paddle:'.($transactionId ?: $session->id);
|
||||
$lock = Cache::lock($lockKey, 30);
|
||||
|
||||
if (! $lock->get()) {
|
||||
Log::info('[CheckoutWebhook] PayPal lock busy', [
|
||||
'order_id' => $orderId,
|
||||
Log::info('[CheckoutWebhook] Paddle lock busy', [
|
||||
'transaction_id' => $transactionId,
|
||||
'session_id' => $session->id,
|
||||
]);
|
||||
|
||||
@@ -102,22 +116,29 @@ class CheckoutWebhookService
|
||||
}
|
||||
|
||||
try {
|
||||
$session->forceFill([
|
||||
'paypal_order_id' => $orderId ?: $session->paypal_order_id,
|
||||
'provider' => CheckoutSession::PROVIDER_PAYPAL,
|
||||
])->save();
|
||||
if ($transactionId) {
|
||||
$session->forceFill([
|
||||
'paddle_transaction_id' => $transactionId,
|
||||
'provider' => CheckoutSession::PROVIDER_PADDLE,
|
||||
])->save();
|
||||
} elseif ($session->provider !== CheckoutSession::PROVIDER_PADDLE) {
|
||||
$session->forceFill(['provider' => CheckoutSession::PROVIDER_PADDLE])->save();
|
||||
}
|
||||
|
||||
$metadata = [
|
||||
'paypal_last_event' => $eventType,
|
||||
'paypal_last_event_id' => $event['id'] ?? null,
|
||||
'paypal_last_update_at' => now()->toIso8601String(),
|
||||
'paypal_order_id' => $orderId,
|
||||
'paypal_capture_id' => $resource['id'] ?? null,
|
||||
'paddle_last_event' => $eventType,
|
||||
'paddle_transaction_id' => $transactionId,
|
||||
'paddle_status' => $data['status'] ?? null,
|
||||
'paddle_last_update_at' => now()->toIso8601String(),
|
||||
];
|
||||
|
||||
if (! empty($data['checkout_id'])) {
|
||||
$metadata['paddle_checkout_id'] = $data['checkout_id'];
|
||||
}
|
||||
|
||||
$this->mergeProviderMetadata($session, $metadata);
|
||||
|
||||
return $this->applyPayPalEvent($session, $eventType, $resource);
|
||||
return $this->applyPaddleEvent($session, $eventType, $data);
|
||||
} finally {
|
||||
$lock->release();
|
||||
}
|
||||
@@ -131,16 +152,19 @@ class CheckoutWebhookService
|
||||
$this->sessions->markProcessing($session, [
|
||||
'stripe_intent_status' => $intent['status'] ?? null,
|
||||
]);
|
||||
|
||||
return true;
|
||||
|
||||
case 'payment_intent.requires_action':
|
||||
$reason = $intent['next_action']['type'] ?? 'requires_action';
|
||||
$this->sessions->markRequiresCustomerAction($session, $reason);
|
||||
|
||||
return true;
|
||||
|
||||
case 'payment_intent.payment_failed':
|
||||
$failure = $intent['last_payment_error']['message'] ?? 'payment_failed';
|
||||
$this->sessions->markFailed($session, $failure);
|
||||
|
||||
return true;
|
||||
|
||||
case 'payment_intent.succeeded':
|
||||
@@ -165,25 +189,30 @@ class CheckoutWebhookService
|
||||
}
|
||||
}
|
||||
|
||||
protected function applyPayPalEvent(CheckoutSession $session, string $eventType, array $resource): bool
|
||||
protected function applyPaddleEvent(CheckoutSession $session, string $eventType, array $data): bool
|
||||
{
|
||||
$status = strtolower((string) ($data['status'] ?? ''));
|
||||
|
||||
switch ($eventType) {
|
||||
case 'CHECKOUT.ORDER.APPROVED':
|
||||
case 'transaction.created':
|
||||
case 'transaction.processing':
|
||||
$this->sessions->markProcessing($session, [
|
||||
'paypal_order_status' => $resource['status'] ?? null,
|
||||
'paddle_status' => $status ?: null,
|
||||
]);
|
||||
|
||||
return true;
|
||||
|
||||
case 'PAYMENT.CAPTURE.COMPLETED':
|
||||
case 'transaction.completed':
|
||||
if ($session->status !== CheckoutSession::STATUS_COMPLETED) {
|
||||
$this->sessions->markProcessing($session, [
|
||||
'paypal_order_status' => $resource['status'] ?? null,
|
||||
'paddle_status' => $status ?: 'completed',
|
||||
]);
|
||||
|
||||
$this->assignment->finalise($session, [
|
||||
'source' => 'paypal_webhook',
|
||||
'paypal_order_id' => $resource['order_id'] ?? null,
|
||||
'paypal_capture_id' => $resource['id'] ?? null,
|
||||
'source' => 'paddle_webhook',
|
||||
'provider' => CheckoutSession::PROVIDER_PADDLE,
|
||||
'provider_reference' => $data['id'] ?? null,
|
||||
'payload' => $data,
|
||||
]);
|
||||
|
||||
$this->sessions->markCompleted($session, now());
|
||||
@@ -191,8 +220,11 @@ class CheckoutWebhookService
|
||||
|
||||
return true;
|
||||
|
||||
case 'PAYMENT.CAPTURE.DENIED':
|
||||
$this->sessions->markFailed($session, 'paypal_capture_denied');
|
||||
case 'transaction.failed':
|
||||
case 'transaction.cancelled':
|
||||
$reason = $status ?: ($eventType === 'transaction.failed' ? 'paddle_failed' : 'paddle_cancelled');
|
||||
$this->sessions->markFailed($session, $reason);
|
||||
|
||||
return true;
|
||||
|
||||
default:
|
||||
@@ -200,6 +232,169 @@ class CheckoutWebhookService
|
||||
}
|
||||
}
|
||||
|
||||
protected function handlePaddleSubscriptionEvent(string $eventType, array $data): bool
|
||||
{
|
||||
$subscriptionId = $data['id'] ?? null;
|
||||
|
||||
if (! $subscriptionId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$metadata = $data['metadata'] ?? [];
|
||||
$tenant = $this->resolveTenantFromSubscription($data, $metadata, $subscriptionId);
|
||||
|
||||
if (! $tenant) {
|
||||
Log::info('[CheckoutWebhook] Paddle subscription tenant not resolved', [
|
||||
'subscription_id' => $subscriptionId,
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$package = $this->resolvePackageFromSubscription($data, $metadata, $subscriptionId);
|
||||
|
||||
if (! $package) {
|
||||
Log::info('[CheckoutWebhook] Paddle subscription package not resolved', [
|
||||
'subscription_id' => $subscriptionId,
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$status = strtolower((string) ($data['status'] ?? ''));
|
||||
$expiresAt = $this->resolveSubscriptionExpiry($data);
|
||||
$startedAt = $this->resolveSubscriptionStart($data);
|
||||
|
||||
$tenantPackage = TenantPackage::firstOrNew([
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
]);
|
||||
|
||||
$tenantPackage->fill([
|
||||
'paddle_subscription_id' => $subscriptionId,
|
||||
'price' => $package->price,
|
||||
]);
|
||||
|
||||
$tenantPackage->expires_at = $expiresAt ?? $tenantPackage->expires_at ?? $startedAt?->copy()->addYear();
|
||||
$tenantPackage->purchased_at = $tenantPackage->purchased_at
|
||||
?? $tenant->purchases()->where('package_id', $package->id)->latest('purchased_at')->value('purchased_at')
|
||||
?? $startedAt;
|
||||
|
||||
$tenantPackage->active = $this->isSubscriptionActive($status);
|
||||
$tenantPackage->save();
|
||||
|
||||
if ($eventType === 'subscription.cancelled' || $eventType === 'subscription.paused') {
|
||||
$tenantPackage->forceFill(['active' => false])->save();
|
||||
}
|
||||
|
||||
$tenant->forceFill([
|
||||
'subscription_status' => $this->mapSubscriptionStatus($status),
|
||||
'subscription_expires_at' => $expiresAt,
|
||||
'paddle_customer_id' => $tenant->paddle_customer_id ?: ($data['customer_id'] ?? null),
|
||||
])->save();
|
||||
|
||||
Log::info('[CheckoutWebhook] Paddle subscription event processed', [
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'subscription_id' => $subscriptionId,
|
||||
'event_type' => $eventType,
|
||||
'status' => $status,
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function resolveTenantFromSubscription(array $data, array $metadata, string $subscriptionId): ?Tenant
|
||||
{
|
||||
if (isset($metadata['tenant_id'])) {
|
||||
$tenant = Tenant::find((int) $metadata['tenant_id']);
|
||||
if ($tenant) {
|
||||
return $tenant;
|
||||
}
|
||||
}
|
||||
|
||||
$customerId = $data['customer_id'] ?? null;
|
||||
|
||||
if ($customerId) {
|
||||
$tenant = Tenant::where('paddle_customer_id', $customerId)->first();
|
||||
if ($tenant) {
|
||||
return $tenant;
|
||||
}
|
||||
}
|
||||
|
||||
$subscription = $this->paddleSubscriptions->retrieve($subscriptionId);
|
||||
$customerId = Arr::get($subscription, 'data.customer_id');
|
||||
|
||||
if ($customerId) {
|
||||
return Tenant::where('paddle_customer_id', $customerId)->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function resolvePackageFromSubscription(array $data, array $metadata, string $subscriptionId): ?Package
|
||||
{
|
||||
if (isset($metadata['package_id'])) {
|
||||
$package = Package::find((int) $metadata['package_id']);
|
||||
if ($package) {
|
||||
return $package;
|
||||
}
|
||||
}
|
||||
|
||||
$priceId = Arr::get($data, 'items.0.price_id') ?? Arr::get($data, 'items.0.price.id');
|
||||
|
||||
if ($priceId) {
|
||||
$package = Package::where('paddle_price_id', $priceId)->first();
|
||||
if ($package) {
|
||||
return $package;
|
||||
}
|
||||
}
|
||||
|
||||
$subscription = $this->paddleSubscriptions->retrieve($subscriptionId);
|
||||
$priceId = Arr::get($subscription, 'data.items.0.price_id') ?? Arr::get($subscription, 'data.items.0.price.id');
|
||||
|
||||
if ($priceId) {
|
||||
return Package::where('paddle_price_id', $priceId)->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function resolveSubscriptionExpiry(array $data): ?Carbon
|
||||
{
|
||||
$nextBilling = Arr::get($data, 'next_billing_date') ?? Arr::get($data, 'next_payment_date');
|
||||
|
||||
if ($nextBilling) {
|
||||
return Carbon::parse($nextBilling);
|
||||
}
|
||||
|
||||
$endsAt = Arr::get($data, 'billing_period_ends_at') ?? Arr::get($data, 'pays_out_at');
|
||||
|
||||
return $endsAt ? Carbon::parse($endsAt) : null;
|
||||
}
|
||||
|
||||
protected function resolveSubscriptionStart(array $data): Carbon
|
||||
{
|
||||
$created = Arr::get($data, 'created_at') ?? Arr::get($data, 'activated_at');
|
||||
|
||||
return $created ? Carbon::parse($created) : now();
|
||||
}
|
||||
|
||||
protected function isSubscriptionActive(string $status): bool
|
||||
{
|
||||
return in_array($status, ['active', 'trialing'], true);
|
||||
}
|
||||
|
||||
protected function mapSubscriptionStatus(string $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
'active', 'trialing' => 'active',
|
||||
'paused' => 'suspended',
|
||||
'cancelled', 'past_due', 'halted' => 'expired',
|
||||
default => 'free',
|
||||
};
|
||||
}
|
||||
|
||||
protected function mergeProviderMetadata(CheckoutSession $session, array $data): void
|
||||
{
|
||||
$session->provider_metadata = array_merge($session->provider_metadata ?? [], $data);
|
||||
@@ -230,42 +425,45 @@ class CheckoutWebhookService
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function locatePayPalSession(array $resource, ?string $orderId): ?CheckoutSession
|
||||
protected function locatePaddleSession(array $data): ?CheckoutSession
|
||||
{
|
||||
if ($orderId) {
|
||||
$session = CheckoutSession::query()
|
||||
->where('paypal_order_id', $orderId)
|
||||
->first();
|
||||
$metadata = $data['metadata'] ?? [];
|
||||
|
||||
if ($session) {
|
||||
if (is_array($metadata)) {
|
||||
$sessionId = $metadata['checkout_session_id'] ?? null;
|
||||
|
||||
if ($sessionId && $session = CheckoutSession::find($sessionId)) {
|
||||
return $session;
|
||||
}
|
||||
|
||||
$tenantId = $metadata['tenant_id'] ?? null;
|
||||
$packageId = $metadata['package_id'] ?? null;
|
||||
|
||||
if ($tenantId && $packageId) {
|
||||
$session = CheckoutSession::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('package_id', $packageId)
|
||||
->whereNotIn('status', [CheckoutSession::STATUS_COMPLETED, CheckoutSession::STATUS_CANCELLED])
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if ($session) {
|
||||
return $session;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$metadata = $this->extractPayPalMetadata($resource);
|
||||
$sessionId = $metadata['checkout_session_id'] ?? null;
|
||||
$checkoutId = $data['checkout_id'] ?? Arr::get($data, 'details.checkout_id');
|
||||
|
||||
if ($sessionId) {
|
||||
return CheckoutSession::find($sessionId);
|
||||
if ($checkoutId) {
|
||||
return CheckoutSession::query()
|
||||
->where('provider_metadata->paddle_checkout_id', $checkoutId)
|
||||
->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function extractPayPalMetadata(array $resource): array
|
||||
{
|
||||
$customId = $resource['custom_id'] ?? ($resource['purchase_units'][0]['custom_id'] ?? null);
|
||||
if ($customId) {
|
||||
$decoded = json_decode($customId, true);
|
||||
if (is_array($decoded)) {
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
$meta = Arr::get($resource, 'supplementary_data.related_ids', []);
|
||||
return is_array($meta) ? $meta : [];
|
||||
}
|
||||
|
||||
protected function extractStripeChargeId(array $intent): ?string
|
||||
{
|
||||
$charges = $intent['charges']['data'] ?? null;
|
||||
@@ -276,4 +474,3 @@ class CheckoutWebhookService
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
23
app/Services/Paddle/Exceptions/PaddleException.php
Normal file
23
app/Services/Paddle/Exceptions/PaddleException.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Paddle\Exceptions;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class PaddleException extends RuntimeException
|
||||
{
|
||||
public function __construct(string $message, private readonly ?int $status = null, private readonly array $context = [])
|
||||
{
|
||||
parent::__construct($message, $status ?? 0);
|
||||
}
|
||||
|
||||
public function status(): ?int
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function context(): array
|
||||
{
|
||||
return $this->context;
|
||||
}
|
||||
}
|
||||
245
app/Services/Paddle/PaddleCatalogService.php
Normal file
245
app/Services/Paddle/PaddleCatalogService.php
Normal file
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Paddle;
|
||||
|
||||
use App\Models\Package;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PaddleCatalogService
|
||||
{
|
||||
public function __construct(private readonly PaddleClient $client) {}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function fetchProduct(string $productId): array
|
||||
{
|
||||
return $this->extractEntity($this->client->get("/products/{$productId}"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function fetchPrice(string $priceId): array
|
||||
{
|
||||
return $this->extractEntity($this->client->get("/prices/{$priceId}"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function createProduct(Package $package, array $overrides = []): array
|
||||
{
|
||||
$payload = $this->buildProductPayload($package, $overrides);
|
||||
|
||||
return $this->extractEntity($this->client->post('/products', $payload));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function updateProduct(string $productId, Package $package, array $overrides = []): array
|
||||
{
|
||||
$payload = $this->buildProductPayload($package, $overrides);
|
||||
|
||||
return $this->extractEntity($this->client->patch("/products/{$productId}", $payload));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function createPrice(Package $package, string $productId, array $overrides = []): array
|
||||
{
|
||||
$payload = $this->buildPricePayload($package, $productId, $overrides);
|
||||
|
||||
return $this->extractEntity($this->client->post('/prices', $payload));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function updatePrice(string $priceId, Package $package, array $overrides = []): array
|
||||
{
|
||||
$payload = $this->buildPricePayload($package, $overrides['product_id'] ?? $package->paddle_product_id, $overrides);
|
||||
|
||||
return $this->extractEntity($this->client->patch("/prices/{$priceId}", $payload));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function buildProductPayload(Package $package, array $overrides = []): array
|
||||
{
|
||||
$payload = array_merge([
|
||||
'name' => $this->resolveName($package, $overrides),
|
||||
'description' => $this->resolveDescription($package, $overrides),
|
||||
'tax_category' => $overrides['tax_category'] ?? 'standard',
|
||||
'type' => $overrides['type'] ?? 'standard',
|
||||
'custom_data' => $this->buildCustomData($package, $overrides['custom_data'] ?? []),
|
||||
], Arr::except($overrides, ['tax_category', 'type', 'custom_data']));
|
||||
|
||||
return $this->cleanPayload($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function buildPricePayload(Package $package, string $productId, array $overrides = []): array
|
||||
{
|
||||
$unitPrice = $overrides['unit_price'] ?? [
|
||||
'amount' => (string) $this->priceToMinorUnits($package->price),
|
||||
'currency_code' => Str::upper((string) ($package->currency ?? 'EUR')),
|
||||
];
|
||||
|
||||
$payload = array_merge([
|
||||
'product_id' => $productId,
|
||||
'description' => $this->resolvePriceDescription($package, $overrides),
|
||||
'unit_price' => $unitPrice,
|
||||
'custom_data' => $this->buildCustomData($package, $overrides['custom_data'] ?? []),
|
||||
], Arr::except($overrides, ['unit_price', 'description', 'custom_data']));
|
||||
|
||||
return $this->cleanPayload($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $response
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function extractEntity(array $response): array
|
||||
{
|
||||
return Arr::get($response, 'data', $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function cleanPayload(array $payload): array
|
||||
{
|
||||
$filtered = collect($payload)
|
||||
->reject(static fn ($value) => $value === null || $value === '' || $value === [])
|
||||
->all();
|
||||
|
||||
if (array_key_exists('custom_data', $filtered)) {
|
||||
$filtered['custom_data'] = collect($filtered['custom_data'])
|
||||
->reject(static fn ($value) => $value === null || $value === '' || $value === [])
|
||||
->all();
|
||||
}
|
||||
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $extra
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function buildCustomData(Package $package, array $extra = []): array
|
||||
{
|
||||
$base = [
|
||||
'fotospiel_package_id' => (string) $package->id,
|
||||
'slug' => $package->slug,
|
||||
'type' => $package->type,
|
||||
'features' => $package->features,
|
||||
'limits' => array_filter([
|
||||
'max_photos' => $package->max_photos,
|
||||
'max_guests' => $package->max_guests,
|
||||
'gallery_days' => $package->gallery_days,
|
||||
'max_tasks' => $package->max_tasks,
|
||||
'max_events_per_year' => $package->max_events_per_year,
|
||||
], static fn ($value) => $value !== null),
|
||||
'translations' => array_filter([
|
||||
'name' => $package->name_translations,
|
||||
'description' => $package->description_translations,
|
||||
], static fn ($value) => ! empty($value)),
|
||||
];
|
||||
|
||||
return array_merge($base, $extra);
|
||||
}
|
||||
|
||||
protected function resolveName(Package $package, array $overrides): string
|
||||
{
|
||||
if (isset($overrides['name']) && is_string($overrides['name'])) {
|
||||
return $overrides['name'];
|
||||
}
|
||||
|
||||
if (! empty($package->name)) {
|
||||
return $package->name;
|
||||
}
|
||||
|
||||
$translations = $package->name_translations ?? [];
|
||||
|
||||
return $translations['en'] ?? $translations['de'] ?? $package->slug;
|
||||
}
|
||||
|
||||
protected function resolveDescription(Package $package, array $overrides): string
|
||||
{
|
||||
if (array_key_exists('description', $overrides)) {
|
||||
$value = is_string($overrides['description']) ? trim($overrides['description']) : null;
|
||||
|
||||
if ($value !== null && $value !== '') {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($package->description)) {
|
||||
return strip_tags((string) $package->description);
|
||||
}
|
||||
|
||||
$translations = $package->description_translations ?? [];
|
||||
|
||||
$fallback = $translations['en'] ?? $translations['de'] ?? null;
|
||||
|
||||
if ($fallback !== null) {
|
||||
$fallback = trim(strip_tags((string) $fallback));
|
||||
if ($fallback !== '') {
|
||||
return $fallback;
|
||||
}
|
||||
}
|
||||
|
||||
return sprintf('Fotospiel package %s', $package->slug ?? $package->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $overrides
|
||||
*/
|
||||
protected function resolvePriceDescription(Package $package, array $overrides): string
|
||||
{
|
||||
if (array_key_exists('description', $overrides)) {
|
||||
$value = is_string($overrides['description']) ? trim($overrides['description']) : null;
|
||||
|
||||
if ($value !== null && $value !== '') {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($package->description)) {
|
||||
return strip_tags((string) $package->description);
|
||||
}
|
||||
|
||||
$translations = $package->description_translations ?? [];
|
||||
$fallback = $translations['en'] ?? $translations['de'] ?? null;
|
||||
|
||||
if ($fallback !== null) {
|
||||
$fallback = trim(strip_tags((string) $fallback));
|
||||
if ($fallback !== '') {
|
||||
return $fallback;
|
||||
}
|
||||
}
|
||||
|
||||
$name = $package->name ?? $package->getNameForLocale('en');
|
||||
|
||||
if ($name) {
|
||||
return sprintf('%s package', trim($name));
|
||||
}
|
||||
|
||||
return sprintf('Package %s', $package->slug ?? $package->id);
|
||||
}
|
||||
|
||||
protected function priceToMinorUnits(mixed $price): int
|
||||
{
|
||||
$value = is_string($price) ? (float) $price : (float) ($price ?? 0);
|
||||
|
||||
return (int) round($value * 100);
|
||||
}
|
||||
}
|
||||
84
app/Services/Paddle/PaddleCheckoutService.php
Normal file
84
app/Services/Paddle/PaddleCheckoutService.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Paddle;
|
||||
|
||||
use App\Models\Package;
|
||||
use App\Models\Tenant;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PaddleCheckoutService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PaddleClient $client,
|
||||
private readonly PaddleCustomerService $customers,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array{success_url?: string|null, return_url?: string|null} $options
|
||||
*/
|
||||
public function createCheckout(Tenant $tenant, Package $package, array $options = []): array
|
||||
{
|
||||
$customerId = $this->customers->ensureCustomerId($tenant);
|
||||
|
||||
$successUrl = $options['success_url'] ?? route('marketing.success', ['packageId' => $package->id]);
|
||||
$returnUrl = $options['return_url'] ?? route('packages', ['highlight' => $package->slug]);
|
||||
|
||||
$metadata = $this->buildMetadata($tenant, $package, $options['metadata'] ?? []);
|
||||
|
||||
$payload = [
|
||||
'customer_id' => $customerId,
|
||||
'items' => [
|
||||
[
|
||||
'price_id' => $package->paddle_price_id,
|
||||
'quantity' => 1,
|
||||
],
|
||||
],
|
||||
'metadata' => $metadata,
|
||||
'success_url' => $successUrl,
|
||||
'cancel_url' => $returnUrl,
|
||||
];
|
||||
|
||||
if ($tenant->contact_email) {
|
||||
$payload['customer_email'] = $tenant->contact_email;
|
||||
}
|
||||
|
||||
$response = $this->client->post('/checkout/links', $payload);
|
||||
|
||||
$checkoutUrl = Arr::get($response, 'data.url') ?? Arr::get($response, 'url');
|
||||
|
||||
if (! $checkoutUrl) {
|
||||
Log::warning('Paddle checkout response missing url', ['response' => $response]);
|
||||
}
|
||||
|
||||
return [
|
||||
'checkout_url' => $checkoutUrl,
|
||||
'expires_at' => Arr::get($response, 'data.expires_at') ?? Arr::get($response, 'expires_at'),
|
||||
'id' => Arr::get($response, 'data.id') ?? Arr::get($response, 'id'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $extra
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function buildMetadata(Tenant $tenant, Package $package, array $extra = []): array
|
||||
{
|
||||
$metadata = [
|
||||
'tenant_id' => (string) $tenant->id,
|
||||
'package_id' => (string) $package->id,
|
||||
];
|
||||
|
||||
foreach ($extra as $key => $value) {
|
||||
if (! is_string($key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_scalar($value) || (is_object($value) && method_exists($value, '__toString'))) {
|
||||
$metadata[$key] = (string) $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
}
|
||||
82
app/Services/Paddle/PaddleClient.php
Normal file
82
app/Services/Paddle/PaddleClient.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Paddle;
|
||||
|
||||
use App\Services\Paddle\Exceptions\PaddleException;
|
||||
use Illuminate\Http\Client\Factory as HttpFactory;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PaddleClient
|
||||
{
|
||||
public function __construct(
|
||||
private readonly HttpFactory $http,
|
||||
) {}
|
||||
|
||||
public function get(string $endpoint, array $query = []): array
|
||||
{
|
||||
return $this->send('GET', $endpoint, ['query' => $query]);
|
||||
}
|
||||
|
||||
public function post(string $endpoint, array $payload = []): array
|
||||
{
|
||||
return $this->send('POST', $endpoint, ['json' => $payload]);
|
||||
}
|
||||
|
||||
public function patch(string $endpoint, array $payload = []): array
|
||||
{
|
||||
return $this->send('PATCH', $endpoint, ['json' => $payload]);
|
||||
}
|
||||
|
||||
public function delete(string $endpoint, array $payload = []): array
|
||||
{
|
||||
return $this->send('DELETE', $endpoint, ['json' => $payload]);
|
||||
}
|
||||
|
||||
protected function send(string $method, string $endpoint, array $options = []): array
|
||||
{
|
||||
$request = $this->preparedRequest();
|
||||
|
||||
try {
|
||||
$response = $request->send(strtoupper($method), ltrim($endpoint, '/'), $options);
|
||||
} catch (RequestException $exception) {
|
||||
throw new PaddleException($exception->getMessage(), $exception->response?->status(), $exception->response?->json() ?? []);
|
||||
}
|
||||
|
||||
if ($response->failed()) {
|
||||
$body = $response->json() ?? [];
|
||||
$message = Arr::get($body, 'error.message')
|
||||
?? Arr::get($body, 'message')
|
||||
?? sprintf('Paddle request failed with status %s', $response->status());
|
||||
|
||||
throw new PaddleException($message, $response->status(), $body);
|
||||
}
|
||||
|
||||
return $response->json() ?? [];
|
||||
}
|
||||
|
||||
protected function preparedRequest(): PendingRequest
|
||||
{
|
||||
$apiKey = config('paddle.api_key');
|
||||
if (! $apiKey) {
|
||||
throw new PaddleException('Paddle API key is not configured.');
|
||||
}
|
||||
|
||||
$baseUrl = rtrim((string) config('paddle.base_url'), '/');
|
||||
$environment = (string) config('paddle.environment', 'production');
|
||||
|
||||
$headers = [
|
||||
'User-Agent' => sprintf('FotospielApp/%s PaddleClient', app()->version()),
|
||||
'Paddle-Environment' => Str::lower($environment) === 'sandbox' ? 'sandbox' : 'production',
|
||||
];
|
||||
|
||||
return $this->http
|
||||
->baseUrl($baseUrl)
|
||||
->withHeaders($headers)
|
||||
->withToken($apiKey)
|
||||
->acceptJson()
|
||||
->asJson();
|
||||
}
|
||||
}
|
||||
41
app/Services/Paddle/PaddleCustomerService.php
Normal file
41
app/Services/Paddle/PaddleCustomerService.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Paddle;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Services\Paddle\Exceptions\PaddleException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PaddleCustomerService
|
||||
{
|
||||
public function __construct(private readonly PaddleClient $client) {}
|
||||
|
||||
public function ensureCustomerId(Tenant $tenant): string
|
||||
{
|
||||
if ($tenant->paddle_customer_id) {
|
||||
return $tenant->paddle_customer_id;
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'email' => $tenant->contact_email ?: ($tenant->user?->email ?? null),
|
||||
'name' => $tenant->name,
|
||||
];
|
||||
|
||||
if (! $payload['email']) {
|
||||
throw new PaddleException('Tenant email address required to create Paddle customer.');
|
||||
}
|
||||
|
||||
$response = $this->client->post('/customers', $payload);
|
||||
$customerId = Arr::get($response, 'data.id') ?? Arr::get($response, 'id');
|
||||
|
||||
if (! $customerId) {
|
||||
Log::error('Paddle customer creation returned no id', ['tenant' => $tenant->id, 'response' => $response]);
|
||||
throw new PaddleException('Failed to create Paddle customer.');
|
||||
}
|
||||
|
||||
$tenant->forceFill(['paddle_customer_id' => $customerId])->save();
|
||||
|
||||
return $customerId;
|
||||
}
|
||||
}
|
||||
33
app/Services/Paddle/PaddleSubscriptionService.php
Normal file
33
app/Services/Paddle/PaddleSubscriptionService.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Paddle;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class PaddleSubscriptionService
|
||||
{
|
||||
public function __construct(private readonly PaddleClient $client) {}
|
||||
|
||||
/**
|
||||
* Retrieve a subscription record directly from Paddle.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function retrieve(string $subscriptionId): array
|
||||
{
|
||||
$response = $this->client->get("/subscriptions/{$subscriptionId}");
|
||||
|
||||
return is_array($response) ? $response : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience helper to extract metadata from the subscription response.
|
||||
*
|
||||
* @param array<string, mixed> $subscription
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function metadata(array $subscription): array
|
||||
{
|
||||
return Arr::get($subscription, 'data.metadata', []);
|
||||
}
|
||||
}
|
||||
92
app/Services/Paddle/PaddleTransactionService.php
Normal file
92
app/Services/Paddle/PaddleTransactionService.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Paddle;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class PaddleTransactionService
|
||||
{
|
||||
public function __construct(private readonly PaddleClient $client) {}
|
||||
|
||||
/**
|
||||
* @return array{data: array<int, array<string, mixed>>, meta: array<string, mixed>}
|
||||
*/
|
||||
public function listForCustomer(string $customerId, array $query = []): array
|
||||
{
|
||||
$payload = array_filter(array_merge([
|
||||
'customer_id' => $customerId,
|
||||
'order_by' => '-created_at',
|
||||
], $query), static fn ($value) => $value !== null && $value !== '');
|
||||
|
||||
$response = $this->client->get('/transactions', $payload);
|
||||
|
||||
$transactions = Arr::get($response, 'data', []);
|
||||
$meta = Arr::get($response, 'meta.pagination', []);
|
||||
|
||||
if (! is_array($transactions)) {
|
||||
$transactions = [];
|
||||
}
|
||||
|
||||
return [
|
||||
'data' => array_map([$this, 'mapTransaction'], $transactions),
|
||||
'meta' => $this->mapPagination($meta),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $transaction
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function mapTransaction(array $transaction): array
|
||||
{
|
||||
$totals = Arr::get($transaction, 'totals', []);
|
||||
|
||||
return [
|
||||
'id' => $transaction['id'] ?? null,
|
||||
'status' => $transaction['status'] ?? null,
|
||||
'amount' => $this->resolveAmount($transaction, $totals),
|
||||
'currency' => $transaction['currency_code'] ?? Arr::get($transaction, 'currency') ?? 'EUR',
|
||||
'origin' => $transaction['origin'] ?? null,
|
||||
'checkout_id' => $transaction['checkout_id'] ?? Arr::get($transaction, 'details.checkout_id'),
|
||||
'created_at' => $transaction['created_at'] ?? null,
|
||||
'updated_at' => $transaction['updated_at'] ?? null,
|
||||
'receipt_url' => Arr::get($transaction, 'invoice_url') ?? Arr::get($transaction, 'receipt_url'),
|
||||
'tax' => Arr::get($totals, 'tax_total') ?? null,
|
||||
'grand_total' => Arr::get($totals, 'grand_total') ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $transaction
|
||||
* @param array<string, mixed>|null $totals
|
||||
*/
|
||||
protected function resolveAmount(array $transaction, $totals): ?float
|
||||
{
|
||||
$amount = Arr::get($totals ?? [], 'subtotal') ?? Arr::get($totals ?? [], 'grand_total');
|
||||
|
||||
if ($amount !== null) {
|
||||
return (float) $amount;
|
||||
}
|
||||
|
||||
$raw = $transaction['amount'] ?? null;
|
||||
|
||||
if ($raw === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (float) $raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $pagination
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function mapPagination(array $pagination): array
|
||||
{
|
||||
return [
|
||||
'next' => $pagination['next'] ?? null,
|
||||
'previous' => $pagination['previous'] ?? null,
|
||||
'has_more' => (bool) ($pagination['has_more'] ?? false),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\PayPal;
|
||||
|
||||
use PaypalServerSdkLib\PaypalServerSdkClient;
|
||||
use PaypalServerSdkLib\PaypalServerSdkClientBuilder;
|
||||
use PaypalServerSdkLib\Authentication\ClientCredentialsAuthCredentialsBuilder;
|
||||
use PaypalServerSdkLib\Environment;
|
||||
|
||||
class PaypalClientFactory
|
||||
{
|
||||
public function make(?bool $sandbox = null, ?string $clientId = null, ?string $clientSecret = null): PaypalServerSdkClient
|
||||
{
|
||||
$clientId = $clientId ?? config('services.paypal.client_id');
|
||||
$clientSecret = $clientSecret ?? config('services.paypal.secret');
|
||||
$isSandbox = $sandbox ?? config('services.paypal.sandbox', true);
|
||||
|
||||
$environment = $isSandbox ? Environment::SANDBOX : Environment::PRODUCTION;
|
||||
|
||||
return PaypalServerSdkClientBuilder::init()
|
||||
->clientCredentialsAuthCredentials(
|
||||
ClientCredentialsAuthCredentialsBuilder::init($clientId, $clientSecret)
|
||||
)
|
||||
->environment($environment)
|
||||
->build();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user