Hi Kill Bill Team,
I'd like to report a bug related to addon subscription creation for existing subscriptions when a new catalog version introduces addon availability that was not present in the original catalog version.
## Kill Bill Version
This bug exists in ALL Kill Bill versions including the latest (0.24.16). The affected code (AddonUtils.java, CatalogResource.java, SubscriptionCatalog.java) is identical across 0.22.33 and the latest master branch. We verified this by comparing the source code line by line.
## Summary
When a base subscription is created under catalog version V1 (where no addons are marked as <available>), and a new catalog version V2 is uploaded that adds an addon as <available> for the base product, existing subscriptions:
1. Cannot subscribe to the newly available addon — fails with error code 1019 (SUB_CREATE_AO_NOT_AVAILABLE)
2. The GET /catalog/product?subscriptionId= API returns an empty available list instead of showing the newly added addon
The addon can only be subscribed if the base subscription is changed via PUT /subscriptions/{id} (change plan to the same plan), which is not acceptable as it triggers re-billing (fixed + recurring charges).
## Steps to Reproduce
### Step 1: Upload Catalog V1 (Base product WITHOUT addon availability)
POST /1.0/kb/catalog/xml
Content-Type: application/xml
xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<catalog>
  <effectiveDate>2026-04-18T12:40:12Z</effectiveDate>
  <catalogName>DEFAULT</catalogName>
  <recurringBillingMode>IN_ADVANCE</recurringBillingMode>
  <currencies>
    <currency>USD</currency>
  </currencies>
  <units>
    <unit name="cell-phone-message-one"/>
    <unit name="cell-phone-message-two"/>
    <unit name="cell-phone-message-three"/>
    <unit name="cell-phone-message-four"/>
    <unit name="cell-phone-message-five"/>
  </units>
  <products>
    <product name="Standard">
      <category>BASE</category>
      <!-- NOTE: No <available> section — addon is NOT mapped -->
    </product>
    <product name="RemoteControl">
      <category>ADD_ON</category>
    </product>
    <product name="OilSlick">
      <category>ADD_ON</category>
    </product>
  </products>
  <rules>
    <changePolicy>
      <changePolicyCase>
        <policy>IMMEDIATE</policy>
      </changePolicyCase>
    </changePolicy>
    <changeAlignment>
      <changeAlignmentCase>
        <alignment>START_OF_BUNDLE</alignment>
      </changeAlignmentCase>
    </changeAlignment>
    <cancelPolicy>
      <cancelPolicyCase>
        <productCategory>BASE</productCategory>
        <policy>IMMEDIATE</policy>
      </cancelPolicyCase>
      <cancelPolicyCase>
        <policy>IMMEDIATE</policy>
      </cancelPolicyCase>
    </cancelPolicy>
    <createAlignment>
      <createAlignmentCase>
        <alignment>START_OF_BUNDLE</alignment>
      </createAlignmentCase>
    </createAlignment>
    <billingAlignment>
      <billingAlignmentCase>
        <alignment>ACCOUNT</alignment>
      </billingAlignmentCase>
    </billingAlignment>
    <priceList>
      <priceListCase>
        <toPriceList>DEFAULT</toPriceList>
      </priceListCase>
    </priceList>
  </rules>
  <plans>
    <plan name="standard-daily" prettyName="standard-daily-prepaid-prettyname">
      <product>Standard</product>
      <recurringBillingMode>IN_ADVANCE</recurringBillingMode>
      <initialPhases/>
      <finalPhase type="EVERGREEN">
        <duration>
          <unit>UNLIMITED</unit>
        </duration>
        <fixed type="ONE_TIME">
          <fixedPrice>
            <price>
              <currency>USD</currency>
              <value>723</value>
            </price>
          </fixedPrice>
        </fixed>
        <recurring>
          <billingPeriod>DAILY</billingPeriod>
          <recurringPrice>
            <price>
              <currency>USD</currency>
              <value>234</value>
            </price>
          </recurringPrice>
        </recurring>
      </finalPhase>
    </plan>
    <plan name="remotecontrol-daily" prettyName="remotecontrol-daily-prettyname">
      <product>RemoteControl</product>
      <recurringBillingMode>IN_ADVANCE</recurringBillingMode>
      <initialPhases/>
      <finalPhase type="EVERGREEN">
        <duration>
          <unit>UNLIMITED</unit>
        </duration>
        <recurring>
          <billingPeriod>DAILY</billingPeriod>
          <recurringPrice>
            <price>
              <currency>USD</currency>
              <value>0</value>
            </price>
          </recurringPrice>
        </recurring>
        <usages/>
      </finalPhase>
    </plan>
    <plan name="oilslick-daily" prettyName="oilslick-daily-prettyname">
      <product>OilSlick</product>
      <recurringBillingMode>IN_ADVANCE</recurringBillingMode>
      <initialPhases/>
      <finalPhase type="EVERGREEN">
        <duration>
          <unit>UNLIMITED</unit>
        </duration>
        <recurring>
          <billingPeriod>DAILY</billingPeriod>
          <recurringPrice>
            <price>
              <currency>USD</currency>
              <value>0</value>
            </price>
          </recurringPrice>
        </recurring>
        <usages/>
      </finalPhase>
    </plan>
  </plans>
  <priceLists>
    <defaultPriceList name="DEFAULT">
      <plans>
        <plan>standard-daily</plan>
        <plan>remotecontrol-daily</plan>
        <plan>oilslick-daily</plan>
      </plans>
    </defaultPriceList>
  </priceLists>
