From e1557ac595db652a23c449734dd265f0f47be0bb Mon Sep 17 00:00:00 2001
From: Dominik Hebeler <dominik@suma-ev.de>
Date: Mon, 12 Dec 2022 17:07:53 +0100
Subject: [PATCH] included Paypal webhooks

---
 pass/app.js                    |   1 +
 pass/app/Key.js                |  23 ++++
 pass/app/Order.js              | 157 ++++++++++++++++++++-------
 pass/package-lock.json         |  90 ----------------
 pass/package.json              |   4 +-
 pass/routes/checkout/paypal.js | 189 ++++++++++++++++++++++++---------
 6 files changed, 281 insertions(+), 183 deletions(-)

diff --git a/pass/app.js b/pass/app.js
index dc812c4..488e1e8 100644
--- a/pass/app.js
+++ b/pass/app.js
@@ -46,6 +46,7 @@ app.get(
   browserify(path.join(__dirname, "resources", "js", "checkout_paypal.js"))
 );
 
+app.use("/webhooks/paypal", paypalCheckoutRouter);
 app.use("/js/paypal", paypalCheckoutRouter);
 // catch 404 and forward to error handler
 app.use(function (req, res, next) {
diff --git a/pass/app/Key.js b/pass/app/Key.js
index 82dd1ae..153ed8a 100644
--- a/pass/app/Key.js
+++ b/pass/app/Key.js
@@ -75,6 +75,29 @@ class Key {
       .expireat(Key.DATABASE_PREFIX + key, expiration.unix())
       .exec();
   }
+
+  static async DISCHARGE_KEY(key, amount) {
+    let redis_client = Key.REDIS_CLIENT;
+    let expiration = require("dayjs")().add(
+      Key.EXPIRATION_AFTER_CHARGE_DAYS,
+      "day"
+    );
+    // Check if key exists and is eligable for recharge
+    redis_client.get(Key.DATABASE_PREFIX + key).then((current_amount) => {
+      if (current_amount && current_amount > 0) {
+        if (current_amount > amount) {
+          return redis_client.set(
+            Key.DATABASE_PREFIX + key,
+            current_amount - amount
+          );
+        } else {
+          return redis_client.del(Key.DATABASE_PREFIX + key);
+        }
+      } else {
+        throw "Key Does not exist or is not charged";
+      }
+    });
+  }
 }
 
 module.exports = Key;
diff --git a/pass/app/Order.js b/pass/app/Order.js
index 2a3a26d..e31f6b8 100644
--- a/pass/app/Order.js
+++ b/pass/app/Order.js
@@ -5,7 +5,7 @@ const path = require("path");
 
 class Order {
   static get STORAGE_MUTEX_KEY_PREFIX() {
-    return "order_mutex";
+    return "order_mutex_";
   }
 
   static get STORAGE_KEY_PREFIX() {
@@ -17,9 +17,13 @@ class Order {
   }
 
   // How long is a link between order and key stored
-  static get PURCHASE_LINK_TIME_HOURS() {
+  static get PURCHASE_LINK_TIME_DAYS() {
     return 6;
   }
+  static get PURCHASE_LINK_KEY_PREFIX() {
+    return "order_link_";
+  }
+
   // How many minutes is a user allowed to take for finishing the payment
   static get PURCHASE_STORAGE_TIME_UNCOMPLETED_HOURS() {
     return 6;
@@ -53,10 +57,9 @@ class Order {
    */
   #order_date;
   #order_path;
-  #create_mode;
   #redis_client;
 
-  constructor(order_id, amount, price) {
+  constructor(order_id, amount, price, payment_method_link, payment_completed) {
     this.#order_id = order_id;
     this.#expires_at = dayjs().add(6, "month");
     this.#order_date = dayjs.unix(this.#order_id.substr(0, 10));
@@ -66,8 +69,13 @@ class Order {
     this.#amount = parseInt(amount);
     this.#price = parseInt(price);
 
-    this.#payment_completed = false;
-    this.#create_mode = true;
+    if (payment_method_link) {
+      this.#payment_method_link = payment_method_link;
+    }
+
+    if (payment_completed) {
+      this.#payment_completed = payment_completed;
+    }
 
     let Redis = require("ioredis");
     this.#redis_client = new Redis({
@@ -91,16 +99,40 @@ class Order {
     return this.#payment_method_link;
   }
 
-  setPaymentCompleted(payment_completed) {
-    this.#payment_completed = payment_completed;
+  async setPaymentCompleted(completed) {
+    return this.getWriteLock()
+      .then(() => {
+        this.#payment_completed = completed;
+        return this.save();
+      })
+      .then(() => this.releaseWriteLock());
   }
 
   isPaymentComplete() {
     return this.#payment_completed;
   }
 
-  setPaymentMethodLink(payment_method_link) {
-    this.#payment_method_link = payment_method_link;
+  async setPaymentMethodLink(payment_method_link) {
+    return this.getWriteLock()
+      .then(() => {
+        this.#payment_method_link = payment_method_link;
+        return this.save();
+      })
+      .then(() => this.releaseWriteLock());
+  }
+
+  static async CREATE_NEW_ORDER(amount, price, key) {
+    return Order.GENERATE_UNIQUE_ORDER_ID().then(async (order_id) => {
+      let new_order = new Order(order_id, amount, price);
+      return new_order
+        .getWriteLock()
+        .then(() => new_order.createOrderLink())
+        .then(() => new_order.save())
+        .then(() => new_order.releaseWriteLock())
+        .then(() => {
+          return new_order;
+        });
+    });
   }
 
   static async LOAD_ORDER_FROM_ID(order_id) {
@@ -110,17 +142,7 @@ class Order {
     });
     return new Promise((resolve, reject) => {
       redis_client
-        .setnx(Order.STORAGE_MUTEX_KEY_PREFIX + order_id, 1)
-        .then((mutex) => {
-          console.log(mutex);
-          if (mutex !== 1) {
-            // Could not acquire lock. Try again
-            throw "LOCK_NOT_ACQUIRED";
-          } else {
-            return redis_client.expire(Order.STORAGE_MUTEX_KEY_PREFIX, 15);
-          }
-        })
-        .then(() => redis_client.hgetall(Order.STORAGE_KEY_PREFIX + order_id))
+        .hgetall(Order.STORAGE_KEY_PREFIX + order_id)
         .then((order_data) => {
           if (Object.keys(order_data).length === 0) {
             // Checking FS for order
@@ -136,29 +158,17 @@ class Order {
               throw "Could not find Order in our database! Checking FS";
             }
           }
-          console.log(order_data);
           let loaded_order = new Order(
             order_data.order_id,
             order_data.amount,
-            order_data.price
+            order_data.price,
+            JSON.parse(order_data.payment_method_link),
+            order_data.payment_completed ? true : false
           );
-          if (order_data.payment_method_link) {
-            loaded_order.setPaymentMethodLink(
-              JSON.parse(order_data.payment_method_link)
-            );
-          }
-          if (order_data.payment_completed) {
-            loaded_order.setPaymentCompleted(true);
-          }
           resolve(loaded_order);
         })
         .catch((reason) => {
-          if (reason === "LOCK_NOT_ACQUIRED") {
-            console.log("lock not acquired");
-            setTimeout(Order.LOAD_ORDER_FROM_ID(order_id), 5000);
-          } else {
-            reject(reason);
-          }
+          reject(reason);
         });
     });
   }
@@ -188,13 +198,11 @@ class Order {
         fs.mkdirSync(path.dirname(order_file), { recursive: true });
       }
       let redis_client = this.#redis_client;
-      let mutex_key = Order.STORAGE_MUTEX_KEY_PREFIX + this.#order_id;
       return this.#redis_client
         .del(redis_key)
         .then(() =>
           fs.writeFileSync(order_file, JSON.stringify(stored_data, null, 4))
-        )
-        .then(() => redis_client.del(mutex_key));
+        );
     } else {
       // Store Order in Redis
       let expiration = new dayjs();
@@ -206,7 +214,6 @@ class Order {
         .pipeline()
         .hmset(redis_key, stored_data)
         .expireat(redis_key, expiration.unix())
-        .del(Order.STORAGE_MUTEX_KEY_PREFIX + this.#order_id)
         .exec();
     }
   }
@@ -227,6 +234,74 @@ class Order {
       });
   }
 
+  async getWriteLock() {
+    let write_lock_key = Order.STORAGE_MUTEX_KEY_PREFIX + this.#order_id;
+
+    return new Promise(async (resolve, reject) => {
+      let timestart = dayjs();
+      let write_lock = 0;
+      do {
+        write_lock = await this.#redis_client
+          .pipeline()
+          .setnx(write_lock_key, 1)
+          .expiretime(write_lock_key)
+          .expire(write_lock_key, 15)
+          .exec();
+        let expire_at = write_lock[1][1];
+        write_lock = write_lock[0][1];
+        if (write_lock === 1) {
+          resolve(true);
+          break;
+        } else if (dayjs().diff(timestart, "second") >= 15) {
+          if (expire_at >= 0) {
+            await this.#redis_client.expireat(write_lock_key, expire_at);
+          }
+          reject("Timed out waiting for write lock for Order");
+          break;
+        } else {
+          if (expire_at >= 0) {
+            await this.#redis_client.expireat(write_lock_key, expire_at);
+          }
+          await new Promise((resolve) => setTimeout(resolve, 1000));
+        }
+      } while (true);
+    });
+  }
+
+  async releaseWriteLock() {
+    let write_lock_key = Order.STORAGE_MUTEX_KEY_PREFIX + this.#order_id;
+    return this.#redis_client.del(write_lock_key);
+  }
+
+  /**
+   * Creates a link between Order and Key that will automatically expire
+   * @param {string} key
+   */
+  async createOrderLink(key) {
+    let expiration = dayjs().add(Order.PURCHASE_LINK_TIME_DAYS, "day");
+    let redis_key = Order.PURCHASE_LINK_KEY_PREFIX + this.#order_id;
+
+    return this.#redis_client
+      .pipeline()
+      .set(redis_key, key)
+      .expireat(expiration.unix())
+      .exec();
+  }
+
+  /**
+   * Tries to get a key for this order. If the order link already expired this function rejects the promise
+   */
+  async getKeyFromOrderLink() {
+    let redis_key = Order.PURCHASE_LINK_KEY_PREFIX + this.#order_id;
+    return this.#redis_client.get(redis_key).then((result) => {
+      if (result) {
+        return result;
+      } else {
+        throw "Order Link does not exist";
+      }
+    });
+  }
+
   static async GENERATE_UNIQUE_ORDER_ID() {
     // Generate Order ID => time in seconds since 1.1.1970 and and add a mutex to it to allow multiple order ids per second
     let Redis = require("ioredis");
diff --git a/pass/package-lock.json b/pass/package-lock.json
index 9b24ddd..60d09f0 100644
--- a/pass/package-lock.json
+++ b/pass/package-lock.json
@@ -13,7 +13,6 @@
         "browserify-middleware": "^8.1.1",
         "config": "^3.3.8",
         "cookie-parser": "~1.4.4",
-        "country-locale-map": "^1.8.11",
         "dayjs": "^1.11.6",
         "debug": "~2.6.9",
         "ejs": "~2.6.1",
@@ -22,7 +21,6 @@
         "express-validator": "^6.14.2",
         "http-errors": "~1.6.3",
         "ioredis": "^5.2.4",
-        "ip-locale": "^1.0.3",
         "less-middleware": "~2.2.1",
         "morgan": "~1.9.1",
         "node-forge": "^1.3.1",
@@ -1041,14 +1039,6 @@
       "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
       "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="
     },
-    "node_modules/country-locale-map": {
-      "version": "1.8.11",
-      "resolved": "https://registry.npmjs.org/country-locale-map/-/country-locale-map-1.8.11.tgz",
-      "integrity": "sha512-xLSokf48z0MGSxcZFCH5MQq+rRbWQEe0BwQAuFDH6er92iEa3WEg3eeCpfFFeFeNriKiq8cNJ0+YIiIHofUQJA==",
-      "dependencies": {
-        "fuzzball": "^1.3.0"
-      }
-    },
     "node_modules/create-ecdh": {
       "version": "4.0.4",
       "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
@@ -1796,17 +1786,6 @@
       "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
       "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
     },
-    "node_modules/fuzzball": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/fuzzball/-/fuzzball-1.4.0.tgz",
-      "integrity": "sha512-ufKO0SHW65RSqZNu4rmLmraQVuwb8kVf8S/ICpkih/PfIff2YW3sa8zTvt7d7hJFXY1IvOOGJTeXxs69XLBd4Q==",
-      "dependencies": {
-        "heap": ">=0.2.0",
-        "setimmediate": "^1.0.5",
-        "string.fromcodepoint": "^0.2.1",
-        "string.prototype.codepointat": "^0.2.0"
-      }
-    },
     "node_modules/get-assigned-identifiers": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz",
@@ -2058,11 +2037,6 @@
         "node": ">=0.10.32"
       }
     },
-    "node_modules/heap": {
-      "version": "0.2.7",
-      "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz",
-      "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg=="
-    },
     "node_modules/hmac-drbg": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@@ -2259,11 +2233,6 @@
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
       "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
     },
-    "node_modules/ip-locale": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/ip-locale/-/ip-locale-1.0.3.tgz",
-      "integrity": "sha512-HWo/MhFbAz/aO1isJeMsWnm59bimBEKY9thU8kQ70OkRhTEXpQ6PxSeIB0TBAvTMzqy7d+4PfNw9wS0oR0qjAg=="
-    },
     "node_modules/ipaddr.js": {
       "version": "1.9.1",
       "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -3888,11 +3857,6 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/setimmediate": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
-      "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
-    },
     "node_modules/setprototypeof": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
