CaiWuXiTong/tests/Feature/Business/SupplierSummaryConfirmationLinkTest.php
苏木 770ddea8a9 feat(供应商对账): 增加免登录送货确认链接 (#1180)
Co-authored-by: 苏木 <594326199@qq.com>
Co-committed-by: 苏木 <594326199@qq.com>
2026-11-15 11:36:27 +08:00

158 lines
6.4 KiB
PHP

<?php
namespace Tests\Feature\Business;
use App\Models\Auth\Permission;
use Illuminate\Routing\Exceptions\InvalidSignatureException;
use Illuminate\Support\Facades\Http;
use Modules\EcommerceFinance\Services\SupplierDingTalkNotifier;
use Modules\EcommerceFinance\Services\SupplierSummaryConfirmation;
use Modules\EcommerceFinance\Services\SupplierSummaryShareLink;
use Tests\Feature\FeatureTestCase;
class SupplierSummaryConfirmationLinkTest extends FeatureTestCase
{
private const WEBHOOK = 'https://oapi.dingtalk.com/robot/send?access_token=summary-link-test-token';
protected function setUp(): void
{
parent::setUp();
$this->company->makeCurrent(true);
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
config([
'dingtalk.summary_confirmation_mode' => 'link',
'app.platform_timezone' => 'Asia/Shanghai',
]);
}
public function test_summary_push_creates_a_signed_manufacturer_confirmation_page_and_records_confirmation(): void
{
$companyId = (int) $this->company->id;
app(SupplierDingTalkNotifier::class)->save(
'常州富来居',
self::WEBHOOK,
'SEC-summary-link-test',
false,
);
Http::fake(['*' => Http::response(['errcode' => 0, 'errmsg' => 'ok'])]);
$result = app(SupplierDingTalkNotifier::class)->pushReconciliationSummary(
'常州富来居',
'2026-10',
855,
103420.50,
$companyId,
'settlement',
(int) $this->user->id,
);
$pageUrl = (string) $result['confirmation_url'];
$sentRequest = collect(Http::recorded())->first()[0] ?? null;
$this->assertNotNull($sentRequest);
$sentText = (string) ($sentRequest['markdown']['text'] ?? '');
$this->assertStringContainsString('oapi.dingtalk.com/robot/send', $sentRequest->url());
$this->assertStringContainsString('件数:**855** 件', $sentText);
$this->assertStringContainsString('货款:**¥103,420.50**', $sentText);
$this->assertStringNotContainsString('确认口径', $sentText);
$this->assertStringContainsString('[打开货款确认页](' . $pageUrl . ')', $sentText);
$this->assertStringContainsString('无需登录', $sentText);
$page = $this->get($pageUrl)
->assertOk()
->assertHeader('X-Robots-Tag', 'noindex, nofollow, noarchive')
->assertHeader('Referrer-Policy', 'no-referrer')
->assertSeeText('常州富来居 · 2026年10月')
->assertSeeText('855 件')
->assertSeeText('¥103,420.50')
->assertDontSeeText('确认口径')
->assertDontSeeText('按结算口径')
->assertSeeText('待厂家确认')
->assertSeeText('确认本期货款')
->assertSeeText('无需登录');
$html = (string) $page->getContent();
preg_match('/action="([^"]*supplier-summary-confirmations[^"]*\/confirm[^"]*)"/', $html, $matches);
$this->assertNotEmpty($matches);
$confirmUrl = html_entity_decode($matches[1], ENT_QUOTES | ENT_HTML5, 'UTF-8');
$redirect = $this->post($confirmUrl)
->assertStatus(302)
->assertSessionHas('success', '已确认本期货款,财务系统已实时更新。');
$record = app(SupplierSummaryConfirmation::class)->find($result['request_id'], $companyId);
$this->assertSame(SupplierSummaryConfirmation::STATUS_CONFIRMED, $record['status'] ?? null);
$this->assertSame('manufacturer_link', $record['confirmed_source'] ?? null);
$this->assertNotEmpty($record['confirmed_at'] ?? null);
$this->assertArrayNotHasKey('confirmed_by_dingtalk_user_id', $record);
$receiptRequest = collect(Http::recorded())->last()[0] ?? null;
$this->assertNotNull($receiptRequest);
$receiptText = (string) ($receiptRequest['markdown']['text'] ?? '');
$this->assertStringContainsString('供应商货款确认回执 · 常州富来居', $receiptText);
$this->assertStringContainsString('855** 件', $receiptText);
$this->assertStringContainsString('¥103,420.50', $receiptText);
$this->assertStringContainsString('厂家通过货款确认页点击确认', $receiptText);
$this->get($redirect->headers->get('Location'))
->assertOk()
->assertSeeText('本期货款已确认')
->assertSeeText('厂家已确认')
->assertSeeText('财务系统已实时收到确认结果')
->assertDontSee('<button type="submit" class="summary-confirm-button">', false);
$this->loginAs()->getJson(route('business.supplier-reconciliation.summary-confirmation.status', [
'supplier' => '常州富来居',
'month' => '2026-10',
'push_basis' => 'settlement',
]))->assertOk()->assertJson([
'status' => 'confirmed',
'label' => '厂家已确认',
]);
}
public function test_summary_confirmation_page_rejects_a_tampered_token(): void
{
$companyId = (int) $this->company->id;
$requestId = $this->recordPendingSummary($companyId, '晨丰');
$url = app(SupplierSummaryShareLink::class)->pageUrl(
$companyId,
'晨丰',
'2026-10',
'mine',
$requestId,
);
$tampered = preg_replace_callback(
'/supplier-summary-confirmations\/([^?\/]+)/',
static fn (array $matches): string => 'supplier-summary-confirmations/'
. ($matches[1][0] === 'A' ? 'B' : 'A') . substr($matches[1], 1),
$url,
1,
);
$this->assertNotSame($url, $tampered);
$this->expectException(InvalidSignatureException::class);
$this->get($tampered);
}
private function recordPendingSummary(int $companyId, string $supplier): string
{
$requestId = 'supplier-summary-l' . $companyId . '-' . str_repeat('a', 32);
app(SupplierSummaryConfirmation::class)->create(
$requestId,
$supplier,
'2026-10',
'mine',
12,
360.50,
$companyId,
null,
);
app(SupplierSummaryConfirmation::class)->markSent($requestId, $companyId);
return $requestId;
}
}