-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Task: Implement PaymentService with subscription and payment methods
Description
Implement the core PaymentService that orchestrates payment processing, subscription management, and billing operations across multiple payment gateways. This service acts as the unified payment engine for the Coolify Enterprise platform, providing a clean abstraction over Stripe, PayPal, and other payment providers while handling the complex workflows of subscription lifecycle management, payment method storage, transaction processing, and webhook validation.
The service provides a consistent PHP interface to diverse payment gateway APIs, enabling Coolify to process payments, manage recurring subscriptions, calculate usage-based billing, and handle refunds—all through a single, well-designed service layer that abstracts provider-specific implementations.
Core Responsibilities:
- Subscription Management: Create, update, pause, resume, and cancel subscriptions with automatic billing
- Payment Processing: Process one-time payments and recurring charges with comprehensive error handling
- Payment Method Storage: Securely tokenize and store payment methods (credit cards, bank accounts, PayPal)
- Usage-Based Billing: Calculate overage charges based on resource consumption from monitoring system
- Refund Processing: Handle full and partial refunds with automatic reconciliation
- Webhook Management: Validate and process payment gateway webhooks with HMAC verification
- Invoice Generation: Create detailed invoices with line items, taxes, and payment history
- Failed Payment Recovery: Automatic retry logic with configurable schedules and dunning management
Integration Architecture:
Upstream Dependencies (Payment Gateways):
- Stripe Integration (Task 44): Credit cards, ACH, subscription billing, webhook handling
- PayPal Integration (Task 45): PayPal balance, credit cards via PayPal, alternative payment methods
- Payment Gateway Factory: Provider selection based on organization preferences or fallback logic
Downstream Consumers:
- SubscriptionManager.vue (Task 50): Frontend subscription management interface
- BillingDashboard.vue (Task 50): Usage metrics, cost breakdowns, invoice history
- OrganizationService: Subscription-driven feature activation and quota enforcement
- ResourceMonitoringJob (Task 24): Usage metrics for overage calculations
- License Validation: Subscription status affects license validity and feature access
Database Models:
- OrganizationSubscription: Subscription lifecycle, plan details, billing cycle
- PaymentMethod: Tokenized payment instruments with gateway-specific metadata
- PaymentTransaction: Immutable transaction log with full audit trail
- Invoice: Generated invoices with line items and payment status
Why This Task Is Critical:
Payment processing is the revenue engine of the enterprise platform. Without this service, organizations cannot subscribe to paid plans, limiting monetization to manual invoicing or external billing systems. This service enables:
- Self-Service Subscriptions: Organizations can upgrade, downgrade, or cancel without manual intervention
- Automated Revenue Recognition: Subscriptions create predictable recurring revenue with automatic billing
- Usage-Based Monetization: Charge for actual resource consumption, enabling pay-as-you-go pricing
- Global Payment Methods: Support multiple payment providers and currencies for international customers
- Churn Reduction: Automated payment retry and dunning management recovers failed payments automatically
The service transforms Coolify from a self-hosted platform into a monetizable SaaS business with enterprise billing capabilities, subscription tiers, and usage-based pricing—all while maintaining PCI compliance through tokenization and never storing raw payment card data.
Acceptance Criteria
- PaymentService class implements PaymentServiceInterface with all required methods
-
createSubscription()method creates subscriptions with plan selection and billing cycle -
updateSubscription()method handles plan changes with prorated billing -
pauseSubscription()andresumeSubscription()methods manage subscription lifecycle -
cancelSubscription()method cancels with immediate or end-of-period options -
processPayment()method handles one-time charges with idempotency -
refundPayment()method processes full and partial refunds with reason tracking -
addPaymentMethod()method tokenizes and stores payment instruments -
removePaymentMethod()method safely deletes with orphan subscription checks -
setDefaultPaymentMethod()updates organization's preferred payment method -
calculateUsageBilling()method computes overages from resource monitoring data -
generateInvoice()method creates invoices with line items, taxes, discounts -
retryFailedPayment()method with exponential backoff and maximum attempts - Gateway abstraction supports multiple providers (Stripe, PayPal) via factory pattern
- Webhook signature validation with HMAC verification for security
- Idempotency key support prevents duplicate payment processing
- Comprehensive error handling with gateway-specific error codes
- Transaction logging with immutable audit trail
- Integration with OrganizationService for feature activation/deactivation
- Unit tests covering all public methods with >90% coverage
- Integration tests with payment gateway mocking
Technical Details
File Paths
Service Layer:
/home/topgun/topgun/app/Services/Enterprise/PaymentService.php(implementation)/home/topgun/topgun/app/Contracts/PaymentServiceInterface.php(interface)
Gateway Implementations:
/home/topgun/topgun/app/Services/Enterprise/Gateways/StripeGateway.php(existing from Task 44)/home/topgun/topgun/app/Services/Enterprise/Gateways/PayPalGateway.php(existing from Task 45)/home/topgun/topgun/app/Services/Enterprise/Gateways/PaymentGatewayFactory.php(existing from Task 43)
Configuration:
/home/topgun/topgun/config/payment.php(payment settings)
Models:
/home/topgun/topgun/app/Models/OrganizationSubscription.php(existing from Task 42)/home/topgun/topgun/app/Models/PaymentMethod.php(existing from Task 42)/home/topgun/topgun/app/Models/PaymentTransaction.php(existing from Task 42)/home/topgun/topgun/app/Models/Invoice.php(existing from Task 42)
DTOs:
/home/topgun/topgun/app/DTOs/PaymentResult.php(new)/home/topgun/topgun/app/DTOs/SubscriptionResult.php(new)/home/topgun/topgun/app/DTOs/UsageBillingResult.php(new)
Service Interface
File: app/Contracts/PaymentServiceInterface.php
<?php
namespace App\Contracts;
use App\Models\Organization;
use App\Models\OrganizationSubscription;
use App\Models\PaymentMethod;
use App\Models\PaymentTransaction;
use App\DTOs\PaymentResult;
use App\DTOs\SubscriptionResult;
use App\DTOs\UsageBillingResult;
interface PaymentServiceInterface
{
/**
* Create a new subscription for an organization
*
* @param Organization $organization
* @param string $planId Plan identifier (e.g., 'pro-monthly')
* @param PaymentMethod|null $paymentMethod Payment method to use (null = use default)
* @param array $options Additional options (trial_days, coupon_code, etc.)
* @return SubscriptionResult
* @throws \App\Exceptions\PaymentException
*/
public function createSubscription(
Organization $organization,
string $planId,
?PaymentMethod $paymentMethod = null,
array $options = []
): SubscriptionResult;
/**
* Update an existing subscription (plan change, quantity, etc.)
*
* @param OrganizationSubscription $subscription
* @param array $updates Changes to apply (plan_id, quantity, billing_cycle, etc.)
* @param bool $prorate Whether to prorate billing (default: true)
* @return SubscriptionResult
* @throws \App\Exceptions\PaymentException
*/
public function updateSubscription(
OrganizationSubscription $subscription,
array $updates,
bool $prorate = true
): SubscriptionResult;
/**
* Pause a subscription (billing stops, features may be limited)
*
* @param OrganizationSubscription $subscription
* @param string|null $resumeAt Optional resume date (ISO 8601)
* @return bool
* @throws \App\Exceptions\PaymentException
*/
public function pauseSubscription(
OrganizationSubscription $subscription,
?string $resumeAt = null
): bool;
/**
* Resume a paused subscription
*
* @param OrganizationSubscription $subscription
* @return bool
* @throws \App\Exceptions\PaymentException
*/
public function resumeSubscription(OrganizationSubscription $subscription): bool;
/**
* Cancel a subscription
*
* @param OrganizationSubscription $subscription
* @param bool $immediately Cancel now vs. at period end (default: false)
* @param string|null $reason Optional cancellation reason
* @return bool
* @throws \App\Exceptions\PaymentException
*/
public function cancelSubscription(
OrganizationSubscription $subscription,
bool $immediately = false,
?string $reason = null
): bool;
/**
* Process a one-time payment
*
* @param Organization $organization
* @param int $amountCents Amount in cents
* @param PaymentMethod $paymentMethod
* @param array $metadata Additional metadata (description, invoice_id, etc.)
* @return PaymentResult
* @throws \App\Exceptions\PaymentException
*/
public function processPayment(
Organization $organization,
int $amountCents,
PaymentMethod $paymentMethod,
array $metadata = []
): PaymentResult;
/**
* Refund a payment (full or partial)
*
* @param PaymentTransaction $transaction
* @param int|null $amountCents Amount to refund (null = full refund)
* @param string|null $reason Refund reason
* @return PaymentResult
* @throws \App\Exceptions\PaymentException
*/
public function refundPayment(
PaymentTransaction $transaction,
?int $amountCents = null,
?string $reason = null
): PaymentResult;
/**
* Add a payment method to an organization
*
* @param Organization $organization
* @param string $gatewayToken Token from payment gateway (Stripe token, PayPal agreement, etc.)
* @param string $gateway Gateway identifier (stripe, paypal)
* @param bool $setAsDefault Set as default payment method
* @return PaymentMethod
* @throws \App\Exceptions\PaymentException
*/
public function addPaymentMethod(
Organization $organization,
string $gatewayToken,
string $gateway,
bool $setAsDefault = false
): PaymentMethod;
/**
* Remove a payment method
*
* @param PaymentMethod $paymentMethod
* @param bool $force Force removal even if active subscriptions exist
* @return bool
* @throws \App\Exceptions\PaymentException
*/
public function removePaymentMethod(
PaymentMethod $paymentMethod,
bool $force = false
): bool;
/**
* Set the default payment method for an organization
*
* @param Organization $organization
* @param PaymentMethod $paymentMethod
* @return bool
*/
public function setDefaultPaymentMethod(
Organization $organization,
PaymentMethod $paymentMethod
): bool;
/**
* Calculate usage-based billing charges
*
* @param OrganizationSubscription $subscription
* @param \Carbon\Carbon $periodStart Billing period start
* @param \Carbon\Carbon $periodEnd Billing period end
* @return UsageBillingResult
*/
public function calculateUsageBilling(
OrganizationSubscription $subscription,
\Carbon\Carbon $periodStart,
\Carbon\Carbon $periodEnd
): UsageBillingResult;
/**
* Generate an invoice for a billing period
*
* @param Organization $organization
* @param \Carbon\Carbon $periodStart
* @param \Carbon\Carbon $periodEnd
* @param array $lineItems Additional line items to include
* @return \App\Models\Invoice
*/
public function generateInvoice(
Organization $organization,
\Carbon\Carbon $periodStart,
\Carbon\Carbon $periodEnd,
array $lineItems = []
): \App\Models\Invoice;
/**
* Retry a failed payment
*
* @param PaymentTransaction $transaction
* @param PaymentMethod|null $alternativePaymentMethod Try different payment method
---
**Note:** Full task details in repository at `.claude/epics/topgun/46.md`