@@ -4424,16 +4388,6 @@
         "node": ">=8"
       }
     },
-    "node_modules/string.fromcodepoint": {
-      "version": "0.2.1",
-      "resolved": "https://registry.npmjs.org/string.fromcodepoint/-/string.fromcodepoint-0.2.1.tgz",
-      "integrity": "sha512-n69H31OnxSGSZyZbgBlvYIXlrMhJQ0dQAX1js1QDhpaUH6zmU3QYlj07bCwCNlPOu3oRXIubGPl2gDGnHsiCqg=="
-    },
-    "node_modules/string.prototype.codepointat": {
-      "version": "0.2.1",
-      "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz",
-      "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg=="
-    },
     "node_modules/stringstream": {
       "version": "0.0.6",
       "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz",
@@ -5983,14 +5937,6 @@
       "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
       "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="
     },
-    "country-locale-map": {
-      "version": "1.8.11",
-      "resolved": "https://registry.npmjs.org/country-locale-map/-/country-locale-map-1.8.11.tgz",
-      "integrity": "sha512-xLSokf48z0MGSxcZFCH5MQq+rRbWQEe0BwQAuFDH6er92iEa3WEg3eeCpfFFeFeNriKiq8cNJ0+YIiIHofUQJA==",
-      "requires": {
-        "fuzzball": "^1.3.0"
-      }
-    },
     "create-ecdh": {
       "version": "4.0.4",
       "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
@@ -6603,17 +6549,6 @@
       "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
       "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
     },
