using Sockeye.Models; using System.Linq; using Newtonsoft.Json.Linq; namespace Sockeye.Biz { //VALIDATE **USER DEFINED** (not stock) REQUIRED FIELDS THAT ARE NOT CUSTOM //(fields that are stock required are validated on their own not here) internal static class RequiredFieldsValidator { internal static void Validate(BizObject biz, FormCustom formCustom, object proposedObject) { //No form custom = no template to check against so nothing to do if (formCustom == null || string.IsNullOrWhiteSpace(formCustom.Template)) return; var FormTemplate = JArray.Parse(formCustom.Template); var FormFields = Biz.FormFieldOptionalCustomizableReference.FormFieldReferenceList(formCustom.FormKey); foreach (JObject jo in FormTemplate) { if (jo["required"].Value() == true) { //First get the LT key var FldLtKey = jo["fld"].Value(); // - e.g.: {template:[{fld:"ltkeyfieldname",hide:"true/false",required:"true/false", type:"bool"},{fld:"ltkeyfieldname",hide:"true/false",required:"true/false", type:"text"]} //get the FormField object FormField FF = FormFields.Where(z => z.FieldKey == FldLtKey).Single(); //get the type and because of quote and pm subsections "TKeySection" property being named "WorkOrder" in the formFieldReference due to lack of separate //translations for quote and pm subitems it's necessary to adjust the name here first before matching var proposedObjectType = proposedObject.GetType().ToString().Replace("Sockeye.Models.", "").Replace("QuoteItem", "WorkOrderItem").Replace("PMItem", "WorkOrderItem"); //hacky last minute work around but workorder, quote and pm header objects have no tkeysection normally which can cause interference here with duplicate fields i.e. Tags in subsections //as they will have the header rule applied if we just leave the tkeysection as null so for here we workaround that by creating a temporary tkeysection if (FF.TKeySection == null) { switch (proposedObjectType) { case "WorkOrder": FF.TKeySection = proposedObjectType; break; case "Quote": FF.TKeySection = proposedObjectType; break; case "PM": FF.TKeySection = proposedObjectType; break; } } //don't validate custom fields, just skip them, make sure if it's sectional it matches the section of the object type (workorder sub sections) if (!FF.IsCustomField && (FF.TKeySection == null || proposedObjectType == FF.TKeySection)) { //bugbug: if section is workorderitem and field is tags but there is a tags required in workorder htat has NOT tkeysection it applies that rule //Now get the actual property name from the available fields using the lt key string RequiredPropertyName = FF.FieldKey; //there might be a more specific model property due to being a workorder sub item duplicate such as WorkOrderItemTags vs WorkOrderTags if (FF.ModelProperty != null) RequiredPropertyName = FF.ModelProperty; //Is it an indexed collection field? if (RequiredPropertyName.Contains(".")) { /* flag errors to include parent e.g in PO item validation error field name could be "Items[3].QuantityReceived" to indicate poitems collection 4th row has error in qtyreceived Note: collections are logically never more than 3 deep so for example the deepest would be workorder granchildren: e.g. Workorder.Items.Parts and most are two deep however like Po.PoItems.field for purposes of rule checking they would be flagged by their immediate parent "Items[3].QuantityReceived" for po or for workorder it could be a required UPC field in Workorder.Items.Parts.UPC would result in a target error return of "Items[2].Parts[3].UPC" would be valid */ var FieldKeyParts = RequiredPropertyName.Split('.'); if (FieldKeyParts.Length == 2) { //parent collection -> child field //target name like "Items.FieldName" var parentCollection = proposedObject.GetType().GetProperty(FieldKeyParts[0]).GetValue(proposedObject, null); int index = 0; foreach (object ChildObject in (parentCollection as System.Collections.IEnumerable)) { var fieldValue = ChildObject.GetType().GetProperty(FieldKeyParts[1]).GetValue(ChildObject, null); if (fieldValue == null || string.IsNullOrWhiteSpace(fieldValue.ToString())) biz.AddError(ApiErrorCode.VALIDATION_CUSTOM_REQUIRED_EMPTY, $"{FieldKeyParts[0]}[{index}].{FieldKeyParts[1]}"); index++; } } else if (FieldKeyParts.Length == 3) { //grandparent collection -> Parent collection -> Child field //target name like "WorkOrderItems.WorkOrderItemParts.UPC var GrandParentCollection = proposedObject.GetType().GetProperty(FieldKeyParts[0]).GetValue(proposedObject, null); int GrandParentIndex = 0; foreach (object GrandParentObject in (GrandParentCollection as System.Collections.IEnumerable)) { var ParentCollection = GrandParentObject.GetType().GetProperty(FieldKeyParts[1]).GetValue(GrandParentObject, null); int ParentIndex = 0; foreach (object ChildObject in (ParentCollection as System.Collections.IEnumerable)) { var fieldValue = ChildObject.GetType().GetProperty(FieldKeyParts[1]).GetValue(ChildObject, null); if (fieldValue == null || string.IsNullOrWhiteSpace(fieldValue.ToString())) biz.AddError(ApiErrorCode.VALIDATION_CUSTOM_REQUIRED_EMPTY, $"{FieldKeyParts[0]}[{GrandParentIndex}].{FieldKeyParts[1]}[{ParentIndex}].{FieldKeyParts[2]}"); ParentIndex++; } GrandParentIndex++; } } } else { //It's a simple property on the main object //use reflection to get the underlying value from the proposed object to be saved //issue the error on the *Models* property name to mirror how all other server error handling and validation works //so that client end consumes it properly (fieldkey is just for the UI and showing / hiding overall form values) object propertyValue = proposedObject.GetType().GetProperty(RequiredPropertyName).GetValue(proposedObject, null); if (propertyValue == null) biz.AddError(ApiErrorCode.VALIDATION_CUSTOM_REQUIRED_EMPTY, RequiredPropertyName); else { if (RequiredPropertyName == "Tags") { if (((System.Collections.Generic.List)propertyValue).Count == 0) { biz.AddError(ApiErrorCode.VALIDATION_CUSTOM_REQUIRED_EMPTY, RequiredPropertyName); } } else if (string.IsNullOrWhiteSpace(propertyValue.ToString())) biz.AddError(ApiErrorCode.VALIDATION_CUSTOM_REQUIRED_EMPTY, RequiredPropertyName); } } } } } } }//eoc }//ens