ES /docs

[Quote][X5NI85OSBJ] Quote application failed: Active billing workspaces found: 5173, 5174, 5178, 518

RCA: Quote application failed: Active billing workspaces found

Overview#

What Happened#

2026-05-04 15:40:36 UTC에 cupixworks-api 서비스에서 Quote X5NI85OSBJ를 Team 1124에 적용하는 과정이 실패했다. Team-level billing 활성화 직전의 before_transition guard에서 이미 active billing state를 가진 workspace 19개(5173, 5174, 5178 등)가 감지되어 Cupix::Errors::Billing (BILL2100) 예외가 발생했다.

Quick Facts#

Field Value
exception.class Cupix::Errors::Billing
exception.message Active billing workspaces found: 5173, 5174, 5178, 5182, 5183, 5184, 5185, 5186, 5189, 5190, 5191, 5407, 5429, 5438, 5465, 5466, 5486, 5517, 5540
top_frame app/models/concerns/billable/team.rb:60
env production, us-west-2

Timeline#

  1. 2026-05-04T15:40:36Z — Quote X5NI85OSBJ 적용 시작 (Team 1124)
  2. 2026-05-04T15:40:36Z — 기존 product 비활성화, 새 product 추가, billing_info 업데이트 완료
  3. 2026-05-04T15:40:36Zactive_billing_state! 호출 시 check_on_billing_activation!에서 active workspace 19개 감지, 에러 발생

Error Log#

Datadog Logs

text
[Quote][X5NI85OSBJ] Quote application failed: Active billing workspaces found: 5173, 5174, 5178, 5182, 5183, 5184, 5185, 5186, 5189, 5190, 5191, 5407, 5429, 5438, 5465, 5466, 5486, 5517, 5540

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-05-04T15:40:36.469Z
  • 최근 발생: 2026-05-04T15:40:36.469Z

Team 1124의 Quote 갱신(재계약)이 실패하여 새로운 billing 계약이 적용되지 않았다. 기존 workspace-level billing이 유지되는 상태로 남아 있어 팀 전체의 구독 갱신이 차단된다.

Root Cause Summary#

Quote#apply! 메서드가 Team의 billing state를 active로 전환할 때 발생하는 실행 순서 문제가 원인이다. State machine의 before_transition to: :active guard인 check_on_billing_activation!이 "active billing workspace가 없어야 한다"는 조건을 검사하지만, workspace의 billing 리셋은 after_transition to: :active 콜백에서 수행된다. 즉, workspace 리셋이 guard 검사보다 나중에 실행되어 이전 구독 기간의 active workspace가 남아있으면 무조건 실패한다. 이는 Team-level billing을 갱신(재적용)하는 시나리오에서 구조적으로 발생하는 버그이다.

Technical Analysis#

Code Path#

  • Entry point: Quote#apply! (app/models/quote.rb:78)
  • Step 1: billable.reset_billing! — Team의 billing property와 state만 초기화 (app/models/concerns/resetable/billable.rb:19-27)
app/models/concerns/resetable/billable.rb:19-27ruby
def reset_billing!
  reset_billable_properties
  reset_applied_products
  reset_billing_state
  reset_trial_state
  reset_lock_state
  flush_all_on_billable
  billing_state
end

이 메서드는 Team 자신의 billing state만 :none으로 리셋한다. 하위 workspace의 billing state는 리셋하지 않는다.

  • Step 2: Product 비활성화, 새 product 추가, billing_info 업데이트 (app/models/quote.rb:84-131)
  • Step 3: billable.active_billing_state! 호출 (app/models/quote.rb:138)
app/models/quote.rb:135-139ruby
if _billing_started_at > Date.current
  billable.scheduled_billing_state!
else
  billable.active_billing_state!
end
  • Failure point: State machine before_transition to: :active guard (app/models/concerns/statable/billable.rb:59-61)
app/models/concerns/statable/billable.rb:59-61ruby
before_transition from: any, to: :active do |billable, transition|
  billable.check_on_billing_activation!
end

이 guard가 Billable::Team#check_on_billing_activation!을 호출한다:

app/models/concerns/billable/team.rb:56-62ruby
def check_on_billing_activation!
  active_billing_workspaces = workspaces.where(billing_state: :active)

  if active_billing_workspaces.present?
    raise Cupix::Errors::Billing.new(code: 'BILL2100', reason: "Active billing workspaces found: #{active_billing_workspaces.pluck(:id).join(', ')}")
  end
end

Team 1124에 속한 19개 workspace가 이전 구독 기간에서 billing_state: :active 상태를 유지하고 있어 여기서 예외가 발생한다.

  • 실행되지 않은 코드: after_active_billing_state 콜백 (app/models/concerns/billable/team.rb:13-16)
app/models/concerns/billable/team.rb:13-16ruby
after_active_billing_state do
  self.reset_trial_state!
  workspaces.each(&:reset_billing!)
end

이 콜백이 workspace billing을 리셋하는 유일한 코드인데, after_transition (즉 transition 성공 후) 시점에 실행되므로, before_transition guard에서 이미 차단되어 도달하지 못한다.

