feat: 淘系微信账单增加淘系支付宝账户余额 #898
@ -7,13 +7,18 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Modules\EcommerceFinance\AccountBillColumns;
|
||||
use Modules\EcommerceFinance\Http\Concerns\DecoratesLedgerRemarks;
|
||||
use Modules\EcommerceFinance\Repositories\TaobaoAlipaySettlementBillRepository;
|
||||
use Modules\EcommerceFinance\Repositories\TaobaoWechatSettlementBillRepository;
|
||||
use RuntimeException;
|
||||
|
||||
class TaobaoWechatAccountBills extends Controller
|
||||
{
|
||||
use DecoratesLedgerRemarks;
|
||||
public function index(Request $request, TaobaoWechatSettlementBillRepository $repository)
|
||||
public function index(
|
||||
Request $request,
|
||||
TaobaoWechatSettlementBillRepository $repository,
|
||||
TaobaoAlipaySettlementBillRepository $alipayRepository,
|
||||
)
|
||||
{
|
||||
$this->authorizeStoreReceivingBills();
|
||||
|
||||
@ -50,16 +55,48 @@ class TaobaoWechatAccountBills extends Controller
|
||||
'current_page' => 1,
|
||||
'last_page' => 1,
|
||||
'per_page' => (int) ($validated['limit'] ?? 10),
|
||||
'summary' => ['income' => 0, 'expense' => 0, 'balance' => 0, 'count' => 0],
|
||||
'summary' => $this->emptySummary(),
|
||||
'columns' => AccountBillColumns::for('taobao-wechat'),
|
||||
], 503);
|
||||
}
|
||||
|
||||
$alipaySummary = null;
|
||||
try {
|
||||
// 微信账单没有余额列,期初和期末统一取同一筛选条件下淘系支付宝的账户汇总。
|
||||
$alipaySummary = $alipayRepository->summary($filters);
|
||||
} catch (RuntimeException $exception) {
|
||||
if ($exception->getMessage() !== 'taobao_alipay_authoritative_bills_not_ready') {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
$data['summary'] = array_merge($data['summary'], [
|
||||
'opening_balance' => $alipaySummary['opening_balance'] ?? null,
|
||||
'opening_balance_date' => $alipaySummary['opening_balance_date'] ?? null,
|
||||
'opening_balance_source' => $alipaySummary['opening_balance_source'] ?? null,
|
||||
'balance' => $alipaySummary['balance'] ?? null,
|
||||
'balance_source' => $alipaySummary === null ? 'unavailable' : 'taobao_alipay',
|
||||
]);
|
||||
$data['columns'] = AccountBillColumns::for('taobao-wechat');
|
||||
|
||||
return response()->json($this->withLedgerRemarks($data, 'taobao-wechat'));
|
||||
}
|
||||
|
||||
/** @return array<string,int|float|null> */
|
||||
private function emptySummary(): array
|
||||
{
|
||||
return [
|
||||
'income' => 0,
|
||||
'expense' => 0,
|
||||
'opening_balance' => null,
|
||||
'opening_balance_date' => null,
|
||||
'opening_balance_source' => null,
|
||||
'balance' => null,
|
||||
'balance_source' => 'unavailable',
|
||||
'count' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
public function filterOptions(TaobaoWechatSettlementBillRepository $repository)
|
||||
{
|
||||
$this->authorizeStoreReceivingBills();
|
||||
|
||||
@ -57,14 +57,8 @@ class TaobaoAlipaySettlementBillRepository
|
||||
$perPage = min(max(1, $perPage), 100);
|
||||
$query = $this->query();
|
||||
$this->applyFilters($query, $filters);
|
||||
$summary = (clone $query)->selectRaw(
|
||||
'COALESCE(SUM(income_amount), 0) AS income_total, '
|
||||
. 'COALESCE(SUM(expense_amount), 0) AS expense_total, COUNT(*) AS row_count'
|
||||
)->first();
|
||||
$total = (int) ($summary->row_count ?? 0);
|
||||
$income = (float) ($summary->income_total ?? 0);
|
||||
$expense = (float) ($summary->expense_total ?? 0);
|
||||
$opening = $this->openingBalance($filters);
|
||||
$summary = $this->summaryForQuery($query, $filters);
|
||||
$total = $summary['count'];
|
||||
$sortColumn = $this->sortColumn($sort);
|
||||
$direction = $direction === 'asc' ? 'asc' : 'desc';
|
||||
$query->orderBy($sortColumn, $direction);
|
||||
@ -79,15 +73,7 @@ class TaobaoAlipaySettlementBillRepository
|
||||
'current_page' => $page,
|
||||
'last_page' => max(1, (int) ceil($total / $perPage)),
|
||||
'per_page' => $perPage,
|
||||
'summary' => [
|
||||
'income' => $income,
|
||||
'expense' => $expense,
|
||||
'opening_balance' => $opening['balance'],
|
||||
'opening_balance_date' => $opening['date'],
|
||||
'opening_balance_source' => $opening['source'],
|
||||
'balance' => round($opening['balance'] + $income - $expense, 2),
|
||||
'count' => $total,
|
||||
],
|
||||
'summary' => $summary,
|
||||
'sync' => [
|
||||
'status' => 'succeeded',
|
||||
'last_succeeded_at' => $this->connection->table(self::IMPORT_TABLE)
|
||||
@ -96,6 +82,46 @@ class TaobaoAlipaySettlementBillRepository
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前筛选条件的账户余额汇总。
|
||||
*
|
||||
* 淘系微信页签本身没有余额列,直接复用这里的期初、收支和期末口径,
|
||||
* 避免两个页签对同一账期计算出不同的账户余额。
|
||||
*
|
||||
* @return array{income:float,expense:float,opening_balance:float,opening_balance_date:?string,opening_balance_source:string,balance:float,count:int}
|
||||
*/
|
||||
public function summary(array $filters): array
|
||||
{
|
||||
$query = $this->query();
|
||||
$this->applyFilters($query, $filters);
|
||||
|
||||
return $this->summaryForQuery($query, $filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{income:float,expense:float,opening_balance:float,opening_balance_date:?string,opening_balance_source:string,balance:float,count:int}
|
||||
*/
|
||||
private function summaryForQuery(Builder $query, array $filters): array
|
||||
{
|
||||
$row = (clone $query)->selectRaw(
|
||||
'COALESCE(SUM(income_amount), 0) AS income_total, '
|
||||
. 'COALESCE(SUM(expense_amount), 0) AS expense_total, COUNT(*) AS row_count'
|
||||
)->first();
|
||||
$income = (float) ($row->income_total ?? 0);
|
||||
$expense = (float) ($row->expense_total ?? 0);
|
||||
$opening = $this->openingBalance($filters);
|
||||
|
||||
return [
|
||||
'income' => $income,
|
||||
'expense' => $expense,
|
||||
'opening_balance' => $opening['balance'],
|
||||
'opening_balance_date' => $opening['date'],
|
||||
'opening_balance_source' => $opening['source'],
|
||||
'balance' => round($opening['balance'] + $income - $expense, 2),
|
||||
'count' => (int) ($row->row_count ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return iterable<int,array<string,mixed>> */
|
||||
public function exportRows(array $filters, string $sort = 'occurred_at', string $direction = 'asc'): iterable
|
||||
{
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -4,6 +4,7 @@ namespace Tests\Feature\Business;
|
||||
|
||||
use App\Models\Auth\Permission;
|
||||
use Mockery;
|
||||
use Modules\EcommerceFinance\Repositories\TaobaoAlipaySettlementBillRepository;
|
||||
use Modules\EcommerceFinance\Repositories\TaobaoWechatSettlementBillRepository;
|
||||
use RuntimeException;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
@ -43,6 +44,18 @@ class TaobaoWechatReceivingBillsTest extends FeatureTestCase
|
||||
]);
|
||||
$this->app->instance(TaobaoWechatSettlementBillRepository::class, $repository);
|
||||
|
||||
$alipayRepository = Mockery::mock(TaobaoAlipaySettlementBillRepository::class);
|
||||
$alipayRepository->shouldReceive('summary')->once()->with($filters)->andReturn([
|
||||
'income' => 320.0,
|
||||
'expense' => 25.0,
|
||||
'opening_balance' => 230.0,
|
||||
'opening_balance_date' => '2026-08-31',
|
||||
'opening_balance_source' => 'previous_period',
|
||||
'balance' => 525.0,
|
||||
'count' => 12,
|
||||
]);
|
||||
$this->app->instance(TaobaoAlipaySettlementBillRepository::class, $alipayRepository);
|
||||
|
||||
$this->withExceptionHandling()
|
||||
->loginAs()
|
||||
->getJson(route('ecommerce-finance.account-bills.index', [
|
||||
@ -54,7 +67,12 @@ class TaobaoWechatReceivingBillsTest extends FeatureTestCase
|
||||
->assertOk()
|
||||
->assertJsonPath('data.0.payload.entry_type', '收入')
|
||||
->assertJsonPath('columns.title', '淘系微信账户')
|
||||
->assertJsonPath('summary.count', 41);
|
||||
->assertJsonPath('summary.count', 41)
|
||||
->assertJsonPath('summary.opening_balance', 230)
|
||||
->assertJsonPath('summary.opening_balance_date', '2026-08-31')
|
||||
->assertJsonPath('summary.opening_balance_source', 'previous_period')
|
||||
->assertJsonPath('summary.balance', 525)
|
||||
->assertJsonPath('summary.balance_source', 'taobao_alipay');
|
||||
}
|
||||
|
||||
public function test_filter_options_and_selected_column_export_use_the_same_mirror(): void
|
||||
|
||||
@ -58,6 +58,13 @@ class TaobaoAlipaySettlementBillRepositoryTest extends TestCase
|
||||
$this->assertSame('2026-09-30', $result['summary']['opening_balance_date']);
|
||||
$this->assertSame('previous_period', $result['summary']['opening_balance_source']);
|
||||
$this->assertSame(275.0, $result['summary']['balance']);
|
||||
$this->assertSame(
|
||||
$result['summary'],
|
||||
app(TaobaoAlipaySettlementBillRepository::class)->summary([
|
||||
'start' => '2026-10-01',
|
||||
'end' => '2026-10-31',
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
public function test_month_balance_respects_store_filter(): void
|
||||
|
||||
@ -82,6 +82,22 @@ class AccountBillsTableTest extends TestCase
|
||||
$this->assertStringContainsString("netLabel=JD_BILL_TABS.concat(['vipshop-jx']).indexOf(this.billType)!==-1", $source);
|
||||
}
|
||||
|
||||
public function test_taobao_wechat_places_alipay_balances_before_income(): void
|
||||
{
|
||||
$source = file_get_contents(__DIR__ . '/../../../resources/assets/js/components/business/AccountBills.vue');
|
||||
|
||||
$this->assertStringContainsString("'tw-stats--taobao-wechat': billType === 'taobao-wechat'", $source);
|
||||
$this->assertStringContainsString("if(this.billType==='taobao-wechat')", $source);
|
||||
$this->assertStringContainsString("label:'期初余额'", $source);
|
||||
$this->assertStringContainsString("label:'期末余额(最新)'", $source);
|
||||
$this->assertStringContainsString("this.summary.balance_source==='unavailable'?'淘系支付宝余额暂不可用':'同淘系支付宝账户'", $source);
|
||||
$this->assertLessThan(
|
||||
strpos($source, "label:'收入合计'"),
|
||||
strpos($source, "label:'期初余额'"),
|
||||
);
|
||||
$this->assertStringContainsString('.tw-stats--taobao-wechat{grid-template-columns:repeat(5,minmax(0,1fr))}', $source);
|
||||
}
|
||||
|
||||
public function test_promotion_marketing_card_opens_the_store_detail_modal(): void
|
||||
{
|
||||
$source = file_get_contents(__DIR__ . '/../../../resources/assets/js/components/business/AccountBills.vue');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user