-    "fuzzball": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/fuzzball/-/fuzzball-1.4.0.tgz",
-      "integrity": "sha512-ufKO0SHW65RSqZNu4rmLmraQVuwb8kVf8S/ICpkih/PfIff2YW3sa8zTvt7d7hJFXY1IvOOGJTeXxs69XLBd4Q==",
-      "requires": {
-        "heap": ">=0.2.0",
-        "setimmediate": "^1.0.5",
-        "string.fromcodepoint": "^0.2.1",
-        "string.prototype.codepointat": "^0.2.0"
-      }
-    },
     "get-assigned-identifiers": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz",
@@ -6803,11 +6738,6 @@
         "sntp": "1.x.x"
       }
     },
-    "heap": {
-      "version": "0.2.7",
-      "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz",
-      "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg=="
-    },
     "hmac-drbg": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@@ -6951,11 +6881,6 @@
         }
       }
     },
-    "ip-locale": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/ip-locale/-/ip-locale-1.0.3.tgz",
-      "integrity": "sha512-HWo/MhFbAz/aO1isJeMsWnm59bimBEKY9thU8kQ70OkRhTEXpQ6PxSeIB0TBAvTMzqy7d+4PfNw9wS0oR0qjAg=="
-    },
     "ipaddr.js": {
       "version": "1.9.1",
       "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -8242,11 +8167,6 @@
         }
       }
     },
