Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/split-meow-mixed-currency-guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@app/split-meow': patch
---

修正混幣行程(同一行程記到多種幣別)會把不同幣別金額直接相加、顯示錯誤總額與結算的問題;偵測到多幣別時改顯示警告並隱藏無效的總額與結算(含分享文字),引導使用者改用單一幣別記帳。
22 changes: 16 additions & 6 deletions apps/split-meow/src/components/HistoryTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ export function HistoryTab() {
const tripCurrency: CurrencyCode = tripExpenses[tripExpenses.length - 1]?.currency ?? currency;
// 取得單筆記錄的顯示幣別(優先使用記帳當下快照,舊資料 fallback 主導幣別)。
const expenseCurrency = (exp: ExpenseRecord): CurrencyCode => exp.currency ?? tripCurrency;
// 混幣行程:跨幣別直接相加的總額與結算為無效運算,改顯示警告而非誤導數字。
const isMixedCurrency = new Set(tripExpenses.map((exp) => expenseCurrency(exp))).size > 1;
Comment thread
s123104 marked this conversation as resolved.

// 計算各人餘額
const balances: Record<string, number> = {};
Expand Down Expand Up @@ -222,7 +224,9 @@ export function HistoryTab() {
const lines: string[] = [
`🐾 喵喵分帳 — ${tripName}`,
`${'─'.repeat(24)}`,
`💰 總花費:${formatAmount(totalSpent, tripCurrency)}`,
isMixedCurrency
? `⚠️ ${t('history.mixed_currency_warning')}`
: `💰 總花費:${formatAmount(totalSpent, tripCurrency)}`,
'',
];
if (tripExpenses.length > 0) {
Expand All @@ -239,7 +243,7 @@ export function HistoryTab() {
});
lines.push('');
}
if (settlements.length > 0) {
if (settlements.length > 0 && !isMixedCurrency) {
lines.push(`💸 結清方式`);
settlements.forEach((s) => {
const from = members.find((m) => m.id === s.from)?.name ?? s.from;
Expand Down Expand Up @@ -281,9 +285,15 @@ export function HistoryTab() {
<span className="text-xs font-medium uppercase tracking-widest text-primary mb-1 block">
{t('history.total_spent')}
</span>
<h2 className="text-4xl font-medium text-on-surface tracking-tight">
{formatAmount(totalSpent, tripCurrency)}
</h2>
{isMixedCurrency ? (
<p className="text-sm font-medium text-error leading-snug">
{t('history.mixed_currency_warning')}
</p>
) : (
<h2 className="text-4xl font-medium text-on-surface tracking-tight">
{formatAmount(totalSpent, tripCurrency)}
</h2>
)}
</div>
<div className="mt-6 flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
Expand Down Expand Up @@ -327,7 +337,7 @@ export function HistoryTab() {
</section>

{/* 結清方式 */}
{settlements.length > 0 && (
{settlements.length > 0 && !isMixedCurrency && (
Comment thread
s123104 marked this conversation as resolved.
<section className="mb-10">
<h3 className="text-xs font-medium uppercase tracking-widest text-outline px-2 mb-4">
{t('history.settlements')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,46 @@ describe('HistoryTab', () => {
// Should render settlement amounts somewhere
expect(document.body).toBeTruthy();
});

it('混幣行程顯示警告並隱藏總額與結算(避免跨幣別錯誤加總)', () => {
useStore.setState({
expenses: [
{ ...EXPENSE_1, id: 'mix-twd', currency: 'TWD', totalAmount: 300 },
{
id: 'mix-krw',
tripId: 'trip-1',
type: 'split_evenly' as const,
participantIds: ['me', 'm1'],
paidBy: 'm1',
totalAmount: 9000,
perPersonAmounts: { me: 4500, m1: 4500 },
note: '',
createdAt: Date.now() + 1,
currency: 'KRW',
},
],
});
renderWith(<HistoryTab />);
expect(screen.getByText(i18n.t('history.mixed_currency_warning'))).toBeInTheDocument();
// 結算區塊標題不應出現:跨幣別結算為無效運算,不得顯示誤導金額。
expect(screen.queryByText(i18n.t('history.settlements'))).not.toBeInTheDocument();
});

it('單一幣別行程不顯示混幣警告', () => {
useStore.setState({
expenses: [
{ ...EXPENSE_1, id: 'same-1', currency: 'TWD' },
{
...EXPENSE_1,
id: 'same-2',
currency: 'TWD',
paidBy: 'm1',
totalAmount: 60,
perPersonAmounts: { me: 30, m1: 30 },
},
],
});
renderWith(<HistoryTab />);
expect(screen.queryByText(i18n.t('history.mixed_currency_warning'))).not.toBeInTheDocument();
});
});
7 changes: 7 additions & 0 deletions apps/split-meow/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const resources = {
total_spent: '總花費',
members_count: '由 {{count}} 位探險家分攤',
settlements: '結清方式',
mixed_currency_warning: '此行程含多種幣別,無法合併總額與結算,請改用單一幣別記帳',
pay_to: '{{from}} 付給 {{to}}',
balances: '各人結算',
owed: '應收',
Expand Down Expand Up @@ -123,6 +124,8 @@ const resources = {
total_spent: 'Total Spent',
members_count: '{{count}} adventurers splitting',
settlements: 'How to Settle',
mixed_currency_warning:
'This trip mixes currencies; totals and settlement cannot be combined. Use a single currency.',
pay_to: '{{from}} pays {{to}}',
balances: 'Balances',
owed: 'Receives',
Expand Down Expand Up @@ -213,6 +216,8 @@ const resources = {
total_spent: '총 지출',
members_count: '{{count}}명이 분담',
settlements: '정산 방법',
mixed_currency_warning:
'이 여행에는 여러 통화가 섞여 있어 합계와 정산을 합칠 수 없습니다. 단일 통화를 사용하세요.',
pay_to: '{{from}} → {{to}}',
balances: '개인 정산',
owed: '받을 돈',
Expand Down Expand Up @@ -303,6 +308,8 @@ const resources = {
total_spent: '総支出',
members_count: '{{count}}人で割り勘',
settlements: '精算方法',
mixed_currency_warning:
'この旅行は複数の通貨が混在しており、合計と精算を統合できません。単一通貨を使用してください。',
pay_to: '{{from}} → {{to}}',
balances: '各自の精算',
owed: '受け取り',
Expand Down
Loading