Automations: - 2-column layout: WhatsApp phone LEFT, education RIGHT - Right column: 'How it works' (5 numbered steps), performance stats, timing controls, reply commands, tips - Hero spans full width with photo+dark panel - Improvement CTA is a prominent card, not floating text - No misalignment — phone fills left column naturally Collect: - Appeals shown as visible gap-px grid cards (not hidden dropdown) - Each card shows name, platform, amount raised, pledge count, collection rate - Active appeal has border-l-2 blue indicator - Platform integration clarity: shows 'Donors redirected to JustGiving' etc - Educational section: 'Where to share your link' + 'How payment works' - Explains bank transfer vs JustGiving vs card payment inline AI model: Stripped all model name comments from code (no user-facing references existed)
158 lines
5.5 KiB
PHP
158 lines
5.5 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Illuminate\Support\Facades\Log;
|
|
use Stripe\Exception\ApiErrorException;
|
|
use Stripe\StripeClient;
|
|
|
|
/**
|
|
* Centralised Stripe refund & payment-method management.
|
|
*
|
|
* Every action is logged. Every response is a simple array so callers
|
|
* can show user-friendly notifications without catching exceptions.
|
|
*/
|
|
class StripeRefundService
|
|
{
|
|
private StripeClient $stripe;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->stripe = new StripeClient(config('paisa.gateways.stripe.secret_key'));
|
|
}
|
|
|
|
// ── Refund a PaymentIntent (full or partial) ────────────────
|
|
|
|
public function refundPaymentIntent(string $paymentIntentId, ?int $amountInPence = null, string $reason = ''): array
|
|
{
|
|
try {
|
|
$params = [
|
|
'payment_intent' => $paymentIntentId,
|
|
'reason' => 'requested_by_customer',
|
|
];
|
|
|
|
if ($amountInPence !== null && $amountInPence > 0) {
|
|
$params['amount'] = $amountInPence;
|
|
}
|
|
|
|
$refund = $this->stripe->refunds->create($params);
|
|
|
|
Log::info('Stripe refund created', [
|
|
'refund_id' => $refund->id,
|
|
'payment_intent' => $paymentIntentId,
|
|
'amount' => $refund->amount,
|
|
'status' => $refund->status,
|
|
'reason' => $reason,
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'refund_id' => $refund->id,
|
|
'amount' => $refund->amount,
|
|
'status' => $refund->status,
|
|
];
|
|
} catch (ApiErrorException $e) {
|
|
Log::error('Stripe refund failed', [
|
|
'payment_intent' => $paymentIntentId,
|
|
'error' => $e->getMessage(),
|
|
'reason' => $reason,
|
|
]);
|
|
|
|
return ['success' => false, 'error' => $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
// ── Detach payment method from a SetupIntent ────────────────
|
|
// This is how we "cancel" recurring: remove the card so no
|
|
// future charges can be made.
|
|
|
|
public function detachPaymentMethod(string $setupIntentId): array
|
|
{
|
|
try {
|
|
$si = $this->stripe->setupIntents->retrieve($setupIntentId);
|
|
|
|
if (! $si->payment_method) {
|
|
return ['success' => true, 'message' => 'No payment method attached'];
|
|
}
|
|
|
|
$pmId = is_string($si->payment_method)
|
|
? $si->payment_method
|
|
: $si->payment_method->id;
|
|
|
|
$this->stripe->paymentMethods->detach($pmId);
|
|
|
|
Log::info('Stripe payment method detached', [
|
|
'setup_intent' => $setupIntentId,
|
|
'payment_method' => $pmId,
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => "Payment method {$pmId} detached",
|
|
'payment_method' => $pmId,
|
|
];
|
|
} catch (ApiErrorException $e) {
|
|
Log::error('Failed to detach payment method', [
|
|
'setup_intent' => $setupIntentId,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return ['success' => false, 'error' => $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
// ── Retrieve PaymentIntent details for display ──────────────
|
|
|
|
public function getPaymentDetails(string $paymentIntentId): ?array
|
|
{
|
|
try {
|
|
$pi = $this->stripe->paymentIntents->retrieve($paymentIntentId, [
|
|
'expand' => ['latest_charge'],
|
|
]);
|
|
|
|
$charge = $pi->latest_charge;
|
|
|
|
return [
|
|
'id' => $pi->id,
|
|
'status' => $pi->status,
|
|
'amount' => $pi->amount,
|
|
'currency' => strtoupper($pi->currency),
|
|
'created' => date('d M Y H:i', $pi->created),
|
|
'refunded' => $charge?->refunded ?? false,
|
|
'amount_refunded' => $charge?->amount_refunded ?? 0,
|
|
'card_brand' => $charge?->payment_method_details?->card?->brand ?? null,
|
|
'card_last4' => $charge?->payment_method_details?->card?->last4 ?? null,
|
|
];
|
|
} catch (ApiErrorException $e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ── Retrieve SetupIntent details for display ────────────────
|
|
|
|
public function getSetupIntentDetails(string $setupIntentId): ?array
|
|
{
|
|
try {
|
|
$si = $this->stripe->setupIntents->retrieve($setupIntentId, [
|
|
'expand' => ['payment_method'],
|
|
]);
|
|
|
|
$pm = $si->payment_method;
|
|
|
|
return [
|
|
'id' => $si->id,
|
|
'status' => $si->status,
|
|
'created' => date('d M Y H:i', $si->created),
|
|
'payment_method_id' => is_string($pm) ? $pm : ($pm?->id ?? null),
|
|
'card_brand' => is_object($pm) ? ($pm->card?->brand ?? null) : null,
|
|
'card_last4' => is_object($pm) ? ($pm->card?->last4 ?? null) : null,
|
|
'card_exp_month' => is_object($pm) ? ($pm->card?->exp_month ?? null) : null,
|
|
'card_exp_year' => is_object($pm) ? ($pm->card?->exp_year ?? null) : null,
|
|
'customer_id' => $si->customer,
|
|
];
|
|
} catch (ApiErrorException $e) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|