Skip to content
Open
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
9 changes: 9 additions & 0 deletions public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,12 @@
"warning": "Warning: Don't use send invite if it's invalid email. use add to Split Pro instead. Your account will be blocked if this feature is misused"
},
"split_type_section": {
"direction": {
"no_money_flow": "No one else owes anything",
"owes_payer": "{{debtor}} owes {{payer}}",
"owes_you": "{{debtor}} owes you",
"you_owe": "You owe {{payer}}"
},
"split_equally": "split equally",
"split_unequally": "split unequally",
"types": {
Expand All @@ -213,6 +219,9 @@
"title": "Share",
"total_shares": "Total shares"
}
},
"validation": {
"invalid_split": "Adjust who owes this expense before saving."
}
},
"sponsor_us": "Sponsor us",
Expand Down
9 changes: 1 addition & 8 deletions src/components/AddExpense/AddExpensePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ export const AddOrEditExpensePage: React.FC<{
setAmount,
setAmountStr,
resetState,
setSplitScreenOpen,
setExpenseDate,
setMultipleTransactions,
setIsTransactionLoading,
Expand Down Expand Up @@ -110,11 +109,6 @@ export const AddOrEditExpensePage: React.FC<{
return;
}

if (!isExpenseSettled) {
setSplitScreenOpen(true);
return;
}

setMultipleTransactions([]);
setIsTransactionLoading(false);

Expand Down Expand Up @@ -196,7 +190,6 @@ export const AddOrEditExpensePage: React.FC<{
}
}
}, [
setSplitScreenOpen,
description,
currency,
isNegative,
Expand All @@ -212,7 +205,6 @@ export const AddOrEditExpensePage: React.FC<{
paidBy,
splitType,
fileKey,
isExpenseSettled,
setMultipleTransactions,
transactionId,
setIsTransactionLoading,
Expand Down Expand Up @@ -351,6 +343,7 @@ export const AddOrEditExpensePage: React.FC<{
splitShares,
paidBy,
currentUser,
isNegative,
)}
</Button>
</SplitExpenseForm>
Expand Down
5 changes: 5 additions & 0 deletions src/components/AddExpense/SplitTypeSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,11 @@ const SplitSection: React.FC<SplitSectionProps> = (props) => {
>
{fmtSummartyText(amount, totalShares, toUIString)}
</p>
{!canSplitScreenClosed ? (
<p className="text-center text-xs text-red-500">
{t('expense_details.add_expense_details.split_type_section.validation.invalid_split')}
</p>
) : null}
{isBoolean && (
<Button
variant="outline"
Expand Down
12 changes: 9 additions & 3 deletions src/store/addStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,9 +296,7 @@ export function calculateParticipantSplit(
...p,
amount: 0n === getSplitShare(p) ? 0n : amount / BigInt(totalParticipants),
}));
canSplitScreenClosed = Boolean(
Object.values(splitShares).find((p) => 0n !== p[SplitType.EQUAL]),
);
canSplitScreenClosed = 0 < totalParticipants;
break;
case SplitType.PERCENTAGE:
updatedParticipants = participants.map((p) => ({
Expand Down Expand Up @@ -371,6 +369,14 @@ export function calculateParticipantSplit(
}
}
}

if (
canSplitScreenClosed &&
1 < participants.length &&
updatedParticipants.every((p) => 0n === (p.amount ?? 0n))
) {
canSplitScreenClosed = false;
}
Comment on lines 299 to +379

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Care to explain why do you need to refactor such a huge chunk of the core logic? AFAIK all you want is to check an additional edge case, where all the participant.amount fields are zero.

}

return { ...state, participants: updatedParticipants, canSplitScreenClosed };
Expand Down
58 changes: 58 additions & 0 deletions src/tests/addStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,64 @@ describe('calculateParticipantSplit', () => {
expect(result.participants[2]?.amount).toBe(0n); // Excluded from split
});

it('should allow one non-payer to owe the full amount', () => {
const participants = createParticipants([user1, user2]);
const splitShares = createSplitShares(participants, SplitType.EQUAL, [0n, 1n]);

const state: Partial<AddExpenseState> = {
amount: 10000n,
participants,
splitType: SplitType.EQUAL,
splitShares,
paidBy: user1,
expenseDate: new Date('2024-01-01'),
};

const result = calculateParticipantSplit(state as AddExpenseState);

expect(result.participants[0]?.amount).toBe(10000n);
expect(result.participants[1]?.amount).toBe(-10000n);
expect(result.canSplitScreenClosed).toBe(true);
});

it('should mark a multi-person self-only split as incomplete', () => {
const participants = createParticipants([user1, user2]);
const splitShares = createSplitShares(participants, SplitType.EQUAL, [1n, 0n]);

const state: Partial<AddExpenseState> = {
amount: 10000n,
participants,
splitType: SplitType.EQUAL,
splitShares,
paidBy: user1,
expenseDate: new Date('2024-01-01'),
};

const result = calculateParticipantSplit(state as AddExpenseState);

expect(result.participants[0]?.amount).toBe(0n);
expect(result.participants[1]?.amount).toBe(0n);
expect(result.canSplitScreenClosed).toBe(false);
});

it('should mark equal split incomplete when every share is disabled', () => {
const participants = createParticipants([user1, user2]);
const splitShares = createSplitShares(participants, SplitType.EQUAL, [0n, 0n]);

const state: Partial<AddExpenseState> = {
amount: 10000n,
participants,
splitType: SplitType.EQUAL,
splitShares,
paidBy: user1,
expenseDate: new Date('2024-01-01'),
};

const result = calculateParticipantSplit(state as AddExpenseState);

expect(result.canSplitScreenClosed).toBe(false);
});

it('should handle uneven division with penny adjustment', () => {
const participants = createParticipants([user1, user2, user3]);
const splitShares = createSplitShares(participants, SplitType.EQUAL, [1n, 1n, 1n]);
Expand Down
54 changes: 35 additions & 19 deletions src/utils/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,42 +52,58 @@ export function generateSplitDescription(
splitShares: AddExpenseState['splitShares'],
paidBy?: Participant,
currentUser?: Participant,
isNegative = false,
): string {
// Only enhance the description for EQUAL split type
if (splitType !== SplitType.EQUAL) {
if (SplitType.EQUAL !== splitType) {
return t('expense_details.add_expense_details.split_type_section.split_unequally');
}

const splitEquallyText = t(
'expense_details.add_expense_details.split_type_section.split_equally',
);
if (!paidBy || !currentUser) {
return t('expense_details.add_expense_details.split_type_section.split_equally');
return splitEquallyText;
}

// Get participants who are actually selected for the split (have non-zero shares)
// If split shares are not initialized yet (undefined), include all participants
const selectedParticipants = participants.filter((p) => {
const share = splitShares[p.id]?.[SplitType.EQUAL];
return share === undefined || share !== 0n;
return share === undefined || 0n !== share;
});

// If no one is selected, fall back to default
if (selectedParticipants.length === 0) {
return t('expense_details.add_expense_details.split_type_section.split_equally');
const splitParticipant = selectedParticipants[0];
if (!splitParticipant) {
return splitEquallyText;
}

// Case 1: Paying for exactly one person
if (selectedParticipants.length === 1) {
const beneficiary = selectedParticipants[0];
const beneficiaryName = displayName(t, beneficiary as User, currentUser.id) || 'someone';
return `${t('ui.expense.user.paid')} ${t('ui.expense.for')} ${beneficiaryName}`;
if (1 !== selectedParticipants.length) {
return `${splitEquallyText} (${selectedParticipants.length})`;
}

// Case 2: Splitting with multiple people
if (selectedParticipants.length > 1) {
return `${t('expense_details.add_expense_details.split_type_section.split_equally')} (${selectedParticipants.length})`;
if (splitParticipant.id === paidBy.id) {
return t('expense_details.add_expense_details.split_type_section.direction.no_money_flow');
}

// Fallback to default for all other cases
return t('expense_details.add_expense_details.split_type_section.split_equally');
const debtor = isNegative ? paidBy : splitParticipant;
const payer = isNegative ? splitParticipant : paidBy;
const debtorName = displayName(t, debtor, currentUser.id);
const payerName = displayName(t, payer, currentUser.id);

if (payer.id === currentUser.id) {
return t('expense_details.add_expense_details.split_type_section.direction.owes_you', {
debtor: debtorName,
});
}

if (debtor.id === currentUser.id) {
return t('expense_details.add_expense_details.split_type_section.direction.you_owe', {
payer: payerName,
});
}

return t('expense_details.add_expense_details.split_type_section.direction.owes_payer', {
debtor: debtorName,
payer: payerName,
});
}

export function getCurrencyName(t: TFunction, code: CurrencyCode, plural = false): string {
Expand Down
Loading