CaiWuXiTong/tests/Feature/Business/SupplierReconciliationDingTalkTest.php
renshen ad8da1264d 供应商对账:按采购单号日期条码核对数量金额 (#405)
厂家账单改为按采购单号+日期+条码匹配,逐项比较双方数量、单价和金额,页面与钉钉通知明确展示厂家多算或少算。
2026-10-20 13:44:00 +08:00

289 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Tests\Feature\Business;
use App\Models\Auth\Permission;
use Illuminate\Support\Facades\Http;
use Modules\EcommerceFinance\Services\SupplierDingTalkNotifier;
use Modules\EcommerceFinance\Services\SupplierPayableOverrideStore;
use Modules\EcommerceFinance\Services\SupplierStatementStore;
use Tests\Feature\FeatureTestCase;
class SupplierReconciliationDingTalkTest extends FeatureTestCase
{
private const WEBHOOK = 'https://oapi.dingtalk.com/robot/send?access_token=supplier-test-token';
protected function setUp(): void
{
parent::setUp();
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
}
public function test_finance_detail_shows_push_buttons_and_robot_configuration(): void
{
$this->seedReconciliation();
$this->loginAs()->get($this->pageUrl())
->assertOk()
->assertSeeText('推送全部差异(3 条)')
->assertSeeText('推送选中(')
->assertSee('aria-label="选择全部差异"', false)
->assertSeeText('推送本条')
->assertSeeText('厂家钉钉群机器人')
->assertSeeText('未配置,配置后才能推送差异')
->assertDontSee(self::WEBHOOK);
}
public function test_robot_configuration_is_saved_and_secret_is_not_rendered(): void
{
$this->seedReconciliation();
$this->loginAs()
->post(route('business.supplier-reconciliation.dingtalk-robot.save'), [
'supplier' => '晨丰',
'dingtalk_webhook' => self::WEBHOOK,
'dingtalk_secret' => 'SEC-test-123',
'dingtalk_at_all' => '1',
])
->assertStatus(302)
->assertSessionHas('success', '晨丰 的钉钉群机器人已保存。');
$config = app(SupplierDingTalkNotifier::class)->configuration('晨丰');
$this->assertTrue($config['configured']);
$this->assertTrue($config['at_all']);
$this->assertStringContainsString('access_token=••••••', $config['webhook_hint']);
$this->assertStringNotContainsString('supplier-test-token', (string) setting('supplier_reconciliation.dingtalk_robots'));
$this->assertStringNotContainsString('SEC-test-123', (string) setting('supplier_reconciliation.dingtalk_robots'));
$this->loginAs()->get($this->pageUrl())
->assertOk()
->assertSeeText('已配置')
->assertDontSeeText('supplier-test-token')
->assertDontSeeText('SEC-test-123');
}
public function test_invalid_robot_webhook_is_rejected(): void
{
$this->withExceptionHandling();
$this->loginAs()
->post(route('business.supplier-reconciliation.dingtalk-robot.save'), [
'supplier' => '晨丰',
'dingtalk_webhook' => 'https://example.com/robot/send?access_token=unsafe',
])
->assertStatus(302)
->assertSessionHasErrors('dingtalk_webhook');
}
public function test_push_all_differences_sends_only_current_supplier_and_filtered_rows(): void
{
$this->seedReconciliation();
$this->saveRobot();
Http::fake(['*' => Http::response(['errcode' => 0, 'errmsg' => 'ok'])]);
$this->loginAs()
->post(route('business.supplier-reconciliation.push-differences'), [
'supplier' => '晨丰',
'month' => '2026-09',
'reconciler' => '张三',
'scope' => 'all',
])
->assertStatus(302)
->assertSessionHas('success', '已推送 3 条差异到 晨丰 钉钉群(1 条消息)。');
Http::assertSentCount(1);
Http::assertSent(function ($request): bool {
$text = (string) ($request['markdown']['text'] ?? '');
return str_contains($request->url(), 'oapi.dingtalk.com/robot/send')
&& str_contains($request->url(), 'timestamp=')
&& str_contains($request->url(), 'sign=')
&& str_contains($text, '货款对账差异通知 · 晨丰')
&& str_contains($text, 'B-1')
&& str_contains($text, 'C-1')
&& str_contains($text, 'D-1')
&& str_contains($text, '采购单号:编号-B-1')
&& str_contains($text, '日期:2026-09-30')
&& str_contains($text, '厂家数量少 1')
&& ! str_contains($text, '`A-1`')
&& ! str_contains($text, '鸣飞家具');
});
$config = app(SupplierDingTalkNotifier::class)->configuration('晨丰');
$this->assertNotSame('', $config['last_pushed_at']);
$this->assertSame(3, $config['last_push_count']);
}
public function test_push_single_difference_only_sends_the_selected_row(): void
{
$this->seedReconciliation();
$this->saveRobot();
Http::fake(['*' => Http::response(['errcode' => 0, 'errmsg' => 'ok'])]);
$this->loginAs()
->post(route('business.supplier-reconciliation.push-differences'), [
'supplier' => '晨丰',
'month' => '2026-09',
'reconciler' => '张三',
'scope' => 'row',
'purchase_no' => '编号-B-1',
'date' => '2026-09-30',
'barcode' => 'B-1',
'difference_type' => '数量/金额不一致',
])
->assertStatus(302)
->assertSessionHas('success', '已推送 1 条差异到 晨丰 钉钉群(1 条消息)。');
Http::assertSent(function ($request): bool {
$text = (string) ($request['markdown']['text'] ?? '');
return str_contains($text, 'B-1')
&& ! str_contains($text, '`C-1`')
&& ! str_contains($text, '`D-1`');
});
}
public function test_push_selected_differences_only_sends_checked_rows(): void
{
$this->seedReconciliation();
$this->saveRobot();
Http::fake(['*' => Http::response(['errcode' => 0, 'errmsg' => 'ok'])]);
$this->loginAs()
->post(route('business.supplier-reconciliation.push-differences'), [
'supplier' => '晨丰',
'month' => '2026-09',
'reconciler' => '张三',
'scope' => 'selected',
'selected_rows' => [
json_encode(['purchaseNo' => '编号-B-1', 'date' => '2026-09-30', 'barcode' => 'B-1', 'type' => '数量/金额不一致']),
json_encode(['purchaseNo' => '编号-D-1', 'date' => '2026-09-30', 'barcode' => 'D-1', 'type' => '厂家多']),
],
])
->assertStatus(302)
->assertSessionHas('success', '已推送 2 条差异到 晨丰 钉钉群(1 条消息)。');
Http::assertSent(function ($request): bool {
$text = (string) ($request['markdown']['text'] ?? '');
return str_contains($text, 'B-1')
&& str_contains($text, 'D-1')
&& ! str_contains($text, '`C-1`');
});
}
public function test_push_without_robot_configuration_is_rejected_without_http_call(): void
{
$this->seedReconciliation();
Http::fake();
$this->loginAs()
->post(route('business.supplier-reconciliation.push-differences'), [
'supplier' => '晨丰',
'month' => '2026-09',
'reconciler' => '张三',
'scope' => 'all',
])
->assertStatus(302)
->assertSessionHasErrors('dingtalk_push');
Http::assertNothingSent();
}
public function test_large_difference_set_is_sent_in_multiple_robot_messages(): void
{
$this->saveRobot();
Http::fake(['*' => Http::response(['errcode' => 0, 'errmsg' => 'ok'])]);
$rows = [];
for ($index = 1; $index <= 31; $index++) {
$rows[] = [
'barcode' => sprintf('BULK-%02d', $index),
'supplierBarcode' => '',
'purchaseNo' => sprintf('PO-BULK-%02d', $index),
'date' => '2026-09-30',
'unitPrice' => 10,
'mineUnitPrice' => 10,
'theirsUnitPrice' => 10,
'mineQty' => 2,
'theirsQty' => 1,
'mineAmount' => 20,
'theirsAmount' => 10,
'qtyDifference' => -1,
'amountDifference' => -10,
'type' => '数量/金额不一致',
'settleAmount' => 10,
'note' => '厂家数量少 1;按金额低的一方结算',
];
}
$result = app(SupplierDingTalkNotifier::class)->push('晨丰', $rows, [
'month' => '2026-09',
'reconciler' => '张三',
]);
$this->assertSame(31, $result['rows']);
$this->assertSame(2, $result['messages']);
Http::assertSentCount(2);
}
private function seedReconciliation(): void
{
$companyId = (int) $this->company->id;
$store = app(SupplierPayableOverrideStore::class);
foreach ([
'mine:1' => ['skuCode' => 'A-1', 'qty' => '2', 'unitPrice' => '100', 'amount' => '200'],
'mine:2' => ['skuCode' => 'B-1', 'qty' => '2', 'unitPrice' => '50', 'amount' => '100'],
'mine:3' => ['skuCode' => 'C-1', 'qty' => '1', 'unitPrice' => '30', 'amount' => '30'],
] as $key => $row) {
$store->save('final:supplier:payment-reconciliation', $key, $row + [
'summary_month' => '2026-09',
'purchaseNo' => '编号-' . $row['skuCode'],
'date' => '2026-09-30',
'supplier' => '晨丰',
'reconciler' => '张三',
], $companyId, null);
}
app(SupplierStatementStore::class)->save('晨丰', [
$this->statementRow('A-1', '100', '2', '200'),
$this->statementRow('B-1', '50', '1', '50'),
$this->statementRow('D-1', '80', '1', '80'),
], 'statement.csv', $companyId, null);
}
private function saveRobot(): void
{
app(SupplierDingTalkNotifier::class)->save('晨丰', self::WEBHOOK, 'SEC-test-123', false);
}
private function pageUrl(): string
{
return route('business.pages.show', [
'section' => 'supplier-reconciliation',
'page' => 'finance-data',
'supplier' => '晨丰',
'month' => '2026-09',
'reconciler' => '张三',
]);
}
private function statementRow(string $barcode, string $unitPrice, string $qty, string $amount): array
{
return [
'expressName' => null,
'expressNo' => null,
'purchaseNo' => '编号-' . $barcode,
'createdAt' => '2026-09-30',
'supplier' => '晨丰',
'barcode' => $barcode,
'skuName' => '规格甲',
'qty' => $qty,
'unitPrice' => $unitPrice,
'amount' => $amount,
];
}
}