ClassLab Engineering の Dev チームメンバーが執筆しました。
*この記事は、大規模トランザクション処理やSOAP API連携、信頼性設計に興味があるバックエンド/インフラエンジニア向けです。*
1. 背景
ClassLab は年間約30万人の新生活をサポートするライフラインプラットフォームを運営しています。引越しに伴う電気・ガス・水道の開始・停止・変更手続きを、数千社超のパートナー企業と連携して処理しています。
この手続きの中核を担うのが、電力広域的運営推進機関(OCCTO)のスイッチング支援システムとの連携基盤です。OCCTOは電力・ガスの小売自由化に伴い、供給地点の切替手続きを一元管理する機関です。当社のシステムは、このOCCTOのSOAP APIを介して、供給地点の照会・再点(通電開始)・廃止(停止)・需要者情報変更などの手続きを自動化しています。
「止まったら困る」ライフラインの申込処理において、年間15万件超のトランザクションを安全に処理し続けるために、どのような信頼性設計を行ったかを紹介します。
2. 課題
| 課題 | 影響 | 定量データ |
|---|---|---|
| OCCTO APIの複雑な仕様 | SOAP/WSDL + SSL相互認証 + 独自エラーコード体系 | 8つのSOAP API、25以上のエンドポイント |
| エラー原因の特定困難 | 申込エラー時の原因が不明瞭 | エラーコード数十種、原因がAPI横断的 |
| 一括処理の信頼性 | バッチ処理中の部分失敗 | 1回の一括処理で数百〜数千件を処理 |
| 複数ライフラインの統合 | 電気・ガス・水道で異なるAPI仕様 | 電力会社10社超 + ガス事業者 + 水道局 |
| 個人情報の保護 | 需要者名・住所等のセンシティブデータ | 全リクエスト/レスポンスのマスキング必須 |
特に難しかったのは、OCCTOのエラー原因の特定です。たとえば再点申込(IF_10410)がエラーになった場合、その原因が「供給地点特定番号の誤り」なのか「既に別事業者が申込済み」なのか「設備情報自体に問題がある」のかは、エラーメッセージだけでは判断できません。追加で設備情報照会(IF_10110)や事業者一覧取得(IS_30110)を呼び出して原因を切り分ける必要がありました。
3. 設計
全体アーキテクチャ
graph TB
SF[Salesforce CRM] -->|REST API| APP[Laravel API Server]
APP -->|SOAP / SSL相互認証| OCCTO[OCCTO スイッチング支援システム]
APP -->|REST API| GAS[ガス事業者API]
APP -->|Web自動化| WATER[水道局システム]
APP --> DB[(MySQL / RDB)]
APP --> LOG[監査ログ]
subgraph "OCCTO SOAP APIs"
IF10110[IF_10110 設備情報照会]
IF10410[IF_10410 再点申込]
IF10510[IF_10510 廃止申込]
IF10420[IF_10420 再点状態照会]
IF10520[IF_10520 廃止状態照会]
IF10820[IF_10820 需要者情報変更]
IF11110[IF_11110 申込一括照会]
IS30110[IS_30110 事業者一覧取得]
end
OCCTO --- IF10110
OCCTO --- IF10410
OCCTO --- IF10510
OCCTO --- IF10420
OCCTO --- IF10520
OCCTO --- IF10820
OCCTO --- IF11110
OCCTO --- IS30110
技術選定
| 項目 | 選定技術 | 選定理由 |
|---|---|---|
| フレームワーク | Laravel (PHP) | SOAP拡張の成熟度、既存チームの習熟度 |
| SOAP通信 | カスタムCurlSoapClient | PHP標準SoapClientのタイムアウト制御不足を補完 |
| SSL認証 | クライアント証明書 (.pem/.key) | OCCTO仕様書で必須のSSL相互認証 |
| テスト | PHPUnit + モックレスポンス | OCCTO APIのXMLレスポンスを再現 |
| バリデーション | DTO + FormRequest | OCCTO仕様書の必須項目を型安全に検証 |
Go や Python ではなく PHP/Laravel を選んだのは、SOAPの取り扱いやすさが決定的な理由でした。OCCTOのAPIはWSDLベースのSOAPインターフェースで、PHPのSoapClientはWSDLの自動パース・型マッピングをネイティブでサポートしています。
エラー自動調査アーキテクチャ
本システムの設計上の最大の特徴は、エラー発生時に追加のAPI呼び出しで原因を自動調査する仕組みです。
flowchart TD
REQ[再点/廃止申込] --> CALL[OCCTO API呼び出し]
CALL --> OK{成功?}
OK -->|Yes| SUCCESS[SF更新: 成功]
OK -->|No| INVESTIGATE[エラー調査開始]
INVESTIGATE --> EQ_CALL[設備情報照会 IF_10110]
EQ_CALL --> EQ_OK{照会成功?}
EQ_OK -->|失敗| EQ_FAIL[equipment_inquiry_failed<br/>設備情報も取得不可]
EQ_OK -->|エラーあり| EQ_ERR[equipment_error<br/>供給地点に問題あり]
EQ_OK -->|正常| BIZ_CALL[事業者一覧 IS_30110]
BIZ_CALL --> BIZ_CHECK{他事業者の<br/>申込あり?}
BIZ_CHECK -->|Yes| CONFLICT[already_applied<br/>他社申込済み]
BIZ_CHECK -->|No| UNKNOWN[original_error<br/>OCCTO原文を返却]
EQ_FAIL --> SF_UPDATE[Salesforce更新: エラー + 調査結果]
EQ_ERR --> SF_UPDATE
CONFLICT --> SF_UPDATE
UNKNOWN --> SF_UPDATE
この設計により、オペレーターは「なぜエラーになったか」の調査を手作業で行う必要がなくなりました。
4. 実装
SOAP通信の基盤クラス
OCCTOのSOAP APIは8つのサービスIDを持ち、すべてSSL相互認証が必要です。基盤クラスでこれらを一元管理しています。
class OCCTOBaseService
{
protected const SERVICE_ID_MAP = [
'requestSetsubiYokyuT' => 'IF_10110', // 設備情報照会
'requestSaitenYokyuT' => 'IF_10410', // 再点申込
'requestHaishiYokyuT' => 'IF_10510', // 廃止申込
'requestSaitenIdoYokyuT' => 'IF_10420', // 再点状態照会
'requestHaishiIdoYokyuT' => 'IF_10520', // 廃止状態照会
'requestJuyoshaHenkoYokyuT' => 'IF_10820', // 需要者情報変更
'requestIdoIkkatsuYokyuT' => 'IF_11110', // 申込一括照会
'requestJigyoshaIchiranYokyu' => 'IS_30110', // 事業者一覧
];
// 抜粋 — 実際は住所・連絡先等を含む十数項目
protected const SENSITIVE_KEYS = [
'juyoshaShimeiKanji', // 需要者氏名(漢字)
'juyoshaShimeiKana', // 需要者氏名(カナ)
'moshikomiRenrakuTel', // 連絡先電話番号
];
}
エラー自動調査の実装
エラー発生時に追加APIを呼び出して原因を特定する OcctoErrorInvestigator の設計です。
class OcctoErrorInvestigator
{
public function __construct(
private EquipmentInformationService $equipmentService,
private BusinessOperatorInformationService $businessService,
) {}
public function investigate(
string $supplyPointId,
string $originalError
): array {
// Step 1: 設備情報照会で供給地点の状態を確認
$equipment = $this->equipmentService
->inquire($supplyPointId);
if ($equipment['status'] === 'failed') {
return [
'reason' => 'equipment_inquiry_failed',
'message' => "設備情報の確認もできませんでした。"
. "OCCTO原文: {$originalError}",
];
}
if ($equipment['hasError']) {
return [
'reason' => 'equipment_error',
'message' => "この供給地点の設備情報に問題があります。"
. "OCCTO原文: {$originalError}",
];
}
// Step 2: 事業者一覧で他社申込の有無を確認
$operators = $this->businessService
->searchBySupplyPoint($supplyPointId);
if ($operators['hasConflict']) {
return [
'reason' => 'already_applied',
'message' => "他の事業者が申込済みです。",
];
}
// 原因不明:OCCTO原文をそのまま返却
return [
'reason' => 'original_error',
'message' => $originalError,
];
}
}
個人情報のマスキング
全SOAP通信のリクエスト/レスポンスをログに記録しますが、個人情報は自動マスキングしています。
protected function maskSensitiveData(array $data): array
{
$masked = $data;
foreach (self::SENSITIVE_KEYS as $key) {
if (isset($masked[$key]) && is_string($masked[$key])) {
// 先頭1文字のみ残す(ログ照合用。完全値はDB側で暗号化保持)
$masked[$key] = mb_substr($masked[$key], 0, 1)
. str_repeat('*', mb_strlen($masked[$key]) - 1);
}
}
return $masked;
}
// 入力: "田中太郎" --> 出力: "田***"
一括処理のバッチ設計
数百件の再点/廃止申込を一括処理する際、部分失敗に対応するバッチ設計を採用しています。
public function sfRelightBatch(Request $request): JsonResponse
{
$validated = $request->validate([
'records' => 'required|array|max:5000',
'records.*' => 'required|array',
]);
$results = [];
foreach ($validated['records'] as $record) {
try {
$result = $this->processRelight($record);
$results[] = ['status' => 'success', ...$result];
} catch (SoapFault $e) {
// 個別失敗はスキップし、残りを継続処理
$investigation = $this->errorInvestigator
->investigate(
$record['supplyPointId'],
$e->getMessage()
);
$results[] = [
'status' => 'error',
'investigation' => $investigation,
];
}
}
return response()->json([
'total' => count($results),
'success' => count(array_filter(
$results, fn($r) => $r['status'] === 'success'
)),
'errors' => count(array_filter(
$results, fn($r) => $r['status'] === 'error'
)),
'details' => $results,
]);
}
5. 結果(数値)
| 指標 | Before | After | 改善率 |
|---|---|---|---|
| 申込処理時間(1件あたり) | 約15分(手動入力) | 数秒(API自動化) | -99%超 |
| エラー原因特定時間 | 約30分(手動調査) | 即時(自動調査) | 大幅短縮 |
| 月間処理件数 | 数千件(人手上限) | 1万件超(自動化後) | 数倍 |
| 入力ミスによるエラー率 | 数%(手動起因) | 0.1%未満(バリデーション通過後) | -97%超 |
| OCCTO API応答の成功率 | — | 99%超(月間平均) | — |
特筆すべきはエラー自動調査の効果です。従来はOCCTOからエラーが返ると、オペレーターが手動で設備情報を照会し、他社の申込状況を確認し、原因を推測するという作業に30分以上かかっていました。自動調査により、エラーの原因がレスポンスに即座に含まれるようになり、オペレーターは「何をすべきか」にすぐ集中できるようになりました。
6. 展望
次に取り組む課題
- SSL証明書ローテーションの自動化: 現在は手動更新。証明書切替のブルーグリーン方式を一緒に設計してくれるエンジニアを探しています
- Salesforce Apex バッチとの統合強化: Laravel側とSalesforce側のバッチ処理の二重管理を解消し、ステータス同期を一元化する設計が必要です
- プロアクティブな監視基盤: APIレスポンスタイムの推移や、特定供給地点でのエラー集中を検知する監視を構築予定です
- LWCで動的トークスクリプトを実現した設計 — 同じライフライン業務の別側面、Salesforce LWCでのUI設計
- Claude CodeとGitHub Copilotを全員が使う開発チームの実態 — 本記事のシステムもAI駆動で開発しています
- Findyでカジュアル面談する
- 採用情報を見る
関連記事
—
採用情報
ClassLab では、年間15万件のライフライン申込を支える基盤の設計・改善を一緒に進めてくれるバックエンド/インフラエンジニアを募集しています。外部API連携や信頼性設計の経験を活かせるポジションです。
—
ClassLab Engineering チームメンバーが執筆しました。
>
ClassLab.では、一緒にプロダクトを作るエンジニアを募集しています。
カジュアル面談も大歓迎です!
>