Skip to content

feat: reconcile licensed subscription quantities - #1637

Merged
kang-heewon merged 1 commit into
trunkfrom
feat/1570-reconcile-subscription-quantity
Aug 1, 2026
Merged

feat: reconcile licensed subscription quantities#1637
kang-heewon merged 1 commit into
trunkfrom
feat/1570-reconcile-subscription-quantity

Conversation

@kang-heewon

@kang-heewon kang-heewon commented Jul 30, 2026

Copy link
Copy Markdown
Member

Outcome

Subscriptions can now converge provider-billed licensed quantities from immutable, versioned membership evidence without coupling membership commits to remote billing availability.

The provider-neutral contract adds explicit licensed-quantity capability discovery, stable reconciliation and provider-operation identities, stale-source protection, deterministic retry and terminal evidence, bounded repair discovery, drift diagnostics, and lifecycle events. The SaaS golden path creates intents only after membership commit and performs provider I/O in the reconciliation worker.

Fixes #1570

Verification

  • pnpm check — 24/25 repository gates passed, 1 not applicable
  • pnpm test — 232/232 workspace tasks passed
  • pnpm typecheck — 231/231 workspace tasks passed
  • pnpm --filter @croco/billing-core test — 121/121 tests passed
  • SaaS billing golden path — 10/10 tests passed
  • pnpm public-api:check — 114 package snapshots matched
  • pnpm docs:api:check — 115/115 build tasks passed and generated API docs matched
  • pnpm provider-certification:check — 0 blocking failures
  • pnpm problem-registry:check — 540/540 codes passed
  • changeset-required verification passed
  • pre-push workspace test and typecheck hooks passed

Review gates

  • Issue fulfillment: PASS — immutable plan policy, after-commit intents, idempotent provider updates, stale-write supersession, bounded missed-event repair, unsupported capability handling, deterministic failure evidence, and required burst/outage/recovery cases are covered.
  • Repository impact and release: PASS — public exports, conformance fixtures, Problem registry, provider profile, package catalog, API docs, public API snapshot, lockfile, and changesets are synchronized.
  • CI readiness and maintainability: PASS — full repository gates pass; independent adversarial review found no remaining correctness, concurrency, observability, API, security, or scope findings.

Review focus

  • reconciliation identity remains stable while retry-safe provider operation identity changes only for newly observed drift
  • repair scheduling stays bounded and fair across subscriptions, including those previously marked in sync
  • membership transaction commits never depend on provider I/O, while after-commit intent creation remains inspectable
  • providers reporting a newer accepted source version become terminal evidence instead of entering an infinite retry loop

Residual risk

The included reconciliation store and SaaS source are in-memory reference implementations. Production compositions must provide durable implementations of both contracts and schedule the bounded repair worker.

Summary by CodeRabbit

  • 새로운 기능

    • 멤버십 수와 좌석 정책을 기반으로 구독 라이선스 수량을 자동 계산·동기화합니다.
    • 중복 이벤트, 오래된 업데이트, 제공자 불일치를 처리하고 제한된 복구를 지원합니다.
    • 수량 변경 상태, 실패 원인, 드리프트 및 복구 결과를 진단할 수 있습니다.
    • 멤버십 이벤트가 트랜잭션 커밋 후 발행됩니다.
  • 문서

    • 수량 관리 API, 문제 코드, 진단 정보 및 제공자 지원 현황을 문서화했습니다.
    • Polar의 라이선스 수량 기능 미지원 상태를 명확히 표시했습니다.
  • 테스트

    • 재시도, 복구, 동시성, 멱등성 및 제공자 적합성 검증을 추가했습니다.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Too many files!

This PR contains 215 files, which is 115 over the limit of 100.

To get a review, narrow the scope:
• coderabbit review --committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4f44aac4-0385-4198-89f9-0a1310fb4c21

📥 Commits

Reviewing files that changed from the base of the PR and between a06ab31 and 92b0f73.

