Files
submanager/lib/utils/billing_cost_util.dart
JiWoong Sul 903906c880 fix(billing): 월별 비용 계산 시 연도 무시 버그 수정
- hasBillingInMonth()에서 targetYear를 실제로 사용하도록 수정
- 연간 구독 수정 시 잘못된 월에 비용이 포함되던 문제 해결
- 연도+월을 포함한 개월 차이 계산으로 정확한 결제 발생 여부 판단
2026-01-29 23:55:57 +09:00

249 lines
7.0 KiB
Dart

/// 결제 주기에 따른 비용 변환 유틸리티
class BillingCostUtil {
/// 결제 주기별 비용을 월 비용으로 변환
///
/// [amount]: 입력된 비용
/// [billingCycle]: 결제 주기 ('monthly', 'yearly', 'quarterly', 'half-yearly' 등)
///
/// Returns: 월 환산 비용
static double convertToMonthlyCost(double amount, String billingCycle) {
final normalizedCycle = _normalizeBillingCycle(billingCycle);
switch (normalizedCycle) {
case 'monthly':
return amount;
case 'yearly':
return amount / 12;
case 'quarterly':
return amount / 3;
case 'half-yearly':
return amount / 6;
case 'weekly':
return amount * 4.33; // 평균 주당 4.33주
default:
return amount; // 알 수 없는 주기는 그대로 반환
}
}
/// 월 비용을 결제 주기별 비용으로 역변환
///
/// [monthlyCost]: 월 비용
/// [billingCycle]: 결제 주기
///
/// Returns: 해당 주기의 실제 결제 금액
static double convertFromMonthlyCost(double monthlyCost, String billingCycle) {
final normalizedCycle = _normalizeBillingCycle(billingCycle);
switch (normalizedCycle) {
case 'monthly':
return monthlyCost;
case 'yearly':
return monthlyCost * 12;
case 'quarterly':
return monthlyCost * 3;
case 'half-yearly':
return monthlyCost * 6;
case 'weekly':
return monthlyCost / 4.33;
default:
return monthlyCost;
}
}
/// 결제 주기를 정규화된 영어 키값으로 변환
static String _normalizeBillingCycle(String cycle) {
switch (cycle.toLowerCase()) {
case 'monthly':
case '월간':
case '매월':
case '月間':
case '月付':
case '每月':
case '毎月':
return 'monthly';
case 'yearly':
case 'annual':
case 'annually':
case '연간':
case '매년':
case '年間':
case '年付':
case '每年':
return 'yearly';
case 'quarterly':
case 'quarter':
case '분기별':
case '분기':
case '季付':
case '季度付':
case '四半期':
case '每季度':
return 'quarterly';
case 'half-yearly':
case 'half yearly':
case 'semiannual':
case 'semi-annual':
case '반기별':
case '半年付':
case '半年払い':
case '半年ごと':
case '每半年':
return 'half-yearly';
case 'weekly':
case '주간':
case '週間':
case '周付':
case '每周':
return 'weekly';
default:
return 'monthly';
}
}
/// 결제 주기의 배수 반환 (월 기준)
///
/// 예: yearly = 12, quarterly = 3
static double getBillingCycleMultiplier(String billingCycle) {
final normalizedCycle = _normalizeBillingCycle(billingCycle);
switch (normalizedCycle) {
case 'monthly':
return 1.0;
case 'yearly':
return 12.0;
case 'quarterly':
return 3.0;
case 'half-yearly':
return 6.0;
case 'weekly':
return 1 / 4.33;
default:
return 1.0;
}
}
/// 다음 결제일에서 이전 결제일 계산
///
/// [nextBillingDate]: 다음 결제 예정일
/// [billingCycle]: 결제 주기
///
/// Returns: 이전 결제일 (마지막으로 결제가 발생한 날짜)
static DateTime getLastBillingDate(
DateTime nextBillingDate, String billingCycle) {
final normalizedCycle = _normalizeBillingCycle(billingCycle);
switch (normalizedCycle) {
case 'yearly':
return DateTime(
nextBillingDate.year - 1,
nextBillingDate.month,
nextBillingDate.day,
);
case 'half-yearly':
return DateTime(
nextBillingDate.month <= 6
? nextBillingDate.year - 1
: nextBillingDate.year,
nextBillingDate.month <= 6
? nextBillingDate.month + 6
: nextBillingDate.month - 6,
nextBillingDate.day,
);
case 'quarterly':
return DateTime(
nextBillingDate.month <= 3
? nextBillingDate.year - 1
: nextBillingDate.year,
nextBillingDate.month <= 3
? nextBillingDate.month + 9
: nextBillingDate.month - 3,
nextBillingDate.day,
);
case 'monthly':
return DateTime(
nextBillingDate.month == 1
? nextBillingDate.year - 1
: nextBillingDate.year,
nextBillingDate.month == 1 ? 12 : nextBillingDate.month - 1,
nextBillingDate.day,
);
case 'weekly':
return nextBillingDate.subtract(const Duration(days: 7));
default:
return DateTime(
nextBillingDate.month == 1
? nextBillingDate.year - 1
: nextBillingDate.year,
nextBillingDate.month == 1 ? 12 : nextBillingDate.month - 1,
nextBillingDate.day,
);
}
}
/// 특정 월에 결제가 발생하는지 확인
///
/// [nextBillingDate]: 다음 결제 예정일
/// [billingCycle]: 결제 주기
/// [targetYear]: 확인할 연도
/// [targetMonth]: 확인할 월 (1-12)
///
/// Returns: 해당 월에 결제가 발생하면 true
static bool hasBillingInMonth(
DateTime nextBillingDate,
String billingCycle,
int targetYear,
int targetMonth,
) {
final normalizedCycle = _normalizeBillingCycle(billingCycle);
// 주간 결제는 매주 발생하므로 항상 true
if (normalizedCycle == 'weekly') {
return true;
}
// 월간 결제는 매월 발생하므로 항상 true
if (normalizedCycle == 'monthly') {
return true;
}
// 결제 주기에 따른 개월 수
final cycleMonths = _getCycleMonths(normalizedCycle);
// 연도+월을 포함한 개월 차이 계산
// nextBillingDate와 target 월 사이의 차이가 cycleMonths의 배수인지 확인
final targetStart = DateTime(targetYear, targetMonth, 1);
final billingStart =
DateTime(nextBillingDate.year, nextBillingDate.month, 1);
final monthDiff = (billingStart.year - targetStart.year) * 12 +
(billingStart.month - targetStart.month);
// monthDiff가 cycleMonths의 배수이면 해당 월에 결제 발생
// 예: 연간 결제(2027-01), target=2026-01 → monthDiff=12, 12%12=0 → true (이전 결제)
// 예: 연간 결제(2027-01), target=2026-02 → monthDiff=11, 11%12≠0 → false
// 예: 연간 결제(2027-01), target=2027-01 → monthDiff=0, 0%12=0 → true
return monthDiff % cycleMonths == 0;
}
/// 결제 주기별 개월 수 반환
static int _getCycleMonths(String normalizedCycle) {
switch (normalizedCycle) {
case 'yearly':
return 12;
case 'half-yearly':
return 6;
case 'quarterly':
return 3;
case 'monthly':
return 1;
default:
return 1;
}
}
}