CaiWuXiTong/tests/Unit/EcommerceFinance/OrderDataMirrorRepositoryTest.php
renshen 916fb30f37 修复批量筛选:纯数字单号当数组键会被 PHP 转成 int
batchValues() 原来拿值本身当数组键去重,纯数字订单号(如 3317080368285022883)会被
PHP 转成 int,后续 whereIn 就成了数字和长文本比较,走不上 so_id / outer_oi_id 的
前缀索引,实测退化成全表扫描(70 秒以上)。

改成键加 v 前缀、返回原字符串,ctype_digit 显式传字符串。新增用例覆盖前导零单号
0001234 与 1234,防止再回归。
2026-10-07 14:05:53 +08:00

451 lines
24 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Tests\Unit\EcommerceFinance;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
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']);
}
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);
$this->assertSame(['京东-乙店', '拼多多-甲店'], $names);
}
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)->shopNames();
$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();
});
}
}