小程序用户改用门店登录
This commit is contained in:
@@ -9,7 +9,6 @@ use App\Models\ProductModel;
|
||||
use App\Models\PurchaseOrderModel;
|
||||
use App\Models\StoreModel;
|
||||
use App\Models\StoreOrderItemModel;
|
||||
use App\Models\UserModel;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
|
||||
/**
|
||||
@@ -219,7 +218,7 @@ class BillExportTest extends ProcurementTestCase
|
||||
}
|
||||
|
||||
/** 小程序端导出:强制本店过滤,混入他店账单整批拒绝 */
|
||||
public function test_mini_export_scoped_to_bound_store(): void
|
||||
public function test_mini_export_scoped_to_own_store(): void
|
||||
{
|
||||
$this->freezeTime();
|
||||
|
||||
@@ -233,8 +232,7 @@ class BillExportTest extends ProcurementTestCase
|
||||
$this->makeItem($billA, $product, 10, '2.00');
|
||||
$this->makeItem($billB, $product, 5, '20.00');
|
||||
|
||||
$user = UserModel::factory()->forStore($storeA->id)->create();
|
||||
$this->actingAsMiniUser($user);
|
||||
$this->actingAsMiniStore($storeA);
|
||||
|
||||
// 本店账单正常导出
|
||||
Excel::fake();
|
||||
@@ -256,14 +254,4 @@ class BillExportTest extends ProcurementTestCase
|
||||
$this->get("/mini/bill/export?ids={$billB->id}")
|
||||
->assertJsonPath('success', false);
|
||||
}
|
||||
|
||||
/** 小程序端导出:未绑定门店 → 拒绝 */
|
||||
public function test_mini_export_requires_store_binding(): void
|
||||
{
|
||||
$user = UserModel::factory()->create();
|
||||
$this->actingAsMiniUser($user);
|
||||
|
||||
$this->get('/mini/bill/export?ids=1')
|
||||
->assertJsonPath('success', false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ use App\Models\ProductModel;
|
||||
use App\Models\PurchaseOrderModel;
|
||||
use App\Models\StoreModel;
|
||||
use App\Models\StoreOrderModel;
|
||||
use App\Models\UserModel;
|
||||
use Modules\SystemTool\Models\SysFileModel;
|
||||
|
||||
/**
|
||||
@@ -29,9 +28,8 @@ class BillPaymentTest extends ProcurementTestCase
|
||||
$level = CustomerLevelModel::factory()->create();
|
||||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']);
|
||||
$user = UserModel::factory()->forStore($store->id)->create();
|
||||
|
||||
$this->actingAsMiniUser($user);
|
||||
$this->actingAsMiniStore($store);
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 4]]])
|
||||
->assertJsonPath('success', true);
|
||||
$order = StoreOrderModel::where('store_id', $store->id)->first();
|
||||
@@ -80,9 +78,8 @@ class BillPaymentTest extends ProcurementTestCase
|
||||
$level = CustomerLevelModel::factory()->create();
|
||||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']);
|
||||
$user = UserModel::factory()->forStore($store->id)->create();
|
||||
|
||||
$this->actingAsMiniUser($user);
|
||||
$this->actingAsMiniStore($store);
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 4]]])
|
||||
->assertJsonPath('success', true);
|
||||
$order = StoreOrderModel::where('store_id', $store->id)->first();
|
||||
@@ -144,12 +141,10 @@ class BillPaymentTest extends ProcurementTestCase
|
||||
public function test_payment_audit_pass_accumulates_store_total_purchase_amount(): void
|
||||
{
|
||||
$store = StoreModel::factory()->create();
|
||||
$user = UserModel::factory()->forStore($store->id)->create();
|
||||
|
||||
$payment = PaymentModel::create([
|
||||
'payment_no' => 'ZF202608140001',
|
||||
'store_id' => $store->id,
|
||||
'user_id' => $user->id,
|
||||
'amount' => '150.00',
|
||||
'pay_method' => PaymentModel::METHOD_BANK,
|
||||
'voucher_ids' => '',
|
||||
@@ -176,12 +171,10 @@ class BillPaymentTest extends ProcurementTestCase
|
||||
public function test_payment_audit_reject_does_not_accumulate(): void
|
||||
{
|
||||
$store = StoreModel::factory()->create();
|
||||
$user = UserModel::factory()->forStore($store->id)->create();
|
||||
|
||||
$payment = PaymentModel::create([
|
||||
'payment_no' => 'ZF202608140002',
|
||||
'store_id' => $store->id,
|
||||
'user_id' => $user->id,
|
||||
'amount' => '100.00',
|
||||
'pay_method' => PaymentModel::METHOD_WECHAT,
|
||||
'voucher_ids' => '',
|
||||
@@ -234,7 +227,6 @@ class BillPaymentTest extends ProcurementTestCase
|
||||
public function test_payment_detail_returns_voucher_urls(): void
|
||||
{
|
||||
$store = StoreModel::factory()->create();
|
||||
$user = UserModel::factory()->forStore($store->id)->create();
|
||||
|
||||
$makeFile = static function (string $path): int {
|
||||
return (int) SysFileModel::create([
|
||||
@@ -255,7 +247,6 @@ class BillPaymentTest extends ProcurementTestCase
|
||||
$payment = PaymentModel::create([
|
||||
'payment_no' => 'ZF202608140003',
|
||||
'store_id' => $store->id,
|
||||
'user_id' => $user->id,
|
||||
'amount' => '100.00',
|
||||
'pay_method' => PaymentModel::METHOD_BANK,
|
||||
'voucher_ids' => [$fileA, $fileB],
|
||||
|
||||
+52
-90
@@ -6,17 +6,17 @@ use App\Models\CartModel;
|
||||
use App\Models\CustomerLevelModel;
|
||||
use App\Models\ProductModel;
|
||||
use App\Models\StoreModel;
|
||||
use App\Models\UserModel;
|
||||
|
||||
/**
|
||||
* 小程序购物车:加购合并、等级上浮价/金额服务端计算、归属校验、数据隔离、清空
|
||||
* (门店即用户:购物车按 store_id 归属)
|
||||
*/
|
||||
class CartTest extends ProcurementTestCase
|
||||
{
|
||||
/**
|
||||
* 造一家门店 + 一个上架商品(成本价即售价,等级上浮 0%)+ 该店用户
|
||||
* 造一家门店(含等级)+ 一个上架商品(成本价即售价,等级上浮 0%)
|
||||
*
|
||||
* @return array{0: StoreModel, 1: ProductModel, 2: UserModel}
|
||||
* @return array{0: StoreModel, 1: ProductModel}
|
||||
*/
|
||||
private function makeStoreWithProduct(string $price = '5.00'): array
|
||||
{
|
||||
@@ -27,14 +27,14 @@ class CartTest extends ProcurementTestCase
|
||||
'cost_price' => $price,
|
||||
]);
|
||||
|
||||
return [$store, $product, UserModel::factory()->forStore($store->id)->create()];
|
||||
return [$store, $product];
|
||||
}
|
||||
|
||||
/** 加购成功:数量入库、返回购物车项 */
|
||||
public function test_add_to_cart_succeeds(): void
|
||||
{
|
||||
[, $product, $user] = $this->makeStoreWithProduct('5.50');
|
||||
$this->actingAsMiniUser($user);
|
||||
[$store, $product] = $this->makeStoreWithProduct('5.50');
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$this->postJson('/mini/cart', [
|
||||
'product_id' => $product->id,
|
||||
@@ -46,15 +46,15 @@ class CartTest extends ProcurementTestCase
|
||||
|
||||
$cart = CartModel::first();
|
||||
$this->assertNotNull($cart);
|
||||
$this->assertSame($user->id, $cart->user_id);
|
||||
$this->assertSame($store->id, $cart->store_id);
|
||||
$this->assertSame('2.50', (string) $cart->quantity);
|
||||
}
|
||||
|
||||
/** 重复加购同一商品合并累加(同商品唯一行) */
|
||||
public function test_duplicate_add_merges_quantity(): void
|
||||
{
|
||||
[, $product, $user] = $this->makeStoreWithProduct();
|
||||
$this->actingAsMiniUser($user);
|
||||
[$store, $product] = $this->makeStoreWithProduct();
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 2]);
|
||||
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 3])
|
||||
@@ -67,9 +67,9 @@ class CartTest extends ProcurementTestCase
|
||||
/** 已下架商品拒绝加购 */
|
||||
public function test_off_shelf_product_rejected(): void
|
||||
{
|
||||
[, $product, $user] = $this->makeStoreWithProduct();
|
||||
[$store, $product] = $this->makeStoreWithProduct();
|
||||
$product->update(['status' => ProductModel::STATUS_OFF]);
|
||||
$this->actingAsMiniUser($user);
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1])
|
||||
->assertOk()
|
||||
@@ -78,25 +78,11 @@ class CartTest extends ProcurementTestCase
|
||||
$this->assertSame(0, CartModel::count());
|
||||
}
|
||||
|
||||
/** 已软删除商品拒绝加购 */
|
||||
public function test_soft_deleted_product_rejected(): void
|
||||
/** 门店未设置客户等级时拒绝加购 */
|
||||
public function test_store_without_level_rejected(): void
|
||||
{
|
||||
[, $product, $user] = $this->makeStoreWithProduct();
|
||||
$product->delete();
|
||||
$this->actingAsMiniUser($user);
|
||||
|
||||
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1])
|
||||
->assertJsonPath('success', false);
|
||||
|
||||
$this->assertSame(0, CartModel::count());
|
||||
}
|
||||
|
||||
/** 门店绑定的客户等级被删除时拒绝加购 */
|
||||
public function test_store_level_missing_rejected(): void
|
||||
{
|
||||
[$store, $product, $user] = $this->makeStoreWithProduct();
|
||||
CustomerLevelModel::whereKey($store->level_id)->delete();
|
||||
$this->actingAsMiniUser($user);
|
||||
[, $product] = $this->makeStoreWithProduct();
|
||||
$this->actingAsMiniStore(StoreModel::factory()->create(['level_id' => 0]));
|
||||
|
||||
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1])
|
||||
->assertJsonPath('success', false)
|
||||
@@ -105,28 +91,6 @@ class CartTest extends ProcurementTestCase
|
||||
$this->assertSame(0, CartModel::count());
|
||||
}
|
||||
|
||||
/** 未绑定门店的用户拒绝加购 */
|
||||
public function test_unbound_user_rejected(): void
|
||||
{
|
||||
[, $product] = $this->makeStoreWithProduct();
|
||||
$this->actingAsMiniUser(UserModel::factory()->create());
|
||||
|
||||
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1])
|
||||
->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '尚未绑定门店,请联系客服处理');
|
||||
}
|
||||
|
||||
/** 门店未设置客户等级时拒绝加购 */
|
||||
public function test_store_without_level_rejected(): void
|
||||
{
|
||||
[, $product] = $this->makeStoreWithProduct();
|
||||
$store = StoreModel::factory()->create(['level_id' => 0]);
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||
|
||||
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1])
|
||||
->assertJsonPath('success', false);
|
||||
}
|
||||
|
||||
/** 列表:实时等级上浮价、服务端金额、汇总(同店两商品,新加入在前) */
|
||||
public function test_list_with_prices_amounts_and_totals(): void
|
||||
{
|
||||
@@ -134,7 +98,7 @@ class CartTest extends ProcurementTestCase
|
||||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$p1 = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.50']);
|
||||
$p2 = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '3.00']);
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$this->postJson('/mini/cart', ['product_id' => $p1->id, 'quantity' => 3]);
|
||||
$this->postJson('/mini/cart', ['product_id' => $p2->id, 'quantity' => 2]);
|
||||
@@ -163,7 +127,7 @@ class CartTest extends ProcurementTestCase
|
||||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$p1 = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.50']);
|
||||
$p2 = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '3.00']);
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$this->postJson('/mini/cart', ['product_id' => $p1->id, 'quantity' => 3]);
|
||||
$this->postJson('/mini/cart', ['product_id' => $p2->id, 'quantity' => 2]);
|
||||
@@ -181,7 +145,7 @@ class CartTest extends ProcurementTestCase
|
||||
public function test_empty_cart_returns_empty_items(): void
|
||||
{
|
||||
$store = StoreModel::factory()->create(['level_id' => CustomerLevelModel::factory()->create()->id]);
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$this->getJson('/mini/cart')->assertOk()
|
||||
->assertJsonPath('data.items', [])
|
||||
@@ -192,8 +156,8 @@ class CartTest extends ProcurementTestCase
|
||||
/** 修改数量 */
|
||||
public function test_update_quantity(): void
|
||||
{
|
||||
[, $product, $user] = $this->makeStoreWithProduct();
|
||||
$this->actingAsMiniUser($user);
|
||||
[$store, $product] = $this->makeStoreWithProduct();
|
||||
$this->actingAsMiniStore($store);
|
||||
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1]);
|
||||
$cart = CartModel::first();
|
||||
|
||||
@@ -201,30 +165,30 @@ class CartTest extends ProcurementTestCase
|
||||
->assertJsonPath('success', true)
|
||||
->assertJsonPath('data.quantity', '7.00');
|
||||
|
||||
$this->assertSame('7.00', (string) $cart->fresh()->quantity);
|
||||
$this->assertSame('7.00', (string) $cart->refresh()->quantity);
|
||||
}
|
||||
|
||||
/** 修改他人购物车项拒绝(同店另一用户) */
|
||||
public function test_update_other_users_item_rejected(): void
|
||||
/** 修改他店购物车项拒绝 */
|
||||
public function test_update_other_stores_item_rejected(): void
|
||||
{
|
||||
[$store, $product, $userA] = $this->makeStoreWithProduct();
|
||||
$this->actingAsMiniUser($userA);
|
||||
[$store, $product] = $this->makeStoreWithProduct();
|
||||
$this->actingAsMiniStore($store);
|
||||
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1]);
|
||||
$cart = CartModel::first();
|
||||
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||
$this->actingAsMiniStore(StoreModel::factory()->create());
|
||||
$this->putJson("/mini/cart/{$cart->id}", ['quantity' => 9])
|
||||
->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '购物车项不存在');
|
||||
|
||||
$this->assertSame('1.00', (string) $cart->fresh()->quantity, '他人改数量不应生效');
|
||||
$this->assertSame('1.00', (string) $cart->refresh()->quantity, '他店改数量不应生效');
|
||||
}
|
||||
|
||||
/** 修改不存在的购物车项拒绝 */
|
||||
public function test_update_missing_item_rejected(): void
|
||||
{
|
||||
[$store, $product, $user] = $this->makeStoreWithProduct();
|
||||
$this->actingAsMiniUser($user);
|
||||
[$store, $product] = $this->makeStoreWithProduct();
|
||||
$this->actingAsMiniStore($store);
|
||||
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1]);
|
||||
|
||||
$this->putJson('/mini/cart/999999', ['quantity' => 1])->assertJsonPath('success', false);
|
||||
@@ -233,8 +197,8 @@ class CartTest extends ProcurementTestCase
|
||||
/** 数量校验:0 与负值拒绝 */
|
||||
public function test_invalid_quantity_rejected(): void
|
||||
{
|
||||
[, $product, $user] = $this->makeStoreWithProduct();
|
||||
$this->actingAsMiniUser($user);
|
||||
[$store, $product] = $this->makeStoreWithProduct();
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 0])
|
||||
->assertOk()
|
||||
@@ -247,8 +211,8 @@ class CartTest extends ProcurementTestCase
|
||||
/** 删除单项 */
|
||||
public function test_delete_item(): void
|
||||
{
|
||||
[, $product, $user] = $this->makeStoreWithProduct();
|
||||
$this->actingAsMiniUser($user);
|
||||
[$store, $product] = $this->makeStoreWithProduct();
|
||||
$this->actingAsMiniStore($store);
|
||||
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1]);
|
||||
$cart = CartModel::first();
|
||||
|
||||
@@ -256,15 +220,15 @@ class CartTest extends ProcurementTestCase
|
||||
$this->assertSame(0, CartModel::count());
|
||||
}
|
||||
|
||||
/** 删除他人购物车项拒绝 */
|
||||
public function test_delete_other_users_item_rejected(): void
|
||||
/** 删除他店购物车项拒绝 */
|
||||
public function test_delete_other_stores_item_rejected(): void
|
||||
{
|
||||
[$store, $product, $userA] = $this->makeStoreWithProduct();
|
||||
$this->actingAsMiniUser($userA);
|
||||
[$store, $product] = $this->makeStoreWithProduct();
|
||||
$this->actingAsMiniStore($store);
|
||||
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1]);
|
||||
$cart = CartModel::first();
|
||||
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||
$this->actingAsMiniStore(StoreModel::factory()->create());
|
||||
$this->deleteJson("/mini/cart/{$cart->id}")
|
||||
->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '购物车项不存在');
|
||||
@@ -272,28 +236,27 @@ class CartTest extends ProcurementTestCase
|
||||
$this->assertSame(1, CartModel::count());
|
||||
}
|
||||
|
||||
/** 清空购物车仅影响当前用户 */
|
||||
/** 清空购物车仅影响本店 */
|
||||
public function test_clear_only_own_cart(): void
|
||||
{
|
||||
$level = CustomerLevelModel::factory()->create();
|
||||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$storeA = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$storeB = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$p1 = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']);
|
||||
$p2 = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']);
|
||||
$userA = UserModel::factory()->forStore($store->id)->create();
|
||||
$userB = UserModel::factory()->forStore($store->id)->create();
|
||||
|
||||
$this->actingAsMiniUser($userA);
|
||||
$this->actingAsMiniStore($storeA);
|
||||
$this->postJson('/mini/cart', ['product_id' => $p1->id, 'quantity' => 1]);
|
||||
$this->postJson('/mini/cart', ['product_id' => $p2->id, 'quantity' => 1]);
|
||||
|
||||
$this->actingAsMiniUser($userB);
|
||||
$this->actingAsMiniStore($storeB);
|
||||
$this->postJson('/mini/cart', ['product_id' => $p1->id, 'quantity' => 1]);
|
||||
|
||||
$this->actingAsMiniUser($userA);
|
||||
$this->actingAsMiniStore($storeA);
|
||||
$this->deleteJson('/mini/cart')->assertJsonPath('success', true);
|
||||
|
||||
$this->assertSame(0, CartModel::where('user_id', $userA->id)->count());
|
||||
$this->assertSame(1, CartModel::where('user_id', $userB->id)->count(), '他人购物车不受影响');
|
||||
$this->assertSame(0, CartModel::where('store_id', $storeA->id)->count());
|
||||
$this->assertSame(1, CartModel::where('store_id', $storeB->id)->count(), '他店购物车不受影响');
|
||||
}
|
||||
|
||||
/** 未登录访问购物车 → 401 */
|
||||
@@ -302,19 +265,18 @@ class CartTest extends ProcurementTestCase
|
||||
$this->getJson('/mini/cart')->assertStatus(401);
|
||||
}
|
||||
|
||||
/** 跨用户列表隔离:B 看不到 A 的购物车 */
|
||||
public function test_cart_isolated_between_users(): void
|
||||
/** 跨门店列表隔离:B 店看不到 A 店的购物车 */
|
||||
public function test_cart_isolated_between_stores(): void
|
||||
{
|
||||
$level = CustomerLevelModel::factory()->create();
|
||||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$storeA = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$storeB = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']);
|
||||
$userA = UserModel::factory()->forStore($store->id)->create();
|
||||
$userB = UserModel::factory()->forStore($store->id)->create();
|
||||
|
||||
$this->actingAsMiniUser($userA);
|
||||
$this->actingAsMiniStore($storeA);
|
||||
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1]);
|
||||
|
||||
$this->actingAsMiniUser($userB);
|
||||
$this->actingAsMiniStore($storeB);
|
||||
$this->getJson('/mini/cart')->assertOk()
|
||||
->assertJsonPath('data.items', [])
|
||||
->assertJsonPath('data.total_count', 0);
|
||||
|
||||
@@ -10,7 +10,6 @@ use App\Models\ProductModel;
|
||||
use App\Models\PurchaseOrderModel;
|
||||
use App\Models\StoreModel;
|
||||
use App\Models\StoreOrderModel;
|
||||
use App\Models\UserModel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Modules\SystemTool\Services\SysSiteConfigService;
|
||||
@@ -58,9 +57,8 @@ class ContainerReturnTest extends ProcurementTestCase
|
||||
$level = CustomerLevelModel::factory()->create();
|
||||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']);
|
||||
$user = UserModel::factory()->forStore($store->id)->create();
|
||||
|
||||
$this->actingAsMiniUser($user);
|
||||
$this->actingAsMiniStore($store);
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 4]]])
|
||||
->assertJsonPath('success', true);
|
||||
$order = StoreOrderModel::where('store_id', $store->id)->first();
|
||||
|
||||
@@ -11,7 +11,6 @@ use App\Models\StoreModel;
|
||||
use App\Models\StoreOrderItemModel;
|
||||
use App\Models\StoreOrderModel;
|
||||
use App\Models\SupplierModel;
|
||||
use App\Models\UserModel;
|
||||
|
||||
/**
|
||||
* 仪表盘分析页聚合接口:权限校验、指标口径(排除已取消)、趋势补日、排行与对账概览
|
||||
@@ -262,7 +261,6 @@ class DashboardTest extends ProcurementTestCase
|
||||
PaymentModel::create([
|
||||
'payment_no' => 'ZF' . random_int(100000000000, 999999999999),
|
||||
'store_id' => $store->id,
|
||||
'user_id' => 0,
|
||||
'amount' => '88.00',
|
||||
'pay_method' => PaymentModel::METHOD_WECHAT,
|
||||
'status' => PaymentModel::STATUS_PENDING,
|
||||
@@ -302,7 +300,7 @@ class DashboardTest extends ProcurementTestCase
|
||||
$this->assertSame('已完成', $latest[0]['status_name']);
|
||||
}
|
||||
|
||||
/** 基础档案仅统计启用中的门店/商品/供应商/用户 */
|
||||
/** 基础档案仅统计启用中的门店/商品/供应商 */
|
||||
public function test_archives_count_active_records_only(): void
|
||||
{
|
||||
StoreModel::factory()->count(2)->create(['status' => StoreModel::STATUS_NORMAL]);
|
||||
@@ -311,15 +309,14 @@ class DashboardTest extends ProcurementTestCase
|
||||
ProductModel::factory()->create(['status' => ProductModel::STATUS_OFF]);
|
||||
SupplierModel::factory()->create(['status' => SupplierModel::STATUS_NORMAL]);
|
||||
SupplierModel::factory()->create(['status' => SupplierModel::STATUS_DISABLED]);
|
||||
UserModel::factory()->count(2)->create(['status' => UserModel::STATUS_NORMAL]);
|
||||
UserModel::factory()->create(['status' => UserModel::STATUS_DISABLED]);
|
||||
|
||||
$this->actingAsSysUser(['dashboard.analysis']);
|
||||
$this->getJson(self::ROUTE)
|
||||
$response = $this->getJson(self::ROUTE)
|
||||
->assertOk()
|
||||
->assertJsonPath('data.archives.stores', 2)
|
||||
->assertJsonPath('data.archives.products', 3)
|
||||
->assertJsonPath('data.archives.suppliers', 1)
|
||||
->assertJsonPath('data.archives.users', 2);
|
||||
->assertJsonPath('data.archives.suppliers', 1);
|
||||
|
||||
$this->assertArrayNotHasKey('users', $response->json('data.archives'), '档案计数不再返回 users 键(与门店数重复)');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ use App\Models\PurchaseOrderModel;
|
||||
use App\Models\StoreModel;
|
||||
use App\Models\StoreOrderModel;
|
||||
use App\Models\SupplierModel;
|
||||
use App\Models\UserModel;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
|
||||
/**
|
||||
@@ -34,7 +33,7 @@ class ExportTest extends ProcurementTestCase
|
||||
$veg = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'category_id' => $vegRoot->id, 'cost_price' => '5.00']);
|
||||
$meat = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'category_id' => $meatRoot->id, 'cost_price' => '20.00']);
|
||||
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||
$this->actingAsMiniStore($store);
|
||||
$this->postJson('/mini/order', ['items' => [
|
||||
['product_id' => $veg->id, 'quantity' => 2],
|
||||
['product_id' => $meat->id, 'quantity' => 1],
|
||||
@@ -117,12 +116,12 @@ class ExportTest extends ProcurementTestCase
|
||||
'supplier_id' => $supplierB->id,
|
||||
]);
|
||||
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($storeA->id)->create());
|
||||
$this->actingAsMiniStore($storeA);
|
||||
$this->postJson('/mini/order', ['items' => [
|
||||
['product_id' => $veg->id, 'quantity' => 2],
|
||||
['product_id' => $meat->id, 'quantity' => 1],
|
||||
]])->assertJsonPath('success', true);
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($storeB->id)->create());
|
||||
$this->actingAsMiniStore($storeB);
|
||||
$this->postJson('/mini/order', ['items' => [
|
||||
['product_id' => $veg->id, 'quantity' => 3],
|
||||
]])->assertJsonPath('success', true);
|
||||
|
||||
+120
-173
@@ -3,195 +3,142 @@
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\StoreModel;
|
||||
use App\Models\UserModel;
|
||||
use App\Services\WechatService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Modules\SystemTool\Services\SysSiteConfigService;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||
|
||||
/**
|
||||
* 小程序认证:EasyWeChat MockHttpClient 拦截微信调用、
|
||||
* 注册绑定门店、登录签发 token、停用拒绝、双端 token 隔离
|
||||
* 小程序认证:门店 账号 + 密码 登录(不再使用微信能力)、
|
||||
* 停用拒绝、修改密码、双端 token 隔离
|
||||
*/
|
||||
class MiniAuthTest extends ProcurementTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
/** 正确账号密码 → 签发 token,回传门店信息(含等级),刷新最后登录时间 */
|
||||
public function test_login_issues_token_for_valid_credentials(): void
|
||||
{
|
||||
parent::setUp();
|
||||
// 微信配置为 DB 驱动(后台「小程序设置」→ sys_site_config,WechatService 经
|
||||
// site_config('wechatMini') 读取):测试落库并刷新配置缓存
|
||||
DB::table('sys_site_config_group')->insert([
|
||||
'id' => 100,
|
||||
'title' => '小程序设置',
|
||||
'key' => 'wechatMini',
|
||||
'remark' => '',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
$level = \App\Models\CustomerLevelModel::factory()->create();
|
||||
$store = StoreModel::factory()->withPassword('abc12345')->create([
|
||||
'username' => 'store_login_1',
|
||||
'level_id' => $level->id,
|
||||
]);
|
||||
DB::table('sys_site_config_items')->insert([
|
||||
[
|
||||
'group_id' => 100, 'key' => 'appid', 'title' => 'APPID', 'describe' => '',
|
||||
'values' => 'test_appid', 'type' => 'Input', 'options' => null, 'props' => null, 'sort' => 0,
|
||||
'created_at' => now(), 'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'group_id' => 100, 'key' => 'secret', 'title' => 'SecretKey', 'describe' => '',
|
||||
'values' => 'test_secret', 'type' => 'Input', 'options' => null, 'props' => null, 'sort' => 1,
|
||||
'created_at' => now(), 'updated_at' => now(),
|
||||
],
|
||||
|
||||
$response = $this->postJson('/mini/auth/login', [
|
||||
'username' => 'store_login_1',
|
||||
'password' => 'abc12345',
|
||||
]);
|
||||
SysSiteConfigService::refreshSiteConfig();
|
||||
$response->assertOk()
|
||||
->assertJsonPath('success', true)
|
||||
->assertJsonStructure(['data' => ['token', 'user' => ['id', 'name', 'code', 'level']]]);
|
||||
|
||||
$this->assertNotEmpty($response->json('data.token'));
|
||||
$this->assertSame($store->id, $response->json('data.user.id'));
|
||||
// 密码永不回显
|
||||
$this->assertArrayNotHasKey('password', $response->json('data.user'));
|
||||
$this->assertNotNull($store->refresh()->last_login_at);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向 WechatService 注入 MockHttpClient,按 URL 分发微信接口 mock 响应
|
||||
*
|
||||
* @param callable(string, string): MockResponse $responder
|
||||
*/
|
||||
private function mockWechat(callable $responder): void
|
||||
/** 密码错误 → 拒绝 */
|
||||
public function test_login_rejects_wrong_password(): void
|
||||
{
|
||||
app(WechatService::class)->setHttpClient(
|
||||
new MockHttpClient($responder, 'https://api.weixin.qq.com')
|
||||
);
|
||||
StoreModel::factory()->withPassword('abc12345')->create(['username' => 'store_login_2']);
|
||||
|
||||
$this->postJson('/mini/auth/login', [
|
||||
'username' => 'store_login_2',
|
||||
'password' => 'wrong_pwd',
|
||||
])->assertOk()
|
||||
->assertJsonPath('success', false);
|
||||
}
|
||||
|
||||
/** wx.login code → 已注册用户签发 token */
|
||||
public function test_login_issues_token_for_registered_user(): void
|
||||
/** 账号不存在 → 拒绝 */
|
||||
public function test_login_rejects_unknown_username(): void
|
||||
{
|
||||
$this->postJson('/mini/auth/login', [
|
||||
'username' => 'not_exist',
|
||||
'password' => 'abc12345',
|
||||
])->assertOk()
|
||||
->assertJsonPath('success', false);
|
||||
}
|
||||
|
||||
/** 停用门店拒绝登录 */
|
||||
public function test_disabled_store_cannot_login(): void
|
||||
{
|
||||
StoreModel::factory()->disabled()->withPassword('abc12345')->create(['username' => 'store_login_3']);
|
||||
|
||||
$this->postJson('/mini/auth/login', [
|
||||
'username' => 'store_login_3',
|
||||
'password' => 'abc12345',
|
||||
])->assertOk()
|
||||
->assertJsonPath('success', false);
|
||||
}
|
||||
|
||||
/** 登录参数缺失 → 校验失败 */
|
||||
public function test_login_validates_required_params(): void
|
||||
{
|
||||
$this->postJson('/mini/auth/login', [])
|
||||
->assertOk()
|
||||
->assertJsonPath('success', false);
|
||||
}
|
||||
|
||||
/** 登录签发的 token 可访问受保护接口,auth/info 返回当前门店 */
|
||||
public function test_login_token_accesses_protected_routes(): void
|
||||
{
|
||||
$store = StoreModel::factory()->withPassword('abc12345')->create(['username' => 'store_login_4']);
|
||||
|
||||
$login = $this->postJson('/mini/auth/login', [
|
||||
'username' => 'store_login_4',
|
||||
'password' => 'abc12345',
|
||||
]);
|
||||
$token = $login->json('data.token');
|
||||
|
||||
$this->withToken($token)->getJson('/mini/auth/info')
|
||||
->assertOk()
|
||||
->assertJsonPath('success', true)
|
||||
->assertJsonPath('data.id', $store->id);
|
||||
}
|
||||
|
||||
/** 修改密码:原密码正确 → 新密码可登录 */
|
||||
public function test_set_password_succeeds(): void
|
||||
{
|
||||
$store = StoreModel::factory()->withPassword('old_pwd_1')->create();
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$this->putJson('/mini/auth/password', [
|
||||
'oldPassword' => 'old_pwd_1',
|
||||
'newPassword' => 'new_pwd_1',
|
||||
'rePassword' => 'new_pwd_1',
|
||||
])->assertOk()
|
||||
->assertJsonPath('success', true);
|
||||
|
||||
$this->postJson('/mini/auth/login', [
|
||||
'username' => $store->username,
|
||||
'password' => 'new_pwd_1',
|
||||
])->assertOk()
|
||||
->assertJsonPath('success', true);
|
||||
}
|
||||
|
||||
/** 修改密码:原密码错误 → 拒绝 */
|
||||
public function test_set_password_rejects_wrong_old_password(): void
|
||||
{
|
||||
$store = StoreModel::factory()->withPassword('old_pwd_2')->create();
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$this->putJson('/mini/auth/password', [
|
||||
'oldPassword' => 'bad_old',
|
||||
'newPassword' => 'new_pwd_2',
|
||||
'rePassword' => 'new_pwd_2',
|
||||
])->assertOk()
|
||||
->assertJsonPath('success', false);
|
||||
}
|
||||
|
||||
/** 修改密码:两次输入不一致 → 校验失败 */
|
||||
public function test_set_password_validates_confirmation(): void
|
||||
{
|
||||
$store = StoreModel::factory()->create();
|
||||
$user = UserModel::factory()->forStore($store->id)->create(['openid' => 'openid_test_001']);
|
||||
$this->mockWechat(fn () => new MockResponse((string) json_encode([
|
||||
'openid' => 'openid_test_001',
|
||||
'session_key' => 'session_key_x',
|
||||
])));
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$response = $this->postJson('/mini/auth/login', ['code' => 'wx_code']);
|
||||
$response->assertOk()
|
||||
->assertJsonPath('success', true)
|
||||
->assertJsonStructure(['data' => ['token', 'user' => ['id']]]);
|
||||
|
||||
$this->assertNotEmpty($response->json('data.token'));
|
||||
$this->assertNotNull($user->fresh()->last_login_at);
|
||||
}
|
||||
|
||||
/** openid 未注册 → 拒绝登录 */
|
||||
public function test_login_rejects_unregistered_openid(): void
|
||||
{
|
||||
$this->mockWechat(fn () => new MockResponse((string) json_encode([
|
||||
'openid' => 'openid_unknown',
|
||||
'session_key' => 'sk',
|
||||
])));
|
||||
|
||||
$this->postJson('/mini/auth/login', ['code' => 'wx_code'])
|
||||
->assertOk()
|
||||
$this->putJson('/mini/auth/password', [
|
||||
'oldPassword' => '123456',
|
||||
'newPassword' => 'new_pwd_3',
|
||||
'rePassword' => 'different',
|
||||
])->assertOk()
|
||||
->assertJsonPath('success', false);
|
||||
$this->assertSame(0, UserModel::count());
|
||||
}
|
||||
|
||||
/** 注册:门店编码绑定门店并签发 token */
|
||||
public function test_register_binds_store_and_issues_token(): void
|
||||
{
|
||||
$store = StoreModel::factory()->create(['code' => 'ST001', 'phone' => '13800138000']);
|
||||
$this->mockWechat(function (string $method, string $url): MockResponse {
|
||||
if (str_contains($url, 'jscode2session')) {
|
||||
return new MockResponse((string) json_encode([
|
||||
'openid' => 'openid_reg_001',
|
||||
'session_key' => 'sk',
|
||||
]));
|
||||
}
|
||||
if (str_contains($url, 'cgi-bin/token')) {
|
||||
return new MockResponse((string) json_encode([
|
||||
'access_token' => 'mock_access_token',
|
||||
'expires_in' => 7200,
|
||||
]));
|
||||
}
|
||||
|
||||
return new MockResponse((string) json_encode([
|
||||
'errcode' => 0,
|
||||
'phone_info' => ['phoneNumber' => '13800138000'],
|
||||
]));
|
||||
});
|
||||
|
||||
$response = $this->postJson('/mini/auth/register', [
|
||||
'code' => 'wx_code',
|
||||
'phoneCode' => 'phone_code',
|
||||
'storeCode' => 'ST001',
|
||||
]);
|
||||
$response->assertOk()
|
||||
->assertJsonPath('success', true)
|
||||
->assertJsonStructure(['data' => ['token', 'user' => ['id', 'store_id']]]);
|
||||
$this->assertNotEmpty($response->json('data.token'));
|
||||
|
||||
$user = UserModel::where('openid', 'openid_reg_001')->first();
|
||||
$this->assertNotNull($user, '应按 openid 注册用户');
|
||||
$this->assertSame($store->id, $user->store_id);
|
||||
$this->assertSame('13800138000', $user->phone);
|
||||
}
|
||||
|
||||
/** 注册:门店编码不存在 → 拒绝 */
|
||||
public function test_register_rejects_unknown_store(): void
|
||||
{
|
||||
$this->mockWechat(fn () => new MockResponse((string) json_encode([
|
||||
'openid' => 'openid_reg_002',
|
||||
'session_key' => 'sk',
|
||||
])));
|
||||
|
||||
$this->postJson('/mini/auth/register', [
|
||||
'code' => 'wx_code',
|
||||
'phoneCode' => 'phone_code',
|
||||
'storeCode' => 'NOT_EXIST',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('success', false);
|
||||
$this->assertSame(0, UserModel::count());
|
||||
}
|
||||
|
||||
/** 注册:openid 已注册 → 拒绝重复注册 */
|
||||
public function test_register_rejects_duplicate_openid(): void
|
||||
{
|
||||
UserModel::factory()->create(['openid' => 'openid_dup']);
|
||||
$this->mockWechat(fn () => new MockResponse((string) json_encode([
|
||||
'openid' => 'openid_dup',
|
||||
'session_key' => 'sk',
|
||||
])));
|
||||
|
||||
$this->postJson('/mini/auth/register', [
|
||||
'code' => 'wx_code',
|
||||
'phoneCode' => 'phone_code',
|
||||
'storeCode' => 'ST001',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('success', false);
|
||||
$this->assertSame(1, UserModel::count());
|
||||
}
|
||||
|
||||
/** 停用账号拒绝登录 */
|
||||
public function test_disabled_user_cannot_login(): void
|
||||
{
|
||||
UserModel::factory()->disabled()->create(['openid' => 'openid_disabled']);
|
||||
$this->mockWechat(fn () => new MockResponse((string) json_encode([
|
||||
'openid' => 'openid_disabled',
|
||||
'session_key' => 'sk',
|
||||
])));
|
||||
|
||||
$this->postJson('/mini/auth/login', ['code' => 'c'])
|
||||
->assertOk()
|
||||
->assertJsonPath('success', false);
|
||||
}
|
||||
|
||||
/** 微信接口报错时登录失败(错误被转译为业务异常,不泄露原始报文结构) */
|
||||
public function test_login_fails_when_wechat_rejects_code(): void
|
||||
{
|
||||
$this->mockWechat(fn () => new MockResponse((string) json_encode([
|
||||
'errcode' => 40029,
|
||||
'errmsg' => 'invalid code',
|
||||
])));
|
||||
|
||||
$this->postJson('/mini/auth/login', ['code' => 'bad_code'])
|
||||
->assertOk()
|
||||
->assertJsonPath('success', false);
|
||||
$this->assertSame(0, UserModel::count());
|
||||
}
|
||||
|
||||
/** 跨端隔离:后台 token 访问小程序接口 → 401 */
|
||||
@@ -205,7 +152,7 @@ class MiniAuthTest extends ProcurementTestCase
|
||||
/** 跨端隔离:小程序 token 访问后台接口 → 401 */
|
||||
public function test_mini_token_cannot_access_admin_api(): void
|
||||
{
|
||||
$this->actingAsMiniUser(UserModel::factory()->create());
|
||||
$this->actingAsMiniStore(StoreModel::factory()->create());
|
||||
|
||||
$this->getJson('/customer/level')->assertStatus(401);
|
||||
}
|
||||
|
||||
@@ -8,27 +8,21 @@ use App\Models\PurchaseOrderModel;
|
||||
use App\Models\StoreModel;
|
||||
use App\Models\StoreOrderItemModel;
|
||||
use App\Models\StoreOrderModel;
|
||||
use App\Models\UserModel;
|
||||
use Modules\SystemTool\Models\SysFileModel;
|
||||
|
||||
/**
|
||||
* 小程序账单:支付进度推导(待支付/审核中/已支付)、应结算日期(回款周期)、
|
||||
* 待支付汇总、可付款筛选、门店数据隔离、详情归属校验
|
||||
* (门店即用户:账单按 store_id 归属)
|
||||
*/
|
||||
class MiniBillTest extends ProcurementTestCase
|
||||
{
|
||||
private static int $billSeq = 0;
|
||||
|
||||
/**
|
||||
* 造一个绑定正常门店的小程序用户
|
||||
*
|
||||
* @return array{0: StoreModel, 1: UserModel}
|
||||
*/
|
||||
private function makeStoreWithUser(int $cycleDays = 3): array
|
||||
/** 造一家门店(指定回款周期) */
|
||||
private function makeStore(int $cycleDays = 3): StoreModel
|
||||
{
|
||||
$store = StoreModel::factory()->paymentCycle($cycleDays)->create();
|
||||
|
||||
return [$store, UserModel::factory()->forStore($store->id)->create()];
|
||||
return StoreModel::factory()->paymentCycle($cycleDays)->create();
|
||||
}
|
||||
|
||||
/** 造一张账单(默认 2026-08-10 出账,总额 140.00,未支付) */
|
||||
@@ -56,8 +50,8 @@ class MiniBillTest extends ProcurementTestCase
|
||||
/** 支付进度推导与应结算日期(账单日期 + 门店回款周期) */
|
||||
public function test_bill_list_derives_pay_state_and_settlement_date(): void
|
||||
{
|
||||
[$store, $user] = $this->makeStoreWithUser(3);
|
||||
$this->actingAsMiniUser($user);
|
||||
$store = $this->makeStore(3);
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$unpaid = $this->makeBill($store);
|
||||
$reviewing = $this->makeBill($store, ['payment_id' => 99]);
|
||||
@@ -88,8 +82,8 @@ class MiniBillTest extends ProcurementTestCase
|
||||
/** 待支付汇总:未支付笔数/金额(含审核中),不受列表筛选影响 */
|
||||
public function test_bill_list_summary_aggregates_unpaid(): void
|
||||
{
|
||||
[$store, $user] = $this->makeStoreWithUser();
|
||||
$this->actingAsMiniUser($user);
|
||||
$store = $this->makeStore();
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$this->makeBill($store, ['total_amount' => '100.00']);
|
||||
$this->makeBill($store, ['total_amount' => '50.50', 'payment_id' => 99]);
|
||||
@@ -104,8 +98,8 @@ class MiniBillTest extends ProcurementTestCase
|
||||
/** 可付款筛选:仅未支付且未锁定到支付记录的账单(合并付款选择页) */
|
||||
public function test_bill_list_payable_filter(): void
|
||||
{
|
||||
[$store, $user] = $this->makeStoreWithUser();
|
||||
$this->actingAsMiniUser($user);
|
||||
$store = $this->makeStore();
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$payable = $this->makeBill($store);
|
||||
$this->makeBill($store, ['payment_id' => 99]);
|
||||
@@ -119,8 +113,8 @@ class MiniBillTest extends ProcurementTestCase
|
||||
/** 账单日期区间筛选 */
|
||||
public function test_bill_list_filters_by_bill_date_range(): void
|
||||
{
|
||||
[$store, $user] = $this->makeStoreWithUser();
|
||||
$this->actingAsMiniUser($user);
|
||||
$store = $this->makeStore();
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$this->makeBill($store, ['bill_date' => '2026-08-01']);
|
||||
$this->makeBill($store, ['bill_date' => '2026-08-05']);
|
||||
@@ -134,11 +128,11 @@ class MiniBillTest extends ProcurementTestCase
|
||||
/** 门店数据隔离:只能查看本店账单 */
|
||||
public function test_bill_data_isolated_between_stores(): void
|
||||
{
|
||||
[$storeA] = $this->makeStoreWithUser();
|
||||
$storeA = $this->makeStore();
|
||||
$billOfA = $this->makeBill($storeA);
|
||||
|
||||
[, $userB] = $this->makeStoreWithUser();
|
||||
$this->actingAsMiniUser($userB);
|
||||
$storeB = $this->makeStore();
|
||||
$this->actingAsMiniStore($storeB);
|
||||
|
||||
$this->getJson('/mini/bill')->assertJsonPath('data.total', 0);
|
||||
$this->getJson("/mini/bill/{$billOfA->id}")->assertJsonPath('success', false);
|
||||
@@ -147,8 +141,8 @@ class MiniBillTest extends ProcurementTestCase
|
||||
/** 详情:格式化账单 + 合并商品明细 + 关联订单 */
|
||||
public function test_bill_detail_outputs_bill_items_and_orders(): void
|
||||
{
|
||||
[$store, $user] = $this->makeStoreWithUser(0);
|
||||
$this->actingAsMiniUser($user);
|
||||
$store = $this->makeStore(0);
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$file = SysFileModel::create([
|
||||
'group_id' => 1,
|
||||
|
||||
@@ -4,42 +4,36 @@ namespace Tests\Feature;
|
||||
|
||||
use App\Models\NoticeModel;
|
||||
use App\Models\StoreModel;
|
||||
use App\Models\UserModel;
|
||||
|
||||
/**
|
||||
* 小程序消息通知:本人通知 + 全员广播;广播已读复制本人副本(data.broadcast_from 记来源),
|
||||
* 小程序消息通知:本店通知 + 全员广播;广播已读复制本店副本(data.broadcast_from 记来源),
|
||||
* 列表排除已读广播。回归:列表曾报 Undefined property: stdClass::$data->broadcast_from
|
||||
* (pluck JSON 路径被 Query Builder 当作结果列名读取 stdClass 属性)
|
||||
* (门店即用户:通知按 store_id 归属,store_id=0 为全员广播)
|
||||
*/
|
||||
class MiniNoticeTest extends ProcurementTestCase
|
||||
{
|
||||
/**
|
||||
* 造一个绑定门店的小程序用户
|
||||
*
|
||||
* @return array{0: StoreModel, 1: UserModel}
|
||||
*/
|
||||
private function makeStoreWithUser(): array
|
||||
/** 造一家门店 */
|
||||
private function makeStore(): StoreModel
|
||||
{
|
||||
$store = StoreModel::factory()->create();
|
||||
|
||||
return [$store, UserModel::factory()->forStore($store->id)->create()];
|
||||
return StoreModel::factory()->create();
|
||||
}
|
||||
|
||||
/** 通知列表:本人通知 + 未读广播,unread_count 正确 */
|
||||
/** 通知列表:本店通知 + 未读广播,unread_count 正确 */
|
||||
public function test_notice_list_includes_personal_and_broadcast(): void
|
||||
{
|
||||
[, $user] = $this->makeStoreWithUser();
|
||||
$this->actingAsMiniUser($user);
|
||||
$store = $this->makeStore();
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
NoticeModel::create([
|
||||
'user_id' => NoticeModel::BROADCAST_USER_ID,
|
||||
'store_id' => NoticeModel::BROADCAST_STORE_ID,
|
||||
'type' => NoticeModel::TYPE_SYSTEM,
|
||||
'title' => '系统公告',
|
||||
'content' => '全员可见',
|
||||
'is_read' => NoticeModel::UNREAD,
|
||||
]);
|
||||
NoticeModel::create([
|
||||
'user_id' => $user->id,
|
||||
'store_id' => $store->id,
|
||||
'type' => NoticeModel::TYPE_ORDER,
|
||||
'title' => '订单状态更新',
|
||||
'content' => '您的订单已接单',
|
||||
@@ -52,14 +46,14 @@ class MiniNoticeTest extends ProcurementTestCase
|
||||
$this->assertSame(2, $data['unread_count']);
|
||||
}
|
||||
|
||||
/** 广播标记已读:复制本人已读副本,列表排除原广播(回归:列表不再报错) */
|
||||
/** 广播标记已读:复制本店已读副本,列表排除原广播(回归:列表不再报错) */
|
||||
public function test_broadcast_read_creates_personal_copy(): void
|
||||
{
|
||||
[, $user] = $this->makeStoreWithUser();
|
||||
$this->actingAsMiniUser($user);
|
||||
$store = $this->makeStore();
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$broadcast = NoticeModel::create([
|
||||
'user_id' => NoticeModel::BROADCAST_USER_ID,
|
||||
'store_id' => NoticeModel::BROADCAST_STORE_ID,
|
||||
'type' => NoticeModel::TYPE_SYSTEM,
|
||||
'title' => '系统公告',
|
||||
'content' => '全员可见',
|
||||
@@ -69,8 +63,8 @@ class MiniNoticeTest extends ProcurementTestCase
|
||||
|
||||
$this->putJson("/mini/notice/{$broadcast->id}/read")->assertJsonPath('success', true);
|
||||
|
||||
// 已读副本:本人记录、已读、broadcast_from 记来源且保留原 data
|
||||
$copy = NoticeModel::where('user_id', $user->id)->first();
|
||||
// 已读副本:本店记录、已读、broadcast_from 记来源且保留原 data
|
||||
$copy = NoticeModel::where('store_id', $store->id)->first();
|
||||
$this->assertNotNull($copy);
|
||||
$this->assertSame(NoticeModel::READ, $copy->is_read);
|
||||
$this->assertSame($broadcast->id, $copy->data['broadcast_from']);
|
||||
@@ -84,17 +78,17 @@ class MiniNoticeTest extends ProcurementTestCase
|
||||
|
||||
// 重复标记不重复复制
|
||||
$this->putJson("/mini/notice/{$broadcast->id}/read")->assertJsonPath('success', true);
|
||||
$this->assertSame(1, NoticeModel::where('user_id', $user->id)->count());
|
||||
$this->assertSame(1, NoticeModel::where('store_id', $store->id)->count());
|
||||
}
|
||||
|
||||
/** 个人通知标记已读:直接更新原记录,不产生副本 */
|
||||
/** 定向通知标记已读:直接更新原记录,不产生副本 */
|
||||
public function test_personal_notice_read_marks_directly(): void
|
||||
{
|
||||
[, $user] = $this->makeStoreWithUser();
|
||||
$this->actingAsMiniUser($user);
|
||||
$store = $this->makeStore();
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$notice = NoticeModel::create([
|
||||
'user_id' => $user->id,
|
||||
'store_id' => $store->id,
|
||||
'type' => NoticeModel::TYPE_ORDER,
|
||||
'title' => '订单状态更新',
|
||||
'content' => '您的订单已接单',
|
||||
@@ -102,26 +96,26 @@ class MiniNoticeTest extends ProcurementTestCase
|
||||
]);
|
||||
|
||||
$this->putJson("/mini/notice/{$notice->id}/read")->assertJsonPath('success', true);
|
||||
$this->assertSame(NoticeModel::READ, $notice->fresh()->is_read);
|
||||
$this->assertSame(1, NoticeModel::count(), '个人通知直接更新,不产生副本');
|
||||
$this->assertSame(NoticeModel::READ, $notice->refresh()->is_read);
|
||||
$this->assertSame(1, NoticeModel::count(), '定向通知直接更新,不产生副本');
|
||||
}
|
||||
|
||||
/** 他人通知不可标记(仅本人通知与广播可读) */
|
||||
public function test_other_users_notice_not_found(): void
|
||||
/** 他店通知不可标记(仅本店通知与广播可读) */
|
||||
public function test_other_stores_notice_not_found(): void
|
||||
{
|
||||
[, $user] = $this->makeStoreWithUser();
|
||||
$this->actingAsMiniUser($user);
|
||||
$store = $this->makeStore();
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
[, $other] = $this->makeStoreWithUser();
|
||||
$other = $this->makeStore();
|
||||
$notice = NoticeModel::create([
|
||||
'user_id' => $other->id,
|
||||
'store_id' => $other->id,
|
||||
'type' => NoticeModel::TYPE_ORDER,
|
||||
'title' => '订单状态更新',
|
||||
'content' => '他人通知',
|
||||
'content' => '他店通知',
|
||||
'is_read' => NoticeModel::UNREAD,
|
||||
]);
|
||||
|
||||
$this->putJson("/mini/notice/{$notice->id}/read")->assertJsonPath('success', false);
|
||||
$this->assertSame(NoticeModel::UNREAD, $notice->fresh()->is_read);
|
||||
$this->assertSame(NoticeModel::UNREAD, $notice->refresh()->is_read);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,18 +5,18 @@ namespace Tests\Feature;
|
||||
use App\Models\CustomerLevelModel;
|
||||
use App\Models\ProductModel;
|
||||
use App\Models\StoreModel;
|
||||
use App\Models\UserModel;
|
||||
|
||||
/**
|
||||
* 小程序商品:商品详情接口(免登录浏览,登录门店按等级上浮比例显示换算价);
|
||||
* 商品列表回归:登录门店场景曾数组误用对象访问 + 价格缺失导致 500
|
||||
* (门店即用户:登录主体为 StoreModel)
|
||||
*/
|
||||
class MiniProductTest extends ProcurementTestCase
|
||||
{
|
||||
/**
|
||||
* 造门店 + 上架商品(成本价即售价,等级上浮 0%)
|
||||
*
|
||||
* @return array{0: StoreModel, 1: ProductModel, 2: UserModel, 3: CustomerLevelModel}
|
||||
* @return array{0: StoreModel, 1: ProductModel, 2: CustomerLevelModel}
|
||||
*/
|
||||
private function makeStoreWithProduct(string $price = '5.00'): array
|
||||
{
|
||||
@@ -27,7 +27,7 @@ class MiniProductTest extends ProcurementTestCase
|
||||
'cost_price' => $price,
|
||||
]);
|
||||
|
||||
return [$store, $product, UserModel::factory()->forStore($store->id)->create(), $level];
|
||||
return [$store, $product, $level];
|
||||
}
|
||||
|
||||
/** 商品详情:未登录浏览 price=null,基础字段完整 */
|
||||
@@ -45,10 +45,10 @@ class MiniProductTest extends ProcurementTestCase
|
||||
}
|
||||
|
||||
/** 商品详情:登录门店按等级显示固定价 */
|
||||
public function test_product_detail_store_user_sees_level_price(): void
|
||||
public function test_product_detail_store_sees_level_price(): void
|
||||
{
|
||||
[, $product, $user] = $this->makeStoreWithProduct('6.50');
|
||||
$this->actingAsMiniUser($user);
|
||||
[$store, $product] = $this->makeStoreWithProduct('6.50');
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$data = $this->getJson("/mini/product/{$product->id}")->assertOk()->json('data');
|
||||
|
||||
@@ -58,10 +58,10 @@ class MiniProductTest extends ProcurementTestCase
|
||||
/** 商品详情:按门店等级上浮比例换算(成本价上浮 30%) */
|
||||
public function test_product_detail_percent_price_calculated_from_cost(): void
|
||||
{
|
||||
[, $product, $user, $level] = $this->makeStoreWithProduct();
|
||||
[$store, $product, $level] = $this->makeStoreWithProduct();
|
||||
$level->update(['percent' => 30]);
|
||||
$product->update(['cost_price' => '20.00']);
|
||||
$this->actingAsMiniUser($user);
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$data = $this->getJson("/mini/product/{$product->id}")->assertOk()->json('data');
|
||||
|
||||
@@ -79,10 +79,10 @@ class MiniProductTest extends ProcurementTestCase
|
||||
}
|
||||
|
||||
/** 商品列表:登录门店显示等级价(回归:曾数组误用对象访问导致 500) */
|
||||
public function test_product_list_store_user_sees_level_price(): void
|
||||
public function test_product_list_store_sees_level_price(): void
|
||||
{
|
||||
[, $product, $user] = $this->makeStoreWithProduct('7.00');
|
||||
$this->actingAsMiniUser($user);
|
||||
[$store, $product] = $this->makeStoreWithProduct('7.00');
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$rows = $this->getJson('/mini/product/list')->assertOk()->json('data.data');
|
||||
$row = collect($rows)->firstWhere('id', $product->id);
|
||||
|
||||
@@ -3,23 +3,23 @@
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\StoreModel;
|
||||
use App\Models\UserModel;
|
||||
|
||||
/**
|
||||
* 小程序门店设置:详情回显、联系人/电话/地址维护、
|
||||
* 名称/编码/回款周期等字段不可通过小程序端修改
|
||||
* (门店即用户:登录主体就是门店,操作对象即当前登录门店)
|
||||
*/
|
||||
class MiniStoreTest extends ProcurementTestCase
|
||||
{
|
||||
/** 门店详情:返回当前绑定门店信息(含只读回款周期) */
|
||||
public function test_info_returns_bound_store(): void
|
||||
/** 门店详情:返回当前门店信息(含只读回款周期) */
|
||||
public function test_info_returns_current_store(): void
|
||||
{
|
||||
$store = StoreModel::factory()->paymentCycle(7)->create([
|
||||
'contact' => '张三',
|
||||
'phone' => '13800138000',
|
||||
'address' => '幸福路 1 号',
|
||||
]);
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$this->getJson('/mini/store/info')
|
||||
->assertOk()
|
||||
@@ -35,7 +35,7 @@ class MiniStoreTest extends ProcurementTestCase
|
||||
public function test_update_info(): void
|
||||
{
|
||||
$store = StoreModel::factory()->create();
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$this->putJson('/mini/store/info', [
|
||||
'contact' => '李四',
|
||||
@@ -48,10 +48,10 @@ class MiniStoreTest extends ProcurementTestCase
|
||||
->assertJsonPath('data.phone', '13900139000')
|
||||
->assertJsonPath('data.address', '建设路 88 号');
|
||||
|
||||
$fresh = $store->fresh();
|
||||
$this->assertSame('李四', $fresh->contact);
|
||||
$this->assertSame('13900139000', $fresh->phone);
|
||||
$this->assertSame('建设路 88 号', $fresh->address);
|
||||
$store->refresh();
|
||||
$this->assertSame('李四', $store->contact);
|
||||
$this->assertSame('13900139000', $store->phone);
|
||||
$this->assertSame('建设路 88 号', $store->address);
|
||||
}
|
||||
|
||||
/** 白名单更新:名称 / 编码 / 回款周期等字段提交后被忽略 */
|
||||
@@ -61,7 +61,7 @@ class MiniStoreTest extends ProcurementTestCase
|
||||
'name' => '原门店',
|
||||
'code' => 'ST100',
|
||||
]);
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$this->putJson('/mini/store/info', [
|
||||
'contact' => '王五',
|
||||
@@ -72,22 +72,23 @@ class MiniStoreTest extends ProcurementTestCase
|
||||
->assertOk()
|
||||
->assertJsonPath('success', true);
|
||||
|
||||
$fresh = $store->fresh();
|
||||
$this->assertSame('王五', $fresh->contact);
|
||||
$this->assertSame('原门店', $fresh->name);
|
||||
$this->assertSame('ST100', $fresh->code);
|
||||
$this->assertSame(5, $fresh->payment_cycle_days);
|
||||
$store->refresh();
|
||||
$this->assertSame('王五', $store->contact);
|
||||
$this->assertSame('原门店', $store->name);
|
||||
$this->assertSame('ST100', $store->code);
|
||||
$this->assertSame(5, $store->payment_cycle_days);
|
||||
}
|
||||
|
||||
/** 未绑定门店 → 拒绝访问 */
|
||||
public function test_requires_bound_store(): void
|
||||
/** 停用门店 → 拒绝访问 */
|
||||
public function test_disabled_store_rejected(): void
|
||||
{
|
||||
$this->actingAsMiniUser(UserModel::factory()->create());
|
||||
$store = StoreModel::factory()->disabled()->create();
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$this->getJson('/mini/store/info')
|
||||
->assertOk()
|
||||
->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '尚未绑定门店,请联系客服处理');
|
||||
->assertJsonPath('msg', '账号不存在或已被停用');
|
||||
|
||||
$this->putJson('/mini/store/info', ['contact' => '路人'])
|
||||
->assertOk()
|
||||
@@ -98,13 +99,13 @@ class MiniStoreTest extends ProcurementTestCase
|
||||
public function test_update_info_validates_length(): void
|
||||
{
|
||||
$store = StoreModel::factory()->create();
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$this->putJson('/mini/store/info', ['contact' => str_repeat('长', 51)])
|
||||
->assertOk()
|
||||
->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '联系人最长 50 个字符');
|
||||
|
||||
$this->assertSame($store->contact, $store->fresh()->contact, '校验失败不应落库');
|
||||
$this->assertSame($store->contact, $store->refresh()->contact, '校验失败不应落库');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\UserModel;
|
||||
use App\Models\StoreModel;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Modules\SystemUser\Models\SysUserModel;
|
||||
use Tests\TestCase;
|
||||
@@ -13,6 +13,7 @@ use Tests\TestCase;
|
||||
* - SQLite :memory: + RefreshDatabase,每个测试方法独立迁移
|
||||
* - 双端认证辅助:createToken 签发 Sanctum token,withToken 注入 Authorization 头,
|
||||
* 走真实链路(auth:sanctum → AuthGuardMiddleware 双端隔离 → abilities 校验)
|
||||
* - 门店即用户:小程序端登录主体为 StoreModel(store 表)
|
||||
*/
|
||||
abstract class ProcurementTestCase extends TestCase
|
||||
{
|
||||
@@ -52,16 +53,16 @@ abstract class ProcurementTestCase extends TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* 以小程序用户身份发起后续请求(token abilities 默认 ['mini'])
|
||||
* 以门店身份(小程序端)发起后续请求(token abilities 默认 ['mini'])
|
||||
*
|
||||
* @param array<int, string> $abilities
|
||||
*/
|
||||
protected function actingAsMiniUser(UserModel $user, array $abilities = ['mini']): UserModel
|
||||
protected function actingAsMiniStore(StoreModel $store, array $abilities = ['mini']): StoreModel
|
||||
{
|
||||
$this->app['auth']->forgetGuards();
|
||||
|
||||
$this->withToken($user->createToken('mini', $abilities)->plainTextToken);
|
||||
$this->withToken($store->createToken('mini', $abilities)->plainTextToken);
|
||||
|
||||
return $user;
|
||||
return $store;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\ProductCategoryModel;
|
||||
use App\Models\UserModel;
|
||||
use App\Models\StoreModel;
|
||||
|
||||
/**
|
||||
* 商品分类两级限制 + 商品只能挂末级分类:
|
||||
@@ -169,7 +169,7 @@ class ProductCategoryTest extends ProcurementTestCase
|
||||
'status' => ProductCategoryModel::STATUS_DISABLED,
|
||||
]);
|
||||
|
||||
$this->actingAsMiniUser(UserModel::factory()->create());
|
||||
$this->actingAsMiniStore(StoreModel::factory()->create());
|
||||
|
||||
$response = $this->getJson('/mini/product/categories');
|
||||
$response->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
@@ -7,7 +7,6 @@ use App\Models\NoticeModel;
|
||||
use App\Models\ProductCategoryModel;
|
||||
use App\Models\ProductModel;
|
||||
use App\Models\StoreModel;
|
||||
use App\Models\UserModel;
|
||||
|
||||
/**
|
||||
* 商品成本价 + 等级上浮计价:
|
||||
@@ -26,7 +25,7 @@ class ProductCostPriceTest extends ProcurementTestCase
|
||||
]);
|
||||
}
|
||||
|
||||
/** 造门店(等级上浮 percent)+ 上架商品(成本价 cost) + 该店用户 */
|
||||
/** 造门店(等级上浮 percent)+ 上架商品(成本价 cost) */
|
||||
private function makeStoreWithPercentProduct(float $cost = 10, float $percent = 30): array
|
||||
{
|
||||
$level = CustomerLevelModel::factory()->create(['percent' => $percent]);
|
||||
@@ -36,7 +35,7 @@ class ProductCostPriceTest extends ProcurementTestCase
|
||||
'cost_price' => $cost,
|
||||
]);
|
||||
|
||||
return [$store, $product, UserModel::factory()->forStore($store->id)->create()];
|
||||
return [$store, $product];
|
||||
}
|
||||
|
||||
/** 售价换算:成本价 × (100 + percent) / 100 */
|
||||
@@ -78,8 +77,8 @@ class ProductCostPriceTest extends ProcurementTestCase
|
||||
/** 小程序列表:返回换算后实际价,且成本价不泄漏 */
|
||||
public function test_mini_list_returns_actual_price_without_cost_price(): void
|
||||
{
|
||||
[$store, $product, $user] = $this->makeStoreWithPercentProduct();
|
||||
$this->actingAsMiniUser($user);
|
||||
[$store, $product] = $this->makeStoreWithPercentProduct();
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$response = $this->getJson('/mini/product/list');
|
||||
$response->assertOk()->assertJsonPath('success', true);
|
||||
@@ -116,7 +115,6 @@ class ProductCostPriceTest extends ProcurementTestCase
|
||||
|
||||
$level = CustomerLevelModel::factory()->create(['percent' => 30]);
|
||||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$user = UserModel::factory()->forStore($store->id)->create();
|
||||
$product = ProductModel::factory()->create(['cost_price' => 10]);
|
||||
|
||||
$this->putJson('/product/goods/batchPrice', [
|
||||
@@ -125,15 +123,15 @@ class ProductCostPriceTest extends ProcurementTestCase
|
||||
],
|
||||
])->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
$this->assertSame('20.00', (string) $product->fresh()->cost_price, '成本价应已更新');
|
||||
$this->assertSame('20.00', (string) $product->refresh()->cost_price, '成本价应已更新');
|
||||
$this->assertSame(
|
||||
1,
|
||||
NoticeModel::where('user_id', $user->id)->where('type', NoticeModel::TYPE_PRICE)->count(),
|
||||
'门店用户应收到价格变更通知'
|
||||
NoticeModel::where('store_id', $store->id)->where('type', NoticeModel::TYPE_PRICE)->count(),
|
||||
'门店应收到价格变更通知'
|
||||
);
|
||||
|
||||
// 等级售价联动:20 元上浮 30% = 26.00
|
||||
$this->actingAsMiniUser($user);
|
||||
$this->actingAsMiniStore($store);
|
||||
$row = collect($this->getJson('/mini/product/list')->json('data.data'))->firstWhere('id', $product->id);
|
||||
$this->assertSame('26.00', (string) $row['price'], '成本变更后小程序端售价应按上浮比例联动');
|
||||
}
|
||||
@@ -178,8 +176,8 @@ class ProductCostPriceTest extends ProcurementTestCase
|
||||
/** 小程序下单:按等级上浮换算价重算金额并快照 */
|
||||
public function test_mini_order_uses_level_price(): void
|
||||
{
|
||||
[$store, $product, $user] = $this->makeStoreWithPercentProduct(10, 30);
|
||||
$this->actingAsMiniUser($user);
|
||||
[$store, $product] = $this->makeStoreWithPercentProduct(10, 30);
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$this->postJson('/mini/order', [
|
||||
'items' => [['product_id' => $product->id, 'quantity' => 3]],
|
||||
@@ -195,8 +193,8 @@ class ProductCostPriceTest extends ProcurementTestCase
|
||||
/** 小程序购物车:列表金额按等级上浮换算价计算 */
|
||||
public function test_mini_cart_uses_level_price(): void
|
||||
{
|
||||
[$store, $product, $user] = $this->makeStoreWithPercentProduct(10, 30);
|
||||
$this->actingAsMiniUser($user);
|
||||
[$store, $product] = $this->makeStoreWithPercentProduct(10, 30);
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 2.5])
|
||||
->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
@@ -6,11 +6,10 @@ use App\Models\CustomerLevelModel;
|
||||
use App\Models\NoticeModel;
|
||||
use App\Models\ProductModel;
|
||||
use App\Models\StoreModel;
|
||||
use App\Models\UserModel;
|
||||
|
||||
/**
|
||||
* A2 等级价格体系(等级上浮比例):列表按等级换算价、批量调价(成本价)事务与调价通知、
|
||||
* 等级上浮比例变更通知
|
||||
* 等级上浮比例变更通知(门店即用户:通知按门店下发,一店一条)
|
||||
*/
|
||||
class ProductPriceTest extends ProcurementTestCase
|
||||
{
|
||||
@@ -23,13 +22,13 @@ class ProductPriceTest extends ProcurementTestCase
|
||||
$storeB = StoreModel::factory()->create(['level_id' => $levelB->id]);
|
||||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => 10.00]);
|
||||
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($storeA->id)->create());
|
||||
$this->actingAsMiniStore($storeA);
|
||||
$rowA = collect($this->getJson('/mini/product/list')->assertOk()->json('data.data'))
|
||||
->firstWhere('id', $product->id);
|
||||
$this->assertNotNull($rowA, '商品应出现在列表中');
|
||||
$this->assertSame(13.00, (float) $rowA['price'], 'A 等级:10 元上浮 30% = 13.00');
|
||||
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($storeB->id)->create());
|
||||
$this->actingAsMiniStore($storeB);
|
||||
$rowB = collect($this->getJson('/mini/product/list')->assertOk()->json('data.data'))
|
||||
->firstWhere('id', $product->id);
|
||||
$this->assertSame(11.00, (float) $rowB['price'], 'B 等级:10 元上浮 10% = 11.00');
|
||||
@@ -40,7 +39,7 @@ class ProductPriceTest extends ProcurementTestCase
|
||||
{
|
||||
$store = StoreModel::factory()->create(['level_id' => 0]);
|
||||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]);
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$response = $this->getJson('/mini/product/list');
|
||||
$response->assertOk()->assertJsonPath('success', true);
|
||||
@@ -50,37 +49,13 @@ class ProductPriceTest extends ProcurementTestCase
|
||||
$this->assertNull($row['price'], '未设等级时价格应为 null');
|
||||
}
|
||||
|
||||
/** 未绑定门店:可浏览商品与分类,但价格不可见;加购仍被拦截(购买前置校验不变) */
|
||||
public function test_unbound_user_can_browse_products_without_price_but_cannot_cart(): void
|
||||
{
|
||||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]);
|
||||
$this->actingAsMiniUser(UserModel::factory()->create()); // type=0 待绑定
|
||||
|
||||
$list = $this->getJson('/mini/product/list');
|
||||
$list->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
$row = collect($list->json('data.data'))->firstWhere('id', $product->id);
|
||||
$this->assertNotNull($row, '未绑定门店应能看到商品');
|
||||
$this->assertNull($row['price'], '未绑定门店不得看到价格');
|
||||
|
||||
$this->getJson('/mini/product/categories')
|
||||
->assertOk()
|
||||
->assertJsonPath('success', true);
|
||||
|
||||
$this->postJson('/mini/cart', ['product_id' => $product->id, 'quantity' => 1])
|
||||
->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '尚未绑定门店,请联系客服处理');
|
||||
}
|
||||
|
||||
/** 批量调价(成本价):事务写入 + 通知全部正常门店用户(成本变更影响所有等级售价) */
|
||||
/** 批量调价(成本价):事务写入 + 通知全部正常门店(成本变更影响所有等级售价,一店一条) */
|
||||
public function test_batch_price_updates_cost_and_notifies_all_stores(): void
|
||||
{
|
||||
$this->actingAsSysUser();
|
||||
|
||||
$storeA = StoreModel::factory()->create(['level_id' => CustomerLevelModel::factory()->create()->id]);
|
||||
$storeB = StoreModel::factory()->create(['level_id' => CustomerLevelModel::factory()->create()->id]);
|
||||
$userA = UserModel::factory()->forStore($storeA->id)->create();
|
||||
$userB = UserModel::factory()->forStore($storeB->id)->create();
|
||||
|
||||
$product = ProductModel::factory()->create(['cost_price' => 10]);
|
||||
|
||||
@@ -90,17 +65,17 @@ class ProductPriceTest extends ProcurementTestCase
|
||||
],
|
||||
])->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
$this->assertSame('12.00', (string) $product->fresh()->cost_price, '成本价应已更新');
|
||||
$this->assertSame('12.00', (string) $product->refresh()->cost_price, '成本价应已更新');
|
||||
|
||||
$this->assertSame(
|
||||
1,
|
||||
NoticeModel::where('user_id', $userA->id)->where('type', NoticeModel::TYPE_PRICE)->count(),
|
||||
'成本价变更影响所有等级售价,门店用户应收到价格变更通知'
|
||||
NoticeModel::where('store_id', $storeA->id)->where('type', NoticeModel::TYPE_PRICE)->count(),
|
||||
'成本价变更影响所有等级售价,门店应收到价格变更通知'
|
||||
);
|
||||
$this->assertSame(
|
||||
1,
|
||||
NoticeModel::where('user_id', $userB->id)->where('type', NoticeModel::TYPE_PRICE)->count(),
|
||||
'成本价变更影响所有等级售价,门店用户应收到价格变更通知'
|
||||
NoticeModel::where('store_id', $storeB->id)->where('type', NoticeModel::TYPE_PRICE)->count(),
|
||||
'成本价变更影响所有等级售价,门店应收到价格变更通知'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,35 +100,31 @@ class ProductPriceTest extends ProcurementTestCase
|
||||
$this->assertSame(12.0, (float) $row['price_' . $level->id], '矩阵等级格应为换算价:10 元上浮 20% = 12.00');
|
||||
}
|
||||
|
||||
/** 等级上浮比例变更:通知该等级下正常门店用户(不影响其他等级门店) */
|
||||
/** 等级上浮比例变更:通知该等级下正常门店(不影响其他等级门店) */
|
||||
public function test_level_percent_update_notifies_level_stores(): void
|
||||
{
|
||||
$this->actingAsSysUser();
|
||||
|
||||
$level = CustomerLevelModel::factory()->create(['percent' => 10]);
|
||||
$otherLevel = CustomerLevelModel::factory()->create(['percent' => 20]);
|
||||
$user = UserModel::factory()->forStore(
|
||||
StoreModel::factory()->create(['level_id' => $level->id])->id
|
||||
)->create();
|
||||
$otherUser = UserModel::factory()->forStore(
|
||||
StoreModel::factory()->create(['level_id' => $otherLevel->id])->id
|
||||
)->create();
|
||||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$otherStore = StoreModel::factory()->create(['level_id' => $otherLevel->id]);
|
||||
|
||||
$this->putJson('/customer/level/' . $level->id, [
|
||||
'name' => $level->name,
|
||||
'percent' => 30,
|
||||
])->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
$this->assertSame('30.00', (string) $level->fresh()->percent, '上浮比例应已更新');
|
||||
$this->assertSame('30.00', (string) $level->refresh()->percent, '上浮比例应已更新');
|
||||
$this->assertSame(
|
||||
1,
|
||||
NoticeModel::where('user_id', $user->id)->where('type', NoticeModel::TYPE_PRICE)->count(),
|
||||
'该等级门店用户应收到价格变更通知'
|
||||
NoticeModel::where('store_id', $store->id)->where('type', NoticeModel::TYPE_PRICE)->count(),
|
||||
'该等级门店应收到价格变更通知'
|
||||
);
|
||||
$this->assertSame(
|
||||
0,
|
||||
NoticeModel::where('user_id', $otherUser->id)->count(),
|
||||
'其他等级门店用户不应收到通知'
|
||||
NoticeModel::where('store_id', $otherStore->id)->count(),
|
||||
'其他等级门店不应收到通知'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -163,16 +134,14 @@ class ProductPriceTest extends ProcurementTestCase
|
||||
$this->actingAsSysUser();
|
||||
|
||||
$level = CustomerLevelModel::factory()->create(['percent' => 10]);
|
||||
$user = UserModel::factory()->forStore(
|
||||
StoreModel::factory()->create(['level_id' => $level->id])->id
|
||||
)->create();
|
||||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
|
||||
$this->putJson('/customer/level/' . $level->id, [
|
||||
'name' => $level->name . '(改)',
|
||||
'percent' => 10,
|
||||
])->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
$this->assertSame(0, NoticeModel::where('user_id', $user->id)->count(), '上浮比例未变不应发通知');
|
||||
$this->assertSame(0, NoticeModel::where('store_id', $store->id)->count(), '上浮比例未变不应发通知');
|
||||
}
|
||||
|
||||
/** 等级表单:上浮比例校验(负数/超限拒绝) */
|
||||
|
||||
@@ -9,7 +9,6 @@ use App\Models\StoreModel;
|
||||
use App\Models\StoreOrderItemModel;
|
||||
use App\Models\StoreOrderModel;
|
||||
use App\Models\SupplierModel;
|
||||
use App\Models\UserModel;
|
||||
|
||||
/**
|
||||
* C4 采购单数据修改:详情矩阵(商品行 × 门店列)、门店单元格下钻编辑、行级修改同步商品档案、
|
||||
@@ -37,7 +36,7 @@ class PurchaseEditTest extends ProcurementTestCase
|
||||
]);
|
||||
|
||||
foreach ([[$storeA, 2], [$storeB, 3]] as [$store, $qty]) {
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||
$this->actingAsMiniStore($store);
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
|
||||
->assertJsonPath('success', true);
|
||||
}
|
||||
@@ -298,7 +297,7 @@ class PurchaseEditTest extends ProcurementTestCase
|
||||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => 10]);
|
||||
|
||||
foreach ([[$storeA, 2], [$storeB, 3]] as [$store, $qty]) {
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||
$this->actingAsMiniStore($store);
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
|
||||
->assertJsonPath('success', true);
|
||||
}
|
||||
@@ -411,8 +410,7 @@ class PurchaseEditTest extends ProcurementTestCase
|
||||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => 10]);
|
||||
|
||||
// 同一门店两笔订单各订 2 件(同商品)→ 同一采购单
|
||||
$user = UserModel::factory()->forStore($store->id)->create();
|
||||
$this->actingAsMiniUser($user);
|
||||
$this->actingAsMiniStore($store);
|
||||
foreach ([2, 2] as $qty) {
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
|
||||
->assertJsonPath('success', true);
|
||||
@@ -515,7 +513,7 @@ class PurchaseEditTest extends ProcurementTestCase
|
||||
'unit' => '斤',
|
||||
]);
|
||||
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||
$this->actingAsMiniStore($store);
|
||||
foreach ([2, 3] as $qty) {
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
|
||||
->assertJsonPath('success', true);
|
||||
|
||||
@@ -8,7 +8,6 @@ use App\Models\PurchaseOrderModel;
|
||||
use App\Models\StoreModel;
|
||||
use App\Models\StoreOrderItemModel;
|
||||
use App\Models\StoreOrderModel;
|
||||
use App\Models\UserModel;
|
||||
|
||||
/**
|
||||
* C1 订单汇总生成采购单:订单/明细归集回写、头部汇总、无订单/重复生成防护
|
||||
@@ -28,7 +27,7 @@ class PurchaseGenerateTest extends ProcurementTestCase
|
||||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '6.00']);
|
||||
|
||||
foreach ([[$storeA, 3], [$storeA, 3], [$storeB, 4]] as [$store, $qty]) {
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||
$this->actingAsMiniStore($store);
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
|
||||
->assertJsonPath('success', true);
|
||||
}
|
||||
@@ -89,7 +88,7 @@ class PurchaseGenerateTest extends ProcurementTestCase
|
||||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']);
|
||||
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||
$this->actingAsMiniStore($store);
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||||
->assertJsonPath('success', true);
|
||||
|
||||
|
||||
@@ -9,19 +9,19 @@ use App\Models\StoreModel;
|
||||
use App\Models\StoreOrderItemModel;
|
||||
use App\Models\StoreOrderModel;
|
||||
use App\Models\SupplierModel;
|
||||
use App\Models\UserModel;
|
||||
|
||||
/**
|
||||
* 门店订单明细(商品快照):
|
||||
* 下单快照完整商品档案、成本价防泄漏、采购单内门店单品增删改重算汇总、按商品名称搜索订单
|
||||
* (门店即用户:小程序端登录主体为 StoreModel)
|
||||
*/
|
||||
class StoreOrderItemTest extends ProcurementTestCase
|
||||
{
|
||||
/**
|
||||
* 造门店 + 上架商品(指定成本价)+ 门店用户;
|
||||
* 造门店 + 上架商品(指定成本价);
|
||||
* 等级上浮比例按 price/costPrice 反算(售价 = 成本价 × (100 + percent) / 100)
|
||||
*
|
||||
* @return array{0: StoreModel, 1: ProductModel, 2: UserModel}
|
||||
* @return array{0: StoreModel, 1: ProductModel}
|
||||
*/
|
||||
private function makeStoreWithProduct(string $price = '5.00', string $costPrice = '4.00'): array
|
||||
{
|
||||
@@ -33,17 +33,17 @@ class StoreOrderItemTest extends ProcurementTestCase
|
||||
'cost_price' => $costPrice,
|
||||
]);
|
||||
|
||||
return [$store, $product, UserModel::factory()->forStore($store->id)->create()];
|
||||
return [$store, $product];
|
||||
}
|
||||
|
||||
/** 以小程序身份下一单并返回订单(可指定初始状态) */
|
||||
private function placeOrder(ProductModel $product, UserModel $user, int $quantity = 2, int $status = StoreOrderModel::STATUS_PENDING): StoreOrderModel
|
||||
private function placeOrder(ProductModel $product, StoreModel $store, int $quantity = 2, int $status = StoreOrderModel::STATUS_PENDING): StoreOrderModel
|
||||
{
|
||||
$this->actingAsMiniUser($user);
|
||||
$this->actingAsMiniStore($store);
|
||||
$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();
|
||||
$order = StoreOrderModel::where('store_id', $store->id)->latest('id')->first();
|
||||
$this->assertNotNull($order);
|
||||
if ($status !== StoreOrderModel::STATUS_PENDING) {
|
||||
$order->update(['status' => $status]);
|
||||
@@ -80,9 +80,8 @@ class StoreOrderItemTest extends ProcurementTestCase
|
||||
'shelf_life' => 30,
|
||||
'cost_price' => '4.00',
|
||||
]);
|
||||
$user = UserModel::factory()->forStore($store->id)->create();
|
||||
|
||||
$this->actingAsMiniUser($user);
|
||||
$this->actingAsMiniStore($store);
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 3]]])
|
||||
->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
@@ -100,10 +99,10 @@ class StoreOrderItemTest extends ProcurementTestCase
|
||||
/** 小程序订单详情不泄漏成本价 */
|
||||
public function test_mini_detail_hides_cost_price(): void
|
||||
{
|
||||
[, $product, $user] = $this->makeStoreWithProduct('5.00', '4.00');
|
||||
$order = $this->placeOrder($product, $user);
|
||||
[$store, $product] = $this->makeStoreWithProduct('5.00', '4.00');
|
||||
$order = $this->placeOrder($product, $store);
|
||||
|
||||
$this->actingAsMiniUser($user);
|
||||
$this->actingAsMiniStore($store);
|
||||
$this->getJson("/mini/order/{$order->id}")
|
||||
->assertOk()
|
||||
->assertJsonPath('success', true)
|
||||
@@ -114,9 +113,9 @@ class StoreOrderItemTest extends ProcurementTestCase
|
||||
public function test_admin_detail_shows_cost_price_and_supplier(): void
|
||||
{
|
||||
$supplier = SupplierModel::factory()->create();
|
||||
[, $product, $user] = $this->makeStoreWithProduct('5.00', '4.00');
|
||||
[$store, $product] = $this->makeStoreWithProduct('5.00', '4.00');
|
||||
$product->update(['supplier_id' => $supplier->id]);
|
||||
$order = $this->placeOrder($product, $user);
|
||||
$order = $this->placeOrder($product, $store);
|
||||
|
||||
$this->actingAsSysUser();
|
||||
$this->getJson("/order/store/{$order->id}")
|
||||
@@ -130,8 +129,8 @@ class StoreOrderItemTest extends ProcurementTestCase
|
||||
/** 采购单内修改门店单品:重算单品金额与订单/采购单汇总 */
|
||||
public function test_update_store_item_recalculates_totals(): void
|
||||
{
|
||||
[$store, $product, $user] = $this->makeStoreWithProduct('5.00');
|
||||
$order = $this->placeOrder($product, $user, 2, StoreOrderModel::STATUS_SUMMARIZED);
|
||||
[$store, $product] = $this->makeStoreWithProduct('5.00');
|
||||
$order = $this->placeOrder($product, $store, 2, StoreOrderModel::STATUS_SUMMARIZED);
|
||||
$item = $order->items->first();
|
||||
$purchase = $this->generatePurchase();
|
||||
|
||||
@@ -162,10 +161,9 @@ class StoreOrderItemTest extends ProcurementTestCase
|
||||
/** 采购单已完成:门店单品增删改均拒绝,明细不变 */
|
||||
public function test_store_item_edits_rejected_when_purchase_completed(): void
|
||||
{
|
||||
[, $product, $user] = $this->makeStoreWithProduct('5.00');
|
||||
$order = $this->placeOrder($product, $user, 1, StoreOrderModel::STATUS_SUMMARIZED);
|
||||
[$store, $product] = $this->makeStoreWithProduct('5.00');
|
||||
$order = $this->placeOrder($product, $store, 1, StoreOrderModel::STATUS_SUMMARIZED);
|
||||
$purchase = $this->generatePurchase();
|
||||
$store = StoreModel::find($order->store_id);
|
||||
|
||||
$purchase->update(['status' => PurchaseOrderModel::STATUS_COMPLETED]);
|
||||
$this->actingAsSysUser();
|
||||
@@ -182,15 +180,15 @@ class StoreOrderItemTest extends ProcurementTestCase
|
||||
->assertJsonPath('success', false);
|
||||
|
||||
$item = $order->items->first();
|
||||
$this->assertSame(1, $item->fresh()->quantity, '被拒绝后明细不变');
|
||||
$this->assertSame('5.00', (string) $order->fresh()->total_amount, '被拒绝后总金额不变');
|
||||
$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, $user] = $this->makeStoreWithProduct('5.00');
|
||||
$this->placeOrder($product, $user, 1, StoreOrderModel::STATUS_SUMMARIZED);
|
||||
[$store, $product] = $this->makeStoreWithProduct('5.00');
|
||||
$this->placeOrder($product, $store, 1, StoreOrderModel::STATUS_SUMMARIZED);
|
||||
$purchase = $this->generatePurchase();
|
||||
|
||||
$this->actingAsSysUser();
|
||||
@@ -215,9 +213,8 @@ class StoreOrderItemTest extends ProcurementTestCase
|
||||
$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']);
|
||||
$user = UserModel::factory()->forStore($store->id)->create();
|
||||
|
||||
$this->placeOrder($product, $user, 2, StoreOrderModel::STATUS_SUMMARIZED);
|
||||
$this->placeOrder($product, $store, 2, StoreOrderModel::STATUS_SUMMARIZED);
|
||||
$purchase = $this->generatePurchase();
|
||||
|
||||
$this->actingAsSysUser();
|
||||
@@ -237,8 +234,8 @@ class StoreOrderItemTest extends ProcurementTestCase
|
||||
/** 新增单品:商品已删除时拒绝 */
|
||||
public function test_add_store_item_rejected_when_product_deleted(): void
|
||||
{
|
||||
[$store, $product, $user] = $this->makeStoreWithProduct('5.00');
|
||||
$this->placeOrder($product, $user, 1, StoreOrderModel::STATUS_SUMMARIZED);
|
||||
[$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']);
|
||||
@@ -258,12 +255,11 @@ class StoreOrderItemTest extends ProcurementTestCase
|
||||
{
|
||||
$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', 'cost_price' => '5.00']);
|
||||
$potato = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'name' => '土豆B', 'cost_price' => '5.00']);
|
||||
$this->placeOrder($cabbage, $user);
|
||||
$this->placeOrder($potato, $user);
|
||||
$this->placeOrder($cabbage, $store);
|
||||
$this->placeOrder($potato, $store);
|
||||
|
||||
$this->actingAsSysUser();
|
||||
$response = $this->getJson('/order/store?product_name=' . urlencode('白菜'));
|
||||
|
||||
@@ -6,16 +6,16 @@ use App\Models\CustomerLevelModel;
|
||||
use App\Models\ProductModel;
|
||||
use App\Models\StoreModel;
|
||||
use App\Models\StoreOrderModel;
|
||||
use App\Models\UserModel;
|
||||
use Modules\SystemTool\Models\SysFileModel;
|
||||
|
||||
/**
|
||||
* 小程序下单:等级上浮价快照、服务端重算总价、取消限制、门店数据隔离
|
||||
* (门店即用户:小程序端登录主体为 StoreModel)
|
||||
*/
|
||||
class StoreOrderTest extends ProcurementTestCase
|
||||
{
|
||||
/**
|
||||
* @return array{0: StoreModel, 1: ProductModel, 2: UserModel}
|
||||
* @return array{0: StoreModel, 1: ProductModel}
|
||||
*/
|
||||
private function makeStoreWithProduct(string $price = '5.00'): array
|
||||
{
|
||||
@@ -26,14 +26,14 @@ class StoreOrderTest extends ProcurementTestCase
|
||||
'cost_price' => $price,
|
||||
]);
|
||||
|
||||
return [$store, $product, UserModel::factory()->forStore($store->id)->create()];
|
||||
return [$store, $product];
|
||||
}
|
||||
|
||||
/** 下单快照等级价,服务端重算单品金额与订单总价 */
|
||||
public function test_place_order_snapshots_level_price_and_recalculates(): void
|
||||
{
|
||||
[$store, $product, $user] = $this->makeStoreWithProduct('5.50');
|
||||
$this->actingAsMiniUser($user);
|
||||
[$store, $product] = $this->makeStoreWithProduct('5.50');
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$this->postJson('/mini/order', [
|
||||
'items' => [['product_id' => $product->id, 'quantity' => 3]],
|
||||
@@ -54,8 +54,8 @@ class StoreOrderTest extends ProcurementTestCase
|
||||
/** 前端传入的金额字段一律被忽略 */
|
||||
public function test_client_supplied_amount_is_ignored(): void
|
||||
{
|
||||
[$store, $product, $user] = $this->makeStoreWithProduct('5.00');
|
||||
$this->actingAsMiniUser($user);
|
||||
[$store, $product] = $this->makeStoreWithProduct('5.00');
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$this->postJson('/mini/order', [
|
||||
'items' => [[
|
||||
@@ -73,9 +73,9 @@ class StoreOrderTest extends ProcurementTestCase
|
||||
/** 门店绑定的客户等级被删除时拒绝下单 */
|
||||
public function test_store_level_missing_rejected(): void
|
||||
{
|
||||
[$store, $product, $user] = $this->makeStoreWithProduct('5.00');
|
||||
[$store, $product] = $this->makeStoreWithProduct('5.00');
|
||||
CustomerLevelModel::whereKey($store->level_id)->delete();
|
||||
$this->actingAsMiniUser($user);
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$this->postJson('/mini/order', [
|
||||
'items' => [['product_id' => $product->id, 'quantity' => 1]],
|
||||
@@ -87,13 +87,13 @@ class StoreOrderTest extends ProcurementTestCase
|
||||
/** 取消:仅待汇总订单可取消 */
|
||||
public function test_cancel_only_pending_orders(): void
|
||||
{
|
||||
[$store, $product, $user] = $this->makeStoreWithProduct();
|
||||
$this->actingAsMiniUser($user);
|
||||
[$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->fresh()->status);
|
||||
$this->assertSame(StoreOrderModel::STATUS_CANCELLED, $order->refresh()->status);
|
||||
|
||||
// 已取消不可重复取消
|
||||
$this->putJson("/mini/order/{$order->id}/cancel")->assertJsonPath('success', false);
|
||||
@@ -102,27 +102,27 @@ class StoreOrderTest extends ProcurementTestCase
|
||||
/** 已汇总订单不可取消 */
|
||||
public function test_summarized_order_cannot_be_cancelled(): void
|
||||
{
|
||||
[$store, $product, $user] = $this->makeStoreWithProduct();
|
||||
$this->actingAsMiniUser($user);
|
||||
[$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->fresh()->status);
|
||||
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $order->refresh()->status);
|
||||
}
|
||||
|
||||
/** 门店数据隔离:只能查看本店订单 */
|
||||
public function test_order_data_isolated_between_stores(): void
|
||||
{
|
||||
[$storeA, $product, $userA] = $this->makeStoreWithProduct();
|
||||
$this->actingAsMiniUser($userA);
|
||||
[$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 的订单
|
||||
// 门店 B 访问门店 A 的订单
|
||||
$storeB = StoreModel::factory()->create(['level_id' => $storeA->level_id]);
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($storeB->id)->create());
|
||||
$this->actingAsMiniStore($storeB);
|
||||
|
||||
$this->getJson("/mini/order/{$orderOfA->id}")->assertJsonPath('success', false);
|
||||
$this->getJson('/mini/order')->assertJsonPath('data.total', 0);
|
||||
@@ -133,7 +133,7 @@ class StoreOrderTest extends ProcurementTestCase
|
||||
{
|
||||
$level = CustomerLevelModel::factory()->create();
|
||||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$items = [];
|
||||
foreach (['白菜', '土豆', '番茄', '黄瓜'] as $name) {
|
||||
@@ -161,8 +161,8 @@ class StoreOrderTest extends ProcurementTestCase
|
||||
/** 订货日期区间筛选(配合 /order/summary 周期下钻) */
|
||||
public function test_order_list_filters_by_order_date_range(): void
|
||||
{
|
||||
[$store, $product, $user] = $this->makeStoreWithProduct();
|
||||
$this->actingAsMiniUser($user);
|
||||
[$store, $product] = $this->makeStoreWithProduct();
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
foreach ([1, 1, 1] as $qty) {
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
|
||||
@@ -181,8 +181,8 @@ class StoreOrderTest extends ProcurementTestCase
|
||||
/** 非法筛选参数被拦截(状态枚举 / 分页上限 / 日期区间倒置) */
|
||||
public function test_order_list_rejects_invalid_filters(): void
|
||||
{
|
||||
[, , $user] = $this->makeStoreWithProduct();
|
||||
$this->actingAsMiniUser($user);
|
||||
[$store] = $this->makeStoreWithProduct();
|
||||
$this->actingAsMiniStore($store);
|
||||
|
||||
$this->getJson('/mini/order?status=7')->assertJsonPath('success', false);
|
||||
$this->getJson('/mini/order?pageSize=100')->assertJsonPath('success', false);
|
||||
@@ -192,8 +192,8 @@ class StoreOrderTest extends ProcurementTestCase
|
||||
/** 造一笔指定状态的订单(商品 5.00 × 2 = 10.00) */
|
||||
private function makeOrderWithStatus(int $status): StoreOrderModel
|
||||
{
|
||||
[$store, $product, $user] = $this->makeStoreWithProduct('5.00');
|
||||
$this->actingAsMiniUser($user);
|
||||
[$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();
|
||||
@@ -213,7 +213,7 @@ class StoreOrderTest extends ProcurementTestCase
|
||||
$this->actingAsSysUser();
|
||||
$this->deleteJson("/order/store/{$order->id}")->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
$this->assertNotNull($order->fresh()->deleted_at, '软删除应写入 deleted_at');
|
||||
$this->assertNotNull($order->refresh()->deleted_at, '软删除应写入 deleted_at');
|
||||
$this->assertNull(StoreOrderModel::find($order->id), '默认查询不可见软删除订单');
|
||||
$this->assertNotNull(StoreOrderModel::withTrashed()->find($order->id), '数据仍保留在库中');
|
||||
|
||||
@@ -222,7 +222,7 @@ class StoreOrderTest extends ProcurementTestCase
|
||||
$this->getJson("/order/store/{$order->id}")->assertJsonPath('success', false);
|
||||
|
||||
// 小程序端历史订单同样不可见
|
||||
$this->actingAsMiniUser(UserModel::where('store_id', $order->store_id)->first());
|
||||
$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);
|
||||
}
|
||||
@@ -241,7 +241,7 @@ class StoreOrderTest extends ProcurementTestCase
|
||||
$this->actingAsSysUser();
|
||||
$this->deleteJson("/order/store/{$order->id}")
|
||||
->assertOk()->assertJsonPath('success', false);
|
||||
$this->assertNull($order->fresh()->deleted_at, '非已取消订单不得删除');
|
||||
$this->assertNull($order->refresh()->deleted_at, '非已取消订单不得删除');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,13 +263,13 @@ class StoreOrderTest extends ProcurementTestCase
|
||||
|
||||
$this->putJson("/order/store/{$order->id}/status", ['status' => StoreOrderModel::STATUS_SUMMARIZED])
|
||||
->assertJsonPath('success', true);
|
||||
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $order->fresh()->status);
|
||||
$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->fresh()->status);
|
||||
$this->assertSame(StoreOrderModel::STATUS_CANCELLED, $order2->refresh()->status);
|
||||
}
|
||||
|
||||
/** 手动流转:配送中/已完成等目标状态被拒绝(由采购/账单链路自动推进) */
|
||||
@@ -280,7 +280,7 @@ class StoreOrderTest extends ProcurementTestCase
|
||||
$this->actingAsSysUser();
|
||||
$this->putJson("/order/store/{$order->id}/status", ['status' => $target])
|
||||
->assertJsonPath('success', false);
|
||||
$this->assertSame(StoreOrderModel::STATUS_DELIVERING, $order->fresh()->status);
|
||||
$this->assertSame(StoreOrderModel::STATUS_DELIVERING, $order->refresh()->status);
|
||||
}
|
||||
|
||||
// 已接单不能手动转采购中(须经生成采购单)
|
||||
@@ -302,8 +302,8 @@ class StoreOrderTest extends ProcurementTestCase
|
||||
'ids' => [$pending1->id, $pending2->id],
|
||||
'status' => StoreOrderModel::STATUS_SUMMARIZED,
|
||||
])->assertJsonPath('success', true);
|
||||
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending1->fresh()->status);
|
||||
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending2->fresh()->status);
|
||||
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending1->refresh()->status);
|
||||
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending2->refresh()->status);
|
||||
|
||||
// 批量完成被拒绝(目标状态校验拦截)
|
||||
$this->putJson('/order/store/batchStatus', [
|
||||
@@ -316,7 +316,7 @@ class StoreOrderTest extends ProcurementTestCase
|
||||
'ids' => [$pending1->id],
|
||||
'status' => StoreOrderModel::STATUS_CANCELLED,
|
||||
])->assertJsonPath('success', false);
|
||||
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending1->fresh()->status);
|
||||
$this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending1->refresh()->status);
|
||||
}
|
||||
|
||||
/** 订单列表商品预览:附带首图/规格/单位供小程序端展示 */
|
||||
@@ -343,9 +343,8 @@ class StoreOrderTest extends ProcurementTestCase
|
||||
'image_ids' => (string) $file->id,
|
||||
'cost_price' => '5.00',
|
||||
]);
|
||||
$user = UserModel::factory()->forStore($store->id)->create();
|
||||
|
||||
$this->actingAsMiniUser($user);
|
||||
$this->actingAsMiniStore($store);
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 2]]])
|
||||
->assertJsonPath('success', true);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user