diff --git a/package.json b/package.json index 6864c3a..145da48 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cpg-api", - "version": "v3.1", + "version": "v3.2", "description": "Central Payment Gateway", "main": "./build/Main.js", "dependencies": { @@ -39,6 +39,8 @@ "graphql-compose-mongoose": "^9.7.1", "graphql-resolvers": "^0.4.2", "inquirer": "^8.2.2", + "inquirer-search-checkbox": "^1.0.0", + "inquirer-search-list": "^1.2.6", "jimp": "^0.16.1", "jsonwebtoken": "^8.5.1", "mongoose": "^6.1.4", @@ -48,6 +50,7 @@ "nodemailer": "^6.7.0", "npm": "^7.20.5", "paypal-rest-sdk": "^1.8.1", + "pdfkit-table": "^0.1.86", "pg": "^8.7.1", "pg-hstore": "^2.3.4", "prompt": "^1.2.0", diff --git a/src/Admin/AdminHandler.ts b/src/Admin/AdminHandler.ts index b6db48e..8f87703 100644 --- a/src/Admin/AdminHandler.ts +++ b/src/Admin/AdminHandler.ts @@ -1,27 +1,10 @@ -import { stripIndent } from "common-tags"; -import prompt from "prompt"; -import { CacheAdmin } from "../Cache/Admin.cache"; -import { cron_chargeStripePayment, cron_notifyInvoices, cron_notifyLateInvoicePaid } from "../Cron/Methods/Invoices.cron.methods"; -import AdminModel from "../Database/Models/Administrators.model"; -import ConfigModel from "../Database/Models/Configs.model"; import Logger from "../Lib/Logger"; -import { getPlugins, installPlugin } from "../Plugins/PluginHandler"; -import createAdmin from "./CreateAdmin"; import chalk from "chalk"; import clear from "clear"; import figlet from "figlet"; import Prompt, { cacheCommands } from "./Commands/Prompt"; import inquirer from 'inquirer'; -export interface ICommandsAdmin -{ - [key: string]: { - description: string; - method: any; - [key: string]: any; - } -} - export default class AdminHandler { @@ -35,6 +18,8 @@ export default class AdminHandler }) ) ) + inquirer.registerPrompt('search-list', require('inquirer-search-list')); + inquirer.registerPrompt('search-checkbox', require('inquirer-search-checkbox')); this.action(); } @@ -67,178 +52,9 @@ export default class AdminHandler } }).catch((error) => { - console.log(error) - }); - } - - - private async show_emails() - { - return new Promise(async (resolve,) => - { - // Get our config from database - const config = (await ConfigModel.find())[0]; - Logger.info(`Emails:`, config.smtp_emails); - resolve(true); - }); - } - - private async add_email() - { - return new Promise((resolve) => - { - prompt.get([ - { - name: "email", - description: "Email for administrator", - required: true - }, - ], async (err, result) => - { - const email = result.email as string; - Logger.info(`Adding email..`); - // Check if email is valid - // eslint-disable-next-line no-useless-escape - if(!email.match(/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/)) - { - Logger.error(`Invalid email`); - return resolve(false); - } - // Get our config from database - const config = (await ConfigModel.find())[0]; - - config.smtp_emails.push(email); - - // Save our config - await config.save(); - return resolve(true) - }); - }); - } - - private async delete_email() - { - return new Promise((resolve) => - { - prompt.get([ - { - name: "email", - description: "Email for administrator", - required: true - }, - ], async (err, result) => - { - const email = result.email as string; - Logger.info(`Deleting email..`); - // Get our config from database - const config = (await ConfigModel.find())[0]; - - // Remove email - config.smtp_emails = config.smtp_emails.filter(e => e !== email); - - // Save our config - await config.save(); - return resolve(true) - }); + Logger.error(error); + this.action(); }); } - - private async add_webhook() - { - return new Promise((resolve) => - { - prompt.get([ - { - name: "url", - description: "URL for webhook", - required: true - }, - ], async (err, result) => - { - const url = result.url as string; - Logger.info(`Adding webhook..`); - // Get our config from database - const config = (await ConfigModel.find())[0]; - - // Add webhook - config.webhooks_urls.push(url); - - // Save our config - await config.save(); - return resolve(true) - }); - }); - } - - private async delete_webhook() - { - return new Promise((resolve) => - { - prompt.get([ - { - name: "url", - description: "URL for webhook", - required: true - }, - ], async (err, result) => - { - const url = result.url as string; - Logger.info(`Deleting webhook..`); - // Get our config from database - const config = (await ConfigModel.find())[0]; - - // Remove webhook - config.webhooks_urls = config.webhooks_urls.filter((e: any) => e !== url); - - // Save our config - await config.save(); - return resolve(true) - }); - }); - } - - private async show_webhooks() - { - return new Promise(async (resolve) => - { - // Get our config from database - const config = (await ConfigModel.find())[0]; - Logger.info(`Webhooks:`, config.webhooks_urls); - resolve(true); - }); - } - - private async show_plugins() - { - return new Promise(async (resolve) => - { - // Get our config from database - const plugins = getPlugins() - Logger.info(`Plugins:`, ...plugins); - resolve(true); - }); - } - - private async update_plugin() - { - return new Promise(async (resolve) => - { - prompt.get([ - { - name: "plugin", - description: "Plugin", - required: false, - type: "string", - }, - ], async (err, result) => - { - Logger.info(`Updating plugins..`); - const plugin = result.plugin as string; - if(plugin) - await installPlugin(`${plugin}@latest`); - return resolve(true) - }); - }); - } } \ No newline at end of file diff --git a/src/Admin/Commands/Admin.prompt.ts b/src/Admin/Commands/Admin.prompt.ts index 6db952c..8a2ebe8 100644 --- a/src/Admin/Commands/Admin.prompt.ts +++ b/src/Admin/Commands/Admin.prompt.ts @@ -1,9 +1,6 @@ /* eslint-disable no-case-declarations */ import Logger from "../../Lib/Logger"; import prompt from "prompt"; -import { CacheConfig } from "../../Cache/Configs.cache"; -import ConfigModel from "../../Database/Models/Configs.model"; -import updateSMTP from "../updateSMTP"; import AdminModel from "../../Database/Models/Administrators.model"; import createAdmin from "../CreateAdmin"; diff --git a/src/Admin/Commands/Company.prompt.ts b/src/Admin/Commands/Company.prompt.ts index 140c0db..38c9714 100644 --- a/src/Admin/Commands/Company.prompt.ts +++ b/src/Admin/Commands/Company.prompt.ts @@ -3,6 +3,7 @@ import Logger from "../../Lib/Logger"; import prompt from "prompt"; import { CacheConfig } from "../../Cache/Configs.cache"; import ConfigModel from "../../Database/Models/Configs.model"; +import { TPaymentCurrency } from "../../Lib/Currencies"; export default { @@ -133,7 +134,7 @@ export default email: result.email as string, logo_url: result.logo_url as string, tax_registered: result.tax_registered === "true", - currency: result.currency as string, + currency: result.currency as TPaymentCurrency, website: result.website as string }; diff --git a/src/Admin/Commands/Email.prompt.ts b/src/Admin/Commands/Email.prompt.ts new file mode 100644 index 0000000..08d9c46 --- /dev/null +++ b/src/Admin/Commands/Email.prompt.ts @@ -0,0 +1,102 @@ +/* eslint-disable no-case-declarations */ +import Logger from "../../Lib/Logger"; +import prompt from "prompt"; +import ConfigModel from "../../Database/Models/Configs.model"; + +export default +{ + name: 'Emails', + description: 'Get all email jobs', + args: [ + { + name: 'action', + type: "list", + message: "Select the email job you want to run", + choices: [ + { + name: 'Show emails', + value: 'show_emails', + }, + { + name: 'Add email', + value: 'add_email', + }, + { + name: 'Delete email', + value: 'delete_email', + } + ], + } + ], + method: async ({action}: {action: string}) => + { + switch(action) + { + case 'show_emails': + return new Promise(async (resolve,) => + { + // Get our config from database + const config = (await ConfigModel.find())[0]; + Logger.info(`Emails:`, config.smtp_emails); + resolve(true); + }); + + case 'add_email': + return new Promise((resolve) => + { + prompt.get([ + { + name: "email", + description: "Email for administrator", + required: true + }, + ], async (err, result) => + { + const email = result.email as string; + Logger.info(`Adding email..`); + // Check if email is valid + // eslint-disable-next-line no-useless-escape + if(!email.match(/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/)) + { + Logger.error(`Invalid email`); + return resolve(false); + } + // Get our config from database + const config = (await ConfigModel.find())[0]; + + config.smtp_emails.push(email); + + // Save our config + await config.save(); + return resolve(true) + }); + }); + case 'delete_email': + return new Promise((resolve) => + { + prompt.get([ + { + name: "email", + description: "Email for administrator", + required: true + }, + ], async (err, result) => + { + const email = result.email as string; + Logger.info(`Deleting email..`); + // Get our config from database + const config = (await ConfigModel.find())[0]; + + // Remove email + config.smtp_emails = config.smtp_emails.filter(e => e !== email); + + // Save our config + await config.save(); + return resolve(true) + }); + }); + + } + return true; + } +} \ No newline at end of file diff --git a/src/Admin/Commands/Invoices.prompt.ts b/src/Admin/Commands/Invoices.prompt.ts index d2b37a1..9c69fa3 100644 --- a/src/Admin/Commands/Invoices.prompt.ts +++ b/src/Admin/Commands/Invoices.prompt.ts @@ -5,8 +5,15 @@ import inquirer from 'inquirer'; import TransactionsModel from "../../Database/Models/Transactions.model"; import { Company_Currency } from "../../Config"; import { getDate } from "../../Lib/Time"; -import { idTransactions } from "../../Lib/Generator"; +import { idInvoice, idTransactions } from "../../Lib/Generator"; import sendEmailOnTransactionCreation from "../../Lib/Transaction/SendEmailOnCreation"; +import { getDates30DaysAgo } from "../../Cron/Methods/Invoices.cron.methods"; +import { A_CC_Payments } from "../../Types/PaymentMethod"; +import { currencyCodes } from "../../Lib/Currencies"; +import CustomerModel from "../../Database/Models/Customers/Customer.model"; +import mainEvent from "../../Events/Main.event"; +import { sendInvoiceEmail } from "../../Lib/Invoices/SendEmail"; +import { IInvoice } from "@interface/Invoice.interface"; export default { @@ -26,9 +33,17 @@ export default name: 'Get invoice', value: 'get_invoice', }, + { + name: 'Get late invoices', + value: 'get_late_invoices', + }, { name: 'Mark invoice as paid', value: 'mark_invoice_paid', + }, + { + name: 'Create invoice', + value: 'create_invoice', } ], } @@ -39,7 +54,21 @@ export default { case 'get_invoices': // Getting all invoices - Logger.info(`All invoices:`, await InvoiceModel.find()); + const invoices = (await InvoiceModel.find()).map(invoice => + { + return { + id: invoice.id, + customer: invoice.customer_uid, + amount: invoice.amount, + currency: invoice.currency, + due_date: invoice.dates?.due_date, + invoice_date: invoice.dates?.invoice_date, + notified: invoice.notified, + paid: invoice.paid, + status: invoice.status, + } + }); + console.table(invoices); break; case 'get_invoice': @@ -59,14 +88,28 @@ export default const { invoiceId, isOCR } = await inquirer.prompt(action); let id = invoiceId; if(isOCR) - // get id from ocr, by removing the last 8 digits in the start - id = invoiceId.substring(0, invoiceId.length - 8); + // get id from ocr, by removing first 8 characters + id = invoiceId.substring(8); Logger.info(`Invoice:`, await InvoiceModel.findOne({ id: id, })); break; - + case 'get_late_invoices': + { + const invoices = await InvoiceModel.find({ + "dates.due_date": { + $in: [...(getDates30DaysAgo())] + }, + paid: false, + status: { + $not: /fraud|cancelled|draft|refunded/g + } + }); + Logger.info(`Total late invoices:`, invoices.length); + Logger.info(`Late invoices ids:`, invoices.map(e => e.id)); + break; + } case 'mark_invoice_paid': { const action = [ @@ -109,7 +152,7 @@ export default const t = await (new TransactionsModel({ amount: invoice.amount+invoice.amount*invoice.tax_rate/100, payment_method: invoice.payment_method, - fees: 0, + fees: invoice.fees, invoice_uid: invoice.id, customer_uid: invoice.customer_uid, currency: invoice.currency ?? await Company_Currency(), @@ -126,6 +169,132 @@ export default await invoice.save(); Logger.info(`Invoice with id ${invoiceId} marked as paid`); + break; + } + case 'create_invoice': + { + const action = [ + { + name: 'customerId', + type: 'search-list', + message: 'Customer', + choices: (await CustomerModel.find()).map(e => + { + return { + name: `${e.personal.first_name} ${e.personal.last_name} (${e.id})`, + value: e.id, + } + }) + }, + { + name: 'invoice_date', + type: 'input', + message: 'Enter the invoiced date', + }, + { + name: 'due_date', + type: 'input', + message: 'Enter the due date', + }, + { + name: 'amount', + type: 'number', + message: 'Enter the amount', + }, + { + name: 'tax_rate', + type: 'number', + message: 'Enter the tax rate', + }, + { + name: 'payment_method', + type: 'search-list', + message: 'Enter the payment method', + choices: A_CC_Payments + }, + { + name: 'currency', + type: 'search-list', + message: 'Enter the currency', + choices: currencyCodes + }, + { + name: 'notes', + type: 'input', + message: 'Enter the notes', + }, + { + name: "items", + type: "input", + message: "Enter the items (separated by ';') (name,quantity,price;...)", + response: 'array', + validate: (value: string) => + { + if (value.split(';').length < 1) + return 'Please enter at least one item'; + return true; + } + }, + { + name: 'fees', + type: 'number', + message: 'Enter the fees', + }, + { + name: "send_email", + type: "confirm", + message: "Send notification?", + default: true, + } + ] + const { customerId, invoice_date, due_date, amount, tax_rate, payment_method, currency, notes, items, send_email, fees } = await inquirer.prompt(action); + const customer = await CustomerModel.findOne({ id: customerId }) + if(!customer) + return Logger.error(`Customer with id ${customerId} not found`); + // parse items + const nItems = (items as string).split(';').map((e: string) => + { + if(e === "") + return null; + const [notes, quantity, price] = e.split(','); + if(!notes || !quantity || !price) + null; + return { + notes, + quantity: Number(quantity), + amount: Number(price), + } + }).filter(e => e); + + const invoice = await (new InvoiceModel({ + customer_uid: customerId, + dates: { + invoice_date: invoice_date, + due_date: due_date, + date_refunded: null, + date_cancelled: null, + date_paid: null + }, + amount, + tax_rate, + payment_method, + currency, + notes, + fees: parseInt(fees ?? '0'), + items: nItems as IInvoice['items'], + status: "active", + paid: false, + notified: false, + uid: idInvoice(), + transactions: [], + }).save()); + + Logger.info(`Invoice created with id ${invoice.id}`); + mainEvent.emit("invoice_created", invoice); + if(send_email) + await sendInvoiceEmail(invoice, customer); + + break; } } return true; diff --git a/src/Admin/Commands/Orders.prompt.ts b/src/Admin/Commands/Orders.prompt.ts new file mode 100644 index 0000000..fedc60d --- /dev/null +++ b/src/Admin/Commands/Orders.prompt.ts @@ -0,0 +1,225 @@ +import inquirer from "inquirer"; +import CustomerModel from "../../Database/Models/Customers/Customer.model"; +import OrderModel from "../../Database/Models/Orders.model"; +import ProductModel from "../../Database/Models/Products.model"; +import PromotionCodeModel from "../../Database/Models/PromotionsCode.model"; +import { currencyCodes } from "../../Lib/Currencies"; +import { idOrder } from "../../Lib/Generator"; +import Logger from "../../Lib/Logger"; +import { A_CC_Payments, A_RecurringMethod } from "../../Types/PaymentMethod"; +import dateFormat from "date-and-time"; +import nextRycleDate from "../../Lib/Dates/DateCycle"; +import mainEvent from "../../Events/Main.event"; +import { sendEmail } from "../../Email/Send"; +import NewOrderCreated from "../../Email/Templates/Orders/NewOrderCreated"; +import { Company_Name } from "../../Config"; + +export default +{ + name: 'Orders', + description: 'Get all invoice jobs', + args: [ + { + name: 'action', + type: "list", + message: "Select the cron you want to run", + choices: [ + { + name: 'Get orders', + value: 'get_orders', + }, + { + name: 'Get order', + value: 'get_order', + }, + { + name: 'Create order', + value: 'create_order', + } + ], + } + ], + method: async ({action}: {action: string}) => + { + switch(action) + { + case 'get_orders': + { + const orders = (await OrderModel.find()); + Logger.info(orders); + break; + } + + case 'get_order': + { + const action = [ + { + name: 'orderId', + type: 'input', + message: 'Enter the order id', + }, + ] + const { orderId } = await inquirer.prompt(action); + + Logger.info(`Order:`, await OrderModel.findOne({ + id: orderId, + })); + break; + } + case 'create_order': + { + const action1 = [ + { + name: 'customer_uid', + type: 'search-list', + message: 'Customer', + choices: (await CustomerModel.find()).map(e => + { + return { + name: `${e.personal.first_name} ${e.personal.last_name} (${e.id})`, + value: e.id, + } + }) + }, + { + name: 'payment_method', + type: 'search-list', + message: 'Enter the payment method', + choices: A_CC_Payments + }, + { + name: 'currency', + type: 'search-list', + message: 'Enter the currency', + choices: currencyCodes + }, + { + name: 'products', + type: 'search-checkbox', + message: 'Enter the products', + choices: (await ProductModel.find()).map(e => + { + return { + name: `${e.name} (${e.id})`, + value: e.id, + } + }), + }, + ] + + // eslint-disable-next-line prefer-const + let { currency, customer_uid, payment_method, products } = await inquirer.prompt(action1); + + // @ts-ignore + const action2 = [...products.map(e => + { + return { + name: `quantity_${e}`, + type: 'number', + message: `Enter the quantity for #${e}`, + } + }), + { + name: 'billing_type', + type: 'list', + message: 'Enter the billing type', + choices: [ + { + name: 'One time', + value: 'one_time', + }, + { + name: 'Recurring', + value: 'recurring', + } + ], + }, + { + name: 'billing_cycle', + type: 'list', + message: 'Enter the billing cycle', + choices: A_RecurringMethod + }, + { + name: "fees", + type: 'number', + message: 'Enter the fees', + + }, + { + name: "promotion_code", + type: 'search-list', + message: 'Enter the promotion code', + choices: [...(await PromotionCodeModel.find({ + products_ids: { + $in: products, + } + })).map(e => + { + return { + name: `${e.name} (${e.id})`, + value: e.id, + } + }), { + name: 'None', + value: null, + }] + } + ] + + const action2Result = await inquirer.prompt(action2); + + // @ts-ignore + const newProduct = products.map(e => + { + return { + product_id: e, + quantity: action2Result[`quantity_${e}`], + } + }); + + const b_recurring = action2Result.billing_type === "recurring"; + const newOrder = await (new OrderModel({ + uid: idOrder(), + invoices: [], + currency, + customer_uid, + dates: { + createdAt: new Date(), + last_recycle: b_recurring ? dateFormat.format(new Date(), "YYYY-MM-DD") : undefined, + next_recycle: b_recurring ? dateFormat.format(nextRycleDate(new Date(), action2Result.billing_cycle), "YYYY-MM-DD") : undefined, + }, + order_status: 'pending', + payment_method, + products: newProduct, + billing_type: action2Result.billing_type, + billing_cycle: action2Result.billing_cycle, + fees: action2Result.fees, + promotion_code: action2Result.promotion_code, + }).save()); + + mainEvent.emit("order_created", newOrder); + + const customer = await CustomerModel.findOne({ + id: customer_uid, + }); + + if(!customer) + throw new Error(`Fail to find customer with id: ${customer_uid}`); + + await sendEmail({ + receiver: customer.personal.email, + subject: `New order from ${await Company_Name() !== "" ? await Company_Name() : "CPG"} #${newOrder.id}`, + body: { + body: await NewOrderCreated(newOrder, customer) + } + }); + + Logger.info(newOrder); + + break; + } + + } + } +}; \ No newline at end of file diff --git a/src/Admin/Commands/Webhook.prompt.ts b/src/Admin/Commands/Webhook.prompt.ts new file mode 100644 index 0000000..0c56f99 --- /dev/null +++ b/src/Admin/Commands/Webhook.prompt.ts @@ -0,0 +1,96 @@ +/* eslint-disable no-case-declarations */ +import Logger from "../../Lib/Logger"; +import prompt from "prompt"; +import ConfigModel from "../../Database/Models/Configs.model"; + +export default +{ + name: 'Webhooks', + description: 'Get all webhook jobs', + args: [ + { + name: 'action', + type: "list", + message: "Select the webhook job you want to run", + choices: [ + { + name: 'Show webhooks', + value: 'show_webhooks', + }, + { + name: 'Add webhook', + value: 'add_webhook', + }, + { + name: 'Delete webhook', + value: 'delete_webhook', + } + ], + } + ], + method: async ({action}: {action: string}) => + { + switch(action) + { + case 'show_webhooks': + return new Promise(async (resolve) => + { + // Get our config from database + const config = (await ConfigModel.find())[0]; + Logger.info(`Webhooks:`, config.webhooks_urls); + resolve(true); + }); + + case 'add_email': + return new Promise((resolve) => + { + prompt.get([ + { + name: "url", + description: "URL for webhook", + required: true + }, + ], async (err, result) => + { + const url = result.url as string; + Logger.info(`Adding webhook..`); + // Get our config from database + const config = (await ConfigModel.find())[0]; + + // Add webhook + config.webhooks_urls.push(url); + + // Save our config + await config.save(); + return resolve(true) + }); + }); + case 'delete_email': + return new Promise((resolve) => + { + prompt.get([ + { + name: "url", + description: "URL for webhook", + required: true + }, + ], async (err, result) => + { + const url = result.url as string; + Logger.info(`Deleting webhook..`); + // Get our config from database + const config = (await ConfigModel.find())[0]; + + // Remove webhook + config.webhooks_urls = config.webhooks_urls.filter((e: any) => e !== url); + + // Save our config + await config.save(); + return resolve(true) + }); + }); + + } + return true; + } +} \ No newline at end of file diff --git a/src/Config.ts b/src/Config.ts index 41c17a2..217dddf 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -104,6 +104,7 @@ export const Company_Currency = async (): Promise => diff --git a/src/Cron/Invoices.cron.ts b/src/Cron/Invoices.cron.ts index 7140ccd..430afa9 100644 --- a/src/Cron/Invoices.cron.ts +++ b/src/Cron/Invoices.cron.ts @@ -11,7 +11,7 @@ import { export = function Cron_Invoices() { // Every hour - new CronJob("0 */12 * * *", () => + new CronJob("0 12 * * *", () => { Logger.info(GetText(Default_Language).cron.txt_Invoice_Checking); diff --git a/src/Cron/Orders.cron.ts b/src/Cron/Orders.cron.ts index 1878e96..fc3c03d 100644 --- a/src/Cron/Orders.cron.ts +++ b/src/Cron/Orders.cron.ts @@ -11,7 +11,7 @@ import GetText from "../Translation/GetText"; export = function Cron_Orders() { // Every hour - new CronJob("0 */12 * * *", () => + new CronJob("0 12 * * *", () => { Logger.info(GetText(Default_Language).cron.txt_Orders_Checking); // Logger.info(`Checking orders..`); diff --git a/src/Database/Models/Invoices.model.ts b/src/Database/Models/Invoices.model.ts index 9690c2c..4939715 100644 --- a/src/Database/Models/Invoices.model.ts +++ b/src/Database/Models/Invoices.model.ts @@ -57,6 +57,11 @@ const InvoiceSchema = new Schema default: 0, }, + fees: { + type: Number, + default: 0, + }, + items: { type: [ { diff --git a/src/Database/Models/Orders.model.ts b/src/Database/Models/Orders.model.ts index dff38c1..18c9b14 100644 --- a/src/Database/Models/Orders.model.ts +++ b/src/Database/Models/Orders.model.ts @@ -74,7 +74,7 @@ const OrderSchema = new Schema required: false, }, - price_override: { + fees: { type: Number, default: 0, }, diff --git a/src/Email/Templates/Methods/OrderProducts.print.ts b/src/Email/Templates/Methods/OrderProducts.print.ts index 09e2fa4..48be69e 100644 --- a/src/Email/Templates/Methods/OrderProducts.print.ts +++ b/src/Email/Templates/Methods/OrderProducts.print.ts @@ -64,6 +64,16 @@ export default async function printOrderProductTable(order: IOrder, customer: IC } } + if(order.fees) + { + result += stripIndents` + + Fees + 1 + ${order.fees.toFixed(2)} ${GetCurrencySymbol(order.currency)} + ` + } + return result; }))).join("")} diff --git a/src/Email/Templates/Orders/NewOrderCreated.ts b/src/Email/Templates/Orders/NewOrderCreated.ts index 0c6c023..40c6e55 100644 --- a/src/Email/Templates/Orders/NewOrderCreated.ts +++ b/src/Email/Templates/Orders/NewOrderCreated.ts @@ -15,6 +15,9 @@ export default async (order: IOrder, customer: ICustomer) => await UseStyles(str

