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