가짜 송장 의심으로 부과된 건의 이의 문의가 왔다. 실제로 발송했다는 주장이어서 부과 로직을 봤다.
Table of contents
Open Table of contents
상황 — 실제를 안 보는 조건 넷
커맨드는 이것이었다.
// AddPenaltyRevisionForFakeInvoice
조건이 넷 있었다.
① 상품주문 상태 = 출고완료
② 채널이 예외 목록에 없음
③ 입점사가 예외 목록에 없음
④ 출고 레코드 생성일이 영업일 기준 6일 이전
AddPenaltyRevisionForFakeInvoice 의 조건 넷 어디에도 실제 배송 여부를 보는 것이 없다.
출고완료 상태에 오래 머물러 있음
↓
가짜 송장 의심
출고완료 에 머문 시간만으로 판정하는 구조였다.
조건 ④의 출고 레코드 생성일 이 체류를 재는 기준이 되는데 그 값이 실제 출고 시점과 다르면 판정도 함께 어긋난다.
왜 체류로 잡나
실제 배송을 확인하려면 택배사 추적을 봐야 한다.
[추적 가능] 확인 후 판정
[추적 안 됨] 판정 못 함
가짜 송장이면 추적 자체가 안 될 가능성이 높다.
번호만 만들고 실제로는 안 보내는 것이라 택배사 쪽에 기록이 안 생긴다. 확인할 자료가 없다는 것이 곧 그 건의 성격인 셈이다.
추적으로 판정하면 정보가 없는 동안 아무 판단도 못 하고 가짜일수록 더 오래 못 잡는다. 잡으려는 대상이 그 방식에서 가장 잘 빠져나간다.
정상 배송 → 며칠 안에 배송완료로 넘어감
가짜 송장 → 출고완료에 머물러 있음
그래서 역으로 배송완료 로 안 넘어가는 것을 잡는다. 확인할 수 없는 것을 확인하는 대신 확인할 수 있는 것으로 대신 판정하는 방식이다.
대응 — 먼저 걸고 나중에 푸는 순서
이 방식은 실제로 보낸 건까지 걸린다는 것을 전제한다.
실제로 보냈는데
↓
추적 연동 문제로 상태가 안 넘어감
↓
부과됨
추적 연동이 늦으면 우리 쪽 사정으로 출고완료 에 남아 부과된다.
정상 발송인데 배송완료 로만 안 넘어간 건이 여기 걸린다. 이 오탐을 줄이는 대신 가짜를 놓치지 않는 쪽을 고른 것이다.
그래서 철회 처리가 붙어 있었다.
배송 추적에서 배송완료가 확인되면
↓
해당 패널티 자동 제거
나중에 배송완료 가 추적에서 확인되면 그 패널티를 스스로 푼다.
[확인 후 부과] 가짜 송장은 영영 확인 안 됨 → 부과 못 함
[먼저 부과] 가짜는 남고 정상은 풀림
목적이 가짜를 잡는 것이면 이 순서가 맞고 놓치는 비용이 잘못 거는 비용보다 크다고 본 설계다.
이의 대응이 달라졌다
구조를 알고 나니 물어볼 것이 바뀌었다.
[전] "실제로 발송했나"를 확인 — 증빙 요청
[후] "왜 상태가 안 넘어갔나"를 확인
실제로 발송했는지는 이 판정과 무관한 물음이었다.
부과 조건 넷 어디에도 발송 여부가 없으니 증빙을 받아도 판정이 안 바뀐다. 물어야 할 것은 출고완료 에서 왜 안 넘어갔느냐였다.
봐야 할 것이 둘로 좁혀졌다.
① 상태가 왜 출고완료에 머물러 있나
② 출고 레코드의 생성일이 언제인가
둘째인 출고 레코드 생성일 이 이번 건의 답이었고 그 값이 실제 출고와 달랐다.
원인 — 두 설계가 만나는 자리
교환 재출고 건은 새 행을 만들지 않고 기존 행을 고치므로 출고 레코드 생성일 이 원본 출고일로 남는다.
원본 출고 3월 2일
재발송 3월 14일
생성일 3월 2일 (그대로)
↓
6영업일 계산 → 이미 초과
재발송 직후인데 6영업일 이 이미 지난 것으로 계산된다.
출고 레코드 생성일 이 최근 동작 시점을 안 가리키기 때문인데 실제로는 3월 14일에 나갔지만 기준은 3월 2일에 머물러 있다.
[덮어쓰기 설계] 생성일이 최근 동작 시점이 아님
[체류 판정] 생성일 기준으로 경과를 잼
덮어쓰기 설계 는 행 수를 줄이려는 선택이고 체류 판정 은 확인할 수 없는 것을 대신 판정하려는 선택이다.
각각만 보면 둘 다 합리적이고 둘이 만나는 자리에서만 오탐이 생기므로 나란히 놓고 봐야 보인다.
제약 — 코드에 박힌 예외 목록
조건 둘과 셋을 보니 목록이 코드에 있었다.
->whereNotIn('channel', ['채널A', '채널B', '채널C', '채널D'])
->whereNotIn('provider_id', [특정 값들])
whereNotIn 목록에 한 줄을 넣으려면 코드를 고치고 배포해야 한다.
운영이 판단하는 값이 개발 배포에 묶여 있는 셈이라 penalty_exception 같은 표로 빼면 화면에서 넣을 수 있다.
penalty_exception
type channel / provider
value
reason
created_by
reason 과 created_by 를 같이 두면 나중에 왜 예외인지도 남는다.
다만 코드에 둔 데는 이유가 있을 수도 있다. 돈이 걸린 판정이라 커밋 이력과 리뷰를 거치게 하려던 것일 수 있고 어느 쪽이 나은지는 이 목록을 얼마나 자주 바꾸느냐에 달렸다.
커맨드가 매일 자정에 도는 것도 눈에 걸렸는데 그래서 자동 부과는 부과 시각이 전부 자정 근처에 몰린다. 부과 시각만 보고도 자동인지 수동인지 갈리는 셈이라 이번 조사에서도 그 구분을 썼다.
정리
- 실제 여부를 확인하지 않고 상태 체류만으로 자동 부과한다
- 추적으로 판정하면 정보가 없는 동안 판단할 수 없다
- 가짜일수록 추적이 안 되니 그 방식은 대상을 놓친다
- 확인할 수 있는 것으로 대신 판정하는 구조다
- 먼저 걸고 확인되면 자동으로 철회한다
- 놓치는 비용이 잘못 거는 비용보다 크다고 본 설계다
- 이의 대응은 실제로 했나가 아니라 왜 상태가 안 넘어갔나를 본다
- 덮어쓰기 설계와 체류 판정이 만나면 오탐이 난다
- 각각은 합리적이라 나란히 놓아야 보인다
- 예외 목록이 코드에 박혀 있으면 추가에 배포가 필요하다
- 자동 실행 시각이 고정이면 자동인지 수동인지 구분된다