メインコンテンツへスキップ

アカウント照合

アカウント照合は、Omise transactionレコードが銀行明細書および会計システムと一致することを保証します。このガイドでは、照合プロセス、自動照合、およびベストプラクティスについて説明します。

概要

照合は以下の目的で不可欠です:

  • 正確な会計: 財務記録が正確であることを保証
  • 不正検出: 不正なtransactionを特定
  • コンプライアンス: 規制要件を満たす
  • キャッシュフロー管理: 実際の資金と予想資金を追跡
  • エラー特定: 処理の問題を早期に発見
  • 監査証跡: 完全な財務記録を維持

主要な概念

  • Settlement: 銀行口座に支払われるtransactionのバッチ
  • Settlement Date: 資金が銀行に到着する日
  • Transaction Date: 顧客の支払いが発生した日
  • Net Amount: 手数料控除後のsettlement金額
  • Gross Amount: 手数料控除前の総payment金額

Settlementの理解

Settlementサイクル

const omise = require('omise')({
secretKey: 'skey_test_123456789',
});

// 最近のtransfer(settlements)を取得
async function getRecentSettlements(days = 30) {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);

const transfers = await omise.transfers.list({
from: startDate.toISOString(),
to: endDate.toISOString(),
limit: 100
});

console.log(`Found ${transfers.data.length} settlements`);

transfers.data.forEach(transfer => {
console.log(`Transfer ${transfer.id}:`);
console.log(` Amount: ${transfer.amount / 100} ${transfer.currency.toUpperCase()}`);
console.log(` Fee: ${transfer.fee / 100} ${transfer.currency.toUpperCase()}`);
console.log(` Date: ${new Date(transfer.created).toLocaleDateString()}`);
});

return transfers.data;
}

// settlement合計を計算
function calculateSettlementTotal(settlement) {
return {
gross_amount: settlement.amount,
fee: settlement.fee,
net_amount: settlement.amount - settlement.fee,
currency: settlement.currency,
formatted: {
gross: `${settlement.amount / 100} ${settlement.currency.toUpperCase()}`,
fee: `${settlement.fee / 100} ${settlement.currency.toUpperCase()}`,
net: `${(settlement.amount - settlement.fee) / 100} ${settlement.currency.toUpperCase()}`
}
};
}

// 使用例
getRecentSettlements(30);

照合プロセス

基本的な照合

