From 51d36caccf4361acb7c68230ab559c5705f8a356 Mon Sep 17 00:00:00 2001
From: Dominik Hebeler <dominik@hebeler.club>
Date: Fri, 24 Mar 2023 17:09:46 +0100
Subject: [PATCH] added base interface to process cash payments

---
 pass/app.js                         |  6 +++
 pass/app/Exchangerates.js           | 58 +++++++++++++++++++++++++++++
 pass/bin/cron                       | 19 ++++++++++
 pass/config/default.json            | 15 +++++++-
 pass/lang/de/admin.json             | 24 ++++++++++++
 pass/public/styles/admin/base.css   |  1 +
 pass/public/styles/admin/base.less  | 28 ++++++++++++++
 pass/public/styles/admin/index.css  |  1 +
 pass/public/styles/admin/index.less | 23 ++++++++++++
 pass/routes/admin/index.js          | 16 ++++++++
 pass/routes/index.js                |  3 ++
 pass/views/admin/index.ejs          | 15 ++++++++
 pass/views/admin/payments/cash.ejs  | 32 ++++++++++++++++
 13 files changed, 239 insertions(+), 2 deletions(-)
 create mode 100644 pass/app/Exchangerates.js
 create mode 100644 pass/lang/de/admin.json
 create mode 100644 pass/public/styles/admin/base.css
 create mode 100644 pass/public/styles/admin/base.less
 create mode 100644 pass/public/styles/admin/index.css
 create mode 100644 pass/public/styles/admin/index.less
 create mode 100644 pass/routes/admin/index.js
 create mode 100644 pass/views/admin/index.ejs
 create mode 100644 pass/views/admin/payments/cash.ejs

diff --git a/pass/app.js b/pass/app.js
index 24137de..9bb15e4 100644
--- a/pass/app.js
+++ b/pass/app.js
@@ -16,6 +16,12 @@ const mglangdetector = require("./app/Langdetector");
 const langdetector = new i18nextmiddleware.LanguageDetector();
 langdetector.addDetector(mglangdetector);
 let ns = [];
