From e41f9c14c3807cbfbdb5d359f4f73eec8b1fa937 Mon Sep 17 00:00:00 2001 From: chym1217 Date: Fri, 24 Jul 2026 19:21:18 +0900 Subject: [PATCH] =?UTF-8?q?fix(message=5Fbizppurio):=20SMS=20=EB=8C=80?= =?UTF-8?q?=EC=B2=B4=EB=B0=9C=EC=86=A1=20=EA=B2=B0=EA=B3=BC=EA=B0=80=20?= =?UTF-8?q?=EB=AF=B8=EC=9A=94=EC=B2=AD=20=EA=B1=B4=EC=97=90=EB=8F=84=20?= =?UTF-8?q?=EC=9E=98=EB=AA=BB=20=ED=91=9C=EC=8B=9C=EB=90=98=EB=8D=98=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 비즈뿌리오가 대체발송 미요청 건에도 webhook TELRES/KAORES 값을 채워 보내 빈 문자열 검사만으로는 요청 여부를 구분할 수 없었다. 발송 시점에 우리가 실제로 대체발송을 요청했는지(request_payload의 resend 키 존재)를 판단 기준으로 바꿔, 미요청 건은 webhook 값과 무관하게 결과를 표시하지 않는다. --- .../sirsoft-message_bizppurio/CHANGELOG.md | 2 +- .../src/Services/WebhookReportService.php | 16 ++++- .../Feature/Webhook/BizppurioWebhookTest.php | 72 ++++++++++++++++++- 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/plugins/_bundled/sirsoft-message_bizppurio/CHANGELOG.md b/plugins/_bundled/sirsoft-message_bizppurio/CHANGELOG.md index 17684dd5..611c7ec6 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/CHANGELOG.md +++ b/plugins/_bundled/sirsoft-message_bizppurio/CHANGELOG.md @@ -17,7 +17,7 @@ - 비회원 주문 알림도 주문 시 입력한 연락처로 문자를 받을 수 있습니다. - 카카오 알림톡 채널을 알림 설정에 추가했습니다. (알림톡 실제 발송·템플릿 연동은 후속 단계에서 제공됩니다.) - 비즈뿌리오가 보내는 발송 결과 통보를 받아 각 발송의 실제 도달 여부(성공/실패)를 기록합니다. 실패한 경우 사유를 함께 확인할 수 있습니다(예: 음영 지역, 잘못된 번호, 스팸 차단, 지갑 잔액 부족 등). -- 관리자 "알림 발송 이력" 화면에서 문자·알림톡의 발송 결과(성공/실패·사유·지갑 잔액 부족·SMS 대체발송 여부)를 각 발송 행에서 바로 확인할 수 있습니다. 문자·알림톡 탭에서만 결과가 표시되며, 행을 펼치면 상세 결과를 볼 수 있습니다. +- 관리자 "알림 발송 이력" 화면에서 문자·알림톡의 발송 결과(성공/실패·사유·지갑 잔액 부족·SMS 대체발송 여부)를 각 발송 행에서 바로 확인할 수 있습니다. 문자·알림톡 탭에서만 결과가 표시되며, 행을 펼치면 상세 결과를 볼 수 있습니다. 대체발송을 실제로 요청한 건에서만 결과가 표시되어, 사용하지 않은 건에 잘못 표시되지 않습니다. 알림톡은 카카오 승인 템플릿의 실제 발송 내용을 별도로 함께 보여줘, 위에 표시되는 알림 기본 본문과 헷갈리지 않도록 합니다. - 지갑 잔액 부족으로 발송이 실패하면 관리자에게 알립니다. 대량 실패 시 알림이 반복 발송되지 않도록 일정 시간 간격으로 한 번만 안내합니다. - 설정 페이지의 알림톡 템플릿 탭에서 카카오 알림톡 템플릿의 목록·상태·내용을 확인할 수 있습니다. 템플릿 등록·수정·검수·상태변경은 비즈뿌리오 콘솔에서 하며, 이 화면은 목록·상태·내용 조회와 알림 연결을 담당합니다. 콘솔에서 등록한 템플릿도 이 목록에 그대로 표시되고 알림에 연결할 수 있습니다. - 템플릿 목록을 상태(발송가능·검수중·반려·미검수·중지 등) 배지와 함께 보여주고, 상태·이름으로 필터·검색할 수 있습니다. 발송가능(승인) 상태의 템플릿만 알림에 연결해 발송할 수 있습니다. diff --git a/plugins/_bundled/sirsoft-message_bizppurio/src/Services/WebhookReportService.php b/plugins/_bundled/sirsoft-message_bizppurio/src/Services/WebhookReportService.php index 98b26299..0a566668 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/src/Services/WebhookReportService.php +++ b/plugins/_bundled/sirsoft-message_bizppurio/src/Services/WebhookReportService.php @@ -10,6 +10,7 @@ use App\Services\PluginSettingsService; use Plugins\Sirsoft\MessageBizppurio\Concerns\PreventsReplayWebhook; use Plugins\Sirsoft\MessageBizppurio\Enums\DispatchStatus; use Plugins\Sirsoft\MessageBizppurio\Enums\ResultCategory; +use Plugins\Sirsoft\MessageBizppurio\Models\BizppurioDispatch; use Plugins\Sirsoft\MessageBizppurio\Repositories\Contracts\BizppurioDispatchRepositoryInterface; /** @@ -79,7 +80,7 @@ class WebhookReportService 'media' => $report['MEDIA'] ?? null, 'result_code' => $resultCode, 'result_message' => $this->resolver->reason($resultCode), - 'fallback_status' => $this->resolveFallbackStatus($report), + 'fallback_status' => $this->resolveFallbackStatus($report, $dispatch), 'reported_at' => now(), 'raw_payload' => $report['raw'] ?? $report, ]); @@ -131,11 +132,22 @@ class WebhookReportService /** * 대체발송 결과를 해석합니다 (webhook TELRES/KAORES). * + * 비즈뿌리오는 대체발송 미요청 건에도 TELRES/KAORES 에 "0" 같은 값을 채워 보내므로 + * (실측), webhook 응답값만으로는 대체발송 발생 여부를 판단할 수 없다. 우리가 발송 + * 시점에 실제로 대체발송을 요청했는지(`request_payload` 의 resend/recontent 존재)를 + * 먼저 확인해, 요청하지 않았으면 webhook 값과 무관하게 null 을 반환한다. + * * @param array $report 리포트 + * @param BizppurioDispatch $dispatch webhook 매칭된 발송 이력 (발송 시점 요청 payload 보유) * @return string|null 대체발송 결과 코드 또는 null */ - private function resolveFallbackStatus(array $report): ?string + private function resolveFallbackStatus(array $report, BizppurioDispatch $dispatch): ?string { + $requestPayload = $dispatch->request_payload ?? []; + if (! isset($requestPayload['resend'])) { + return null; + } + $telres = $report['TELRES'] ?? null; $kaores = $report['KAORES'] ?? null; diff --git a/plugins/_bundled/sirsoft-message_bizppurio/tests/Feature/Webhook/BizppurioWebhookTest.php b/plugins/_bundled/sirsoft-message_bizppurio/tests/Feature/Webhook/BizppurioWebhookTest.php index 00ad7efb..9d50d5d4 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/tests/Feature/Webhook/BizppurioWebhookTest.php +++ b/plugins/_bundled/sirsoft-message_bizppurio/tests/Feature/Webhook/BizppurioWebhookTest.php @@ -40,9 +40,10 @@ class BizppurioWebhookTest extends PluginTestCase * * @param string $refkey 우리 부여 키 * @param string $channel 채널 + * @param array|null $requestPayload 발송 시점 요청 payload (resend 포함 여부 검증용) * @return BizppurioDispatch */ - private function seedDispatch(string $refkey, string $channel = 'sms'): BizppurioDispatch + private function seedDispatch(string $refkey, string $channel = 'sms', ?array $requestPayload = null): BizppurioDispatch { return BizppurioDispatch::create([ 'refkey' => $refkey, @@ -52,6 +53,7 @@ class BizppurioWebhookTest extends PluginTestCase 'notification_type' => 'welcome', 'status' => DispatchStatus::Sent->value, 'source' => 'auto', + 'request_payload' => $requestPayload, 'sent_at' => now(), ]); } @@ -246,4 +248,72 @@ class BizppurioWebhookTest extends PluginTestCase ->postJson(self::ENDPOINT, ['REFKEY' => 'ref_noauth', 'RESULT' => '4100']) ->assertStatus(200); } + + /** + * 대체발송(SMS 대체발송)을 요청하지 않은 발송건(request_payload 에 resend 없음)은, + * webhook 응답에 TELRES/KAORES 값이 채워져 와도 fallback_status 가 null 로 남아야 한다. + * + * 비즈뿌리오는 대체발송 미요청 건에도 TELRES="0" 같은 값을 채워 보내므로(실서버 관측: + * alimtalk 대체발송 OFF 인데 결과 화면에 SMS 대체발송 뱃지가 표시된 회귀), webhook 값 + * 존재 여부만으로 판단하면 안 되고 우리가 실제로 요청했는지(request_payload)를 봐야 한다. + * + * @scenario source=webhook,ip=allowed,result=success,fallback_requested=false + * + * @effects fallback_status_remains_null_when_not_requested + */ + public function test_fallback_status_stays_null_when_resend_was_not_requested(): void + { + $dispatch = $this->seedDispatch( + 'ref_no_resend_requested', + DispatchChannel::Alimtalk->value, + ['account' => 'sirsoft', 'content' => ['at' => ['templatecode' => 'tpl_1']]], + ); + + $this->withServerVariables(['REMOTE_ADDR' => self::ALLOWED_IP]) + ->postJson(self::ENDPOINT, [ + 'REFKEY' => 'ref_no_resend_requested', + 'RESULT' => '7000', + 'MEDIA' => 'KAT', + 'TELRES' => '0', + 'KAORES' => '0', + ]) + ->assertStatus(200); + + $dispatch->refresh(); + $this->assertSame(DispatchStatus::Success, $dispatch->status); + $this->assertNull($dispatch->fallback_status); + } + + /** + * 대체발송을 실제로 요청한 발송건(request_payload 에 resend 있음)은 webhook TELRES 값을 + * fallback_status 에 정상 반영한다. + * + * @scenario source=webhook,ip=allowed,result=balance_low,fallback_requested=true + * + * @effects fallback_status_set_when_resend_was_requested + */ + public function test_fallback_status_set_when_resend_was_requested(): void + { + $dispatch = $this->seedDispatch( + 'ref_resend_requested', + DispatchChannel::Alimtalk->value, + [ + 'account' => 'sirsoft', + 'content' => ['at' => ['templatecode' => 'tpl_1']], + 'resend' => ['first' => 'sms'], + ], + ); + + $this->withServerVariables(['REMOTE_ADDR' => self::ALLOWED_IP]) + ->postJson(self::ENDPOINT, [ + 'REFKEY' => 'ref_resend_requested', + 'RESULT' => '7436', + 'MEDIA' => 'KAT', + 'TELRES' => '4100', + ]) + ->assertStatus(200); + + $dispatch->refresh(); + $this->assertSame('4100', $dispatch->fallback_status); + } }