class ReconciliationManager {
constructor() {
this.discrepancies = [];
}

async reconcileWithBankStatement(bankStatement, startDate, endDate) {
// Omise settlementsを取得
const settlements = await this.getOmiseSettlements(startDate, endDate);

// Omise transactionsを取得
const transactions = await this.getOmiseTransactions(startDate, endDate);

// settlementsを銀行エントリーと照合
const matchResults = this.matchSettlements(settlements, bankStatement);

// 照合レポートを生成
const report = this.generateReconciliationReport(
settlements,
transactions,
bankStatement,
matchResults
);

return report;
}

async getOmiseSettlements(startDate, endDate) {
const transfers = await omise.transfers.list({
from: startDate.toISOString(),
to: endDate.toISOString(),
limit: 100
});

return transfers.data.map(t => ({
id: t.id,
date: new Date(t.created),
amount: t.amount - t.fee, // 正味金額
gross: t.amount,
fee: t.fee,
currency: t.currency,
status: t.paid ? 'paid' : 'pending'
}));
}

async getOmiseTransactions(startDate, endDate) {
const transactions = await omise.transactions.list({
from: startDate.toISOString(),
to: endDate.toISOString(),
limit: 1000
});

return transactions.data;
}

matchSettlements(settlements, bankStatement) {
const matched = [];
const unmatchedOmise = [];
const unmatchedBank = [];

const bankEntries = [...bankStatement];

settlements.forEach(settlement => {
const match = this.findMatchingBankEntry(settlement, bankEntries);

if (match) {
matched.push({
settlement: settlement,
bankEntry: match.entry,
confidence: match.confidence
});
// 一致したエントリーを削除
bankEntries.splice(match.index, 1);
} else {
unmatchedOmise.push(settlement);
}
});

unmatchedBank.push(...bankEntries);

return {
matched,
unmatchedOmise,
unmatchedBank,
matchRate: (matched.length / settlements.length * 100).toFixed(2)
};
}

findMatchingBankEntry(settlement, bankEntries) {
for (let i = 0; i < bankEntries.length; i++) {
const entry = bankEntries[i];

// 金額で照合(手数料のわずかな差を許容)
const amountDiff = Math.abs(settlement.amount - entry.amount);

// 日付で照合(数日の差を許容)
const daysDiff = Math.abs(
(settlement.date - new Date(entry.date)) / (1000 * 60 * 60 * 24)
);

if (amountDiff < 100 && daysDiff <= 3) {
const confidence = this.calculateMatchConfidence(
amountDiff,
daysDiff
);

return {
entry: entry,
index: i,
confidence: confidence
};
}
}

return null;
}

calculateMatchConfidence(amountDiff, daysDiff) {
let confidence = 100;

// 金額差に基づいて信頼度を減らす
confidence -= (amountDiff / 100) * 5;

// 日付差に基づいて信頼度を減らす
confidence -= daysDiff * 10;

return Math.max(0, confidence);
}

generateReconciliationReport(settlements, transactions, bankStatement, matchResults) {
return {
period: {
start: settlements[0]?.date,
end: settlements[settlements.length - 1]?.date
},
summary: {
total_settlements: settlements.length,
total_bank_entries: bankStatement.length,
matched: matchResults.matched.length,
unmatched_omise: matchResults.unmatchedOmise.length,
unmatched_bank: matchResults.unmatchedBank.length,
match_rate: matchResults.matchRate + '%'
},
totals: {
omise_total: settlements.reduce((sum, s) => sum + s.amount, 0),
bank_total: bankStatement.reduce((sum, e) => sum + e.amount, 0),
difference: 0
},
matched: matchResults.matched,
discrepancies: {
unmatched_omise: matchResults.unmatchedOmise,
unmatched_bank: matchResults.unmatchedBank
}
};
}

printReconciliationReport(report) {
console.log('\n' + '='.repeat(60));
console.log('照合レポート');
console.log('='.repeat(60));
console.log(`期間: ${report.period.start.toLocaleDateString()} - ${report.period.end.toLocaleDateString()}`);
console.log('\n概要:');
console.log(` Omise Settlements: ${report.summary.total_settlements}`);
console.log(` 銀行エントリー: ${report.summary.total_bank_entries}`);
console.log(` 一致: ${report.summary.matched}`);
console.log(` 一致率: ${report.summary.match_rate}`);
console.log('\n不一致:');
console.log(` 未一致のOmise: ${report.summary.unmatched_omise}`);
console.log(` 未一致の銀行: ${report.summary.unmatched_bank}`);
console.log('='.repeat(60));
}
}

// 使用例
const reconciler = new ReconciliationManager();

const bankStatement = [
{ date: '2024-01-15', amount: 97500, reference: 'OMISE-001' },
{ date: '2024-01-22', amount: 195000, reference: 'OMISE-002' }
];

reconciler.reconcileWithBankStatement(
bankStatement,
new Date('2024-01-01'),
new Date('2024-01-31')
).then(report => {
reconciler.printReconciliationReport(report);
});

自動照合

日次照合スクリプト

<?php
require_once 'vendor/autoload.php';

