コンテンツにスキップ

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;
}

価格計算連携

価格計算フロー

  1. 税抜き価格計算: すべての価格計算を税抜きで実行
  2. 消費税適用: 最終段階で消費税を適用
  3. 処理優先度: 図の下にある処理が優先される

連携仕様

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: 基本連携

  1. 顧客情報の同期
  2. 商品マスタの同期
  3. 基本的な価格計算連携

Phase 2: 高度な連携

  1. 単価マスタの連携
  2. セット商品の処理
  3. 特別価格計算の連携

Phase 3: キャンペーン連携

  1. 割引パターンの連携
  2. ブランド別価格の連携
  3. 複雑な価格計算の連携

Phase 4: 最適化

  1. パフォーマンス最適化
  2. エラーハンドリング強化
  3. 監査ログの充実

パッケージ情報

  • パッケージ名: @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出力ログのID
  • shippingDate (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

セキュリティ考慮事項

  1. アクセス制御: CSV出力は管理者権限が必要
  2. ファイル暗号化: 機密データを含むCSVは暗号化オプション
  3. 監査ログ: すべての出力操作を記録
  4. 自動削除: 保持期限経過後のファイル自動削除

パフォーマンス最適化

  • ストリーミング出力: 大量データはストリーミングで処理
  • バッチ処理: 深夜時間帯に自動実行
  • インデックス最適化: 出力クエリのパフォーマンス向上
  • 並列処理: 複数の出力タイプを並列実行

トラブルシューティング

よくある問題

問題: SHIFT_JISエンコードエラー

  • 原因: 対応していない文字が含まれている
  • 解決: 文字をサニタイズ、または UTF-8 出力に切り替え

問題: 納品先コードの重複

  • 原因: 自動採番の並行実行
  • 解決: トランザクションで採番処理を保護

問題: CSVファイルがSMILEで読み込めない

  • 原因: フォーマットのバージョン不一致
  • 解決: smileVersion 設定を確認(V1 or V2)

関連ドキュメント

今後の拡張予定

  • リアルタイムSMILE連携(API連携)
  • 差分CSV出力機能
  • 複数SMILEインスタンス対応
  • CSV出力プレビュー機能