diff --git a/app/Http/Controllers/Business/SupplierReconciliation.php b/app/Http/Controllers/Business/SupplierReconciliation.php index 4e61fdcc..a1b5d739 100644 --- a/app/Http/Controllers/Business/SupplierReconciliation.php +++ b/app/Http/Controllers/Business/SupplierReconciliation.php @@ -224,11 +224,15 @@ class SupplierReconciliation extends Controller ]); } + $channel = trim((string) config('dingtalk.summary_confirmation_mode', 'link')) === 'card' + ? '确认卡片' + : '确认链接'; + return back()->withInput([ 'push_basis' => $pushBasis, ])->with( 'success', - "已将 {$result['period']} 对账确认卡片推送到 {$supplier} 钉钉群,等待厂家确认。", + "已将 {$result['period']} 对账{$channel}推送到 {$supplier} 钉钉群,等待厂家确认。", ); } diff --git a/app/Http/Controllers/Business/SupplierSummaryConfirmationShare.php b/app/Http/Controllers/Business/SupplierSummaryConfirmationShare.php new file mode 100644 index 00000000..074bc13b --- /dev/null +++ b/app/Http/Controllers/Business/SupplierSummaryConfirmationShare.php @@ -0,0 +1,150 @@ +resolveRecord($request, $token); + $expiresAt = $this->expiresAt($request); + + return response() + ->view('business.pages.supplier-reconciliation.summary-confirmation-share', [ + 'supplier' => $target['supplier'], + 'month' => $target['month'], + 'monthLabel' => $this->displayMonth($target['month']), + 'qty' => (float) ($target['record']['qty'] ?? 0), + 'amount' => (float) ($target['record']['amount'] ?? 0), + 'status' => (string) ($target['record']['status'] ?? ''), + 'confirmedAt' => (string) ($target['record']['confirmed_at'] ?? ''), + 'expiresAt' => $expiresAt, + 'confirmUrl' => $this->shareLinks->signedRoute( + 'signed.supplier-summary-confirmations.confirm', + $token, + $target['companyId'], + $expiresAt, + ), + ]) + ->withHeaders([ + 'Cache-Control' => 'private, no-store, max-age=0', + 'X-Robots-Tag' => 'noindex, nofollow, noarchive', + 'Referrer-Policy' => 'no-referrer', + ]); + } + + public function confirm(Request $request, string $token): RedirectResponse + { + $target = $this->resolveRecord($request, $token); + if (($target['record']['status'] ?? '') === SupplierSummaryConfirmation::STATUS_CONFIRMED) { + return $this->redirectToPage($request, $token, $target) + ->with('success', '本期货款已经确认,无需重复操作。'); + } + + $result = $this->confirmations->confirm( + $target['request_id'], + $target['companyId'], + null, + '', + 'manufacturer_link', + ); + + if ($result['newly_confirmed']) { + try { + $this->notifier->sendSummaryConfirmationReceipt($result['record']); + } catch (Throwable $exception) { + report($exception); + } + } + + return $this->redirectToPage($request, $token, $target) + ->with( + 'success', + $result['newly_confirmed'] + ? '已确认本期货款,财务系统已实时更新。' + : '本期货款已经确认,无需重复操作。', + ); + } + + /** + * @return array{ + * supplier:string, + * month:string, + * push_basis:string, + * request_id:string, + * companyId:int, + * record:array + * } + */ + private function resolveRecord(Request $request, string $token): array + { + $companyId = (int) ($request->route('company_id') ?: company_id() ?: 1); + $payload = $this->shareLinks->resolveToken($token, $companyId); + abort_if($payload === null, 404, '该货款确认链接无效或已过期。'); + + $record = $this->confirmations->find($payload['request_id'], $companyId); + abort_if($record === null, 404, '该货款确认记录不存在,请联系财务重新发送。'); + abort_unless( + trim((string) ($record['supplier'] ?? '')) === $payload['supplier'] + && trim((string) ($record['month'] ?? '')) === $payload['month'] + && trim((string) ($record['push_basis'] ?? '')) === $payload['push_basis'], + 404, + '该货款确认记录与链接不一致。', + ); + + return [ + ...$payload, + 'companyId' => $companyId, + 'record' => $record, + ]; + } + + /** @param array $target */ + private function redirectToPage(Request $request, string $token, array $target): RedirectResponse + { + $pageUrl = $this->shareLinks->signedRoute( + 'signed.supplier-summary-confirmations.show', + $token, + $target['companyId'], + $this->expiresAt($request), + ); + + return redirect()->to($pageUrl); + } + + private function expiresAt(Request $request): CarbonImmutable + { + $expires = (int) $request->query('expires', 0); + + return $expires > 0 + ? CarbonImmutable::createFromTimestamp($expires) + : now()->addDays(SupplierSummaryShareLink::TTL_DAYS); + } + + private function displayMonth(string $month): string + { + [$year, $monthNumber] = explode('-', $month, 2); + + return (int) $year . '年' . (int) $monthNumber . '月'; + } +} diff --git a/config/dingtalk.php b/config/dingtalk.php index 2bdd0158..efd756b4 100644 --- a/config/dingtalk.php +++ b/config/dingtalk.php @@ -60,6 +60,9 @@ return [ 'robot_code' => env('DINGTALK_CARD_ROBOT_CODE'), ], + // card:钉钉互动卡片内直接确认;link:厂家打开签名确认页后点击确认。 + 'summary_confirmation_mode' => env('DINGTALK_SUMMARY_CONFIRMATION_MODE', 'link'), + 'worklog' => [ 'enabled' => filter_var(env('DINGTALK_WORKLOG_ENABLED', true), FILTER_VALIDATE_BOOL), 'template_name' => env('DINGTALK_WORKLOG_TEMPLATE_NAME', '财务日报'), diff --git a/modules/EcommerceFinance/Services/SupplierDingTalkNotifier.php b/modules/EcommerceFinance/Services/SupplierDingTalkNotifier.php index bb254fc6..dec48229 100644 --- a/modules/EcommerceFinance/Services/SupplierDingTalkNotifier.php +++ b/modules/EcommerceFinance/Services/SupplierDingTalkNotifier.php @@ -22,6 +22,8 @@ class SupplierDingTalkNotifier public function __construct( private readonly SupplierReturnShareLink $returnShareLinks, private readonly SupplierSummaryAICard $summaryAICard, + private readonly SupplierSummaryConfirmation $summaryConfirmations, + private readonly SupplierSummaryShareLink $summaryShareLinks, ) { } @@ -222,7 +224,7 @@ class SupplierDingTalkNotifier /** * 推送当前账期的原始件数和货款汇总。 * - * @return array{messages:int,qty:float,amount:float,period:string} + * @return array{messages:int,qty:float,amount:float,period:string,request_id?:string,confirmation_url?:string} */ public function pushReconciliationSummary( string $supplier, @@ -249,7 +251,8 @@ class SupplierDingTalkNotifier } $this->assertWebhook($webhook); - if (config('dingtalk.card.enabled', true)) { + $mode = trim((string) config('dingtalk.summary_confirmation_mode', 'link')); + if ($mode === 'card' && config('dingtalk.card.enabled', true)) { $result = $this->summaryAICard->push( $supplier, $month, @@ -260,6 +263,7 @@ class SupplierDingTalkNotifier $userId, trim((string) ($config['card_conversation_id'] ?? '')), trim((string) ($config['card_robot_code'] ?? '')), + trim((string) ($config['card_robot_name'] ?? '')), ); $config['last_pushed_at'] = $this->platformTimestamp(); $config['last_push_count'] = 1; @@ -270,16 +274,51 @@ class SupplierDingTalkNotifier } $period = $this->displayMonth($month); - $this->send( - $webhook, - trim((string) ($config['secret'] ?? '')), - (bool) ($config['at_all'] ?? false), - $supplier . ' · 货款对账汇总 · ' . $period, - '账期:' . $period - . ',件数' . $this->number($qty) - . '件,货款' . number_format($amount, 2, '.', '') - . '元', + $resolvedCompanyId = $companyId > 0 ? $companyId : (int) (company_id() ?: 1); + $resolvedBasis = in_array($pushBasis, ['settlement', 'mine'], true) ? $pushBasis : 'settlement'; + $requestId = 'supplier-summary-l' . $resolvedCompanyId . '-' . bin2hex(random_bytes(16)); + $this->summaryConfirmations->create( + $requestId, + $supplier, + $month, + $resolvedBasis, + $qty, + $amount, + $resolvedCompanyId, + $userId, ); + $confirmationUrl = $this->summaryShareLinks->pageUrl( + $resolvedCompanyId, + $supplier, + $month, + $resolvedBasis, + $requestId, + ); + try { + $this->send( + $webhook, + trim((string) ($config['secret'] ?? '')), + (bool) ($config['at_all'] ?? false), + $supplier . ' · 货款对账确认 · ' . $period, + implode("\n", [ + '### 供应商货款对账确认 · ' . $supplier, + '- 账期:**' . $period . '**', + '- 件数:**' . $this->number($qty) . '** 件', + '- 货款:**¥' . number_format($amount, 2) . '**', + '', + '#### [打开货款确认页](' . $confirmationUrl . ')', + '- 打开后核对数据,点击“确认本期货款”。', + '- 确认结果会实时回传财务系统。', + '', + '> 无需登录;链接仅可查看本厂家本次账期,有效期 ' + . SupplierSummaryShareLink::TTL_DAYS . ' 天。', + ]), + ); + $this->summaryConfirmations->markSent($requestId, $resolvedCompanyId); + } catch (Throwable $exception) { + $this->summaryConfirmations->markFailed($requestId, $resolvedCompanyId, $exception->getMessage()); + throw $exception; + } $config['last_pushed_at'] = $this->platformTimestamp(); $config['last_push_count'] = 1; @@ -291,9 +330,48 @@ class SupplierDingTalkNotifier 'qty' => round($qty, 4), 'amount' => round($amount, 2), 'period' => $period, + 'request_id' => $requestId, + 'confirmation_url' => $confirmationUrl, ]; } + /** @param array $record */ + public function sendSummaryConfirmationReceipt(array $record): bool + { + $supplier = trim((string) ($record['supplier'] ?? '')); + if ($supplier === '') { + return false; + } + + $configs = $this->configs(); + $config = is_array($configs[$supplier] ?? null) ? $configs[$supplier] : []; + $webhook = trim((string) ($config['webhook'] ?? '')); + if ($webhook === '') { + return false; + } + $this->assertWebhook($webhook); + + $month = trim((string) ($record['month'] ?? '')); + $period = preg_match('/^\d{4}-\d{2}$/', $month) === 1 ? $this->displayMonth($month) : $month; + $this->send( + $webhook, + trim((string) ($config['secret'] ?? '')), + (bool) ($config['at_all'] ?? false), + $supplier . ' · 货款确认回执 · ' . $period, + implode("\n", [ + '### 供应商货款确认回执 · ' . $supplier, + '- 账期:**' . $period . '**', + '- 本次确认:**' . $this->number((float) ($record['qty'] ?? 0)) . '** 件,**¥' + . number_format((float) ($record['amount'] ?? 0), 2) . '**', + '- 确认方式:厂家通过货款确认页点击确认', + '- 确认时间:' . (string) ($record['confirmed_at'] ?? $this->platformTimestamp()), + '- 系统状态:**已实时回传财务系统**', + ]), + ); + + return true; + } + /** * @param list> $returns * @return array{messages:int,rows:int} diff --git a/modules/EcommerceFinance/Services/SupplierSummaryAICard.php b/modules/EcommerceFinance/Services/SupplierSummaryAICard.php index 4d1661dd..22e39823 100644 --- a/modules/EcommerceFinance/Services/SupplierSummaryAICard.php +++ b/modules/EcommerceFinance/Services/SupplierSummaryAICard.php @@ -83,7 +83,6 @@ class SupplierSummaryAICard $period, $qty, $amount, - $pushBasis, ); $this->confirmations->markSent($outTrackId, $companyId); } catch (\Throwable $exception) { @@ -294,13 +293,10 @@ class SupplierSummaryAICard string $period, float $qty, float $amount, - string $pushBasis, ): void { - $basisLabel = $pushBasis === 'mine' ? '我方原始数据' : '结算口径'; $summary = "### 货款对账确认\n\n" . '- 厂家:**' . $this->markdown($supplier) . "**\n" . '- 账期:**' . $period . "**\n" - . '- 口径:' . $basisLabel . "\n" . '- 件数:**' . $this->number($qty) . "** 件\n" . '- 货款:**¥' . number_format($amount, 2) . "**\n\n" . '核对无误后,请直接点击下方“厂家确认”。'; diff --git a/modules/EcommerceFinance/Services/SupplierSummaryConfirmation.php b/modules/EcommerceFinance/Services/SupplierSummaryConfirmation.php index 466a649d..2adcebe1 100644 --- a/modules/EcommerceFinance/Services/SupplierSummaryConfirmation.php +++ b/modules/EcommerceFinance/Services/SupplierSummaryConfirmation.php @@ -73,8 +73,9 @@ class SupplierSummaryConfirmation public function confirm( string $outTrackId, int $companyId, - string $dingTalkUserId, + ?string $dingTalkUserId, string $corpId = '', + string $source = 'dingtalk_card', ): array { $record = $this->find($outTrackId, $companyId); if ($record === null) { @@ -85,13 +86,19 @@ class SupplierSummaryConfirmation return ['record' => $record, 'newly_confirmed' => false]; } - $confirmed = array_merge($record, [ + $changes = [ 'status' => self::STATUS_CONFIRMED, 'confirmed_at' => $this->timestamp(), - 'confirmed_by_dingtalk_user_id' => $dingTalkUserId, - 'dingtalk_corp_id' => $corpId, - 'confirmed_source' => 'dingtalk_card', - ]); + 'confirmed_source' => $source, + ]; + if ($dingTalkUserId !== null && trim($dingTalkUserId) !== '') { + $changes['confirmed_by_dingtalk_user_id'] = trim($dingTalkUserId); + } + if ($corpId !== '') { + $changes['dingtalk_corp_id'] = $corpId; + } + + $confirmed = array_merge($record, $changes); unset($confirmed['_action_key']); $this->store->save( self::DATASET, diff --git a/modules/EcommerceFinance/Services/SupplierSummaryShareLink.php b/modules/EcommerceFinance/Services/SupplierSummaryShareLink.php new file mode 100644 index 00000000..d6206353 --- /dev/null +++ b/modules/EcommerceFinance/Services/SupplierSummaryShareLink.php @@ -0,0 +1,103 @@ +signedRoute( + 'signed.supplier-summary-confirmations.show', + $this->token($companyId, $supplier, $month, $pushBasis, $requestId), + $companyId, + now()->addDays(self::TTL_DAYS), + ); + } + + public function signedRoute( + string $routeName, + string $token, + int $companyId, + ?DateTimeInterface $expires = null, + ): string { + return URL::temporarySignedRoute($routeName, $expires ?? now()->addDays(self::TTL_DAYS), [ + 'company_id' => $companyId, + 'token' => $token, + ]); + } + + /** @return array{supplier:string,month:string,push_basis:string,request_id:string}|null */ + public function resolveToken(string $token, int $companyId): ?array + { + try { + $padding = (4 - strlen($token) % 4) % 4; + $encoded = strtr($token . str_repeat('=', $padding), '-_', '+/'); + $encrypted = base64_decode($encoded, true); + if ($encrypted === false) { + return null; + } + + $payload = json_decode(Crypt::decryptString($encrypted), true, flags: JSON_THROW_ON_ERROR); + } catch (Throwable) { + return null; + } + + $supplier = is_array($payload) ? trim((string) ($payload['supplier'] ?? '')) : ''; + $month = is_array($payload) ? trim((string) ($payload['month'] ?? '')) : ''; + $pushBasis = is_array($payload) ? trim((string) ($payload['push_basis'] ?? '')) : ''; + $requestId = is_array($payload) ? trim((string) ($payload['request_id'] ?? '')) : ''; + $payloadCompanyId = is_array($payload) ? (int) ($payload['company_id'] ?? 0) : 0; + if ( + $supplier === '' + || $payloadCompanyId !== $companyId + || preg_match('/^\d{4}-\d{2}$/', $month) !== 1 + || ! in_array($pushBasis, ['settlement', 'mine'], true) + || preg_match('/^supplier-summary-l\d+-[a-f0-9]{16,128}$/', $requestId) !== 1 + ) { + return null; + } + + return [ + 'supplier' => $supplier, + 'month' => $month, + 'push_basis' => $pushBasis, + 'request_id' => $requestId, + ]; + } + + private function token( + int $companyId, + string $supplier, + string $month, + string $pushBasis, + string $requestId, + ): string { + $payload = json_encode([ + 'company_id' => $companyId, + 'supplier' => $supplier, + 'month' => $month, + 'push_basis' => $pushBasis, + 'request_id' => $requestId, + ], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR); + + return rtrim(strtr(base64_encode(Crypt::encryptString($payload)), '+/', '-_'), '='); + } +} diff --git a/resources/views/business/pages/supplier-reconciliation/finance-data.blade.php b/resources/views/business/pages/supplier-reconciliation/finance-data.blade.php index d1409889..b0fa220b 100644 --- a/resources/views/business/pages/supplier-reconciliation/finance-data.blade.php +++ b/resources/views/business/pages/supplier-reconciliation/finance-data.blade.php @@ -1,4 +1,7 @@
+ @php + $summaryConfirmationMode = config('dingtalk.summary_confirmation_mode', 'link') === 'card' ? 'card' : 'link'; + @endphp + +
+
+
+
+ @if (is_string(session('success')) && session('success') !== '') +
{{ session('success') }}
+ @endif + @if ($errors->any()) + + @endif + +
+
+

