using System; using System.ServiceModel; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Messages; using Microsoft.Xrm.Sdk.Query; namespace Nova.OrderProcessing { public sealed class PlaceOrderPlugin : global::PlaceOrderPlugin.PluginBase { private const string ApiName = "demo_PlaceOrder"; private const int Pending = 100000000; private const int Confirmed = 100000001; private const int ConcurrencyVersionMismatch = -2147088254; public PlaceOrderPlugin() : base(typeof(PlaceOrderPlugin)) { } public PlaceOrderPlugin(string unsecureConfiguration, string secureConfiguration) : this() { } protected override void ExecuteDataversePlugin(global::PlaceOrderPlugin.ILocalPluginContext local) { if (local == null) throw new ArgumentNullException(nameof(local)); IPluginExecutionContext context = local.PluginExecutionContext; IOrganizationService service = local.PluginUserService; if (!string.Equals(context.MessageName, ApiName, StringComparison.OrdinalIgnoreCase) || context.Stage != 30) { throw new InvalidPluginExecutionException( "This plug-in is designed for the demo_PlaceOrder Custom API main operation."); } Guid customerId = GetGuid(context, "CustomerId"); Guid productId = GetGuid(context, "ProductId"); int quantity = GetInteger(context, "Quantity"); if (quantity <= 0) { Respond(context, false, Guid.Empty, 0m, "Quantity must be greater than zero."); return; } if (!context.IsInTransaction) { throw new InvalidPluginExecutionException( "Order processing requires a synchronous transactional execution."); } try { Entity customer = service.Retrieve("account", customerId, new ColumnSet("name", "statecode")); if (customer.GetAttributeValue("statecode")?.Value == 1) { Respond(context, false, Guid.Empty, 0m, "The selected customer account is inactive."); return; } Entity product = service.Retrieve("demo_product", productId, new ColumnSet("demo_name", "demo_unitprice", "demo_stock", "transactioncurrencyid", "statecode")); if (product.GetAttributeValue("statecode")?.Value == 1) { Respond(context, false, Guid.Empty, 0m, "The selected product is inactive."); return; } string productName = product.GetAttributeValue("demo_name") ?? "Product"; Money unitPrice = product.GetAttributeValue("demo_unitprice"); EntityReference currency = product.GetAttributeValue( "transactioncurrencyid"); int available = product.GetAttributeValue("demo_stock"); if (unitPrice == null || unitPrice.Value <= 0m || currency == null) { Respond(context, false, Guid.Empty, 0m, "Product price or currency is missing or invalid."); return; } if (available < quantity) { Respond(context, false, Guid.Empty, 0m, $"Insufficient stock for {productName}. Available: {available}; requested: {quantity}."); return; } if (string.IsNullOrWhiteSpace(product.RowVersion)) { throw new InvalidPluginExecutionException( "Product row version is unavailable. The order was not created."); } decimal total = Math.Round(unitPrice.Value * quantity, 2, MidpointRounding.AwayFromZero); int remaining = available - quantity; Entity stockUpdate = new Entity("demo_product", productId) { RowVersion = product.RowVersion }; stockUpdate["demo_stock"] = remaining; service.Execute(new UpdateRequest { Target = stockUpdate, ConcurrencyBehavior = ConcurrencyBehavior.IfRowVersionMatches }); Guid orderId = Guid.NewGuid(); string orderNumber = "NOVA-ORD-" + orderId.ToString("N") .Substring(0, 10).ToUpperInvariant(); Entity order = new Entity("demo_order", orderId); order["demo_name"] = orderNumber; order["demo_customer"] = new EntityReference("account", customerId); order["transactioncurrencyid"] = currency; order["demo_total"] = new Money(total); order["demo_orderstatus"] = new OptionSetValue(Pending); service.Create(order); Entity item = new Entity("demo_orderitem"); item["demo_name"] = $"{quantity} x {productName}"; item["demo_order"] = new EntityReference("demo_order", orderId); item["demo_product"] = new EntityReference("demo_product", productId); item["demo_quantity"] = quantity; item["demo_unitprice"] = new Money(unitPrice.Value); item["demo_linetotal"] = new Money(total); item["transactioncurrencyid"] = currency; service.Create(item); Entity confirm = new Entity("demo_order", orderId); confirm["demo_orderstatus"] = new OptionSetValue(Confirmed); service.Update(confirm); Respond(context, true, orderId, total, $"Order {orderNumber} confirmed. {quantity} x {productName}. " + $"Total: INR {total:0.00}. Remaining inventory: {remaining}."); local.Trace($"Order created. OrderId={orderId}; ProductId={productId}; " + $"Quantity={quantity}; RemainingStock={remaining}"); } catch (FaultException ex) when (ex.Detail != null && ex.Detail.ErrorCode == ConcurrencyVersionMismatch) { throw new InvalidPluginExecutionException( "Inventory changed while processing the order. Please refresh and try again.", ex); } catch (InvalidPluginExecutionException) { throw; } catch (Exception ex) { local.Trace($"PlaceOrder failure: {ex}"); throw new InvalidPluginExecutionException( "Order processing failed. The transaction was cancelled. Check the plug-in trace log.", ex); } } private static Guid GetGuid(IPluginExecutionContext context, string key) { if (!context.InputParameters.Contains(key) || !(context.InputParameters[key] is Guid) || (Guid)context.InputParameters[key] == Guid.Empty) { throw new InvalidPluginExecutionException( $"'{key}' must be a valid, non-empty Dataverse record GUID."); } return (Guid)context.InputParameters[key]; } private static int GetInteger(IPluginExecutionContext context, string key) { if (!context.InputParameters.Contains(key) || !(context.InputParameters[key] is int)) { throw new InvalidPluginExecutionException( $"'{key}' must be an integer."); } return (int)context.InputParameters[key]; } private static void Respond(IPluginExecutionContext context, bool success, Guid orderId, decimal amount, string message) { context.OutputParameters["Success"] = success; context.OutputParameters["OrderId"] = orderId; context.OutputParameters["TotalAmount"] = amount; context.OutputParameters["Message"] = message; } } }