修复唯品会账户店铺及账单项目筛选 #259
@ -144,6 +144,19 @@ class AccountBills extends Controller
|
||||
{
|
||||
$this->authorizeStoreReceivingBills();
|
||||
|
||||
if ($bill_type === 'vipshop') {
|
||||
$query = AccountBill::query()
|
||||
->where('company_id', company_id())
|
||||
->where('bill_type', $bill_type);
|
||||
|
||||
return response()->json([
|
||||
'shops' => (clone $query)->whereNotNull('store_name')->where('store_name', '<>', '')
|
||||
->distinct()->orderBy('store_name')->pluck('store_name')->all(),
|
||||
'bill_items' => (clone $query)->whereNotNull('bill_item')->where('bill_item', '<>', '')
|
||||
->distinct()->orderBy('bill_item')->pluck('bill_item')->all(),
|
||||
]);
|
||||
}
|
||||
|
||||
abort_unless(in_array($bill_type, ['douyin', 'pinduoduo', 'dewu', 'jd-transactions', 'jd-wallet'], true), 404, '该账户类型没有可用的筛选选项。');
|
||||
|
||||
$label = $this->mirrorLabel($bill_type);
|
||||
|
||||
@ -84,7 +84,7 @@ export default {
|
||||
// Every account-bill tab uses the same explicit picker path. Keeping one
|
||||
// control avoids browser-native date inputs appearing on older tabs.
|
||||
usesStableDateControl(){return true;},
|
||||
usesMirrorOptions(){return ['douyin','pinduoduo','dewu','jd-transactions','jd-wallet'].indexOf(this.billType)!==-1;},
|
||||
usesMirrorOptions(){return ['douyin','pinduoduo','dewu','jd-transactions','jd-wallet','vipshop'].indexOf(this.billType)!==-1;},
|
||||
invalidDateRange(){return Boolean(this.filters.start&&this.filters.end&&this.filters.start>this.filters.end);},
|
||||
startDateConfig(){return {...dateConfig,maxDate:this.filters.end||null};},
|
||||
endDateConfig(){return {...dateConfig,minDate:this.filters.start||null};},
|
||||
|
||||
@ -11,10 +11,10 @@ class AccountBillsTableTest extends TestCase
|
||||
$source = file_get_contents(__DIR__ . '/../../../resources/assets/js/components/business/AccountBills.vue');
|
||||
|
||||
$this->assertStringContainsString("isDouyin(){return this.billType==='douyin';}", $source);
|
||||
$this->assertStringContainsString("usesStableDateControl(){return ['pinduoduo','dewu'].indexOf(this.billType)!==-1;}", $source);
|
||||
$this->assertStringContainsString("usesStableDateControl(){return true;}", $source);
|
||||
$this->assertStringContainsString("{ 'is-stable-date': usesStableDateControl }", $source);
|
||||
$this->assertStringContainsString('.tw-date.is-stable-date .tw-date-control', $source);
|
||||
$this->assertStringContainsString("['douyin','pinduoduo','dewu','jd-transactions','jd-wallet'].indexOf(this.billType)!==-1", $source);
|
||||
$this->assertStringContainsString("['douyin','pinduoduo','dewu','jd-transactions','jd-wallet','vipshop'].indexOf(this.billType)!==-1", $source);
|
||||
$this->assertStringContainsString('v-if="usesMirrorOptions && !filterOptionsError"', $source);
|
||||
$this->assertStringContainsString('page-key="store-receiving-bills"', $source);
|
||||
$this->assertStringContainsString(':tab-key="billType"', $source);
|
||||
|
||||
@ -24,6 +24,46 @@ function component(get, billType = 'douyin') {
|
||||
|
||||
const response = (total = 51) => ({ data: { data: [{ record_code: '1' }], total, current_page: 1, last_page: 3, per_page: 20 } });
|
||||
|
||||
test('vipshop loads real options, queries selections and exports the same scope', async () => {
|
||||
const requests = [];
|
||||
const { instance, navigations } = component(async (url, request) => {
|
||||
if (url.endsWith('/filter-options')) return { data: { shops: ['唯品会MP卓禾'], bill_items: ['销售货款'] } };
|
||||
requests.push(request.params);
|
||||
return response(3);
|
||||
}, 'vipshop');
|
||||
assert.equal(instance.usesMirrorOptions, true);
|
||||
await instance.fetchFilterOptions();
|
||||
instance.filters.store_name = instance.filterOptions.shops[0];
|
||||
instance.filters.bill_item = instance.filterOptions.bill_items[0];
|
||||
instance.currentPage = 4;
|
||||
await instance.applyFilters();
|
||||
assert.equal(requests[0].page, 1);
|
||||
assert.equal(requests[0].store_name, '唯品会MP卓禾');
|
||||
assert.equal(requests[0].bill_item, '销售货款');
|
||||
instance.exportRows();
|
||||
const params = new URL(navigations[0], 'https://example.test').searchParams;
|
||||
assert.equal(params.get('store_name'), requests[0].store_name);
|
||||
assert.equal(params.get('bill_item'), requests[0].bill_item);
|
||||
await instance.resetFilters();
|
||||
assert.equal(requests[1].store_name, '');
|
||||
assert.equal(requests[1].bill_item, '');
|
||||
});
|
||||
|
||||
test('vipshop option failure allows retry without reporting empty successful options', async () => {
|
||||
let fail = true;
|
||||
const { instance } = component(async () => {
|
||||
if (fail) throw new Error('unavailable');
|
||||
return { data: { shops: ['唯品会MP卓禾'], bill_items: [] } };
|
||||
}, 'vipshop');
|
||||
await instance.fetchFilterOptions();
|
||||
assert.equal(instance.filterOptionsError, true);
|
||||
assert.equal(instance.filterOptionsLoading, false);
|
||||
fail = false;
|
||||
await instance.fetchFilterOptions();
|
||||
assert.equal(instance.filterOptionsError, false);
|
||||
assert.equal(instance.filterOptions.shops.length, 1);
|
||||
});
|
||||
|
||||
test('template compiles searchable filters and export scopes', () => {
|
||||
assert.deepEqual(compiler.compile(sfc.template.content).errors, []);
|
||||
});
|
||||
|
||||
43
tests/scripts/vipshop-filter-smoke.php
Normal file
43
tests/scripts/vipshop-filter-smoke.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
// Run in an isolated application container with --network none; fixtures only.
|
||||
require '/var/www/html/vendor/autoload.php';
|
||||
$app = require '/var/www/html/bootstrap/app.php';
|
||||
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
config(['database.default' => 'filter_test', 'database.connections.filter_test' => [
|
||||
'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '',
|
||||
], 'model-caching.enabled' => false, 'cache.default' => 'array']);
|
||||
$company = new App\Models\Common\Company;
|
||||
$company->id = 1;
|
||||
$app->instance(App\Models\Common\Company::class, $company);
|
||||
(require '/workspace/modules/EcommerceFinance/Database/Migrations/2026_07_21_000002_create_ec_account_bills_table.php')->up();
|
||||
require '/workspace/modules/EcommerceFinance/Http/Controllers/AccountBills.php';
|
||||
$controller = new class extends Modules\EcommerceFinance\Http\Controllers\AccountBills {
|
||||
protected function authorizeStoreReceivingBills(): void {}
|
||||
};
|
||||
function check($condition, $message) { if (!$condition) throw new RuntimeException($message); }
|
||||
foreach ([
|
||||
[1, 'vipshop', 'Store A', 'Sale', null],
|
||||
[1, 'vipshop', 'Store A', 'Sale', null],
|
||||
[1, 'vipshop', 'Store B', 'Refund', null],
|
||||
[2, 'vipshop', 'Other company', 'Private item', null],
|
||||
[1, 'douyin', 'Other platform', 'Other item', null],
|
||||
[1, 'vipshop', 'Deleted store', 'Deleted item', '2026-09-01'],
|
||||
] as $i => [$companyId, $type, $store, $item, $deleted]) {
|
||||
Illuminate\Support\Facades\DB::table('ec_account_bills')->insert([
|
||||
'company_id' => $companyId, 'bill_type' => $type, 'record_code' => (string) $i,
|
||||
'store_name' => $store, 'bill_item' => $item, 'deleted_at' => $deleted,
|
||||
'occurred_at' => '2026-09-22 12:00:00', 'income' => '12.34', 'expense' => '0',
|
||||
]);
|
||||
}
|
||||
$options = $controller->filterOptions('vipshop')->getData(true);
|
||||
check($options['shops'] === ['Store A', 'Store B'], 'Shop options must be unique and company/platform/deletion scoped');
|
||||
check($options['bill_items'] === ['Refund', 'Sale'], 'Item options must be unique and scoped');
|
||||
$request = Illuminate\Http\Request::create('/', 'GET', ['store_name' => 'Store A', 'bill_item' => 'Sale', 'limit' => 1]);
|
||||
$result = $controller->index($request, 'vipshop')->getData(true);
|
||||
check($result['total'] === 2 && count($result['data']) === 1, 'Filtering and pagination failed');
|
||||
check(abs($result['summary']['income'] - 24.68) < 0.001, 'Summary must cover all matching rows');
|
||||
$export = $controller->export($request, 'vipshop');
|
||||
ob_start(); $export->sendContent(); $csv = ob_get_clean();
|
||||
check(substr_count($csv, 'Store A') === 2 && !str_contains($csv, 'Store B'), 'Export must contain all filtered rows only');
|
||||
echo "PASS: real SQLite query; scoped options; filtering; pagination; summary; full filtered export\n";
|
||||
Loading…
x
Reference in New Issue
Block a user