Efx Error Handling

I wrote a library that connects an external software with Epicor that processes POs and Invoices. The function takes a long string and separates the values on a specific basis, giving me all the properties needed to create an Invoice.

I’m having an issue with Error Handling on the AP Detail level. The program works as expected in every aspect except when trying to push an error through the system on the detail. In this test, we tried to send a receipt line and PO that doesn’t exist in the system. I was expecting the detail to error out, propagate the error to the caller which is also surrounded by a catch block. In this catch, I exit the loop, handle the error by notating the invoice head that errored, and then removing the details that may have been posted to the invoice head previously as the missing line would cause a variance.

When sending bad data to the head, the program errors properly and sends back an error to the caller which is handled like the detail should, it stores the error and moves on to the next head. Do I have my try/catch blocks misplaced? Am I missing something with the error handling?

‘Main’ Function

string errorLines = "";
string grpID = "";
int hedCount = 0;
int dtlCount = 0;
string dtlReturn = "";

if (!string.IsNullOrEmpty(VisionData))
{

  try
  {

    .....

    //
    // Create APInvGroups for Invoices to be held in
    //
    grpID = this.ThisLib.CreateAPInvGroup();


    //
    // Create multidimensional list to hold the values of each invoice head
    //   Dim.1 = Invoice Head  ------  Dim.2 = Invoice Detail information
    //
    List<List<string>> InvoiceList = new List<List<string>>();
    

    ....... // Create multidimensional list using loops to verify if it's been added already or not. If so, it adds another list item to the already populated head, else it adds a new invoice item to the list
    
    //
    // Loop through each head (first dimension of list)
    //
    bool hedCreated = false;
    foreach (var line in InvoiceList)
    {
      
    
        try
        {
        
            //
            // Create array of fields to populate APDtl & APHed
            //
            var fields = line[0].Split('|');
            hedCreated = false;
            
            
            //
            // Verfies that the head hasn't been created already
            //  resulting in an error being sent and a failure occurring
            //  on the invoice as a whole
            //
            if (!hedCreated)
            {
            
            
                //
                // *Create New Head
                // *Toggle bool that holds the value
                //   whether the hed has been created
                // *Increment the hed count for the 
                //   multidimensional list
                //
                this.ThisLib.CreateNewHed(line[0], grpID);
                hedCreated = true;
                hedCount++;
                
            }
            

            //
            // Set the detail counter to 0 and loop through
            //  each line and add the detail into Epicor.
            //
            
              dtlCount = 0;
              foreach(var apDtl in line)
              {

                //
                // Create New Dtl
                //
                this.ThisLib.CreateNewDtl(apDtl, grpID); // Doesn't error properly here
                
                
                //
                // Increment the counter for use in an error
                // 
                dtlCount++;
  
            }
        
        } catch(Exception ex)
        {
        
        
            //
            // Add errored line to listing
            //
        
            if(string.IsNullOrEmpty(errorLines))
            {
              
              //
              // if the string is empty, we don't want a tilde
              //  at the beginning of the string causing issues
              //  in the Console App outside of Epicor.
              //
              
              errorLines = $"{line[0]}|{ex.InnerException}";
            
            } else
            {
              
              //
              // If the string isn't blank, we want to append the
              //  new errored line and tag on the error description
              //  on the end of the line.
              //
            
              errorLines = $"{errorLines}~{line[0]}|{ex.InnerException}";
            
            }


            if (hedCreated)
            {

                //
                // Details need deleted prior to the head deletion
                //

                if (dtlCount > 0)
                {
                  
                  //
                  // If the detail count if 0, we skip looping
                  //  but we have already determined that the 
                  //  head was created thus needs deleted.
                  //
                  // If the count is greater than 0, we will
                  //  loop through each of the details and
                  //  send the lines to the deletion method
                  //  for handling
                  //
                  
                  for(int i = 0; i < dtlCount; i++)
                  {
                  
                      //
                      // Send the section defined to the deletion function
                      //
                      this.ThisLib.DeleteAPDetail(InvoiceList[hedCount][i], grpID);
  
                  }
                
                }

                
                //
                // Send the first item on list to get Invoice information to
                //  function for deletion
                //
                this.ThisLib.DeleteAPHead(InvoiceList[hedCount][0], grpID);

            }
        
        }
    
    }
    
    if (InvoiceList.Count == 0)
    {
    
      this.ThisLib.DeleteAPGroup(grpID);
      grpID = "";
    
    }
    
  } catch (Exception ex)
  {
  
    this.ThisLib.DeleteAPGroup(grpID);
  
    EpiResponse = false;
    ErroredLines = VisionData;
    GroupID = "";
  
  }

}

