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

250 lines
11 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\NoticeModel;
use App\Models\ProductCategoryModel;
use App\Models\ProductModel;
use App\Models\StoreModel;
/**
* 商品成本价 + 等级上浮计价:
* 售价换算、后台/小程序列表展示、批量调价(成本价)、成本价防泄漏
*/
class ProductCostPriceTest extends ProcurementTestCase
{
/** 造一个商品分类(后台创建商品必填) */
private function makeCategory(): ProductCategoryModel
{
return ProductCategoryModel::create([
'parent_id' => 0,
'name' => '测试分类',
'sort' => 1,
'status' => ProductCategoryModel::STATUS_NORMAL,
]);
}
/** 造门店(等级上浮 percent)+ 上架商品(成本价 cost */
private function makeStoreWithPercentProduct(float $cost = 10, float $percent = 30): array
{
$level = CustomerLevelModel::factory()->create(['percent' => $percent]);
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$product = ProductModel::factory()->create([
'status' => ProductModel::STATUS_ON,
'cost_price' => $cost,
]);
return [$store, $product];
}
/** 售价换算:成本价 × (100 + percent) / 100 */
public function test_calc_level_price(): void
{
$this->assertSame('13.00', CustomerLevelModel::calcLevelPrice('10.00', '30'), '10 元上浮 30% = 13.00');
$this->assertSame('13.05', CustomerLevelModel::calcLevelPrice('10.00', '30.50'), '10 元上浮 30.5% = 13.05');
$this->assertSame('5.50', CustomerLevelModel::calcLevelPrice('5.50', '0'), '上浮 0% = 成本价');
}
/** 换算边界:成本为 0 时售价按 0 兜底 */
public function test_calc_level_price_edge_cases(): void
{
$this->assertSame('0.00', CustomerLevelModel::calcLevelPrice('0', '30'), '成本为 0 时售价按 0 兜底');
$this->assertSame('10.00', CustomerLevelModel::calcLevelPrice('10.00', '0'), '上浮 0% = 成本价');
}
/** 后台商品列表:含成本价列 + 各启用等级的换算价 */
public function test_admin_list_contains_cost_price_and_level_prices(): void
{
$this->actingAsSysUser();
$level = CustomerLevelModel::factory()->create(['percent' => 30]);
$product = ProductModel::factory()->create(['cost_price' => 10.00]);
$response = $this->getJson('/product/goods');
$response->assertOk()->assertJsonPath('success', true);
$row = collect($response->json('data.data'))->firstWhere('id', $product->id);
$this->assertNotNull($row);
$this->assertSame('10.00', (string) $row['cost_price'], '后台列表应含成本价');
$priceRow = collect($row['prices'])->firstWhere('level_id', $level->id);
$this->assertNotNull($priceRow, '后台列表应含该等级的换算价');
$this->assertSame('13.00', (string) $priceRow['price'], '10 元上浮 30% = 13.00');
$this->assertSame(30.0, (float) $priceRow['percent']);
}
/** 小程序列表:返回换算后实际价,且成本价不泄漏 */
public function test_mini_list_returns_actual_price_without_cost_price(): void
{
[$store, $product] = $this->makeStoreWithPercentProduct();
$this->actingAsMiniStore($store);
$response = $this->getJson('/mini/product/list');
$response->assertOk()->assertJsonPath('success', true);
$row = collect($response->json('data.data'))->firstWhere('id', $product->id);
$this->assertNotNull($row);
$this->assertSame('13.00', (string) $row['price'], '小程序端应返回按等级上浮换算的实际价');
$this->assertArrayNotHasKey('cost_price', $row, '成本价为商业敏感数据,不得泄漏到小程序端');
$this->assertArrayNotHasKey('prices', $row, '价格明细(含上浮比例)不应下发给小程序');
}
/** 后台创建商品:成本价落库(售价由等级上浮比例换算,不再随商品提交价格行) */
public function test_create_product_with_cost_price(): void
{
$this->actingAsSysUser();
$category = $this->makeCategory();
$this->postJson('/product/goods', [
'category_id' => $category->id,
'name' => '上浮计价商品',
'content' => '测试图文详情',
'cost_price' => 10,
])->assertOk()->assertJsonPath('success', true);
$product = ProductModel::where('name', '上浮计价商品')->first();
$this->assertNotNull($product);
$this->assertSame('10.00', (string) $product->cost_price);
}
/** 单价单位:不传默认「元/斤」,创建/编辑可修改 */
public function test_product_price_unit_default_and_editable(): void
{
$this->actingAsSysUser();
$category = $this->makeCategory();
// 不传单价单位 → 默认「元/斤」
$this->postJson('/product/goods', [
'category_id' => $category->id,
'name' => '默认单价单位商品',
'content' => '测试图文详情',
])->assertOk()->assertJsonPath('success', true);
$product = ProductModel::where('name', '默认单价单位商品')->first();
$this->assertSame('元/斤', $product->price_unit);
// 创建时指定 + 编辑修改
$this->postJson('/product/goods', [
'category_id' => $category->id,
'name' => '箱装商品',
'content' => '测试图文详情',
'price_unit' => '元/箱',
])->assertOk()->assertJsonPath('success', true);
$product2 = ProductModel::where('name', '箱装商品')->first();
$this->assertSame('元/箱', $product2->price_unit);
$this->putJson("/product/goods/{$product2->id}", [
'category_id' => $category->id,
'name' => '箱装商品',
'price_unit' => '元/件',
])->assertOk()->assertJsonPath('success', true);
$this->assertSame('元/件', $product2->refresh()->price_unit);
// 超长拒绝
$this->postJson('/product/goods', [
'category_id' => $category->id,
'name' => '异常单价单位商品',
'content' => '测试图文详情',
'price_unit' => str_repeat('元', 21),
])->assertOk()->assertJsonPath('success', false);
}
/** 批量调价-成本价行:更新成本并通知门店;等级售价随上浮比例联动 */
public function test_batch_price_cost_row_update(): void
{
$this->actingAsSysUser();
$level = CustomerLevelModel::factory()->create(['percent' => 30]);
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$product = ProductModel::factory()->create(['cost_price' => 10]);
$this->putJson('/product/goods/batchPrice', [
'updates' => [
['product_id' => $product->id, 'cost_price' => 20],
],
])->assertOk()->assertJsonPath('success', true);
$this->assertSame('20.00', (string) $product->refresh()->cost_price, '成本价应已更新');
$this->assertSame(
1,
NoticeModel::where('store_id', $store->id)->where('type', NoticeModel::TYPE_PRICE)->count(),
'门店应收到价格变更通知'
);
// 等级售价联动:20 元上浮 30% = 26.00
$this->actingAsMiniStore($store);
$row = collect($this->getJson('/mini/product/list')->json('data.data'))->firstWhere('id', $product->id);
$this->assertSame('26.00', (string) $row['price'], '成本变更后小程序端售价应按上浮比例联动');
}
/** 批量调价-空行(仅 product_id,缺成本价)→ 验证失败 */
public function test_batch_price_empty_row_rejected(): void
{
$this->actingAsSysUser();
$product = ProductModel::factory()->create();
$this->putJson('/product/goods/batchPrice', [
'updates' => [
['product_id' => $product->id],
],
])->assertOk()
->assertJsonPath('success', false)
->assertJsonPath('msg', '调价行缺少成本价');
}
/** 价格矩阵:行含成本价,等级格为按上浮比例换算的售价;成本价未设置时等级格为 null */
public function test_price_matrix_includes_cost_and_level_price_columns(): void
{
$this->actingAsSysUser();
$level = CustomerLevelModel::factory()->create(['percent' => 30]);
$product = ProductModel::factory()->create(['cost_price' => 10]);
$noCostProduct = ProductModel::factory()->create(['cost_price' => 0]);
$response = $this->getJson('/product/goods/priceMatrix');
$response->assertOk()->assertJsonPath('success', true);
$row = collect($response->json('data.rows'))->firstWhere('id', $product->id);
$this->assertNotNull($row);
$this->assertSame(10.0, (float) $row['cost_price']);
$this->assertSame(13.0, (float) $row['price_' . $level->id], '矩阵等级格应显示换算价');
$noCostRow = collect($response->json('data.rows'))->firstWhere('id', $noCostProduct->id);
$this->assertNotNull($noCostRow);
$this->assertNull($noCostRow['price_' . $level->id], '成本价未设置时等级格应为 null');
}
/** 小程序下单:按等级上浮换算价重算金额并快照 */
public function test_mini_order_uses_level_price(): void
{
[$store, $product] = $this->makeStoreWithPercentProduct(10, 30);
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', [
'items' => [['product_id' => $product->id, 'quantity' => 3]],
])->assertOk()
->assertJsonPath('success', true)
->assertJsonPath('data.total_amount', '39.00');
$item = $store->orders()->latest('id')->first()->items->first();
$this->assertSame('13.00', (string) $item->price, '订单明细快照应为换算价');
$this->assertSame('39.00', (string) $item->amount);
}
/** 小程序购物车:列表金额按等级上浮换算价计算 */
public function test_mini_cart_uses_level_price(): void
{
[$store, $product] = $this->makeStoreWithPercentProduct(10, 30);
$this->actingAsMiniStore($store);
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 2.5])
->assertOk()->assertJsonPath('success', true);
$this->getJson('/mini/cart')
->assertOk()
->assertJsonPath('data.items.0.price', '13.00')
->assertJsonPath('data.items.0.amount', '32.50')
->assertJsonPath('data.total_amount', '32.50');
}
}