Compare commits

...

13 Commits

Author SHA1 Message Date
Behnamrhp74
4291fa832a doc: finalize failure document 2025-03-14 00:07:13 +03:00
Behnamrhp74
da8bbc5c9e doc: Add class diagram and image for failure 2025-03-13 23:01:52 +03:00
Behnamrhp74
1f6431eec6 doc: Add translation description for failure doc 2025-03-11 23:51:27 +03:00
Behnamrhp74
6ac70ac455 doc: Add base failure document 2025-03-11 22:44:02 +03:00
behnam rahimpour
5a29c54a6a
Merge pull request #3 from behnamrhp/refactor/namings
Refactor/namings
2025-03-09 00:53:03 +03:00
Behnamrhp74
4ee7d4b8e3 refactor: usecase usage example for calling with interface between controller and usecases 2025-03-08 22:25:29 +03:00
Behnamrhp74
4306ea1bdc refactor: structure of calling usecases to just be from controller 2025-03-08 22:16:29 +03:00
Behnamrhp74
7d9a6e77bd refactor: Change name of files and add some comments for them 2025-03-08 21:54:24 +03:00
Behnamrhp74
38f0d0a596 refactor: Change name of i18n and its usage in server 2025-03-08 21:04:46 +03:00
behnam rahimpour
096496df64
Merge pull request #2 from behnamrhp/fix/storybook-server-calling
fix: Not showing create invoice button for server usecase
2025-03-08 21:02:13 +03:00
86732339ea fix: Not showing create invoice button for server usecase 2025-03-07 11:38:10 +03:00
behnam rahimpour
3a44fdafb6
Merge pull request #1 from behnamrhp/fix/translation-issues
fix: Fix translation issue for not switching the lang
2025-03-07 11:01:40 +03:00
693f07528e fix: Fix translation issue for not switching the lang 2025-03-07 10:53:01 +03:00
82 changed files with 621 additions and 177 deletions

View File

