Files
xin-procurement/tests/Feature/StoreOrderItemTest.php
2026-08-27 14:20:09 +08:00

323 lines
15 KiB
PHP
Raw Permalink 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\PurchaseOrderModel;
use App\Models\StoreModel;
use App\Models\StoreOrderItemModel;
use App\Models\StoreOrderModel;
use App\Models\SupplierModel;
/**
* 门店订单明细(商品快照):
* 下单快照完整商品档案、成本价防泄漏、采购单内门店单品增删改重算汇总、按商品名称搜索订单
* (门店即用户:小程序端登录主体为 StoreModel
*/
class StoreOrderItemTest extends ProcurementTestCase
{
/**
* 造门店 + 上架商品(指定成本价);
* 等级上浮比例按 price/costPrice 反算(售价 = 成本价 × (100 + percent) / 100
*
* @return array{0: StoreModel, 1: ProductModel}
*/
private function makeStoreWithProduct(string $price = '5.00', string $costPrice = '4.00'): array
{
$percent = round(((float) $price / (float) $costPrice - 1) * 100, 2);
$level = CustomerLevelModel::factory()->create(['percent' => $percent]);
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$product = ProductModel::factory()->create([
'status' => ProductModel::STATUS_ON,
'cost_price' => $costPrice,
]);
return [$store, $product];
}
/** 以小程序身份下一单并返回订单(可指定初始状态) */
private function placeOrder(ProductModel $product, StoreModel $store, int $quantity = 2, int $status = StoreOrderModel::STATUS_PENDING): StoreOrderModel
{
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $quantity]]])
->assertOk()->assertJsonPath('success', true);
$order = StoreOrderModel::where('store_id', $store->id)->latest('id')->first();
$this->assertNotNull($order);
if ($status !== StoreOrderModel::STATUS_PENDING) {
$order->update(['status' => $status]);
}
return $order;
}
/** 下单 → 接单 → 生成采购单,返回采购单 */
private function generatePurchase(): PurchaseOrderModel
{
StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
$this->actingAsSysUser();
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
->assertJsonPath('success', true);
return PurchaseOrderModel::first();
}
/** 下单时明细快照完整商品档案(分类/供应商/单位/图片/图文/保质期/成本价) */
public function test_place_order_snapshots_full_product_info(): void
{
$supplier = SupplierModel::factory()->create();
// 上浮 37.5%:成本 4.00 → 售价 5.50
$level = CustomerLevelModel::factory()->create(['percent' => 37.5]);
$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',
]);
$this->actingAsMiniStore($store);
$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
{
[$store, $product] = $this->makeStoreWithProduct('5.00', '4.00');
$order = $this->placeOrder($product, $store);
$this->actingAsMiniStore($store);
$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();
[$store, $product] = $this->makeStoreWithProduct('5.00', '4.00');
$product->update(['supplier_id' => $supplier->id]);
$order = $this->placeOrder($product, $store);
$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_store_item_recalculates_totals(): void
{
[$store, $product] = $this->makeStoreWithProduct('5.00');
$order = $this->placeOrder($product, $store, 2, StoreOrderModel::STATUS_SUMMARIZED);
$item = $order->items->first();
$purchase = $this->generatePurchase();
$this->actingAsSysUser();
$this->putJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}", [
'price' => '6.00',
'quantity' => 5,
'weight' => '2.5',
])->assertOk()->assertJsonPath('success', true);
$item->refresh();
$this->assertSame('6.00', (string) $item->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->total_amount, '订单总金额 = 明细金额合计');
$purchase->refresh();
$this->assertSame('5.00', (string) $purchase->total_quantity);
$this->assertSame('2.500', (string) $purchase->total_weight);
$this->assertSame('20.00', (string) $purchase->estimate_amount, '5 包 × 成本 4.00');
}
/** 采购单已完成:门店单品增删改均拒绝,明细不变 */
public function test_store_item_edits_rejected_when_purchase_completed(): void
{
[$store, $product] = $this->makeStoreWithProduct('5.00');
$order = $this->placeOrder($product, $store, 1, StoreOrderModel::STATUS_SUMMARIZED);
$purchase = $this->generatePurchase();
$purchase->update(['status' => PurchaseOrderModel::STATUS_COMPLETED]);
$this->actingAsSysUser();
$this->putJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}", [
'quantity' => 9,
])->assertOk()->assertJsonPath('success', false)
->assertJsonPath('msg', '采购单已完成,不允许修改明细');
$this->postJson("/purchase/order/{$purchase->id}/store/{$store->id}/item", [
'product_id' => $product->id,
'quantity' => 1,
])->assertJsonPath('success', false);
$this->deleteJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}")
->assertJsonPath('success', false);
$item = $order->items->first();
$this->assertSame(1, $item->refresh()->quantity, '被拒绝后明细不变');
$this->assertSame('5.00', (string) $order->refresh()->total_amount, '被拒绝后总金额不变');
}
/** 修改门店单品参数校验:负单价/负数量被拦截 */
public function test_update_store_item_validates_negative_values(): void
{
[$store, $product] = $this->makeStoreWithProduct('5.00');
$this->placeOrder($product, $store, 1, StoreOrderModel::STATUS_SUMMARIZED);
$purchase = $this->generatePurchase();
$this->actingAsSysUser();
$this->putJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}", [
'quantity' => 1,
'price' => -1,
])->assertOk()
->assertJsonPath('success', false)
->assertJsonPath('msg', '单价不能小于 0');
$this->putJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}", [
'quantity' => -1,
])->assertOk()
->assertJsonPath('success', false)
->assertJsonPath('msg', '采购数量不能小于 0');
}
/** 新增单品:单价按门店等级上浮比例换算(成本 20 上浮 30% = 26.00 */
public function test_add_store_item_uses_level_percent_price(): void
{
$level = CustomerLevelModel::factory()->create(['percent' => 30]);
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '10.00']);
$extra = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '20.00']);
$this->placeOrder($product, $store, 2, StoreOrderModel::STATUS_SUMMARIZED);
$purchase = $this->generatePurchase();
$this->actingAsSysUser();
$this->postJson("/purchase/order/{$purchase->id}/store/{$store->id}/item", [
'product_id' => $extra->id,
'quantity' => 2,
])->assertOk()->assertJsonPath('success', true);
$item = StoreOrderItemModel::where('product_id', $extra->id)->first();
$this->assertNotNull($item);
$this->assertSame('26.00', (string) $item->price, '20 元上浮 30% = 26.00');
$this->assertSame('52.00', (string) $item->amount, '26.00 × 2');
$this->assertSame('20.00', (string) $item->cost_price, '快照成本价');
$this->assertSame($extra->name, $item->product_name, '快照品名取自商品档案');
}
/** 新增单品:商品已删除时拒绝 */
public function test_add_store_item_rejected_when_product_deleted(): void
{
[$store, $product] = $this->makeStoreWithProduct('5.00');
$this->placeOrder($product, $store, 1, StoreOrderModel::STATUS_SUMMARIZED);
$purchase = $this->generatePurchase();
$extra = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '9.00']);
$extra->delete(); // 软删除
$this->actingAsSysUser();
$this->postJson("/purchase/order/{$purchase->id}/store/{$store->id}/item", [
'product_id' => $extra->id,
'quantity' => 1,
])->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]);
$cabbage = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'name' => '大白菜A', 'cost_price' => '5.00']);
$potato = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'name' => '土豆B', 'cost_price' => '5.00']);
$this->placeOrder($cabbage, $store);
$this->placeOrder($potato, $store);
$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);
}
/** 后台订单列表按订货日期区间筛选(快捷按钮下发 Y-m-d 起止日期) */
public function test_admin_order_list_filters_by_order_date_range(): void
{
[$store, $product] = $this->makeStoreWithProduct();
$this->placeOrder($product, $store, 1);
$this->placeOrder($product, $store, 1);
$this->placeOrder($product, $store, 1);
$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->actingAsSysUser();
$this->getJson('/order/store?order_date[]=2026-08-05&order_date[]=2026-08-10')
->assertOk()->assertJsonPath('data.total', 2);
$this->getJson('/order/store?order_date[]=2026-08-05&order_date[]=2026-08-05')
->assertOk()->assertJsonPath('data.total', 1, '今天/昨天等单日快捷按钮起止同日');
$this->getJson('/order/store?order_date[]=2026-08-11&order_date[]=2026-08-12')
->assertOk()->assertJsonPath('data.total', 0);
}
/** 后台订单列表按采购单号模糊搜索 */
public function test_admin_order_list_search_by_purchase_no(): void
{
[$store, $product] = $this->makeStoreWithProduct();
$this->placeOrder($product, $store, 1, StoreOrderModel::STATUS_SUMMARIZED);
$purchase = $this->generatePurchase();
// 生成采购单外的另一笔订单(不应被命中)
$this->placeOrder($product, $store, 1);
$this->actingAsSysUser();
$response = $this->getJson('/order/store?purchase_no=' . $purchase->purchase_no);
$response->assertOk()->assertJsonPath('success', true);
$this->assertSame(1, $response->json('data.total'));
$this->assertSame($purchase->purchase_no, $response->json('data.data.0.purchase.purchase_no'));
// 单号片段模糊命中
$fragment = substr($purchase->purchase_no, -6);
$this->getJson('/order/store?purchase_no=' . $fragment)
->assertOk()->assertJsonPath('data.total', 1);
// 无匹配单号 → 空列表
$this->getJson('/order/store?purchase_no=NOT-EXIST-NO')
->assertOk()->assertJsonPath('data.total', 0);
}
}