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
- Confirm the SubscriptionId belongs to the payer's active subscription before checkout.
- 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).
- Pre-check existence in the controller and return 404.
- 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
- Confirm the subscription belongs to the payer and is renewable before checkout.
- Pre-validate subscription type/country/money against the price list.
- Reject empty Guid and negative money client-side.
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
- Pricelist item for activation must exist.
- Pricelist item for changing must exist.
- Pricelist item for deactivation must exist.
- Meeting commenting configuration for disabling commenting mu
- Meeting commenting configuration for enabling commenting mus
AI-assisted analysis of kgrzybek/modular-monolith-with-ddd@91c8ef24b4 (2026-08-13).
Data as JSON: /api/errors/da0b196bce172b9e.
Report an issue: GitHub.