+readdirSync(path.join(__dirname, 'lang', "de")).forEach((fileName) => {
+  const joinedPath = path.join(path.join(__dirname, 'lang', 'de'), fileName)
+  if (!lstatSync(joinedPath).isDirectory()) {
+    ns.push(fileName.replace(".json", ""));
+  }
+});
 
 i18next.use(langdetector).use(i18nextfsbackend).init({
   debug: false,
diff --git a/pass/app/Exchangerates.js b/pass/app/Exchangerates.js
new file mode 100644
index 0000000..1497987
--- /dev/null
+++ b/pass/app/Exchangerates.js
@@ -0,0 +1,58 @@
+const { writeFileSync, readFileSync, existsSync } = require("fs");
+const dayjs = require("dayjs");
+const config = require("config");
+
+class Exchangerates {
+    #data_path = "/data/exchangerates.json";
+    #currencies = [
+        "USD",
+        "CAD",
+        "GBP"
+    ];
+
+    #exchangerates = {
+
+    };
+
+    constructor() {
+        if (existsSync(this.#data_path)) {
+            this.#exchangerates = JSON.parse(readFileSync(this.#data_path));
+        }
+    }
+
+    async update() {
+        let currentdate = dayjs();
+        let date_formatted = currentdate.format("YYYY-MM-DD");
+        if (!(date_formatted in this.#exchangerates)) {
+            let api_url = new URL(config.get("payments.exchange_api_apilayer.host"));
+            let exchangerate = {}
+            for (let i = 0; i < this.#currencies.length; i++) {
+
+                let params = new URLSearchParams({
+                    symbols: "EUR",
+                    base: this.#currencies[i]
+                });
+                api_url.search = params.toString();
+                await fetch(api_url).then(result => result.json()).then(result => {
+                    exchangerate[this.#currencies[i]] = result.rates.EUR;
+                }).catch(reason => {
+                    console.error("reason");
+                    exchangerate = null;
+                })
+                if (exchangerate === null) {
+                    break;
+                }
+            }
+            if (exchangerate !== null) {
+                this.#exchangerates[date_formatted] = exchangerate;
+                this.save();
+            }
+        }
+    }
+
+    save() {
+        writeFileSync(this.#data_path, JSON.stringify(this.#exchangerates));
+    }
+}
+
+module.exports = Exchangerates;
\ No newline at end of file
diff --git a/pass/bin/cron b/pass/bin/cron
index 013cc31..b67da6c 100644
--- a/pass/bin/cron
+++ b/pass/bin/cron
@@ -5,6 +5,7 @@ const config = require("config");
 const path = require("path");
 const { writeFileSync } = require("fs");
 const { exec } = require("child_process");
+const Exchangerates = require("../app/Exchangerates");
 let redis_client;
 /**
  * Will call every cron script every minute.
@@ -25,6 +26,12 @@ let cronjobs = async () => {
       "YYYY-MM-DD HH:mm:ss"
     )}] Written ${written_logs} key changes.`
   );
+  await fetchExchangerates();
+  console.log(
+    `[${now.format(
+      "YYYY-MM-DD HH:mm:ss"
+    )}] Fetched exchange rates.`
+  );
   await accentSync();
   await redis_client.quit();
   console.log(`[${now.format("YYYY-MM-DD HH:mm:ss")}] Finish`);
@@ -66,6 +73,18 @@ async function accentSync() {
 
 }
 
+async function fetchExchangerates() {
+  let redis_lock_key = "cron:fetchExchangerates";
+  let interval_seconds = 60 * 15; // Will execute every 15 minutes
+  let lock = await tryToGetLock(redis_lock_key, interval_seconds);
+  if (!lock) {
+    return 0;
+  }
+
+  let exchagerates = new Exchangerates();
+  await exchagerates.update();
+}
+
 async function writeLogsToOrder() {
   let redis_lock_key = "cron:writeLogsToOrder";
   let interval_seconds = 60; // Will execute every minute
diff --git a/pass/config/default.json b/pass/config/default.json
index e9dc337..f757894 100644
--- a/pass/config/default.json
+++ b/pass/config/default.json
@@ -36,7 +36,14 @@
   "price": {
     "per_300": 10,
     "vat": 7,
-    "purchasable": [300, 600, 900, 1200, 1800, 3600]
+    "purchasable": [
+      300,
+      600,
+      900,
+      1200,
+      1800,
+      3600
+    ]
   },
   "redis": {
     "host": "express_redis",
@@ -80,6 +87,10 @@
       "services": {
         "paysafecard": {}
       }
+    },
+    "exchange_api_apilayer": {
+      "host": "https://api.apilayer.com/exchangerates_data/live",
+      "api_key": "<APILAYER_API_KEY>"
     }
   }
-}
+}
\ No newline at end of file
diff --git a/pass/lang/de/admin.json b/pass/lang/de/admin.json
new file mode 100644
index 0000000..cfed163
--- /dev/null
+++ b/pass/lang/de/admin.json
@@ -0,0 +1,24 @@
+{
+  "breadcrumps": {
+    "overview": "Ãœbersicht",
+    "payments-cash": "Barzahlung erfassen"
+  },
+  "index": {
+    "actions": {
+      "cash-payment": "Bargeldzahlung erfassen",
+      "heading": "Aktionen:"
+    },
+    "heading": "MetaGer Schlüssel Admin"
+  },
+  "cash-payment": {
+    "info": "Erfasse hier eingegangene Barzahlungen um die zugehörigen Bestellungen zu buchen",
+    "price": {
+      "placeholder": "10,00",
+      "label": "Eingegangener Betrag"
+    },
+    "orderid": {
+      "label": "Bestellnummer:",
+      "placeholder": "123456789"
+    }
+  }
+}
diff --git a/pass/public/styles/admin/base.css b/pass/public/styles/admin/base.css
new file mode 100644
index 0000000..bb4dcd4
--- /dev/null
+++ b/pass/public/styles/admin/base.css
@@ -0,0 +1 @@
+div#admin-container{max-width:980px;margin:0 auto}div#admin-container h1{border-bottom:1px solid #ef7700;text-align:center}div#admin-container h2{width:max-content}div#admin-container ul.breadcrumps{padding:0;display:flex;flex-wrap:wrap;gap:1.5rem;color:#6a6a6a}div#admin-container ul.breadcrumps>li:first-child{list-style-type:none}
\ No newline at end of file
diff --git a/pass/public/styles/admin/base.less b/pass/public/styles/admin/base.less
new file mode 100644
index 0000000..f967aba
--- /dev/null
+++ b/pass/public/styles/admin/base.less
@@ -0,0 +1,28 @@
+@import "../misc/vars.less";
+
+div#admin-container {
+  max-width: @max-content-width;
+  margin: 0 auto;
+
+  h1 {
+    border-bottom: 1px solid @color-main;
+    text-align: center;
+  }
+
+  h2 {
+    width: max-content;
+  }
+
+  ul.breadcrumps {
+    padding: 0;
+    display: flex;
+    flex-wrap: wrap;
+    gap: 1.5rem;
+    color: lighten(@font-color-on-white, 10%);
+    > li {
+      &:first-child {
+        list-style-type: none;
+      }
+    }
+  }
+}
diff --git a/pass/public/styles/admin/index.css b/pass/public/styles/admin/index.css
new file mode 100644
index 0000000..28890d1
--- /dev/null
+++ b/pass/public/styles/admin/index.css
@@ -0,0 +1 @@
+div#admin-container div#action-container{display:flex;flex-wrap:wrap;margin-top:2rem}div#admin-container div#action-container>a{display:flex;gap:.5rem;align-items:center;border:1px solid #ef7700;padding:1rem;border-radius:10px;text-decoration:none;color:inherit;cursor:pointer}div#admin-container div#action-container>a>img{width:2em}
\ No newline at end of file
diff --git a/pass/public/styles/admin/index.less b/pass/public/styles/admin/index.less
new file mode 100644
index 0000000..9c7074d
--- /dev/null
+++ b/pass/public/styles/admin/index.less
@@ -0,0 +1,23 @@
+@import "../misc/vars.less";
+
+div#admin-container {
+  div#action-container {
+    display: flex;
+    flex-wrap: wrap;
+    margin-top: 2rem;
+    > a {
+      display: flex;
+      gap: 0.5rem;
+      align-items: center;
+      border: 1px solid @color-main;
+      padding: 1rem;
+      border-radius: 10px;
+      text-decoration: none;
+      color: inherit;
+      cursor: pointer;
+      > img {
+        width: 2em;
+      }
+    }
+  }
+}
diff --git a/pass/routes/admin/index.js b/pass/routes/admin/index.js
new file mode 100644
index 0000000..d32a3fa
--- /dev/null
+++ b/pass/routes/admin/index.js
@@ -0,0 +1,16 @@
+var express = require("express");
+var router = express.Router();
+const config = require("config");
+const { auth, requiresAuth, claimCheck } = require("express-openid-connect");
+
+router.use(requiresAuth());
+
+router.get("/", (req, res) => {
+    res.render("admin/index");
+});
+
+router.get("/payments/cash", (req, res) => {
+    res.render("admin/payments/cash");
+})
+
+module.exports = router;
\ No newline at end of file
diff --git a/pass/routes/index.js b/pass/routes/index.js
index 6019f8a..a9218b6 100644
--- a/pass/routes/index.js
+++ b/pass/routes/index.js
@@ -7,6 +7,7 @@ var browserify = require("browserify-middleware");
 var paypalCheckoutRouter = require("../routes/checkout/paypal.js");
 var micropaymentCheckoutRouter = require("../routes/checkout/micropayment");
 var apiRouter = require("./api");
+var adminRouter = require("./admin/index");
 var path = require("path");
 
 const config = require("config");
@@ -19,6 +20,8 @@ router.use("/api/json", apiRouter);
 var authenticationRouter = require("../routes/authentication");
 router.use("/", authenticationRouter);
 
+router.use("/admin", adminRouter);
+
 /* GET home page. */
 router.get("/", function (req, res, next) {
   req.i18n.setDefaultNamespace("index");  // Default NS for localized Strings
diff --git a/pass/views/admin/index.ejs b/pass/views/admin/index.ejs
new file mode 100644
index 0000000..b7131bc
--- /dev/null
+++ b/pass/views/admin/index.ejs
@@ -0,0 +1,15 @@
+<%- include('../templates/page_header', {css: [`${baseDir}/styles/admin/base.css`, `${baseDir}/styles/admin/index.css`], js: []}); %>
+<div id="admin-container">
+  <h1><%= req.t("index.heading", {ns: "admin"}) _%></h1>
+  <ul class="breadcrumps">
+    <li><%= req.t("breadcrumps.overview", {ns: "admin"}) _%></li>
+  </ul>
+  <h2><%= req.t("index.actions.heading", {ns: "admin"}) _%></h2>
+  <div id="action-container">
+    <a id="action-cash-payment" href="<%= baseDir %>/admin/payments/cash">
+      <img src="<%= baseDir %>/images/money.svg" alt="Geldschein">
+      <div><%= req.t("index.actions.cash-payment", {ns: "admin"}) _%></div>
+    </a>
+  </div>
+</div>
+<%- include('../templates/page_footer'); -%>
\ No newline at end of file
diff --git a/pass/views/admin/payments/cash.ejs b/pass/views/admin/payments/cash.ejs
new file mode 100644
index 0000000..914e602
--- /dev/null
+++ b/pass/views/admin/payments/cash.ejs
@@ -0,0 +1,32 @@
+<%- include('../../templates/page_header', {css: [`${baseDir}/styles/admin/base.css`, `${baseDir}/styles/admin/payments/cash.css`], js: []}); %>
+<div id="admin-container">
+  <h1><%= req.t("index.heading", {ns: "admin"}) _%></h1>
+  <ul class="breadcrumps">
+    <li>
+      <a href="<%= baseDir _%>/admin/">
+        <%= req.t("breadcrumps.overview", {ns: "admin"}) _%>
+      </a>
+    </li>
+    <li><%= req.t("breadcrumps.payments-cash", {ns: "admin"}) _%></li>
+  </ul>
+
+  <p><%= req.t("cash-payment.info", {ns: "admin"}) _%></p>
+  <form method="POST">
+    <div class="input-group">
+      <label for="orderid"><%= req.t("cash-payment.orderid.label", {ns: "admin"}) _%></label>
+      <input type=" text" name="orderid" id="orderid" placeholder="<%= req.t("cash-payment.orderid.placeholder", {ns: "admin"}) _%>">
+    </div>
+    <div class="input-group">
+      <label for="price"><%= req.t("cash-payment.price.label", {ns: "admin"}) _%></label>
+      <input type="number" name="price" id="price" placeholder="<%= req.t("cash-payment.price.placeholder", {ns: "admin"}) _%>" step="0.01">
+      <select name="currency" id="currency">
+        <option value="EUR" selected>EUR</option>
+        <option value="USD">USD</option>
+        <option value="CAD">CAD</option>
+        <option value="GBP">GBP</option>
+      </select>
+    </div>
+  </form>
+
+</div>
+<%- include('../../templates/page_footer'); -%>
\ No newline at end of file
-- 
GitLab