we need to modularize this code
bring the common validations to a seperate file
const utils = require('../../../../utils/utils');
const cfHandler = require('../utils/custom-field-handler');

const txF = require('../utils/netsuite-transaction-factory');

const salesOrderF = require('./sales-order-interface');
const itemFulfillmentF = require('./item-fulfillment-interface');
const invoiceF = require('./invoice-interface');
const creditMemoF = require('../credit-memo/credit-memo-interface');

const constants = require('../utils/constants');
const nspolicy = require('../../policy-constants');

function InvoiceEngine(app, job) {
    let self = this;

    let billServ = app.services.bill;

    let docUtilsServ = app.services.docUtils;
    
    let jd = job.getDescriptor();    

    let salesOrderInterf;
    let itemFulfillmentInterf;
    let invoiceInterf;
    let creditMemoInterf;

    self.getId = function() {
	return 'INVOICE';
    }

    async function markAsPermanentlyFailed(bill, msg) {
	let filter = {
	    _id: bill._id
	    //internalId: bill.internalId,
	    //tranType: bill.tranType
	};
	    
	let fields = {
	    txStatus: 'PERMANENTLY-FAILED',
	    txMessage: msg
	};
	    
	await docUtilsServ.updateFields(billServ, filter, fields);
    }
    
    // Return the Discount Subgroup to use based on Visit Type (OP, IP, and RETAIL) and Patient Category
    function getDiscountSubgroupForBill(bill) {
	const visitType = bill.incident.visitType;
	const patientCategory = bill.patientCategory;

	return constants.charge['DISCOUNT_' + visitType + '_' + patientCategory];
    }

    // Discount is always on the Patient amount
    function createDiscountBillItem(bill, amount) {

	let discountSubgroup = getDiscountSubgroupForBill(bill);
	
	return {
	    chargeType: 'CHARGE',
	    chargeSubgroup: {
		code: discountSubgroup,
		name: discountSubgroup
	    },
	    chargeCode: discountSubgroup,
	    itemType: constants.itemType.SERVICE,
	    rate: amount,
	    patientAmount: amount,
	    quantity: 1
	};		
    }

    // Roundoff is always on the Patient amount
    function createRoundoffBillItem(amount) {

	return {
	    chargeType: 'CHARGE',
	    chargeSubgroup: {
		code: constants.charge.ROUNDOFF,
		name: constants.charge.ROUNDOFF
	    },
	    chargeCode: constants.charge.ROUNDOFF,
	    itemType: constants.itemType.SERVICE,
	    rate: amount,
	    patientAmount: amount,
	    quantity: 1
	};		
    }

    function handleDiscount(jd, bill, bi, newItems) {
	if(jd.options.policy.discount === nspolicy.DISCOUNT_SEPARATE_LINE_ITEM) {
	    if(bi.discountAmount > 0) {
		let di = createDiscountBillItem(bill, -1 * bi.discountAmount);

		newItems.push(di);
	    }
	} else if(jd.options.policy.discount === nspolicy.DISCOUNT_RATE) {
	    bi.rate -= bi.discountAmount/bi.quantity;
	}
    }
    
    // Prepare Bill Items for submission:
    //
    // 1. Aggregate Charges by Subgroup, if posting is by Subgroup
    // 2. Create Charge Id based line item, if posting is by Charge Id.
    // 3. Add a Discount item below each entry if there is a discount, if policy requires separate Discount Line Item
    function prepareBillItems(bill) {
	let itemHash = {};

	let newItems = [];

	job.logDebug(utils.recursiveStringify(jd.options.policy));

	for(let bi of bill.items) {

	    if(['CHARGE', 'PACKAGE', 'BED-CHARGE'].includes(bi.chargeType)) {
		if(jd.options.policy.chargePosting === nspolicy.CHARGE_SUBGROUP_WISE_POSTING) {
		    // Aggregate CHARGE, PACKAGE, BED-CHARGE items but not the Inventory Items
		    let bkt = itemHash[bi.chargeSubgroup.code];
		
		    if(!bkt) {
			itemHash[bi.chargeSubgroup.code] = [];

			bkt = itemHash[bi.chargeSubgroup.code];
		    }

		    bkt.push(bi);
		} else { // CHARGE_ID_WISE_POSTING -- Posting by individual charge items (by chargeId)
		    bi = {
			// As a builtin capability, we map charges by chargeId and VisitType
			chargeCode: bi.chargeId + '::' + bill.incident.visitType,
			statutoryAccountId: bi.statutoryAccountId,
			chargeType: 'CHARGE',
			itemType: constants.itemType.SERVICE,
			chargeSubgroup: bi.chargeSubgroup, // ???? Check use,
			quantity: bi.quantity,
			patientAmount: bi.patientAmount,
			payer1Amount: bi.payer1Amount,
			taxAmount: bi.taxAmount,
			quantity: bi.quantity
		    }

		    newItems.push(bi);

		    handleDiscount(jd, bill, bi, newItems);
		}
	    } else {
		// Inventory Item. Currently only supports ITEM_WISE_POSTING
		bi.chargeCode = bi.chargeId;
		bi.itemType = constants.itemType.GOODS,

		newItems.push(bi);

		handleDiscount(jd, bill, bi, newItems);
	    }
	}

	if(jd.options.policy.chargePosting === nspolicy.CHARGE_SUBGROUP_WISE_POSTING) {
	    // Now process the Charge Subgroup buckets and create Bill Line Items for each
	    // Bucket
	    for(let k of Object.keys(itemHash)) {
		let bkt = itemHash[k];

		let quantity = 0;
		let patientAmount = 0;
		let payerAmount = 0;
		let taxAmount = 0;
		let discountAmount = 0;
		let statutoryAccountId;

		for(let it of bkt) {
		    patientAmount += it.patientAmount;
		    payerAmount += it.payer1Amount;
		    taxAmount += it.taxAmount;
		    discountAmount += it.discountAmount;

		    if(!statutoryAccountId) {
			statutoryAccountId = it.statutoryAccountId;
		    }		
		}

		// Intentionally dropping chargeGroup, expenseAccount, etc.
		// The dropped fields are not required for posting Invoices
		let bi = {
		    chargeCode: k + '::' + bill.incident.visitType,
		    statutoryAccountId: statutoryAccountId,
		    chargeType: 'CHARGE',
		    itemType: constants.itemType.SERVICE,
		    chargeSubgroup: {
			code: k
		    },

		    // Setting of quantity to 1 is intentional. Rate will be set to the patientAmount/payer1Amount
		    quantity: 1,
		
		    taxAmount: taxAmount,
		    patientAmount: patientAmount, // Note that patientAmount is the discounted amount
		    payer1Amount: payerAmount
		};
	    
		newItems.push(bi);

		handleDiscount(jd, bill, bi, newItems);

		// DISCOUNT_SEPARATE_LINE_ITEM is the only option we provide. Expecting to have that
		// checked during validateBill()
		if(jd.options.policy.discount === nspolicy.DISCOUNT_SEPARATE_LINE_ITEM) {

		    // bi.patientAmount is the discounted amount.
		    // Since we will be adding the Discount as a separate line item (a -ive value),
		    // we will add the discountAmount to bi.patientAmount first and then add a -ive
		    // discountAmount as a separate line item
		    if(discountAmount > 0) {
			bi.patientAmount += discountAmount;

			let di = createDiscountBillItem(bill, -1 * discountAmount);

			newItems.push(di);
		    }
		}
	    }
	}
	
	bill.items = newItems;
    }

    const COPAY_PREFIX = 'COPAY: ';
    
    // Preprocess every Bill
    //
    // 1. Aggregate bill line items by chargeSubgroup (all except Inventory Items DRUG/MATERIAL/FRAME/LENS)
    // 2. Discount amounts have to appear as separate bill line amounts with -ive amount <<<<
    // 3. Roundoff amount has to be added as a separate bill item with signed amount
    // 4. Add the Bill Number in Remarks
    // 5. Add taxNexus
    // 6. Renumber the Bill Items
    
    async function preprocessBill(bill) {

	// Step 1: Prepare bill items for submission
	prepareBillItems(bill);

	// Step 2: Process roundoff amount -- add as separate Discount bill item
	if(bill.totalRoundoffAmount !==0) {
	    let ro = createRoundoffBillItem(bill.totalRoundoffAmount);

	    bill.items.push(ro);
	}

	// Step 3: Add the Bill Number in Remarks
	let remarks;

	if(bill.tranType === 'BILL-CREATE') {
	    remarks = 'Bill';
	} else if(bill.tranType === 'RETAIL-BILL-CREATE') {
	    remarks = 'Retail Bill';
	} else if(bill.tranType === 'BILL-CANCEL') {
	    remarks = 'Bill Cancellation';
	} else {
	    let errMsg = 'Unknown tranType ' + bill.tranType + '....marking as PERMANENTLY-FAILED';

	    job.logError(errMsg);

	    await markAsPermanentlyFailed(bill, errMsg);

	    return null;
	}
	
	bill.remarks = remarks + ':' + bill.tranId + (bill.remarks ? (', ' + bill.remarks) : '');

	if(bill.totalPayer1Amount > 0 && bill.totalPatientAmount > 0) {
	    bill.remarks = COPAY_PREFIX + bill.remarks;
	}
	
	// Step 4: Add taxNexus
	bill.taxNexus = bill.customer.code;
	
	// Step 5: Renumber all the items
	let line = 1;
	for(let i in bill.items) {
	    bill.items[i].serialNumber = line++;
	}
    }

    async function sendInvoice(bill, tranName, successFunc) {
	await cfHandler.addFinancialTransactionFields(bill);

	let bills = [];

	bills.push(bill);

	let interfFunc = invoiceInterf.add;
	
	if(bill.payer !== bill.payer1.code) {
	    // It's a Patient Invoice. Suppress Payer Custom Fields
	    interfFunc = function(bill) {
		return invoiceInterf.add(bill, {suppressPayerCustomFields: true});
	    }
	}

	await docUtilsServ.sendDocuments(job, interfFunc, billServ, bills, tranName, successFunc);
    }
    
    async function sendPatientBill(bill) {

	async function recordPatientBillSuccess(bill, externalId) {
	    bill.patientBillExternalId = externalId;

	    let filter = {
		internalId: bill.internalId,
		tranType: bill.tranType
	    };
	    
	    let fields = {
		patientBillExternalId: externalId,
	    };

	    if(bill.totalPayer1Amount == 0 || bill.payerBillExternalId) {
		fields.txStatus = 'PROCESSED';
		fields.txMessage = 'Successfully posted';
	    }
	    
	    await docUtilsServ.updateFields(billServ, filter, fields);
	}

	await sendInvoice(bill, 'PATIENT-BILL', recordPatientBillSuccess);
    }

    async function sendPayerBill(bill) {

	bill.payer = bill.payer1.code;

	async function recordPayerBillSuccess(bill, externalId) {
	    bill.payerBillExternalId = externalId;

	    let filter = {
		internalId: bill.internalId,
		tranType: bill.tranType
	    };
	    
	    let fields = {
		payerBillExternalId: externalId
	    };

	    // Here we are assuming that the Patient part of a CREDIT bill will be
	    // posted BEFORE the Payer Bill
	    if(bill.totalPatientAmount == 0 || bill.patientBillExternalId) {
		fields.txStatus = 'PROCESSED';
		fields.txMessage = 'Successfully posted';
	    }

	    await docUtilsServ.updateFields(billServ, filter, fields);
	}

	await sendInvoice(bill, 'PAYER-BILL', recordPayerBillSuccess);
    }

    function extractPatientBill(bill) {
	// Create a clone
	let patientBill = JSON.parse(JSON.stringify(bill));

	patientBill.payer = constants.payer.MAIN_CASH_PATIENT;

	// We will go down the Bill Items and keep only items which have patientAmount > 0
	// We will set the netAmount to patientAmount
	patientBill.items =
	    patientBill.items.filter(
		function(bi) {
		    if(bi.patientAmount != 0) {
			bi.rate = bi.patientAmount  / bi.quantity;

			return true;
		    } else {
			return false;
		    }
		});
	
	return (patientBill.items.length > 0) ? patientBill : null;
    }
    
    function extractPayerBill(bill) {
	// Create a clone
	let payerBill = JSON.parse(JSON.stringify(bill));

	// Setting this so that sendPayerBill will advance the bill status to PROCESSED. @@ Can be cleaner
	payerBill.totalPatientAmount = 0;

	payerBill.payer = bill.payer1.code;

	// We will go down the Bill Items and keep only items which have payer1Amount > 0
	// We will set the netAmount to payer1Amount
	payerBill.items =
	    payerBill.items.filter(
		function(bi) {
		    if(bi.payer1Amount > 0) {
			bi.rate = bi.payer1Amount  / bi.quantity;

			return true;
		    } else {
			return false;
		    }
		});
	
	return (payerBill.items.length > 0) ? payerBill : null;
    }

    async function sendMainPatientBill(bill) {
	bill.payer = constants.payer.MAIN_CASH_PATIENT;

	for(bi of bill.items) {
	    bi.rate = bi.patientAmount / bi.quantity;
	}

	await sendPatientBill(bill);
    }

    async function sendMainCreditBill(bill) {

	if(!bill.patientBillExternalId) {
	    let patientBill = extractPatientBill(bill);

	    // Send Invoice only if there is a Patient part of the Bill
	    if(patientBill) {
		await sendMainPatientBill(patientBill);

		bill.patientBillExternalId = patientBill.patientBillExternalId;
	    }
	}

	// Process the Payer Bill, if not done already, ONLY if the Paitient Bill is done or Patient part is 0 
	if((bill.totalPatientAmount == 0 || bill.patientBillExternalId) && !bill.payerBillExternalId) {
	    let payerBill  = extractPayerBill(bill);

	    // Send Invoice only if there is a Payer part of the Bill
	    if(payerBill) {
		await sendPayerBill(payerBill);
	    }
	}

	// Patient and Payer parts are 0 and no Patient Bill has been posted
	if(!bill.patientBillExternalId && bill.totalPatientAmount == 0 && bill.totalPayer1Amount == 0 ) {
	    // Mark bill as PROCESSED
	    let filter = {
		internalId: bill.internalId,
		tranType: bill.tranType
	    };
	    
	    let fields = {
		txStatus: 'PROCESSED',
		txMessage: 'Patient and Payer amounts are 0. Marked as PROCESSED'
	    };

	    await docUtilsServ.updateFields(billServ, filter, fields);
	}
    }


    async function sendItemFulfillment(bill, options) {

	async function itemFulfillmentSuccess(doc, externalId) {
	    bill.fulfillmentId = externalId;

	    let filter = {
		internalId: bill.internalId,
		tranType: 'RETAIL-BILL-CREATE'
	    };
	    
	    let fields = {
		fulfillmentId: externalId
	    };
	    
	    await docUtilsServ.updateFields(billServ, filter, fields);
	}

	let bills = [];

	bills.push(bill);
	
	let interfFunc = itemFulfillmentInterf.add;
	
	if(options || bill.payer !== bill.payer1.code) {
	    let opts = options ? options : {};

	    opts.suppressPayerCustomFields = true;
		
	    interfFunc = function(bill) {

		return itemFulfillmentInterf.add(bill, opts);
	    }
	}

	await docUtilsServ.sendDocuments(job, interfFunc, billServ, bills, 'ITEM-FULFILLMENT', itemFulfillmentSuccess);
    }

    async function sendSalesOrder(bill, options) {

	async function salesOrderSuccess(order, externalId) {
	    bill.orderId = externalId;

	    let filter = {
		internalId: bill.internalId,
		tranType: 'RETAIL-BILL-CREATE'
	    };
	    
	    let fields = {
		orderId: externalId
	    };
	    
	    await docUtilsServ.updateFields(billServ, filter, fields);
	}
	
	let bills = [];

	bills.push(bill);

	let interfFunc = salesOrderInterf.add;
	
	if(options || bill.payer !== bill.payer1.code) {
	    let opts = options ? options : {};

	    opts.suppressPayerCustomFields = true;
		
	    interfFunc = function(bill) {

		return salesOrderInterf.add(bill, opts);
	    }
	}
	
	await docUtilsServ.sendDocuments(job, interfFunc, billServ, bills, 'SALES-ORDER', salesOrderSuccess);
    }

    async function sendCreditMemo(bill, successFunc, options) {
	await cfHandler.addFinancialTransactionFields(bill);
	
	let bills = [];

	bills.push(bill);

	let interfFunc = creditMemoInterf.add;
	
	if(options || bill.payer !== bill.payer1.code) {
	    let opts = options ? options : {};

	    opts.suppressPayerCustomFields = true;
		
	    interfFunc = function(bill) {

		return creditMemoInterf.add(bill, opts);
	    }
	}

	await docUtilsServ.sendDocuments(job, interfFunc, billServ, bills, 'CREDIT-MEMO', successFunc);
    }
    
    async function sendAdjustmentCreditMemo(bill) {

	async function creditMemoSuccess(order, externalId) {
	    bill.creditNoteId = externalId;

	    let filter = {
		internalId: bill.internalId,
		tranType: 'RETAIL-BILL-CREATE'
	    };
	    
	    let fields = {
		creditNoteId: externalId
	    };
	    
	    await docUtilsServ.updateFields(billServ, filter, fields);
	}

	await sendCreditMemo(bill, creditMemoSuccess);
    }
    
    async function sendRetailPatientBill(bill) {
	bill.payer = constants.payer.RETAIL_CASH_PATIENT;

	await sendPatientBill(bill);
    }

    function createCopayAdjustmentBillItem(serialNumber, amount, remarks) {

	return {
	    serialNumber: serialNumber,
	    chargeType: 'CHARGE',
	    chargeSubgroup: {
		code: constants.charge.COPAY_ADJUSTMENT,
		name: constants.charge.COPAY_ADJUSTMENT,
	    },
	    chargeCode: constants.charge.COPAY_ADJUSTMENT,
	    itemType: constants.itemType.SERVICE,
	    quantity: 1,
	    rate: amount,
	    remarks: remarks
	};		
    }

    function appendRemarks(bill, addRemarks) {
	return (bill.remarks ? (bill.remarks + ', '): '') + addRemarks;
    }

    async function sendRetailBill(bill) {

	let inventoryChargeTypes = ['DRUG', 'MATERIAL', 'FRAME', 'LENS'];

	// Kind of HACK to handle cases when someone has incorrectly set status as NEW
	// but all steps are actually done. If neededProcessing remains false till the
	// end, we will force it to be PROCESSED
	let neededProcessing = false;  
	    
	if(!bill.orderId || !bill.fulfillmentId) {
	    neededProcessing = true;

	    // Either Sales Order or Item Fulfillment needs to be sent

	    let billCopy = JSON.parse(JSON.stringify(bill));

	    // Sales Order and Item Fulfillment will be for the Patient
	    billCopy.payer = constants.payer.RETAIL_CASH_PATIENT;
	    billCopy.tranType = 'ITEM-ORDER-CREATE';
	    
	    // Set all Inventory related custom fields
	    //await cfHandler.addInventoryTransactionFields(billCopy);
	    app.custom.transformMapFactory.addInventoryTransactionFields(billCopy);

	    // Renumber all the items. @@ NOT REQUIRED!!
	    let line = 1;
	    for(let i in billCopy.items) {
		billCopy.items[i].serialNumber = line++;
	    }

	    job.logDebug('Bill object for Sales Order and Item Fulfillment...');
	    job.logDebug(utils.recursiveStringify(billCopy));
	    
	    // Step 1: Send Sales Order on Patient, if not already sent successfully
	    if(!billCopy.orderId) {
		await sendSalesOrder(billCopy);

		bill.orderId = billCopy.orderId;
	    }

	    // No need to send non-Inventory Items in Item Fulfillment
	    billCopy.items =
		billCopy.items.filter(function(bi)
				      {
					  return inventoryChargeTypes.includes(bi.chargeType);
				      });

	    billCopy.tranType = 'PATIENT-RETAIL-ISSUE';
	    
	    // Set all Inventory related custom fields
	    // await cfHandler.addInventoryTransactionFields(billCopy);
	    app.custom.transformMapFactory.addInventoryTransactionFields(billCopy);
	    
	    // Step 2: Send Item Fulfillment on Sales Order, if not already sent successfully
	    if(billCopy.orderId && !billCopy.fulfillmentId) {
		await sendItemFulfillment(billCopy);

		bill.fulfillmentId = billCopy.fulfillmentId;
	    }
	}

	// Step 3: Send Invoice for Patient, if not already sent successfully
	let billForInvoice = JSON.parse(JSON.stringify(bill));

	billForInvoice.tranType = 'RETAIL-BILL-CREATE';
	billForInvoice.remarks = appendRemarks(bill, 'Patient Invoice');
	billForInvoice.source = bill.orderId;
	
	// Not sending ANY Items in Invoice. Will pick up from Sales Order
	billForInvoice.items = [];
	
	if(bill.fulfillmentId && !bill.patientBillExternalId) {
	    neededProcessing = true;

	    // Time to send the Patient Bill
	    await sendRetailPatientBill(billForInvoice);

	    bill.patientBillExternalId = billForInvoice.patientBillExternalId;
	}

	// Step 4: Credit the Patient for Payer Amount, if any
	if(bill.patientBillExternalId && (!bill.creditNoteId || !bill.payerBillExternalId) && bill.totalPayer1Amount > 0) {
	    neededProcessing = true;

	    let adjBill = JSON.parse(JSON.stringify(bill));

	    // Redo the itemList
	    adjBill.items = [];

	    let bi = createCopayAdjustmentBillItem(1, bill.totalPayer1Amount, 'Copay Adjustment for ' + bill.tranId);

	    adjBill.items.push(bi);

	    if(!bill.creditNoteId) {
		adjBill.tranType = 'RETAIL-BILL-CANCEL';
		adjBill.payer = constants.payer.RETAIL_CASH_PATIENT;
		adjBill.remarks = appendRemarks(bill, 'Copay Adjustment');
		
		job.logDebug('Adjustment Bill....');
		job.logDebug(utils.recursiveStringify(adjBill));
	    
		await sendAdjustmentCreditMemo(adjBill);

		bill.creditNoteId = adjBill.creditNoteId;
	    }

	    // Step 5: Send Invoice for Payer for Payer Amount, if any
	    if(bill.creditNoteId && !bill.payerBillExternalId) {
		adjBill.tranType = 'RETAIL-BILL-CREATE';
		
		adjBill.remarks = appendRemarks(bill, 'Payer Invoice');
		
		await sendPayerBill(adjBill);
	    }
	
	}

	if(!neededProcessing) {
	    job.logInfo('Bill ' + bill.tranId + ' is incorrectly marked as NEW. Marking it as PROCESSED');

	    // Incorrectly marked as NEW. Mark as PROCESSED
	    // Mark bill as PROCESSED
	    let filter = {
		internalId: bill.internalId,
		tranType: bill.tranType
	    };
	    
	    let fields = {
		txStatus: 'PROCESSED',
		txMessage: 'Overwriting NEW to PROCESSED'
	    };

	    await docUtilsServ.updateFields(billServ, filter, fields);
	}
    }

    async function sendCancellationCreditMemo(bill, externalIdFieldName, options) {
	
	async function creditMemoSuccess(doc, externalId) {
	    bill[externalIdFieldName] = externalId;

	    let filter = {
		_id: bill._id
	    };

	    // This is the last step in posting a Sales Return. Can mark it as PROCESSED now.
	    let fields;

	    if(externalIdFieldName === 'patientBillExternalId') {
		fields = {
		    patientBillExternalId: externalId
		}

		if(bill.totalPayer1Amount == 0) {
		    fields.txStatus = 'PROCESSED';
		    fields.txMessage = 'Successfully posted';
		}
	    } else if(externalIdFieldName === 'payerBillExternalId') {
		fields = {
		    payerBillExternalId: externalId,
		    txStatus: 'PROCESSED',
		    txMessage: 'Successfully posted'
		}
	    }

	    await docUtilsServ.updateFields(billServ, filter, fields);
	}
	
	await sendCreditMemo(bill, creditMemoSuccess, options);
    }

    async function sendBillCancellation(bill) {

	// Let's get the original Bill
	let origBill = await billServ.findOne({internalId: bill.internalId, tranType: 'BILL-CREATE', txStatus: 'PROCESSED'});

	if(!origBill) {
	    // The original Bill is not in our database as yet. Skip.

	    const msg = 'SKIPPING:' + bill.tranType + ' ' + bill.tranId + ': Waiting for BILL-CREATE';
	        
	    //job.logError(msg);

	    await docUtilsServ.setMessage(billServ, bill, msg);
	    
	    return null;
	}
	
	if((bill.totalPatientAmount + bill.totalDiscountAmount) > 0 && !bill.patientBillExternalId) {
	    let patientBill = extractPatientBill(bill);

	    // Send Credit Memo only if there is a Patient part of the Bill
	    if(patientBill) {
		let options = {
		    source: {
			txType: txF.INVOICE,
			id: origBill.patientBillExternalId
		    },
		    externalId: bill.internalId + '-PATIENT-CM',
		    setItemList: true,
		    setOrderLine: true
		};
		
		await sendCancellationCreditMemo(patientBill, 'patientBillExternalId', options);

		if(!patientBill.patientBillExternalId) {
		    // Some error happened
		    // Return without processing the Payer part
		    return;
		}
	    }
	}

	if(bill.totalPayer1Amount > 0 && !bill.payerBillExternalId) {
	    let payerBill  = extractPayerBill(bill);

	    // Send Credit Memo only if there is a Payer part of the Bill
	    if(payerBill) {
		let options = {
		    source: {
			txType: txF.INVOICE,
			id: origBill.payerBillExternalId
		    },
		    externalId: bill.internalId + '-PAYER-CM',
		    setItemList: true,
		    setOrderLine: true
		};

		await sendCancellationCreditMemo(payerBill, 'payerBillExternalId', options);
	    }
	}
    }

    async function validateBill(bill) {

	// Validate policies
	if(jd.options.policy.chargePosting === nspolicy.CHARGE_SUBGROUP_WISE_POSTING &&
	   jd.options.policy.discount !== nspolicy.DISCOUNT_SEPARATE_LINE_ITEM) {

	    let errMsg = 'With CHARGE_SUBGROUP_WISE_POSTING only DISCOUNT_SEPARATE_LINE_ITEM is supported. Please fix the policy settings';

	    job.logError(errMsg);

	    return false;
	}
	
	// Check if total Patient and Payer ammounts in header matches with the details
	let totalPatientAmount = 0;
	let totalPayerAmount = 0;

	for(let bi of bill.items) {
	    totalPatientAmount += bi.patientAmount;
	    totalPayerAmount += bi.payer1Amount;
	}

	if(Math.abs(totalPatientAmount - bill.totalPatientAmount) < 1 &&
	   Math.abs(totalPayerAmount - bill.totalPayer1Amount) < 1) {
	    return true;
	} else {
	    let errMsg = 'Amounts in Bill Header do not match those in Bill items for ' + bill.tranId;
		
	    job.logError('Header Patient Amount = ' + bill.totalPatientAmount + ', Detail Patient Amount = ' + totalPatientAmount);
	    job.logError('Header Payer Amount = ' + bill.totalPayer1Amount + ', Detail Payer Amount = ' + totalPayerAmount);
	    
	    job.logError(errMsg);

	    await markAsPermanentlyFailed(bill, errMsg);
	    
	    return false;
	}
    }
    
    async function processBills(fromTime, toTime) {

	let bills;

	job.logDebug('Bill Filter = ' + utils.recursiveStringify(jd.options.filter));

	bills = await billServ.getAllPending(jd.options.filter);

	if(!bills) {
	    job.logError('billServ.getAllPending() returns ' + bills);

	    return true;
	}

	job.logDebug('###### ' + bills.length + (bills.length == 1 ? ' Bill' : ' Bills') + ' to process...');

	if(bills.length == 0) {
	    // Nothing to do
	    return true;
	}
	
	if(job.singleStepMode) {
	    job.logDebug('>>>>> SINGLE STEP MODE....PROCESSING SINGLE BILL');
	    
	    if(bills.length > 1) {
		bills.splice(-1 * (bills.length - 1), bills.length - 1);
	    }
	}
	
	for(let b of bills) {

	    if(app.interrupted) {
		return true;
	    }

	    b = b.toJSON();
	    
	    // Validate if this Bill has any problems. Ignore if it fails validation
	    if(!await validateBill(b)) {
		continue;
	    }
	    
	    //	    try {

	   // Required for every Bill
	    await preprocessBill(b);

	    if(b.totalGrossAmount == 0) {
		job.logInfo(b.tranType + ' ' + b.tranId + ': ' + 'Gross Amount 0. Marking as PROCESSED.');
		
		await docUtilsServ.markAsProcessed(billServ, {_id: b._id}, 0, 'Gross Amount 0. Marking as PROCESSED.');

		continue;
	    }

//	    job.logDebug('Pre-processed bill...');
//	    job.logDebug(utils.recursiveStringify(b));

	    if(b.tranType === 'BILL-CANCEL') {
		await sendBillCancellation(b);
	    } else if(b.tranType === 'BILL-CREATE' || b.tranType == 'RETAIL-BILL-CREATE') {

		switch(b.category) {

		case 'MAIN':

		    if(!b.payer1.code) {
			// MAIN Patient Bill
			await sendMainPatientBill(b);
		    } else {
			// MAIN Credit Bill
			await sendMainCreditBill(b);
		    }
		    
		    break;

		case 'RETAIL':
		    await sendRetailBill(b);
		    
		    break;

		default:
		    job.logError('Unknown Bill Category for Bill ' + b.tranId + ' (' + b.internalId + ')');

		    break;
		}
	    } else {
		job.logError('Unknown tranType ' + b.tranType);
	    }
	    //	    } catch(err) {
	    //		job.logError('Error while processing Bill ' + b.tranId);
	    //		job.logError(JSON.stringify(err));
	    //          job.logError('Skipping to next');
	    //	    }
	}

	return true;
    }

    self.exec = async function(fromTime, toTime) {
	let jd = job.getDescriptor();
	
	let log = {
	    source: jd.sourceEntity,
	    target: jd.targetEntity
	};

	let nsClient = await app.adapters.netsuite.createInstance(app, jd.connectionSpec, job, log);

	salesOrderInterf = salesOrderF.createInstance(app, nsClient, job);
	itemFulfillmentInterf = itemFulfillmentF.createInstance(app, nsClient, job);
	invoiceInterf = invoiceF.createInstance(app, nsClient, job);
	creditMemoInterf = creditMemoF.createInstance(app, nsClient, job);

	return await processBills(fromTime, toTime);
    }
}

function createInstance(app, job) {
    
    let inve = new InvoiceEngine(app, job);

    return inve;
}

module.exports = {
    createInstance: createInstance
}