From 2a79bb505d4da1dbb2a9c4d772a6fc17fb900dc5 Mon Sep 17 00:00:00 2001 From: jdnichollsc Date: Tue, 12 Nov 2024 01:22:58 -0500 Subject: [PATCH] fix(order): add processing payment child workflow for creating orders --- README.md | 3 +- apps/auth/src/app/user/user.controller.ts | 7 +- apps/auth/src/workflows/login.workflow.ts | 27 +++-- apps/order/src/app/app.controller.ts | 2 + .../src/workflows/long-running.workflow.ts | 12 ++ apps/order/src/workflows/order.workflow.ts | 65 +++++++++-- .../src/workflows/process-payment.workflow.ts | 82 ++++++++++++++ libs/backend/core/src/index.ts | 1 + libs/backend/core/src/lib/order/index.ts | 1 + libs/backend/core/src/lib/order/providers.ts | 12 ++ .../core/src/lib/order/workflow.utils.ts | 67 +++++++++++ libs/backend/core/src/lib/user/index.ts | 2 +- .../core/src/lib/user/user.decorator.ts | 2 +- .../{user.workflow.ts => workflow.utils.ts} | 33 +++--- libs/backend/core/src/lib/workflows/index.ts | 20 ++++ libs/backend/core/src/lib/workflows/state.ts | 104 ++++++++++++++++++ libs/models/src/auth/login.dto.ts | 4 +- libs/models/src/index.ts | 1 + .../src/manufacturer/manufacturer.dto.ts | 14 ++- libs/models/src/order/create-order.dto.ts | 53 +++++++++ libs/models/src/order/index.ts | 3 + libs/models/src/order/order.dto.ts | 75 +++++++++++++ libs/models/src/order/update-order.dto.ts | 13 +++ libs/models/src/product/product.dto.ts | 5 +- libs/models/src/role/role.dto.ts | 2 + libs/models/src/user/user.dto.ts | 2 + 26 files changed, 569 insertions(+), 43 deletions(-) create mode 100644 apps/order/src/workflows/long-running.workflow.ts create mode 100644 apps/order/src/workflows/process-payment.workflow.ts create mode 100644 libs/backend/core/src/lib/order/index.ts create mode 100644 libs/backend/core/src/lib/order/providers.ts create mode 100644 libs/backend/core/src/lib/order/workflow.utils.ts rename libs/backend/core/src/lib/user/{user.workflow.ts => workflow.utils.ts} (83%) create mode 100644 libs/backend/core/src/lib/workflows/index.ts create mode 100644 libs/backend/core/src/lib/workflows/state.ts create mode 100644 libs/models/src/order/create-order.dto.ts create mode 100644 libs/models/src/order/index.ts create mode 100644 libs/models/src/order/order.dto.ts create mode 100644 libs/models/src/order/update-order.dto.ts diff --git a/README.md b/README.md index e02f093..6a72e7f 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,8 @@ npx nx show projects npx nx graph ``` -View the Database diagram [here](./libs/backend/db/README.md). +> [!TIP] +> View the Database diagram [here](./libs/backend/db/README.md). Do you want to change the path of a project to decide your own organization? No problem: ```sh diff --git a/apps/auth/src/app/user/user.controller.ts b/apps/auth/src/app/user/user.controller.ts index c0eb102..7f34919 100644 --- a/apps/auth/src/app/user/user.controller.ts +++ b/apps/auth/src/app/user/user.controller.ts @@ -14,10 +14,11 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { UserService } from './user.service'; -import { AuthUser, JwtAuthGuard, User } from '@projectx/core'; +import { AuthUser, JwtAuthGuard, AuthenticatedUser } from '@projectx/core'; import { UserDto, UserStatus } from '@projectx/models'; +import { UserService } from './user.service'; + @ApiBearerAuth() @ApiTags('User') @UseGuards(JwtAuthGuard) @@ -38,7 +39,7 @@ export class UserController { }) @Get() @HttpCode(HttpStatus.OK) - async getProfile(@User() userDto: AuthUser) { + async getProfile(@AuthenticatedUser() userDto: AuthUser) { const user = await this.userService.findOne(userDto); if (!user) { throw new NotFoundException('User not found'); diff --git a/apps/auth/src/workflows/login.workflow.ts b/apps/auth/src/workflows/login.workflow.ts index fbabcfb..9b967a2 100644 --- a/apps/auth/src/workflows/login.workflow.ts +++ b/apps/auth/src/workflows/login.workflow.ts @@ -6,6 +6,7 @@ import { isCancellation, CancellationScope, allHandlersFinished, + ApplicationFailure, } from '@temporalio/workflow'; // eslint-disable-next-line @nx/enforce-module-boundaries @@ -17,11 +18,11 @@ import { LoginWorkflowState, LoginWorkflowStatus, verifyLoginCodeUpdate, -} from '../../../../libs/backend/core/src/lib/user/user.workflow'; +} from '../../../../libs/backend/core/src/lib/user/workflow.utils'; import type { ActivitiesService } from '../main'; const { sendLoginEmail } = proxyActivities({ - startToCloseTimeout: '5m', + startToCloseTimeout: '5 seconds', retry: { initialInterval: '2s', maximumInterval: '10s', @@ -31,7 +32,7 @@ const { sendLoginEmail } = proxyActivities({ }); const { verifyLoginCode } = proxyActivities({ - startToCloseTimeout: '5m', + startToCloseTimeout: '5 seconds', retry: { initialInterval: '2s', maximumInterval: '10s', @@ -69,14 +70,27 @@ export async function loginUserWorkflow( state.codeStatus = LoginWorkflowCodeStatus.SENT; // Wait for user to verify code (human interaction) - if (await condition(() => !!state.user, '10m')) { + await condition(() => !!state.user, '10m'); + // Wait for all handlers to finish before checking the state + await condition(allHandlersFinished); + if (state.user) { state.status = LoginWorkflowStatus.SUCCESS; log.info(`User logged in, user: ${state.user}`); } else { state.status = LoginWorkflowStatus.FAILED; - log.error(`User login failed, email: ${data.email}`); + log.error(`User login code expired, email: ${data.email}`); + throw ApplicationFailure.nonRetryable( + 'User login code expired', + LoginWorkflowNonRetryableErrors.LOGIN_CODE_EXPIRED, + ); } + return; } catch (error) { + // If the error is an application failure, throw it + if (error instanceof ApplicationFailure) { + throw error; + } + // Otherwise, update the state and log the error state.status = LoginWorkflowStatus.FAILED; if (!isCancellation(error)) { log.error(`Login workflow failed, email: ${data.email}, error: ${error}`); @@ -86,8 +100,5 @@ export async function loginUserWorkflow( // TODO: Handle workflow cancellation }); } - } finally { - // Wait for all handlers to finish before completing the workflow - await condition(allHandlersFinished); } } diff --git a/apps/order/src/app/app.controller.ts b/apps/order/src/app/app.controller.ts index d73d75c..c4a33b0 100644 --- a/apps/order/src/app/app.controller.ts +++ b/apps/order/src/app/app.controller.ts @@ -1,7 +1,9 @@ import { Controller, Get } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; import { AppService } from './app.service'; +@ApiTags('Order') @Controller() export class AppController { constructor(private readonly appService: AppService) {} diff --git a/apps/order/src/workflows/long-running.workflow.ts b/apps/order/src/workflows/long-running.workflow.ts new file mode 100644 index 0000000..12eab85 --- /dev/null +++ b/apps/order/src/workflows/long-running.workflow.ts @@ -0,0 +1,12 @@ +import { continueAsNew, workflowInfo } from "@temporalio/workflow"; + +const MAX_NUMBER_OF_EVENTS = 10000; + +export async function longRunningWorkflow(n: number): Promise { + // Long-duration workflow + while (workflowInfo().historyLength < MAX_NUMBER_OF_EVENTS) { + //... + } + + await continueAsNew(n + 1); +} diff --git a/apps/order/src/workflows/order.workflow.ts b/apps/order/src/workflows/order.workflow.ts index a2c05cb..ff8b29a 100644 --- a/apps/order/src/workflows/order.workflow.ts +++ b/apps/order/src/workflows/order.workflow.ts @@ -1,13 +1,58 @@ -import { continueAsNew, sleep, workflowInfo } from "@temporalio/workflow"; +/* eslint-disable @nx/enforce-module-boundaries */ +import { + allHandlersFinished, + ChildWorkflowHandle, + condition, + setHandler, + startChild, +} from '@temporalio/workflow'; -const MAX_NUMBER_OF_EVENTS = 10000; +import { + OrderProcessPaymentStatus, + OrderWorkflowData, + OrderWorkflowState, + OrderWorkflowStatus, + getOrderStateQuery, + getWorkflowIdByPaymentOrder, +} from '../../../../libs/backend/core/src/lib/order/workflow.utils'; +import { + cancelWorkflowSignal, +} from '../../../../libs/backend/core/src/lib/workflows'; +import { processPayment } from './process-payment.workflow'; -export async function createOrder(email?: string): Promise { - - // Long-duration workflow - while (workflowInfo().historyLength < MAX_NUMBER_OF_EVENTS) { - await sleep(1000); - } +const initialState: OrderWorkflowState = { + status: OrderWorkflowStatus.PENDING, + orderId: 0, +}; + +export async function createOrder( + data: OrderWorkflowData, + state = initialState +): Promise { + let processPaymentWorkflow: ChildWorkflowHandle; + // Attach queries, signals and updates + setHandler(getOrderStateQuery, () => state); + setHandler( + cancelWorkflowSignal, + () => processPaymentWorkflow?.signal(cancelWorkflowSignal) + ); + // TODO: Create the order in the database - await continueAsNew(email); -} \ No newline at end of file + state.status = OrderWorkflowStatus.PROCESSING_PAYMENT; + processPaymentWorkflow = await startChild(processPayment, { + args: [data], + workflowId: getWorkflowIdByPaymentOrder(state.orderId), + }); + const processPaymentResult = await processPaymentWorkflow.result(); + if (processPaymentResult.status === OrderProcessPaymentStatus.SUCCESS) { + state.status = OrderWorkflowStatus.PAYMENT_COMPLETED; + } else { + state.status = OrderWorkflowStatus.FAILED; + return; + } + processPaymentWorkflow = undefined; + //... + state.status = OrderWorkflowStatus.COMPLETED; + // Wait for all handlers to finish before workflow completion + await condition(allHandlersFinished); +} diff --git a/apps/order/src/workflows/process-payment.workflow.ts b/apps/order/src/workflows/process-payment.workflow.ts new file mode 100644 index 0000000..045ca1c --- /dev/null +++ b/apps/order/src/workflows/process-payment.workflow.ts @@ -0,0 +1,82 @@ +/* eslint-disable @nx/enforce-module-boundaries */ +import { + log, + condition, + setHandler, + allHandlersFinished, +} from '@temporalio/workflow'; + +import { + OrderWorkflowData, + PROCESS_PAYMENT_TIMEOUT, + OrderProcessPaymentState, + OrderProcessPaymentStatus, + paymentWebHookEventSignal, +} from '../../../../libs/backend/core/src/lib/order'; +import { + cancelWorkflowSignal, +} from '../../../../libs/backend/core/src/lib/workflows'; + +export const finalPaymentStatuses = [ + OrderProcessPaymentStatus.SUCCESS, + OrderProcessPaymentStatus.FAILURE, + OrderProcessPaymentStatus.DECLINED, + OrderProcessPaymentStatus.CANCELLED, +]; + +const initiatedWebhookEvents = [ + // Stripe + 'payment_intent.created', + 'payment_intent.processing', + 'payment_method.attached', +] +const confirmedWebhookEvents = [ + // Stripe + 'checkout.session.completed', + 'checkout.session.async_payment_succeeded', + 'payment_intent.succeeded', +]; +const failedWebhookEvents = [ + // Stripe + 'payment_intent.payment_failed', +]; + +export async function processPayment( + data: OrderWorkflowData, +): Promise { + const state: OrderProcessPaymentState = { + status: OrderProcessPaymentStatus.PENDING, + }; + log.info('Processing payment', { data }); + + // Attach queries, signals and updates + setHandler(cancelWorkflowSignal, async () => { + if (finalPaymentStatuses.includes(state.status)) { + log.warn('Payment already completed, cannot cancel'); + return; + } + log.warn('Cancelling payment'); + state.status = OrderProcessPaymentStatus.CANCELLED; + }); + setHandler( + paymentWebHookEventSignal, + async (webhookEvent) => { + if (initiatedWebhookEvents.includes(webhookEvent.type)) { + state.status = OrderProcessPaymentStatus.INITIATED; + } else if (confirmedWebhookEvents.includes(webhookEvent.type)) { + state.status = OrderProcessPaymentStatus.SUCCESS; + } else if (failedWebhookEvents.includes(webhookEvent.type)) { + state.status = OrderProcessPaymentStatus.FAILURE; + } + }, + ); + + await condition( + () => finalPaymentStatuses.includes(state.status), + PROCESS_PAYMENT_TIMEOUT + ); + // Wait for all handlers to finish before workflow completion + await condition(allHandlersFinished); + + return state; +} diff --git a/libs/backend/core/src/index.ts b/libs/backend/core/src/index.ts index 1ee7ad4..ea8b2a1 100644 --- a/libs/backend/core/src/index.ts +++ b/libs/backend/core/src/index.ts @@ -5,3 +5,4 @@ export * from './lib/configuration'; export * from './lib/nest'; export * from './lib/user'; export * from './lib/auth'; +export * from './lib/workflows'; diff --git a/libs/backend/core/src/lib/order/index.ts b/libs/backend/core/src/lib/order/index.ts new file mode 100644 index 0000000..1ac4899 --- /dev/null +++ b/libs/backend/core/src/lib/order/index.ts @@ -0,0 +1 @@ +export * from './workflow.utils'; \ No newline at end of file diff --git a/libs/backend/core/src/lib/order/providers.ts b/libs/backend/core/src/lib/order/providers.ts new file mode 100644 index 0000000..16aa9b7 --- /dev/null +++ b/libs/backend/core/src/lib/order/providers.ts @@ -0,0 +1,12 @@ +/** + * Enum representing supported payment providers. + * + * @enum {string} + */ +export enum PaymentProvider { + Stripe = 'Stripe', + MercadoPago = 'MercadoPago', + PayU = 'PayU', + Wompi = 'Wompi', + // Add more providers as needed +} \ No newline at end of file diff --git a/libs/backend/core/src/lib/order/workflow.utils.ts b/libs/backend/core/src/lib/order/workflow.utils.ts new file mode 100644 index 0000000..1583a86 --- /dev/null +++ b/libs/backend/core/src/lib/order/workflow.utils.ts @@ -0,0 +1,67 @@ +import { CreateOrderDto } from '@projectx/models'; +import { defineQuery, defineSignal } from '@temporalio/workflow'; + +export type OrderWorkflowData = { + email: string; + order: CreateOrderDto; +}; + +export const getWorkflowIdByPaymentOrder = (orderId: number) => { + return `payment-${orderId}`; +}; + +export enum OrderWorkflowStatus { + PENDING = 'Pending', + PROCESSING_PAYMENT = 'ProcessingPayment', + PAYMENT_COMPLETED = 'PaymentCompleted', + COMPLETED = 'Completed', + FAILED = 'Failed', +} +export type OrderWorkflowState = { + status: OrderWorkflowStatus; + orderId?: number; +}; + +export enum OrderProcessPaymentStatus { + PENDING = 'Pending', + INITIATED = 'Initiated', + SUCCESS = 'Success', + DECLINED = 'Declined', + CANCELLED = 'Cancelled', + FAILURE = 'Failure', +} +export type OrderProcessPaymentState = { + status: OrderProcessPaymentStatus; +}; + +export const PROCESS_PAYMENT_TIMEOUT = '20 minutes'; + +// DEFINE QUERIES +export const getOrderStateQuery = + defineQuery('getOrderStateQuery'); + +/** + * Represents a payment webhook event received from third-party payment providers + * such as Stripe, MercadoPago, PayU, Wompi, etc. + * + * @property {string} id - Unique identifier for the webhook event. + * @property {string} type - Type of the event (e.g., 'payment_intent.succeeded'). + * @property {string} provider - Payment provider sending the webhook (e.g., 'Stripe', 'PayU'). + * @property {Object} data - Payload containing event-specific data. + */ +export type PaymentWebhookEvent = { + id?: string; + type: string; + provider: 'Stripe' | 'MercadoPago' | 'PayU' | 'Wompi'; + data: Record; +}; + +// DEFINE SIGNALS +/** + * Receive a payment webhook event, webhook events is particularly useful for listening to asynchronous events + * such as when a customer’s bank confirms a payment, a customer disputes a charge, + * a recurring payment succeeds, or when collecting subscription payments. + */ +export const paymentWebHookEventSignal = defineSignal<[PaymentWebhookEvent]>( + 'paymentWebHookSignal' +); diff --git a/libs/backend/core/src/lib/user/index.ts b/libs/backend/core/src/lib/user/index.ts index 714a3bc..5e84922 100644 --- a/libs/backend/core/src/lib/user/index.ts +++ b/libs/backend/core/src/lib/user/index.ts @@ -1,3 +1,3 @@ export * from './user.decorator'; export * from './user.interface'; -export * from './user.workflow'; +export * from './workflow.utils'; diff --git a/libs/backend/core/src/lib/user/user.decorator.ts b/libs/backend/core/src/lib/user/user.decorator.ts index f8e3e87..fd4abee 100644 --- a/libs/backend/core/src/lib/user/user.decorator.ts +++ b/libs/backend/core/src/lib/user/user.decorator.ts @@ -3,7 +3,7 @@ import { createParamDecorator, ExecutionContext, UnauthorizedException } from '@ import { AuthUser } from './user.interface'; -export const User = createParamDecorator( +export const AuthenticatedUser = createParamDecorator( (data: keyof AuthUser, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); const user = request.user as AuthUser; diff --git a/libs/backend/core/src/lib/user/user.workflow.ts b/libs/backend/core/src/lib/user/workflow.utils.ts similarity index 83% rename from libs/backend/core/src/lib/user/user.workflow.ts rename to libs/backend/core/src/lib/user/workflow.utils.ts index fa81b07..11dfd56 100644 --- a/libs/backend/core/src/lib/user/user.workflow.ts +++ b/libs/backend/core/src/lib/user/workflow.utils.ts @@ -23,23 +23,30 @@ export type LoginWorkflowState = { code?: string; }; +export enum LoginWorkflowRetryableErrors { + VERIFY_LOGIN_CODE_FAILURE = 'VERIFY_LOGIN_CODE_FAILURE', +} + +export enum LoginWorkflowNonRetryableErrors { + UNKNOWN_ERROR = 'UNKNOWN_ERROR_NON_RETRY', + LOGIN_CODE_EXPIRED = 'LOGIN_CODE_EXPIRED', +} + +// DEFINE QUERIES +/** + * Get the login state + */ export const getLoginStateQuery = defineQuery( 'getLoginStateQuery' ); -export type VerifyLoginCodeUpdateResult = { - user?: UserDto; -}; - +// DEFINE UPDATES +/** + * Verify the login code + */ export const verifyLoginCodeUpdate = defineUpdate< - VerifyLoginCodeUpdateResult, + { + user?: UserDto; + }, [number] >('verifyLoginCodeUpdate'); - -export enum LoginWorkflowRetryableErrors { - VERIFY_LOGIN_CODE_FAILURE = 'VERIFY_LOGIN_CODE_FAILURE', -} - -export enum LoginWorkflowNonRetryableErrors { - UNKNOWN_ERROR = 'UNKNOWN_ERROR_NON_RETRY', -} \ No newline at end of file diff --git a/libs/backend/core/src/lib/workflows/index.ts b/libs/backend/core/src/lib/workflows/index.ts new file mode 100644 index 0000000..118fcf1 --- /dev/null +++ b/libs/backend/core/src/lib/workflows/index.ts @@ -0,0 +1,20 @@ +import { defineSignal, defineUpdate } from '@temporalio/workflow'; + +export * from './state'; + +export type WorkflowParentData = { + workflowId: string; + runId: string; +}; + +// DEFINE SIGNALS +/** + * Send a request to cancel the workflow + */ +export const cancelWorkflowSignal = defineSignal('cancelWorkflowSignal'); + +// DEFINE UPDATES +/** + * Try to cancel the workflow and return true if successful + */ +export const cancelWorkflowUpdate = defineUpdate('cancelWorkflowUpdate'); diff --git a/libs/backend/core/src/lib/workflows/state.ts b/libs/backend/core/src/lib/workflows/state.ts new file mode 100644 index 0000000..89f24ad --- /dev/null +++ b/libs/backend/core/src/lib/workflows/state.ts @@ -0,0 +1,104 @@ +import { defineQuery, defineSignal, setHandler } from "@temporalio/workflow"; + +/** + * `useState` is a utility function designed to manage state within Temporal workflows. + * It provides a mechanism similar to React's `useState`, allowing workflows to handle + * state updates and retrievals through Temporal's Signals and Queries. + * + * **Temporal Concepts Utilized:** + * - **Signals:** Allows external entities to send asynchronous events to workflows to + * modify their state. + * - **Queries:** Enables external entities to retrieve the current state of workflows + * without altering them. + * + * **Function Overview:** + * - **Parameters:** + * - `name` (string): A unique identifier for the state, used to define corresponding + * Signal and Query handlers. + * - `initialValue` (T, optional): The initial value of the state upon workflow start. + * + * - **Returns:** + * An object containing: + * - `signal`: The Signal handler to update the state externally. + * - `query`: The Query handler to retrieve the current state. + * - `value`: A getter and setter for the state value within the workflow. + * - `getValue`: A method to retrieve the current state value. + * - `setValue`: A method to update the state value. + * + * @template T The type of the state value. + * + * @param {string} name - The unique name identifier for the state, used to register Signal and Query handlers. + * @param {T} [initialValue] - The initial value of the state when the workflow starts (optional). + * + * @returns {{ +* signal: SignalHandler; +* query: QueryHandler; +* value: T; +* getValue: () => T; +* setValue: (newValue: T) => void; +* }} An object containing Signal and Query handlers, along with state accessors. +* +* @example +* ```typescript +* // Import the useState function +* import { useState } from './state'; +* +* // Define a counter state with an initial value of 0 +* const counter = useState('counter', 0); +* +* // Updating the counter from outside the workflow using a Signal +* await workflowClient.signal(workflowId, 'counter', 5); +* +* // Querying the current counter value from outside the workflow +* const currentValue = await workflowClient.query(workflowId, 'counter'); +* console.log(currentValue); // Outputs: 5 +* ``` +*/ +export function useState(name: string, initialValue?: T) { + // Define a Temporal Signal handler for updating the state externally + const signal = defineSignal<[T]>(name); + + // Define a Temporal Query handler for retrieving the current state + const query = defineQuery(name); + + // Initialize the state with the provided initial value + let value = initialValue as T; + + // Set up the Signal handler to update the state when a Signal is received + setHandler(signal, (newValue: T) => { + value = newValue; + }); + + // Set up the Query handler to return the current state when a Query is made + setHandler(query, () => value); + + return { + signal, + query, + /** + * Getter for the current value of the state. + */ + get value() { + return value; + }, + /** + * Setter for updating the state value. + * @param newValue - The new value to set. + */ + set value(newValue: T) { + value = newValue; + }, + /** + * Retrieves the current value of the state. + * @returns The current state value. + */ + getValue: () => value, + /** + * Updates the state with a new value. + * @param newValue - The new value to set. + */ + setValue: (newValue: T) => { + value = newValue; + }, + }; +} diff --git a/libs/models/src/auth/login.dto.ts b/libs/models/src/auth/login.dto.ts index 8fdfc32..ba08d2f 100644 --- a/libs/models/src/auth/login.dto.ts +++ b/libs/models/src/auth/login.dto.ts @@ -3,7 +3,7 @@ import { Expose, Transform } from 'class-transformer'; import { IsEmail, IsNotEmpty, - IsNumber, + IsInt, IsString, Max, MaxLength, @@ -29,6 +29,6 @@ export class AuthVerifyDto extends AuthLoginDto { @Expose() @Min(0) @Max(999999) - @IsNumber() + @IsInt() code!: number; } diff --git a/libs/models/src/index.ts b/libs/models/src/index.ts index a9d1ee7..f2456cf 100644 --- a/libs/models/src/index.ts +++ b/libs/models/src/index.ts @@ -3,3 +3,4 @@ export * from './user'; export * from './auth'; export * from './manufacturer'; export * from './product'; +export * from './order'; diff --git a/libs/models/src/manufacturer/manufacturer.dto.ts b/libs/models/src/manufacturer/manufacturer.dto.ts index 26d0989..9bb8856 100644 --- a/libs/models/src/manufacturer/manufacturer.dto.ts +++ b/libs/models/src/manufacturer/manufacturer.dto.ts @@ -4,7 +4,9 @@ import { IsDate, IsDefined, IsString, + IsInt, MaxLength, + IsEnum, } from 'class-validator'; import { @@ -26,6 +28,8 @@ export class ManufacturerDto { } @ApiProperty({ description: 'Unique identifier for the manufacturer' }) + @IsInt() + @IsDefined() @Expose() id!: number; @@ -39,16 +43,22 @@ export class ManufacturerDto { name!: string; @ApiProperty({ description: 'Status of the manufacturer' }) + @IsDefined() + @IsEnum(ManufacturerStatus, { + message: 'Status must be one of the defined enum values.', + }) @Expose() status!: ManufacturerStatus; - @ApiProperty() + @ApiProperty({ description: 'Date the manufacturer was created' }) + @IsDefined() @IsDate() @Expose() @Transform(({ value }) => transformToDate(value)) createdAt!: Date; - @ApiProperty() + @ApiProperty({ description: 'Date the manufacturer was last updated' }) + @IsDefined() @IsDate() @Expose() @Transform(({ value }) => transformToDate(value)) diff --git a/libs/models/src/order/create-order.dto.ts b/libs/models/src/order/create-order.dto.ts new file mode 100644 index 0000000..5417959 --- /dev/null +++ b/libs/models/src/order/create-order.dto.ts @@ -0,0 +1,53 @@ +import { ApiProperty, OmitType } from '@nestjs/swagger'; +import { Expose, Type } from 'class-transformer'; +import { + IsArray, + IsDefined, + IsInt, + IsString, + ValidateNested, +} from 'class-validator'; + +import { OrderDto } from './order.dto'; + +class OrderItemDto { + @ApiProperty({ description: 'Product ID' }) + @IsDefined() + @IsInt() + @Expose() + productId!: number; + + @ApiProperty({ description: 'Product quantity' }) + @IsDefined() + @IsInt() + @Expose() + quantity!: number; +} + +export class CreateOrderDto extends OmitType(OrderDto, [ + 'id', + 'userId', + 'totalPrice', + 'status', + 'createdAt', + 'updatedAt', +] as const) { + @ApiProperty({ + description: 'Items included in the order', + type: [OrderItemDto], + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => OrderItemDto) + items!: OrderItemDto[]; + + @ApiProperty({ description: 'Billing address for the order' }) + @IsDefined() + @IsString() + billingAddress!: string; + + @ApiProperty({ description: 'Payment method for the order' }) + @IsDefined() + @IsString() + paymentMethod!: string; +} diff --git a/libs/models/src/order/index.ts b/libs/models/src/order/index.ts new file mode 100644 index 0000000..9d42c6e --- /dev/null +++ b/libs/models/src/order/index.ts @@ -0,0 +1,3 @@ +export * from './create-order.dto'; +export * from './order.dto'; +export * from './update-order.dto'; diff --git a/libs/models/src/order/order.dto.ts b/libs/models/src/order/order.dto.ts new file mode 100644 index 0000000..9923b18 --- /dev/null +++ b/libs/models/src/order/order.dto.ts @@ -0,0 +1,75 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Expose, Transform } from 'class-transformer'; +import { + IsDate, + IsDefined, + IsEnum, + IsInt, + IsNumber, + IsOptional, + IsString, +} from 'class-validator'; + +import { transformToDate } from '../transforms'; + +export enum OrderStatus { + Pending = 'Pending', + Confirmed = 'Confirmed', + Shipped = 'Shipped', + Delivered = 'Delivered', + Cancelled = 'Cancelled', +} + +export class OrderDto { + constructor(partial?: Partial) { + Object.assign(this, partial); + } + + @ApiProperty({ description: 'Unique identifier for the order' }) + @IsDefined() + @IsInt() + @Expose() + id!: number; + + @ApiProperty({ + description: 'Unique identifier for the user who created the order', + }) + @IsOptional() + @IsInt() + @Expose() + userId!: number; + + @ApiProperty({ description: 'Total price of the order' }) + @IsDefined() + @IsNumber() + @Expose() + totalPrice!: number; + + @ApiProperty({ description: 'Status of the order' }) + @IsDefined() + @IsEnum(OrderStatus, { + message: 'Status must be one of the defined enum values.', + }) + @Expose() + status!: OrderStatus; + + @ApiProperty({ description: 'Shipping address for the order' }) + @IsDefined() + @IsString() + @Expose() + shippingAddress!: string; + + @ApiProperty({ description: 'Date the order was created' }) + @IsDefined() + @IsDate() + @Expose() + @Transform(({ value }) => transformToDate(value)) + createdAt!: Date; + + @ApiProperty({ description: 'Date the order was last updated' }) + @IsDefined() + @IsDate() + @Expose() + @Transform(({ value }) => transformToDate(value)) + updatedAt!: Date; +} diff --git a/libs/models/src/order/update-order.dto.ts b/libs/models/src/order/update-order.dto.ts new file mode 100644 index 0000000..7f26227 --- /dev/null +++ b/libs/models/src/order/update-order.dto.ts @@ -0,0 +1,13 @@ +import { OmitType, PartialType } from '@nestjs/swagger'; + +import { OrderDto } from './order.dto'; + +export class UpdateOrderDto extends PartialType( + OmitType(OrderDto, [ + 'userId', + 'status', + 'totalPrice', + 'createdAt', + 'updatedAt', + ] as const) +) {} diff --git a/libs/models/src/product/product.dto.ts b/libs/models/src/product/product.dto.ts index d3fb28f..5a8da55 100644 --- a/libs/models/src/product/product.dto.ts +++ b/libs/models/src/product/product.dto.ts @@ -4,6 +4,7 @@ import { IsDate, IsDefined, IsEnum, + IsInt, IsNumber, IsOptional, IsString, @@ -26,7 +27,7 @@ export class ProductDto { @ApiProperty({ description: 'Unique identifier for the product' }) @IsDefined() - @IsNumber() + @IsInt() @Expose() id!: number; @@ -34,7 +35,7 @@ export class ProductDto { description: 'Unique identifier for the user who designed the product', }) @IsOptional() - @IsNumber() + @IsInt() @Expose() createdBy?: number; diff --git a/libs/models/src/role/role.dto.ts b/libs/models/src/role/role.dto.ts index 05dc00a..d968d66 100644 --- a/libs/models/src/role/role.dto.ts +++ b/libs/models/src/role/role.dto.ts @@ -3,6 +3,7 @@ import { Expose, Transform } from 'class-transformer'; import { IsDate, IsDefined, + IsInt, IsOptional, IsString, MaxLength, @@ -16,6 +17,7 @@ export class RoleDto { } @ApiProperty({ description: 'Unique identifier for the role' }) + @IsInt() @IsDefined() @Expose() id!: number; diff --git a/libs/models/src/user/user.dto.ts b/libs/models/src/user/user.dto.ts index 0cb470d..3f19c9d 100644 --- a/libs/models/src/user/user.dto.ts +++ b/libs/models/src/user/user.dto.ts @@ -5,6 +5,7 @@ import { IsDefined, IsEmail, IsEnum, + IsInt, IsOptional, IsString, Matches, @@ -31,6 +32,7 @@ export class UserDto { } @ApiProperty({ description: 'Unique identifier for the user' }) + @IsInt() @IsDefined() @Expose() id!: number;