Merge branch 'develop' into feature-paymentAssetManagement

This commit is contained in:
Joao Ramos
2023-12-13 23:43:52 +00:00
8 changed files with 264 additions and 153 deletions

View File

@@ -1,5 +1,4 @@
import {infoButtonStyle} from "@/constants/buttonStyles"; import {infoButtonStyle} from "@/constants/buttonStyles";
import {BAND_SCORES} from "@/constants/ielts";
import {Module} from "@/interfaces"; import {Module} from "@/interfaces";
import {User} from "@/interfaces/user"; import {User} from "@/interfaces/user";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";

View File

@@ -60,7 +60,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
const [paymentValue, setPaymentValue] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.value : undefined); const [paymentValue, setPaymentValue] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.value : undefined);
const [paymentCurrency, setPaymentCurrency] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.currency : "EUR"); const [paymentCurrency, setPaymentCurrency] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.currency : "EUR");
const [monthlyDuration, setMonthlyDuration] = useState(user.type === "corporate" ? user.corporateInformation?.monthlyDuration : undefined); const [monthlyDuration, setMonthlyDuration] = useState(user.type === "corporate" ? user.corporateInformation?.monthlyDuration : undefined);
const [commissionValue, setCommission] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.commission : undefined);
const {stats} = useStats(user.id); const {stats} = useStats(user.id);
const {users} = useUsers(); const {users} = useUsers();
@@ -106,6 +106,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
payment: { payment: {
value: paymentValue, value: paymentValue,
currency: paymentCurrency, currency: paymentCurrency,
...referralAgent === '' ? {} : { commission: commissionValue }
}, },
} }
: undefined, : undefined,
@@ -194,8 +195,31 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
placeholder="Enter monthly duration" placeholder="Enter monthly duration"
defaultValue={monthlyDuration} defaultValue={monthlyDuration}
/> />
<div className="flex flex-col gap-3 w-full lg:col-span-2">
<div className="flex flex-col gap-3 w-full"> <label className="font-normal text-base text-mti-gray-dim">Pricing</label>
<div className="w-full grid grid-cols-5 gap-2">
<Input
name="paymentValue"
onChange={(e) => setPaymentValue(e ? parseInt(e) : undefined)}
type="number"
defaultValue={paymentValue || 0}
className="col-span-3"
/>
<select
defaultValue={paymentCurrency}
onChange={(e) => setPaymentCurrency(e.target.value)}
className="p-6 col-span-2 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer bg-white">
{CURRENCIES.map(({label, currency}) => (
<option value={currency} key={currency}>
{label}
</option>
))}
</select>
</div>
</div>
</div>
<div className="flex gap-3 w-full">
<div className="flex flex-col gap-3 w-8/12">
<label className="font-normal text-base text-mti-gray-dim">Country Manager</label> <label className="font-normal text-base text-mti-gray-dim">Country Manager</label>
{referralAgentLabel && ( {referralAgentLabel && (
<Select <Select
@@ -228,28 +252,19 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
/> />
)} )}
</div> </div>
<div className="flex flex-col gap-3 w-4/12">
<div className="flex flex-col gap-3 w-full lg:col-span-2"> {referralAgent !== '' ? (
<label className="font-normal text-base text-mti-gray-dim">Pricing</label> <>
<div className="w-full grid grid-cols-5 gap-2"> <label className="font-normal text-base text-mti-gray-dim">Commission</label>
<Input <Input
name="paymentValue" name="commissionValue"
onChange={(e) => setPaymentValue(e ? parseInt(e) : undefined)} onChange={(e) => setCommission(e ? parseInt(e) : undefined)}
type="number" type="number"
defaultValue={paymentValue || 0} defaultValue={commissionValue || 0}
className="col-span-3" className="col-span-3"
/> />
<select </>
defaultValue={paymentCurrency} ) : <div />}
onChange={(e) => setPaymentCurrency(e.target.value)}
className="p-6 col-span-2 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer bg-white">
{CURRENCIES.map(({label, currency}) => (
<option value={currency} key={currency}>
{label}
</option>
))}
</select>
</div>
</div> </div>
</div> </div>
<Divider className="w-full !m-0" /> <Divider className="w-full !m-0" />

