Refundの制限と制約
Refundの制限と制約を理解することは、お客様の期待を管理し、スムーズな返金操作を確保するために重要です。このガイドは、すべての返金ルール、制約、ベストプラクティスをカバーします。
概要
OmiseのRefundsは、以下に基づくさまざまな制限の対象となります:
- Chargeステータス: 成功したchargesのみ返金可能
- 時間制約: 一部の決済方法には時間制限がある
- 金額制限: 元のcharge金額を超えることはできない
- 残高要件: アカウント残高が十分である必要がある
- 決済方法: 異なる方法には異なるルールがある
- 通貨: Refundsは元の通貨を使用する必要がある
- ネットワークルール: カードネットワークと銀行のポリシーが適用される
一般的な制限
Chargeステータス要件
| Chargeステータス | Refund可能? | 備考 |
|---|---|---|
successful | はい | 標準的なrefundシナリオ |
pending | いいえ | chargeが完了するまで待機 |
failed | いいえ | 資金がキャプチャされていない |
expired | いいえ | chargeが完了しなかった |
reversed | いいえ | すでに取り消された |
async function checkRefundEligibility(chargeId) {
const charge = await omise.charges.retrieve(chargeId);
if (charge.status !== 'successful') {
return {
eligible: false,
reason: `Charge status is ${charge.status}, must be successful`
};
}
if (charge.refunded) {
return {
eligible: false,
reason: 'Charge has already been fully refunded'
};
}
const remaining = charge.amount - charge.refunded_amount;
if (remaining === 0) {
return {
eligible: false,
reason: 'No remaining balance to refund'
};
}
return {
eligible: true,
remaining: remaining,
currency: charge.currency
};
}
金額の制限
最大Refund金額
- 元のcharge金額を超えることはできない
- すべてのrefundsの合計がcharge金額を超えることはできない
- 既存のrefundsを考慮する必要がある
最小Refund金額
- 通貨による
- THB: 20サタン(0.20 THB)最小
- USD: 1セント最小
- JPY: 1円最小
def validate_refund_amount(charge_id, refund_amount):
"""制限に対してrefund金額を検証"""
charge = omise.Charge.retrieve(charge_id)
# 最小金額を確認(通貨固有)
minimums = {
'thb': 20, # 0.20 THB
'usd': 1, # 0.01 USD
'jpy': 1, # 1 JPY
'sgd': 1, # 0.01 SGD
'eur': 1 # 0.01 EUR
}
min_amount = minimums.get(charge.currency.lower(), 1)
if refund_amount < min_amount:
raise ValueError(f"Refund amount below minimum of {min_amount} {charge.currency}")
# 最大金額を確認
remaining = charge.amount - charge.refunded_amount
if refund_amount > remaining:
raise ValueError(
f"Refund amount {refund_amount} exceeds remaining balance {remaining}"
)
return True
時間制限
一般的なタイムライン
- ほとんどのrefundsに厳密な時間制限はない
- 古いrefundsは却下率が高くなる可能性がある
- 推奨: 180日 以内にrefund
決済方法固有
- クレジットカード: 時間制限なし
- デビットカード: 時間制限なし
- インターネットバンキング: 通常90日
- モバイルバンキング: 通常90日
- E-wallets: プロバイダーによって異なる
def check_refund_timing(charge_id)
charge = Omise::Charge.retrieve(charge_id)
charge_age_days = (Time.now - Time.at(charge.created)) / 86400
warnings = []
# 年齢ベースの警告を確認
if charge_age_days > 180
warnings << "Charge is more than 180 days old - refund may have higher decline rate"
end
if charge_age_days > 365
warnings << "Charge is more than 1 year old - consider alternative compensation"
end
# 決済方法固有のチェック
case charge.source.type
when 'internet_banking'
if charge_age_days > 90
warnings << "Internet banking refunds are best within 90 days"
end
when 'mobile_banking'
if charge_age_days > 90
warnings << "Mobile banking refunds are best within 90 days"
end
end
{
charge_age_days: charge_age_days.round(1),
warnings: warnings,
recommended: charge_age_days <= 180
}
end
残高要件
残高不足
RefundsにはOmiseアカウント に十分な残高が必要です:
async function checkRefundBalance(chargeId, refundAmount) {
try {
// アカウント残高を取得
const balance = await omise.balance.retrieve();
// charge詳細を取得
const charge = await omise.charges.retrieve(chargeId);
// 十分な残高があるかを確認
if (balance.available < refundAmount) {
return {
canRefund: false,
reason: 'insufficient_balance',
available: balance.available,
required: refundAmount,
shortfall: refundAmount - balance.available,
nextSettlement: await getNextSettlementDate()
};
}
return {
canRefund: true,
available: balance.available,
afterRefund: balance.available - refundAmount
};
} catch (error) {
return {
canRefund: false,
reason: 'error',
error: error.message
};
}
}
決済方法の制限
Credit & Debit Cards
一般的なルール
- 元のカードにrefundする必要がある
- 別のカードにrefundすることはできない
- カードの有効期限はrefundsを妨げない
- 閉鎖されたアカウントは問題を引き起こす可能性がある
function validateCardRefund($charge) {
$restrictions = [
'can_refund_to_different_card' => false,
'requires_active_card' => false,
'time_limit_days' => null, // 時間制限なし
'min_amount' => 1, // 1 cent/satang
'max_refund_count' => null // 無制限
];
// カードが期限切れかを確認
if (isset($charge['card'])) {
$currentYear = intval(date('Y'));
$currentMonth = intval(date('m'));
$cardYear = intval($charge['card']['expiration_year']);
$cardMonth = intval($charge['card']['expiration_month']);
if ($cardYear < $currentYear ||
($cardYear == $currentYear && $cardMonth < $currentMonth)) {
$restrictions['warnings'][] = 'Card has expired - refund may still work';
}
}
return $restrictions;
}
Internet Banking
制限
- 90日間の推奨ウィンドウ
- 銀行固有のルールが適用される場合がある
- 一部の銀行はrefundsをサポートしていない
- お客様の再認証が必要な場合がある
def validate_internet_banking_refund(charge)
restrictions = {
recommended_time_limit: 90, # 日
requires_active_account: true,
bank_specific_rules: true
}
# 既知の制限がある銀行
limited_banks = {
'bay' => { max_days: 90, notes: 'Strict 90-day limit' },
'bbl' => { max_days: 180, notes: 'Longer window available' }
}
if charge.source.type == 'internet_banking'
bank_code = charge.source.bank_code
if limited_banks.key?(bank_code)
restrictions[:bank_limits] = limited_banks[bank_code]
end
end
restrictions
end
Mobile Banking & E-Wallets
一般的な制限
- プロバイダー固有の制限
- アカウントがアクティブである必要がある
- お客様の確認が必要な場合がある
- 異なる期間ウィンドウ
const PAYMENT_METHOD_LIMITS = {
'promptpay': {
timeLimit: 180,
canRefund: true,
requiresActiveAccount: true,
notes: 'PromptPay refunds require active registration'
},
'truemoney': {
timeLimit: 90,
canRefund: true,
requiresActiveAccount: true,
notes: 'TrueMoney wallet must be active'
},
'alipay': {
timeLimit: 180,
canRefund: true,
requiresActiveAccount: true,
notes: 'Alipay account must be accessible'
}
};
function getPaymentMethodLimits(paymentMethod) {
return PAYMENT_METHOD_LIMITS[paymentMethod] || {
timeLimit: 180,
canRefund: true,
requiresActiveAccount: false,
notes: 'Standard refund rules apply'
};
}
通貨の制限
同じ通貨要件
Refundsは元のcharge通貨を使用する必要があります:
def validate_refund_currency(charge_id, refund_amount, refund_currency=None):
"""refund通貨がcharge通貨と一致することを検証"""
charge = omise.Charge.retrieve(charge_id)
# 通貨は自動的に継承されるが、指定されている場合は検証
if refund_currency and refund_currency.lower() != charge.currency.lower():
raise ValueError(
f"Refund currency {refund_currency} must match charge currency {charge.currency}"
)
# 通貨固有の検証
currency_rules = {
'thb': {
'decimal_places': 2,
'min_amount': 20, # 0.20 THB
'unit_name': 'satang'
},
'usd': {
'decimal_places': 2,
'min_amount': 1, # 0.01 USD
'unit_name': 'cent'
},
'jpy': {
'decimal_places': 0,
'min_amount': 1, # 1 JPY (小数なし)
'unit_name': 'yen'
}
}
rules = currency_rules.get(charge.currency.lower(), {
'decimal_places': 2,
'min_amount': 1,
'unit_name': 'unit'
})
return {
'currency': charge.currency,
'amount_in_smallest_unit': refund_amount,
'rules': rules,
'valid': True
}
制限を扱うためのベストプラクティス
1. Refund前の検証
class RefundValidator {
public static function validateRefund($chargeId, $amount, $metadata = []) {
$errors = [];
$warnings = [];
try {
$charge = OmiseCharge::retrieve($chargeId);
// ステータスチェック
if ($charge['status'] !== 'successful') {
$errors[] = "Charge status is {$charge['status']}, must be successful";
}
// 金額チェック
$remaining = $charge['amount'] - $charge['refunded_amount'];
if ($amount > $remaining) {
$errors[] = "Amount {$amount} exceeds remaining balance {$remaining}";
}
// 時間チェック
$ageInDays = (time() - $charge['created']) / 86400;
if ($ageInDays > 180) {
$warnings[] = "Charge is {$ageInDays} days old - may have higher decline rate";
}
return [
'valid' => empty($errors),
'errors' => $errors,
'warnings' => $warnings,
'charge' => $charge,
'remaining' => $remaining
];
} catch (Exception $e) {
return [
'valid' => false,
'errors' => [$e->getMessage()],
'warnings' => []
];
}
}
}
2. 段階的劣化
class RefundWithFallback
def self.attempt_refund(charge_id, amount, options = {})
max_retries = options[:max_retries] || 3
retry_count = 0
begin
# 最初に検証
validation = validate_refund(charge_id, amount)
unless validation[:valid]
return {
success: false,
reason: 'validation_failed',
errors: validation[:errors]
}
end
# refundを試みる
charge = Omise::Charge.retrieve(charge_id)
refund = charge.refund(amount: amount, metadata: options[:metadata])
{
success: true,
refund: refund,
warnings: validation[:warnings]
}
rescue Omise::InvalidRequestError => e
if e.message.include?('insufficient') && retry_count < max_retries
# 後でキューに入れる
queue_refund(charge_id, amount, options)
{
success: false,
reason: 'insufficient_balance',
queued: true,
message: 'Refund queued for when balance is available'
}
else
{
success: false,
reason: 'invalid_request',
error: e.message
}
end
end
end
end
3. ユーザーフレンドリーなエラーメッセージ
def get_user_friendly_error(error_code, context={}):
"""技術的なエラーをユーザーフレンドリーなメッセージに変換"""
messages = {
'charge_already_refunded': {
'user': 'この支払いはすでに返金されています。',
'action': '詳細については返金履歴を確認してください。'
},
'insufficient_fund': {
'user': 'アカウント残高のため、現時点でrefundを処理できません。',
'action': f"refundは{context.get('wait_days', 2)}営業日以内に自動的に処理されます。"
},
'invalid_charge': {
'user': 'この支払いが見つからないか無効です。',
'action': '支払いIDを確認して再試行してください。'
},
'charge_not_paid': {
'user': 'この支払いはまだ完了していません。',
'action': 'Refundsは成功した支払いに対してのみ作成できます。'
},
'amount_exceeds_refundable': {
'user': f"refund金額が利用可能な残高{context.get('remaining', 'unknown')}を超えています。",
'action': 'より低い金額を入力するか、サポートに連絡してください。'
}
}
return messages.get(error_code, {
'user': 'refundの処理中にエラーが発生しま した。',
'action': '再試行するか、問題が解決しない場合はサポートに連絡してください。'
})
FAQ
pendingのchargeを返金できないのはなぜですか?
Pendingのchargesはまだ完全に処理されていません。refundを作成する前に、chargeがsuccessfulステータスに達するのを待つ必要があります。pendingのchargeをキャンセルする必要がある場合は、chargeキャンセルエンドポイントを使用してください。