供应商货款对账

+

{{ $supplier }} · {{ $monthLabel }}

+

请核对本期货款,确认无误后提交。

+
+ + + {{ $isConfirmed ? '厂家已确认' : '待厂家确认' }} + +
+ +
+
+ 本期件数 + {{ rtrim(rtrim(number_format($qty, 2, '.', ''), '0'), '.') }} 件 +
+
+ 应付货款 + ¥{{ number_format($amount, 2) }} +
+
+ +
+
供应商{{ $supplier }}
+
账期{{ $monthLabel }}
+
+ + @if ($isConfirmed) +
+ 本期货款已确认 + 财务系统已实时收到确认结果,无需重复操作。 + @if ($confirmedAt !== '') + 确认时间:{{ $confirmedAt }} + @endif +
+ @else +
+ @csrf + +

+ 点击即代表厂家确认本页数据无误。如数量或金额有差异,请先联系财务,不要点击确认。 +

+
+ @endif +
+
+ +
+ 无需登录,仅可查看本厂家本次账期。 + 链接有效至 {{ $expiresAt->format('Y-m-d H:i') }} +
+
+ + + +@push('head_end') + + +@endpush diff --git a/routes/signed.php b/routes/signed.php index 1756f781..4aa4106e 100644 --- a/routes/signed.php +++ b/routes/signed.php @@ -35,3 +35,11 @@ Route::get('supplier-returns/{token}/excel', 'Business\SupplierReturnShare@downl Route::get('supplier-returns/{token}/csv', 'Business\SupplierReturnShare@downloadCsv') ->middleware('throttle:20,1') ->name('signed.supplier-returns.csv'); + +Route::get('supplier-summary-confirmations/{token}', 'Business\SupplierSummaryConfirmationShare@show') + ->middleware('throttle:60,1') + ->name('signed.supplier-summary-confirmations.show'); + +Route::post('supplier-summary-confirmations/{token}/confirm', 'Business\SupplierSummaryConfirmationShare@confirm') + ->middleware('throttle:10,1') + ->name('signed.supplier-summary-confirmations.confirm'); diff --git a/tests/Feature/Business/SupplierReconciliationDingTalkTest.php b/tests/Feature/Business/SupplierReconciliationDingTalkTest.php index 4d6150da..9fa58c2c 100644 --- a/tests/Feature/Business/SupplierReconciliationDingTalkTest.php +++ b/tests/Feature/Business/SupplierReconciliationDingTalkTest.php @@ -392,7 +392,7 @@ class SupplierReconciliationDingTalkTest extends FeatureTestCase $components = (array) (json_decode((string) ($messages[2] ?? '[]'), true)['updateComponents']['components'] ?? []); $summary = collect($components)->firstWhere('id', 'summary'); - return str_contains((string) ($summary['content'] ?? ''), '口径:我方原始数据') + return ! str_contains((string) ($summary['content'] ?? ''), '口径') && str_contains((string) ($summary['content'] ?? ''), '件数:**5** 件') && str_contains((string) ($summary['content'] ?? ''), '货款:**¥330.00**'); }); @@ -402,6 +402,7 @@ class SupplierReconciliationDingTalkTest extends FeatureTestCase { $this->seedReconciliation(); $this->saveRobot(); + config(['dingtalk.summary_confirmation_mode' => 'card']); Http::fake(); $this->loginAs() @@ -1478,6 +1479,7 @@ class SupplierReconciliationDingTalkTest extends FeatureTestCase 'dingtalk.client_id' => 'card-test-client', 'dingtalk.client_secret' => 'card-test-secret', 'dingtalk.card.enabled' => true, + 'dingtalk.summary_confirmation_mode' => 'card', 'dingtalk.card.callback_url' => 'https://caiwu.example.test/webhooks/dingtalk/card/supplier-confirmation', 'dingtalk.card.callback_route_key' => 'supplier-summary-test-route', 'dingtalk.card.robot_code' => null, diff --git a/tests/Feature/Business/SupplierSummaryConfirmationLinkTest.php b/tests/Feature/Business/SupplierSummaryConfirmationLinkTest.php new file mode 100644 index 00000000..7a1146db --- /dev/null +++ b/tests/Feature/Business/SupplierSummaryConfirmationLinkTest.php @@ -0,0 +1,157 @@ +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('