退款治理:支持标记部分退款/已支付以回退退款状态
This commit is contained in:
@@ -613,6 +613,72 @@ class PlatformOrderController extends Controller
|
|||||||
return redirect()->back()->with('success', '已将订单支付状态标记为已退款(未自动写入退款回执)。');
|
return redirect()->back()->with('success', '已将订单支付状态标记为已退款(未自动写入退款回执)。');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function markPartiallyRefunded(Request $request, PlatformOrder $order): RedirectResponse
|
||||||
|
{
|
||||||
|
$admin = $this->ensurePlatformAdmin($request);
|
||||||
|
|
||||||
|
if ((float) ($order->paid_amount ?? 0) <= 0) {
|
||||||
|
return redirect()->back()->with('warning', '当前订单已付金额为 0,无法标记为部分退款。');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((string) $order->payment_status === 'partially_refunded') {
|
||||||
|
return redirect()->back()->with('warning', '当前订单已是部分退款状态,无需重复操作。');
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = now();
|
||||||
|
$order->payment_status = 'partially_refunded';
|
||||||
|
$order->refunded_at = $order->refunded_at ?: $now;
|
||||||
|
|
||||||
|
$meta = (array) ($order->meta ?? []);
|
||||||
|
$audit = (array) (data_get($meta, 'audit', []) ?? []);
|
||||||
|
$audit[] = [
|
||||||
|
'action' => 'mark_partially_refunded',
|
||||||
|
'scope' => 'single',
|
||||||
|
'at' => $now->toDateTimeString(),
|
||||||
|
'admin_id' => $admin->id,
|
||||||
|
'note' => '手动标记为部分退款(仅修正支付状态,不自动写退款回执)',
|
||||||
|
];
|
||||||
|
data_set($meta, 'audit', $audit);
|
||||||
|
|
||||||
|
$order->meta = $meta;
|
||||||
|
$order->save();
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '已将订单支付状态标记为部分退款(未自动写入退款回执)。');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function markPaidStatus(Request $request, PlatformOrder $order): RedirectResponse
|
||||||
|
{
|
||||||
|
$admin = $this->ensurePlatformAdmin($request);
|
||||||
|
|
||||||
|
if ((float) ($order->paid_amount ?? 0) <= 0) {
|
||||||
|
return redirect()->back()->with('warning', '当前订单已付金额为 0,无法标记为已支付。');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((string) $order->payment_status === 'paid') {
|
||||||
|
return redirect()->back()->with('warning', '当前订单已是已支付状态,无需重复操作。');
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = now();
|
||||||
|
$order->payment_status = 'paid';
|
||||||
|
// paid 状态不强依赖 refunded_at,这里不做清空,避免丢历史痕迹
|
||||||
|
|
||||||
|
$meta = (array) ($order->meta ?? []);
|
||||||
|
$audit = (array) (data_get($meta, 'audit', []) ?? []);
|
||||||
|
$audit[] = [
|
||||||
|
'action' => 'mark_paid_status',
|
||||||
|
'scope' => 'single',
|
||||||
|
'at' => $now->toDateTimeString(),
|
||||||
|
'admin_id' => $admin->id,
|
||||||
|
'note' => '手动标记为已支付(仅修正支付状态,不自动写回执/退款回执)',
|
||||||
|
];
|
||||||
|
data_set($meta, 'audit', $audit);
|
||||||
|
|
||||||
|
$order->meta = $meta;
|
||||||
|
$order->save();
|
||||||
|
|
||||||
|
return redirect()->back()->with('success', '已将订单支付状态标记为已支付(未自动写入回执/退款回执)。');
|
||||||
|
}
|
||||||
|
|
||||||
public function markActivated(Request $request, PlatformOrder $order): RedirectResponse
|
public function markActivated(Request $request, PlatformOrder $order): RedirectResponse
|
||||||
{
|
{
|
||||||
$admin = $this->ensurePlatformAdmin($request);
|
$admin = $this->ensurePlatformAdmin($request);
|
||||||
|
|||||||
@@ -85,6 +85,11 @@
|
|||||||
$canMarkRefunded = $paidAmountFloat > 0
|
$canMarkRefunded = $paidAmountFloat > 0
|
||||||
&& $order->payment_status !== 'refunded'
|
&& $order->payment_status !== 'refunded'
|
||||||
&& round($refundTotal * 100) >= round($paidAmountFloat * 100);
|
&& round($refundTotal * 100) >= round($paidAmountFloat * 100);
|
||||||
|
|
||||||
|
// - refunded 但退款总额不足 => 提供降级动作:标记为部分退款/已支付(仍不自动写回执)
|
||||||
|
$canFixRefundedButNotEnough = $paidAmountFloat > 0
|
||||||
|
&& $order->payment_status === 'refunded'
|
||||||
|
&& (round($refundTotal * 100) + 1) < round($paidAmountFloat * 100);
|
||||||
@endphp
|
@endphp
|
||||||
@if($canMarkRefunded)
|
@if($canMarkRefunded)
|
||||||
<div class="muted" style="margin-top:10px;">
|
<div class="muted" style="margin-top:10px;">
|
||||||
@@ -94,6 +99,18 @@
|
|||||||
<button type="submit">标记为已退款</button>
|
<button type="submit">标记为已退款</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@elseif($canFixRefundedButNotEnough)
|
||||||
|
<div class="muted" style="margin-top:10px;">
|
||||||
|
提示:当前支付状态为「已退款」,但退款总额不足。如确认无误,可将状态修正为「部分退款」或「已支付」。
|
||||||
|
<form method="post" action="/admin/platform-orders/{{ $order->id }}/mark-partially-refunded" style="display:inline; margin-left:8px;" onsubmit="return confirm('确认将该订单支付状态标记为部分退款?该操作不会自动写入退款回执,仅修正状态');">
|
||||||
|
@csrf
|
||||||
|
<button type="submit">标记为部分退款</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="/admin/platform-orders/{{ $order->id }}/mark-paid-status" style="display:inline; margin-left:8px;" onsubmit="return confirm('确认将该订单支付状态标记为已支付?该操作不会自动写入回执/退款回执,仅修正状态');">
|
||||||
|
@csrf
|
||||||
|
<button type="submit">标记为已支付</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if($order->payment_status === 'refunded' && ($refundTotal + 0.01) < $paidAmountFloat)
|
@if($order->payment_status === 'refunded' && ($refundTotal + 0.01) < $paidAmountFloat)
|
||||||
@@ -393,6 +410,8 @@
|
|||||||
'batch_mark_activated' => '批量仅标记为已生效',
|
'batch_mark_activated' => '批量仅标记为已生效',
|
||||||
'activate_subscription' => '同步订阅',
|
'activate_subscription' => '同步订阅',
|
||||||
'mark_refunded' => '手动标记为已退款',
|
'mark_refunded' => '手动标记为已退款',
|
||||||
|
'mark_partially_refunded' => '手动标记为部分退款',
|
||||||
|
'mark_paid_status' => '手动标记为已支付',
|
||||||
];
|
];
|
||||||
@endphp
|
@endphp
|
||||||
<table>
|
<table>
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ Route::prefix('admin')->group(function () {
|
|||||||
Route::post('/platform-orders/{order}/add-payment-receipt', [PlatformOrderController::class, 'addPaymentReceipt']);
|
Route::post('/platform-orders/{order}/add-payment-receipt', [PlatformOrderController::class, 'addPaymentReceipt']);
|
||||||
Route::post('/platform-orders/{order}/add-refund-receipt', [PlatformOrderController::class, 'addRefundReceipt']);
|
Route::post('/platform-orders/{order}/add-refund-receipt', [PlatformOrderController::class, 'addRefundReceipt']);
|
||||||
Route::post('/platform-orders/{order}/mark-refunded', [PlatformOrderController::class, 'markRefunded']);
|
Route::post('/platform-orders/{order}/mark-refunded', [PlatformOrderController::class, 'markRefunded']);
|
||||||
|
Route::post('/platform-orders/{order}/mark-partially-refunded', [PlatformOrderController::class, 'markPartiallyRefunded']);
|
||||||
|
Route::post('/platform-orders/{order}/mark-paid-status', [PlatformOrderController::class, 'markPaidStatus']);
|
||||||
Route::post('/platform-orders/{order}/mark-activated', [PlatformOrderController::class, 'markActivated']);
|
Route::post('/platform-orders/{order}/mark-activated', [PlatformOrderController::class, 'markActivated']);
|
||||||
|
|
||||||
Route::get('/site-subscriptions', [SiteSubscriptionController::class, 'index']);
|
Route::get('/site-subscriptions', [SiteSubscriptionController::class, 'index']);
|
||||||
|
|||||||
80
tests/Feature/AdminPlatformOrderMarkRefundStatusBackTest.php
Normal file
80
tests/Feature/AdminPlatformOrderMarkRefundStatusBackTest.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Merchant;
|
||||||
|
use App\Models\Plan;
|
||||||
|
use App\Models\PlatformOrder;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class AdminPlatformOrderMarkRefundStatusBackTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function loginAsPlatformAdmin(): void
|
||||||
|
{
|
||||||
|
$this->seed();
|
||||||
|
|
||||||
|
$this->post('/admin/login', [
|
||||||
|
'email' => 'platform.admin@demo.local',
|
||||||
|
'password' => 'Platform@123456',
|
||||||
|
])->assertRedirect('/admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_platform_admin_can_mark_order_refund_status_back_to_partially_refunded_and_paid(): void
|
||||||
|
{
|
||||||
|
$this->loginAsPlatformAdmin();
|
||||||
|
|
||||||
|
$merchant = Merchant::query()->firstOrFail();
|
||||||
|
$plan = Plan::query()->create([
|
||||||
|
'code' => 'mark_refund_status_back_test_plan',
|
||||||
|
'name' => '退款状态回退治理测试套餐',
|
||||||
|
'billing_cycle' => 'monthly',
|
||||||
|
'price' => 10,
|
||||||
|
'list_price' => 10,
|
||||||
|
'status' => 'active',
|
||||||
|
'sort' => 10,
|
||||||
|
'published_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$order = PlatformOrder::query()->create([
|
||||||
|
'merchant_id' => $merchant->id,
|
||||||
|
'plan_id' => $plan->id,
|
||||||
|
'order_no' => 'PO_MARK_REFUND_BACK_0001',
|
||||||
|
'order_type' => 'new_purchase',
|
||||||
|
'status' => 'activated',
|
||||||
|
'payment_status' => 'refunded',
|
||||||
|
'plan_name' => $plan->name,
|
||||||
|
'billing_cycle' => $plan->billing_cycle,
|
||||||
|
'period_months' => 1,
|
||||||
|
'quantity' => 1,
|
||||||
|
'payable_amount' => 10,
|
||||||
|
'paid_amount' => 10,
|
||||||
|
'placed_at' => now(),
|
||||||
|
'paid_at' => now(),
|
||||||
|
'activated_at' => now(),
|
||||||
|
'refunded_at' => now(),
|
||||||
|
'meta' => [
|
||||||
|
'refund_summary' => [
|
||||||
|
'count' => 1,
|
||||||
|
'total_amount' => 1.00,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 回退到部分退款
|
||||||
|
$this->post('/admin/platform-orders/' . $order->id . '/mark-partially-refunded')
|
||||||
|
->assertRedirect();
|
||||||
|
$order->refresh();
|
||||||
|
$this->assertSame('partially_refunded', $order->payment_status);
|
||||||
|
$this->assertSame('mark_partially_refunded', (string) data_get($order->meta, 'audit.0.action'));
|
||||||
|
|
||||||
|
// 再回退到已支付
|
||||||
|
$this->post('/admin/platform-orders/' . $order->id . '/mark-paid-status')
|
||||||
|
->assertRedirect();
|
||||||
|
$order->refresh();
|
||||||
|
$this->assertSame('paid', $order->payment_status);
|
||||||
|
$this->assertSame('mark_paid_status', (string) data_get($order->meta, 'audit.1.action'));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user