‘CreateNewHed’ Function that works and returns errors as intended

try
{

 ..... // Set a bunch of 'out' variables

  using(var trx = IceContext.CreateDefaultTransactionScope())
  {
  
  
    //
    // Build context and get services from Epicor to perform 
    //  database operations and set PO Variable
    //
    var context = Ice.Services.ContextFactory.CreateContext<ErpContext>();
    var APInv = ServiceRenderer.GetService<Erp.Contracts.APInvoiceSvcContract>(context);
    int PONum = fields[24] == "" ? 0 : Convert.ToInt32(fields[24].Trim());
    
    
    //
    //  Follow trace performed within Epicor to verify the
    //  methods utilized to create Heads
    //
    APInv.GetNewAPInvHedInvoice(ref ts, GroupID);
    
    
    //
    // Add the proper company to the invoice
    //
    ts.APInvHed[0].Company = fields[0].Trim();
    
    
    APInv.ChangeRefPONum(PONum, true, out confMess, ref ts);
    APInv.ValidateInvoiceID(fields[1], fields[3], out vendorNumOut, out APInvFound);
    APInv.ChangeInvoiceDateWithDateCheck(new DateTime(Convert.ToInt32(fields[15].Substring(0, 4)), Convert.ToInt32(fields[15].Substring(4, 2)), Convert.ToInt32(fields[15].Substring(6, 2))), "", out messText, out DateMess, ref ts);
    APInv.PreUpdate(ref ts, out userInput);
    APInv.CheckVendorTaxID(fields[1].Trim(), out errMess);
    
    
    //
    // Add values to specific fields on the Head
    //
    ts.APInvHed[0].InvoiceNum = fields[3].Trim();
    ts.APInvHed[0].DueDate = new DateTime(Convert.ToInt32(fields[15].Substring(0, 4)), Convert.ToInt32(fields[15].Substring(4, 2)), Convert.ToInt32(fields[15].Substring(6, 2)));
    ts.APInvHed[0].InvoiceDate = new DateTime(Convert.ToInt32(fields[4].Substring(0, 4)), Convert.ToInt32(fields[4].Substring(4, 2)), Convert.ToInt32(fields[4].Substring(6, 2)));
    ts.APInvHed[0].RateGrpCode = "MAIN";
    ts.APInvHed[0].ScrDocInvoiceVendorAmt = fields[18].Trim() == "" ? 0.0m : Convert.ToDecimal(fields[18].Trim());
    
    
    //
    // In the instance the record is a NON-PO, this will
    // include the required fields on the new row
    //
    if (PONum == 0)
    {
    
      ts.APInvHed[0].VendorNum = vendorNumOut;
      ts.APInvHed[0].TermsCode = "N30";
      ts.APInvHed[0].TaxAmt = fields[22].Trim() == "" ? 0.0m : Convert.ToDecimal(fields[22].Trim());
    
    }
    
    
    //
    // Final update
    //
    APInv.UpdateMaster(ref ts, GroupID, "APInvHed", true, true, true, true, out grpTotalInvAmt, out reqUserInput, out opMess, out opMsgChk, out opChkRev, out GenLedgNum, out UpdateRan, out DUAMsg);
    
    
    //
    // Close and dispose of the Transaction
    // Scope for the next use
    //
    trx.Complete();
    trx.Dispose();
    
    APInv.Dispose();
  
  }

} catch (Exception ex)
{
  
  throw new Exception("There was an error with the Head", ex);

}

‘CreateNewDtl’ Function doesn’t return properly

var fields = Line.Split('|');