</catalog>
### Step 2: Create a base subscription on the "standard-daily" plan
POST /1.0/kb/subscriptions
Content-Type: application/json
json
{
  "accountId": "4dfde893-a220-4820-ace9-44b47985cbef",
  "planName": "standard-daily"
}
Response: Subscription created successfully (subscriptionId: bb3bf566-7b4d-4b90-8ba4-2302596b52ab)
### Step 3: Verify product info — available list is empty (expected)
GET /1.0/kb/catalog/product?subscriptionId=bb3bf566-7b4d-4b90-8ba4-2302596b52ab
Response (correct — no addons were configured as available in V1):
json
{
  "type": "BASE",
  "name": "Standard",
  "prettyName": "Standard",
  "plans": [],
  "included": [],
  "available": []
}
### Step 4: Upload Catalog V2 (Base product WITH addon availability)
Note: The only change from V1 is adding <available><addonProduct>RemoteControl</addonProduct></available> to the Standard product. The effective date is 1 second later.
POST /1.0/kb/catalog/xml
Content-Type: application/xml
xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<catalog>
  <effectiveDate>2026-04-18T12:40:13Z</effectiveDate>
  <catalogName>DEFAULT</catalogName>
  <recurringBillingMode>IN_ADVANCE</recurringBillingMode>
  <currencies>
    <currency>USD</currency>
  </currencies>
  <units>
    <unit name="cell-phone-message-one"/>
    <unit name="cell-phone-message-two"/>
    <unit name="cell-phone-message-three"/>
    <unit name="cell-phone-message-four"/>
    <unit name="cell-phone-message-five"/>
  </units>
  <products>
    <product name="Standard">
      <category>BASE</category>
      <available>
        <addonProduct>RemoteControl</addonProduct>
      </available>
    </product>
    <product name="RemoteControl">
      <category>ADD_ON</category>
    </product>
    <product name="OilSlick">
      <category>ADD_ON</category>
    </product>
  </products>
  <rules>
    <changePolicy>
      <changePolicyCase>
        <policy>IMMEDIATE</policy>
      </changePolicyCase>
    </changePolicy>
    <changeAlignment>
      <changeAlignmentCase>
        <alignment>START_OF_BUNDLE</alignment>
      </changeAlignmentCase>
    </changeAlignment>
    <cancelPolicy>
      <cancelPolicyCase>
        <productCategory>BASE</productCategory>
        <policy>IMMEDIATE</policy>
      </cancelPolicyCase>
      <cancelPolicyCase>
        <policy>IMMEDIATE</policy>
      </cancelPolicyCase>
    </cancelPolicy>
    <createAlignment>
      <createAlignmentCase>
        <alignment>START_OF_BUNDLE</alignment>
      </createAlignmentCase>
    </createAlignment>
    <billingAlignment>
      <billingAlignmentCase>
        <alignment>ACCOUNT</alignment>
      </billingAlignmentCase>
    </billingAlignment>
    <priceList>
      <priceListCase>
        <toPriceList>DEFAULT</toPriceList>
      </priceListCase>
    </priceList>
  </rules>
  <plans>
    <plan name="standard-daily" prettyName="standard-daily-prepaid-prettyname">
      <product>Standard</product>
      <recurringBillingMode>IN_ADVANCE</recurringBillingMode>
      <initialPhases/>
      <finalPhase type="EVERGREEN">
        <duration>
          <unit>UNLIMITED</unit>
        </duration>
        <fixed type="ONE_TIME">
          <fixedPrice>
            <price>
              <currency>USD</currency>
              <value>723</value>
            </price>
          </fixedPrice>
        </fixed>
        <recurring>
          <billingPeriod>DAILY</billingPeriod>
          <recurringPrice>
            <price>
              <currency>USD</currency>
              <value>234</value>
            </price>
          </recurringPrice>
        </recurring>
      </finalPhase>
    </plan>
    <plan name="remotecontrol-daily" prettyName="remotecontrol-daily-prettyname">
      <product>RemoteControl</product>
      <recurringBillingMode>IN_ADVANCE</recurringBillingMode>
      <initialPhases/>
      <finalPhase type="EVERGREEN">
        <duration>
          <unit>UNLIMITED</unit>
        </duration>
        <recurring>
          <billingPeriod>DAILY</billingPeriod>
          <recurringPrice>
            <price>
              <currency>USD</currency>
              <value>0</value>
            </price>
          </recurringPrice>
        </recurring>
        <usages/>
      </finalPhase>
    </plan>
    <plan name="oilslick-daily" prettyName="oilslick-daily-prettyname">
      <product>OilSlick</product>
      <recurringBillingMode>IN_ADVANCE</recurringBillingMode>
      <initialPhases/>
      <finalPhase type="EVERGREEN">
        <duration>
          <unit>UNLIMITED</unit>
        </duration>
        <recurring>
          <billingPeriod>DAILY</billingPeriod>
          <recurringPrice>
            <price>
              <currency>USD</currency>
              <value>0</value>
            </price>
          </recurringPrice>
        </recurring>
        <usages/>
      </finalPhase>
    </plan>
  </plans>
  <priceLists>
    <defaultPriceList name="DEFAULT">
      <plans>
        <plan>standard-daily</plan>
        <plan>remotecontrol-daily</plan>
        <plan>oilslick-daily</plan>
      </plans>
    </defaultPriceList>
  </priceLists>
