diff --git a/index.js b/index.js index 5dbc823..e8837d6 100644 --- a/index.js +++ b/index.js @@ -7,6 +7,8 @@ const { // Actions // Searches +const getCaseGoodsLotId = + require('./searches/get_casegoods_lotid'); // Fields const listWineriesDropdown = @@ -20,6 +22,9 @@ const listAnalysisTypesDropdown = const listDryGoodsDropdown = require('./fields/list_drygoodtypes_dropdown'); +// Creates +const createCaseGoodsAdjustment = + require('./creates/create_casegoods_adjustment'); // Triggers @@ -52,10 +57,14 @@ const App = { }, // If you want your searches to show up, you better include it here! - searches: {}, + searches: { + [getCaseGoodsLotId.key]: getCaseGoodsLotId, + }, // If you want your creates to show up, you better include it here! - creates: {}, + creates: { + [createCaseGoodsAdjustment.key]: createCaseGoodsAdjustment, + }, }; // Finally, export the app. diff --git a/searches/get_casegoods_lotid.js b/searches/get_casegoods_lotid.js new file mode 100644 index 0000000..620539f --- /dev/null +++ b/searches/get_casegoods_lotid.js @@ -0,0 +1,81 @@ +/** + * Retrieves the ID of a case goods lot from the API. + * + * @param {Object} z - The Zapier core object. + * @param {Object} bundle - The Zapier bundle object. + * @return {Promise} - A promise that resolves to an array with the lot ID object. + * @throws {Error} - Throws an error for invalid JSON response or unexpected status codes. + */ +const getCaseGoodsLotId = async (z, bundle) => { + const headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': `Access-Token ${bundle.authData.accessToken}`, + }; + + const url = `https://sutter.innovint.us/api/v1/wineries/${bundle.inputData.wineryId}/lots`; + const responseData = await z.request(url, 'GET', + {limit: 1, q: bundle.inputData.caseGoodsName}, headers); + + if (responseData) { + if (responseData.status === 200) { + // Check if the responseData.data is undefined (invalid JSON response) + if (!responseData.data) { + throw new Error('Invalid JSON response'); + } + + // Handle a successful response (HTTP 200) + const lotData = responseData.data.results[0]?.data; + if (lotData && lotData.id) { + return [{id: lotData.id}]; // Return an array with the lot ID object + } else { + throw new Error( + 'No lot found with the provided name, or the data structure is unexpected.'); + } + } else if (responseData.status === 400) { + // Handle a Bad Request (HTTP 400) response + if (responseData.data && responseData.data.errors && + responseData.data.errors.length > 0) { + const firstError = responseData.data.errors[0]; + const errorMessage = firstError.details || 'Bad Request'; + throw new Error(errorMessage); + } else { + throw new Error('Bad Request: The request parameters are invalid.'); + } + } else if (responseData.status === 500) { + // Handle a Server Error (HTTP 500) response + if (responseData.data && responseData.data.errors && + responseData.data.errors.length > 0) { + const firstError = responseData.data.errors[0]; + const errorMessage = firstError.details || 'Internal Server Error'; + throw new Error(errorMessage); + } else { + throw new Error( + 'Internal Server Error: An internal server error occurred.'); + } + } else { + // Handle other response status codes if needed + throw new Error(`Request failed with status ${responseData.status}`); + } + } else { + // Handle unexpected response data (null or undefined) + throw new Error('Unexpected response data from the API.'); + } +}; + +module.exports = { + key: 'getCaseGoodsLotId', + noun: 'Lot ID', + display: { + label: 'Get Lot By Case Goods Name', + description: 'Gets a lot ID based on the provided case goods name.', + }, + operation: { + perform: getCaseGoodsLotId, + inputFields: [ + {key: 'wineryId', required: true, type: 'string'}, + {key: 'caseGoodsName', required: true, type: 'string'}, + ], + sample: {id: 'lot_Z1LPW8OQMY23L6QM3KXJD45Y'}, + }, +}; diff --git a/searches/get_lotid.js b/searches/get_lotid.js deleted file mode 100644 index 90deb62..0000000 --- a/searches/get_lotid.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * The base URL for the wineries API. - * - * @type {string} - */ -const BASE_URL = 'https://sutter.innovint.us/api/v1/wineries'; - -/** - * Makes an asynchronous HTTP request using the provided parameters - * - * @param {object} z - The instance of the currently running Zapier app - * @param {string} url - The URL to which the request will be sent - * @param {string} method - The HTTP method to be used for the request, e.g. 'GET', 'POST', etc. - * @param {object} params - The parameters to be included with the request - * @param {object} headers - The headers to be included with the request - * - * @return {Promise} - A Promise that resolves to the JSON data returned by the API - */ -async function request(z, url, method, params, headers) { - const response = await z.request(url, {method, params, headers}); - response.throwForStatus(); - return response.json(); -} - -/** - * Retrieves lot data by case goods name. - * - * @async - * @param {object} z - The z object provided by Zapier. - * @param {object} bundle - The bundle object provided by Zapier. - * @return {Promise} - A promise that resolves to the lot data. - * @throws {Error} - If no lot is found with the provided name, or the data structure is unexpected. - * - * @requires request - */ -const getLotByCaseGoodsName = async (z, bundle) => { - const headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Authorization': `Access-Token ${bundle.authData.accessToken}`, - }; - const url = `${BASE_URL}/${bundle.inputData.wineryId}/lots`; - const response = await request(z, url, 'GET', - {limit: 1, q: bundle.inputData.caseGoodsName}, headers); - const lotData = response.results[0]?.data; - if (!lotData || !lotData.id) { - throw new Error( - 'No lot found with the provided name, ' + - 'or the data structure is unexpected.'); - } - return {id: lotData.id}; -}; - -module.exports = { - key: 'getLotByCaseGoodsName', - noun: 'Lot ID', - display: { - label: 'Get Lot By Case Goods Name', - description: 'Gets a lot ID based on the provided case goods name.', - }, - operation: { - perform: getLotByCaseGoodsName, - inputFields: [ - {key: 'wineryId', required: true, type: 'string'}, - {key: 'caseGoodsName', required: true, type: 'string'}, - ], - sample: {id: 'lot_Z1LPW8OQMY23L6QM3KXJD45Y'}, - }, -}; - - diff --git a/test/unit/searches/get_casegoods_lotid.test.js b/test/unit/searches/get_casegoods_lotid.test.js new file mode 100644 index 0000000..87a527f --- /dev/null +++ b/test/unit/searches/get_casegoods_lotid.test.js @@ -0,0 +1,147 @@ +const zapier = require('zapier-platform-core'); +const nock = require('nock'); +const should = require('should'); +const App = require('../../../index'); // Adjust this path as necessary +/** + * Creates a testing instance for the specified Zapier app. + * + * @param {Object} App - The Zapier app object. + * @returns {Object} - The testing instance for the app. + */ +const appTester = zapier.createAppTester(App); +const {bundle} = require('../../_bundle'); + +describe('getCaseGoodsLotId', function() { + afterEach(() => { + nock.cleanAll(); + }); + + it('should handle HTTP errors', async () => { + nock(bundle.baseUrl) + .get(`/api/v1/wineries/${bundle.inputData.wineryId}/lots`) + .query(true) + .reply(400, {}); + + try { + await appTester(App.searches.getCaseGoodsLotId.operation.perform, bundle); + should.fail('No error', 'Error', 'No error was thrown', 'should.fail'); + } catch (error) { + const errorObject = JSON.parse(error.message); + should(errorObject.status).be.equal(400); + } + }); + + it('should fetch the correct Lot ID', async () => { + const lotIdResponse = { + results: [{data: {id: 'lot_Z1LPW8OQMY23L6QM3KXJD45Y'}}], // Mock response structure + }; + + nock(bundle.baseUrl) + .get(`/api/v1/wineries/${bundle.inputData.wineryId}/lots`) + .query(true) + .reply(200, lotIdResponse); + + const result = await appTester( + App.searches.getCaseGoodsLotId.operation.perform, bundle); + should(result[0]).have.property('id', 'lot_Z1LPW8OQMY23L6QM3KXJD45Y'); // Check the first element of the array + }); + + it('should throw an error if no lot is found', async () => { + nock(bundle.baseUrl) + .get(`/api/v1/wineries/${bundle.inputData.wineryId}/lots`) + .query(true) + .reply(200, + {results: [], pagination: {count: 0, next: null, previous: null}}); + + try { + await appTester(App.searches.getCaseGoodsLotId.operation.perform, bundle); + should.fail('No error', 'Error', 'No error was thrown', 'should.fail'); + } catch (error) { + error.message.should.containEql( + 'No lot found with the provided name, or the data structure is unexpected.'); + } + }); + + it('should handle HTTP 400 Bad Request errors', async () => { + nock(bundle.baseUrl) + .get(`/api/v1/wineries/${bundle.inputData.wineryId}/lots`) + .query(true) + .reply(400, { + errors: [{details: 'Invalid input data'}], + }); + + try { + await appTester(App.searches.getCaseGoodsLotId.operation.perform, bundle); + should.fail('No error', 'Error', 'No error was thrown', 'should.fail'); + } catch (error) { + try { + const errorContent = JSON.parse(error.message).content; + const errorObject = JSON.parse(errorContent); + should.exist(errorObject); // Ensure errorObject is not null or undefined + should(errorObject.errors[0].details).be.equal('Invalid input data'); + } catch (parseError) { + should.fail('Error parsing JSON response', 'Error', parseError.message, + 'should.fail'); + } + } + }); + + it('should handle HTTP 500 Internal Server Error', async () => { + nock(bundle.baseUrl) + .get(`/api/v1/wineries/${bundle.inputData.wineryId}/lots`) + .query(true) + .reply(500, { + errors: [{details: 'Internal server error'}], + }); + + try { + await appTester(App.searches.getCaseGoodsLotId.operation.perform, bundle); + should.fail('No error', 'Error', 'No error was thrown', 'should.fail'); + } catch (error) { + try { + const errorContent = JSON.parse(error.message).content; + const errorObject = JSON.parse(errorContent); + should.exist(errorObject); // Ensure errorObject is not null or undefined + should(errorObject.errors[0].details).be.equal('Internal server error'); + } catch (parseError) { + should.fail('Error parsing JSON response', 'Error', parseError.message, + 'should.fail'); + } + } + }); + + it('should handle invalid JSON responses', async () => { + nock(bundle.baseUrl) + .get(`/api/v1/wineries/${bundle.inputData.wineryId}/lots`) + .query(true) + .reply(200, 'This is not JSON'); + + try { + await appTester(App.searches.getCaseGoodsLotId.operation.perform, bundle); + should.fail('No error', 'Error', 'Expected an invalid JSON error', + 'should.fail'); + } catch (error) { + should(error.message).containEql('Invalid JSON response'); + } + }); + + it('should handle invalid input data', async function() { + // Modify the bundle to simulate invalid input + const invalidBundle = { + ...bundle, + inputData: { + wineryId: 'invalid-id', + caseGoodsName: 'InvalidName', + }, + }; + + try { + await appTester(App.searches.getCaseGoodsLotId.operation.perform, + invalidBundle); + should.fail('No error', 'Error', 'Expected an invalid input error', + 'should.fail'); + } catch (error) { + should(error.message).containEql('Innovint server error!'); + } + }); +}); diff --git a/test/unit/searches/get_lotids.test.js b/test/unit/searches/get_lotids.test.js deleted file mode 100644 index b098720..0000000 --- a/test/unit/searches/get_lotids.test.js +++ /dev/null @@ -1,226 +0,0 @@ -const nock = require('nock'); -const should = require('should'); -const {operation} = require('../../../searches/get_lotid'); -const perform = operation.perform; -const {bundle} = require('../../_bundle'); - -describe('getLotId', function() { - afterEach(() => { - nock.cleanAll(); - }); - - it('should handle HTTP errors', async function() { - nock(bundle.baseUrl) - .get(`/api/v1/wineries/${bundle.inputData.wineryId}/lots`) - .query({ - limit: 1, - q: bundle.inputData.caseGoodsName, - }) - .reply(400); - - const z = { - request: () => Promise.resolve({ - status: 400, - throwForStatus: function() { - if (this.status < 200 || this.status >= 300) { - throw new Error(`Request failed with status ${this.status}`); - } - }, - }), - }; - - try { - await perform(z, bundle); - should.fail('No error', 'Error', 'No error was thrown', 'should.fail'); - } catch (error) { - should(error.message).be.equal('Request failed with status 400'); - } - }); - - it('should fetch the correct Lot ID', async function() { - nock(bundle.baseUrl) - .get(`/api/v1/wineries/${bundle.inputData.wineryId}/lots`) - .query({ - limit: 1, - q: bundle.inputData.caseGoodsName, - }) - .reply(200, { - 'results': [ - { - 'data': { - 'id': 'lot_Z1LPW8OQMY23L6QM3KXJD45Y', - 'internalId': 13562193, - 'access': { - 'globalAccess': false, - 'ownerTags': [], - }, - 'archived': false, - 'bondId': 'bond_OKQZLPJD5MZRVP789VYEW204', - 'bottlesOnHand': { - 'cases': 14.0, - 'bottles': 8.0, - }, - 'code': 'CG-2200RCVTOUROSE', - 'color': 'rose', - 'expectedYield': 0.0, - 'fruitWeight': { - 'value': 0.0, - 'unit': 'tons', - }, - 'lotStyle': 'STILL', - 'lotType': 'CASE_GOODS', - 'name': '2022 Robert Clay Vineyards Touriga Nacional Rose', - 'stage': 'TAXPAID', - 'tags': [ - 'ROBERT CLAY VINEYARDS', - 'ROSE', - ], - 'taxClass': 'TC_LESS_THAN_16', - 'volume': { - 'value': 34.87044000000001, - 'unit': 'gal', - }, - 'weight': { - 'value': 0.0, - 'unit': 'tons', - }, - }, - 'relationships': { - /* eslint-disable max-len */ - 'bond': 'https://sutter.innovint.us/api/v1/wineries/wnry_JEW56RPYO7JWR59GVXK8QDZ3/bonds/bond_OKQZLPJD5MZRVP789VYEW204', - 'blockComponents': 'https://sutter.innovint.us/api/v1/wineries/wnry_JEW56RPYO7JWR59GVXK8QDZ3/lots/lot_Z1LPW8OQMY23L6QM3KXJD45Y/blockComponents', - 'juiceMakeup': 'https://sutter.innovint.us/api/v1/wineries/wnry_JEW56RPYO7JWR59GVXK8QDZ3/lots/lot_Z1LPW8OQMY23L6QM3KXJD45Y/componentsSummary', - 'vessels': 'https://sutter.innovint.us/api/v1/wineries/wnry_JEW56RPYO7JWR59GVXK8QDZ3/vessels?lot=lot_Z1LPW8OQMY23L6QM3KXJD45Y', - /* eslint-enable max-len */ - }, - }, - ], - 'pagination': { - 'count': 2, - /* eslint-disable max-len */ - 'next': 'https://sutter.innovint.us/api/v1/wineries/wnry_JEW56RPYO7JWR59GVXK8QDZ3/lots?limit=1&offset=1&q=CG-2200RCVTOUROSE', - /* eslint-enable max-len */ - 'previous': null, - }, - }); - - const z = { - request: () => Promise.resolve({ - status: 200, - json: () => Promise.resolve({ - 'results': [ - { - 'data': { - 'id': 'lot_Z1LPW8OQMY23L6QM3KXJD45Y', - 'internalId': 13562193, - 'access': { - 'globalAccess': false, - 'ownerTags': [], - }, - 'archived': false, - 'bondId': 'bond_OKQZLPJD5MZRVP789VYEW204', - 'bottlesOnHand': { - 'cases': 14.0, - 'bottles': 8.0, - }, - 'code': 'CG-2200RCVTOUROSE', - 'color': 'rose', - 'expectedYield': 0.0, - 'fruitWeight': { - 'value': 0.0, - 'unit': 'tons', - }, - 'lotStyle': 'STILL', - 'lotType': 'CASE_GOODS', - 'name': '2022 Robert Clay Vineyards Touriga Nacional Rose', - 'stage': 'TAXPAID', - 'tags': [ - 'ROBERT CLAY VINEYARDS', - 'ROSE', - ], - 'taxClass': 'TC_LESS_THAN_16', - 'volume': { - 'value': 34.87044000000001, - 'unit': 'gal', - }, - 'weight': { - 'value': 0.0, - 'unit': 'tons', - }, - }, - 'relationships': { - /* eslint-disable max-len */ - 'bond': 'https://sutter.innovint.us/api/v1/wineries/wnry_JEW56RPYO7JWR59GVXK8QDZ3/bonds/bond_OKQZLPJD5MZRVP789VYEW204', - 'blockComponents': 'https://sutter.innovint.us/api/v1/wineries/wnry_JEW56RPYO7JWR59GVXK8QDZ3/lots/lot_Z1LPW8OQMY23L6QM3KXJD45Y/blockComponents', - 'juiceMakeup': 'https://sutter.innovint.us/api/v1/wineries/wnry_JEW56RPYO7JWR59GVXK8QDZ3/lots/lot_Z1LPW8OQMY23L6QM3KXJD45Y/componentsSummary', - 'vessels': 'https://sutter.innovint.us/api/v1/wineries/wnry_JEW56RPYO7JWR59GVXK8QDZ3/vessels?lot=lot_Z1LPW8OQMY23L6QM3KXJD45Y', - /* eslint-enable max-len */ - }, - }, - ], - 'pagination': { - 'count': 2, - /* eslint-disable max-len */ - 'next': 'https://sutter.innovint.us/api/v1/wineries/wnry_JEW56RPYO7JWR59GVXK8QDZ3/lots?limit=1&offset=1&q=CG-2200RCVTOUROSE', - /* eslint-enable max-len */ - 'previous': null, - }, - }), - throwForStatus: function() { - if (this.status < 200 || this.status >= 300) { - throw new Error(`Request failed with status ${this.status}`); - } - }, - }), - }; - - try { - const result = await perform(z, bundle); - should(result).have.property('id', 'lot_Z1LPW8OQMY23L6QM3KXJD45Y'); - } catch (error) { - should.fail('Unexpected error', 'No error', error.message, 'should.fail'); - } - }); - - it('should throw an error if no lot is found', async function() { - nock(bundle.baseUrl) - .get(`/api/v1/wineries/${bundle.inputData.wineryId}/lots`) - .query({ - limit: 1, - q: 'NonExistentCaseGood', - }) - .reply(200, { - results: [], - pagination: { - count: 0, - next: null, - previous: null, - }, - }); - - bundle.inputData.caseGoodsName = 'NonExistentCaseGood'; - - const z = { - request: () => Promise.resolve({ - status: 200, - json: () => Promise.resolve( - {results: [], pagination: {count: 0, next: null, previous: null}}), - throwForStatus: function() { - if (this.status < 200 || this.status >= 300) { - throw new Error(`Request failed with status ${this.status}`); - } - }, - }), - }; - - try { - await perform(z, bundle); - should.fail('No error', 'Error', 'No error was thrown', - 'should.fail'); - } catch (error) { - should(error.message).be.equal( - 'No lot found with the provided name, ' + - 'or the data structure is unexpected.'); - } - }); -});