[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#
- 2026-05-04T15:40:36Z — Quote
X5NI85OSBJ적용 시작 (Team 1124) - 2026-05-04T15:40:36Z — 기존 product 비활성화, 새 product 추가, billing_info 업데이트 완료
- 2026-05-04T15:40:36Z —
active_billing_state!호출 시check_on_billing_activation!에서 active workspace 19개 감지, 에러 발생
Error Log#
[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)
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)
if _billing_started_at > Date.current
billable.scheduled_billing_state!
else
billable.active_billing_state!
end
- Failure point: State machine
before_transition to: :activeguard (app/models/concerns/statable/billable.rb:59-61)
before_transition from: any, to: :active do |billable, transition|
billable.check_on_billing_activation!
end
이 guard가 Billable::Team#check_on_billing_activation!을 호출한다:
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)
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의 전체 실행 흐름을 확인했다:
service:cupixworks-api "X5NI85OSBJ"
Time range: 2026-05-04T14:00:00Z to 2026-05-04T16:40:00Z
시간순 로그 (모두 2026-05-05 00:40:36 KST):
[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
로그에서 확인할 수 있는 사항:
- "Activate billing state" info 로그가 출력된 후 바로 에러가 발생 —
active_billing_state!호출은 되었으나 state machine guard에서 실패 - "Quote application finished" 로그가 없음 —
apply!메서드의else블록(quote.rb:149)에 도달하지 못함 - 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-62—check_on_billing_activation!메서드 수정- 현재 guard는 Team billing 첫 활성화(workspace-level에서 team-level로 전환)를 위해 설계된 것으로 보임. 그러나 Team billing 갱신(재적용) 시나리오를 고려하지 않음.
- 두 가지 접근 방식 중 택일:
check_on_billing_activation!에서 "이전 Team billing이 있었는지" 확인하여 갱신 시나리오에서는 guard를 skipQuote#apply!에서active_billing_state!호출 전에workspaces.each(&:reset_billing!)을 명시적으로 선행 실행
단기 개선 (1주 이내)#
after_active_billing_state콜백의 workspace reset 로직을before_transitionguard 이전에 실행되도록 재배치reset_billing!메서드에 cascade 옵션을 추가하여 Team reset 시 하위 workspace도 함께 리셋하는 옵션 제공 검토
장기 개선 (재발 방지)#
- Team/Workspace billing state transition의 의존 관계를 문서화
- Quote 적용 프로세스에 대한 통합 테스트 추가 — 특히 "기존 active workspace가 있는 Team에 새 Quote를 적용하는" 갱신 시나리오
Monitoring#
check_on_billing_activation!실패 모니터링:
service:cupixworks-api status:error "Active billing workspaces found"
- Quote 적용 실패 전체 추적:
service:cupixworks-api status:error "Quote application failed"
Risk Assessment#
- Risk level: medium
- 예상 복잡도: standard
- 영향 범위: Team-level billing을 갱신하는 모든 Quote 적용에 영향. workspace-level billing이 active인 Team은 갱신 불가. 현재 1건 발생이지만, 구독 갱신 시즌에 동일 패턴으로 다수 발생 가능.