Files
xin-procurement/tests/Feature/StoreOrderItemTest.php
T
2026-08-11 23:26:34 +08:00

331 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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);
}
}