diff --git a/pass/app/Order.js b/pass/app/Order.js index 4b5e09d33315e0b5705ee3009a12884c0db7b1b4..67351cb9c4cc22501d86521b14ba5d363a91ad69 100644 --- a/pass/app/Order.js +++ b/pass/app/Order.js @@ -382,6 +382,47 @@ class Order { }); } + async attachReceipt(blob) { + let civicrm_data = config.get("app.civicrm"); + if (!civicrm_data.enabled) { + return; + } + console.log(this.#order_date.format("YYYY-MM-DD HH:mm:ss")); + return fetch(civicrm_data.url + "/Attachment/create", { + method: "post", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "X-Civi-Auth": `Bearer ${civicrm_data.api_key}`, + }, + body: + "params=" + + encodeURIComponent( + JSON.stringify({ + values: { + name: `INV_${this.getOrderID()}.pdf`, + mime_type: "application/pdf", + content: blob, + entity_table: "civicrm_contribution", + entity_id: this.#civicrm_contribution_id, + }, + }) + ), + }) + .then((response) => { + console.log(response.status); + return response.json(); + if (response.status !== 200) { + throw "Error Creating Civicrm Contribution"; + } else { + return response.json(); + } + }) + .then((response_json) => { + console.log(JSON.stringify(response_json)); + //this.#civicrm_contribution_id = response_json.values[0].id; + }); + } + async updateOrderLinkTTL() { let redis_key = Order.PURCHASE_LINK_ORDER_TO_KEY_PREFIX + this.#order_id; diff --git a/pass/app/pdf/OrderReceipt.js b/pass/app/pdf/OrderReceipt.js index 2a5534f1b4780c7d934e6893f40e796afcb5d329..aa8c6b42971d98e471c2d026a55b546f65207b05 100644 --- a/pass/app/pdf/OrderReceipt.js +++ b/pass/app/pdf/OrderReceipt.js @@ -6,15 +6,19 @@ class OrderReceipt { * * @param {Order} order */ - static CREATE_ORDER_RECEIPT(order, target) { + static CREATE_ORDER_RECEIPT(order, invoice) { let letter_left_margin = OrderReceipt.CM_TO_POINTS(2.5, false); let letter_right_margin = OrderReceipt.CM_TO_POINTS(2, false); let PDFDocument = require("pdfkit"); + let title = `Bestellung ${order.getOrderID()}`; + if (invoice) { + title = `Rechnung INV_${order.getOrderID()}`; + } const doc = new PDFDocument({ size: "A4", info: { - Title: `Bestellung ${order.getOrderID()}`, + Title: title, Author: "SUMA-EV - Verein für freien Wissenszugang", Subject: "MetaGer Schlüssel: Suchanfragen (x300)", }, @@ -37,23 +41,33 @@ class OrderReceipt { .stroke(); // Our Address Information - /* - doc - .fontSize(8) - .text( - "SUMA-EV | Röselerstraße 3 | 30159 Hannover | Deutschland", - OrderReceipt.CM_TO_POINTS(2, false), - OrderReceipt.CM_TO_POINTS(4.5, true), - { - width: OrderReceipt.CM_TO_POINTS(8.5, false), - } - ); -*/ + if (invoice) { + console.log(invoice); + doc + .fontSize(8) + .text( + "SUMA-EV | Röselerstraße 3 | 30159 Hannover | Deutschland", + OrderReceipt.CM_TO_POINTS(2, false), + OrderReceipt.CM_TO_POINTS(4.5, true), + { + width: OrderReceipt.CM_TO_POINTS(8.5, false), + } + ) + .moveDown() + .fontSize(10) + .text(invoice.name); + invoice.address.split("\r\n").forEach((line) => { + doc.text(line); + }); + } + // General Information + let text = invoice ? "Rechnung:" : "Bestellnummer:"; + let number = invoice ? "INV_" + order.getOrderID() : order.getOrderID(); doc .fontSize(12) .text( - "Bestellnummer: ", + text, OrderReceipt.CM_TO_POINTS(21 - 8.5, false), OrderReceipt.CM_TO_POINTS(5, true), { @@ -63,7 +77,7 @@ class OrderReceipt { } ) .font("public/fonts/liberation-sans/LiberationSans-Bold.ttf") - .text(order.getOrderID(), { align: "right" }) + .text(number, { align: "right" }) .font("public/fonts/liberation-sans/LiberationSans-Regular.ttf") .text("Telefon:", { width: OrderReceipt.CM_TO_POINTS(7.5, false), @@ -104,11 +118,13 @@ class OrderReceipt { ) .strokeColor("#ff7f00") .stroke(); + + text = invoice ? "Rechnung" : "Auftragsbestätigung"; doc .fontSize(12) .font("public/fonts/liberation-sans/LiberationSans-Bold.ttf") .text( - "Auftragsbestätigung " + order.getOrderID(), + text + " " + number, OrderReceipt.CM_TO_POINTS(2.5, false), OrderReceipt.CM_TO_POINTS(9.846, true) ); @@ -189,6 +205,19 @@ class OrderReceipt { align: "right", }) .font("public/fonts/liberation-sans/LiberationSans-Regular.ttf"); + if (invoice) { + doc + .font("public/fonts/liberation-sans/LiberationSans-Bold.ttf") + .text( + "Rechnungsbetrag dankend erhalten!", + OrderReceipt.CM_TO_POINTS(2.5, false), + OrderReceipt.CM_TO_POINTS(18, true), + { + align: "center", + } + ) + .font("public/fonts/liberation-sans/LiberationSans-Regular.ttf"); + } // Footer doc diff --git a/pass/routes/authentication.js b/pass/routes/authentication.js index 5f5d3b07a9a6647a9ab670645f798bd8d9cfd68b..c8c016da62aa82d342c68a832a10d4380d34586d 100644 --- a/pass/routes/authentication.js +++ b/pass/routes/authentication.js @@ -12,7 +12,7 @@ const session_prefix = "auth:"; * * The authenticated user needs to be member of the group defined in the config * - * You can call any url with the parameter moderate=true to trigger a login with + * You can call any url with the parameter moderation=true to trigger a login with * redirect to the same URL after successfull authentication or 401 error on * failed authentication. * @@ -34,7 +34,7 @@ router.use( }, }), (req, res, next) => { - if (req.query.moderate) { + if (req.query.moderation) { requiresAuth()(req, res, next); } else { next("route"); @@ -57,13 +57,17 @@ router.use( } } - if (req.query.moderate) { + if (req.query.moderation) { if (!req.data.admin) { res.locals.error = { status: 401 }; res.locals.message = "Unauthorized"; res.status(401).render("error"); } else { - let redirect_url = req.originalUrl.replace(/moderate=true/, ""); + let params = req.query; + delete params.moderation; + let redirect_url = `${req.path}?${new URLSearchParams( + params + ).toString()}`; res.redirect(redirect_url); } } else { diff --git a/pass/routes/index.js b/pass/routes/index.js index 4312648e2caf2d4797819c85905920e335f818cd..0c6cec15d7211111611fa4ec21ea64960be481e2 100644 --- a/pass/routes/index.js +++ b/pass/routes/index.js @@ -13,7 +13,9 @@ router.get("/", function (req, res, next) { }); router.get("/login", (req, res) => { - let gitlab_url = `https://gitlab.example.com/oauth/authorize?client_id=${config.get("app.gitlab.client_id")}&redirect_uri=REDIRECT_URI&response_type=code&state=STATE&scope=REQUESTED_SCOPES` + let gitlab_url = `https://gitlab.example.com/oauth/authorize?client_id=${config.get( + "app.gitlab.client_id" + )}&redirect_uri=REDIRECT_URI&response_type=code&state=STATE&scope=REQUESTED_SCOPES`; }); module.exports = router; diff --git a/pass/routes/key.js b/pass/routes/key.js index 73f27be0c7b8163865ba5f96802a0e716d26c4ed..281423d0e039c91d088878341e709402f39862bc 100644 --- a/pass/routes/key.js +++ b/pass/routes/key.js @@ -28,7 +28,7 @@ router.use("/:key", param("key").isUUID(4), async (req, res, next) => { let qr_data_uri = await QRCode.toDataURL(metager_url); - req.data = { + req.data = Object.assign(req.data, { created_new: req.query.new === "true" ? true : false, key: { key: req.params.key, @@ -38,7 +38,7 @@ router.use("/:key", param("key").isUUID(4), async (req, res, next) => { }, js: [], css: ["/styles/key/key.css"], - }; + }); next("route"); }); diff --git a/pass/routes/orders.js b/pass/routes/orders.js index b13b65e04d58951528c9ea6441a8ada266c9c4aa..83a85d514bec84db92ca504fd5efd5b4bbe08ce4 100644 --- a/pass/routes/orders.js +++ b/pass/routes/orders.js @@ -42,7 +42,7 @@ router.get("/:order_id", (req, res) => { }); router.get("/:order_id/pdf", (req, res) => { - let doc = OrderReceipt.CREATE_ORDER_RECEIPT(req.data.order.order, res); + let doc = OrderReceipt.CREATE_ORDER_RECEIPT(req.data.order.order); res.status(200).header({ "Content-Type": "application/pdf", }); @@ -52,21 +52,24 @@ router.get("/:order_id/pdf", (req, res) => { router.get("/:order_id/invoice", (req, res) => { req.data.order.invoice = { params: { - name: "", - email: "", - address: "", + name: req.query.name || "", + email: req.query.email || "", + address: req.query.address || "", }, + create_invoice_url: `/key/${ + req.data.key.key + }/orders/${req.data.order.order.getOrderID()}/invoice/create`, errors: {}, }; res.render("key", req.data); }); router.post( - "/:order_id/invoice", + "/:order_id/invoice*", body("email").isEmail({ domain_specific_validation: true }), body("address").isLength({ max: 1000 }), body("name").isLength({ max: 500 }), - (req, res) => { + (req, res, next) => { req.data.order.invoice = { params: { name: req.body.name || "", @@ -84,9 +87,52 @@ router.post( res.render("key", req.data); return; } + next(); + } +); + +router.post( + "/:order_id/invoice", + (req, res, next) => { + // Check if admin parameter is set and if so if the user is authenticated + if (req.body.admin) { + if (req.data.admin) { + next(); + } else { + res.locals.error = { status: 401 }; + res.locals.message = "Unauthorized"; + res.status(401).render("error"); + } + } else { + next(); + } + }, + (req, res) => { + let moderation_params = { + moderation: true, + name: req.data.order.invoice.params.name, + email: req.data.order.invoice.params.email, + address: req.data.order.invoice.params.address, + }; + + req.data.order.invoice.moderation_url = `${config.get("app.url")}/key/${ + req.data.key.key + }/orders/${req.data.order.order.getOrderID()}/invoice?${new URLSearchParams( + moderation_params + ).toString()}#invoice-form`; + + // Render the message + let ejs = require("ejs"), + fs = require("fs"), + template = fs.readFileSync( + `${__dirname}/../views/orders/invoice_message.ejs`, + "utf-8" + ); + + let message = ejs.render(template, req.data); // No validation errors. Try to create a new Ticket - return fetch(config.get("app.osticket.url"), { + return fetch(`${config.get("app.osticket.url")}/api/tickets.json`, { method: "post", headers: { "X-API-Key": config.get("app.osticket.api_key"), @@ -98,13 +144,14 @@ router.post( source: "API", name: req.data.order.invoice.params.name, email: req.data.order.invoice.params.email, - subject: "MetaGer Schlüssel: Rechnung", - message: req.data.order.invoice.params.address, + subject: `MetaGer Schlüssel: Rechnung (${req.data.order.order.getOrderID()})`, + message: `data:text/html;charset=utf-8,${message}`, topicId: 12, // ToDo change topic for english autoresponder }), }) .then((response) => { - if (response.status != 200) { + if (response.status != 201) { + console.error(response.body); throw "Fehler beim Erstellen der Benachrichtigung,"; } else { req.data.order.invoice.success = true; @@ -112,6 +159,7 @@ router.post( } }) .catch((reason) => { + console.log(reason); req.data.order.invoice.errors["send_email"] = "Fehler beim Erstellen der Benachrichtigung,"; res.render("key", req.data); @@ -120,4 +168,33 @@ router.post( } ); +router.post( + "/:order_id/invoice/create", + (req, res, next) => { + // Check if admin parameter is set and if so if the user is authenticated + if (req.data.admin) { + next(); + } else { + res.locals.error = { status: 401 }; + res.locals.message = "Unauthorized"; + res.status(401).render("error"); + } + }, + (req, res) => { + let doc = OrderReceipt.CREATE_ORDER_RECEIPT( + req.data.order.order, + req.data.order.invoice.params + ); + res.header({ + "Content-Type": "application/pdf", + }); + let blob_stream = require("blob-stream"); + let stream = doc.pipe(blob_stream()); + stream.on("finish", () => { + const blob = stream.toBlob("application/pdf"); + req.data.order.order.attachReceipt(blob); + }); + } +); + module.exports = router; diff --git a/pass/views/orders/invoice_message.ejs b/pass/views/orders/invoice_message.ejs new file mode 100644 index 0000000000000000000000000000000000000000..be654f0329ebbe4c6b639d0a03b700f15f91b685 --- /dev/null +++ b/pass/views/orders/invoice_message.ejs @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Document</title> +</head> +<body> + <p>Eine Rechnung für die Bestellung mit der ID <span style="font-weight: bold"><%= order.order.getOrderID() %></span> wurde angefragt. Folgende Rechnungsdaten wurden übermittelt:</p> + <div style="display: grid; grid-template-columns: auto 1fr; gap: 1rem; align-items: center; justify-content: center;"> + <label for="name">Name</label> + <input type="text" name="name" id="name" value="<%= order.invoice.params.name %>" disabled> + <label for="email">E-Mail</label> + <input type="email" name="email" id="email" value="<%= order.invoice.params.email %>" disabled> + <label for="address" <%_ if(order.invoice.errors["address"]) { _%>class="error"<%_ } _%>>Anschrift</label> + <textarea name="address" id="address" cols="30" rows="3" placeholder="Mustergasse 3 30159 Musterstadt Deutschland" disabled><%= order.invoice.params["address"] %></textarea> + <a href="<%= order.invoice.moderation_url %>" target="_blank" style="display: block;text-decoration: none;border: 1px solid rgb(168, 84, 0);border-radius: 5px;padding: .2rem .5rem;background-color: rgb(185, 92, 0);color: white!important;font-weight: bold;grid-column: span 2;text-align: center;max-width: 15em;justify-self: center;">Rechnung erstellen</a> + </div> + <p style="margin-top: 1rem">Bitte Rechnungsdaten überprüfen und die Rechnung erstellen. Anschließend die erstellte Rechnung als Antwort auf dieses Ticket an den Nutzer schicken.</p> +</body> +</html> \ No newline at end of file diff --git a/pass/views/orders/order.ejs b/pass/views/orders/order.ejs index 034f58b91136efac97f466a4455e2f1dc76bd86a..8fccdd413c4139619aa24ea668117bed9f44ba4b 100644 --- a/pass/views/orders/order.ejs +++ b/pass/views/orders/order.ejs @@ -31,13 +31,14 @@ </a> </div> <%_ } else { _%> - <form method="POST" id="invoice-form"> + <form <% if(admin) { %>action="<%= order.invoice.create_invoice_url %>"<%_ } _%> method="POST" id="invoice-form"> <h2>Rechnung</h2> <ul class="breadcrumbs"> <li><a href="<%= order.orders_url %>">Bestellungen</a></li> <li><a href="<%= order.order_url %>"><%= order.order.getOrderID() %></a></li> <li>Rechnung</li> </ul> + <%_ if(!order.invoice.success) { _%> <p> Wenn Sie eine Rechnung benötigen, tragen Sie bitte Ihre Rechnungsdaten in das nachfolgende Formular ein. Wir benötigen von Ihnen dafür Ihren vollständigen Namen, Ihre postalische Anschrift und Ihre E-Mail Adresse um Ihnen die Rechnung zuzustellen. </p> @@ -52,18 +53,31 @@ </div> <div class="invoice-form-field"> <label for="address" <%_ if(order.invoice.errors["address"]) { _%>class="error"<%_ } _%>>Anschrift*</label> - <textarea name="address" id="address" cols="30" rows="4" placeholder="Mustergasse 3 30159 Musterstadt Deutschland" required><%= order.invoice.params["name-and-address"] %></textarea> + <textarea name="address" id="address" cols="30" rows="4" placeholder="Mustergasse 3 30159 Musterstadt Deutschland" required><%= order.invoice.params["address"] %></textarea> </div> + <% if (admin) { %> + <input type="hidden" name="admin" value="true"> + <div class="invoice-form-field"> + <button type="submit" class="button"> + <img src="/images/invoice.svg" alt="" /> + <span>Rechnung erstellen</span> + </button> + </div> + <%_ } else { _%> <div class="invoice-form-field"> <button type="submit" class="button"> <img src="/images/invoice.svg" alt="" /> <span>Rechnung anfragen</span> </button> </div> + <% } %> </div> <p class="storage-time"> Wir sind rechtlich dazu verpflichtet einmal ausgestellte Rechnungen <span class="bold">10 Jahre</span> lang aufzubewahren. Da eine Rechnung auf Sie persönlich ausgestellt sein muss, enthält sie zwangsläufig personenbeziehbare Daten (Name, Anschrift, E-Mail). </p> + <%_ } else { _%> + <p>Ihre Nachricht wurde uns zugestellt. Wir bearbeiten die Anfrage so schnell wie möglich und antworten an die hinterlegte E-Mail Adresse.</p> + <%_ } _%> </form> <%_ } _%> </div> \ No newline at end of file