From d5dd6dad0b885df8c140bee3fd3bd81277010713 Mon Sep 17 00:00:00 2001 From: Dominik Hebeler <dominik@hebeler.club> Date: Sat, 14 Sep 2024 14:09:48 +0200 Subject: [PATCH] automatic invoice generation --- pass/app.js | 13 +- pass/routes/orders/receipt.js | 503 +++++++++++++++++++++------------- 2 files changed, 315 insertions(+), 201 deletions(-) diff --git a/pass/app.js b/pass/app.js index 0271f51..91c7adc 100644 --- a/pass/app.js +++ b/pass/app.js @@ -106,13 +106,19 @@ app.use((req, res, next) => { if ( allowed_hosts.includes(req.hostname) || (process.env.NODE_ENV === "development" && - req.hostname.match(/^(localhost|.*\.ngrok-free\.app|.*\.review\.metager\.de)$/)) + req.hostname.match( + /^(localhost|.*\.ngrok-free\.app|.*\.review\.metager\.de)$/ + )) ) { let proto = req.get("x-forwarded-proto") ?? req.protocol; let host = req.get("x-forwarded-host") ?? req.get("host"); let port = req.get("x-forwarded-port"); - if (host.match(/^(.*\.ngrok-free\.app|metager\.de|metager\.org|metager3\.de)$/)) { + if ( + host.match( + /^(.*\.ngrok-free\.app|metager\.de|metager\.org|metager3\.de)$/ + ) + ) { proto = "https"; port = undefined; } @@ -151,8 +157,7 @@ app.use(function (err, req, res, next) { res.locals.message = err.message; res.locals.error = err; - if (err.status != 404) - console.error(err.stack); + if (err.status != 404) console.error(err.stack); // render the error page res.status(err.status || 500); diff --git a/pass/routes/orders/receipt.js b/pass/routes/orders/receipt.js index 7758d35..7ef06a7 100644 --- a/pass/routes/orders/receipt.js +++ b/pass/routes/orders/receipt.js @@ -6,7 +6,7 @@ const { body, validationResult } = require("express-validator"); const Receipt = require("../../app/Receipt"); const Zammad = require("../../app/Zammad"); const Payment = require("../../app/Payment"); -const { t } = require("i18next"); +const dayjs = require("dayjs"); router.get("/", async (req, res) => { req.data.order.invoice = { @@ -15,38 +15,53 @@ router.get("/", async (req, res) => { email: req.query.email || "", address: req.query.address || "", }, - create_invoice_url: `${res.locals.baseDir - }/key/${req.data.key.key.get_key()}/orders/${req.data.order.payment.public_id - }/invoice/create`, + create_invoice_url: `${ + res.locals.baseDir + }/key/${req.data.key.key.get_key()}/orders/${ + req.data.order.payment.public_id + }/invoice/create`, errors: {}, }; let orders = req.data.key.key.get_charge_orders(); let payment_reference_ids = []; - orders.forEach(order => { + orders.forEach((order) => { payment_reference_ids.push(order.payment_reference_id); }); + // Populate Invoice Data from older orders + req.data.order.invoice_old = {}; let receipt = await Receipt.GET_LATEST_RECEIPT(payment_reference_ids); if (receipt != null) { - await fetch(config.get("app.invoiceninja.url") + "/api/v1/invoices/" + receipt.invoice_id, { - method: "GET", - headers: { - "X-API-TOKEN": config.get("app.invoiceninja.api_token"), - "Content-Type": "application/json" - }, - }) - .then(response => response.json()) - .then(invoice => { - return fetch(config.get("app.invoiceninja.url") + "/api/v1/clients/" + invoice.data.client_id, { - method: "GET", - headers: { - "X-API-TOKEN": config.get("app.invoiceninja.api_token"), - "Content-Type": "application/json" - }, - }); - }).then(response => response.json()) - .then(client => { + await fetch( + config.get("app.invoiceninja.url") + + "/api/v1/invoices/" + + receipt.invoice_id, + { + method: "GET", + headers: { + "X-API-TOKEN": config.get("app.invoiceninja.api_token"), + "Content-Type": "application/json", + }, + } + ) + .then((response) => response.json()) + .then((invoice) => { + return fetch( + config.get("app.invoiceninja.url") + + "/api/v1/clients/" + + invoice.data.client_id, + { + method: "GET", + headers: { + "X-API-TOKEN": config.get("app.invoiceninja.api_token"), + "Content-Type": "application/json", + }, + } + ); + }) + .then((response) => response.json()) + .then((client) => { req.data.order.invoice_old = { company: client.data.name, first_name: client.data.contacts[0].first_name, @@ -55,9 +70,10 @@ router.get("/", async (req, res) => { address2: client.data.address2, zip: client.data.postal_code, city: client.data.city, - state: client.data.state - } - }).catch(reason => { }); + state: client.data.state, + }; + }) + .catch((reason) => {}); } res.render("key", req.data); @@ -104,7 +120,11 @@ router.post("/", async (req, res) => { let order_id = req.data.order.payment.public_id; let payment_reference = req.data.order.payment_reference.public_id; - let url = req.data.links.order_actions_base + "/" + req.data.order.payment.public_id + "/receipt/download"; + let url = + req.data.links.order_actions_base + + "/" + + req.data.order.payment.public_id + + "/receipt/download"; // Check if there already is a receipt if (req.data.order.payment.receipt_id != null) { @@ -115,43 +135,23 @@ router.post("/", async (req, res) => { // Check if there is a previous client let orders = req.data.key.key.get_charge_orders(); let payment_reference_ids = []; - orders.forEach(order => { + orders.forEach((order) => { payment_reference_ids.push(order.payment_reference_id); }); - let receipt = await Receipt.GET_LATEST_RECEIPT(payment_reference_ids); - let client_id = null; - if (receipt != null) { - await fetch(config.get("app.invoiceninja.url") + "/api/v1/invoices/" + receipt.invoice_id, { - method: "GET", - headers: { - "X-API-TOKEN": config.get("app.invoiceninja.api_token"), - "Content-Type": "application/json" - }, - }) - .then(response => response.json()) - .then(invoice => { - return fetch(config.get("app.invoiceninja.url") + "/api/v1/clients/" + invoice.data.client_id, { - method: "GET", - headers: { - "X-API-TOKEN": config.get("app.invoiceninja.api_token"), - "Content-Type": "application/json" - }, - }); - }).then(response => response.json()) - .then(client => { - client_id = client.data.id; - }).catch(reason => { }); - } - // Create a new Client let post_data = { - contacts: [{ - "first_name": req.data.order.invoice.params.first_name, - "last_name": req.data.order.invoice.params.last_name, - "send_email": true - }], - name: req.data.order.invoice.params.company.length > 0 ? req.data.order.invoice.params.company : null, + contacts: [ + { + first_name: req.data.order.invoice.params.first_name, + last_name: req.data.order.invoice.params.last_name, + send_email: true, + }, + ], + name: + req.data.order.invoice.params.company.length > 0 + ? req.data.order.invoice.params.company + : null, address1: req.data.order.invoice.params.address1, address2: req.data.order.invoice.params.address2, city: req.data.order.invoice.params.city, @@ -159,95 +159,178 @@ router.post("/", async (req, res) => { postal_code: req.data.order.invoice.params.zip, country_code: "DE", currency_code: "EUR", - group_settings_id: config.get("app.invoiceninja.group_id") + group_settings_id: config.get("app.invoiceninja.group_id"), }; + let receipt = await Receipt.GET_LATEST_RECEIPT(payment_reference_ids); + let client_id = null; + if (receipt != null) { + await fetch( + config.get("app.invoiceninja.url") + + "/api/v1/invoices/" + + receipt.invoice_id, + { + method: "GET", + headers: { + "X-API-TOKEN": config.get("app.invoiceninja.api_token"), + "Content-Type": "application/json", + }, + } + ) + .then((response) => response.json()) + .then((invoice) => { + return fetch( + config.get("app.invoiceninja.url") + + "/api/v1/clients/" + + invoice.data.client_id, + { + method: "GET", + headers: { + "X-API-TOKEN": config.get("app.invoiceninja.api_token"), + "Content-Type": "application/json", + }, + } + ); + }) + .then((response) => response.json()) + .then((client) => { + post_data = { ...client.data, ...post_data }; + client_id = client.data.id; + post_data.contacts[0].id = client.data.contacts[0].id; + }) + .catch((reason) => {}); + } + + if (req.lng.indexOf("de") != 0) { + post_data.settings = { + language_id: 1, + }; // Set language to en + } else { + post_data.settings = {}; + } + let create_or_update_promise = null; if (client_id != null) { // Update - create_or_update_promise = fetch(config.get("app.invoiceninja.url") + "/api/v1/clients/" + client_id, { - method: "PUT", - headers: { - "X-API-TOKEN": config.get("app.invoiceninja.api_token"), - "Content-Type": "application/json" - }, - body: JSON.stringify(post_data) - }); + create_or_update_promise = fetch( + config.get("app.invoiceninja.url") + "/api/v1/clients/" + client_id, + { + method: "PUT", + headers: { + "X-API-TOKEN": config.get("app.invoiceninja.api_token"), + "Content-Type": "application/json", + }, + body: JSON.stringify(post_data), + } + ); } else { - create_or_update_promise = fetch(config.get("app.invoiceninja.url") + "/api/v1/clients", { - method: "POST", - headers: { - "X-API-TOKEN": config.get("app.invoiceninja.api_token"), - "Content-Type": "application/json" - }, - body: JSON.stringify(post_data) - }); - } - - - - return create_or_update_promise.then(response => response.json()).then(response => { - let client = response.data; - let assigned_user_id = client.contacts[0].id; - return fetch(config.get("app.invoiceninja.url") + "/api/v1/clients/" + client.id, { - method: "PUT", - headers: { - "X-API-TOKEN": config.get("app.invoiceninja.api_token"), - "Content-Type": "application/json" - }, - body: JSON.stringify({ - assigned_user_id: assigned_user_id, - contacts: client.contacts, - }) - }); - }).then(response => response.json()).then(response => { - let client = response.data; - Payment.LOAD_FROM_PUBLIC_ID(req.data.order.payment.public_id).then(payment => { - // Create the invoice - let actions = new URLSearchParams({ - send_email: "false", - mark_sent: "true", - amount_paid: payment.converted_price - }); - return fetch(config.get("app.invoiceninja.url") + "/api/v1/invoices?" + actions.toString(), { + create_or_update_promise = fetch( + config.get("app.invoiceninja.url") + "/api/v1/clients", + { method: "POST", headers: { "X-API-TOKEN": config.get("app.invoiceninja.api_token"), - "Content-Type": "application/json" + "Content-Type": "application/json", }, - body: JSON.stringify({ - client_id: client.id, - po_number: req.data.order.payment.public_id, - line_items: [{ - quantity: 1, - cost: req.data.order.payment_reference.price, - notes: t("subject", { - ns: "invoice", - amount: Math.round( - (payment.converted_price / req.data.order.payment_reference.price) * req.data.order.payment_reference.amount - ), - payment_reference: req.data.order.payment_reference.public_id - }), - public_notes: t("payment-received", { ns: "invoice" }), - tax_name1: "MwSt.", - tax_rate1: 7 - }] - }) - }) - .then(response => response.json()) - .then(invoice => { - // Return the new invoice as pdf - return Receipt.CREATE_NEW_RECEIPT({ - invoice_id: invoice.data.id, - payment_id: req.data.order.payment.id + body: JSON.stringify(post_data), + } + ); + } + + return create_or_update_promise + .then((response) => response.json()) + .then((response) => { + let client = response.data; + client.assigned_user_id = client.contacts[0].id; + if (client_id == null) { + return fetch( + config.get("app.invoiceninja.url") + "/api/v1/clients/" + client.id, + { + method: "PUT", + headers: { + "X-API-TOKEN": config.get("app.invoiceninja.api_token"), + "Content-Type": "application/json", + }, + body: JSON.stringify(client), + } + ) + .then((response) => response.json()) + .then((response) => response.data); + } else { + return client; + } + }) + .then((client) => { + Payment.LOAD_FROM_PUBLIC_ID(req.data.order.payment.public_id).then( + (payment) => { + // Create the invoice + let actions = new URLSearchParams({ + send_email: "false", + mark_sent: "true", + amount_paid: payment.converted_price, }); - }).then(receipt => { - payment.setReceipt(receipt.id) - return res.redirect(req.data.links.order_actions_base + "/" + receipt.payment_id + "/receipt/download") - }); + return fetch( + config.get("app.invoiceninja.url") + + "/api/v1/invoices?" + + actions.toString(), + { + method: "POST", + headers: { + "X-API-TOKEN": config.get("app.invoiceninja.api_token"), + "Content-Type": "application/json", + }, + body: JSON.stringify({ + client_id: client.id, + po_number: req.data.order.payment.public_id, + due_date: dayjs().format("YYYY-MM-DD"), + line_items: [ + { + quantity: 1, + cost: req.data.order.payment_reference.price, + notes: req.t("subject", { + ns: "invoice", + amount: Math.round( + (payment.converted_price / + req.data.order.payment_reference.price) * + req.data.order.payment_reference.amount + ), + payment_reference: + req.data.order.payment_reference.public_id, + }), + public_notes: req.t("payment-received", { + ns: "invoice", + }), + tax_name1: "MwSt.", + tax_rate1: 7, + }, + ], + }), + } + ) + .then((response) => response.json()) + .then((invoice) => { + // Return the new invoice as pdf + return Receipt.CREATE_NEW_RECEIPT({ + invoice_id: invoice.data.id, + payment_id: req.data.order.payment.id, + }); + }) + .then((receipt) => { + payment.setReceipt(receipt.id); + return res.redirect(url); + }); + } + ); }) - - }) + .catch((reason) => { + console.error(reason); + res.redirect( + req.data.links.order_actions_base + + "/" + + req.data.order.payment.public_id + + "/receipt" + ); + }); let moderation_params = { order: req.data.order.payment.public_id, @@ -258,13 +341,12 @@ router.post("/", async (req, res) => { address: req.data.order.invoice.params.address, }; - - req.data.order.invoice.params = moderation_params; - req.data.order.invoice.moderation_url = `${res.locals.baseDir - }/admin/payments/receipt?${new URLSearchParams( - moderation_params - ).toString()}`; + req.data.order.invoice.moderation_url = `${ + res.locals.baseDir + }/admin/payments/receipt?${new URLSearchParams( + moderation_params + ).toString()}`; // Render the message let ejs = require("ejs"), @@ -276,23 +358,30 @@ router.post("/", async (req, res) => { let message = ejs.render(template, req.data); - return Zammad.CREATE_RECEIPT_TICKET(moderation_params.name, moderation_params.email, message, req.data.order.payment.public_id).then(success => { - if (success) { - req.data.order.invoice.success = true; - res.render("key", req.data); - } else { - console.error(response.body); - throw "Fehler beim Erstellen der Benachrichtigung,"; - } - }).then((response) => { - if (response.status != 201) { - console.error(response.body); - throw "Fehler beim Erstellen der Benachrichtigung,"; - } else { - req.data.order.invoice.success = true; - res.render("key", req.data); - } - }) + return Zammad.CREATE_RECEIPT_TICKET( + moderation_params.name, + moderation_params.email, + message, + req.data.order.payment.public_id + ) + .then((success) => { + if (success) { + req.data.order.invoice.success = true; + res.render("key", req.data); + } else { + console.error(response.body); + throw "Fehler beim Erstellen der Benachrichtigung,"; + } + }) + .then((response) => { + if (response.status != 201) { + console.error(response.body); + throw "Fehler beim Erstellen der Benachrichtigung,"; + } else { + req.data.order.invoice.success = true; + res.render("key", req.data); + } + }) .catch((reason) => { console.log(reason); req.data.order.invoice.errors["send_email"] = @@ -307,50 +396,70 @@ router.get("/download", (req, res) => { res.locals.message = "Receipt not found"; res.status(404).render("error"); } - return Receipt.LOAD_RECEIPT_FROM_INTERNAL_ID(req.data.order.payment.receipt_id).then(receipt => { - - // Receipt can either be in the database itself, or via invoiceninja - if (receipt.invoice_id != null) { - // Load invoice - let invoice_number = null; - return fetch(config.get("app.invoiceninja.url") + "/api/v1/invoices/" + receipt.invoice_id, { - method: "GET", - headers: { - "X-API-TOKEN": config.get("app.invoiceninja.api_token"), - "Content-Type": "application/json" - }, - }) - .then(response => response.json()) - .then(invoice => { - invoice_number = invoice.data.number; - return fetch(config.get("app.invoiceninja.url") + "/api/v1/invoice/" + invoice.data.invitations[0].key + "/download", { + return Receipt.LOAD_RECEIPT_FROM_INTERNAL_ID( + req.data.order.payment.receipt_id + ) + .then((receipt) => { + // Receipt can either be in the database itself, or via invoiceninja + if (receipt.invoice_id != null) { + // Load invoice + let invoice_number = null; + return fetch( + config.get("app.invoiceninja.url") + + "/api/v1/invoices/" + + receipt.invoice_id, + { method: "GET", headers: { "X-API-TOKEN": config.get("app.invoiceninja.api_token"), - "Content-Type": "application/json" + "Content-Type": "application/json", }, + } + ) + .then((response) => response.json()) + .then((invoice) => { + invoice_number = invoice.data.number; + return fetch( + config.get("app.invoiceninja.url") + + "/api/v1/invoice/" + + invoice.data.invitations[0].key + + "/download", + { + method: "GET", + headers: { + "X-API-TOKEN": config.get("app.invoiceninja.api_token"), + "Content-Type": "application/json", + }, + } + ); }) - }) - .then(response => response.blob()) - .then(blob => { - res.setHeader("Content-Length", blob.size); - res.setHeader("Content-Type", blob.type); - res.setHeader("Content-Disposition", `attachment; filename=receipt-metager-${invoice_number}.pdf`); - return blob.arrayBuffer().then(buf => { - res.send(Buffer.from(buf)); + .then((response) => response.blob()) + .then((blob) => { + res.setHeader("Content-Length", blob.size); + res.setHeader("Content-Type", blob.type); + res.setHeader( + "Content-Disposition", + `attachment; filename=receipt-metager-${invoice_number}.pdf` + ); + return blob.arrayBuffer().then((buf) => { + res.send(Buffer.from(buf)); + }); + }); + } else { + res + .type("pdf") + .header({ + "Content-Disposition": `inline; filename=${receipt.public_id}.pdf`, }) - }); - } else { - res.type("pdf").header({ - "Content-Disposition": `inline; filename=${receipt.public_id}.pdf` - }).send(Buffer.from(receipt.receipt.toString(), "base64")); - } - }).catch(reason => { - console.error(reason); - res.locals.error = { status: 404 }; - res.locals.message = "Receipt not found"; - res.status(404).render("error"); - }); + .send(Buffer.from(receipt.receipt.toString(), "base64")); + } + }) + .catch((reason) => { + console.error(reason); + res.locals.error = { status: 404 }; + res.locals.message = "Receipt not found"; + res.status(404).render("error"); + }); }); module.exports = router; -- GitLab