@accounter/server
Version:
Accounter GraphQL server
273 lines (237 loc) • 8.4 kB
text/typescript
import { addHours, subYears } from 'date-fns';
import { GraphQLError } from 'graphql';
import { Inject, Injectable, Scope } from 'graphql-modules';
import { Currency } from '../../../shared/enums.js';
import { dateToTimelessDateString } from '../../../shared/helpers/index.js';
import { ENVIRONMENT } from '../../../shared/tokens.js';
import type { Environment, TimelessDateString } from '../../../shared/types/index.js';
import { ProviderCredentialsProvider } from '../../provider-credentials/providers/provider-credentials.provider.js';
import {
ContractSchema,
downloadInvoicePdfSchema,
Invoice,
retrieveInvoicesSchema,
retrievePaymentBreakdownSchema,
retrievePaymentReceiptsSchema,
} from './schemas.js';
({
scope: Scope.Operation,
global: true,
})
export class DeelClientProvider {
private host = 'https://api.letsdeel.com/rest/v2';
private tokenPromise: Promise<string> | null = null;
constructor(
(ENVIRONMENT) private env: Environment,
private credentialsProvider: ProviderCredentialsProvider,
) {}
// This is a workaround for the Deel API returning PST dates as UTC dates.
private timeZoneFix(dateString: string) {
const date = new Date(dateString);
return addHours(date, 7).toUTCString();
}
private getApiToken(): Promise<string> {
this.tokenPromise ??= this._fetchToken();
return this.tokenPromise;
}
private async _fetchToken(): Promise<string> {
const fromDb = await this.credentialsProvider.getDeelCredentials();
const token = fromDb?.apiToken ?? null;
if (!token) {
throw new GraphQLError('Deel credentials not configured for this tenant', {
extensions: { code: 'PROVIDER_NOT_CONFIGURED' },
});
}
return token;
}
public async getPaymentReceipts() {
try {
const queryVars: {
date_from: TimelessDateString;
date_to: TimelessDateString;
currencies?: Currency;
entities?: 'company' | 'individual';
} = {
date_from: dateToTimelessDateString(subYears(new Date(), 1)),
date_to: dateToTimelessDateString(new Date()),
};
const url = new URL(`${this.host}/payments`);
Object.entries(queryVars).map(([key, value]) => {
url.searchParams.set(key, value as string);
});
const res = await fetch(url, {
headers: {
accept: 'application/json',
authorization: `Bearer ${await this.getApiToken()}`,
},
});
if (!res.ok) {
console.error(await res.text());
throw new Error(`Deel API returned status ${res.status}`);
}
const rawData = await res.json();
if ('errors' in rawData) {
console.log(rawData);
throw new Error('Deel API returned an error');
}
const data = retrievePaymentReceiptsSchema.parse(rawData);
return data;
} catch (error) {
const message = 'Failed to fetch Deel payment receipts';
console.error(`${message}: ${error}`);
throw new Error(message, { cause: error });
}
}
public async getPaymentBreakdown(paymentId: string) {
try {
const url = new URL(`${this.host}/payments/${paymentId}/breakdown`);
const res = await fetch(url, {
headers: {
accept: 'application/json',
authorization: `Bearer ${await this.getApiToken()}`,
},
});
if (!res.ok) {
console.error(await res.text());
throw new Error(`Deel API returned status ${res.status}`);
}
const rawData = await res.json();
if ('errors' in rawData) {
console.log(rawData);
throw new Error('Deel API returned an error');
}
const data = retrievePaymentBreakdownSchema.parse(rawData);
return data;
} catch (error) {
const message = 'Failed to fetch Deel payment breakdown';
console.error(`${message}: ${error}`);
throw new Error(message, { cause: error });
}
}
public async getSalaryInvoices() {
const invoices: Invoice[] = [];
try {
const apiToken = await this.getApiToken();
const queryVars: {
issued_from_date?: TimelessDateString;
issued_to_date?: TimelessDateString;
entities?: 'company' | 'individual';
limit?: number;
offset?: number;
} = {
limit: 50,
offset: 0,
issued_from_date: dateToTimelessDateString(subYears(new Date(), 1)),
};
for (let i = 0; i < 10; i++) {
const url = new URL(`${this.host}/invoices`);
Object.entries({ ...queryVars, offset: i * 50 }).map(([key, value]) => {
url.searchParams.set(key, value as string);
});
const res = await fetch(url, {
headers: {
accept: 'application/json',
authorization: `Bearer ${apiToken}`,
},
});
if (!res.ok) {
console.error(await res.text());
throw new Error(`Deel API returned status ${res.status}`);
}
const rawData = await res.json();
if ('errors' in rawData) {
console.log(rawData);
throw new Error('Deel API returned an error');
}
const data = retrieveInvoicesSchema.parse(rawData);
if (data.data.length) {
data.data = data.data.map(invoice => {
return {
...invoice,
issued_at: this.timeZoneFix(invoice.issued_at),
due_date: this.timeZoneFix(invoice.due_date),
};
});
} else {
break;
}
invoices.push(...data.data);
}
return invoices;
} catch (error) {
const message = 'Failed to fetch Deel salary invoices';
console.error(`${message}: ${error}`);
throw new Error(message, { cause: error });
}
}
public async getSalaryInvoiceFile(id: string) {
try {
const url = new URL(`${this.host}/invoices/${id}/download`);
const res = await fetch(url, {
headers: {
accept: 'application/json',
authorization: `Bearer ${await this.getApiToken()}`,
},
});
if (!res.ok) {
console.error(await res.text());
throw new Error(`Deel API returned status ${res.status}`);
}
const rawData = await res.json();
const data = downloadInvoicePdfSchema.parse(rawData);
if (!data.data.url) {
throw new Error('Deel invoice file download URL is missing');
}
const invoiceFileRes = await fetch(data.data.url);
if (!invoiceFileRes.ok) {
throw new Error('Deel invoice file download failed');
}
const invoiceFileBuffer = await invoiceFileRes.arrayBuffer();
const blob = new Blob([invoiceFileBuffer], { type: 'application/pdf' });
return blob;
} catch (error) {
const message = 'Failed to fetch Deel invoice file';
console.error(`${message}: ${error}`);
throw new Error(message, { cause: error });
}
}
public async getContractDetails(contractId: string) {
try {
const url = new URL(`${this.host}/contracts/${contractId}`);
const res = await fetch(url, {
headers: {
accept: 'application/json',
authorization: `Bearer ${await this.getApiToken()}`,
},
});
if (!res.ok) {
console.error(await res.text());
throw new Error(`Deel API returned status ${res.status}`);
}
const rawData = await res.json();
if ('errors' in rawData) {
console.log(rawData);
throw new Error('Deel API returned an error');
}
const contract = ContractSchema.safeParse(rawData);
if (!contract.success) {
let errorMessage = 'Deel contract data validation failed';
if (contract.error.issues) {
for (const issue of contract.error.issues) {
let value = rawData;
for (const pathPart of issue.path) {
value = value ? value[pathPart] : undefined;
}
errorMessage += `\n - Path: ${issue.path.join('.')}, Message: ${issue.message}, value: ${value}`;
}
}
throw new Error(errorMessage);
}
return contract.data;
} catch (error) {
const message = 'Failed to fetch Deel contract details';
console.error(`${message}: ${error}`);
throw new Error(message, { cause: error });
}
}
}