📒 Files selected for processing (215)
  • .changeset/calm-seats-reconcile.md
  • README.md
  • docs/package-catalog.json
  • docs/package-docs-report.md
  • docs/problem-code-registry.json
  • examples/saas-billing-golden-path/README.md
  • examples/saas-billing-golden-path/package.json
  • examples/saas-billing-golden-path/src/integrations/MembershipSeatQuantitySource.ts
  • examples/saas-billing-golden-path/src/tests/membership-quantity.spec.ts
  • packages/billing-core/README.md
  • packages/billing-core/src/index.ts
  • packages/billing-core/src/libs/BillingProvider.ts
  • packages/billing-core/src/libs/BillingProviderCapabilities.ts
  • packages/billing-core/src/libs/InMemoryPlanRegistry.ts
  • packages/billing-core/src/libs/SubscriptionQuantity.ts
  • packages/billing-core/src/libs/problems/BillingProblems.ts
  • packages/billing-core/src/tests/BillingProvider.spec.ts
  • packages/billing-core/src/tests/InMemoryPlanRegistry.spec.ts
  • packages/billing-core/src/tests/SubscriptionQuantityReconciler.spec.ts
  • packages/billing-core/src/types.ts
  • packages/billing-polar/src/libs/PolarBillingDiagnosticsProvider.ts
  • packages/billing-polar/src/libs/PolarBillingProviderProfile.ts
  • packages/billing-polar/src/tests/PolarBillingDiagnosticsProvider.spec.ts
  • packages/billing-polar/src/tests/PolarBillingProviderProfile.spec.ts
  • packages/billing-polar/src/tests/PolarWebhookHandler.spec.ts
  • packages/billing-polar/src/tests/PolarWebhookHandlerSdk.spec.ts
  • packages/docs/src/content/docs/api/auth-core/src/classes/AesGcmApiKeyRotationProtector.md
  • packages/docs/src/content/docs/api/auth-core/src/classes/ApiKeyManager.md
  • packages/docs/src/content/docs/api/auth-core/src/classes/ApiKeyRotationConflictProblem.md
  • packages/docs/src/content/docs/api/auth-core/src/classes/ApiKeyRotationProtectionProblem.md
  • packages/docs/src/content/docs/api/auth-core/src/classes/ApiKeyStore.md
  • packages/docs/src/content/docs/api/auth-core/src/classes/InvalidApiKeyRotationIdempotencyKeyProblem.md
  • packages/docs/src/content/docs/api/auth-core/src/interfaces/ApiKeyRotationProtector.md
  • packages/docs/src/content/docs/api/auth-core/src/type-aliases/AesGcmApiKeyRotationProtectorOptions.md
  • packages/docs/src/content/docs/api/auth-core/src/type-aliases/ApiKeyRotation.md
  • packages/docs/src/content/docs/api/auth-core/src/type-aliases/ApiKeyRotationProtectionContext.md
  • packages/docs/src/content/docs/api/auth-core/src/type-aliases/RotateApiKeyResult.md
  • packages/docs/src/content/docs/api/auth-core/src/variables/API_KEY_ROTATION_PROTECTOR_TOKEN.md
  • packages/docs/src/content/docs/api/auth-drizzle/src/classes/DrizzleApiKeyStore.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/BillingLifecycleCommandConflictProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/BillingLifecycleCommandInProgressProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/BillingLifecycleCommandNotFoundProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/BillingProvider.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/BillingService.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/BillingStore.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/InMemoryBillingStore.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/InMemorySubscriptionQuantityReconciliationStore.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/InvalidBillingLifecycleIdempotencyKeyProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/InvalidSubscriptionQuantityProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/ProviderCapabilityUnavailableProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionCanceledEvent.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityDriftDetectedEvent.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityDriftRecoveredEvent.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityProviderMismatchProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityProviderSourceAheadProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityReconciler.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityReconciliationConflictProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityReconciliationFailedEvent.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityReconciliationFailedProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityReconciliationSucceededEvent.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantitySourceMismatchProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/functions/calculateDesiredQuantity.md
  • packages/docs/src/content/docs/api/billing-core/src/functions/createSubscriptionQuantityReconciliationId.md
  • packages/docs/src/content/docs/api/billing-core/src/functions/defineBillingProvider.md
  • packages/docs/src/content/docs/api/billing-core/src/functions/defineBillingProviderProfile.md
  • packages/docs/src/content/docs/api/billing-core/src/interfaces/BillingGateway.md
  • packages/docs/src/content/docs/api/billing-core/src/interfaces/LicensedQuantityGateway.md
  • packages/docs/src/content/docs/api/billing-core/src/interfaces/SubscriptionQuantityReconciliationStore.md
  • packages/docs/src/content/docs/api/billing-core/src/interfaces/SubscriptionQuantityRepairSource.md
  • packages/docs/src/content/docs/api/billing-core/src/interfaces/SubscriptionQuantitySource.md
  • packages/docs/src/content/docs/api/billing-core/src/interfaces/UsageBillingGateway.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/BillingLifecycleCommand.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/BillingLifecycleCommandFailure.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/BillingProviderCapability.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/BillingProviderCapabilityProfile.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/BillingProviderImplementations.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/BillingProviderProfile.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/BillingServiceDependencies.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/CancelSubscriptionParams.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/CreateSubscriptionQuantityIntent.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/CustomerMeterState.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/CustomerMeterStateQuery.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/DuplicateUsageBillingEventReceipt.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/InsertedUsageBillingEventReceipt.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/LicensedQuantityObservation.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/MembershipRole.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/PlanVersionDefinition.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/ReconcileBillingLifecycleCommandsResult.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/ReconcileSubscriptionQuantitiesResult.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/ReconcileSubscriptionQuantityInput.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/ResumeSubscriptionParams.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SetLicensedQuantityInput.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SetLicensedQuantityResult.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityDiagnostics.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityFailureEvidence.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityPolicy.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityReconcilerDependencies.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityReconciliationEventPublisher.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityReconciliationState.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantitySnapshot.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantitySourceInput.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantitySourceSnapshot.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/UsageBillingEvent.md
  • packages/docs/src/content/docs/api/billing-core/src/variables/BILLING_PROVIDER_CAPABILITIES.md
  • packages/docs/src/content/docs/api/billing-polar/src/classes/PolarCheckoutIdempotencyConflictProblem.md
  • packages/docs/src/content/docs/api/billing-polar/src/variables/POLAR_BILLING_PROVIDER_PROFILE.md
  • packages/docs/src/content/docs/api/credits-core/src/classes/CreditLedgerStore.md
  • packages/docs/src/content/docs/api/entitlements-core/src/classes/EntitlementOverageAllowedEvent.md
  • packages/docs/src/content/docs/api/entitlements-core/src/classes/InMemoryPlanEntitlementRegistry.md
  • packages/docs/src/content/docs/api/entitlements-core/src/classes/PlanEntitlementRegistry.md
  • packages/docs/src/content/docs/api/entitlements-core/src/classes/StaticSubscriptionProvider.md
  • packages/docs/src/content/docs/api/entitlements-core/src/classes/SubscriptionProvider.md
  • packages/docs/src/content/docs/api/entitlements-core/src/type-aliases/EntitlementCheckResult.md
  • packages/docs/src/content/docs/api/entitlements-core/src/type-aliases/EntitlementRule.md
  • packages/docs/src/content/docs/api/entitlements-core/src/type-aliases/PlanEntitlements.md
  • packages/docs/src/content/docs/api/entitlements-drizzle/src/classes/BillingStoreSubscriptionProvider.md
  • packages/docs/src/content/docs/api/entitlements-drizzle/src/classes/DrizzlePlanEntitlementRegistry.md
  • packages/docs/src/content/docs/api/events-core/src/classes/DomainEvent.md
  • packages/docs/src/content/docs/api/events-core/src/classes/EventAfterCommitOutcomeRequiredProblem.md
  • packages/docs/src/content/docs/api/execution-core/src/classes/ExecutionProblems.md
  • packages/docs/src/content/docs/api/execution-core/src/classes/ExecutionStore.md
  • packages/docs/src/content/docs/api/execution-core/src/classes/InvalidContinuationLeaseDurationProblem.md
  • packages/docs/src/content/docs/api/execution-core/src/enumerations/ExecutionProblemCode.md
  • packages/docs/src/content/docs/api/execution-core/src/functions/createExecutionCheckpointStoreConformanceSuite.md
  • packages/docs/src/content/docs/api/execution-core/src/interfaces/CreateExecutionParams.md
  • packages/docs/src/content/docs/api/execution-core/src/interfaces/CreateExecutionRecordParams.md
  • packages/docs/src/content/docs/api/execution-core/src/interfaces/Execution.md
  • packages/docs/src/content/docs/api/execution-core/src/interfaces/ExecutionManagerOptions.md
  • packages/docs/src/content/docs/api/execution-core/src/type-aliases/ExecutionCheckpointStoreConformanceOptions.md
  • packages/docs/src/content/docs/api/execution-core/src/type-aliases/ExecutionCheckpointWrite.md
  • packages/docs/src/content/docs/api/execution-core/src/type-aliases/InvalidContinuationLeaseDurationProblemOptions.md
  • packages/docs/src/content/docs/api/execution-core/src/type-aliases/PreparedExecutionCheckpoint.md
  • packages/docs/src/content/docs/api/execution-core/src/variables/MAX_CONTINUATION_LEASE_DURATION_MS.md
  • packages/docs/src/content/docs/api/execution-core/src/variables/MIN_CONTINUATION_LEASE_DURATION_MS.md
  • packages/docs/src/content/docs/api/execution-drizzle/src/classes/DrizzleExecutionStore.md
  • packages/docs/src/content/docs/api/framework-context/src/interfaces/TransactionContext.md
  • packages/docs/src/content/docs/api/invitation-core/src/classes/InvitationManager.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/classes/InMemoryLifecycleRuleStateStore.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/classes/InMemoryLifecycleRunStore.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/classes/InMemoryMonetizationThresholdStore.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/classes/LifecycleRuleRegistry.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/classes/MonetizationRecipeCapabilityProblem.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/classes/MonetizationSignalDefinitionProblem.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/classes/MonetizationSubscriptionConditionTracker.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/classes/MonetizationThresholdClaimProblem.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/classes/MonetizationThresholdTracker.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/interfaces/LifecycleRuleStateStore.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/interfaces/LifecycleRunStore.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/interfaces/MonetizationThresholdStore.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/InMemoryLifecycleRuleStateStoreOptions.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/InMemoryMonetizationThresholdStoreOptions.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/LifecycleDiagnosticsDetails.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/LifecycleDiagnosticsDryRunDetails.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/LifecycleDiagnosticsProviderOptions.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/LifecycleDiagnosticsRunDetails.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/LifecycleMonetizationOperationalDiagnostic.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/LifecycleRuleExecutionClaim.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/LifecycleRunClaim.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/MonetizationConditionEvaluation.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/MonetizationConditionTransition.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/MonetizationConditionTransitionResult.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/MonetizationLifecycleArtifact.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/MonetizationLifecycleSignal.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/MonetizationRecipeCapabilities.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/MonetizationRecipeCapabilityDiagnostic.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/MonetizationRecipeDefinition.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/MonetizationRecipeDescriptor.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/MonetizationSignalData.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/MonetizationSignalDescriptor.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/MonetizationSignalEvidenceByType.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/MonetizationSignalMetadataByType.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/MonetizationThresholdClaimResult.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/MonetizationThresholdDiagnostics.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/MonetizationThresholdEvaluation.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/MonetizationThresholdScope.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/variables/MONETIZATION_SIGNAL_DESCRIPTORS.md
  • packages/docs/src/content/docs/api/metering-core/src/classes/IdempotencyManager.md
  • packages/docs/src/content/docs/api/metering-core/src/classes/InvalidUsageQueryProblem.md
  • packages/docs/src/content/docs/api/metering-core/src/classes/QuotaExceededEvent.md
  • packages/docs/src/content/docs/api/metering-core/src/classes/RedisUsageStorage.md
  • packages/docs/src/content/docs/api/metering-core/src/classes/UsageRecordedEvent.md
  • packages/docs/src/content/docs/api/metering-core/src/interfaces/RedisClient.md
  • packages/docs/src/content/docs/api/metering-core/src/interfaces/UsageStorage.md
  • packages/docs/src/content/docs/api/metering-core/src/type-aliases/MeteringProcessingClaim.md
  • packages/docs/src/content/docs/api/metering-core/src/type-aliases/PendingMeteringDelivery.md
  • packages/docs/src/content/docs/api/metering-core/src/type-aliases/UsageQueryOptions.md
  • packages/docs/src/content/docs/api/metering-upstash/src/classes/UpstashRedisClient.md
  • packages/docs/src/content/docs/api/migration-runner/src/classes/MigrationFileLoadProblem.md
  • packages/docs/src/content/docs/api/problems-core/src/classes/Problem.md
  • packages/docs/src/content/docs/api/problems-core/src/variables/CROCO_PROBLEM_CODE_REGISTRY.md
  • packages/docs/src/content/docs/api/ratelimit-upstash/src/classes/UpstashSlidingWindowStore.md
  • packages/docs/src/content/docs/api/ratelimit-upstash/src/classes/UpstashTokenBucketStore.md
  • packages/docs/src/content/docs/api/retry-core/src/classes/RetrySuccessHookProblem.md
  • packages/docs/src/content/docs/api/retry-core/src/interfaces/RetryHooks.md
  • packages/docs/src/content/docs/api/testing/src/classes/TestingTransactionContext.md
  • packages/docs/src/content/docs/api/testing/src/type-aliases/BillingProviderCapabilityConformanceOptions.md
  • packages/docs/src/content/docs/api/testing/src/type-aliases/BillingProviderConformanceOptions.md
  • packages/docs/src/content/docs/api/testing/src/type-aliases/LicensedQuantityGatewayConformanceOptions.md
  • packages/docs/src/content/docs/api/tx-core/src/classes/AfterCommitHooksProblem.md
  • packages/docs/src/content/docs/api/tx-core/src/classes/AfterCommitOutcomeRequiredProblem.md
  • packages/docs/src/content/docs/api/tx-core/src/classes/AfterCommitRegistrationClosedProblem.md
  • packages/docs/src/content/docs/api/tx-core/src/classes/DetachedTransactionOperationProblem.md
  • packages/docs/src/content/docs/api/tx-core/src/classes/TransactionOutcomeContextProblem.md
  • packages/docs/src/content/docs/api/tx-core/src/classes/TransactionOutcomeUnknownProblem.md
  • packages/docs/src/content/docs/api/tx-core/src/classes/TransactionRollbackConfirmedProblem.md
  • packages/docs/src/content/docs/api/tx-core/src/classes/TxManager.md
  • packages/docs/src/content/docs/api/tx-core/src/interfaces/TxAdapter.md
  • packages/docs/src/content/docs/api/tx-core/src/type-aliases/AfterCommitFailure.md
  • packages/docs/src/content/docs/api/tx-core/src/type-aliases/TxRunOutcome.md
  • packages/docs/src/content/docs/en/reference/extension-matrix.md
  • packages/docs/src/content/docs/en/reference/problem-recovery-cookbook.md
  • packages/membership-core/README.md
  • packages/membership-core/src/libs/MembershipManager.ts
  • packages/membership-core/src/libs/MembershipService.ts
  • packages/membership-core/src/tests/MembershipManager.spec.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

