diff --git a/pass/app/Key.js b/pass/app/Key.js index 294b58c8bace0c83d0820738b1576133db46157f..6b1a144f1451f8cc8e8b549d8eab555cee0b48c6 100644 --- a/pass/app/Key.js +++ b/pass/app/Key.js @@ -106,7 +106,7 @@ class Key { } }); if (expiration === null) { - expiration = dayjs().add(config.get("keys.expiration_days"), "day"); + expiration = dayjs().add(config.get("keys.expiration_years"), "year"); } return expiration; } diff --git a/pass/app/Order.js b/pass/app/Order.js deleted file mode 100644 index 49b9c99dd0d2dcb90ac40f0317a3fc975ea2f6ff..0000000000000000000000000000000000000000 --- a/pass/app/Order.js +++ /dev/null @@ -1,787 +0,0 @@ -const config = require("config"); -const dayjs = require("dayjs"); -const path = require("path"); -const Key = require("./Key"); -const RedisClient = require("./RedisClient"); -const PaymentProcessor = require("./payment_processor/PaymentProcessor"); -const { CLIENT } = require("./RedisClient"); -const i18next = require("i18next"); - -class Order { - static get STORAGE_MUTEX_KEY_PREFIX() { - return "order_mutex:"; - } - - static get STORAGE_KEY_PREFIX() { - return "order:"; - } - - static get PURCHASE_TAX_AMOUNT() { - return 0.07; - } - - // How long is a link between order <=> key stored - static get PURCHASE_LINK_TIME_DAYS() { - return 31; - } - static get PURCHASE_LINK_ORDER_TO_KEY_PREFIX() { - return "order_link_order_key:"; - } - - // How many minutes is a user allowed to take for finishing the payment - static get PURCHASE_STORAGE_TIME_UNCOMPLETED_HOURS() { - return 6; - } - - static GET_ORDER_FILE_BASE_PATH(order_date) { - let order_path = config.get("storage.data_path"); - order_path = path.join(order_path, process.env.NODE_ENV, "orders"); - order_path = path.join( - order_path, - order_date.format("YYYY"), - order_date.format("MM") - ); - - return order_path; - } - /** - * Data stored in Redis Database - */ - #order_id; - #order_date; - #expires_at; - #amount; - #amount_refund_requested = 0; - #amount_refunded = 0; - #price; - #foreign_currency_price; - #foreign_currency; - #order_key_charged = false; - #payment_captured = false; - #order_key_changes = []; - - #receipt_created = false; - #civicrm_contribution_id; - /** @type {PaymentProcessor} */ - #payment_processor; - - /** - * Data populated by context - */ - #order_path; - - constructor( - order_id, - order_date = null, - amount, - price, - foreign_currency_price, - foreign_currency, - payment_processor, - amount_refund_requested = 0, - order_key_charged = false, - payment_captured = false, - amount_refunded = 0, - receipt_created = false, - civicrm_contribution_id, - order_key_changes = [] - ) { - this.#order_id = order_id; - if (order_date !== null) { - this.#order_date = order_date; - } else { - this.#order_date = dayjs.unix(this.#order_id.substr(0, 10)); - } - this.#expires_at = dayjs().add(config.get("keys.expiration_days"), "days"); - - - this.#order_path = Order.GET_ORDER_FILE_BASE_PATH(this.#order_date); - - this.#amount = parseInt(amount); - this.#price = parseFloat(price); - if (foreign_currency_price !== undefined) { - this.#foreign_currency_price = parseFloat(foreign_currency_price); - } - if (foreign_currency !== undefined) { - this.#foreign_currency = foreign_currency; - } - - this.#payment_processor = payment_processor; - - 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; - - this.#civicrm_contribution_id = civicrm_contribution_id; - this.#order_key_changes = order_key_changes; - } - - getOrderID() { - return this.#order_id; - } - - getAmount() { - return this.#amount; - } - - getNettoPrice() { - let amount = this.getPrice(); - let vat = config.get("price.vat") / 100; // Vat is configured in % - return amount / (1 + vat); - } - getVat() { - return config.get("price.vat"); - } - getVatAmount() { - let vat = config.get("price.vat") / 100; // Vat is configured in % - let amount = this.getPrice(); - return (amount / (1 + vat)) * vat; - } - getPrice() { - return this.#price; - } - getForeignCurrencyPrice() { - return this.#foreign_currency_price; - } - getForeignCurrency() { - return this.#foreign_currency; - } - - /** - * - * @returns {PaymentProcessor} - */ - getPaymentProcessor() { - return this.#payment_processor; - } - - /** - * - * @param {dayjs.Dayjs} expiration - */ - setExpiration(expiration) { - this.#expires_at = expiration; - } - - async captureOrder() { - if (this.isPaymentCaptured()) { - throw new Error("Order already captured"); - } - return this.getWriteLock() - .then(() => this.getPaymentProcessor().captureOrder()) - .then(() => this.createCiviCRMOrder()) - .then(() => { - this.#payment_captured = true; - this.#order_date = dayjs(); - return this.save(); - }) - .then(() => this.releaseWriteLock()); - } - - async chargeKey() { - if (this.isOrderKeyCharged()) { - throw new Error("Key already charged"); - } - return this.getWriteLock() - .then(() => this.getKeyFromOrderLink()) - .then((key) => Key.GET_KEY(key, true)) - .then((key) => { - key.charge_key_order( - this.getAmount(), - this.getOrderID(), - this.#expires_at - ); - return key.save(); - }) - .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_key = null; - return this.getWriteLock() - .then(() => this.getKeyFromOrderLink()) - .then((key) => Key.GET_KEY(key, true)) - .then((key) => { - key.discharge_key(amount, this.getOrderID()); - return key.save(); - }) - .then((key) => { - new_key = key; - this.#amount_refund_requested = amount; - return this.save(); - }) - .then(() => this.releaseWriteLock()) - .then(() => new_key); - } - - /** - * Processes a refund which was previously issued by the customer. - * Will issue a refund from the payment processor - * - * @returns - */ - 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.#amount_refunded = this.#amount_refund_requested; - return this.save(); - }) - .then(() => this.releaseWriteLock()); - } - - /** - * Processes an external refund. I.e. when the refund was issued through Payment Gateway directly - * - * @param {float} price price that was refunded externally - */ - async external_refund(price) { - // Create amount from refunded price - price = Math.min(this.#price, price); - let amount = Math.floor((price / this.#price) * this.#amount); - - return this.getWriteLock() - .then(() => this.getKeyFromOrderLink()) - .then((key) => Key.GET_KEY(key, true)) - .then((key) => { - try { - key.discharge_key(amount, this.getOrderID()); - return key.save(); - } catch (err) { - throw err; - } - }) - .then(() => { - this.#amount_refunded = amount; - return this.save(); - }) - .then(() => this.releaseWriteLock()); - } - - async denyRefund() { - if (this.#amount_refunded > 0 || this.#amount_refund_requested === 0) { - return false; - } - return this.getWriteLock() - .then(() => this.getKeyFromOrderLink()) - .then((key) => Key.GET_KEY(key, true)) - .then((key) => { - key.charge_key_order( - this.#amount_refund_requested, - this.getOrderID(), - this.#expires_at - ); - return key.save(); - }) - .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; - } - - /** - * - * @param {int} amount - * @param {string} key - * @param {PaymentProcessor} payment_processor - * @param {i18next.TFunction} t - * @returns - */ - static async CREATE_NEW_ORDER(amount, key, payment_processor, t) { - // Calculate price from amount - let price = (amount / 300) * config.get("price.per_300"); - price = price.toFixed(2); - - // Validate that Key can be charged with another order - /** - * @type {Key} - */ - let loaded_key = null; - return Key.GET_KEY(key) - .then((key) => { - if (!key.isChargable()) { - throw "Key cannot be charged"; - } else { - loaded_key = key; - return true; - } - }) - .then(() => Order.GENERATE_UNIQUE_ORDER_ID()) - .then((order_id) => { - let new_order = new Order( - order_id, - null, - amount, - price, - null, - null, - payment_processor - ); - return new_order - .getWriteLock() - .then(() => new_order.getPaymentProcessor().createOrder(new_order, t)) // Create Order on PaymentProcessor side - .then(() => new_order.createOrderLink(loaded_key.get_key())) - .then(() => new_order.save()) - .then(() => new_order.releaseWriteLock()) - .then(() => { - return new_order; - }); - }); - } - - /** - * - * @param {Number} order_id - * @returns {Promise<Order>} - */ - static async LOAD_ORDER_FROM_ID(order_id) { - return new Promise((resolve, reject) => { - __redis_client - .get(Order.STORAGE_KEY_PREFIX + order_id) - .then((order_data) => { - let order_date = dayjs.unix(order_id.substr(0, 10)); - if (order_data === null) { - let order_file = path.join( - Order.GET_ORDER_FILE_BASE_PATH(order_date), - order_id.toString() + ".json" - ); - let fs = require("fs"); - if (fs.existsSync(order_file)) { - order_data = JSON.parse(fs.readFileSync(order_file)); - if ("order_date" in order_data && order_data.order_date) { - order_date = dayjs(order_data.order_date); - } - } else { - throw "Could not find Order in our database!"; - } - } else { - order_data = JSON.parse(order_data); - } - let loaded_order = new Order( - order_data.order_id, - order_date, - order_data.amount, - order_data.price, - order_data.foreign_currency_price, - order_data.foreign_currency, - PaymentProcessor.LOAD_PAYMENT_PROCESSOR( - order_data.payment_processor - ), - order_data.amount_refund_requested, - order_data.order_key_charged, - order_data.payment_captured, - order_data.amount_refunded, - order_data.receipt_created, - order_data.civicrm_contribution_id, - order_data.order_key_changes - ); - resolve(loaded_order); - }) - .catch((reason) => { - reject(reason); - }); - }); - } - - async save(redis_expiration = null) { - let stored_data = { - order_id: this.#order_id, - order_date: this.#order_date.unix(), - expires_at: this.#expires_at, - amount: this.#amount, - price: this.#price, - foreign_currency_price: this.#foreign_currency_price, - foreign_currency: this.#foreign_currency, - 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: this.#payment_processor.serialize(), - civicrm_contribution_id: this.#civicrm_contribution_id, - order_key_changes: this.#order_key_changes, - }; - /** - * Completed Orders will be stored in Filesystem - * Uncompleted Orders will be stored in Redis - */ - if (redis_expiration === null) { - if (this.isPaymentCaptured()) { - redis_expiration = dayjs().add(Order.PURCHASE_LINK_TIME_DAYS, "day"); - } else { - redis_expiration = dayjs().add( - Order.PURCHASE_STORAGE_TIME_UNCOMPLETED_HOURS, - "hour" - ); - } - } else { - let min_expiration = dayjs().add( - Order.PURCHASE_STORAGE_TIME_UNCOMPLETED_HOURS, - "hour" - ); - if (this.isPaymentCaptured()) { - min_expiration = dayjs().add(Order.PURCHASE_LINK_TIME_DAYS, "day"); - } - if (min_expiration.isAfter(redis_expiration)) { - redis_expiration = min_expiration; - } - } - - let redis_key = Order.STORAGE_KEY_PREFIX + this.#order_id; - if (this.isPaymentCaptured()) { - let fs = require("fs"); - let order_file = path.join( - this.#order_path, - this.#order_id.toString() + ".json" - ); - // Create directory if it does not exist - if (!fs.existsSync(path.dirname(order_file))) { - fs.mkdirSync(path.dirname(order_file), { recursive: true }); - } - return __redis_client - .del(redis_key) - .then(() => this.updateOrderLinkTTL(redis_expiration)) - .then(() => - fs.writeFileSync(order_file, JSON.stringify(stored_data, null, 4)) - ); - } else { - // Store Order in Redis - return __redis_client - .pipeline() - .set(redis_key, JSON.stringify(stored_data)) - .expireat(redis_key, redis_expiration.unix()) - .exec() - .then(() => this.updateOrderLinkTTL(redis_expiration)); - } - } - - async delete() { - let redis_key = Order.STORAGE_KEY_PREFIX + this.#order_id; - if (!this.isPaymentCaptured()) { - return this.deleteOrderLink() - .then(() => __redis_client.del(redis_key)) - .then(async (deleted_keys) => { - if (deleted_keys > 0) { - return true; - } else { - return false; - } - }) - .catch(async () => { - return false; - }); - } else { - return false; - } - } - - 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 __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 __redis_client.expireat(write_lock_key, expire_at); - } - reject("Timed out waiting for write lock for Order"); - break; - } else { - if (expire_at >= 0) { - await __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 __redis_client.del(write_lock_key); - } - - addOrderKeyChanges(changes) { - this.#order_key_changes.push(changes); - } - - /** - * 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_key = - Order.PURCHASE_LINK_ORDER_TO_KEY_PREFIX + this.#order_id; - return __redis_client - .pipeline() - .set(redis_key_order_key, key) - .expireat(redis_key_order_key, expiration.unix()) - .exec(); - } - - async deleteOrderLink() { - let redis_key_order_key = - Order.PURCHASE_LINK_ORDER_TO_KEY_PREFIX + this.#order_id; - return this.getKeyFromOrderLink().then((key) => { - return __redis_client.pipeline().del(redis_key_order_key).exec(); - }); - } - - async createCiviCRMOrder() { - if (this.getPaymentProcessor().serialize().processor_name === "Manual") { - // Manual Orders will not get stored in CiviCRM - return; - } - - let civicrm_data = config.get("app.civicrm"); - if (!civicrm_data.enabled) { - return; - } - return fetch(civicrm_data.url + "/Contribution/create", { - method: "post", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - "X-Civi-Auth": `Bearer ${civicrm_data.api_key}`, - }, - body: - "params=" + - encodeURIComponent( - JSON.stringify({ - values: { - contact_id: civicrm_data.user_id, - financial_type_id: 5, - receive_date: this.#order_date.format("YYYY-MM-DD HH:mm:ss"), - total_amount: this.getPrice(), - is_test: process.env.NODE_ENV === "development" ? true : false, - is_pay_later: false, - tax_amount: this.getVatAmount(), - is_template: false, - }, - }) - ), - }) - .then((response) => { - if (response.status !== 200) { - throw "Error Creating Civicrm Contribution"; - } else { - return response.json(); - } - }) - .then((response_json) => { - this.#civicrm_contribution_id = response_json.values[0].id; - }); - } - - getOrderDate() { - return this.#order_date; - } - - /** - * - * @param {number} price - * @param {number} foreign_price - * @param {string} foreign_currency - */ - setPrice(price, foreign_price = 0, foreign_currency = "EUR") { - if ( - config.get("price.allowed_currencies").indexOf(foreign_currency) === -1 - ) { - throw new Error(`Currency ${foreign_currency} not allowed`); - } - if (this.isPaymentCaptured()) { - throw new Error("Cannot change details of processed Order"); - } - this.#price = price; - this.#foreign_currency_price = foreign_price; - this.#foreign_currency = foreign_currency; - - // Calculate new amount from price - this.#amount = parseInt( - Math.ceil((price / config.get("price.per_300")) * 300) - ); - } - - async attachReceipt(data, invoice_id) { - let civicrm_data = config.get("app.civicrm"); - if (!civicrm_data.enabled) { - return; - } - - return this.getWriteLock() - .then(() => - fetch(civicrm_data.url + "/Contribution/addReceipt", { - method: "post", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - "X-Civi-Auth": `Bearer ${civicrm_data.api_key}`, - }, - body: - "params=" + - encodeURIComponent( - JSON.stringify({ - receipt: data, - invoice_id, - id: this.#civicrm_contribution_id, - }) - ), - }) - ) - .then((response) => { - if (response.status !== 200) { - throw "Error Creating Civicrm Contribution"; - } else { - return response.json(); - } - }) - .then((response_json) => { - if (response_json["count"] !== 1) { - throw JSON.stringify(response_json, null, 4); - } else { - this.#receipt_created = true; - return response_json; - } - }) - .then(() => this.save()) - .then(() => this.releaseWriteLock()); - } - - async getReceipt() { - let civicrm_data = config.get("app.civicrm"); - if (!civicrm_data.enabled) { - return Promise.reject("No Civicrm Config supplied"); - } - return fetch(civicrm_data.url + "/Contribution/getReceipt", { - method: "post", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - "X-Civi-Auth": `Bearer ${civicrm_data.api_key}`, - }, - body: - "params=" + - encodeURIComponent( - JSON.stringify({ - id: this.#civicrm_contribution_id, - }) - ), - }) - .then((result) => result.json()) - .then((result_json) => { - if ( - typeof result_json.error_code !== "undefined" && - result_json.error_code !== 0 - ) { - throw result_json; - } else { - return result_json; - } - }); - } - - /** - * - * @param {dayjs.Dayjs} redis_expiration - * @returns - */ - async updateOrderLinkTTL(redis_expiration) { - let redis_key = Order.PURCHASE_LINK_ORDER_TO_KEY_PREFIX + this.#order_id; - - return __redis_client.expireat(redis_key, redis_expiration.unix()); - } - - /** - * 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_ORDER_TO_KEY_PREFIX + this.#order_id; - return __redis_client.get(redis_key).then(async (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 order_id = null; - let order_base = Math.round(new Date().getTime() / 1000); - let order_mutex = Math.floor(Math.random() * 10000); - do { - // make sure this order_id is not already registered - order_id = "" + order_base + order_mutex; - let order_lock = await __redis_client.setnx(order_id, true); - if (order_lock === 1) { - await __redis_client.expire(order_id, 5); - } else { - order_mutex++; - order_id = null; - } - } while (order_id === null); - return order_id; - } -} - -module.exports = Order; diff --git a/pass/app/PaymentReference.js b/pass/app/PaymentReference.js index 39cc5631d74cbc738ce05ba6a028d9ad64eac9cb..545f2ff63bfcdf0013cebe73d6119572a61c1f1e 100644 --- a/pass/app/PaymentReference.js +++ b/pass/app/PaymentReference.js @@ -197,7 +197,7 @@ class PaymentReference { } resolve( parseInt(matcher[2]) - - config.get("price.number_range.payment_reference") + config.get("price.number_range.payment_reference") ); }); } @@ -297,13 +297,13 @@ class PaymentReference { key.charge_key_order( charge_amount, this.id, - dayjs().add(config.get("keys.expiration_days"), "days") + dayjs().add(config.get("keys.expiration_years"), "year") ); } else { key.discharge_key( Math.abs(charge_amount), this.id, - dayjs().add(config.get("keys.expiration_days"), "days") + dayjs().add(config.get("keys.expiration_years"), "year") ); } return key.save().then(() => payment); @@ -318,7 +318,7 @@ class PaymentReference { */ async chargeKey() { return this.getKey(true).then((key) => { - let expiration = dayjs().add(config.get("keys.expiration_days"), "days"); + let expiration = dayjs().add(config.get("keys.expiration_years"), "year"); return this.setExpiration(expiration).then(() => { key.charge_key_order(this.amount, this.id, expiration); return key.save(); diff --git a/pass/app/payment_processor/Cash.js b/pass/app/payment_processor/Cash.js index 1bd7c5f264a2e1cd9ff67d7ba74cd8581e647292..6a730ff4c3ff20ae4abed2623b75beb24d0ffa75 100644 --- a/pass/app/payment_processor/Cash.js +++ b/pass/app/payment_processor/Cash.js @@ -16,10 +16,10 @@ class Cash extends PaymentProcessor { /** * - * @param {Order} order + * @param {PaymentReference} payment_reference * @param {i18next.TFunction} t */ - async createOrder(order, t) { + async createOrder(payment_reference, t) { return Promise.resolve(); } diff --git a/pass/app/payment_processor/Manual.js b/pass/app/payment_processor/Manual.js index f9f1b0a6761eb1db20c955b11d933704e04acddb..780b58a7003379605e84c9e67eb3fdbaa742b8c2 100644 --- a/pass/app/payment_processor/Manual.js +++ b/pass/app/payment_processor/Manual.js @@ -20,10 +20,10 @@ class Manual extends PaymentProcessor { /** * - * @param {Order} order + * @param {PaymentReference} payment_reference * @param {i18next.TFunction} t */ - async createOrder(order, t) { + async createOrder(payment_reference, t) { return Promise.resolve(); } @@ -31,9 +31,9 @@ class Manual extends PaymentProcessor { return Promise.resolve(); } /** - * - * @returns {boolean} - */ + * + * @returns {boolean} + */ isRefundSupported() { return false; } diff --git a/pass/app/payment_processor/Micropayment.js b/pass/app/payment_processor/Micropayment.js index b2f057b5c8488fa285c5c3baf991e269c2ff9e15..eccfa432d0c37477592e20ab16600c66f76abf90 100644 --- a/pass/app/payment_processor/Micropayment.js +++ b/pass/app/payment_processor/Micropayment.js @@ -1,10 +1,12 @@ -const Order = require("../Order"); const PaymentProcessor = require("./PaymentProcessor"); const i18next = require("i18next"); const config = require("config"); +const PaymentReference = require("../PaymentReference"); class Micropayment extends PaymentProcessor { - static get NAME() { return "Micropayment" }; + static get NAME() { + return "Micropayment"; + } /** * @var {string[]} */ @@ -38,29 +40,13 @@ class Micropayment extends PaymentProcessor { * Generates a URL for the customer where he is redirected to * to complete Payment. * - * @param {Order} order + * @param {PaymentReference} payment_reference * @param {i18next.TFunction} t * * @returns {Promise<URL>} */ - async createOrder(order, t) { - let payment_window_url = new URL( - `https://${this.#service}.micropayment.de/${this.#service}/event` - ); - let searchParams = new URLSearchParams({ - project: config.get("payments.micropayment.project"), - amount: order.getPrice() * 100, - title: t("product.name", { ns: "order" }), - mp_user_id: order.getOrderID(), - producttype: "quantity", - paytext: t("product.name", { ns: "order", count: order.getAmount() }), - testmode: process.env.NODE_ENV === "development" ? "1" : "0", - orderid: order.getOrderID(), - vatinfo: "1", - }); - payment_window_url.search = searchParams.toString(); - this.#capture_url = payment_window_url; - return Promise.resolve(this.#capture_url); + async createOrder(payment_reference, t) { + return Promise.resolve(); } /** @@ -73,9 +59,9 @@ class Micropayment extends PaymentProcessor { } /** - * - * @returns {boolean} - */ + * + * @returns {boolean} + */ isRefundSupported() { return true; } @@ -99,7 +85,7 @@ class Micropayment extends PaymentProcessor { return Promise.reject("Testmode Payment not accepted in production"); } if (!("currency" in query) || query.currency !== "EUR") { - return Promise.reject("Can only receive Payments in EUR") + return Promise.reject("Can only receive Payments in EUR"); } return Promise.resolve(); } diff --git a/pass/app/payment_processor/PaymentProcessor.js b/pass/app/payment_processor/PaymentProcessor.js index aa5594f8bad724f423b1153aab8e349427327437..704ed09ab815da276565e3ad521bcb32fad6f511 100644 --- a/pass/app/payment_processor/PaymentProcessor.js +++ b/pass/app/payment_processor/PaymentProcessor.js @@ -1,5 +1,5 @@ -const Order = require("../Order"); const i18next = require("i18next"); +const PaymentReference = require("../PaymentReference"); class PaymentProcessor { constructor() { @@ -10,10 +10,10 @@ class PaymentProcessor { /** * - * @param {Order} order + * @param {PaymentReference} payment_reference * @param {i18next.TFunction} t */ - async createOrder(order, t) { + async createOrder(payment_reference, t) { throw new Error("Function createOrder() must be implemented"); } @@ -21,9 +21,9 @@ class PaymentProcessor { throw new Error("Function captureOrder() must be implemented"); } /** - * - * @returns {boolean} - */ + * + * @returns {boolean} + */ isRefundSupported() { throw new Error("Function isRefundPossible() must be implemented"); } @@ -66,13 +66,9 @@ class PaymentProcessor { }; if (!(processor_name in processors)) { - throw new Error( - `Cannot find Payment Processor ${processor_name}` - ); + throw new Error(`Cannot find Payment Processor ${processor_name}`); } - return processors[processor_name].DESERIALIZE( - serialized_data - ); + return processors[processor_name].DESERIALIZE(serialized_data); } } diff --git a/pass/app/payment_processor/Paypal.js b/pass/app/payment_processor/Paypal.js index c8d1341ff8ce865b15bc56f186d1b20cd7864505..7dbad29ff2c5d0e20ee3f8ba85042bbbecf7ebb9 100644 --- a/pass/app/payment_processor/Paypal.js +++ b/pass/app/payment_processor/Paypal.js @@ -1,11 +1,8 @@ -const Order = require("../Order"); const PaymentReference = require("../PaymentReference"); const Payment = require("../Payment"); const dayjs = require("dayjs"); const config = require("config"); const PaymentProcessor = require("./PaymentProcessor"); -const Key = require("../Key"); -const RedisClient = require("../RedisClient"); const webhook_redis_key = "checkout_paypal_webhook_id"; const base = config.get(`payments.paypal.base`); const i18next = require("i18next"); @@ -156,7 +153,7 @@ class Paypal extends PaymentProcessor { if ( response_data.status !== "COMPLETED" || response_data.purchase_units[0].payments.captures[0].status !== - "COMPLETED" + "COMPLETED" ) { console.error(JSON.stringify(response_data)); throw "PAYMENT_NOT_COMPLETED_ERROR"; @@ -290,7 +287,8 @@ class Paypal extends PaymentProcessor { ); } } - }); + } + ); } else { console.log(req.body); throw "Webhook not implemented"; diff --git a/pass/app/pdf/OrderReceipt.js b/pass/app/pdf/OrderReceipt.js index d0c05835c75ed3cfb64876f8d590c9798f4b5452..a5491750688781eb7ce3effb757aa68e9071fc5b 100644 --- a/pass/app/pdf/OrderReceipt.js +++ b/pass/app/pdf/OrderReceipt.js @@ -1,4 +1,3 @@ -const Order = require("../Order"); const dayjs = require("dayjs"); const i18next = require("i18next"); const config = require("config"); @@ -15,7 +14,12 @@ class OrderReceipt { * @param {Object} invoice * @param {i18next.TFunction} t */ - static async CREATE_ORDER_RECEIPT(payment_reference, payment, receipt = undefined, t) { + static async CREATE_ORDER_RECEIPT( + payment_reference, + payment, + receipt = undefined, + t + ) { let letter_left_margin = OrderReceipt.CM_TO_POINTS(2.5, false); let letter_right_margin = OrderReceipt.CM_TO_POINTS(2, false); @@ -256,10 +260,10 @@ class OrderReceipt { .fontSize(8) .text( " 1 " + - payment.converted_currency + - " = " + - (payment.price / payment.converted_price).toFixed(6) + - "€", + payment.converted_currency + + " = " + + (payment.price / payment.converted_price).toFixed(6) + + "€", { width: OrderReceipt.CM_TO_POINTS(3.25, false), align: "right", diff --git a/pass/bin/cron b/pass/bin/cron index efc8a8488cb3fc7f8d873390ed4eaf5ffab84b96..be05d090625c3f23164834d152e8bba9f1612ce9 100644 --- a/pass/bin/cron +++ b/pass/bin/cron @@ -1,5 +1,4 @@ const dayjs = require("dayjs"); -const Order = require("../app/Order"); const RedisClient = require("../app/RedisClient"); const config = require("config"); const path = require("path"); diff --git a/pass/config/default.json b/pass/config/default.json index dad5b3abf4db99736c053fb90970273be3db4c87..64fda646a98820163a65b3386c257b10d1bc28e7 100644 --- a/pass/config/default.json +++ b/pass/config/default.json @@ -45,20 +45,8 @@ "price": { "per_token": 0.01, "vat": 7, - "purchasable": [ - 1000, - 2000, - 3000, - 4000, - 6000, - 12000 - ], - "allowed_currencies": [ - "EUR", - "USD", - "CAD", - "GBP" - ], + "purchasable": [1000, 2000, 3000, 4000, 6000, 12000], + "allowed_currencies": ["EUR", "USD", "CAD", "GBP"], "number_range": { "payment_reference": 0, "invoices": 0, @@ -85,7 +73,7 @@ "data_path": "/data" }, "keys": { - "expiration_days": 365 + "expiration_years": 2 }, "crypto": { "hmac_integrity_seed": "<insert_secret_for_hmac_seed>", @@ -110,4 +98,4 @@ } } } -} \ No newline at end of file +} diff --git a/pass/lang/de/agb.json b/pass/lang/de/agb.json index 373555b4589296a19e7aee2c7a8011780a272e20..60ab8238f52ba644a4b0fa767745398bf14949fb 100644 --- a/pass/lang/de/agb.json +++ b/pass/lang/de/agb.json @@ -38,7 +38,7 @@ "heading": "§4 Lieferung", "paragraphs": [ "(1) Sofern wir dies in der Produktbeschreibung nicht deutlich anders angegeben haben, sind alle von uns angebotenen Artikel sofort versandfertig. Da es sich um eine Dienstleistung handelt, ist kein Versand nötig und das Produkt wird sofort zur Verfügung gestellt.", - "(2) Das gekaufte Produkt wird von uns für einen Zeitraum von 365 Tagen ab Kaufdatum bereitgestellt. Nach Ablauf dieser Frist ist eine weitere Nutzung nicht möglich." + "(2) Das gekaufte Produkt wird von uns für einen Zeitraum von 2 Jahren ab Kaufdatum bereitgestellt. Nach Ablauf dieser Frist ist eine weitere Nutzung nicht möglich." ] }, { @@ -112,9 +112,7 @@ }, { "heading": "§9 Gewährleistung", - "texts": [ - "Es gelten die gesetzlichen Gewährleistungsregelungen." - ] + "texts": ["Es gelten die gesetzlichen Gewährleistungsregelungen."] }, { "heading": "§10 Vertragssprache", diff --git a/pass/lang/de/cost.json b/pass/lang/de/cost.json index 8082755965bf5e996ef151b4c1ac4fd3a03ae43d..7c55db9a351f957dae4bf185049b6fd2641a8988 100644 --- a/pass/lang/de/cost.json +++ b/pass/lang/de/cost.json @@ -12,7 +12,7 @@ "months_other": "{{count}} Monate", "short-info": [ { - "heading": "Gekaufte Suchen bleiben 1 Jahr lang gültig", + "heading": "Gekaufte Suchen bleiben 2 Jahre lang gültig", "text": "Ihre gekauften Tokens sind darauf ausgelegt so lange gültig zu bleiben, bis sie verbraucht wurden. Es gibt kein Abo." }, { diff --git a/pass/routes/admin/index.js b/pass/routes/admin/index.js index 15d341e52697ff2d4e04feab09eb2798c306ebaf..4748c30c82581601b5cc77c18a875037017a8c6d 100644 --- a/pass/routes/admin/index.js +++ b/pass/routes/admin/index.js @@ -2,8 +2,7 @@ var express = require("express"); var router = express.Router(); const config = require("config"); const dayjs = require("dayjs"); -const Order = require("../../app/Order"); -const { auth, requiresAuth, claimCheck } = require("express-openid-connect"); +const { auth } = require("express-openid-connect"); const { validationResult, matchedData, @@ -88,18 +87,21 @@ router.get( if (reqData.order && reqData.order.receipt_id !== null) { // There is already a receipt: Send it back - return Receipt.LOAD_RECEIPT_FROM_INTERNAL_ID(reqData.order.receipt_id).then(receipt => { - let receipt_data = Buffer.from(receipt.receipt, 'base64'); - res.header({ - "Content-Type": "application/pdf", - "Content-Disposition": `inline; filename=${receipt.public_id}.pdf` - }).send(receipt_data); + return Receipt.LOAD_RECEIPT_FROM_INTERNAL_ID( + reqData.order.receipt_id + ).then((receipt) => { + let receipt_data = Buffer.from(receipt.receipt, "base64"); + res + .header({ + "Content-Type": "application/pdf", + "Content-Disposition": `inline; filename=${receipt.public_id}.pdf`, + }) + .send(receipt_data); }); } if (data_complete) { // Create Invoice for preview - let receiptid = "xxxxxxxx"; return OrderReceipt.CREATE_ORDER_RECEIPT( res.locals.payment_reference, reqData.order, @@ -110,7 +112,7 @@ router.get( email: res.locals.email, address: res.locals.address, created_at: dayjs().format("YYYY-MM-DD HH:mm:ss"), - payment_id: reqData.order.id + payment_id: reqData.order.id, }), req.t ) @@ -120,9 +122,9 @@ router.get( let hasher = crypto.createHash("sha256"); hasher.update( reqData.company + - res.locals.name + - res.locals.email + - res.locals.address + res.locals.name + + res.locals.email + + res.locals.address ); res.locals.datahash = hasher.digest("hex"); res.render("admin/payments/receipt"); @@ -199,29 +201,35 @@ router.post( email: reqData.email, address: reqData.address, payment_id: reqData.order.id, - }).then((receipt) => { - reqData.id = receipt.public_id; - let receipt_data; - return OrderReceipt.CREATE_ORDER_RECEIPT( - payment_reference, - reqData.order, - receipt, - req.t - ) - .then((data) => { - receipt_data = Buffer.concat(data); - return receipt.attachReceipt(receipt_data.toString("base64")); - }) - .then((receipt) => reqData.order.setReceipt(receipt.id)).then(() => { - res.type("pdf").header({ - "Content-Disposition": `inline; filename=${receipt.public_id}.pdf` - }).send(receipt_data); - }) - }).catch(reason => { - console.error(reason); - res.redirect(url.toString()); - return; - }); + }) + .then((receipt) => { + reqData.id = receipt.public_id; + let receipt_data; + return OrderReceipt.CREATE_ORDER_RECEIPT( + payment_reference, + reqData.order, + receipt, + req.t + ) + .then((data) => { + receipt_data = Buffer.concat(data); + return receipt.attachReceipt(receipt_data.toString("base64")); + }) + .then((receipt) => reqData.order.setReceipt(receipt.id)) + .then(() => { + res + .type("pdf") + .header({ + "Content-Disposition": `inline; filename=${receipt.public_id}.pdf`, + }) + .send(receipt_data); + }); + }) + .catch((reason) => { + console.error(reason); + res.redirect(url.toString()); + return; + }); reqData.id = await OrderReceipt.CREATE_UNIQUE_RECEIPT_ID(); OrderReceipt.CREATE_ORDER_RECEIPT(reqData.order, reqData, req.t) .then((data) => { @@ -298,7 +306,7 @@ router.post( converted_currency: price_data.converted_currency, payment_processor: Cash.NAME, }) - .then((payment) => { + .then(() => { res.locals.orderid = payment_reference.public_id; res.render("admin/payments/cash_success"); }) diff --git a/pass/routes/api.js b/pass/routes/api.js index 4ec5e6fb8afe95b956ae9d586776b219430e7ef6..36cfb8560dadae05747a3bb1e96bbb665723151c 100644 --- a/pass/routes/api.js +++ b/pass/routes/api.js @@ -5,13 +5,10 @@ const { body, validationResult } = require("express-validator"); const config = require("config"); const Key = require("../app/Key"); -const Order = require("../app/Order"); -const Manual = require("../app/payment_processor/Manual"); const Crypto = require("../app/Crypto"); const dayjs = require("dayjs"); const NodeRSA = require("node-rsa"); const PaymentReference = require("../app/PaymentReference"); -const RedisClient = require("../app/RedisClient"); router.use("/key", authorizedOnly); @@ -32,23 +29,29 @@ router.post("/key/create", (req, res) => { } } - return Key.GET_NEW_KEY().then(key => { - return PaymentReference.CREATE_NEW_REQUEST(amount, key.get_key(), dayjs().add("10", "years")).then(payment_reference => { - return payment_reference.chargeKey().then(() => { - return res.status(201).json({ - key: key.get_key(), - payment_reference: payment_reference.public_id, - charged: payment_reference.amount, + return Key.GET_NEW_KEY() + .then((key) => { + return PaymentReference.CREATE_NEW_REQUEST( + amount, + key.get_key(), + dayjs().add("10", "years") + ).then((payment_reference) => { + return payment_reference.chargeKey().then(() => { + return res.status(201).json({ + key: key.get_key(), + payment_reference: payment_reference.public_id, + charged: payment_reference.amount, + }); }); - }) - }); - }).catch((reason) => { - res.status(423).json({ - code: 423, - error: reason, - charged: 0, + }); + }) + .catch((reason) => { + res.status(423).json({ + code: 423, + error: reason, + charged: 0, + }); }); - }); }); router.get("/key/:key", (req, res) => { @@ -111,23 +114,29 @@ router.post("/key/:key/charge", (req, res) => { return; } } - return Key.GET_KEY(req.params.key).then(key => { - return PaymentReference.CREATE_NEW_REQUEST(amount, key.get_key(), dayjs().add("10", "years")).then(payment_reference => { - return payment_reference.chargeKey().then(() => { - return res.status(201).json({ - key: key.get_key(), - payment_reference: payment_reference.public_id, - charged: payment_reference.amount, + return Key.GET_KEY(req.params.key) + .then((key) => { + return PaymentReference.CREATE_NEW_REQUEST( + amount, + key.get_key(), + dayjs().add("10", "years") + ).then((payment_reference) => { + return payment_reference.chargeKey().then(() => { + return res.status(201).json({ + key: key.get_key(), + payment_reference: payment_reference.public_id, + charged: payment_reference.amount, + }); }); - }) - }); - }).catch((reason) => { - res.status(423).json({ - code: 423, - error: reason, - charged: 0, + }); + }) + .catch((reason) => { + res.status(423).json({ + code: 423, + error: reason, + charged: 0, + }); }); - }); }); router.get("/token/pubkey", async (req, res) => { diff --git a/pass/routes/checkout/cash.js b/pass/routes/checkout/cash.js index 6e359014193f02e558b4c34f88a09034c778bea9..909ba58a7b599e8c3c13dc459b99b8a359c1e2d1 100644 --- a/pass/routes/checkout/cash.js +++ b/pass/routes/checkout/cash.js @@ -1,13 +1,7 @@ var express = require("express"); -const Order = require("../../app/Order"); -const Manual = require("../../app/payment_processor/Manual"); -const config = require("config"); var router = express.Router({ mergeParams: true }); const { query, validationResult, matchedData } = require("express-validator"); const dayjs = require("dayjs"); -const crypto = require("crypto"); -const RedisClient = require("../../app/RedisClient"); -const Cash = require("../../app/payment_processor/Cash"); const PaymentReference = require("../../app/PaymentReference"); router.use("/", (req, res, next) => { diff --git a/pass/routes/checkout/checkout.js b/pass/routes/checkout/checkout.js deleted file mode 100644 index c0351865500779064ebd7b94a6dcba78e604eaf1..0000000000000000000000000000000000000000 --- a/pass/routes/checkout/checkout.js +++ /dev/null @@ -1,184 +0,0 @@ -const Crypto = require("../../app/Crypto.js"); -const Order = require("../../app/Order.js"); -const config = require("config"); -const dayjs = require("dayjs"); - -var express = require("express"); -var router = express.Router(); -const { query, body, validationResult } = require("express-validator"); - -/* GET home page. */ -router.get( - "/", - query("amount") - .isInt({ min: -1, max: 12 }) - .withMessage("Amount needs to be between 0 and 4.") - .toInt(), - async function (req, res, next) { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - - /** - * The user interface allows either 100 searches or steps of 250 searches (up to 12 * 250 = 3000) - * 100 searches are a little bit more expensive than 250 - * - * an amount of 0 corresponds to 100 searches - * an amount of 1-12 corresponds to 1-12 * 250 searches - */ - let params = { - amount: req.query.amount === 0 ? 1 : req.query.amount, - unit_size: req.query.amount === 0 ? 100 : 250, - price_per_unit: - req.query.amount === 0 ? Order.PRICE_FOR_100 : Order.PRICE_FOR_250, - order_id: await generate_unique_order_id(), - payments: { - paypal: { - client_id: config.get(`payments.paypal.client_id`), - }, - }, - }; - - let crypto = new Crypto(); - let order_date = dayjs(); - let expiration_date = order_date.add( - Order.PURCHASE_STORAGE_TIME_MONTHS, - "month" - ); - let private_key = await crypto.private_key_get(order_date, expiration_date); - params.crypto = { - N: private_key.keyPair.n, - E: private_key.keyPair.e, - }; - params.expires_at = expiration_date.format("YYYY-MM-DD"); - - // Generate hmac hash of the payment data so we are able to verify them when the client submits them again - // Generate hmac hash of the payment data so we are able to verify them when the client submits them again - params.integrity = crypto.createIntegrityHash( - params.order_id, - params.expires_at, - params.amount, - params.unit_size, - params.price_per_unit, - params.crypto.N, - params.crypto.E - ); - - res.render("checkout/checkout", params); - } -); - -router.use( - "/payment/order", - body("amount") - .isInt({ min: 1, max: 12 }) - .withMessage("Invalid amount submitted") - .toInt(), - body("unit_size").isIn(["100", "250"]).toInt(), - body("price_per_unit") - .isCurrency({ symbol: "", allow_negatives: false, thousands_separator: "" }) - .withMessage("Invalid Price Value.") - .toFloat(), - body("order_id").isInt({ min: 0 }).withMessage("Invalid `order_id` value"), - body("expires_at") - .isDate() - .matches(/^\d{4}-\d{2}-\d{2}$/) - .withMessage("Expiration date is not correct"), - body("public_key_e") - .isNumeric({ no_symbols: true }) - .withMessage("Invalid Public Key"), - body("public_key_n") - .isNumeric({ no_symbols: true }) - .withMessage("Invalid Public Key"), - body("integrity") - .isHash("sha256") - .custom((value, { req }) => { - let crypto = new Crypto(); - if ( - !crypto.validateIntegrityHash( - value, - req.body.order_id, - req.body.expires_at, - req.body.amount, - req.body.unit_size, - req.body.price_per_unit, - req.body.public_key_n, - req.body.public_key_e - ) - ) { - console.log("Invalid integrity"); - return Promise.reject("Integrity is not matching"); - } - return true; - }) - .withMessage( - "Value for `integrity` must be a SHA256 Hash and validate against the purchase data." - ), - body("encrypted_sales_receipts") - .custom((value, { req }) => { - if (!Array.isArray(value)) { - return Promise.reject("Parameter is not an array"); - } - for (let i = 0; i < value.length; i++) { - if (!/^\d+$/.test(value[i])) { - return Promise.reject( - "Encrypted Sales Receipt contains invalid data" - ); - } - } - let expected_ticket_count = (req.body.amount * req.body.unit_size) / 50; - if (expected_ticket_count % 1 !== 0) { - return Promise.reject("Expected ticket count is not an integer."); - } - if (value.length !== expected_ticket_count) { - return Promise.reject("Two many receipts compared to the order."); - } - return true; - }) - .withMessage("Invalid Sales Receipt"), - (req, res, next) => { - // This route gets called when the user initiates a payment: Validate the submitted data here - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - - next("route"); - } -); - -/** Cancel is the same for all payment gateways */ -router.post("/payment/order/*/cancel", (req, res) => { - Order.LOAD_ORDER_FROM_ID(req.body.order_id).then((loaded_order) => { - if (loaded_order.isPaymentComplete()) { - res.status(400).json({ - msg: "Cannot delete a completed order", - }); - return; - } - loaded_order.delete().then((success) => { - if (success) { - res.status(200).json({ - msg: "Order deleted", - }); - } else { - res.status(400).json({ - errors: [ - { - msg: "Could not delete specified order", - }, - ], - }); - } - }); - }); -}); - -var developmentRouter = require("./development.js"); -router.use("/payment/order/development", developmentRouter); - -var paypalRouter = require("./paypal.js"); -router.use("/payment/order/paypal", paypalRouter); - -module.exports = router; diff --git a/pass/routes/checkout/development.js b/pass/routes/checkout/development.js deleted file mode 100644 index d37388586a4655144fa661c0705aa5a8366f56ce..0000000000000000000000000000000000000000 --- a/pass/routes/checkout/development.js +++ /dev/null @@ -1,56 +0,0 @@ -var express = require("express"); -var router = express.Router(); - -const config = require("config"); -const Order = require("../../app/Order.js"); - -router.post( - "/", - (req, res, next) => { - if (process.env.NODE_ENV !== "development") { - res.status(400).json({ - errors: [ - { msg: "Payment method only allowed in development environment." }, - ], - }); - } else { - next(); - } - }, - (req, res) => { - // Order data is validated: Create and store the order in the redis database - let order = new Order( - req.body.order_id, - req.body.expires_at, - req.body.amount, - req.body.unit_size, - req.body.price_per_unit, - req.body.encrypted_sales_receipts - ); - - order - .save() - .then(() => { - order.signOrder().then(() => { - order.save().then(() => { - res.json({ - order_id: req.body.order_id, - expires_at: req.body.expires_at, - signatures: order.getSignatures(), - }); - }); - }); - }) - .catch((reason) => { - return res.status(400).json({ - errors: [ - { - msg: reason, - }, - ], - }); - }); - } -); - -module.exports = router; diff --git a/pass/routes/checkout/manual.js b/pass/routes/checkout/manual.js index b03b72e9ae29b2d05761cf2acbaf676a73f7dfc1..eb961752ea3949a0a548265de7afe880ff732588 100644 --- a/pass/routes/checkout/manual.js +++ b/pass/routes/checkout/manual.js @@ -1,7 +1,5 @@ var express = require("express"); -const Order = require("../../app/Order"); -const Manual = require("../../app/payment_processor/Manual"); -const config = require("config"); +const PaymentReference = require("../../app/PaymentReference"); var router = express.Router({ mergeParams: true }); router.use("/", (req, res, next) => { @@ -24,27 +22,14 @@ router.get("/", (req, res) => { }); router.post("/", (req, res) => { - /** - * @type {Order} - */ - let new_order = null; - Order.CREATE_NEW_ORDER( + return PaymentReference.CREATE_NEW_REQUEST( req.params.amount, - req.data.key.key.get_key(), - new Manual(req.body.note), - req.t + req.data.key.key.get_key() ) - .then((order) => { - new_order = order; - return order.captureOrder(); - }) - .then(() => new_order.chargeKey()) + .then((payment_reference) => payment_reference.chargeKey()) .then(() => { let redirect_url = - `${res.locals.baseDir}/key/` + - req.data.key.key.get_key() + - "/orders/" + - new_order.getOrderID(); + `${res.locals.baseDir}/key/` + req.data.key.key.get_key(); res.redirect(redirect_url); }); }); diff --git a/pass/routes/checkout/micropayment.js b/pass/routes/checkout/micropayment.js index abebc4e0c726774b1b594797f85d02d63443d87b..f3a5e4bff703574da3d91478dd84a3c6475e5357 100644 --- a/pass/routes/checkout/micropayment.js +++ b/pass/routes/checkout/micropayment.js @@ -2,31 +2,39 @@ var express = require("express"); var router = express.Router({ mergeParams: true }); const config = require("config"); -const Order = require("../../app/Order"); const PaymentReference = require("../../app/PaymentReference"); const Micropayment = require("../../app/payment_processor/Micropayment"); const crypto = require("crypto"); router.get("/event", (req, res) => { Micropayment.VERIFY_WEBHOOK(req.query) - .then(() => PaymentReference.LOAD_FROM_PUBLIC_ID(req.query.paymentreference)) + .then(() => + PaymentReference.LOAD_FROM_PUBLIC_ID(req.query.paymentreference) + ) .then((payment_reference) => { let price = (req.query.amount / 100).toFixed(2); if (req.query.function === "refund") { price *= -1; } - let redirect_url = new URL(`${res.locals.baseDir}/key/` + payment_reference.key.get_key() + "/orders/" + payment_reference.public_id); + let redirect_url = new URL( + `${res.locals.baseDir}/key/` + + payment_reference.key.get_key() + + "/orders/" + + payment_reference.public_id + ); if (req.query.function !== "billing" && req.query.function !== "refund") { return redirect_url; } - return payment_reference.createPayment({ - price: (req.query.amount / 100).toFixed(2), - converted_currency: req.query.currency, - converted_price: (req.query.amount / 100).toFixed(2), - payment_processor: Micropayment.NAME, - payment_processor_id: req.query.auth, - payment_processor_data: req.query - }).then(payment => redirect_url); + return payment_reference + .createPayment({ + price: (req.query.amount / 100).toFixed(2), + converted_currency: req.query.currency, + converted_price: (req.query.amount / 100).toFixed(2), + payment_processor: Micropayment.NAME, + payment_processor_id: req.query.auth, + payment_processor_data: req.query, + }) + .then(() => redirect_url); }) .then((redirect_url) => { let response = { @@ -82,39 +90,41 @@ router.get("/:service", (req, res) => { }); router.post("/:service", async (req, res) => { - return PaymentReference.CREATE_NEW_REQUEST(req.data.checkout.amount, - req.data.key.key.get_key()).then(payment_reference => { - /** - * @type {URL} - */ - let payment_window_url = - req.data.checkout.payment.micropayment.payment_window_url; - let searchParams = { - project: config.get("payments.micropayment.project"), - amount: payment_reference.price * 100, - title: "MetaGer Schlüssel", - mp_user_id: payment_reference.public_id, - producttype: "quantity", - paytext: `${payment_reference.amount} MetaGer Token`, - testmode: process.env.NODE_ENV === "development" ? "1" : "0", - paymentreference: payment_reference.public_id, - vatinfo: "1", - }; - let seal = ""; - for (let key in searchParams) { - seal += `${key}=${searchParams[key]}&`; - } - seal = seal.replace(/&$/, ""); - seal += config.get("payments.micropayment.access_key"); + return PaymentReference.CREATE_NEW_REQUEST( + req.data.checkout.amount, + req.data.key.key.get_key() + ).then((payment_reference) => { + /** + * @type {URL} + */ + let payment_window_url = + req.data.checkout.payment.micropayment.payment_window_url; + let searchParams = { + project: config.get("payments.micropayment.project"), + amount: payment_reference.price * 100, + title: "MetaGer Schlüssel", + mp_user_id: payment_reference.public_id, + producttype: "quantity", + paytext: `${payment_reference.amount} MetaGer Token`, + testmode: process.env.NODE_ENV === "development" ? "1" : "0", + paymentreference: payment_reference.public_id, + vatinfo: "1", + }; + let seal = ""; + for (let key in searchParams) { + seal += `${key}=${searchParams[key]}&`; + } + seal = seal.replace(/&$/, ""); + seal += config.get("payments.micropayment.access_key"); - let hasher = crypto.createHash("md5"); - seal = hasher.update(seal).digest("hex"); - searchParams.seal = seal; + let hasher = crypto.createHash("md5"); + seal = hasher.update(seal).digest("hex"); + searchParams.seal = seal; - payment_window_url.search = (new URLSearchParams(searchParams)).toString(); + payment_window_url.search = new URLSearchParams(searchParams).toString(); - res.redirect(payment_window_url.toString()); - }); + res.redirect(payment_window_url.toString()); + }); }); module.exports = router; diff --git a/pass/routes/checkout/paypal.js b/pass/routes/checkout/paypal.js index c92397d7c13f0a15fe7d79a31bf447893d7f2633..a337609acfa207527d4d1f5acc83bc758b0aa0a6 100644 --- a/pass/routes/checkout/paypal.js +++ b/pass/routes/checkout/paypal.js @@ -2,10 +2,7 @@ var express = require("express"); var router = express.Router({ mergeParams: true }); const config = require("config"); -const Order = require("../../app/Order.js"); const PaymentReference = require("../../app/PaymentReference.js"); -const Payment = require("../../app/Payment"); -const Key = require("../../app/Key.js"); const Paypal = require("../../app/payment_processor/Paypal.js"); const { body } = require("express-validator"); @@ -73,7 +70,7 @@ router.post("/:funding_source/order/create", async (req, res) => { 600, paypal.getOrderId() ) - .then((result) => { + .then(() => { return res.status(200).json({ payment_reference: payment_reference.public_id, paypal_order_id: paypal.getOrderId(), @@ -224,24 +221,4 @@ module.exports = router; ////////////////////// -async function refundPayment(capture_ids) { - const accessToken = await generateAccessToken(); - let promises = []; - capture_ids.forEach((capture_id) => { - promises.push( - fetch(`${base}/v2/payments/captures/${capture_id}/refund`, { - method: "post", - headers: { - Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - note_to_payer: "Something went wrong when processing your payment.", - }), - }) - ); - }); - return Promise.all(promises); -} - // generate an access token using client id and app secret diff --git a/pass/routes/index.js b/pass/routes/index.js index a37729052c7e89cc9c3ea55930eafaa5390fb1b2..7740df24f87793e9d5628b936ed2f4c09a979a39 100644 --- a/pass/routes/index.js +++ b/pass/routes/index.js @@ -1,5 +1,4 @@ var express = require("express"); -const Order = require("../app/Order.js"); var router = express.Router({ mergeParams: true }); var lessMiddleware = require("less-middleware"); @@ -23,7 +22,7 @@ router.use("/admin", adminRouter); router.use("/help", helpRouter); /* GET home page. */ -router.get("/", function (req, res, next) { +router.get("/", function (req, res) { req.i18n.setDefaultNamespace("index"); // Default NS for localized Strings res.render("index"); }); diff --git a/pass/routes/orders/orders.js b/pass/routes/orders/orders.js index 8365b07637c5aeecc939d013c75c67e4358b31ef..028cd9081ff200536f563f44f9eec3768e6c8172 100644 --- a/pass/routes/orders/orders.js +++ b/pass/routes/orders/orders.js @@ -7,7 +7,6 @@ const { validationResult, matchedData, } = require("express-validator"); -const Order = require("../../app/Order"); const OrderReceipt = require("../../app/pdf/OrderReceipt"); const PaymentReference = require("../../app/PaymentReference"); const Payment = require("../../app/Payment"); @@ -22,7 +21,7 @@ router.use("/", (req, res, next) => { } next(); }); -router.get("/", (req, res, next) => { +router.get("/", (req, res) => { req.data.page = "order"; req.data.css.push(`${res.locals.baseDir}/styles/orders/orders.css`); res.render("key", req.data); @@ -31,7 +30,7 @@ router.get("/", (req, res, next) => { router.post( "/", body("payment_reference").matches(/^(Z)?(\d+)$/), - (req, res, next) => { + (req, res) => { let errors = validationResult(req); if (!errors.isEmpty()) { req.data.page = "order"; @@ -60,7 +59,9 @@ router.post( res.render("key", req.data); } else { res.redirect( - `${res.locals.baseDir}/key/${req.data.key.key.get_key()}/orders/${queryData.payment_reference}#order` + `${res.locals.baseDir}/key/${req.data.key.key.get_key()}/orders/${ + queryData.payment_reference + }#order` ); } }); @@ -118,21 +119,31 @@ router.use( req.data.order.payments = payments; req.data.css.push(`${res.locals.baseDir}/styles/orders/order.css`); - req.data.links.order_url = `${res.locals.baseDir - }/key/${req.data.key.key.get_key()}/orders/${queryData.payment_reference.public_id - }#order`; - req.data.links.order_actions_base = `${res.locals.baseDir - }/key/${req.data.key.key.get_key()}/orders/${queryData.payment_reference.public_id - }`; - req.data.links.receipt_url = `${res.locals.baseDir - }/key/${req.data.key.key.get_key()}/orders/${queryData.payment_reference.public_id - }/pdf`; - req.data.links.invoice_url = `${res.locals.baseDir - }/key/${req.data.key.key.get_key()}/orders/${queryData.payment_reference.public_id - }/invoice#invoice-form`; - req.data.links.refund_url = `${res.locals.baseDir - }/key/${req.data.key.key.get_key()}/orders/${queryData.payment_reference.public_id - }/refund#refund-form`; + req.data.links.order_url = `${ + res.locals.baseDir + }/key/${req.data.key.key.get_key()}/orders/${ + queryData.payment_reference.public_id + }#order`; + req.data.links.order_actions_base = `${ + res.locals.baseDir + }/key/${req.data.key.key.get_key()}/orders/${ + queryData.payment_reference.public_id + }`; + req.data.links.receipt_url = `${ + res.locals.baseDir + }/key/${req.data.key.key.get_key()}/orders/${ + queryData.payment_reference.public_id + }/pdf`; + req.data.links.invoice_url = `${ + res.locals.baseDir + }/key/${req.data.key.key.get_key()}/orders/${ + queryData.payment_reference.public_id + }/invoice#invoice-form`; + req.data.links.refund_url = `${ + res.locals.baseDir + }/key/${req.data.key.key.get_key()}/orders/${ + queryData.payment_reference.public_id + }/refund#refund-form`; /*if (req.data.order.order.isReceiptCreated()) { req.data.order.download_invoice_url = `${res.locals.baseDir}/key/${req.data.key.key.get_key() }/orders/${req.data.order.order.getOrderID()}/invoice/download`; @@ -177,7 +188,7 @@ router.get("/:payment_reference/:order_id/pdf", (req, res) => { .status(200) .header({ "Content-Type": "application/pdf", - "Content-Disposition": `inline, filename=${req.params.order_id}.pdf` + "Content-Disposition": `inline, filename=${req.params.order_id}.pdf`, }) .send(Buffer.concat(data)); }) diff --git a/pass/routes/redeem.js b/pass/routes/redeem.js deleted file mode 100644 index b11108d81b582420770bc058d9c9064387974a4d..0000000000000000000000000000000000000000 --- a/pass/routes/redeem.js +++ /dev/null @@ -1,247 +0,0 @@ -var express = require("express"); -var router = express.Router(); -const { query, body, validationResult } = require("express-validator"); - -const config = require("config"); -const dayjs = require("dayjs"); -const Crypto = require("../app/Crypto"); -var customParseFormat = require("dayjs/plugin/customParseFormat"); -const Key = require("../app/Key"); -const Order = require("../app/Order"); -const path = require('path'); -const fs = require('fs'); -const readline = require('readline'); -dayjs.extend(customParseFormat); - -/** - * This routes are called after a purchase and intended to - * exchange a purchase receipt against a MetaGer-Pass Key - * - * This middleware validates the purchase receipt for all of the other routes - */ -router.use( - "/", - body("expiration_month") - .matches(/^\d{4}-\d{2}$/) - .custom((expiration_month, { req }) => { - dayjs(expiration_month + "-01"); - return true; - }) - .customSanitizer(expiration_month => { - return dayjs(expiration_month); - }) - .withMessage("Invalid Expiration Month supplied"), - body("generation_month") - .matches(/^\d{4}-\d{2}$/) - .custom((generation_month, { req }) => { - return dayjs(generation_month + "-01"); - }) - .customSanitizer(generation_month => { - return dayjs(generation_month); - }) - .withMessage("Invalid Generation Month supplied"), - body("receipts").custom(async (receipts, { req }) => { - return new Promise((resolve, reject) => { - new Crypto() - .validateMetaGerPassCode( - req.body.generation_month, - req.body.expiration_month, - req.body.metager_pass_codes - ) - .then(() => { - resolve(true); - }) - .catch((reason) => { - reject(reason); - }); - }); - }), - (req, res, next) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - next("route"); - } -); - -/* Recharge a MetaGer-Pass Key */ -router.post("/", body("metager_pass_key").isWhitelisted(Key.KEY_CHARSET).isLength({ min: 6, max: 20 }), async (req, res, next) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - - let max_recharge_tries = 10; - let max_recharge_tries_hours = 1; // Defines in which time frame the recharge tries are counted; recharge tries get reset after this amount of hours without recharge try have passed - - let charge_amount = 0; - - // Redis Client - let Redis = require("ioredis"); - let redis_client = new Redis({ - host: config.get("redis.host"), - }); - // Dayjs - let dayjs = require("dayjs"); - - let key_recharge_cache_keys = []; - let key_recharge_cache_prefix = "recharge_key_cache"; - for (let i = 0; i < req.body.metager_pass_codes.length; i++) { - let code = req.body.metager_pass_codes[i].code; - key_recharge_cache_keys.push(key_recharge_cache_prefix + "_" + code); - charge_amount += Order.PACKET_SIZE; - } - - // Check one or more of the codes was already used to redeem a MetaGer-Pass Key - let order_month = dayjs(req.body.generation_month); - let redeem_file_path = path.join( - config.get("storage.data_path"), - process.env.NODE_ENV, - order_month.format("YYYY"), - order_month.format("MM"), - "redeemed.json"); - if (!fs.existsSync(path.dirname(redeem_file_path))) { - fs.mkdirSync(path.dirname(redeem_file_path), { recursive: true }); - } - - if (fs.existsSync(redeem_file_path)) { - let rl = readline.createInterface({ - input: fs.createReadStream(redeem_file_path), - output: process.stdout, - terminal: false - }); - for await (const line of rl) { - for (let i = 0; i < req.body.metager_pass_codes.length; i++) { - if (req.body.metager_pass_codes[i].code === line.trim()) { - return res.status(400).json({ - status: "FAILED", - msg: ["One or more of the provided MetaGer-Pass Codes are already redeemed."] - }); - } - } - } - } - - - let recharge_tries = 0; - redis_client.mget(key_recharge_cache_keys).then(async response => { - for (let i = 0; i < response.length; i++) { - let tries = response[i]; - if (tries && tries > recharge_tries) { - recharge_tries = tries; - } - } - - if (recharge_tries >= max_recharge_tries) { - res.status(400).json({ - status: "FAILED", - msg: "Too many failed attempts against non existing MetaGer-Pass Keys. Please try again later." - }); - } else { - recharge_tries++; - - let redis_pipeline = redis_client.pipeline(); - let expiration = dayjs().add(max_recharge_tries_hours, "hour"); - for (let i = 0; i < key_recharge_cache_keys.length; i++) { - redis_pipeline.set(key_recharge_cache_keys[i], recharge_tries); - redis_pipeline.expireat(key_recharge_cache_keys[i], expiration.unix()); - } - await redis_pipeline.exec(); - - Key.CHARGE_EXISTING_KEY(req.body.metager_pass_key, charge_amount).then(result => { - // Key is charged store the redeem codes into filesystem so they can only be used once - for (let i = 0; i < req.body.metager_pass_codes.length; i++) { - fs.appendFileSync(redeem_file_path, req.body.metager_pass_codes[i].code + "\n"); - } - - res.json(result); - }).catch(reason => { - res.status(400).json({ - status: "FAILED", - msg: [reason] - }); - }); - } - }); -}); - -/* Create a new MetaGer-Pass Key from purchase receipt */ -router.post("/create", async (req, res, next) => { - let key = ""; - - // Redis Client - let Redis = require("ioredis"); - let redis_client = new Redis({ - host: config.get("redis.host"), - }); - // Dayjs - let dayjs = require("dayjs"); - - let key_cache_redis_keys = []; - let key_redis_cache_prefix = "create_key_cache"; - // Check if the user already created a key which is in redis - for (let i = 0; i < req.body.metager_pass_codes.length; i++) { - let code = req.body.metager_pass_codes[i].code; - key_cache_redis_keys.push(key_redis_cache_prefix + "_" + code); - } - redis_client.mget(key_cache_redis_keys).then(async response => { - let existing_key = null; - let existing_key_expiration = null; - // Check if there is at least one existing key associated with the pass codes - for (let i = 0; i < response.length; i++) { - if (response[i]) { - existing_key = response[i]; - existing_key_expiration = await redis_client.expiretime(key_cache_redis_keys[i]); - break; - } - } - if (existing_key) { - // If there is we will return it and store this key to all the empty keys - for (let i = 0; i < response.length; i++) { - if (!response[i]) { - await redis_client.set(key_cache_redis_keys[i], existing_key); - await redis_client.expireat(key_cache_redis_keys[i], existing_key_expiration); - } - } - res.json({ - status: "SUCCESS", - metager_pass_key: { - key: existing_key, - valid_until: dayjs.unix(existing_key_expiration).format() - } - }); - } else { - // There is no Key yet. Let's generate one - let expiration = (new dayjs()).add(6, 'hour').unix(); - - Key.CREATE_NEW_KEY(expiration).then(async key => { - for (let i = 0; i < key_cache_redis_keys.length; i++) { - await redis_client.set(key_cache_redis_keys[i], key); - await redis_client.expireat(key_cache_redis_keys[i], expiration); - } - res.json({ - status: "SUCCESS", - metager_pass_key: { - key: key, - valid_until: dayjs.unix(expiration).format() - } - }); - }).catch(reason => { - res.status(400).json({ - status: "FAILURE", - msg: reason - }); - }); - } - }).catch(reason => { - res.status(400).json({ - status: "FAILURE", - msg: reason - }); - }); - - -}); - -module.exports = router;