Deep UX: 2-column automations, visible appeal cards, platform education, strip model refs

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)
This commit is contained in:
2026-03-05 03:20:20 +08:00
parent 3c3336383e
commit 8366054bd7
11 changed files with 2058 additions and 368 deletions

View File

@@ -0,0 +1,157 @@
<?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;
}
}
}