Files
xin-procurement/tests/Feature/StoreOrderTest.php
T
2026-08-12 09:42:48 +08:00

307 lines
13 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\UserModel;
use Illuminate\Support\Facades\Cache;
use Modules\SystemTool\Models\SysSiteConfigGroupModel;
use Modules\SystemTool\Models\SysSiteConfigItemsModel;
/**
* 小程序下单:等级价快照、服务端重算总价、取消限制、门店数据隔离
*/
class StoreOrderTest extends ProcurementTestCase
{
/**
* @return array{0: StoreModel, 1: ProductModel, 2: UserModel}
*/
private function makeStoreWithProduct(string $price = '5.00'): array
{
$level = CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]);
ProductPriceModel::factory()->create([
'product_id' => $product->id,
'level_id' => $level->id,
'price' => $price,
]);
return [$store, $product, UserModel::factory()->forStore($store->id)->create()];
}
/** 下单快照等级价,服务端重算单品金额与订单总价 */
public function test_place_order_snapshots_level_price_and_recalculates(): void
{
[$store, $product, $user] = $this->makeStoreWithProduct('5.50');
$this->actingAsMiniUser($user);
$this->postJson('/mini/order', [
'items' => [['product_id' => $product->id, 'quantity' => 3]],
])->assertOk()->assertJsonPath('success', true);
$order = StoreOrderModel::where('store_id', $store->id)->first();
$this->assertNotNull($order);
$this->assertSame(StoreOrderModel::STATUS_PENDING, $order->status);
$this->assertStringStartsWith('SO', $order->order_no);
$this->assertSame('16.50', (string) $order->total_amount, '5.50 × 3');
$item = $order->items->first();
$this->assertSame('5.50', (string) $item->price, '明细快照等级价');
$this->assertSame('16.50', (string) $item->amount);
$this->assertSame($product->name, $item->product_name, '明细快照商品名');
}
/** 前端传入的金额字段一律被忽略 */
public function test_client_supplied_amount_is_ignored(): void
{
[$store, $product, $user] = $this->makeStoreWithProduct('5.00');
$this->actingAsMiniUser($user);
$this->postJson('/mini/order', [
'items' => [[
'product_id' => $product->id,
'quantity' => 2,
'price' => 0.01,
'amount' => 0.02,
]],
])->assertOk()->assertJsonPath('success', true);
$order = StoreOrderModel::where('store_id', $store->id)->first();
$this->assertSame('10.00', (string) $order->total_amount, '应按等级价 5.00×2 计算,忽略前端金额');
}
/** 商品未设置门店等级价格时拒绝下单 */
public function test_product_without_level_price_rejected(): void
{
[, $product, $user] = $this->makeStoreWithProduct('5.00');
ProductPriceModel::where('product_id', $product->id)->delete();
$this->actingAsMiniUser($user);
$this->postJson('/mini/order', [
'items' => [['product_id' => $product->id, 'quantity' => 1]],
])->assertOk()->assertJsonPath('success', false);
$this->assertSame(0, StoreOrderModel::count());
}
/** 取消:仅待汇总订单可取消 */
public function test_cancel_only_pending_orders(): void
{
[$store, $product, $user] = $this->makeStoreWithProduct();
$this->actingAsMiniUser($user);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]);
$order = StoreOrderModel::where('store_id', $store->id)->first();
$this->putJson("/mini/order/{$order->id}/cancel")->assertJsonPath('success', true);
$this->assertSame(StoreOrderModel::STATUS_CANCELLED, $order->fresh()->status);
// 已取消不可重复取消
$this->putJson("/mini/order/{$order->id}/cancel")->assertJsonPath('success', false);
}
/** 已汇总订单不可取消 */
public function test_summarized_order_cannot_be_cancelled(): void
{
[$store, $product, $user] = $this->makeStoreWithProduct();
$this->actingAsMiniUser($user);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]);
$order = StoreOrderModel::where('store_id', $store->id)->first();
$order->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
$this->putJson("/mini/order/{$order->id}/cancel")->assertJsonPath('success', false);
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $order->fresh()->status);
}
/** 门店数据隔离:只能查看本店订单 */
public function test_order_data_isolated_between_stores(): void
{
[$storeA, $product, $userA] = $this->makeStoreWithProduct();
$this->actingAsMiniUser($userA);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]);
$orderOfA = StoreOrderModel::where('store_id', $storeA->id)->first();
// 门店 B 用户访问门店 A 的订单
$storeB = StoreModel::factory()->create(['level_id' => $storeA->level_id]);
$this->actingAsMiniUser(UserModel::factory()->forStore($storeB->id)->create());
$this->getJson("/mini/order/{$orderOfA->id}")->assertJsonPath('success', false);
$this->getJson('/mini/order')->assertJsonPath('data.total', 0);
}
/** 种子化周转框/托盘单价配置(框 2.00、托盘 10.00 */
private function seedContainerConfig(): void
{
$group = SysSiteConfigGroupModel::create(['title' => '业务配置', 'key' => 'services']);
foreach ([['box_amount', '周转框单价', '2.00'], ['tray_amount', '周转托盘单价', '10.00']] as [$key, $title, $value]) {
SysSiteConfigItemsModel::create([
'group_id' => $group->id,
'key' => $key,
'title' => $title,
'type' => 'InputNumber',
'values' => $value,
'sort' => 0,
]);
}
Cache::forget('site_config');
}
/** 造一笔指定状态的订单(商品 5.00 × 2 = 10.00 */
private function makeOrderWithStatus(int $status): StoreOrderModel
{
[$store, $product, $user] = $this->makeStoreWithProduct('5.00');
$this->actingAsMiniUser($user);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 2]]])
->assertJsonPath('success', true);
$order = StoreOrderModel::where('store_id', $store->id)->first();
$order->update(['status' => $status]);
return $order;
}
/** 修改周转框/托盘:自动重算附加金额与订单总金额 */
public function test_container_update_recalculates_amounts(): void
{
$this->seedContainerConfig();
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED);
$this->assertSame('10.00', (string) $order->product_amount, '下单时应写入商品金额');
$this->actingAsSysUser();
$this->putJson("/order/store/{$order->id}/container", ['box_num' => 3, 'tray_num' => 1])
->assertOk()->assertJsonPath('success', true);
$order->refresh();
$this->assertSame(3, $order->box_num);
$this->assertSame(1, $order->tray_num);
$this->assertSame('16.00', (string) $order->added_amount, '3×2.00 + 1×10.00');
$this->assertSame('10.00', (string) $order->product_amount);
$this->assertSame('26.00', (string) $order->total_amount, '10.00 + 16.00');
}
/** 已接单、采购中、配送中均可修改 */
public function test_container_update_allowed_in_editable_statuses(): void
{
$this->seedContainerConfig();
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED);
$this->actingAsSysUser();
foreach ([
StoreOrderModel::STATUS_SUMMARIZED,
StoreOrderModel::STATUS_DELIVERING,
StoreOrderModel::STATUS_DISTRIBUTION,
] as $status) {
$order->update(['status' => $status]);
$this->putJson("/order/store/{$order->id}/container", ['box_num' => 1, 'tray_num' => 0])
->assertOk()->assertJsonPath('success', true);
$this->assertSame('2.00', (string) $order->fresh()->added_amount);
}
}
/** 待接单、已完成、已取消不允许修改 */
public function test_container_update_rejected_in_other_statuses(): void
{
$this->seedContainerConfig();
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_PENDING);
$this->actingAsSysUser();
foreach ([
StoreOrderModel::STATUS_PENDING,
StoreOrderModel::STATUS_COMPLETED,
StoreOrderModel::STATUS_CANCELLED,
] as $status) {
$order->update(['status' => $status]);
$this->putJson("/order/store/{$order->id}/container", ['box_num' => 5, 'tray_num' => 5])
->assertOk()->assertJsonPath('success', false);
}
$order->refresh();
$this->assertSame(0, $order->box_num, '被拒绝后数量不变');
$this->assertSame('10.00', (string) $order->total_amount, '被拒绝后总金额不变');
}
/** 历史订单未写商品金额时按「总额 - 附加」反推回写 */
public function test_container_update_heals_legacy_product_amount(): void
{
$this->seedContainerConfig();
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED);
$order->update(['product_amount' => 0]); // 模拟历史数据
$this->actingAsSysUser();
$this->putJson("/order/store/{$order->id}/container", ['box_num' => 2, 'tray_num' => 0])
->assertOk()->assertJsonPath('success', true);
$order->refresh();
$this->assertSame('10.00', (string) $order->product_amount, '反推回写商品金额');
$this->assertSame('4.00', (string) $order->added_amount, '2×2.00');
$this->assertSame('14.00', (string) $order->total_amount);
}
/** 数量为负时校验失败 */
public function test_container_update_validates_negative_numbers(): void
{
$this->seedContainerConfig();
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED);
$this->actingAsSysUser();
$this->putJson("/order/store/{$order->id}/container", ['box_num' => -1, 'tray_num' => 0])
->assertJsonPath('success', false)
->assertJsonPath('msg', '周转框数量不能小于 0');
}
/** 软删除:仅已取消订单可由后台删除,删除后后台/小程序端均不可见 */
public function test_soft_delete_only_cancelled_orders(): void
{
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_CANCELLED);
// 删除前小程序端可见
$this->getJson('/mini/order')->assertOk()->assertJsonPath('data.total', 1);
$this->actingAsSysUser();
$this->deleteJson("/order/store/{$order->id}")->assertOk()->assertJsonPath('success', true);
$this->assertNotNull($order->fresh()->deleted_at, '软删除应写入 deleted_at');
$this->assertNull(StoreOrderModel::find($order->id), '默认查询不可见软删除订单');
$this->assertNotNull(StoreOrderModel::withTrashed()->find($order->id), '数据仍保留在库中');
// 后台列表/详情不可见
$this->getJson('/order/store')->assertOk()->assertJsonPath('data.total', 0);
$this->getJson("/order/store/{$order->id}")->assertJsonPath('success', false);
// 小程序端历史订单同样不可见
$this->actingAsMiniUser(UserModel::where('store_id', $order->store_id)->first());
$this->getJson('/mini/order')->assertOk()->assertJsonPath('data.total', 0);
$this->getJson("/mini/order/{$order->id}")->assertJsonPath('success', false);
}
/** 软删除:非已取消订单拒绝删除 */
public function test_soft_delete_rejected_for_non_cancelled_orders(): void
{
foreach ([
StoreOrderModel::STATUS_PENDING,
StoreOrderModel::STATUS_SUMMARIZED,
StoreOrderModel::STATUS_DELIVERING,
StoreOrderModel::STATUS_DISTRIBUTION,
StoreOrderModel::STATUS_COMPLETED,
] as $status) {
$order = $this->makeOrderWithStatus($status);
$this->actingAsSysUser();
$this->deleteJson("/order/store/{$order->id}")
->assertOk()->assertJsonPath('success', false);
$this->assertNull($order->fresh()->deleted_at, '非已取消订单不得删除');
}
}
/** 软删除:重复删除返回订单不存在 */
public function test_soft_delete_twice_rejected(): void
{
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_CANCELLED);
$this->actingAsSysUser();
$this->deleteJson("/order/store/{$order->id}")->assertOk()->assertJsonPath('success', true);
$this->deleteJson("/order/store/{$order->id}")->assertJsonPath('success', false);
}
}