feat: 결제 금액 계산 유틸리티 추가
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'package:intl/intl.dart';
|
||||
import '../models/subscription_model.dart';
|
||||
import '../utils/billing_cost_util.dart';
|
||||
import 'exchange_rate_service.dart';
|
||||
import 'cache_manager.dart';
|
||||
|
||||
@@ -129,7 +130,8 @@ class CurrencyUtil {
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 구독 목록의 총 월 비용을 계산 (언어별 기본 통화로)
|
||||
/// 구독 목록의 이번 달 총 비용을 계산 (언어별 기본 통화로)
|
||||
/// 이번 달에 결제가 발생하는 구독만 포함하며, 실제 결제 금액을 사용
|
||||
static Future<double> calculateTotalMonthlyExpenseInDefaultCurrency(
|
||||
List<SubscriptionModel> subscriptions,
|
||||
String locale,
|
||||
@@ -137,16 +139,33 @@ class CurrencyUtil {
|
||||
final defaultCurrency = getDefaultCurrency(locale);
|
||||
double total = 0.0;
|
||||
|
||||
final now = DateTime.now();
|
||||
final currentYear = now.year;
|
||||
final currentMonth = now.month;
|
||||
|
||||
for (var subscription in subscriptions) {
|
||||
final price = subscription.currentPrice;
|
||||
// 이번 달에 결제가 발생하는지 확인
|
||||
final hasBilling = BillingCostUtil.hasBillingInMonth(
|
||||
subscription.nextBillingDate,
|
||||
subscription.billingCycle,
|
||||
currentYear,
|
||||
currentMonth,
|
||||
);
|
||||
if (!hasBilling) continue;
|
||||
|
||||
// 실제 결제 금액으로 역변환
|
||||
final actualPrice = BillingCostUtil.convertFromMonthlyCost(
|
||||
subscription.currentPrice,
|
||||
subscription.billingCycle,
|
||||
);
|
||||
|
||||
final converted = await _exchangeRateService.convertBetweenCurrencies(
|
||||
price,
|
||||
actualPrice,
|
||||
subscription.currency,
|
||||
defaultCurrency,
|
||||
);
|
||||
|
||||
total += converted ?? price;
|
||||
total += converted ?? actualPrice;
|
||||
}
|
||||
|
||||
return total;
|
||||
@@ -158,17 +177,46 @@ class CurrencyUtil {
|
||||
return calculateTotalMonthlyExpenseInDefaultCurrency(subscriptions, 'ko');
|
||||
}
|
||||
|
||||
/// 구독 목록의 예상 연간 총 비용을 계산 (언어별 기본 통화로)
|
||||
/// 모든 구독의 연간 비용을 합산 (월 환산 비용 × 12)
|
||||
static Future<double> calculateTotalAnnualExpenseInDefaultCurrency(
|
||||
List<SubscriptionModel> subscriptions,
|
||||
String locale,
|
||||
) async {
|
||||
final defaultCurrency = getDefaultCurrency(locale);
|
||||
double total = 0.0;
|
||||
|
||||
for (var subscription in subscriptions) {
|
||||
// 월 환산 비용 × 12 = 연간 비용
|
||||
final annualPrice = subscription.currentPrice * 12;
|
||||
|
||||
final converted = await _exchangeRateService.convertBetweenCurrencies(
|
||||
annualPrice,
|
||||
subscription.currency,
|
||||
defaultCurrency,
|
||||
);
|
||||
|
||||
total += converted ?? annualPrice;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
/// 구독의 월 비용을 표시 형식에 맞게 변환 (언어별 통화)
|
||||
static Future<String> formatSubscriptionAmountWithLocale(
|
||||
SubscriptionModel subscription, String locale) async {
|
||||
final price = subscription.currentPrice;
|
||||
// 구독 단위 캐시 키 (통화/가격/locale + id)
|
||||
// 월 환산 금액을 실제 결제 금액으로 역변환
|
||||
final price = BillingCostUtil.convertFromMonthlyCost(
|
||||
subscription.currentPrice,
|
||||
subscription.billingCycle,
|
||||
);
|
||||
// 구독 단위 캐시 키 (통화/가격/locale + id + billingCycle)
|
||||
final decimals =
|
||||
(subscription.currency == 'KRW' || subscription.currency == 'JPY')
|
||||
? 0
|
||||
: 2;
|
||||
final key =
|
||||
'subfmt:$locale:${subscription.currency}:${price.toStringAsFixed(decimals)}:${subscription.id}';
|
||||
'subfmt:$locale:${subscription.currency}:${price.toStringAsFixed(decimals)}:${subscription.id}:${subscription.billingCycle}';
|
||||
final cached = _fmtCache.get(key);
|
||||
if (cached != null) return cached;
|
||||
|
||||
|
||||
248
lib/utils/billing_cost_util.dart
Normal file
248
lib/utils/billing_cost_util.dart
Normal file
@@ -0,0 +1,248 @@
|
||||
/// 결제 주기에 따른 비용 변환 유틸리티
|
||||
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 기준으로 역산)
|
||||
final billingMonth = nextBillingDate.month;
|
||||
|
||||
// 대상 월이 결제 발생 월과 일치하는지 확인
|
||||
// 예: 연간 결제(1월), targetMonth = 1 → true
|
||||
// 예: 연간 결제(1월), targetMonth = 2 → false
|
||||
for (int i = 0; i < 12; i += cycleMonths) {
|
||||
final checkMonth = ((billingMonth - 1 + i) % 12) + 1;
|
||||
if (checkMonth == targetMonth) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 결제 주기별 개월 수 반환
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user