@ -0,0 +1,53 @@
@startuml Failure Class Diagram
abstract BaseFailure {
- String BASE_FAILURE_MESSAGE = "failure"
+ String message = this.BASE_FAILURE_MESSAGE
constructor(key: string)
}
class UserFailure {
constructor(key: string)
}
note left
Extends parent message key by sending
`user` keyword as a domain to parent.
So in `UserFailure` our `message`
property will be `failure.user`.
end note
UserFailure --|> BaseFailure
class UserModificationFailure {
constructor(key: string)
}
note right
Extends parent message key by sending
`modification` keyword to parent.
So in `UserModificationFailure` our `message`
property will be `failure.user.modification`
end note
UserModificationFailure --|> UserFailure
class UserModificationAlreadyExistsFailure {
constructor()
}
note left
Extends parent message key by sending
`alreadyExists` keyword to parent.
So in `UserModificationAlreadyExistsFailure`
our `message` property will be
`failure.user.modification.AlreadyExists`
end note
class DeviceFailure {
constructor()
}
DeviceFailure --|> BaseFailure
UserModificationAlreadyExistsFailure --|> UserModificationFailure
@enduml

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,246 @@
# Error handling with failure
## What is failure
In each application and in each logic, there can be failures in the process, and based on their complexity, there can be a few or many possible scenarios for these failures.
In software development, we always try to have more control over these failures to:
Avoid possible bugs
Help users understand the state of the application with proper messages
Control processes for side effects
Monitor the behavior of the application
So, having a specific way of handling errors to achieve all these requirements in our app helps us build a more robust, reliable, and maintainable application.
Many frameworks provide their own ways to handle these failures, which they may call exceptions, failures, or other terms. But we shouldn't always depend on the behavior of frameworks for our logic and apps. Besides, many frameworks do not provide error handling tools, so we need to design a reliable architecture for the error handling in our application.
## Failure handling with base failure
To have granular control over failures and define a specific type for all errors, we can use the power of inheritance and abstraction in OOP.
We can define an abstract class as our base failure, which serves as the specific type for all failures in the application.
```ts
export default abstract class BaseFailure<META_DATA> {
metadata: META_DATA | undefined;
constructor(metadata?: META_DATA) {
this.metadata = metadata ?? undefined;
}
}
```
As you can see, it's just a simple abstract class that takes some metadata about the error details in any form. But wait, this is just the beginning of the story, we can explore many ideas with this failure.
## How to write a simple failure
So, to create a simple failure, we can define our failure in any domain for any scenario we need, like this:
```ts
export default class CreateUserFailure extends BaseFailure<{ userId: string }> {
constructor(metadata?: { userId: string }) {
super(metadata);
}
}
```
So in our logics for creating user we can return specific type of failure for creating user.
## Combination with Functional programming
Functional programming is a deep topic that we cannot fully cover here. For more details, you can check out various courses and books available online.
However, for this article, we focus on one of the most useful functors in functional programming and how failure fits perfectly with it. This concept is the Either type.
Either is an algebraic data type (ADT) that represents computations that can return either a success or a failure. It consists of two possible values:
- Right(value): Represents a successful result.
- Left(error): Represents a failure or unexpected result.
You're guessing right, our base failure will serve as the Left value in Either, allowing us to handle errors in a structured and functional way.
```ts
Either<
BaseFailure<unknown>,
ResponseType
>
```
So as we always have specific type for handling unexpected results, so we can define a new type for either in our app.
```ts
export type ApiEither<ResponseType> = Either<
BaseFailure<unknown>,
ResponseType
>;
```
So, for any API calls, we can use the Either type to handle both success and failure cases.
Additionally, for asynchronous processes, we use TaskEither, which works similarly to Either but is designed for handling asynchronous operations.
```ts
type ApiTask<ResponseType> = TaskEither<BaseFailure<unknown>, ResponseType>;
```
For example, when creating a customer repository to handle all API calls for customers, we can use this type to manage success and failure cases.
```ts
export default interface CustomerRepo {
fetchList(query: string): ApiTask<Customer[]>;
}
```
And in the repo we can have this pipe to get customer data:
> Note: In functional programming, a pipe is a composition of functions where the output of one function is passed as the input to the next, allowing for a clean, readable flow of data transformations.
```ts
pipe(
tryCatch(
async () => {
...// calling api and returning result
},
(l) => failureOr(l, new NetworkFailure(l as Error)),
),
map(this.customersDto.bind(this)),
) as ApiTask<Customer[]>;
```
As you can see, in the try-catch block, which is the constructor of ApiEither, we define the right response in the first callback and the failure in the second callback argument.
failureOr is just a helper function that takes an error and converts it into a specific failure type, NetworkFailure in this example.
This ensures that during the process of fetching customer data, we always know the unexpected result will be of a specific type.
```ts
export function failureOr(
reason: unknown,
failure: BaseFailure<any>,
): BaseFailure<any> {
if (reason instanceof BaseFailure) {
return reason;
}
return failure;
}
```
So in the process of fetching customer we know the unexpected result, always will be a speicfic type.
So in any layer we can get the failure do some logics on left response based on its metadata and turn the failure shape to any other failure shape and use it for different purposes.
## Usecases of this idea
There are many situations where, if an important process encounters problems, we want to have control over it. We need to know when and why these issues happened and store that information in one of the monitoring tools.
For example, when a CreateUserFailure occurs in the repository layer, we can send a log with the specific time and relevant parameter data to any logging or monitoring tool.
### Monitoring on bugs with dev failures
There are many situations, especially in frontend applications, where unexpected behavior occurs due to development mistakes or bugs. For example, when bugs or data changes in APIs happen, it's possible to face unexpected behaviors. In such cases, we want to show a specific message or redirect the user to an error page with a clear message.
Additionally, in frontend applications, logs may not be directly available in these situations, as the issue occurs on the user's system. To handle this, we can send metadata as a log to an API when encountering development failures.
To achieve this, we can simply define another abstract failure like this:
```ts
export default abstract class BaseDevFailure<
META_DATA,
> extends BaseFailure<META_DATA> {}
```
As you can see, its just another failure that extends from the base failure.
For example, in some parts of the application, when sending dynamic arguments into the domain layer, there's a possibility of sending unexpected data. In such situations, we can define a specific development failure like this:
```ts
export default class ArgumentsFailure<
META_DATA,
> extends BaseDevFailure<META_DATA> {
constructor(metadata?: META_DATA) {
super(metadata);
}
}
```
So we can consider this scenario in our logics and facing with this failure we can make a log request to our log api even from frontend applications, so on facing with this situation they can show a descent message to user to contact to support team at the same time they store the bug log to have full controll on these situations.
### Manage translations and error messages with failure
With this approach, we can go a step further than just error handling and even manage translations and display related messages in frontend applications automatically.
For each process and scenario, we should define a specific failure. At the same time, for each failure, we should display a corresponding message in the selected language based on the user's preference.
We can use this idea to automate both the error handling and message translation process.
To achieve this, we can pass a unique string key from the constructor based on the failure scenario. Our base failure will look like this:
```ts
export default abstract class BaseFailure<META_DATA> {
private readonly BASE_FAILURE_MESSAGE = "failure";
/**
* Use this message as key lang for failure messages
*/
message = this.BASE_FAILURE_MESSAGE;
metadata: META_DATA | undefined;
constructor(key: string, metadata?: META_DATA) {
this.message = makeFailureMessage(this.message, key);
this.metadata = metadata ?? undefined;
}
}
/**
* Gets Message key and it'll add it to the failure message key hierarchy
*/
export function makeFailureMessage(message: string, key: string) {
if (!key) return message;
return `${message}.${key}`;
}
```
As you can see, we have a message property, which contains `BASE_FAILURE_MESSAGE`, the base key for all failure messages. It also accepts a key from the constructor, and with the makeFailureMessage function, it concatenates the new key with the message, shaping a unique message for each failure.
Each failure can have its own key passed from its constructor.
In the end, we can have a chained message key that we can use as the message key for each failure.
For example, for a failure like `UserAlreadyExistsFailure`, we can have a parent failure for all user domain failures, like this:
```ts
export default class UserFailure extends BaseFailure {
constructor(key: string) {
super(makeFailureMessage("user", key));
}
}
```
and now we can define our failure:
```ts
export default class UserAlreadyExistsFailure extends UserFailure {
constructor() {
super("alreadyExists");
}
}
```
so the result of message for `UserAlreadyExistsFailure`, will be `failure.user.alreadyExists`.
At the same time, in another part of our project, we're using a langKey object to specify the translation key. This object, like the failure structure, follows the domain and folder structure to specify the language key.
```ts
const langKey = {
// ...
failure: {
user: {
alreadyExists: "failure.user.alreadyExists",
}
}
}
```
So, we can use our failure message key to retrieve the language key. By passing it to the translation method, we can get the translated failure message and automate the process of displaying the error message based on the failure we encounter.
```ts
const usecaseResponse = await getUsersUsecase() as Promise<Either<BaseFailure, User[]>>
if (!isLeft(usecaseResponse)) return;
if (!(usecaseResponse instanceOf BaseFailure)) return;
const translatedFailureMessage = t(usecaseResponse.left.message)
```
This is the final version, class diagram for our failur architecture:
![Failure class diagram](./failure-class-diagram.svg)
## Conclusion
In this article, we've explored how to handle failures effectively in software applications by combining error handling with functional programming concepts like the Either type.
Furthermore, by integrating these failure handling mechanisms with automated processes for translating and displaying error messages, we create a more seamless experience for users, no matter the scenario. This approach not only improves the user experience by offering clear and context-specific messages, but it also provides valuable insights through monitoring and logging, enabling teams to quickly address issues.
Ultimately, this architecture supports building more robust, maintainable, and user-friendly applications, which I have used in many of my own projects, especially in frontend ones.

