Files
saasshop/app/Models/PlatformOrder.php

158 lines
5.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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');
}
}