{"record":{"id":"d2f9d95faafe9d1c","repo":"fullstackhero/dotnet-starter-kit","slug":"product-command-productid-not-found-d2f9d9","errorCode":null,"errorMessage":"Product {command.ProductId} not found.","messagePattern":"Product (.+?) not found\\.","errorType":"exception","errorClass":"NotFoundException","httpStatus":404,"severity":"warning","filePath":"src/Modules/Catalog/Modules.Catalog/Features/v1/Products/AdjustProductStock/AdjustProductStockCommandHandler.cs","lineNumber":20,"sourceCode":"using FSH.Framework.Core.Exceptions;\nusing FSH.Modules.Catalog.Contracts.v1.Products;\nusing FSH.Modules.Catalog.Data;\nusing Mediator;\nusing Microsoft.EntityFrameworkCore;\n\nnamespace FSH.Modules.Catalog.Features.v1.Products.AdjustProductStock;\n\npublic 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":2,"sourceCodeEnd":35,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Catalog/Modules.Catalog/Features/v1/Products/AdjustProductStock/AdjustProductStockCommandHandler.cs#L2-L35","documentation":"AdjustProductStock loads the product by command.ProductId and throws NotFoundException when no product matches. The stock adjustment cannot be applied to a missing aggregate; default soft-delete filters apply.","triggerScenarios":"POST stock-adjustment for a ProductId that: (1) was deleted, (2) is a wrong Guid, (3) exists in another tenant.","commonSituations":"Inventory tools batch-adjusting from an exported (stale) id list; product removed by a catalog manager while warehouse staff adjust stock; environment mismatch in scripts.","solutions":["Verify the product id against the live catalog before adjusting.","Skip/flag the row and continue batch processing, reporting not-found ids at the end.","Check whether the product was soft-deleted and restore it.","Ensure the job runs against the correct database/tenant."],"exampleFix":"// before: fail whole batch on one bad id\nforeach (var r in rows) await api.adjustStock(r.productId, r.delta);\n\n// after: tolerate missing products\ntry { await api.adjustStock(r.productId, r.delta); }\ncatch (ApiError e) when (e.status === 404) { skipped.push(r.productId); }","handlingStrategy":"try-catch","validationCode":"const exists = await api.listProducts({ ids: [productId] });\nif (!exists.items.length) return skipRow(productId, 'product not found');","typeGuard":"const isGuid = (v) => typeof v === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v);","tryCatchPattern":"try { await api.adjustStock(productId, delta); }\ncatch (e) { if (e.status === 404) reportMissing(productId); else throw e; }","preventionTips":["Re-export id lists immediately before batch inventory runs","Log and skip missing products instead of aborting the batch","Confirm the batch job targets the right tenant/database"],"tags":["not-found","products","stock","inventory"],"backgroundTag":"entity-not-found","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"}