509 lines
27 KiB
PHP
509 lines
27 KiB
PHP
<?php
|
||
|
||
namespace Tests\Unit\EcommerceFinance;
|
||
|
||
use Illuminate\Database\Schema\Blueprint;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Support\Facades\Cache;
|
||
use Illuminate\Support\Facades\Schema;
|
||
use Mockery;
|
||
use Modules\EcommerceFinance\Repositories\GenericBusinessMirrorRepository;
|
||
use Modules\EcommerceFinance\Repositories\OrderDataMirrorRepository;
|
||
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
|
||
use Tests\TestCase;
|
||
|
||
class OrderDataMirrorRepositoryTest extends TestCase
|
||
{
|
||
protected function setUp(): void
|
||
{
|
||
parent::setUp();
|
||
config(['database.connections.business_mirror' => [
|
||
'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '',
|
||
]]);
|
||
DB::purge('business_mirror');
|
||
$this->createTables();
|
||
}
|
||
|
||
public function test_after_sales_reads_complete_rows_from_shared_mirror(): void
|
||
{
|
||
$db = DB::connection('business_mirror');
|
||
$db->table('ops_jst_orders')->insert([
|
||
'o_id' => 1, 'so_id' => 'SO-1', 'pay_date' => '2026-08-01 08:00:00',
|
||
'end_time' => '2026-08-02 12:00:00', 'send_date' => '2026-08-02 10:00:00',
|
||
'order_date' => '2026-08-01 09:00:00', 'shop_name' => '京东店铺',
|
||
'shop_id' => 'SHOP-1', 'shop_site' => 'jd', 'type' => '普通订单',
|
||
'labels' => '正常', 'wms_co_id' => 'WH-1', 'l_id' => 'EXP-1',
|
||
'logistics_company' => '物流', 'receiver_state' => '江苏', 'receiver_city' => '常州',
|
||
'creator_name' => '销售员', 'free_amount' => 2, 'freight' => 5,
|
||
]);
|
||
$db->table('ops_jst_order_item')->insert([
|
||
'oi_id' => 11, 'o_id' => 1, 'outer_oi_id' => 'SUB-1', 'sku_id' => 'SKU-1',
|
||
'i_id' => 'STYLE-1', 'shop_sku_id' => 'SHOP-SKU-1', 'shop_i_id' => '1001',
|
||
'properties_value' => '黑色', 'name' => '线上商品', 'qty' => 2,
|
||
'amount' => 100, 'buyer_paid_amount' => 95, 'seller_income_amount' => 98,
|
||
'is_gift' => 0, 'src_combine_sku_id' => null, 'send_warehouse' => 'WH-1',
|
||
]);
|
||
$db->table('ops_jst_sku')->insert([
|
||
'autoid' => 1, 'sku_type' => 'normal', 'sku_id' => 'SKU-1', 'brand' => '品牌A', 'category' => '家具', 'vc_name' => '桌子',
|
||
'cost_price' => 20, 'name' => '镜像商品', 'supplier_id' => 'SUP-1', 'supplier_name' => '供应商A',
|
||
]);
|
||
$db->table('ops_jst_warehouse_info')->insert([
|
||
'wms_co_id' => 'WH-1', 'name' => '主仓', 'remark1' => '运营云仓',
|
||
]);
|
||
$db->table('ops_jst_return_refund_info')->insert([
|
||
'as_id' => 21, 'outer_as_id' => 'AF-1', 'o_id' => 1,
|
||
'as_date' => '2026-08-02 12:00:00', 'type' => '退货退款',
|
||
'question_type' => '质量问题', 'labels' => '售后', 'refund' => 50, 'free_amount' => 3,
|
||
]);
|
||
$db->table('ops_jst_return_refund_item')->insert([
|
||
'as_id' => 21, 'outer_oi_id' => 'SUB-1', 'sku_id' => 'SKU-1', 'type' => '退货',
|
||
'qty' => 1, 'amount' => 50, 'r_qty' => 1, 'shop_amount' => 50, 'defective_qty' => 1,
|
||
]);
|
||
$mirror = Mockery::mock(GenericBusinessMirrorRepository::class);
|
||
$mirror->shouldReceive('connection')->andReturn($db);
|
||
$repository = new OrderDataMirrorRepository(
|
||
Mockery::mock(BusinessMirrorDatasetRegistry::class), $mirror,
|
||
);
|
||
|
||
$result = $repository->afterSales([
|
||
'from' => '2026-08-01', 'to' => '2026-08-02',
|
||
'order_no' => '', 'shop_name' => '', 'sku_id' => '',
|
||
], 1, 25);
|
||
|
||
$this->assertCount(1, $result['rows']);
|
||
$row = $result['rows'][0];
|
||
$this->assertSame('SO-1', $row['online_order_no']);
|
||
$this->assertSame('AF-1', $row['after_sales_no']);
|
||
$this->assertSame(40.0, $row['product_cost']);
|
||
$this->assertSame(1.0, $row['post_return_quantity']);
|
||
$this->assertSame('https://item.jd.com/1001.html', $row['product_link']);
|
||
$this->assertNull($row['account_500112']);
|
||
$this->assertNull($row['account_8001201']);
|
||
}
|
||
|
||
public function test_promotion_expenses_use_the_bill_item_without_scanning_the_large_sku_map(): void
|
||
{
|
||
$db = DB::connection('business_mirror');
|
||
$db->table('ops_item_sales_cost_ratio')->insert([
|
||
'stat_date' => '2026-08-09 00:00:00', 'shop_name' => '京东店铺',
|
||
'item_id' => 'ITEM-1', 'push_fee' => 9.05,
|
||
]);
|
||
$mirror = Mockery::mock(GenericBusinessMirrorRepository::class);
|
||
$mirror->shouldReceive('connection')->andReturn($db);
|
||
$repository = new OrderDataMirrorRepository(
|
||
Mockery::mock(BusinessMirrorDatasetRegistry::class), $mirror,
|
||
);
|
||
|
||
$result = $repository->promotionExpenses([
|
||
'date' => '2026-08-09', 'shop_name' => '', 'sku_id' => '',
|
||
], 1, 25);
|
||
|
||
$this->assertSame('2026-08-09', $result['date']);
|
||
$this->assertNull($result['rows'][0]['product_code']);
|
||
$this->assertSame('ITEM-1', $result['rows'][0]['shop_product_code']);
|
||
$this->assertSame(9.05, $result['rows'][0]['promotion_fee']);
|
||
}
|
||
|
||
public function test_promotion_readiness_does_not_wait_for_the_unused_sku_map(): void
|
||
{
|
||
$datasets = app(BusinessMirrorDatasetRegistry::class);
|
||
$mirror = Mockery::mock(GenericBusinessMirrorRepository::class);
|
||
$mirror->shouldReceive('connection')->andReturn(DB::connection('business_mirror'));
|
||
$mirror->shouldReceive('ready')->once()->andReturnTrue();
|
||
|
||
$this->assertTrue((new OrderDataMirrorRepository($datasets, $mirror))->promotionReady());
|
||
}
|
||
|
||
public function test_after_sales_filters_by_multiple_shop_names(): void
|
||
{
|
||
$db = DB::connection('business_mirror');
|
||
$this->insertOrder($db, 1, 'SO-A', '拼多多-甲店');
|
||
$this->insertOrder($db, 2, 'SO-B', '京东-乙店');
|
||
$this->insertOrder($db, 3, 'SO-C', '抖音-丙店');
|
||
|
||
$repository = $this->repository($db);
|
||
$base = ['from' => '2026-08-01', 'to' => '2026-08-02', 'order_no' => '', 'shop_name' => '', 'sku_id' => ''];
|
||
|
||
$both = $repository->afterSales(array_replace($base, ['shop_names' => ['甲店', '乙店']]), 1, 25);
|
||
$this->assertSame(['京东-乙店', '拼多多-甲店'], $this->sortedShops($both['rows']));
|
||
|
||
$single = $repository->afterSales(array_replace($base, ['shop_names' => ['甲店']]), 1, 25);
|
||
$this->assertSame(['拼多多-甲店'], $this->sortedShops($single['rows']));
|
||
|
||
// 兼容旧的单值 shop_name 过滤。
|
||
$legacy = $repository->afterSales(array_replace($base, ['shop_name' => '丙店']), 1, 25);
|
||
$this->assertSame(['抖音-丙店'], $this->sortedShops($legacy['rows']));
|
||
|
||
$none = $repository->afterSales(array_replace($base, ['shop_names' => ['没有这个店铺']]), 1, 25);
|
||
$this->assertSame([], $none['rows']);
|
||
\Illuminate\Support\Facades\Cache::put(OrderDataMirrorRepository::SHOP_NAMES_CACHE_KEY, ['拼多多-甲店', '京东-乙店', '抖音-丙店'], 300);
|
||
$db->enableQueryLog();
|
||
$selected = $repository->afterSales(array_replace($base, ['shop_names' => ['拼多多-甲店'], 'shop_names_exact' => true]), 1, 25);
|
||
$this->assertSame(['拼多多-甲店'], $this->sortedShops($selected['rows']));
|
||
$this->assertDoesNotMatchRegularExpression('/shop_name\s+LIKE/i', collect($db->getQueryLog())->pluck('query')->implode("\n"));
|
||
}
|
||
|
||
public function test_shop_names_lists_distinct_shops_from_the_order_mirror(): void
|
||
{
|
||
$db = DB::connection('business_mirror');
|
||
$this->insertOrder($db, 1, 'SO-A', '拼多多-甲店');
|
||
$this->insertOrder($db, 2, 'SO-B', '拼多多-甲店');
|
||
$this->insertOrder($db, 3, 'SO-C', '京东-乙店');
|
||
|
||
$names = $this->repository($db)->shopNames();
|
||
sort($names);
|
||
|
||
// 冷缓存只返回毫秒级的店铺目录,不能为了下拉候选去扫订单宽表。
|
||
$db->table('ops_jst_shop_info')->insert(['shop_id' => 1, 'shop_name' => '抖音-丙店']);
|
||
$directory = $this->repository($db)->shopNames();
|
||
sort($directory);
|
||
$this->assertSame(['抖音-丙店'], $directory);
|
||
|
||
// 完整候选由定时预热写入:店铺目录 ∪ 订单镜像里出现过的店铺。
|
||
$names = $this->repository($db)->refreshShopNamesCache();
|
||
sort($names);
|
||
|
||
$this->assertSame(['京东-乙店', '抖音-丙店', '拼多多-甲店'], $names);
|
||
}
|
||
|
||
public function test_shop_candidates_scan_only_changed_rows_and_reconcile_deleted_names_at_night(): void
|
||
{
|
||
$db = DB::connection('business_mirror');
|
||
foreach (['ops_jst_orders', 'ops_jst_qmcust_orders'] as $table) {
|
||
$db->getSchemaBuilder()->table($table, function (Blueprint $t): void {
|
||
$t->dateTime('_mirror_synced_at')->nullable()->index();
|
||
});
|
||
}
|
||
$this->insertOrder($db, 1, 'SO-A', '历史店铺');
|
||
$db->table('ops_jst_orders')->update(['_mirror_synced_at' => '2026-10-01 00:00:00']);
|
||
$repository = $this->repository($db);
|
||
$this->assertSame(['历史店铺'], $repository->refreshShopNamesCache());
|
||
$this->insertOrder($db, 2, 'SO-B', '新增店铺');
|
||
$db->table('ops_jst_orders')->where('o_id', 2)->update(['_mirror_synced_at' => '2026-10-19 12:00:00']);
|
||
$db->enableQueryLog();
|
||
$this->assertSame(['历史店铺', '新增店铺'], $repository->refreshShopNamesCache());
|
||
$scans = array_values(array_filter($db->getQueryLog(), static fn ($q) => str_contains($q['query'], 'select distinct "shop_name" from "ops_jst_orders"')));
|
||
$this->assertCount(1, $scans);
|
||
$this->assertStringContainsString('"_mirror_synced_at" >= ?', $scans[0]['query']);
|
||
$this->assertSame('2026-10-01 00:00:00', $scans[0]['bindings'][1]);
|
||
// A second write with the exact same timestamp must also be discovered.
|
||
$this->insertOrder($db, 3, 'SO-C', '同秒新增');
|
||
$db->table('ops_jst_orders')->where('o_id', 3)->update(['_mirror_synced_at' => '2026-10-19 12:00:00']);
|
||
$this->assertContains('同秒新增', $repository->refreshShopNamesCache());
|
||
$db->table('ops_jst_orders')->where('o_id', 1)->delete();
|
||
$this->assertNotContains('历史店铺', $repository->refreshShopNamesCache(true));
|
||
}
|
||
|
||
public function test_upgrade_keeps_legacy_complete_candidates_and_reads_a_day_of_changes(): void
|
||
{
|
||
$db = DB::connection('business_mirror');
|
||
foreach (['ops_jst_orders', 'ops_jst_qmcust_orders'] as $table) {
|
||
$db->getSchemaBuilder()->table($table, function (Blueprint $t): void {
|
||
$t->dateTime('_mirror_synced_at')->nullable();
|
||
});
|
||
}
|
||
Cache::put(OrderDataMirrorRepository::SHOP_NAMES_CACHE_KEY, ['历史店铺']);
|
||
$this->insertOrder($db, 1, 'SO-A', '新增店铺');
|
||
$db->table('ops_jst_orders')->update(['_mirror_synced_at' => '2026-10-19 12:00:00']);
|
||
$this->assertSame(['历史店铺', '新增店铺'], $this->repository($db)->refreshShopNamesCache());
|
||
}
|
||
|
||
public function test_after_sales_includes_taobao_orders_and_their_refunds(): void
|
||
{
|
||
$db = DB::connection('business_mirror');
|
||
$this->insertOrder($db, 1, 'SO-A', '拼多多-甲店');
|
||
|
||
// 淘系(天猫)订单与售后走另一套表,必须一起出现在报表里。
|
||
$db->table('ops_jst_qmcust_orders')->insert([
|
||
'Id' => 9001, 'o_id' => 9325277, 'so_id' => '5127742981080004116',
|
||
'order_date' => '2026-09-11 13:22:54', 'pay_date' => '2026-09-11 13:23:08',
|
||
'send_date' => '2026-09-15 10:00:00',
|
||
'shop_name' => '天猫-卓禾家具旗舰店', 'shop_id' => '18525257', 'shop_site' => '淘宝天猫',
|
||
'type' => '普通订单', 'wms_co_id' => 'WH-1',
|
||
]);
|
||
$db->table('ops_jst_qmcust_order_items')->insert([
|
||
'Id' => 8001, 'oi_id' => 9380102, 'o_id' => 9325277,
|
||
'outer_oi_id' => '5127742981080004116', 'sku_id' => 'SKU-TB-1',
|
||
'i_id' => 'STYLE-TB-1', 'name' => '天猫商品', 'qty' => 1, 'amount' => 88.0,
|
||
]);
|
||
$db->table('ops_jst_qmcust_refunds')->insert([
|
||
'as_id' => 7001, 'o_id' => 9325277, 'outer_as_id' => 'AF-TB-1',
|
||
'as_date' => '2026-09-20 10:00:00', 'type' => '退货退款',
|
||
'question_type' => '质量问题', 'labels' => '售后', 'refund' => 30,
|
||
]);
|
||
$db->table('ops_jst_qmcust_refund_items')->insert([
|
||
'asi_id' => 6001, 'as_id' => 7001, 'outer_oi_id' => '5127742981080004116',
|
||
'sku_id' => 'SKU-TB-1', 'type' => '退货', 'qty' => 1, 'amount' => 30, 'r_qty' => 1,
|
||
]);
|
||
|
||
$result = $this->repository($db)->afterSales([
|
||
'from' => '2026-09-01', 'to' => '2026-09-30',
|
||
'order_no' => '', 'shop_name' => '', 'sku_id' => '',
|
||
], 1, 25);
|
||
|
||
$rows = collect($result['rows'])->keyBy('shop_name');
|
||
$this->assertTrue($rows->has('天猫-卓禾家具旗舰店'));
|
||
|
||
$taobao = $rows->get('天猫-卓禾家具旗舰店');
|
||
$this->assertSame('5127742981080004116', $taobao['online_order_no']);
|
||
$this->assertSame('AF-TB-1', $taobao['after_sales_no']);
|
||
$this->assertSame('质量问题', $taobao['question_type']);
|
||
$this->assertSame(1.0, $taobao['post_return_quantity']);
|
||
$this->assertArrayNotHasKey('source_branch', $taobao);
|
||
|
||
// 店铺候选也要包含淘系店铺(完整候选由定时预热写入)。
|
||
$names = $this->repository($db)->refreshShopNamesCache();
|
||
$this->assertContains('天猫-卓禾家具旗舰店', $names);
|
||
}
|
||
|
||
public function test_after_sales_accepts_batch_order_numbers_and_sku_codes(): void
|
||
{
|
||
$db = DB::connection('business_mirror');
|
||
$this->insertOrder($db, 1, 'SO-2026-0001', '拼多多-甲店');
|
||
$this->insertOrder($db, 2, 'SO-2026-0002', '拼多多-乙店');
|
||
$this->insertOrder($db, 3, 'SO-2026-0003', '拼多多-丙店');
|
||
|
||
$repository = $this->repository($db);
|
||
$base = ['from' => '', 'to' => '', 'shop_name' => '', 'sku_id' => ''];
|
||
|
||
// 批量订单号:换行、逗号、空格分隔都能识别。
|
||
$batch = $repository->afterSales(
|
||
array_replace($base, ['order_no' => "SO-2026-0001\nSO-2026-0003"]),
|
||
1,
|
||
25,
|
||
);
|
||
$this->assertSame(['SO-2026-0001', 'SO-2026-0003'], $this->sortedOnlineOrders($batch['rows']));
|
||
|
||
$comma = $repository->afterSales(
|
||
array_replace($base, ['order_no' => 'SO-2026-0002, SO-2026-0003']),
|
||
1,
|
||
25,
|
||
);
|
||
$this->assertSame(['SO-2026-0002', 'SO-2026-0003'], $this->sortedOnlineOrders($comma['rows']));
|
||
|
||
// 批量商品编码:每个编码在 商品/款式/店铺编码 三个字段里匹配,多组取并集。
|
||
$skus = $repository->afterSales(
|
||
array_replace($base, ['order_no' => '', 'sku_id' => 'SKU-1, SKU-3']),
|
||
1,
|
||
25,
|
||
);
|
||
$this->assertSame(['SO-2026-0001', 'SO-2026-0003'], $this->sortedOnlineOrders($skus['rows']));
|
||
}
|
||
|
||
public function test_batch_order_numbers_keep_their_exact_text_form(): void
|
||
{
|
||
$db = DB::connection('business_mirror');
|
||
$this->insertOrder($db, 1, '0001234', '拼多多-甲店');
|
||
$this->insertOrder($db, 2, '1234', '拼多多-乙店');
|
||
|
||
$result = $this->repository($db)->afterSales([
|
||
'from' => '', 'to' => '', 'order_no' => "0001234\n9999999", 'shop_name' => '', 'sku_id' => '',
|
||
], 1, 25);
|
||
|
||
// 纯数字单号不能被 PHP 当数组键转成 int,否则前导零的单号会串到别的订单。
|
||
$this->assertSame(['0001234'], $this->sortedOnlineOrders($result['rows']));
|
||
}
|
||
|
||
public function test_promotion_expenses_filters_by_multiple_shops(): void
|
||
{
|
||
$db = DB::connection('business_mirror');
|
||
foreach ([['天猫_甲店', 'ITEM-1', 5.5], ['京东_乙店', 'ITEM-2', 3.0], ['拼多多_丙店', 'ITEM-3', 1.0]] as [$shop, $item, $fee]) {
|
||
$db->table('ops_item_sales_cost_ratio')->insert([
|
||
'stat_date' => '2026-08-13 00:00:00', 'shop_name' => $shop, 'item_id' => $item, 'push_fee' => $fee,
|
||
]);
|
||
}
|
||
|
||
$repository = $this->repository($db);
|
||
|
||
$all = $repository->promotionExpenses(['date' => '2026-08-13', 'shop_names' => []], 1, 25);
|
||
$this->assertSame(['天猫_甲店', '京东_乙店', '拼多多_丙店'], array_column($all['rows'], 'shop_name'));
|
||
|
||
$multi = $repository->promotionExpenses(
|
||
['date' => '2026-08-13', 'shop_names' => ['天猫_甲店', '拼多多_丙店']],
|
||
1,
|
||
25,
|
||
);
|
||
$this->assertSame(['天猫_甲店', '拼多多_丙店'], array_column($multi['rows'], 'shop_name'));
|
||
|
||
// 兼容原来的单值 shop_name。
|
||
$legacy = $repository->promotionExpenses(['date' => '2026-08-13', 'shop_name' => '京东_乙店'], 1, 25);
|
||
$this->assertSame(['京东_乙店'], array_column($legacy['rows'], 'shop_name'));
|
||
|
||
// 商品编码同样支持批量。
|
||
$batch = $repository->promotionExpenses(
|
||
['date' => '2026-08-13', 'shop_names' => [], 'sku_id' => "ITEM-1\nITEM-3"],
|
||
1,
|
||
25,
|
||
);
|
||
$this->assertSame(['天猫_甲店', '拼多多_丙店'], array_column($batch['rows'], 'shop_name'));
|
||
}
|
||
|
||
public function test_promotion_shop_names_lists_the_shops_of_the_selected_date(): void
|
||
{
|
||
$db = DB::connection('business_mirror');
|
||
foreach ([['天猫_甲店', '2026-08-13'], ['京东_乙店', '2026-08-13'], ['天猫_甲店', '2026-08-12']] as [$shop, $date]) {
|
||
$db->table('ops_item_sales_cost_ratio')->insert([
|
||
'stat_date' => $date . ' 00:00:00', 'shop_name' => $shop, 'item_id' => 'ITEM-1', 'push_fee' => 1.0,
|
||
]);
|
||
}
|
||
|
||
$repository = $this->repository($db);
|
||
|
||
$this->assertSame(['京东_乙店', '天猫_甲店'], $repository->promotionShopNames('2026-08-13'));
|
||
$this->assertSame([], $repository->promotionShopNames(''));
|
||
}
|
||
|
||
public function test_after_sales_order_number_is_resolved_to_candidates_before_filtering(): void
|
||
{
|
||
$db = DB::connection('business_mirror');
|
||
$this->insertOrder($db, 1, 'SO-2026-0001', '拼多多-甲店');
|
||
$this->insertOrder($db, 2, 'SO-2026-0002', '拼多多-乙店');
|
||
|
||
$repository = $this->repository($db);
|
||
$base = ['from' => '', 'to' => '', 'shop_name' => '', 'sku_id' => ''];
|
||
|
||
// 完整线上订单号:只返回这一单。
|
||
$exact = $repository->afterSales(array_replace($base, ['order_no' => 'SO-2026-0001']), 1, 25);
|
||
$this->assertSame(['SO-2026-0001'], $this->sortedOnlineOrders($exact['rows']));
|
||
|
||
// 子订单号(明细表的 outer_oi_id)同样能定位到订单。
|
||
$sub = $repository->afterSales(array_replace($base, ['order_no' => 'SUB-2']), 1, 25);
|
||
$this->assertSame(['SO-2026-0002'], $this->sortedOnlineOrders($sub['rows']));
|
||
|
||
// 查不到的单号直接返回空,不再对整张宽表做包含匹配。
|
||
$missing = $repository->afterSales(array_replace($base, ['order_no' => 'SO-9999-0000']), 1, 25);
|
||
$this->assertSame([], $missing['rows']);
|
||
}
|
||
|
||
public function test_after_sales_short_order_fragment_keeps_contains_match(): void
|
||
{
|
||
$db = DB::connection('business_mirror');
|
||
$this->insertOrder($db, 1, 'SO-2026-0001', '拼多多-甲店');
|
||
$this->insertOrder($db, 2, 'SO-2026-0002', '拼多多-乙店');
|
||
|
||
$result = $this->repository($db)->afterSales([
|
||
'from' => '', 'to' => '', 'order_no' => 'SO-', 'shop_name' => '', 'sku_id' => '',
|
||
], 1, 25);
|
||
|
||
$this->assertSame(['SO-2026-0001', 'SO-2026-0002'], $this->sortedOnlineOrders($result['rows']));
|
||
}
|
||
|
||
/** @param array<int,array<string,mixed>> $rows @return list<string> */
|
||
private function sortedOnlineOrders(array $rows): array
|
||
{
|
||
$numbers = array_map(static fn (array $row): string => (string) ($row['online_order_no'] ?? ''), $rows);
|
||
sort($numbers);
|
||
|
||
return array_values($numbers);
|
||
}
|
||
|
||
private function repository(\Illuminate\Database\ConnectionInterface $db): OrderDataMirrorRepository
|
||
{
|
||
$mirror = Mockery::mock(GenericBusinessMirrorRepository::class);
|
||
$mirror->shouldReceive('connection')->andReturn($db);
|
||
|
||
return new OrderDataMirrorRepository(Mockery::mock(BusinessMirrorDatasetRegistry::class), $mirror);
|
||
}
|
||
|
||
private function insertOrder(\Illuminate\Database\ConnectionInterface $db, int $id, string $soId, string $shopName): void
|
||
{
|
||
$db->table('ops_jst_orders')->insert([
|
||
'o_id' => $id, 'so_id' => $soId, 'order_date' => '2026-08-01 09:00:00',
|
||
'shop_name' => $shopName, 'shop_id' => 'SHOP-' . $id,
|
||
]);
|
||
$db->table('ops_jst_order_item')->insert([
|
||
'oi_id' => 100 + $id, 'o_id' => $id, 'outer_oi_id' => 'SUB-' . $id,
|
||
'sku_id' => 'SKU-' . $id, 'qty' => 1, 'amount' => 10,
|
||
]);
|
||
}
|
||
|
||
/** @param array<int,array<string,mixed>> $rows */
|
||
private function sortedShops(array $rows): array
|
||
{
|
||
$names = array_map(static fn (array $row): string => (string) ($row['shop_name'] ?? ''), $rows);
|
||
sort($names);
|
||
|
||
return $names;
|
||
}
|
||
|
||
private function createTables(): void
|
||
{
|
||
$schema = Schema::connection('business_mirror');
|
||
$schema->create('ops_jst_orders', function (Blueprint $t): void {
|
||
$t->integer('o_id'); $t->string('so_id')->nullable(); $t->dateTime('pay_date')->nullable();
|
||
$t->dateTime('end_time')->nullable(); $t->dateTime('send_date')->nullable(); $t->dateTime('order_date');
|
||
$t->string('shop_name'); $t->string('shop_id')->nullable(); $t->string('shop_site')->nullable();
|
||
$t->string('type')->nullable(); $t->string('labels')->nullable(); $t->string('wms_co_id')->nullable();
|
||
$t->string('l_id')->nullable(); $t->string('logistics_company')->nullable();
|
||
$t->string('receiver_state')->nullable(); $t->string('receiver_city')->nullable();
|
||
$t->string('creator_name')->nullable(); $t->decimal('free_amount')->nullable(); $t->decimal('freight')->nullable();
|
||
});
|
||
$schema->create('ops_jst_order_item', function (Blueprint $t): void {
|
||
$t->integer('oi_id'); $t->integer('o_id'); $t->string('outer_oi_id')->nullable();
|
||
$t->string('sku_id'); $t->string('i_id')->nullable(); $t->string('shop_sku_id')->nullable();
|
||
$t->string('shop_i_id')->nullable(); $t->string('properties_value')->nullable(); $t->string('name')->nullable();
|
||
$t->decimal('qty')->nullable(); $t->decimal('amount')->nullable(); $t->decimal('buyer_paid_amount')->nullable();
|
||
$t->decimal('seller_income_amount')->nullable(); $t->integer('is_gift')->nullable();
|
||
$t->string('src_combine_sku_id')->nullable(); $t->string('send_warehouse')->nullable();
|
||
});
|
||
$schema->create('ops_jst_sku', function (Blueprint $t): void {
|
||
$t->integer('autoid'); $t->string('sku_type'); $t->string('sku_id'); $t->string('brand')->nullable(); $t->string('category')->nullable();
|
||
$t->string('vc_name')->nullable(); $t->decimal('cost_price')->nullable(); $t->string('name')->nullable();
|
||
$t->string('supplier_id')->nullable(); $t->string('supplier_name')->nullable();
|
||
});
|
||
$schema->create('ops_jst_warehouse_info', function (Blueprint $t): void {
|
||
$t->string('wms_co_id'); $t->string('name')->nullable(); $t->string('remark1')->nullable();
|
||
});
|
||
$schema->create('ops_jst_return_refund_info', function (Blueprint $t): void {
|
||
$t->integer('as_id'); $t->string('outer_as_id')->nullable(); $t->integer('o_id');
|
||
$t->dateTime('as_date'); $t->string('type')->nullable(); $t->string('question_type')->nullable();
|
||
$t->string('labels')->nullable(); $t->decimal('refund')->nullable(); $t->decimal('free_amount')->nullable();
|
||
});
|
||
$schema->create('ops_jst_return_refund_item', function (Blueprint $t): void {
|
||
$t->integer('as_id'); $t->string('outer_oi_id')->nullable(); $t->string('sku_id')->nullable();
|
||
$t->string('type')->nullable(); $t->decimal('qty')->nullable(); $t->decimal('amount')->nullable();
|
||
$t->decimal('r_qty')->nullable(); $t->decimal('shop_amount')->nullable(); $t->decimal('defective_qty')->nullable();
|
||
});
|
||
$schema->create('ops_item_sales_cost_ratio', function (Blueprint $t): void {
|
||
$t->dateTime('stat_date'); $t->string('shop_name'); $t->string('item_id'); $t->decimal('push_fee');
|
||
});
|
||
$schema->create('ops_jst_sku_map', function (Blueprint $t): void {
|
||
$t->string('shop_i_id'); $t->integer('shop_id'); $t->string('sku_id')->nullable();
|
||
$t->string('i_id')->nullable(); $t->string('shop_sku_id')->nullable();
|
||
$t->string('properties_value')->nullable(); $t->dateTime('modified')->nullable();
|
||
});
|
||
$schema->create('ops_jst_shop_info', function (Blueprint $t): void {
|
||
$t->integer('shop_id'); $t->string('shop_name');
|
||
});
|
||
// 淘系(天猫/淘宝)订单与售后:与 jst_Orders 系列并列的第二套来源。
|
||
$schema->create('ops_jst_qmcust_orders', function (Blueprint $t): void {
|
||
$t->integer('Id'); $t->integer('o_id'); $t->string('so_id')->nullable(); $t->dateTime('order_date');
|
||
$t->dateTime('pay_date')->nullable(); $t->dateTime('end_time')->nullable(); $t->dateTime('send_date')->nullable();
|
||
$t->string('shop_name'); $t->string('shop_id')->nullable(); $t->string('shop_site')->nullable();
|
||
$t->string('type')->nullable(); $t->string('labels')->nullable(); $t->string('wms_co_id')->nullable();
|
||
$t->string('l_id')->nullable(); $t->string('logistics_company')->nullable();
|
||
$t->string('receiver_state')->nullable(); $t->string('receiver_city')->nullable();
|
||
$t->string('creator_name')->nullable(); $t->decimal('free_amount')->nullable(); $t->decimal('freight')->nullable();
|
||
});
|
||
$schema->create('ops_jst_qmcust_order_items', function (Blueprint $t): void {
|
||
$t->integer('Id'); $t->integer('oi_id'); $t->integer('o_id'); $t->string('outer_oi_id')->nullable();
|
||
$t->string('sku_id'); $t->string('i_id')->nullable(); $t->string('shop_sku_id')->nullable();
|
||
$t->string('shop_i_id')->nullable(); $t->string('properties_value')->nullable(); $t->string('name')->nullable();
|
||
$t->decimal('qty')->nullable(); $t->decimal('amount')->nullable(); $t->decimal('buyer_paid_amount')->nullable();
|
||
$t->decimal('seller_income_amount')->nullable(); $t->integer('is_gift')->nullable();
|
||
$t->string('src_combine_sku_id')->nullable();
|
||
});
|
||
$schema->create('ops_jst_qmcust_refunds', function (Blueprint $t): void {
|
||
$t->integer('as_id'); $t->integer('o_id'); $t->string('outer_as_id')->nullable();
|
||
$t->dateTime('as_date'); $t->string('type')->nullable(); $t->string('question_type')->nullable();
|
||
$t->string('labels')->nullable(); $t->decimal('refund')->nullable(); $t->decimal('free_amount')->nullable();
|
||
});
|
||
$schema->create('ops_jst_qmcust_refund_items', function (Blueprint $t): void {
|
||
$t->integer('asi_id'); $t->integer('as_id'); $t->string('outer_oi_id')->nullable();
|
||
$t->string('sku_id')->nullable(); $t->string('type')->nullable(); $t->decimal('qty')->nullable();
|
||
$t->decimal('amount')->nullable(); $t->decimal('r_qty')->nullable(); $t->decimal('shop_amount')->nullable();
|
||
$t->decimal('defective_qty')->nullable();
|
||
});
|
||
}
|
||
}
|