kgrzybek/modular-monolith-with-ddd · error · InvalidCommandException

Subscription for renewal must exist.

Error message

Subscription for renewal must exist.

What it means

Thrown by BuySubscriptionRenewalCommandHandler when _aggregateStore.Load(new SubscriptionId(...)) returns null. Renewal needs the existing Subscription aggregate to attach the renewal payment; a missing subscription aborts with InvalidCommandException (HTTP 400). Note the price list is loaded first, then the subscription, so a bad id fails at the subscription step.

Source

Thrown at src/Modules/Payments/Application/Subscriptions/BuySubscriptionRenewal/BuySubscriptionRenewalCommandHandler.cs:40

            IPayerContext payerContext,
            ISqlConnectionFactory sqlConnectionFactory)
        {
            _aggregateStore = aggregateStore;
            _payerContext = payerContext;
            _sqlConnectionFactory = sqlConnectionFactory;
        }

        public async Task<Guid> Handle(BuySubscriptionRenewalCommand command, CancellationToken cancellationToken)
        {
            var priceList = await PriceListFactory.CreatePriceList(_sqlConnectionFactory.GetOpenConnection());

            var subscriptionId = new SubscriptionId(command.SubscriptionId);

            var subscription = await _aggregateStore.Load(new SubscriptionId(command.SubscriptionId));

            if (subscription == null)
            {
                throw new InvalidCommandException(["Subscription for renewal must exist."]);
            }

            var subscriptionRenewalPayment = SubscriptionRenewalPayment.Buy(
                _payerContext.PayerId,
                subscriptionId,
                SubscriptionPeriod.Of(command.SubscriptionTypeCode),
                command.CountryCode,
                MoneyValue.Of(command.Value, command.Currency),
                priceList);

            _aggregateStore.AppendChanges(subscriptionRenewalPayment);

            return subscriptionRenewalPayment.Id;
        }
    }
}

View on GitHub (pinned to 91c8ef24b4)

Solutions

  1. Confirm the SubscriptionId belongs to the payer's active subscription before checkout.
  2. Verify the SubscriptionTypeCode/CountryCode/Value/Currency are valid (these feed SubscriptionRenewalPayment.Buy and MoneyValue.Of, which can also throw or fail rule checks against the price list).
  3. Pre-check existence in the controller and return 404.
  4. Map InvalidCommandException to 400/404 at the API boundary.

Example fix

// before
var paymentId = await _commandDispatcher.SendAsync(new BuySubscriptionRenewalCommand(subscriptionId, typeCode, countryCode, value, currency));

// after
var subscription = await _subscriptionQueries.GetSubscriptionAsync(subscriptionId, payerId);
if (subscription is null) return NotFound("Subscription not found.");
var paymentId = await _commandDispatcher.SendAsync(new BuySubscriptionRenewalCommand(subscriptionId, typeCode, countryCode, value, currency));
Defensive patterns

Strategy: validation

Validate before calling

var subscription = await _subscriptionQueries.GetSubscriptionAsync(subscriptionId, payerId);
if (subscription is null) return NotFound("Subscription not found.");
var paymentId = await _commandDispatcher.SendAsync(new BuySubscriptionRenewalCommand(subscriptionId, typeCode, countryCode, value, currency));

Type guard

public static bool IsValidRenewalCommand(BuySubscriptionRenewalCommand c) =>
    c.SubscriptionId != Guid.Empty
    && !string.IsNullOrWhiteSpace(c.SubscriptionTypeCode)
    && c.Value >= 0
    && !string.IsNullOrWhiteSpace(c.Currency);

Try / catch

try { await _commandDispatcher.SendAsync(cmd); }
catch (InvalidCommandException ex) when (ex.Errors.Any(m => m.Contains("must exist")))
{ return NotFound(new { errors = ex.Errors }); }

Prevention

When it happens

Trigger: Dispatching BuySubscriptionRenewalCommand with a SubscriptionId that has no aggregate: wrong Guid, subscription expired/cancelled and removed, wrong tenant, or stream not found.

Common situations: User renews from a stale account page after cancellation; cross-environment id; checkout retries with an id from a deleted test subscription.

Related errors


AI-assisted analysis of kgrzybek/modular-monolith-with-ddd@91c8ef24b4 (2026-08-13). Data as JSON: /api/errors/da0b196bce172b9e. Report an issue: GitHub.