멤버십 스냅샷에서 라이선스 수량을 계산하고, 커밋 후 이벤트와 제한된 복구 스캔으로 결제 제공자 수량을 조정하는 기능을 추가했습니다. 수량 정책, provider capability, 상태·문제·이벤트·진단 API와 관련 테스트 및 문서도 추가했습니다.

Changes

라이선스 수량 조정 핵심 기능

Layer / File(s) Summary
수량 정책과 조정 계약
packages/billing-core/src/types.ts, packages/billing-core/src/libs/BillingProviderCapabilities.ts, packages/billing-core/src/libs/BillingProvider.ts, packages/billing-core/src/libs/SubscriptionQuantity.ts (1~281줄)
플랜 버전별 SubscriptionQuantityPolicy를 정의했습니다. 최소 수량, 포함 좌석, 좌석 할당량, 청구 대상 역할을 포함합니다. LicensedQuantityGateway 계약을 명시적 provider capability로 추가했습니다. SubscriptionQuantitySource 계약이 원천 snapshot과 증거를 반환합니다.
조정 실행과 저장소
packages/billing-core/src/libs/SubscriptionQuantity.ts (283~946줄)
InMemorySubscriptionQuantityReconciliationStore가 revision과 source version으로 supersede를 처리합니다. SubscriptionQuantityReconciler가 reconciliation을 실행하고, provider drift를 감지하며, 오류를 unsupported·retryable·terminal로 분류합니다. Bounded repair 스캔이 누락 전달과 재시도 가능한 실패를 복구합니다. getDiagnostics가 pending·drift·실패 상태를 집계합니다.
조정 문제와 이벤트
packages/billing-core/src/libs/problems/BillingProblems.ts, packages/billing-core/src/libs/SubscriptionQuantity.ts (787~836줄)
InvalidSubscriptionQuantityProblem, SubscriptionQuantitySourceMismatchProblem, SubscriptionQuantityReconciliationConflictProblem, SubscriptionQuantityReconciliationFailedProblem, SubscriptionQuantityProviderMismatchProblem, SubscriptionQuantityProviderSourceAheadProblem을 정의했습니다. SubscriptionQuantityDriftDetectedEvent, SubscriptionQuantityDriftRecoveredEvent, SubscriptionQuantityReconciliationSucceededEvent, SubscriptionQuantityReconciliationFailedEvent를 구현했습니다.
멤버십 이벤트 커밋 후 발행
packages/membership-core/src/libs/MembershipManager.ts, packages/membership-core/src/libs/MembershipService.ts, packages/membership-core/README.md
멤버십 생성·삭제·역할 변경 이벤트를 publishAfterCommitOrNow로 변경했습니다. 활성 트랜잭션이 있으면 커밋 후 발행하고, 없으면 즉시 발행합니다. 트랜잭션이 필요한 오류는 즉시 발행으로 대체합니다.
골든 패스 멤버십 조정
examples/saas-billing-golden-path/src/integrations/MembershipSeatQuantitySource.ts, examples/saas-billing-golden-path/src/tests/membership-quantity.spec.ts
MembershipSeatQuantitySource가 버전 관리 멤버십 스냅샷에서 활성 수·청구 대상 수·좌석 할당량을 반환합니다. MembershipQuantityReconciliationHandler가 이벤트를 의도로 변환합니다. 368줄 통합 테스트가 추가·제거·역할 변경, 장애·복구, 중복·stale, 플랜 변경을 검증합니다.
Provider 적응과 Polar
packages/testing/src/libs/billing-provider-conformance.ts, packages/billing-polar/src/libs/PolarBillingProviderProfile.ts, packages/billing-polar/src/libs/PolarBillingDiagnosticsProvider.ts, packages/testing/src/tests/testing.spec.ts
licensed-quantity 초기 조회·적용·중복·stale 거부를 검증하는 conformance 테스트를 추가했습니다. Polar는 미지원 capability를 진단으로 노출합니다.

공개 API와 문서

