小程序导出账单接口
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Exports\BillExport;
|
||||
use App\Models\BillModel;
|
||||
use App\Models\ProductCategoryModel;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 门店账单合并导出:跨账单按商品合并明细(加权平均单价)/ 一级分类过滤 /
|
||||
* 配送费与附加金额全额汇总 / ids 校验 / 权限拦截 / 分类下无明细拒绝
|
||||
*/
|
||||
class BillExportTest extends ProcurementTestCase
|
||||
{
|
||||
private static int $billSeq = 0;
|
||||
|
||||
/** 造一张账单(默认配送费 10、筐 2×5 + 托盘 1×20 = 附加 30、总额 140,未支付) */
|
||||
private function makeBill(StoreModel $store, array $attributes = []): BillModel
|
||||
{
|
||||
$seq = ++self::$billSeq;
|
||||
|
||||
return BillModel::create(array_merge([
|
||||
'bill_no' => 'ZD' . str_pad((string) $seq, 12, '0', STR_PAD_LEFT),
|
||||
'purchase_id' => PurchaseOrderModel::factory()->create()->id,
|
||||
'store_id' => $store->id,
|
||||
'bill_date' => '2026-08-10',
|
||||
'product_amount' => '100.00',
|
||||
'delivery_fee' => '10.00',
|
||||
'box_num' => 2,
|
||||
'tray_num' => 1,
|
||||
'box_price' => '5.00',
|
||||
'tray_price' => '20.00',
|
||||
'added_amount' => '30.00',
|
||||
'total_amount' => '140.00',
|
||||
'status' => BillModel::STATUS_UNPAID,
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
/** 造一条账单明细(快照品名/包规/单位取自商品档案) */
|
||||
private function makeItem(BillModel $bill, ProductModel $product, int $quantity, string $price): StoreOrderItemModel
|
||||
{
|
||||
return StoreOrderItemModel::factory()->create([
|
||||
'bill_id' => $bill->id,
|
||||
'store_id' => $bill->store_id,
|
||||
'product_id' => $product->id,
|
||||
'category_id' => $product->category_id,
|
||||
'product_name' => $product->name,
|
||||
'product_spec' => $product->spec,
|
||||
'unit' => $product->unit,
|
||||
'price' => $price,
|
||||
'quantity' => $quantity,
|
||||
'weight' => $quantity * 10,
|
||||
'amount' => bcmul($price, (string) $quantity, 2),
|
||||
]);
|
||||
}
|
||||
|
||||
/** 跨账单合并:同商品数量累加、单价加权平均,配送费/附加/总金额全额汇总 */
|
||||
public function test_export_merges_items_across_bills(): void
|
||||
{
|
||||
$this->freezeTime();
|
||||
|
||||
$root = ProductCategoryModel::create(['name' => '蔬菜', 'parent_id' => 0, 'sort' => 0, 'status' => 1]);
|
||||
$store = StoreModel::factory()->create();
|
||||
$productA = ProductModel::factory()->create(['category_id' => $root->id]);
|
||||
$productB = ProductModel::factory()->create(['category_id' => $root->id]);
|
||||
|
||||
$bill1 = $this->makeBill($store);
|
||||
$bill2 = $this->makeBill($store);
|
||||
// 同一商品跨账单:10×2.00 + 6×3.00 → 数量 16、金额 38.00、加权单价 2.37
|
||||
$this->makeItem($bill1, $productA, 10, '2.00');
|
||||
$this->makeItem($bill2, $productA, 6, '3.00');
|
||||
$this->makeItem($bill2, $productB, 4, '5.00');
|
||||
|
||||
$this->actingAsSysUser();
|
||||
Excel::fake();
|
||||
$this->get("/recon/bill/export?ids={$bill1->id},{$bill2->id}")->assertOk();
|
||||
|
||||
Excel::assertDownloaded(
|
||||
'门店账单_' . now()->format('Ymd_His') . '.xlsx',
|
||||
static function (BillExport $export) use ($productA, $productB): bool {
|
||||
$rows = $export->collection()->values();
|
||||
// 标题1 + 范围2 + 空行1 + 列头1 + 明细2 + 合计4 = 11 行
|
||||
if ($rows->count() !== 11) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$details = $rows->slice(5, 2)->values();
|
||||
$rowA = $details->firstWhere(1, $productA->name);
|
||||
$rowB = $details->firstWhere(1, $productB->name);
|
||||
if ($rowA === null || $rowB === null) {
|
||||
return false;
|
||||
}
|
||||
// 跨账单同商品合并:数量 16、加权单价 2.37、金额 38.00
|
||||
if ((float) $rowA[4] !== 2.37 || (int) $rowA[5] !== 16 || (float) $rowA[7] !== 38.0) {
|
||||
return false;
|
||||
}
|
||||
if ((int) $rowB[5] !== 4 || (float) $rowB[7] !== 20.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$summary = $rows->slice(7)->values();
|
||||
// 合计:数量 20、商品金额 58.00;配送费 20.00;附加 60.00(筐 4 / 托盘 2);总金额 138.00
|
||||
if ((int) $summary[0][5] !== 20 || (float) $summary[0][7] !== 58.0) {
|
||||
return false;
|
||||
}
|
||||
if ($summary[1][0] !== '配送费合计' || (float) $summary[1][7] !== 20.0) {
|
||||
return false;
|
||||
}
|
||||
if (! str_contains((string) $summary[2][0], '筐 4 个 / 周转托盘 2 个') || (float) $summary[2][7] !== 60.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (float) $summary[3][7] === 138.0;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** 一级分类过滤:子分类商品归入根分类,其他分类商品忽略;费用行仍全额汇总 */
|
||||
public function test_export_filters_by_root_category(): void
|
||||
{
|
||||
$this->freezeTime();
|
||||
|
||||
$vegRoot = ProductCategoryModel::create(['name' => '蔬菜', 'parent_id' => 0, 'sort' => 0, 'status' => 1]);
|
||||
$vegChild = ProductCategoryModel::create(['name' => '叶菜类', 'parent_id' => $vegRoot->id, 'sort' => 0, 'status' => 1]);
|
||||
$meatRoot = ProductCategoryModel::create(['name' => '肉禽蛋', 'parent_id' => 0, 'sort' => 1, 'status' => 1]);
|
||||
|
||||
$store = StoreModel::factory()->create();
|
||||
$veg = ProductModel::factory()->create(['category_id' => $vegChild->id]);
|
||||
$meat = ProductModel::factory()->create(['category_id' => $meatRoot->id]);
|
||||
|
||||
$bill = $this->makeBill($store);
|
||||
$this->makeItem($bill, $veg, 10, '2.00');
|
||||
$this->makeItem($bill, $meat, 5, '20.00');
|
||||
|
||||
$this->actingAsSysUser();
|
||||
Excel::fake();
|
||||
$this->get("/recon/bill/export?ids={$bill->id}&category_id={$vegRoot->id}")->assertOk();
|
||||
|
||||
Excel::assertDownloaded(
|
||||
'门店账单_' . now()->format('Ymd_His') . '.xlsx',
|
||||
static function (BillExport $export) use ($veg): bool {
|
||||
$rows = $export->collection()->values();
|
||||
// 范围行标注所选分类
|
||||
if (! str_contains((string) $rows[2][0], '分类:蔬菜')) {
|
||||
return false;
|
||||
}
|
||||
// 明细仅剩蔬菜(子分类归入根分类),肉禽被忽略
|
||||
$detail = $rows->slice(5, 1)->values()->first();
|
||||
if ($detail === null || $detail[1] !== $veg->name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$summary = $rows->slice(6)->values();
|
||||
// 商品金额合计 20.00(仅蔬菜);配送费 10.00 / 附加 30.00 全额;总金额 60.00
|
||||
if ((float) $summary[0][7] !== 20.0) {
|
||||
return false;
|
||||
}
|
||||
if ((float) $summary[1][7] !== 10.0 || (float) $summary[2][7] !== 30.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (float) $summary[3][7] === 60.0;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** 分类过滤后无明细 → 拒绝(真实下载链路,导出类抛业务异常) */
|
||||
public function test_export_empty_category_rejected(): void
|
||||
{
|
||||
$vegRoot = ProductCategoryModel::create(['name' => '蔬菜', 'parent_id' => 0, 'sort' => 0, 'status' => 1]);
|
||||
$meatRoot = ProductCategoryModel::create(['name' => '肉禽蛋', 'parent_id' => 0, 'sort' => 1, 'status' => 1]);
|
||||
|
||||
$store = StoreModel::factory()->create();
|
||||
$meat = ProductModel::factory()->create(['category_id' => $meatRoot->id]);
|
||||
|
||||
$bill = $this->makeBill($store);
|
||||
$this->makeItem($bill, $meat, 5, '20.00');
|
||||
|
||||
$this->actingAsSysUser();
|
||||
$this->get("/recon/bill/export?ids={$bill->id}&category_id={$vegRoot->id}")
|
||||
->assertJsonPath('success', false);
|
||||
}
|
||||
|
||||
/** 无 recon.bill.export 权限点 → 拦截 */
|
||||
public function test_export_requires_export_permission(): void
|
||||
{
|
||||
$store = StoreModel::factory()->create();
|
||||
$bill = $this->makeBill($store);
|
||||
|
||||
// 先建一个占位用户:每个测试方法内首个系统用户自增 id=1,
|
||||
// SysAccessToken::can() 对 tokenable_id==1(超管)放行全部权限,会绕过 abilities 校验
|
||||
$this->actingAsSysUser();
|
||||
// 仅持有查询权限的用户
|
||||
$this->actingAsSysUser(['recon.bill.query']);
|
||||
|
||||
$response = $this->get("/recon/bill/export?ids={$bill->id}");
|
||||
$this->assertFalse($response->json('success'), '缺少权限点应被拦截');
|
||||
}
|
||||
|
||||
/** ids 缺失 / 超 100 张 / 账单不存在 → 拒绝 */
|
||||
public function test_export_invalid_ids_rejected(): void
|
||||
{
|
||||
$this->actingAsSysUser();
|
||||
|
||||
$this->get('/recon/bill/export')
|
||||
->assertJsonPath('success', false);
|
||||
|
||||
$this->get('/recon/bill/export?ids=' . implode(',', range(1, 101)))
|
||||
->assertJsonPath('success', false);
|
||||
|
||||
$this->get('/recon/bill/export?ids=99999')
|
||||
->assertJsonPath('success', false);
|
||||
}
|
||||
|
||||
/** 小程序端导出:强制本店过滤,混入他店账单整批拒绝 */
|
||||
public function test_mini_export_scoped_to_bound_store(): void
|
||||
{
|
||||
$this->freezeTime();
|
||||
|
||||
$root = ProductCategoryModel::create(['name' => '蔬菜', 'parent_id' => 0, 'sort' => 0, 'status' => 1]);
|
||||
$storeA = StoreModel::factory()->create();
|
||||
$storeB = StoreModel::factory()->create();
|
||||
$product = ProductModel::factory()->create(['category_id' => $root->id]);
|
||||
|
||||
$billA = $this->makeBill($storeA);
|
||||
$billB = $this->makeBill($storeB);
|
||||
$this->makeItem($billA, $product, 10, '2.00');
|
||||
$this->makeItem($billB, $product, 5, '20.00');
|
||||
|
||||
$user = UserModel::factory()->forStore($storeA->id)->create();
|
||||
$this->actingAsMiniUser($user);
|
||||
|
||||
// 本店账单正常导出
|
||||
Excel::fake();
|
||||
$this->get("/mini/bill/export?ids={$billA->id}")->assertOk();
|
||||
Excel::assertDownloaded(
|
||||
'门店账单_' . now()->format('Ymd_His') . '.xlsx',
|
||||
static function (BillExport $export): bool {
|
||||
$rows = $export->collection()->values();
|
||||
// 明细仅本店账单的 1 行(他店账单未混入)
|
||||
return $rows->count() === 10;
|
||||
}
|
||||
);
|
||||
|
||||
// 混入他店账单 → 张数不匹配整批拒绝
|
||||
$this->get("/mini/bill/export?ids={$billA->id},{$billB->id}")
|
||||
->assertJsonPath('success', false);
|
||||
|
||||
// 仅他店账单 → 本店口径查无账单
|
||||
$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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user