From 6f1b894b45f25e2f981dfb79f988be6aeb7db45f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=90=9D=E5=8D=9C?= Date: Fri, 13 Mar 2026 15:51:26 +0000 Subject: [PATCH] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E9=80=80=E6=AC=BE=E4=B8=8D?= =?UTF-8?q?=E4=B8=80=E8=87=B4(refund=5Finconsistent)=E5=8F=A3=E5=BE=84?= =?UTF-8?q?=EF=BC=9A=E5=BC=95=E5=85=A5=20amounts.tolerance=20=E5=B9=B6?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=E6=A8=A1=E5=9E=8B=E4=B8=8E=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Admin/PlatformOrderController.php | 33 ++++---- app/Models/PlatformOrder.php | 13 ++- ...ionsRefundInconsistentFilterFieldsTest.php | 2 +- ...atedRefundInconsistentFilterFieldsTest.php | 2 +- ...rorsRefundInconsistentFilterFieldsTest.php | 2 +- ...rderExportRefundInconsistentFilterTest.php | 4 +- ...tformOrderRefundInconsistentFilterTest.php | 8 +- ...formOrderRefundInconsistentRowHintTest.php | 2 +- .../PlatformOrderRefundInconsistentTest.php | 8 +- ...rRefundInconsistentToleranceConfigTest.php | 84 +++++++++++++++++++ 10 files changed, 128 insertions(+), 30 deletions(-) create mode 100644 tests/Unit/PlatformOrderRefundInconsistentToleranceConfigTest.php diff --git a/app/Http/Controllers/Admin/PlatformOrderController.php b/app/Http/Controllers/Admin/PlatformOrderController.php index 9340742..d577e45 100644 --- a/app/Http/Controllers/Admin/PlatformOrderController.php +++ b/app/Http/Controllers/Admin/PlatformOrderController.php @@ -1837,40 +1837,45 @@ class PlatformOrderController extends Controller }) ->when(($filters['refund_inconsistent'] ?? '') !== '', function (Builder $builder) { // 退款数据不一致(可治理): - // - 状态=refunded 但 退款总额 < 已付金额(允许 0.01 容差) - // - 状态!=refunded 且 已付金额>0 且 退款总额 >= 已付金额 + // - 状态=refunded 但 退款总额 + 容差 < 已付金额 + // - 状态!=refunded 且 已付金额>0 且 退款总额 >= 已付金额 + 容差 // 退款总额口径:优先 refund_summary.total_amount;缺省回退汇总 refund_receipts[].amount + // 容差口径与模型 PlatformOrder::isRefundInconsistent() 对齐(config('saasshop.amounts.tolerance')) + $tol = (float) config('saasshop.amounts.tolerance', 0.01); + $tolCents = (int) round($tol * 100); + $tolCents = max(1, $tolCents); + $driver = $builder->getQuery()->getConnection()->getDriverName(); if ($driver === 'sqlite') { $refundTotalExpr = "(CASE WHEN JSON_EXTRACT(meta, '$.refund_summary.total_amount') IS NOT NULL THEN CAST(JSON_EXTRACT(meta, '$.refund_summary.total_amount') AS REAL) ELSE (SELECT IFNULL(SUM(CAST(JSON_EXTRACT(value, '$.amount') AS REAL)), 0) FROM json_each(COALESCE(JSON_EXTRACT(meta, '$.refund_receipts'), '[]'))) END)"; - $builder->where(function (Builder $q) use ($refundTotalExpr) { - // refunded 但退款不够 - $q->where(function (Builder $q2) use ($refundTotalExpr) { + $builder->where(function (Builder $q) use ($refundTotalExpr, $tolCents) { + // refunded 但退款不够:refund_total + tol < paid + $q->where(function (Builder $q2) use ($refundTotalExpr, $tolCents) { $q2->where('payment_status', 'refunded') ->whereRaw("paid_amount > 0") - ->whereRaw("(ROUND($refundTotalExpr * 100) + 1) < ROUND(paid_amount * 100)"); + ->whereRaw("(ROUND($refundTotalExpr * 100) + {$tolCents}) < ROUND(paid_amount * 100)"); }) - // 非 refunded 但退款已达到/超过已付 - ->orWhere(function (Builder $q2) use ($refundTotalExpr) { + // 非 refunded 但退款已达到/超过已付 + tol:refund_total >= paid + tol + ->orWhere(function (Builder $q2) use ($refundTotalExpr, $tolCents) { $q2->where('payment_status', '!=', 'refunded') ->whereRaw("paid_amount > 0") - ->whereRaw("ROUND($refundTotalExpr * 100) >= ROUND(paid_amount * 100)"); + ->whereRaw("ROUND($refundTotalExpr * 100) >= (ROUND(paid_amount * 100) + {$tolCents})"); }); }); } else { $refundTotalExpr = "(CASE WHEN JSON_EXTRACT(meta, '$.refund_summary.total_amount') IS NOT NULL THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(meta, '$.refund_summary.total_amount')) AS DECIMAL(12,2)) ELSE (SELECT IFNULL(SUM(j.amount), 0) FROM JSON_TABLE(meta, '$.refund_receipts[*]' COLUMNS(amount DECIMAL(12,2) PATH '$.amount')) j) END)"; - $builder->where(function (Builder $q) use ($refundTotalExpr) { - $q->where(function (Builder $q2) use ($refundTotalExpr) { + $builder->where(function (Builder $q) use ($refundTotalExpr, $tolCents) { + $q->where(function (Builder $q2) use ($refundTotalExpr, $tolCents) { $q2->where('payment_status', 'refunded') ->whereRaw("paid_amount > 0") - ->whereRaw("(ROUND($refundTotalExpr * 100) + 1) < ROUND(paid_amount * 100)"); - })->orWhere(function (Builder $q2) use ($refundTotalExpr) { + ->whereRaw("(ROUND($refundTotalExpr * 100) + {$tolCents}) < ROUND(paid_amount * 100)"); + })->orWhere(function (Builder $q2) use ($refundTotalExpr, $tolCents) { $q2->where('payment_status', '!=', 'refunded') ->whereRaw("paid_amount > 0") - ->whereRaw("ROUND($refundTotalExpr * 100) >= ROUND(paid_amount * 100)"); + ->whereRaw("ROUND($refundTotalExpr * 100) >= (ROUND(paid_amount * 100) + {$tolCents})"); }); }); } diff --git a/app/Models/PlatformOrder.php b/app/Models/PlatformOrder.php index 35316c9..51580c2 100644 --- a/app/Models/PlatformOrder.php +++ b/app/Models/PlatformOrder.php @@ -48,19 +48,24 @@ class PlatformOrder extends Model public function isRefundInconsistent(): bool { - // 口径与平台订单列表 refund_inconsistent 保持一致:按分取整 + 0.01 容差 + // 口径与平台订单列表 refund_inconsistent 保持一致:按分取整 + 容差(config('saasshop.amounts.tolerance')) $refundTotal = (float) $this->refundTotal(); $paidAmount = (float) ($this->paid_amount ?? 0); $refundCents = (int) round($refundTotal * 100); $paidCents = (int) round($paidAmount * 100); + $tol = (float) config('saasshop.amounts.tolerance', 0.01); + $tolCents = (int) round($tol * 100); + $tolCents = max(1, $tolCents); + if ((string) $this->payment_status === 'refunded') { - // 允许 0.01 容差:refund_total + 0.01 < paid - return ($refundCents + 1) < $paidCents; + // 已退款但退款总额不足:refund_total + tol < paid + return ($refundCents + $tolCents) < $paidCents; } - return $paidCents > 0 && $refundCents >= $paidCents; + // 非已退款但退款总额已达到/超过已付:refund_total >= paid + tol + return $paidCents > 0 && $refundCents >= ($paidCents + $tolCents); } public function isReconcileMismatch(): bool diff --git a/tests/Feature/AdminPlatformOrderBatchActivateSubscriptionsRefundInconsistentFilterFieldsTest.php b/tests/Feature/AdminPlatformOrderBatchActivateSubscriptionsRefundInconsistentFilterFieldsTest.php index 054418f..490ab90 100644 --- a/tests/Feature/AdminPlatformOrderBatchActivateSubscriptionsRefundInconsistentFilterFieldsTest.php +++ b/tests/Feature/AdminPlatformOrderBatchActivateSubscriptionsRefundInconsistentFilterFieldsTest.php @@ -58,7 +58,7 @@ class AdminPlatformOrderBatchActivateSubscriptionsRefundInconsistentFilterFields 'meta' => [ 'refund_summary' => [ 'count' => 1, - 'total_amount' => 10.00, + 'total_amount' => 10.01, ], ], ]); diff --git a/tests/Feature/AdminPlatformOrderBatchMarkActivatedRefundInconsistentFilterFieldsTest.php b/tests/Feature/AdminPlatformOrderBatchMarkActivatedRefundInconsistentFilterFieldsTest.php index ef2bc88..e061f43 100644 --- a/tests/Feature/AdminPlatformOrderBatchMarkActivatedRefundInconsistentFilterFieldsTest.php +++ b/tests/Feature/AdminPlatformOrderBatchMarkActivatedRefundInconsistentFilterFieldsTest.php @@ -57,7 +57,7 @@ class AdminPlatformOrderBatchMarkActivatedRefundInconsistentFilterFieldsTest ext 'meta' => [ 'refund_summary' => [ 'count' => 1, - 'total_amount' => 10.00, + 'total_amount' => 10.01, ], ], ]); diff --git a/tests/Feature/AdminPlatformOrderClearSyncErrorsRefundInconsistentFilterFieldsTest.php b/tests/Feature/AdminPlatformOrderClearSyncErrorsRefundInconsistentFilterFieldsTest.php index fe9a990..bf0d35f 100644 --- a/tests/Feature/AdminPlatformOrderClearSyncErrorsRefundInconsistentFilterFieldsTest.php +++ b/tests/Feature/AdminPlatformOrderClearSyncErrorsRefundInconsistentFilterFieldsTest.php @@ -58,7 +58,7 @@ class AdminPlatformOrderClearSyncErrorsRefundInconsistentFilterFieldsTest extend 'meta' => [ 'refund_summary' => [ 'count' => 1, - 'total_amount' => 10.00, + 'total_amount' => 10.01, ], 'subscription_activation_error' => [ 'message' => '测试错误A', diff --git a/tests/Feature/AdminPlatformOrderExportRefundInconsistentFilterTest.php b/tests/Feature/AdminPlatformOrderExportRefundInconsistentFilterTest.php index 837b24b..4bf969f 100644 --- a/tests/Feature/AdminPlatformOrderExportRefundInconsistentFilterTest.php +++ b/tests/Feature/AdminPlatformOrderExportRefundInconsistentFilterTest.php @@ -84,7 +84,7 @@ class AdminPlatformOrderExportRefundInconsistentFilterTest extends TestCase 'meta' => [ 'refund_summary' => [ 'count' => 1, - 'total_amount' => 10.00, + 'total_amount' => 10.01, ], ], ]); @@ -110,7 +110,7 @@ class AdminPlatformOrderExportRefundInconsistentFilterTest extends TestCase 'meta' => [ 'refund_summary' => [ 'count' => 1, - 'total_amount' => 10.00, + 'total_amount' => 10.01, ], ], ]); diff --git a/tests/Feature/AdminPlatformOrderRefundInconsistentFilterTest.php b/tests/Feature/AdminPlatformOrderRefundInconsistentFilterTest.php index 2fd75f9..c1d33d7 100644 --- a/tests/Feature/AdminPlatformOrderRefundInconsistentFilterTest.php +++ b/tests/Feature/AdminPlatformOrderRefundInconsistentFilterTest.php @@ -87,9 +87,9 @@ class AdminPlatformOrderRefundInconsistentFilterTest extends TestCase 'meta' => [ 'refund_summary' => [ 'count' => 1, - 'total_amount' => 10.00, + 'total_amount' => 10.01, 'last_at' => now()->toDateTimeString(), - 'last_amount' => 10.00, + 'last_amount' => 10.01, 'last_channel' => 'bank', ], ], @@ -116,9 +116,9 @@ class AdminPlatformOrderRefundInconsistentFilterTest extends TestCase 'meta' => [ 'refund_summary' => [ 'count' => 1, - 'total_amount' => 10.00, + 'total_amount' => 10.01, 'last_at' => now()->toDateTimeString(), - 'last_amount' => 10.00, + 'last_amount' => 10.01, 'last_channel' => 'bank', ], ], diff --git a/tests/Feature/AdminPlatformOrderRefundInconsistentRowHintTest.php b/tests/Feature/AdminPlatformOrderRefundInconsistentRowHintTest.php index 5a55fe0..17e5095 100644 --- a/tests/Feature/AdminPlatformOrderRefundInconsistentRowHintTest.php +++ b/tests/Feature/AdminPlatformOrderRefundInconsistentRowHintTest.php @@ -57,7 +57,7 @@ class AdminPlatformOrderRefundInconsistentRowHintTest extends TestCase 'meta' => [ 'refund_summary' => [ 'count' => 1, - 'total_amount' => 10.00, + 'total_amount' => 10.01, ], ], ]); diff --git a/tests/Unit/PlatformOrderRefundInconsistentTest.php b/tests/Unit/PlatformOrderRefundInconsistentTest.php index 2face1a..ed2c09a 100644 --- a/tests/Unit/PlatformOrderRefundInconsistentTest.php +++ b/tests/Unit/PlatformOrderRefundInconsistentTest.php @@ -96,10 +96,12 @@ class PlatformOrderRefundInconsistentTest extends TestCase ], ]); + // refunded 状态下:只有当退款总额 + 容差 < 已付金额 才算不一致。 + // 这里 refund_total=9.98、paid=10.00,差额 0.02 > 默认容差 0.01,因此应判定为不一致。 $this->assertTrue($order->isRefundInconsistent()); } - public function test_is_refund_inconsistent_when_status_not_refunded_but_refund_total_reaches_paid_amount(): void + public function test_is_refund_inconsistent_when_status_not_refunded_but_refund_total_reaches_paid_amount_plus_tolerance(): void { $merchant = Merchant::query()->create([ 'name' => '单测站点3', @@ -139,6 +141,8 @@ class PlatformOrderRefundInconsistentTest extends TestCase ], ]); - $this->assertTrue($order->isRefundInconsistent()); + // paid(非 refunded)状态下:只有当退款总额 >= 已付金额 + 容差 才算不一致。 + // 这里 refund_total=10.00 与 paid=10.00 相等,未达到 paid + 0.01,因此不应判定不一致。 + $this->assertFalse($order->isRefundInconsistent()); } } diff --git a/tests/Unit/PlatformOrderRefundInconsistentToleranceConfigTest.php b/tests/Unit/PlatformOrderRefundInconsistentToleranceConfigTest.php new file mode 100644 index 0000000..3033be3 --- /dev/null +++ b/tests/Unit/PlatformOrderRefundInconsistentToleranceConfigTest.php @@ -0,0 +1,84 @@ + 0.05]); + + $merchant = Merchant::query()->create([ + 'name' => '容差单测站点', + 'slug' => 'unit-test-merchant-refund-tol', + 'status' => 'active', + ]); + + $plan = Plan::query()->create([ + 'code' => 'unit_test_plan_refund_tol_1', + 'name' => '容差单测套餐', + 'billing_cycle' => 'monthly', + 'price' => 10, + 'list_price' => 10, + 'status' => 'active', + 'sort' => 10, + 'published_at' => now(), + ]); + + // 场景1:状态=refunded,但退款总额只差 0.02(在容差 0.05 内)=> 不应判定不一致 + $order1 = PlatformOrder::query()->create([ + 'merchant_id' => $merchant->id, + 'plan_id' => $plan->id, + 'order_no' => 'PO_UNIT_REFUND_TOL_0001', + 'order_type' => 'subscription', + 'status' => 'activated', + 'payment_status' => 'refunded', + 'plan_name' => 'Unit Test Plan', + 'billing_cycle' => 'monthly', + 'period_months' => 1, + 'quantity' => 1, + 'payable_amount' => 10, + 'paid_amount' => 10, + 'placed_at' => now(), + 'meta' => [ + 'refund_summary' => [ + 'total_amount' => 9.98, + ], + ], + ]); + + $this->assertFalse($order1->isRefundInconsistent()); + + // 场景2:状态!=refunded,但退款总额只比已付多 0.02(在容差 0.05 内)=> 不应判定不一致 + $order2 = PlatformOrder::query()->create([ + 'merchant_id' => $merchant->id, + 'plan_id' => $plan->id, + 'order_no' => 'PO_UNIT_REFUND_TOL_0002', + 'order_type' => 'subscription', + 'status' => 'activated', + 'payment_status' => 'paid', + 'plan_name' => 'Unit Test Plan', + 'billing_cycle' => 'monthly', + 'period_months' => 1, + 'quantity' => 1, + 'payable_amount' => 10, + 'paid_amount' => 10, + 'placed_at' => now(), + 'meta' => [ + 'refund_summary' => [ + 'total_amount' => 10.02, + ], + ], + ]); + + $this->assertFalse($order2->isRefundInconsistent()); + } +}