Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
pull_request:
branches:
- main
- master

jobs:
build:
Expand Down
264 changes: 262 additions & 2 deletions nodes/EasyBill/EasyBill.node.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type {
GenericValue,
IDataObject,
IExecuteFunctions,
IHttpRequestOptions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionTypes, sleep } from 'n8n-workflow';
import { NodeConnectionTypes, NodeOperationError, sleep } from 'n8n-workflow';
import { customerOperations } from './Customers/CustomerOperations';
import { customerFields } from './Customers/CustomerFields';
import { documentFields } from './Documents/DocumentsFields';
Expand All @@ -16,6 +17,8 @@ import { customerGroupOperations } from './CustomerGroup/CustomerGroupOperations
import { customerGroupFields } from './CustomerGroup/CustomerGroupFields';
import { discountOperations } from './Discount/DiscountOperations';
import { discountFields } from './Discount/DiscountFields';
import { sepaPaymentOperations } from './SEPAPayment/SEPAPaymentOperations';
import { sepaPaymentFields } from './SEPAPayment/SEPAPaymentFields';
import { easyBillApiRequest } from './GenericFunctions';
/**
* HAUPTEINSTIEG: EasyBill Node
Expand Down Expand Up @@ -65,9 +68,10 @@ export class EasyBill implements INodeType {
noDataExpression: true,
options: [
{ name: 'Customer', value: 'customer' },
{ name: 'Document', value: 'document' },
{ name: 'Customer Group', value: 'customerGroup' },
{ name: 'Discount', value: 'discount' },
{ name: 'Document', value: 'document' },
{ name: 'SEPA Payment', value: 'sepaPayment' },

// Weitere Ressourcen können hier ergänzt werden.
],
Expand All @@ -82,6 +86,8 @@ export class EasyBill implements INodeType {
...customerGroupFields,
...discountOperations,
...discountFields,
...sepaPaymentOperations,
...sepaPaymentFields,

{
displayName: 'Options',
Expand Down Expand Up @@ -1289,6 +1295,260 @@ export class EasyBill implements INodeType {
json: true,
};

responseData = await easyBillApiRequest.call(this, options);
returnData.push(responseData);
}
}
/* -------------------------------------------------------------------------- */
/* SEPA Payment */
/* -------------------------------------------------------------------------- */
if (resource === 'sepaPayment') {
const toDateString = (value?: string) => (value ? value.split('T')[0] : undefined);
const mapSepaAdditionalFields = (fields: IDataObject) => {
const payload: IDataObject = {};
const mapping: Record<string, string> = {
creditorBic: 'creditor_bic',
creditorIban: 'creditor_iban',
creditorName: 'creditor_name',
debitorBic: 'debitor_bic',
debitorAddressLine1: 'debitor_address_line_1',
debitorAddressLine2: 'debitor_address_line2',
debitorCountry: 'debitor_country',
remittanceInformation: 'remittance_information',
type: 'type',
};

for (const [key, value] of Object.entries(fields)) {
if (value === '' || value === undefined || value === null) {
continue;
}

if (key === 'exportAt') {
payload.export_at = value;
continue;
}

if (key === 'requestedAt') {
const dateValue = toDateString(value as string);
if (dateValue) {
payload.requested_at = dateValue;
}
continue;
}

const mappedKey = mapping[key];
if (mappedKey) {
payload[mappedKey] = value;
}
}

return payload;
};

/* ╔══════════════════════════╗ */
/* ║ GET SEPA PAYMENTS LIST ║ */
/* ╚══════════════════════════╝ */
if (operation === 'getSepaPayments') {
const limit = this.getNodeParameter('limit', i) as number | undefined;
const page = this.getNodeParameter('page', i) as number | undefined;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const qs: IDataObject = {};

if (limit !== undefined) {
qs.limit = limit;
}
if (page !== undefined) {
qs.page = page;
}
if (additionalFields && Object.keys(additionalFields).length > 0) {
Object.assign(qs, additionalFields);
}

const options: IHttpRequestOptions = {
headers: {
Accept: 'application/json',
},
method: 'GET',
url: `/sepa-payments`,
json: true,
qs,
};

responseData = await easyBillApiRequest.call(this, options);
returnData.push(responseData);
}
/* ╔════════════════════════╗ */
/* ║ CREATE SEPA PAYMENT ║ */
/* ╚════════════════════════╝ */
if (operation === 'createSepaPayment') {
const documentId = this.getNodeParameter('documentId', i) as number;
const debitorName = this.getNodeParameter('debitorName', i) as string;
const debitorIban = this.getNodeParameter('debitorIban', i) as string;
const mandateId = this.getNodeParameter('mandateId', i) as string;
const mandateDateOfSignature = this.getNodeParameter(
'mandateDateOfSignature',
i,
) as string;
const localInstrument = this.getNodeParameter('localInstrument', i) as string;
const sequenceType = this.getNodeParameter('sequenceType', i) as string;
const amount = this.getNodeParameter('amount', i) as number;
const reference = this.getNodeParameter('reference', i) as string;
const requestedAt = this.getNodeParameter('requestedAt', i, '') as string;
const additionalFields = this.getNodeParameter('additionalFields', i, {}) as IDataObject;
const mandateDate = toDateString(mandateDateOfSignature) ?? mandateDateOfSignature;
const requiredFields: Array<{ key: string; value: string | undefined }> = [
{ key: 'debitor_name', value: debitorName },
{ key: 'debitor_iban', value: debitorIban },
{ key: 'mandate_id', value: mandateId },
{ key: 'mandate_date_of_signature', value: mandateDateOfSignature },
{ key: 'local_instrument', value: localInstrument },
{ key: 'sequence_type', value: sequenceType },
{ key: 'reference', value: reference },
];
const missingFields = requiredFields
.filter(({ value }) => typeof value !== 'string' || value.trim() === '')
.map(({ key }) => key);

if (missingFields.length > 0) {
throw new NodeOperationError(
this.getNode(),
`Missing required fields: ${missingFields.join(', ')}`,
);
}

const requestedDate = requestedAt
? (toDateString(requestedAt) ?? requestedAt)
: undefined;

const body: IDataObject = {
document_id: documentId,
debitor_name: debitorName,
debitor_iban: debitorIban,
mandate_id: mandateId,
mandate_date_of_signature: mandateDate,
local_instrument: localInstrument,
sequence_type: sequenceType,
amount,
reference,
};

if (requestedDate) {
body.requested_at = requestedDate;
}

Object.assign(body, mapSepaAdditionalFields(additionalFields));

const options: IHttpRequestOptions = {
headers: {
Accept: 'application/json',
},
method: 'POST',
url: `/sepa-payments`,
json: true,
body,
};

responseData = await easyBillApiRequest.call(this, options);
returnData.push(responseData);
}
/* ╔═══════════════════════╗ */
/* ║ GET SEPA PAYMENT ║ */
/* ╚═══════════════════════╝ */
if (operation === 'getSepaPayment') {
const id = this.getNodeParameter('sepaPaymentId', i) as number;

const options: IHttpRequestOptions = {
headers: {
Accept: 'application/json',
},
method: 'GET',
url: `/sepa-payments/${id}`,
json: true,
};

responseData = await easyBillApiRequest.call(this, options);
returnData.push(responseData);
}
/* ╔════════════════════════╗ */
/* ║ UPDATE SEPA PAYMENT ║ */
/* ╚════════════════════════╝ */
if (operation === 'updateSepaPayment') {
const id = this.getNodeParameter('sepaPaymentId', i) as number;
const documentId = this.getNodeParameter('documentId', i) as number;
const updateFields = this.getNodeParameter('updateFields', i, {}) as IDataObject;
const additionalFields = this.getNodeParameter('additionalFields', i, {}) as IDataObject;

const body: IDataObject = {
document_id: documentId,
};

const addFieldIfProvided = (
fieldName: string,
targetName: string,
transform?: (
value: unknown,
) => IDataObject | IDataObject[] | GenericValue | GenericValue[],
) => {
if (!Object.prototype.hasOwnProperty.call(updateFields, fieldName)) {
return;
}
const value = updateFields[fieldName];
if (value === undefined || value === null || value === '' || value === '__KEEP__') {
return;
}
body[targetName] = transform ? transform(value) : value;
};

addFieldIfProvided('debitorName', 'debitor_name');
addFieldIfProvided('debitorIban', 'debitor_iban');
addFieldIfProvided('mandateId', 'mandate_id');
addFieldIfProvided(
'mandateDateOfSignature',
'mandate_date_of_signature',
(value: unknown) => {
const dateValue = toDateString(value as string);
return dateValue ?? (value as string);
},
);
addFieldIfProvided('localInstrument', 'local_instrument');
addFieldIfProvided('sequenceType', 'sequence_type');
addFieldIfProvided('amount', 'amount');
addFieldIfProvided('reference', 'reference');
addFieldIfProvided('requestedAt', 'requested_at', (value: unknown) => {
const dateValue = toDateString(value as string);
return dateValue ?? (value as string);
});

Object.assign(body, mapSepaAdditionalFields(additionalFields));

const options: IHttpRequestOptions = {
headers: {
Accept: 'application/json',
},
method: 'PUT',
url: `/sepa-payments/${id}`,
json: true,
body,
};

responseData = await easyBillApiRequest.call(this, options);
returnData.push(responseData);
}
/* ╔════════════════════════╗ */
/* ║ DELETE SEPA PAYMENT ║ */
/* ╚════════════════════════╝ */
if (operation === 'deleteSepaPayment') {
const id = this.getNodeParameter('sepaPaymentId', i) as number;

const options: IHttpRequestOptions = {
headers: {
Accept: 'application/json',
},
method: 'DELETE',
url: `/sepa-payments/${id}`,
json: true,
};

responseData = await easyBillApiRequest.call(this, options);
returnData.push(responseData);
}
Expand Down
14 changes: 13 additions & 1 deletion nodes/EasyBill/GenericFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import {
const TOTAL_RETRIES = 9;
const BASE_URL = 'https://api.easybill.de/rest/v1';

type ErrorResponseBody = {
code?: number;
message?: string;
arguments?: string[];
};

export async function easyBillApiRequest<T = unknown>(
this: IExecuteFunctions | IHookFunctions | ILoadOptionsFunctions | IPollFunctions,
options: Omit<IHttpRequestOptions, 'baseURL' | 'returnFullResponse' | 'ignoreHttpStatusErrors'>,
Expand Down Expand Up @@ -58,13 +64,19 @@ export async function easyBillApiRequest<T = unknown>(
},
);
} else if (response.statusCode >= 400) {
const errorResponseBody = response.body as ErrorResponseBody;
let detailedErrorDescription = `${errorResponseBody.message ?? response.statusMessage}`;
const faultyArguments = errorResponseBody.arguments;
if (faultyArguments) {
detailedErrorDescription += ` ${faultyArguments.length === 1 ? 'Feld: ' : 'Felder: '}${faultyArguments.join(', ')}`;
}
throw new NodeApiError(
this.getNode(),
{},
{
message: response.statusMessage,
httpCode: response.statusCode.toString(),
description: response.statusMessage,
description: detailedErrorDescription,
},
);
}
Expand Down
Loading