Skip to content

Commit 98347b0

Browse files
committed
feat: add input validation and error handling to QuickBooks utilities - Add comprehensive validation to buildSalesLineItems and buildPurchaseLineItems - Refactor conditional logic for better readability - Add error handling in update-invoice after getInvoice call
1 parent 1009642 commit 98347b0

File tree

5 files changed

+189
-33
lines changed

5 files changed

+189
-33
lines changed

components/quickbooks/actions/create-estimate/create-estimate.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,9 @@ export default {
151151
? parseLineItems(this.lineItems)
152152
: this.buildLineItems();
153153

154-
lines.forEach((line) => {
154+
lines.forEach((line, index) => {
155155
if (line.DetailType !== "SalesItemLineDetail" && line.DetailType !== "GroupLineDetail" && line.DetailType !== "DescriptionOnly") {
156-
throw new ConfigurationError("Line Item DetailType must be `SalesItemLineDetail`, `GroupLineDetail`, or `DescriptionOnly`");
156+
throw new ConfigurationError(`Line Item at index ${index + 1} has invalid DetailType '${line.DetailType}'. Must be 'SalesItemLineDetail', 'GroupLineDetail', or 'DescriptionOnly'`);
157157
}
158158
});
159159

components/quickbooks/actions/create-purchase-order/create-purchase-order.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,9 @@ export default {
131131
throw new ConfigurationError("No valid line items were provided.");
132132
}
133133

134-
lines.forEach((line) => {
134+
lines.forEach((line, index) => {
135135
if (line.DetailType !== "ItemBasedExpenseLineDetail" && line.DetailType !== "AccountBasedExpenseLineDetail") {
136-
throw new ConfigurationError("Line Item DetailType must be `ItemBasedExpenseLineDetail` or `AccountBasedExpenseLineDetail`");
136+
throw new ConfigurationError(`Line Item at index ${index + 1} has invalid DetailType '${line.DetailType}'. Must be 'ItemBasedExpenseLineDetail' or 'AccountBasedExpenseLineDetail'`);
137137
}
138138
});
139139

components/quickbooks/actions/update-estimate/update-estimate.mjs

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,19 @@ export default {
149149
buildLineItems() {
150150
return buildSalesLineItems(this.numLineItems, this);
151151
},
152+
// Helper function to conditionally add properties
153+
addIfDefined(target, source, mappings) {
154+
Object.entries(mappings).forEach(([sourceKey, targetConfig]) => {
155+
const value = source[sourceKey];
156+
if (value !== undefined && value !== null) {
157+
if (typeof targetConfig === "string") {
158+
target[targetConfig] = value;
159+
} else if (typeof targetConfig === "object") {
160+
target[targetConfig.key] = targetConfig.transform ? targetConfig.transform(value) : value;
161+
}
162+
}
163+
});
164+
},
152165
},
153166
async run({ $ }) {
154167
// Get the current estimate to obtain SyncToken
@@ -174,22 +187,25 @@ export default {
174187
? parseLineItems(this.lineItems)
175188
: this.buildLineItems();
176189

177-
lines.forEach((line) => {
190+
lines.forEach((line, index) => {
178191
if (line.DetailType !== "SalesItemLineDetail" && line.DetailType !== "GroupLineDetail" && line.DetailType !== "DescriptionOnly") {
179-
throw new ConfigurationError("Line Item DetailType must be `SalesItemLineDetail`, `GroupLineDetail`, or `DescriptionOnly`");
192+
throw new ConfigurationError(`Line Item at index ${index + 1} has invalid DetailType '${line.DetailType}'. Must be 'SalesItemLineDetail', 'GroupLineDetail', or 'DescriptionOnly'`);
180193
}
181194
});
182195

183196
data.Line = lines;
184197
}
185198

186-
if (this.expirationDate) data.ExpirationDate = this.expirationDate;
187-
if (this.acceptedBy) data.AcceptedBy = this.acceptedBy;
188-
if (this.acceptedDate) data.AcceptedDate = this.acceptedDate;
189-
if (this.docNumber) data.DocNumber = this.docNumber;
190-
if (this.billAddr) data.BillAddr = this.billAddr;
191-
if (this.shipAddr) data.ShipAddr = this.shipAddr;
192-
if (this.privateNote) data.PrivateNote = this.privateNote;
199+
// Add simple field mappings
200+
this.addIfDefined(data, this, {
201+
expirationDate: "ExpirationDate",
202+
acceptedBy: "AcceptedBy",
203+
acceptedDate: "AcceptedDate",
204+
docNumber: "DocNumber",
205+
billAddr: "BillAddr",
206+
shipAddr: "ShipAddr",
207+
privateNote: "PrivateNote",
208+
});
193209

194210
if (this.billEmail) {
195211
data.BillEmail = {
@@ -214,8 +230,10 @@ export default {
214230
data,
215231
});
216232

217-
if (response) {
233+
if (response?.Estimate?.Id) {
218234
$.export("summary", `Successfully updated estimate with ID ${response.Estimate.Id}`);
235+
} else {
236+
throw new ConfigurationError("Failed to update estimate: Invalid response from QuickBooks API");
219237
}
220238

221239
return response;

components/quickbooks/actions/update-invoice/update-invoice.mjs

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,42 @@ export default {
155155
buildLineItems() {
156156
return buildSalesLineItems(this.numLineItems, this);
157157
},
158+
addIfDefined(target, source, mappings) {
159+
Object.entries(mappings).forEach(([sourceKey, targetConfig]) => {
160+
const value = source[sourceKey];
161+
if (value !== undefined && value !== null) {
162+
if (typeof targetConfig === "string") {
163+
target[targetConfig] = value;
164+
} else if (typeof targetConfig === "object") {
165+
target[targetConfig.key] = targetConfig.transform ? targetConfig.transform(value) : value;
166+
}
167+
}
168+
});
169+
},
158170
},
159171
async run({ $ }) {
160172
// Get the current invoice to obtain SyncToken
161-
const { Invoice: invoice } = await this.quickbooks.getInvoice({
173+
const response = await this.quickbooks.getInvoice({
162174
$,
163175
invoiceId: this.invoiceId,
164176
});
165177

178+
// Validate that the invoice was found and is valid
179+
if (!response || !response.Invoice) {
180+
throw new ConfigurationError(`Invoice with ID '${this.invoiceId}' not found. Please verify the invoice ID is correct.`);
181+
}
182+
183+
const invoice = response.Invoice;
184+
185+
// Validate that the invoice has required properties
186+
if (!invoice.Id) {
187+
throw new ConfigurationError(`Invalid invoice data received: missing Invoice ID`);
188+
}
189+
190+
if (!invoice.SyncToken) {
191+
throw new ConfigurationError(`Invalid invoice data received: missing SyncToken. This may indicate the invoice is in an invalid state.`);
192+
}
193+
166194
const data = {
167195
Id: this.invoiceId,
168196
SyncToken: invoice.SyncToken,
@@ -180,23 +208,30 @@ export default {
180208
? parseLineItems(this.lineItems)
181209
: this.buildLineItems();
182210

183-
lines.forEach((line) => {
211+
lines.forEach((line, index) => {
184212
if (line.DetailType !== "SalesItemLineDetail" && line.DetailType !== "GroupLineDetail" && line.DetailType !== "DescriptionOnly") {
185-
throw new ConfigurationError("Line Item DetailType must be `SalesItemLineDetail`, `GroupLineDetail`, or `DescriptionOnly`");
213+
throw new ConfigurationError(`Line Item at index ${index + 1} has invalid DetailType '${line.DetailType}'. Must be 'SalesItemLineDetail', 'GroupLineDetail', or 'DescriptionOnly'`);
186214
}
187215
});
188216

189217
data.Line = lines;
190218
}
191219

192-
if (this.dueDate) data.DueDate = this.dueDate;
193-
if (typeof this.allowOnlineCreditCardPayment === "boolean") data.AllowOnlineCreditCardPayment = this.allowOnlineCreditCardPayment;
194-
if (typeof this.allowOnlineACHPayment === "boolean") data.AllowOnlineACHPayment = this.allowOnlineACHPayment;
195-
if (this.docNumber) data.DocNumber = this.docNumber;
196-
if (this.billAddr) data.BillAddr = this.billAddr;
197-
if (this.shipAddr) data.ShipAddr = this.shipAddr;
198-
if (this.trackingNum) data.TrackingNum = this.trackingNum;
199-
if (this.privateNote) data.PrivateNote = this.privateNote;
220+
this.addIfDefined(data, this, {
221+
dueDate: "DueDate",
222+
docNumber: "DocNumber",
223+
billAddr: "BillAddr",
224+
shipAddr: "ShipAddr",
225+
trackingNum: "TrackingNum",
226+
privateNote: "PrivateNote",
227+
});
228+
229+
if (typeof this.allowOnlineCreditCardPayment === "boolean") {
230+
data.AllowOnlineCreditCardPayment = this.allowOnlineCreditCardPayment;
231+
}
232+
if (typeof this.allowOnlineACHPayment === "boolean") {
233+
data.AllowOnlineACHPayment = this.allowOnlineACHPayment;
234+
}
200235

201236
if (this.billEmail) {
202237
data.BillEmail = {
@@ -216,15 +251,17 @@ export default {
216251
};
217252
}
218253

219-
const response = await this.quickbooks.updateInvoice({
254+
const updateResponse = await this.quickbooks.updateInvoice({
220255
$,
221256
data,
222257
});
223258

224-
if (response) {
225-
$.export("summary", `Successfully updated invoice with ID ${response.Invoice.Id}`);
259+
if (updateResponse?.Invoice?.Id) {
260+
$.export("summary", `Successfully updated invoice with ID ${updateResponse.Invoice.Id}`);
261+
} else {
262+
throw new ConfigurationError("Failed to update invoice: Invalid response from QuickBooks API");
226263
}
227264

228-
return response;
265+
return updateResponse;
229266
},
230267
};

components/quickbooks/common/utils.mjs

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,45 @@ export function parseLineItems(arr) {
4646
}
4747

4848
export function buildSalesLineItems(numLineItems, context) {
49+
// Validate numLineItems parameter
50+
if (typeof numLineItems !== "number" || !Number.isInteger(numLineItems) || numLineItems <= 0) {
51+
throw new ConfigurationError("numLineItems must be a positive integer");
52+
}
53+
54+
// Validate context parameter
55+
if (!context || typeof context !== "object" || Array.isArray(context)) {
56+
throw new ConfigurationError("context must be an object");
57+
}
58+
59+
// Validate required keys exist for each line item
60+
const missingKeys = [];
61+
for (let i = 1; i <= numLineItems; i++) {
62+
if (!context.hasOwnProperty(`amount_${i}`)) {
63+
missingKeys.push(`amount_${i}`);
64+
}
65+
if (!context.hasOwnProperty(`item_${i}`)) {
66+
missingKeys.push(`item_${i}`);
67+
}
68+
}
69+
70+
if (missingKeys.length > 0) {
71+
throw new ConfigurationError(`Missing required keys in context: ${missingKeys.join(", ")}`);
72+
}
73+
74+
// Validate amount values are valid numbers
75+
const invalidAmounts = [];
76+
for (let i = 1; i <= numLineItems; i++) {
77+
const amount = context[`amount_${i}`];
78+
if (amount !== undefined && amount !== null && amount !== "" &&
79+
(typeof amount !== "number" && (typeof amount !== "string" || isNaN(parseFloat(amount))))) {
80+
invalidAmounts.push(`amount_${i}`);
81+
}
82+
}
83+
84+
if (invalidAmounts.length > 0) {
85+
throw new ConfigurationError(`Invalid amount values for: ${invalidAmounts.join(", ")}. Amounts must be valid numbers.`);
86+
}
87+
4988
const lineItems = [];
5089
for (let i = 1; i <= numLineItems; i++) {
5190
lineItems.push({
@@ -62,18 +101,80 @@ export function buildSalesLineItems(numLineItems, context) {
62101
}
63102

64103
export function buildPurchaseLineItems(numLineItems, context) {
104+
// Validate numLineItems parameter
105+
if (typeof numLineItems !== "number" || !Number.isInteger(numLineItems) || numLineItems <= 0) {
106+
throw new ConfigurationError("numLineItems must be a positive integer");
107+
}
108+
109+
// Validate context parameter
110+
if (!context || typeof context !== "object" || Array.isArray(context)) {
111+
throw new ConfigurationError("context must be an object");
112+
}
113+
114+
// Validate required keys exist for each line item
115+
const missingKeys = [];
116+
for (let i = 1; i <= numLineItems; i++) {
117+
if (!context.hasOwnProperty(`amount_${i}`)) {
118+
missingKeys.push(`amount_${i}`);
119+
}
120+
if (!context.hasOwnProperty(`item_${i}`)) {
121+
missingKeys.push(`item_${i}`);
122+
}
123+
}
124+
125+
if (missingKeys.length > 0) {
126+
throw new ConfigurationError(`Missing required keys in context: ${missingKeys.join(", ")}`);
127+
}
128+
129+
// Validate amount values are valid numbers
130+
const invalidAmounts = [];
131+
for (let i = 1; i <= numLineItems; i++) {
132+
const amount = context[`amount_${i}`];
133+
if (amount !== undefined && amount !== null && amount !== "" &&
134+
(typeof amount !== "number" && (typeof amount !== "string" || isNaN(parseFloat(amount))))) {
135+
invalidAmounts.push(`amount_${i}`);
136+
}
137+
}
138+
139+
if (invalidAmounts.length > 0) {
140+
throw new ConfigurationError(`Invalid amount values for: ${invalidAmounts.join(", ")}. Amounts must be valid numbers.`);
141+
}
142+
143+
// Validate detailType values if provided
144+
const validDetailTypes = ["ItemBasedExpenseLineDetail", "AccountBasedExpenseLineDetail"];
145+
const invalidDetailTypes = [];
146+
for (let i = 1; i <= numLineItems; i++) {
147+
const detailType = context[`detailType_${i}`];
148+
if (detailType && !validDetailTypes.includes(detailType)) {
149+
invalidDetailTypes.push(`detailType_${i}: ${detailType}`);
150+
}
151+
}
152+
153+
if (invalidDetailTypes.length > 0) {
154+
throw new ConfigurationError(`Invalid detailType values for: ${invalidDetailTypes.join(", ")}. Valid types are: ${validDetailTypes.join(", ")}`);
155+
}
156+
65157
const lineItems = [];
66158
for (let i = 1; i <= numLineItems; i++) {
67159
const detailType = context[`detailType_${i}`] || "ItemBasedExpenseLineDetail";
68-
lineItems.push({
160+
161+
// Extract conditional logic into clear variables
162+
const isItemBased = detailType === "ItemBasedExpenseLineDetail";
163+
const detailPropertyName = isItemBased ? "ItemBasedExpenseLineDetail" : "AccountBasedExpenseLineDetail";
164+
const refPropertyName = isItemBased ? "ItemRef" : "AccountRef";
165+
166+
// Build line item with clearer structure
167+
const lineItem = {
69168
DetailType: detailType,
70169
Amount: context[`amount_${i}`],
71-
[detailType === "ItemBasedExpenseLineDetail" ? "ItemBasedExpenseLineDetail" : "AccountBasedExpenseLineDetail"]: {
72-
[detailType === "ItemBasedExpenseLineDetail" ? "ItemRef" : "AccountRef"]: {
170+
[detailPropertyName]: {
171+
[refPropertyName]: {
73172
value: context[`item_${i}`],
74173
},
75174
},
76-
});
175+
};
176+
177+
lineItems.push(lineItem);
77178
}
78179
return lineItems;
79180
}

0 commit comments

Comments
 (0)