656 lines
23 KiB
PHP
656 lines
23 KiB
PHP
<?php
|
|
|
|
namespace Core\Commerce\Services\PaymentGateway;
|
|
|
|
use Core\Mod\Tenant\Models\Workspace;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Core\Commerce\Models\Order;
|
|
use Core\Commerce\Models\Payment;
|
|
use Core\Commerce\Models\PaymentMethod;
|
|
use Core\Commerce\Models\Refund;
|
|
use Core\Commerce\Models\Subscription;
|
|
use Stripe\StripeClient;
|
|
|
|
/**
|
|
* Stripe payment gateway implementation.
|
|
*
|
|
* Secondary gateway - implemented but not exposed to users initially.
|
|
*/
|
|
class StripeGateway implements PaymentGatewayContract
|
|
{
|
|
protected ?StripeClient $stripe = null;
|
|
|
|
protected string $webhookSecret;
|
|
|
|
public function __construct()
|
|
{
|
|
$secret = config('commerce.gateways.stripe.secret');
|
|
if ($secret) {
|
|
$this->stripe = new StripeClient($secret);
|
|
}
|
|
$this->webhookSecret = config('commerce.gateways.stripe.webhook_secret') ?? '';
|
|
}
|
|
|
|
/**
|
|
* Get the Stripe client instance.
|
|
*
|
|
* @throws \RuntimeException If Stripe is not configured.
|
|
*/
|
|
protected function getStripe(): StripeClient
|
|
{
|
|
if (! $this->stripe) {
|
|
throw new \RuntimeException('Stripe is not configured. Please set STRIPE_SECRET in your environment.');
|
|
}
|
|
|
|
return $this->stripe;
|
|
}
|
|
|
|
public function getIdentifier(): string
|
|
{
|
|
return 'stripe';
|
|
}
|
|
|
|
public function isEnabled(): bool
|
|
{
|
|
return config('commerce.gateways.stripe.enabled', false)
|
|
&& $this->stripe !== null;
|
|
}
|
|
|
|
// Customer Management
|
|
|
|
public function createCustomer(Workspace $workspace): string
|
|
{
|
|
$customer = $this->getStripe()->customers->create([
|
|
'name' => $workspace->billing_name ?? $workspace->name,
|
|
'email' => $workspace->billing_email,
|
|
'address' => [
|
|
'line1' => $workspace->billing_address_line1,
|
|
'line2' => $workspace->billing_address_line2,
|
|
'city' => $workspace->billing_city,
|
|
'state' => $workspace->billing_state,
|
|
'postal_code' => $workspace->billing_postal_code,
|
|
'country' => $workspace->billing_country,
|
|
],
|
|
'metadata' => [
|
|
'workspace_id' => $workspace->id,
|
|
'workspace_slug' => $workspace->slug,
|
|
],
|
|
]);
|
|
|
|
$workspace->update(['stripe_customer_id' => $customer->id]);
|
|
|
|
return $customer->id;
|
|
}
|
|
|
|
public function updateCustomer(Workspace $workspace): void
|
|
{
|
|
if (! $workspace->stripe_customer_id) {
|
|
return;
|
|
}
|
|
|
|
$this->getStripe()->customers->update($workspace->stripe_customer_id, [
|
|
'name' => $workspace->billing_name ?? $workspace->name,
|
|
'email' => $workspace->billing_email,
|
|
'address' => [
|
|
'line1' => $workspace->billing_address_line1,
|
|
'line2' => $workspace->billing_address_line2,
|
|
'city' => $workspace->billing_city,
|
|
'state' => $workspace->billing_state,
|
|
'postal_code' => $workspace->billing_postal_code,
|
|
'country' => $workspace->billing_country,
|
|
],
|
|
]);
|
|
}
|
|
|
|
// Checkout
|
|
|
|
public function createCheckoutSession(Order $order, string $successUrl, string $cancelUrl): array
|
|
{
|
|
try {
|
|
$lineItems = $this->buildLineItems($order);
|
|
|
|
// Ensure customer exists
|
|
$customerId = $order->workspace->stripe_customer_id;
|
|
if (! $customerId) {
|
|
$customerId = $this->createCustomer($order->workspace);
|
|
}
|
|
|
|
$sessionParams = [
|
|
'customer' => $customerId,
|
|
'line_items' => $lineItems,
|
|
'mode' => $this->hasRecurringItems($order) ? 'subscription' : 'payment',
|
|
'success_url' => $successUrl.'?session_id={CHECKOUT_SESSION_ID}',
|
|
'cancel_url' => $cancelUrl,
|
|
'metadata' => [
|
|
'order_id' => $order->id,
|
|
'order_number' => $order->order_number,
|
|
'workspace_id' => $order->workspace_id,
|
|
],
|
|
'automatic_tax' => ['enabled' => false], // We handle tax ourselves
|
|
'allow_promotion_codes' => false, // We handle coupons ourselves
|
|
];
|
|
|
|
// Add discount if applicable
|
|
if ($order->discount_amount > 0 && $order->coupon) {
|
|
$sessionParams['discounts'] = [['coupon' => $this->createOrderCoupon($order)]];
|
|
}
|
|
|
|
$session = $this->getStripe()->checkout->sessions->create($sessionParams);
|
|
|
|
$order->update(['gateway_session_id' => $session->id]);
|
|
|
|
return [
|
|
'session_id' => $session->id,
|
|
'checkout_url' => $session->url,
|
|
];
|
|
} catch (\Stripe\Exception\CardException $e) {
|
|
Log::warning('Stripe checkout failed: card error', [
|
|
'order_id' => $order->id,
|
|
'error' => $e->getMessage(),
|
|
'code' => $e->getStripeCode(),
|
|
]);
|
|
throw new \RuntimeException('Payment card error: '.$e->getMessage(), 0, $e);
|
|
} catch (\Stripe\Exception\RateLimitException $e) {
|
|
Log::error('Stripe checkout failed: rate limit', [
|
|
'order_id' => $order->id,
|
|
]);
|
|
throw new \RuntimeException('Payment service temporarily unavailable. Please try again.', 0, $e);
|
|
} catch (\Stripe\Exception\InvalidRequestException $e) {
|
|
Log::error('Stripe checkout failed: invalid request', [
|
|
'order_id' => $order->id,
|
|
'error' => $e->getMessage(),
|
|
'param' => $e->getStripeParam(),
|
|
]);
|
|
throw new \RuntimeException('Unable to create checkout session. Please contact support.', 0, $e);
|
|
} catch (\Stripe\Exception\AuthenticationException $e) {
|
|
Log::critical('Stripe authentication failed - check API keys', [
|
|
'order_id' => $order->id,
|
|
]);
|
|
throw new \RuntimeException('Payment service configuration error. Please contact support.', 0, $e);
|
|
} catch (\Stripe\Exception\ApiConnectionException $e) {
|
|
Log::error('Stripe checkout failed: connection error', [
|
|
'order_id' => $order->id,
|
|
]);
|
|
throw new \RuntimeException('Unable to connect to payment service. Please try again.', 0, $e);
|
|
} catch (\Stripe\Exception\ApiErrorException $e) {
|
|
Log::error('Stripe checkout failed: API error', [
|
|
'order_id' => $order->id,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
throw new \RuntimeException('Payment service error. Please try again or contact support.', 0, $e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build line items array for Stripe checkout session.
|
|
*/
|
|
protected function buildLineItems(Order $order): array
|
|
{
|
|
$lineItems = [];
|
|
|
|
foreach ($order->items as $item) {
|
|
$lineItem = [
|
|
'price_data' => [
|
|
'currency' => strtolower($order->currency),
|
|
'product_data' => [
|
|
'name' => $item->name,
|
|
],
|
|
'unit_amount' => (int) round($item->unit_price * 100),
|
|
],
|
|
'quantity' => $item->quantity,
|
|
];
|
|
|
|
// Only add description if present (Stripe rejects empty strings)
|
|
if (! empty($item->description)) {
|
|
$lineItem['price_data']['product_data']['description'] = $item->description;
|
|
}
|
|
|
|
// Add recurring config if applicable
|
|
if ($item->billing_cycle) {
|
|
$lineItem['price_data']['recurring'] = [
|
|
'interval' => $item->billing_cycle === 'yearly' ? 'year' : 'month',
|
|
];
|
|
}
|
|
|
|
$lineItems[] = $lineItem;
|
|
}
|
|
|
|
return $lineItems;
|
|
}
|
|
|
|
/**
|
|
* Create a one-time Stripe coupon for an order discount.
|
|
*/
|
|
protected function createOrderCoupon(Order $order): string
|
|
{
|
|
$stripeCoupon = $this->getStripe()->coupons->create([
|
|
'amount_off' => (int) round($order->discount_amount * 100),
|
|
'currency' => strtolower($order->currency),
|
|
'duration' => 'once',
|
|
'name' => $order->coupon->code,
|
|
]);
|
|
|
|
return $stripeCoupon->id;
|
|
}
|
|
|
|
public function getCheckoutSession(string $sessionId): array
|
|
{
|
|
$session = $this->getStripe()->checkout->sessions->retrieve($sessionId, [
|
|
'expand' => ['payment_intent', 'subscription'],
|
|
]);
|
|
|
|
return [
|
|
'id' => $session->id,
|
|
'status' => $this->mapSessionStatus($session->status),
|
|
'amount' => $session->amount_total / 100,
|
|
'currency' => strtoupper($session->currency),
|
|
'paid_at' => $session->payment_status === 'paid' ? now() : null,
|
|
'subscription_id' => $session->subscription?->id,
|
|
'payment_intent_id' => $session->payment_intent?->id,
|
|
'metadata' => (array) $session->metadata,
|
|
'raw' => $session,
|
|
];
|
|
}
|
|
|
|
// Payments
|
|
|
|
public function charge(Workspace $workspace, int $amountCents, string $currency, array $metadata = []): Payment
|
|
{
|
|
$customerId = $workspace->stripe_customer_id;
|
|
if (! $customerId) {
|
|
$customerId = $this->createCustomer($workspace);
|
|
}
|
|
|
|
$paymentIntent = $this->getStripe()->paymentIntents->create([
|
|
'amount' => $amountCents,
|
|
'currency' => strtolower($currency),
|
|
'customer' => $customerId,
|
|
'metadata' => array_merge($metadata, ['workspace_id' => $workspace->id]),
|
|
'automatic_payment_methods' => ['enabled' => true],
|
|
]);
|
|
|
|
return Payment::create([
|
|
'workspace_id' => $workspace->id,
|
|
'gateway' => 'stripe',
|
|
'gateway_payment_id' => $paymentIntent->id,
|
|
'amount' => $amountCents / 100,
|
|
'currency' => strtoupper($currency),
|
|
'status' => $this->mapPaymentIntentStatus($paymentIntent->status),
|
|
'gateway_response' => $paymentIntent->toArray(),
|
|
]);
|
|
}
|
|
|
|
public function chargePaymentMethod(PaymentMethod $paymentMethod, int $amountCents, string $currency, array $metadata = []): Payment
|
|
{
|
|
$workspace = $paymentMethod->workspace;
|
|
|
|
$paymentIntent = $this->getStripe()->paymentIntents->create([
|
|
'amount' => $amountCents,
|
|
'currency' => strtolower($currency),
|
|
'customer' => $workspace->stripe_customer_id,
|
|
'payment_method' => $paymentMethod->gateway_payment_method_id,
|
|
'off_session' => true,
|
|
'confirm' => true,
|
|
'metadata' => array_merge($metadata, ['workspace_id' => $workspace->id]),
|
|
]);
|
|
|
|
return Payment::create([
|
|
'workspace_id' => $workspace->id,
|
|
'gateway' => 'stripe',
|
|
'gateway_payment_id' => $paymentIntent->id,
|
|
'payment_method_id' => $paymentMethod->id,
|
|
'amount' => $amountCents / 100,
|
|
'currency' => strtoupper($currency),
|
|
'status' => $this->mapPaymentIntentStatus($paymentIntent->status),
|
|
'gateway_response' => $paymentIntent->toArray(),
|
|
]);
|
|
}
|
|
|
|
// Subscriptions
|
|
|
|
public function createSubscription(Workspace $workspace, string $priceId, array $options = []): Subscription
|
|
{
|
|
$customerId = $workspace->stripe_customer_id;
|
|
if (! $customerId) {
|
|
$customerId = $this->createCustomer($workspace);
|
|
}
|
|
|
|
$params = [
|
|
'customer' => $customerId,
|
|
'items' => [['price' => $priceId]],
|
|
'metadata' => ['workspace_id' => $workspace->id],
|
|
];
|
|
|
|
if (isset($options['trial_days']) && $options['trial_days'] > 0) {
|
|
$params['trial_period_days'] = $options['trial_days'];
|
|
}
|
|
|
|
if (isset($options['coupon'])) {
|
|
$params['coupon'] = $options['coupon'];
|
|
}
|
|
|
|
$stripeSubscription = $this->getStripe()->subscriptions->create($params);
|
|
|
|
return Subscription::create([
|
|
'workspace_id' => $workspace->id,
|
|
'gateway' => 'stripe',
|
|
'gateway_subscription_id' => $stripeSubscription->id,
|
|
'gateway_customer_id' => $customerId,
|
|
'gateway_price_id' => $priceId,
|
|
'status' => $this->mapSubscriptionStatus($stripeSubscription->status),
|
|
'current_period_start' => \Carbon\Carbon::createFromTimestamp($stripeSubscription->current_period_start),
|
|
'current_period_end' => \Carbon\Carbon::createFromTimestamp($stripeSubscription->current_period_end),
|
|
'trial_ends_at' => $stripeSubscription->trial_end
|
|
? \Carbon\Carbon::createFromTimestamp($stripeSubscription->trial_end)
|
|
: null,
|
|
'metadata' => ['stripe_subscription' => $stripeSubscription->toArray()],
|
|
]);
|
|
}
|
|
|
|
public function updateSubscription(Subscription $subscription, array $options): Subscription
|
|
{
|
|
$params = [];
|
|
|
|
if (isset($options['price_id'])) {
|
|
$params['items'] = [
|
|
[
|
|
'id' => $this->getSubscriptionItemId($subscription),
|
|
'price' => $options['price_id'],
|
|
],
|
|
];
|
|
$params['proration_behavior'] = ($options['prorate'] ?? true)
|
|
? 'create_prorations'
|
|
: 'none';
|
|
}
|
|
|
|
if (isset($options['cancel_at_period_end'])) {
|
|
$params['cancel_at_period_end'] = $options['cancel_at_period_end'];
|
|
}
|
|
|
|
$stripeSubscription = $this->getStripe()->subscriptions->update(
|
|
$subscription->gateway_subscription_id,
|
|
$params
|
|
);
|
|
|
|
$subscription->update([
|
|
'gateway_price_id' => $options['price_id'] ?? $subscription->gateway_price_id,
|
|
'status' => $this->mapSubscriptionStatus($stripeSubscription->status),
|
|
'cancel_at_period_end' => $stripeSubscription->cancel_at_period_end,
|
|
'current_period_start' => \Carbon\Carbon::createFromTimestamp($stripeSubscription->current_period_start),
|
|
'current_period_end' => \Carbon\Carbon::createFromTimestamp($stripeSubscription->current_period_end),
|
|
]);
|
|
|
|
return $subscription->fresh();
|
|
}
|
|
|
|
public function cancelSubscription(Subscription $subscription, bool $immediately = false): void
|
|
{
|
|
if ($immediately) {
|
|
$this->getStripe()->subscriptions->cancel($subscription->gateway_subscription_id);
|
|
$subscription->update([
|
|
'status' => 'cancelled',
|
|
'cancelled_at' => now(),
|
|
'ended_at' => now(),
|
|
]);
|
|
} else {
|
|
$this->getStripe()->subscriptions->update($subscription->gateway_subscription_id, [
|
|
'cancel_at_period_end' => true,
|
|
]);
|
|
$subscription->update([
|
|
'cancel_at_period_end' => true,
|
|
'cancelled_at' => now(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function resumeSubscription(Subscription $subscription): void
|
|
{
|
|
$this->getStripe()->subscriptions->update($subscription->gateway_subscription_id, [
|
|
'cancel_at_period_end' => false,
|
|
]);
|
|
|
|
$subscription->resume();
|
|
}
|
|
|
|
public function pauseSubscription(Subscription $subscription): void
|
|
{
|
|
$this->getStripe()->subscriptions->update($subscription->gateway_subscription_id, [
|
|
'pause_collection' => ['behavior' => 'void'],
|
|
]);
|
|
|
|
$subscription->pause();
|
|
}
|
|
|
|
// Payment Methods
|
|
|
|
public function createSetupSession(Workspace $workspace, string $returnUrl): array
|
|
{
|
|
$customerId = $workspace->stripe_customer_id;
|
|
if (! $customerId) {
|
|
$customerId = $this->createCustomer($workspace);
|
|
}
|
|
|
|
$session = $this->getStripe()->checkout->sessions->create([
|
|
'customer' => $customerId,
|
|
'mode' => 'setup',
|
|
'success_url' => $returnUrl.'?setup_intent={SETUP_INTENT}',
|
|
'cancel_url' => $returnUrl,
|
|
]);
|
|
|
|
return [
|
|
'session_id' => $session->id,
|
|
'setup_url' => $session->url,
|
|
];
|
|
}
|
|
|
|
public function attachPaymentMethod(Workspace $workspace, string $gatewayPaymentMethodId): PaymentMethod
|
|
{
|
|
$stripePaymentMethod = $this->getStripe()->paymentMethods->attach($gatewayPaymentMethodId, [
|
|
'customer' => $workspace->stripe_customer_id,
|
|
]);
|
|
|
|
return PaymentMethod::create([
|
|
'workspace_id' => $workspace->id,
|
|
'gateway' => 'stripe',
|
|
'gateway_payment_method_id' => $stripePaymentMethod->id,
|
|
'type' => $stripePaymentMethod->type,
|
|
'last_four' => $stripePaymentMethod->card?->last4,
|
|
'brand' => $stripePaymentMethod->card?->brand,
|
|
'exp_month' => $stripePaymentMethod->card?->exp_month,
|
|
'exp_year' => $stripePaymentMethod->card?->exp_year,
|
|
'is_default' => false,
|
|
]);
|
|
}
|
|
|
|
public function detachPaymentMethod(PaymentMethod $paymentMethod): void
|
|
{
|
|
$this->getStripe()->paymentMethods->detach($paymentMethod->gateway_payment_method_id);
|
|
// Don't delete - the PaymentMethodService handles deactivation
|
|
}
|
|
|
|
public function setDefaultPaymentMethod(PaymentMethod $paymentMethod): void
|
|
{
|
|
$this->getStripe()->customers->update($paymentMethod->workspace->stripe_customer_id, [
|
|
'invoice_settings' => [
|
|
'default_payment_method' => $paymentMethod->gateway_payment_method_id,
|
|
],
|
|
]);
|
|
|
|
// Update local records
|
|
PaymentMethod::where('workspace_id', $paymentMethod->workspace_id)
|
|
->where('id', '!=', $paymentMethod->id)
|
|
->update(['is_default' => false]);
|
|
|
|
$paymentMethod->update(['is_default' => true]);
|
|
}
|
|
|
|
// Refunds
|
|
|
|
public function refund(Payment $payment, float $amount, ?string $reason = null): array
|
|
{
|
|
$amountCents = (int) round($amount * 100);
|
|
|
|
try {
|
|
$stripeRefund = $this->getStripe()->refunds->create([
|
|
'payment_intent' => $payment->gateway_payment_id,
|
|
'amount' => $amountCents,
|
|
'reason' => $this->mapRefundReason($reason),
|
|
]);
|
|
|
|
$refund = Refund::create([
|
|
'payment_id' => $payment->id,
|
|
'gateway_refund_id' => $stripeRefund->id,
|
|
'amount' => $amount,
|
|
'currency' => $payment->currency,
|
|
'status' => $stripeRefund->status === 'succeeded' ? 'succeeded' : 'pending',
|
|
'reason' => $reason,
|
|
'gateway_response' => $stripeRefund->toArray(),
|
|
]);
|
|
|
|
if ($stripeRefund->status === 'succeeded') {
|
|
$refund->markAsSucceeded($stripeRefund->id);
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'refund_id' => $stripeRefund->id,
|
|
];
|
|
} catch (\Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
// Invoices
|
|
|
|
public function getInvoice(string $gatewayInvoiceId): array
|
|
{
|
|
$invoice = $this->getStripe()->invoices->retrieve($gatewayInvoiceId);
|
|
|
|
return $invoice->toArray();
|
|
}
|
|
|
|
public function getInvoicePdfUrl(string $gatewayInvoiceId): ?string
|
|
{
|
|
$invoice = $this->getStripe()->invoices->retrieve($gatewayInvoiceId);
|
|
|
|
return $invoice->invoice_pdf;
|
|
}
|
|
|
|
// Webhooks
|
|
|
|
public function verifyWebhookSignature(string $payload, string $signature): bool
|
|
{
|
|
try {
|
|
\Stripe\Webhook::constructEvent($payload, $signature, $this->webhookSecret);
|
|
|
|
return true;
|
|
} catch (\Exception $e) {
|
|
Log::warning('Stripe webhook signature verification failed', ['error' => $e->getMessage()]);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public function parseWebhookEvent(string $payload): array
|
|
{
|
|
$event = json_decode($payload, true);
|
|
|
|
return [
|
|
'type' => $event['type'] ?? 'unknown',
|
|
'id' => $event['data']['object']['id'] ?? null,
|
|
'object_type' => $event['data']['object']['object'] ?? null,
|
|
'metadata' => $event['data']['object']['metadata'] ?? [],
|
|
'raw' => $event,
|
|
];
|
|
}
|
|
|
|
// Tax
|
|
|
|
public function createTaxRate(string $name, float $percentage, string $country, bool $inclusive = false): string
|
|
{
|
|
$taxRate = $this->getStripe()->taxRates->create([
|
|
'display_name' => $name,
|
|
'percentage' => $percentage,
|
|
'country' => $country,
|
|
'inclusive' => $inclusive,
|
|
]);
|
|
|
|
return $taxRate->id;
|
|
}
|
|
|
|
// Portal
|
|
|
|
public function getPortalUrl(Workspace $workspace, string $returnUrl): ?string
|
|
{
|
|
if (! $workspace->stripe_customer_id) {
|
|
return null;
|
|
}
|
|
|
|
$session = $this->getStripe()->billingPortal->sessions->create([
|
|
'customer' => $workspace->stripe_customer_id,
|
|
'return_url' => $returnUrl,
|
|
]);
|
|
|
|
return $session->url;
|
|
}
|
|
|
|
// Helper Methods
|
|
|
|
protected function hasRecurringItems(Order $order): bool
|
|
{
|
|
return $order->items->contains(fn ($item) => $item->billing_cycle !== null);
|
|
}
|
|
|
|
protected function getSubscriptionItemId(Subscription $subscription): string
|
|
{
|
|
$stripeSubscription = $this->getStripe()->subscriptions->retrieve($subscription->gateway_subscription_id);
|
|
|
|
return $stripeSubscription->items->data[0]->id;
|
|
}
|
|
|
|
protected function mapSessionStatus(string $status): string
|
|
{
|
|
return match ($status) {
|
|
'complete' => 'succeeded',
|
|
'expired' => 'expired',
|
|
'open' => 'pending',
|
|
default => 'pending',
|
|
};
|
|
}
|
|
|
|
protected function mapPaymentIntentStatus(string $status): string
|
|
{
|
|
return match ($status) {
|
|
'succeeded' => 'succeeded',
|
|
'processing' => 'processing',
|
|
'requires_payment_method', 'requires_confirmation', 'requires_action' => 'pending',
|
|
'canceled' => 'failed',
|
|
default => 'pending',
|
|
};
|
|
}
|
|
|
|
protected function mapSubscriptionStatus(string $status): string
|
|
{
|
|
return match ($status) {
|
|
'active' => 'active',
|
|
'trialing' => 'trialing',
|
|
'past_due' => 'past_due',
|
|
'paused' => 'paused',
|
|
'canceled' => 'cancelled',
|
|
'incomplete', 'incomplete_expired' => 'incomplete',
|
|
default => 'active',
|
|
};
|
|
}
|
|
|
|
protected function mapRefundReason(?string $reason): string
|
|
{
|
|
return match ($reason) {
|
|
'duplicate' => 'duplicate',
|
|
'fraudulent' => 'fraudulent',
|
|
default => 'requested_by_customer',
|
|
};
|
|
}
|
|
}
|