561 lines
27 KiB
PHP
561 lines
27 KiB
PHP
<?php
|
||
|
||
namespace Tests\Feature;
|
||
|
||
use App\Models\BillModel;
|
||
use App\Models\CustomerLevelModel;
|
||
use App\Models\ProductModel;
|
||
use App\Models\PurchaseOrderModel;
|
||
use App\Models\StoreModel;
|
||
use App\Models\StoreOrderModel;
|
||
use Modules\SystemTool\Models\SysFileModel;
|
||
use Modules\SystemTool\Models\SysSiteConfigGroupModel;
|
||
use Modules\SystemTool\Models\SysSiteConfigItemsModel;
|
||
use Modules\SystemTool\Services\SysSiteConfigService;
|
||
|
||
/**
|
||
* 小程序下单:等级上浮价快照、服务端重算总价、取消限制、门店数据隔离
|
||
* (门店即用户:小程序端登录主体为 StoreModel)
|
||
*/
|
||
class StoreOrderTest extends ProcurementTestCase
|
||
{
|
||
/**
|
||
* @return array{0: StoreModel, 1: ProductModel}
|
||
*/
|
||
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,
|
||
'cost_price' => $price,
|
||
]);
|
||
|
||
return [$store, $product];
|
||
}
|
||
|
||
/** 下单快照等级价,服务端重算单品金额与订单总价 */
|
||
public function test_place_order_snapshots_level_price_and_recalculates(): void
|
||
{
|
||
[$store, $product] = $this->makeStoreWithProduct('5.50');
|
||
$this->actingAsMiniStore($store);
|
||
|
||
$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] = $this->makeStoreWithProduct('5.00');
|
||
$this->actingAsMiniStore($store);
|
||
|
||
$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_place_order_prefills_reference_weight(): void
|
||
{
|
||
$level = CustomerLevelModel::factory()->create();
|
||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||
$byBox = ProductModel::factory()->create([
|
||
'status' => ProductModel::STATUS_ON,
|
||
'cost_price' => '5.00',
|
||
'spec' => '10斤/箱',
|
||
'unit' => '箱',
|
||
]);
|
||
$byJin = ProductModel::factory()->create([
|
||
'status' => ProductModel::STATUS_ON,
|
||
'cost_price' => '5.00',
|
||
'spec' => '散装',
|
||
'unit' => '斤',
|
||
]);
|
||
|
||
$this->actingAsMiniStore($store);
|
||
$this->postJson('/mini/order', [
|
||
'items' => [
|
||
['product_id' => $byBox->id, 'quantity' => 3],
|
||
['product_id' => $byJin->id, 'quantity' => 5],
|
||
],
|
||
])->assertOk()->assertJsonPath('success', true);
|
||
|
||
$order = StoreOrderModel::where('store_id', $store->id)->first();
|
||
$boxItem = $order->items->firstWhere('product_id', $byBox->id);
|
||
$jinItem = $order->items->firstWhere('product_id', $byJin->id);
|
||
$this->assertSame('30.000', (string) $boxItem->weight, '3 箱 × 10斤/箱');
|
||
$this->assertSame('5.000', (string) $jinItem->weight, '按斤计价:订货量即重量');
|
||
$this->assertSame('35.000', (string) $order->total_weight, '订单总重量 = 明细参考重量合计');
|
||
}
|
||
|
||
/** 门店绑定的客户等级被删除时拒绝下单 */
|
||
public function test_store_level_missing_rejected(): void
|
||
{
|
||
[$store, $product] = $this->makeStoreWithProduct('5.00');
|
||
CustomerLevelModel::whereKey($store->level_id)->delete();
|
||
$this->actingAsMiniStore($store);
|
||
|
||
$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] = $this->makeStoreWithProduct();
|
||
$this->actingAsMiniStore($store);
|
||
$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->refresh()->status);
|
||
|
||
// 已取消不可重复取消
|
||
$this->putJson("/mini/order/{$order->id}/cancel")->assertJsonPath('success', false);
|
||
}
|
||
|
||
/** 已汇总订单不可取消 */
|
||
public function test_summarized_order_cannot_be_cancelled(): void
|
||
{
|
||
[$store, $product] = $this->makeStoreWithProduct();
|
||
$this->actingAsMiniStore($store);
|
||
$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->refresh()->status);
|
||
}
|
||
|
||
/** 造一笔指定账单日期/状态的门店账单(默认总额 100.00 未支付) */
|
||
private function makeStoreBill(StoreModel $store, string $billDate, array $attributes = []): BillModel
|
||
{
|
||
return BillModel::create(array_merge([
|
||
'bill_no' => 'ZD' . random_int(100000000000, 999999999999),
|
||
'purchase_id' => PurchaseOrderModel::factory()->create()->id,
|
||
'store_id' => $store->id,
|
||
'bill_date' => $billDate,
|
||
'total_amount' => '100.00',
|
||
'status' => BillModel::STATUS_UNPAID,
|
||
], $attributes));
|
||
}
|
||
|
||
/** 回款周期校验:存在逾期未回款账单(含审核中)时禁止下单,结清后恢复 */
|
||
public function test_place_order_blocked_by_overdue_unpaid_bill(): void
|
||
{
|
||
$level = CustomerLevelModel::factory()->create();
|
||
$store = StoreModel::factory()->paymentCycle(7)->create(['level_id' => $level->id]);
|
||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']);
|
||
|
||
// 账单日 10 天前(应结算日=3 天前,已逾期)且未支付 → 拒绝
|
||
$this->makeStoreBill($store, now()->subDays(10)->toDateString());
|
||
$this->actingAsMiniStore($store);
|
||
$response = $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]);
|
||
$response->assertOk()->assertJsonPath('success', false);
|
||
$this->assertStringContainsString('已超过回款周期未回款', (string) $response->json('msg'));
|
||
$this->assertStringContainsString('¥100.00', (string) $response->json('msg'));
|
||
$this->assertSame(0, StoreOrderModel::count(), '拦截时不应生成订单');
|
||
|
||
// 审核中(已提交付款凭证)同样拦截
|
||
$this->makeStoreBill($store, now()->subDays(10)->toDateString(), ['payment_id' => 999]);
|
||
$blocked = $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]);
|
||
$blocked->assertJsonPath('success', false);
|
||
$this->assertStringContainsString('2 笔', (string) $blocked->json('msg'));
|
||
|
||
// 全部结清后恢复下单
|
||
BillModel::where('store_id', $store->id)->update(['status' => BillModel::STATUS_PAID]);
|
||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||
->assertJsonPath('success', true);
|
||
}
|
||
|
||
/** 回款周期校验:周期内账单与到期日当天均不拦截;回款周期 0 天=任何未回款账单(含当天)都拦截 */
|
||
public function test_place_order_allowed_within_payment_cycle(): void
|
||
{
|
||
$level = CustomerLevelModel::factory()->create();
|
||
$store = StoreModel::factory()->paymentCycle(7)->create(['level_id' => $level->id]);
|
||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']);
|
||
$this->actingAsMiniStore($store);
|
||
|
||
// 账单日 7 天前 → 应结算日=今天,尚未超过周期,允许下单
|
||
$this->makeStoreBill($store, now()->subDays(7)->toDateString());
|
||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||
->assertJsonPath('success', true);
|
||
|
||
// 回款周期 0 天:昨天的未回款账单拦截;当天的未回款账单同样拦截(立即结清口径)
|
||
$store->update(['payment_cycle_days' => 0]);
|
||
$this->makeStoreBill($store, now()->subDays(1)->toDateString());
|
||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||
->assertJsonPath('success', false);
|
||
BillModel::where('store_id', $store->id)->delete();
|
||
$this->makeStoreBill($store, now()->toDateString());
|
||
$response = $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]);
|
||
$response->assertJsonPath('success', false);
|
||
$this->assertStringContainsString('当日结清', (string) $response->json('msg'));
|
||
|
||
// 0 天周期下无未回款账单 → 正常下单
|
||
BillModel::where('store_id', $store->id)->update(['status' => BillModel::STATUS_PAID]);
|
||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||
->assertJsonPath('success', true);
|
||
}
|
||
|
||
/** 门店数据隔离:只能查看本店订单 */
|
||
public function test_order_data_isolated_between_stores(): void
|
||
{
|
||
[$storeA, $product] = $this->makeStoreWithProduct();
|
||
$this->actingAsMiniStore($storeA);
|
||
$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->actingAsMiniStore($storeB);
|
||
|
||
$this->getJson("/mini/order/{$orderOfA->id}")->assertJsonPath('success', false);
|
||
$this->getJson('/mini/order')->assertJsonPath('data.total', 0);
|
||
}
|
||
|
||
/** 列表输出:状态名 / 可取消标记 / 商品预览(前 3 条)与明细种数 */
|
||
public function test_order_list_outputs_status_name_and_item_preview(): void
|
||
{
|
||
$level = CustomerLevelModel::factory()->create();
|
||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||
$this->actingAsMiniStore($store);
|
||
|
||
$items = [];
|
||
foreach (['白菜', '土豆', '番茄', '黄瓜'] as $name) {
|
||
$product = ProductModel::factory()->create(['name' => $name, 'status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']);
|
||
$items[] = ['product_id' => $product->id, 'quantity' => 2];
|
||
}
|
||
$this->postJson('/mini/order', ['items' => $items])->assertJsonPath('success', true);
|
||
$order = StoreOrderModel::where('store_id', $store->id)->first();
|
||
|
||
$row = collect($this->getJson('/mini/order')->assertOk()->json('data.data'))->firstWhere('id', $order->id);
|
||
$this->assertSame('待接单', $row['status_name']);
|
||
$this->assertTrue($row['can_cancel']);
|
||
$this->assertSame(4, $row['item_count']);
|
||
$this->assertCount(3, $row['items'], '预览仅输出前 3 条明细,完整明细走详情接口');
|
||
$this->assertSame('白菜', $row['items'][0]['product_name']);
|
||
$this->assertSame(2, $row['items'][0]['quantity']);
|
||
|
||
// 已接单后状态名变化且不可取消
|
||
$order->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
|
||
$row = collect($this->getJson('/mini/order')->json('data.data'))->firstWhere('id', $order->id);
|
||
$this->assertSame('已接单', $row['status_name']);
|
||
$this->assertFalse($row['can_cancel']);
|
||
}
|
||
|
||
/** 订货日期区间筛选(配合 /order/summary 周期下钻) */
|
||
public function test_order_list_filters_by_order_date_range(): void
|
||
{
|
||
[$store, $product] = $this->makeStoreWithProduct();
|
||
$this->actingAsMiniStore($store);
|
||
|
||
foreach ([1, 1, 1] as $qty) {
|
||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
|
||
->assertJsonPath('success', true);
|
||
}
|
||
$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->getJson('/mini/order?start_date=2026-08-03')->assertJsonPath('data.total', 2);
|
||
$this->getJson('/mini/order?start_date=2026-08-03&end_date=2026-08-06')->assertJsonPath('data.total', 1);
|
||
$this->getJson('/mini/order?end_date=2026-08-06')->assertJsonPath('data.total', 2);
|
||
}
|
||
|
||
/** 非法筛选参数被拦截(状态枚举 / 分页上限 / 日期区间倒置) */
|
||
public function test_order_list_rejects_invalid_filters(): void
|
||
{
|
||
[$store] = $this->makeStoreWithProduct();
|
||
$this->actingAsMiniStore($store);
|
||
|
||
$this->getJson('/mini/order?status=7')->assertJsonPath('success', false);
|
||
$this->getJson('/mini/order?pageSize=100')->assertJsonPath('success', false);
|
||
$this->getJson('/mini/order?start_date=2026-08-10&end_date=2026-08-01')->assertJsonPath('success', false);
|
||
}
|
||
|
||
/** 造一笔指定状态的订单(商品 5.00 × 2 = 10.00) */
|
||
private function makeOrderWithStatus(int $status): StoreOrderModel
|
||
{
|
||
[$store, $product] = $this->makeStoreWithProduct('5.00');
|
||
$this->actingAsMiniStore($store);
|
||
$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_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->refresh()->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->actingAsMiniStore(StoreModel::find($order->store_id));
|
||
$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->refresh()->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);
|
||
}
|
||
|
||
/** 手动流转:仅保留接单与取消(待接单 → 已接单/已取消) */
|
||
public function test_manual_status_flow_only_accept_and_cancel(): void
|
||
{
|
||
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_PENDING);
|
||
$this->actingAsSysUser();
|
||
|
||
$this->putJson("/order/store/{$order->id}/status", ['status' => StoreOrderModel::STATUS_SUMMARIZED])
|
||
->assertJsonPath('success', true);
|
||
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $order->refresh()->status);
|
||
|
||
$order2 = $this->makeOrderWithStatus(StoreOrderModel::STATUS_PENDING);
|
||
$this->actingAsSysUser();
|
||
$this->putJson("/order/store/{$order2->id}/status", ['status' => StoreOrderModel::STATUS_CANCELLED])
|
||
->assertJsonPath('success', true);
|
||
$this->assertSame(StoreOrderModel::STATUS_CANCELLED, $order2->refresh()->status);
|
||
}
|
||
|
||
/** 手动流转:配送中/已完成等目标状态被拒绝(由采购/账单链路自动推进) */
|
||
public function test_manual_status_flow_rejects_delivery_and_completion(): void
|
||
{
|
||
foreach ([StoreOrderModel::STATUS_DISTRIBUTION, StoreOrderModel::STATUS_COMPLETED] as $target) {
|
||
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_DELIVERING);
|
||
$this->actingAsSysUser();
|
||
$this->putJson("/order/store/{$order->id}/status", ['status' => $target])
|
||
->assertJsonPath('success', false);
|
||
$this->assertSame(StoreOrderModel::STATUS_DELIVERING, $order->refresh()->status);
|
||
}
|
||
|
||
// 已接单不能手动转采购中(须经生成采购单)
|
||
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED);
|
||
$this->actingAsSysUser();
|
||
$this->putJson("/order/store/{$order->id}/status", ['status' => StoreOrderModel::STATUS_DELIVERING])
|
||
->assertJsonPath('success', false);
|
||
}
|
||
|
||
/** 批量流转:仅接单/取消可选,配送/完成目标整批校验拒绝 */
|
||
public function test_batch_status_only_accept_and_cancel(): void
|
||
{
|
||
$pending1 = $this->makeOrderWithStatus(StoreOrderModel::STATUS_PENDING);
|
||
$pending2 = $this->makeOrderWithStatus(StoreOrderModel::STATUS_PENDING);
|
||
$this->actingAsSysUser();
|
||
|
||
// 批量接单
|
||
$this->putJson('/order/store/batchStatus', [
|
||
'ids' => [$pending1->id, $pending2->id],
|
||
'status' => StoreOrderModel::STATUS_SUMMARIZED,
|
||
])->assertJsonPath('success', true);
|
||
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending1->refresh()->status);
|
||
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending2->refresh()->status);
|
||
|
||
// 批量完成被拒绝(目标状态校验拦截)
|
||
$this->putJson('/order/store/batchStatus', [
|
||
'ids' => [$pending1->id],
|
||
'status' => StoreOrderModel::STATUS_COMPLETED,
|
||
])->assertJsonPath('success', false);
|
||
|
||
// 已接单订单不可批量取消(仅待接单可取消),整批中止
|
||
$this->putJson('/order/store/batchStatus', [
|
||
'ids' => [$pending1->id],
|
||
'status' => StoreOrderModel::STATUS_CANCELLED,
|
||
])->assertJsonPath('success', false);
|
||
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending1->refresh()->status);
|
||
}
|
||
|
||
/** 写入下单时段业务配置并刷新缓存(value 传 null 表示该项不配置) */
|
||
private function setOrderTimeWindow(?string $start, ?string $end): void
|
||
{
|
||
$group = SysSiteConfigGroupModel::create(['title' => '业务配置', 'key' => 'services', 'remark' => '']);
|
||
foreach (['order_time_start' => $start, 'order_time_end' => $end] as $key => $value) {
|
||
if ($value === null) {
|
||
continue;
|
||
}
|
||
SysSiteConfigItemsModel::create([
|
||
'group_id' => $group->id,
|
||
'key' => $key,
|
||
'title' => $key,
|
||
'describe' => '',
|
||
'values' => $value,
|
||
'type' => 'Input',
|
||
'options' => '',
|
||
'sort' => 0,
|
||
]);
|
||
}
|
||
SysSiteConfigService::refreshSiteConfig();
|
||
}
|
||
|
||
/** 截单时间:下单时段外禁止下单,时段内正常下单 */
|
||
public function test_place_order_blocked_outside_order_time_window(): void
|
||
{
|
||
[$store, $product] = $this->makeStoreWithProduct('5.00');
|
||
$this->setOrderTimeWindow('08:00', '18:00');
|
||
$this->actingAsMiniStore($store);
|
||
|
||
// 20:00 已截单 → 拒绝且不生成订单
|
||
$this->travelTo('2026-09-05 20:00:00');
|
||
$response = $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]);
|
||
$response->assertOk()->assertJsonPath('success', false);
|
||
$this->assertStringContainsString('不在下单时段内', (string) $response->json('msg'));
|
||
$this->assertStringContainsString('08:00 - 18:00', (string) $response->json('msg'));
|
||
$this->assertSame(0, StoreOrderModel::count());
|
||
|
||
// 边界:08:00 开始时间与 18:00 截单时间均可下单
|
||
$this->travelTo('2026-09-05 08:00:00');
|
||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||
->assertJsonPath('success', true);
|
||
$this->travelTo('2026-09-05 18:00:00');
|
||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||
->assertJsonPath('success', true);
|
||
$this->assertSame(2, StoreOrderModel::count());
|
||
}
|
||
|
||
/** 截单时间:跨天时段(开始时间晚于截单时间,如 20:00-次日06:00) */
|
||
public function test_place_order_cross_midnight_time_window(): void
|
||
{
|
||
[$store, $product] = $this->makeStoreWithProduct('5.00');
|
||
$this->setOrderTimeWindow('20:00', '06:00');
|
||
$this->actingAsMiniStore($store);
|
||
|
||
// 当晚 22:00 与 次日 05:00 均在时段内
|
||
$this->travelTo('2026-09-05 22:00:00');
|
||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||
->assertJsonPath('success', true);
|
||
$this->travelTo('2026-09-06 05:00:00');
|
||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||
->assertJsonPath('success', true);
|
||
|
||
// 中午 12:00 不在时段内 → 拒绝
|
||
$this->travelTo('2026-09-06 12:00:00');
|
||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||
->assertJsonPath('success', false);
|
||
$this->assertSame(2, StoreOrderModel::count());
|
||
}
|
||
|
||
/** 截单时间:只配置截单时间时按单边限制(截止后不能下单);格式非法按未配置处理 */
|
||
public function test_place_order_only_cutoff_time_and_invalid_config(): void
|
||
{
|
||
[$store, $product] = $this->makeStoreWithProduct('5.00');
|
||
$this->setOrderTimeWindow(null, '18:00');
|
||
$this->actingAsMiniStore($store);
|
||
|
||
$this->travelTo('2026-09-05 17:59:00');
|
||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||
->assertJsonPath('success', true);
|
||
$this->travelTo('2026-09-05 18:01:00');
|
||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||
->assertJsonPath('success', false);
|
||
|
||
// 非法格式不拦截(避免误配置导致全天无法下单)
|
||
SysSiteConfigItemsModel::query()->update(['values' => 'abc']);
|
||
SysSiteConfigService::refreshSiteConfig();
|
||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||
->assertJsonPath('success', true);
|
||
}
|
||
|
||
/** 订单列表商品预览:附带首图/规格/单位供小程序端展示 */
|
||
public function test_order_list_preview_includes_image_spec_and_unit(): void
|
||
{
|
||
$file = SysFileModel::create([
|
||
'group_id' => 1,
|
||
'disk' => 'local',
|
||
'channel' => 10,
|
||
'file_type' => 10,
|
||
'file_name' => '白菜.jpg',
|
||
'file_path' => 'product/baicai.jpg',
|
||
'file_size' => 1024,
|
||
'file_ext' => 'jpg',
|
||
'uploader_id' => 1,
|
||
]);
|
||
|
||
$level = CustomerLevelModel::factory()->create();
|
||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||
$product = ProductModel::factory()->create([
|
||
'status' => ProductModel::STATUS_ON,
|
||
'spec' => '30斤/筐',
|
||
'unit' => '筐',
|
||
'image_ids' => (string) $file->id,
|
||
'cost_price' => '5.00',
|
||
]);
|
||
|
||
$this->actingAsMiniStore($store);
|
||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 2]]])
|
||
->assertJsonPath('success', true);
|
||
|
||
$rows = $this->getJson('/mini/order')->assertOk()->json('data.data');
|
||
$preview = $rows[0]['items'][0];
|
||
|
||
$this->assertSame($product->name, $preview['product_name']);
|
||
$this->assertSame('30斤/筐', $preview['product_spec'], '预览附规格');
|
||
$this->assertSame('筐', $preview['unit'], '预览附单位');
|
||
$this->assertStringContainsString('baicai.jpg', (string) $preview['image'], '预览附首图URL');
|
||
$this->assertArrayNotHasKey('image_ids', $preview, 'image_ids 解析后不输出');
|
||
}
|
||
}
|