feat: implement comprehensive multi-provider payment processing system
- Add unified payment provider architecture with contract-based design - Implement 6 payment providers: Stripe, Lemon Squeezy, Polar, Oxapay, Crypto, Activation Keys - Create subscription management with lifecycle handling (create, cancel, pause, resume, update) - Add coupon system with usage tracking and trial extensions - Build Filament admin resources for payment providers, subscriptions, coupons, and trials - Implement payment orchestration service with provider registry and configuration management - Add comprehensive payment logging and webhook handling for all providers - Create customer analytics dashboard with revenue, churn, and lifetime value metrics - Add subscription migration service for provider switching - Include extensive test coverage for all payment functionality
This commit is contained in:
304
app/Models/PaymentEvent.php
Normal file
304
app/Models/PaymentEvent.php
Normal file
@@ -0,0 +1,304 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PaymentEvent extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'event_type',
|
||||
'level',
|
||||
'data',
|
||||
'user_type',
|
||||
'user_id',
|
||||
'request_id',
|
||||
'ip_address',
|
||||
'user_agent',
|
||||
'expires_at',
|
||||
'provider',
|
||||
'webhook_event_type',
|
||||
'payload',
|
||||
'success',
|
||||
'stored_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'data' => 'array',
|
||||
'expires_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Scope to get only security events
|
||||
*/
|
||||
public function scopeSecurity(Builder $query): Builder
|
||||
{
|
||||
return $query->where('event_type', 'like', 'security_%');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get only compliance events
|
||||
*/
|
||||
public function scopeCompliance(Builder $query): Builder
|
||||
{
|
||||
return $query->where('event_type', 'like', 'compliance_%');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get only webhook events
|
||||
*/
|
||||
public function scopeWebhooks(Builder $query): Builder
|
||||
{
|
||||
return $query->where('event_type', 'like', 'webhook_%');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get only error events
|
||||
*/
|
||||
public function scopeErrors(Builder $query): Builder
|
||||
{
|
||||
return $query->where('level', 'error');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get events for a specific provider
|
||||
*/
|
||||
public function scopeForProvider(Builder $query, string $provider): Builder
|
||||
{
|
||||
return $query->whereJsonContains('data->provider', $provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get events for a specific subscription
|
||||
*/
|
||||
public function scopeForSubscription(Builder $query, int $subscriptionId): Builder
|
||||
{
|
||||
return $query->whereJsonContains('data->subscription_id', $subscriptionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get events that require review
|
||||
*/
|
||||
public function scopeRequiresReview(Builder $query): Builder
|
||||
{
|
||||
return $query->whereJsonContains('data->requires_review', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get events that haven't expired
|
||||
*/
|
||||
public function scopeNotExpired(Builder $query): Builder
|
||||
{
|
||||
return $query->where(function ($q) {
|
||||
$q->whereNull('expires_at')
|
||||
->orWhere('expires_at', '>', now());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get expired events (for cleanup)
|
||||
*/
|
||||
public function scopeExpired(Builder $query): Builder
|
||||
{
|
||||
return $query->where('expires_at', '<', now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user relationship
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if event is security-related
|
||||
*/
|
||||
public function isSecurityEvent(): bool
|
||||
{
|
||||
return str_starts_with($this->event_type, 'security_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if event is compliance-related
|
||||
*/
|
||||
public function isComplianceEvent(): bool
|
||||
{
|
||||
return str_starts_with($this->event_type, 'compliance_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if event is webhook-related
|
||||
*/
|
||||
public function isWebhookEvent(): bool
|
||||
{
|
||||
return str_starts_with($this->event_type, 'webhook_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if event requires review
|
||||
*/
|
||||
public function requiresReview(): bool
|
||||
{
|
||||
return ($this->data['requires_review'] ?? false) ||
|
||||
$this->isSecurityEvent() ||
|
||||
$this->level === 'error';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the provider from event data
|
||||
*/
|
||||
public function getProvider(): ?string
|
||||
{
|
||||
return $this->data['provider'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the subscription ID from event data
|
||||
*/
|
||||
public function getSubscriptionId(): ?int
|
||||
{
|
||||
return $this->data['subscription_id'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the action from event data
|
||||
*/
|
||||
public function getAction(): ?string
|
||||
{
|
||||
return $this->data['action'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if event contains sensitive data
|
||||
*/
|
||||
public function containsSensitiveData(): bool
|
||||
{
|
||||
$sensitiveKeys = ['payment_method', 'card_number', 'bank_account', 'ssn', 'full_credit_card'];
|
||||
|
||||
foreach ($sensitiveKeys as $key) {
|
||||
if (isset($this->data[$key])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sanitized data for display (removes sensitive information)
|
||||
*/
|
||||
public function getSanitizedData(): array
|
||||
{
|
||||
$data = $this->data;
|
||||
|
||||
// Remove or mask sensitive fields
|
||||
$sensitivePatterns = [
|
||||
'/payment_method.*?number/i',
|
||||
'/card_?number/i',
|
||||
'/cvv/i',
|
||||
'/cvc/i',
|
||||
'/ssn/i',
|
||||
'/bank_?account/i',
|
||||
'/routing_?number/i',
|
||||
];
|
||||
|
||||
foreach ($sensitivePatterns as $pattern) {
|
||||
$data = array_map(function ($value) use ($pattern) {
|
||||
if (is_string($value) && preg_match($pattern, $value)) {
|
||||
return str_repeat('*', strlen($value) - 4).substr($value, -4);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}, $data);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export event for compliance reporting
|
||||
*/
|
||||
public function toComplianceArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'event_type' => $this->event_type,
|
||||
'level' => $this->level,
|
||||
'created_at' => $this->created_at->toISOString(),
|
||||
'user_id' => $this->user_id,
|
||||
'user_type' => $this->user_type,
|
||||
'request_id' => $this->request_id,
|
||||
'ip_address' => $this->ip_address,
|
||||
'provider' => $this->getProvider(),
|
||||
'subscription_id' => $this->getSubscriptionId(),
|
||||
'action' => $this->getAction(),
|
||||
'requires_review' => $this->requiresReview(),
|
||||
'is_security_event' => $this->isSecurityEvent(),
|
||||
'is_compliance_event' => $this->isComplianceEvent(),
|
||||
'contains_sensitive_data' => $this->containsSensitiveData(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old events based on retention policy
|
||||
*/
|
||||
public static function cleanup(): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
// Clean up expired webhook payloads
|
||||
$webhookCleanup = static::webhooks()
|
||||
->expired()
|
||||
->delete();
|
||||
|
||||
$results['webhook_payloads'] = $webhookCleanup;
|
||||
|
||||
// Clean up old debug events
|
||||
$debugCleanup = static::where('level', 'debug')
|
||||
->where('created_at', '<', now()->subDays(90))
|
||||
->delete();
|
||||
|
||||
$results['debug_events'] = $debugCleanup;
|
||||
|
||||
// Clean up old info events (keep for 1 year)
|
||||
$infoCleanup = static::where('level', 'info')
|
||||
->where('created_at', '<', now()->subYear())
|
||||
->whereNot(function ($query) {
|
||||
$query->security()
|
||||
->compliance();
|
||||
})
|
||||
->delete();
|
||||
|
||||
$results['info_events'] = $infoCleanup;
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events by date range for reporting
|
||||
*/
|
||||
public static function getByDateRange(\DateTime $start, \DateTime $end, array $filters = []): Builder
|
||||
{
|
||||
$query = static::whereBetween('created_at', [$start, $end]);
|
||||
|
||||
if (! empty($filters['event_types'])) {
|
||||
$query->whereIn('event_type', $filters['event_types']);
|
||||
}
|
||||
|
||||
if (! empty($filters['levels'])) {
|
||||
$query->whereIn('level', $filters['levels']);
|
||||
}
|
||||
|
||||
if (! empty($filters['user_id'])) {
|
||||
$query->where('user_id', $filters['user_id']);
|
||||
}
|
||||
|
||||
if (! empty($filters['provider'])) {
|
||||
$query->forProvider($filters['provider']);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user