diff --git a/package.json b/package.json
index 0f37039..9bdfcd5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cpg-api",
- "version": "v2.7",
+ "version": "v2.8",
"description": "Central Payment Gateway",
"main": "./build/Main.js",
"dependencies": {
diff --git a/src/Database/Models/Quotes.model.ts b/src/Database/Models/Quotes.model.ts
index 8c944dc..e578a5d 100644
--- a/src/Database/Models/Quotes.model.ts
+++ b/src/Database/Models/Quotes.model.ts
@@ -5,6 +5,7 @@ import { IQuotes } from "@interface/Quotes.interface";
import Logger from "../../Lib/Logger";
import GetText from "../../Translation/GetText";
import { A_CC_Payments } from "../../Types/PaymentMethod";
+import { currencyCodes } from "../../Lib/Currencies";
const QuotesSchema = new Schema
(
@@ -27,7 +28,6 @@ const QuotesSchema = new Schema
type: [
{
name: String,
- tax_rate: Number,
price: Number,
quantity: Number,
}
@@ -50,6 +50,28 @@ const QuotesSchema = new Schema
default: "",
},
+ currency: {
+ type: String,
+ required: true,
+ enum: currencyCodes,
+ default: "EUR",
+ },
+
+ tax_rate: {
+ type: Number,
+ default: 0,
+ },
+
+ accepted: {
+ type: Boolean,
+ default: false
+ },
+
+ declined: {
+ type: Boolean,
+ default: false
+ },
+
payment_method: {
type: String,
enum: [...A_CC_Payments],
diff --git a/src/Database/Models/Transactions.model.ts b/src/Database/Models/Transactions.model.ts
index a30d2b1..febf518 100644
--- a/src/Database/Models/Transactions.model.ts
+++ b/src/Database/Models/Transactions.model.ts
@@ -1,7 +1,7 @@
-import mongoose, { model, Schema } from "mongoose"
+import mongoose, { Document, model, Schema } from "mongoose"
import increment from "mongoose-auto-increment";
import { Default_Language, MongoDB_URI } from "../../Config";
-import { IDTransactions } from "@interface/Transactions.interface";
+import { ITransactions } from "@interface/Transactions.interface";
import Logger from "../../Lib/Logger";
import GetText from "../../Translation/GetText";
import { A_CC_Payments } from "../../Types/PaymentMethod";
@@ -58,7 +58,7 @@ const TransactionsSchema = new Schema
);
// Log when a transaction is created
-TransactionsSchema.post('save', function(doc: IDTransactions)
+TransactionsSchema.post('save', function(doc: ITransactions & Document)
{
Logger.db(GetText(Default_Language).database.txt_Model_Created(doc.modelName, doc.uid));
// Logger.db(`Created transaction ${doc.uid}`);
@@ -74,6 +74,6 @@ TransactionsSchema.plugin(increment.plugin, {
incrementBy: 1
});
-const TransactionsModel = model("transactions", TransactionsSchema);
+const TransactionsModel = model("transactions", TransactionsSchema);
export default TransactionsModel;
\ No newline at end of file
diff --git a/src/Email/Templates/Invoices/Invoice.template.ts b/src/Email/Templates/Invoices/Invoice.template.ts
index 74181b2..36262e2 100644
--- a/src/Email/Templates/Invoices/Invoice.template.ts
+++ b/src/Email/Templates/Invoices/Invoice.template.ts
@@ -78,7 +78,7 @@ export default async (invoice: IInvoice & IInvoiceMethods, customer: ICustomer)
${CPG_Customer_Panel_Domain ? `
- View Invoice
+ View Invoice
` : ''}
diff --git a/src/Email/Templates/Invoices/LateInvoice.Template.ts b/src/Email/Templates/Invoices/LateInvoice.Template.ts
index 2566eca..e37764f 100644
--- a/src/Email/Templates/Invoices/LateInvoice.Template.ts
+++ b/src/Email/Templates/Invoices/LateInvoice.Template.ts
@@ -78,7 +78,7 @@ export default async (invoice: IInvoice & IInvoiceMethods, customer: ICustomer)
${CPG_Customer_Panel_Domain ? `
- View Invoice
+ View Invoice
` : ''}
diff --git a/src/Email/Templates/Methods/QuotesItems.print.ts b/src/Email/Templates/Methods/QuotesItems.print.ts
new file mode 100644
index 0000000..329429e
--- /dev/null
+++ b/src/Email/Templates/Methods/QuotesItems.print.ts
@@ -0,0 +1,27 @@
+import { IQuotes } from "@interface/Quotes.interface";
+import { GetCurrencySymbol } from "../../../Lib/Currencies";
+import GetTableStyle from "../CSS/GetTableStyle";
+
+export default async function printQuotesItemsTable(quote: IQuotes)
+{
+ return `
+
+
+
+ Product |
+ Quantity |
+ Price |
+
+
+
+ ${(await Promise.all(quote.items.map(async item => `
+
+ ${item.name} |
+ ${item.quantity} |
+ ${item.price} ${GetCurrencySymbol(quote.currency)} |
+
+ `))).join('')}
+
+
+ `
+}
\ No newline at end of file
diff --git a/src/Email/Templates/Orders/NewOrderCreated.ts b/src/Email/Templates/Orders/NewOrderCreated.ts
index 48c743c..91b26d2 100644
--- a/src/Email/Templates/Orders/NewOrderCreated.ts
+++ b/src/Email/Templates/Orders/NewOrderCreated.ts
@@ -52,7 +52,7 @@ export default async (order: IOrder, customer: ICustomer) => await UseStyles(str
${CPG_Customer_Panel_Domain ? `
- View Order
+ View Order
` : ''}
diff --git a/src/Email/Templates/Payments/PaymentFailed.template.ts b/src/Email/Templates/Payments/PaymentFailed.template.ts
index 6959d9e..5f16581 100644
--- a/src/Email/Templates/Payments/PaymentFailed.template.ts
+++ b/src/Email/Templates/Payments/PaymentFailed.template.ts
@@ -41,7 +41,7 @@ export = async (invoice: IInvoice, customer: ICustomer) => UseStyles(stripIndent
${CPG_Customer_Panel_Domain ? `
- View invoice
+ View invoice
` : ''}
diff --git a/src/Email/Templates/Quotes/Quote.accepted.template.ts b/src/Email/Templates/Quotes/Quote.accepted.template.ts
new file mode 100644
index 0000000..c2f4a36
--- /dev/null
+++ b/src/Email/Templates/Quotes/Quote.accepted.template.ts
@@ -0,0 +1,23 @@
+import { stripIndents } from "common-tags";
+import { CPG_Customer_Panel_Domain } from "../../../Config";
+import { ICustomer } from "@interface/Customer.interface";
+import getFullName from "../../../Lib/Customers/getFullName";
+import UseStyles from "../General/UseStyles";
+import { IQuotes } from "@interface/Quotes.interface";
+
+export default async (quote: IQuotes, customer: ICustomer) => await UseStyles(stripIndents`
+
+
Hello ${getFullName(customer)}.
+
+ This is a notice that quote #${quote.id} has been accepted.
+
+
+ We will generate a invoice for you shortly.
+
+ ${CPG_Customer_Panel_Domain ? `
+
+ View quote.
+
+ ` : ''}
+
+`);
\ No newline at end of file
diff --git a/src/Email/Templates/Quotes/Quote.create.template.ts b/src/Email/Templates/Quotes/Quote.create.template.ts
new file mode 100644
index 0000000..d443a43
--- /dev/null
+++ b/src/Email/Templates/Quotes/Quote.create.template.ts
@@ -0,0 +1,39 @@
+import { stripIndents } from "common-tags";
+import { Company_Name, CPG_Customer_Panel_Domain } from "../../../Config";
+import { ICustomer } from "@interface/Customer.interface";
+import getFullName from "../../../Lib/Customers/getFullName";
+import UseStyles from "../General/UseStyles";
+import { IQuotes } from "@interface/Quotes.interface";
+import printQuotesItemsTable from "../Methods/QuotesItems.print";
+
+export default async (quote: IQuotes, customer: ICustomer) => await UseStyles(stripIndents`
+
+
Hello ${getFullName(customer)}.
+
+ This is a notice that ${await Company_Name()} has sent a quote to you.
+
+
+ Memo: ${quote.memo}
+
+
+ Due Date: ${quote.due_date}
+
+
+ Payment Method: ${quote.payment_method}
+
+
+ ${await printQuotesItemsTable(quote)}
+
+
+
+ Total:
+
+ ${quote.items.reduce((total, item) => total + (item.price * item.quantity), 0) + ((quote.tax_rate/100) * quote.items.reduce((total, item) => total + (item.price * item.quantity), 0))}
+
+ ${CPG_Customer_Panel_Domain ? `
+
+ View quote to accept or decline.
+
+ ` : ''}
+
+`);
\ No newline at end of file
diff --git a/src/Email/Templates/Transaction/NewTransaction.template.ts b/src/Email/Templates/Transaction/NewTransaction.template.ts
index 663fec8..86a4d97 100644
--- a/src/Email/Templates/Transaction/NewTransaction.template.ts
+++ b/src/Email/Templates/Transaction/NewTransaction.template.ts
@@ -29,7 +29,7 @@ export = async (t: ITransactions, c: ICustomer, charged = false) => UseStyles(st
${CPG_Customer_Panel_Domain ? `
- View Transaction
+ View Transaction
` : ''}
diff --git a/src/Interfaces/Quotes.interface.ts b/src/Interfaces/Quotes.interface.ts
index eabccc9..ac09fb7 100644
--- a/src/Interfaces/Quotes.interface.ts
+++ b/src/Interfaces/Quotes.interface.ts
@@ -1,3 +1,5 @@
+import { TPaymentCurrency } from "../Lib/Currencies";
+import { ICustomer } from "./Customer.interface";
import { IInvoice } from "./Invoice.interface";
import { IPayments } from "./Payments.interface";
import { IPromotionsCodes } from "./PromotionsCodes.interface";
@@ -6,7 +8,7 @@ export interface IQuotes
{
uid: `QUO_${string}`;
id: number;
- customer_uid: string;
+ customer_uid: ICustomer["uid"];
items: IQuoteItem[];
promotion_codes: IPromotionsCodes["id"][] | [];
due_date: string;
@@ -14,13 +16,16 @@ export interface IQuotes
payment_method: keyof IPayments;
notified: boolean;
created_invoice: boolean;
+ tax_rate: number;
+ currency: TPaymentCurrency;
+ accepted: boolean;
+ declined: boolean;
invoice_uid?: IInvoice["uid"] | IInvoice["id"];
}
export interface IQuoteItem
{
name: string;
- tax_rate: number;
price: number;
quantity: number;
}
\ No newline at end of file
diff --git a/src/Interfaces/Transactions.interface.ts b/src/Interfaces/Transactions.interface.ts
index 03eaa04..d45776d 100644
--- a/src/Interfaces/Transactions.interface.ts
+++ b/src/Interfaces/Transactions.interface.ts
@@ -13,6 +13,7 @@ import { IInvoice } from "./Invoice.interface";
*/
export interface ITransactions
{
+ id: number;
uid: `TRAN_${string}`;
customer_uid: ICustomer["uid"];
invoice_uid: IInvoice["uid"];
@@ -21,6 +22,4 @@ export interface ITransactions
amount: IInvoice["amount"];
currency: TPaymentCurrency;
fees: number;
-}
-
-export interface IDTransactions extends ITransactions, Document {}
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/Lib/Invoices/CreatePDFInvoice.ts b/src/Lib/Invoices/CreatePDFInvoice.ts
index 21381a6..b8f7dc4 100644
--- a/src/Lib/Invoices/CreatePDFInvoice.ts
+++ b/src/Lib/Invoices/CreatePDFInvoice.ts
@@ -147,7 +147,7 @@ export default function createPDFInvoice(invoice: IInvoice): Promise
)
data["client"]["custom1"] = `
Innehar ${(await Company_Tax_Registered()) ? "" : "inte"} F-Skattsedel`;
- if(Company_Logo_Url && PDF_Template_Url === "")
+ if(await Company_Logo_Url() !== "" && PDF_Template_Url === "")
// @ts-ignore
data["images"]["logo"] = await Company_Logo_Url();
diff --git a/src/Lib/Orders/newInvoice.ts b/src/Lib/Orders/newInvoice.ts
index f91ef08..15cb1da 100644
--- a/src/Lib/Orders/newInvoice.ts
+++ b/src/Lib/Orders/newInvoice.ts
@@ -90,7 +90,9 @@ export async function createInvoiceFromOrder(order: IOrder)
customer_uid: Customer_Id,
dates: {
due_date: order.dates.next_recycle,
- invoice_date: dateFormat.format(new Date(), "YYYY-MM-DD"),
+ // Possible fix to issue #94
+ invoice_date: order.dates.last_recycle,
+ // invoice_date: dateFormat.format(new Date(), "YYYY-MM-DD"),
},
amount: items.reduce((acc, item) =>
{
diff --git a/src/Lib/Quotes/CreateQuotePdf.ts b/src/Lib/Quotes/CreateQuotePdf.ts
index 51d36db..126b32c 100644
--- a/src/Lib/Quotes/CreateQuotePdf.ts
+++ b/src/Lib/Quotes/CreateQuotePdf.ts
@@ -29,7 +29,7 @@ export default function createQuotePdf(quote: IQuotes): Promise
},
"translate": {
- "invoice": `Quote`,
+ "invoice": `Quote #${quote.id}`,
"number": GetText().invoice.txt_Number,
"date": GetText().invoice.txt_Date,
"due-date": GetText().invoice.txt_DueDate,
@@ -49,11 +49,11 @@ export default function createQuotePdf(quote: IQuotes): Promise
"margin-bottom": 25,
},
"sender": {
- "company": Company_Name,
- "address": Company_Address,
- "zip": Company_Zip,
- "city": Company_City,
- "country": Company_Country,
+ "company": (await Company_Name()),
+ "address": await Company_Address(),
+ "zip": await Company_Zip(),
+ "city": await Company_City(),
+ "country": await Company_Country(),
},
"client": {
"company": Customer.billing.company ?? `${Customer.personal.first_name} ${Customer.personal.last_name}`,
@@ -71,7 +71,7 @@ export default function createQuotePdf(quote: IQuotes): Promise
return {
"quantity": item.quantity,
"description": item.name,
- "tax-rate": item.tax_rate,
+ "tax-rate": quote.tax_rate,
"price": item.price
}
}),
@@ -84,9 +84,9 @@ export default function createQuotePdf(quote: IQuotes): Promise
data["client"]["custom1"] = `
Innehar ${await Company_Tax_Registered() ? "" : "inte"} F-Skattsedel`;
- if(Company_Logo_Url && PDF_Template_Url === "")
+ if(await Company_Logo_Url() !== "" && PDF_Template_Url === "")
// @ts-ignore
- data["images"]["logo"] = Company_Logo_Url;
+ data["images"]["logo"] = await Company_Logo_Url();
if(PDF_Template_Url !== "")
// @ts-ignore
diff --git a/src/Lib/Quotes/QuoteToInvoice.ts b/src/Lib/Quotes/QuoteToInvoice.ts
new file mode 100644
index 0000000..452364c
--- /dev/null
+++ b/src/Lib/Quotes/QuoteToInvoice.ts
@@ -0,0 +1,35 @@
+import { IQuotes } from "@interface/Quotes.interface";
+import InvoiceModel from "../../Database/Models/Invoices.model";
+import { idInvoice } from "../Generator";
+import dateFormat from "date-and-time";
+import mainEvent from "../../Events/Main.event";
+
+export default async (quote: IQuotes) =>
+{
+ // Converts quote to invoice
+ const invoice = await (new InvoiceModel({
+ uid: idInvoice(),
+ customer_uid: quote.customer_uid,
+ items: quote.items.map(item => ({
+ notes: item.name,
+ amount: item.price,
+ quantity: item.quantity,
+ })),
+ dates: {
+ invoice_date: dateFormat.format(new Date(), "YYYY-MM-DD"),
+ due_date: quote.due_date,
+ },
+ amount: quote.items.reduce((acc, item) => acc + item.price * item.quantity, 0),
+ currency: quote.currency,
+ tax_rate: quote.tax_rate,
+ notified: false,
+ transactions: [],
+ paid: false,
+ notes: quote.memo,
+ payment_method: quote.payment_method,
+ }).save());
+
+ mainEvent.emit("invoice_created", invoice);
+
+ return invoice;
+}
\ No newline at end of file
diff --git a/src/Server/Routes/v2/Customers/Customers.config.ts b/src/Server/Routes/v2/Customers/Customers.config.ts
index 2a3eaf6..72a62c7 100644
--- a/src/Server/Routes/v2/Customers/Customers.config.ts
+++ b/src/Server/Routes/v2/Customers/Customers.config.ts
@@ -27,6 +27,7 @@ import { idImages } from "../../../../Lib/Generator";
import { CacheImages } from "../../../../Cache/Image.cache";
import ImageModel from "../../../../Database/Models/Images.model";
import Jimp from 'jimp';
+import QuotesModel from "../../../../Database/Models/Quotes.model";
export = class CustomerRouter
{
@@ -319,6 +320,62 @@ export = class CustomerRouter
return APISuccess("Order cancelled.")(res);
});
+ this.router.get("/my/quotes", EnsureAuth(), async (req, res) =>
+ {
+ const customer = await CustomerModel.findOne({
+ // @ts-ignore
+ id: req.customer.id
+ });
+
+ if(!customer)
+ return APIError(`Unable to find customer`)(res);
+
+ const data = await MongoFind(QuotesModel, req.query,{
+ $or: [
+ { customer_uid: customer.uid },
+ { customer_uid: customer.id }
+ ]
+ });
+
+ res.setHeader("X-Total-Pages", data.totalPages);
+ res.setHeader("X-Total", data.totalCount);
+
+ return APISuccess(data.data)(res);
+ });
+
+ this.router.get("/my/quotes/:id", EnsureAuth(), async (req, res) =>
+ {
+ const quoteId = req.params.id;
+
+ if(!quoteId)
+ return APIError(`Invalid invoice id`)(res);
+
+ const customer = await CustomerModel.findOne({
+ // @ts-ignore
+ id: req.customer.id
+ });
+
+ if(!customer)
+ return APIError(`Unable to find customer`)(res);
+
+ const {data: [order]} = await MongoFind(QuotesModel, req.query,{
+ $or: [
+ {
+ customer_uid: customer.uid,
+ },
+ {
+ customer_uid: customer.id,
+ },
+ ],
+ id: quoteId,
+ });
+
+ if(!order)
+ return APIError(`Unable to find quote`)(res);
+
+ return APISuccess(order)(res);
+ });
+
this.router.get("/my/transactions", EnsureAuth(), async (req, res) =>
{
const customer = await CustomerModel.findOne({
diff --git a/src/Server/Routes/v2/Quotes/Quotes.config.ts b/src/Server/Routes/v2/Quotes/Quotes.config.ts
index 209bf15..2ea4f45 100644
--- a/src/Server/Routes/v2/Quotes/Quotes.config.ts
+++ b/src/Server/Routes/v2/Quotes/Quotes.config.ts
@@ -3,9 +3,14 @@ import CustomerModel from "../../../../Database/Models/Customers/Customer.model"
import QuotesModel from "../../../../Database/Models/Quotes.model";
import AW from "../../../../Lib/AW";
import createQuotePdf from "../../../../Lib/Quotes/CreateQuotePdf";
-import { APIError } from "../../../../Lib/Response";
+import QuoteToInvoice from "../../../../Lib/Quotes/QuoteToInvoice";
+import { APIError, APISuccess } from "../../../../Lib/Response";
import EnsureAdmin from "../../../../Middlewares/EnsureAdmin";
+import EnsureAuth from "../../../../Middlewares/EnsureAuth";
import QuotesController from "./Quotes.controller";
+import { sendInvoiceEmail } from "../../../../Lib/Invoices/SendEmail";
+import { sendEmail } from "../../../../Email/Send";
+import QuoteAcceptedTemplate from "../../../../Email/Templates/Quotes/Quote.accepted.template";
export = class QuotesRouter
{
@@ -27,11 +32,13 @@ export = class QuotesRouter
QuotesController.getByUid
]);
- this.router.get("/:uid/view", async (req, res) =>
+ this.router.get("/:uid/view", EnsureAuth(), async (req, res) =>
{
- //
const uid = req.params.uid;
- const [quote, e_quote] = await AW(await QuotesModel.findOne({ uid: uid }));
+ const [quote, e_quote] = await AW(await QuotesModel.findOne({ $or: [
+ { uid: uid },
+ { id: uid }
+ ] }));
if(e_quote || !quote)
return APIError(`Failed to fetch quote with uid ${uid}`)(res);
@@ -53,6 +60,69 @@ export = class QuotesRouter
res.end(result, "base64");
});
+ this.router.post("/:uid/accept", EnsureAuth(), async (req, res) =>
+ {
+ const uid = req.params.uid;
+ const [quote, e_quote] = await AW(await QuotesModel.findOne({ $or: [
+ { uid: uid },
+ { id: uid }
+ ] }));
+
+ if(e_quote || !quote)
+ return APIError(`Failed to fetch quote with uid ${uid}`)(res);
+
+ const customer = await CustomerModel.findOne({ $or: [
+ { id: quote.customer_uid },
+ { uid: quote.customer_uid as any }
+ ] });
+
+ if(!customer)
+ return APIError(`Failed to fetch customer with uid ${quote.customer_uid}`)(res);
+
+ if(quote.accepted)
+ return APIError(`Quote already accepted`)(res);
+
+ quote.accepted = true;
+
+ await quote.save();
+
+ await sendEmail({
+ receiver: customer.personal.email,
+ subject: `Quote accepted | #${quote.id}`,
+ body: {
+ body: QuoteAcceptedTemplate(quote, customer),
+ }
+ });
+
+ // Convert quote to invoice
+ const invoice = await QuoteToInvoice(quote);
+ if(!invoice)
+ return APIError("Failed to convert quote to invoice")(res);
+
+ // Send email to customer, no need to await since if it fails it will run cron either way
+ sendInvoiceEmail(invoice, customer);
+
+ return APISuccess(invoice)(res);
+ });
+
+ this.router.post("/:uid/decline", EnsureAuth(), async (req, res) =>
+ {
+ const uid = req.params.uid;
+ const [quote, e_quote] = await AW(await QuotesModel.findOne({ $or: [
+ { uid: uid },
+ { id: uid }
+ ] }));
+
+ if(e_quote || !quote)
+ return APIError(`Failed to fetch quote with uid ${uid}`)(res);
+
+ quote.declined = true;
+
+ await quote.save();
+
+ return APISuccess(`Declined quote offer.`)(res);
+ });
+
this.router.post("/", [
EnsureAdmin(),
QuotesController.insert
diff --git a/src/Server/Routes/v2/Quotes/Quotes.controller.ts b/src/Server/Routes/v2/Quotes/Quotes.controller.ts
index 5c1e1e0..bc39f81 100644
--- a/src/Server/Routes/v2/Quotes/Quotes.controller.ts
+++ b/src/Server/Routes/v2/Quotes/Quotes.controller.ts
@@ -1,14 +1,14 @@
import { Request, Response } from "express";
-import { Company_Name, Full_Domain } from "../../../../Config";
+import { Company_Name } from "../../../../Config";
import CustomerModel from "../../../../Database/Models/Customers/Customer.model";
import QuotesModel from "../../../../Database/Models/Quotes.model";
import { SendEmail } from "../../../../Email/Send";
import mainEvent from "../../../../Events/Main.event";
import { IQuotes } from "@interface/Quotes.interface";
-import getFullName from "../../../../Lib/Customers/getFullName";
import { idQuotes } from "../../../../Lib/Generator";
import { APISuccess } from "../../../../Lib/Response";
import BaseModelAPI from "../../../../Models/BaseModelAPI";
+import QuoteCreateTemplate from "../../../../Email/Templates/Quotes/Quote.create.template";
const API = new BaseModelAPI(idQuotes, QuotesModel);
@@ -32,20 +32,7 @@ function insert(req: Request, res: Response)
// Send email to customer.
await SendEmail(Customer.personal.email, `Quote from ${await Company_Name() === "" ? "CPG" : await Company_Name()}`, {
isHTML: true,
- body: `
- Quote
-
- Hello ${getFullName(Customer)}!
-
-
- You have a new quote.
-
-
-
- Click here to view the quote.
-
-
- `
+ body: await QuoteCreateTemplate(result, Customer)
});
}
}