</catalog>
### Step 5: Verify product info — available list is STILL empty (BUG)
GET /1.0/kb/catalog/product?subscriptionId=bb3bf566-7b4d-4b90-8ba4-2302596b52ab
Actual response (WRONG — should show RemoteControl):
json
{
  "type": "BASE",
  "name": "Standard",
  "prettyName": "Standard",
  "plans": [],
  "included": [],
  "available": []
}
Expected response:
json
{
  "type": "BASE",
  "name": "Standard",
  "prettyName": "Standard",
  "plans": [],
  "included": [],
  "available": ["RemoteControl"]
}
### Step 6: Attempt to subscribe the addon — FAILS with error 1019
POST /1.0/kb/subscriptions/createSubscriptionWithAddOns
Content-Type: application/json
json
[
  {
    "accountId": "4dfde893-a220-4820-ace9-44b47985cbef",
    "bundleId": "6332f4e4-e70f-48aa-8199-0510e290ad7c",
    "planName": "remotecontrol-daily"
  }
]
Error response:
json
{
  "className": "org.killbill.billing.subscription.api.user.SubscriptionBaseApiException",
  "code": 1019,
  "message": "Can't create AddOn remotecontrol-daily for BasePlan Standard (Not available)",
  "causeClassName": null,
  "causeMessage": null,
  "stackTrace": []
}
## Root Cause Analysis
The issue is in the catalog version resolution logic. Two files are affected:
### 1. AddonUtils.checkAddonCreationRights() (subscription module)
File: subscription/src/main/java/org/killbill/billing/subscription/engine/addon/AddonUtils.java
When validating addon creation, this method does:
java
final Plan currentOrPendingPlan = baseSubscription.getCurrentOrPendingPlan();
final Product baseProduct = currentOrPendingPlan.getProduct();
The currentOrPendingPlan is resolved from the subscription's transitions, which were built during DefaultSubscriptionBase.rebuildTransitions() using:
java
catalog.findPlan(nextPlanName, cur.getEffectiveDate(), lastPlanChangeTime)
Where lastPlanChangeTime = the subscription's CREATE event time. This calls SubscriptionCatalog.findCatalogPlanEntry(), which walks catalog versions backwards. Since the subscription was created BEFORE catalog V2's effective date, and the plan in V2 has no effectiveDateForExistingSubscriptions set, the code resolves to catalog V1 — the version where <available> is empty.
Meanwhile, the addon plan (remotecontrol-daily) is resolved from the LATEST catalog version (V2) via catalog.versionForDate(effectiveDate) in DefaultSubscriptionBaseCreateApi.createPlansIfNeededAndReorderBPOrStandaloneSpecFirstWithSanity().
So there's a mismatch: the addon plan comes from V2, but the base product comes from V1. The isAddonAvailable() check fails because V1's Standard product has no addons in <available>.
### 2. CatalogResource.getProductForSubscriptionAndDate() (jaxrs module)
File: jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/CatalogResource.java
The /catalog/product?subscriptionId= endpoint returns the product from the subscription's last event transition, which was also resolved from catalog V1. It never looks at the latest catalog version.
### Why does PUT /subscriptions/{id} (change plan) fix it?
Because a plan change creates a new CHANGE event with lastPlanChangeTime = now. Since "now" is after V2's effective date, findCatalogPlanEntry() resolves to V2, and the base product now has RemoteControl in <available>. However, this triggers re-billing (fixed + recurring charges), which is not acceptable.
### Why effectiveDateForExistingSubscriptions doesn't help
This catalog feature was designed for price changes, not for addon availability changes. Even if set, it only affects plan/price resolution — the AddonUtils.checkAddonCreationRights() code still gets the base product from the subscription's current transition, which may or may not have been updated depending on timing.
### Interesting contrast: Dry-Run API does it correctly
The getDryRunChangePlanStatus() method in DefaultSubscriptionInternalApi already uses the correct approach:
java
final StaticCatalog catalogVersion = catalog.versionForDate(requestedDate);
final Product baseProduct = catalogVersion.findProduct(baseProductName);
It looks up the base product from the latest catalog version. But this logic is not applied to the actual addon creation flow.
## Proposed Fix
### Fix 1: AddonUtils.java — Allow addon creation using latest catalog version
When isAddonAvailable() fails using the base product from the subscription's old catalog version, add a fallback check using the base product from the addon plan's catalog version (which is always the latest):
java
if (!isAddonAvailable(baseProduct, targetAddOnPlan)) {
  // Fallback: check the base product from the addon plan's catalog version (latest catalog)
  try {
    Product baseProductFromAddonCatalog = targetAddOnPlan.getCatalog().findProduct(baseProduct.getName());
    if (baseProductFromAddonCatalog == null || !isAddonAvailable(baseProductFromAddonCatalog, targetAddOnPlan)) {
      throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_AO_NOT_AVAILABLE, ...);
    }
  } catch (CatalogApiException e) {
    throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_AO_NOT_AVAILABLE, ...);
  }
}
### Fix 2: CatalogResource.java — Return product from latest catalog version
In getProductForSubscriptionAndDate(), look up the product from the current catalog version instead of using the stale product from the subscription's transition:
java
Product latestProduct = product;
try {
  TenantContext tenantContext = context.createTenantContextNoAccountId(request);
  StaticCatalog latestCatalogVersion = catalogUserApi.getCurrentCatalog("unused", tenantContext);
  latestProduct = latestCatalogVersion.findProduct(product.getName());
} catch (CatalogApiException e) {
  // Fall back to original product
}
final ProductJson productJson = new ProductJson(latestProduct);
Both fixes are backward-compatible — if the catalog lookup fails, they fall back to the original behavior.
## Affected Versions
ALL versions of Kill Bill are affected, including the latest release (0.24.16) and the current master branch. We compared the source code of AddonUtils.java, SubscriptionCatalog.java, and CatalogResource.java between version 0.22.33 and the latest master — the code is identical. No fix for this issue was found in any release notes from 0.22.33 through 0.24.16.
## Impact
Any Kill Bill deployment that uses versioned catalogs and needs to add addon availability for existing base products after initial catalog creation is affected. The only current workaround is to change the base subscription's plan via PUT (which triggers unwanted re-billing).
Thank you for looking into this.