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