-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add prices and other improvements #11
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,7 +9,8 @@ | |
], | ||
"scripts": { | ||
"build": "tsc", | ||
"test": "./node_modules/.bin/nyc ./node_modules/.bin/mocha -r ts-node/register test/*.ts" | ||
"auth": "./node_modules/.bin/ts-node ./test/http-server-oauth-authorize.ts", | ||
"test": "./node_modules/.bin/nyc ./node_modules/.bin/mocha -r ts-node/register test/*.spec.ts" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. changes |
||
}, | ||
"author": "Gustaf Ridderstolpe, Enfo Sweden AB", | ||
"repository": "github:obrut/fortnox", | ||
|
@@ -45,7 +46,9 @@ | |
"src/**/*" | ||
], | ||
"exclude": [ | ||
"**/*.d.ts" | ||
"**/*.d.ts", | ||
"src/dispatch.ts", | ||
"src/utils.ts" | ||
], | ||
"reporter": [ | ||
"text" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,12 +8,10 @@ type ArticleResult = { | |
|
||
export class Articles { | ||
private dispatch: Dispatch; | ||
private util: Util; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Made Util use static methods (they are private anyway and not used outside). There seem to be no reason for extra allocations and repeating code. |
||
private path = 'articles'; | ||
|
||
constructor(dispatch: Dispatch){ | ||
this.dispatch = dispatch; | ||
this.util = new Util(); | ||
} | ||
|
||
async get(articleNumber: string) { | ||
|
@@ -22,10 +20,7 @@ export class Articles { | |
} | ||
|
||
async getAll(filter?: string) { | ||
let path = this.path; | ||
if (filter) | ||
path += '?filter=' + filter; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unified all "filter" parameters (remove repeated code) |
||
const result = await this.util.getAllPages(path, 'Articles', this.dispatch) as FNArticle[]; | ||
const result = await Util.getAllPages(this.path, 'Articles', this.dispatch, filter) as FNArticle[]; | ||
return result; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,40 +1,55 @@ | ||
import fetch from 'node-fetch'; | ||
import { Defaults } from './types/defaults'; | ||
import { Util } from './utils'; | ||
|
||
export class Dispatch { | ||
private host: string; | ||
private defaults: Defaults; | ||
|
||
constructor(config: {Host: string, Defaults: Defaults}) { | ||
constructor(config: { Host: string, Defaults: Defaults }) { | ||
this.host = config.Host, | ||
this.defaults = config.Defaults | ||
this.defaults = config.Defaults | ||
} | ||
|
||
async get(path?: string) { | ||
const response = await fetch(`${this.host}${path}`, { method: 'GET', headers: this.defaults.headers }); | ||
if (response.status === 200) | ||
return await response.json() as object; | ||
throw new Error(response.statusText); | ||
const timeouts = [250, 250, 250, 500, 1000, 2000, 4000, 8000]; | ||
let timeout = 0; | ||
|
||
while (true) { | ||
const response = await fetch(`${this.host}${path}`, { method: 'GET', headers: this.defaults.headers }) | ||
if (response.status === 200) { | ||
return await response.json() as object; | ||
} | ||
|
||
// 429 = too many requests (overload), use exponential retries | ||
if (response.status === 429 && timeout < timeouts.length) { | ||
await Util.delay(timeouts[timeout]); | ||
timeout += 1; | ||
continue; | ||
} | ||
|
||
throw await Util.makeErrorFromResponse(response) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "exponential" exception handling for "too many requests". |
||
} | ||
|
||
async post(path: string, body: any) { | ||
const response = await fetch(`${this.host}${path}`, { method: 'POST', headers: this.defaults.headers, body: JSON.stringify(body, null, 4) }); | ||
if (response.status === 201) | ||
return await response.json() as object; | ||
throw new Error(response.statusText); | ||
throw await Util.makeErrorFromResponse(response); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use FORTNOX error, do not ignore it - it is really useful to figure out what went wrong |
||
} | ||
|
||
async put(path: string, body?: any) { | ||
const response = await fetch(`${this.host}${path}`, { method: 'PUT', headers: this.defaults.headers, body: body && JSON.stringify(body, null, 4) }); | ||
if (response.status === 200) | ||
return await response.json() as object; | ||
throw new Error(response.statusText); | ||
throw await Util.makeErrorFromResponse(response); | ||
} | ||
|
||
async delete(path: string) { | ||
const response = await fetch(`${this.host}${path}`, { method: 'DELETE', headers: this.defaults.headers }); | ||
if (response && response.status === 204) | ||
return response.ok; | ||
throw new Error(response.statusText); | ||
throw await Util.makeErrorFromResponse(response); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,33 +1,42 @@ | ||
import { Articles } from './articles'; | ||
import { Customers } from './customers'; | ||
import { Invoices } from './invoices'; | ||
import { Prices } from './prices'; | ||
import { SupplierInvoices } from './supplierinvoices' | ||
import { Dispatch } from './dispatch'; | ||
import { Suppliers } from './suppliers'; | ||
import { Defaults } from './types/defaults'; | ||
|
||
export class Fortnox { | ||
constructor(config: { host: string, clientSecret: string, accessToken: string }){ | ||
constructor(config: { | ||
host: string, | ||
bearerToken?: string, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add bearer token support for OAuth2 |
||
clientSecret?: string, | ||
accessToken?: string, | ||
}) { | ||
const defaults: Defaults = { | ||
json: true, | ||
headers: { | ||
'client-secret': config.clientSecret, | ||
'access-token': config.accessToken, | ||
...(config.bearerToken && { 'Authorization': `Bearer ${config.bearerToken}` }), | ||
...(config.clientSecret && { 'client-secret': config.clientSecret }), | ||
...(config.accessToken && { 'access-token': config.accessToken }), | ||
'Content-Type': 'application/json', | ||
'Accept': 'application/json' | ||
} | ||
} | ||
const dispatch = new Dispatch( {Host: config.host, Defaults: defaults } ); | ||
const dispatch = new Dispatch({ Host: config.host, Defaults: defaults }); | ||
this.articles = new Articles(dispatch); | ||
this.customers = new Customers(dispatch); | ||
this.invoices = new Invoices(dispatch); | ||
this.supplierInvoices = new SupplierInvoices(dispatch); | ||
this.suppliers = new Suppliers(dispatch); | ||
this.prices = new Prices(dispatch); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add prices support |
||
} | ||
|
||
public articles: Articles; | ||
public customers: Customers; | ||
public invoices: Invoices; | ||
public supplierInvoices: SupplierInvoices; | ||
public suppliers: Suppliers; | ||
public prices: Prices; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,26 +8,24 @@ type InvoiceResult = { | |
|
||
export class Invoices { | ||
private dispatch: Dispatch; | ||
private util: Util; | ||
private path = 'invoices'; | ||
|
||
constructor(dispatch: Dispatch){ | ||
this.dispatch = dispatch; | ||
this.util = new Util(); | ||
} | ||
|
||
async get(documentNumber?: string) { | ||
const result = await this.dispatch.get(`${this.path}/${documentNumber || ''}`) as InvoiceResult; | ||
return result.Invoice; | ||
} | ||
|
||
async getAll(filter: string) { | ||
const result = await this.util.getAllPages(this.path + '?filter=' + filter, 'Invoices', this.dispatch) as InvoiceResult[]; | ||
async getAll(filter?: string) { | ||
const result = await Util.getAllPages(this.path, 'Invoices', this.dispatch, filter) as FNInvoice[]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix a bug - the results is array of FNInvoice[] actually, not of InvoiceResult[]. |
||
return result; | ||
} | ||
|
||
async getByCustomer(customerNumber: string) { | ||
const allInvoices = await this.util.getAllPages(`${this.path}/`, 'Invoices', this.dispatch); | ||
const allInvoices = await Util.getAllPages(this.path, 'Invoices', this.dispatch); | ||
return allInvoices.filter(invoice => invoice.CustomerNumber.toLowerCase() == customerNumber.toLowerCase()); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { Dispatch } from './dispatch'; | ||
import { FNPrice } from './types/FNPrice'; | ||
import { Util } from './utils'; | ||
|
||
type PriceResult = { | ||
Price: FNPrice | ||
} | ||
|
||
export class Prices { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prices/price lists support |
||
private dispatch: Dispatch; | ||
private path = 'prices'; | ||
|
||
constructor(dispatch: Dispatch) { | ||
this.dispatch = dispatch; | ||
} | ||
|
||
async getAll(priceList?: string, articleNumber?: string) { | ||
let path = `${this.path}/sublist`; | ||
if (priceList) | ||
path += `/${priceList}`; | ||
if (articleNumber) | ||
path += `/${articleNumber}` | ||
const result = await Util.getAllPages(path, 'Prices', this.dispatch) as FNPrice[]; | ||
return result; | ||
} | ||
|
||
async create(price: any) { | ||
const result = await this.dispatch.post(this.path, { Price: price }) as PriceResult; | ||
return result.Price; | ||
} | ||
|
||
async update(price: FNPrice) { | ||
const result = await this.dispatch.put(`${this.path}/${price.PriceList}/${price.ArticleNumber}/${price.FromQuantity}`, { Price: price }) as PriceResult; | ||
return result.Price; | ||
} | ||
|
||
async remove(priceList: string, articleNumber: string, fromQuantity?: number) { | ||
let path = `${this.path}/${priceList}/${articleNumber}`; | ||
if (fromQuantity) | ||
path += `/${fromQuantity}`; | ||
const result = await this.dispatch.delete(path); | ||
return result; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,30 +3,24 @@ import { FNSupplier } from './types/FNSupplier'; | |
import { Util } from './utils'; | ||
|
||
type SupplierResult = { | ||
Supplier: FNSupplier, | ||
Suppliers: FNSupplier[] | ||
Supplier: FNSupplier | ||
} | ||
|
||
export class Suppliers { | ||
private dispatch: Dispatch; | ||
private util: Util; | ||
private path = 'suppliers'; | ||
|
||
constructor(dispatch: Dispatch){ | ||
this.dispatch = dispatch; | ||
this.util = new Util(); | ||
} | ||
|
||
async get(supplierNumber?: string) { | ||
const result = await this.dispatch.get(this.path) as SupplierResult; | ||
return supplierNumber ? result.Supplier : result.Suppliers; | ||
async get(supplierNumber: string) { | ||
const result = await this.dispatch.get(`${this.path}/${supplierNumber}`) as SupplierResult; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix a bug - parameter not used (should be part of the query path) |
||
return result.Supplier; | ||
} | ||
|
||
async getAll(filter?: string) { | ||
let path = this.path; | ||
if (filter) | ||
path += '?filter=' + filter; | ||
const result = await this.util.getAllPages(path, 'Suppliers', this.dispatch) as FNSupplier[]; | ||
const result = await Util.getAllPages(this.path, 'Suppliers', this.dispatch, filter) as FNSupplier[]; | ||
return result; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export type FNPrice = { | ||
ArticleNumber?: string, | ||
FromQuantity?: number, | ||
Price?: number, | ||
PriceList?: string, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,47 @@ | ||
import { Dispatch } from "./dispatch"; | ||
|
||
export class Util { | ||
async getAllPages(path: string, arrayName: string, dispatch: Dispatch) { | ||
static async getAllPages(path: string, arrayName: string, dispatch: Dispatch, filter?: string) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unify filter/paging. Before it had an issue that filter + paging did not work together. |
||
const items: any = await dispatch.get(path); | ||
|
||
if (filter) | ||
path += '?filter=' + filter; | ||
|
||
let allItems: any[] = []; | ||
const totalPages = Number.parseInt(items.MetaInformation['@TotalPages']); | ||
allItems.push(...items[arrayName]); | ||
|
||
if (totalPages > 1){ | ||
let currentPage: number = 2; | ||
while(currentPage <= totalPages) { | ||
allItems.push(...await dispatch.get(path + '&page=' + currentPage)[arrayName]); | ||
const pageItems: any = await dispatch.get(path + (filter ? '&' : '?') + 'page=' + currentPage); | ||
allItems.push(...pageItems[arrayName]); | ||
currentPage++; | ||
} | ||
} | ||
|
||
return allItems; | ||
} | ||
|
||
// convert fortnox error into standard error | ||
static async makeErrorFromResponse (response: any) { | ||
|
||
// get fortnox error if exist | ||
if (response?.headers?.get('content-type')?.startsWith('application/json')) { | ||
const fnError = await response.json(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fortnox error is packed like |
||
const fnMessage = fnError?.ErrorInformation?.message; | ||
if (fnMessage) { | ||
return new Error(fnMessage); | ||
} | ||
} | ||
|
||
return new Error(response.statusText ?? "An error has occured"); | ||
} | ||
|
||
// a promise that resolves after specified amount of time | ||
static delay(ms: number): Promise<void> { | ||
return new Promise((resolve: () => void) => { | ||
setTimeout(resolve, ms); | ||
}); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To get OAuth2 token:
npm run auth
. As far as I understand, now fortnox only allows OAuth2 for "new" apps