ES /docs

[Quote][KJPGP20RV6] Quote application failed: Active billing team found

RCA: [Quote][KJPGP20RV6] Quote application failed: Active billing team found

Overview#

What Happened#

2026-05-26 15:34 UTC에 cupixworks-api 서비스에서 관리자(philip.hall@cupix.com)가 Quote 8228(번호 KJPGP20RV6)을 Workspace 5646에 적용하려 시도했으나, 해당 Workspace의 상위 Team이 이미 active billing 상태여서 BILL2100 에러와 함께 HTTP 400으로 실패했다. 단일 발생이며 비즈니스 로직 검증 에러이다.

Quick Facts#

Field Value
exception.class Cupix::Errors::Billing
exception.message Active billing team found
top_frame app/models/concerns/billable/workspace.rb:45
deploy production-us-west-2-20260526T0443Z0-dd7bd097-cupixworks
env production, us-west-2

Timeline#

  1. 2026-05-26T15:34:00.277Z — Quote KJPGP20RV6 apply 시작, 중간 단계 진행 후 check_on_billing_activation!에서 실패
  2. 2026-05-26T15:34:00.328Z — API 응답 HTTP 400 반환 (303ms 소요)
  3. 2026-05-27 — RCA 수행

Error Log#

Datadog Logs

text
[Quote][KJPGP20RV6] Quote application failed: Active billing team found

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-05-26T15:34:00.277Z
  • 최근 발생: 2026-05-26T15:34:00.277Z

단일 admin 사용자의 quote apply 요청 실패. 최종 사용자 영향 없음. Quote 미적용 상태 유지.

Root Cause Summary#

Workspace 5646에 Quote를 apply하는 과정에서, 상태 머신의 before_transition to: :active 훅이 check_on_billing_activation!을 호출한다. 이 메서드는 Workspace의 상위 Team이 이미 billing_state_active?인지 확인하며, active 상태일 경우 BILL2100 에러를 발생시킨다. 이는 하나의 billing 계층(Team ↔ Workspace)에서 중복 billing activation을 방지하는 의도적 비즈니스 검증 로직이다. 에러 자체는 정상적인 비즈니스 규칙 위반 감지이나, error 레벨로 로깅되어 모니터링에 잡힌 것이 문제이다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/admin/quotes_controller.rbapply action
  • Repository: app/repositories/admin/quote_repository.rb:48@model.apply! 호출
  • Quote model: app/models/quote.rb:78-151apply! 메서드
  • Failure point: app/models/concerns/billable/workspace.rb:45check_on_billing_activation!

실행 흐름:

  1. Admin이 PUT /api/v1/admin/quotes/8228/apply 호출
  2. QuoteRepository#apply가 권한 확인 후 @model.apply! 호출
app/repositories/admin/quote_repository.rb:45-50ruby
def apply
  raise Cupix::Errors::Billing.new(code: 'BILL10000', reason: 'Permission denied operation: Only accounting deparment can apply the quote') unless Pundit.policy(self.current_user, @model).apply?

  @model.apply!
  @model
end
  1. Quote#apply!billable.reset_billing!으로 기존 billing 초기화 후, 제품 추가 및 billing 정보 업데이트 진행
app/models/quote.rb:78-84ruby
def apply!
  Cupix::Logger.info("[Quote][#{number}] Quote application begins #{billable.class.name} #{billable.id}")
  check_appliable

  billable.reset_billing!

  Cupix::Logger.info("[Quote][#{number}] Deactivate existing products")
  1. 마지막 단계에서 billable.active_billing_state! 호출 시 상태 머신의 before_transition 훅 실행
app/models/quote.rb:133-139ruby
Cupix::Logger.info("[Quote][#{number}] Activate billing state: #{_billing_started_at} to #{_billing_expires_at} for #{_contract_months} months")

if _billing_started_at > Date.current
  billable.scheduled_billing_state!
else
  billable.active_billing_state!
end
  1. 상태 머신 훅이 check_on_billing_activation! 호출
app/models/concerns/statable/billable.rb:59-61ruby
before_transition from: any, to: :active do |billable, transition|
  billable.check_on_billing_activation!
end
  1. Workspace의 구현이 상위 Team의 billing 상태 확인 후 에러 발생