View File

@@ -2,20 +2,20 @@ import {Module} from "@/interfaces";
export const MODULES: Module[] = ["reading", "listening", "writing", "speaking"]; export const MODULES: Module[] = ["reading", "listening", "writing", "speaking"];
export const BAND_SCORES: {[key in Module]: number[]} = { // BAND SCORES is not in use anymore and level scoring is made based on thresholds
reading: [0, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9], // export const BAND_SCORES: {[key in Module]: number[]} = {
listening: [0, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9], // reading: [0, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9],
writing: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], // listening: [0, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9],
speaking: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], // writing: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
level: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], // speaking: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
}; // level: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
// };
export const moduleResultText = (level: number) => { export type LevelScore = "Advanced" | "Upper-Intermediate" | "Intermediate" | "Pre-Intermediate" | "Elementary" | "Beginner";
if (level === 9) {
return (
const generateHighestScoreText = () : React.ReactNode => (
<> <>
Congratulations on your exam performance! You achieved an impressive <span className="font-bold">level {level}</span>, demonstrating
excellent mastery of the assessed knowledge.
<br /> <br />
<br /> <br />
If you disagree with the result, you can request a review by a qualified teacher. We are committed to the accuracy and transparency of If you disagree with the result, you can request a review by a qualified teacher. We are committed to the accuracy and transparency of
@@ -26,13 +26,9 @@ export const moduleResultText = (level: number) => {
academic journey. academic journey.
</> </>
); );
}
if (level >= 6) { const generateAverageScoreText = () : React.ReactNode => (
return (
<> <>
Congratulations on your exam performance! You achieved a commendable <span className="font-bold">level {level}</span>, demonstrating a
good understanding of the assessed knowledge.
<br /> <br />
<br /> <br />
If you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and If you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and
@@ -43,29 +39,9 @@ export const moduleResultText = (level: number) => {
journey. journey.
</> </>
); );
}
if (level >= 3) { const generateLowestScoreText = () : React.ReactNode => (
return (
<> <>
Congratulations on your exam performance! You achieved a <span className="font-bold">level of {level}</span>, demonstrating a
satisfactory understanding of the assessed knowledge.
<br />
<br />
If you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and
transparency of the results.
<br />
<br />
Please contact us for further information. Congratulations again on your achievement! We are here to support you on your academic
journey.
</>
);
}
return (
<>
Thank you for taking the exam. You achieved a <span className="font-bold">level {level}</span>, but unfortunately, it did not meet the
required standards.
<br /> <br />
<br /> <br />
If you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and If you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and
@@ -75,23 +51,70 @@ export const moduleResultText = (level: number) => {
Please contact us for further information. We encourage you to continue your studies and wish you the best of luck in your future Please contact us for further information. We encourage you to continue your studies and wish you the best of luck in your future
endeavors. endeavors.
</> </>
); )
};
export const levelResultText = (level: number) => { export const levelResultText = (level: number) => {
if(level === 9) {
return (
<>
{"Outstanding! Your command of English is excellent. Focus on fine-tuning subtle language nuances and exploring sophisticated vocabulary. Keep up the excellent work!"}
{generateHighestScoreText()}
</>
);
}
if(level >= 8) {
return (
<>
{"Impressive! You're approaching fluency. Continue refining nuances in grammar and expanding your vocabulary to express ideas more precisely."}
{generateAverageScoreText()}
</>
);
}
if(level >= 6) {
return (
<>
{"Great job! You're navigating the complexities of English. Keep honing your grammar skills and exploring more advanced vocabulary."}
{generateAverageScoreText()}
</>
);
}
if(level >= 4) {
return (
<>
{"Well done! You're moving beyond the basics. Work on expanding your vocabulary and refining your understanding of grammar structures."}
{generateAverageScoreText()}
</>
);
}
if(level >= 2) {
return (
<>
{"Good effort! You're making progress. Continue studying and pay attention to common vocabulary and fundamental grammar rules."}
{generateAverageScoreText()}
</>
);
}
if(level >= 0) {
return (
<>
{"Keep practicing! You're just starting, and improvement takes time. Focus on building your vocabulary and basic grammar skills."}
{generateLowestScoreText()}
</>
);
}
return null;
};
export const moduleResultText = (module: Module, level: number) => {
if(module === 'level') return levelResultText(level);
if (level === 9) { if (level === 9) {
return ( return (
<> <>
Congratulations on your exam performance! You achieved an impressive <span className="font-bold">level {level}</span>, demonstrating Congratulations on your exam performance! You achieved an impressive <span className="font-bold">level {level}</span>, demonstrating
excellent mastery of the assessed knowledge. excellent mastery of the assessed knowledge.
<br /> {generateHighestScoreText()}
<br />
If you disagree with the result, you can request a review by a qualified teacher. We are committed to the accuracy and transparency of
the results.
<br />
<br />
Please contact us for further information. Congratulations again on your outstanding achievement! We are here to support you on your
academic journey.
</> </>
); );
} }
@@ -101,14 +124,7 @@ export const levelResultText = (level: number) => {
<> <>
Congratulations on your exam performance! You achieved a commendable <span className="font-bold">level {level}</span>, demonstrating a Congratulations on your exam performance! You achieved a commendable <span className="font-bold">level {level}</span>, demonstrating a
good understanding of the assessed knowledge. good understanding of the assessed knowledge.
<br /> {generateAverageScoreText()}
<br />
If you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and
transparency of the results.
<br />
<br />
Please contact us for further information. Congratulations again on your achievement! We are here to support you on your academic
journey.
</> </>
); );
} }
@@ -118,14 +134,7 @@ export const levelResultText = (level: number) => {
<> <>
Congratulations on your exam performance! You achieved a <span className="font-bold">level of {level}</span>, demonstrating a Congratulations on your exam performance! You achieved a <span className="font-bold">level of {level}</span>, demonstrating a
satisfactory understanding of the assessed knowledge. satisfactory understanding of the assessed knowledge.
<br /> {generateAverageScoreText()}
<br />
If you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and
transparency of the results.
<br />
<br />
Please contact us for further information. Congratulations again on your achievement! We are here to support you on your academic
journey.
</> </>
); );
} }
@@ -134,14 +143,7 @@ export const levelResultText = (level: number) => {
<> <>
Thank you for taking the exam. You achieved a <span className="font-bold">level {level}</span>, but unfortunately, it did not meet the Thank you for taking the exam. You achieved a <span className="font-bold">level {level}</span>, but unfortunately, it did not meet the
required standards. required standards.
<br /> {generateLowestScoreText()}
<br />
If you have any concerns about the result, you can request a review by a qualified teacher. We are committed to the accuracy and
transparency of the results.
<br />
<br />
Please contact us for further information. We encourage you to continue your studies and wish you the best of luck in your future
endeavors.
</> </>
); );
}; };

