create(); $store = StoreModel::factory()->create(['level_id' => $level->id]); $product = ProductModel::factory()->create([ 'status' => ProductModel::STATUS_ON, 'cost_price' => $price, ]); return [$store, $product]; } /** 下单快照等级价,服务端重算单品金额与订单总价 */ public function test_place_order_snapshots_level_price_and_recalculates(): void { [$store, $product] = $this->makeStoreWithProduct('5.50'); $this->actingAsMiniStore($store); $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] = $this->makeStoreWithProduct('5.00'); $this->actingAsMiniStore($store); $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_place_order_prefills_reference_weight(): void { $level = CustomerLevelModel::factory()->create(); $store = StoreModel::factory()->create(['level_id' => $level->id]); $byBox = ProductModel::factory()->create([ 'status' => ProductModel::STATUS_ON, 'cost_price' => '5.00', 'spec' => '10斤/箱', 'unit' => '箱', ]); $byJin = ProductModel::factory()->create([ 'status' => ProductModel::STATUS_ON, 'cost_price' => '5.00', 'spec' => '散装', 'unit' => '斤', ]); $this->actingAsMiniStore($store); $this->postJson('/mini/order', [ 'items' => [ ['product_id' => $byBox->id, 'quantity' => 3], ['product_id' => $byJin->id, 'quantity' => 5], ], ])->assertOk()->assertJsonPath('success', true); $order = StoreOrderModel::where('store_id', $store->id)->first(); $boxItem = $order->items->firstWhere('product_id', $byBox->id); $jinItem = $order->items->firstWhere('product_id', $byJin->id); $this->assertSame('30.000', (string) $boxItem->weight, '3 箱 × 10斤/箱'); $this->assertSame('5.000', (string) $jinItem->weight, '按斤计价:订货量即重量'); $this->assertSame('35.000', (string) $order->total_weight, '订单总重量 = 明细参考重量合计'); } /** 门店绑定的客户等级被删除时拒绝下单 */ public function test_store_level_missing_rejected(): void { [$store, $product] = $this->makeStoreWithProduct('5.00'); CustomerLevelModel::whereKey($store->level_id)->delete(); $this->actingAsMiniStore($store); $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] = $this->makeStoreWithProduct(); $this->actingAsMiniStore($store); $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->refresh()->status); // 已取消不可重复取消 $this->putJson("/mini/order/{$order->id}/cancel")->assertJsonPath('success', false); } /** 已汇总订单不可取消 */ public function test_summarized_order_cannot_be_cancelled(): void { [$store, $product] = $this->makeStoreWithProduct(); $this->actingAsMiniStore($store); $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->refresh()->status); } /** 造一笔指定账单日期/状态的门店账单(默认总额 100.00 未支付) */ private function makeStoreBill(StoreModel $store, string $billDate, array $attributes = []): BillModel { return BillModel::create(array_merge([ 'bill_no' => 'ZD' . random_int(100000000000, 999999999999), 'purchase_id' => PurchaseOrderModel::factory()->create()->id, 'store_id' => $store->id, 'bill_date' => $billDate, 'total_amount' => '100.00', 'status' => BillModel::STATUS_UNPAID, ], $attributes)); } /** 回款周期校验:存在逾期未回款账单(含审核中)时禁止下单,结清后恢复 */ public function test_place_order_blocked_by_overdue_unpaid_bill(): void { $level = CustomerLevelModel::factory()->create(); $store = StoreModel::factory()->paymentCycle(7)->create(['level_id' => $level->id]); $product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']); // 账单日 10 天前(应结算日=3 天前,已逾期)且未支付 → 拒绝 $this->makeStoreBill($store, now()->subDays(10)->toDateString()); $this->actingAsMiniStore($store); $response = $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]); $response->assertOk()->assertJsonPath('success', false); $this->assertStringContainsString('已超过回款周期未回款', (string) $response->json('msg')); $this->assertStringContainsString('¥100.00', (string) $response->json('msg')); $this->assertSame(0, StoreOrderModel::count(), '拦截时不应生成订单'); // 审核中(已提交付款凭证)同样拦截 $this->makeStoreBill($store, now()->subDays(10)->toDateString(), ['payment_id' => 999]); $blocked = $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]); $blocked->assertJsonPath('success', false); $this->assertStringContainsString('2 笔', (string) $blocked->json('msg')); // 全部结清后恢复下单 BillModel::where('store_id', $store->id)->update(['status' => BillModel::STATUS_PAID]); $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]) ->assertJsonPath('success', true); } /** 回款周期校验:周期内账单与到期日当天均不拦截;回款周期 0 天=任何未回款账单(含当天)都拦截 */ public function test_place_order_allowed_within_payment_cycle(): void { $level = CustomerLevelModel::factory()->create(); $store = StoreModel::factory()->paymentCycle(7)->create(['level_id' => $level->id]); $product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']); $this->actingAsMiniStore($store); // 账单日 7 天前 → 应结算日=今天,尚未超过周期,允许下单 $this->makeStoreBill($store, now()->subDays(7)->toDateString()); $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]) ->assertJsonPath('success', true); // 回款周期 0 天:昨天的未回款账单拦截;当天的未回款账单同样拦截(立即结清口径) $store->update(['payment_cycle_days' => 0]); $this->makeStoreBill($store, now()->subDays(1)->toDateString()); $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]) ->assertJsonPath('success', false); BillModel::where('store_id', $store->id)->delete(); $this->makeStoreBill($store, now()->toDateString()); $response = $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]); $response->assertJsonPath('success', false); $this->assertStringContainsString('当日结清', (string) $response->json('msg')); // 0 天周期下无未回款账单 → 正常下单 BillModel::where('store_id', $store->id)->update(['status' => BillModel::STATUS_PAID]); $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]) ->assertJsonPath('success', true); } /** 门店数据隔离:只能查看本店订单 */ public function test_order_data_isolated_between_stores(): void { [$storeA, $product] = $this->makeStoreWithProduct(); $this->actingAsMiniStore($storeA); $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->actingAsMiniStore($storeB); $this->getJson("/mini/order/{$orderOfA->id}")->assertJsonPath('success', false); $this->getJson('/mini/order')->assertJsonPath('data.total', 0); } /** 列表输出:状态名 / 可取消标记 / 商品预览(前 3 条)与明细种数 */ public function test_order_list_outputs_status_name_and_item_preview(): void { $level = CustomerLevelModel::factory()->create(); $store = StoreModel::factory()->create(['level_id' => $level->id]); $this->actingAsMiniStore($store); $items = []; foreach (['白菜', '土豆', '番茄', '黄瓜'] as $name) { $product = ProductModel::factory()->create(['name' => $name, 'status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']); $items[] = ['product_id' => $product->id, 'quantity' => 2]; } $this->postJson('/mini/order', ['items' => $items])->assertJsonPath('success', true); $order = StoreOrderModel::where('store_id', $store->id)->first(); $row = collect($this->getJson('/mini/order')->assertOk()->json('data.data'))->firstWhere('id', $order->id); $this->assertSame('待接单', $row['status_name']); $this->assertTrue($row['can_cancel']); $this->assertSame(4, $row['item_count']); $this->assertCount(3, $row['items'], '预览仅输出前 3 条明细,完整明细走详情接口'); $this->assertSame('白菜', $row['items'][0]['product_name']); $this->assertSame(2, $row['items'][0]['quantity']); // 已接单后状态名变化且不可取消 $order->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]); $row = collect($this->getJson('/mini/order')->json('data.data'))->firstWhere('id', $order->id); $this->assertSame('已接单', $row['status_name']); $this->assertFalse($row['can_cancel']); } /** 订货日期区间筛选(配合 /order/summary 周期下钻) */ public function test_order_list_filters_by_order_date_range(): void { [$store, $product] = $this->makeStoreWithProduct(); $this->actingAsMiniStore($store); foreach ([1, 1, 1] as $qty) { $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]]) ->assertJsonPath('success', true); } $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->getJson('/mini/order?start_date=2026-08-03')->assertJsonPath('data.total', 2); $this->getJson('/mini/order?start_date=2026-08-03&end_date=2026-08-06')->assertJsonPath('data.total', 1); $this->getJson('/mini/order?end_date=2026-08-06')->assertJsonPath('data.total', 2); } /** 非法筛选参数被拦截(状态枚举 / 分页上限 / 日期区间倒置) */ public function test_order_list_rejects_invalid_filters(): void { [$store] = $this->makeStoreWithProduct(); $this->actingAsMiniStore($store); $this->getJson('/mini/order?status=7')->assertJsonPath('success', false); $this->getJson('/mini/order?pageSize=100')->assertJsonPath('success', false); $this->getJson('/mini/order?start_date=2026-08-10&end_date=2026-08-01')->assertJsonPath('success', false); } /** 造一笔指定状态的订单(商品 5.00 × 2 = 10.00) */ private function makeOrderWithStatus(int $status): StoreOrderModel { [$store, $product] = $this->makeStoreWithProduct('5.00'); $this->actingAsMiniStore($store); $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_soft_delete_only_cancelled_orders(): void { $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_CANCELLED); // 删除前小程序端可见 $this->getJson('/mini/order')->assertOk()->assertJsonPath('data.total', 1); $this->actingAsSysUser(); $this->deleteJson("/order/store/{$order->id}")->assertOk()->assertJsonPath('success', true); $this->assertNotNull($order->refresh()->deleted_at, '软删除应写入 deleted_at'); $this->assertNull(StoreOrderModel::find($order->id), '默认查询不可见软删除订单'); $this->assertNotNull(StoreOrderModel::withTrashed()->find($order->id), '数据仍保留在库中'); // 后台列表/详情不可见 $this->getJson('/order/store')->assertOk()->assertJsonPath('data.total', 0); $this->getJson("/order/store/{$order->id}")->assertJsonPath('success', false); // 小程序端历史订单同样不可见 $this->actingAsMiniStore(StoreModel::find($order->store_id)); $this->getJson('/mini/order')->assertOk()->assertJsonPath('data.total', 0); $this->getJson("/mini/order/{$order->id}")->assertJsonPath('success', false); } /** 软删除:非已取消订单拒绝删除 */ public function test_soft_delete_rejected_for_non_cancelled_orders(): void { foreach ([ StoreOrderModel::STATUS_PENDING, StoreOrderModel::STATUS_SUMMARIZED, StoreOrderModel::STATUS_DELIVERING, StoreOrderModel::STATUS_DISTRIBUTION, StoreOrderModel::STATUS_COMPLETED, ] as $status) { $order = $this->makeOrderWithStatus($status); $this->actingAsSysUser(); $this->deleteJson("/order/store/{$order->id}") ->assertOk()->assertJsonPath('success', false); $this->assertNull($order->refresh()->deleted_at, '非已取消订单不得删除'); } } /** 软删除:重复删除返回订单不存在 */ public function test_soft_delete_twice_rejected(): void { $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_CANCELLED); $this->actingAsSysUser(); $this->deleteJson("/order/store/{$order->id}")->assertOk()->assertJsonPath('success', true); $this->deleteJson("/order/store/{$order->id}")->assertJsonPath('success', false); } /** 手动流转:仅保留接单与取消(待接单 → 已接单/已取消) */ public function test_manual_status_flow_only_accept_and_cancel(): void { $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_PENDING); $this->actingAsSysUser(); $this->putJson("/order/store/{$order->id}/status", ['status' => StoreOrderModel::STATUS_SUMMARIZED]) ->assertJsonPath('success', true); $this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $order->refresh()->status); $order2 = $this->makeOrderWithStatus(StoreOrderModel::STATUS_PENDING); $this->actingAsSysUser(); $this->putJson("/order/store/{$order2->id}/status", ['status' => StoreOrderModel::STATUS_CANCELLED]) ->assertJsonPath('success', true); $this->assertSame(StoreOrderModel::STATUS_CANCELLED, $order2->refresh()->status); } /** 手动流转:配送中/已完成等目标状态被拒绝(由采购/账单链路自动推进) */ public function test_manual_status_flow_rejects_delivery_and_completion(): void { foreach ([StoreOrderModel::STATUS_DISTRIBUTION, StoreOrderModel::STATUS_COMPLETED] as $target) { $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_DELIVERING); $this->actingAsSysUser(); $this->putJson("/order/store/{$order->id}/status", ['status' => $target]) ->assertJsonPath('success', false); $this->assertSame(StoreOrderModel::STATUS_DELIVERING, $order->refresh()->status); } // 已接单不能手动转采购中(须经生成采购单) $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED); $this->actingAsSysUser(); $this->putJson("/order/store/{$order->id}/status", ['status' => StoreOrderModel::STATUS_DELIVERING]) ->assertJsonPath('success', false); } /** 批量流转:仅接单/取消可选,配送/完成目标整批校验拒绝 */ public function test_batch_status_only_accept_and_cancel(): void { $pending1 = $this->makeOrderWithStatus(StoreOrderModel::STATUS_PENDING); $pending2 = $this->makeOrderWithStatus(StoreOrderModel::STATUS_PENDING); $this->actingAsSysUser(); // 批量接单 $this->putJson('/order/store/batchStatus', [ 'ids' => [$pending1->id, $pending2->id], 'status' => StoreOrderModel::STATUS_SUMMARIZED, ])->assertJsonPath('success', true); $this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending1->refresh()->status); $this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending2->refresh()->status); // 批量完成被拒绝(目标状态校验拦截) $this->putJson('/order/store/batchStatus', [ 'ids' => [$pending1->id], 'status' => StoreOrderModel::STATUS_COMPLETED, ])->assertJsonPath('success', false); // 已接单订单不可批量取消(仅待接单可取消),整批中止 $this->putJson('/order/store/batchStatus', [ 'ids' => [$pending1->id], 'status' => StoreOrderModel::STATUS_CANCELLED, ])->assertJsonPath('success', false); $this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending1->refresh()->status); } /** 订单列表商品预览:附带首图/规格/单位供小程序端展示 */ public function test_order_list_preview_includes_image_spec_and_unit(): void { $file = SysFileModel::create([ 'group_id' => 1, 'disk' => 'local', 'channel' => 10, 'file_type' => 10, 'file_name' => '白菜.jpg', 'file_path' => 'product/baicai.jpg', 'file_size' => 1024, 'file_ext' => 'jpg', 'uploader_id' => 1, ]); $level = CustomerLevelModel::factory()->create(); $store = StoreModel::factory()->create(['level_id' => $level->id]); $product = ProductModel::factory()->create([ 'status' => ProductModel::STATUS_ON, 'spec' => '30斤/筐', 'unit' => '筐', 'image_ids' => (string) $file->id, 'cost_price' => '5.00', ]); $this->actingAsMiniStore($store); $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 2]]]) ->assertJsonPath('success', true); $rows = $this->getJson('/mini/order')->assertOk()->json('data.data'); $preview = $rows[0]['items'][0]; $this->assertSame($product->name, $preview['product_name']); $this->assertSame('30斤/筐', $preview['product_spec'], '预览附规格'); $this->assertSame('筐', $preview['unit'], '预览附单位'); $this->assertStringContainsString('baicai.jpg', (string) $preview['image'], '预览附首图URL'); $this->assertArrayNotHasKey('image_ids', $preview, 'image_ids 解析后不输出'); } }