-    "setimmediate": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
-      "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
-    },
     "setprototypeof": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
@@ -8666,16 +8586,6 @@
         "strip-ansi": "^6.0.1"
       }
     },
-    "string.fromcodepoint": {
-      "version": "0.2.1",
-      "resolved": "https://registry.npmjs.org/string.fromcodepoint/-/string.fromcodepoint-0.2.1.tgz",
-      "integrity": "sha512-n69H31OnxSGSZyZbgBlvYIXlrMhJQ0dQAX1js1QDhpaUH6zmU3QYlj07bCwCNlPOu3oRXIubGPl2gDGnHsiCqg=="
-    },
-    "string.prototype.codepointat": {
-      "version": "0.2.1",
-      "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz",
-      "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg=="
-    },
     "stringstream": {
       "version": "0.0.6",
       "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz",
diff --git a/pass/package.json b/pass/package.json
index e98db44..e6e9cd7 100644
--- a/pass/package.json
+++ b/pass/package.json
@@ -12,7 +12,6 @@
     "browserify-middleware": "^8.1.1",
     "config": "^3.3.8",
     "cookie-parser": "~1.4.4",
-    "country-locale-map": "^1.8.11",
     "dayjs": "^1.11.6",
     "debug": "~2.6.9",
     "ejs": "~2.6.1",
@@ -21,7 +20,6 @@
     "express-validator": "^6.14.2",
     "http-errors": "~1.6.3",
     "ioredis": "^5.2.4",
-    "ip-locale": "^1.0.3",
     "less-middleware": "~2.2.1",
     "morgan": "~1.9.1",
     "node-forge": "^1.3.1",
@@ -31,4 +29,4 @@
   "devDependencies": {
     "nodemon": "^2.0.20"
   }
-}
+}
\ No newline at end of file
diff --git a/pass/routes/checkout/paypal.js b/pass/routes/checkout/paypal.js
index 7ec7c3e..4a444aa 100644
--- a/pass/routes/checkout/paypal.js
+++ b/pass/routes/checkout/paypal.js
@@ -1,10 +1,6 @@
 var express = require("express");
 var router = express.Router({ mergeParams: true });
 
