CaiWuXiTong/tests/Unit/EcommerceFinance/OrderDataMirrorRepositoryTest.php
renshen e5922dd78e 订单售后费用明细 / 商品推广费用明细:订单号和商品编码支持批量查询
两个页签的「订单号」「商品编码」改成多行输入框,可以直接粘一整列:
每行一个,或用逗号、空格、分号、顿号分隔,一次最多 50 个值,多值取并集。

- 订单号批量:先去两套来源解析候选 o_id(单表、等值 IN,走各自前缀索引),
  再按订单过滤;单个订单号仍保留「从头匹配」的模糊语义。
  线上实测 20 个真实单号解析 0.01 秒。
- 商品编码批量:每个编码在 商品/款式/店铺编码(推广页为 item_id)里匹配一组,
  多组 OR;仍受统计日期/订单日期范围约束。
- 非淘系旧查询路径同步支持批量,保持两套口径一致。
2026-10-07 13:55:02 +08:00

437 lines
23 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_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();
});
}
}