Skip to content

fix: clearer unequal-split language and prevent silent save failure - #676

Open
mvanhorn wants to merge 6 commits into
oss-apps:mainfrom
mvanhorn:fix/645-2026-06-12-1741-fix-unequal-split-silent-save
Open

fix: clearer unequal-split language and prevent silent save failure#676
mvanhorn wants to merge 6 commits into
oss-apps:mainfrom
mvanhorn:fix/645-2026-06-12-1741-fix-unequal-split-silent-save

Conversation

@mvanhorn

@mvanhorn mvanhorn commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Description

Scopes the fix for #645 to the two things @krokosik was receptive to in the thread: the silent save failure and the ambiguous "paid for/by you" wording.

When a split left one person owed the full amount, the secondary screen's Save could no-op and the expense would silently disappear. This adds a blocking validation guard in addStore so Save is prevented with an inline message instead of discarding the expense, and rewords the directional labels so the direction of money is explicit. Split-definition semantics are unchanged — this only clarifies labels and adds the missing guard, staying inside the scope @krokosik agreed to (no rework of the core split modal to show money flows).

Fixes #645

Demo

Non-visual validation + wording change, covered by unit tests. src/tests/addStore.test.ts adds cases for the one-person-owed split's canSplitScreenClosed/validity flag and the adjustment-exceeds-total guard; the previously-vanishing expense now blocks Save with a validation error. SKIP_ENV_VALIDATION=true pnpm test passes (49/49). No jest config change is needed — CI's check.yml already exports SKIP_ENV_VALIDATION.

Checklist

  • I have read CONTRIBUTING.md in its entirety
  • I have performed a self-review of my own code
  • I have added unit tests to cover my changes
  • The last commit successfully passed pre-commit checks
  • Any AI code was thoroughly reviewed by me

Summary by CodeRabbit

  • New Features

    • Added clearer debt-direction labels for expense splits, including who owes whom.
    • Added localized guidance when an expense split is invalid or incomplete.
  • Bug Fixes

    • Improved equal-split validation to prevent closing splits with no valid amounts.
    • Corrected split descriptions for negative balances, single participants, and no-money-flow scenarios.
  • Tests

    • Added coverage for incomplete and single-participant equal-split scenarios.