try
{

  //
  // 'out' variables used within Epicor space
  //
  string errMess;
  string opLocMsg;
  decimal grpTotalAmt;
  bool requireUserInput;
  string opMess;
  string opMsgChk;
  string opChkRev;
  bool GenLedgNum;
  bool UpdateRan;
  string opDUAMsg;
  string glAcctDisp;
  string glAcctDesc;
  decimal grpTotalInvAmt;
  
  string comp = fields[0].Trim();
  string ID = fields[1];

  //
  // Required Variables
  //
  var RcptBill = new Erp.Tablesets.APInvReceiptBillingTableset();
  var APInvTS = new Erp.Tablesets.APInvoiceTableset();
  var Vend_Row = this.Db.Vendor.Where(vend => vend.Company == comp && vend.VendorID == ID).FirstOrDefault();
  
  if (Vend_Row != null)
  {
  
    //
    // Verify that the Vendor Row does have data prior to performing actions
    //

    using (var trx = IceContext.CreateDefaultTransactionScope())
    {
    
    
      //
      // Set the vendor number and PO Number for use in multiple 
      // fields. Run once and store the value
      //
      int VendNum = Vend_Row.VendorNum;
      int PONum = fields[24] == "" ? 0 : Convert.ToInt32(fields[24].Trim());
      
      
      //
      // Build context and get services from Epicor to perform 
      //  database operations
      //
      var context = Ice.Services.ContextFactory.CreateContext<ErpContext>();
      var APInv = ServiceRenderer.GetService<Erp.Contracts.APInvoiceSvcContract>(context);
      var GLAcc = ServiceRenderer.GetService<Erp.Contracts.GLAccountSvcContract>(context);
      
      
      //
      // Get Invoice Details
      //
      APInv.GetByID(VendNum, fields[3].Trim());
      
      
      //
      // Determine if there is a PO Number attached 
      //  to the invoice, if so we can grab details from 
      //  the PO, else we need to populate the fields
      //  with Vision Data provided
      //
      if (PONum != 0)
      {
        
        
        //
        // Gets all uninvoiced receipts against the PONumber
        //  and the corresponding receipt lines 
        //
        APInv.GetAPUninvoicedReceipts(ref RcptBill, VendNum, fields[3].Trim(), PONum);
        APInv.GetAPUninvoicedReceiptLines(ref RcptBill, VendNum, "", fields[28].Trim(), false, fields[3].Trim(), PONum);  // Believed to be error line; not caught in Exception - FATAL error if failed
        
        
        //
        // Set the proper Receipt Line = true
        //  and trigger the row mod to 'U' so 
        //  Epicor knows the value has changed
        //
        foreach(var prt in RcptBill.APUninvoicedRcptLines)
        {
        
          if(prt.PackSlip == fields[28].Trim() && prt.PackLine == Convert.ToInt32(fields[29].Trim()))
          {
          
            prt.SelectLine = true;
            prt.RowMod = "U";
          
          }
        
        }
        
        
        //
        // attached the selected Receipt Line to the Invoice Detail
        //
        APInv.SelectUninvoicedRcptLines(ref RcptBill, VendNum, "", PONum, fields[28].Trim(), false, fields[3].Trim(), false);
        
        
        //
        // Grabs the rest of the Detail data. Think
        //  assigned G/L Code, Receipt information, cost, etc.
        //
        APInv.InvokeInvoiceSelectedLines(ref RcptBill, out opLocMsg);
     
        
      } else
      {
        
        
        //
        // Get new Misc Dtl object with defaulted fields
        //  then call the ChangePartNum method to bring in details about
        //  the part if it was within the part table
        //
        APInv.GetNewAPInvDtlMiscellaneous(ref APInvTS, VendNum, fields[3].Trim());
        APInv.ChangePartNum("Vision NON-PO Part", ref APInvTS);
        
        
        //
        // Set the description of the part, if nod esscription provided, set
        //  the value to Vision Part.
        //
        APInvTS.APInvDtl[0].Description = fields[33].Trim() == "" ? "Vision Part" : fields[33].Trim();
        
        
        //
        // Change the quantity and cost with Epicor methods to verify data
        // and create other objects such as GL Acct and Tax information
        //
        APInv.ChangeVendorQty(Convert.ToDecimal(fields[30].Trim()), ref APInvTS);
        APInv.ChangeUnitCost((fields[31].Trim() == "" ? 0.0m : Convert.ToDecimal(fields[31])), ref APInvTS);
        
      }
      
      
      
      //
      // Verify the Vendor and make the final save
      //
      APInv.CheckVendorTaxID(fields[1].Trim(), out errMess);
      APInv.UpdateMaster(ref APInvTS, GroupID, "APInvDtl", true, false, true, false, out grpTotalAmt, out requireUserInput, out opMess, out opMsgChk, out opChkRev, out GenLedgNum, out UpdateRan, out opDUAMsg);
      
      
      //
      // Update GL code again due to data-saving needing to taking place
      //  prior to changing G/L code. Once the data is in DB, we can 
      //  alter the data in the table and resubmit
      //
      if (PONum == 0)
      {
      
        //
        // Update the APInvExp table with the correct G/L code and toggle RowMod = U
        //  for updating
        //
        APInvTS.APInvExp[0].GLAccount = $"{fields[9].Trim()}|00|{fields[10].Trim()}";
        APInvTS.APInvExp[0].ExpDispGLAcct = $"{fields[9].Trim()}|00|{fields[10].Trim()}";
        APInvTS.APInvExp[0].RowMod = "U";
        
        
        //
        // Perform the same actions on the APInvExpTGLC table
        //
        APInvTS.APInvExpTGLC[0].GLAccount = $"{fields[9].Trim()}|00|{fields[10].Trim()}";
        APInvTS.APInvExpTGLC[0].SegValue1 = fields[9].Trim();
        APInvTS.APInvExpTGLC[0].SegValue2 = "00";
        APInvTS.APInvExpTGLC[0].SegValue3 = fields[10].Trim();
        APInvTS.APInvExpTGLC[0].RowMod = "U";
       
        
        //
        // Validate the G/L Account is active and current
        //
        GLAcc.ValidateGLAccount("MAIN", $"{fields[9].Trim()}|00|{fields[10].Trim()}", "", "", null, true);
        GLAcc.GetGLAcctDispAndDesc("MAIN", $"{fields[9].Trim()}|00|{fields[10].Trim()}", "", false, "", out glAcctDisp, out glAcctDesc);
        
        
        //
        // Make a save against the APInvExp table
        //
        APInv.CheckVendorTaxID(fields[1].Trim(), out errMess);
        APInv.UpdateMaster(ref APInvTS, GroupID, "APInvExp", false, false, false, true, out grpTotalAmt, out requireUserInput, out opMess, out opMsgChk, out opChkRev, out GenLedgNum, out UpdateRan, out opDUAMsg);
      
     }
      
      //
      // Complete and close the transaction for 
      //  the next
      //
      trx.Complete();
      trx.Dispose();

      APInv.Dispose();
      GLAcc.Dispose();
      
    } // End of using statement
    
    
  } // end of Vend_Row If-statement
  
  
} catch (Exception ex)
{

  TestString = ex.Message;
  throw new Exception("There was an error building the Detail", ex);

}

