create(['percent' => $percent]); $store = StoreModel::factory()->create(['level_id' => $level->id]); $product = ProductModel::factory()->create([ 'status' => ProductModel::STATUS_ON, 'cost_price' => $costPrice, ]); return [$store, $product]; } /** 以小程序身份下一单并返回订单(可指定初始状态) */ private function placeOrder(ProductModel $product, StoreModel $store, int $quantity = 2, int $status = StoreOrderModel::STATUS_PENDING): StoreOrderModel { $this->actingAsMiniStore($store); $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $quantity]]]) ->assertOk()->assertJsonPath('success', true); $order = StoreOrderModel::where('store_id', $store->id)->latest('id')->first(); $this->assertNotNull($order); if ($status !== StoreOrderModel::STATUS_PENDING) { $order->update(['status' => $status]); } return $order; } /** 下单 → 接单 → 生成采购单,返回采购单 */ private function generatePurchase(): PurchaseOrderModel { StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]); $this->actingAsSysUser(); $this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()]) ->assertJsonPath('success', true); return PurchaseOrderModel::first(); } /** 下单时明细快照完整商品档案(分类/供应商/单位/图片/图文/保质期/成本价) */ public function test_place_order_snapshots_full_product_info(): void { $supplier = SupplierModel::factory()->create(); // 上浮 37.5%:成本 4.00 → 售价 5.50 $level = CustomerLevelModel::factory()->create(['percent' => 37.5]); $store = StoreModel::factory()->create(['level_id' => $level->id]); $product = ProductModel::factory()->create([ 'status' => ProductModel::STATUS_ON, 'supplier_id' => $supplier->id, 'category_id' => 7, 'unit' => '箱', 'image_ids' => '1,2', 'content' => '图文详情', 'shelf_life' => 30, 'cost_price' => '4.00', ]); $this->actingAsMiniStore($store); $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 3]]]) ->assertOk()->assertJsonPath('success', true); $item = StoreOrderModel::where('store_id', $store->id)->first()->items->first(); $this->assertSame($supplier->id, $item->supplier_id, '快照供应商'); $this->assertSame(7, $item->category_id, '快照分类'); $this->assertSame('箱', $item->unit, '快照单位'); $this->assertSame('1,2', implode(',', $item->image_ids), '快照图片'); $this->assertSame('图文详情', $item->content, '快照图文详情'); $this->assertSame(30, $item->shelf_life, '快照保质期'); $this->assertSame('4.00', (string) $item->cost_price, '快照成本价'); $this->assertSame('5.50', (string) $item->price, '快照等级单价'); } /** 小程序订单详情不泄漏成本价 */ public function test_mini_detail_hides_cost_price(): void { [$store, $product] = $this->makeStoreWithProduct('5.00', '4.00'); $order = $this->placeOrder($product, $store); $this->actingAsMiniStore($store); $this->getJson("/mini/order/{$order->id}") ->assertOk() ->assertJsonPath('success', true) ->assertJsonMissingPath('data.items.0.cost_price'); } /** 后台订单详情:成本价可见、附供应商名与单位 */ public function test_admin_detail_shows_cost_price_and_supplier(): void { $supplier = SupplierModel::factory()->create(); [$store, $product] = $this->makeStoreWithProduct('5.00', '4.00'); $product->update(['supplier_id' => $supplier->id]); $order = $this->placeOrder($product, $store); $this->actingAsSysUser(); $this->getJson("/order/store/{$order->id}") ->assertOk() ->assertJsonPath('success', true) ->assertJsonPath('data.items.0.cost_price', '4.00') ->assertJsonPath('data.items.0.unit', $product->unit) ->assertJsonPath('data.items.0.supplier.name', $supplier->name); } /** 采购单内修改门店单品:重算单品金额与订单/采购单汇总 */ public function test_update_store_item_recalculates_totals(): void { [$store, $product] = $this->makeStoreWithProduct('5.00'); $order = $this->placeOrder($product, $store, 2, StoreOrderModel::STATUS_SUMMARIZED); $item = $order->items->first(); $purchase = $this->generatePurchase(); $this->actingAsSysUser(); $this->putJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}", [ 'price' => '6.00', 'quantity' => 5, 'weight' => '2.5', ])->assertOk()->assertJsonPath('success', true); $item->refresh(); $this->assertSame('6.00', (string) $item->price); $this->assertSame(5, $item->quantity); $this->assertSame('2.500', (string) $item->weight); $this->assertSame('30.00', (string) $item->amount, '6.00 × 5'); $order->refresh(); $this->assertSame(5, $order->total_quantity, '订货总量 = 明细合计'); $this->assertSame('2.500', (string) $order->total_weight, '总重量 = 明细合计'); $this->assertSame('30.00', (string) $order->total_amount, '订单总金额 = 明细金额合计'); $purchase->refresh(); $this->assertSame('5.00', (string) $purchase->total_quantity); $this->assertSame('2.500', (string) $purchase->total_weight); $this->assertSame('20.00', (string) $purchase->estimate_amount, '5 包 × 成本 4.00'); } /** 采购单已完成:门店单品增删改均拒绝,明细不变 */ public function test_store_item_edits_rejected_when_purchase_completed(): void { [$store, $product] = $this->makeStoreWithProduct('5.00'); $order = $this->placeOrder($product, $store, 1, StoreOrderModel::STATUS_SUMMARIZED); $purchase = $this->generatePurchase(); $purchase->update(['status' => PurchaseOrderModel::STATUS_COMPLETED]); $this->actingAsSysUser(); $this->putJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}", [ 'quantity' => 9, ])->assertOk()->assertJsonPath('success', false) ->assertJsonPath('msg', '采购单已完成,不允许修改明细'); $this->postJson("/purchase/order/{$purchase->id}/store/{$store->id}/item", [ 'product_id' => $product->id, 'quantity' => 1, ])->assertJsonPath('success', false); $this->deleteJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}") ->assertJsonPath('success', false); $item = $order->items->first(); $this->assertSame(1, $item->refresh()->quantity, '被拒绝后明细不变'); $this->assertSame('5.00', (string) $order->refresh()->total_amount, '被拒绝后总金额不变'); } /** 修改门店单品参数校验:负单价/负数量被拦截 */ public function test_update_store_item_validates_negative_values(): void { [$store, $product] = $this->makeStoreWithProduct('5.00'); $this->placeOrder($product, $store, 1, StoreOrderModel::STATUS_SUMMARIZED); $purchase = $this->generatePurchase(); $this->actingAsSysUser(); $this->putJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}", [ 'quantity' => 1, 'price' => -1, ])->assertOk() ->assertJsonPath('success', false) ->assertJsonPath('msg', '单价不能小于 0'); $this->putJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}", [ 'quantity' => -1, ])->assertOk() ->assertJsonPath('success', false) ->assertJsonPath('msg', '采购数量不能小于 0'); } /** 新增单品:单价按门店等级上浮比例换算(成本 20 上浮 30% = 26.00) */ public function test_add_store_item_uses_level_percent_price(): void { $level = CustomerLevelModel::factory()->create(['percent' => 30]); $store = StoreModel::factory()->create(['level_id' => $level->id]); $product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '10.00']); $extra = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '20.00']); $this->placeOrder($product, $store, 2, StoreOrderModel::STATUS_SUMMARIZED); $purchase = $this->generatePurchase(); $this->actingAsSysUser(); $this->postJson("/purchase/order/{$purchase->id}/store/{$store->id}/item", [ 'product_id' => $extra->id, 'quantity' => 2, ])->assertOk()->assertJsonPath('success', true); $item = StoreOrderItemModel::where('product_id', $extra->id)->first(); $this->assertNotNull($item); $this->assertSame('26.00', (string) $item->price, '20 元上浮 30% = 26.00'); $this->assertSame('52.00', (string) $item->amount, '26.00 × 2'); $this->assertSame('20.00', (string) $item->cost_price, '快照成本价'); $this->assertSame($extra->name, $item->product_name, '快照品名取自商品档案'); } /** 新增单品:商品已删除时拒绝 */ public function test_add_store_item_rejected_when_product_deleted(): void { [$store, $product] = $this->makeStoreWithProduct('5.00'); $this->placeOrder($product, $store, 1, StoreOrderModel::STATUS_SUMMARIZED); $purchase = $this->generatePurchase(); $extra = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '9.00']); $extra->delete(); // 软删除 $this->actingAsSysUser(); $this->postJson("/purchase/order/{$purchase->id}/store/{$store->id}/item", [ 'product_id' => $extra->id, 'quantity' => 1, ])->assertOk() ->assertJsonPath('success', false) ->assertJsonPath('msg', '商品不存在或已被删除,无法添加'); } /** 订单列表按包含的商品名称搜索 */ public function test_order_list_search_by_product_name(): void { $level = CustomerLevelModel::factory()->create(); $store = StoreModel::factory()->create(['level_id' => $level->id]); $cabbage = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'name' => '大白菜A', 'cost_price' => '5.00']); $potato = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'name' => '土豆B', 'cost_price' => '5.00']); $this->placeOrder($cabbage, $store); $this->placeOrder($potato, $store); $this->actingAsSysUser(); $response = $this->getJson('/order/store?product_name=' . urlencode('白菜')); $response->assertOk()->assertJsonPath('success', true); $this->assertSame(1, $response->json('data.total'), '仅命中包含「白菜」的订单'); $this->assertSame('大白菜A', $response->json('data.data.0.items.0.product_name')); // 无匹配关键字 → 空列表 $this->getJson('/order/store?product_name=' . urlencode('不存在的商品')) ->assertOk()->assertJsonPath('data.total', 0); } /** 后台订单列表按订货日期区间筛选(快捷按钮下发 Y-m-d 起止日期) */ public function test_admin_order_list_filters_by_order_date_range(): void { [$store, $product] = $this->makeStoreWithProduct(); $this->placeOrder($product, $store, 1); $this->placeOrder($product, $store, 1); $this->placeOrder($product, $store, 1); $orders = StoreOrderModel::where('store_id', $store->id)->orderBy('id')->get(); $orders[0]->update(['order_date' => '2026-08-01']); $orders[1]->update(['order_date' => '2026-08-05']); $orders[2]->update(['order_date' => '2026-08-10']); $this->actingAsSysUser(); $this->getJson('/order/store?order_date[]=2026-08-05&order_date[]=2026-08-10') ->assertOk()->assertJsonPath('data.total', 2); $this->getJson('/order/store?order_date[]=2026-08-05&order_date[]=2026-08-05') ->assertOk()->assertJsonPath('data.total', 1, '今天/昨天等单日快捷按钮起止同日'); $this->getJson('/order/store?order_date[]=2026-08-11&order_date[]=2026-08-12') ->assertOk()->assertJsonPath('data.total', 0); } /** 后台订单列表按采购单号模糊搜索 */ public function test_admin_order_list_search_by_purchase_no(): void { [$store, $product] = $this->makeStoreWithProduct(); $this->placeOrder($product, $store, 1, StoreOrderModel::STATUS_SUMMARIZED); $purchase = $this->generatePurchase(); // 生成采购单外的另一笔订单(不应被命中) $this->placeOrder($product, $store, 1); $this->actingAsSysUser(); $response = $this->getJson('/order/store?purchase_no=' . $purchase->purchase_no); $response->assertOk()->assertJsonPath('success', true); $this->assertSame(1, $response->json('data.total')); $this->assertSame($purchase->purchase_no, $response->json('data.data.0.purchase.purchase_no')); // 单号片段模糊命中 $fragment = substr($purchase->purchase_no, -6); $this->getJson('/order/store?purchase_no=' . $fragment) ->assertOk()->assertJsonPath('data.total', 1); // 无匹配单号 → 空列表 $this->getJson('/order/store?purchase_no=NOT-EXIST-NO') ->assertOk()->assertJsonPath('data.total', 0); } }