Eコマース例
この例では、MBC CQRS Serverlessを使用した注文管理、在庫追跡、マルチテナントサポートを備えた完全なEコマース実装を示します。
概要
Eコマースの例では以下をカバーします:
- 注文ライフサイクル管理(作成、更新、キャンセル)
- 楽観的ロックによる在庫追跡
- マルチテナントストアフロントの分離
- イベント駆動の注文処理
データモデル
キー構造
パーティションキー (pk) ソートキー (sk)
──────────────────────────────────────────────────
TENANT#shop-a ORDER#ORD-000001
TENANT#shop-a ORDER#ORD-000002
TENANT#shop-a PRODUCT#PRD-001
TENANT#shop-a INVENTORY#PRD-001
TENANT#shop-b ORDER#ORD-000001
エンティティ定義
export interface OrderItem {
productCode: string;
quantity: number;
unitPrice: number;
}
export interface Address {
street: string;
city: string;
state: string;
postalCode: string;
country: string;
}
// Order Entity (注文エンティティ)
export interface OrderAttributes {
customerId: string;
items: OrderItem[];
shippingAddress: Address;
paymentMethod: string;
subtotal: number;
tax: number;
total: number;
status: OrderStatus;
placedAt: string;
shippedAt?: string;
deliveredAt?: string;
}
export type OrderStatus =
| 'pending'
| 'confirmed'
| 'processing'
| 'shipped'
| 'delivered'
| 'cancelled';
// Product Entity (商品エンティティ)
export interface ProductAttributes {
categoryCode: string;
sku: string;
price: number;
currency: string;
description: string;
images: string[];
isActive: boolean;
}
// Inventory Entity (在庫エンティティ)
export interface InventoryAttributes {
productCode: string;
quantity: number;
reservedQuantity: number;
warehouseCode: string;
lastRestockedAt: string;
}
モジュール実装
注文モジュール
// order.module.ts
import { Module } from '@nestjs/common';
import { CommandModule } from '@mbc-cqrs-serverless/core';
import { SequencesModule } from '@mbc-cqrs-serverless/sequence';
import { OrderController } from './order.controller';
import { OrderService } from './order.service';
import { OrderDataSyncHandler } from './order-data-sync.handler';
@Module({
imports: [
CommandModule.register({
tableName: 'order',
dataSyncHandlers: [OrderDataSyncHandler],
}),
SequencesModule,
],
controllers: [OrderController],
providers: [OrderService],
exports: [OrderService],
})
export class OrderModule {}
注文サービス
// order.service.ts
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import {
CommandService,
DataService,
IInvoke,
KEY_SEPARATOR,
VERSION_FIRST,
getUserContext,
} from '@mbc-cqrs-serverless/core';
import { SequencesService } from '@mbc-cqrs-serverless/sequence';
// Example helper: per-tenant partition key (テナントごとのパーティションキー)
const generatePk = (tenantCode: string): string =>
`TENANT${KEY_SEPARATOR}${tenantCode}`;
@Injectable()
export class OrderService {
constructor(
private readonly commandService: CommandService,
private readonly dataService: DataService,
private readonly sequencesService: SequencesService,
) {}
// Create a new order (新規注文を作成)
async createOrder(dto: CreateOrderDto, context: IInvoke) {
const { tenantCode } = getUserContext(context);
// Generate unique order number (一意の注文番号を生成)
const sequence = await this.sequencesService.generateSequenceItem(
{ tenantCode, typeCode: 'ORDER' },
{ invokeContext: context },
);
const orderCode = sequence.formattedNo;
// Calculate totals (合計を計算)
const subtotal = dto.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const tax = subtotal * 0.1; // 10% tax
const total = subtotal + tax;
const command = {
pk: generatePk(tenantCode),
sk: `ORDER#${orderCode}`,
version: VERSION_FIRST,
code: orderCode,
name: `Order ${orderCode}`,
tenantCode,
attributes: {
customerId: dto.customerId,
items: dto.items,
shippingAddress: dto.shippingAddress,
paymentMethod: dto.paymentMethod,
subtotal,
tax,
total,
status: 'pending' as OrderStatus,
placedAt: new Date().toISOString(),
},
};
return this.commandService.publishAsync(command, { invokeContext: context });
}
// Update order status (注文ステータスを更新)
async updateOrderStatus(
orderCode: string,
newStatus: OrderStatus,
context: IInvoke,
) {
const { tenantCode } = getUserContext(context);
const pk = generatePk(tenantCode);
const sk = `ORDER#${orderCode}`;
// Fetch current order (現在の注文を取得)
const current = await this.dataService.getItem({ pk, sk });
if (!current) {
throw new NotFoundException(`Order ${orderCode} not found`);
}
// Validate status transition (ステータス遷移を検証)
this.validateStatusTransition(current.attributes.status, newStatus);
// Build update command with version for optimistic locking (楽観的ロック用のバージョン付き更新コマンドを構築)
const command = {
...current,
version: current.version, // Required for optimistic locking (楽観的ロックに必要)
attributes: {
...current.attributes,
status: newStatus,
...(newStatus === 'shipped' && { shippedAt: new Date().toISOString() }),
...(newStatus === 'delivered' && { deliveredAt: new Date().toISOString() }),
},
};
return this.commandService.publishAsync(command, { invokeContext: context });
}
// List orders with pagination (ページネーション付きで注文を一覧表示)
async listOrders(options: ListOrdersDto, context: IInvoke) {
const { tenantCode } = getUserContext(context);
return this.dataService.listItemsByPk(generatePk(tenantCode), {
sk: {
skExpression: 'begins_with(sk, :prefix)',
skAttributeValues: { ':prefix': 'ORDER#' },
},
limit: options.limit || 20,
startFromSk: options.cursor,
});
}
// Validate order status transitions (注文ステータス遷移を検証)
private validateStatusTransition(current: OrderStatus, next: OrderStatus) {
const validTransitions: Record<OrderStatus, OrderStatus[]> = {
pending: ['confirmed', 'cancelled'],
confirmed: ['processing', 'cancelled'],
processing: ['shipped', 'cancelled'],
shipped: ['delivered'],
delivered: [],
cancelled: [],
};
if (!validTransitions[current].includes(next)) {
throw new BadRequestException(
`Cannot transition from ${current} to ${next}`
);
}
}
}
注文コントローラー
// order.controller.ts
import { Controller, Get, Post, Patch, Body, Param, Query } from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
import { INVOKE_CONTEXT, IInvoke } from '@mbc-cqrs-serverless/core';
import { OrderService } from './order.service';
@ApiTags('orders')
@Controller('orders')
export class OrderController {
constructor(private readonly orderService: OrderService) {}
@Post()
@ApiOperation({ summary: 'Create a new order' })
async create(
@Body() dto: CreateOrderDto,
@INVOKE_CONTEXT() invokeContext: IInvoke,
) {
return this.orderService.createOrder(dto, invokeContext);
}
@Get()
@ApiOperation({ summary: 'List orders' })
async list(
@Query() options: ListOrdersDto,
@INVOKE_CONTEXT() invokeContext: IInvoke,
) {
return this.orderService.listOrders(options, invokeContext);
}
@Patch(':code/status')
@ApiOperation({ summary: 'Update order status' })
async updateStatus(
@Param('code') code: string,
@Body() dto: UpdateStatusDto,
@INVOKE_CONTEXT() invokeContext: IInvoke,
) {
return this.orderService.updateOrderStatus(code, dto.status, invokeContext);
}
}