I believe the issue lands on the GetAPUninvoicedReceiptLines as I threw a catch around it and returned the error (which worked properly) saying that it was an invalid receipt. I would expect that error to be passed through, but it isn’t for some reason. I’ve been testing using the Swagger page and Schedule Function module in Epicor. Any help would be super appreciated! I’ve spent more time than I’d like to admit running through this trying to identify the error and it’s for a SOX compliance action at our company.

I would add some logging in to shed some light on where your exception is being thrown and caught. I don’t really like how you silently handle the exception. How do you know that the caught error can be handled by deleting the detail/head? I don’t see anywhere where it would show the original exception to the caller.
I would re-write this using a transaction and let the BO throw the BLException back to the caller. If an error occurs, nothing is committed to the database.

2 Likes

Your try…catch is situated so that any exceptions should be caught.
Have you confirmed that the BO method you’re testing throws an exception under the conditions you’re testing? or does it return some other value?

So I do get some logging in the System Monitor when I run the function in the Schedule Function method which is why I utilize that. I can deduce that the error occurs specifically on the CreateNewDtl function. When running in Schedule Function, it returns the error properly in the System Monitor but when calling it from Main, the error doesn’t seem to propagate.

As for silently handles, what do you mean? In our preference, I would want this to be silent as it’s an automatic process with no active user monitoring or activating. This is tied into a program that lives on our in-house server to process the data back and forth. When an error is encountered, I’m wanting the catch blocks to perform certain instructions as we have to send back which Invoice Heads failed and the reasoning as to why. When an error occurs on the Head processing, it errors successfully but the Detail function is setup similarly but fails to perform similarly. As for if the error can be handled by deleting, I’m not concerned on the error generated because whatever the error is, a variance is now introduced into the AP Group and Invoice causing posting errors. I’ve been instructed to delete the whole head and detail upon error and return the head that errored out along with the reason.