Layer / File(s) Summary
공개 export
packages/billing-core/src/index.ts, packages/testing/src/index.ts
calculateDesiredQuantity, createSubscriptionQuantityReconciliationId, SubscriptionQuantityReconciler, InMemorySubscriptionQuantityReconciliationStore, 4개 이벤트 클래스, 6개 Problem 클래스, 18개 타입·interface를 export했습니다. LicensedQuantityGatewayConformanceOptions를 conformance export에 추가했습니다.
API 문서
packages/docs/src/content/docs/api/billing-core/src/**, packages/docs/src/content/docs/api/testing/src/**
클래스·함수·interface·타입별 API 문서 파일을 생성했습니다. 메서드 시그니처, 매개변수, 반환 타입, 상속 관계를 명시합니다.
문제 레지스트리
docs/problem-code-registry.json, packages/docs/src/content/docs/en/reference/problem-recovery-cookbook.md
6개 billing Problem 코드(billing/invalid-subscription-quantity, billing/subscription-quantity-*)를 등록했습니다. HTTP 상태, 재시도 가능성, redaction 정책, 복구 지침, 텔레메트리를 정의합니다.
메타데이터 갱신
docs/package-catalog.json, README.md, packages/docs/src/content/docs/en/reference/extension-matrix.md, .changeset/calm-seats-reconcile.md
Polar 진단에 licensed-quantity 미지원 정보를 추가했습니다. 6개 패키지의 릴리스 버전과 변경 이유를 changeset에 명시했습니다.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MembershipService
  participant EventPublisher
  participant ReconciliationHandler
  participant SubscriptionQuantityReconciler
  participant LicensedQuantityGateway
  MembershipService->>EventPublisher: 멤버십 변경을 commitAfter로 발행 요청
  EventPublisher->>ReconciliationHandler: 커밋 후 이벤트 전달
  ReconciliationHandler->>SubscriptionQuantityReconciler: createIntent 호출
  SubscriptionQuantityReconciler->>LicensedQuantityGateway: getQuantity 호출
  SubscriptionQuantityReconciler->>LicensedQuantityGateway: setQuantity로 멱등 업데이트
  LicensedQuantityGateway-->>SubscriptionQuantityReconciler: applied, duplicate, stale 응답
  SubscriptionQuantityReconciler->>SubscriptionQuantityReconciler: snapshot 저장 및 이벤트 발행
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 멤버십 근거에 따른 라이선스 구독 수량 조정이라는 PR의 주요 변경을 간결하게 설명합니다.
Linked Issues check ✅ Passed 구현은 #1570의 수량 정책, 비동기 조정, 멱등성, stale 보호, 복구, 진단, 이벤트, 문서 및 테스트 요구를 충족합니다.
Out of Scope Changes check ✅ Passed 변경 사항은 #1570의 코어 계약, 멤버십 커밋 후 이벤트, 제공자 연동, 테스트, 문서 및 릴리스 메타데이터 범위에 포함됩니다.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/1570-reconcile-subscription-quantity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

📊 Benchmark Results

✅ All benchmarks passed

Benchmark p75 Threshold Baseline vs Baseline Status Notes
CrocoApp constructor 9.9μs 30.0ms 8.2μs +20.7% -
CrocoApp lambdaHandler (10 controllers) 349.0μs 50.0ms 258.4μs +35.0% -
Lambda cold-start simulation 453.5μs 80.0ms 418.1μs +8.5% -
Lambda cold-start with headers 543.0μs 80.0ms 369.7μs +46.9% -
Lambda cold-start with binary body 379.0μs 80.0ms 339.1μs +11.7% -
Lambda cold-start with query params 308.2μs 80.0ms 301.3μs +2.3% -
Lambda cold-start with authorizer context 303.2μs 80.0ms 299.8μs +1.1% -
Lambda cold-start realistic scenario 304.2μs 80.0ms 299.2μs +1.7% -
EventBusConfig.start (10 handlers) 2.0μs 10.0ms 1.4μs +39.1% -
EventPublisher.publishNow single event 2.1μs 2.0ms 1.7μs +24.4% -
DefaultHandlerResolver.resolve × 10 0.1μs 5.0ms 0.1μs +0.0% -
Container.get singleton (cold) 63.7μs 5.0ms 70.3μs -9.3% -
Container.register × 50 components 3.5ms 10.0ms 3.2ms +8.2% -
Container.validate (50 components) 4.0ms 20.0ms 3.4ms +17.1% -
Container.get singleton (warm) 1.7μs 500.0μs 1.6μs +1.2% -
TelemetryRuntime.init (lambda preset) 2.5μs 200.0ms 1.1ms -99.8% -
lambdaPreset config creation 1.5μs 2.0ms 1.4μs +2.8% -

Updated: 2026-08-01T13:42:56.818Z · Commit: c713b40

@kang-heewon
kang-heewon force-pushed the feat/1570-reconcile-subscription-quantity branch 5 times, most recently from 4e8fc6a to 3b12864 Compare July 31, 2026 03:27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 23

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/billing-polar/src/tests/PolarBillingProviderProfile.spec.ts (1)

5-13: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

테스트 제목을 검증 범위에 맞추십시오.

제목은 usage capability gap만 언급합니다. 이 테스트는 이제 licensed-quantity gap도 검증합니다. 테스트 실패 출력에서 원인을 즉시 알 수 있도록 제목을 갱신하십시오.

🔧 제안 수정
-  it("publishes checkout support and an explicit usage capability gap", () => {
+  it("publishes checkout support and explicit usage and licensed-quantity capability gaps", () => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/billing-polar/src/tests/PolarBillingProviderProfile.spec.ts` around
lines 5 - 13, Update the test title in PolarBillingProviderProfile.spec.ts
around POLAR_BILLING_PROVIDER_PROFILE to mention both the usage capability gap
and the licensed-quantity capability gap, matching the assertions covered by the
test.
packages/membership-core/src/libs/MembershipManager.ts (1)

35-219: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

MembershipManagerMembershipService가 동일한 구현을 중복 유지함. 두 클래스는 생성자, addMember, removeMember, updateRole, transferOwnership, checkSeatLimit, ensureValidRole, getMembershipOrThrow, 그리고 이번에 추가된 publishSafely/publishAfterCommitOrNow까지 거의 전부 동일하다. 근본 원인은 하나의 로직이 두 클래스에 각각 구현되어 있다는 점이며, 이번 이벤트 발행 방식 변경도 두 파일에 똑같이 적용해야 했다.

  • packages/membership-core/src/libs/MembershipManager.ts#L35-L219: 공유 가능한 로직(특히 publishSafely/publishAfterCommitOrNow)을 공통 기반 클래스나 헬퍼로 추출한다.
  • packages/membership-core/src/libs/MembershipService.ts#L35-L219: 추출된 공통 로직에 위임하도록 변경하고, 중복된 메서드 본문을 제거한다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/membership-core/src/libs/MembershipManager.ts` around lines 35 -
219, MembershipManager와 MembershipService에 동일한 멤버십 로직이 중복되어 있으므로 공통 기반 클래스 또는
헬퍼로 생성자와 addMember, removeMember, updateRole, transferOwnership, checkSeatLimit,
ensureValidRole, getMembershipOrThrow, publishSafely, publishAfterCommitOrNow를
추출한다. packages/membership-core/src/libs/MembershipManager.ts#L35-219의 구현을 공통
로직으로 정리하고, packages/membership-core/src/libs/MembershipService.ts#L35-219는 해당 공통
구현에 위임하도록 변경해 중복 메서드 본문을 제거한다.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/calm-seats-reconcile.md:
- Line 4: Update the changeset entry for `@croco/membership-core` from patch to
minor, reflecting the publish timing behavior change introduced by
publishAfterCommitOrNow prioritizing publishAfterCommit on transactional paths.

In `@docs/package-catalog.json`:
- Around line 744-750: Update the package entry’s features array to replace
“licensed quantity capability diagnostic” with wording that clearly identifies
it as unsupported, consistent with the existing knownGaps entry. Then rerun the
catalog generation script so the generated README Features output reflects the
corrected wording.

In `@packages/billing-core/src/libs/problems/BillingProblems.ts`:
- Around line 222-274: Remove externalSubscriptionId from the detail messages in
SubscriptionQuantityProviderMismatchProblem and
SubscriptionQuantityReconciliationFailedProblem, keeping only safe descriptions;
pass the identifier through the Problem options extensions instead so it remains
available for diagnostics without being exposed in serialized 5xx responses.

In `@packages/billing-core/src/libs/SubscriptionQuantity.ts`:
- Around line 697-708: Update
packages/billing-core/src/libs/SubscriptionQuantity.ts lines 697-708 in fail()
so a null saveIfCurrent() result is handled using the findCurrent() fallback
pattern already used by complete() at lines 635-642; if persistence still fails,
record the error with a diagnostic code and skip publishing the failure event.
Update lines 403-423 so the repair flow no longer passes item.snapshot to
fail(); fetch the latest snapshot with findCurrent() and pass that value
instead.
- Around line 429-431: Update SubscriptionQuantity.getDiagnostics so diagnostics
are not limited to listRecent(limit): use a durable-store status aggregation
query that covers the full dataset, and extend SubscriptionQuantityDiagnostics
with a field indicating whether results were truncated when only a limited
sample is available. Preserve ensureRepairLimit validation and clearly populate
the truncation indicator in every result.
- Around line 490-494: Update the provider operation ID selection in the
reconciliation logic around the snapshot state check to reuse
snapshot.providerOperationId only for retryable failures that are not
SubscriptionQuantityProviderMismatchProblem cases. When a provider quantity
mismatch is observed, clear or bypass the persisted operation ID so the next
attempt generates a fresh `${reconciliationId}:${snapshot.revision + 1}`
identity; apply the same behavior to the corresponding logic around the
additional referenced range.
- Around line 438-455: Update the diagnostics aggregation around
providerMismatches and oldestPendingAt: exclude state === "drifted" from
providerMismatches, leaving only the provider-mismatch failure codes, and
calculate oldestPendingAt using createdAt rather than updatedAt while preserving
the pending/drifted filter.
- Around line 681-689: Update the retryable calculation in the subscription
quantity retry flow to include timeout responses with status 408 and rate-limit
problems categorized as ProblemCategory.TooManyRequests, while preserving the
existing exclusion for ProviderCapabilityUnavailableProblem and the current
500-or-higher handling.
- Around line 271-280: Update SubscriptionQuantityReconcilerDependencies to
expose the minimum I/O controls required by LicensedQuantityGateway: AbortSignal
or timeoutMs, plus circuit-breaker options. Thread these settings through
reconcileWithinSpan() so its direct gateway.getQuantity() and
gateway.setQuantity() calls honor cancellation, timeout, and circuit-breaker
behavior.

In `@packages/billing-core/src/tests/BillingProvider.spec.ts`:
- Around line 99-106: BillingProvider 런타임 게이팅 테스트에 licensed-quantity 지원을 true로
설정한 프로필과 해당 LicensedQuantityGateway 구현이 없는 구성을 추가하고, BillingProvider 생성자가
ProviderCapabilityUnavailableProblem을 던지는지 rejects.toThrow 또는
expect(...).toThrow로 검증하십시오. 기존 expectTypeOf 타입 검증은 유지하세요.

In `@packages/billing-core/src/tests/InMemoryPlanRegistry.spec.ts`:
- Around line 252-279: Expand the “requires an internally valid quantity policy
on every plan version” test to also assert rejection for includedSeats < 0,
seatQuota < 0, and an empty billableMembershipRoles array. Reuse the existing
createVersion and InvalidPlanVersionDefinitionProblem expectations while
preserving the current minimumQuantity and duplicate-role cases.

In `@packages/billing-core/src/tests/SubscriptionQuantityReconciler.spec.ts`:
- Around line 613-652: Move the “BillingProvider licensed-quantity capability”
describe block from SubscriptionQuantityReconciler.spec.ts into
packages/billing-core/src/tests/BillingProvider.spec.ts, preserving its existing
assertions and setup. Keep the test focused on defineBillingProvider,
provider.profile, and requireCapability rather than the subscription reconciler.
- Around line 237-247: Wrap the second reconcile flow around
reconcile(reconciler), releaseFirstGet, and staleResult in a try/finally block
so releaseFirstGet is always invoked when the current reconcile throws. Keep the
existing assertions and result handling unchanged while ensuring the blocked
first get is released before the test exits.

In `@packages/billing-core/src/types.ts`:
- Around line 129-134: Define a literal MembershipRole union within billing-core
and update SubscriptionQuantityPolicy.billableMembershipRoles to use readonly
MembershipRole[] instead of readonly string[].

In `@packages/billing-polar/src/libs/PolarBillingDiagnosticsProvider.ts`:
- Around line 102-104: Update the diagnostics capability output in
PolarBillingDiagnosticsProvider to derive entirely from
POLAR_BILLING_PROVIDER_PROFILE.capabilities instead of hardcoded checkout,
subscription-lifecycle, customer-portal, and licensedQuantity fields. Iterate
the profile capabilities and report each capability’s supported status and
reason, using the canonical BILLING_PROVIDER_CAPABILITIES terminology so
unsupported usage is included and representation is consistent. Update the
corresponding expectations in PolarBillingDiagnosticsProvider.spec.ts.

In
`@packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityDriftDetectedEvent.md`:
- Line 8: Update the source comments for SubscriptionQuantityDriftDetectedEvent
to describe the drift event and its event payload, and for
SubscriptionQuantityPolicy to describe minimum quantity, included seats, seat
limits, and membership role. Then regenerate both public API documents:
packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityDriftDetectedEvent.md:8-8
and
packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityPolicy.md:10-10.

In
`@packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityReconciliationFailedEvent.md`:
- Line 8: packages/billing-core/src/index.ts의 SubscriptionQuantityReconciler,
InMemorySubscriptionQuantityReconciliationStore, 세 reconciliation 이벤트,
calculateDesiredQuantity, createSubscriptionQuantityReconciliationId에 각각의 역할과
evidence/quantity 또는 계산 동작을 설명하는 정확한 TypeDoc JSDoc을 작성한 뒤 astro build로 문서를
재생성하세요. 생성 결과는
packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityReconciliationFailedEvent.md
8-8,
packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityReconciliationSucceededEvent.md
8-8,
packages/docs/src/content/docs/api/billing-core/src/functions/calculateDesiredQuantity.md
10-10을 포함해 각 API 설명이 해당 심볼의 역할과 일치하도록 갱신되어야 합니다.

In
`@packages/docs/src/content/docs/api/billing-core/src/functions/createSubscriptionQuantityReconciliationId.md`:
- Line 10: createSubscriptionQuantityReconciliationId의 TypeScript 원본 문서 주석을 찾아
인메모리 저장소 설명을 제거하고, 테넌트·구독·플랜 버전·희망 수량·source version으로 결정적인 reconciliation ID를
생성하는 함수 계약을 설명하도록 수정하세요. 이후 TypeDoc을 재생성해 해당 Markdown 생성물을 갱신하고, 생성된 파일을 직접 편집하지
마세요.

In `@packages/membership-core/src/tests/MembershipManager.spec.ts`:
- Around line 122-135: MembershipManager.updateRole lacks coverage verifying
deferred event publication during an active transaction. Add an updateRole test
in MembershipManager.spec.ts using the existing addMember/removeMember pattern,
asserting the event is sent through publishAfterCommit and publishNow is not
called.

In `@packages/membership-core/src/tests/MembershipService.spec.ts`:
- Around line 147-153: MembershipService 테스트에는 활성 트랜잭션에서 이벤트가 지연 발행되는 동작 검증이
없습니다. MembershipService.spec.ts의 기존 membership-created 및 membership-removed 테스트에
활성 트랜잭션을 설정하고 publishAfterCommit 호출을 검증하는 케이스를 추가하며, 같은 상황에서 publishNow가 호출되지 않는
것도 확인하세요. 기존 publishAfterCommit 폴백 mock과 publishAfterCommitOrNow 경로를 재사용하고,
트랜잭션이 없거나 폴백이 필요한 테스트 동작은 변경하지 마세요.

In `@packages/testing/src/libs/billing-provider-conformance.ts`:
- Line 143: Update LicensedQuantityGatewayConformanceOptions usage in
BillingProviderConformanceOptions and the related
BillingGatewayConformanceOptions definitions so the unused TGateway generic is
removed consistently, unless a concrete gateway type is required; if retained,
add and propagate the gateway generic through BillingProviderConformanceOptions
instead of relying on the default LicensedQuantityGateway.
- Line 296: Update the two assert.equal calls checking first.status in the
billing provider conformance flow to include failure messages containing
providerName, matching the message style used by the other assertions in this
file.

In `@packages/testing/src/tests/testing.spec.ts`:
- Around line 177-202: 분리되지 않은 구독별 상태 때문에 테스트 더블이 다중 구독 격리를 검증하지 못합니다.
packages/testing/src/tests/testing.spec.ts 177-202의
InMemoryLicensedQuantityGateway에서 quantity와 acceptedSourceVersion을
externalSubscriptionId 키의 Map으로 관리하고, getQuantity(externalSubscriptionId)와
setQuantity의 상태 조회·갱신에 해당 키를 사용하십시오.
packages/billing-core/src/tests/SubscriptionQuantityReconciler.spec.ts 74-99의
ConformingQuantityGateway에서도 quantity, acceptedSourceVersion, results를
externalSubscriptionId별로 분리하여 구독별 커서 테스트가 독립적인 수량을 검증하도록 수정하십시오.

---

Outside diff comments:
In `@packages/billing-polar/src/tests/PolarBillingProviderProfile.spec.ts`:
- Around line 5-13: Update the test title in PolarBillingProviderProfile.spec.ts
around POLAR_BILLING_PROVIDER_PROFILE to mention both the usage capability gap
and the licensed-quantity capability gap, matching the assertions covered by the
test.

In `@packages/membership-core/src/libs/MembershipManager.ts`:
- Around line 35-219: MembershipManager와 MembershipService에 동일한 멤버십 로직이 중복되어
있으므로 공통 기반 클래스 또는 헬퍼로 생성자와 addMember, removeMember, updateRole,
transferOwnership, checkSeatLimit, ensureValidRole, getMembershipOrThrow,
publishSafely, publishAfterCommitOrNow를 추출한다.
packages/membership-core/src/libs/MembershipManager.ts#L35-219의 구현을 공통 로직으로
정리하고, packages/membership-core/src/libs/MembershipService.ts#L35-219는 해당 공통 구현에
위임하도록 변경해 중복 메서드 본문을 제거한다.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3f27ceb3-dc01-481a-9a77-3d2cd960a61d

📥 Commits

Reviewing files that changed from the base of the PR and between 88c6ce1 and 3b12864.

⛔ Files ignored due to path filters (2)
  • packages/problems-core/src/generated/problem-code-registry.ts is excluded by !**/generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (75)
  • .changeset/calm-seats-reconcile.md
  • README.md
  • docs/package-catalog.json
  • docs/package-docs-report.md
  • docs/problem-code-registry.json
  • examples/saas-billing-golden-path/README.md
  • examples/saas-billing-golden-path/package.json
  • examples/saas-billing-golden-path/src/integrations/MembershipSeatQuantitySource.ts
  • examples/saas-billing-golden-path/src/tests/membership-quantity.spec.ts
  • packages/billing-core/README.md
  • packages/billing-core/src/index.ts
  • packages/billing-core/src/libs/BillingProvider.ts
  • packages/billing-core/src/libs/BillingProviderCapabilities.ts
  • packages/billing-core/src/libs/InMemoryPlanRegistry.ts
  • packages/billing-core/src/libs/SubscriptionQuantity.ts
  • packages/billing-core/src/libs/problems/BillingProblems.ts
  • packages/billing-core/src/tests/BillingProvider.spec.ts
  • packages/billing-core/src/tests/InMemoryPlanRegistry.spec.ts
  • packages/billing-core/src/tests/SubscriptionQuantityReconciler.spec.ts
  • packages/billing-core/src/types.ts
  • packages/billing-polar/src/libs/PolarBillingDiagnosticsProvider.ts
  • packages/billing-polar/src/libs/PolarBillingProviderProfile.ts
  • packages/billing-polar/src/tests/PolarBillingDiagnosticsProvider.spec.ts
  • packages/billing-polar/src/tests/PolarBillingProviderProfile.spec.ts
  • packages/billing-polar/src/tests/PolarWebhookHandler.spec.ts
  • packages/billing-polar/src/tests/PolarWebhookHandlerSdk.spec.ts
  • packages/docs/src/content/docs/api/billing-core/src/classes/InMemorySubscriptionQuantityReconciliationStore.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/InvalidSubscriptionQuantityProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityDriftDetectedEvent.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityDriftRecoveredEvent.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityProviderMismatchProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityProviderSourceAheadProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityReconciler.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityReconciliationConflictProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityReconciliationFailedEvent.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityReconciliationFailedProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityReconciliationSucceededEvent.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantitySourceMismatchProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/functions/calculateDesiredQuantity.md
  • packages/docs/src/content/docs/api/billing-core/src/functions/createSubscriptionQuantityReconciliationId.md
  • packages/docs/src/content/docs/api/billing-core/src/interfaces/LicensedQuantityGateway.md
  • packages/docs/src/content/docs/api/billing-core/src/interfaces/SubscriptionQuantityReconciliationStore.md
  • packages/docs/src/content/docs/api/billing-core/src/interfaces/SubscriptionQuantityRepairSource.md
  • packages/docs/src/content/docs/api/billing-core/src/interfaces/SubscriptionQuantitySource.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/CreateSubscriptionQuantityIntent.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/LicensedQuantityObservation.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/PlanVersionDefinition.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/ReconcileSubscriptionQuantitiesResult.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/ReconcileSubscriptionQuantityInput.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SetLicensedQuantityInput.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SetLicensedQuantityResult.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityDiagnostics.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityFailureEvidence.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityPolicy.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityReconcilerDependencies.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityReconciliationEventPublisher.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityReconciliationState.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantitySnapshot.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantitySourceInput.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantitySourceSnapshot.md
  • packages/docs/src/content/docs/api/events-core/src/classes/DomainEvent.md
  • packages/docs/src/content/docs/api/testing/src/type-aliases/LicensedQuantityGatewayConformanceOptions.md
  • packages/docs/src/content/docs/en/reference/extension-matrix.md
  • packages/docs/src/content/docs/en/reference/problem-recovery-cookbook.md
  • packages/membership-core/README.md
  • packages/membership-core/src/libs/MembershipManager.ts
  • packages/membership-core/src/libs/MembershipService.ts
  • packages/membership-core/src/tests/MembershipManager.spec.ts
  • packages/membership-core/src/tests/MembershipService.spec.ts
  • packages/metrics-billing/src/tests/BillingEventHandler.spec.ts
  • packages/testing/src/index.ts
  • packages/testing/src/libs/billing-provider-conformance.ts
  • packages/testing/src/tests/BillingProviderCapabilityConformance.spec.ts
  • packages/testing/src/tests/testing.spec.ts
  • public-api-surface.snapshot.json

