158 lines
5.6 KiB
PHP
158 lines
5.6 KiB
PHP
<?php
|
||
|
||
namespace App\Models;
|
||
|
||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||
use Illuminate\Database\Eloquent\Model;
|
||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||
|
||
class PlatformOrder extends Model
|
||
{
|
||
use HasFactory;
|
||
|
||
public function orderTypeLabel(): string
|
||
{
|
||
$labels = [
|
||
'new_purchase' => '新购',
|
||
'renewal' => '续费',
|
||
'upgrade' => '升级',
|
||
'downgrade' => '降级',
|
||
];
|
||
|
||
$type = (string) ($this->order_type ?? '');
|
||
|
||
return (string) ($labels[$type] ?? $type);
|
||
}
|
||
|
||
public function receiptTotal(): float
|
||
{
|
||
// 优先读扁平字段 payment_summary.total_amount(更稳定、避免遍历 receipts)
|
||
$total = (float) (data_get($this->meta, 'payment_summary.total_amount') ?? 0);
|
||
if ($total > 0) {
|
||
return $total;
|
||
}
|
||
|
||
// 回退:遍历 payment_receipts[].amount
|
||
$receipts = (array) (data_get($this->meta, 'payment_receipts', []) ?? []);
|
||
$sum = 0.0;
|
||
foreach ($receipts as $r) {
|
||
$sum += (float) (data_get($r, 'amount') ?? 0);
|
||
}
|
||
|
||
return $sum;
|
||
}
|
||
|
||
public function hasLedgerEvidence(): bool
|
||
{
|
||
return (data_get($this->meta, 'payment_summary.total_amount') !== null)
|
||
|| (data_get($this->meta, 'payment_receipts.0.amount') !== null)
|
||
|| (data_get($this->meta, 'refund_summary.total_amount') !== null)
|
||
|| (data_get($this->meta, 'refund_receipts.0.amount') !== null);
|
||
}
|
||
|
||
public function refundTotal(): float
|
||
{
|
||
// 优先读扁平字段 refund_summary.total_amount
|
||
$total = data_get($this->meta, 'refund_summary.total_amount');
|
||
if ($total !== null) {
|
||
return (float) $total;
|
||
}
|
||
|
||
// 回退:遍历 refund_receipts[].amount
|
||
$refunds = (array) (data_get($this->meta, 'refund_receipts', []) ?? []);
|
||
$sum = 0.0;
|
||
foreach ($refunds as $r) {
|
||
$sum += (float) (data_get($r, 'amount') ?? 0);
|
||
}
|
||
|
||
return $sum;
|
||
}
|
||
|
||
public function isRefundInconsistent(): bool
|
||
{
|
||
// 口径与平台订单列表 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') {
|
||
// 已退款但退款总额不足:refund_total + tol < paid
|
||
return ($refundCents + $tolCents) < $paidCents;
|
||
}
|
||
|
||
// 非已退款但退款总额已达到/超过已付:refund_total >= paid + tol
|
||
return $paidCents > 0 && $refundCents >= ($paidCents + $tolCents);
|
||
}
|
||
|
||
public function isReconcileMismatch(): bool
|
||
{
|
||
// 口径与平台订单列表 reconcile_mismatch 保持一致:支付回执总额 与订单 paid_amount 不一致(按分取整,差额>=容差)
|
||
//
|
||
// 重要:若该订单尚无任何“回执证据”(payment_summary/payment_receipts 都为空),则不判定为对账不一致。
|
||
// 原因:测试/人工补数据场景下,订单可能已标记 paid,但尚未沉淀回执;此时应通过 receipt_status=none 暴露问题,
|
||
// 而不是把它强制归入 reconcile_mismatch 并阻断正常 SOP。
|
||
$hasSummary = data_get($this->meta, 'payment_summary.total_amount') !== null;
|
||
$hasReceipt = data_get($this->meta, 'payment_receipts.0.amount') !== null;
|
||
if (! $hasSummary && ! $hasReceipt) {
|
||
return false;
|
||
}
|
||
|
||
$receiptCents = (int) round(((float) $this->receiptTotal()) * 100);
|
||
$paidCents = (int) round(((float) ($this->paid_amount ?? 0)) * 100);
|
||
|
||
$tol = (float) config('saasshop.amounts.tolerance', 0.01);
|
||
$tolCents = (int) round($tol * 100);
|
||
// 以“分”为最小粒度,至少 1 分(若配置 0,则视为要求严格一致)
|
||
$tolCents = max(1, $tolCents);
|
||
|
||
return abs($receiptCents - $paidCents) >= $tolCents;
|
||
}
|
||
|
||
protected $fillable = [
|
||
'merchant_id', 'plan_id', 'site_subscription_id', 'created_by_admin_id', 'order_no', 'order_type', 'status',
|
||
'payment_status', 'payment_channel', 'plan_name', 'billing_cycle', 'period_months', 'quantity', 'list_amount',
|
||
'discount_amount', 'payable_amount', 'paid_amount', 'placed_at', 'paid_at', 'activated_at', 'cancelled_at',
|
||
'refunded_at', 'plan_snapshot', 'meta', 'remark',
|
||
];
|
||
|
||
protected $casts = [
|
||
'list_amount' => 'decimal:2',
|
||
'discount_amount' => 'decimal:2',
|
||
'payable_amount' => 'decimal:2',
|
||
'paid_amount' => 'decimal:2',
|
||
'placed_at' => 'datetime',
|
||
'paid_at' => 'datetime',
|
||
'activated_at' => 'datetime',
|
||
'cancelled_at' => 'datetime',
|
||
'refunded_at' => 'datetime',
|
||
'plan_snapshot' => 'array',
|
||
'meta' => 'array',
|
||
];
|
||
|
||
public function merchant(): BelongsTo
|
||
{
|
||
return $this->belongsTo(Merchant::class, 'merchant_id');
|
||
}
|
||
|
||
public function plan(): BelongsTo
|
||
{
|
||
return $this->belongsTo(Plan::class);
|
||
}
|
||
|
||
public function siteSubscription(): BelongsTo
|
||
{
|
||
return $this->belongsTo(SiteSubscription::class);
|
||
}
|
||
|
||
public function creator(): BelongsTo
|
||
{
|
||
return $this->belongsTo(Admin::class, 'created_by_admin_id');
|
||
}
|
||
}
|