class DailyReconciliation {
private $omiseKey;
private $emailRecipients;

public function __construct($omiseKey, $emailRecipients) {
$this->omiseKey = $omiseKey;
$this->emailRecipients = $emailRecipients;
}

public function runDailyReconciliation() {
// 昨日のデータを取得
$yesterday = date('Y-m-d', strtotime('-1 day'));

// 銀行明細書を取得(銀行システムから)
$bankStatement = $this->fetchBankStatement($yesterday);

// Omise settlementsを取得
$omiseSettlements = $this->fetchOmiseSettlements($yesterday);

// 照合
$report = $this->reconcile($omiseSettlements, $bankStatement);

// 不一致をチェック
if ($report['discrepancy_count'] > 0) {
$this->sendAlertEmail($report);
}

// レポートを保存
$this->saveReport($report);

return $report;
}

private function fetchOmiseSettlements($date) {
$startDate = date('c', strtotime($date . ' 00:00:00'));
$endDate = date('c', strtotime($date . ' 23:59:59'));

$transfers = OmiseTransfer::retrieve([
'from' => $startDate,
'to' => $endDate,
'limit' => 100
]);

return array_map(function($t) {
return [
'id' => $t['id'],
'date' => date('Y-m-d', $t['created']),
'amount' => $t['amount'] - $t['fee'],
'gross' => $t['amount'],
'fee' => $t['fee']
];
}, $transfers['data']);
}

private function fetchBankStatement($date) {
// ここに銀行API統合を実装
// これはプレースホルダーです
return [];
}

private function reconcile($omiseSettlements, $bankStatement) {
$matched = 0;
$unmatched = [];

foreach ($omiseSettlements as $settlement) {
$found = false;
foreach ($bankStatement as $entry) {
if ($this->isMatch($settlement, $entry)) {
$matched++;
$found = true;
break;
}
}

if (!$found) {
$unmatched[] = $settlement;
}
}

return [
'date' => date('Y-m-d'),
'total_settlements' => count($omiseSettlements),
'matched' => $matched,
'discrepancy_count' => count($unmatched),
'unmatched' => $unmatched
];
}

private function isMatch($settlement, $bankEntry) {
$amountMatch = abs($settlement['amount'] - $bankEntry['amount']) < 100;
$dateMatch = $settlement['date'] === $bankEntry['date'];

return $amountMatch && $dateMatch;
}

private function sendAlertEmail($report) {
$subject = "⚠️ 照合の不一致 - " . $report['date'];
$message = "{$report['discrepancy_count']}件の未一致のsettlementが見つかりました。\n\n";

foreach ($report['unmatched'] as $settlement) {
$message .= "Settlement {$settlement['id']}: " .
number_format($settlement['amount'] / 100, 2) . " THB\n";
}

foreach ($this->emailRecipients as $recipient) {
mail($recipient, $subject, $message);
}
}

private function saveReport($report) {
$filename = 'reconciliation_' . $report['date'] . '.json';
file_put_contents($filename, json_encode($report, JSON_PRETTY_PRINT));
}
}

// 日次照合を実行
$reconciliation = new DailyReconciliation(
'skey_test_123456789',
['finance@company.com', 'accounting@company.com']
);

$report = $reconciliation->runDailyReconciliation();
echo "照合が完了しました。一致: {$report['matched']}/{$report['total_settlements']}\n";
?>

ベストプラクティス

1. 日次照合の自動化

// 日次照合をスケジュール
const cron = require('node-cron');

class AutomatedReconciliation {
constructor() {
this.reconciler = new ReconciliationManager();
}

scheduleDailyReconciliation() {
// 毎日午前9時に実行
cron.schedule('0 9 * * *', async () => {
console.log('日次照合を開始しています...');

const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);

try {
const report = await this.runReconciliation(yesterday);

if (report.summary.unmatched_omise > 0 || report.summary.unmatched_bank > 0) {
await this.sendAlertNotification(report);
}

await this.saveReport(report);

console.log('日次照合が完了しました');
} catch (error) {
console.error('照合に失敗しました:', error);
await this.sendErrorNotification(error);
}
});
}

async runReconciliation(date) {
const bankStatement = await this.fetchBankStatement(date);

return await this.reconciler.reconcileWithBankStatement(
bankStatement,
date,
date
);
}

async sendAlertNotification(report) {
// メールまたはSlack通知を送信
console.log('⚠️ 照合に不一致が見つかりました');
}
}