Comment thread .changeset/calm-seats-reconcile.md Outdated
Comment thread docs/package-catalog.json
Comment thread packages/billing-core/src/libs/problems/BillingProblems.ts
Comment thread packages/billing-core/src/libs/SubscriptionQuantity.ts
Comment thread packages/billing-core/src/libs/SubscriptionQuantity.ts Outdated
Comment thread packages/membership-core/src/tests/MembershipService.spec.ts
Comment thread packages/testing/src/libs/billing-provider-conformance.ts
Comment thread packages/testing/src/libs/billing-provider-conformance.ts Outdated
Comment thread packages/testing/src/tests/testing.spec.ts
@kang-heewon
kang-heewon force-pushed the feat/1570-reconcile-subscription-quantity branch from 3b12864 to 6d77cc8 Compare July 31, 2026 06:42
@kang-heewon

Copy link
Copy Markdown
Member Author

@coderabbitai resolve

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/membership-core/src/libs/MembershipManager.ts (1)

206-218: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

중복 멤버십 구현을 단일 클래스로 유지하십시오. MembershipManagerMembershipService의 본문이 동일하지만 MembershipService는 현재 패키지와 테스트에서 사용 중이라 단순히 삭제할 수 없습니다. MembershipServiceMembershipManager 래퍼로 바꾸거나, 실제 서비스职责이 추가되어야 할 경우 본문 중첩을 피하도록 분리하십시오.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/membership-core/src/libs/MembershipManager.ts` around lines 206 -
218, 중복된 멤버십 구현을 단일 클래스로 유지하도록
packages/membership-core/src/libs/MembershipManager.ts 206-218의
MembershipManager 구현을 기준으로 정리하고,
packages/membership-core/src/libs/MembershipService.ts 206-218의 동일한 본문은
MembershipManager를 위임하는 래퍼로 변경하십시오. 기존 패키지와 테스트의 MembershipService 사용 계약은 유지하고,
두 클래스에 동일한 로직이 남지 않도록 하십시오.
♻️ Duplicate comments (1)
packages/billing-core/src/libs/problems/BillingProblems.ts (1)

239-249: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

두 Conflict Problem의 detailexternalSubscriptionId가 남아 있습니다.

이전 리뷰에서 SubscriptionQuantityProviderMismatchProblemSubscriptionQuantityReconciliationFailedProblemdetail에서 식별자를 제거하고 extensions로 옮겼습니다. 그러나 SubscriptionQuantityReconciliationConflictProblem(Line 246)과 SubscriptionQuantityProviderSourceAheadProblem(Line 286)은 여전히 detail 문자열에 externalSubscriptionId를 포함합니다. 두 Problem 모두 ProblemCategory.Conflict이므로 409 응답으로 클라이언트에 도달할 수 있습니다. 같은 근본 원인이므로 동일하게 처리하십시오.

🔒️ 제안 수정
   constructor(externalSubscriptionId: string, sourceVersion: number) {
     super(
       undefined,
       undefined,
-      `Subscription '${externalSubscriptionId}' source version ${sourceVersion} resolved to conflicting quantity intents`,
+      `Source version ${sourceVersion} resolved to conflicting quantity intents`,
+      { extensions: { externalSubscriptionId } },
     );
   }
     super(
       undefined,
       undefined,
-      `Subscription '${externalSubscriptionId}' provider accepted source version ${providerSourceVersion} ahead of local version ${localSourceVersion}`,
+      `Provider accepted source version ${providerSourceVersion} ahead of local version ${localSourceVersion}`,
+      { extensions: { externalSubscriptionId } },
     );
   }

Also applies to: 275-289

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/billing-core/src/libs/problems/BillingProblems.ts` around lines 239
- 249, Update SubscriptionQuantityReconciliationConflictProblem and
SubscriptionQuantityProviderSourceAheadProblem so their detail messages no
longer include externalSubscriptionId; move the identifier into the Problems’
extensions, matching SubscriptionQuantityProviderMismatchProblem and
SubscriptionQuantityReconciliationFailedProblem while preserving the existing
conflict codes and messages otherwise.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/billing-core/README.md`:
- Around line 202-210: Update the README SubscriptionQuantityReconciler example
to replace the nonexistent billingProvider.require() call with
BillingProvider.requireCapability("licensed-quantity"), matching the API used
elsewhere in the README.
- Around line 109-110: Update the defineBillingProviderProfile() example in the
README to include the required "licensed-quantity" capability alongside
"checkout" and "usage", so it conforms to BillingProviderCapabilityProfile.

In `@packages/billing-core/src/index.ts`:
- Around line 155-170: Combine the eight value exports from
./libs/SubscriptionQuantity into a single grouped export block in the index
barrel. Preserve each symbol and its descriptive comment within the block,
following the category-grouping convention; no separate type exports are
involved here.

In `@packages/billing-core/src/libs/InMemoryPlanRegistry.ts`:
- Around line 131-140: Update the plan-version validation around the
quantityPolicy checks to first verify that planVersion.quantityPolicy exists and
is non-null, returning or raising InvalidPlanVersionDefinitionProblem through
the existing validation path when it is missing. Only access minimumQuantity,
includedSeats, seatQuota, or billableMembershipRoles after this guard; preserve
the current field-value validation for present policies.

In `@packages/billing-core/src/libs/SubscriptionQuantity.ts`:
- Around line 914-927: Update freezeSnapshot() to clone every Date field in
SubscriptionQuantitySnapshot before freezing, including createdAt, updatedAt,
lastAttemptAt, and lastSuccessAt, matching cloneSnapshot()’s defensive-copy
behavior; preserve the existing sourceEvidence and lastFailure freezing while
ensuring callers cannot mutate stored timestamp objects.
- Around line 413-418: Update the result-state aggregation in repair() so only
actual failure states increment failed; do not classify drifted or pending as
failures, and preserve the existing in_sync and superseded counts. If those
non-failure states must be exposed, add dedicated pending and drifted fields to
ReconcileSubscriptionQuantitiesResult and populate them separately, including
unsupported only according to its defined failure semantics.

In `@packages/billing-core/src/tests/SubscriptionQuantityReconciler.spec.ts`:
- Around line 159-183: Update createFixture to accept an options object
containing eventPublisher and maxAttempts, using the existing event publisher
and reconciliation configuration as defaults. Pass maxAttempts into
SubscriptionQuantityReconciler and allow callers to override eventPublisher,
then refactor the five affected tests to use these options instead of rebuilding
the registry, sources, gateway, store, and reconciler.

In
`@packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityProviderMismatchProblem.md`:
- Line 8: Update the original TypeDoc comments for
SubscriptionQuantityProviderMismatchProblem and
SubscriptionQuantityProviderSourceAheadProblem so their descriptions distinguish
provider quantity mismatches from provider source versions ahead of the local
version; do not edit the generated Markdown directly. Regenerate the API
documentation, including both affected files at
packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityProviderMismatchProblem.md:8-8
and
packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityProviderSourceAheadProblem.md:8-8,
then validate with pnpm docs:api:check.

In
`@packages/docs/src/content/docs/api/billing-core/src/type-aliases/MembershipRole.md`:
- Line 10: TypeScript 원본의 MembershipRole 문서 주석을 실제 계약인 owner, admin, member,
viewer 역할 집합에 맞게 수정하세요. 생성된 MembershipRole Markdown은 직접 편집하지 말고 pnpm
docs:api:check를 실행해 문서를 재생성하고 검증하세요.

In `@packages/membership-core/src/tests/MembershipService.spec.ts`:
- Around line 133-146: Update the deferred membership-created event test around
MembershipService.addMember to assert the queued event’s data with toEqual,
including tenantId "tenant-1", userId "user-1", and role "member", while
retaining the existing length and publishNow assertions; remove any unused
type-only import or toBeInstanceOf usage related to this test.

---

Outside diff comments:
In `@packages/membership-core/src/libs/MembershipManager.ts`:
- Around line 206-218: 중복된 멤버십 구현을 단일 클래스로 유지하도록
packages/membership-core/src/libs/MembershipManager.ts 206-218의
MembershipManager 구현을 기준으로 정리하고,
packages/membership-core/src/libs/MembershipService.ts 206-218의 동일한 본문은
MembershipManager를 위임하는 래퍼로 변경하십시오. 기존 패키지와 테스트의 MembershipService 사용 계약은 유지하고,
두 클래스에 동일한 로직이 남지 않도록 하십시오.

---

Duplicate comments:
In `@packages/billing-core/src/libs/problems/BillingProblems.ts`:
- Around line 239-249: Update SubscriptionQuantityReconciliationConflictProblem
and SubscriptionQuantityProviderSourceAheadProblem so their detail messages no
longer include externalSubscriptionId; move the identifier into the Problems’
extensions, matching SubscriptionQuantityProviderMismatchProblem and
SubscriptionQuantityReconciliationFailedProblem while preserving the existing
conflict codes and messages otherwise.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1687c90c-c667-41f5-9fc5-62caaedfdfba

📥 Commits

Reviewing files that changed from the base of the PR and between 3b12864 and 6d77cc8.

⛔ Files ignored due to path filters (2)
  • packages/problems-core/src/generated/problem-code-registry.ts is excluded by !**/generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (84)
  • .changeset/calm-seats-reconcile.md
  • README.md
  • docs/package-catalog.json
  • docs/package-docs-report.md
  • docs/problem-code-registry.json
  • examples/saas-billing-golden-path/README.md
  • examples/saas-billing-golden-path/package.json
  • examples/saas-billing-golden-path/src/integrations/MembershipSeatQuantitySource.ts
  • examples/saas-billing-golden-path/src/tests/membership-quantity.spec.ts
  • packages/billing-core/README.md
  • packages/billing-core/src/index.ts
  • packages/billing-core/src/libs/BillingProvider.ts
  • packages/billing-core/src/libs/BillingProviderCapabilities.ts
  • packages/billing-core/src/libs/InMemoryPlanRegistry.ts
  • packages/billing-core/src/libs/SubscriptionQuantity.ts
  • packages/billing-core/src/libs/problems/BillingProblems.ts
  • packages/billing-core/src/tests/BillingProvider.spec.ts
  • packages/billing-core/src/tests/InMemoryPlanRegistry.spec.ts
  • packages/billing-core/src/tests/SubscriptionQuantityReconciler.spec.ts
  • packages/billing-core/src/types.ts
  • packages/billing-polar/src/libs/PolarBillingDiagnosticsProvider.ts
  • packages/billing-polar/src/libs/PolarBillingProviderProfile.ts
  • packages/billing-polar/src/tests/PolarBillingDiagnosticsProvider.spec.ts
  • packages/billing-polar/src/tests/PolarBillingProviderProfile.spec.ts
  • packages/billing-polar/src/tests/PolarWebhookHandler.spec.ts
  • packages/billing-polar/src/tests/PolarWebhookHandlerSdk.spec.ts
  • packages/docs/src/content/docs/api/billing-core/src/classes/BillingProvider.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/InMemorySubscriptionQuantityReconciliationStore.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/InvalidSubscriptionQuantityProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/ProviderCapabilityUnavailableProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityDriftDetectedEvent.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityDriftRecoveredEvent.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityProviderMismatchProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityProviderSourceAheadProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityReconciler.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityReconciliationConflictProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityReconciliationFailedEvent.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityReconciliationFailedProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityReconciliationSucceededEvent.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantitySourceMismatchProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/functions/calculateDesiredQuantity.md
  • packages/docs/src/content/docs/api/billing-core/src/functions/createSubscriptionQuantityReconciliationId.md
  • packages/docs/src/content/docs/api/billing-core/src/interfaces/LicensedQuantityGateway.md
  • packages/docs/src/content/docs/api/billing-core/src/interfaces/SubscriptionQuantityReconciliationStore.md
  • packages/docs/src/content/docs/api/billing-core/src/interfaces/SubscriptionQuantityRepairSource.md
  • packages/docs/src/content/docs/api/billing-core/src/interfaces/SubscriptionQuantitySource.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/BillingProviderCapabilityProfile.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/CreateSubscriptionQuantityIntent.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/LicensedQuantityObservation.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/MembershipRole.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/PlanVersionDefinition.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/ReconcileSubscriptionQuantitiesResult.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/ReconcileSubscriptionQuantityInput.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SetLicensedQuantityInput.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SetLicensedQuantityResult.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityDiagnostics.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityFailureEvidence.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityPolicy.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityReconcilerDependencies.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityReconciliationEventPublisher.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityReconciliationState.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantitySnapshot.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantitySourceInput.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantitySourceSnapshot.md
  • packages/docs/src/content/docs/api/billing-core/src/variables/BILLING_PROVIDER_CAPABILITIES.md
  • packages/docs/src/content/docs/api/billing-polar/src/variables/POLAR_BILLING_PROVIDER_PROFILE.md
  • packages/docs/src/content/docs/api/events-core/src/classes/DomainEvent.md
  • packages/docs/src/content/docs/api/problems-core/src/classes/Problem.md
  • packages/docs/src/content/docs/api/problems-core/src/variables/CROCO_PROBLEM_CODE_REGISTRY.md
  • packages/docs/src/content/docs/api/testing/src/type-aliases/BillingProviderConformanceOptions.md
  • packages/docs/src/content/docs/api/testing/src/type-aliases/LicensedQuantityGatewayConformanceOptions.md
  • packages/docs/src/content/docs/en/reference/extension-matrix.md
  • packages/docs/src/content/docs/en/reference/problem-recovery-cookbook.md
  • packages/membership-core/README.md
  • packages/membership-core/src/libs/MembershipManager.ts
  • packages/membership-core/src/libs/MembershipService.ts
  • packages/membership-core/src/tests/MembershipManager.spec.ts
  • packages/membership-core/src/tests/MembershipService.spec.ts
  • packages/metrics-billing/src/tests/BillingEventHandler.spec.ts
  • packages/testing/src/index.ts
  • packages/testing/src/libs/billing-provider-conformance.ts
  • packages/testing/src/tests/BillingProviderCapabilityConformance.spec.ts
  • packages/testing/src/tests/testing.spec.ts
  • public-api-surface.snapshot.json

Comment thread packages/billing-core/README.md
Comment thread packages/billing-core/README.md
Comment thread packages/billing-core/src/index.ts
Comment thread packages/billing-core/src/libs/InMemoryPlanRegistry.ts
Comment thread packages/billing-core/src/libs/SubscriptionQuantity.ts
Comment thread packages/billing-core/src/libs/SubscriptionQuantity.ts
Comment thread packages/membership-core/src/tests/MembershipService.spec.ts
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
✅ Action performed

Comments resolved and changes approved.

@kang-heewon
kang-heewon force-pushed the feat/1570-reconcile-subscription-quantity branch from 6d77cc8 to a06ab31 Compare July 31, 2026 14:48

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/billing-core/src/libs/BillingProvider.ts (1)

64-71: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

capability 동결을 BILLING_PROVIDER_CAPABILITIES 순회로 유도하십시오.

Line 66-70은 capability 키를 수동으로 나열합니다. 이번 변경도 "licensed-quantity" 한 줄을 직접 추가해야 했습니다. 다음 capability 추가 시 이 줄을 빠뜨리면 해당 capability 설정이 동결된 profile에서 누락됩니다. 타입 검사는 이 누락을 잡지 못합니다. BILLING_PROVIDER_CAPABILITIES를 단일 출처로 사용하십시오.

♻️ 제안 리팩터
   return Object.freeze({
     providerName: profile.providerName,
-    capabilities: Object.freeze({
-      checkout: Object.freeze({ ...profile.capabilities.checkout }),
-      "licensed-quantity": Object.freeze({ ...profile.capabilities["licensed-quantity"] }),
-      usage: Object.freeze({ ...profile.capabilities.usage }),
-    }),
+    capabilities: Object.freeze(
+      Object.fromEntries(
+        BILLING_PROVIDER_CAPABILITIES.map((capability) => [
+          capability,
+          Object.freeze({ ...profile.capabilities[capability] }),
+        ]),
+      ),
+    ),
   }) as BillingProviderProfile<ProviderName, Capabilities>;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/billing-core/src/libs/BillingProvider.ts` around lines 64 - 71,
