修复:商品价格切换经营分析副本库 #49
@ -497,18 +497,14 @@ class Pages extends Controller
|
||||
|
||||
protected function contentForProductPrices(): array
|
||||
{
|
||||
$priceType = trim((string) request('price_type', ''));
|
||||
$costReduction = trim((string) request('cost_reduction', ''));
|
||||
$filters = [
|
||||
'barcode' => mb_substr(trim((string) request('barcode', '')), 0, 191),
|
||||
'price_type' => in_array($priceType, ['pricing', 'repricing'], true) ? $priceType : '',
|
||||
'cost_reduction' => in_array($costReduction, ['yes', 'no'], true) ? $costReduction : '',
|
||||
'effective_from' => $this->dateFilter(request('effective_from')),
|
||||
'effective_to' => $this->dateFilter(request('effective_to')),
|
||||
'completed_from' => $this->dateFilter(request('completed_from')),
|
||||
'completed_to' => $this->dateFilter(request('completed_to')),
|
||||
];
|
||||
$page = max((int) request('page', 1), 1);
|
||||
$limit = min(max((int) setting('default.list_limit', 25), 1), 100);
|
||||
$cache = $this->upstreamCaches->forSource('jiansun');
|
||||
$cache = $this->upstreamCaches->forSource('operations_replica');
|
||||
|
||||
try {
|
||||
$result = $cache->remember(
|
||||
|
||||
@ -6,22 +6,13 @@ use Modules\EcommerceFinance\Services\UpstreamReadOnlyQuery;
|
||||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQueryFactory;
|
||||
|
||||
/**
|
||||
* Supplier product pricing and repricing records from JianSun.
|
||||
* Completed product price-limit approvals from the operations replica.
|
||||
*/
|
||||
class ProductPriceRepository
|
||||
{
|
||||
private const SOURCE = 'jiansun';
|
||||
private const TABLE = '[dbo].[pm_priceAdjustment_log]';
|
||||
private const SOURCE = 'operations_replica';
|
||||
|
||||
private const PRICE_TYPES = [
|
||||
'pricing' => '0',
|
||||
'repricing' => '1',
|
||||
];
|
||||
|
||||
private const COST_REDUCTION = [
|
||||
'yes' => '1',
|
||||
'no' => '0',
|
||||
];
|
||||
private const TABLE = '[rpt].[Bulk_Price_Limit]';
|
||||
|
||||
private readonly UpstreamReadOnlyQuery $query;
|
||||
|
||||
@ -39,54 +30,41 @@ class ProductPriceRepository
|
||||
$bindings = [];
|
||||
|
||||
if (($filters['barcode'] ?? '') !== '') {
|
||||
$where[] = "[skucode] LIKE ? ESCAPE '\\'";
|
||||
$where[] = "[product_code] LIKE ? ESCAPE '\\'";
|
||||
$bindings[] = $this->contains((string) $filters['barcode']);
|
||||
}
|
||||
|
||||
$priceType = self::PRICE_TYPES[(string) ($filters['price_type'] ?? '')] ?? null;
|
||||
|
||||
if ($priceType !== null) {
|
||||
$where[] = '[priceType] = ?';
|
||||
$bindings[] = $priceType;
|
||||
if (($filters['completed_from'] ?? '') !== '') {
|
||||
$where[] = '[process_end_date] >= CAST(? AS date)';
|
||||
$bindings[] = (string) $filters['completed_from'];
|
||||
}
|
||||
|
||||
$costReduction = self::COST_REDUCTION[(string) ($filters['cost_reduction'] ?? '')] ?? null;
|
||||
|
||||
if ($costReduction !== null) {
|
||||
$where[] = '[isCostReduction] = ?';
|
||||
$bindings[] = $costReduction;
|
||||
}
|
||||
|
||||
if (($filters['effective_from'] ?? '') !== '') {
|
||||
$where[] = '[effectStartTime] >= CAST(? AS date)';
|
||||
$bindings[] = (string) $filters['effective_from'];
|
||||
}
|
||||
|
||||
if (($filters['effective_to'] ?? '') !== '') {
|
||||
$where[] = '[effectStartTime] < DATEADD(day, 1, CAST(? AS date))';
|
||||
$bindings[] = (string) $filters['effective_to'];
|
||||
if (($filters['completed_to'] ?? '') !== '') {
|
||||
$where[] = '[process_end_date] < DATEADD(day, 1, CAST(? AS date))';
|
||||
$bindings[] = (string) $filters['completed_to'];
|
||||
}
|
||||
|
||||
$whereSql = $where === [] ? '' : ' WHERE ' . implode(' AND ', $where);
|
||||
$offset = ($page - 1) * $limit;
|
||||
$fetch = $limit + 1;
|
||||
$sql = 'SELECT '
|
||||
. '[skucode] AS barcode, '
|
||||
. "CASE [priceType] WHEN '0' THEN N'定价' WHEN '1' THEN N'变价' ELSE NULLIF([priceType], '') END AS price_type, "
|
||||
. "CASE [priceChangeType] WHEN '0' THEN N'普通变价' WHEN '1' THEN N'清货变价' "
|
||||
. "WHEN '2' THEN N'阶段变价' WHEN '4' THEN N'打折入库' ELSE NULLIF([priceChangeType], '') END AS change_type, "
|
||||
. "CASE [isCostReduction] WHEN '1' THEN N'是' WHEN '0' THEN N'否' ELSE NULL END AS cost_reduction, "
|
||||
. '[originalPrice] AS price_before, '
|
||||
. '[afterPrice] AS price_after, '
|
||||
. '[diffPrice] AS price_diff, '
|
||||
. '[applyUser] AS applicant, '
|
||||
. '[remark] AS remark, '
|
||||
. 'CONVERT(varchar(19), [effectStartTime], 120) AS effective_start, '
|
||||
. 'CONVERT(varchar(19), [effectEndTime], 120) AS effective_end, '
|
||||
. 'CONVERT(varchar(19), COALESCE([updateTime], [createTime]), 120) AS completed_at '
|
||||
. '[product_code] AS barcode, '
|
||||
. '[spu] AS spu, '
|
||||
. '[product_name] AS product_name, '
|
||||
. '[category] AS category, '
|
||||
. '[production_status] AS production_status, '
|
||||
. '[min_price] AS minimum_price, '
|
||||
. '[price_limit] AS price_limit, '
|
||||
. '[purchase_price] AS purchase_price, '
|
||||
. '[shipping_fee] AS shipping_fee, '
|
||||
. '[jingxi_limit_price] AS jingxi_limit_price, '
|
||||
. "CASE [status] WHEN 'COMPLETED' THEN N'已完成' ELSE NULLIF([status], '') END AS approval_status, "
|
||||
. "CASE [result] WHEN 'agree' THEN N'已同意' WHEN 'refuse' THEN N'已拒绝' ELSE NULLIF([result], '') END AS approval_result, "
|
||||
. '[title] AS workflow_title, '
|
||||
. 'CONVERT(varchar(19), [process_end_date], 120) AS completed_at '
|
||||
. 'FROM ' . self::TABLE
|
||||
. $whereSql
|
||||
. ' ORDER BY COALESCE([updateTime], [createTime]) DESC, [Id] DESC, [skucode] ASC'
|
||||
. ' ORDER BY [process_end_date] DESC, [businessId] DESC, [product_code] ASC'
|
||||
. " OFFSET {$offset} ROWS FETCH NEXT {$fetch} ROWS ONLY";
|
||||
|
||||
return ['rows' => array_map([$this, 'normalize'], $this->query->select($sql, $bindings))];
|
||||
@ -100,7 +78,7 @@ class ProductPriceRepository
|
||||
/** @param array<string,mixed> $row */
|
||||
private function normalize(array $row): array
|
||||
{
|
||||
foreach (['price_before', 'price_after', 'price_diff'] as $field) {
|
||||
foreach (['minimum_price', 'price_limit', 'purchase_price', 'shipping_fee', 'jingxi_limit_price'] as $field) {
|
||||
$row[$field] = $row[$field] === null || $row[$field] === ''
|
||||
? null
|
||||
: (float) $row[$field];
|
||||
|
||||
@ -10,7 +10,6 @@ use Modules\EcommerceFinance\Repositories\GoodsReconciliationRepository;
|
||||
use Modules\EcommerceFinance\Repositories\InventoryRepository;
|
||||
use Modules\EcommerceFinance\Repositories\JdWalletBillRepository;
|
||||
use Modules\EcommerceFinance\Repositories\PinduoduoSpreadBillRepository;
|
||||
use Modules\EcommerceFinance\Repositories\ProductPriceRepository;
|
||||
use Modules\EcommerceFinance\Repositories\RawBoardPayablesRepository;
|
||||
use Modules\EcommerceFinance\Repositories\SupplierPayableRepository;
|
||||
use Modules\EcommerceFinance\Repositories\SupplierPayableReportRepository;
|
||||
@ -28,7 +27,6 @@ class JianSunSnapshotRefresher implements UpstreamSnapshotRefresher
|
||||
private readonly InventoryRepository $inventory,
|
||||
private readonly AlibabaBillRepository $alibaba,
|
||||
private readonly PinduoduoSpreadBillRepository $pinduoduo,
|
||||
private readonly ProductPriceRepository $productPrices,
|
||||
private readonly RawBoardPayablesRepository $rawBoardPayables,
|
||||
private readonly TmallTaobaoSpreadBillRepository $taobaoAlipay,
|
||||
private readonly SupplierPayableRepository $supplier,
|
||||
@ -162,11 +160,6 @@ class JianSunSnapshotRefresher implements UpstreamSnapshotRefresher
|
||||
),
|
||||
'taobao-alipay-bills.options' => $this->taobaoAlipay->storeOptions(),
|
||||
|
||||
'supplier-payables.product-prices.list' => $this->productPrices->paginate(
|
||||
$filters,
|
||||
$page,
|
||||
$limit,
|
||||
),
|
||||
'supplier-payables.board-deductions.list' => $this->supplierPayableReports->boardDeductions(
|
||||
$filters,
|
||||
$page,
|
||||
|
||||
@ -5,6 +5,7 @@ namespace Modules\EcommerceFinance\Services;
|
||||
use App\Business\UpstreamBusinessInformation;
|
||||
use Modules\EcommerceFinance\Contracts\UpstreamSnapshotRefresher;
|
||||
use Modules\EcommerceFinance\Repositories\OrderDataRepository;
|
||||
use Modules\EcommerceFinance\Repositories\ProductPriceRepository;
|
||||
|
||||
/**
|
||||
* 将经营分析副本库已登记的业务信息查询还原为共享快照刷新任务。
|
||||
@ -17,6 +18,7 @@ class OperationsReplicaSnapshotRefresher implements UpstreamSnapshotRefresher
|
||||
private readonly UpstreamQueryCacheFactory $caches,
|
||||
private readonly UpstreamBusinessInformation $information,
|
||||
private readonly OrderDataRepository $orders,
|
||||
private readonly ProductPriceRepository $productPrices,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -44,6 +46,11 @@ class OperationsReplicaSnapshotRefresher implements UpstreamSnapshotRefresher
|
||||
(int) ($parameters['page'] ?? 1),
|
||||
(int) ($parameters['limit'] ?? 25),
|
||||
),
|
||||
'supplier-payables.product-prices.list' => $this->productPrices->paginate(
|
||||
(array) ($parameters['filters'] ?? []),
|
||||
(int) ($parameters['page'] ?? 1),
|
||||
(int) ($parameters['limit'] ?? 25),
|
||||
),
|
||||
default => $this->information->snapshot($scope, $parameters),
|
||||
},
|
||||
);
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<style>
|
||||
.product-prices-filter-bar {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 1.4fr) repeat(4, minmax(140px, 1fr)) auto;
|
||||
grid-template-columns: minmax(220px, 1.4fr) repeat(2, minmax(160px, 1fr)) auto;
|
||||
align-items: flex-end;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
@ -87,40 +87,27 @@
|
||||
border-color: var(--vip-content-accent-dark, #856632);
|
||||
}
|
||||
|
||||
.price-tag {
|
||||
.approval-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.price-tag.dingjia,
|
||||
.price-tag.bianjia {
|
||||
background: var(--vip-content-accent-soft, #F7F0DE);
|
||||
color: var(--vip-content-accent-dark, #856632);
|
||||
}
|
||||
|
||||
.price-tag.bianjia { box-shadow: inset 0 0 0 1px var(--vip-content-accent, #B9954F); }
|
||||
|
||||
.price-diff-positive {
|
||||
.approval-tag.is-approved {
|
||||
background: rgba(83, 121, 62, 0.10);
|
||||
color: var(--vip-success, #53793E);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.price-diff-negative {
|
||||
.approval-tag.is-rejected {
|
||||
background: rgba(184, 0, 0, 0.08);
|
||||
color: var(--vip-danger, #B80000);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.price-diff-zero {
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.cost-reduction-icon.is-yes { color: var(--vip-success, #53793E); }
|
||||
.cost-reduction-icon.is-no { color: var(--vip-danger, #B80000); }
|
||||
|
||||
.product-price-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -144,7 +131,7 @@
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
min-width: 1380px;
|
||||
min-width: 1540px;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.78125rem;
|
||||
}
|
||||
@ -197,7 +184,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-width: 1380px;
|
||||
min-width: 1540px;
|
||||
padding: 10px 12px;
|
||||
background: #ffffff;
|
||||
border-top: 1px solid var(--vip-border, #D8D2C7);
|
||||
@ -210,7 +197,7 @@
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
min-width: 1380px;
|
||||
min-width: 1540px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@ -266,28 +253,12 @@
|
||||
value="{{ $productPriceFilters['barcode'] ?? '' }}">
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label class="filter-label">价格类型</label>
|
||||
<select name="price_type">
|
||||
<option value="">全部</option>
|
||||
<option value="pricing" {{ ($productPriceFilters['price_type'] ?? '') === 'pricing' ? 'selected' : '' }}>定价</option>
|
||||
<option value="repricing" {{ ($productPriceFilters['price_type'] ?? '') === 'repricing' ? 'selected' : '' }}>变价</option>
|
||||
</select>
|
||||
<label class="filter-label">流程完成日期起</label>
|
||||
<input type="date" name="completed_from" value="{{ $productPriceFilters['completed_from'] ?? '' }}">
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label class="filter-label">是否采购降本</label>
|
||||
<select name="cost_reduction">
|
||||
<option value="">全部</option>
|
||||
<option value="yes" {{ ($productPriceFilters['cost_reduction'] ?? '') === 'yes' ? 'selected' : '' }}>是</option>
|
||||
<option value="no" {{ ($productPriceFilters['cost_reduction'] ?? '') === 'no' ? 'selected' : '' }}>否</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label class="filter-label">生效日期起</label>
|
||||
<input type="date" name="effective_from" value="{{ $productPriceFilters['effective_from'] ?? '' }}">
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label class="filter-label">生效日期止</label>
|
||||
<input type="date" name="effective_to" value="{{ $productPriceFilters['effective_to'] ?? '' }}">
|
||||
<label class="filter-label">流程完成日期止</label>
|
||||
<input type="date" name="completed_to" value="{{ $productPriceFilters['completed_to'] ?? '' }}">
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label class="filter-label"> </label>
|
||||
@ -318,74 +289,56 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>条形码</th>
|
||||
<th>价格类型</th>
|
||||
<th>变价类型</th>
|
||||
<th>是否采购降本</th>
|
||||
<th class="right">变价前价格</th>
|
||||
<th class="right">变价后价格</th>
|
||||
<th class="right">差价</th>
|
||||
<th>申请人</th>
|
||||
<th>备注</th>
|
||||
<th>生效开始时间</th>
|
||||
<th>生效结束时间</th>
|
||||
<th>商品编码</th>
|
||||
<th>SPU</th>
|
||||
<th>商品名称</th>
|
||||
<th>类目</th>
|
||||
<th>生产状态</th>
|
||||
<th class="right">最低售价</th>
|
||||
<th class="right">计算限价</th>
|
||||
<th class="right">采购价</th>
|
||||
<th class="right">运费</th>
|
||||
<th class="right">京喜限价</th>
|
||||
<th>流程状态</th>
|
||||
<th>审批结果</th>
|
||||
<th>流程标题</th>
|
||||
<th>流程完成时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($productPrices ?? [] as $index => $item)
|
||||
@php
|
||||
$priceTypeLabel = $item->price_type ?: '-';
|
||||
@endphp
|
||||
<tr>
|
||||
<td class="mono">{{ ($productPrices->currentPage() - 1) * $productPrices->perPage() + $index + 1 }}</td>
|
||||
<td class="mono">{{ $item->barcode ?? '-' }}</td>
|
||||
<td class="mono">{{ $item->spu ?? '-' }}</td>
|
||||
<td>{{ $item->product_name ?? '-' }}</td>
|
||||
<td>{{ $item->category ?? '-' }}</td>
|
||||
<td>{{ $item->production_status ?? '-' }}</td>
|
||||
<td class="right">{{ isset($item->minimum_price) && $item->minimum_price !== null ? number_format((float) $item->minimum_price, 2) : '-' }}</td>
|
||||
<td class="right">{{ isset($item->price_limit) && $item->price_limit !== null ? number_format((float) $item->price_limit, 2) : '-' }}</td>
|
||||
<td class="right">{{ isset($item->purchase_price) && $item->purchase_price !== null ? number_format((float) $item->purchase_price, 2) : '-' }}</td>
|
||||
<td class="right">{{ isset($item->shipping_fee) && $item->shipping_fee !== null ? number_format((float) $item->shipping_fee, 2) : '-' }}</td>
|
||||
<td class="right">{{ isset($item->jingxi_limit_price) && $item->jingxi_limit_price !== null ? number_format((float) $item->jingxi_limit_price, 2) : '-' }}</td>
|
||||
<td><span class="approval-tag">{{ $item->approval_status ?? '-' }}</span></td>
|
||||
<td>
|
||||
<span class="price-tag {{ $priceTypeLabel === '变价' ? 'bianjia' : 'dingjia' }}">
|
||||
{{ $priceTypeLabel }}
|
||||
<span class="approval-tag {{ ($item->approval_result ?? '') === '已同意' ? 'is-approved' : (($item->approval_result ?? '') === '已拒绝' ? 'is-rejected' : '') }}">
|
||||
{{ $item->approval_result ?? '-' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ $item->change_type ?? '-' }}</td>
|
||||
<td>
|
||||
@if (($item->cost_reduction ?? '') === '是')
|
||||
<span class="material-icons-outlined cost-reduction-icon is-yes" title="是">check_circle</span>
|
||||
@elseif (($item->cost_reduction ?? '') === '否')
|
||||
<span class="material-icons-outlined cost-reduction-icon is-no" title="否">cancel</span>
|
||||
@else
|
||||
<span>-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="right">{{ isset($item->price_before) && $item->price_before !== null ? number_format((float) $item->price_before, 2) : '-' }}</td>
|
||||
<td class="right">{{ isset($item->price_after) && $item->price_after !== null ? number_format((float) $item->price_after, 2) : '-' }}</td>
|
||||
<td class="right">
|
||||
@php
|
||||
$diff = $item->price_diff ?? null;
|
||||
@endphp
|
||||
@if (isset($diff))
|
||||
<span class="{{ $diff > 0 ? 'price-diff-positive' : ($diff < 0 ? 'price-diff-negative' : 'price-diff-zero') }}">
|
||||
{{ $diff > 0 ? '+' : '' }}{{ number_format((float) $diff, 2) }}
|
||||
@if (!empty($item->workflow_title))
|
||||
<span title="{{ $item->workflow_title }}">
|
||||
{{ mb_strlen($item->workflow_title) > 20 ? mb_substr($item->workflow_title, 0, 20) . '...' : $item->workflow_title }}
|
||||
</span>
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</td>
|
||||
<td>{{ $item->applicant ?? '-' }}</td>
|
||||
<td>
|
||||
@if (!empty($item->remark))
|
||||
<span title="{{ $item->remark }}">
|
||||
{{ mb_strlen($item->remark) > 12 ? mb_substr($item->remark, 0, 12) . '...' : $item->remark }}
|
||||
</span>
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</td>
|
||||
<td>{{ $item->effective_start ?? '-' }}</td>
|
||||
<td>{{ $item->effective_end ?? '-' }}</td>
|
||||
<td>{{ $item->completed_at ?? '-' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="13" class="text-center py-12">
|
||||
<td colspan="15" class="text-center py-12">
|
||||
<span class="material-icons-outlined text-4xl mb-2 block">inventory_2</span>
|
||||
@if ($productPriceError)
|
||||
数据读取失败,请稍后重试。
|
||||
|
||||
@ -4,6 +4,8 @@ namespace Tests\Feature\Business;
|
||||
|
||||
use App\Models\Auth\Permission;
|
||||
use Modules\EcommerceFinance\Services\JianSunQueryCache;
|
||||
use Modules\EcommerceFinance\Services\UpstreamQueryCache;
|
||||
use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
@ -105,6 +107,68 @@ class SupplierPayablePagesTest extends FeatureTestCase
|
||||
$this->assertCount(34, $columns[0]);
|
||||
}
|
||||
|
||||
public function testProductPricesPageUsesOperationsReplicaPriceLimitFields(): void
|
||||
{
|
||||
$this->grantAdminPermission();
|
||||
|
||||
$cache = \Mockery::mock(UpstreamQueryCache::class);
|
||||
$cache->shouldReceive('remember')
|
||||
->once()
|
||||
->withArgs(static function (string $scope, array $parameters): bool {
|
||||
return $scope === 'supplier-payables.product-prices.list'
|
||||
&& $parameters['filters']['barcode'] === 'YMJ005203N1176V01'
|
||||
&& $parameters['filters']['completed_from'] === '2025-12-01'
|
||||
&& $parameters['filters']['completed_to'] === '2025-12-31';
|
||||
})
|
||||
->andReturn(['rows' => [[
|
||||
'barcode' => 'YMJ005203N1176V01',
|
||||
'spu' => 'YMJ0052',
|
||||
'product_name' => '款式二 36/36/176 胡桃木色 BY-008',
|
||||
'category' => '住宅家具 - 衣帽架',
|
||||
'production_status' => '销售',
|
||||
'minimum_price' => 152.8,
|
||||
'price_limit' => 152.6,
|
||||
'purchase_price' => 91.11,
|
||||
'shipping_fee' => 11.62,
|
||||
'jingxi_limit_price' => 123.0,
|
||||
'approval_status' => '已完成',
|
||||
'approval_result' => '已同意',
|
||||
'workflow_title' => '特殊限价需求申请',
|
||||
'completed_at' => '2025-12-31 00:00:00',
|
||||
]]]);
|
||||
$cache->shouldReceive('responseHeaders')->once()->andReturn([]);
|
||||
$factory = \Mockery::mock(UpstreamQueryCacheFactory::class);
|
||||
$factory->shouldReceive('forSource')
|
||||
->once()
|
||||
->with('operations_replica')
|
||||
->andReturn($cache);
|
||||
$this->app->instance(UpstreamQueryCacheFactory::class, $factory);
|
||||
|
||||
$response = $this->loginAs()->get(route('business.pages.show', [
|
||||
'section' => 'supplier-payables',
|
||||
'page' => 'product-prices',
|
||||
'barcode' => 'YMJ005203N1176V01',
|
||||
'completed_from' => '2025-12-01',
|
||||
'completed_to' => '2025-12-31',
|
||||
]));
|
||||
|
||||
$response
|
||||
->assertOk()
|
||||
->assertSee('商品编码')
|
||||
->assertSee('最低售价')
|
||||
->assertSee('计算限价')
|
||||
->assertSee('采购价')
|
||||
->assertSee('京喜限价')
|
||||
->assertSee('YMJ005203N1176V01')
|
||||
->assertSee('152.80')
|
||||
->assertSee('已同意')
|
||||
->assertDontSee('是否采购降本');
|
||||
|
||||
preg_match_all('/<th(?:\s|>)/', $response->getContent(), $headers);
|
||||
|
||||
$this->assertCount(15, $headers[0]);
|
||||
}
|
||||
|
||||
public function testCompensationPageUsesTheFiveColumnsFromTheReferenceSheet(): void
|
||||
{
|
||||
$this->grantAdminPermission();
|
||||
|
||||
@ -9,7 +9,6 @@ use Modules\EcommerceFinance\Repositories\GoodsReconciliationRepository;
|
||||
use Modules\EcommerceFinance\Repositories\InventoryRepository;
|
||||
use Modules\EcommerceFinance\Repositories\JdWalletBillRepository;
|
||||
use Modules\EcommerceFinance\Repositories\PinduoduoSpreadBillRepository;
|
||||
use Modules\EcommerceFinance\Repositories\ProductPriceRepository;
|
||||
use Modules\EcommerceFinance\Repositories\RawBoardPayablesRepository;
|
||||
use Modules\EcommerceFinance\Repositories\SupplierPayableRepository;
|
||||
use Modules\EcommerceFinance\Repositories\SupplierPayableReportRepository;
|
||||
@ -411,7 +410,6 @@ class JianSunSnapshotRefresherTest extends TestCase
|
||||
?InventoryRepository $inventory = null,
|
||||
?AlibabaBillRepository $alibaba = null,
|
||||
?PinduoduoSpreadBillRepository $pinduoduo = null,
|
||||
?ProductPriceRepository $productPrices = null,
|
||||
?RawBoardPayablesRepository $rawBoardPayables = null,
|
||||
?TmallTaobaoSpreadBillRepository $taobaoAlipay = null,
|
||||
?SupplierPayableReportRepository $supplierPayableReports = null,
|
||||
@ -437,7 +435,6 @@ class JianSunSnapshotRefresherTest extends TestCase
|
||||
$inventory ?? $this->placeholder(InventoryRepository::class),
|
||||
$alibaba ?? $this->placeholder(AlibabaBillRepository::class),
|
||||
$pinduoduo ?? $this->placeholder(PinduoduoSpreadBillRepository::class),
|
||||
$productPrices ?? $this->placeholder(ProductPriceRepository::class),
|
||||
$rawBoardPayables ?? $this->placeholder(RawBoardPayablesRepository::class),
|
||||
$taobaoAlipay ?? $this->placeholder(TmallTaobaoSpreadBillRepository::class),
|
||||
$this->placeholder(SupplierPayableRepository::class),
|
||||
|
||||
@ -6,6 +6,7 @@ use App\Business\UpstreamBusinessInformation;
|
||||
use Closure;
|
||||
use InvalidArgumentException;
|
||||
use Modules\EcommerceFinance\Repositories\OrderDataRepository;
|
||||
use Modules\EcommerceFinance\Repositories\ProductPriceRepository;
|
||||
use Modules\EcommerceFinance\Services\OperationsReplicaSnapshotRefresher;
|
||||
use Modules\EcommerceFinance\Services\UpstreamQueryCache;
|
||||
use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory;
|
||||
@ -70,7 +71,9 @@ class OperationsReplicaSnapshotRefresherTest extends TestCase
|
||||
};
|
||||
|
||||
$orders = $this->createMock(OrderDataRepository::class);
|
||||
$refresher = new OperationsReplicaSnapshotRefresher($caches, $information, $orders);
|
||||
$productPrices = $this->createMock(ProductPriceRepository::class);
|
||||
$productPrices->expects($this->never())->method('paginate');
|
||||
$refresher = new OperationsReplicaSnapshotRefresher($caches, $information, $orders, $productPrices);
|
||||
|
||||
$this->assertSame('operations_replica', $refresher->sourceKey());
|
||||
$this->assertSame('fresh', $refresher->refresh(compact('scope', 'parameters')));
|
||||
@ -119,13 +122,72 @@ class OperationsReplicaSnapshotRefresherTest extends TestCase
|
||||
->method($method)
|
||||
->with($parameters['filters'], $parameters['page'], $parameters['limit'])
|
||||
->willReturn(['rows' => [['id' => 1]]]);
|
||||
$productPrices = $this->createMock(ProductPriceRepository::class);
|
||||
$productPrices->expects($this->never())->method('paginate');
|
||||
|
||||
$refresher = new OperationsReplicaSnapshotRefresher($caches, $information, $orders);
|
||||
$refresher = new OperationsReplicaSnapshotRefresher($caches, $information, $orders, $productPrices);
|
||||
|
||||
$this->assertSame('fresh', $refresher->refresh(compact('scope', 'parameters')));
|
||||
$this->assertSame(['rows' => [['id' => 1]]], $cache->value);
|
||||
}
|
||||
|
||||
public function test_product_price_scope_uses_the_operations_replica_repository(): void
|
||||
{
|
||||
$cache = new class extends UpstreamQueryCache {
|
||||
public mixed $value = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function refresh(string $scope, array $parameters, Closure $resolver): string
|
||||
{
|
||||
$this->value = $resolver();
|
||||
|
||||
return 'fresh';
|
||||
}
|
||||
};
|
||||
$caches = new class($cache) extends UpstreamQueryCacheFactory {
|
||||
public function __construct(private readonly UpstreamQueryCache $cache)
|
||||
{
|
||||
}
|
||||
|
||||
public function forSource(string $source): UpstreamQueryCache
|
||||
{
|
||||
return $this->cache;
|
||||
}
|
||||
};
|
||||
$information = $this->createMock(UpstreamBusinessInformation::class);
|
||||
$information->expects($this->never())->method('snapshot');
|
||||
$orders = $this->createMock(OrderDataRepository::class);
|
||||
$orders->expects($this->never())->method('afterSales');
|
||||
$orders->expects($this->never())->method('promotionExpenses');
|
||||
$productPrices = $this->createMock(ProductPriceRepository::class);
|
||||
$filters = [
|
||||
'barcode' => 'YMJ005203N1176V01',
|
||||
'completed_from' => '2025-12-01',
|
||||
'completed_to' => '2025-12-31',
|
||||
];
|
||||
$productPrices->expects($this->once())
|
||||
->method('paginate')
|
||||
->with($filters, 2, 50)
|
||||
->willReturn(['rows' => [['barcode' => 'YMJ005203N1176V01']]]);
|
||||
$refresher = new OperationsReplicaSnapshotRefresher(
|
||||
$caches,
|
||||
$information,
|
||||
$orders,
|
||||
$productPrices,
|
||||
);
|
||||
|
||||
$parameters = ['filters' => $filters, 'page' => 2, 'limit' => 50];
|
||||
|
||||
$this->assertSame('fresh', $refresher->refresh([
|
||||
'scope' => 'supplier-payables.product-prices.list',
|
||||
'parameters' => $parameters,
|
||||
]));
|
||||
$this->assertSame(['rows' => [['barcode' => 'YMJ005203N1176V01']]], $cache->value);
|
||||
}
|
||||
|
||||
public static function orderDataScopes(): array
|
||||
{
|
||||
$parameters = [
|
||||
|
||||
@ -15,43 +15,59 @@ class ProductPriceRepositoryTest extends TestCase
|
||||
$query->expects($this->once())->method('select')->willReturnCallback(
|
||||
function (string $sql, array $bindings): array {
|
||||
UpstreamReadOnlyQuery::assertReadOnlySql($sql);
|
||||
$this->assertStringContainsString('[dbo].[pm_priceAdjustment_log]', $sql);
|
||||
$this->assertStringContainsString('[skucode] LIKE ?', $sql);
|
||||
$this->assertStringContainsString("CASE [priceType] WHEN '0' THEN N'定价' WHEN '1' THEN N'变价'", $sql);
|
||||
$this->assertStringContainsString("WHEN '1' THEN N'清货变价'", $sql);
|
||||
$this->assertStringContainsString("WHEN '2' THEN N'阶段变价'", $sql);
|
||||
$this->assertStringContainsString("WHEN '4' THEN N'打折入库'", $sql);
|
||||
$this->assertStringContainsString("CASE [isCostReduction] WHEN '1' THEN N'是' WHEN '0' THEN N'否'", $sql);
|
||||
$this->assertStringContainsString('[effectStartTime] < DATEADD(day, 1, CAST(? AS date))', $sql);
|
||||
$this->assertStringContainsString('[rpt].[Bulk_Price_Limit]', $sql);
|
||||
$this->assertStringContainsString('[product_code] LIKE ?', $sql);
|
||||
$this->assertStringContainsString("WHEN 'COMPLETED' THEN N'已完成'", $sql);
|
||||
$this->assertStringContainsString("WHEN 'agree' THEN N'已同意'", $sql);
|
||||
$this->assertStringContainsString('[process_end_date] < DATEADD(day, 1, CAST(? AS date))', $sql);
|
||||
$this->assertStringContainsString('OFFSET 2 ROWS FETCH NEXT 3 ROWS ONLY', $sql);
|
||||
$this->assertSame(
|
||||
['%ABC\\%\\_%', '0', '1', '2026-07-01', '2026-07-31'],
|
||||
['%ABC\\%\\_%', '2026-07-01', '2026-07-31'],
|
||||
$bindings,
|
||||
);
|
||||
|
||||
return [
|
||||
['barcode' => 'A', 'price_before' => '10.00', 'price_after' => '11.50', 'price_diff' => '1.50'],
|
||||
['barcode' => 'B', 'price_before' => '', 'price_after' => null, 'price_diff' => '0.00'],
|
||||
['barcode' => 'C', 'price_before' => '3.00', 'price_after' => '4.00', 'price_diff' => '1.00'],
|
||||
[
|
||||
'barcode' => 'A',
|
||||
'minimum_price' => '10.00',
|
||||
'price_limit' => '11.50',
|
||||
'purchase_price' => '7.25',
|
||||
'shipping_fee' => '2.00',
|
||||
'jingxi_limit_price' => '9.50',
|
||||
],
|
||||
[
|
||||
'barcode' => 'B',
|
||||
'minimum_price' => '',
|
||||
'price_limit' => null,
|
||||
'purchase_price' => '0.00',
|
||||
'shipping_fee' => null,
|
||||
'jingxi_limit_price' => '',
|
||||
],
|
||||
[
|
||||
'barcode' => 'C',
|
||||
'minimum_price' => '3.00',
|
||||
'price_limit' => '4.00',
|
||||
'purchase_price' => '2.00',
|
||||
'shipping_fee' => '1.00',
|
||||
'jingxi_limit_price' => '3.50',
|
||||
],
|
||||
];
|
||||
},
|
||||
);
|
||||
|
||||
$factory = $this->createMock(UpstreamReadOnlyQueryFactory::class);
|
||||
$factory->expects($this->once())->method('forSource')->with('jiansun')->willReturn($query);
|
||||
$factory->expects($this->once())->method('forSource')->with('operations_replica')->willReturn($query);
|
||||
|
||||
$result = (new ProductPriceRepository($factory))->paginate([
|
||||
'barcode' => 'ABC%_',
|
||||
'price_type' => 'pricing',
|
||||
'cost_reduction' => 'yes',
|
||||
'effective_from' => '2026-07-01',
|
||||
'effective_to' => '2026-07-31',
|
||||
'completed_from' => '2026-07-01',
|
||||
'completed_to' => '2026-07-31',
|
||||
], 2, 2);
|
||||
|
||||
$this->assertCount(3, $result['rows']);
|
||||
$this->assertSame(10.0, $result['rows'][0]['price_before']);
|
||||
$this->assertSame(11.5, $result['rows'][0]['price_after']);
|
||||
$this->assertNull($result['rows'][1]['price_before']);
|
||||
$this->assertSame(0.0, $result['rows'][1]['price_diff']);
|
||||
$this->assertSame(10.0, $result['rows'][0]['minimum_price']);
|
||||
$this->assertSame(11.5, $result['rows'][0]['price_limit']);
|
||||
$this->assertNull($result['rows'][1]['minimum_price']);
|
||||
$this->assertSame(0.0, $result['rows'][1]['purchase_price']);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user