create(); $store = StoreModel::factory()->create(['level_id' => $level->id]); $product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]); ProductPriceModel::factory()->create([ 'product_id' => $product->id, 'level_id' => $level->id, 'price' => $price, ]); return [$store, $product, UserModel::factory()->forStore($store->id)->create()]; } /** 下单快照等级价,服务端重算单品金额与订单总价 */ public function test_place_order_snapshots_level_price_and_recalculates(): void { [$store, $product, $user] = $this->makeStoreWithProduct('5.50'); $this->actingAsMiniUser($user); $this->postJson('/mini/order', [ 'items' => [['product_id' => $product->id, 'quantity' => 3]], ])->assertOk()->assertJsonPath('success', true); $order = StoreOrderModel::where('store_id', $store->id)->first(); $this->assertNotNull($order); $this->assertSame(StoreOrderModel::STATUS_PENDING, $order->status); $this->assertStringStartsWith('SO', $order->order_no); $this->assertSame('16.50', (string) $order->total_amount, '5.50 × 3'); $item = $order->items->first(); $this->assertSame('5.50', (string) $item->price, '明细快照等级价'); $this->assertSame('16.50', (string) $item->amount); $this->assertSame($product->name, $item->product_name, '明细快照商品名'); } /** 前端传入的金额字段一律被忽略 */ public function test_client_supplied_amount_is_ignored(): void { [$store, $product, $user] = $this->makeStoreWithProduct('5.00'); $this->actingAsMiniUser($user); $this->postJson('/mini/order', [ 'items' => [[ 'product_id' => $product->id, 'quantity' => 2, 'price' => 0.01, 'amount' => 0.02, ]], ])->assertOk()->assertJsonPath('success', true); $order = StoreOrderModel::where('store_id', $store->id)->first(); $this->assertSame('10.00', (string) $order->total_amount, '应按等级价 5.00×2 计算,忽略前端金额'); } /** 商品未设置门店等级价格时拒绝下单 */ public function test_product_without_level_price_rejected(): void { [, $product, $user] = $this->makeStoreWithProduct('5.00'); ProductPriceModel::where('product_id', $product->id)->delete(); $this->actingAsMiniUser($user); $this->postJson('/mini/order', [ 'items' => [['product_id' => $product->id, 'quantity' => 1]], ])->assertOk()->assertJsonPath('success', false); $this->assertSame(0, StoreOrderModel::count()); } /** 取消:仅待汇总订单可取消 */ public function test_cancel_only_pending_orders(): void { [$store, $product, $user] = $this->makeStoreWithProduct(); $this->actingAsMiniUser($user); $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]); $order = StoreOrderModel::where('store_id', $store->id)->first(); $this->putJson("/mini/order/{$order->id}/cancel")->assertJsonPath('success', true); $this->assertSame(StoreOrderModel::STATUS_CANCELLED, $order->fresh()->status); // 已取消不可重复取消 $this->putJson("/mini/order/{$order->id}/cancel")->assertJsonPath('success', false); } /** 已汇总订单不可取消 */ public function test_summarized_order_cannot_be_cancelled(): void { [$store, $product, $user] = $this->makeStoreWithProduct(); $this->actingAsMiniUser($user); $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]); $order = StoreOrderModel::where('store_id', $store->id)->first(); $order->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]); $this->putJson("/mini/order/{$order->id}/cancel")->assertJsonPath('success', false); $this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $order->fresh()->status); } /** 门店数据隔离:只能查看本店订单 */ public function test_order_data_isolated_between_stores(): void { [$storeA, $product, $userA] = $this->makeStoreWithProduct(); $this->actingAsMiniUser($userA); $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]); $orderOfA = StoreOrderModel::where('store_id', $storeA->id)->first(); // 门店 B 用户访问门店 A 的订单 $storeB = StoreModel::factory()->create(['level_id' => $storeA->level_id]); $this->actingAsMiniUser(UserModel::factory()->forStore($storeB->id)->create()); $this->getJson("/mini/order/{$orderOfA->id}")->assertJsonPath('success', false); $this->getJson('/mini/order')->assertJsonPath('data.total', 0); } /** 种子化周转框/托盘单价配置(框 2.00、托盘 10.00) */ private function seedContainerConfig(): void { $group = SysSiteConfigGroupModel::create(['title' => '业务配置', 'key' => 'services']); foreach ([['box_amount', '周转框单价', '2.00'], ['tray_amount', '周转托盘单价', '10.00']] as [$key, $title, $value]) { SysSiteConfigItemsModel::create([ 'group_id' => $group->id, 'key' => $key, 'title' => $title, 'type' => 'InputNumber', 'values' => $value, 'sort' => 0, ]); } Cache::forget('site_config'); } /** 造一笔指定状态的订单(商品 5.00 × 2 = 10.00) */ private function makeOrderWithStatus(int $status): StoreOrderModel { [$store, $product, $user] = $this->makeStoreWithProduct('5.00'); $this->actingAsMiniUser($user); $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 2]]]) ->assertJsonPath('success', true); $order = StoreOrderModel::where('store_id', $store->id)->first(); $order->update(['status' => $status]); return $order; } /** 修改周转框/托盘:自动重算附加金额与订单总金额 */ public function test_container_update_recalculates_amounts(): void { $this->seedContainerConfig(); $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED); $this->assertSame('10.00', (string) $order->product_amount, '下单时应写入商品金额'); $this->actingAsSysUser(); $this->putJson("/order/store/{$order->id}/container", ['box_num' => 3, 'tray_num' => 1]) ->assertOk()->assertJsonPath('success', true); $order->refresh(); $this->assertSame(3, $order->box_num); $this->assertSame(1, $order->tray_num); $this->assertSame('16.00', (string) $order->added_amount, '3×2.00 + 1×10.00'); $this->assertSame('10.00', (string) $order->product_amount); $this->assertSame('26.00', (string) $order->total_amount, '10.00 + 16.00'); } /** 已接单、采购中、配送中均可修改 */ public function test_container_update_allowed_in_editable_statuses(): void { $this->seedContainerConfig(); $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED); $this->actingAsSysUser(); foreach ([ StoreOrderModel::STATUS_SUMMARIZED, StoreOrderModel::STATUS_DELIVERING, StoreOrderModel::STATUS_DISTRIBUTION, ] as $status) { $order->update(['status' => $status]); $this->putJson("/order/store/{$order->id}/container", ['box_num' => 1, 'tray_num' => 0]) ->assertOk()->assertJsonPath('success', true); $this->assertSame('2.00', (string) $order->fresh()->added_amount); } } /** 待接单、已完成、已取消不允许修改 */ public function test_container_update_rejected_in_other_statuses(): void { $this->seedContainerConfig(); $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_PENDING); $this->actingAsSysUser(); foreach ([ StoreOrderModel::STATUS_PENDING, StoreOrderModel::STATUS_COMPLETED, StoreOrderModel::STATUS_CANCELLED, ] as $status) { $order->update(['status' => $status]); $this->putJson("/order/store/{$order->id}/container", ['box_num' => 5, 'tray_num' => 5]) ->assertOk()->assertJsonPath('success', false); } $order->refresh(); $this->assertSame(0, $order->box_num, '被拒绝后数量不变'); $this->assertSame('10.00', (string) $order->total_amount, '被拒绝后总金额不变'); } /** 历史订单未写商品金额时按「总额 - 附加」反推回写 */ public function test_container_update_heals_legacy_product_amount(): void { $this->seedContainerConfig(); $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED); $order->update(['product_amount' => 0]); // 模拟历史数据 $this->actingAsSysUser(); $this->putJson("/order/store/{$order->id}/container", ['box_num' => 2, 'tray_num' => 0]) ->assertOk()->assertJsonPath('success', true); $order->refresh(); $this->assertSame('10.00', (string) $order->product_amount, '反推回写商品金额'); $this->assertSame('4.00', (string) $order->added_amount, '2×2.00'); $this->assertSame('14.00', (string) $order->total_amount); } /** 数量为负时校验失败 */ public function test_container_update_validates_negative_numbers(): void { $this->seedContainerConfig(); $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED); $this->actingAsSysUser(); $this->putJson("/order/store/{$order->id}/container", ['box_num' => -1, 'tray_num' => 0]) ->assertJsonPath('success', false) ->assertJsonPath('msg', '周转框数量不能小于 0'); } }