View File

@@ -10,6 +10,8 @@ import Link from "next/link";
import {useRouter} from "next/router"; import {useRouter} from "next/router";
import {Fragment, useEffect, useState} from "react"; import {Fragment, useEffect, useState} from "react";
import {BsArrowCounterclockwise, BsBook, BsClipboard, BsEyeFill, BsHeadphones, BsMegaphone, BsPen, BsShareFill} from "react-icons/bs"; import {BsArrowCounterclockwise, BsBook, BsClipboard, BsEyeFill, BsHeadphones, BsMegaphone, BsPen, BsShareFill} from "react-icons/bs";
import { LevelScore } from "@/constants/ielts";
import { getLevelScore } from "@/utils/score";
interface Score { interface Score {
module: Module; module: Module;
@@ -66,6 +68,24 @@ export default function Finish({user, scores, modules, isLoading, onViewResults}
return exam.exercises.length; return exam.exercises.length;
}; };
const bandScore: number = calculateBandScore(selectedScore.correct, selectedScore.total, selectedModule, user.focus);
const showLevel = (level: number) => {
if(selectedModule === "level") {
const [levelStr, grade] = getLevelScore(level);
return (
<div className="flex flex-col items-center justify-center gap-1">
<span className="text-xl font-bold">{levelStr}</span>
<span className="text-xl">{grade}</span>
</div>
)
}
return <span className="text-3xl font-bold">{level}</span>;
}
return ( return (
<> <>
<div className="w-full min-h-full h-fit flex flex-col items-center justify-between gap-8"> <div className="w-full min-h-full h-fit flex flex-col items-center justify-between gap-8">
@@ -142,7 +162,7 @@ export default function Finish({user, scores, modules, isLoading, onViewResults}
{!isLoading && ( {!isLoading && (
<div className="w-full flex gap-9 mt-32 items-center justify-between mb-20"> <div className="w-full flex gap-9 mt-32 items-center justify-between mb-20">
<span className="max-w-3xl"> <span className="max-w-3xl">
{moduleResultText(calculateBandScore(selectedScore.correct, selectedScore.total, selectedModule, user.focus))} {moduleResultText(selectedModule, bandScore)}
</span> </span>
<div className="flex gap-9 px-16"> <div className="flex gap-9 px-16">
<div <div
@@ -156,9 +176,7 @@ export default function Finish({user, scores, modules, isLoading, onViewResults}
moduleColors[selectedModule].inner, moduleColors[selectedModule].inner,
)}> )}>
<span className="text-xl">Level</span> <span className="text-xl">Level</span>
<span className="text-3xl font-bold"> {showLevel(bandScore)}
{calculateBandScore(selectedScore.correct, selectedScore.total, selectedModule, user.focus)}
</span>
</div> </div>
</div> </div>
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-5">

View File

@@ -31,7 +31,7 @@ export interface Payment {
currency: string; currency: string;
value: number; value: number;
isPaid: boolean; isPaid: boolean;
date: Date; date: Date | string;
corporateTransfer?: string; corporateTransfer?: string;
commissionTransfer?: string; commissionTransfer?: string;
} }

View File

@@ -57,6 +57,7 @@ export interface CorporateInformation {
payment?: { payment?: {
value: number; value: number;
currency: string; currency: string;
commission: number;
}; };
referralAgent?: string; referralAgent?: string;
} }

View File

@@ -1,19 +1,72 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next"; import type {NextApiRequest, NextApiResponse} from "next";
import {app, storage} from "@/firebase"; import {app, storage} from "@/firebase";
import {getFirestore, collection, getDocs, getDoc, doc, setDoc} from "firebase/firestore"; import {getFirestore, collection, getDocs, getDoc, doc, setDoc, query, where} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next"; import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session"; import {sessionOptions} from "@/lib/session";
import {User} from "@/interfaces/user"; import {User} from "@/interfaces/user";
import {getDownloadURL, getStorage, ref, uploadBytes} from "firebase/storage"; import {getDownloadURL, getStorage, ref, uploadBytes} from "firebase/storage";
import {getAuth, signInWithEmailAndPassword, updateEmail, updatePassword} from "firebase/auth"; import {getAuth, signInWithEmailAndPassword, updateEmail, updatePassword} from "firebase/auth";
import {errorMessages} from "@/constants/errors"; import {errorMessages} from "@/constants/errors";
import moment from "moment";
import ShortUniqueId from "short-unique-id";
import {Payment} from "@/interfaces/paypal";
const db = getFirestore(app); const db = getFirestore(app);
const auth = getAuth(app); const auth = getAuth(app);
export default withIronSessionApiRoute(handler, sessionOptions); export default withIronSessionApiRoute(handler, sessionOptions);
// TODO: Data is set as any as data cannot be parsed to Payment
// because the id is not a par of the hash and payment expects date to be of type Date
// but if it is not inserted as a string, some UI components will not work (Invalid Date)
const addPaymentRecord = async (data: any) => {
await setDoc(doc(db, "payments", data.id), data);
}
const managePaymentRecords = async (user: User, userId: string | undefined): Promise<boolean> => {
try {
if(user.type === 'corporate' && userId) {
const shortUID = new ShortUniqueId();
const data: Payment = {
id: shortUID.randomUUID(8),
corporate: userId,
agent: user.corporateInformation.referralAgent,
agentCommission: user.corporateInformation.payment!.commission,
agentValue: (user.corporateInformation.payment!.commission / 100) * user.corporateInformation.payment!.value,
currency: user.corporateInformation.payment!.currency,
value: user.corporateInformation.payment!.value,
isPaid: false,
date: new Date().toISOString(),
};
const corporatePayments = await getDocs(query(collection(db, "payments"), where("corporate", "==", userId)));
if(corporatePayments.docs.length === 0) {
await addPaymentRecord(data);
return true;
}
const hasPaymentPaidAndExpiring = corporatePayments.docs.filter((doc) => {
const data = doc.data();
return data.isPaid
&& moment().isAfter(moment(user.subscriptionExpirationDate).subtract(30, "days"))
&& moment().isBefore(moment(user.subscriptionExpirationDate));
});
if(hasPaymentPaidAndExpiring.length > 0) {
await addPaymentRecord(data);
return true;
}
}
return false;
} catch(e) {
// if this process fails it should not stop the rest of the process
console.log(e);
return false;
}
}
async function handler(req: NextApiRequest, res: NextApiResponse) { async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) { if (!req.session.user) {
res.status(401).json({ok: false}); res.status(401).json({ok: false});
@@ -24,7 +77,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const updatedUser = req.body as User & {password?: string; newPassword?: string}; const updatedUser = req.body as User & {password?: string; newPassword?: string};
if (!!req.query.id) { if (!!req.query.id) {
await setDoc(userRef, updatedUser, {merge: true}); const user = await setDoc(userRef, updatedUser, {merge: true});
await managePaymentRecords(updatedUser, updatedUser.id);
res.status(200).json({ok: true}); res.status(200).json({ok: true});
return; return;
} }
@@ -73,6 +127,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
await req.session.save(); await req.session.save();
} }
await managePaymentRecords(user, req.query.id);
res.status(200).json({user}); res.status(200).json({user});
} }

View File

@@ -1,4 +1,5 @@
import {Module} from "@/interfaces"; import {Module} from "@/interfaces";
import { LevelScore } from "@/constants/ielts";
type Type = "academic" | "general"; type Type = "academic" | "general";
@@ -94,11 +95,12 @@ const academicMarking: {[key: number]: number} = {
}; };
const levelMarking: {[key: number]: number} = { const levelMarking: {[key: number]: number} = {
88: 9, 88: 9, // Advanced
64: 8, 64: 8 , // Upper-Intermediate
52: 6, 52: 6, // Intermediate
32: 4, 32: 4, // Pre-Intermediate
16: 2, 16: 2, // Elementary
0: 0, // Beginner
}; };
const moduleMarkings: {[key in Module]: {[key in Type]: {[key: number]: number}}} = { const moduleMarkings: {[key in Module]: {[key in Type]: {[key: number]: number}}} = {
@@ -142,3 +144,21 @@ export const calculateBandScore = (correct: number, total: number, module: Modul
export const calculateAverageLevel = (levels: {[key in Module]: number}) => { export const calculateAverageLevel = (levels: {[key in Module]: number}) => {
return Object.keys(levels).reduce((accumulator, current) => levels[current as Module] + accumulator, 0) / 4; return Object.keys(levels).reduce((accumulator, current) => levels[current as Module] + accumulator, 0) / 4;
}; };
export const getLevelScore = (level: number) => {
switch(level) {
case 0:
return ['Beginner', 'Low A1'];
case 2:
return ['Elementary', 'High A1/Low A2'];
case 4:
return ['Pre-Intermediate', 'High A2/Low B1'];
case 6:
return ['Intermediate', 'High B1/Low B2'];
case 8:
return ['Upper-Intermediate', 'High B2/Low C1'];
case 9:
return ['Advanced', 'C1'];
default: return [];
}
}