商品成本:按历史唯一规格映射平台数字SKU #482
84
app/Business/StoreReceiptPlatformSkuMapper.php
Normal file
84
app/Business/StoreReceiptPlatformSkuMapper.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Business;
|
||||
|
||||
/**
|
||||
* 平台数字 SKU → 历史已拆分内部商品编码。
|
||||
*
|
||||
* 平台 shop_sku_id 已包含平台规格维度;仅当同一 shop_sku_id + shop_i_id
|
||||
* 的历史内部编码唯一时才允许替换,多候选保持待核验。
|
||||
*/
|
||||
class StoreReceiptPlatformSkuMapper
|
||||
{
|
||||
/** @var array<string,array<string,int>> */
|
||||
private array $internalSkus = [];
|
||||
|
||||
/** @var array<string,array<string,int>> */
|
||||
private array $internalProperties = [];
|
||||
|
||||
/** @var array<string,int> */
|
||||
private array $historyRows = [];
|
||||
|
||||
/** @param iterable<object|array<string,mixed>> $historyRows */
|
||||
public function __construct(iterable $historyRows)
|
||||
{
|
||||
foreach ($historyRows as $row) {
|
||||
$shopSkuId = trim((string) $this->value($row, 'shop_sku_id'));
|
||||
$shopItemId = trim((string) $this->value($row, 'shop_i_id'));
|
||||
$skuId = trim((string) $this->value($row, 'sku_id'));
|
||||
$itemId = trim((string) $this->value($row, 'i_id'));
|
||||
if ($shopSkuId === '' || $shopItemId === '' || $skuId === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 平台原始行的 i_id 等于 shop_i_id;内部拆分行的 i_id 是内部款式编码。
|
||||
if ($itemId === '' || $itemId === $shopItemId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = $this->key($shopSkuId, $shopItemId);
|
||||
$this->internalSkus[$key][$skuId] = ($this->internalSkus[$key][$skuId] ?? 0) + 1;
|
||||
$property = trim((string) $this->value($row, 'properties_value'));
|
||||
if ($property !== '') {
|
||||
$this->internalProperties[$key][$property] = ($this->internalProperties[$key][$property] ?? 0) + 1;
|
||||
}
|
||||
$this->historyRows[$key] = ($this->historyRows[$key] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* status:string,
|
||||
* internal_sku:?string,
|
||||
* candidates:list<string>,
|
||||
* history_rows:int,
|
||||
* internal_properties:list<string>
|
||||
* }
|
||||
*/
|
||||
public function resolve(string $shopSkuId, string $shopItemId): array
|
||||
{
|
||||
$key = $this->key(trim($shopSkuId), trim($shopItemId));
|
||||
$skus = array_keys($this->internalSkus[$key] ?? []);
|
||||
sort($skus);
|
||||
$properties = array_keys($this->internalProperties[$key] ?? []);
|
||||
sort($properties);
|
||||
|
||||
return [
|
||||
'status' => count($skus) === 1 ? 'unique' : ($skus === [] ? 'missing' : 'ambiguous'),
|
||||
'internal_sku' => count($skus) === 1 ? $skus[0] : null,
|
||||
'candidates' => $skus,
|
||||
'history_rows' => (int) ($this->historyRows[$key] ?? 0),
|
||||
'internal_properties' => $properties,
|
||||
];
|
||||
}
|
||||
|
||||
private function key(string $shopSkuId, string $shopItemId): string
|
||||
{
|
||||
return $shopSkuId . "\0" . $shopItemId;
|
||||
}
|
||||
|
||||
private function value(object|array $row, string $key): mixed
|
||||
{
|
||||
return is_array($row) ? ($row[$key] ?? null) : ($row->{$key} ?? null);
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Business\StoreReceiptPlatformSkuMapper;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
@ -186,7 +187,7 @@ class SyncStoreReceiptProductCost extends Command
|
||||
}
|
||||
$this->info('订单侧候选单号:' . count($candidates));
|
||||
|
||||
/** @var array<string,list<array{source:string,sku:string,qty:float,amount:float,warehouse:string,o_id:string,parent_o_id:string,row_key:string,status:string,shop_i_id:string,src_combine_sku_id:string}>> $lines */
|
||||
/** @var array<string,list<array{source:string,sku:string,qty:float,amount:float,warehouse:string,o_id:string,parent_o_id:string,row_key:string,status:string,shop_i_id:string,shop_sku_id:string,properties_value:string,src_combine_sku_id:string}>> $lines */
|
||||
$lines = [];
|
||||
$excludedSkuSql = "'" . implode("','", self::EXCLUDED_SKUS) . "'";
|
||||
foreach (self::SOURCES as $source) {
|
||||
@ -199,7 +200,8 @@ class SyncStoreReceiptProductCost extends Command
|
||||
. " o.o_id as o_id, o.link_o_id as parent_o_id, "
|
||||
. " CONCAT(o.o_id, '-', i.oi_id) as row_key, "
|
||||
. " i.sku_id as sku_id, i.qty as qty, i.amount as amount, o.status as status, "
|
||||
. " i.shop_i_id as shop_i_id, i.src_combine_sku_id as src_combine_sku_id
|
||||
. " i.shop_i_id as shop_i_id, i.shop_sku_id as shop_sku_id, "
|
||||
. " i.properties_value as properties_value, i.src_combine_sku_id as src_combine_sku_id
|
||||
FROM tmp_cost_order_nos t
|
||||
STRAIGHT_JOIN {$source['orders']} o ON o.so_id = t.no
|
||||
STRAIGHT_JOIN {$source['items']} i ON i.o_id = o.o_id
|
||||
@ -261,12 +263,15 @@ class SyncStoreReceiptProductCost extends Command
|
||||
'status' => (string) ($row->status ?? ''),
|
||||
'row_key' => (string) $row->row_key,
|
||||
'shop_i_id' => trim((string) ($row->shop_i_id ?? '')),
|
||||
'shop_sku_id' => trim((string) ($row->shop_sku_id ?? '')),
|
||||
'properties_value' => trim((string) ($row->properties_value ?? '')),
|
||||
'src_combine_sku_id' => mb_strtoupper(trim((string) ($row->src_combine_sku_id ?? ''))),
|
||||
];
|
||||
}
|
||||
}
|
||||
$this->info('订单侧命中核心单号:' . count($lines));
|
||||
|
||||
$platformSkuMapper = $this->platformSkuMapper($mirror, $lines);
|
||||
$shortNames = $this->warehouseShortNames();
|
||||
$prices = $this->weightedAverageUnitPrices($mirror, $reportMonth);
|
||||
// 月加权平均单价匹配不到时,用「线上代发价格」(采购单价 / 钉钉代发单价)兜底。
|
||||
@ -297,6 +302,9 @@ class SyncStoreReceiptProductCost extends Command
|
||||
$matchedPriceRows = 0;
|
||||
$placeholderExcludedRows = 0;
|
||||
$bundleParentExcludedRows = 0;
|
||||
$autoReplacedRows = 0;
|
||||
$mappingAmbiguousRows = 0;
|
||||
$mappingMissingRows = 0;
|
||||
$pendingCostRows = [];
|
||||
|
||||
foreach ($billByCore as $core => $bill) {
|
||||
@ -314,6 +322,11 @@ class SyncStoreReceiptProductCost extends Command
|
||||
);
|
||||
$placeholderExcludedRows += $excludedPlaceholders;
|
||||
$bundleParentExcludedRows += $excludedBundleParents;
|
||||
$mapped = $this->mapPlatformSkus($costLines, $platformSkuMapper);
|
||||
$costLines = $mapped['lines'];
|
||||
$autoReplacedRows += $mapped['auto_replaced_rows'];
|
||||
$mappingAmbiguousRows += $mapped['ambiguous_rows'];
|
||||
$mappingMissingRows += $mapped['missing_rows'];
|
||||
|
||||
foreach ($costLines as $line) {
|
||||
$short = $shortNames['by_provider'][$line['warehouse']] ?? '';
|
||||
@ -373,10 +386,20 @@ class SyncStoreReceiptProductCost extends Command
|
||||
'warehouse' => $line['warehouse'] !== '' ? $line['warehouse'] : null,
|
||||
'warehouse_short_name' => $short !== '' ? $short : null,
|
||||
'product_code' => $line['sku'],
|
||||
'original_platform_sku' => $line['original_platform_sku'] ?? null,
|
||||
'shop_sku_id' => $line['shop_sku_id'] ?? null,
|
||||
'shop_i_id' => $line['shop_i_id'] ?? null,
|
||||
'properties_value' => $line['properties_value'] ?? null,
|
||||
'mapping_status' => $line['platform_mapping_status'] ?? null,
|
||||
'mapping_candidates' => $line['platform_mapping_candidates'] ?? [],
|
||||
'product_quantity' => $quantity,
|
||||
'reason' => $short === ''
|
||||
? '未匹配仓库简称'
|
||||
: '仓库简称 + 商品编码在月加权、线上代发、普通商品资料中均无有效单价',
|
||||
: (($line['platform_mapping_status'] ?? null) === 'ambiguous'
|
||||
? '平台 shop_sku_id 对应多个历史内部商品编码'
|
||||
: (($line['platform_mapping_status'] ?? null) === 'missing'
|
||||
? '平台 shop_sku_id 未找到历史内部商品编码'
|
||||
: '仓库简称 + 商品编码在月加权、线上代发、普通商品资料中均无有效单价')),
|
||||
'code_type' => $this->pendingCodeType((string) $line['sku']),
|
||||
];
|
||||
}
|
||||
@ -437,6 +460,12 @@ class SyncStoreReceiptProductCost extends Command
|
||||
$missingPriceRows,
|
||||
number_format((float) $pendingCostQuantity, 2),
|
||||
));
|
||||
$this->info(sprintf(
|
||||
' 平台数字 SKU 自动替换:%d 行;多候选保留:%d 行;无历史映射保留:%d 行',
|
||||
$autoReplacedRows,
|
||||
$mappingAmbiguousRows,
|
||||
$mappingMissingRows,
|
||||
));
|
||||
|
||||
$result = [
|
||||
'status' => 'success',
|
||||
@ -450,6 +479,9 @@ class SyncStoreReceiptProductCost extends Command
|
||||
'placeholder_excluded_rows' => $placeholderExcludedRows,
|
||||
'bundle_parent_excluded_rows' => $bundleParentExcludedRows,
|
||||
'excluded_rows' => $placeholderExcludedRows + $bundleParentExcludedRows,
|
||||
'auto_replaced_platform_sku_rows' => $autoReplacedRows,
|
||||
'platform_sku_mapping_ambiguous_rows' => $mappingAmbiguousRows,
|
||||
'platform_sku_mapping_missing_rows' => $mappingMissingRows,
|
||||
'pending_cost_rows' => $missingPriceRows,
|
||||
'pending_cost_quantity' => round((float) $pendingCostQuantity, 4),
|
||||
'unit_mismatch_rows' => $mismatchRows,
|
||||
@ -498,6 +530,152 @@ class SyncStoreReceiptProductCost extends Command
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,list<array<string,mixed>>> $lines
|
||||
*/
|
||||
private function platformSkuMapper($mirror, array $lines): StoreReceiptPlatformSkuMapper
|
||||
{
|
||||
$candidates = [];
|
||||
foreach ($lines as $orderLines) {
|
||||
foreach ($orderLines as $line) {
|
||||
$sku = mb_strtoupper(trim((string) ($line['sku'] ?? '')));
|
||||
$shopSkuId = trim((string) ($line['shop_sku_id'] ?? ''));
|
||||
$shopItemId = trim((string) ($line['shop_i_id'] ?? ''));
|
||||
if (
|
||||
preg_match('/^\d+$/', $sku) === 1
|
||||
&& $shopSkuId !== ''
|
||||
&& $shopItemId !== ''
|
||||
) {
|
||||
$candidates[$shopSkuId . "\0" . $shopItemId] = [
|
||||
'shop_sku_id' => $shopSkuId,
|
||||
'shop_i_id' => $shopItemId,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($candidates === []) {
|
||||
return new StoreReceiptPlatformSkuMapper([]);
|
||||
}
|
||||
|
||||
$mirror->statement('DROP TEMPORARY TABLE IF EXISTS tmp_platform_sku_keys');
|
||||
$mirror->statement(
|
||||
'CREATE TEMPORARY TABLE tmp_platform_sku_keys ('
|
||||
. 'shop_sku_id varchar(255) NOT NULL, '
|
||||
. 'shop_i_id varchar(255) NOT NULL, '
|
||||
. 'PRIMARY KEY (shop_sku_id, shop_i_id)'
|
||||
. ') ENGINE=InnoDB',
|
||||
);
|
||||
|
||||
$historyRows = [];
|
||||
try {
|
||||
foreach (array_chunk(array_values($candidates), 500) as $chunk) {
|
||||
$mirror->table('tmp_platform_sku_keys')->insertOrIgnore($chunk);
|
||||
}
|
||||
|
||||
foreach (self::SOURCES as $source) {
|
||||
$rows = $mirror->select(
|
||||
"SELECT i.shop_sku_id, i.shop_i_id, i.sku_id, i.i_id, i.properties_value
|
||||
FROM {$source['items']} i
|
||||
STRAIGHT_JOIN tmp_platform_sku_keys k
|
||||
ON k.shop_sku_id = i.shop_sku_id
|
||||
AND k.shop_i_id = i.shop_i_id
|
||||
WHERE COALESCE(i.i_id, '') <> ''
|
||||
AND i.i_id <> i.shop_i_id",
|
||||
);
|
||||
foreach ($rows as $row) {
|
||||
$historyRows[] = $row;
|
||||
}
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
$this->warn('平台 SKU 历史映射读取失败,本批数字 SKU 保持待核验:' . $exception->getMessage());
|
||||
} finally {
|
||||
$mirror->statement('DROP TEMPORARY TABLE IF EXISTS tmp_platform_sku_keys');
|
||||
}
|
||||
|
||||
$this->info(sprintf(
|
||||
'平台数字 SKU 候选:%d 个;历史内部商品映射记录:%d 条',
|
||||
count($candidates),
|
||||
count($historyRows),
|
||||
));
|
||||
|
||||
return new StoreReceiptPlatformSkuMapper($historyRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string,mixed>> $lines
|
||||
* @return array{
|
||||
* lines:list<array<string,mixed>>,
|
||||
* auto_replaced_rows:int,
|
||||
* ambiguous_rows:int,
|
||||
* missing_rows:int
|
||||
* }
|
||||
*/
|
||||
private function mapPlatformSkus(
|
||||
array $lines,
|
||||
StoreReceiptPlatformSkuMapper $mapper,
|
||||
): array {
|
||||
$mappedLines = [];
|
||||
foreach ($lines as $line) {
|
||||
$sku = mb_strtoupper(trim((string) $line['sku']));
|
||||
$shopSkuId = trim((string) ($line['shop_sku_id'] ?? ''));
|
||||
$shopItemId = trim((string) ($line['shop_i_id'] ?? ''));
|
||||
$line['platform_mapping_status'] = null;
|
||||
$line['platform_mapping_candidates'] = [];
|
||||
$line['original_platform_sku'] = null;
|
||||
|
||||
if (
|
||||
preg_match('/^\d+$/', $sku) === 1
|
||||
&& $shopSkuId !== ''
|
||||
&& $shopItemId !== ''
|
||||
) {
|
||||
$resolution = $mapper->resolve($shopSkuId, $shopItemId);
|
||||
$line['platform_mapping_status'] = $resolution['status'];
|
||||
$line['platform_mapping_candidates'] = $resolution['candidates'];
|
||||
$line['original_platform_sku'] = $sku;
|
||||
if ($resolution['status'] === 'unique' && $resolution['internal_sku'] !== null) {
|
||||
$line['sku'] = mb_strtoupper($resolution['internal_sku']);
|
||||
$line['auto_mapped_platform_sku'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$key = $line['warehouse'] . "\0" . $line['sku'];
|
||||
if (! isset($mappedLines[$key])) {
|
||||
$mappedLines[$key] = $line;
|
||||
continue;
|
||||
}
|
||||
|
||||
$mappedLines[$key]['qty'] += $line['qty'];
|
||||
$mappedLines[$key]['amount'] += $line['amount'];
|
||||
if (! empty($line['auto_mapped_platform_sku'])) {
|
||||
$mappedLines[$key]['auto_mapped_platform_sku'] = true;
|
||||
$mappedLines[$key]['original_platform_sku'] = $line['original_platform_sku'];
|
||||
$mappedLines[$key]['platform_mapping_status'] = 'unique';
|
||||
$mappedLines[$key]['platform_mapping_candidates'] = $line['platform_mapping_candidates'];
|
||||
}
|
||||
}
|
||||
|
||||
$autoReplacedRows = 0;
|
||||
$ambiguousRows = 0;
|
||||
$missingRows = 0;
|
||||
foreach ($mappedLines as $line) {
|
||||
if (! empty($line['auto_mapped_platform_sku'])) {
|
||||
$autoReplacedRows++;
|
||||
} elseif (($line['platform_mapping_status'] ?? null) === 'ambiguous') {
|
||||
$ambiguousRows++;
|
||||
} elseif (($line['platform_mapping_status'] ?? null) === 'missing') {
|
||||
$missingRows++;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'lines' => array_values($mappedLines),
|
||||
'auto_replaced_rows' => $autoReplacedRows,
|
||||
'ambiguous_rows' => $ambiguousRows,
|
||||
'missing_rows' => $missingRows,
|
||||
];
|
||||
}
|
||||
|
||||
private function pendingCodeType(string $code): string
|
||||
{
|
||||
$code = mb_strtoupper(trim($code));
|
||||
@ -532,8 +710,8 @@ class SyncStoreReceiptProductCost extends Command
|
||||
* 如果带仓库的行全部是 Cancelled,则整单不参与商品成本;
|
||||
* 4) 排除 Cancelled 行,同一订单的多条末级行按「发货仓库 + 商品编码」汇总数量。
|
||||
*
|
||||
* @param list<array{source:string,sku:string,qty:float,amount:float,warehouse:string,o_id:string,parent_o_id:string,row_key:string,status:string,shop_i_id:string,src_combine_sku_id:string}> $rows
|
||||
* @return list<array{sku:string,qty:float,amount:float,warehouse:string}>
|
||||
* @param list<array{source:string,sku:string,qty:float,amount:float,warehouse:string,o_id:string,parent_o_id:string,row_key:string,status:string,shop_i_id:string,shop_sku_id:string,properties_value:string,src_combine_sku_id:string}> $rows
|
||||
* @return list<array{sku:string,qty:float,amount:float,warehouse:string,shop_i_id:string,shop_sku_id:string,properties_value:string}>
|
||||
*/
|
||||
private function costLines(
|
||||
array $rows,
|
||||
@ -657,10 +835,16 @@ class SyncStoreReceiptProductCost extends Command
|
||||
// 8) 汇总到「发货仓库 + 商品编码」。
|
||||
$aggregated = [];
|
||||
foreach ($unique as $row) {
|
||||
$key = $row['warehouse'] . "\0" . $row['sku'];
|
||||
$isPlatformSku = preg_match('/^\d+$/', (string) $row['sku']) === 1;
|
||||
$key = $row['warehouse']
|
||||
. "\0" . $row['sku']
|
||||
. ($isPlatformSku ? "\0" . ($row['shop_sku_id'] ?? '') : '');
|
||||
$aggregated[$key] = [
|
||||
'sku' => $row['sku'],
|
||||
'warehouse' => $row['warehouse'],
|
||||
'shop_i_id' => (string) ($row['shop_i_id'] ?? ''),
|
||||
'shop_sku_id' => (string) ($row['shop_sku_id'] ?? ''),
|
||||
'properties_value' => (string) ($row['properties_value'] ?? ''),
|
||||
'qty' => ($aggregated[$key]['qty'] ?? 0.0) + $row['qty'],
|
||||
'amount' => ($aggregated[$key]['amount'] ?? 0.0) + $row['amount'],
|
||||
];
|
||||
|
||||
@ -112,6 +112,7 @@
|
||||
常州简尚家居有限公司对应映射表里的「配件单仓」行),
|
||||
商品单价按「仓库简称 + 商品编码」取「商品月加权平均单价」里的加权平均价;匹配不到时退回「线上代发价格」
|
||||
(采购单价优先、其次钉钉代发单价),仍匹配不到时用「普通商品资料」里的商品成本价兜底;商品成本 = 商品数量 × 商品单价。
|
||||
平台数字 SKU 会按平台 SKU ID + 商品 ID 回查历史已拆分订单,仅唯一内部编码自动替换;多候选保持待核验。
|
||||
同一订单下已有内部商品编码时,平台数字商品占位行不参与成本;组合装拆分后,组合装主行不参与成本,只取组件行。
|
||||
拆单只取最末级订单行(没有任何下级的叶子行),主单和中间汇总层不参与、避免重复计数;
|
||||
同一单分多仓发货的按末级实际仓库分行;虚拟条码(X0000x)不计入;
|
||||
|
||||
86
tests/Unit/Business/StoreReceiptPlatformSkuMapperTest.php
Normal file
86
tests/Unit/Business/StoreReceiptPlatformSkuMapperTest.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Business;
|
||||
|
||||
use App\Business\StoreReceiptPlatformSkuMapper;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class StoreReceiptPlatformSkuMapperTest extends TestCase
|
||||
{
|
||||
public function test_unique_historical_internal_sku_is_mapped(): void
|
||||
{
|
||||
$mapper = new StoreReceiptPlatformSkuMapper([
|
||||
$this->row('958530694526', '3768947181063635112', '3768947181063635112'),
|
||||
$this->row('SG006367N1028V01', 'SG0063', '3768947181063635112'),
|
||||
$this->row('SG006367N1028V01', 'SG0063', '3768947181063635112'),
|
||||
]);
|
||||
|
||||
$result = $mapper->resolve('3533103677164802', '3768947181063635112');
|
||||
|
||||
$this->assertSame('unique', $result['status']);
|
||||
$this->assertSame('SG006367N1028V01', $result['internal_sku']);
|
||||
$this->assertSame(2, $result['history_rows']);
|
||||
$this->assertSame(['经典北美黑胡桃色'], $result['internal_properties']);
|
||||
}
|
||||
|
||||
public function test_multiple_internal_skus_remain_ambiguous(): void
|
||||
{
|
||||
$mapper = new StoreReceiptPlatformSkuMapper([
|
||||
$this->row('SG001', 'SGSTYLE1', '3768947181063635112'),
|
||||
$this->row('SG002', 'SGSTYLE2', '3768947181063635112'),
|
||||
]);
|
||||
|
||||
$result = $mapper->resolve('3533103677164802', '3768947181063635112');
|
||||
|
||||
$this->assertSame('ambiguous', $result['status']);
|
||||
$this->assertNull($result['internal_sku']);
|
||||
$this->assertSame(['SG001', 'SG002'], $result['candidates']);
|
||||
}
|
||||
|
||||
public function test_missing_history_stays_unmapped(): void
|
||||
{
|
||||
$mapper = new StoreReceiptPlatformSkuMapper([
|
||||
$this->row('958530694526', '3768947181063635112', '3768947181063635112'),
|
||||
]);
|
||||
|
||||
$result = $mapper->resolve('3533103677164802', '3768947181063635112');
|
||||
|
||||
$this->assertSame('missing', $result['status']);
|
||||
$this->assertNull($result['internal_sku']);
|
||||
$this->assertSame([], $result['candidates']);
|
||||
}
|
||||
|
||||
public function test_mapping_requires_the_same_platform_spec_id(): void
|
||||
{
|
||||
$mapper = new StoreReceiptPlatformSkuMapper([
|
||||
$this->row(
|
||||
'SG006367N1028V01',
|
||||
'SG0063',
|
||||
'3768947181063635112',
|
||||
'3533103677164802',
|
||||
),
|
||||
]);
|
||||
|
||||
$result = $mapper->resolve('3533103677164999', '3768947181063635112');
|
||||
|
||||
$this->assertSame('missing', $result['status']);
|
||||
$this->assertNull($result['internal_sku']);
|
||||
}
|
||||
|
||||
/** @return array<string,string> */
|
||||
private function row(
|
||||
string $skuId,
|
||||
string $itemId,
|
||||
string $shopItemId,
|
||||
string $shopSkuId = '3533103677164802',
|
||||
): array
|
||||
{
|
||||
return [
|
||||
'shop_sku_id' => $shopSkuId,
|
||||
'shop_i_id' => $shopItemId,
|
||||
'sku_id' => $skuId,
|
||||
'i_id' => $itemId,
|
||||
'properties_value' => $itemId === 'SG0063' ? '经典北美黑胡桃色' : '',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Business\StoreReceiptPlatformSkuMapper;
|
||||
use App\Console\Commands\SyncStoreReceiptProductCost;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionMethod;
|
||||
@ -41,6 +42,37 @@ class StoreReceiptProductCostLineTest extends TestCase
|
||||
$this->assertSame(2.0, $lines[0]['qty']);
|
||||
}
|
||||
|
||||
public function test_unique_platform_sku_is_replaced_after_line_filtering(): void
|
||||
{
|
||||
[$lines] = $this->costLines([
|
||||
$this->row(
|
||||
'platform',
|
||||
'958530694526',
|
||||
'3768947181063635112',
|
||||
1,
|
||||
93.25,
|
||||
),
|
||||
]);
|
||||
$mapper = new StoreReceiptPlatformSkuMapper([
|
||||
[
|
||||
'shop_sku_id' => '3533103677164802',
|
||||
'shop_i_id' => '3768947181063635112',
|
||||
'sku_id' => 'SG006367N1028V01',
|
||||
'i_id' => 'SG0063',
|
||||
'properties_value' => '经典北美黑胡桃色',
|
||||
],
|
||||
]);
|
||||
$command = new SyncStoreReceiptProductCost();
|
||||
$method = new ReflectionMethod($command, 'mapPlatformSkus');
|
||||
$method->setAccessible(true);
|
||||
$result = $method->invoke($command, $lines, $mapper);
|
||||
|
||||
$this->assertSame(1, $result['auto_replaced_rows']);
|
||||
$this->assertSame('SG006367N1028V01', $result['lines'][0]['sku']);
|
||||
$this->assertSame('958530694526', $result['lines'][0]['original_platform_sku']);
|
||||
$this->assertSame('unique', $result['lines'][0]['platform_mapping_status']);
|
||||
}
|
||||
|
||||
/** @return array{0:list<array<string,mixed>>,1:int,2:int} */
|
||||
private function costLines(array $rows): array
|
||||
{
|
||||
@ -74,6 +106,8 @@ class StoreReceiptProductCostLineTest extends TestCase
|
||||
'row_key' => $rowKey,
|
||||
'status' => 'Sent',
|
||||
'shop_i_id' => $shopItemId,
|
||||
'shop_sku_id' => '3533103677164802',
|
||||
'properties_value' => '颜色分类:六格-90*30*75cm-木纹胡桃色?赠送后背板',
|
||||
'src_combine_sku_id' => $bundleParent,
|
||||
];
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user