[Quote][F43544W7U8] Quote application failed: Billable already has a scheduled quote: 870
RCA: [Quote][F43544W7U8] Quote application failed: Billable already has a scheduled quote: 870
Overview#
What Happened#
2026-07-22 00:58 KST, cupixworks-api (ap-southeast-2, tenant cupix) 의 admin quote apply 엔드포인트에서 [Quote][F43544W7U8] Quote application failed: Billable already has a scheduled quote: 870 에러가 발생. Workspace 530 에 이미 예약된 quote 870 이 존재하는 상태에서 다른 미래일자 quote (F43544W7U8, 내부 id 878) 를 apply 하려다 schedule_apply! 의 가드 조건에 걸려 Cupix::Errors::Billing (code: BILL10000) 이 raise 됨. 동일 원인의 실패가 15:54 ~ 16:08 UTC 사이 총 7건 발생하며 status-board 인시던트 2026-07-21-svc-cupixworks-api--unknown-3 로 그룹핑됨.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Cupix::Errors::Billing |
| exception.message | Billable already has a scheduled quote: 870 |
| exception.code | BILL10000 |
| top_frame | app/models/quote.rb:168 (schedule_apply!) |
| endpoint | PUT /api/v1/admin/quotes/878/apply |
| env | production, ap-southeast-2, tenant cupix |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| Admin billing (cupixworks-api Quote apply) | 7 (같은 워크스페이스, 15:54~16:08 UTC) | Workspace 530 에 미래일자 quote 를 apply 하려는 admin 조작이 반복 실패. billing schedule 이 이미 설정된 상태이므로 사용자(고객) 청구/서비스 영향은 없음. |
Timeline#
- 2026-07-22 00:54 KST —
[Quote][GYVGXJU55F](quote 872) apply 시도 → 동일 에러 (첫 발생, 클러스터 79f45086) - 2026-07-22 00:54 KST —
[Quote][M9X89PV6G6](quote 874) apply 시도 → 동일 에러 (0e9768a3) - 2026-07-22 00:55 KST —
[Quote][ZT5TKAT9CA](quote 876) apply 시도 → 동일 에러 (79a9ed15) - 2026-07-22 00:58 KST —
[Quote][F43544W7U8](quote 878) apply 시도 → 동일 에러 (본 클러스터 ffe05413) - 2026-07-22 01:06 KST — quote 880, 884 apply 시도 → 동일 에러 (d00e1f4b, 45bcf1d2)
- 2026-07-22 01:08 KST — quote 886 apply 시도 → 동일 에러 (fdedbc62), status-board 마지막 이벤트
Error Log#
[Quote][F43544W7U8] Quote application failed: Billable already has a scheduled quote: 870
Impact#
- Service:
cupixworks-api - Region: ap-southeast-2
- Tenant: cupix
- 발생 횟수: 1 (본 클러스터). 동일 원인/워크스페이스로 status-board 상 총 7 클러스터.
- 최초 발생: 2026-07-22 00:58 KST
- 최근 발생: 2026-07-22 00:58 KST
Root Cause Summary#
origin/develop 의 app/models/quote.rb#schedule_apply! (TSLA-12975 에서 도입) 는 미래일자로 지정된 quote 를 apply 할 때 billable 의 scheduled_quote_id 가 이미 다른 quote 로 채워져 있으면 Cupix::Errors::Billing (BILL10000) 을 raise 하도록 의도된 가드 로직이다. Workspace 530 에는 이전 admin 조작으로 quote 870 이 이미 scheduled_quote 로 연결되어 있었고, 그 상태에서 admin 이 새로운 미래일자 quote (872, 874, 876, 878, 880, 884, 886) 를 순차적으로 apply 시도했다. 각 시도마다 billable.scheduled_quote_id(=870) 가 새 quote id 와 다르므로 가드가 raise 되어 apply 가 실패했다. 즉, 이는 코드 버그가 아니라 의도된 정합성 가드가 정상 동작한 사례이며, 실제 원인은 "이미 예약된 quote 를 정리(취소/활성화)하지 않은 상태에서 새 미래일자 quote 를 계속 apply 하려 한 운영 흐름"이다.
Technical Analysis#
Code Path#
Entry point: PUT /api/v1/admin/quotes/{id}/apply → Admin::QuoteRepository#apply → Quote#apply! → schedule_apply! (미래일자 분기).
Datadog info 로그가 이를 뒷받침한다:
[400] PUT /api/v1/admin/quotes/878/apply (Api::V1::Admin::QuotesController#apply)
error.class=Cupix::Errors::Billing
error.code=BILL10000
error.reason="Billable already has a scheduled quote: 870"
Repository layer:
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
Model apply!:
def apply!
Cupix::Logger.info("[Quote][#{number}] Quote application begins #{billable.class.name} #{billable.id}")
check_appliable
_billing_started_at, _billing_expires_at = extract_billing_dates
# ...
ActiveRecord::Base.transaction do
if _billing_started_at > Time.zone.today
schedule_apply!(_billing_started_at)
else
activate_apply!(_billing_started_at, _billing_expires_at)
end
end
Failure point (가드 자체):
def schedule_apply!(billing_started_at)
if billable.scheduled_quote_id.present? && billable.scheduled_quote_id != id
raise Cupix::Errors::Billing.new(
code: 'BILL10000',
reason: "Billable already has a scheduled quote: #{billable.scheduled_quote_id}"
)
end
Cupix::Logger.info("[Quote][#{number}] Schedule quote application for #{billing_started_at} on #{billable.class.name} #{billable.id}")
billable.update!(
scheduled_quote: self,
scheduled_billing_started_at: billing_started_at
)
applied_state! if state_paid?
end
Rescue chain (에러 메시지 형태 확인):
rescue Cupix::Errors::Parameter => e
Cupix::Logger.error("[Quote][#{number}] Invalid Parameter : #{e.message}", ...)
raise e
rescue StandardError => e
Cupix::Logger.error("[Quote][#{number}] Quote application failed: #{e.message}")
raise e
기대 동작: billable.scheduled_quote_id 가 이미 다른 quote 로 세팅된 상태라면 새 미래일자 quote 를 apply 하기 전에 (a) 기존 예약 quote 를 활성화(activate_scheduled!)하거나 (b) 예약을 명시적으로 해제한 뒤 apply 하는 것이 정상 흐름.
실제 동작: 운영자가 (a)/(b) 없이 quote 872 → 874 → 876 → 878 → 880 → 884 → 886 을 연속 apply 하여 매번 동일 지점에서 raise. schedule_apply! 는 이 상황을 명시적으로 차단하도록 설계되어 있으므로 예상 대로 동작한 것.
관련 컨텍스트: 미래일자 스케줄된 quote 는 Cupix::Cron::ScheduledQuote.activate_for cron 이 scheduled_billing_started_at 시각 도달 시 자동 활성화한다:
def activate_for(billable_class)
candidates = billable_class.scheduled_quote_activation_candidate
Cupix::Logger.info("Activating scheduled quotes for #{billable_class.name}: #{candidates.size} candidate(s)", ...)
candidates.find_each do |billable|
quote = billable.scheduled_quote
next if quote.nil?
ActiveRecord::Base.transaction do
quote.activate_scheduled!
end
Cupix::Logger.info("Activated scheduled quote #{quote.number} on #{billable_class.name} #{billable.id}", ...)
rescue StandardError => e
Cupix::Logger.error("Failed to activate scheduled quote on #{billable_class.name} #{billable.id}: #{e.message}", ...)
end
end
Log Evidence#
Datadog 쿼리 (본 클러스터):
service:cupixworks-api "F43544W7U8"
Datadog 쿼리 (동일 워크스페이스 상의 모든 apply 시도):
service:cupixworks-api "Quote application begins" "Workspace 530"
Datadog 쿼리 (전체 유사 실패):
service:cupixworks-api "scheduled quote"
핵심 로그 (본 클러스터, F43544W7U8 / quote 878):
2026-07-22 00:58:15 [info] [Quote][F43544W7U8] Quote application begins Workspace 530
2026-07-22 00:58:15 [info] [400] PUT /api/v1/admin/quotes/878/apply (Api::V1::Admin::QuotesController#apply)
error.class=Cupix::Errors::Billing
error.code=BILL10000
error.reason="Billable already has a scheduled quote: 870"
error.message="Billable already has a scheduled quote: 870"
2026-07-22 00:58:15 [error] [Quote][F43544W7U8] Quote application failed: Billable already has a scheduled quote: 870
같은 워크스페이스에서 반복 실패한 다른 시도 (동일 status-board 인시던트):
2026-07-22 00:54:03 [info] [Quote][GYVGXJU55F] Quote application begins Workspace 530
2026-07-22 00:54:03 [error] [Quote][GYVGXJU55F] Quote application failed: Billable already has a scheduled quote: 870
2026-07-22 00:54:19 [error] [Quote][M9X89PV6G6] Quote application failed: Billable already has a scheduled quote: 870
2026-07-22 00:55:27 [error] [Quote][ZT5TKAT9CA] Quote application failed: Billable already has a scheduled quote: 870
2026-07-22 01:06:13 [error] [Quote][VA8VQHH64X] Quote application failed: Billable already has a scheduled quote: 870
2026-07-22 01:06:49 [error] [Quote][N7KE58TH44] Quote application failed: Billable already has a scheduled quote: 870
2026-07-22 01:08:37 [error] [Quote][U5XQ1208JW] Quote application failed: Billable already has a scheduled quote: 870
Status-board (bun run cli/incident-board.ts for-cluster ...) 결과에서 확인된 그룹핑:
scope: svc:cupixworks-api::unknown
active.id: 2026-07-21-svc-cupixworks-api--unknown-3
cluster_ids: [79f45086, 0e9768a3, 79a9ed15, ffe05413, d00e1f4b, 45bcf1d2, fdedbc62] # 7건, 모두 동일 에러
started_at: 2026-07-21T15:54:03.770Z
last_event_at: 2026-07-21T16:08:37.954Z
7건이 모두 동일 워크스페이스(530), 동일 기존 scheduled quote id (870), 동일 에러 메시지를 갖는 것이 결정적 근거.
미확인 사항 (uncertain -- needs verification):
- quote 870 을 최초로
scheduled상태로 만든 apply/조작의 로그는 Datadog 14일 retention 내에는 있을 것으로 보이나,scheduled_quote_id=870로 세팅되는 시점을 정확히 특정하지는 못했다. 필요시service:cupixworks-api "Schedule quote application for" 530로 확인 가능. - 왜 admin 이 이미 예약된 상태를 인지하지 못하고 다른 quote 를 반복 apply 했는지 (UI 상 상태 미노출 여부) 는 프런트엔드/UX 조사 필요.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Admin 이 이미 예약된 quote (870) 를 정리하지 않은 채 다른 미래일자 quote 를 반복 apply 하여 schedule_apply! 가드가 raise 됨 |
Datadog: 동일 워크스페이스 530 에 대해 7개 서로 다른 quote(872~886) 가 모두 ... : 870 로 실패. 코드: app/models/quote.rb:165-170 의 명시적 가드. |
— | Confirmed |
| H2 | Quote#apply! 로직에 회귀 버그가 생겨 정상 apply 흐름도 실패 |
만약 회귀라면 모든 워크스페이스에서 에러가 발생해야 함 | Datadog: 최근 3일간 다른 워크스페이스 (24, 130, 2424, 5866, 5867, 5868, 5869, 5870) 의 apply 는 Quote application begins 만 남고 실패 로그 없음. Workspace 530 에 국한됨. |
Rejected |
| H3 | Cupix::Cron::ScheduledQuote.activate_for cron 이 실패하여 오래된 scheduled quote 가 해소되지 않아 이후 apply 가 실패 |
cron 실패 시 동일 증상 가능. | Datadog 상 Failed to activate scheduled quote 로그가 최근 3일 내 조회되지 않음 (검색: service:cupixworks-api "Failed to activate scheduled quote"). 또한 billing_started_at 가 아직 미래일 수 있어 cron 이 정상적으로 skip 중일 가능성이 높음. |
Rejected (증거 부족 → 대체 가설로 배제) |
| H4 | 특정 리전(ap-southeast-2) 의 데이터 오염/마이그레이션 누락 | 클러스터 region 이 ap-southeast-2 | 에러 원인이 코드 가드(BILL10000)이고 스택/타입이 리전 특이 요소를 보이지 않음. 다른 리전에서 동일 유즈케이스가 발생하지 않았을 뿐. | Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- 코드 변경 불필요.
app/models/quote.rb:165-170의 가드는 정합성 유지를 위해 의도적으로 도입된 것이며(BILL10000, TSLA-12975), 정상 동작 중이다. - 운영 조치: Workspace 530 의 현재
scheduled_quote_id(=870) 를 확인하고, 다음 중 하나를 결정한다:- quote 870 이 정말 이번에 적용되어야 할 것이라면 그대로 두고, 반복 실패한 apply 시도는 취소.
- quote 870 이 잘못 예약된 것이라면 admin 툴/DB 로
workspaces.scheduled_quote_id및scheduled_billing_started_at을 nil 로 해제한 뒤 원하는 quote 를 다시 apply.
- 협업 필요: 어느 quote 가 최종 적용되어야 하는지는 accounting/영업 컨텍스트에 달려 있으므로 자동화된 code-fix 로 처리하지 말 것.
단기 개선 (1주 이내)#
- 에러 로그 레벨 재검토:
schedule_apply!가드로 인한 실패는 "코드 결함" 이 아니라 "운영 상태 충돌" 이므로Cupix::Logger.error대신warn으로 로깅하거나,Cupix::Errors::Billing(BILL10000) 을rescue Cupix::Errors::Billing분기로 분리하여 warn 처리하는 것이 적절하다. 현재는rescue StandardError => e하나로 모두 error 레벨 처리되어 error-sweeper 가 반복적으로 클러스터를 생성하고 status-board 인시던트를 발동시킨다. 관련 파일:app/models/quote.rb:98-104. - Admin API 응답 개선:
PUT /api/v1/admin/quotes/{id}/apply400 응답에 "이미 예약된 quote id" 를 클라이언트가 파싱 가능한 필드로 명시(현재도error.reason에 포함되어 있으나 클라이언트 UX 관점에서 별도 필드 권장). 프런트엔드 UI 협의 필요.
장기 개선 (재발 방지)#
- Admin UI 에 "scheduled quote" 상태 노출: Workspace/Team/Facility admin 화면에서 현재
scheduled_quote_id,scheduled_billing_started_at을 표시하고, "이미 예약된 quote 가 있으면 새 미래일자 quote apply 를 disable" 하는 프리체크를 추가. 프런트엔드 담당자와 협의 대상. - 문서화:
docs/license-creation-flow.md(TSLA-12975 로 추가됨) 에 "이미 scheduled 상태인 billable 에 새 미래일자 quote 를 적용하려면 먼저 기존 예약을 해제(또는 활성화)해야 한다" 는 운영 가이드 명시 여부 확인. 없다면 추가.
Monitoring#
Datadog 대시보드 timeseries widget 용 쿼리 (writing-datadog-monitoring-queries 규칙 준수: monitor-only 문법 미사용):
BILL10000 scheduled quote 가드 발동 빈도:
logs("service:cupixworks-api \"Billable already has a scheduled quote\"").index("*").rollup("count").by("env").last("1d")
Quote apply 실패 전체 (모든 원인):
logs("service:cupixworks-api \"Quote application failed\"").index("*").rollup("count").by("env").last("1d")
Admin quote apply 400 응답 비율:
logs("service:cupixworks-api \"PUT /api/v1/admin/quotes\" \"[400]\"").index("*").rollup("count").by("env").last("1d")
알림 아이디어 (운영 판단 하에):
- 같은 워크스페이스/팀에서 5분 내 3회 이상
Billable already has a scheduled quote반복 시 admin 알림 (UI 프리체크가 도입되기 전 임시 대응).
Risk Assessment#
- Risk level: low. 사용자 청구/서비스에는 영향 없음(가드가 정합성 유지). 실패한 apply 는 이후 재시도 가능.
- 예상 복잡도: trivial. 코드 변경이 필요하지 않음. 로깅 레벨 재조정만 진행할 경우 standard.