fix: 账单冲减复用订单售后费用口径 #146
@ -45,6 +45,7 @@ use Modules\EcommerceFinance\Repositories\SupplierPayableReportRepository;
|
||||
use Modules\EcommerceFinance\Repositories\TmallTaobaoSpreadBillRepository;
|
||||
use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory;
|
||||
use Modules\EcommerceFinance\View\PlatformPromotionAccountPageData;
|
||||
use Modules\EcommerceFinance\View\OrderDataPageData;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
|
||||
use Throwable;
|
||||
@ -255,24 +256,11 @@ class Pages extends Controller
|
||||
$section = collect(SidebarPermissions::navigationFor(user()))->firstWhere('slug', 'order-data');
|
||||
abort_unless($section && collect($section['pages'] ?? [])->contains('slug', 'bill-offset'), 403, '当前账号没有该页面权限。');
|
||||
|
||||
$normalizeDate = static function (mixed $value): string {
|
||||
$date = trim((string) $value);
|
||||
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $date, $matches) !== 1
|
||||
|| ! checkdate((int) $matches[2], (int) $matches[3], (int) $matches[1])) {
|
||||
return '';
|
||||
}
|
||||
return $date;
|
||||
};
|
||||
$filters = [
|
||||
'from' => $normalizeDate($request->query('from')) ?: now()->subDays(6)->toDateString(),
|
||||
'to' => $normalizeDate($request->query('to')) ?: now()->toDateString(),
|
||||
'order_no' => mb_substr(trim((string) $request->query('order_no', '')), 0, 100),
|
||||
'shop_name' => mb_substr(trim((string) $request->query('shop_name', '')), 0, 100),
|
||||
'sku_id' => mb_substr(trim((string) $request->query('sku_id', '')), 0, 100),
|
||||
];
|
||||
$repository = app(BillOffsetPageData::class);
|
||||
$filters = $request->query();
|
||||
$result = $repository->data($filters, 1, 100);
|
||||
abort_if($result['billOffsetError'] !== null, 503, $result['billOffsetError']);
|
||||
$filters = $result['billOffsetFilters'];
|
||||
|
||||
return response()->streamDownload(function () use ($repository, $filters, $result): void {
|
||||
$csv = fopen('php://output', 'w');
|
||||
@ -287,21 +275,27 @@ class Pages extends Controller
|
||||
'shop_style_code' => '店铺款式编码', 'account_500112' => '500112冲减收入',
|
||||
];
|
||||
fputcsv($csv, array_values($labels));
|
||||
$page = 1;
|
||||
$current = $result;
|
||||
$page = 1;
|
||||
do {
|
||||
foreach ($current['billOffsetRows'] as $row) {
|
||||
fputcsv($csv, array_map(
|
||||
fn (string $field): string => $field === 'account_500112'
|
||||
? (string) (data_get($row, $field) ?? '')
|
||||
: ($field === 'shop_name'
|
||||
? $this->csvText(data_get($row, $field) ?? '')
|
||||
: $this->csvIdentifier(data_get($row, $field) ?? '')),
|
||||
function (string $field) use ($row): string {
|
||||
$value = data_get($row, $field);
|
||||
|
||||
if ($field === 'account_500112') {
|
||||
return $value === null || $value === '' ? '' : number_format((float) $value, 2, '.', '');
|
||||
}
|
||||
|
||||
return $field === 'shop_name'
|
||||
? $this->csvText($value)
|
||||
: $this->csvIdentifier($value);
|
||||
},
|
||||
array_keys($labels),
|
||||
));
|
||||
}
|
||||
if (! $current['billOffsetRows']->hasMorePages()) break;
|
||||
$current = $repository->data($filters, ++$page, 100);
|
||||
$current = $repository->dataForNormalizedFilters($filters, ++$page, 100);
|
||||
if ($current['billOffsetError'] !== null) {
|
||||
throw new \RuntimeException('账单冲减导出未完成,请重试。');
|
||||
}
|
||||
|
||||
@ -2,34 +2,86 @@
|
||||
|
||||
namespace Modules\EcommerceFinance\View;
|
||||
|
||||
/** 账单冲减复用原始数据的订单售后费用明细,不重新推断科目。 */
|
||||
use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Throwable;
|
||||
|
||||
class BillOffsetPageData
|
||||
{
|
||||
public function __construct(private readonly OrderDataPageData $orders)
|
||||
public function __construct(private readonly OrderDataPageData $orderData)
|
||||
{
|
||||
}
|
||||
|
||||
/** 账单冲减复用原始数据的订单售后费用明细,不重新推断科目。 */
|
||||
public function page(): array
|
||||
{
|
||||
return ['view' => 'business.pages.order-data.bill-offset'] + $this->data();
|
||||
}
|
||||
|
||||
public function data(?array $filters = null, ?int $page = null, ?int $limit = null): array
|
||||
/** @param array<string,mixed>|null $input */
|
||||
public function data(?array $input = null, ?int $page = null, ?int $limit = null): array
|
||||
{
|
||||
$source = $this->orders->afterSales($filters, $page, $limit, false);
|
||||
$rows = $source['orderDataRows'];
|
||||
$amounts = collect($rows->items())->pluck('account_500112');
|
||||
// 未接入的科目值保留为空;只有本页金额都已知时才显示合计。
|
||||
$filters = $this->orderData->afterSalesFilters($input);
|
||||
|
||||
return $this->dataForNormalizedFilters(
|
||||
$filters,
|
||||
$page ?? (int) request('page', 1),
|
||||
$limit ?? (int) setting('default.list_limit', 25),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reuse an already normalized filter set for subsequent export pages.
|
||||
*
|
||||
* @param array{from:string,to:string,order_no:string,shop_name:string,sku_id:string} $filters
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function dataForNormalizedFilters(array $filters, int $page, int $limit): array
|
||||
{
|
||||
$page = max($page, 1);
|
||||
$limit = min(max($limit, 1), 100);
|
||||
$rows = $this->emptyPaginator($page, $limit);
|
||||
$status = null;
|
||||
$error = null;
|
||||
|
||||
try {
|
||||
$result = $this->orderData->afterSalesRows($filters, $page, $limit);
|
||||
$rows = new Paginator(
|
||||
Collection::make($result['rows'] ?? [])->map(static fn (array $row): object => (object) $row),
|
||||
$limit,
|
||||
$page,
|
||||
[
|
||||
'path' => request()->url(),
|
||||
'query' => request()->except('page'),
|
||||
],
|
||||
);
|
||||
$rows->hasMorePagesWhen((bool) ($result['has_more'] ?? false));
|
||||
$status = data_get($result, 'status.X-Caiwu-Data-Status');
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
$error = '订单售后费用数据暂时无法读取,请稍后重试。';
|
||||
}
|
||||
|
||||
$amounts = Collection::make($rows->items())->pluck('account_500112');
|
||||
// 500112 未接入时保留为空;本页有任何未知金额时不生成误导性合计。
|
||||
$amount = $amounts->isNotEmpty() && $amounts->every(static fn ($value): bool => is_numeric($value))
|
||||
? round((float) $amounts->sum(), 2)
|
||||
: null;
|
||||
|
||||
return [
|
||||
'billOffsetFilters' => $filters,
|
||||
'billOffsetRows' => $rows,
|
||||
'billOffsetFilters' => $source['orderDataFilters'],
|
||||
'billOffsetSummary' => ['count' => $rows->count(), 'amount' => $amount],
|
||||
'billOffsetStatus' => $source['orderDataStatus']['X-Caiwu-Data-Status'] ?? null,
|
||||
'billOffsetError' => $source['orderDataError'],
|
||||
'billOffsetStatus' => $status,
|
||||
'billOffsetError' => $error,
|
||||
];
|
||||
}
|
||||
|
||||
private function emptyPaginator(int $page, int $limit): Paginator
|
||||
{
|
||||
return new Paginator([], $limit, $page, [
|
||||
'path' => request()->url(),
|
||||
'query' => request()->except('page'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,42 +25,23 @@ class OrderDataPageData
|
||||
|
||||
public function afterSales(?array $filters = null, ?int $page = null, ?int $limit = null, bool $includeWorkOrders = true): array
|
||||
{
|
||||
$filters ??= [
|
||||
'from' => $this->date(request('from')) ?: now()->subDays(6)->toDateString(),
|
||||
'to' => $this->date(request('to')) ?: now()->toDateString(),
|
||||
'order_no' => $this->text(request('order_no')),
|
||||
'shop_name' => $this->text(request('shop_name')),
|
||||
'sku_id' => $this->text(request('sku_id')),
|
||||
];
|
||||
$filters = $this->afterSalesFilters($filters);
|
||||
[$requestPage, $requestLimit] = $this->pageSettings();
|
||||
$page = max($page ?? $requestPage, 1);
|
||||
$limit = min(max($limit ?? $requestLimit, 1), 100);
|
||||
|
||||
try {
|
||||
$mirror = $this->mirrorOrders ?? app(OrderDataMirrorRepository::class);
|
||||
if ($mirror->ready()) {
|
||||
$result = $mirror->afterSales($filters, $page, $limit);
|
||||
|
||||
return [
|
||||
'orderDataRows' => $this->paginator((array) ($result['rows'] ?? []), $page, $limit),
|
||||
'orderDataFilters' => $filters,
|
||||
'orderDataStatus' => ['X-Caiwu-Data-Status' => 'mirror'],
|
||||
'orderDataError' => null,
|
||||
'afterSalesWorkOrders' => $includeWorkOrders ? $this->afterSalesWorkOrders($filters) : $this->emptyAfterSalesWorkOrders(),
|
||||
];
|
||||
}
|
||||
|
||||
$cache = $this->caches->forSource(self::SOURCE);
|
||||
$result = $cache->remember(
|
||||
'order-data.after-sales.list',
|
||||
compact('filters', 'page', 'limit'),
|
||||
fn () => $this->orders->afterSales($filters, $page, $limit),
|
||||
);
|
||||
$result = $this->afterSalesRows($filters, $page, $limit);
|
||||
|
||||
return [
|
||||
'orderDataRows' => $this->paginator((array) ($result['rows'] ?? []), $page, $limit),
|
||||
'orderDataRows' => $this->paginator(
|
||||
(array) ($result['rows'] ?? []),
|
||||
$page,
|
||||
$limit,
|
||||
(bool) ($result['has_more'] ?? false),
|
||||
),
|
||||
'orderDataFilters' => $filters,
|
||||
'orderDataStatus' => $cache->responseHeaders(),
|
||||
'orderDataStatus' => $result['status'] ?? [],
|
||||
'orderDataError' => null,
|
||||
'afterSalesWorkOrders' => $includeWorkOrders ? $this->afterSalesWorkOrders($filters) : $this->emptyAfterSalesWorkOrders(),
|
||||
];
|
||||
@ -73,6 +54,66 @@ class OrderDataPageData
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the same normalized filters used by the raw after-sales page.
|
||||
* Exporters and sibling views must use this contract instead of rebuilding
|
||||
* date and text normalization independently.
|
||||
*
|
||||
* @param array<string,mixed>|null $input
|
||||
* @return array{from:string,to:string,order_no:string,shop_name:string,sku_id:string}
|
||||
*/
|
||||
public function afterSalesFilters(?array $input = null): array
|
||||
{
|
||||
$input ??= request()->query();
|
||||
|
||||
return [
|
||||
'from' => $this->date($input['from'] ?? null) ?: now()->subDays(6)->toDateString(),
|
||||
'to' => $this->date($input['to'] ?? null) ?: now()->toDateString(),
|
||||
'order_no' => $this->text($input['order_no'] ?? null),
|
||||
'shop_name' => $this->text($input['shop_name'] ?? null),
|
||||
'sku_id' => $this->text($input['sku_id'] ?? null),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch raw after-sales rows for a page. Both the page and CSV export use
|
||||
* this method so source selection, filters, joins and row normalization stay
|
||||
* identical. Repositories over-fetch one row to determine whether another
|
||||
* page exists; the sentinel is removed before this shared contract returns.
|
||||
*
|
||||
* @return array{rows:array<int,array<string,mixed>>,has_more:bool,status:array<string,string>}
|
||||
*/
|
||||
public function afterSalesRows(array $filters, int $page, int $limit): array
|
||||
{
|
||||
$limit = min(max($limit, 1), 100);
|
||||
$mirror = $this->mirrorOrders ?? app(OrderDataMirrorRepository::class);
|
||||
if ($mirror->ready()) {
|
||||
$result = $mirror->afterSales($filters, $page, $limit);
|
||||
$rows = array_values((array) ($result['rows'] ?? []));
|
||||
$hasMore = count($rows) > $limit;
|
||||
|
||||
return [
|
||||
'rows' => array_slice($rows, 0, $limit),
|
||||
'has_more' => $hasMore,
|
||||
'status' => ['X-Caiwu-Data-Status' => 'mirror'],
|
||||
];
|
||||
}
|
||||
|
||||
$cache = $this->caches->forSource(self::SOURCE);
|
||||
$result = $cache->remember(
|
||||
'order-data.after-sales.list',
|
||||
compact('filters', 'page', 'limit'),
|
||||
fn () => $this->orders->afterSales($filters, $page, $limit),
|
||||
);
|
||||
$rows = array_values((array) ($result['rows'] ?? []));
|
||||
|
||||
return [
|
||||
'rows' => array_slice($rows, 0, $limit),
|
||||
'has_more' => count($rows) > $limit,
|
||||
'status' => $cache->responseHeaders(),
|
||||
];
|
||||
}
|
||||
|
||||
public function promotionExpenses(): array
|
||||
{
|
||||
$filters = [
|
||||
@ -165,9 +206,9 @@ class OrderDataPageData
|
||||
];
|
||||
}
|
||||
|
||||
private function paginator(array $rows, int $page, int $limit): Paginator
|
||||
private function paginator(array $rows, int $page, int $limit, ?bool $hasMore = null): Paginator
|
||||
{
|
||||
return new Paginator(
|
||||
$paginator = new Paginator(
|
||||
Collection::make($rows)->map(static fn (array $row): object => (object) $row),
|
||||
$limit,
|
||||
$page,
|
||||
@ -176,6 +217,12 @@ class OrderDataPageData
|
||||
'query' => request()->except('page'),
|
||||
],
|
||||
);
|
||||
|
||||
if ($hasMore !== null) {
|
||||
$paginator->hasMorePagesWhen($hasMore);
|
||||
}
|
||||
|
||||
return $paginator;
|
||||
}
|
||||
|
||||
/** @return array{0:int,1:int} */
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
@php
|
||||
$rows = $billOffsetRows ?? null;
|
||||
$filters = $billOffsetFilters ?? [];
|
||||
$summary = $billOffsetSummary ?? ['amount' => 0, 'count' => 0];
|
||||
$summary = $billOffsetSummary ?? ['amount' => null, 'count' => 0];
|
||||
$status = $billOffsetStatus ?? null;
|
||||
$columns = [
|
||||
'internal_order_no' => '内部订单号',
|
||||
@ -31,15 +31,15 @@
|
||||
|
||||
<form method="GET" action="{{ request()->url() }}" class="upstream-data-toolbar" aria-label="账单冲减筛选">
|
||||
<div class="upstream-data-field">
|
||||
<label for="bill-offset-from">开始日期</label>
|
||||
<div class="bill-offset-date-picker">
|
||||
<label for="bill-offset-from">订单开始日期</label>
|
||||
<div class="bill-offset-date-picker" data-date-picker-contract="order-data-after-sales-date-picker">
|
||||
<input id="bill-offset-from" name="from" type="date" value="{{ $filters['from'] ?? '' }}">
|
||||
<button type="button" data-date-picker-input="bill-offset-from" aria-label="选择开始日期" title="选择开始日期"><span class="material-icons-outlined" aria-hidden="true">calendar_today</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="upstream-data-field">
|
||||
<label for="bill-offset-to">结束日期</label>
|
||||
<div class="bill-offset-date-picker">
|
||||
<label for="bill-offset-to">订单结束日期</label>
|
||||
<div class="bill-offset-date-picker" data-date-picker-contract="order-data-after-sales-date-picker">
|
||||
<input id="bill-offset-to" name="to" type="date" value="{{ $filters['to'] ?? '' }}">
|
||||
<button type="button" data-date-picker-input="bill-offset-to" aria-label="选择结束日期" title="选择结束日期"><span class="material-icons-outlined" aria-hidden="true">calendar_today</span></button>
|
||||
</div>
|
||||
@ -72,7 +72,7 @@
|
||||
<div class="upstream-data-table-panel">
|
||||
<div class="bill-offset-summary" aria-label="账单冲减汇总">
|
||||
<div class="bill-offset-summary__item"><span>本页记录数</span><strong class="bill-offset-summary__value">{{ number_format((int) ($summary['count'] ?? 0)) }}</strong></div>
|
||||
<div class="bill-offset-summary__item"><span>本页冲减收入合计</span><strong class="bill-offset-summary__value">{{ $summary['amount'] === null ? '-' : number_format((float) $summary['amount'], 2) }}</strong></div>
|
||||
<div class="bill-offset-summary__item"><span>本页 500112 冲减收入合计</span><strong class="bill-offset-summary__value">{{ ($summary['amount'] ?? null) === null ? '-' : number_format((float) $summary['amount'], 2) }}</strong></div>
|
||||
</div>
|
||||
@if ($status)
|
||||
<div class="bill-offset-sync">数据状态:{{ strtoupper((string) $status) }}</div>
|
||||
@ -85,7 +85,7 @@
|
||||
<tr>
|
||||
@foreach ($columns as $field => $header)
|
||||
@php $value = data_get($row, $field); @endphp
|
||||
<td data-export-value="{{ $value ?? '' }}" @class(['is-number' => $field === 'account_500112'])>{{ ($value === null || $value === '') ? '-' : ($field === 'account_500112' ? number_format((float) $value, 2) : $value) }}</td>
|
||||
<td data-export-value="{{ $value ?? '' }}" data-export-number="{{ $field === 'account_500112' ? 'true' : 'false' }}" @class(['is-number' => $field === 'account_500112'])>{{ ($value === null || $value === '') ? '-' : ($field === 'account_500112' ? number_format((float) $value, 2) : $value) }}</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@empty
|
||||
@ -95,7 +95,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<div class="upstream-data-pagination">
|
||||
<span>当前页 {{ $rows?->count() ?? 0 }} 条</span>
|
||||
<span>当前页 {{ $rows?->count() ?? 0 }} 条 @if ($status) · {{ strtoupper((string) $status) }} @endif</span>
|
||||
<div class="upstream-data-pagination__controls">
|
||||
<a href="{{ $rows?->previousPageUrl() ?? '#' }}" @class(['upstream-data-button', 'is-disabled' => !$rows || $rows->onFirstPage()]) aria-label="上一页"> <span class="material-icons-outlined" aria-hidden="true">chevron_left</span></a>
|
||||
<span>第 {{ $rows?->currentPage() ?? 1 }} 页</span>
|
||||
|
||||
@ -4,11 +4,11 @@ namespace Tests\Feature\Business;
|
||||
|
||||
use App\Models\Auth\Permission;
|
||||
use Mockery;
|
||||
use Modules\EcommerceFinance\View\OrderDataPageData;
|
||||
use Modules\EcommerceFinance\Repositories\CourierFreightReconciliationMirrorRepository;
|
||||
use Modules\EcommerceFinance\Repositories\NonJingxiRefundMirrorRepository;
|
||||
use Modules\EcommerceFinance\Repositories\StoreReceivingAlipayBillMirrorRepository;
|
||||
use Modules\EcommerceFinance\Repositories\StoreReceivingDouyinBillMirrorRepository;
|
||||
use Modules\EcommerceFinance\View\OrderDataPageData;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
class PagesTest extends FeatureTestCase
|
||||
@ -323,6 +323,17 @@ class PagesTest extends FeatureTestCase
|
||||
Permission::where('name', 'read-admin-panel')->firstOrFail()
|
||||
);
|
||||
|
||||
$filters = [
|
||||
'from' => now()->subDays(6)->toDateString(), 'to' => now()->toDateString(),
|
||||
'order_no' => '', 'shop_name' => '', 'sku_id' => '',
|
||||
];
|
||||
$orderData = Mockery::mock(OrderDataPageData::class);
|
||||
$orderData->shouldReceive('afterSalesFilters')->zeroOrMoreTimes()->andReturn($filters);
|
||||
$orderData->shouldReceive('afterSalesRows')->zeroOrMoreTimes()->andReturn([
|
||||
'rows' => [], 'has_more' => false, 'status' => ['X-Caiwu-Data-Status' => 'direct'],
|
||||
]);
|
||||
$this->app->instance(OrderDataPageData::class, $orderData);
|
||||
|
||||
$this->loginAs();
|
||||
|
||||
$this->get(route('business.pages.show', [
|
||||
@ -348,7 +359,8 @@ class PagesTest extends FeatureTestCase
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSeeText('账单冲减')
|
||||
->assertSeeText('账单冲减镜像尚未完成首次同步');
|
||||
->assertSeeText('暂无数据')
|
||||
->assertDontSeeText('账单冲减镜像尚未完成首次同步');
|
||||
|
||||
$this->withExceptionHandling();
|
||||
|
||||
@ -362,55 +374,121 @@ class PagesTest extends FeatureTestCase
|
||||
}
|
||||
}
|
||||
|
||||
private function billOffsetSourceRows(array $rows, array $filters = [], bool $hasMore = false, ?string $error = null): array
|
||||
public function testBillOffsetPageUsesAfterSalesRowsAndTheOriginalAccountField(): void
|
||||
{
|
||||
$paginator = new \Illuminate\Pagination\Paginator($rows, 100, 1);
|
||||
$paginator->hasMorePagesWhen($hasMore);
|
||||
|
||||
return [
|
||||
'orderDataRows' => $paginator,
|
||||
'orderDataFilters' => $filters,
|
||||
'orderDataStatus' => ['X-Caiwu-Data-Status' => 'mirror'],
|
||||
'orderDataError' => $error,
|
||||
$this->user->attachPermission(
|
||||
Permission::where('name', 'read-admin-panel')->firstOrFail()
|
||||
);
|
||||
$filters = [
|
||||
'from' => '2026-08-01', 'to' => '2026-08-31', 'order_no' => '', 'shop_name' => '', 'sku_id' => '',
|
||||
];
|
||||
}
|
||||
|
||||
public function testBillOffsetPageUsesTheSameOriginalOrderRowsAndAccountField(): void
|
||||
{
|
||||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||||
$source = Mockery::mock(OrderDataPageData::class);
|
||||
$source->shouldReceive('afterSales')->once()->with(null, null, null, false)->andReturn($this->billOffsetSourceRows([
|
||||
['internal_order_no' => '9228703', 'online_order_no' => '5127724585360006527',
|
||||
'shop_name' => '测试店铺', 'product_code' => 'SKU-1', 'account_500112' => -20,
|
||||
'reversal_order_amount' => 987.65],
|
||||
], ['from' => '2026-08-01', 'to' => '2026-08-31']));
|
||||
$this->app->instance(OrderDataPageData::class, $source);
|
||||
$orderData = Mockery::mock(OrderDataPageData::class);
|
||||
$orderData->shouldReceive('afterSalesFilters')->once()->andReturn($filters);
|
||||
$orderData->shouldReceive('afterSalesRows')->once()->with($filters, 1, 25)->andReturn([
|
||||
'rows' => [[
|
||||
'internal_order_no' => '9228703', 'online_order_no' => '5127724585360006527',
|
||||
'shop_name' => '天猫-亿家达旗舰店', 'shop_code' => 'SHOP-1', 'product_code' => 'DNZ005374N79V01',
|
||||
'style_code' => 'STYLE-1', 'shop_product_code' => 'SP-1', 'shop_style_code' => 'SI-1', 'account_500112' => -20,
|
||||
]],
|
||||
'has_more' => false, 'status' => ['X-Caiwu-Data-Status' => 'direct'],
|
||||
]);
|
||||
$this->app->instance(OrderDataPageData::class, $orderData);
|
||||
|
||||
$this->loginAs()->get(route('business.pages.show', [
|
||||
'section' => 'order-data', 'page' => 'bill-offset', 'from' => '2026-08-01', 'to' => '2026-08-31',
|
||||
]))->assertOk()->assertSeeText('9228703')->assertSeeText('测试店铺')->assertSeeText('-20.00')
|
||||
->assertDontSeeText('987.65')->assertSeeText('本页冲减收入合计')
|
||||
->assertSee('id="bill-offset-export-button"', false)->assertSeeText('数据状态:MIRROR');
|
||||
]))->assertOk()
|
||||
->assertSeeText('9228703')
|
||||
->assertSeeText('天猫-亿家达旗舰店')
|
||||
->assertSeeText('-20.00')
|
||||
->assertSeeText('本页 500112 冲减收入合计')
|
||||
->assertSee(e(route('business.bill-offset.export', [
|
||||
'from' => '2026-08-01',
|
||||
'to' => '2026-08-31',
|
||||
])), false)
|
||||
->assertSee('id="bill-offset-export-button"', false)
|
||||
->assertSeeText('数据状态:DIRECT');
|
||||
}
|
||||
|
||||
public function testBillOffsetKeepsUnknownAccountAmountsEmpty(): void
|
||||
public function testBillOffsetPageKeepsEmptyAccountFieldAsDash(): void
|
||||
{
|
||||
$this->user->attachPermission(
|
||||
Permission::where('name', 'read-admin-panel')->firstOrFail()
|
||||
);
|
||||
$filters = [
|
||||
'from' => '2026-08-01', 'to' => '2026-08-31', 'order_no' => '', 'shop_name' => '', 'sku_id' => '',
|
||||
];
|
||||
$orderData = Mockery::mock(OrderDataPageData::class);
|
||||
$orderData->shouldReceive('afterSalesFilters')->once()->andReturn($filters);
|
||||
$orderData->shouldReceive('afterSalesRows')->once()->with($filters, 1, 25)->andReturn([
|
||||
'rows' => [[
|
||||
'internal_order_no' => 'ORDER-EMPTY', 'online_order_no' => 'ONLINE-EMPTY',
|
||||
'shop_name' => '空值店铺', 'account_500112' => null,
|
||||
]],
|
||||
'has_more' => false, 'status' => ['X-Caiwu-Data-Status' => 'direct'],
|
||||
]);
|
||||
$this->app->instance(OrderDataPageData::class, $orderData);
|
||||
|
||||
$this->loginAs()->get(route('business.pages.show', [
|
||||
'section' => 'order-data', 'page' => 'bill-offset', 'from' => '2026-08-01', 'to' => '2026-08-31',
|
||||
]))->assertOk()
|
||||
->assertSeeText('ORDER-EMPTY')
|
||||
->assertSeeText('空值店铺')
|
||||
->assertSeeText('本页 500112 冲减收入合计')
|
||||
->assertSee('data-export-value=""', false)
|
||||
->assertSeeText('-')
|
||||
->assertDontSeeText('0.00');
|
||||
}
|
||||
|
||||
public function testBillOffsetPageShowsReadFailureSeparatelyFromAnUnreadyMirror(): void
|
||||
{
|
||||
$this->user->attachPermission(
|
||||
Permission::where('name', 'read-admin-panel')->firstOrFail()
|
||||
);
|
||||
$filters = [
|
||||
'from' => now()->subDays(6)->toDateString(), 'to' => now()->toDateString(),
|
||||
'order_no' => '', 'shop_name' => '', 'sku_id' => '',
|
||||
];
|
||||
$orderData = Mockery::mock(OrderDataPageData::class);
|
||||
$orderData->shouldReceive('afterSalesFilters')->once()->andReturn($filters);
|
||||
$orderData->shouldReceive('afterSalesRows')->once()->andThrow(new \RuntimeException('database unavailable'));
|
||||
$this->app->instance(OrderDataPageData::class, $orderData);
|
||||
|
||||
$this->loginAs()->get(route('business.pages.show', [
|
||||
'section' => 'order-data', 'page' => 'bill-offset',
|
||||
]))->assertOk()
|
||||
->assertSeeText('订单售后费用数据暂时无法读取,请稍后重试。');
|
||||
}
|
||||
|
||||
public function testBillOffsetExportKeepsUnknownAccountAmountsEmpty(): void
|
||||
{
|
||||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||||
$source = Mockery::mock(OrderDataPageData::class);
|
||||
$source->shouldReceive('afterSales')->once()->andReturn($this->billOffsetSourceRows([
|
||||
['internal_order_no' => 'ORDER-EMPTY', 'account_500112' => null, 'reversal_order_amount' => 987.65],
|
||||
]));
|
||||
$this->app->instance(OrderDataPageData::class, $source);
|
||||
$this->loginAs()->get(route('business.pages.show', ['section' => 'order-data', 'page' => 'bill-offset']))
|
||||
->assertOk()->assertSeeText('ORDER-EMPTY')->assertDontSeeText('987.65')->assertDontSeeText('0.00');
|
||||
$filters = [
|
||||
'from' => '2026-08-01', 'to' => '2026-08-31', 'order_no' => 'ORDER-EMPTY', 'shop_name' => '', 'sku_id' => '',
|
||||
];
|
||||
$orderData = Mockery::mock(OrderDataPageData::class);
|
||||
$orderData->shouldReceive('afterSalesFilters')->once()->with($filters)->andReturn($filters);
|
||||
$orderData->shouldReceive('afterSalesRows')->once()->with($filters, 1, 100)->andReturn([
|
||||
'rows' => [[
|
||||
'internal_order_no' => 'ORDER-EMPTY', 'account_500112' => null, 'reversal_order_amount' => 987.65,
|
||||
]],
|
||||
'has_more' => false, 'status' => ['X-Caiwu-Data-Status' => 'direct'],
|
||||
]);
|
||||
$this->app->instance(OrderDataPageData::class, $orderData);
|
||||
|
||||
$csv = $this->loginAs()->get(route('business.bill-offset.export', $filters))->streamedContent();
|
||||
$this->assertStringNotContainsString('987.65', $csv);
|
||||
$lines = preg_split('/\r?\n/', trim($csv));
|
||||
$this->assertSame('', str_getcsv($lines[1])[8]);
|
||||
}
|
||||
|
||||
public function testBillOffsetShowsTheOriginalDataErrorAndBlocksAnEmptyExport(): void
|
||||
{
|
||||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||||
$source = Mockery::mock(OrderDataPageData::class);
|
||||
$source->shouldReceive('afterSales')->twice()->andReturn($this->billOffsetSourceRows([], [], false, '订单售后费用数据暂时无法读取,请稍后重试。'));
|
||||
$this->app->instance(OrderDataPageData::class, $source);
|
||||
$filters = ['from' => '2026-08-01', 'to' => '2026-08-31', 'order_no' => '', 'shop_name' => '', 'sku_id' => ''];
|
||||
$orderData = Mockery::mock(OrderDataPageData::class);
|
||||
$orderData->shouldReceive('afterSalesFilters')->twice()->andReturn($filters);
|
||||
$orderData->shouldReceive('afterSalesRows')->twice()->andThrow(new \RuntimeException('database unavailable'));
|
||||
$this->app->instance(OrderDataPageData::class, $orderData);
|
||||
$this->loginAs()->get(route('business.pages.show', ['section' => 'order-data', 'page' => 'bill-offset']))
|
||||
->assertOk()->assertSeeText('订单售后费用数据暂时无法读取,请稍后重试。');
|
||||
$this->withExceptionHandling()->get(route('business.bill-offset.export'))->assertStatus(503);
|
||||
@ -419,19 +497,29 @@ class PagesTest extends FeatureTestCase
|
||||
public function testBillOffsetExportIncludesAllPagesFromOriginalDataWithTheSameFilters(): void
|
||||
{
|
||||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||||
$source = Mockery::mock(OrderDataPageData::class);
|
||||
$filters = ['from' => '2026-08-01', 'to' => '2026-08-31', 'order_no' => 'ORDER', 'shop_name' => '', 'sku_id' => ''];
|
||||
$source->shouldReceive('afterSales')->once()->with($filters, 1, 100, false)->andReturn($this->billOffsetSourceRows([
|
||||
['internal_order_no' => 'ORDER-1', 'online_order_no' => '001234567890123456789', 'shop_name' => '测试店铺', 'account_500112' => -20],
|
||||
], $filters, true));
|
||||
$source->shouldReceive('afterSales')->once()->with($filters, 2, 100, false)->andReturn($this->billOffsetSourceRows([
|
||||
['internal_order_no' => 'ORDER-101', 'account_500112' => null, 'reversal_order_amount' => 987.65],
|
||||
], $filters));
|
||||
$this->app->instance(OrderDataPageData::class, $source);
|
||||
$orderData = Mockery::mock(OrderDataPageData::class);
|
||||
$orderData->shouldReceive('afterSalesFilters')->once()->with($filters)->andReturn($filters);
|
||||
$orderData->shouldReceive('afterSalesRows')->once()->with($filters, 1, 100)->andReturn([
|
||||
'rows' => [[
|
||||
'internal_order_no' => 'ORDER-1', 'online_order_no' => '001234567890123456789',
|
||||
'shop_name' => '测试店铺', 'account_500112' => -20,
|
||||
]],
|
||||
'has_more' => true, 'status' => ['X-Caiwu-Data-Status' => 'direct'],
|
||||
]);
|
||||
$orderData->shouldReceive('afterSalesRows')->once()->with($filters, 2, 100)->andReturn([
|
||||
'rows' => [[
|
||||
'internal_order_no' => 'ORDER-101', 'account_500112' => null, 'reversal_order_amount' => 987.65,
|
||||
]],
|
||||
'has_more' => false, 'status' => ['X-Caiwu-Data-Status' => 'direct'],
|
||||
]);
|
||||
$this->app->instance(OrderDataPageData::class, $orderData);
|
||||
$response = $this->loginAs()->get(route('business.bill-offset.export', $filters));
|
||||
$response->assertOk()->assertHeader('Content-Type', 'text/csv; charset=UTF-8');
|
||||
$csv = $response->streamedContent();
|
||||
$this->assertStringContainsString('500112冲减收入', $csv);
|
||||
$this->assertStringContainsString('测试店铺', $csv);
|
||||
$this->assertStringContainsString('-20.00', $csv);
|
||||
$this->assertStringContainsString("'001234567890123456789", $csv);
|
||||
$this->assertStringContainsString('ORDER-1', $csv);
|
||||
$this->assertStringContainsString('ORDER-101', $csv);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user