const automated = new AutomatedReconciliation();
automated.scheduleDailyReconciliation();

2. 手数料差異の追跡

def analyze_fee_discrepancies(omise_settlements, bank_statement):
"""手数料関連の不一致を分析"""

discrepancies = []

for settlement in omise_settlements:
# 予想される銀行金額を計算(settlement金額 - Omise手数料)
expected_bank_amount = settlement['gross'] - settlement['fee']

# 対応する銀行エントリーを検索
bank_entry = find_bank_entry_by_date(settlement['date'], bank_statement)

if bank_entry:
actual_bank_amount = bank_entry['amount']
difference = expected_bank_amount - actual_bank_amount

if abs(difference) > 10: # 0.10 THBを超える差
discrepancies.append({
'settlement_id': settlement['id'],
'expected': expected_bank_amount / 100,
'actual': actual_bank_amount / 100,
'difference': difference / 100,
'possible_cause': analyze_difference(difference)
})

return discrepancies

def analyze_difference(difference):
"""差の原因を判断"""
if abs(difference) < 500: # 5 THB未満
return '端数処理の差異'
elif difference > 0:
return '銀行手数料または調整が不足'
else:
return '追加の銀行手数料が請求されました'

3. 監査証跡の維持

class ReconciliationAuditLog
def self.log_reconciliation(report, discrepancies = [])
log_entry = {
timestamp: Time.now.iso8601,
report_date: report[:period][:start].to_s,
match_rate: report[:summary][:match_rate],
total_settlements: report[:summary][:total_settlements],
matched: report[:summary][:matched],
unmatched: report[:summary][:unmatched_omise] + report[:summary][:unmatched_bank],
discrepancies: discrepancies.map { |d| d[:settlement_id] }
}

# 監査ログファイルに保存
File.open('reconciliation_audit.log', 'a') do |f|
f.puts JSON.generate(log_entry)
end

# レポート用にデータベースにも保存
save_to_database(log_entry)
end

def self.save_to_database(entry)
# データベースストレージを実装
end
end

FAQ

アカウントをどのくらいの頻度で照合すべきですか?

取引量の多いビジネスには日次照合をお勧めします。小規模ビジネスは週次または月次で照合できます。自動化された日次照合は、問題を早期に発見するのに役立ちます。

照合の不一致の原因は何ですか?

一般的な原因:

  • タイミングの違い(transaction dateとsettlement date)
  • 手数料の不一致
  • 銀行処理の遅延
  • 失敗したtransaction
  • 手動調整
  • 通貨換算の違い

GrossまたはNet金額を照合すべきですか?

Net金額(手数料控除後)を照合してください。これが銀行口座に表示される金額です。ただし、手数料計算が正しいことを確認するために、Gross金額を個別に追跡してください。

複数日のsettlement遅延にどう対処すべきですか?

TransactionをBankエントリーと照合する際、2〜3日の猶予を許可してください。Settlementは通常1〜2営業日かかりますが、銀行や支払い方法によって異なる場合があります。

Settlementが照合できない場合はどうすればよいですか?

次のように調査してください:

  1. まだpendingかどうかを確認
  2. 日付範囲を確認(別の期間にsettleされた可能性)
  3. 失敗またはreversedされたtransactionを確認
  4. Omiseサポートに問い合わせ

銀行明細のインポートを自動化できますか?

はい、ほとんどの銀行はAPIまたはファイルエクスポート(CSV、OFX)を提供しており、自動化できます。これらを照合システムと統合して、完全に自動化された照合を実現します。

照合レコードをどのくらいの期間保管すべきですか?

税務および監査のために、少なくとも7年間照合レコードを保管してください。適切なアクセス制御で安全に保管してください。

不一致を処理する最良の方法は何ですか?

すべての不一致を直ちに文書化し、24時間以内に調査し、解決のログを維持してください。特定のしきい値を超える不一致のアラートを設定してください。

関連リソース

次のステップ