Comment on lines +59 to +115
const { t, displayName, getCurrencyHelpersCached } = useTranslationWithUtils();
const splitValidationMessage = t(
'expense_details.add_expense_details.split_type_section.validation.invalid_split',
);
const splitDescription = React.useMemo(() => {
const splitEquallyText = t(
'expense_details.add_expense_details.split_type_section.split_equally',
);

if (SplitType.EQUAL !== splitType) {
return t('expense_details.add_expense_details.split_type_section.split_unequally');
}

if (!paidBy || !currentUser) {
return splitEquallyText;
}

const selectedParticipants = participants.filter((participant) => {
const share = splitShares[participant.id]?.[SplitType.EQUAL];
return share === undefined || 0n !== share;
});

if (0 === selectedParticipants.length) {
return splitValidationMessage;
}

const splitParticipant = selectedParticipants[0];
if (1 === selectedParticipants.length && splitParticipant) {
if (splitParticipant.id === paidBy.id) {
return t('expense_details.add_expense_details.split_type_section.direction.no_money_flow');
}

const debtor = isNegative ? paidBy : splitParticipant;
const payer = isNegative ? splitParticipant : paidBy;
const debtorName = displayName(debtor, currentUser.id);
const payerName = displayName(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,
});
}

return `${splitEquallyText} (${selectedParticipants.length})`;
}, [

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.

There is already a util function encapsulating this behavior generateSplitDescription. I would prefer for such extended logic to be kept out of the already huge AddExpensePage file.

</Button>
</SplitExpenseForm>
</div>
{!isExpenseSettled ? (

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.

The validation should already be handled in splitTypeSection. There should be no way of finding oneself outside of that input with an invalid state, so this logic is redundant, please remove it along with the toast.

Comment thread src/store/addStore.ts Outdated
...p,
amount: 0n === getSplitShare(p) ? 0n : amount / BigInt(totalParticipants),
amount:
0 === totalParticipants || 0n === getSplitShare(p)

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.

See how totalParticipants is calculated. It's the amount of non-zero splitshares, so this check is completely redundant and complicates the most sensitivie part of the code

Comment thread src/store/addStore.ts
Comment on lines 302 to +389
@@ -350,7 +351,14 @@ export function calculateParticipantSplit(

if (canSplitScreenClosed) {
let penniesLeft = updatedParticipants.reduce((acc, p) => acc + (p.amount ?? 0n), 0n);
const participantsToPick = updatedParticipants.filter((p) => p.amount);
const roundedToZeroParticipants =
SplitType.EQUAL === splitType
? updatedParticipants.filter((p) => 0n === (p.amount ?? 0n) && 0n !== getSplitShare(p))
: [];
const participantsToPick =
0 < roundedToZeroParticipants.length
? roundedToZeroParticipants
: updatedParticipants.filter((p) => p.amount);
const seed =
cyrb128(
`${participantsToPick
@@ -371,6 +379,14 @@ export function calculateParticipantSplit(
}
}
}

if (
canSplitScreenClosed &&
1 < participants.length &&
updatedParticipants.every((p) => 0n === (p.amount ?? 0n))
) {
canSplitScreenClosed = false;
}

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.

Reworks the unequal-split wording and guard per the inline review.
Lint, type-check, 141 tests, and a production build verified.
@mvanhorn

Copy link
Copy Markdown
Contributor Author

Pushed the requested changes in caa4f29. Lint, type-check, the full 141-test suite, and a production build all pass. Ready for another look.

Comment thread public/locales/en/common.json Outdated
}
},
"validation": {
"invalid_split": "Adjust who owes this expense before saving."

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.

move this to errors

Per review: the split validation message belongs with the other error strings
rather than nested under expense_details.add_expense_details.split_type_section.
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The expense split flow now validates zero-amount equal splits before closure, shows a localized error, submits unsettled expenses directly, and generates direction-specific debt descriptions.

Expense split flow

Layer / File(s) Summary
Equal-split closure validation and feedback
src/store/addStore.ts, src/components/AddExpense/SplitTypeSection.tsx, src/tests/addStore.test.ts, public/locales/en/common.json
Equal splits now require valid nonzero participant amounts. Tests cover single-participant, payer-only, and disabled-share cases. The interface displays a localized validation error.
Expense submission flow
src/components/AddExpense/AddExpensePage.tsx
Unsettled expenses no longer open the split screen during submission. Related callback state and actions were removed.
Direction-specific split descriptions
src/utils/strings.ts, src/components/AddExpense/AddExpensePage.tsx, public/locales/en/common.json
generateSplitDescription now accepts isNegative and selects localized labels for debt direction, no money flow, and payer relationships.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the two primary changes: clearer split language and prevention of silent save failures.
Description check ✅ Passed The description covers the change, issue, demo, tests, validation behavior, and all checklist items.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/AddExpense/AddExpensePage.tsx (1)

107-112: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Block invalid submission from the header Save button.

The header Save button at Lines 289-295 remains enabled when isExpenseSettled is false. It calls addExpense, which now checks only paidBy. An invalid self-only split can therefore reach addOrEditExpense.

Disable the header Save button when !isExpenseSettled.

Proposed fix
           disabled={
-            addExpenseMutation.isPending || !amount || '' === description || isFileUploading
+            addExpenseMutation.isPending ||
+            !amount ||
+            '' === description ||
+            isFileUploading ||
+            !isExpenseSettled
           }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/AddExpense/AddExpensePage.tsx` around lines 107 - 112, Disable
the header Save button in the AddExpensePage component when isExpenseSettled is
false, while preserving its existing disabled conditions. Ensure the button
cannot invoke addExpense for invalid self-only splits before reaching
addOrEditExpense.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/AddExpense/SplitTypeSection.tsx`:
- Around line 307-309: Add role="alert" to the invalid_split validation message
rendered by SplitTypeSection so assistive technology announces dynamic
validation updates, while preserving the existing conditional rendering and
styling.

---

Outside diff comments:
In `@src/components/AddExpense/AddExpensePage.tsx`:
- Around line 107-112: Disable the header Save button in the AddExpensePage
component when isExpenseSettled is false, while preserving its existing disabled
conditions. Ensure the button cannot invoke addExpense for invalid self-only
splits before reaching addOrEditExpense.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a3269a40-eb30-403b-a7d2-f8820594fcfe

📥 Commits

Reviewing files that changed from the base of the PR and between dce49ae and dd70674.

📒 Files selected for processing (6)
  • public/locales/en/common.json
  • src/components/AddExpense/AddExpensePage.tsx
  • src/components/AddExpense/SplitTypeSection.tsx
  • src/store/addStore.ts
  • src/tests/addStore.test.ts
  • src/utils/strings.ts

Comment on lines +307 to +309
{!canSplitScreenClosed ? (
<p className="text-center text-xs text-red-500">{t('errors.invalid_split')}</p>
) : null}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Announce the validation message to assistive technology.

The dynamic error text has no live-region semantics. Screen-reader users may not receive the validation result after they change a split. Add role="alert" to this message.

Proposed fix
-        <p className="text-center text-xs text-red-500">{t('errors.invalid_split')}</p>
+        <p role="alert" className="text-center text-xs text-red-500">
+          {t('errors.invalid_split')}
+        </p>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{!canSplitScreenClosed ? (
<p className="text-center text-xs text-red-500">{t('errors.invalid_split')}</p>
) : null}
{!canSplitScreenClosed ? (
<p role="alert" className="text-center text-xs text-red-500">
{t('errors.invalid_split')}
</p>
) : null}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/AddExpense/SplitTypeSection.tsx` around lines 307 - 309, Add
role="alert" to the invalid_split validation message rendered by
SplitTypeSection so assistive technology announces dynamic validation updates,
while preserving the existing conditional rendering and styling.

@mvanhorn

mvanhorn commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

Moved it to errors.invalid_split in dd70674 and updated the call site. No other locale carried the old key, so nothing else needed touching.

@krokosik

krokosik commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

One more thing that could be added to this PR is disabling the Save button when the expense is not properly settled. We are good to go then :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve confusing language for unequal splits

2 participants