Update the capability-freezing logic in the profile construction block to
iterate over BILLING_PROVIDER_CAPABILITIES instead of manually listing checkout,
licensed-quantity, and usage. Build the frozen capabilities object from every
configured capability key so future additions are automatically included, while
preserving the existing shallow-clone and Object.freeze behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/calm-seats-reconcile.md:
- Line 7: `.changeset/calm-seats-reconcile.md`에서 `@croco/problems-core`의 릴리스 범위를
`patch`에서 `minor`로 변경하세요. `index.ts`를 통해 공개되는 `CrocoProblemCode`에 새 billing 코드가
추가되는 변경이므로 다른 패키지나 변경 내용은 수정하지 마세요.

In `@packages/billing-core/src/libs/InMemoryPlanRegistry.ts`:
- Around line 135-140: Update the quantityPolicy validation in
InMemoryPlanRegistry to reject policies where includedSeats or minimumQuantity
exceeds seatQuota, while preserving the existing independent field checks.
Ensure invalid cross-field combinations are rejected during plan publication
before calculateDesiredQuantity can apply an over-quota quantity.

In `@packages/billing-core/src/libs/problems/BillingProblems.ts`:
- Around line 239-249: Update SubscriptionQuantityReconciliationConflictProblem
in packages/billing-core/src/libs/problems/BillingProblems.ts:239-249 to pass
externalSubscriptionId and sourceVersion through the Problem extensions object
instead of embedding them in the human-readable detail message. Align the
constructor and detail format with SubscriptionQuantityProviderMismatchProblem
at packages/billing-core/src/libs/problems/BillingProblems.ts:275-289; no direct
change is required there because it already uses extensions.

