feature/research-di #1

Merged
behnam merged 37 commits from feature/research-di into develop 2024-11-21 15:50:19 +00:00
12 changed files with 183 additions and 180 deletions
Showing only changes of commit 46bbb9f8da - Show all commits

View File

@ -1,8 +1,5 @@
import { sql } from '@/bootstrap/db/db'; import { sql } from '@/bootstrap/db/db';
import { import {
CustomersTableType,
InvoiceForm,
InvoicesTable,
Revenue, Revenue,
Invoice, Invoice,
Customer, Customer,
@ -97,127 +94,3 @@ export async function fetchCardData() {
throw new Error('Failed to fetch card data.'); 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 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.');
}
}

View File

@ -26,42 +26,6 @@ export type LatestInvoice = {
amount: 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 InvoiceForm = { export type InvoiceForm = {
id: string; id: string;
customer_id: string; customer_id: string;

View File

@ -0,0 +1,16 @@
import di from "@/bootstrap/di/init-di";
import CustomerDbRepo from "@/feature/customer/data/repo/customer-db-repo";
import { customerRepoKey } from "@/feature/customer/domain/i-repo/customer-repo";
import fetchCustomersUsecase from "@/feature/customer/domain/usecase/fetch-customers-usecase";
import { DependencyContainer } from "tsyringe";
export default function getCustomerDi(): DependencyContainer {
const customerDi = di.createChildContainer()
customerDi.register(fetchCustomersUsecase.name, {
useValue: fetchCustomersUsecase
})
customerDi.register(customerRepoKey, CustomerDbRepo)
return customerDi
}

View File

@ -0,0 +1,51 @@
import { formatCurrency } from "@/app/lib/utils";
import { sql } from "@/bootstrap/db/db";
import CustomerInvoice from "@/feature/customer-invoice/domain/entity/customer-invoice";
import CustomerInvoiceRepo from "@/feature/customer-invoice/domain/i-repo/customer-invoice-repo";
import { connection } from "next/server";
import postgres from "postgres";
type customerInvoiceDbResponse = {
id: string;
name: string;
image_url: string;
email: string;
amount: string;
}
export default class CustomerDbRepo implements CustomerInvoiceRepo {
async fetchList(): Promise<CustomerInvoice[]> {
// 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<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),
})
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,7 @@
import CustomerInvoice from "@/feature/customer-invoice/domain/entity/customer-invoice"
export default interface CustomerInvoiceRepo {
fetchList(): Promise<CustomerInvoice[]>
}
export const customerInvoiceRepoKey = "customerInvoiceRepoKey"

View File

@ -0,0 +1,11 @@
"use server"
import serverDi from "@/feature/common/server-di";
import CustomerInvoice from "@/feature/customer-invoice/domain/entity/customer-invoice";
import CustomerInvoiceRepo, { customerInvoiceRepoKey } from "@/feature/customer-invoice/domain/i-repo/customer-invoice-repo";
import { customerInvoiceModuleKey } from "@/feature/customer-invoice/invoice-module-key";
export default function fetchCustomerInvoicesUsecase(): Promise<CustomerInvoice[]> {
const repo = serverDi(customerInvoiceModuleKey).resolve<CustomerInvoiceRepo>(customerInvoiceRepoKey)
return repo.fetchList()
}

View File

@ -0,0 +1 @@
export const customerInvoiceModuleKey = "customerInvoiceModuleKey"

View File

@ -1,4 +1,6 @@
import di from "@/bootstrap/di/init-di"; import di from "@/bootstrap/di/init-di";
import CustomerDbRepo from "@/feature/customer/data/repo/customer-db-repo";
import { customerRepoKey } from "@/feature/customer/domain/i-repo/customer-repo";
import fetchCustomersUsecase from "@/feature/customer/domain/usecase/fetch-customers-usecase"; import fetchCustomersUsecase from "@/feature/customer/domain/usecase/fetch-customers-usecase";
import { DependencyContainer } from "tsyringe"; import { DependencyContainer } from "tsyringe";
@ -9,5 +11,6 @@ export default function getCustomerDi(): DependencyContainer {
useValue: fetchCustomersUsecase useValue: fetchCustomersUsecase
}) })
customerDi.register(customerRepoKey, CustomerDbRepo)
return customerDi return customerDi
} }

View File

@ -0,0 +1,66 @@
import { formatCurrency } from "@/app/lib/utils";
import { sql } from "@/bootstrap/db/db";
import Customer from "@/feature/customer/domain/entity/customer";
import CustomerRepo from "@/feature/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.');
}
}
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)),
})
}
}

View File

@ -1,21 +1,11 @@
export type CustomersTableType = {
id: string;
name: string;
email: string;
image_url: string;
total_invoices: number;
total_pending: number;
total_paid: number;
};
export default class Customer { export default class Customer {
id: string; id: string;
name: string; name: string;
email: string; email: string;
imageUrl: string; imageUrl: string;
totalInvoices: number; totalInvoices: string;
totalPending: number; totalPending: string;
totalPaid: number; totalPaid: string;
constructor({ constructor({
id, id,

View File

@ -8,9 +8,9 @@ export default class CustomerFakeFactory {
name: faker.person.fullName(), name: faker.person.fullName(),
email: faker.internet.email(), email: faker.internet.email(),
imageUrl: faker.image.url(), imageUrl: faker.image.url(),
totalInvoices: faker.number.int(), totalInvoices: faker.number.int().toLocaleString(),
totalPaid: faker.number.int(), totalPaid: faker.finance.amount(),
totalPending: faker.number.int(), totalPending: faker.number.int().toLocaleString(),
}) })
} }