php-commerce/Services/PaymentMethodService.php
Snider 8f27fe85c3 refactor: update Tenant module imports after namespace migration
Updates all references from Core\Mod\Tenant to Core\Tenant following
the monorepo separation. The Tenant module now lives in its own package
with the simplified namespace.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 17:39:12 +00:00

335 lines
11 KiB
PHP

<?php
namespace Core\Mod\Commerce\Services;
use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Core\Mod\Commerce\Models\PaymentMethod;
use Core\Mod\Commerce\Services\PaymentGateway\StripeGateway;
/**
* Service for managing payment methods.
*
* Handles adding, removing, and managing saved payment methods
* for workspaces with full gateway integration.
*/
class PaymentMethodService
{
public function __construct(
protected StripeGateway $stripeGateway,
) {}
/**
* Get all active payment methods for a workspace.
*/
public function getPaymentMethods(Workspace $workspace): Collection
{
return $workspace->paymentMethods()
->where('is_active', true)
->orderByDesc('is_default')
->orderByDesc('created_at')
->get();
}
/**
* Get the default payment method for a workspace.
*/
public function getDefaultPaymentMethod(Workspace $workspace): ?PaymentMethod
{
return $workspace->paymentMethods()
->where('is_active', true)
->where('is_default', true)
->first();
}
/**
* Add a new payment method to a workspace.
*
* @param string $gatewayPaymentMethodId The payment method ID from the gateway (e.g., pm_xxx for Stripe)
*/
public function addPaymentMethod(
Workspace $workspace,
string $gatewayPaymentMethodId,
?User $user = null,
string $gateway = 'stripe'
): PaymentMethod {
return DB::transaction(function () use ($workspace, $gatewayPaymentMethodId, $user, $gateway) {
// For Stripe, attach and get details from the gateway
if ($gateway === 'stripe') {
return $this->addStripePaymentMethod($workspace, $gatewayPaymentMethodId, $user);
}
throw new \InvalidArgumentException("Unsupported payment gateway: {$gateway}");
});
}
/**
* Add a Stripe payment method.
*/
protected function addStripePaymentMethod(
Workspace $workspace,
string $gatewayPaymentMethodId,
?User $user = null
): PaymentMethod {
// Check if payment method already exists
$existing = PaymentMethod::where('workspace_id', $workspace->id)
->where('gateway', 'stripe')
->where('gateway_payment_method_id', $gatewayPaymentMethodId)
->first();
if ($existing) {
// Reactivate if it was deactivated
if (! $existing->is_active) {
$existing->update(['is_active' => true]);
}
return $existing;
}
// Attach to Stripe customer
$paymentMethod = $this->stripeGateway->attachPaymentMethod($workspace, $gatewayPaymentMethodId);
// Update with user info
if ($user) {
$paymentMethod->update(['user_id' => $user->id]);
}
// If this is the first payment method, make it the default
$hasOtherMethods = $workspace->paymentMethods()
->where('is_active', true)
->where('id', '!=', $paymentMethod->id)
->exists();
if (! $hasOtherMethods) {
$this->setDefaultPaymentMethod($workspace, $paymentMethod);
}
Log::info('Payment method added', [
'workspace_id' => $workspace->id,
'payment_method_id' => $paymentMethod->id,
'type' => $paymentMethod->type,
'brand' => $paymentMethod->brand,
]);
return $paymentMethod;
}
/**
* Remove a payment method from a workspace.
*
* @throws \RuntimeException If the payment method cannot be removed
*/
public function removePaymentMethod(Workspace $workspace, PaymentMethod $paymentMethod): void
{
// Verify ownership
if ($paymentMethod->workspace_id !== $workspace->id) {
throw new \RuntimeException('Payment method does not belong to this workspace.');
}
// Check if this is the last active payment method
$activeCount = $workspace->paymentMethods()
->where('is_active', true)
->count();
if ($activeCount === 1) {
// Check for active subscriptions
$hasActiveSubscription = $workspace->subscriptions()
->active()
->exists();
if ($hasActiveSubscription) {
throw new \RuntimeException(
'Cannot remove the last payment method while you have an active subscription.'
);
}
}
DB::transaction(function () use ($workspace, $paymentMethod) {
// Detach from gateway (Stripe)
if ($paymentMethod->gateway === 'stripe' && $paymentMethod->gateway_payment_method_id) {
try {
$this->stripeGateway->detachPaymentMethod($paymentMethod);
} catch (\Exception $e) {
Log::warning('Failed to detach payment method from Stripe', [
'payment_method_id' => $paymentMethod->id,
'error' => $e->getMessage(),
]);
// Continue with local removal even if gateway fails
}
}
// If this was the default, make another one the default
if ($paymentMethod->is_default) {
$newDefault = $workspace->paymentMethods()
->where('is_active', true)
->where('id', '!=', $paymentMethod->id)
->first();
if ($newDefault) {
$this->setDefaultPaymentMethod($workspace, $newDefault);
}
}
// Soft-delete by marking as inactive
$paymentMethod->update(['is_active' => false]);
Log::info('Payment method removed', [
'workspace_id' => $workspace->id,
'payment_method_id' => $paymentMethod->id,
]);
});
}
/**
* Set a payment method as the default for a workspace.
*/
public function setDefaultPaymentMethod(Workspace $workspace, PaymentMethod $paymentMethod): void
{
// Verify ownership
if ($paymentMethod->workspace_id !== $workspace->id) {
throw new \RuntimeException('Payment method does not belong to this workspace.');
}
DB::transaction(function () use ($workspace, $paymentMethod) {
// Update gateway default (for Stripe)
if ($paymentMethod->gateway === 'stripe') {
try {
$this->stripeGateway->setDefaultPaymentMethod($paymentMethod);
} catch (\Exception $e) {
Log::warning('Failed to set default payment method in Stripe', [
'payment_method_id' => $paymentMethod->id,
'error' => $e->getMessage(),
]);
// Continue with local update even if gateway fails
}
}
// Remove default from all other methods
$workspace->paymentMethods()
->where('id', '!=', $paymentMethod->id)
->update(['is_default' => false]);
// Set this one as default
$paymentMethod->update(['is_default' => true]);
Log::info('Default payment method updated', [
'workspace_id' => $workspace->id,
'payment_method_id' => $paymentMethod->id,
]);
});
}
/**
* Sync payment methods from Stripe to local database.
*
* This is useful when payment methods are added via Stripe's
* hosted checkout or customer portal.
*/
public function syncPaymentMethodsFromStripe(Workspace $workspace): Collection
{
if (! $workspace->stripe_customer_id) {
return collect();
}
// This would need the Stripe SDK to list payment methods
// For now, we rely on webhooks to keep data in sync
Log::info('Payment method sync requested', [
'workspace_id' => $workspace->id,
'stripe_customer_id' => $workspace->stripe_customer_id,
]);
return $this->getPaymentMethods($workspace);
}
/**
* Check if a payment method is expiring soon.
*/
public function isExpiringSoon(PaymentMethod $paymentMethod, int $monthsThreshold = 2): bool
{
if (! $paymentMethod->exp_month || ! $paymentMethod->exp_year) {
return false;
}
$expiry = \Carbon\Carbon::createFromDate(
$paymentMethod->exp_year,
$paymentMethod->exp_month
)->endOfMonth();
return $expiry->isBefore(now()->addMonths($monthsThreshold));
}
/**
* Get all workspaces with expiring payment methods.
*
* Useful for sending expiry warning notifications.
*/
public function getExpiringPaymentMethods(int $monthsThreshold = 2): Collection
{
$thresholdDate = now()->addMonths($monthsThreshold);
return PaymentMethod::query()
->where('is_active', true)
->where('is_default', true)
->where('type', 'card')
->whereNotNull('exp_month')
->whereNotNull('exp_year')
->whereRaw("STR_TO_DATE(CONCAT(exp_year, '-', exp_month, '-01'), '%Y-%m-%d') <= ?", [
$thresholdDate->format('Y-m-d'),
])
->with('workspace')
->get();
}
/**
* Update payment method details from gateway.
*
* Called when card details are updated (e.g., new expiry date from card networks).
*/
public function updateFromGateway(PaymentMethod $paymentMethod, array $gatewayData): PaymentMethod
{
$updates = [];
if (isset($gatewayData['card'])) {
$card = $gatewayData['card'];
$updates['brand'] = $card['brand'] ?? $paymentMethod->brand;
$updates['last_four'] = $card['last4'] ?? $paymentMethod->last_four;
$updates['exp_month'] = $card['exp_month'] ?? $paymentMethod->exp_month;
$updates['exp_year'] = $card['exp_year'] ?? $paymentMethod->exp_year;
}
if (! empty($updates)) {
$paymentMethod->update($updates);
}
return $paymentMethod->fresh();
}
/**
* Create a setup session for adding a payment method.
*
* Returns the URL to redirect the user to Stripe's hosted setup page.
*/
public function createSetupSession(Workspace $workspace, string $returnUrl): array
{
if (! $this->stripeGateway->isEnabled()) {
throw new \RuntimeException('Stripe payments are not currently available.');
}
return $this->stripeGateway->createSetupSession($workspace, $returnUrl);
}
/**
* Get the billing portal URL for full payment management.
*/
public function getBillingPortalUrl(Workspace $workspace, string $returnUrl): ?string
{
if (! $this->stripeGateway->isEnabled()) {
return null;
}
return $this->stripeGateway->getPortalUrl($workspace, $returnUrl);
}
}