app/models/concerns/billable/workspace.rb:43-47ruby
def check_on_billing_activation!
  if team.billing_state_active?
    raise Cupix::Errors::Billing.new(code: 'BILL2100', reason: 'Active billing team found')
  end
end
  1. Quote#apply!의 rescue 블록이 에러를 error 레벨로 로깅 후 re-raise
app/models/quote.rb:145-147ruby
rescue StandardError => e
  Cupix::Logger.error("[Quote][#{number}] Quote application failed: #{e.message}")
  raise e

기대 동작: Workspace의 상위 Team이 active billing이 아닌 경우에만 quote가 적용되어야 한다. 실제 동작: Team이 이미 active billing 상태이므로 검증에서 정상적으로 거부됨. 비즈니스 규칙 위반은 정확히 감지되었으나, error 레벨 로깅이 과도하다.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-api @environment:production "KJPGP20RV6"

실행 로그 (시간순):

text
[Quote][KJPGP20RV6] Quote application begins Workspace 5646
[Quote][KJPGP20RV6] Deactivate existing products
[Quote][KJPGP20RV6] Add team products
[Quote][KJPGP20RV6] Update billing_info to Workspace 5646
[Quote][KJPGP20RV6] Activate billing state: 2026-05-22 to 2027-06-01 for 12 months
[Quote][KJPGP20RV6] Quote application failed: Active billing team found

HTTP 요청 로그:

text
[400] PUT /api/v1/admin/quotes/8228/apply (Api::V1::Admin::QuotesController#apply)
Error Code: BILL2100
Error Class: Cupix::Errors::Billing
Error Reason: Active billing team found
User: philip.hall@cupix.com (ID: 4568)
Duration: 303.81ms

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Workspace의 상위 Team이 이미 active billing이라 비즈니스 규칙에 의해 정당하게 거부됨 코드 workspace.rb:44에서 team.billing_state_active? 체크, 로그에서 모든 중간 단계 정상 진행 후 마지막 activation 단계에서만 실패 Confirmed
H2 reset_billing!이 Team 레벨의 billing state도 초기화했어야 하나 Workspace만 초기화함 reset_billing!은 해당 billable(Workspace)만 초기화 (resetable/billable.rb:19-27), Team의 billing은 별도 reset_billing!의 설계 의도가 자신의 billing만 초기화하는 것이므로 버그 아님 Rejected
H3 Transaction이 없어 부분 적용(제품 추가, billing_info 업데이트)이 롤백되지 않는 데이터 정합성 문제 Quote#apply!와 controller에 transaction 블록 없음, grep 결과 확인 이 에러 자체의 root cause는 아님. 별도의 잠재적 문제 Inconclusive

Fix Recommendation#

즉시 조치 (Critical)#

이 에러는 의도된 비즈니스 검증 로직의 정상 작동이다. error 레벨로 로깅하는 것이 과도하므로, Quote#apply!의 rescue 블록에서 Cupix::Errors::Billing 예외를 별도 처리하여 warn 레벨로 다운그레이드하는 것을 권장한다.

  • 파일: app/models/quote.rb:145-147
  • 방향: Cupix::Errors::Billing 예외는 warn 레벨로 로깅, 그 외 StandardError는 기존대로 error 레벨 유지

단기 개선 (1주 이내)#

  • check_on_billing_activation! 검증을 apply! 메서드 초반(check_appliable 직후)으로 이동하여, 부분적 상태 변경(제품 추가, billing_info 업데이트) 전에 조기 실패하도록 개선
  • 또는 apply! 전체를 DB transaction으로 감싸서 실패 시 롤백 보장

장기 개선 (재발 방지)#

  • Quote apply 프로세스에 pre-validation 단계를 추가하여, 실제 적용 전에 모든 조건을 사전 검증하는 dry-run 모드 구현
  • billing state 관련 비즈니스 에러를 error 대신 warn으로 로깅하는 정책 수립 (모든 Cupix::Errors::Billing 예외에 대해)

Monitoring#

  • 비즈니스 로직 에러를 error에서 분리하는 쿼리:
text
service:cupixworks-api status:error "Quote application failed" -"Active billing team found" -"Active billing workspaces found"
  • Quote apply 실패율 추적:
text
service:cupixworks-api "Quote application failed" | stats count by @error.code

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial

단일 발생이며, 비즈니스 규칙이 정상 작동한 결과이다. 사용자 영향 없음. 로그 레벨 조정만으로 해결 가능.