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

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キャンセルエンドポイントを使用してください。

charge金額より多く返金しようとするとどうなりますか?

APIは、refund金額が利用可能な残高を超えていることを示すエラーを返します。すべての以前のrefundsを含む元のcharge金額より多く返金することはできません。

お客様のカードが期限切れの場合にchargeを返金できますか?

はい、ほとんどの場合。カードネットワークは通常、期限切れのカードへのrefundsを許可し、資金は依然としてお客様のアカウントに到達します。ただし、アカウントが閉鎖されている場合、refundが失敗する可能性があります。

お客様の銀行アカウントが閉鎖されている場合はどうなりますか?

閉鎖されたアカウントへの返金の場合、refundは通常失敗するか、銀行によって拒否されます。新しいアカウントへの手動銀行振込やストアクレジットなど、代替の補償を手配する必要があります。

関連リソース

次のステップ