Compare commits
10 Commits
3cfc073dd8
...
43a622851d
Author | SHA1 | Date | |
---|---|---|---|
43a622851d | |||
8b79d9aa4b | |||
e592fef4ad | |||
2d8774ae36 | |||
b6a15e0579 | |||
5704cf6ff9 | |||
67be77ab9c | |||
46bbb9f8da | |||
7b2ec83068 | |||
a9bb588365 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -9,7 +9,7 @@
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
.vscode
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
|
12
package.json
12
package.json
@ -7,6 +7,7 @@
|
||||
"build": "next build",
|
||||
"start": "next start --port 4000",
|
||||
"lint": "next lint",
|
||||
"test": "vitest",
|
||||
"seed": "node -r dotenv/config ./src/bootstrap/db/seed.js"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -15,7 +16,7 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.454.0",
|
||||
"next": "15.0.1",
|
||||
"next": "15.0.2",
|
||||
"postgres": "^3.4.5",
|
||||
"react": "19.0.0-rc-69d4b800-20241021",
|
||||
"react-dom": "19.0.0-rc-69d4b800-20241021",
|
||||
@ -26,15 +27,22 @@
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^9.1.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/react": "^16.0.1",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"bcrypt": "^5.1.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "15.0.1",
|
||||
"jsdom": "^25.0.1",
|
||||
"moq.ts": "^10.0.8",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
"typescript": "^5",
|
||||
"vitest": "^2.1.4"
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import fetchSummaryInfoUsecase from '@/feature/core/summary-info/domain/usecase/fetch-summary-info-usecase';
|
||||
import {
|
||||
BanknotesIcon,
|
||||
ClockIcon,
|
||||
UserGroupIcon,
|
||||
InboxIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { fetchCardData } from '@/app/lib/data';
|
||||
|
||||
const iconMap = {
|
||||
collected: BanknotesIcon,
|
||||
@ -14,21 +14,16 @@ const iconMap = {
|
||||
};
|
||||
|
||||
export default async function CardWrapper() {
|
||||
const {
|
||||
numberOfInvoices,
|
||||
numberOfCustomers,
|
||||
totalPaidInvoices,
|
||||
totalPendingInvoices,
|
||||
} = await fetchCardData();
|
||||
const {customersNumber, invoicesNumber, invoicesSummary } = await fetchSummaryInfoUsecase();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card title="Collected" value={totalPaidInvoices} type="collected" />
|
||||
<Card title="Pending" value={totalPendingInvoices} type="pending" />
|
||||
<Card title="Total Invoices" value={numberOfInvoices} type="invoices" />
|
||||
<Card title="Collected" value={invoicesSummary.paid} type="collected" />
|
||||
<Card title="Pending" value={invoicesSummary.pending} type="pending" />
|
||||
<Card title="Total Invoices" value={invoicesNumber} type="invoices" />
|
||||
<Card
|
||||
title="Total Customers"
|
||||
value={numberOfCustomers}
|
||||
value={customersNumber}
|
||||
type="customers"
|
||||
/>
|
||||
</>
|
||||
|
@ -1,9 +1,9 @@
|
||||
import fetchCustomerInvoicesUsecase from '@/feature/core/customer-invoice/domain/usecase/fetch-customer-invoices-usecase';
|
||||
import { ArrowPathIcon } from '@heroicons/react/24/outline';
|
||||
import clsx from 'clsx';
|
||||
import Image from 'next/image';
|
||||
import { fetchLatestInvoices } from '@/app/lib/data';
|
||||
export default async function LatestInvoices() {
|
||||
const latestInvoices = await fetchLatestInvoices();
|
||||
const latestInvoices = await fetchCustomerInvoicesUsecase();
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col md:col-span-4">
|
||||
@ -26,25 +26,25 @@ export default async function LatestInvoices() {
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Image
|
||||
src={invoice.image_url}
|
||||
alt={`${invoice.name}'s profile picture`}
|
||||
src={invoice.customerImageUrl}
|
||||
alt={`${invoice.customerName}'s profile picture`}
|
||||
className="mr-4 rounded-full"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-semibold md:text-base">
|
||||
{invoice.name}
|
||||
{invoice.customerName}
|
||||
</p>
|
||||
<p className="hidden text-sm text-gray-500 sm:block">
|
||||
{invoice.email}
|
||||
{invoice.customerEmail}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
className="truncate text-sm font-medium md:text-base"
|
||||
>
|
||||
{invoice.amount}
|
||||
{invoice.invoicesAmount}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { generateYAxis } from '@/app/lib/utils';
|
||||
import fetchRevenuesUsecase from '@/feature/core/revenue/domain/usecase/fetch-revenues-usecase';
|
||||
import { CalendarIcon } from '@heroicons/react/24/outline';
|
||||
import { fetchRevenue } from '@/app/lib/data';
|
||||
|
||||
export default async function RevenueChart() {
|
||||
const revenue = await fetchRevenue();
|
||||
const revenue = await fetchRevenuesUsecase();
|
||||
|
||||
const chartHeight = 350;
|
||||
|
||||
|
27
src/app/dashboard/module/dashboard-app-module.ts
Normal file
27
src/app/dashboard/module/dashboard-app-module.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import di from "@/bootstrap/di/init-di"
|
||||
import fetchCustomerInvoicesUsecase from "@/feature/core/customer-invoice/domain/usecase/fetch-customer-invoices-usecase";
|
||||
import fetchCustomersUsecase from "@/feature/core/customer/domain/usecase/fetch-customers-usecase";
|
||||
import fetchAllInvoicesAmountUsecase from "@/feature/core/invoice/domain/usecase/fetch-all-invoices-amount-usecase";
|
||||
import fetchRevenuesUsecase from "@/feature/core/revenue/domain/usecase/fetch-revenues-usecase";
|
||||
|
||||
export default function dashboardAppModule() {
|
||||
const dashboardDi = di.createChildContainer()
|
||||
|
||||
dashboardDi.register(fetchCustomersUsecase.name, {
|
||||
useValue: fetchCustomersUsecase
|
||||
})
|
||||
|
||||
dashboardDi.register(fetchAllInvoicesAmountUsecase.name, {
|
||||
useValue: fetchAllInvoicesAmountUsecase
|
||||
})
|
||||
dashboardDi.register(fetchAllInvoicesAmountUsecase.name, {
|
||||
useValue: fetchAllInvoicesAmountUsecase
|
||||
})
|
||||
dashboardDi.register(fetchCustomerInvoicesUsecase.name, {
|
||||
useValue: fetchCustomerInvoicesUsecase
|
||||
})
|
||||
dashboardDi.register(fetchRevenuesUsecase.name, {
|
||||
useValue: fetchRevenuesUsecase
|
||||
})
|
||||
return dashboardDi
|
||||
}
|
@ -1,254 +0,0 @@
|
||||
import { sql } from '@/bootstrap/db/db';
|
||||
import {
|
||||
CustomerField,
|
||||
CustomersTableType,
|
||||
InvoiceForm,
|
||||
InvoicesTable,
|
||||
User,
|
||||
Revenue,
|
||||
Invoice,
|
||||
Customer,
|
||||
LatestInvoice,
|
||||
} from './definitions';
|
||||
import { formatCurrency } from './utils';
|
||||
import postgres from 'postgres';
|
||||
import { connection } from 'next/server';
|
||||
|
||||
export async function fetchRevenue() {
|
||||
// This is equivalent to in fetch(..., {cache: 'no-store'}).
|
||||
connection()
|
||||
|
||||
try {
|
||||
// Artificially delay a response for demo purposes.
|
||||
// Don't do this in production :)
|
||||
|
||||
console.log('Fetching revenue data...');
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
|
||||
const data = await sql`SELECT * FROM revenue`;
|
||||
|
||||
console.log('Data fetch completed after 3 seconds.');
|
||||
|
||||
return data as postgres.RowList<Revenue[]>;
|
||||
} catch (error) {
|
||||
console.error('Database Error:', error);
|
||||
throw new Error('Failed to fetch revenue data.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchLatestInvoices() {
|
||||
// This is equivalent to in fetch(..., {cache: 'no-store'}).
|
||||
connection()
|
||||
|
||||
try {
|
||||
const data = await sql`
|
||||
SELECT invoices.amount, customers.name, customers.image_url, customers.email, invoices.id
|
||||
FROM invoices
|
||||
JOIN customers ON invoices.customer_id = customers.id
|
||||
ORDER BY invoices.date DESC
|
||||
LIMIT 5` as postgres.RowList<LatestInvoice[]>;
|
||||
|
||||
const latestInvoices = data.map((invoice) => ({
|
||||
...invoice,
|
||||
amount: formatCurrency(+invoice.amount),
|
||||
}));
|
||||
return latestInvoices;
|
||||
} catch (error) {
|
||||
console.error('Database Error:', error);
|
||||
throw new Error('Failed to fetch the latest invoices.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchCardData() {
|
||||
// This is equivalent to in fetch(..., {cache: 'no-store'}).
|
||||
connection()
|
||||
|
||||
try {
|
||||
// You can probably combine these into a single SQL query
|
||||
// However, we are intentionally splitting them to demonstrate
|
||||
// how to initialize multiple queries in parallel with JS.
|
||||
const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
|
||||
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;
|
||||
const invoiceStatusPromise = sql`SELECT
|
||||
SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS "paid",
|
||||
SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS "pending"
|
||||
FROM invoices`;
|
||||
|
||||
const data = await Promise.all([
|
||||
invoiceCountPromise,
|
||||
customerCountPromise,
|
||||
invoiceStatusPromise,
|
||||
]);
|
||||
|
||||
const invoices = data[0] as postgres.RowList<Invoice[]>
|
||||
const customres = data[1] as postgres.RowList<Customer[]>
|
||||
const invoiceStatus = data[2] as postgres.RowList<({paid: string, pending: string})[]>
|
||||
const numberOfInvoices = Number(invoices.count ?? '0');
|
||||
const numberOfCustomers = Number(customres.count ?? '0');
|
||||
const totalPaidInvoices = formatCurrency(Number(invoiceStatus.at(0)?.paid ?? '0'));
|
||||
const totalPendingInvoices = formatCurrency(Number(invoiceStatus.at(0)?.pending ?? '0'));
|
||||
|
||||
return {
|
||||
numberOfCustomers,
|
||||
numberOfInvoices,
|
||||
totalPaidInvoices,
|
||||
totalPendingInvoices,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Database Error:', error);
|
||||
throw new Error('Failed to fetch card data.');
|
||||
}
|
||||
}
|
||||
|
||||
const ITEMS_PER_PAGE = 6;
|
||||
export async function fetchFilteredInvoices(
|
||||
query: string,
|
||||
currentPage: number,
|
||||
) {
|
||||
// This is equivalent to in fetch(..., {cache: 'no-store'}).
|
||||
connection()
|
||||
|
||||
const offset = (currentPage - 1) * ITEMS_PER_PAGE;
|
||||
|
||||
try {
|
||||
const invoices = await sql`
|
||||
SELECT
|
||||
invoices.id,
|
||||
invoices.amount,
|
||||
invoices.date,
|
||||
invoices.status,
|
||||
customers.name,
|
||||
customers.email,
|
||||
customers.image_url
|
||||
FROM invoices
|
||||
JOIN customers ON invoices.customer_id = customers.id
|
||||
WHERE
|
||||
customers.name ILIKE ${`%${query}%`} OR
|
||||
customers.email ILIKE ${`%${query}%`} OR
|
||||
invoices.amount::text ILIKE ${`%${query}%`} OR
|
||||
invoices.date::text ILIKE ${`%${query}%`} OR
|
||||
invoices.status ILIKE ${`%${query}%`}
|
||||
ORDER BY invoices.date DESC
|
||||
LIMIT ${ITEMS_PER_PAGE} OFFSET ${offset}
|
||||
` as postgres.RowList<InvoicesTable[]>;
|
||||
|
||||
return invoices;
|
||||
} catch (error) {
|
||||
console.error('Database Error:', error);
|
||||
throw new Error('Failed to fetch invoices.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchInvoicesPages(query: string) {
|
||||
// This is equivalent to in fetch(..., {cache: 'no-store'}).
|
||||
connection()
|
||||
try {
|
||||
const count = await sql`SELECT COUNT(*)
|
||||
FROM invoices
|
||||
JOIN customers ON invoices.customer_id = customers.id
|
||||
WHERE
|
||||
customers.name ILIKE ${`%${query}%`} OR
|
||||
customers.email ILIKE ${`%${query}%`} OR
|
||||
invoices.amount::text ILIKE ${`%${query}%`} OR
|
||||
invoices.date::text ILIKE ${`%${query}%`} OR
|
||||
invoices.status ILIKE ${`%${query}%`}
|
||||
`;
|
||||
|
||||
const totalPages = Math.ceil(Number(count.at(0)?.count) / ITEMS_PER_PAGE);
|
||||
return totalPages;
|
||||
} catch (error) {
|
||||
console.error('Database Error:', error);
|
||||
throw new Error('Failed to fetch total number of invoices.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchInvoiceById(id: string) {
|
||||
// This is equivalent to in fetch(..., {cache: 'no-store'}).
|
||||
connection()
|
||||
try {
|
||||
const data = await sql`
|
||||
SELECT
|
||||
invoices.id,
|
||||
invoices.customer_id,
|
||||
invoices.amount,
|
||||
invoices.status
|
||||
FROM invoices
|
||||
WHERE invoices.id = ${id};
|
||||
` as postgres.RowList<InvoiceForm[]>;
|
||||
|
||||
const invoice = data.map((invoice) => ({
|
||||
...invoice,
|
||||
// Convert amount from cents to dollars
|
||||
amount: invoice.amount / 100,
|
||||
}));
|
||||
|
||||
return invoice[0];
|
||||
} catch (error) {
|
||||
console.error('Database Error:', error);
|
||||
throw new Error('Failed to fetch invoice.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchCustomers() {
|
||||
// This is equivalent to in fetch(..., {cache: 'no-store'}).
|
||||
connection()
|
||||
try {
|
||||
const data = await sql`
|
||||
SELECT
|
||||
id,
|
||||
name
|
||||
FROM customers
|
||||
ORDER BY name ASC
|
||||
` as postgres.RowList<CustomerField[]>;
|
||||
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error('Database Error:', err);
|
||||
throw new Error('Failed to fetch all customers.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchFilteredCustomers(query: string) {
|
||||
// This is equivalent to in fetch(..., {cache: 'no-store'}).
|
||||
connection()
|
||||
try {
|
||||
const data = await sql`
|
||||
SELECT
|
||||
customers.id,
|
||||
customers.name,
|
||||
customers.email,
|
||||
customers.image_url,
|
||||
COUNT(invoices.id) AS total_invoices,
|
||||
SUM(CASE WHEN invoices.status = 'pending' THEN invoices.amount ELSE 0 END) AS total_pending,
|
||||
SUM(CASE WHEN invoices.status = 'paid' THEN invoices.amount ELSE 0 END) AS total_paid
|
||||
FROM customers
|
||||
LEFT JOIN invoices ON customers.id = invoices.customer_id
|
||||
WHERE
|
||||
customers.name ILIKE ${`%${query}%`} OR
|
||||
customers.email ILIKE ${`%${query}%`}
|
||||
GROUP BY customers.id, customers.name, customers.email, customers.image_url
|
||||
ORDER BY customers.name ASC
|
||||
` as postgres.RowList<CustomersTableType[]>;
|
||||
|
||||
const customers = data.map((customer) => ({
|
||||
...customer,
|
||||
total_pending: formatCurrency(customer.total_pending),
|
||||
total_paid: formatCurrency(customer.total_paid),
|
||||
}));
|
||||
|
||||
return customers;
|
||||
} catch (err) {
|
||||
console.error('Database Error:', err);
|
||||
throw new Error('Failed to fetch customer table.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUser(email: string) {
|
||||
try {
|
||||
const user = await sql`SELECT * FROM users WHERE email=${email}`;
|
||||
return user.at(0) as User;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch user:', error);
|
||||
throw new Error('Failed to fetch user.');
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
// This file contains type definitions for your data.
|
||||
// It describes the shape of the data, and what data type each property should accept.
|
||||
// For simplicity of teaching, we're manually defining these types.
|
||||
// However, these types are generated automatically if you're using an ORM such as Prisma.
|
||||
export type User = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type Customer = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
image_url: string;
|
||||
};
|
||||
|
||||
export type Invoice = {
|
||||
id: string; // Will be created on the database
|
||||
customer_id: string;
|
||||
amount: number; // Stored in cents
|
||||
status: 'pending' | 'paid';
|
||||
date: string;
|
||||
};
|
||||
|
||||
export type Revenue = {
|
||||
month: string;
|
||||
revenue: number;
|
||||
};
|
||||
|
||||
export type LatestInvoice = {
|
||||
id: string;
|
||||
name: string;
|
||||
image_url: string;
|
||||
email: string;
|
||||
amount: string;
|
||||
};
|
||||
|
||||
// The database returns a number for amount, but we later format it to a string with the formatCurrency function
|
||||
export type LatestInvoiceRaw = Omit<LatestInvoice, 'amount'> & {
|
||||
amount: number;
|
||||
};
|
||||
|
||||
export type InvoicesTable = {
|
||||
id: string;
|
||||
customer_id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
image_url: string;
|
||||
date: string;
|
||||
amount: number;
|
||||
status: 'pending' | 'paid';
|
||||
};
|
||||
|
||||
export type CustomersTableType = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
image_url: string;
|
||||
total_invoices: number;
|
||||
total_pending: number;
|
||||
total_paid: number;
|
||||
};
|
||||
|
||||
export type FormattedCustomersTable = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
image_url: string;
|
||||
total_invoices: number;
|
||||
total_pending: string;
|
||||
total_paid: string;
|
||||
};
|
||||
|
||||
export type CustomerField = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type InvoiceForm = {
|
||||
id: string;
|
||||
customer_id: string;
|
||||
amount: number;
|
||||
status: 'pending' | 'paid';
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { Revenue } from './definitions';
|
||||
import Revenue from "@/feature/core/revenue/domain/entity/revenue";
|
||||
|
||||
export const formatCurrency = (amount: number) => {
|
||||
return (amount / 100).toLocaleString('en-US', {
|
||||
@ -7,20 +7,6 @@ export const formatCurrency = (amount: number) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const formatDateToLocal = (
|
||||
dateStr: string,
|
||||
locale: string = 'en-US',
|
||||
) => {
|
||||
const date = new Date(dateStr);
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
};
|
||||
const formatter = new Intl.DateTimeFormat(locale, options);
|
||||
return formatter.format(date);
|
||||
};
|
||||
|
||||
export const generateYAxis = (revenue: Revenue[]) => {
|
||||
// Calculate what labels we need to display on the y-axis
|
||||
// based on highest record and in 1000s
|
||||
@ -34,36 +20,3 @@ export const generateYAxis = (revenue: Revenue[]) => {
|
||||
|
||||
return { yAxisLabels, topLabel };
|
||||
};
|
||||
|
||||
export const generatePagination = (currentPage: number, totalPages: number) => {
|
||||
// If the total number of pages is 7 or less,
|
||||
// display all pages without any ellipsis.
|
||||
if (totalPages <= 7) {
|
||||
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||
}
|
||||
|
||||
// If the current page is among the first 3 pages,
|
||||
// show the first 3, an ellipsis, and the last 2 pages.
|
||||
if (currentPage <= 3) {
|
||||
return [1, 2, 3, '...', totalPages - 1, totalPages];
|
||||
}
|
||||
|
||||
// If the current page is among the last 3 pages,
|
||||
// show the first 2, an ellipsis, and the last 3 pages.
|
||||
if (currentPage >= totalPages - 2) {
|
||||
return [1, 2, '...', totalPages - 2, totalPages - 1, totalPages];
|
||||
}
|
||||
|
||||
// If the current page is somewhere in the middle,
|
||||
// show the first page, an ellipsis, the current page and its neighbors,
|
||||
// another ellipsis, and the last page.
|
||||
return [
|
||||
1,
|
||||
'...',
|
||||
currentPage - 1,
|
||||
currentPage,
|
||||
currentPage + 1,
|
||||
'...',
|
||||
totalPages,
|
||||
];
|
||||
};
|
||||
|
@ -1,13 +1,33 @@
|
||||
import getCustomerInvoiceDi from "@/feature/core/customer-invoice/data/module/customer-invoice-di";
|
||||
import { customerInvoiceModuleKey } from "@/feature/core/customer-invoice/invoice-module-key";
|
||||
import { customerKey } from "@/feature/core/customer/customer-key";
|
||||
import getCustomerDi from "@/feature/core/customer/data/module/customer-di";
|
||||
import { testModuleKey } from "@/feature/domain/test/test-module-key";
|
||||
import getTestModule from "@/feature/infra/test/module/test-module";
|
||||
import getInvoiceDi from "@/feature/core/invoice/data/module/invoice-di";
|
||||
import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
|
||||
import { DependencyContainer } from "tsyringe";
|
||||
import { summaryInfoModuleKey } from "@/feature/core/summary-info/domain/summary-info-module-key";
|
||||
import getSummaryInfoDi from "@/feature/core/summary-info/data/module/summary-info-di";
|
||||
import { revenueModuleKey } from "@/feature/core/revenue/domain/revenue-module-key";
|
||||
import getRevenueDi from "@/feature/core/revenue/data/module/revenue-di";
|
||||
|
||||
const memoizedDis: Record<string, DependencyContainer> = {}
|
||||
|
||||
export default function serverDi(module: string): DependencyContainer {
|
||||
if (memoizedDis[module]) return memoizedDis[module]
|
||||
const getDi = {
|
||||
[testModuleKey]: getTestModule
|
||||
[testModuleKey]: getTestModule,
|
||||
[customerKey]: getCustomerDi,
|
||||
[customerInvoiceModuleKey]: getCustomerInvoiceDi,
|
||||
[invoiceModuleKey]: getInvoiceDi,
|
||||
[summaryInfoModuleKey]: getSummaryInfoDi,
|
||||
[revenueModuleKey]: getRevenueDi,
|
||||
}[module]
|
||||
|
||||
if (!getDi) throw new Error("Server Di didn't found for module: " + module)
|
||||
|
||||
return getDi()
|
||||
|
||||
const di = getDi()
|
||||
memoizedDis[module] = di
|
||||
return di
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import di from "@/bootstrap/di/init-di";
|
||||
import CustomerInvoiceDbRepo from "@/feature/core/customer-invoice/data/repo/customer-invoice-db-repo";
|
||||
import { customerInvoiceRepoKey } from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice-repo";
|
||||
import { DependencyContainer } from "tsyringe";
|
||||
|
||||
export default function getCustomerInvoiceDi(): DependencyContainer {
|
||||
const customerInvoiceDi = di.createChildContainer()
|
||||
|
||||
customerInvoiceDi.register(customerInvoiceRepoKey, CustomerInvoiceDbRepo)
|
||||
return customerInvoiceDi
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
import { formatCurrency } from "@/app/lib/utils";
|
||||
import { sql } from "@/bootstrap/db/db";
|
||||
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice";
|
||||
import CustomerInvoiceRepo from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice-repo";
|
||||
import postgres from "postgres";
|
||||
|
||||
type customerInvoiceDbResponse = {
|
||||
id: string;
|
||||
name: string;
|
||||
image_url: string;
|
||||
email: string;
|
||||
amount: string;
|
||||
}
|
||||
|
||||
export default class CustomerInvoiceDbRepo implements CustomerInvoiceRepo {
|
||||
async fetchList(): Promise<CustomerInvoice[]> {
|
||||
try {
|
||||
const data = await sql`
|
||||
SELECT invoices.amount, customers.name, customers.image_url, customers.email, invoices.id
|
||||
FROM invoices
|
||||
JOIN customers ON invoices.customer_id = customers.id
|
||||
ORDER BY invoices.date DESC
|
||||
LIMIT 5` as postgres.RowList<customerInvoiceDbResponse[]>;
|
||||
|
||||
return this.customerInvoicesDto(data)
|
||||
} catch (error) {
|
||||
console.error('Database Error:', error);
|
||||
throw new Error('Failed to fetch the latest invoices.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private customerInvoicesDto(dbCustomers: customerInvoiceDbResponse[]): CustomerInvoice[] {
|
||||
return dbCustomers.map((customer) => this.customerInvoiceDto(customer));
|
||||
}
|
||||
|
||||
private customerInvoiceDto(dbCustomer: customerInvoiceDbResponse): CustomerInvoice {
|
||||
return new CustomerInvoice({
|
||||
id: dbCustomer.id,
|
||||
customerName: dbCustomer.name,
|
||||
customerEmail: dbCustomer.email,
|
||||
customerImageUrl: dbCustomer.image_url,
|
||||
invoicesAmount: formatCurrency(+dbCustomer.amount),
|
||||
})
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
export default class CustomerInvoice {
|
||||
id: string;
|
||||
customerName: string;
|
||||
customerImageUrl: string;
|
||||
customerEmail: string;
|
||||
invoicesAmount: string;
|
||||
|
||||
constructor({
|
||||
id,
|
||||
customerEmail,
|
||||
customerImageUrl,
|
||||
customerName,
|
||||
invoicesAmount
|
||||
}: CustomerInvoice) {
|
||||
this.id = id;
|
||||
this.customerEmail = customerEmail
|
||||
this.customerImageUrl = customerImageUrl
|
||||
this.customerName = customerName
|
||||
this.invoicesAmount = invoicesAmount
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice"
|
||||
|
||||
export default interface CustomerInvoiceRepo {
|
||||
fetchList(): Promise<CustomerInvoice[]>
|
||||
}
|
||||
|
||||
export const customerInvoiceRepoKey = "customerInvoiceRepoKey"
|
@ -0,0 +1,12 @@
|
||||
"use server"
|
||||
import serverDi from "@/feature/common/server-di";
|
||||
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice";
|
||||
import CustomerInvoiceRepo, { customerInvoiceRepoKey } from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice-repo";
|
||||
import { customerInvoiceModuleKey } from "@/feature/core/customer-invoice/invoice-module-key";
|
||||
import { connection } from "next/server";
|
||||
|
||||
export default async function fetchCustomerInvoicesUsecase(): Promise<CustomerInvoice[]> {
|
||||
connection()
|
||||
const repo = serverDi(customerInvoiceModuleKey).resolve<CustomerInvoiceRepo>(customerInvoiceRepoKey)
|
||||
return repo.fetchList()
|
||||
}
|
1
src/feature/core/customer-invoice/invoice-module-key.ts
Normal file
1
src/feature/core/customer-invoice/invoice-module-key.ts
Normal file
@ -0,0 +1 @@
|
||||
export const customerInvoiceModuleKey = "customerInvoiceModuleKey"
|
1
src/feature/core/customer/customer-key.ts
Normal file
1
src/feature/core/customer/customer-key.ts
Normal file
@ -0,0 +1 @@
|
||||
export const customerKey = "customerKey"
|
11
src/feature/core/customer/data/module/customer-di.ts
Normal file
11
src/feature/core/customer/data/module/customer-di.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import di from "@/bootstrap/di/init-di";
|
||||
import CustomerDbRepo from "@/feature/core/customer/data/repo/customer-db-repo";
|
||||
import { customerRepoKey } from "@/feature/core/customer/domain/i-repo/customer-repo";
|
||||
import { DependencyContainer } from "tsyringe";
|
||||
|
||||
export default function getCustomerDi(): DependencyContainer {
|
||||
const customerDi = di.createChildContainer()
|
||||
|
||||
customerDi.register(customerRepoKey, CustomerDbRepo)
|
||||
return customerDi
|
||||
}
|
71
src/feature/core/customer/data/repo/customer-db-repo.ts
Normal file
71
src/feature/core/customer/data/repo/customer-db-repo.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { formatCurrency } from "@/app/lib/utils";
|
||||
import { sql } from "@/bootstrap/db/db";
|
||||
import Customer from "@/feature/core/customer/domain/entity/customer";
|
||||
import CustomerRepo from "@/feature/core/customer/domain/i-repo/customer-repo";
|
||||
import { connection } from "next/server";
|
||||
import postgres from "postgres";
|
||||
|
||||
type customerDbResponse = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
image_url: string;
|
||||
total_invoices: string;
|
||||
total_pending: string;
|
||||
total_paid: string;
|
||||
}
|
||||
|
||||
export default class CustomerDbRepo implements CustomerRepo {
|
||||
async fetchList(query: string): Promise<Customer[]> {
|
||||
// This is equivalent to in fetch(..., {cache: 'no-store'}).
|
||||
connection()
|
||||
try {
|
||||
const data = await sql`
|
||||
SELECT
|
||||
customers.id,
|
||||
customers.name,
|
||||
customers.email,
|
||||
customers.image_url,
|
||||
COUNT(invoices.id) AS total_invoices,
|
||||
SUM(CASE WHEN invoices.status = 'pending' THEN invoices.amount ELSE 0 END) AS total_pending,
|
||||
SUM(CASE WHEN invoices.status = 'paid' THEN invoices.amount ELSE 0 END) AS total_paid
|
||||
FROM customers
|
||||
LEFT JOIN invoices ON customers.id = invoices.customer_id
|
||||
WHERE
|
||||
customers.name ILIKE ${`%${query}%`} OR
|
||||
customers.email ILIKE ${`%${query}%`}
|
||||
GROUP BY customers.id, customers.name, customers.email, customers.image_url
|
||||
ORDER BY customers.name ASC
|
||||
` as postgres.RowList<customerDbResponse[]>;
|
||||
|
||||
|
||||
return this.customersDto(data);
|
||||
} catch (err) {
|
||||
console.error('Database Error:', err);
|
||||
throw new Error('Failed to fetch customer table.');
|
||||
}
|
||||
}
|
||||
|
||||
async fetchCustomersAmount(): Promise<number> {
|
||||
const data = await sql`SELECT COUNT(*) FROM customers`as postgres.RowList<unknown[]>;
|
||||
return Number(data.count ?? '0');
|
||||
}
|
||||
|
||||
|
||||
private customersDto(dbCustomers: customerDbResponse[]): Customer[] {
|
||||
return dbCustomers.map((customer) => this.customerDto(customer));
|
||||
}
|
||||
|
||||
private customerDto(dbCustomer: customerDbResponse): Customer {
|
||||
return new Customer({
|
||||
id: dbCustomer.id,
|
||||
name: dbCustomer.name,
|
||||
email: dbCustomer.email,
|
||||
imageUrl: dbCustomer.image_url,
|
||||
totalInvoices: dbCustomer.total_invoices,
|
||||
totalPending: formatCurrency(Number(dbCustomer.total_pending)),
|
||||
totalPaid: formatCurrency(Number(dbCustomer.total_paid)),
|
||||
})
|
||||
}
|
||||
|
||||
}
|
27
src/feature/core/customer/domain/entity/customer.ts
Normal file
27
src/feature/core/customer/domain/entity/customer.ts
Normal file
@ -0,0 +1,27 @@
|
||||
export default class Customer {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
imageUrl: string;
|
||||
totalInvoices: string;
|
||||
totalPending: string;
|
||||
totalPaid: string;
|
||||
|
||||
constructor({
|
||||
id,
|
||||
email,
|
||||
imageUrl,
|
||||
name,
|
||||
totalInvoices,
|
||||
totalPaid,
|
||||
totalPending
|
||||
}: Customer) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
this.imageUrl = imageUrl;
|
||||
this.totalInvoices = totalInvoices;
|
||||
this.totalPaid = totalPaid;
|
||||
this.totalPending = totalPending;
|
||||
}
|
||||
}
|
8
src/feature/core/customer/domain/i-repo/customer-repo.ts
Normal file
8
src/feature/core/customer/domain/i-repo/customer-repo.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import Customer from "@/feature/core/customer/domain/entity/customer"
|
||||
|
||||
export default interface CustomerRepo {
|
||||
fetchList(query: string): Promise<Customer[]>
|
||||
fetchCustomersAmount(): Promise<number>
|
||||
}
|
||||
|
||||
export const customerRepoKey = "customerRepoKey"
|
@ -0,0 +1,8 @@
|
||||
import serverDi from "@/feature/common/server-di";
|
||||
import { customerKey } from "@/feature/core/customer/customer-key";
|
||||
import CustomerRepo, { customerRepoKey } from "@/feature/core/customer/domain/i-repo/customer-repo";
|
||||
|
||||
export default async function fetchCustomersAmountUsecase(): Promise<number> {
|
||||
const repo = serverDi(customerKey).resolve<CustomerRepo>(customerRepoKey)
|
||||
return repo.fetchCustomersAmount()
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
"use server"
|
||||
|
||||
import serverDi from "@/feature/common/server-di";
|
||||
import { customerKey } from "@/feature/core/customer/customer-key";
|
||||
import Customer from "@/feature/core/customer/domain/entity/customer";
|
||||
import CustomerRepo, { customerRepoKey } from "@/feature/core/customer/domain/i-repo/customer-repo";
|
||||
|
||||
export default async function fetchCustomersUsecase(query: string): Promise<Customer[]> {
|
||||
const repo = serverDi(customerKey).resolve<CustomerRepo>(customerRepoKey)
|
||||
|
||||
return repo.fetchList(query)
|
||||
}
|
11
src/feature/core/invoice/data/module/invoice-di.ts
Normal file
11
src/feature/core/invoice/data/module/invoice-di.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import di from "@/bootstrap/di/init-di";
|
||||
import invoiceDbRepo from "@/feature/core/invoice/data/repo/invoice-db-repo";
|
||||
import { invoiceRepoKey } from "@/feature/core/invoice/domain/i-repo/invoice-repo";
|
||||
import { DependencyContainer } from "tsyringe";
|
||||
|
||||
export default function getInvoiceDi(): DependencyContainer {
|
||||
const invoiceDi = di.createChildContainer()
|
||||
|
||||
invoiceDi.register(invoiceRepoKey, invoiceDbRepo)
|
||||
return invoiceDi
|
||||
}
|
31
src/feature/core/invoice/data/repo/invoice-db-repo.ts
Normal file
31
src/feature/core/invoice/data/repo/invoice-db-repo.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { formatCurrency } from "@/app/lib/utils";
|
||||
import { sql } from "@/bootstrap/db/db";
|
||||
import InvoiceRepo from "@/feature/core/invoice/domain/i-repo/invoice-repo";
|
||||
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
|
||||
import postgres from "postgres";
|
||||
|
||||
type InvoiceSummaryDbResponse = {paid: string, pending: string}
|
||||
export default class InvoiceDbRepo implements InvoiceRepo {
|
||||
async fetchAllInvoicesAmount(): Promise<number> {
|
||||
const data = await sql`SELECT COUNT(*) FROM invoices` as postgres.RowList<unknown[]>;
|
||||
|
||||
return data.count ?? 0
|
||||
}
|
||||
|
||||
async fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary> {
|
||||
const invoiceStatusPromise = await sql`SELECT
|
||||
SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS "paid",
|
||||
SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS "pending"
|
||||
FROM invoices` as postgres.RowList<InvoiceSummaryDbResponse[]>;
|
||||
|
||||
return this.invoiceSummaryDto(invoiceStatusPromise.at(0))
|
||||
|
||||
}
|
||||
|
||||
private invoiceSummaryDto(dbResponse?: InvoiceSummaryDbResponse): InvoiceStatusSummary {
|
||||
return new InvoiceStatusSummary({
|
||||
paid: formatCurrency(Number(dbResponse?.paid ?? '0')),
|
||||
pending: formatCurrency(Number(dbResponse?.pending ?? '0'))
|
||||
})
|
||||
}
|
||||
}
|
8
src/feature/core/invoice/domain/i-repo/invoice-repo.ts
Normal file
8
src/feature/core/invoice/domain/i-repo/invoice-repo.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status"
|
||||
|
||||
export default interface InvoiceRepo {
|
||||
fetchAllInvoicesAmount(): Promise<number>
|
||||
fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary>
|
||||
}
|
||||
|
||||
export const invoiceRepoKey = "invoiceRepoKey"
|
@ -0,0 +1,10 @@
|
||||
"use server"
|
||||
import serverDi from "@/feature/common/server-di";
|
||||
import InvoiceRepo, { invoiceRepoKey } from "@/feature/core/invoice/domain/i-repo/invoice-repo";
|
||||
import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
|
||||
|
||||
export default async function fetchAllInvoicesAmountUsecase(): Promise<number> {
|
||||
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey)
|
||||
|
||||
return repo.fetchAllInvoicesAmount()
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import serverDi from "@/feature/common/server-di";
|
||||
import InvoiceRepo, { invoiceRepoKey } from "@/feature/core/invoice/domain/i-repo/invoice-repo";
|
||||
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
|
||||
import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
|
||||
|
||||
export default async function fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary> {
|
||||
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey)
|
||||
return repo.fetchInvoicesStatusSummary()
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
export default class InvoiceStatusSummary {
|
||||
paid: string;
|
||||
pending: string;
|
||||
|
||||
constructor({
|
||||
paid,
|
||||
pending
|
||||
}: InvoiceStatusSummary) {
|
||||
this.paid = paid;
|
||||
this.pending = pending;
|
||||
}
|
||||
}
|
1
src/feature/core/invoice/invoice-module-key.ts
Normal file
1
src/feature/core/invoice/invoice-module-key.ts
Normal file
@ -0,0 +1 @@
|
||||
export const invoiceModuleKey = "invoiceModuleKey"
|
10
src/feature/core/revenue/data/module/revenue-di.ts
Normal file
10
src/feature/core/revenue/data/module/revenue-di.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import di from "@/bootstrap/di/init-di"
|
||||
import RevenueDbRepo from "@/feature/core/revenue/data/repo/revenue-db-repo"
|
||||
import { revenueRepoKey } from "@/feature/core/revenue/domain/i-repo/revenue-repo"
|
||||
|
||||
export default function getRevenueDi() {
|
||||
const revenueDi = di.createChildContainer()
|
||||
|
||||
revenueDi.register(revenueRepoKey, RevenueDbRepo)
|
||||
return revenueDi
|
||||
}
|
42
src/feature/core/revenue/data/repo/revenue-db-repo.ts
Normal file
42
src/feature/core/revenue/data/repo/revenue-db-repo.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { sql } from "@/bootstrap/db/db";
|
||||
import Revenue from "@/feature/core/revenue/domain/entity/revenue";
|
||||
import RevenueRepo from "@/feature/core/revenue/domain/i-repo/revenue-repo";
|
||||
import { connection } from "next/server";
|
||||
import postgres from "postgres";
|
||||
|
||||
export type RevenueDbResponse = {
|
||||
month: string;
|
||||
revenue: number;
|
||||
};
|
||||
export default class RevenueDbRepo implements RevenueRepo {
|
||||
async fetchRevenues(): Promise<Revenue[]> {
|
||||
// This is equivalent to in fetch(..., {cache: 'no-store'}).
|
||||
try {
|
||||
// Artificially delay a response for demo purposes.
|
||||
// Don't do this in production :)
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
|
||||
const data = await sql`SELECT * FROM revenue` as postgres.RowList<RevenueDbResponse[]>;
|
||||
|
||||
console.log('Data fetch completed after 3 seconds.');
|
||||
|
||||
return this.revenuesDto(data);
|
||||
} catch (error) {
|
||||
console.error('Database Error:', error);
|
||||
throw new Error('Failed to fetch revenue data.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private revenuesDto(dbResponse: RevenueDbResponse[]): Revenue[] {
|
||||
return dbResponse.map((dbRevenue) => this.revenueDto(dbRevenue))
|
||||
}
|
||||
|
||||
private revenueDto(dbResponse: RevenueDbResponse): Revenue {
|
||||
return new Revenue({
|
||||
month: dbResponse.month,
|
||||
revenue: dbResponse.revenue
|
||||
})
|
||||
}
|
||||
|
||||
}
|
14
src/feature/core/revenue/domain/entity/revenue.ts
Normal file
14
src/feature/core/revenue/domain/entity/revenue.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export default class Revenue {
|
||||
month: string;
|
||||
revenue: number;
|
||||
|
||||
constructor(
|
||||
{
|
||||
month,
|
||||
revenue
|
||||
}: Revenue
|
||||
) {
|
||||
this.month = month
|
||||
this.revenue = revenue
|
||||
}
|
||||
}
|
7
src/feature/core/revenue/domain/i-repo/revenue-repo.ts
Normal file
7
src/feature/core/revenue/domain/i-repo/revenue-repo.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import Revenue from "@/feature/core/revenue/domain/entity/revenue";
|
||||
|
||||
export default interface RevenueRepo {
|
||||
fetchRevenues(): Promise<Revenue[]>
|
||||
}
|
||||
|
||||
export const revenueRepoKey = "revenueRepoKey"
|
1
src/feature/core/revenue/domain/revenue-module-key.ts
Normal file
1
src/feature/core/revenue/domain/revenue-module-key.ts
Normal file
@ -0,0 +1 @@
|
||||
export const revenueModuleKey = "RevenueModuleKey"
|
@ -0,0 +1,9 @@
|
||||
import serverDi from "@/feature/common/server-di";
|
||||
import Revenue from "@/feature/core/revenue/domain/entity/revenue";
|
||||
import RevenueRepo, { revenueRepoKey } from "@/feature/core/revenue/domain/i-repo/revenue-repo";
|
||||
import { revenueModuleKey } from "@/feature/core/revenue/domain/revenue-module-key";
|
||||
|
||||
export default async function fetchRevenuesUsecase(): Promise<Revenue[]> {
|
||||
const repo = serverDi(revenueModuleKey).resolve<RevenueRepo>(revenueRepoKey)
|
||||
return repo.fetchRevenues()
|
||||
}
|
19
src/feature/core/summary-info/data/module/summary-info-di.ts
Normal file
19
src/feature/core/summary-info/data/module/summary-info-di.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import di from "@/bootstrap/di/init-di"
|
||||
import fetchCustomersAmountUsecase from "@/feature/core/customer/domain/usecase/fetch-customers-amount-usecase"
|
||||
import fetchAllInvoicesAmountUsecase from "@/feature/core/invoice/domain/usecase/fetch-all-invoices-amount-usecase"
|
||||
import fetchInvoicesStatusSummary from "@/feature/core/invoice/domain/usecase/fetch-invoices-status-summary"
|
||||
|
||||
export default function getSummaryInfoDi() {
|
||||
const summaryInfoDi = di.createChildContainer()
|
||||
|
||||
summaryInfoDi.register(fetchAllInvoicesAmountUsecase.name, {
|
||||
useValue: fetchAllInvoicesAmountUsecase
|
||||
})
|
||||
summaryInfoDi.register(fetchCustomersAmountUsecase.name, {
|
||||
useValue: fetchCustomersAmountUsecase
|
||||
})
|
||||
summaryInfoDi.register(fetchInvoicesStatusSummary.name, {
|
||||
useValue: fetchInvoicesStatusSummary
|
||||
})
|
||||
return summaryInfoDi
|
||||
}
|
@ -0,0 +1 @@
|
||||
export const summaryInfoModuleKey = "summaryInfoModuleKey"
|
@ -0,0 +1,34 @@
|
||||
import serverDi from "@/feature/common/server-di";
|
||||
import fetchCustomersAmountUsecase from "@/feature/core/customer/domain/usecase/fetch-customers-amount-usecase";
|
||||
import fetchAllInvoicesAmountUsecase from "@/feature/core/invoice/domain/usecase/fetch-all-invoices-amount-usecase";
|
||||
import fetchInvoicesStatusSummary from "@/feature/core/invoice/domain/usecase/fetch-invoices-status-summary";
|
||||
import { summaryInfoModuleKey } from "@/feature/core/summary-info/domain/summary-info-module-key";
|
||||
import SummaryInfo from "@/feature/core/summary-info/domain/value-object/summary-info";
|
||||
import { connection } from "next/server";
|
||||
|
||||
export default async function fetchSummaryInfoUsecase(): Promise<SummaryInfo> {
|
||||
connection()
|
||||
|
||||
try{
|
||||
const summaryInfoDi = serverDi(summaryInfoModuleKey)
|
||||
const invoicesAmountPromise = summaryInfoDi.resolve<typeof fetchAllInvoicesAmountUsecase>(fetchAllInvoicesAmountUsecase.name)()
|
||||
const customersAmountPromise = summaryInfoDi.resolve<typeof fetchCustomersAmountUsecase>(fetchCustomersAmountUsecase.name)()
|
||||
const invoiceSummaryPomise = summaryInfoDi.resolve<typeof fetchInvoicesStatusSummary>(fetchInvoicesStatusSummary.name)()
|
||||
|
||||
const [invoicesAmount, customersAmount, invoicesSummary] = await Promise.all([
|
||||
invoicesAmountPromise,
|
||||
customersAmountPromise,
|
||||
invoiceSummaryPomise,
|
||||
]);
|
||||
|
||||
|
||||
return new SummaryInfo({
|
||||
invoicesNumber: invoicesAmount,
|
||||
customersNumber: customersAmount,
|
||||
invoicesSummary: invoicesSummary
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Database Error:', error);
|
||||
throw new Error('Failed to fetch card data.');
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
|
||||
|
||||
export default class SummaryInfo {
|
||||
customersNumber: number;
|
||||
invoicesNumber: number;
|
||||
invoicesSummary: InvoiceStatusSummary
|
||||
|
||||
constructor({
|
||||
customersNumber,
|
||||
invoicesNumber,
|
||||
invoicesSummary
|
||||
}: SummaryInfo) {
|
||||
this.customersNumber = customersNumber
|
||||
this.invoicesNumber = invoicesNumber
|
||||
this.invoicesSummary = invoicesSummary
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import Customer from "@/feature/core/customer/domain/entity/customer";
|
||||
import { faker } from "@faker-js/faker";
|
||||
|
||||
export default class CustomerFakeFactory {
|
||||
static getFakeCustomer(): Customer {
|
||||
return new Customer({
|
||||
id: faker.string.uuid(),
|
||||
name: faker.person.fullName(),
|
||||
email: faker.internet.email(),
|
||||
imageUrl: faker.image.url(),
|
||||
totalInvoices: faker.number.int().toLocaleString(),
|
||||
totalPaid: faker.finance.amount(),
|
||||
totalPending: faker.number.int().toLocaleString(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
static getFakeCustomerList(length: number = 10): Customer[] {
|
||||
return Array.from({length}).map(() => CustomerFakeFactory.getFakeCustomer())
|
||||
}
|
||||
}
|
7
src/test/common/mock/mock-di.ts
Normal file
7
src/test/common/mock/mock-di.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import di from "@/bootstrap/di/init-di"
|
||||
import * as serverDi from "@/feature/common/server-di"
|
||||
|
||||
export default function mockDi() {
|
||||
vi.spyOn(serverDi, "default").mockReturnValue(di)
|
||||
return di
|
||||
}
|
5
src/test/common/mock/mock-factory.ts
Normal file
5
src/test/common/mock/mock-factory.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { Mock } from "moq.ts";
|
||||
|
||||
export function getMock<T>() {
|
||||
return new Mock<T>();
|
||||
}
|
1
src/test/setup.ts
Normal file
1
src/test/setup.ts
Normal file
@ -0,0 +1 @@
|
||||
import "reflect-metadata";
|
@ -0,0 +1,48 @@
|
||||
import CustomerRepo, { customerRepoKey } from "@/feature/core/customer/domain/i-repo/customer-repo";
|
||||
import { getMock } from "@/test/common/mock/mock-factory";
|
||||
import { describe } from "vitest";
|
||||
import { faker } from "@faker-js/faker";
|
||||
import CustomerFakeFactory from "@/test/common/fake-factory/customer/customer-fake-factory";
|
||||
import fetchCustomersUsecase from "@/feature/core/customer/domain/usecase/fetch-customers-usecase";
|
||||
import mockDi from "@/test/common/mock/mock-di";
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Faking */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
const fakedCustomerList = CustomerFakeFactory.getFakeCustomerList()
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Mocking */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
const customerDi = mockDi()
|
||||
|
||||
const mockedFetchList = vi.fn<CustomerRepo['fetchList']>()
|
||||
const MockedRepo = getMock<CustomerRepo>()
|
||||
MockedRepo.setup((instance) => instance.fetchList).returns(mockedFetchList)
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* DI */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
customerDi.register(fetchCustomersUsecase.name, {
|
||||
useValue: fetchCustomersUsecase
|
||||
})
|
||||
customerDi.register(customerRepoKey, {
|
||||
useValue: MockedRepo.object()
|
||||
})
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Testing */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
const usecase = customerDi.resolve<typeof fetchCustomersUsecase>(fetchCustomersUsecase.name)
|
||||
describe("Fetch customers", () => {
|
||||
describe("On given query string", () => {
|
||||
const fakedQuery = faker.person.fullName();
|
||||
describe("And returning list from repo", () => {
|
||||
beforeEach(() => {
|
||||
mockedFetchList.mockResolvedValue(fakedCustomerList)
|
||||
})
|
||||
it("Then should return correct list of customers", async () => {
|
||||
// ! Act
|
||||
const response = await usecase(fakedQuery)
|
||||
// ? Assert
|
||||
expect(response).toEqual(fakedCustomerList)
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
@ -15,6 +15,7 @@
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"types": ["vitest/globals"],
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
|
15
vite.config.ts
Normal file
15
vite.config.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/// <reference types="vitest" />
|
||||
import { defineConfig } from "vitest/config";
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
setupFiles: 'src/test/setup.ts',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user