develop #3

Merged
behnam merged 39 commits from develop into main 2024-11-26 15:47:00 +00:00
14 changed files with 109 additions and 65 deletions
Showing only changes of commit 6f54fa0c67 - Show all commits

View File

@ -1,5 +1,5 @@
import fetchCustomerInvoicesUsecase from "@/feature/core/customer-invoice/domain/usecase/fetch-customer-invoices-usecase"; import fetchCustomerInvoicesUsecase from "@/feature/core/customer-invoice/domain/usecase/fetch-customer-invoices-usecase";
export default function latestInvoicesController() { export default async function latestInvoicesController() {
return fetchCustomerInvoicesUsecase() return await fetchCustomerInvoicesUsecase()
} }

View File

@ -2,12 +2,15 @@ import CreateRandomInvoiceContainer from '@/app/dashboard/components/client/crea
import latestInvoicesController from '@/app/dashboard/components/server/latest-invoices/latest-invoices-controller'; import latestInvoicesController from '@/app/dashboard/components/server/latest-invoices/latest-invoices-controller';
import { ArrowPathIcon } from '@heroicons/react/24/outline'; import { ArrowPathIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx'; import clsx from 'clsx';
import { isLeft } from 'fp-ts/lib/Either';
import Image from 'next/image'; import Image from 'next/image';
export default async function LatestInvoices() { export default async function LatestInvoices() {
const latestInvoices = await latestInvoicesController(); const latestInvoices = await latestInvoicesController();
const invoices = latestInvoices.map((invoice, i) => { if (isLeft(latestInvoices)) return <div>Error</div>
const invoices = latestInvoices.right.map((invoice, i) => {
return ( return (
<div <div
key={invoice.id} key={invoice.id}

View File

@ -4,7 +4,7 @@ import { makeFailureMessage } from "@/feature/common/failures/failure-helpers";
* This is a class called BaseFailure that extends the Error class. It is * This is a class called BaseFailure that extends the Error class. It is
* used as a base class for creating custom failure classes. * used as a base class for creating custom failure classes.
*/ */
export default abstract class BaseFailure { export default abstract class BaseFailure<META_DATA> {
/* ------------------------------- Attributes ------------------------------- */ /* ------------------------------- Attributes ------------------------------- */
private readonly BASE_FAILURE_MESSAGE = "failure"; private readonly BASE_FAILURE_MESSAGE = "failure";
@ -15,8 +15,12 @@ export default abstract class BaseFailure {
message = this.BASE_FAILURE_MESSAGE; message = this.BASE_FAILURE_MESSAGE;
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
constructor(key: string) { metadata: META_DATA | undefined;
/* -------------------------------------------------------------------------- */
constructor(key: string, metadata?: META_DATA) {
this.message = makeFailureMessage(this.message, key); this.message = makeFailureMessage(this.message, key);
this.metadata = metadata ?? undefined
} }
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
} }

View File

@ -3,10 +3,10 @@ import BaseDevFailure from "@/feature/common/failures/dev/base-dev-failure";
/** /**
* Failure for needed arguments in a method but sent wrong one * Failure for needed arguments in a method but sent wrong one
*/ */
export default class ArgumentsFailure extends BaseDevFailure { export default class ArgumentsFailure<META_DATA> extends BaseDevFailure<META_DATA> {
/* ------------------------------- Constructor ------------------------------ */ /* ------------------------------- Constructor ------------------------------ */
constructor() { constructor(metadata?: META_DATA) {
super("arguments"); super("arguments", metadata);
} }
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
} }

View File

@ -1,3 +1,3 @@
import BaseFailure from "@/feature/common/failures/base-failure"; import BaseFailure from "@/feature/common/failures/base-failure";
export default abstract class BaseDevFailure extends BaseFailure {} export default abstract class BaseDevFailure<META_DATA> extends BaseFailure<META_DATA> {}

View File

@ -3,8 +3,8 @@ import BaseDevFailure from "@/feature/common/failures/dev/base-dev-failure";
/** /**
* This is a failure of not having specific dependency * This is a failure of not having specific dependency
*/ */
export default class DependencyFailure extends BaseDevFailure { export default class DependencyFailure<META_DATA> extends BaseDevFailure<META_DATA> {
constructor() { constructor(metadata: META_DATA) {
super("DependencyFailure"); super("DependencyFailure", metadata);
} }
} }

View File

@ -3,10 +3,10 @@ import BaseFailure from "./base-failure";
/** /**
* Failure for HTTP response when response dosn't have base structure * Failure for HTTP response when response dosn't have base structure
*/ */
export default class NetworkFailure extends BaseFailure { export default class NetworkFailure<META_DATA> extends BaseFailure<META_DATA> {
/* ------------------------------- Constructor ------------------------------ */ /* ------------------------------- Constructor ------------------------------ */
constructor() { constructor(metaData?: META_DATA) {
super("network"); super("network", metaData);
} }
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
} }

View File

@ -0,0 +1,12 @@
import BaseFailure from "./base-failure";
/**
* Failure for params failure
*/
export default class ParamsFailure<META_DATA> extends BaseFailure<META_DATA> {
/* ------------------------------- Constructor ------------------------------ */
constructor(metadata?: META_DATA) {
super("params", metadata);
}
/* -------------------------------------------------------------------------- */
}

View File

@ -1,7 +1,12 @@
import { sql } from "@/bootstrap/boundaries/db/db"; import { sql } from "@/bootstrap/boundaries/db/db";
import ApiTask from "@/feature/common/data/api-task";
import { failureOr } from "@/feature/common/failures/failure-helpers";
import NetworkFailure from "@/feature/common/failures/network-failure";
import { formatCurrency } from "@/feature/common/feature-helpers"; import { formatCurrency } from "@/feature/common/feature-helpers";
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice"; 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 CustomerInvoiceRepo from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice-repo";
import { pipe } from "fp-ts/lib/function";
import { tryCatch } from "fp-ts/lib/TaskEither";
import postgres from "postgres"; import postgres from "postgres";
type customerInvoiceDbResponse = { type customerInvoiceDbResponse = {
@ -13,23 +18,24 @@ type customerInvoiceDbResponse = {
} }
export default class CustomerInvoiceDbRepo implements CustomerInvoiceRepo { export default class CustomerInvoiceDbRepo implements CustomerInvoiceRepo {
async fetchList(): Promise<CustomerInvoice[]> { fetchList(): ApiTask<CustomerInvoice[]> {
try { return pipe(
const data = await sql` tryCatch(
SELECT invoices.amount, customers.name, customers.image_url, customers.email, invoices.id async () => {
FROM invoices const response = await sql`
JOIN customers ON invoices.customer_id = customers.id SELECT invoices.amount, customers.name, customers.image_url, customers.email, invoices.id
ORDER BY invoices.date DESC FROM invoices
LIMIT 20 ` as postgres.RowList<customerInvoiceDbResponse[]>; JOIN customers ON invoices.customer_id = customers.id
ORDER BY invoices.date DESC
return this.customerInvoicesDto(data) LIMIT 20 ` as postgres.RowList<customerInvoiceDbResponse[]>;
} catch (error) {
console.error('Database Error:', error); return this.customerInvoicesDto(response)
throw new Error('Failed to fetch the latest invoices.'); },
} (l) => failureOr(l, new NetworkFailure())
)
)
} }
private customerInvoicesDto(dbCustomers: customerInvoiceDbResponse[]): CustomerInvoice[] { private customerInvoicesDto(dbCustomers: customerInvoiceDbResponse[]): CustomerInvoice[] {
return dbCustomers.map((customer) => this.customerInvoiceDto(customer)); return dbCustomers.map((customer) => this.customerInvoiceDto(customer));
} }

View File

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

View File

@ -1,12 +1,12 @@
"use server" import { ApiEither } from "@/feature/common/data/api-task";
import serverDi from "@/feature/common/server-di"; import serverDi from "@/feature/common/server-di";
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice"; 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 CustomerInvoiceRepo, { customerInvoiceRepoKey } from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice-repo";
import { customerInvoiceModuleKey } from "@/feature/core/customer-invoice/invoice-module-key"; import { customerInvoiceModuleKey } from "@/feature/core/customer-invoice/invoice-module-key";
import { connection } from "next/server"; import { connection } from "next/server";
export default async function fetchCustomerInvoicesUsecase(): Promise<CustomerInvoice[]> { export default async function fetchCustomerInvoicesUsecase(): Promise<ApiEither<CustomerInvoice[]>> {
connection() connection()
const repo = serverDi(customerInvoiceModuleKey).resolve<CustomerInvoiceRepo>(customerInvoiceRepoKey) const repo = serverDi(customerInvoiceModuleKey).resolve<CustomerInvoiceRepo>(customerInvoiceRepoKey)
return repo.fetchList() return repo.fetchList()()
} }

View File

@ -1,8 +1,13 @@
import { sql } from "@/bootstrap/boundaries/db/db"; import { sql } from "@/bootstrap/boundaries/db/db";
import ApiTask from "@/feature/common/data/api-task";
import { failureOr } from "@/feature/common/failures/failure-helpers";
import NetworkFailure from "@/feature/common/failures/network-failure";
import { formatCurrency } from "@/feature/common/feature-helpers"; import { formatCurrency } from "@/feature/common/feature-helpers";
import InvoiceRepo from "@/feature/core/invoice/domain/i-repo/invoice-repo"; import InvoiceRepo from "@/feature/core/invoice/domain/i-repo/invoice-repo";
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice-param"; import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice-param";
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status"; import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
import { pipe } from "fp-ts/lib/function";
import { tryCatch } from "fp-ts/lib/TaskEither";
import postgres from "postgres"; import postgres from "postgres";
type InvoiceSummaryDbResponse = {paid: string, pending: string} type InvoiceSummaryDbResponse = {paid: string, pending: string}
@ -13,26 +18,33 @@ export default class InvoiceDbRepo implements InvoiceRepo {
return data.count ?? 0 return data.count ?? 0
} }
async createInvoice(params: InvoiceParam): Promise<string> { createInvoice(params: InvoiceParam): ApiTask<string> {
const firstCustomerIdDb = await sql`SELECT return pipe(
id FROM customers tryCatch(
ORDER BY id DESC async () => {
LIMIT 1 const firstCustomerIdDb = await sql`SELECT
` id FROM customers
const customerId = firstCustomerIdDb.at(0)?.id ORDER BY id DESC
if (!customerId) throw new Error("There is no customer") LIMIT 1
`
const { amount, status } = params; const customerId = firstCustomerIdDb.at(0)?.id
const amountInCents = amount * 100; if (!customerId) throw new Error("There is no customer")
const date = new Date().toISOString().split('T')[0];
const { amount, status } = params;
// Insert data into the database const amountInCents = amount * 100;
const result = await sql` const date = new Date().toISOString().split('T')[0];
INSERT INTO invoices (customer_id, amount, status, date)
VALUES (${customerId}, ${amountInCents}, ${status}, ${date}) // Insert data into the database
RETURNING id const result = await sql`
`; INSERT INTO invoices (customer_id, amount, status, date)
return result.at(0)?.id ?? "" VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
RETURNING id
`;
return result.at(0)?.id ?? ""
},
(l) => failureOr(l, new NetworkFailure(l as Error))
),
)
} }
async fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary> { async fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary> {

View File

@ -1,10 +1,11 @@
import ApiTask from "@/feature/common/data/api-task"
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice-param" import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice-param"
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status" import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status"
export default interface InvoiceRepo { export default interface InvoiceRepo {
fetchAllInvoicesAmount(): Promise<number> fetchAllInvoicesAmount(): Promise<number>
fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary> fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary>
createInvoice(params: InvoiceParam): Promise<string> createInvoice(params: InvoiceParam): ApiTask<string>
} }
export const invoiceRepoKey = "invoiceRepoKey" export const invoiceRepoKey = "invoiceRepoKey"

View File

@ -1,19 +1,24 @@
"use server" "use server"
import { ApiEither } from "@/feature/common/data/api-task";
import ParamsFailure from "@/feature/common/failures/params-failure";
import serverDi from "@/feature/common/server-di"; import serverDi from "@/feature/common/server-di";
import InvoiceRepo, { invoiceRepoKey } from "@/feature/core/invoice/domain/i-repo/invoice-repo"; import InvoiceRepo, { invoiceRepoKey } from "@/feature/core/invoice/domain/i-repo/invoice-repo";
import { InvoiceParam, invoiceSchema } from "@/feature/core/invoice/domain/param/invoice-param"; import { InvoiceParam, invoiceSchema } from "@/feature/core/invoice/domain/param/invoice-param";
import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key"; import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
import { pipe } from "fp-ts/lib/function";
import { chain, fromNullable, left, map, right } from "fp-ts/lib/TaskEither";
export default async function createInvoiceUsecase(params: InvoiceParam): Promise<string | {errorMessage: string}> { export default async function createInvoiceUsecase(params: InvoiceParam): Promise<ApiEither<string>> {
const isParamsValid = invoiceSchema.safeParse(params)
if (!isParamsValid) {
return {
errorMessage: "Please pass correct params"
}
}
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey) const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey)
return repo.createInvoice(params) return pipe(
fromNullable(new ParamsFailure())(params),
map((params) => invoiceSchema.safeParse(params)),
chain((params) => {
const isParamsValid = invoiceSchema.safeParse(params)
if (!isParamsValid.success) left(new ParamsFailure())
return right(params.data as InvoiceParam)
}),
chain((params) => repo.createInvoice(params))
)()
} }