View File

@ -5,6 +5,9 @@ import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random
import { useDI } from "@/bootstrap/di/di-context";
import { useRef } from "react";
/**
* From a parent component Vm and view will be connected together.
*/
export default function CreateRandomInvoiceContainer() {
const di = useDI();
const vm = useRef(di.resolve(CreateRandomInvoiceButtonVM));

View File

@ -2,17 +2,26 @@ import { DocumentIcon } from "@/app/components/icons/document";
import HomeIcon from "@/app/components/icons/home";
import { UserIcon } from "@/app/components/icons/user";
import { usePathname } from "next/navigation";
import { useRef } from "react";
type LinkItem = {
name: string;
href: string;
icon: (props: { className?: string }) => JSX.Element;
};
/**
* Beside of reusable vm each View can have it's own personal vm to handle it's ownlogics.
* Difference between personal vm and other vms which extends BaseVM, is that
* personal vm directly will be called inside of view and instinctly connected to the view,
* so they come together always and there is no need to be connected with interface for reusable
* vms.
*/
export default function navLinkPersonalVM() {
const pathname = usePathname();
// Map of links to display in the side navigation.
// Depending on the size of the application, this would be stored in a database.
const links: LinkItem[] = [
const links = useRef<LinkItem[]>([
{ name: "Home", href: "/dashboard", icon: HomeIcon },
{
name: "Invoices",
@ -20,7 +29,7 @@ export default function navLinkPersonalVM() {
icon: DocumentIcon,
},
{ name: "Customers", href: "/dashboard/customers", icon: UserIcon },
];
]).current;
return {
links,
isLinkActive: (link: LinkItem) => pathname === link.href,

View File

@ -1,6 +1,6 @@
"use client";
import navLinkPersonalVM from "@/app/[lang]/dashboard/components/client/nav-links/nav-link-vm";
import navLinkPersonalVM from "@/app/[lang]/dashboard/components/client/nav-links/nav-link.personal-vm";
import clsx from "clsx";
import Link from "next/link";

View File

@ -1,11 +1,13 @@
"use client"
/* eslint-disable react/jsx-props-no-spreading */
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

View File

@ -1,4 +1,4 @@
import cardController from "@/app/[lang]/dashboard/components/server/card/card-controller";
import cardController from "@/app/[lang]/dashboard/controller/card.controller";
export function Card({
title,

View File

@ -1,5 +1,5 @@
import { Card } from "@/app/[lang]/dashboard/components/server/card/card";
import cardsController from "@/app/[lang]/dashboard/components/server/cards/cards-controller";
import { Card } from "@/app/[lang]/dashboard/components/server/card";
import cardsController from "@/app/[lang]/dashboard/controller/cards.controller";
export default async function CardWrapper() {
const { customersNumber, invoicesNumber, invoicesSummary } =

View File

@ -1,7 +0,0 @@
import fetchSummaryInfoUsecase from "@/feature/core/summary-info/domain/usecase/fetch-summary-info-usecase";
import { connection } from "next/server";
export default function cardsController() {
connection();
return fetchSummaryInfoUsecase();
}

View File

@ -1,5 +1,5 @@
import CreateRandomInvoiceContainer from "@/app/[lang]/dashboard/components/client/create-random-invoice/create-random-invoice";
import latestInvoicesController from "@/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices-controller";
import latestInvoicesController from "@/app/[lang]/dashboard/controller/latest-invoices.controller";
import { ArrowPathIcon } from "@heroicons/react/24/outline";
import clsx from "clsx";
import { isLeft } from "fp-ts/lib/Either";

View File

@ -1,7 +0,0 @@
import fetchCustomerInvoicesUsecase from "@/feature/core/customer-invoice/domain/usecase/fetch-customer-invoices-usecase";
import { connection } from "next/server";
export default function latestInvoicesController() {
connection();
return fetchCustomerInvoicesUsecase();
}

View File

@ -1,4 +1,4 @@
import revenueChartController from "@/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart-controller";
import revenueChartController from "@/app/[lang]/dashboard/controller/revenue-chart.controller";
import { CalendarIcon } from "@heroicons/react/24/outline";
export default async function RevenueChart() {

View File

@ -5,6 +5,12 @@ import {
InboxIcon,
} from "@heroicons/react/24/outline";
/**
* Controllers are bridge between feature layer and application layer.
* They decide, feature layer will be cached or not, where to run in client or server
* Or connect multiple usecases and run them, handle their failure, hydrate and store data in
* client state managements.
*/
export default function cardController(props: {
type: "invoices" | "customers" | "pending" | "collected";
}) {

View File

@ -0,0 +1,13 @@
import fetchSummaryInfoUsecase from "@/feature/core/summary-info/domain/usecase/fetch-summary-info.usecase";
import { connection } from "next/server";
/**
* Controllers are bridge between feature layer and application layer.
* They decide, feature layer will be cached or not, where to run in client or server
* Or connect multiple usecases and run them, handle their failure, hydrate and store data in
* client state managements.
*/
export default function cardsController() {
connection();
return fetchSummaryInfoUsecase();
}

View File

@ -0,0 +1,27 @@
"use server";
import { ApiEither } from "@/feature/common/data/api-task";
import serverDi from "@/feature/common/server.di";
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice.param";
import {
CreateInvoiceUsecase,
createInvoiceUsecaseKey,
} from "@/feature/core/invoice/domain/usecase/create-invoice/create-invoice.usecase";
import { invoiceModuleKey } from "@/feature/core/invoice/invoice.module-key";
import { connection } from "next/server";
/**
* Controllers are bridge between feature layer and application layer.
* They decide, feature layer will be cached or not, where to run in client or server
* Or connect multiple usecases and run them, handle their failure, hydrate and store data in
* client state managements.
*/
export default async function createInvoiceController(
params: InvoiceParam,
): Promise<ApiEither<string>> {
connection();
const usecase = serverDi(invoiceModuleKey).resolve<CreateInvoiceUsecase>(
createInvoiceUsecaseKey,
);
return usecase(params);
}

View File

@ -0,0 +1,13 @@
import fetchCustomerInvoicesUsecase from "@/feature/core/customer-invoice/domain/usecase/fetch-customer-invoices.usecase";
import { connection } from "next/server";
/**
* Controllers are bridge between feature layer and application layer.
* They decide, feature layer will be cached or not, where to run in client or server
* Or connect multiple usecases and run them, handle their failure, hydrate and store data in
* client state managements.
*/
export default function latestInvoicesController() {
connection();
return fetchCustomerInvoicesUsecase();
}

View File

@ -1,6 +1,12 @@
import Revenue from "@/feature/core/revenue/domain/entity/revenue";
import fetchRevenuesUsecase from "@/feature/core/revenue/domain/usecase/fetch-revenues-usecase";
import Revenue from "@/feature/core/revenue/domain/entity/revenue.entity";
import fetchRevenuesUsecase from "@/feature/core/revenue/domain/usecase/fetch-revenues.usecase";
/**
* Controllers are bridge between feature layer and application layer.
* They decide, feature layer will be cached or not, where to run in client or server
* Or connect multiple usecases and run them, handle their failure, hydrate and store data in
* client state managements.
*/
export default async function revenueChartController() {
const revenue = await fetchRevenuesUsecase();
const chartHeight = 350;

View File

@ -1,7 +1,7 @@
"use client";
import SideNav from "@/app/[lang]/dashboard/components/server/sidenav";
import dashboardAppModule from "@/app/[lang]/dashboard/module/dashboard-app-module";
import dashboardAppModule from "@/app/[lang]/dashboard/module/dashboard.app-module";
import { DiContext } from "@/bootstrap/di/di-context";
import { useRef } from "react";

View File

@ -1,4 +1,4 @@
import DashboardSkeleton from "@/app/[lang]/dashboard/components/server/skeletons/skeletons";
import DashboardSkeleton from "@/app/[lang]/dashboard/components/server/skeletons";
export default function Loading() {
return <DashboardSkeleton />;

View File

@ -1,13 +1,12 @@
import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm";
import di from "@/bootstrap/di/init-di";
import createInvoiceUsecase from "@/feature/core/invoice/domain/usecase/create-invoice-usecase";
/**
* Each page can have its own di to connect all vms, usecases or controllers
*/
export default function dashboardAppModule() {
const dashboardDi = di.createChildContainer();
dashboardDi.register(createInvoiceUsecase.name, {
useValue: createInvoiceUsecase,
});
dashboardDi.register(
CreateRandomInvoiceButtonVM,
CreateRandomInvoiceButtonVM,

View File

@ -1,13 +1,13 @@
import {
LatestInvoicesSkeleton,
RevenueChartSkeleton,
} from "@/app/[lang]/dashboard/components/server/skeletons/skeletons";
import CardWrapper from "@/app/[lang]/dashboard/components/server/cards/cards";
import LatestInvoices from "@/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices";
import RevenueChart from "@/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart";
} from "@/app/[lang]/dashboard/components/server/skeletons";
import CardWrapper from "@/app/[lang]/dashboard/components/server/cards";
import RevenueChart from "@/app/[lang]/dashboard/components/server/revenue-chart";
import { Suspense } from "react";
import { getServerTranslation, LANGS } from "@/bootstrap/i18n/i18n";
import langKey from "@/bootstrap/i18n/dictionaries/lang-key";
import LatestInvoices from "@/app/[lang]/dashboard/components/server/latest-invoices";
export default async function Dashboard(props: {
params: Promise<{ lang: LANGS }>;

View File

@ -1,20 +1,26 @@
import ButtonVm from "@/app/components/button/button-vm";
import createInvoiceController from "@/app/[lang]/dashboard/controller/create-invoice.controller";
import ButtonVm from "@/app/components/button/button.i-vm";
import { useServerAction } from "@/bootstrap/helpers/hooks/use-server-action";
import useThrottle from "@/bootstrap/helpers/hooks/use-throttle";
import BaseVM from "@/bootstrap/helpers/vm/base-vm";
import langKey from "@/bootstrap/i18n/dictionaries/lang-key";
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice-param";
import createInvoiceUsecase from "@/feature/core/invoice/domain/usecase/create-invoice-usecase";
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice.param";
import { CreateInvoiceUsecase } from "@/feature/core/invoice/domain/usecase/create-invoice/create-invoice.usecase";
import { faker } from "@faker-js/faker";
import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next";
/**
* Viewmodel for the button view to connect to business logics and all UI logics
* For UI logics, all translations, states, sideeffects and events will be handled
* in this layer.
*/
export default class CreateRandomInvoiceButtonVM extends BaseVM<ButtonVm> {
private createInvoice: typeof createInvoiceUsecase;
private createInvoice: CreateInvoiceUsecase;
constructor() {
super();
this.createInvoice = this.di.resolve(createInvoiceUsecase.name);
this.createInvoice = createInvoiceController;
}
useVM(): ButtonVm {

View File

@ -1,5 +1,5 @@
import { ThemeProvider } from "@/app/[lang]/dashboard/components/client/theme-provider/theme-provider";
import { initI18next, LANGS } from "@/bootstrap/i18n/i18n";
import { getI18n, LANGS } from "@/bootstrap/i18n/i18n";
import TranslationsProvider from "@/bootstrap/i18n/i18n-provider";
import localFont from "next/font/local";
import { PropsWithChildren } from "react";
@ -20,7 +20,7 @@ export default async function layout(
) {
const { params, children } = props;
const { lang } = await params;
const { resources } = await initI18next({ lng: lang });
await getI18n({ lng: lang });
return (
<html lang={lang} suppressHydrationWarning>
<body
@ -32,9 +32,7 @@ export default async function layout(
enableSystem
disableTransitionOnChange
>
<TranslationsProvider lng={lang} resources={resources}>
{children}
</TranslationsProvider>
<TranslationsProvider lng={lang}>{children}</TranslationsProvider>
</ThemeProvider>
</body>
</html>

View File

@ -1,7 +0,0 @@
export default interface ButtonVm {
props: {
title: string;
isDisable: boolean;
};
onClick(): void;
}

View File

@ -0,0 +1,11 @@
/**
* Interface for viewmodel of button. this is bridge between view and viewmodel.
* With this interface, view is adapter and vms will be implementations in bridge pattern
*/
export default interface ButtonVm {
props: {
title: string;
isDisable: boolean;
};
onClick(): void;
}

View File

@ -1,7 +1,7 @@
"use client";
import BaseView, { BuildProps } from "@/bootstrap/helpers/view/base-view";
import ButtonVm from "@/app/components/button/button-vm";
import ButtonVm from "@/app/components/button/button.i-vm";
import { ReactNode } from "react";
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";

View File

@ -4,7 +4,7 @@ import { DiContext, useDI } from "@/bootstrap/di/di-context";
import mockedModuleDi from "@/bootstrap/di/mocked-module-di";
import Story from "@/bootstrap/helpers/view/storybook-base-template-type";
import getArgVM from "@/bootstrap/helpers/view/storybook-with-arg-vm";
import createInvoiceUsecase from "@/feature/core/invoice/domain/usecase/create-invoice-usecase";
import { createInvoiceUsecaseKey } from "@/feature/core/invoice/domain/usecase/create-invoice/create-invoice.usecase";
import type { Meta } from "@storybook/react";
import { useRef } from "react";
@ -36,30 +36,32 @@ export const Primary: Story = {
export const WithVM: Story = {
decorators: [
(Story) => {
const di = mockedModuleDi([
const di = useRef(
mockedModuleDi([
{
token: CreateRandomInvoiceButtonVM,
provider: CreateRandomInvoiceButtonVM,
},
{
token: createInvoiceUsecase.name,
token: createInvoiceUsecaseKey,
// eslint-disable-next-line @typescript-eslint/no-explicit-any, no-console
provider: (args: any) => console.log("clicked", args),
},
]);
return <Story di={di} />;
]),
);
return (
<DiContext.Provider value={di.current}>
<Story di={di.current} />
</DiContext.Provider>
);
},
],
render: (_, globalProps) => {
render: () => {
function Child() {
const di = useDI();
const vm = useRef(di.resolve(CreateRandomInvoiceButtonVM));
return <Button vm={vm.current} memoizedByVM={false} />;
}
return (
<DiContext.Provider value={globalProps.di}>
<Child />
</DiContext.Provider>
);
return <Child />;
},
};

View File

@ -2,6 +2,9 @@ import { constructor } from "tsyringe/dist/typings/types";
export const isServer = typeof window === "undefined";
/**
* Checks if the given value is a class
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isClass(fn: any): fn is constructor<unknown> {
return typeof fn === "function" && /^(class|function [A-Z])/.test(fn);

View File

@ -1,6 +1,10 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
/**
* To connect tailwind classes.
* @param inputs Tailwind classes
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@ -66,6 +66,11 @@ export type BuildProps<
children?: ReactNode;
};
/**
* Base view is base component for all views in mvvm architecture which gets
* vm as props and connect it to the view and memoize the component by default
* to just render just on changes of its vm.
*/
export default abstract class BaseView<
IVM extends IVMParent,
PROPS extends IPropParent = undefined,

View File

@ -1,5 +1,12 @@
import IBaseVM from "@/bootstrap/helpers/vm/i-base-vm";
/**
* To use with mvvm library to make a vm based on props so you can pass the result to the view
* @param vmObj All properties which view needs to get from vm.
* @returns Vm which is suitable to be passed to the view
* @example const vm = getArgVM(globalData.parsedProps.vm);
return <Button vm={vm} memoizedByVM={false} />;
*/
const getArgVM = <IVM>(vmObj: IVM) => {
class VM implements IBaseVM<IVM> {
useVM(): IVM {

View File

@ -5,6 +5,12 @@ import { NoOverride } from "@/bootstrap/helpers/type-helper";
import IBaseVM from "@/bootstrap/helpers/vm/i-base-vm";
import { useState } from "react";
/**
* Base class for all viewmodels. It provides
* - dependency injection: To get closes di which serves from di provider
* - rerender method: to rerender your component manually
* - produce method: to produce your vm dynamically by passing and attaching dependencies to it
*/
export default abstract class BaseVM<
IVM,
DEP extends object | undefined = undefined,

View File

@ -1,3 +1,6 @@
/**
* All viewmodels should implement this interface.
*/
export default interface IBaseVM<VM> {
useVM(): VM;
}

View File

@ -1,3 +1,8 @@
/**
* main language keys which will be used for translation to avoid using strings directly and be
* a single source of truth in all changes between all languages dictionaries.
* All languages dictionaries should have the same keys by having this object type.
*/
const langKey = {
global: {
home: "global.home",

View File

@ -1,17 +1,24 @@
"use client";
import { I18nextProvider } from "react-i18next";
import { i18nInstance, initI18next, LANGS } from "@/bootstrap/i18n/i18n";
import { Resource } from "i18next";
import { PropsWithChildren } from "react";
import { getI18n, LANGS } from "@/bootstrap/i18n/i18n";
import { PropsWithChildren, useEffect, useState } from "react";
import { i18n } from "i18next";
import storeLang from "@/bootstrap/i18n/store-lang-action";
export default function TranslationsProvider({
children,
lng,
resources,
}: PropsWithChildren & { lng: LANGS; resources: Resource }) {
if (!resources) return children;
initI18next({ lng, resources });
}: PropsWithChildren & { lng: LANGS }) {
const [i18n, setI18n] = useState<i18n>();
return <I18nextProvider i18n={i18nInstance}>{children}</I18nextProvider>;
useEffect(() => {
(async () => {
storeLang(lng);
setI18n((await getI18n({ lng })).i18n);
})();
}, [lng]);
if (!i18n) return null;
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
}

View File

@ -10,7 +10,7 @@ export enum LANGS {
RU = "ru",
}
export const initI18next = async (params: {
export const getI18n = async (params: {
lng: LANGS;
resources?: Resource;
ns?: string;
@ -43,13 +43,9 @@ export async function getServerTranslation(
ns?: string,
options: { keyPrefix?: string } = {},
) {
await initI18next({ lng });
const { i18n } = await getI18n({ lng });
return {
t: i18nInstance.getFixedT(
lng,
Array.isArray(ns) ? ns[0] : ns,
options?.keyPrefix,
),
t: i18n.getFixedT(lng, Array.isArray(ns) ? ns[0] : ns, options?.keyPrefix),
i18n: i18nInstance,
};
}

View File

@ -0,0 +1,9 @@
"use server";
import { LANGS } from "@/bootstrap/i18n/i18n";
import { cookieName } from "@/bootstrap/i18n/settings";
import { cookies } from "next/headers";
export default async function storeLang(lng: LANGS) {
(await cookies()).set(cookieName, lng);
}

View File

@ -1,6 +1,6 @@
import { Either } from "fp-ts/lib/Either";
import { TaskEither } from "fp-ts/lib/TaskEither";
import BaseFailure from "@/feature/common/failures/base-failure";
import BaseFailure from "@/feature/common/failures/base.failure";
type ApiTask<ResponseType> = TaskEither<BaseFailure<unknown>, ResponseType>;
export type ApiEither<ResponseType> = Either<

View File

@ -1,8 +1,14 @@
import { makeFailureMessage } from "@/feature/common/failures/failure-helpers";
/**
* This is a class called BaseFailure that extends the Error class. It is
* used as a base class for creating custom failure classes.
* This class can be used as a base class for creating custom failure classes.
* With this class you can set message and metadata, with messages and extending
* you can create your failure messages hierarchy and automatically by syncing langKey
* with the hirerarchy of failure messages.
* For example if you pass a key of `user` to the constructor of `UserCreationFailure`
* so in langKey you can have failure message `faiure.user` so automatically,
* you can show translated error message everywhere in the app.
* Also you can use this failure message to have grained control over failures.
*/
export default abstract class BaseFailure<META_DATA> {
/* ------------------------------- Attributes ------------------------------- */

View File

@ -1,4 +1,4 @@
import BaseDevFailure from "@/feature/common/failures/dev/base-dev-failure";
import BaseDevFailure from "@/feature/common/failures/dev/base-dev.failure";
/**
* Failure for needed arguments in a method but sent wrong one

View File

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

View File

@ -0,0 +1,10 @@
import BaseFailure from "@/feature/common/failures/base.failure";
/**
* This is a base class for development failures. All dev failures means we as a developer
* made a mistake in the process and we should fix it and can be used in monitoring and
* should be handled in hotfix ASAP.
*/
export default abstract class BaseDevFailure<
META_DATA,
> extends BaseFailure<META_DATA> {}

View File

@ -1,7 +1,7 @@
import BaseDevFailure from "@/feature/common/failures/dev/base-dev-failure";
import BaseDevFailure from "@/feature/common/failures/dev/base-dev.failure";
/**
* This is a failure of not having specific dependency
* This is a failure when we didn't provice specific dependency.
*/
export default class DependencyFailure<
META_DATA,

View File

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import BaseFailure from "@/feature/common/failures/base-failure";
import BaseFailure from "@/feature/common/failures/base.failure";
/**
* This method is supposed to save previous failure of TaskEither

View File

@ -1,4 +1,4 @@
import BaseFailure from "./base-failure";
import BaseFailure from "./base.failure";
/**
* Failure for HTTP response when response dosn't have base structure

View File

@ -1,7 +1,7 @@
import BaseFailure from "./base-failure";
import BaseFailure from "./base.failure";
/**
* Failure for params failure
* Failure for params failure. which means some params are missing or not valid
*/
export default class ParamsFailure<META_DATA> extends BaseFailure<META_DATA> {
/* ------------------------------- Constructor ------------------------------ */

View File

@ -1,14 +1,14 @@
import getCustomerInvoiceDi from "@/feature/core/customer-invoice/data/module/customer-invoice-di";
import { customerInvoiceModuleKey } from "@/feature/core/customer-invoice/invoice-module-key";
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 getInvoiceDi from "@/feature/core/invoice/data/module/invoice-di";
import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
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";
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> = {};

View File

@ -1,6 +1,6 @@
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 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 {

View File

@ -1,10 +1,10 @@
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 NetworkFailure from "@/feature/common/failures/network.failure";
import { formatCurrency } from "@/feature/common/feature-helpers";
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 CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice.entity";
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";

View File

@ -1,5 +1,5 @@
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.entity";
export default interface CustomerInvoiceRepo {
fetchList(): ApiTask<CustomerInvoice[]>;

View File

@ -1,11 +1,10 @@
import "server-only";
import { ApiEither } from "@/feature/common/data/api-task";
import serverDi from "@/feature/common/server-di";
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice";
import serverDi from "@/feature/common/server.di";
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice.entity";
import CustomerInvoiceRepo, {
customerInvoiceRepoKey,
} from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice-repo";
import { customerInvoiceModuleKey } from "@/feature/core/customer-invoice/invoice-module-key";
} from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice.repo";
import { customerInvoiceModuleKey } from "@/feature/core/customer-invoice/invoice.module-key";
export default function fetchCustomerInvoicesUsecase(): Promise<
ApiEither<CustomerInvoice[]>

View File

@ -1,7 +1,7 @@
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 NetworkFailure from "@/feature/common/failures/network.failure";
import { formatCurrency } from "@/feature/common/feature-helpers";
import Customer from "@/feature/core/customer/domain/entity/customer";
import CustomerRepo from "@/feature/core/customer/domain/i-repo/customer-repo";

View File

@ -1,5 +1,4 @@
import "server-only";
import serverDi from "@/feature/common/server-di";
import serverDi from "@/feature/common/server.di";
import { customerKey } from "@/feature/core/customer/customer-key";
import CustomerRepo, {
customerRepoKey,

View File

@ -1,6 +1,5 @@
import "server-only";
import { ApiEither } from "@/feature/common/data/api-task";
import serverDi from "@/feature/common/server-di";
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, {

View File

@ -1,11 +1,16 @@
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 invoiceDbRepo from "@/feature/core/invoice/data/repo/invoice-db.repo";
import { invoiceRepoKey } from "@/feature/core/invoice/domain/i-repo/invoice.i-repo";
import createInvoiceUsecase from "@/feature/core/invoice/domain/usecase/create-invoice/create-invoice-impl.usecase";
import { createInvoiceUsecaseKey } from "@/feature/core/invoice/domain/usecase/create-invoice/create-invoice.usecase";
import { DependencyContainer } from "tsyringe";
export default function getInvoiceDi(): DependencyContainer {
const invoiceDi = di.createChildContainer();
invoiceDi.register(invoiceRepoKey, invoiceDbRepo);
invoiceDi.register(createInvoiceUsecaseKey, {
useValue: createInvoiceUsecase,
});
return invoiceDi;
}

View File

@ -1,11 +1,12 @@
import "server-only";
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 NetworkFailure from "@/feature/common/failures/network.failure";
import { formatCurrency } from "@/feature/common/feature-helpers";
import InvoiceRepo from "@/feature/core/invoice/domain/i-repo/invoice-repo";
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice-param";
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
import InvoiceRepo from "@/feature/core/invoice/domain/i-repo/invoice.i-repo";
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice.param";
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status.value-object";
import { pipe } from "fp-ts/lib/function";
import { tryCatch } from "fp-ts/lib/TaskEither";
import postgres from "postgres";

View File

@ -1,6 +1,6 @@
import ApiTask from "@/feature/common/data/api-task";
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice-param";
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice.param";
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status.value-object";
export default interface InvoiceRepo {
fetchAllInvoicesAmount(): Promise<number>;

View File

@ -1,22 +1,21 @@
"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 ParamsFailure from "@/feature/common/failures/params.failure";
import serverDi from "@/feature/common/server.di";
import InvoiceRepo, {
invoiceRepoKey,
} from "@/feature/core/invoice/domain/i-repo/invoice-repo";
} from "@/feature/core/invoice/domain/i-repo/invoice.i-repo";
import {
InvoiceParam,
invoiceSchema,
} from "@/feature/core/invoice/domain/param/invoice-param";
import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
} from "@/feature/core/invoice/domain/param/invoice.param";
import { invoiceModuleKey } from "@/feature/core/invoice/invoice.module-key";
import { CreateInvoiceUsecase } from "@/feature/core/invoice/domain/usecase/create-invoice/create-invoice.usecase";
import { pipe } from "fp-ts/lib/function";
import { chain, fromNullable, left, map, right } from "fp-ts/lib/TaskEither";
export default async function createInvoiceUsecase(
const createInvoiceUsecase: CreateInvoiceUsecase = async (
params: InvoiceParam,
): Promise<ApiEither<string>> {
): Promise<ApiEither<string>> => {
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey);
return pipe(
@ -29,4 +28,6 @@ export default async function createInvoiceUsecase(
}),
chain((params) => repo.createInvoice(params)),
)();
}
};
export default createInvoiceUsecase;

View File

@ -0,0 +1,8 @@
import { ApiEither } from "@/feature/common/data/api-task";
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice.param";
export type CreateInvoiceUsecase = (
param: InvoiceParam,
) => Promise<ApiEither<string>>;
export const createInvoiceUsecaseKey = "createInvoiceUsecaseKey";

View File

@ -1,9 +1,8 @@
import "server-only";
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 { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
} from "@/feature/core/invoice/domain/i-repo/invoice.i-repo";
import { invoiceModuleKey } from "@/feature/core/invoice/invoice.module-key";
export default function fetchAllInvoicesAmountUsecase(): Promise<number> {
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey);

View File

@ -1,10 +1,9 @@
import "server-only";
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 InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
} from "@/feature/core/invoice/domain/i-repo/invoice.i-repo";
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status.value-object";
import { invoiceModuleKey } from "@/feature/core/invoice/invoice.module-key";
export default function fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary> {
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey);

View File

@ -1,6 +1,6 @@
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";
import RevenueDbRepo from "@/feature/core/revenue/data/repo/revenue-db.repo";
import { revenueRepoKey } from "@/feature/core/revenue/domain/i-repo/revenue.i-repo";
export default function getRevenueDi() {
const revenueDi = di.createChildContainer();

View File

@ -1,6 +1,6 @@
import { sql } from "@/bootstrap/boundaries/db/db";
import Revenue from "@/feature/core/revenue/domain/entity/revenue";
import RevenueRepo from "@/feature/core/revenue/domain/i-repo/revenue-repo";
import Revenue from "@/feature/core/revenue/domain/entity/revenue.entity";
import RevenueRepo from "@/feature/core/revenue/domain/i-repo/revenue.i-repo";
import postgres from "postgres";
export type RevenueDbResponse = {

View File

@ -1,4 +1,4 @@
import Revenue from "@/feature/core/revenue/domain/entity/revenue";
import Revenue from "@/feature/core/revenue/domain/entity/revenue.entity";
export default interface RevenueRepo {
fetchRevenues(): Promise<Revenue[]>;

View File

@ -1,10 +1,9 @@
import "server-only";
import serverDi from "@/feature/common/server-di";
import Revenue from "@/feature/core/revenue/domain/entity/revenue";
import serverDi from "@/feature/common/server.di";
import Revenue from "@/feature/core/revenue/domain/entity/revenue.entity";
import RevenueRepo, {
revenueRepoKey,
} from "@/feature/core/revenue/domain/i-repo/revenue-repo";
import { revenueModuleKey } from "@/feature/core/revenue/domain/revenue-module-key";
} from "@/feature/core/revenue/domain/i-repo/revenue.i-repo";
import { revenueModuleKey } from "@/feature/core/revenue/domain/revenue.module-key";
export default function fetchRevenuesUsecase(): Promise<Revenue[]> {
const repo = serverDi(revenueModuleKey).resolve<RevenueRepo>(revenueRepoKey);

View File

@ -1,6 +1,6 @@
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 fetchAllInvoicesAmountUsecase from "@/feature/core/invoice/domain/usecase/fetch-all-invoices-amount.usecase";
import fetchInvoicesStatusSummary from "@/feature/core/invoice/domain/usecase/fetch-invoices-status-summary.usecase";
import di from "@/bootstrap/di/init-di";
export default function getSummaryInfoDi() {

View File

@ -1,10 +1,9 @@
import "server-only";
import serverDi from "@/feature/common/server-di";
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 fetchAllInvoicesAmountUsecase from "@/feature/core/invoice/domain/usecase/fetch-all-invoices-amount.usecase";
import fetchInvoicesStatusSummary from "@/feature/core/invoice/domain/usecase/fetch-invoices-status-summary.usecase";
import { summaryInfoModuleKey } from "@/feature/core/summary-info/domain/summary-info.module-key";
import SummaryInfo from "@/feature/core/summary-info/domain/value-object/summary-info.value-object";
export default async function fetchSummaryInfoUsecase(): Promise<SummaryInfo> {
try {

View File

@ -1,4 +1,4 @@
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status.value-object";
export default class SummaryInfo {
customersNumber: number;

View File

@ -1,6 +1,9 @@
import di from "@/bootstrap/di/init-di";
import * as serverDi from "@/feature/common/server-di";
import * as serverDi from "@/feature/common/server.di";
/**
* To mock and get server di
*/
export default function mockDi() {
vi.spyOn(serverDi, "default").mockReturnValue(di);
return di;

View File

@ -1,5 +1,8 @@
import { Mock } from "moq.ts";
/**
* To get mock object to mock objects and classes
*/
export function getMock<T>() {
return new Mock<T>();
}

View File

@ -4,7 +4,7 @@ import CustomerRepo, {
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 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";
import { right } from "fp-ts/lib/TaskEither";