-var createLocaleMiddleware = require("express-locale");
-var ipLocale = require("ip-locale");
-var clm = require("country-locale-map");
-
 const config = require("config");
 const Order = require("../../app/Order.js");
 const Key = require("../../app/Key.js");
@@ -15,23 +11,22 @@ const CLIENT_ID = config.get(
 const APP_SECRET = config.get(`payments.paypal.${process.env.NODE_ENV}.secret`);
 const base = config.get(`payments.paypal.${process.env.NODE_ENV}.base`);
 
-router.use("/", (req, res, next) => {
-  req.data.checkout.payment = {
-    provider: "paypal",
-    paypal: {
-      client_id: config.get(
-        `payments.paypal.${process.env.NODE_ENV}.client_id`
-      ),
-    },
-  };
+router.use("/", async (req, res, next) => {
+  await verifyWebhook();
+  if (req.data && req.data.checkout) {
+    req.data.checkout.payment = {
+      provider: "paypal",
+      paypal: {
+        client_id: config.get(
+          `payments.paypal.${process.env.NODE_ENV}.client_id`
+        ),
+      },
+    };
+  }
   next("route");
 });
 
 router.get("/:funding_source", async (req, res) => {
-  res.cookie("paypal_enabled_by_user", true, {
-    httpOnly: true,
-    sameSite: true,
-  });
   req.data.checkout.payment.paypal.funding_source = req.params.funding_source;
 
   if (req.params.funding_source === "card") {
@@ -68,28 +63,20 @@ router.get("/:funding_source", async (req, res) => {
 
 router.post("/:funding_source/order/create", async (req, res) => {
   // Order data is validated: Create and store the order in the redis database
-  let order = new Order(
-    await Order.GENERATE_UNIQUE_ORDER_ID(),
+  Order.CREATE_NEW_ORDER(
+    // Create Order on our side
     req.params.amount,
-    (req.params.amount / 300) * config.get("price.per_300")
-  );
-
-  order
-    .save()
-    .then(() => {
-      // Order created on our side. Continue the payment with the selected provider
-      return createOrder(order);
-    })
-    .then((order_result) => {
-      res.status(200).json(order_result);
+    (req.params.amount / 300) * config.get("price.per_300"),
+    req.data.key.key
+  )
+    .then(/** @param {Order} order */ (order) => createOrder(order)) // Create Order on PayPal Server
+    .then((order_data) => {
+      res.status(200).json(order_data);
     })
     .catch((reason) => {
-      return res.status(400).json({
-        errors: [
-          {
-            msg: reason,
-          },
-        ],
+      console.error(reason);
+      res.status(400).json({
+        errors: [{ msg: "Failed to create a new Order. Try again later" }],
       });
     });
 });
@@ -97,6 +84,7 @@ router.post("/:funding_source/order/create", async (req, res) => {
 router.post("/:funding_source/order/cancel", async (req, res) => {
   Order.LOAD_ORDER_FROM_ID(req.body.order_id)
     .then((order) => {
+      console.log("Loaded order");
       if (order.isPaymentComplete()) {
         // Not so good. Something went wrong after we captured the Payment
         // Refund it back
@@ -115,17 +103,18 @@ router.post("/:funding_source/order/cancel", async (req, res) => {
       }
     })
     .catch((reason) => {
-      res.status(400).json({ msg: reason.toString() });
+      console.error(reason);
+      res.status(400).json({ msg: "Failed to load/cancel Order." });
     }); // Deletes a order but only if the payment is not yet completed
 });
 
 // capture payment & store order information or fullfill order
 router.post("/:funding_source/order/capture", async (req, res) => {
-  verifyWebhook()
-    .then(() => {
-      return Order.LOAD_ORDER_FROM_ID(req.body.order_id);
-    })
-    .then((loaded_order) => {
+  Order.LOAD_ORDER_FROM_ID(req.body.order_id).then(
+    /**
+     * @param {Order} loaded_order
+     */
+    (loaded_order) => {
       let paypal_order = loaded_order.getPaymentMethodLink();
       capturePayment(paypal_order.order_id)
         .then((captureData) => {
@@ -137,14 +126,114 @@ router.post("/:funding_source/order/capture", async (req, res) => {
           }
         })
         .then(() => {
-          loaded_order.setPaymentCompleted(true);
-          return loaded_order.save();
+          return loaded_order.setPaymentCompleted(true);
         })
         .then(() => Key.CHARGE_KEY(req.data.key.key, loaded_order.getAmount()))
         .catch((reason) => {
           console.error(reason);
           res.status(400).json({ errors: [{ msg: reason.toString() }] });
         });
+    }
+  );
+});
+
+router.post("/webhook", async (req, res) => {
+  // Verify that the webhook came from paypal
+  let accessToken = await generateAccessToken();
+
+  let verification_url = `${base}/v1/notifications/verify-webhook-signature`;
+
+  fetch(verification_url, {
+    method: "post",
+    headers: {
+      Authorization: `Bearer ${accessToken}`,
+      "Content-Type": "application/json",
+    },
+    body: JSON.stringify({
+      auth_algo: req.headers["paypal-auth-algo"],
+      cert_url: req.headers["paypal-cert-url"],
+      transmission_id: req.headers["paypal-transmission-id"],
+      transmission_sig: req.headers["paypal-transmission-sig"],
+      transmission_time: req.headers["paypal-transmission-time"],
+      webhook_event: req.body,
+      webhook_id: config.get(
+        `payments.paypal.${process.env.NODE_ENV}.webhook_id`
+      ),
+    }),
+  })
+    .then((response) => {
+      if (response.status !== 200) {
+        throw "Received status code " + response.status + " from PayPal API.";
+      } else {
+        return response.json();
+      }
+    })
+    .then((response) => {
+      if (
+        !response.verification_status ||
+        response.verification_status !== "SUCCESS"
+      ) {
+        console.error(response);
+        throw "Webhook Verification was not successfull";
+      } else {
+        if (req.body.event_type === "PAYMENT.CAPTURE.COMPLETED") {
+          // Check for a completed payment that did not get processed by us
+          let order_id = req.body.resource.invoice_id.replace(/^INV_/, "");
+          console.log(order_id);
+          return Order.LOAD_ORDER_FROM_ID(order_id)
+            .then(
+              /** @param {Order} order */ (order) => {
+                if (!order.isPaymentComplete()) {
+                  return order
+                    .setPaymentCompleted()
+                    .then(() =>
+                      Key.CHARGE_KEY(
+                        order.getKeyFromOrderLink(),
+                        order.getAmount()
+                      )
+                    );
+                } else {
+                  throw "Order is already completed";
+                }
+              }
+            )
+            .catch((reason) => {
+              console.log(reason);
+              res.status(200).json({ msg: reason });
+            });
+        } else if (req.body.event_type === "PAYMENT.CAPTURE.REFUNDED") {
+          // A Payment was refunded => Discharge the key
+          let order_id = req.body.resource.invoice_id.replace(/^INV_/, "");
+          console.log(order_id);
+          return Order.LOAD_ORDER_FROM_ID(order_id)
+            .then(
+              /** @param {Order} order */ (order) => {
+                if (order.isPaymentComplete()) {
+                  return order
+                    .setPaymentCompleted(false)
+                    .then(() =>
+                      Key.DISCHARGE_KEY(
+                        order.getKeyFromOrderLink(),
+                        order.getAmount()
+                      )
+                    );
+                } else {
+                  throw "Order is already completed";
+                }
+              }
+            )
+            .catch((reason) => {
+              console.log(reason);
+              res.status(200).json({ msg: reason });
+            });
+        }
+        console.log(req.body);
+        res.status(200).send("");
+      }
+    })
+    .catch((reason) => {
+      console.error(reason);
+      res.status(200).json({ errors: [{ msg: "Error verifying Webhook" }] });
     });
 });
 
@@ -178,6 +267,7 @@ async function createOrder(loaded_order) {
     intent: "CAPTURE",
     purchase_units: [
       {
+        invoice_id: "INV_" + loaded_order.getOrderID(),
         description: "MetaGer Pass Einkauf",
         amount: {
           currency_code: "EUR",
@@ -234,10 +324,11 @@ async function createOrder(loaded_order) {
   })
     .then((response) => response.json())
     .then((data) => {
-      loaded_order.setPaymentMethodLink({ name: "paypal", order_id: data.id });
-      return loaded_order.save().then(() => {
-        return { id: data.id, order_id: loaded_order.getOrderID() };
-      });
+      return loaded_order
+        .setPaymentMethodLink({ name: "paypal", order_id: data.id })
+        .then(() => {
+          return { id: data.id, order_id: loaded_order.getOrderID() };
+        });
     });
 }
 
@@ -309,7 +400,7 @@ async function verifyWebhook() {
             { name: "PAYMENT.CAPTURE.REFUNDED" },
             { name: "PAYMENT.CAPTURE.REVERSED" },
           ],
-          url: domain + "/webhooks/paypal",
+          url: domain + "/webhooks/paypal/webhook",
         }),
       }).then((response) => {
         if (response.status !== 201) {
-- 
GitLab