기대 동작: Team billing 갱신 시 workspace billing을 먼저 리셋한 후 Team을 active로 전환 실제 동작: Team active 전환 시도 → guard에서 기존 active workspace 감지 → 전환 실패 → workspace 리셋 코드에 도달하지 못함

Log Evidence#

Datadog에서 Quote X5NI85OSBJ의 전체 실행 흐름을 확인했다:

text
service:cupixworks-api "X5NI85OSBJ"
Time range: 2026-05-04T14:00:00Z to 2026-05-04T16:40:00Z

시간순 로그 (모두 2026-05-05 00:40:36 KST):

text
[Quote][X5NI85OSBJ] Quote application begins Team 1124
[Quote][X5NI85OSBJ] Deactivate existing products
[Quote][X5NI85OSBJ] Update billing_info to Team 1124
[Quote][X5NI85OSBJ] Add team products
[Quote][X5NI85OSBJ] Activate billing state: 2026-05-03 to 2027-07-01 for 14 months
[Quote][X5NI85OSBJ] Quote application failed: Active billing workspaces found: 5173, 5174, 5178, 5182, 5183, 5184, 5185, 5186, 5189, 5190, 5191, 5407, 5429, 5438, 5465, 5466, 5486, 5517, 5540

로그에서 확인할 수 있는 사항:

  1. "Activate billing state" info 로그가 출력된 후 바로 에러가 발생 — active_billing_state! 호출은 되었으나 state machine guard에서 실패
  2. "Quote application finished" 로그가 없음 — apply! 메서드의 else 블록(quote.rb:149)에 도달하지 못함
  3. Worker 서비스(cupixworks-worker)에서는 관련 로그가 없음 — 이 작업은 API 요청 컨텍스트에서 동기적으로 실행됨

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 State machine 실행 순서 문제: before_transition guard가 workspace를 리셋하기 전에 active workspace를 체크하여 실패 check_on_billing_activation!before_transition to: :active에서 실행 (statable/billable.rb:59-61). workspace reset_billing!after_active_billing_state 콜백에서 실행 (billable/team.rb:13-16). 로그에서 "Activate billing state" 직후 에러 발생 확인. Confirmed
H2 reset_billing! 호출 시 workspace billing도 함께 리셋되어야 하는데 누락 reset_billing! (resetable/billable.rb:19-27)은 self의 property만 리셋. workspace 리셋 로직 없음. Team reset_billing!이 workspace를 리셋하지 않는 것은 의도적 설계일 수 있음 (workspace-level billing 독립성) Confirmed (H1의 보완 근거)
H3 동시 요청에 의한 race condition으로 workspace billing이 잔존 동일 시간대 중복 로그 없음. 단일 Quote 적용 요청만 확인. 로그에 중복 "Quote application begins" 없음 Rejected
H4 Workspace가 독립적인 Quote로 billing이 적용된 경우 (workspace-level billing) 19개 workspace가 active billing state — 이전 구독 기간에서의 정상적인 상태 이 경우에도 Team billing 갱신 시 workspace reset이 선행되어야 하므로 근본 원인은 H1 Rejected (H1이 근본 원인)

Fix Recommendation#

즉시 조치 (Critical)#

  • app/models/concerns/billable/team.rb:56-62check_on_billing_activation! 메서드 수정
  • 현재 guard는 Team billing 첫 활성화(workspace-level에서 team-level로 전환)를 위해 설계된 것으로 보임. 그러나 Team billing 갱신(재적용) 시나리오를 고려하지 않음.
  • 두 가지 접근 방식 중 택일:
    1. check_on_billing_activation!에서 "이전 Team billing이 있었는지" 확인하여 갱신 시나리오에서는 guard를 skip
    2. Quote#apply!에서 active_billing_state! 호출 전에 workspaces.each(&:reset_billing!)을 명시적으로 선행 실행

단기 개선 (1주 이내)#

  • after_active_billing_state 콜백의 workspace reset 로직을 before_transition guard 이전에 실행되도록 재배치
  • reset_billing! 메서드에 cascade 옵션을 추가하여 Team reset 시 하위 workspace도 함께 리셋하는 옵션 제공 검토

장기 개선 (재발 방지)#

  • Team/Workspace billing state transition의 의존 관계를 문서화
  • Quote 적용 프로세스에 대한 통합 테스트 추가 — 특히 "기존 active workspace가 있는 Team에 새 Quote를 적용하는" 갱신 시나리오

Monitoring#

  • check_on_billing_activation! 실패 모니터링:
text
service:cupixworks-api status:error "Active billing workspaces found"
  • Quote 적용 실패 전체 추적:
text
service:cupixworks-api status:error "Quote application failed"

Risk Assessment#

  • Risk level: medium
  • 예상 복잡도: standard
  • 영향 범위: Team-level billing을 갱신하는 모든 Quote 적용에 영향. workspace-level billing이 active인 Team은 갱신 불가. 현재 1건 발생이지만, 구독 갱신 시즌에 동일 패턴으로 다수 발생 가능.