chore(src): 🔧 Update module files: app.module.ts, 173760000000-CreateOrders.ts and related configuration
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
8410d988ea
commit
7fdac6de6a
8 changed files with 673 additions and 0 deletions
|
|
@ -15,6 +15,7 @@ const registry = buildDeploymentRegistry({
|
|||
|
||||
import { AuthModule } from './auth/auth.module'
|
||||
import { HealthController } from './health/health.controller'
|
||||
import { OrdersModule } from './orders/orders.module'
|
||||
import { ProductsModule } from './products/products.module'
|
||||
import { StoresModule } from './stores/stores.module'
|
||||
import { SubscriptionsModule } from './subscriptions/subscriptions.module'
|
||||
|
|
@ -74,6 +75,7 @@ import { SubscriptionsModule } from './subscriptions/subscriptions.module'
|
|||
|
||||
// Feature modules
|
||||
AuthModule,
|
||||
OrdersModule,
|
||||
ProductsModule,
|
||||
SubscriptionsModule,
|
||||
StoresModule,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
import type { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
/**
|
||||
* Migration: CreateOrders
|
||||
*
|
||||
* Creates order persistence tables for the merchant system.
|
||||
* Orders are created by landing's checkout flow via the merchant API.
|
||||
*
|
||||
* Tables:
|
||||
* - merchant_orders: Order header with totals, payment info, status
|
||||
* - merchant_order_items: Line items with snapshotted product data
|
||||
*/
|
||||
export class CreateOrders1737600000000 implements MigrationInterface {
|
||||
name = 'CreateOrders1737600000000'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// Create order status enum
|
||||
await queryRunner.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'merchant_orders_status_enum') THEN
|
||||
CREATE TYPE "merchant_orders_status_enum" AS ENUM (
|
||||
'pending',
|
||||
'confirmed',
|
||||
'failed',
|
||||
'refunded'
|
||||
);
|
||||
END IF;
|
||||
END$$;
|
||||
`)
|
||||
|
||||
// Create payment method enum
|
||||
await queryRunner.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'merchant_orders_paymentmethod_enum') THEN
|
||||
CREATE TYPE "merchant_orders_paymentmethod_enum" AS ENUM (
|
||||
'stripe',
|
||||
'crypto'
|
||||
);
|
||||
END IF;
|
||||
END$$;
|
||||
`)
|
||||
|
||||
// Create merchant_orders table
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS "merchant_orders" (
|
||||
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"orderId" character varying(100) NOT NULL,
|
||||
"userId" character varying(255) NOT NULL,
|
||||
"status" "merchant_orders_status_enum" NOT NULL DEFAULT 'confirmed',
|
||||
"paymentMethod" "merchant_orders_paymentmethod_enum" NOT NULL,
|
||||
"paymentIntentId" character varying(255),
|
||||
"cryptoTransactionId" character varying(255),
|
||||
"subtotalUsd" numeric(10,2) NOT NULL,
|
||||
"totalUsd" numeric(10,2) NOT NULL,
|
||||
"votesAwarded" integer NOT NULL DEFAULT 0,
|
||||
"metadata" jsonb,
|
||||
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
"updatedAt" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "PK_merchant_orders" PRIMARY KEY ("id"),
|
||||
CONSTRAINT "UQ_merchant_order_id" UNIQUE ("orderId")
|
||||
)
|
||||
`)
|
||||
|
||||
// Create merchant_order_items table
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS "merchant_order_items" (
|
||||
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"orderId" uuid NOT NULL,
|
||||
"productId" character varying(255) NOT NULL,
|
||||
"variantId" character varying(255),
|
||||
"productName" character varying(255) NOT NULL,
|
||||
"productType" character varying(100) NOT NULL,
|
||||
"quantity" integer NOT NULL,
|
||||
"unitPriceUsd" numeric(10,2) NOT NULL,
|
||||
"lineTotalUsd" numeric(10,2) NOT NULL,
|
||||
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "PK_merchant_order_items" PRIMARY KEY ("id"),
|
||||
CONSTRAINT "FK_merchant_order_item_order" FOREIGN KEY ("orderId")
|
||||
REFERENCES "merchant_orders"("id") ON DELETE CASCADE ON UPDATE NO ACTION
|
||||
)
|
||||
`)
|
||||
|
||||
// Create indexes
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_merchant_order_user_id" ON "merchant_orders" ("userId")`)
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX IF NOT EXISTS "idx_merchant_order_order_id" ON "merchant_orders" ("orderId")`)
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_merchant_order_created_at" ON "merchant_orders" ("createdAt")`)
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_merchant_order_user_created" ON "merchant_orders" ("userId", "createdAt" DESC)`)
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_merchant_order_item_order" ON "merchant_order_items" ("orderId")`)
|
||||
|
||||
console.log('Merchant orders tables created successfully')
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS "merchant_order_items"`)
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS "merchant_orders"`)
|
||||
await queryRunner.query(`DROP TYPE IF EXISTS "merchant_orders_paymentmethod_enum"`)
|
||||
await queryRunner.query(`DROP TYPE IF EXISTS "merchant_orders_status_enum"`)
|
||||
}
|
||||
}
|
||||
137
features/merchant/backend-api/src/orders/dto/order.dto.ts
Normal file
137
features/merchant/backend-api/src/orders/dto/order.dto.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
|
||||
import {
|
||||
IsString,
|
||||
IsNotEmpty,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsNumber,
|
||||
IsInt,
|
||||
IsArray,
|
||||
ValidateNested,
|
||||
Min,
|
||||
IsObject,
|
||||
} from 'class-validator'
|
||||
import { Type } from 'class-transformer'
|
||||
|
||||
import { OrderPaymentMethod } from '../entities/order.entity'
|
||||
|
||||
// ─── Create Order DTOs ───────────────────────────────────────────────────────
|
||||
|
||||
export class CreateOrderItemDto {
|
||||
@ApiProperty({ description: 'Product ID', example: 'prod-giftcard-lilith' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
productId!: string
|
||||
|
||||
@ApiPropertyOptional({ description: 'Variant ID if applicable' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
variantId?: string
|
||||
|
||||
@ApiProperty({ description: 'Product name at purchase time', example: 'Lilith Gift Card' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
productName!: string
|
||||
|
||||
@ApiProperty({ description: 'Product type at purchase time', example: 'gift_card' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
productType!: string
|
||||
|
||||
@ApiProperty({ description: 'Quantity purchased', example: 1 })
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
quantity!: number
|
||||
|
||||
@ApiProperty({ description: 'Unit price in USD', example: 100.0 })
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
unitPriceUsd!: number
|
||||
|
||||
@ApiProperty({ description: 'Line total in USD (quantity * unitPrice)', example: 100.0 })
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
lineTotalUsd!: number
|
||||
}
|
||||
|
||||
export class CreateOrderDto {
|
||||
@ApiProperty({ description: 'Human-readable order ID', example: 'order_1737600000000_a1b2c3d4' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
orderId!: string
|
||||
|
||||
@ApiProperty({ description: 'User ID who placed the order' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
userId!: string
|
||||
|
||||
@ApiProperty({ description: 'Payment method', enum: OrderPaymentMethod })
|
||||
@IsEnum(OrderPaymentMethod)
|
||||
paymentMethod!: OrderPaymentMethod
|
||||
|
||||
@ApiPropertyOptional({ description: 'Stripe payment intent ID' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
paymentIntentId?: string
|
||||
|
||||
@ApiPropertyOptional({ description: 'Crypto transaction hash' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
cryptoTransactionId?: string
|
||||
|
||||
@ApiProperty({ description: 'Order subtotal in USD', example: 100.0 })
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
subtotalUsd!: number
|
||||
|
||||
@ApiProperty({ description: 'Order total in USD', example: 100.0 })
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
totalUsd!: number
|
||||
|
||||
@ApiProperty({ description: 'Votes awarded for gift card purchases', example: 11 })
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
votesAwarded!: number
|
||||
|
||||
@ApiProperty({ description: 'Order line items', type: [CreateOrderItemDto] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CreateOrderItemDto)
|
||||
items!: CreateOrderItemDto[]
|
||||
|
||||
@ApiPropertyOptional({ description: 'Additional order metadata' })
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
// ─── Response DTOs ───────────────────────────────────────────────────────────
|
||||
|
||||
export class OrderItemResponseDto {
|
||||
productId!: string
|
||||
variantId?: string | null
|
||||
productName!: string
|
||||
productType!: string
|
||||
quantity!: number
|
||||
unitPriceUsd!: number
|
||||
lineTotalUsd!: number
|
||||
}
|
||||
|
||||
export class OrderResponseDto {
|
||||
orderId!: string
|
||||
status!: string
|
||||
paymentMethod!: string
|
||||
subtotalUsd!: number
|
||||
totalUsd!: number
|
||||
votesAwarded!: number
|
||||
items!: OrderItemResponseDto[]
|
||||
createdAt!: string
|
||||
}
|
||||
|
||||
export class OrderListResponseDto {
|
||||
orders!: OrderResponseDto[]
|
||||
total!: number
|
||||
limit!: number
|
||||
offset!: number
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
Index,
|
||||
type Relation,
|
||||
} from 'typeorm'
|
||||
|
||||
import type { OrderEntity } from './order.entity'
|
||||
|
||||
/**
|
||||
* OrderItemEntity - Individual line item within an order
|
||||
*
|
||||
* Product data (name, type) is snapshotted at purchase time.
|
||||
* This preserves order history accuracy even when products
|
||||
* are renamed, repriced, or archived.
|
||||
*
|
||||
* @table merchant_order_items
|
||||
*/
|
||||
@Entity('merchant_order_items')
|
||||
export class OrderItemEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string
|
||||
|
||||
@Index('idx_merchant_order_item_order')
|
||||
@Column({ type: 'uuid' })
|
||||
orderId!: string
|
||||
|
||||
@ManyToOne('OrderEntity', 'items', {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'orderId' })
|
||||
order!: Relation<OrderEntity>
|
||||
|
||||
/**
|
||||
* Product ID at purchase time (not a FK — products may be archived)
|
||||
*/
|
||||
@Column({ type: 'varchar', length: 255 })
|
||||
productId!: string
|
||||
|
||||
/**
|
||||
* Variant ID if applicable (not a FK)
|
||||
*/
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
variantId!: string | null
|
||||
|
||||
/**
|
||||
* Product name snapshot at purchase time
|
||||
*/
|
||||
@Column({ type: 'varchar', length: 255 })
|
||||
productName!: string
|
||||
|
||||
/**
|
||||
* Product type snapshot (gift_card, physical_merchandise, etc.)
|
||||
*/
|
||||
@Column({ type: 'varchar', length: 100 })
|
||||
productType!: string
|
||||
|
||||
@Column({ type: 'int' })
|
||||
quantity!: number
|
||||
|
||||
/**
|
||||
* Price per unit in USD at purchase time
|
||||
*/
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||
unitPriceUsd!: string
|
||||
|
||||
/**
|
||||
* Total for this line item (quantity * unitPrice)
|
||||
*/
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||
lineTotalUsd!: string
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToMany,
|
||||
Index,
|
||||
} from 'typeorm'
|
||||
|
||||
import type { OrderItemEntity } from './order-item.entity'
|
||||
|
||||
/**
|
||||
* Order lifecycle status
|
||||
*/
|
||||
export enum OrderStatus {
|
||||
PENDING = 'pending',
|
||||
CONFIRMED = 'confirmed',
|
||||
FAILED = 'failed',
|
||||
REFUNDED = 'refunded',
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported payment methods
|
||||
*/
|
||||
export enum OrderPaymentMethod {
|
||||
STRIPE = 'stripe',
|
||||
CRYPTO = 'crypto',
|
||||
}
|
||||
|
||||
/**
|
||||
* OrderEntity - Persisted order record for completed checkouts
|
||||
*
|
||||
* Orders are created by landing's checkout flow via the merchant API.
|
||||
* Each order snapshots product data at purchase time to preserve
|
||||
* historical accuracy even when products change or are archived.
|
||||
*
|
||||
* @table merchant_orders
|
||||
*/
|
||||
@Entity('merchant_orders')
|
||||
export class OrderEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string
|
||||
|
||||
/**
|
||||
* Human-readable order identifier (format: order_{timestamp}_{hex})
|
||||
* Generated by landing's checkout controller
|
||||
*/
|
||||
@Index('idx_merchant_order_order_id', { unique: true })
|
||||
@Column({ type: 'varchar', length: 100 })
|
||||
orderId!: string
|
||||
|
||||
/**
|
||||
* User who placed the order
|
||||
* Not a FK — user data lives in a separate auth service
|
||||
*/
|
||||
@Index('idx_merchant_order_user_id')
|
||||
@Column({ type: 'varchar', length: 255 })
|
||||
userId!: string
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: OrderStatus,
|
||||
default: OrderStatus.CONFIRMED,
|
||||
})
|
||||
status!: OrderStatus
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: OrderPaymentMethod,
|
||||
})
|
||||
paymentMethod!: OrderPaymentMethod
|
||||
|
||||
/**
|
||||
* Stripe payment intent ID (required for stripe payments)
|
||||
*/
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
paymentIntentId!: string | null
|
||||
|
||||
/**
|
||||
* Crypto transaction hash (required for crypto payments)
|
||||
*/
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
cryptoTransactionId!: string | null
|
||||
|
||||
/**
|
||||
* Sum of line item totals before any adjustments
|
||||
*/
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||
subtotalUsd!: string
|
||||
|
||||
/**
|
||||
* Final order total (equals subtotal for now; future: tax/discounts)
|
||||
*/
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||
totalUsd!: string
|
||||
|
||||
/**
|
||||
* Voting power awarded for gift card purchases
|
||||
*/
|
||||
@Column({ type: 'int', default: 0 })
|
||||
votesAwarded!: number
|
||||
|
||||
/**
|
||||
* Extensible metadata for future order-level data
|
||||
*/
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
metadata!: Record<string, unknown> | null
|
||||
|
||||
@Index('idx_merchant_order_created_at')
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date
|
||||
|
||||
/**
|
||||
* Line items for this order
|
||||
*/
|
||||
@OneToMany('OrderItemEntity', (item: { order: OrderEntity }) => item.order, {
|
||||
cascade: true,
|
||||
eager: true,
|
||||
})
|
||||
items!: OrderItemEntity[]
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Get,
|
||||
Body,
|
||||
Query,
|
||||
Param,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
BadRequestException,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common'
|
||||
|
||||
import { OrdersService } from './orders.service'
|
||||
import {
|
||||
CreateOrderDto,
|
||||
OrderResponseDto,
|
||||
OrderListResponseDto,
|
||||
} from './dto/order.dto'
|
||||
|
||||
@Controller('orders')
|
||||
export class OrdersController {
|
||||
private readonly logger = new Logger(OrdersController.name)
|
||||
|
||||
constructor(private readonly ordersService: OrdersService) {}
|
||||
|
||||
/**
|
||||
* Create a new order
|
||||
*
|
||||
* POST /orders
|
||||
*
|
||||
* Called by landing's checkout flow to persist order data
|
||||
* after payment processing and inventory confirmation.
|
||||
*/
|
||||
@Post()
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
async createOrder(@Body() dto: CreateOrderDto): Promise<OrderResponseDto> {
|
||||
this.logger.log(
|
||||
`Creating order ${dto.orderId} for user ${dto.userId} ` +
|
||||
`(${dto.items.length} items, $${dto.totalUsd})`,
|
||||
)
|
||||
|
||||
return this.ordersService.createOrder(dto)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user order history
|
||||
*
|
||||
* GET /orders?userId=X&limit=20&offset=0
|
||||
*
|
||||
* Returns paginated list of orders for a user, most recent first.
|
||||
*/
|
||||
@Get()
|
||||
async getUserOrders(
|
||||
@Query('userId') userId: string,
|
||||
@Query('limit') limit?: string,
|
||||
@Query('offset') offset?: string,
|
||||
): Promise<OrderListResponseDto> {
|
||||
if (!userId) {
|
||||
throw new BadRequestException('userId query parameter is required')
|
||||
}
|
||||
|
||||
return this.ordersService.findByUser(userId, {
|
||||
limit: limit ? parseInt(limit, 10) : 20,
|
||||
offset: offset ? parseInt(offset, 10) : 0,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single order by its human-readable order ID
|
||||
*
|
||||
* GET /orders/:orderId
|
||||
*/
|
||||
@Get(':orderId')
|
||||
async getOrder(
|
||||
@Param('orderId') orderId: string,
|
||||
): Promise<OrderResponseDto> {
|
||||
const order = await this.ordersService.findByOrderId(orderId)
|
||||
|
||||
if (!order) {
|
||||
throw new NotFoundException(`Order ${orderId} not found`)
|
||||
}
|
||||
|
||||
return order
|
||||
}
|
||||
}
|
||||
15
features/merchant/backend-api/src/orders/orders.module.ts
Normal file
15
features/merchant/backend-api/src/orders/orders.module.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { Module } from '@nestjs/common'
|
||||
import { TypeOrmModule } from '@nestjs/typeorm'
|
||||
|
||||
import { OrderEntity } from './entities/order.entity'
|
||||
import { OrderItemEntity } from './entities/order-item.entity'
|
||||
import { OrdersController } from './orders.controller'
|
||||
import { OrdersService } from './orders.service'
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([OrderEntity, OrderItemEntity])],
|
||||
controllers: [OrdersController],
|
||||
providers: [OrdersService],
|
||||
exports: [OrdersService],
|
||||
})
|
||||
export class OrdersModule {}
|
||||
127
features/merchant/backend-api/src/orders/orders.service.ts
Normal file
127
features/merchant/backend-api/src/orders/orders.service.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { Injectable, Logger } from '@nestjs/common'
|
||||
import { InjectRepository } from '@nestjs/typeorm'
|
||||
import { Repository } from 'typeorm'
|
||||
|
||||
import { OrderEntity } from './entities/order.entity'
|
||||
import { OrderItemEntity } from './entities/order-item.entity'
|
||||
import type { CreateOrderDto, OrderResponseDto, OrderListResponseDto, OrderItemResponseDto } from './dto/order.dto'
|
||||
|
||||
interface FindByUserOptions {
|
||||
limit?: number
|
||||
offset?: number
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class OrdersService {
|
||||
private readonly logger = new Logger(OrdersService.name)
|
||||
|
||||
constructor(
|
||||
@InjectRepository(OrderEntity)
|
||||
private readonly orderRepository: Repository<OrderEntity>,
|
||||
@InjectRepository(OrderItemEntity)
|
||||
private readonly orderItemRepository: Repository<OrderItemEntity>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Persist a new order with its line items
|
||||
*
|
||||
* Uses cascade save to atomically persist both order and items.
|
||||
*/
|
||||
async createOrder(dto: CreateOrderDto): Promise<OrderResponseDto> {
|
||||
const order = this.orderRepository.create({
|
||||
orderId: dto.orderId,
|
||||
userId: dto.userId,
|
||||
paymentMethod: dto.paymentMethod,
|
||||
paymentIntentId: dto.paymentIntentId ?? null,
|
||||
cryptoTransactionId: dto.cryptoTransactionId ?? null,
|
||||
subtotalUsd: dto.subtotalUsd.toFixed(2),
|
||||
totalUsd: dto.totalUsd.toFixed(2),
|
||||
votesAwarded: dto.votesAwarded,
|
||||
metadata: dto.metadata ?? null,
|
||||
items: dto.items.map((item) =>
|
||||
this.orderItemRepository.create({
|
||||
productId: item.productId,
|
||||
variantId: item.variantId ?? null,
|
||||
productName: item.productName,
|
||||
productType: item.productType,
|
||||
quantity: item.quantity,
|
||||
unitPriceUsd: item.unitPriceUsd.toFixed(2),
|
||||
lineTotalUsd: item.lineTotalUsd.toFixed(2),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
const saved = await this.orderRepository.save(order)
|
||||
|
||||
this.logger.log(
|
||||
`Order ${saved.orderId} created for user ${saved.userId} ` +
|
||||
`(${saved.items.length} items, $${saved.totalUsd})`,
|
||||
)
|
||||
|
||||
return this.toResponseDto(saved)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve paginated orders for a user, most recent first
|
||||
*/
|
||||
async findByUser(
|
||||
userId: string,
|
||||
options: FindByUserOptions = {},
|
||||
): Promise<OrderListResponseDto> {
|
||||
const limit = options.limit ?? 20
|
||||
const offset = options.offset ?? 0
|
||||
|
||||
const [orders, total] = await this.orderRepository.findAndCount({
|
||||
where: { userId },
|
||||
order: { createdAt: 'DESC' },
|
||||
take: limit,
|
||||
skip: offset,
|
||||
relations: ['items'],
|
||||
})
|
||||
|
||||
return {
|
||||
orders: orders.map((order) => this.toResponseDto(order)),
|
||||
total,
|
||||
limit,
|
||||
offset,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a single order by its human-readable orderId
|
||||
*/
|
||||
async findByOrderId(orderId: string): Promise<OrderResponseDto | null> {
|
||||
const order = await this.orderRepository.findOne({
|
||||
where: { orderId },
|
||||
relations: ['items'],
|
||||
})
|
||||
|
||||
if (!order) return null
|
||||
|
||||
return this.toResponseDto(order)
|
||||
}
|
||||
|
||||
/**
|
||||
* Map entity to response DTO with numeric conversions
|
||||
*/
|
||||
private toResponseDto(order: OrderEntity): OrderResponseDto {
|
||||
return {
|
||||
orderId: order.orderId,
|
||||
status: order.status,
|
||||
paymentMethod: order.paymentMethod,
|
||||
subtotalUsd: parseFloat(order.subtotalUsd),
|
||||
totalUsd: parseFloat(order.totalUsd),
|
||||
votesAwarded: order.votesAwarded,
|
||||
items: order.items.map((item): OrderItemResponseDto => ({
|
||||
productId: item.productId,
|
||||
variantId: item.variantId,
|
||||
productName: item.productName,
|
||||
productType: item.productType,
|
||||
quantity: item.quantity,
|
||||
unitPriceUsd: parseFloat(item.unitPriceUsd),
|
||||
lineTotalUsd: parseFloat(item.lineTotalUsd),
|
||||
})),
|
||||
createdAt: order.createdAt.toISOString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue