Skip to content
isdnetworks
Go back

만드는 쪽과 처리하는 쪽이 달랐다

정산 대상이 몇 건 빠져 있었다. 만드는 쪽과 처리하는 쪽이 다른 시스템이었다.

Table of contents

Open Table of contents

증상 — 빠져 있던 정산 대상

두 시스템이 이렇게 나눠 맡고 있다.

주문 시스템   정산 대상을 만든다 (settle_target 테이블에 넣는다)
정산 시스템   그 테이블을 읽어 처리한다

같은 DB의 같은 settle_target 을 쓰지만 코드베이스는 서로 다르다.

빠진 건을 찾아보니 settle_target 에 아예 없었다. 처리하는 쪽이 놓친 것이 아니라 만드는 쪽이 안 넣은 것이었다.

들어온 것만 처리하는 쪽에서는 안 들어온 것을 알 방법이 없다. settle_target 을 읽는 SELECT 는 정상으로 돌았고 처리 로그에도 오류가 하나도 없었다.

원인 — MIXED가 조건에 안 걸렸다

넣을지 말지를 정하는 자리는 이랬다.

if ($order->state === 'DONE' && in_array($order->payMethod, ['CARD','BANK','VBANK'], true)) {
    $this->settleTarget->insert($order);
}

전액 포인트 결제를 빼려고 허용할 수단을 나열해 두고 있었다.

빠진 주문의 결제수단은 이랬다.

결제수단  MIXED (포인트 3,000 + 카드 12,000)

MIXED 가 목록에 없으니 in_array 가 거짓이 되어 그대로 지나간다.

포인트와 카드를 같이 쓰는 결제가 나중에 생겼는데 이 목록은 그 전에 쓰인 것이었다. 실제로는 카드로 12,000원이 나갔으니 정산 대상이 맞는 주문이다.

in_array 의 목록은 수단이 늘 때마다 손대야 한다. 고치는 것을 잊으면 그 payMethod 로 결제한 주문이 통째로 빠진다.

사이에서 사라지는 것을 세게 했다

두 시스템 사이에는 아무 확인이 없었다.

-- 정산 대상이어야 하는데 안 들어간 것
SELECT o.no FROM orders o
LEFT JOIN settle_target s ON o.no = s.order_no
WHERE o.state = 'DONE' AND o.done_date BETWEEN ? AND ?
  AND s.order_no IS NULL;

매일 이 조회를 돌리고 0이 아니면 알리게 했다.

2017-12-11  누락 4건

이 조회의 조건은 만드는 쪽 코드를 보지 않고 따로 썼다. 같은 조건을 옮겨 적으면 그 조건이 틀렸을 때 확인도 같이 틀린다.

만드는 쪽이 in_array 로 판단했으니 확인은 state 와 조인만으로 쓴 셈이다. LEFT JOINIS NULL 로만 세는 쪽이 만드는 코드와 겹치지 않아 어긋남이 드러난다.

조치 — 수단이 아니라 금액으로

정산 대상인지를 판단하는 자리를 하나로 모았다.

final class SettleRule {
    public static function isTarget(Order $o): bool {
        if ($o->state !== 'DONE') return false;
        if ($o->totalPaidAmount <= 0) return false;   // 전액 포인트면 0이다
        return true;
    }
}

payMethod 대신 totalPaidAmount 를 본다.

빼려던 것은 실제로 돈이 안 오간 주문이었고 그것은 totalPaidAmount 가 0이라는 결과로 드러난다. payMethod 가 무엇이든 이 조건은 그대로다.

MIXED 가 생겼을 때 조건이 깨진 것은 허용할 수단을 나열해 뒀기 때문이다. 결과로 판단하면 새 수단이 열 개 생겨도 이 줄을 안 고친다.

검증 — 넣는 것과 확인하는 것을 나눴다

판단은 한 곳에서 하되 확인은 다른 방식으로 하게 했다.

넣기    SettleRule::isTarget() 로 판단해서 넣는다
확인    금액 합계로 맞춰 본다

건수가 아니라 금액으로 맞췄다.

SELECT SUM(paid_amount) FROM orders WHERE done_date BETWEEN ? AND ? AND state='DONE';
SELECT SUM(amount) FROM settle_target WHERE done_date BETWEEN ? AND ?;

두 합계가 같아야 하고 다르면 어딘가 어긋난 것이다.

COUNT(*) 는 같은데 SUM 이 다른 날이 나왔다. 부분 취소된 주문의 amount 가 갱신되지 않고 있었다.

세는 방식을 다르게 하니 다른 문제가 드러났다. 건수만 봤으면 이 건은 영영 안 보였을 것이다.

대응 — 늦게 들어오는 것

주문 확정과 정산 대상 생성 사이에 시간이 있었다.

12/10 23:58  주문 확정
12/11 00:03  정산 대상 생성

5분 사이에 날짜가 넘어가서 done_date 가 10일인데 11일 정산으로 갔다.

$this->settleTarget->insert([
    'order_no'   => $o->no,
    'done_date'  => $o->doneDate,      // 주문 확정 시각
    'created_at' => date('Y-m-d H:i:s'),  // 만든 시각
]);

두 시각을 다 남기고 정산은 done_date 기준으로 한다.

늦게 들어온 것을 세는 조회도 뒀다.

SELECT COUNT(*) FROM settle_target
WHERE DATE(created_at) > DATE(done_date);

이미 마감한 날짜에 뒤늦게 들어온 건은 따로 처리해야 한다.

마지막으로 두 시스템 사이의 약속을 문서로 뒀다.

settle_target 테이블 규약

넣는 쪽   주문 시스템
읽는 쪽   정산 시스템

넣는 조건   SettleRule::isTarget() 참조
넣는 시점   주문 확정 직후
필수 컬럼   order_no, amount, done_date, created_at

읽는 쪽은 done_date 기준으로 처리한다.
마감 후 들어온 건은 late_flag 로 표시되며 다음 마감에 포함한다.

컬럼 변경은 양쪽에 알린 뒤 한다.

두 팀이 같이 보는 자리에 두어 한쪽만 아는 것이 없게 했다. 이 약속이 없으면 한쪽이 컬럼을 고칠 때 다른 쪽이 모른다.

정리


Share this post on:

Previous Post
여러 팀이 같이 쓰는 모듈
Next Post
결제 수단에 따라 달랐던 약관