订单详情

This commit is contained in:
liu
2026-08-11 23:26:34 +08:00
parent 5d26f04ba2
commit ce958d690f
10 changed files with 996 additions and 70 deletions
+330
View File
@@ -0,0 +1,330 @@
<?php
namespace Tests\Feature;
use App\Models\CustomerLevelModel;
use App\Models\ProductModel;
use App\Models\ProductPriceModel;
use App\Models\StoreModel;
use App\Models\StoreOrderModel;
use App\Models\SupplierModel;
use App\Models\UserModel;
/**
* 门店订单明细(商品快照):
* 下单快照完整商品档案、成本价防泄漏、后台明细修改重算总价、一键同步商品档案、按商品名称搜索订单
*/
class StoreOrderItemTest extends ProcurementTestCase
{
/**
* 造门店 + 上架商品(指定成本价)+ 固定等级价 + 门店用户
*
* @return array{0: StoreModel, 1: ProductModel, 2: UserModel}
*/
private function makeStoreWithProduct(string $price = '5.00', string $costPrice = '4.00'): array
{
$level = CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$product = ProductModel::factory()->create([
'status' => ProductModel::STATUS_ON,
'cost_price' => $costPrice,
]);
ProductPriceModel::factory()->create([
'product_id' => $product->id,
'level_id' => $level->id,
'price' => $price,
]);
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();
$level = CustomerLevelModel::factory()->create();
$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',
]);
ProductPriceModel::factory()->create([
'product_id' => $product->id,
'level_id' => $level->id,
'price' => '5.50',
]);
$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);
$order->update(['added_amount' => '6.00']); // 已有附加金额
$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('36.00', (string) $order->total_amount, '订单总金额 = 商品 30 + 附加 6');
}
/** 已完成/已取消订单不允许修改明细 */
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
{
$level = CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$product = ProductModel::factory()->create([
'status' => ProductModel::STATUS_ON,
'cost_price' => '10.00',
]);
ProductPriceModel::factory()->create([
'product_id' => $product->id,
'level_id' => $level->id,
'price_type' => ProductPriceModel::PRICE_TYPE_PERCENT,
'percent' => 30,
]);
$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']);
$potato = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'name' => '土豆B']);
foreach ([$cabbage, $potato] as $p) {
ProductPriceModel::factory()->create([
'product_id' => $p->id,
'level_id' => $level->id,
'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);
}
}