From 46bbb9f8da607083979584592004d18e2b0e2e66 Mon Sep 17 00:00:00 2001 From: behnam Date: Fri, 1 Nov 2024 15:34:04 +0300 Subject: [PATCH] Add fetch list of customer invoice --- src/app/lib/data.ts | 129 +----------------- src/app/lib/definitions.ts | 36 ----- .../data/module/customer-di.ts | 16 +++ .../data/repo/customer-invoice-db-repo.ts | 51 +++++++ .../domain/entity/customer-invoice.ts | 21 +++ .../domain/i-repo/customer-invoice-repo.ts | 7 + .../fetch-customer-invoices-usecase.ts | 11 ++ .../customer-invoice/invoice-module-key.ts | 1 + .../customer/data/module/customer-di.ts | 3 + .../customer/data/repo/customer-db-repo.ts | 66 +++++++++ .../customer/domain/entity/customer.ts | 16 +-- .../customer/customer-fake-factory.ts | 6 +- 12 files changed, 183 insertions(+), 180 deletions(-) create mode 100644 src/feature/customer-invoice/data/module/customer-di.ts create mode 100644 src/feature/customer-invoice/data/repo/customer-invoice-db-repo.ts create mode 100644 src/feature/customer-invoice/domain/entity/customer-invoice.ts create mode 100644 src/feature/customer-invoice/domain/i-repo/customer-invoice-repo.ts create mode 100644 src/feature/customer-invoice/domain/usecase/fetch-customer-invoices-usecase.ts create mode 100644 src/feature/customer-invoice/invoice-module-key.ts create mode 100644 src/feature/customer/data/repo/customer-db-repo.ts diff --git a/src/app/lib/data.ts b/src/app/lib/data.ts index 3afe7f1..d76cf24 100644 --- a/src/app/lib/data.ts +++ b/src/app/lib/data.ts @@ -1,8 +1,5 @@ import { sql } from '@/bootstrap/db/db'; import { - CustomersTableType, - InvoiceForm, - InvoicesTable, Revenue, Invoice, Customer, @@ -59,7 +56,7 @@ export async function fetchLatestInvoices() { export async function fetchCardData() { // This is equivalent to in fetch(..., {cache: 'no-store'}). - connection() + connection() try { // You can probably combine these into a single SQL query @@ -97,127 +94,3 @@ export async function fetchCardData() { 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; - - 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; - - 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; - - 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.'); - } -} diff --git a/src/app/lib/definitions.ts b/src/app/lib/definitions.ts index 0860139..e4cf179 100644 --- a/src/app/lib/definitions.ts +++ b/src/app/lib/definitions.ts @@ -26,42 +26,6 @@ export type LatestInvoice = { 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 & { - 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 = { id: string; customer_id: string; diff --git a/src/feature/customer-invoice/data/module/customer-di.ts b/src/feature/customer-invoice/data/module/customer-di.ts new file mode 100644 index 0000000..4d153a8 --- /dev/null +++ b/src/feature/customer-invoice/data/module/customer-di.ts @@ -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 +} \ No newline at end of file diff --git a/src/feature/customer-invoice/data/repo/customer-invoice-db-repo.ts b/src/feature/customer-invoice/data/repo/customer-invoice-db-repo.ts new file mode 100644 index 0000000..bd52acc --- /dev/null +++ b/src/feature/customer-invoice/data/repo/customer-invoice-db-repo.ts @@ -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 { + // 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; + + 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), + }) + } + +} \ No newline at end of file diff --git a/src/feature/customer-invoice/domain/entity/customer-invoice.ts b/src/feature/customer-invoice/domain/entity/customer-invoice.ts new file mode 100644 index 0000000..783654c --- /dev/null +++ b/src/feature/customer-invoice/domain/entity/customer-invoice.ts @@ -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 + } +} \ No newline at end of file diff --git a/src/feature/customer-invoice/domain/i-repo/customer-invoice-repo.ts b/src/feature/customer-invoice/domain/i-repo/customer-invoice-repo.ts new file mode 100644 index 0000000..f3f5eba --- /dev/null +++ b/src/feature/customer-invoice/domain/i-repo/customer-invoice-repo.ts @@ -0,0 +1,7 @@ +import CustomerInvoice from "@/feature/customer-invoice/domain/entity/customer-invoice" + +export default interface CustomerInvoiceRepo { + fetchList(): Promise +} + +export const customerInvoiceRepoKey = "customerInvoiceRepoKey" \ No newline at end of file diff --git a/src/feature/customer-invoice/domain/usecase/fetch-customer-invoices-usecase.ts b/src/feature/customer-invoice/domain/usecase/fetch-customer-invoices-usecase.ts new file mode 100644 index 0000000..1f16825 --- /dev/null +++ b/src/feature/customer-invoice/domain/usecase/fetch-customer-invoices-usecase.ts @@ -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 { + const repo = serverDi(customerInvoiceModuleKey).resolve(customerInvoiceRepoKey) + + return repo.fetchList() +} \ No newline at end of file diff --git a/src/feature/customer-invoice/invoice-module-key.ts b/src/feature/customer-invoice/invoice-module-key.ts new file mode 100644 index 0000000..6ab1512 --- /dev/null +++ b/src/feature/customer-invoice/invoice-module-key.ts @@ -0,0 +1 @@ +export const customerInvoiceModuleKey = "customerInvoiceModuleKey" \ No newline at end of file diff --git a/src/feature/customer/data/module/customer-di.ts b/src/feature/customer/data/module/customer-di.ts index 05df3ae..4d153a8 100644 --- a/src/feature/customer/data/module/customer-di.ts +++ b/src/feature/customer/data/module/customer-di.ts @@ -1,4 +1,6 @@ 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"; @@ -9,5 +11,6 @@ export default function getCustomerDi(): DependencyContainer { useValue: fetchCustomersUsecase }) + customerDi.register(customerRepoKey, CustomerDbRepo) return customerDi } \ No newline at end of file diff --git a/src/feature/customer/data/repo/customer-db-repo.ts b/src/feature/customer/data/repo/customer-db-repo.ts new file mode 100644 index 0000000..fec27e1 --- /dev/null +++ b/src/feature/customer/data/repo/customer-db-repo.ts @@ -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 { + // 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; + + + 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)), + }) + } + +} \ No newline at end of file diff --git a/src/feature/customer/domain/entity/customer.ts b/src/feature/customer/domain/entity/customer.ts index 3b13a69..6f32ec3 100644 --- a/src/feature/customer/domain/entity/customer.ts +++ b/src/feature/customer/domain/entity/customer.ts @@ -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 { id: string; name: string; email: string; imageUrl: string; - totalInvoices: number; - totalPending: number; - totalPaid: number; + totalInvoices: string; + totalPending: string; + totalPaid: string; constructor({ id, diff --git a/src/test/common/fake-factory/customer/customer-fake-factory.ts b/src/test/common/fake-factory/customer/customer-fake-factory.ts index be69bec..cf7a53c 100644 --- a/src/test/common/fake-factory/customer/customer-fake-factory.ts +++ b/src/test/common/fake-factory/customer/customer-fake-factory.ts @@ -8,9 +8,9 @@ export default class CustomerFakeFactory { name: faker.person.fullName(), email: faker.internet.email(), imageUrl: faker.image.url(), - totalInvoices: faker.number.int(), - totalPaid: faker.number.int(), - totalPending: faker.number.int(), + totalInvoices: faker.number.int().toLocaleString(), + totalPaid: faker.finance.amount(), + totalPending: faker.number.int().toLocaleString(), }) }