SMILE連携プラグイン (SMILE Integration Plugin)¶
概要¶
Ritsubiの会計システム「SMILE V2」との連携を実現するVendureカスタムプラグイン。注文データ・顧客データのCSV出力、SHIFT_JISエンコーディング対応、バッチ処理機能を提供します。
得意先/納品先コードの扱い(2025-11-30更新)
- SMILEを正とし、Vendure側で自動採番しない。
- 得意先コードは
Customer.customFields.customerCodeに統一し、SMILEで発行したコードを手入力/取込する。 - 旧
customerNumberは後方互換用に残すが、今後はcustomerCodeを正とする。必要に応じてバッチでコピーする。 - 納品先コードは
Address.customFields.shipToCodeをカスタムフィールドで保持し、直送先を含め必須入力とする。
以前分割していた「SMILE連携仕様書」と本プラグインドキュメントを統合し、連携要件と実装をこの1ページで一元管理します。(更新日: 2025-11-28)
エクスポート方式(2025-12-03更新)¶
- トリガー: Admin GraphQL Mutation(
exportSmileOrders/exportSmileDeliveryPoints)はキックのみを担当。レスポンスはdownloadUrl/filename/recordCountを返す(csvData廃止)。 - ダウンロード: 管理者セッションが必須のRESTエンドポイント
/smile-export/downloadで配信。署名キーや追加環境変数は不要。 - ストレージ: エクスポートファイルは
SMILE_EXPORT_DIRECTORY(デフォルト./exports/smile)に保存し、SmileExportLogにファイルパス/サイズ/ダウンロード回数を記録。 - Dashboard対応: Smileエクスポート画面は
downloadUrlをfetch→Blobダウンロードするフローに更新(大容量でもBase64膨張なし)。
互換性レンジ¶
- Vendure: 3.5.x(apps/vendure-server は ^3.5.1 を使用)
- Node.js: >=22.11.0(apps/vendure-server/package.json の engines に準拠)
連携アーキテクチャ¶
システム構成¶
Vendure Backend ←→ SMILE会計システム
↓
Next.js Storefront
連携方式¶
- API連携: RESTful API + GraphQL
- データ同期: リアルタイム同期
- 認証: OAuth 2.0 + API Key認証
顧客/納品先コード管理¶
カスタムフィールド定義¶
Customer.customFields.customerCode- SMILEで発行された得意先コード。Vendure側での自動採番は禁止。
- UNIQUE / NOT NULL 制約をDB側で付与。UI上は必須入力。
customerNumberが既存データにある場合は移行時にコピーする。Address.customFields.shipToCode- SMILEで発行された納品先コードを保持。直送先も含め必須。
- UNIQUE / NOT NULL 制約を付与。
入力・バリデーション運用¶
- 管理画面の顧客・住所フォームで両コードが未入力の場合は保存不可にする(UIガード)。
- コード重複時はエラーを即時表示し、SMILEコード体系の重複を防止。
- コード発行フロー: SMILEで発行 → Vendureに手入力またはバッチ取込。Vendure側での採番は行わない。
CSVエクスポート対応¶
得意先コード←customer.customFields.customerCode納品先コード←address.customFields.shipToCode- 住所/氏名は Address 標準フィールド(会社名を使う場合は Address.company を優先)
連携要件¶
単価マスタ連携¶
単価マスタの定義¶
- 用途: 通常価格とは異なる特別価格設定
- 管理: SMILE側で管理される特別価格マスタ
- 連携: Vendure側で単価マスタの価格を参照・適用
連携仕様¶
interface UnitPriceMaster {
productCode: string;
specialPrice: number;
customerGroupId: string;
validFrom: Date;
validTo: Date;
isActive: boolean;
}
// SMILE → Vendure データ同期
interface UnitPriceSync {
action: 'CREATE' | 'UPDATE' | 'DELETE';
unitPrice: UnitPriceMaster;
timestamp: Date;
}
顧客グループ制限¶
現行システム制限¶
- 制限内容: 1つの顧客は1つの顧客グループにのみ所属可能
- SMILE制約: SMILE側で1グループ制限が実装済み
- Vendure対応: 同様の制限をVendure側でも実装
連携仕様¶
interface CustomerGroupRestriction {
customerId: string;
currentGroupId: string;
canChangeGroup: boolean;
restrictionReason?: string;
}
// 顧客グループ変更時の制御
interface GroupChangeControl {
customerId: string;
newGroupId: string;
requiresApproval: boolean;
approvalWorkflow: ApprovalStep[];
}
セット商品処理¶
セット商品の扱い¶
- 購入時: 商品セット(X=A+B+C)としてVendureで処理
- SMILE連携時: 分裂した商品(A, B, C)として個別にエクスポート
- データ変換: セット→個別商品への変換処理
連携データ形式¶
// Vendure側のセット商品データ
interface SetProduct {
setCode: string;
setName: string;
individualProducts: {
productCode: string;
quantity: number;
unitPrice: number;
}[];
totalPrice: number;
bonusProduct?: {
productCode: string;
quantity: number;
};
}
// SMILE連携時の個別商品データ
interface IndividualProductExport {
orderId: string;
products: {
productCode: string;
quantity: number;
unitPrice: number;
totalPrice: number;
originalSetCode?: string; // 元のセット商品コード
}[];
exportTimestamp: Date;
}
価格計算連携¶
価格計算フロー¶
- 税抜き価格計算: すべての価格計算を税抜きで実行
- 消費税適用: 最終段階で消費税を適用
- 処理優先度: 図の下にある処理が優先される
連携仕様¶
interface PriceCalculation {
basePrice: number;
specialPrice?: number;
unitPriceMasterPrice?: number;
campaignPrice?: number;
finalPrice: number;
taxAmount: number;
totalPrice: number;
calculationSteps: PriceCalculationStep[];
}
interface PriceCalculationStep {
step: string;
price: number;
description: string;
priority: number;
appliedAt: Date;
}
price_chk_campain() 関数連携¶
特別価格計算処理¶
- 定義: システム側で自動設定される特別価格計算条件
- 対象: セット商品(ユニーク商品)のみ
- 連携: SMILE側の特別価格計算ロジックをVendure側で実装
連携仕様¶
interface PriceCheckCampaign {
campaignId: string;
targetProducts: string[]; // セット商品コード
conditions: CampaignCondition[];
pricingRules: PricingRule[];
isActive: boolean;
}
interface CampaignCondition {
type: 'PRODUCT_COMBINATION' | 'QUANTITY' | 'CUSTOMER_GROUP';
value: any;
operator: 'EQUALS' | 'GREATER_THAN' | 'LESS_THAN' | 'IN';
}
商品ブランド連携¶
RCODE(商品ブランド)¶
- 定義: 商品ブランドの一つ
- 管理: SMILE側でブランド管理
- 連携: Vendure側でブランド情報を同期
連携仕様¶
interface Brand {
rcode: string;
name: string;
description?: string;
pricingRules: BrandPricingRule[];
isActive: boolean;
}
interface BrandPricingRule {
ruleId: string;
condition: BrandCondition;
discount: DiscountRule;
validFrom: Date;
validTo: Date;
}
interface BrandCondition {
type: 'SAME_BRAND_QUANTITY' | 'BRAND_COMBINATION';
quantity?: number;
productCodes?: string[];
}
キャンペーン・割引連携¶
割引パターン連携¶
同一ブランド商品の組み合わせ¶
- パターン: 同一ブランド商品×点数の組み合わせ割引
- 連携: SMILE側の割引ルールをVendure側で実装
セット商品割引¶
- 基本パターン: A+B+C
- おまけ付きパターン: A+B+C(+Dおまけ)
- 亜種パターン: 対象商品1割引(特別安い設定の顧客には適用しない)
連携仕様¶
interface DiscountPattern {
patternId: string;
name: string;
type: 'SET_PRODUCT' | 'BRAND_COMBINATION' | 'SPECIAL_DISCOUNT';
products: string[];
discountRate: number;
conditions: DiscountCondition[];
exclusions: CustomerExclusion[];
}
interface CustomerExclusion {
customerGroupId: string;
reason: string;
isActive: boolean;
}
API仕様¶
エンドポイント一覧¶
単価マスタ連携¶
GET /api/smile/unit-prices # 単価マスタ一覧取得
GET /api/smile/unit-prices/{id} # 単価マスタ詳細取得
POST /api/smile/unit-prices/sync # 単価マスタ同期
PUT /api/smile/unit-prices/{id} # 単価マスタ更新
DELETE /api/smile/unit-prices/{id} # 単価マスタ削除
顧客グループ連携¶
GET /api/smile/customer-groups # 顧客グループ一覧取得
GET /api/smile/customer-groups/{id} # 顧客グループ詳細取得
POST /api/smile/customer-groups/sync # 顧客グループ同期
PUT /api/smile/customer-groups/{id} # 顧客グループ更新
セット商品連携¶
GET /api/smile/set-products # セット商品一覧取得
GET /api/smile/set-products/{id} # セット商品詳細取得
POST /api/smile/set-products/export # セット商品エクスポート
POST /api/smile/set-products/convert # セット→個別変換
価格計算連携¶
POST /api/smile/price-calculation # 価格計算実行
GET /api/smile/price-calculation/{id} # 価格計算結果取得
POST /api/smile/price-calculation/sync # 価格計算同期
認証・認可¶
API認証¶
interface APIAuthentication {
apiKey: string;
customerId: string;
permissions: Permission[];
expiresAt: Date;
}
interface Permission {
resource: string;
actions: string[];
conditions?: PermissionCondition[];
}
顧客認証¶
interface CustomerAuthentication {
customerId: string;
customerGroup: string;
accessLevel: AccessLevel;
permissions: CustomerPermission[];
}
interface CustomerPermission {
resource: 'PRODUCTS' | 'PRICING' | 'ORDERS' | 'CAMPAIGNS';
actions: string[];
restrictions?: PermissionRestriction[];
}
データ同期仕様¶
同期方式¶
- リアルタイム同期: 価格・在庫・顧客情報
- バッチ同期: 商品マスタ・顧客マスタ
- イベント駆動同期: 注文・決済・配送情報
同期データ形式¶
interface SyncData {
syncId: string;
source: 'VENDURE' | 'SMILE';
target: 'VENDURE' | 'SMILE';
dataType: 'PRODUCT' | 'CUSTOMER' | 'PRICE' | 'ORDER';
payload: any;
timestamp: Date;
status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED';
}
エラーハンドリング¶
interface SyncError {
errorId: string;
syncId: string;
errorType: 'VALIDATION' | 'NETWORK' | 'AUTHENTICATION' | 'DATA';
errorMessage: string;
retryCount: number;
maxRetries: number;
nextRetryAt: Date;
}
セキュリティ要件¶
データ保護¶
- 暗号化: すべての通信をTLS 1.3で暗号化
- 認証: OAuth 2.0 + JWT認証
- 認可: リソースベースのアクセス制御
監査ログ¶
interface AuditLog {
logId: string;
timestamp: Date;
userId: string;
action: string;
resource: string;
details: any;
ipAddress: string;
userAgent: string;
}
実装優先度(フェーズ)¶
Phase 1: 基本連携¶
- 顧客情報の同期
- 商品マスタの同期
- 基本的な価格計算連携
Phase 2: 高度な連携¶
- 単価マスタの連携
- セット商品の処理
- 特別価格計算の連携
Phase 3: キャンペーン連携¶
- 割引パターンの連携
- ブランド別価格の連携
- 複雑な価格計算の連携
Phase 4: 最適化¶
- パフォーマンス最適化
- エラーハンドリング強化
- 監査ログの充実
パッケージ情報¶
- パッケージ名:
@ritsubi/smile-integration-plugin - パス:
/packages/plugins/smile-integration - エントリーポイント:
src/index.ts
実装機能¶
1. 注文データCSV出力(SMILE V2フォーマット対応)¶
VendureのOrderデータをSMILE V2が読み込める形式のCSVファイルに出力。
出力データ項目¶
| 項目 | 説明 | サンプル |
|---|---|---|
| 受注番号 | Vendure注文ID | ORD-0001234 |
| 顧客コード | SMILE顧客コード | CUST-12345 |
| 納品先コード | 自動採番された納品先コード | CUST-12345-001 |
| 商品コード | SMILE商品コード | PROD-EXV-001 |
| 数量 | 注文数量 | 10 |
| 単価 | 単価(税抜) | 5000 |
| 金額 | 小計(税抜) | 50000 |
| 配送料 | 配送料金 | 800 |
| 消費税 | 消費税額 | 5080 |
| 合計金額 | 総額(税込) | 55880 |
| 出荷予定日 | 出荷予定日 | 2025-11-15 |
| 特記事項 | 配送指示等 | 直送モード |
CSVエンコーディング¶
- デフォルト: SHIFT_JIS(SMILE V2標準)
- オプション: UTF-8(SMILE V2.1以降)
2. 顧客データCSV出力¶
Vendure顧客データをSMILE取引先マスタ形式のCSVに出力。
出力データ項目¶
| 項目 | 説明 |
|---|---|
| 顧客コード | SMILE顧客コード |
| 顧客名 | 法人名/個人名 |
| 住所 | 郵便番号・住所 |
| 電話番号 | 連絡先電話番号 |
| FAX番号 | FAX番号 |
| メールアドレス | 連絡先メール |
| 掛率 | 基本掛率 |
| 締日 | 請求締日 |
| 支払条件 | 支払方法・期限 |
| 顧客ステータス | ステータスコード |
3. 納品先自動採番管理¶
顧客の配送先住所ごとに、自動的に納品先コードを生成・管理。
採番ルール¶
顧客コード + 連番形式
例: CUST-12345-001, CUST-12345-002, ...
納品先情報¶
- 住所
- 宛名
- 電話番号
- 配送指示
- デフォルト納品先フラグ
4. CSV出力履歴管理¶
すべてのCSV出力履歴を記録し、トレーサビリティを確保。
記録情報¶
- 出力日時
- 出力ファイル名
- 出力レコード数
- 出力ステータス(成功/失敗)
- エラー詳細
- 出力実行者
5. ファイル保持期限管理¶
出力されたCSVファイルを一定期間保持し、期限後は自動削除。
- デフォルト保持期間: 30日間
- 最小保持期間: 7日間(法的要件)
- 自動クリーンアップ: 毎日実行
6. バッチ処理対応¶
定期的な自動CSV出力をスケジューリング。
- デフォルトスケジュール: 毎日午前1時
- カスタマイズ可能: Cron式で設定
7. エラー追跡・復旧機能¶
CSV出力エラーを検知し、自動リトライまたは管理者通知。
技術仕様¶
プラグイン設定オプション¶
export interface SmileIntegrationPluginOptions {
/**
* 注文データCSV出力を有効にする
*/
enableOrderExport?: boolean;
/**
* 顧客データCSV出力を有効にする
*/
enableCustomerExport?: boolean;
/**
* 納品先自動採番管理を有効にする
*/
enableDeliveryPointManagement?: boolean;
/**
* SMILEバージョン
*/
smileVersion?: 'V1' | 'V2';
/**
* CSV出力エンコーディング
*/
exportFormat?: 'SHIFT_JIS' | 'UTF-8';
/**
* 納品先コード採番開始番号
*/
autoNumberingStart?: number;
/**
* CSVファイル保持日数
*/
csvFileRetentionDays?: number;
/**
* バッチ出力スケジュール(Cron式)
*/
batchExportSchedule?: string;
/**
* CSV出力ディレクトリ
*/
exportDirectory?: string;
}
プラグイン初期化¶
import { SmileIntegrationPlugin } from '@ritsubi/smile-integration-plugin';
export const config: VendureConfig = {
plugins: [
SmileIntegrationPlugin.init({
enableOrderExport: true,
enableCustomerExport: true,
enableDeliveryPointManagement: true,
smileVersion: 'V2',
exportFormat: 'SHIFT_JIS',
autoNumberingStart: 1,
csvFileRetentionDays: 30,
batchExportSchedule: '0 1 * * *', // 毎日午前1時
exportDirectory: './exports/smile',
}),
],
};
エンティティ¶
DeliveryPointEntity¶
納品先情報を管理するエンティティ。
@Entity()
export class DeliveryPointEntity extends VendureEntity {
@Column()
customerId: string;
@Column({ unique: true })
deliveryPointCode: string; // CUST-12345-001形式
@Column()
deliveryPointName: string;
@Column()
postalCode: string;
@Column()
address1: string;
@Column({ nullable: true })
address2: string;
@Column()
phoneNumber: string;
@Column('text', { nullable: true })
deliveryInstructions: string;
@Column({ default: false })
isDefault: boolean;
@Column({ default: true })
isActive: boolean;
}
SmileExportLogEntity¶
CSV出力履歴を記録するエンティティ。
@Entity()
export class SmileExportLogEntity extends VendureEntity {
@Column('enum', { enum: ['ORDER', 'CUSTOMER', 'DELIVERY_POINT', 'PRODUCT'] })
exportType: ExportType;
@Column()
filename: string;
@Column()
filePath: string;
@Column('int')
recordCount: number;
@Column('enum', { enum: ['PENDING', 'SUCCESS', 'FAILED', 'PARTIAL'] })
status: ExportStatus;
@Column('text', { nullable: true })
errorDetails: string;
@Column()
exportedAt: Date;
@Column({ nullable: true })
exportedBy: string; // ユーザーID
@Column({ nullable: true })
deletedAt: Date; // ファイル削除日時
}
サービス¶
CsvExportService¶
CSV出力のコアロジックを担当するサービス。
主要メソッド:
exportOrders(startDate: Date, endDate: Date): Promise<ExportResult>- 注文データをCSV出力
exportCustomers(): Promise<ExportResult>- 顧客データをCSV出力
exportDeliveryPoints(customerId: ID): Promise<ExportResult>- 納品先データをCSV出力
generateSmileV2Csv(data: Record<string, unknown>[], type: ExportType): Promise<string>- SMILE V2形式のCSVを生成
convertToShiftJis(csvString: string): Promise<Buffer>- SHIFT_JISエンコーディングに変換
DeliveryPointService¶
納品先管理のロジックを担当するサービス。
主要メソッド:
createDeliveryPoint(customerId: ID, input: CreateDeliveryPointInput): Promise<DeliveryPointEntity>- 納品先を作成(自動採番)
getDeliveryPoints(customerId: ID): Promise<DeliveryPointEntity[]>- 顧客の納品先一覧を取得
generateDeliveryPointCode(customerId: ID): Promise<string>- 納品先コードを自動生成
setDefaultDeliveryPoint(deliveryPointId: ID): Promise<void>- デフォルト納品先を設定
カスタムフィールド¶
Customer¶
customerCode(string): SMILE顧客コード(ユニーク)smileSyncStatus(string): SMILE同期ステータスsmileLastSyncedAt(datetime): SMILE最終同期日時
Order¶
deliveryPointCode(string): 納品先コードsmileExported(boolean): SMILE出力済みsmileExportedAt(datetime): SMILE出力日時smileExportLogId(string): 関連するSMILE出力ログのIDshippingDate(datetime): 出荷予定日specialInstructions(text): 特記事項
Product¶
smileProductCode(string): SMILE商品コードsmileCategory(string): SMILEカテゴリ分類
対応する要件¶
要件定義書との対応¶
- SMILE V2連携: 会計システムとのデータ連携
- CSV出力: SHIFT_JIS形式での注文・顧客データ出力
- 納品先管理: 自動採番による納品先マスタ管理
- バッチ処理: 定期的な自動データ出力
使用例¶
フロントエンド(Next.js)での使用¶
// 納品先を追加
const { data } = await apolloClient.mutate({
mutation: CREATE_DELIVERY_POINT,
variables: {
customerId: currentUser.id,
deliveryPointName: '本社',
postalCode: '100-0001',
address1: '東京都千代田区...',
phoneNumber: '03-1234-5678',
deliveryInstructions: '平日9-17時のみ配送可',
isDefault: true,
},
});
console.log(`納品先コード: ${data.deliveryPoint.deliveryPointCode}`);
// 例: CUST-12345-001
納品先一覧の取得¶
// 顧客の納品先一覧を取得
const { data } = await apolloClient.query({
query: GET_DELIVERY_POINTS,
variables: { customerId: currentUser.id },
});
data.deliveryPoints.forEach(point => {
console.log(`${point.deliveryPointCode}: ${point.deliveryPointName}`);
});
管理画面での使用¶
// 注文データを手動エクスポート
const result = await csvExportService.exportOrders(
new Date('2025-10-01'),
new Date('2025-10-31'),
);
console.log(`出力完了: ${result.filename}`);
console.log(`レコード数: ${result.recordCount}`);
console.log(`ファイルパス: ${result.filePath}`);
// 顧客データをエクスポート
const customerResult = await csvExportService.exportCustomers();
console.log(`顧客データ出力: ${customerResult.recordCount}件`);
// エクスポート履歴を確認
const logs = await smileExportLogRepository.find({
where: { exportType: 'ORDER' },
order: { exportedAt: 'DESC' },
take: 10,
});
logs.forEach(log => {
console.log(`${log.exportedAt}: ${log.filename} - ${log.status}`);
});
バッチ処理¶
自動CSV出力ジョブ¶
毎日定時に注文データを自動出力。
@Cron('0 1 * * *') // 毎日午前1時実行
async autoExportOrders() {
try {
// 前日の注文を出力
const yesterday = moment().subtract(1, 'day').startOf('day');
const today = moment().startOf('day');
const result = await this.csvExportService.exportOrders(
yesterday.toDate(),
today.toDate()
);
this.logger.log(`注文CSV自動出力完了: ${result.filename}`);
} catch (error) {
this.logger.error(`注文CSV出力エラー: ${error.message}`);
await this.notificationService.sendAlert('SMILE CSV出力エラー', error);
}
}
ファイルクリーンアップジョブ¶
期限切れのCSVファイルを自動削除。
@Cron('0 2 * * *') // 毎日午前2時実行
async cleanupOldExports() {
const retentionDays = this.options.csvFileRetentionDays ?? 30;
const cutoffDate = moment().subtract(retentionDays, 'days').toDate();
const deletedCount = await this.csvExportService.deleteExportsOlderThan(cutoffDate);
this.logger.log(`${deletedCount}件の期限切れCSVファイルを削除しました`);
}
CSV出力フロー¶
注文データ出力の詳細フロー¶
1. 出力対象の注文を取得
├─ 期間指定(開始日〜終了日)
├─ ステータス指定(確定済み注文のみ)
└─ 未出力フラグでフィルタ
2. SMILE V2形式に変換
├─ 顧客コードをマッピング
├─ 納品先コードを取得
├─ 商品コードをマッピング
└─ 金額・税額を計算
3. CSVファイル生成
├─ ヘッダー行を追加
├─ データ行を追加
└─ SHIFT_JISにエンコード
4. ファイル保存
├─ ファイル名を生成(日付 + 連番)
├─ 指定ディレクトリに保存
└─ エクスポートログを記録
5. 注文ステータスを更新
└─ smileExported = true
データモデル¶
CSV出力ステータス¶
PENDING (処理待ち)
├─ 出力成功 → SUCCESS
├─ 一部エラー → PARTIAL
└─ 全件エラー → FAILED
セキュリティ考慮事項¶
- アクセス制御: CSV出力は管理者権限が必要
- ファイル暗号化: 機密データを含むCSVは暗号化オプション
- 監査ログ: すべての出力操作を記録
- 自動削除: 保持期限経過後のファイル自動削除
パフォーマンス最適化¶
- ストリーミング出力: 大量データはストリーミングで処理
- バッチ処理: 深夜時間帯に自動実行
- インデックス最適化: 出力クエリのパフォーマンス向上
- 並列処理: 複数の出力タイプを並列実行
トラブルシューティング¶
よくある問題¶
問題: SHIFT_JISエンコードエラー
- 原因: 対応していない文字が含まれている
- 解決: 文字をサニタイズ、または UTF-8 出力に切り替え
問題: 納品先コードの重複
- 原因: 自動採番の並行実行
- 解決: トランザクションで採番処理を保護
問題: CSVファイルがSMILEで読み込めない
- 原因: フォーマットのバージョン不一致
- 解決:
smileVersion設定を確認(V1 or V2)
関連ドキュメント¶
- 連携要件・API仕様(本ページの「連携アーキテクチャ」「連携要件」「API仕様」セクション)
- 配送計算プラグイン - 納品先情報との連携
- 価格システムプラグイン - 掛率情報のCSV出力
今後の拡張予定¶
- リアルタイムSMILE連携(API連携)
- 差分CSV出力機能
- 複数SMILEインスタンス対応
- CSV出力プレビュー機能