In `@packages/billing-core/src/tests/InMemoryPlanRegistry.spec.ts`:
- Around line 287-289: Update the asynchronous rejection assertion in the
publishPlanVersion test to use Vitest’s rejects.toThrow matcher instead of
rejects.toBeInstanceOf, while preserving validation of the
InvalidPlanVersionDefinitionProblem failure.

In `@packages/billing-polar/src/tests/PolarBillingProviderProfile.spec.ts`:
- Around line 5-13: PolarBillingProviderProfile.spec.ts의 테스트 격리를 위해 beforeEach를
추가하십시오. 각 테스트 전에 DI Container를 reset하고 필요한 테스트 인스턴스를 초기화하도록 구성하며, 기존
POLAR_BILLING_PROVIDER_PROFILE 검증은 변경하지 마십시오.

In
`@packages/docs/src/content/docs/api/billing-core/src/classes/InvalidSubscriptionQuantityProblem.md`:
- Line 8: InvalidSubscriptionQuantityProblem 설명이 의미와 reason 인자를 반영하도록 해당 TypeDoc
원본 주석을 구체화하고, 생성된 Markdown은 직접 수정하지 말고 API 문서를 재생성하세요. 변경 후 pnpm docs:api:check로
생성 결과를 검증하세요.

In
`@packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityPolicy.md`:
- Around line 8-10: Update the TypeDoc source for SubscriptionQuantityPolicy in
packages/billing-core/src/index.ts by adding an accurate type-specific
description or relocating its comment so TypeDoc no longer inherits the
surrounding domain description. Regenerate the API documentation and verify the
result with pnpm docs:api:check.

