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, UserModel::factory()->forStore($store->id)->create()]; } /** 以小程序身份下一单并返回订单(可指定初始状态) */ private function placeOrder(ProductModel $product, UserModel $user, int $quantity = 2, int $status = StoreOrderModel::STATUS_PENDING): StoreOrderModel { $this->actingAsMiniUser($user); $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $quantity]]]) ->assertOk()->assertJsonPath('success', true); $order = StoreOrderModel::where('store_id', $user->store_id)->latest('id')->first(); $this->assertNotNull($order); if ($status !== StoreOrderModel::STATUS_PENDING) { $order->update(['status' => $status]); } return $order; } /** 下单时明细快照完整商品档案(分类/供应商/单位/图片/图文/保质期/成本价) */ 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', ]); $user = UserModel::factory()->forStore($store->id)->create(); $this->actingAsMiniUser($user); $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 { [, $product, $user] = $this->makeStoreWithProduct('5.00', '4.00'); $order = $this->placeOrder($product, $user); $this->actingAsMiniUser($user); $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(); [, $product, $user] = $this->makeStoreWithProduct('5.00', '4.00'); $product->update(['supplier_id' => $supplier->id]); $order = $this->placeOrder($product, $user); $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_item_recalculates_order_totals(): void { [$store, $product, $user] = $this->makeStoreWithProduct('5.00'); $order = $this->placeOrder($product, $user, 2, StoreOrderModel::STATUS_SUMMARIZED); $item = $order->items->first(); $supplier = SupplierModel::factory()->create(); $this->actingAsSysUser(); $this->putJson("/order/store/item/{$item->id}", [ 'supplier_id' => $supplier->id, 'product_name' => '改名后的商品', 'product_spec' => '新规格', 'unit' => '箱', 'price' => '6.00', 'cost_price' => '3.50', 'quantity' => 5, 'weight' => '2.5', ])->assertOk()->assertJsonPath('success', true); $item->refresh(); $this->assertSame($supplier->id, $item->supplier_id); $this->assertSame('改名后的商品', $item->product_name); $this->assertSame('新规格', $item->product_spec); $this->assertSame('箱', $item->unit); $this->assertSame('6.00', (string) $item->price); $this->assertSame('3.50', (string) $item->cost_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->product_amount, '商品总金额 = 明细金额合计'); $this->assertSame('30.00', (string) $order->total_amount, '订单总金额 = 商品金额'); } /** 已完成/已取消订单不允许修改明细 */ public function test_update_item_rejected_when_completed_or_cancelled(): void { [, $product, $user] = $this->makeStoreWithProduct('5.00'); $order = $this->placeOrder($product, $user, 1); $item = $order->items->first(); $payload = [ 'supplier_id' => 0, 'product_name' => '不应生效', 'product_spec' => '', 'unit' => '斤', 'price' => '1.00', 'cost_price' => '1.00', 'quantity' => 1, 'weight' => 0, ]; $this->actingAsSysUser(); foreach ([StoreOrderModel::STATUS_COMPLETED, StoreOrderModel::STATUS_CANCELLED] as $status) { $order->update(['status' => $status]); $this->putJson("/order/store/item/{$item->id}", $payload) ->assertOk()->assertJsonPath('success', false); } $this->assertNotSame('不应生效', $item->fresh()->product_name, '被拒绝后明细不变'); $this->assertSame('5.00', (string) $order->fresh()->total_amount, '被拒绝后总金额不变'); } /** 修改明细参数校验:负单价被拦截 */ public function test_update_item_validates_negative_price(): void { [, $product, $user] = $this->makeStoreWithProduct('5.00'); $order = $this->placeOrder($product, $user, 1, StoreOrderModel::STATUS_SUMMARIZED); $item = $order->items->first(); $this->actingAsSysUser(); $this->putJson("/order/store/item/{$item->id}", [ 'supplier_id' => 0, 'product_name' => $item->product_name, 'unit' => '斤', 'price' => -1, 'cost_price' => 0, 'quantity' => 1, 'weight' => 0, ])->assertOk() ->assertJsonPath('success', false) ->assertJsonPath('msg', '单价不能小于 0'); } /** 一键同步:按商品ID同步最新档案(固定价等级保留原单价) */ public function test_sync_item_updates_snapshot_from_product(): void { [$store, $product, $user] = $this->makeStoreWithProduct('5.50', '4.00'); $order = $this->placeOrder($product, $user, 3, StoreOrderModel::STATUS_SUMMARIZED); $item = $order->items->first(); $this->assertSame('16.50', (string) $item->amount); // 下单后商品档案变更:改名/改规格/改单位/换供应商/调成本价 $supplier = SupplierModel::factory()->create(); $product->update([ 'name' => '同步后的品名', 'spec' => '同步后的规格', 'unit' => '箱', 'supplier_id' => $supplier->id, 'cost_price' => '9.99', ]); $this->actingAsSysUser(); $this->putJson("/order/store/item/{$item->id}/sync") ->assertOk()->assertJsonPath('success', true); $item->refresh(); $this->assertSame('同步后的品名', $item->product_name); $this->assertSame('同步后的规格', $item->product_spec); $this->assertSame('箱', $item->unit); $this->assertSame($supplier->id, $item->supplier_id); $this->assertSame('9.99', (string) $item->cost_price); $this->assertSame('5.50', (string) $item->price, '固定价等级单价不随档案变化'); $this->assertSame('16.50', (string) $item->amount, '单价未变,金额不变'); } /** 一键同步:百分比计价等级按最新成本价重算单价与订单总价 */ public function test_sync_item_recalculates_percent_price_with_latest_cost(): void { // 等级上浮 30%:下单时成本 10.00 → 售价 13.00 $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', ]); $user = UserModel::factory()->forStore($store->id)->create(); $order = $this->placeOrder($product, $user, 2, StoreOrderModel::STATUS_SUMMARIZED); $item = $order->items->first(); $this->assertSame('13.00', (string) $item->price, '下单时 10 元上浮 30%'); $this->assertSame('26.00', (string) $order->total_amount); // 成本价上调后同步:单价应按最新成本重算 $product->update(['cost_price' => '20.00']); $this->actingAsSysUser(); $this->putJson("/order/store/item/{$item->id}/sync") ->assertOk()->assertJsonPath('success', true); $item->refresh(); $this->assertSame('20.00', (string) $item->cost_price); $this->assertSame('26.00', (string) $item->price, '20 元上浮 30% = 26.00'); $this->assertSame('52.00', (string) $item->amount, '26.00 × 2'); $order->refresh(); $this->assertSame('52.00', (string) $order->product_amount); $this->assertSame('52.00', (string) $order->total_amount); } /** 一键同步:商品已删除时拒绝同步 */ public function test_sync_item_rejected_when_product_deleted(): void { [, $product, $user] = $this->makeStoreWithProduct('5.00'); $order = $this->placeOrder($product, $user, 1, StoreOrderModel::STATUS_SUMMARIZED); $item = $order->items->first(); $product->delete(); // 软删除 $this->actingAsSysUser(); $this->putJson("/order/store/item/{$item->id}/sync") ->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]); $user = UserModel::factory()->forStore($store->id)->create(); $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, $user); $this->placeOrder($potato, $user); $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); } }