From ae991dfc681082d9aa74db776c69b74ee2abf39d Mon Sep 17 00:00:00 2001
From: Dominik Hebeler <dominik@suma-ev.de>
Date: Wed, 8 Feb 2023 17:00:40 +0100
Subject: [PATCH] fixed webhooks plus work on refunds

---
 pass/app/Key.js                       |   7 +-
 pass/app/Order.js                     | 146 ++++++++++++++++----
 pass/app/payment_processor/Paypal.js  | 115 ++++++++++++++-
 pass/public/styles/orders/refund.css  |   2 +-
 pass/public/styles/orders/refund.less |  15 +-
 pass/routes/checkout/paypal.js        | 192 ++++----------------------
 pass/routes/orders/refund.js          |  71 ++++++----
 pass/views/orders/order.ejs           |  69 +++++----
 pass/views/orders/refund.ejs          |  42 +++---
 9 files changed, 387 insertions(+), 272 deletions(-)

diff --git a/pass/app/Key.js b/pass/app/Key.js
index fc6c802..6f183c1 100644
--- a/pass/app/Key.js
+++ b/pass/app/Key.js
@@ -99,10 +99,9 @@ class Key {
       .decrby(Key.DATABASE_PREFIX + key, amount)
       .then((new_charge) => {
         if (new_charge < 0) {
-          return redis_client.incrby(
-            Key.DATABASE_PREFIX + key,
-            Math.abs(new_charge)
-          );
+          return redis_client
+            .incrby(Key.DATABASE_PREFIX + key, Math.abs(new_charge))
+            .then(() => 0);
         } else {
           return new_charge;
         }
diff --git a/pass/app/Order.js b/pass/app/Order.js
index 50d7e86..1967e1c 100644
--- a/pass/app/Order.js
+++ b/pass/app/Order.js
@@ -1,7 +1,7 @@
 const config = require("config");
-const Crypto = require("./Crypto");
 const dayjs = require("dayjs");
 const path = require("path");
+const Key = require("./Key");
 const PaymentProcessor = require("./payment_processor/PaymentProcessor");
 
 class Order {
@@ -52,9 +52,13 @@ class Order {
   #order_id;
   #expires_at;
   #amount;
+  #amount_refund_requested = 0;
+  #amount_refunded = 0;
   #price;
-  #payment_completed;
-  #receipt_created;
+  #order_key_charged = false;
+  #payment_captured = false;
+
+  #receipt_created = false;
   #civicrm_contribution_id;
   /** @type {PaymentProcessor} */
   #payment_processor;
@@ -71,8 +75,11 @@ class Order {
     amount,
     price,
     payment_processor,
-    payment_completed,
-    receipt_created,
+    amount_refund_requested = 0,
+    order_key_charged = false,
+    payment_captured = false,
+    amount_refunded = 0,
+    receipt_created = false,
     civicrm_contribution_id
   ) {
     this.#order_id = order_id;
@@ -86,9 +93,10 @@ class Order {
 
     this.#payment_processor = payment_processor;
 
-    if (payment_completed) {
-      this.#payment_completed = payment_completed;
-    }
+    this.#amount_refund_requested = amount_refund_requested;
+    this.#order_key_charged = order_key_charged;
+    this.#payment_captured = payment_captured;
+    this.#amount_refunded = amount_refunded;
 
     this.#receipt_created = receipt_created;
 
@@ -134,27 +142,109 @@ class Order {
   }
 
   async captureOrder() {
-    return this.getPaymentProcessor()
-      .captureOrder()
-      .then(() => this.setPaymentCompleted(true));
+    if (this.isPaymentCaptured()) {
+      throw new Error("Order already captured");
+    }
+    return this.getWriteLock()
+      .then(() => this.getPaymentProcessor().captureOrder())
+      .then(() => this.createCiviCRMOrder())
+      .then(() => {
+        this.#payment_captured = true;
+        return this.save();
+      })
+      .then(() => this.releaseWriteLock());
   }
 
-  isReceiptCreated() {
-    return this.#receipt_created && this.#receipt_created === true;
+  async chargeKey() {
+    if (this.isOrderKeyCharged()) {
+      throw new Error("Key already charged");
+    }
+    return this.getWriteLock()
+      .then(() => this.getKeyFromOrderLink())
+      .then((key) => Key.CHARGE_KEY(key, this.getAmount()))
+      .then(() => {
+        this.#order_key_charged = true;
+        return this.save();
+      })
+      .then(() => this.releaseWriteLock());
+  }
+
+  async requestRefund(amount) {
+    if (this.#amount_refund_requested > 0) {
+      throw new Error("Refund is already requested");
+    }
+    let new_charge = 0;
+    return this.getWriteLock()
+      .then(() => this.getKeyFromOrderLink())
+      .then((key) => Key.DISCHARGE_KEY(key, amount))
+      .then((charge) => {
+        new_charge = charge;
+        this.#amount_refund_requested = amount;
+        return this.save();
+      })
+      .then(() => this.releaseWriteLock())
+      .then(() => new_charge);
   }
 
-  async setPaymentCompleted(completed) {
+  async refund() {
+    if (this.#amount_refunded > 0 || this.#amount_refund_requested === 0) {
+      throw "No refund requested or Refund already processed";
+    }
+
+    let refund_price = parseFloat(
+      (
+        Math.ceil(
+          (this.#amount_refund_requested / this.#amount) * this.#price * 100
+        ) / 100
+      ).toFixed(2)
+    );
+
     return this.getWriteLock()
+      .then(() => this.getPaymentProcessor().refundOrder(refund_price))
       .then(() => {
-        this.#payment_completed = completed;
-        return this.createCiviCRMOrder();
+        this.#amount_refunded = this.#amount_refund_requested;
+        return this.save();
       })
-      .then(() => this.save())
       .then(() => this.releaseWriteLock());
   }
 
-  isPaymentComplete() {
-    return this.#payment_completed;
+  async denyRefund() {
+    if (this.#amount_refunded > 0 || this.#amount_refund_requested === 0) {
+      return false;
+    }
+    return this.getWriteLock()
+      .then(() => this.getKeyFromOrderLink())
+      .then((key) => Key.CHARGE_KEY(key, this.#amount_refund_requested))
+      .then(() => {
+        this.#amount_refund_requested = 0;
+        return this.save();
+      })
+      .then(() => this.releaseWriteLock())
+      .then(() => {
+        return true;
+      })
+      .catch((reason) => {
+        console.debug(reason);
+        return false;
+      });
+  }
+
+  isReceiptCreated() {
+    return this.#receipt_created;
+  }
+
+  getAmountRefundRequested() {
+    return this.#amount_refund_requested;
+  }
+
+  isOrderKeyCharged() {
+    return this.#order_key_charged;
+  }
+  isPaymentCaptured() {
+    return this.#payment_captured;
+  }
+  getAmountRefunded() {
+    return this.#amount_refunded;
   }
 
   /**
@@ -210,8 +300,11 @@ class Order {
             PaymentProcessor.LOAD_PAYMENT_PROCESSOR(
               JSON.parse(order_data.payment_processor)
             ),
-            order_data.payment_completed ? true : false,
-            order_data.receipt_created,
+            JSON.parse(order_data.amount_refund_requested),
+            JSON.parse(order_data.order_key_charged),
+            JSON.parse(order_data.payment_captured),
+            JSON.parse(order_data.amount_refunded),
+            JSON.parse(order_data.receipt_created),
             order_data.civicrm_contribution_id
           );
           resolve(loaded_order);
@@ -228,7 +321,10 @@ class Order {
       expires_at: this.#expires_at.format("YYYY-MM-DD"),
       amount: this.#amount,
       price: this.#price,
-      payment_completed: this.#payment_completed,
+      amount_refund_requested: this.#amount_refund_requested,
+      order_key_charged: this.#order_key_charged,
+      payment_captured: this.#payment_captured,
+      amount_refunded: this.#amount_refunded,
       receipt_created: this.#receipt_created,
       payment_processor: JSON.stringify(this.#payment_processor.serialize()),
       civicrm_contribution_id: this.#civicrm_contribution_id,
@@ -238,7 +334,7 @@ class Order {
      * Uncompleted Orders will be stored in Redis
      */
     let redis_key = Order.STORAGE_KEY_PREFIX + this.#order_id;
-    if (this.#payment_completed) {
+    if (this.isPaymentCaptured()) {
       let fs = require("fs");
       let order_file = path.join(
         this.#order_path,
@@ -272,7 +368,7 @@ class Order {
 
   async delete() {
     let redis_key = Order.STORAGE_KEY_PREFIX + this.#order_id;
-    if (!this.isPaymentComplete()) {
+    if (!this.isPaymentCaptured()) {
       return this.deleteOrderLink()
         .then(() => this.#redis_client.del(redis_key))
         .then((deleted_keys) => {
@@ -482,7 +578,7 @@ class Order {
     let redis_key = Order.PURCHASE_LINK_ORDER_TO_KEY_PREFIX + this.#order_id;
 
     let expiration = dayjs().add(Order.PURCHASE_LINK_TIME_DAYS, "day");
-    if (!this.isPaymentComplete()) {
+    if (!this.isPaymentCaptured()) {
       expiration = dayjs().add(
         Order.PURCHASE_STORAGE_TIME_UNCOMPLETED_HOURS,
         "hour"
diff --git a/pass/app/payment_processor/Paypal.js b/pass/app/payment_processor/Paypal.js
index 6613bb9..106839e 100644
--- a/pass/app/payment_processor/Paypal.js
+++ b/pass/app/payment_processor/Paypal.js
@@ -1,6 +1,7 @@
 const Order = require("../Order");
 const config = require("config");
 const PaymentProcessor = require("./PaymentProcessor");
+const Key = require("../Key");
 const webhook_redis_key = "checkout_paypal_webhook_id";
 const CLIENT_ID = config.get(`payments.paypal.client_id`);
 const APP_SECRET = config.get(`payments.paypal.secret`);
@@ -150,6 +151,110 @@ class Paypal extends PaymentProcessor {
       });
   }
 
+  async processWebhook(
+    auth_algo,
+    cert_url,
+    transmission_id,
+    transmission_sig,
+    transmission_time,
+    webhook_event // PayPal Webhook Event data
+  ) {
+    let webhook_id_promise = this.#getWebhookId();
+    let access_token_promise = this.#generateAccessToken();
+    let verification_url = `${base}/v1/notifications/verify-webhook-signature`;
+
+    let webhook_data = await Promise.all([
+      webhook_id_promise,
+      access_token_promise,
+    ])
+      .then(([webhook_id, access_token]) =>
+        fetch(verification_url, {
+          method: "post",
+          headers: {
+            Authorization: `Bearer ${access_token}`,
+            "Content-Type": "application/json",
+          },
+          body: JSON.stringify({
+            auth_algo: auth_algo,
+            cert_url: cert_url,
+            transmission_id: transmission_id,
+            transmission_sig: transmission_sig,
+            transmission_time: transmission_time,
+            webhook_event: webhook_event,
+            webhook_id: webhook_id,
+          }),
+        })
+      )
+      .then((response) => {
+        if (response.status !== 200) {
+          throw "Received status code " + response.status + " from PayPal API.";
+        } else {
+          return response.json();
+        }
+      });
+
+    if (
+      !webhook_data.verification_status ||
+      webhook_data.verification_status !== "SUCCESS"
+    ) {
+      console.error(response);
+      throw "Webhook Verification was not successfull";
+    } else {
+      if (webhook_event.event_type === "PAYMENT.CAPTURE.COMPLETED") {
+        // Check for a completed payment that did not get processed by us
+        let order_id = webhook_event.resource.invoice_id;
+        if (!order_id) {
+          throw "No Order ID attached";
+        } else {
+          order_id = order_id.replace(/^INV_/, "");
+        }
+        return Order.LOAD_ORDER_FROM_ID(order_id).then(
+          /** @param {Order} order */ (order) => order.chargeKey()
+        );
+      } else if (webhook_event.event_type === "PAYMENT.CAPTURE.REFUNDED") {
+        // A Payment was refunded => Discharge the key
+        let order_id = webhook_event.resource.invoice_id;
+        if (!order_id) {
+          throw "No Order ID attached";
+        } else {
+          order_id = order_id.replace(/^INV_/, "");
+        }
+        return Order.LOAD_ORDER_FROM_ID(order_id).then(
+          /** @param {Order} order */ (order) => {
+            if (!order.isPaymentComplete()) {
+              // Update Payment status
+              let payment_link = order.getPaymentMethodLink();
+              if (payment_link.payment_status !== "REFUNDED") {
+                payment_link.payment_status = "REFUNDED";
+                return order
+                  .setPaymentMethodLink(payment_link)
+                  .then(() => order.getKeyFromOrderLink())
+                  .then((key) => Key.DISCHARGE_KEY(key, order.getAmount()));
+              } else {
+                throw "Order is already refunded";
+              }
+            } else {
+              throw "Order is already refunded";
+            }
+          }
+        );
+      } else if (webhook_event.event_type === "CHECKOUT.ORDER.APPROVED") {
+        let order_id = webhook_event.resource.purchase_units[0].invoice_id;
+        if (!order_id) {
+          throw "No Order ID attached";
+        } else {
+          order_id = order_id.replace(/^INV_/, "");
+        }
+        return Order.LOAD_ORDER_FROM_ID(order_id).then(
+          /** @param {Order} order */ (order) => order.captureOrder()
+        );
+      } else {
+        console.log(req.body);
+        throw "Webhook not implemented";
+      }
+    }
+  }
+
   /**
    *
    * @param {float} amount
@@ -167,7 +272,7 @@ class Paypal extends PaymentProcessor {
             if (link.rel === "refund") {
               refund_urls.push({
                 refund_url: link.href,
-                amount: parseFloat(payment.amount.amount),
+                amount: parseFloat(payment.amount.value),
               });
               return false;
             }
@@ -359,6 +464,14 @@ class Paypal extends PaymentProcessor {
       });
   }
 
+  async #getWebhookId() {
+    let Redis = require("ioredis");
+    let redis_client = new Redis({
+      host: config.get("redis.host"),
+    });
+    return redis_client.get(webhook_redis_key);
+  }
+
   // Client Token for handling credit card payments
   async generateClientToken() {
     const accessToken = await this.#generateAccessToken();
diff --git a/pass/public/styles/orders/refund.css b/pass/public/styles/orders/refund.css
index fee50cd..44ef610 100644
--- a/pass/public/styles/orders/refund.css
+++ b/pass/public/styles/orders/refund.css
@@ -1 +1 @@
-#refund{grid-area:content}#refund #refund-form textarea{width:100%;max-width:25rem}#refund #refund-form button{display:flex;text-align:center;width:100%;max-width:25rem;align-items:center;justify-content:center;gap:.5rem;padding:.5rem;font-size:.8rem}#refund #refund-form button>img{height:2em}
\ No newline at end of file
+#refund{grid-area:content}#refund #refund-form textarea{width:100%;max-width:25rem}#refund #refund-form button{display:flex;text-align:center;max-width:25rem;align-items:center;justify-content:center;gap:.5rem;padding:.5rem;font-size:.8rem}#refund #refund-form button>img{height:2em}#refund #refund-form #moderation-buttons{display:flex;justify-content:flex-end;gap:1rem}@media (max-width:365px){#refund #refund-form #moderation-buttons{flex-direction:column}#refund #refund-form #moderation-buttons button{width:100%}}#refund #refund-form #moderation-buttons button[value="deny"]{background-color:#860000}
\ No newline at end of file
diff --git a/pass/public/styles/orders/refund.less b/pass/public/styles/orders/refund.less
index e58f62f..56fc0e1 100644
--- a/pass/public/styles/orders/refund.less
+++ b/pass/public/styles/orders/refund.less
@@ -8,7 +8,6 @@
     button {
       display: flex;
       text-align: center;
-      width: 100%;
       max-width: 25rem;
       align-items: center;
       justify-content: center;
@@ -19,5 +18,19 @@
         height: 2em;
       }
     }
+    #moderation-buttons {
+      display: flex;
+      justify-content: flex-end;
+      gap: 1rem;
+      @media (max-width: 365px) {
+        flex-direction: column;
+        button {
+          width: 100%;
+        }
+      }
+      button[value="deny"] {
+        background-color: #860000;
+      }
+    }
   }
 }
diff --git a/pass/routes/checkout/paypal.js b/pass/routes/checkout/paypal.js
index 90339c6..eea4ff6 100644
--- a/pass/routes/checkout/paypal.js
+++ b/pass/routes/checkout/paypal.js
@@ -89,18 +89,17 @@ 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
-        let paypal_order_data = order.getPaymentMethodLink();
-        if (paypal_order_data.name === "paypal") {
-          console.log(paypal_order_data);
+    .then(
+      /** @param {Order} order */ (order) => {
+        if (order.isPaymentCaptured()) {
+          // Not so good. Something went wrong after we captured the Payment
+          // Refund it back
+          // ToDo Execute refund
+          console.debug("Refund should happen here");
         }
+        return order.delete();
       }
-      return order.delete();
-    })
+    )
     .then((success) => {
       if (success) {
         res.status(200).json({ msg: "Order deleted" });
@@ -140,7 +139,7 @@ router.post("/:funding_source/order/capture", async (req, res) => {
     (loaded_order) => {
       loaded_order
         .captureOrder()
-        .then(() => Key.CHARGE_KEY(req.data.key.key, loaded_order.getAmount()))
+        .then(() => loaded_order.chargeKey())
         .then(() => {
           let redirect_url =
             "/key/" + req.data.key.key + "/orders/" + loaded_order.getOrderID();
@@ -155,16 +154,14 @@ router.post("/:funding_source/order/capture", async (req, res) => {
         })
         .catch((reason) => {
           console.debug(reason);
-          res
-            .status(400)
-            .json({
-              errors: [
-                {
-                  type: "PAYMENT_NOT_COMPLETED_ERROR",
-                  msg: "Couldn't capture the payment",
-                },
-              ],
-            });
+          res.status(400).json({
+            errors: [
+              {
+                type: "PAYMENT_NOT_COMPLETED_ERROR",
+                msg: "Couldn't capture the payment",
+              },
+            ],
+          });
         });
     }
   );
@@ -172,130 +169,22 @@ router.post("/:funding_source/order/capture", async (req, res) => {
 
 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: await getWebhookId(),
-    }),
-  })
-    .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 {
-        console.log(req.body.event_type);
-        if (req.body.event_type === "PAYMENT.CAPTURE.COMPLETED") {
-          console.log(JSON.stringify(req.body));
-          // Check for a completed payment that did not get processed by us
-          let order_id = req.body.resource.invoice_id;
-          if (!order_id) {
-            throw "No Order ID attached";
-          } else {
-            order_id = order_id.replace(/^INV_/, "");
-          }
-          return Order.LOAD_ORDER_FROM_ID(order_id).then(
-            /** @param {Order} order */ (order) => {
-              if (!order.isPaymentComplete()) {
-                return order
-                  .setPaymentCompleted(true)
-                  .then(() => {
-                    // Update Order status
-                    let payment_link = order.getPaymentMethodLink();
-                    payment_link.payment_status = req.body.resource.status;
-                    return order.setPaymentMethodLink(payment_link);
-                  })
-                  .then(() => order.getKeyFromOrderLink())
-                  .then((key) => Key.CHARGE_KEY(key, order.getAmount()));
-              } else {
-                throw "Order is already completed";
-              }
-            }
-          );
-        } else if (req.body.event_type === "PAYMENT.CAPTURE.REFUNDED") {
-          console.log(JSON.stringify(req.body));
-          // A Payment was refunded => Discharge the key
-          let order_id = req.body.resource.invoice_id;
-          if (!order_id) {
-            throw "No Order ID attached";
-          } else {
-            order_id = order_id.replace(/^INV_/, "");
-          }
-          return Order.LOAD_ORDER_FROM_ID(order_id).then(
-            /** @param {Order} order */ (order) => {
-              if (!order.isPaymentComplete()) {
-                // Update Payment status
-                let payment_link = order.getPaymentMethodLink();
-                if (payment_link.payment_status !== "REFUNDED") {
-                  payment_link.payment_status = "REFUNDED";
-                  return order
-                    .setPaymentMethodLink(payment_link)
-                    .then(() => order.getKeyFromOrderLink())
-                    .then((key) => Key.DISCHARGE_KEY(key, order.getAmount()));
-                } else {
-                  throw "Order is already refunded";
-                }
-              } else {
-                throw "Order is already refunded";
-              }
-            }
-          );
-        } else if (req.body.event_type === "CHECKOUT.ORDER.APPROVED") {
-          let order_id = req.body.resource.purchase_units[0].invoice_id;
-          if (!order_id) {
-            throw "No Order ID attached";
-          } else {
-            order_id = order_id.replace(/^INV_/, "");
-          }
-          return Order.LOAD_ORDER_FROM_ID(order_id).then(
-            /** @param {Order} order */ (order) => {
-              let payment_method_link = order.getPaymentMethodLink();
-              if (
-                payment_method_link &&
-                payment_method_link.payment_status &&
-                payment_method_link.payment_status !== "COMPLETED"
-              ) {
-                return capturePayment(order);
-              } else {
-                throw "Order already captured";
-              }
-            }
-          );
-        } else {
-          console.log(req.body);
-          throw "Webhook not implemented";
-        }
-      }
-    })
+  let paypal = new Paypal();
+  paypal
+    .processWebhook(
+      req.headers["paypal-auth-algo"],
+      req.headers["paypal-cert-url"],
+      req.headers["paypal-transmission-id"],
+      req.headers["paypal-transmission-sig"],
+      req.headers["paypal-transmission-time"],
+      req.body
+    )
     .then(() => {
       res.status(200).send("");
     })
     .catch((reason) => {
-      console.error(reason);
-      res.status(200).json({ errors: [{ msg: "Error verifying Webhook" }] });
+      console.debug(reason);
+      res.status(200).send("");
     });
 });
 
@@ -307,27 +196,6 @@ module.exports = router;
 
 //////////////////////
 
-// use the orders api to create an order
-
-async function createOrder(loaded_order) {}
-
-// use the orders api to capture payment for an order
-
-/**
- *
- * @param {Order} order
- * @returns
- */
-async function capturePayment(order) {}
-
-async function getWebhookId() {
-  let Redis = require("ioredis");
-  let redis_client = new Redis({
-    host: config.get("redis.host"),
-  });
-  return redis_client.get(webhook_redis_key);
-}
-
 async function refundPayment(capture_ids) {
   const accessToken = await generateAccessToken();
   let promises = [];
diff --git a/pass/routes/orders/refund.js b/pass/routes/orders/refund.js
index b08784f..45505f3 100644
--- a/pass/routes/orders/refund.js
+++ b/pass/routes/orders/refund.js
@@ -2,23 +2,33 @@ var express = require("express");
 var router = express.Router({ mergeParams: true });
 
 const config = require("config");
+const Key = require("../../app/Key");
 
 // Base URL: /key/:key/orders/:order/refund
 router.use("/", (req, res, next) => {
   let refund_count = Math.min(
-    req.data.key.charge,
+    Math.max(
+      req.data.key.charge,
+      req.data.order.order.getAmountRefundRequested()
+    ),
     req.data.order.order.getAmount()
   );
   req.data.order.refund = {
     count: refund_count,
-    amount:
-      (req.data.order.order.getPrice() / req.data.order.order.getAmount()) *
-      refund_count,
+    amount: parseFloat(
+      (
+        Math.ceil(
+          (refund_count / req.data.order.order.getAmount()) *
+            req.data.order.order.getPrice() *
+            100
+        ) / 100
+      ).toFixed(2)
+    ),
   };
   if (req.data.admin === true) {
     req.data.order.refund.approval_link = `/key/${
       req.data.key.key
-    }/orders/${req.data.order.order.getOrderID()}/refund/approve`;
+    }/orders/${req.data.order.order.getOrderID()}/refund/process`;
   }
   req.data.css.push("/styles/orders/refund.css");
   next();
@@ -31,21 +41,18 @@ router.post("/", (req, res, next) => {
 
   // Refund values as sent back to us by the user.
   // Used only to verify the amount we are refunding corresponds to what we showed the user
-  let user_amount = req.body.amount; // Amount of money refunded back to the user
   let user_count = parseInt(req.body.count); // Amount of search request discharged from key
 
-  if (
-    req.data.order.refund.amount.toFixed(2) !== user_amount ||
-    req.data.order.refund.count !== user_count
-  ) {
+  if (req.data.order.refund.count !== user_count) {
     // Something changed abort here
     req.data.order.refund.error = "invalid_data";
     res.render("key", req.data);
+  } else if (req.data.order.order.getAmountRefundRequested() > 0) {
+    req.data.order.refund.error = "refund_already_requested";
+    res.render("key", req.data);
   } else {
     let request_data = {
       moderation: true,
-      amount: req.data.order.refund.amount,
-      count: req.data.order.refund.count,
     };
     req.data.order.refund.moderation_url = `${config.get("app.url")}/key/${
       req.data.key.key
@@ -85,10 +92,17 @@ router.post("/", (req, res, next) => {
           console.error(await response.text());
           throw "Fehler beim Erstellen der Benachrichtigung,";
         } else {
-          req.data.order.refund.success = true;
-          res.render("key", req.data);
+          return response;
         }
       })
+      .then((response) =>
+        req.data.order.order.requestRefund(req.data.order.refund.count)
+      )
+      .then((new_charge) => {
+        req.data.key.charge = new_charge;
+        req.data.order.refund.success = true;
+        res.render("key", req.data);
+      })
       .catch((reason) => {
         console.log(reason);
         req.data.order.refund.error = "send_email";
@@ -98,7 +112,7 @@ router.post("/", (req, res, next) => {
 });
 
 router.post(
-  "/approve",
+  "/process",
   (req, res, next) => {
     if (!req.data.admin) {
       res.locals.error = { status: 401 };
@@ -109,19 +123,24 @@ router.post(
     }
   },
   (req, res) => {
-    let amount = req.body.amount;
-    req.data.order.order
-      .getPaymentProcessor()
-      .refundOrder(amount)
-      .then(() => {
-        req.data.order.refund.approved = true;
+    if (req.body.action === "approve") {
+      req.data.order.order
+        .refund()
+        .then(() => {
+          req.data.order.refund.result = "REFUNDED";
+          res.render("key", req.data);
+        })
+        .catch((reason) => {
+          console.debug(reason);
+          res.locals.error = { status: 400 };
+          res.locals.message = "Error while executing refund";
+        });
+    } else {
+      req.data.order.order.denyRefund().then(() => {
+        req.data.order.refund.result = "REFUND_DENIED";
         res.render("key", req.data);
-      })
-      .catch((reason) => {
-        console.debug(reason);
-        res.locals.error = { status: 400 };
-        res.locals.message = "Error while executing refund";
       });
+    }
   }
 );
 
diff --git a/pass/views/orders/order.ejs b/pass/views/orders/order.ejs
index b8e0e6b..38391a4 100644
--- a/pass/views/orders/order.ejs
+++ b/pass/views/orders/order.ejs
@@ -1,36 +1,35 @@
 <div id="order">
-    <%_ if(!order.invoice && !order.refund) { _%>
-    <h2>Ihre Bestellung Nr. <%= order.order.getOrderID() %></h2>
-    <ul class="breadcrumbs">
-        <li><a href="<%= links.orders_url %>">Bestellungen</a></li>
-        <li><%= order.order.getOrderID() %></li>
-    </ul>
-    <%- include("order_details") %>
-    <h3>Vielen Dank für Ihren Einkauf!</h3>
-    <div id="order-buttons">
-        <a href="<%= links.receipt_url %>" target="_blank" class="button">
-            <img src="/images/download.svg" alt="" class="order-receipt" />
-            <span>Auftragsbestätigung herunterladen</span>
-        </a>
-        <%_ if(!order.order.isReceiptCreated()) { _%>
-        <a href="<%= links.invoice_url %>" class="button">
-            <img src="/images/invoice.svg" alt="" />
-            <span>Rechnung anfragen</span>
-        </a>
-        <%_ } else { _%>
-        <a href="<%= links.download_invoice_url %>" target="_blank" class="button">
-            <img src="/images/invoice.svg" alt="" />
-            <span>Rechnung herunterladen</span>
-        </a>
-        <%_ } _%>
-        <a href="<%= links.refund_url %>" class="button">
-            <img src="/images/money.svg" alt="" />
-            <span>Erstattung anfragen</span>
-        </a>
-    </div>
-    <%_ } else if(order.invoice) { _%>
-    <%- include('./invoice', {}); %>
-    <%_ } else if(typeof order.refund !== "undefined") { _%>
-    <%- include('./refund') %>
-    <%_ } _%>
-</div>
\ No newline at end of file
+  <%_ if(!order.invoice && !order.refund) { _%>
+  <h2>Ihre Bestellung Nr. <%= order.order.getOrderID() %></h2>
+  <ul class="breadcrumbs">
+    <li><a href="<%= links.orders_url %>">Bestellungen</a></li>
+    <li><%= order.order.getOrderID() %></li>
+  </ul>
+  <%- include("order_details") %>
+  <h3>Vielen Dank für Ihren Einkauf!</h3>
+  <div id="order-buttons">
+    <a href="<%= links.receipt_url %>" target="_blank" class="button">
+      <img src="/images/download.svg" alt="" class="order-receipt" />
+      <span>Auftragsbestätigung herunterladen</span>
+    </a>
+    <%_ if(!order.order.isReceiptCreated()) { _%>
+    <a href="<%= links.invoice_url %>" class="button">
+      <img src="/images/invoice.svg" alt="" />
+      <span>Rechnung anfragen</span>
+    </a>
+    <%_ } else { _%>
+    <a href="<%= links.download_invoice_url %>" target="_blank" class="button">
+      <img src="/images/invoice.svg" alt="" />
+      <span>Rechnung herunterladen</span>
+    </a>
+    <%_ } _%> <% if (order.order.getAmountRefundRequested() === 0) { %>
+    <a href="<%= links.refund_url %>" class="button">
+      <img src="/images/money.svg" alt="" />
+      <span>Erstattung anfragen</span>
+    </a>
+    <% } %>
+  </div>
+  <%_ } else if(order.invoice) { _%> <%- include('./invoice', {}); %> <%_ } else
+  if(typeof order.refund !== "undefined") { _%> <%- include('./refund') %> <%_ }
+  _%>
+</div>
diff --git a/pass/views/orders/refund.ejs b/pass/views/orders/refund.ejs
index cdcd570..695de45 100644
--- a/pass/views/orders/refund.ejs
+++ b/pass/views/orders/refund.ejs
@@ -20,27 +20,30 @@
     action="<%= order.refund.approval_link %>"
     method="post"
   >
-    <input
-      type="hidden"
-      name="amount"
-      value="<%= order.refund.amount.toFixed(2) %>"
-    />
-    <%_ if(typeof order.refund.approved !== "undefined") { _%>
+    <%_ if(typeof order.refund.result !== "undefined" && order.refund.result ==
+    "REFUNDED") { _%>
     <p>Der Zahlungsbetrag wurde erfolgreich erstattet.</p>
+    <%_ } else if(typeof order.refund.result !== "undefined" &&
+    order.refund.result == "REFUND_DENIED") { _%>
+    <p>Die Erstattung wurde erfolgreich abgelehnt.</p>
+    <%_ } else if(order.order.getAmountRefundRequested() === 0) { _%>
+    <p>Für diese Bestellung wurde keine Erstattung beantragt.</p>
+    <%_ } else if(order.order.getAmountRefunded() > 0) { _%>
+    <p>Die Bestellung wurde bereits erstattet.</p>
     <%_ } else { _%>
-    <button class="button">
-      <img src="/images/money.svg" alt="" aria-hidden="true" />
-      <span><%= order.refund.amount.toFixed(2) %>€ erstatten</span>
-    </button>
+    <div id="moderation-buttons">
+      <button class="button" name="action" value="approve">
+        <img src="/images/money.svg" alt="" aria-hidden="true" />
+        <span><%= order.refund.amount %>€ erstatten</span>
+      </button>
+      <button class="button" name="action" value="deny">
+        <span>Erstattung ablehnen</span>
+      </button>
+    </div>
     <%_ } _%>
   </form>
   <%_ } else { _%>
   <form id="refund-form" method="post">
-    <input
-      type="hidden"
-      name="amount"
-      value="<%= order.refund.amount.toFixed(2) %>"
-    />
     <input type="hidden" name="count" value="<%= order.refund.count %>" />
     <p>
       Sind Sie unzufrieden mit Ihrem Schlüssel? Das bedauern wir sehr!
@@ -60,11 +63,16 @@
     </p>
     <%_ } _%>
     <h3>Ihre Erstattung</h3>
-    <%_ if(typeof order.refund.error !== "undefined") { _%>
+    <%_ if(typeof order.refund.error !== "undefined") { _%> <%_
+    if(order.refund.error === "refund_already_requested") { _%>
+    <p class="error">
+      Für diese Bestellung wurde bereits eine Erstattung angefragt.
+    </p>
+    <%_ } else { _%>
     <p class="error">
       Fehler beim Senden Ihrer Nachricht. Bitte versuchen Sie es später erneut.
     </p>
-    <%_ } _%>
+    <%_ } _%> <%_ } _%>
     <textarea
       name="message"
       id="message"
-- 
GitLab