This is where I’m getting stuck. The head is performing this properly but when I pass a faulty detail line to the program, it doesn’t pass the error back and I can’t figure out why it wouldn’t. The only thing I can think is maybe a bad try/catch placement but I can’t identify it.

I do appreciate you taking the time to respond too!!

The transaction scopes will cause hell with your error handling sometimes.
Something to keep in mind.

Let me look at this closer.

1 Like

Where are you expecting the error to be shown? You are handling the exception and it’s possible that nothing is being logged when that exception is caught.

I think your try catch is not specific enough with the type of exception and wraps too much of the code. Wouldn’t you want to know when a NullReferenceException is thrown instead of handling it without throwing the exception?

1 Like

I would use the transaction scope only for the main function, if I’m looking at the code right.
I also would use the checkbox on the function page, not creating it in code.

And you definitely need some logging, or at least a debug output variable.

2 Likes

The way you have it coded, this is not needed.

It will dispose when leaving the using statement.

2 Likes

I missed that you already created a transaction scope within the function. Save yourself some code and just check this box on the function.
image

1 Like

Also if this were me, I would restructure your code to never throw exceptions.

Pass back a boolean Success / Failure object, and the error.

2 Likes

Doing it twice can cause real issues.

1 Like

Yeah, take it out of the called functions. You just need it at the main function to wrap everything in a transaction scope.

3 Likes

In the case that I was running, I used a valid PO but used a line that didn’t exist on the PO. I was expecting some sort of ‘Invalid Line’ or ‘Invalid Receipt’ being sent back. I had put a string variable in the catch on CreateNewDtl and set it to the error message that was returned which said the invoice was invalid. I was expecting that to propagate into the Main.

I could see that the try/catch envelopes too much of the code, what would you recommend in changing that and still catching the errors properly if something fails within the function?

I’m not sure where this screenshot came from, I just verified all of the functions and none of them have the checkbox for Requires Transaction Scope set to true.

My main doesn’t have a scope surrounding it. My understanding when getting into Epicor is that anytime a BO is called, you have to use a transaction scope. Like you mentioned, having 2 scopes open at a single time causes havoc. With having multiple functions being called in one, the scope in Main would be open still and then it would attempt to open up a scope within the functions being called. Can I get away with not requiring transaction on all the functions being called on the only scope being the one in Main? Would that cause issues when I try to go to the DB to get a variable? Like below, I need to get the Vendor file but my recollection says that this errors if a scope is already active. Am I understanding incorrectly?

image

You have that exactly backwards. You should hardly ever have to use a transaction scope.

3 Likes

I would start by only catching BLException. This would narrow it down to the business logic within Kinetic. Other exceptions you will want to deal with by fixing your code likely.

Does the caller of this function have a way to log if the function call was successful or not? I’m not sure what you are integrating with, so it’s hard to say.

This is a good idea if you can somehow keep track of errors returned.

I personally use Hangfire for a lot of my integrations, so I prefer that a BLException is thrown so I can capture all the details and possibly retry the request. In the REST API, a BLException returns 400 with the error details, whereas most other exceptions return with 500 and the famous “Sorry! Something went wrong. Please contact your system administrator.”

It really depends on what you’re integrating with though.

1 Like

Sounds like you’re writing code outside of Epicor?
The screenshot is within Epicor Functions Maintenance under the Function definition

Just picking your brain, why’s that? Isn’t that what Exceptions are for as developers to handle when something goes wrong?

When you pass the error, are you just assigning the Exception.Message to a string and returning that or are you setting a variable to the type of System.Exception and returning the whole Exception object?

When should you use them then, only when you’re not using the BOs and you’re manipulating the DB values directly?

I have both. I have a C# console application that lives on our in-house server which calls the Epicor API to get data and generate a text file to be sFTP’d to the third-party software. I was hopeful they had REST lol

Sure! Background, I have a C# console app that I created and lives on our in-house server and calls the Epicor API. It creates text files of export data and sends the software Vision360 (process invoices and POs) and they in return send Invoices that vendors have emailed them. The C# program creates a string with all the data and passes it to Epicor to decipher and upload into the database. When it is successful, there is an export BAQ that returns all the invoices that were successful.

I’ll have to check that out thank you!!

1 Like