{"record":{"id":"2672f5ccbbdbf021","repo":"fullstackhero/dotnet-starter-kit","slug":"ex-message","errorCode":null,"errorMessage":"ex.Message","messagePattern":"ex\\.Message","errorType":"exception","errorClass":"CustomException","httpStatus":409,"severity":"error","filePath":"src/Modules/Catalog/Modules.Catalog/Features/v1/Products/AdjustProductStock/AdjustProductStockCommandHandler.cs","lineNumber":28,"sourceCode":"public sealed class AdjustProductStockCommandHandler(CatalogDbContext dbContext)\n    : ICommandHandler<AdjustProductStockCommand, int>\n{\n    public async ValueTask<int> Handle(AdjustProductStockCommand command, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(command);\n\n        var product = await dbContext.Products\n            .FirstOrDefaultAsync(p => p.Id == command.ProductId, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException($\"Product {command.ProductId} not found.\");\n\n        try\n        {\n            product.AdjustStock(command.Delta);\n        }\n        catch (InvalidOperationException ex)\n        {\n            throw new CustomException(ex.Message, (IEnumerable<string>?)null, HttpStatusCode.Conflict);\n        }\n\n        await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n        return product.Stock;\n    }\n}\n","sourceCodeStart":10,"sourceCodeEnd":35,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Catalog/Modules.Catalog/Features/v1/Products/AdjustProductStock/AdjustProductStockCommandHandler.cs#L10-L35","documentation":"After loading the product, AdjustProductStock calls product.AdjustStock(delta); the domain invariant (e.g. stock cannot go below zero) raises InvalidOperationException, which the handler converts to CustomException with 409 Conflict carrying the domain message. The request was valid but the current stock state forbids the adjustment.","triggerScenarios":"Sending a negative delta larger than current stock (would underflow below the domain minimum), or any adjustment the Product entity's stock rules reject.","commonSituations":"Warehouse app submitting returns/shipments computed from a stale stock figure; concurrent adjustments where another operation already consumed the stock; entering a wrong (oversized) quantity manually.","solutions":["Fetch current stock and reduce the delta so the result stays within the allowed range.","Re-read the product before retrying — stock may have changed concurrently.","Correct the manual quantity entry (e.g. ship fewer units than available).","Split the adjustment into valid increments if partial fulfilment is possible."],"exampleFix":"// before\nawait api.adjustStock(productId, -1000); // conflict: not enough stock\n\n// after\nconst p = await api.getProduct(productId);\nawait api.adjustStock(productId, Math.max(-p.stock, -1000));","handlingStrategy":"validation","validationCode":"const p = await api.getProduct(productId);\nif (delta < 0 && p.stock + delta < 0) {\n  return alert(`Cannot remove ${-delta}; only ${p.stock} in stock.`);\n}","typeGuard":"const isNonNegative = (n) => typeof n === 'number' && Number.isFinite(n) && n >= 0;","tryCatchPattern":"try { await api.adjustStock(productId, delta); }\ncatch (e) { if (e.status === 409) { await refreshStock(productId); alert(e.message); } else throw e; }","preventionTips":["Refetch current stock before each adjustment","Clamp negative deltas to available stock client-side","Expect 409 under concurrency and retry with fresh data","Validate quantity inputs against a sane range"],"tags":["conflict","stock","domain-invariant","inventory"],"backgroundTag":"invalid-state-transition","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}