Your order has been created.

+

+ A invoice will be generated for you. +

Order number: ${order.id}

diff --git a/src/Interfaces/Admin/Configs.interface.ts b/src/Interfaces/Admin/Configs.interface.ts index 92537eb..630ff5a 100644 --- a/src/Interfaces/Admin/Configs.interface.ts +++ b/src/Interfaces/Admin/Configs.interface.ts @@ -1,4 +1,5 @@ import { Document } from "mongoose"; +import { TPaymentCurrency } from "../../Lib/Currencies"; import { TPayments } from "../../Types/PaymentMethod"; export interface IConfigs @@ -20,7 +21,7 @@ export interface ICompanyConfig phone: string; email: string; vat: string; - currency: string; + currency: TPaymentCurrency; logo_url: string; tax_registered: boolean; website: string; diff --git a/src/Interfaces/Invoice.interface.ts b/src/Interfaces/Invoice.interface.ts index 7e9dc2f..cd4c357 100644 --- a/src/Interfaces/Invoice.interface.ts +++ b/src/Interfaces/Invoice.interface.ts @@ -50,6 +50,7 @@ export interface IInvoice customer_uid: ICustomer["uid"]; dates: IInvoice_Dates; amount: number; + fees?: number; items: Array; transactions: Array; payment_method: keyof IPayments; diff --git a/src/Interfaces/Orders.interface.ts b/src/Interfaces/Orders.interface.ts index 763e3fc..48ee2b7 100644 --- a/src/Interfaces/Orders.interface.ts +++ b/src/Interfaces/Orders.interface.ts @@ -18,7 +18,6 @@ import { IPromotionsCodes } from "./PromotionsCodes.interface"; * @property {string} billing_type "free" | "one_time" | "recurring" * @property {string} billing_cycle "monthly" | "quarterly" | "semi_annually" | "biennially" | "triennially" * @property {number} quantity - * @property {number} price_override Overwrite price from product * @property {object} dates */ @@ -43,14 +42,13 @@ export interface IOrder * if 'billing_type' is "recurring" `billing_cycle` wont be undefined */ billing_cycle?: TRecurringMethod; - price_override?: number; + fees?: number; dates: IOrderDates; invoices: Array; currency: TPaymentCurrency; promotion_code?: IPromotionsCodes["id"]; } - export type TOrderStatus = "active" | "pending" | "fraud" | "cancelled"; export interface IOrderDates diff --git a/src/Lib/Orders/newInvoice.ts b/src/Lib/Orders/newInvoice.ts index 56c318d..b349c6f 100644 --- a/src/Lib/Orders/newInvoice.ts +++ b/src/Lib/Orders/newInvoice.ts @@ -106,6 +106,15 @@ export async function createInvoiceFromOrder(order: IOrder) } } + if(order.fees) + { + items.push({ + amount: order.fees, + notes: "+ Fees", + quantity: 1, + }); + } + // Create invoice const newInvoice = await (new InvoiceModel({ uid: idInvoice(), diff --git a/src/Payments/Paypal.ts b/src/Payments/Paypal.ts index b67d77d..0628e26 100644 --- a/src/Payments/Paypal.ts +++ b/src/Payments/Paypal.ts @@ -133,7 +133,7 @@ export async function retrievePaypalTransaction(payerId: string, paymentId: stri const newTrans = await (new TransactionsModel({ amount: invoice.amount+invoice.amount*invoice.tax_rate/100, payment_method: invoice.payment_method, - fees: 0, + fees: invoice.fees, invoice_uid: invoice.id, customer_uid: invoice.customer_uid, currency: invoice.currency ?? await Company_Currency(), diff --git a/src/Payments/Stripe.ts b/src/Payments/Stripe.ts index 57539ae..10fd5c3 100644 --- a/src/Payments/Stripe.ts +++ b/src/Payments/Stripe.ts @@ -195,7 +195,7 @@ export const ChargeCustomer = async (invoice_id: IInvoice["id"]) => const newTrans = await (new TransactionsModel({ amount: invoice.amount+invoice.amount*invoice.tax_rate/100, payment_method: invoice.payment_method, - fees: 0, + fees: invoice.fees, invoice_uid: invoice.id, customer_uid: invoice.customer_uid, currency: invoice.currency ?? await Company_Currency(), @@ -236,7 +236,7 @@ export const markInvoicePaid = async (intent: stripe.Response; -}>, _products: IProduct[], payment_method: string, billing_type: string, currency: TPaymentCurrency, billing_cycle?: TRecurringMethod) +async function createOrder(payload: { + customer: ICustomer, + products: Array<{ + product_id: IProduct["id"], + quantity: number, + configurable_options?: Array<{ + id: IConfigurableOptions["id"], + option_index?: number, + }>; + }>, + _products: IProduct[], + payment_method: keyof IPayments, + billing_type: TPaymentTypes, + billing_cycle?: TRecurringMethod, + currency: TPaymentCurrency, + fees: number, +}) { const order = await (new OrderModel({ - customer_uid: customer.id, + customer_uid: payload.customer.id, // @ts-ignore - products: products.map(product => + products: payload.products.map(product => { return { product_id: product.product_id, @@ -50,26 +59,27 @@ async function createOrder(customer: ICustomer, products: Array<{ quantity: product.quantity, } }), - payment_method: payment_method as keyof IPayments, + payment_method: payload.payment_method as keyof IPayments, order_status: "active", - billing_type: billing_type as TPaymentTypes, - billing_cycle: billing_cycle, + billing_type: payload.billing_type as TPaymentTypes, + billing_cycle: payload.billing_cycle, + fees: payload.fees, dates: { createdAt: new Date(), next_recycle: dateFormat.format(nextRecycleDate( - new Date(), billing_cycle ?? "monthly") + new Date(), payload.billing_cycle ?? "monthly") , "YYYY-MM-DD"), last_recycle: dateFormat.format(new Date(), "YYYY-MM-DD") }, - currency: currency, + currency: payload.currency, uid: idOrder(), }).save()); mainEvent.emit("order_created", order); - await SendEmail(customer.personal.email, `New order from ${await Company_Name() !== "" ? await Company_Name() : "CPG"} #${order.id}`, { + await SendEmail(payload.customer.personal.email, `New order from ${await Company_Name() !== "" ? await Company_Name() : "CPG"} #${order.id}`, { isHTML: true, - body: await NewOrderCreated(order, customer), + body: await NewOrderCreated(order, payload.customer), }); } export = OrderRoute; @@ -101,7 +111,7 @@ class OrderRoute }>; }>; const payment_method = req.body.payment_method as TPayments; - + const fees = parseInt(req.body.fees ?? "0") as number ?? 0; // Check if payment_method is valid const validPaymentMethods = getEnabledPaymentMethods(); @@ -165,6 +175,7 @@ class OrderRoute , "YYYY-MM-DD"), last_recycle: dateFormat.format(new Date(), "YYYY-MM-DD") }, + fees: fees, uid: idOrder(), // @ts-ignore invoices: [], @@ -172,38 +183,49 @@ class OrderRoute promotion_code: promotion_code?.id, } - const one_timers = []; - const recurring_monthly = []; - const recurring_quarterly = []; - const recurring_semi_annually = []; - const recurring_biennially = []; - const recurring_triennially = []; - const recurring_yearly = []; + const recurringMethods = { + one_timers: [], + monthly: [], + yearly: [], + quarterly: [], + semi_annually: [], + biennially: [], + triennially: [], + }; // Possible to get a Dos attack // ! prevent this for (const p of _products) { if(p.payment_type === "one_time") - one_timers.push(p); - - if(p.payment_type === "recurring" && p.recurring_method === "monthly") - recurring_monthly.push(p); - - if(p.payment_type === "recurring" && p.recurring_method === "quarterly") - recurring_quarterly.push(p); - - if(p.payment_type === "recurring" && p.recurring_method === "semi_annually") - recurring_semi_annually.push(p); + recurringMethods["one_timers"].push(p); - if(p.payment_type === "recurring" && p.recurring_method === "biennially") - recurring_biennially.push(p); - - if(p.payment_type === "recurring" && p.recurring_method === "triennially") - recurring_triennially.push(p); - - if(p.payment_type === "recurring" && p.recurring_method === "yearly") - recurring_yearly.push(p); + if(p.payment_type === "recurring") + { + switch(p.recurring_method) + { + case "monthly": + recurringMethods["monthly"].push(p); + break; + case "quarterly": + recurringMethods["quarterly"].push(p); + break; + case "semi_annually": + recurringMethods["semi_annually"].push(p); + break; + case "biennially": + recurringMethods["biennially"].push(p); + break; + case "triennially": + recurringMethods["triennially"].push(p); + break; + case "yearly": + recurringMethods["yearly"].push(p); + break; + default: + break; + } + } let configurable_option: any = undefined if(products.find(e => e.product_id === p.id)?.configurable_options) @@ -217,69 +239,30 @@ class OrderRoute }); } - // Create new orders - if(recurring_monthly.length > 0) - await createOrder(customer, recurring_monthly.map(p => - { - return products.find(p2 => p2.product_id == p.id) ?? { - product_id: p.id, - quantity: 1 - } - }), recurring_monthly, payment_method, "recurring", _order_.currency, "monthly"); - - if(recurring_quarterly.length > 0) - await createOrder(customer, recurring_quarterly.map(p => - { - return products.find(p2 => p2.product_id == p.id) ?? { - product_id: p.id, - quantity: 1 - } - }), recurring_quarterly, payment_method, "recurring", _order_.currency, "quarterly"); - - if(recurring_semi_annually.length > 0) - await createOrder(customer, recurring_semi_annually.map(p => - { - return products.find(p2 => p2.product_id == p.id) ?? { - product_id: p.id, - quantity: 1 - } - }), recurring_semi_annually, payment_method, "recurring", _order_.currency, "semi_annually"); - - if(recurring_biennially.length > 0) - await createOrder(customer, recurring_biennially.map(p => - { - return products.find(p2 => p2.product_id == p.id) ?? { - product_id: p.id, - quantity: 1 - } - }), recurring_biennially, payment_method, "recurring", _order_.currency, "biennially"); - - if(recurring_triennially.length > 0) - await createOrder(customer, recurring_triennially.map(p => - { - return products.find(p2 => p2.product_id == p.id) ?? { - product_id: p.id, - quantity: 1 - } - }), recurring_triennially, payment_method, "recurring", _order_.currency, "triennially"); - - if(one_timers.length > 0) - await createOrder(customer, one_timers.map(p => - { - return products.find(p2 => p2.product_id == p.id) ?? { - product_id: p.id, - quantity: 1 - } - }), one_timers, payment_method, "one_time", _order_.currency); - - if(recurring_yearly.length > 0) - await createOrder(customer, recurring_yearly.map(p => + // @ts-ignore + Object.keys(recurringMethods).forEach(async (key: keyof (typeof recurringMethods)) => + { + const isOneTimer = key === "one_timers"; + if(recurringMethods[key].length > 0) { - return products.find(p2 => p2.product_id == p.id) ?? { - product_id: p.id, - quantity: 1 - } - }), recurring_yearly, payment_method, "recurring", _order_.currency, "yearly"); + await createOrder({ + customer: customer, + products: recurringMethods[key].map(p => + { + return products.find(p2 => p2.product_id == p.id) ?? { + product_id: p.id, + quantity: 1 + } + }), + payment_method: payment_method, + fees: fees, + currency: _order_.currency, + _products: recurringMethods[key], + billing_type: isOneTimer ? "one_time" : "recurring", + billing_cycle: !isOneTimer ? "monthly" : undefined, + }) + } + }); const invoice = await createInvoiceFromOrder(_order_); diff --git a/src/Server/Routes/v2/Orders/Orders.controller.ts b/src/Server/Routes/v2/Orders/Orders.controller.ts index a9aab0f..d1a5ae7 100644 --- a/src/Server/Routes/v2/Orders/Orders.controller.ts +++ b/src/Server/Routes/v2/Orders/Orders.controller.ts @@ -1,16 +1,16 @@ -import {Request, Response} from "express"; +import { Request, Response } from "express"; import dateFormat from "date-and-time"; import OrderModel from "../../../../Database/Models/Orders.model"; -import {IOrder} from "@interface/Orders.interface"; +import { IOrder } from "@interface/Orders.interface"; import nextRycleDate from "../../../../Lib/Dates/DateCycle"; -import {idOrder} from "../../../../Lib/Generator"; -import {APISuccess} from "../../../../Lib/Response"; +import { idOrder } from "../../../../Lib/Generator"; +import { APISuccess } from "../../../../Lib/Response"; import BaseModelAPI from "../../../../Models/BaseModelAPI"; -import {createInvoiceFromOrder} from "../../../../Lib/Orders/newInvoice"; -import {SendEmail} from "../../../../Email/Send"; +import { createInvoiceFromOrder } from "../../../../Lib/Orders/newInvoice"; +import { SendEmail } from "../../../../Email/Send"; import CustomerModel from "../../../../Database/Models/Customers/Customer.model"; import NewOrderCreated from "../../../../Email/Templates/Orders/NewOrderCreated"; -import {Company_Name} from "../../../../Config"; +import { Company_Name } from "../../../../Config"; import mainEvent from "../../../../Events/Main.event"; const API = new BaseModelAPI(idOrder, OrderModel); diff --git a/src/Server/Routes/v3/Taxes/Taxes.config.ts b/src/Server/Routes/v3/Taxes/Taxes.config.ts index d6e0f48..5a6b522 100644 --- a/src/Server/Routes/v3/Taxes/Taxes.config.ts +++ b/src/Server/Routes/v3/Taxes/Taxes.config.ts @@ -1,7 +1,10 @@ import { Application, Router } from "express"; import TransactionsModel from "../../../../Database/Models/Transactions.model"; -import { APISuccess } from "../../../../Lib/Response"; import EnsureAdmin from "../../../../Middlewares/EnsureAdmin"; +// @ts-ignore +import pdfdocs from "pdfkit-table"; +import { Company_Currency } from "../../../../Config"; +import { convertCurrency } from "../../../../Lib/Currencies"; export = TaxesRouter; class TaxesRouter @@ -27,9 +30,102 @@ class TaxesRouter }, }); - return APISuccess({ - transactions, - })(res); + const companyCurrency = await Company_Currency(); + + const doc = new pdfdocs({ + size: "A4", + margin: 50, + }); + + const income = { + title: "Income", + headers: [ + "Id", + "Date", + "Invoice ID", + "Customer ID", + "Total", + "Fees", + "Payment method" + ], + rows: transactions.filter(e => e.statement === "income").map((t) => + { + return [ + t.id, + t.date, + t.invoice_uid, + t.customer_uid, + `${t.amount.toFixed(2)} ${t.currency}`, + t.fees, + t.payment_method, + ]; + }), + }; + + doc.table(income); + + const expense = { + title: "Expense", + headers: [ + "Date", + "Invoice ID", + "Customer", + "Total", + "Fees", + ], + rows: transactions.filter(e => e.statement === "expense").map((t) => + { + return [ + t.date, + t.invoice_uid, + t.customer_uid, + t.amount, + t.fees, + ]; + }), + }; + + doc.table(expense); + + const nTotal = { + expense: (await Promise.all(transactions.filter(e => e.statement === "expense").map(async (e) => + { + // Convert to company currency + return (await convertCurrency(e.amount, e.currency, companyCurrency)); + }))).reduce((a, b) => a + b, 0).toFixed(2), + income: (await Promise.all(transactions.filter(e => e.statement === "income").map(async (e) => + { + // Convert to company currency + return (await convertCurrency(e.amount, e.currency, companyCurrency)); + }))).reduce((a, b) => a + b, 0).toFixed(2) + } + + const total = { + title: "Total", + headers: [ + "From", + "To", + "Total income", + "Total expense", + "Total" + ], + rows: [ + [ + start_date, + end_date, + nTotal.income + ` ${companyCurrency}`, + nTotal.expense + ` ${companyCurrency}`, + (parseFloat(nTotal.income) - parseFloat(nTotal.expense)).toFixed(2) + ` ${companyCurrency}` + ], + ], + } + + doc.table(total); + + doc.pipe(res); + + // done + doc.end(); }); }