마켓플레이스 수수료 정산 시스템: 판매자 정산 자동화

이커머스

마켓플레이스정산 시스템수수료판매자백엔드

이 글은 누구를 위한 것인가

  • 판매자 정산 시스템을 구축하려는 팀
  • 수수료 계산과 반품 조정 로직을 설계하는 개발자
  • 정산 주기별 배치와 세금계산서 발행을 자동화하려는 팀

들어가며

마켓플레이스 정산은 "판매액 - 수수료 - 반품 조정 = 지급액"이 기본이지만, 카테고리별 수수료율, 에스크로 홀드 기간, 반품/환불 클레임백이 복잡성을 더한다. 원장(ledger) 방식으로 모든 변동을 기록하면 정산 불일치를 추적할 수 있다.

이 글은 bluefoxdev.kr의 마켓플레이스 정산 시스템 가이드 를 참고하여 작성했습니다.


1. 정산 시스템 설계

[정산 원장 (Ledger) 구조]
  모든 금액 변동을 불변 이력으로 기록

  CREDIT (판매자 수입):
    - SALE: 판매 완료 (배송 완료 후)
  
  DEBIT (판매자 차감):
    - COMMISSION: 플랫폼 수수료
    - RETURN_CLAWBACK: 반품으로 인한 환수
    - PENALTY: 위반 패널티

[수수료 구조]
  카테고리별 차등:
    전자기기: 8%
    의류: 12%
    식품: 5%
    디지털: 20%
  
  판매금액 구간별:
    ~100만원: 기본율
    100~500만원: 기본율 - 1%
    500만원↑: 기본율 - 2%

[정산 주기]
  에스크로 홀드: 배송 완료 후 7일
  (반품 기간 종료 대기)
  
  정산 주기: 매주 화요일 (지난 주분)
  지급 방법: 가상계좌 → 판매자 계좌 이체

2. 정산 구현

class SettlementService {
  // 판매 완료 시 원장 기록
  async recordSale(params: {
    sellerId: string;
    orderId: string;
    orderItemId: string;
    saleAmount: number;
    category: string;
  }) {
    const commissionRate = await this.getCommissionRate(params.category, params.saleAmount);
    const commission = Math.round(params.saleAmount * commissionRate);
    const netAmount = params.saleAmount - commission;

    await db.$transaction([
      // 판매 수입 기록
      db.sellerLedger.create({
        data: {
          sellerId: params.sellerId,
          type: 'SALE',
          amount: params.saleAmount,
          orderId: params.orderId,
          orderItemId: params.orderItemId,
          settledAt: new Date(Date.now() + 7 * 86400_000), // 7일 홀드
        },
      }),
      // 수수료 차감 기록
      db.sellerLedger.create({
        data: {
          sellerId: params.sellerId,
          type: 'COMMISSION',
          amount: -commission,
          orderId: params.orderId,
          orderItemId: params.orderItemId,
          commissionRate,
          settledAt: new Date(Date.now() + 7 * 86400_000),
        },
      }),
    ]);

    return { netAmount, commission, commissionRate };
  }

  // 반품 시 클로백
  async processReturnClawback(orderItemId: string) {
    const ledgers = await db.sellerLedger.findMany({
      where: { orderItemId, type: { in: ['SALE', 'COMMISSION'] }, isPaid: false },
    });

    for (const ledger of ledgers) {
      await db.sellerLedger.create({
        data: {
          sellerId: ledger.sellerId,
          type: 'RETURN_CLAWBACK',
          amount: -ledger.amount, // 반대 부호로 상쇄
          orderItemId,
          relatedLedgerId: ledger.id,
          settledAt: new Date(),
        },
      });
    }
  }

  // 주간 정산 배치
  async runWeeklySettlement(sellerId: string) {
    const cutoff = new Date();
    const pendingLedgers = await db.sellerLedger.findMany({
      where: {
        sellerId,
        isPaid: false,
        settledAt: { lte: cutoff },
      },
    });

    const totalAmount = pendingLedgers.reduce((s, l) => s + Number(l.amount), 0);

    if (totalAmount <= 0) return; // 지급할 금액 없음

    const settlement = await db.settlement.create({
      data: {
        sellerId,
        amount: totalAmount,
        ledgerIds: pendingLedgers.map(l => l.id),
        status: 'PENDING',
        scheduledAt: new Date(),
      },
    });

    // 실제 이체 (은행 API 연동)
    await bankTransfer({ sellerId, amount: totalAmount, settlementId: settlement.id });

    await db.sellerLedger.updateMany({
      where: { id: { in: pendingLedgers.map(l => l.id) } },
      data: { isPaid: true, settlementId: settlement.id },
    });
  }

  private async getCommissionRate(category: string, amount: number): Promise<number> {
    const baseRates: Record<string, number> = {
      electronics: 0.08, clothing: 0.12, food: 0.05, digital: 0.20,
    };
    let rate = baseRates[category] ?? 0.10;
    if (amount > 5_000_000) rate -= 0.02;
    else if (amount > 1_000_000) rate -= 0.01;
    return rate;
  }
}

async function bankTransfer(params: any) { /* 은행 API */ }
const db = {} as any;

마무리

정산 시스템의 핵심은 원장(Ledger) 방식이다. 판매, 수수료, 반품 클로백을 모두 불변 레코드로 기록하면 어떤 시점의 잔액이든 재계산할 수 있고, 감사 추적도 완벽하다. 에스크로 홀드 기간은 반품 기간과 연동해 미리 설정하고, 배치로 주기적 정산을 자동화한다.