diff --git a/.vscode/settings.json b/.vscode/settings.json index ff81dcd..5e71d71 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, diff --git a/src/client.test.ts b/src/client.test.ts index a10187a..ed17a88 100644 --- a/src/client.test.ts +++ b/src/client.test.ts @@ -7,9 +7,15 @@ import * as env from "./tests/env.js"; import { ModelCommunicationWay, ModelContact, + ModelContactAddress, ModelDocument, ModelDocumentFolder, ModelInvoice, + ModelPart, + ModelPaymentMethod, + ModelSevUser, + ModelStaticCountry, + ModelTag, ModelUnity, } from "./interfaces.js"; @@ -46,6 +52,104 @@ test("Get next invoice number", async () => { assert.type(nextInvoiceNumber, "string"); }); +// Manual test +// If you run this test, you need to clean up manually afterwards +test.skip("Create a new invoice", async () => { + const contactId = 123; + const contactPersonId = 123; + const invoiceNumber = `TEST-${new Date().toISOString()}`; + + const { + objects: { invoice }, + } = await sevDeskClient.saveInvoice({ + invoice: { + // id: null, + objectName: "Invoice", + invoiceNumber, + contact: { + id: contactId, + objectName: "Contact", + }, + contactPerson: { + id: contactPersonId, + objectName: "SevUser", + }, + invoiceDate: "01.01.2022", + header: `Invoice ${invoiceNumber}`, + headText: "header information", + footText: "footer information", + timeToPay: 20, + discount: 0, + address: "name\nstreet\npostCode city", + addressCountry: { + id: 1, + objectName: "StaticCountry", + }, + payDate: "2019-08-24T14:15:22Z", + deliveryDate: "01.01.2022", + deliveryDateUntil: null, + status: "100", + smallSettlement: 0, + taxRate: 0, + taxRule: { + id: "1", + objectName: "TaxRule", + }, + taxText: "Umsatzsteuer 19%", + taxType: "default", + taxSet: null, + paymentMethod: { + id: 21919, + objectName: "PaymentMethod", + }, + sendDate: "01.01.2020", + invoiceType: "RE", + currency: "EUR", + showNet: "1", + sendType: "VPR", + origin: null, + customerInternalNote: null, + propertyIsEInvoice: false, + mapAll: true, + }, + invoicePosSave: [ + { + id: null, + objectName: "InvoicePos", + mapAll: true, + quantity: 1, + price: 100, + name: "Dragonglass", + unity: { + id: 1, + objectName: "Unity", + }, + positionNumber: 0, + text: "string", + discount: 0.1, + taxRate: 19, + priceGross: 100, + priceTax: 0.1, + }, + ], + invoicePosDelete: null, + filename: "string", + discountSave: [ + { + discount: "true", + text: "string", + percentage: true, + value: 0, + objectName: "Discounts", + mapAll: "true", + }, + ], + discountDelete: null, + }); + + assertIsInvoice(invoice); +}); + test("Get document folders", async () => { const { objects: documentFolders } = await sevDeskClient.getDocumentFolders(); @@ -60,7 +164,8 @@ test("Get documents", async () => { documents.forEach(assertIsDocument); }); -// If you run this test, you need to remove the document manually afterwards +// Manual test +// If you run this test, you need to clean up manually afterwards test.skip("Add document", async () => { const { objects: [document], @@ -78,6 +183,24 @@ test("Get contacts", async () => { contacts.forEach(assertIsContact); }); +test("Get contact addresses (without contact ID)", async () => { + const { objects: contactAddresses } = + await sevDeskClient.getContactAddresses(); + + assert.is(contactAddresses.length > 0, true); + contactAddresses.forEach(assertIsContactAddress); +}); + +test("Get contact addresses (with contact ID)", async () => { + const { objects: contacts } = await sevDeskClient.getContacts(); + const { objects: contactAddresses } = await sevDeskClient.getContactAddresses( + { contactId: contacts[0].id } + ); + + assert.is(contactAddresses.length > 0, true); + contactAddresses.forEach(assertIsContactAddress); +}); + test("Get communication ways", async () => { const { objects: communicationWays } = await sevDeskClient.getCommunicationWays(); @@ -93,6 +216,41 @@ test("Get unities", async () => { unities.forEach(assertIsUnity); }); +test("Get payment methods", async () => { + const { objects: paymentMethods } = await sevDeskClient.getPaymentMethods(); + + assert.is(paymentMethods.length > 0, true); + paymentMethods.forEach(assertIsPaymentMethod); +}); + +test("Get tags", async () => { + const { objects: tags } = await sevDeskClient.getTags(); + + assert.is(tags.length > 0, true); + tags.forEach(assertIsTag); +}); + +test("Get users", async () => { + const { objects: users } = await sevDeskClient.getSevUsers(); + + assert.is(users.length > 0, true); + users.forEach(assertIsSevUser); +}); + +test("Get static countries", async () => { + const { objects: countries } = await sevDeskClient.getStaticCountries(); + + assert.is(countries.length > 0, true); + countries.forEach(assertIsStaticCountry); +}); + +test("Get parts", async () => { + const { objects: parts } = await sevDeskClient.getParts(); + + assert.is(parts.length > 0, true); + parts.forEach(assertIsPart); +}); + const assertIsInvoice = (invoice: ModelInvoice) => { assert.is(invoice.objectName, "Invoice"); }; @@ -109,6 +267,10 @@ const assertIsContact = (contact: ModelContact) => { assert.is(contact.objectName, "Contact"); }; +const assertIsContactAddress = (contact: ModelContactAddress) => { + assert.is(contact.objectName, "ContactAddress"); +}; + const assertIsCommunicationWay = (communicationWay: ModelCommunicationWay) => { assert.is(communicationWay.objectName, "CommunicationWay"); }; @@ -117,4 +279,24 @@ const assertIsUnity = (unity: ModelUnity) => { assert.is(unity.objectName, "Unity"); }; +const assertIsPaymentMethod = (paymentMethod: ModelPaymentMethod) => { + assert.is(paymentMethod.objectName, "PaymentMethod"); +}; + +const assertIsTag = (tag: ModelTag) => { + assert.is(tag.objectName, "Tag"); +}; + +const assertIsSevUser = (user: ModelSevUser) => { + assert.is(user.objectName, "SevUser"); +}; + +const assertIsStaticCountry = (user: ModelStaticCountry) => { + assert.is(user.objectName, "StaticCountry"); +}; + +const assertIsPart = (part: ModelPart) => { + assert.is(part.objectName, "Part"); +}; + test.run(); diff --git a/src/client.ts b/src/client.ts index 78ddb0d..f9e2cf8 100644 --- a/src/client.ts +++ b/src/client.ts @@ -4,9 +4,16 @@ import { UnknownApiError } from "./errors.js"; import { ModelCommunicationWay, ModelContact, + ModelContactAddress, ModelDocument, ModelDocumentFolder, ModelInvoice, + ModelInvoicePos, + ModelPart, + ModelPaymentMethod, + ModelSevUser, + ModelStaticCountry, + ModelTag, ModelUnity, } from "./interfaces.js"; import { SevDeskUrls } from "./urls.js"; @@ -63,7 +70,7 @@ export class SevDeskClient { throw error; } if (response.ok === false || error) { - const message = error?.message ?? body?.error?.message; + const message = error?.message ?? body?.error?.message ?? body.message; throw new UnknownApiError(message, { response }); } @@ -112,6 +119,25 @@ export class SevDeskClient { }>(url, { method: "GET" }); } + /** + * Create a new invoice + */ + async saveInvoice(body: unknown) { + const url = this.urls.apiSaveInvoiceUrl(); + + return this.request<{ + objects: { + invoice: Required; + invoicePos: Array>; + filename: string; + }; + }>(url, { + method: "POST", + body: JSON.stringify(body), + headers: { "Content-Type": "application/json" }, + }); + } + // ------------------------------------------------------- // DocumentFolder // ------------------------------------------------------- @@ -189,6 +215,26 @@ export class SevDeskClient { }); } + // ------------------------------------------------------- + // ContactAddress + // ------------------------------------------------------- + + /** + * Get an overview of all contact addresses + */ + async getContactAddresses( + params: UrlParamsFor<"apiGetContactAddressesUrl"> = {} + ) { + const url = this.urls.apiGetContactAddressesUrl(params); + + return this.request<{ objects: Array> }>( + url, + { + method: "GET", + } + ); + } + // ------------------------------------------------------- // CommunicationWay // ------------------------------------------------------- @@ -228,6 +274,85 @@ export class SevDeskClient { }); } + // ------------------------------------------------------- + // PaymentMethod + // ------------------------------------------------------- + + /** + * Get an overview of all payment methods + */ + async getPaymentMethods( + params: UrlParamsFor<"apiGetPaymentMethodsUrl"> = {} + ) { + const url = this.urls.apiGetPaymentMethodsUrl(params); + + return this.request<{ objects: Array> }>(url, { + method: "GET", + }); + } + + // ------------------------------------------------------- + // Tag + // ------------------------------------------------------- + + /** + * Get an overview of all tags + */ + async getTags(params: UrlParamsFor<"apiGetTagsUrl"> = {}) { + const url = this.urls.apiGetTagsUrl(params); + + return this.request<{ objects: Array> }>(url, { + method: "GET", + }); + } + + // ------------------------------------------------------- + // SevUser + // ------------------------------------------------------- + + /** + * Get an overview of all users + */ + async getSevUsers(params: UrlParamsFor<"apiGetSevUsersUrl"> = {}) { + const url = this.urls.apiGetSevUsersUrl(params); + + return this.request<{ objects: Array> }>(url, { + method: "GET", + }); + } + + // ------------------------------------------------------- + // StaticCountry + // ------------------------------------------------------- + + /** + * Get an overview of all static countries + */ + async getStaticCountries( + params: UrlParamsFor<"apiGetStaticCountriesUrl"> = {} + ) { + const url = this.urls.apiGetStaticCountriesUrl(params); + + return this.request<{ objects: Array> }>(url, { + method: "GET", + }); + } + + // ------------------------------------------------------- + // Part + // ------------------------------------------------------- + + /** + * Get an overview of all parts + */ + async getParts(params: UrlParamsFor<"apiGetPartsUrl"> = {}) { + const url = this.urls.apiGetPartsUrl(params); + + return this.request<{ objects: Array> }>(url, { + method: "GET", + }); + } + // // pending invoices from sevdesk includes also outstanding / due invoices // // we remove them with a filter but you could also include the if you only need everything pending // async getPendingInvoices(options = { includeOutstanding: false }) { diff --git a/src/urls.ts b/src/urls.ts index 1b8b5ae..bfb62c8 100644 --- a/src/urls.ts +++ b/src/urls.ts @@ -97,6 +97,27 @@ export class SevDeskUrls { }); } + // ------------------------------------------------------- + // ContactAddress + // ------------------------------------------------------- + + apiGetContactAddressesUrl({ + contactId, + ...query + }: { contactId?: string | undefined } & DefaultCollectionQuery & Query = {}) { + if (contactId) { + return this.apiUrl({ + path: `Contact/${contactId}/getAddresses`, + query, + }); + } + + return this.apiUrl({ + path: `ContactAddress`, + query, + }); + } + // ------------------------------------------------------- // CommunicationWay // ------------------------------------------------------- @@ -120,4 +141,59 @@ export class SevDeskUrls { query, }); } + + // ------------------------------------------------------- + // PaymentMethod + // ------------------------------------------------------- + + apiGetPaymentMethodsUrl({ ...query }: DefaultCollectionQuery & Query = {}) { + return this.apiUrl({ + path: `PaymentMethod`, + query, + }); + } + + // ------------------------------------------------------- + // Tag + // ------------------------------------------------------- + + apiGetTagsUrl({ ...query }: DefaultCollectionQuery & Query = {}) { + return this.apiUrl({ + path: `Tag`, + query, + }); + } + + // ------------------------------------------------------- + // SevUser + // ------------------------------------------------------- + + apiGetSevUsersUrl({ ...query }: DefaultCollectionQuery & Query = {}) { + return this.apiUrl({ + path: `SevUser`, + query, + }); + } + + // ------------------------------------------------------- + // StaticCountry + // ------------------------------------------------------- + + apiGetStaticCountriesUrl({ ...query }: DefaultCollectionQuery & Query = {}) { + return this.apiUrl({ + path: `StaticCountry`, + query, + }); + } + + // ------------------------------------------------------- + // Part + // ------------------------------------------------------- + + apiGetPartsUrl({ ...query }: DefaultCollectionQuery & Query = {}) { + return this.apiUrl({ + path: `Part`, + query, + }); + } }