From dc3b23840c5978c4cf8df4e9103552ffb9af1f80 Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Sun, 24 Oct 2021 11:33:02 +0200 Subject: [PATCH 01/28] fix(package.json): change project entrypoint to reflect the reality --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2a9432e..57c5fa5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "bling-erp-api", "version": "1.0.2", "description": "Pacote de interação com a REST API do serviço Bling ERP", - "main": "src/bling.ts", + "main": "lib/bling.js", "directories": { "test": "test" }, From 2922f3727b643a1278697826f0e58c5fefb8d585 Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Sun, 24 Oct 2021 11:33:27 +0200 Subject: [PATCH 02/28] fix(export): change export method to work correctly --- src/bling.ts | 2 +- test/main-module.spec.js | 2 +- test/main-module.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bling.ts b/src/bling.ts index e1a1cf4..c845183 100644 --- a/src/bling.ts +++ b/src/bling.ts @@ -6,7 +6,7 @@ import axios, { AxiosInstance } from 'axios' import createError from './core/createError' -export default class Bling { +export class Bling { #api: AxiosInstance #products: Products | undefined #orders: Orders | undefined diff --git a/test/main-module.spec.js b/test/main-module.spec.js index 616c7f3..a4b681a 100644 --- a/test/main-module.spec.js +++ b/test/main-module.spec.js @@ -1,5 +1,5 @@ /* eslint-disable no-undef */ -const Bling = require('../lib/bling').default +const { Bling } = require('../lib/bling') jest.setTimeout(10000) diff --git a/test/main-module.test.ts b/test/main-module.test.ts index b8ae961..b197af2 100644 --- a/test/main-module.test.ts +++ b/test/main-module.test.ts @@ -1,5 +1,5 @@ /* eslint-disable no-undef */ -import Bling from '../lib/bling' +import { Bling } from '../lib/bling' test('should fail when an ordinary request is made with a bad API key', async () => { const bling = new Bling('1234') From 74b73b112160f7b9f31c083c776f4911352be604 Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Sun, 24 Oct 2021 11:37:26 +0200 Subject: [PATCH 03/28] feat(eslint): add global eslint rule "no-undef" to testing files --- .eslintrc.json | 10 +++++++++- test/main-module.spec.js | 1 - test/main-module.test.ts | 1 - 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 2b69357..434d299 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -15,5 +15,13 @@ "@typescript-eslint" ], "rules": { - } + }, + "overrides": [ + { + "files": ["*.spec.js", "*.test.ts", "*.test.js"], + "rules": { + "no-undef": "off" + } + } + ] } diff --git a/test/main-module.spec.js b/test/main-module.spec.js index a4b681a..e4e749f 100644 --- a/test/main-module.spec.js +++ b/test/main-module.spec.js @@ -1,4 +1,3 @@ -/* eslint-disable no-undef */ const { Bling } = require('../lib/bling') jest.setTimeout(10000) diff --git a/test/main-module.test.ts b/test/main-module.test.ts index b197af2..ddca605 100644 --- a/test/main-module.test.ts +++ b/test/main-module.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-undef */ import { Bling } from '../lib/bling' test('should fail when an ordinary request is made with a bad API key', async () => { From 84da293a4c669ea31272c9c86c2ad998a8852c07 Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Mon, 25 Oct 2021 01:37:37 +0200 Subject: [PATCH 04/28] test: reorganize folders and write new tests for main module --- test/bling/main-module.spec.js | 55 ++++++++++++++++++++++++++++ test/{ => bling}/main-module.test.ts | 2 +- test/main-module.spec.js | 31 ---------------- 3 files changed, 56 insertions(+), 32 deletions(-) create mode 100644 test/bling/main-module.spec.js rename test/{ => bling}/main-module.test.ts (85%) delete mode 100644 test/main-module.spec.js diff --git a/test/bling/main-module.spec.js b/test/bling/main-module.spec.js new file mode 100644 index 0000000..2302591 --- /dev/null +++ b/test/bling/main-module.spec.js @@ -0,0 +1,55 @@ +require('dotenv').config() + +const { Bling } = require('../../lib/bling') +const testApiKey = process.env.BLING_API_KEY + +jest.setTimeout(60000) + +test("it shouldn't create a Bling entity when missing constructor argument", async () => { + expect(() => { + const bling = new Bling() + return bling + }).toThrow("The API key wasn't correctly provided for Bling connection.") +}) + +test("it shouldn't create a Bling entity when passing API key as number", async () => { + expect(() => { + const bling = new Bling(1234) + return bling + }).toThrow("The API key wasn't correctly provided for Bling connection.") +}) + +test("it shouldn't create a Bling entity when passing API key as object", async () => { + expect(() => { + const bling = new Bling({}) + return bling + }).toThrow("The API key wasn't correctly provided for Bling connection.") +}) + +test("it shouldn't create a Bling entity when passing API key as array", async () => { + expect(() => { + const bling = new Bling([]) + return bling + }).toThrow("The API key wasn't correctly provided for Bling connection.") +}) + +test("it shouldn't create a Bling entity when passing API key as null", async () => { + expect(() => { + const bling = new Bling(null) + return bling + }).toThrow("The API key wasn't correctly provided for Bling connection.") +}) + +test("it shouldn't create a Bling entity when passing API key as undefined", async () => { + expect(() => { + const bling = new Bling(undefined) + return bling + }).toThrow("The API key wasn't correctly provided for Bling connection.") +}) + +test('it should create a Bling entity when passing a proper API key', async () => { + expect(() => { + const bling = new Bling(testApiKey) + return bling + }).not.toThrow("The API key wasn't correctly provided for Bling connection.") +}) diff --git a/test/main-module.test.ts b/test/bling/main-module.test.ts similarity index 85% rename from test/main-module.test.ts rename to test/bling/main-module.test.ts index ddca605..65fa013 100644 --- a/test/main-module.test.ts +++ b/test/bling/main-module.test.ts @@ -1,4 +1,4 @@ -import { Bling } from '../lib/bling' +import { Bling } from '../../lib/bling' test('should fail when an ordinary request is made with a bad API key', async () => { const bling = new Bling('1234') diff --git a/test/main-module.spec.js b/test/main-module.spec.js deleted file mode 100644 index e4e749f..0000000 --- a/test/main-module.spec.js +++ /dev/null @@ -1,31 +0,0 @@ -const { Bling } = require('../lib/bling') - -jest.setTimeout(10000) - -test("shouldn't create Bling entity when missing constructor argument", async () => { - expect(() => { - const bling = new Bling() - return bling - }).toThrow("The API key wasn't correctly provided for Bling connection.") -}) - -test("shouldn't create Bling entity when passing API key as number", async () => { - expect(() => { - const bling = new Bling(1234) - return bling - }).toThrow("The API key wasn't correctly provided for Bling connection.") -}) - -test("shouldn't create Bling entity when passing API key as object", async () => { - expect(() => { - const bling = new Bling({}) - return bling - }).toThrow("The API key wasn't correctly provided for Bling connection.") -}) - -test("shouldn't create Bling entity when passing API key as array", async () => { - expect(() => { - const bling = new Bling([]) - return bling - }).toThrow("The API key wasn't correctly provided for Bling connection.") -}) From 4647f08075ba1dd71846d29c3ed1123f0fa8075e Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Mon, 25 Oct 2021 01:37:59 +0200 Subject: [PATCH 05/28] test(products): create tests to test .all() function from products entity --- test/products/get.test.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 test/products/get.test.ts diff --git a/test/products/get.test.ts b/test/products/get.test.ts new file mode 100644 index 0000000..20d9b13 --- /dev/null +++ b/test/products/get.test.ts @@ -0,0 +1,25 @@ +import { Bling } from '../../lib/bling' +import { config } from 'dotenv' + +config() +jest.setTimeout(60000) + +const testApiKey = process.env.BLING_API_KEY as string + +test('should get all products when calling `.all()` method with raw option settled to false', async () => { + const bling = new Bling(testApiKey) + + expect(bling.products().all({ raw: false })).resolves.toBeDefined() +}) + +test('should get all products when calling `.all()` method with raw option settled to true', async () => { + const bling = new Bling(testApiKey) + + expect(bling.products().all({ raw: true })).resolves.toBeDefined() +}) + +test('should get all products when calling `.all()` method without raw option', async () => { + const bling = new Bling(testApiKey) + + expect(bling.products().all()).resolves.toBeDefined() +}) From 470fc532e83c866d598702941b3a5c82ac2c4f17 Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Mon, 25 Oct 2021 01:38:33 +0200 Subject: [PATCH 06/28] style(eslint): add new 'overrides' entry in eslint config file to ignore camelcase for a entity --- .eslintrc.json | 47 ++++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 434d299..7071fa6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,27 +1,28 @@ { - "env": { - "es2021": true, - "node": true - }, - "extends": [ - "standard" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 12, - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint" - ], - "rules": { + "env": { + "es2021": true, + "node": true + }, + "extends": ["standard"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 12, + "sourceType": "module" + }, + "plugins": ["@typescript-eslint"], + "rules": {}, + "overrides": [ + { + "files": ["*.spec.js", "*.test.ts", "*.test.js"], + "rules": { + "no-undef": "off" + } }, - "overrides": [ - { - "files": ["*.spec.js", "*.test.ts", "*.test.js"], - "rules": { - "no-undef": "off" - } + { + "files": ["src/entities/*"], + "rules": { + "camelcase": "off" } - ] + } + ] } From f5ec7a41efb2b2a2fea0db1dcd951abef157f8d7 Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Mon, 25 Oct 2021 01:39:26 +0200 Subject: [PATCH 07/28] feat(package.json): add dotenv package into the project (for tests running) --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 57c5fa5..316aa1a 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "commitizen": "^4.2.4", "coveralls": "^3.1.1", "cross-env": "^7.0.3", + "dotenv": "^10.0.0", "eslint": "^7.32.0", "eslint-config-standard": "^16.0.3", "eslint-plugin-import": "^2.25.2", From 2665350e3b22da45dc0cc815c19d7a2e72249128 Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Mon, 25 Oct 2021 01:49:25 +0200 Subject: [PATCH 08/28] feat(entity): refactor code and enhance interface treatment in .all(), .find() and .findBy() methods --- src/core/entity.ts | 162 ++++++++++++++++++++++++++------------------- 1 file changed, 95 insertions(+), 67 deletions(-) diff --git a/src/core/entity.ts b/src/core/entity.ts index 4328fe6..4b9f26a 100644 --- a/src/core/entity.ts +++ b/src/core/entity.ts @@ -3,18 +3,34 @@ import { AxiosInstance } from 'axios' import * as qs from 'querystring' import * as xml2js from 'xml2js' -export interface IBlingBaseResponse { - retorno: T +export interface ISingularEntity { + [singular: string]: T } -export default class BlingBaseEntity< - IEntity, - IFilters, - IInfos, - // eslint-disable-next-line no-unused-vars - IResponse, - IError -> { +export interface IPluralEntity { + [plural: string]: ISingularEntity[] +} + +export interface ISingularError { + erro: { + cod: string + msg: string + } +} + +export interface IPluralError { + erros: ISingularError[] +} + +export interface IPluralResponse { + retorno: IPluralEntity | IPluralError +} + +export interface ISingularResponse { + retorno: ISingularEntity | IPluralError +} + +export default class BaseEntity { api: AxiosInstance qs: typeof qs xml2js: typeof xml2js @@ -31,16 +47,32 @@ export default class BlingBaseEntity< this.pluralName = '' } - async all (params?: IFilters): Promise { - return await this._getAll(this.pluralName, params) + async all (options?: { + params?: IFilters + raw?: boolean + }): Promise> { + return await this._getAll(this.pluralName, options && options.params) } - async find (id: number | string, params?: IInfos): Promise { - return await this._getOne(this.singularName, String(id), params) + async find ( + id: number | string, + options?: { + params?: IInfos + raw?: boolean + } + ): Promise> { + return await this._getOne( + this.singularName, + String(id), + options && options.params, + options && options.raw + ) } - async findBy (params?: IFilters & IInfos): Promise { - return await this._getAll(this.pluralName, params) + async findBy (options?: { + params?: IFilters & IInfos + }): Promise | IError> { + return await this._getAll(this.pluralName, options && options.params) } async create (data: IEntity): Promise { @@ -60,16 +92,17 @@ export default class BlingBaseEntity< * @protected * @access protected * @async - * @param {string} endpoint The entity request endpoint. - * @param {Object} rawQueryParams The query params for the request sent by the user. - * @param {string[]} acceptedParams The actual useful query params for the request. - * @returns {Array} An array of entities. + * @param endpoint The entity request endpoint. + * @param params The query params for the request sent by the user. + * @param raw Boolean value to return either raw data from Bling or beautified processed data. + * @returns An array of entities. */ protected async _getAll ( endpoint: string, - params?: IFilters - ): Promise { - const entities = [] + params?: IFilters, + raw: boolean = true + ): Promise> { + const entities: IEntity[] = [] let hasMore = true let page = 1 @@ -79,20 +112,35 @@ export default class BlingBaseEntity< params }) - const rawData = response.data as any - const { retorno: data } = rawData + const rawData = response.data as IPluralResponse + const data = rawData.retorno if (data.erros) { hasMore = false } else { - // entities.push(...this._extractResponseInformation(data)) - entities.push(...data) + const rawNewEntities = data as IPluralEntity + const newEntities = rawNewEntities[this.pluralName].map( + (item) => item[this.singularName] + ) + for (const entity of newEntities) { + entities.push(entity) + } } page++ } - return entities + if (raw) { + return { + retorno: { + [this.pluralName]: entities.map((entity) => ({ + [this.singularName]: entity + })) + } + } + } else { + return entities + } } /** @@ -100,26 +148,30 @@ export default class BlingBaseEntity< * @protected * @access protected * @async - * @param {string} endpoint The entity request endpoint. - * @param {number} id The entity id. - * @param {Object} rawQueryParams The query params for the request sent by the user. - * @param {string[]} acceptedParams The actual useful query params for the request. - * @returns {Object} The found entity. + * @param endpoint The entity request endpoint. + * @param id The entity id. + * @param params The query params for the request sent by the user. + * @param raw Boolean value to return either raw data from Bling or beautified processed data. + * @returns The found entity. */ protected async _getOne ( endpoint: string, id: string, - params?: IFilters | IInfos | (IFilters & IInfos) - ): Promise { + params: IFilters | IInfos | (IFilters & IInfos) | undefined = undefined, + raw: boolean | undefined = true + ): Promise> { const response = await this.api.get(`/${endpoint}/${id}/json`, { params }) - const rawData = response.data as any - const { retorno: data } = rawData - - // return this._extractResponseInformation(data)[0] - return data + const data = response.data as IPluralResponse + if (raw) { + return data + } else { + const rawResponse = data.retorno as IPluralEntity + const rawEntity = rawResponse[this.pluralName][0] + return rawEntity[this.singularName] + } } /** @@ -129,6 +181,7 @@ export default class BlingBaseEntity< * @async * @param endpoint The entity request endpoint. * @param data The data for the entity to be created. + * @returns The created entity. */ protected async _create ( endpoint: string, @@ -168,7 +221,6 @@ export default class BlingBaseEntity< 'ERR_ENTITY_CREATION_FAILURE' ) } else { - // return this._extractResponseInformation(responseData)[0] return responseData } } @@ -181,6 +233,7 @@ export default class BlingBaseEntity< * @param endpoint The entity request endpoint. * @param id The entity code or id. * @param data The data for the entity to be updated. + * @return The updated entity. */ protected async _update ( endpoint: string, @@ -230,7 +283,6 @@ export default class BlingBaseEntity< 'ERR_ENTITY_UPDATING_FAILURE' ) } else { - // return this._extractResponseInformation(responseData)[0] return responseData } } @@ -242,6 +294,7 @@ export default class BlingBaseEntity< * @async * @param endpoint The entity request endpoint. * @param id The entity code or id. + * @returns The deleted entity. */ protected async _delete ( endpoint: string, @@ -260,32 +313,7 @@ export default class BlingBaseEntity< 'ERR_ENTITY_DELETION_FAILURE' ) } else { - // return this._extractResponseInformation(responseData)[0] return responseData } } - - /** - * Extract main information after GET request return data. - * @protected - * @access protected - * @async - * @param data The object returned from the request after "retorno". - * @returns An array of entities. - */ - // protected _extractResponseInformation (data: IResponse): Array { - // const entities = [] - - // const responseFirstLayer = Object.keys(data)[0] as string - // const arrEntities = data[responseFirstLayer] as any[] - - // entities.push( - // ...arrEntities.map((item: any) => { - // const responseSecondLayer = Object.keys(item)[0] - // return item[responseSecondLayer] - // }) - // ) - - // return entities - // } } From b3c66b1fbc6c5aee7fe7cfab6e4b33352cf3ba49 Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Mon, 25 Oct 2021 01:50:11 +0200 Subject: [PATCH 09/28] refactor: update products and orders entities to satisfy the new proposed pattern --- src/entities/orders.ts | 10 +--------- src/entities/products.ts | 10 +--------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/src/entities/orders.ts b/src/entities/orders.ts index 57ef3b5..b39f125 100644 --- a/src/entities/orders.ts +++ b/src/entities/orders.ts @@ -1,5 +1,4 @@ -/* eslint-disable camelcase */ -import BlingEntity, { IBlingBaseResponse } from '../core/entity' +import BlingEntity from '../core/entity' import { AxiosInstance as IAxiosInstance } from 'axios' export interface IOrder { @@ -100,19 +99,12 @@ export interface IOrderFilters { idContato: string } -export interface IOrderResponse { - pedidos: { - pedido: IOrder - }[] -} - export interface IOrderError {} export default class Orders extends BlingEntity< IOrder, IOrderFilters, IOrderInfos, - IBlingBaseResponse, IOrderError > { constructor (api: IAxiosInstance) { diff --git a/src/entities/products.ts b/src/entities/products.ts index 07ffb29..43e98ef 100644 --- a/src/entities/products.ts +++ b/src/entities/products.ts @@ -1,5 +1,4 @@ -/* eslint-disable camelcase */ -import BlingEntity, { IBlingBaseResponse } from '../core/entity' +import BlingEntity from '../core/entity' import { AxiosInstance as IAxiosInstance } from 'axios' export interface IProduct { @@ -104,12 +103,6 @@ export interface IProductFilters { situacao?: 'A' | 'I' } -export interface IProductResponse { - produtos: { - produto: IProduct - }[] -} - export interface IProductError { code: | '40' @@ -131,7 +124,6 @@ export default class Products extends BlingEntity< IProduct, IProductFilters, IProductInfos, - IBlingBaseResponse, IProductError > { constructor (api: IAxiosInstance) { From 2eab4fae9e2c7cc096325b82b65f9e9219cd7907 Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Mon, 25 Oct 2021 01:50:36 +0200 Subject: [PATCH 10/28] feat(entities): create commercialProposals and purchaseOrders entities --- src/entities/commercialProposals.ts | 94 +++++++++++++++++++++++++++++ src/entities/purchaseOrders.ts | 78 ++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 src/entities/commercialProposals.ts create mode 100644 src/entities/purchaseOrders.ts diff --git a/src/entities/commercialProposals.ts b/src/entities/commercialProposals.ts new file mode 100644 index 0000000..80f2bc7 --- /dev/null +++ b/src/entities/commercialProposals.ts @@ -0,0 +1,94 @@ +import BlingEntity from '../core/entity' +import { AxiosInstance as IAxiosInstance } from 'axios' + +export interface ICommercialProposal { + data?: string + dataProximoContato?: string + contatoAc?: string + loja?: number + numero?: number + vendedor?: string + desconto?: string + outrasDespesas?: number + validade?: number + prazoEntrega?: string + garantia?: number + observacao?: string + obsInterna?: string + assinaturaSaudacao?: string + assinaturaResponsavel?: string + cliente: { + id?: string + nome: string + tipoPessoa?: 'F' | 'J' | 'E' + cpfCnpj?: string + ie?: string + rg?: string + contribuinte?: '1' | '2' | '3' + endereco?: string + numero?: string + complemento?: string + bairro?: string + cep?: string + cidade?: string + uf?: string + fone?: string + celular?: string + email?: string + itens: { + item: { + codigo?: string + descricao: string + un?: string + qtde: number + valorUnidade: number + } + }[] + transporte?: { + transportadora?: string + tipoFrete?: 'R' | 'D' | 'T' | '3' | '4' | '5' + frete?: number + } + parcelas?: { + parcela: { + nrDias: number + valor: number + obs?: string + formaPagamento: { + id: number + } + } + }[] + } +} + +export interface ICommercialProposalFilters { + data?: string + situacao?: + | 'Descrição' + | 'Pendente' + | 'Aguardando' + | 'Não aprovado' + | 'Aprovado' + | 'Concluído' + | 'Rascunho' + idContato?: number +} + +export interface ICommercialProposalInfos {} + +export interface ICommercialProposalError {} + +export default class CommercialProposals extends BlingEntity< + ICommercialProposal, + ICommercialProposalFilters, + ICommercialProposalInfos, + ICommercialProposalError +> { + constructor (api: IAxiosInstance) { + super(api) + + this.singularName = 'propostacomercial' + this.pluralName = 'propostascomerciais' + } +} diff --git a/src/entities/purchaseOrders.ts b/src/entities/purchaseOrders.ts new file mode 100644 index 0000000..78bd7ea --- /dev/null +++ b/src/entities/purchaseOrders.ts @@ -0,0 +1,78 @@ +import BlingEntity from '../core/entity' +import { AxiosInstance as IAxiosInstance } from 'axios' + +export interface IPurchaseOrder { + numeropedido?: string + datacompra?: string + dataprevista?: string + ordemcompra?: string + desconto?: string + observacoes?: string + observacaointerna?: string + idcategoria?: number + fornecedor: { + id?: number + nome: string + tipopessoa?: 'F' | 'J' | 'E' + cpfcnpj?: string + ie?: string + rg?: string + contribuinte?: '1' | '2' | '9' + endereco?: string + endereconro?: string + complemento?: string + bairro?: string + cep?: string + cidade?: string + uf?: string + fone?: string + celular?: string + email?: string + transporte?: { + transportador?: string + freteporconta?: string + qtdvolumes?: number + frete?: number + } + itens: { + item: { + codigo?: string + descricao: string + un?: 'pc' | 'un' | 'cx' + qtde: number + valor: number + } + }[] + parcelas?: { + parcela?: { + nrodias: number + valor: number + obs?: string + idformapagamento: number + } + }[] + } +} + +export interface IPurchaseOrderFilters { + dataEmissao?: string + situacao?: '0' | '1' | '2' | '3' +} + +export interface IPurchaseOrderInfos {} + +export interface IPurchaseOrderError {} + +export default class PurchaseOrders extends BlingEntity< + IPurchaseOrder, + IPurchaseOrderFilters, + IPurchaseOrderInfos, + IPurchaseOrderError +> { + constructor (api: IAxiosInstance) { + super(api) + + this.singularName = 'pedidocompra' + this.pluralName = 'pedidoscompra' + } +} From e817a11439a0b0c1830ab2193c00ff5728fe9de3 Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Mon, 25 Oct 2021 01:51:01 +0200 Subject: [PATCH 11/28] feat(package.json): add .ts extension to run prettier and eslint when committing --- package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/package.json b/package.json index 316aa1a..26b891e 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,11 @@ "prettier --write", "eslint --fix", "git add" + ], + "*.ts": [ + "prettier --write", + "eslint --fix", + "git add" ] }, "dependencies": { From 3c3f3b155797151dd69dbea3e36b6713c48d1f1c Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Mon, 1 Nov 2021 20:12:57 +0100 Subject: [PATCH 12/28] style(eslint): add typescript support in eslint config file --- .eslintrc.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.eslintrc.json b/.eslintrc.json index 7071fa6..5cdf905 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,7 +3,11 @@ "es2021": true, "node": true }, - "extends": ["standard"], + "extends": [ + "standard", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": 12, From 0f8f80bed4f03918c62d99cabdb480f5dacade69 Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Mon, 1 Nov 2021 20:13:23 +0100 Subject: [PATCH 13/28] feat(test): add ignored patterns in testing and max workers and concurrency in jest config file --- jest.config.json | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/jest.config.json b/jest.config.json index 4e4ccec..bcdaed1 100644 --- a/jest.config.json +++ b/jest.config.json @@ -3,6 +3,21 @@ "^.+\\.(t|j)sx?$": "ts-jest" }, "testRegex": "(/test/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", - "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"], - "transformIgnorePatterns": ["/node_modules/", "\\.pnp\\.[^\\\/]+$"] + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "jsx", + "json", + "node" + ], + "transformIgnorePatterns": [ + "/node_modules/", + "\\.pnp\\.[^\\\/]+$" + ], + "testPathIgnorePatterns": [ + "/test/config/" + ], + "maxWorkers": 1, + "maxConcurrency": 2 } From 0f029127711e379d79cb20b20729def2ccf9999f Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Mon, 1 Nov 2021 20:14:05 +0100 Subject: [PATCH 14/28] feat(package.json): add gerar-cpf package as dev dependency to auxiliate in testing --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 26b891e..4a45def 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "eslint-plugin-import": "^2.25.2", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^5.1.0", + "gerar-cpf": "^2.0.3", "husky": "^4.3.6", "jest": "^27.3.0", "lint-staged": "^10.5.3", From 4e9423864f357ebd4dd1ee0dd34434ee37eac372 Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Mon, 1 Nov 2021 20:15:05 +0100 Subject: [PATCH 15/28] test(folders): reorganize current test files to a better comprehensive structure --- test/bling/main-module.spec.js | 55 -------------------- test/bling/main-module.test.ts | 9 ---- test/entities/contacts/all.test.ts | 15 ++++++ test/entities/products/all.test.ts | 15 ++++++ test/package/main-module.spec.js | 80 ++++++++++++++++++++++++++++++ test/package/main-module.test.ts | 12 +++++ test/products/get.test.ts | 25 ---------- 7 files changed, 122 insertions(+), 89 deletions(-) delete mode 100644 test/bling/main-module.spec.js delete mode 100644 test/bling/main-module.test.ts create mode 100644 test/entities/contacts/all.test.ts create mode 100644 test/entities/products/all.test.ts create mode 100644 test/package/main-module.spec.js create mode 100644 test/package/main-module.test.ts delete mode 100644 test/products/get.test.ts diff --git a/test/bling/main-module.spec.js b/test/bling/main-module.spec.js deleted file mode 100644 index 2302591..0000000 --- a/test/bling/main-module.spec.js +++ /dev/null @@ -1,55 +0,0 @@ -require('dotenv').config() - -const { Bling } = require('../../lib/bling') -const testApiKey = process.env.BLING_API_KEY - -jest.setTimeout(60000) - -test("it shouldn't create a Bling entity when missing constructor argument", async () => { - expect(() => { - const bling = new Bling() - return bling - }).toThrow("The API key wasn't correctly provided for Bling connection.") -}) - -test("it shouldn't create a Bling entity when passing API key as number", async () => { - expect(() => { - const bling = new Bling(1234) - return bling - }).toThrow("The API key wasn't correctly provided for Bling connection.") -}) - -test("it shouldn't create a Bling entity when passing API key as object", async () => { - expect(() => { - const bling = new Bling({}) - return bling - }).toThrow("The API key wasn't correctly provided for Bling connection.") -}) - -test("it shouldn't create a Bling entity when passing API key as array", async () => { - expect(() => { - const bling = new Bling([]) - return bling - }).toThrow("The API key wasn't correctly provided for Bling connection.") -}) - -test("it shouldn't create a Bling entity when passing API key as null", async () => { - expect(() => { - const bling = new Bling(null) - return bling - }).toThrow("The API key wasn't correctly provided for Bling connection.") -}) - -test("it shouldn't create a Bling entity when passing API key as undefined", async () => { - expect(() => { - const bling = new Bling(undefined) - return bling - }).toThrow("The API key wasn't correctly provided for Bling connection.") -}) - -test('it should create a Bling entity when passing a proper API key', async () => { - expect(() => { - const bling = new Bling(testApiKey) - return bling - }).not.toThrow("The API key wasn't correctly provided for Bling connection.") -}) diff --git a/test/bling/main-module.test.ts b/test/bling/main-module.test.ts deleted file mode 100644 index 65fa013..0000000 --- a/test/bling/main-module.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Bling } from '../../lib/bling' - -test('should fail when an ordinary request is made with a bad API key', async () => { - const bling = new Bling('1234') - - await expect(bling.products().all()).rejects.toThrow( - 'Request failed with status code 401' - ) -}) diff --git a/test/entities/contacts/all.test.ts b/test/entities/contacts/all.test.ts new file mode 100644 index 0000000..792b7cc --- /dev/null +++ b/test/entities/contacts/all.test.ts @@ -0,0 +1,15 @@ +import { bling } from '../../config/bling' + +jest.setTimeout(60000) + +test('should get all contacts when calling `.all()` method with raw option settled to false', async () => { + await expect(bling.contacts().all({ raw: false })).resolves.toBeDefined() +}) + +test('should get all contacts when calling `.all()` method with raw option settled to true', async () => { + await expect(bling.contacts().all({ raw: true })).resolves.toBeDefined() +}) + +test('should get all contacts when calling `.all()` method without raw option', async () => { + await expect(bling.contacts().all()).resolves.toBeDefined() +}) diff --git a/test/entities/products/all.test.ts b/test/entities/products/all.test.ts new file mode 100644 index 0000000..96276a3 --- /dev/null +++ b/test/entities/products/all.test.ts @@ -0,0 +1,15 @@ +import { bling } from '../../config/bling' + +jest.setTimeout(100000) + +test('should get all products when calling `.all()` method with raw option settled to false', async () => { + await expect(bling.products().all({ raw: false })).resolves.toBeDefined() +}) + +test('should get all products when calling `.all()` method with raw option settled to true', async () => { + await expect(bling.products().all({ raw: true })).resolves.toBeDefined() +}) + +test('should get all products when calling `.all()` method without raw option', async () => { + await expect(bling.products().all()).resolves.toBeDefined() +}) diff --git a/test/package/main-module.spec.js b/test/package/main-module.spec.js new file mode 100644 index 0000000..5eff646 --- /dev/null +++ b/test/package/main-module.spec.js @@ -0,0 +1,80 @@ +import { Bling } from '../../lib/bling' + +import { config } from 'dotenv' +config() + +const testApiKey = process.env.BLING_API_KEY + +jest.setTimeout(60000) + +test.concurrent( + "it shouldn't create a Bling entity when missing constructor argument", + async () => { + expect(() => { + const bling = new Bling() + return bling + }).toThrow("The API key wasn't correctly provided for Bling connection.") + } +) + +test.concurrent( + "it shouldn't create a Bling entity when passing API key as number", + async () => { + expect(() => { + const bling = new Bling(1234) + return bling + }).toThrow("The API key wasn't correctly provided for Bling connection.") + } +) + +test.concurrent( + "it shouldn't create a Bling entity when passing API key as object", + async () => { + expect(() => { + const bling = new Bling({}) + return bling + }).toThrow("The API key wasn't correctly provided for Bling connection.") + } +) + +test.concurrent( + "it shouldn't create a Bling entity when passing API key as array", + async () => { + expect(() => { + const bling = new Bling([]) + return bling + }).toThrow("The API key wasn't correctly provided for Bling connection.") + } +) + +test.concurrent( + "it shouldn't create a Bling entity when passing API key as null", + async () => { + expect(() => { + const bling = new Bling(null) + return bling + }).toThrow("The API key wasn't correctly provided for Bling connection.") + } +) + +test.concurrent( + "it shouldn't create a Bling entity when passing API key as undefined", + async () => { + expect(() => { + const bling = new Bling(undefined) + return bling + }).toThrow("The API key wasn't correctly provided for Bling connection.") + } +) + +test.concurrent( + 'it should create a Bling entity when passing a proper API key', + async () => { + expect(() => { + const bling = new Bling(testApiKey) + return bling + }).not.toThrow( + "The API key wasn't correctly provided for Bling connection." + ) + } +) diff --git a/test/package/main-module.test.ts b/test/package/main-module.test.ts new file mode 100644 index 0000000..adca55f --- /dev/null +++ b/test/package/main-module.test.ts @@ -0,0 +1,12 @@ +import { Bling } from '../../lib/bling' + +test.concurrent( + 'should fail when an ordinary request is made with a bad API key', + async () => { + const bling = new Bling('1234') + + await expect(bling.products().all()).rejects.toThrow( + 'Request failed with status code 401' + ) + } +) diff --git a/test/products/get.test.ts b/test/products/get.test.ts deleted file mode 100644 index 20d9b13..0000000 --- a/test/products/get.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Bling } from '../../lib/bling' -import { config } from 'dotenv' - -config() -jest.setTimeout(60000) - -const testApiKey = process.env.BLING_API_KEY as string - -test('should get all products when calling `.all()` method with raw option settled to false', async () => { - const bling = new Bling(testApiKey) - - expect(bling.products().all({ raw: false })).resolves.toBeDefined() -}) - -test('should get all products when calling `.all()` method with raw option settled to true', async () => { - const bling = new Bling(testApiKey) - - expect(bling.products().all({ raw: true })).resolves.toBeDefined() -}) - -test('should get all products when calling `.all()` method without raw option', async () => { - const bling = new Bling(testApiKey) - - expect(bling.products().all()).resolves.toBeDefined() -}) From 24ffe51975c3264d5b772394c8aad074efe4a807 Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Mon, 1 Nov 2021 20:16:12 +0100 Subject: [PATCH 16/28] feat(createerror): add 'data' as a parameter in createError function, to store the error data --- src/core/createError.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/core/createError.ts b/src/core/createError.ts index 956144a..b2195e6 100644 --- a/src/core/createError.ts +++ b/src/core/createError.ts @@ -1,12 +1,14 @@ export interface IBlingError extends Error { status: number config: any + data: any code: string toJSON: () => { message: string name: string stack?: string config: any + data: any code: string } } @@ -14,16 +16,18 @@ export interface IBlingError extends Error { /** * Create an Error with the specified message, config, error code and status. * - * @param {string} message The error message. - * @param {number} status The error status. - * @param {Bling} config The config. - * @param {string} [code] The error code (for example, 'ECONNABORTED'). - * @returns {IBlingError} The created error. + * @param message The error message. + * @param status The error status. + * @param config The config. + * @param data The error data. + * @param [code] The error code (for example, 'E_CONN_ABORTED'). + * @returns The created error. */ export default function createError ( message: string, status: number, config: any, + data: any, code: string ) { const rawError = new Error(message) @@ -33,12 +37,14 @@ export default function createError ( message, status, config, + data, code, toJSON: () => { return { message, name: rawError.name, stack: rawError.stack, + data, config, code } From 5a2830ab170718795a27df6b254a265966fa7083 Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Mon, 1 Nov 2021 20:16:48 +0100 Subject: [PATCH 17/28] test(config): centralize the global testing information in a unique file --- test/config/bling.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 test/config/bling.ts diff --git a/test/config/bling.ts b/test/config/bling.ts new file mode 100644 index 0000000..49bd5a3 --- /dev/null +++ b/test/config/bling.ts @@ -0,0 +1,10 @@ +import { Bling } from '../../lib/bling' +import { IBlingError } from '../../src/bling' +import { config } from 'dotenv' + +config() + +const testApiKey = process.env.BLING_API_KEY as string +const bling = new Bling(testApiKey) + +export { bling, IBlingError } From d9708f8849234347a2e9eb80a6f0e725370bd5f2 Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Mon, 1 Nov 2021 22:57:25 +0100 Subject: [PATCH 18/28] feat(create error): remove config attribute from create error function --- src/core/createError.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/core/createError.ts b/src/core/createError.ts index b2195e6..c86ec0d 100644 --- a/src/core/createError.ts +++ b/src/core/createError.ts @@ -1,13 +1,11 @@ export interface IBlingError extends Error { status: number - config: any data: any code: string toJSON: () => { message: string name: string stack?: string - config: any data: any code: string } @@ -18,7 +16,6 @@ export interface IBlingError extends Error { * * @param message The error message. * @param status The error status. - * @param config The config. * @param data The error data. * @param [code] The error code (for example, 'E_CONN_ABORTED'). * @returns The created error. @@ -26,7 +23,6 @@ export interface IBlingError extends Error { export default function createError ( message: string, status: number, - config: any, data: any, code: string ) { @@ -36,7 +32,6 @@ export default function createError ( ...rawError, message, status, - config, data, code, toJSON: () => { @@ -45,7 +40,6 @@ export default function createError ( name: rawError.name, stack: rawError.stack, data, - config, code } } From d89c52e7db95f8779ebf7d72d4ffc2b7d740f843 Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Mon, 1 Nov 2021 23:09:11 +0100 Subject: [PATCH 19/28] feat(entity): enhance entity structure, methods and interfaces to work properly and be scalable --- src/core/entity.ts | 401 ++++++++++++++++++++++------ src/entities/commercialProposals.ts | 12 +- src/entities/orders.ts | 20 +- src/entities/products.ts | 27 +- 4 files changed, 337 insertions(+), 123 deletions(-) diff --git a/src/core/entity.ts b/src/core/entity.ts index 4b9f26a..e7b378d 100644 --- a/src/core/entity.ts +++ b/src/core/entity.ts @@ -1,5 +1,5 @@ import createError from './createError' -import { AxiosInstance } from 'axios' +import { AxiosInstance, AxiosError } from 'axios' import * as qs from 'querystring' import * as xml2js from 'xml2js' @@ -8,7 +8,7 @@ export interface ISingularEntity { } export interface IPluralEntity { - [plural: string]: ISingularEntity[] + [plural: string]: ISingularEntity[] | ISingularEntity } export interface ISingularError { @@ -18,8 +18,12 @@ export interface ISingularError { } } +export interface IDeleteError { + [code: string]: string +} + export interface IPluralError { - erros: ISingularError[] + erros: ISingularError[] | IDeleteError } export interface IPluralResponse { @@ -30,15 +34,17 @@ export interface ISingularResponse { retorno: ISingularEntity | IPluralError } -export default class BaseEntity { +export default class BaseEntity { api: AxiosInstance + #apiKey: string qs: typeof qs xml2js: typeof xml2js singularName: string pluralName: string - constructor (api: AxiosInstance) { + constructor (api: AxiosInstance, apiKey: string) { this.api = api + this.#apiKey = apiKey this.qs = qs this.xml2js = xml2js @@ -47,44 +53,115 @@ export default class BaseEntity { this.pluralName = '' } - async all (options?: { - params?: IFilters - raw?: boolean - }): Promise> { + public async all ( + options: { + params?: IFilters + raw?: boolean + } = {} + ): Promise> { return await this._getAll(this.pluralName, options && options.params) } - async find ( + public async find( + id: number | string, + options?: { params?: IInfos; raw?: false } + ): Promise + + public async find( + id: number | string, + options?: { params?: IInfos; raw: true } + ): Promise> + + public async find ( id: number | string, options?: { params?: IInfos raw?: boolean } - ): Promise> { - return await this._getOne( + ): Promise> { + return await this._find( this.singularName, - String(id), + id, options && options.params, options && options.raw ) } - async findBy (options?: { - params?: IFilters & IInfos - }): Promise | IError> { - return await this._getAll(this.pluralName, options && options.params) + public async findBy( + options: IFilters & IInfos, + raw?: false + ): Promise + + public async findBy( + options: IFilters & IInfos, + raw: true + ): Promise> + + public async findBy ( + options: IFilters & IInfos, + raw?: boolean + ): Promise> { + if (!options) { + throw createError( + 'No options passed to `.findBy()` method', + 500, + options, + 'ERR_INCORRECT_OPTIONS_ARG' + ) + } + + return await this._getAll(this.pluralName, options, raw) } - async create (data: IEntity): Promise { - return await this._create(this.singularName, data) + public async create(data: IEntity, raw?: false): Promise + + public async create( + data: IEntity, + raw: true + ): Promise> + + public async create ( + data: IEntity, + raw?: boolean + ): Promise> { + return await this._create(this.singularName, data, raw) } - async update (id: number, data: IEntity): Promise { - return await this._update(this.singularName, id, data) + public async update( + id: number | string, + data: IEntity, + raw?: false + ): Promise + + public async update( + id: number | string, + data: IEntity, + raw: true + ): Promise> + + public async update ( + id: number | string, + data: IEntity, + raw?: boolean + ): Promise> { + return await this._update(this.singularName, id, data, raw) } - async delete (id: number): Promise { - return await this._delete(this.singularName, id) + public async delete( + id: number | string, + raw?: false + ): Promise + + public async delete( + id: number | string, + raw: true + ): Promise> + + public async delete ( + id: number | string, + raw?: boolean + ): Promise> { + return await this._delete(this.singularName, id, raw) } /** @@ -100,11 +177,13 @@ export default class BaseEntity { protected async _getAll ( endpoint: string, params?: IFilters, - raw: boolean = true - ): Promise> { - const entities: IEntity[] = [] + raw = false + ): Promise> { + // @TODO: refactor filter logic to actually work + const entities: IEntityResponse[] = [] let hasMore = true + let reqCount = 0 let page = 1 while (hasMore) { @@ -112,22 +191,38 @@ export default class BaseEntity { params }) - const rawData = response.data as IPluralResponse + const rawData = response.data as IPluralResponse const data = rawData.retorno if (data.erros) { hasMore = false } else { - const rawNewEntities = data as IPluralEntity - const newEntities = rawNewEntities[this.pluralName].map( + const rawNewEntities = data as IPluralEntity + + const newEntities = rawNewEntities[ + this.pluralName + ] as ISingularEntity[] + + const singularEntities = newEntities.map( (item) => item[this.singularName] ) - for (const entity of newEntities) { + for (const entity of singularEntities) { entities.push(entity) } } page++ + + reqCount++ + if (reqCount === 3) { + const sleep = new Promise((resolve) => { + setTimeout(resolve, 1000) + }) + + await sleep + + reqCount = 0 + } } if (raw) { @@ -154,23 +249,53 @@ export default class BaseEntity { * @param raw Boolean value to return either raw data from Bling or beautified processed data. * @returns The found entity. */ - protected async _getOne ( + protected async _find ( endpoint: string, - id: string, + id: number | string, params: IFilters | IInfos | (IFilters & IInfos) | undefined = undefined, - raw: boolean | undefined = true - ): Promise> { + raw = false + ): Promise> { + if (!id) { + throw createError( + 'The "id" argument must be a number or string.', + 500, + id, + 'ERR_INCORRECT_ID_ARG' + ) + } + const response = await this.api.get(`/${endpoint}/${id}/json`, { params }) - const data = response.data as IPluralResponse - if (raw) { - return data + const data = response.data as IPluralResponse + if (data.retorno.erros) { + const errReturn = data.retorno as IPluralError + let errData + if (raw) { + errData = { retorno: errReturn } + } else { + // maybe enhance it to include JSON API standards? + const rawErrData = errReturn.erros as ISingularError[] + errData = rawErrData.map((err: ISingularError) => err.erro) + } + + throw createError( + 'Error on find method after request call', + response.status, + errData, + 'ERR_FIND_METHOD' + ) } else { - const rawResponse = data.retorno as IPluralEntity - const rawEntity = rawResponse[this.pluralName][0] - return rawEntity[this.singularName] + if (raw) { + return data + } else { + const rawResponse = data.retorno as IPluralEntity + const rawEntity = rawResponse[ + this.pluralName + ] as ISingularEntity[] + return rawEntity[0][this.singularName] + } } } @@ -181,22 +306,24 @@ export default class BaseEntity { * @async * @param endpoint The entity request endpoint. * @param data The data for the entity to be created. + * @param raw Boolean value to return either raw data from Bling or beautified processed data. * @returns The created entity. */ protected async _create ( endpoint: string, - data: IEntity - ): Promise { - if (typeof data !== 'object') { + data: IEntity, + raw = false + ): Promise> { + if (typeof data !== 'object' || Object.keys(data).length === 0) { throw createError( - 'The "data" argument must be an object.', + 'The "data" argument must be a not empty object', 500, - this, + data, 'ERR_INCORRECT_DATA_ARG' ) } - const xmlBuilder = new this.xml2js.Builder() + const xmlBuilder = new this.xml2js.Builder({ rootName: this.singularName }) const xml = xmlBuilder.buildObject({ ...data }) @@ -205,23 +332,56 @@ export default class BaseEntity { xml } - const response = await this.api.post( - `/${endpoint}/json`, - this.qs.stringify(params) - ) + const response = await this.api + .post(`/${endpoint}/json`, this.qs.stringify(params)) + .catch((err: AxiosError) => { + const errResponse = err.response + + throw createError( + `Error on create method during request call: ${err.message}`, + err.response?.status || 400, + errResponse, + err.code || 'ERR_POST_REQUEST_FAILURE' + ) + }) - const rawData = response.data as any - const { retorno: responseData } = rawData + const responseData = response.data as IPluralResponse + if (responseData.retorno.erros) { + const errReturn = responseData.retorno as IPluralError + let errData + if (raw) { + errData = { retorno: errReturn } + } else { + // maybe enhance it to include JSON API standards? + const rawErrData = errReturn.erros as ISingularError[] + errData = rawErrData.map((err: ISingularError) => err.erro) + } - if (responseData.erros) { throw createError( - responseData.erros[0].erro.msg, - 500, - this, + 'Error on create method after request call', + 400, + errData, 'ERR_ENTITY_CREATION_FAILURE' ) } else { - return responseData + if (raw) { + return responseData + } else { + const rawResponse = + responseData.retorno as IPluralEntity + + if (Array.isArray(rawResponse[this.pluralName])) { + const rawEntity = rawResponse[ + this.pluralName + ] as ISingularEntity[] + return rawEntity[0][this.singularName] + } else { + const rawEntity = rawResponse[ + this.pluralName + ] as ISingularEntity + return rawEntity[this.singularName] + } + } } } @@ -233,27 +393,29 @@ export default class BaseEntity { * @param endpoint The entity request endpoint. * @param id The entity code or id. * @param data The data for the entity to be updated. + * @param raw Boolean value to return either raw data from Bling or beautified processed data. * @return The updated entity. */ protected async _update ( endpoint: string, - id: number, - data: IEntity - ): Promise { - if (typeof data !== 'object') { + id: number | string, + data: IEntity, + raw = false + ): Promise> { + if (typeof data !== 'object' || Object.keys(data).length === 0) { throw createError( - 'The "data" argument must be an object.', + 'The "data" argument must be a not empty object', 500, - this, + data, 'ERR_INCORRECT_DATA_ARG' ) } if (!id || typeof id === 'object' || Array.isArray(id)) { throw createError( - 'The "id" argument must be a number or string.', + 'The "id" argument must be a number or string', 500, - this, + id, 'ERR_INCORRECT_DATA_ID' ) } @@ -264,26 +426,60 @@ export default class BaseEntity { }) const params = { + apikey: this.#apiKey, xml } - const response = await this.api.put( - `/${endpoint}/${id}/json`, - this.qs.stringify(params) - ) + const response = await this.api + .put(`/${endpoint}/${id}/json`, this.qs.stringify(params)) + .catch((err: AxiosError) => { + const errResponse = err.response - const rawData = response.data as any - const { retorno: responseData } = rawData + throw createError( + `Error on update method during request call: ${err.message}`, + err.response?.status || 400, + errResponse, + err.code || 'ERR_UPDATE_REQUEST_FAILURE' + ) + }) + + const responseData = response.data as IPluralResponse + if (responseData.retorno.erros) { + const errReturn = responseData.retorno as IPluralError + let errData + if (raw) { + errData = { retorno: errReturn } + } else { + // maybe enhance it to include JSON API standards? + const rawErrData = errReturn.erros as ISingularError[] + errData = rawErrData.map((err: ISingularError) => err.erro) + } - if (responseData.erros) { throw createError( - responseData.erros[0].erro.msg, - 500, - this, + 'Error on update method after request call', + 400, + errData, 'ERR_ENTITY_UPDATING_FAILURE' ) } else { - return responseData + if (raw) { + return responseData + } else { + const rawResponse = + responseData.retorno as IPluralEntity + + if (Array.isArray(rawResponse[this.pluralName])) { + const rawEntity = rawResponse[ + this.pluralName + ] as ISingularEntity[] + return rawEntity[0][this.singularName] + } else { + const rawEntity = rawResponse[ + this.pluralName + ] as ISingularEntity + return rawEntity[this.singularName] + } + } } } @@ -294,26 +490,59 @@ export default class BaseEntity { * @async * @param endpoint The entity request endpoint. * @param id The entity code or id. + * @param raw Boolean value to return either raw data from Bling or beautified processed data. * @returns The deleted entity. */ protected async _delete ( endpoint: string, - id: number - ): Promise { - const response = await this.api.delete(`/${endpoint}/${id}/json`) + id: number | string, + raw = false + ): Promise> { + const response = await this.api + .delete(`/${endpoint}/${id}/json`) + .catch((err: AxiosError) => { + const errResponse = err.response + + throw createError( + `Error on delete method during request call: ${err.message}`, + err.response?.status || 400, + errResponse, + err.code || 'ERR_DELETE_REQUEST_FAILURE' + ) + }) - const rawData = response.data as any - const { retorno: responseData } = rawData + const data = response.data as IPluralResponse + if (data.retorno.erros) { + const errReturn = data.retorno as IPluralError + let errData + if (raw) { + errData = { retorno: errReturn } + } else { + // maybe enhance it to include JSON API standards? + const rawErrData = errReturn.erros as IDeleteError + + errData = Object.keys(rawErrData).map((code) => ({ + cod: code, + msg: rawErrData[code] + })) + } - if (responseData.erros) { throw createError( - responseData.erros[0].erro.msg, - 500, - this, + 'Error on delete method after request call', + response.status, + errData, 'ERR_ENTITY_DELETION_FAILURE' ) } else { - return responseData + if (raw) { + return data + } else { + const rawResponse = data.retorno as IPluralEntity + const rawEntity = rawResponse[ + this.pluralName + ] as ISingularEntity[] + return rawEntity[0][this.singularName] + } } } } diff --git a/src/entities/commercialProposals.ts b/src/entities/commercialProposals.ts index 80f2bc7..13dfdba 100644 --- a/src/entities/commercialProposals.ts +++ b/src/entities/commercialProposals.ts @@ -75,18 +75,16 @@ export interface ICommercialProposalFilters { idContato?: number } -export interface ICommercialProposalInfos {} - -export interface ICommercialProposalError {} +export type ICommercialProposalResponse = ICommercialProposal export default class CommercialProposals extends BlingEntity< ICommercialProposal, ICommercialProposalFilters, - ICommercialProposalInfos, - ICommercialProposalError + Record, + ICommercialProposalResponse > { - constructor (api: IAxiosInstance) { - super(api) + constructor (api: IAxiosInstance, apiKey: string) { + super(api, apiKey) this.singularName = 'propostacomercial' this.pluralName = 'propostascomerciais' diff --git a/src/entities/orders.ts b/src/entities/orders.ts index b39f125..7d41b8a 100644 --- a/src/entities/orders.ts +++ b/src/entities/orders.ts @@ -88,27 +88,27 @@ export interface IOrder { } export interface IOrderInfos { - historico: 'true' | 'false' + historico?: 'true' | 'false' } export interface IOrderFilters { - dataEmissao: string - dataAlteracao: string - dataPrevista: string - idSituacao: string - idContato: string + dataEmissao?: string + dataAlteracao?: string + dataPrevista?: string + idSituacao?: string + idContato?: string } -export interface IOrderError {} +export type IOrderResponse = IOrder export default class Orders extends BlingEntity< IOrder, IOrderFilters, IOrderInfos, - IOrderError + IOrderResponse > { - constructor (api: IAxiosInstance) { - super(api) + constructor (api: IAxiosInstance, apiKey: string) { + super(api, apiKey) this.singularName = 'pedido' this.pluralName = 'pedidos' diff --git a/src/entities/products.ts b/src/entities/products.ts index 43e98ef..c57a373 100644 --- a/src/entities/products.ts +++ b/src/entities/products.ts @@ -29,7 +29,7 @@ export interface IProduct { descricaoFornecedor?: string idFabricante?: number codigoFabricante?: string - deposito: { + deposito?: { id?: number estoque?: number } @@ -103,31 +103,18 @@ export interface IProductFilters { situacao?: 'A' | 'I' } -export interface IProductError { - code: - | '40' - | '41' - | '42' - | '43' - | '44' - | '45' - | '46' - | '47' - | '48' - | '49' - | '50' - | '51' - | '54' +export interface IProductResponse extends IProduct { + id: number } export default class Products extends BlingEntity< IProduct, IProductFilters, IProductInfos, - IProductError + IProductResponse > { - constructor (api: IAxiosInstance) { - super(api) + constructor (api: IAxiosInstance, apiKey: string) { + super(api, apiKey) this.singularName = 'produto' this.pluralName = 'produtos' @@ -138,6 +125,6 @@ export default class Products extends BlingEntity< supplierId: number, params: IProductInfos ) { - return await this._getOne(String(supplierId), `produto/${code}`, params) + return await this._find(String(supplierId), `produto/${code}`, params) } } From 541382f9e7ce58b07c0d57355e4b4e960a593b5f Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Mon, 1 Nov 2021 23:14:29 +0100 Subject: [PATCH 20/28] feat(entities): create contacts, deposits and purchase orders entities --- src/entities/contacts.ts | 69 ++++++++++++++++++++++++++++++++++ src/entities/deposits.ts | 29 ++++++++++++++ src/entities/purchaseOrders.ts | 12 +++--- 3 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 src/entities/contacts.ts create mode 100644 src/entities/deposits.ts diff --git a/src/entities/contacts.ts b/src/entities/contacts.ts new file mode 100644 index 0000000..4621a96 --- /dev/null +++ b/src/entities/contacts.ts @@ -0,0 +1,69 @@ +import BlingEntity from '../core/entity' +import { AxiosInstance as IAxiosInstance } from 'axios' + +export interface IContact { + nome: string + fantasia?: string + tipoPessoa: 'F' | 'J' | 'E' + contribuinte: '1' | '2' | '9' + cpf_cnpj: string + ie_rg?: string + endereco?: string + numero?: string + complemento?: string + bairro?: string + cep?: string + cidade?: string + uf?: string + fone?: string + celular?: string + email?: string + emailNfe?: string + informacaoContato?: string + limiteCredito?: number + paisOrigem?: string + codigo?: string + site?: string + obs?: string + tipos_contatos?: { + tipo_contato: { + descricao?: string + } + }[] +} + +export interface IContactFilters { + dataInclusao?: string + dataAlteracao?: string + tipoPessoa?: 'F' | 'J' | 'E' +} + +export interface IContactInfos { + identificador?: '1' | '2' +} + +export interface IContactResponse extends IContact { + tipo: string + cpf: string + cnpj: string + situacao: string + dataAlteracao: string + dataInclusao: string + sexo: string + clienteDesde: string + dataNascimento: string +} + +export default class Contacts extends BlingEntity< + IContact, + IContactFilters, + IContactInfos, + IContactResponse +> { + constructor (api: IAxiosInstance, apiKey: string) { + super(api, apiKey) + + this.singularName = 'contato' + this.pluralName = 'contatos' + } +} diff --git a/src/entities/deposits.ts b/src/entities/deposits.ts new file mode 100644 index 0000000..3416f2d --- /dev/null +++ b/src/entities/deposits.ts @@ -0,0 +1,29 @@ +import BlingEntity from '../core/entity' +import { AxiosInstance as IAxiosInstance } from 'axios' + +export interface IDeposit { + descricao?: string + desconsiderarSaldo?: boolean + depositoPadrao?: boolean + situacao?: 'A' | 'I' +} + +export interface IDepositFilters { + situacao?: 'A' | 'I' +} + +export type IDepositResponse = IDeposit + +export default class Deposits extends BlingEntity< + IDeposit, + IDepositFilters, + Record, + IDepositResponse +> { + constructor (api: IAxiosInstance, apiKey: string) { + super(api, apiKey) + + this.singularName = 'deposito' + this.pluralName = 'depositos' + } +} diff --git a/src/entities/purchaseOrders.ts b/src/entities/purchaseOrders.ts index 78bd7ea..8652953 100644 --- a/src/entities/purchaseOrders.ts +++ b/src/entities/purchaseOrders.ts @@ -59,18 +59,16 @@ export interface IPurchaseOrderFilters { situacao?: '0' | '1' | '2' | '3' } -export interface IPurchaseOrderInfos {} - -export interface IPurchaseOrderError {} +export type IPurchaseOrderResponse = IPurchaseOrder export default class PurchaseOrders extends BlingEntity< IPurchaseOrder, IPurchaseOrderFilters, - IPurchaseOrderInfos, - IPurchaseOrderError + Record, + IPurchaseOrder > { - constructor (api: IAxiosInstance) { - super(api) + constructor (api: IAxiosInstance, apiKey: string) { + super(api, apiKey) this.singularName = 'pedidocompra' this.pluralName = 'pedidoscompra' From 21c5c9658397880982efcf7cef72cc67787b83fe Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Mon, 1 Nov 2021 23:16:49 +0100 Subject: [PATCH 21/28] feat(main-module): add the rest of the entities in the main bling module --- src/bling.ts | 62 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/src/bling.ts b/src/bling.ts index c845183..bf2fd14 100644 --- a/src/bling.ts +++ b/src/bling.ts @@ -1,22 +1,36 @@ 'use strict' +import CommercialProposals from './entities/commercialProposals' +import Contacts from './entities/contacts' +import Deposits from './entities/deposits' import Products from './entities/products' import Orders from './entities/orders' + +import createError, { + IBlingError as IStandardBlingError +} from './core/createError' + import axios, { AxiosInstance } from 'axios' +import PurchaseOrders from './entities/purchaseOrders' -import createError from './core/createError' +export type IBlingError = IStandardBlingError export class Bling { #api: AxiosInstance - #products: Products | undefined + #apiKey: string + #commercialProposals: CommercialProposals | undefined + #contacts: Contacts | undefined + #deposits: Deposits | undefined #orders: Orders | undefined + #products: Products | undefined + #purchaseOrders: PurchaseOrders | undefined constructor (apiKey: string) { if (!apiKey || typeof apiKey !== 'string') { throw createError( "The API key wasn't correctly provided for Bling connection.", 500, - this, + apiKey, 'ERR_NO_API_KEY' ) } @@ -33,20 +47,52 @@ export class Bling { return config }) + this.#apiKey = apiKey this.#api = api } - public products () { - if (!this.#products) { - this.#products = new Products(this.#api) + public commercialProposals () { + if (!this.#commercialProposals) { + this.#commercialProposals = new CommercialProposals( + this.#api, + this.#apiKey + ) } - return this.#products + return this.#commercialProposals + } + + public contacts () { + if (!this.#contacts) { + this.#contacts = new Contacts(this.#api, this.#apiKey) + } + return this.#contacts + } + + public deposits () { + if (!this.#deposits) { + this.#deposits = new Deposits(this.#api, this.#apiKey) + } + return this.#deposits } public orders () { if (!this.#orders) { - this.#orders = new Orders(this.#api) + this.#orders = new Orders(this.#api, this.#apiKey) } return this.#orders } + + public products () { + if (!this.#products) { + this.#products = new Products(this.#api, this.#apiKey) + } + return this.#products + } + + public purchaseOrders () { + if (!this.#purchaseOrders) { + this.#purchaseOrders = new PurchaseOrders(this.#api, this.#apiKey) + } + return this.#purchaseOrders + } } From 9fd40b9370cc8352702e9967449c16f5c190ad5a Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Mon, 1 Nov 2021 23:17:23 +0100 Subject: [PATCH 22/28] feat(tests): create all tests for contacts and delete method test for products --- test/entities/contacts/create.spec.js | 178 +++++++++++++++++++++ test/entities/contacts/find.spec.js | 47 ++++++ test/entities/contacts/find.test.ts | 94 ++++++++++++ test/entities/contacts/findBy.spec.js | 18 +++ test/entities/contacts/findBy.test.ts | 11 ++ test/entities/contacts/update.spec.js | 212 ++++++++++++++++++++++++++ test/entities/products/delete.test.ts | 16 ++ 7 files changed, 576 insertions(+) create mode 100644 test/entities/contacts/create.spec.js create mode 100644 test/entities/contacts/find.spec.js create mode 100644 test/entities/contacts/find.test.ts create mode 100644 test/entities/contacts/findBy.spec.js create mode 100644 test/entities/contacts/findBy.test.ts create mode 100644 test/entities/contacts/update.spec.js create mode 100644 test/entities/products/delete.test.ts diff --git a/test/entities/contacts/create.spec.js b/test/entities/contacts/create.spec.js new file mode 100644 index 0000000..30a303b --- /dev/null +++ b/test/entities/contacts/create.spec.js @@ -0,0 +1,178 @@ +import { bling } from '../../config/bling' +import gerarCpf from 'gerar-cpf' + +jest.setTimeout(60000) + +const testError = (err, message, status, code) => { + expect(err.message).toBe(message) + expect(err.status).toBe(status) + expect(err.code).toBe(code) +} + +test.concurrent( + "shouldn't create a contact when calling `.create()` method without any parameters", + async () => { + try { + await bling.contacts().create() + expect(false).toBe(true) + } catch (err) { + testError( + err, + 'The "data" argument must be a not empty object', + 500, + 'ERR_INCORRECT_DATA_ARG' + ) + } + } +) + +test.concurrent( + "shouldn't create a contact when calling `.create()` method with data as string", + async () => { + try { + await bling.contacts().create('') + expect(false).toBe(true) + } catch (err) { + testError( + err, + 'The "data" argument must be a not empty object', + 500, + 'ERR_INCORRECT_DATA_ARG' + ) + } + } +) + +test.concurrent( + "shouldn't create a contact when calling `.create()` method with data as number", + async () => { + try { + await bling.contacts().create(123) + expect(false).toBe(true) + } catch (err) { + testError( + err, + 'The "data" argument must be a not empty object', + 500, + 'ERR_INCORRECT_DATA_ARG' + ) + } + } +) + +test.concurrent( + "shouldn't create a contact when calling `.create()` method with data as array", + async () => { + try { + await bling.contacts().create([]) + expect(false).toBe(true) + } catch (err) { + testError( + err, + 'The "data" argument must be a not empty object', + 500, + 'ERR_INCORRECT_DATA_ARG' + ) + } + } +) + +test.concurrent( + "shouldn't create a contact when calling `.create()` method with data as an empty object", + async () => { + try { + await bling.contacts().create({}) + expect(false).toBe(true) + } catch (err) { + testError( + err, + 'The "data" argument must be a not empty object', + 500, + 'ERR_INCORRECT_DATA_ARG' + ) + } + } +) + +test("shouldn't create a contact when calling `.create()` method with missing name", async () => { + try { + await bling.contacts().create({ + cpf_cnpj: gerarCpf(), + tipoPessoa: 'F', + contribuinte: '9' + }) + expect(false).toBe(true) + } catch (err) { + testError( + err, + 'Error on create method after request call', + 400, + 'ERR_ENTITY_CREATION_FAILURE' + ) + } +}) + +test("shouldn't create a contact when calling `.create()` method with missing cpf_cnpj", async () => { + try { + await bling.contacts().create({ + nome: 'Usuário Teste', + tipoPessoa: 'F', + contribuinte: '9' + }) + expect(false).toBe(true) + } catch (err) { + testError( + err, + 'Error on create method after request call', + 400, + 'ERR_ENTITY_CREATION_FAILURE' + ) + } +}) + +test("shouldn't create a contact when calling `.create()` method with missing tipoPessoa", async () => { + try { + await bling.contacts().create({ + nome: 'Usuário Teste', + cpf_cnpj: gerarCpf(), + contribuinte: '9' + }) + expect(false).toBe(true) + } catch (err) { + testError( + err, + 'Error on create method after request call', + 400, + 'ERR_ENTITY_CREATION_FAILURE' + ) + } +}) + +test("shouldn't create a contact when calling `.create()` method with missing contribuinte", async () => { + try { + await bling.contacts().create({ + nome: 'Usuário Teste', + cpf_cnpj: gerarCpf(), + tipoPessoa: 'F' + }) + expect(false).toBe(true) + } catch (err) { + testError( + err, + 'Error on create method after request call', + 400, + 'ERR_ENTITY_CREATION_FAILURE' + ) + } +}) + +test('should create a contact when calling `.create()` method with proper data', async () => { + await expect( + bling.contacts().create({ + nome: 'Usuário Teste', + cpf_cnpj: gerarCpf(), + tipoPessoa: 'F', + contribuinte: '9' + }) + ).resolves.toBeDefined() +}) diff --git a/test/entities/contacts/find.spec.js b/test/entities/contacts/find.spec.js new file mode 100644 index 0000000..27f57a8 --- /dev/null +++ b/test/entities/contacts/find.spec.js @@ -0,0 +1,47 @@ +import { bling } from '../../config/bling' + +jest.setTimeout(60000) + +const testIdError = (err) => { + expect(err.message).toEqual('The "id" argument must be a number or string.') + expect(err.status).toEqual(500) + expect(err.code).toEqual('ERR_INCORRECT_ID_ARG') +} + +test.concurrent( + "shouldn't find a contact when calling `.find()` method without any parameters", + async () => { + try { + await bling.contacts().find() + expect(false).toBe(true) + } catch (err) { + testIdError(err) + } + } +) + +test.concurrent( + "shouldn't find a contact when calling `.find()` method with a nullable parameter", + async () => { + try { + await bling.contacts().find(undefined) + expect(false).toBe(true) + } catch (err) { + testIdError(err) + } + + try { + await bling.contacts().find(null) + expect(false).toBe(true) + } catch (err) { + testIdError(err) + } + + try { + await bling.contacts().find('') + expect(false).toBe(true) + } catch (err) { + testIdError(err) + } + } +) diff --git a/test/entities/contacts/find.test.ts b/test/entities/contacts/find.test.ts new file mode 100644 index 0000000..8a72c29 --- /dev/null +++ b/test/entities/contacts/find.test.ts @@ -0,0 +1,94 @@ +import { bling, IBlingError } from '../../config/bling' + +jest.setTimeout(60000) + +const testError = (err: IBlingError) => { + expect(err.message).toEqual('Error on find method after request call') + expect(err.code).toEqual('ERR_FIND_METHOD') +} + +const exampleContactId = '15112668863' + +test.concurrent( + "shouldn't find a contact when calling `.find()` method without defining params", + async () => { + try { + await bling.contacts().find(exampleContactId) + expect(false).toBe(true) + } catch (err) { + testError(err as IBlingError) + } + } +) + +test.concurrent( + 'should find a contact when calling `.find()` method with an existent id', + async () => { + await expect( + bling + .contacts() + .find(exampleContactId, { params: { identificador: '2' }, raw: false }) + ).resolves.toBeDefined() + await expect( + bling + .contacts() + .find(exampleContactId, { params: { identificador: '2' }, raw: true }) + ).resolves.toBeDefined() + } +) + +test.concurrent( + "shouldn't find a contact when calling `.find()` method with an inexistent id with beautified response", + async () => { + try { + await bling + .contacts() + .find('0', { params: { identificador: '1' }, raw: false }) + expect(false).toBe(true) + } catch (err) { + testError(err as IBlingError) + } + } +) + +test.concurrent( + "shouldn't find a contact when calling `.find()` method with an inexistent CPF/CNPJ with beautified response", + async () => { + try { + await bling + .contacts() + .find('0', { params: { identificador: '2' }, raw: false }) + expect(false).toBe(true) + } catch (err) { + testError(err as IBlingError) + } + } +) + +test.concurrent( + "shouldn't find a contact when calling `.find()` method with an inexistent id with raw response", + async () => { + try { + await bling + .contacts() + .find('0', { params: { identificador: '1' }, raw: true }) + expect(false).toBe(true) + } catch (err) { + testError(err as IBlingError) + } + } +) + +test.concurrent( + "shouldn't find a contact when calling `.find()` method with an inexistent CPF/CNPJ with raw response", + async () => { + try { + await bling + .contacts() + .find('0', { params: { identificador: '2' }, raw: true }) + expect(false).toBe(true) + } catch (err) { + testError(err as IBlingError) + } + } +) diff --git a/test/entities/contacts/findBy.spec.js b/test/entities/contacts/findBy.spec.js new file mode 100644 index 0000000..1cac04f --- /dev/null +++ b/test/entities/contacts/findBy.spec.js @@ -0,0 +1,18 @@ +import { bling } from '../../config/bling' + +jest.setTimeout(60000) + +const testError = (err) => { + expect(err.message).toEqual('No options passed to `.findBy()` method') + expect(err.status).toEqual(500) + expect(err.code).toEqual('ERR_INCORRECT_OPTIONS_ARG') +} + +test("shouldn't find contacts when calling `.findBy()` method without options", async () => { + try { + await bling.contacts().findBy() + expect(false).toBe(true) + } catch (err) { + testError(err) + } +}) diff --git a/test/entities/contacts/findBy.test.ts b/test/entities/contacts/findBy.test.ts new file mode 100644 index 0000000..eeec1e8 --- /dev/null +++ b/test/entities/contacts/findBy.test.ts @@ -0,0 +1,11 @@ +import { bling } from '../../config/bling' + +jest.setTimeout(60000) + +test('should find contacts when calling `.findBy()` method with options', async () => { + await expect( + bling + .contacts() + .findBy({ tipoPessoa: 'F', dataInclusao: '01/08/2021 TO 31/08/2021' }) + ).resolves.toBeDefined() +}) diff --git a/test/entities/contacts/update.spec.js b/test/entities/contacts/update.spec.js new file mode 100644 index 0000000..ec2d667 --- /dev/null +++ b/test/entities/contacts/update.spec.js @@ -0,0 +1,212 @@ +import { bling } from '../../config/bling' +import gerarCpf from 'gerar-cpf' + +jest.setTimeout(60000) + +const testError = (err, message, status, code) => { + expect(err.message).toBe(message) + expect(err.status).toBe(status) + expect(err.code).toBe(code) +} + +const exampleClientId = '15112668863' + +test.concurrent( + "shouldn't update a contact when calling `.update()` method with data as string", + async () => { + try { + await bling.contacts().update(exampleClientId, '') + expect(false).toBe(true) + } catch (err) { + testError( + err, + 'The "data" argument must be a not empty object', + 500, + 'ERR_INCORRECT_DATA_ARG' + ) + } + } +) + +test.concurrent( + "shouldn't update a contact when calling `.update()` method with data as number", + async () => { + try { + await bling.contacts().update(exampleClientId, 123) + expect(false).toBe(true) + } catch (err) { + testError( + err, + 'The "data" argument must be a not empty object', + 500, + 'ERR_INCORRECT_DATA_ARG' + ) + } + } +) + +test.concurrent( + "shouldn't update a contact when calling `.update()` method with data as array", + async () => { + try { + await bling.contacts().update(exampleClientId, []) + expect(false).toBe(true) + } catch (err) { + testError( + err, + 'The "data" argument must be a not empty object', + 500, + 'ERR_INCORRECT_DATA_ARG' + ) + } + } +) + +test.concurrent( + "shouldn't update a contact when calling `.update()` method with data as an empty object", + async () => { + try { + await bling.contacts().update(exampleClientId, {}) + expect(false).toBe(true) + } catch (err) { + testError( + err, + 'The "data" argument must be a not empty object', + 500, + 'ERR_INCORRECT_DATA_ARG' + ) + } + } +) + +test.concurrent( + "shouldn't update a contact when calling `.update()` method with id as an empty string", + async () => { + try { + await bling.contacts().update('', { + cpf_cnpj: gerarCpf() + }) + expect(false).toBe(true) + } catch (err) { + testError( + err, + 'The "id" argument must be a number or string', + 500, + 'ERR_INCORRECT_DATA_ID' + ) + } + } +) + +test.concurrent( + "shouldn't update a contact when calling `.update()` method with an inexistent id", + async () => { + try { + await bling.contacts().update(0, { + cpf_cnpj: gerarCpf() + }) + expect(false).toBe(true) + } catch (err) { + testError( + err, + 'The "id" argument must be a number or string', + 500, + 'ERR_INCORRECT_DATA_ID' + ) + } + } +) + +test.concurrent( + "shouldn't update a contact when calling `.update()` method with id as null", + async () => { + try { + await bling.contacts().update(null, { + cpf_cnpj: gerarCpf() + }) + expect(false).toBe(true) + } catch (err) { + testError( + err, + 'The "id" argument must be a number or string', + 500, + 'ERR_INCORRECT_DATA_ID' + ) + } + } +) + +test.concurrent( + "shouldn't update a contact when calling `.update()` method with id as undefined", + async () => { + try { + await bling.contacts().update(null, { + cpf_cnpj: gerarCpf() + }) + expect(false).toBe(true) + } catch (err) { + testError( + err, + 'The "id" argument must be a number or string', + 500, + 'ERR_INCORRECT_DATA_ID' + ) + } + } +) + +test.concurrent( + "shouldn't update a contact when calling `.update()` method with id as object", + async () => { + try { + await bling.contacts().update( + {}, + { + cpf_cnpj: gerarCpf() + } + ) + expect(false).toBe(true) + } catch (err) { + testError( + err, + 'The "id" argument must be a number or string', + 500, + 'ERR_INCORRECT_DATA_ID' + ) + } + } +) + +test.concurrent( + "shouldn't update a contact when calling `.update()` method with id as array", + async () => { + try { + await bling.contacts().update([], { + cpf_cnpj: gerarCpf() + }) + expect(false).toBe(true) + } catch (err) { + testError( + err, + 'The "id" argument must be a number or string', + 500, + 'ERR_INCORRECT_DATA_ID' + ) + } + } +) + +test('should update a contact when calling `.update()` method with proper parameters', async () => { + const contact = await bling + .contacts() + .find(exampleClientId, { params: { identificador: 2 } }) + + await expect( + bling.contacts().update(exampleClientId, { + nome: 'Usuário atualizado', + cpf_cnpj: contact.cnpj, + contribuinte: contact.contribuinte, + tipoPessoa: contact.tipo + }) + ).resolves.toBeDefined() +}) diff --git a/test/entities/products/delete.test.ts b/test/entities/products/delete.test.ts new file mode 100644 index 0000000..28f645e --- /dev/null +++ b/test/entities/products/delete.test.ts @@ -0,0 +1,16 @@ +import { bling } from '../../config/bling' + +jest.setTimeout(100000) + +test('should delete a product when calling `.delete()` method with an existent id', async () => { + const product = await bling.products().create({ + descricao: 'Produto Teste', + codigo: 'codigo-teste' + }) + + if (product.codigo) { + await expect(bling.products().delete(product.codigo)).resolves.toBeDefined() + } else { + expect(false).toBe(true) + } +}) From 28d38ef864836270829cb0c557ea5643cbc8f6ae Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Mon, 1 Nov 2021 23:33:14 +0100 Subject: [PATCH 23/28] test: turn into synchronous some tests related to the find function of contacts --- test/entities/contacts/find.test.ts | 84 +++++++++++++---------------- 1 file changed, 36 insertions(+), 48 deletions(-) diff --git a/test/entities/contacts/find.test.ts b/test/entities/contacts/find.test.ts index 8a72c29..0114529 100644 --- a/test/entities/contacts/find.test.ts +++ b/test/entities/contacts/find.test.ts @@ -37,58 +37,46 @@ test.concurrent( } ) -test.concurrent( - "shouldn't find a contact when calling `.find()` method with an inexistent id with beautified response", - async () => { - try { - await bling - .contacts() - .find('0', { params: { identificador: '1' }, raw: false }) - expect(false).toBe(true) - } catch (err) { - testError(err as IBlingError) - } +test("shouldn't find a contact when calling `.find()` method with an inexistent id with beautified response", async () => { + try { + await bling + .contacts() + .find('0', { params: { identificador: '1' }, raw: false }) + expect(false).toBe(true) + } catch (err) { + testError(err as IBlingError) } -) +}) -test.concurrent( - "shouldn't find a contact when calling `.find()` method with an inexistent CPF/CNPJ with beautified response", - async () => { - try { - await bling - .contacts() - .find('0', { params: { identificador: '2' }, raw: false }) - expect(false).toBe(true) - } catch (err) { - testError(err as IBlingError) - } +test("shouldn't find a contact when calling `.find()` method with an inexistent CPF/CNPJ with beautified response", async () => { + try { + await bling + .contacts() + .find('0', { params: { identificador: '2' }, raw: false }) + expect(false).toBe(true) + } catch (err) { + testError(err as IBlingError) } -) +}) -test.concurrent( - "shouldn't find a contact when calling `.find()` method with an inexistent id with raw response", - async () => { - try { - await bling - .contacts() - .find('0', { params: { identificador: '1' }, raw: true }) - expect(false).toBe(true) - } catch (err) { - testError(err as IBlingError) - } +test("shouldn't find a contact when calling `.find()` method with an inexistent id with raw response", async () => { + try { + await bling + .contacts() + .find('0', { params: { identificador: '1' }, raw: true }) + expect(false).toBe(true) + } catch (err) { + testError(err as IBlingError) } -) +}) -test.concurrent( - "shouldn't find a contact when calling `.find()` method with an inexistent CPF/CNPJ with raw response", - async () => { - try { - await bling - .contacts() - .find('0', { params: { identificador: '2' }, raw: true }) - expect(false).toBe(true) - } catch (err) { - testError(err as IBlingError) - } +test("shouldn't find a contact when calling `.find()` method with an inexistent CPF/CNPJ with raw response", async () => { + try { + await bling + .contacts() + .find('0', { params: { identificador: '2' }, raw: true }) + expect(false).toBe(true) + } catch (err) { + testError(err as IBlingError) } -) +}) From 3983430bd143b8e199cb55a9460d89e3ae6b0373 Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Mon, 1 Nov 2021 23:43:17 +0100 Subject: [PATCH 24/28] test: add a sleep time before each test in find method for contacts --- test/entities/contacts/find.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/entities/contacts/find.test.ts b/test/entities/contacts/find.test.ts index 0114529..922bbc2 100644 --- a/test/entities/contacts/find.test.ts +++ b/test/entities/contacts/find.test.ts @@ -2,6 +2,11 @@ import { bling, IBlingError } from '../../config/bling' jest.setTimeout(60000) +beforeEach(() => { + const wait = new Promise((resolve) => setTimeout(resolve, 1000)) + return wait +}, 2000) + const testError = (err: IBlingError) => { expect(err.message).toEqual('Error on find method after request call') expect(err.code).toEqual('ERR_FIND_METHOD') From 3cf0d22700624ab7923b53ca6065cbea8fe32d0a Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Tue, 2 Nov 2021 01:14:00 +0100 Subject: [PATCH 25/28] refactor(main-module): reorder import lines in main module --- src/bling.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bling.ts b/src/bling.ts index bf2fd14..5332001 100644 --- a/src/bling.ts +++ b/src/bling.ts @@ -5,13 +5,13 @@ import Contacts from './entities/contacts' import Deposits from './entities/deposits' import Products from './entities/products' import Orders from './entities/orders' +import PurchaseOrders from './entities/purchaseOrders' import createError, { IBlingError as IStandardBlingError } from './core/createError' import axios, { AxiosInstance } from 'axios' -import PurchaseOrders from './entities/purchaseOrders' export type IBlingError = IStandardBlingError From 505286f4ecd32fb24a8a90f576965873eb014948 Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Tue, 2 Nov 2021 01:14:33 +0100 Subject: [PATCH 26/28] feat(entity): add method overloading in all() and treat filters correctly in _getAll() function --- src/core/entity.ts | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/core/entity.ts b/src/core/entity.ts index e7b378d..4f7298a 100644 --- a/src/core/entity.ts +++ b/src/core/entity.ts @@ -53,6 +53,16 @@ export default class BaseEntity { this.pluralName = '' } + public async all(options?: { + params?: IFilters + raw?: false + }): Promise + + public async all(options?: { + params?: IFilters + raw: true + }): Promise> + public async all ( options: { params?: IFilters @@ -176,12 +186,25 @@ export default class BaseEntity { */ protected async _getAll ( endpoint: string, - params?: IFilters, + rawParams?: IFilters, raw = false ): Promise> { // @TODO: refactor filter logic to actually work const entities: IEntityResponse[] = [] + const params: { filters?: string } = {} + if (rawParams) { + const typedParams = rawParams as unknown as { [key: string]: string } + const filters = Object.keys(rawParams) + .map((key: string) => + typedParams[key] ? `${key}[${typedParams[key]}]` : null + ) + .filter((item) => !!item) + .join(';') + + params.filters = filters + } + let hasMore = true let reqCount = 0 let page = 1 From 753468145e2b45ce621828f4b020c769c8206b96 Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Tue, 2 Nov 2021 01:15:26 +0100 Subject: [PATCH 27/28] refactor(entities): refactor interfaces of products and contacts with new attributes --- src/entities/contacts.ts | 1 + src/entities/products.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/entities/contacts.ts b/src/entities/contacts.ts index 4621a96..1ccaca0 100644 --- a/src/entities/contacts.ts +++ b/src/entities/contacts.ts @@ -43,6 +43,7 @@ export interface IContactInfos { } export interface IContactResponse extends IContact { + id: string tipo: string cpf: string cnpj: string diff --git a/src/entities/products.ts b/src/entities/products.ts index c57a373..33f3672 100644 --- a/src/entities/products.ts +++ b/src/entities/products.ts @@ -2,7 +2,7 @@ import BlingEntity from '../core/entity' import { AxiosInstance as IAxiosInstance } from 'axios' export interface IProduct { - codigo?: string + codigo: string codigoItem?: '06' | '21' | '22' descricao: string tipo?: 'S' | 'P' | 'N' From 671e717c3b4aebfbb633de5acc7089ef07eb8910 Mon Sep 17 00:00:00 2001 From: Alexandre Batistella Bellas Date: Tue, 2 Nov 2021 01:22:26 +0100 Subject: [PATCH 28/28] docs(readme): update readme with the information about the new features --- README.md | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8db6800..6f7b732 100644 --- a/README.md +++ b/README.md @@ -22,19 +22,19 @@ npm i bling-erp-api ### Javascript ```js -const Bling = require('bling-erp-api') +const { Bling } = require('bling-erp-api') ``` ### Typescript ```ts -import Bling from 'bling-erp-api' +import { Bling } from 'bling-erp-api' ``` ## Criação de uma nova conexão Para criar uma conexão ao serviço do Bling, basta instanciar o objeto com a [API -key](https://ajuda.bling.com.br/hc/pt-br/articles/360046937853-Introdu%C3%A7%C3%A3o-para-a-API-do-Bling-para-desenvolvedores-) em seu construtor. +key](https://ajuda.bling.com.br/hc/pt-br/articles/360046937853-Introdu%C3%A7%C3%A3o-para-a-API-do-Bling-para-desenvolvedores-) em seu construtor. Lembre-se de sempre guardar a sua API key em seu arquivo `.env`. ```js const apiKey = 'sua_api_key' @@ -43,12 +43,19 @@ const blingConnection = new Bling(apiKey) ## Entidades disponíveis -As entidades atualmente permitidas para interação são somente: +As entidades atualmente permitidas para interação são: -- Produtos -- Pedidos +- Contatos (`.contacts()`) +- Depósitos (`.deposits()`) +- Pedidos (`.orders()`) +- Pedidos de compra (`.purchaseOrders()`) +- Produtos (`.products()`) +- Propostas comerciais (`.commercialProposals()`) -Em breve serão adicionadas mais. +Adicionaremos as restantes de acordo com as _releases_. Por ora, estamos focando +no funcionamento do pacote e no teste correto das entidades. +Além disso, as entidades no código estão em inglês. Em breve também deixaremos +disponíveis os métodos em português. ## Métodos permitidos @@ -85,4 +92,4 @@ tecnologias e estrutura do projeto são: - Alexandre Batistella Bellas; [LinkedIn](https://linkedin.com/in/alebatistella/) - Vitor Santana Cordeiro; [LinkedIn](https://linkedin.com/in/vitorsanc) -No futuro, contribuições da comunidade serão extremamente apreciadas! Ainda não possuímos as *guidelines de contribuição* definidas (`CONTRIBUTING.md`), mas assim que as tivermos nós iremos apreciar fortemente a contribuição da comunidade, inclusive por meio da abertura de *issues* 😊 +No futuro, contribuições da comunidade serão extremamente apreciadas! Ainda não possuímos as _guidelines de contribuição_ definidas (`CONTRIBUTING.md`), mas assim que as tivermos nós iremos apreciar fortemente a contribuição da comunidade, inclusive por meio da abertura de _issues_ 😊