330 lines
14 KiB
PHP
330 lines
14 KiB
PHP
<?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);
|
||
$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
|
||
{
|
||
$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);
|
||
}
|
||
}
|