In `@packages/membership-core/src/tests/MembershipManager.spec.ts`:
- Around line 122-135: Extend the deferred membership-created test around
MembershipManager.addMember to assert afterCommitEvents[0]?.data with toEqual(),
verifying tenantId, userId, and role values. Apply the same payload assertions
to the deferred membership-deleted and role-changed event tests referenced in
the comment, while preserving their existing event-count and publishNow
expectations.

---

Outside diff comments:
In `@packages/billing-core/src/libs/BillingProvider.ts`:
- Around line 64-71: Update the capability-freezing logic in the profile
construction block to iterate over BILLING_PROVIDER_CAPABILITIES instead of
manually listing checkout, licensed-quantity, and usage. Build the frozen
capabilities object from every configured capability key so future additions are
automatically included, while preserving the existing shallow-clone and
Object.freeze behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f18d6cc4-a7fc-46f9-8b2e-9598e1c85a18

📥 Commits

Reviewing files that changed from the base of the PR and between 6d77cc8 and a06ab31.

⛔ Files ignored due to path filters (2)
  • packages/problems-core/src/generated/problem-code-registry.ts is excluded by !**/generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (85)
  • .changeset/calm-seats-reconcile.md
  • README.md
  • docs/package-catalog.json
  • docs/package-docs-report.md
  • docs/problem-code-registry.json
  • examples/saas-billing-golden-path/README.md
  • examples/saas-billing-golden-path/package.json
  • examples/saas-billing-golden-path/src/integrations/MembershipSeatQuantitySource.ts
  • examples/saas-billing-golden-path/src/tests/membership-quantity.spec.ts
  • packages/billing-core/README.md
  • packages/billing-core/src/index.ts
  • packages/billing-core/src/libs/BillingProvider.ts
  • packages/billing-core/src/libs/BillingProviderCapabilities.ts
  • packages/billing-core/src/libs/InMemoryPlanRegistry.ts
  • packages/billing-core/src/libs/SubscriptionQuantity.ts
  • packages/billing-core/src/libs/problems/BillingProblems.ts
  • packages/billing-core/src/tests/BillingProvider.spec.ts
  • packages/billing-core/src/tests/InMemoryPlanRegistry.spec.ts
  • packages/billing-core/src/tests/SubscriptionQuantityReconciler.spec.ts
  • packages/billing-core/src/types.ts
  • packages/billing-polar/src/libs/PolarBillingDiagnosticsProvider.ts
  • packages/billing-polar/src/libs/PolarBillingProviderProfile.ts
  • packages/billing-polar/src/tests/PolarBillingDiagnosticsProvider.spec.ts
  • packages/billing-polar/src/tests/PolarBillingProviderProfile.spec.ts
  • packages/billing-polar/src/tests/PolarWebhookHandler.spec.ts
  • packages/billing-polar/src/tests/PolarWebhookHandlerSdk.spec.ts
  • packages/docs/src/content/docs/api/billing-core/src/classes/BillingProvider.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/InMemorySubscriptionQuantityReconciliationStore.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/InvalidSubscriptionQuantityProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/ProviderCapabilityUnavailableProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityDriftDetectedEvent.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityDriftRecoveredEvent.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityProviderMismatchProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityProviderSourceAheadProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityReconciler.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityReconciliationConflictProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityReconciliationFailedEvent.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityReconciliationFailedProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantityReconciliationSucceededEvent.md
  • packages/docs/src/content/docs/api/billing-core/src/classes/SubscriptionQuantitySourceMismatchProblem.md
  • packages/docs/src/content/docs/api/billing-core/src/functions/calculateDesiredQuantity.md
  • packages/docs/src/content/docs/api/billing-core/src/functions/createSubscriptionQuantityReconciliationId.md
  • packages/docs/src/content/docs/api/billing-core/src/interfaces/LicensedQuantityGateway.md
  • packages/docs/src/content/docs/api/billing-core/src/interfaces/SubscriptionQuantityReconciliationStore.md
  • packages/docs/src/content/docs/api/billing-core/src/interfaces/SubscriptionQuantityRepairSource.md
  • packages/docs/src/content/docs/api/billing-core/src/interfaces/SubscriptionQuantitySource.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/BillingProviderCapabilityProfile.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/CreateSubscriptionQuantityIntent.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/LicensedQuantityObservation.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/MembershipRole.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/PlanVersionDefinition.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/ReconcileSubscriptionQuantitiesResult.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/ReconcileSubscriptionQuantityInput.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SetLicensedQuantityInput.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SetLicensedQuantityResult.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityDiagnostics.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityFailureEvidence.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityPolicy.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityReconcilerDependencies.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityReconciliationEventPublisher.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantityReconciliationState.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantitySnapshot.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantitySourceInput.md
  • packages/docs/src/content/docs/api/billing-core/src/type-aliases/SubscriptionQuantitySourceSnapshot.md
  • packages/docs/src/content/docs/api/billing-core/src/variables/BILLING_PROVIDER_CAPABILITIES.md
  • packages/docs/src/content/docs/api/billing-polar/src/variables/POLAR_BILLING_PROVIDER_PROFILE.md
  • packages/docs/src/content/docs/api/events-core/src/classes/DomainEvent.md
  • packages/docs/src/content/docs/api/problems-core/src/classes/Problem.md
  • packages/docs/src/content/docs/api/problems-core/src/variables/CROCO_PROBLEM_CODE_REGISTRY.md
  • packages/docs/src/content/docs/api/testing/src/type-aliases/BillingProviderConformanceOptions.md
  • packages/docs/src/content/docs/api/testing/src/type-aliases/LicensedQuantityGatewayConformanceOptions.md
  • packages/docs/src/content/docs/en/reference/extension-matrix.md
  • packages/docs/src/content/docs/en/reference/problem-recovery-cookbook.md
  • packages/membership-core/README.md
  • packages/membership-core/src/libs/MembershipManager.ts
  • packages/membership-core/src/libs/MembershipService.ts
  • packages/membership-core/src/tests/MembershipManager.spec.ts
  • packages/membership-core/src/tests/MembershipService.spec.ts
  • packages/membership-drizzle/src/tests/DrizzleMembershipStore.postgres.spec.ts
  • packages/metrics-billing/src/tests/BillingEventHandler.spec.ts
  • packages/testing/src/index.ts
  • packages/testing/src/libs/billing-provider-conformance.ts
  • packages/testing/src/tests/BillingProviderCapabilityConformance.spec.ts
  • packages/testing/src/tests/testing.spec.ts
  • public-api-surface.snapshot.json

Comment thread .changeset/calm-seats-reconcile.md Outdated
Comment thread packages/billing-core/src/libs/InMemoryPlanRegistry.ts
Comment thread packages/billing-core/src/libs/problems/BillingProblems.ts
Comment thread packages/billing-core/src/tests/InMemoryPlanRegistry.spec.ts Outdated
Comment thread packages/membership-core/src/tests/MembershipManager.spec.ts
@kang-heewon
kang-heewon force-pushed the feat/1570-reconcile-subscription-quantity branch from a06ab31 to a4372dc Compare July 31, 2026 16:12
@kang-heewon

Copy link
Copy Markdown
Member Author

@coderabbitai resolve

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
✅ Action performed

Comments resolved and changes approved.

@kang-heewon
kang-heewon force-pushed the feat/1570-reconcile-subscription-quantity branch from a4372dc to 0716850 Compare July 31, 2026 16:51
@kang-heewon
kang-heewon force-pushed the feat/1570-reconcile-subscription-quantity branch from 0716850 to 92b0f73 Compare August 1, 2026 13:37
@kang-heewon
kang-heewon merged commit 2bbb09f into trunk Aug 1, 2026
12 checks passed
@kang-heewon
kang-heewon deleted the feat/1570-reconcile-subscription-quantity branch August 1, 2026 14:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[billing-core] Reconcile licensed subscription quantity from membership seats

1 participant