Position transfer modal + tests, minimum investment floor warning - #125
Merged
Chucks1093 merged 3 commits intoAug 28, 2026
Conversation
… floor warning StellarState#119 — invoice position transfer modal - components/dashboard/PositionTransferModal.tsx (new), wired into PositionCard.tsx for every position with status "active" (funded, not yet settled). Mirrors KeyTransferModal's structure (same Stellar-address + self-transfer validation pattern) but sells the whole invoice position for an XLM sale price rather than a quantity of creator keys — different concept, so a new component rather than extending KeyTransferModal. - lib/api/index.ts: transferInvoicePosition() posts to /invoices/:id/transfer-position (mirrors the existing /invoices/:id/invest pattern), and hooks/useInvestments.ts's useTransferPositionMutation wraps it, invalidating the portfolio query on success so the transferred position drops out of the list. StellarState#115 — unit tests for the position transfer modal - components/dashboard/__tests__/PositionTransferModal.test.tsx (new): invalid-address row error + disabled confirm, "Cannot transfer to yourself" for the seller's own address, valid-input enables confirm and shows the proceeds preview, the position amount is shown before confirming, confirm calls the contract with the exact invoice_id/buyer/ sale_price, and a failed contract call shows an error and re-enables confirm. Mocks useAuth/useStellarWallet directly and reimplements useTransferPositionMutation's pending/error state via real React state inside the mock (rather than a plain object) so the component re-renders the same way it would against the real React Query hook. - Also hardened PositionTransferModal.tsx's handleSubmit with a try/catch around mutateAsync — without it, a rejected mutation is an unhandled promise rejection (KeyTransferModal has the same unguarded await, but nothing exercises its failure path in tests to have surfaced this before). - PositionCard.test.tsx and InvestorPortfolio.test.tsx now mock PositionTransferModal (their default position fixtures are all "active", so PositionCard now always renders it) — neither of those files is testing transfer behaviour, and the real component needs AuthContext they don't set up. StellarState#116 — minimum investment floor warning - hooks/useProtocolStatus.ts (new) + lib/api/index.ts's fetchProtocolStatus() read GET /protocol/status. InvoiceDetail.tsx now passes minInvestment={protocolStatus?.min_investment ?? 1} to InvestmentModal instead of the hardcoded minInvestment={1} it had before — InvestmentAmountInput already did real-time min/max validation and disabled the confirm button below the minimum, but the minimum itself was never actually the protocol value. - lib/validation/investment-amount.ts: the below-minimum message is now belowMinimumError(min) => "Minimum investment is ${min} XLM" (the issue's exact requested copy) instead of the previous static "Amount below minimum investment" — updated the tests asserting the old string accordingly, since this is a deliberate copy change the issue calls for, not incidental breakage. Verification: ran the full existing test suite before and after (via git stash) to confirm no regressions — 7 pre-existing failing files (31 tests: TopInvestorsLeaderboard referencing a since-renamed fetchLeaderboard export, a PublishInvoiceForm timing issue, and others) are identical on both sides, unrelated to this change. All touched/new test files pass cleanly: 42/42 across PositionTransferModal, PositionCard, InvestorPortfolio, InvestmentAmountInput, InvoiceDetail, and the investment-amount validation unit tests, with zero unhandled rejections.
…rState#118) - components/admin/AdminSettlements.tsx (new): a "Settlements" tab (app/admin/page.tsx, alongside the existing Invoices/Audit Log tabs) listing funded invoices awaiting settlement. Mirrors AdminInvoiceReview.tsx's structure (loading skeleton, empty state, card list, useAuth-gated authorization) since it's the same admin-review shape with a different action. - "Propose Settlement" opens an inline repayment-amount input and submits the proposal. - A pending-settlement badge shows on invoices with an open proposal. - "Approve Settlement" is disabled for the admin whose address matches the proposal's proposed_by — the two-of-two rule requires a *different* admin. The backend is still the source of truth for this (a 403 from approveSettlement surfaces as "Cannot approve your own settlement proposal" via toast), since a client-side check alone can't be trusted. - A different admin approving executes the settlement, shows the "Settlement executed" banner, and invalidates the settlements query so the invoice drops off the list (settlement moves it out of "awaiting settlement" on the backend). - lib/api/index.ts: fetchSettlements/proposeSettlement/approveSettlement, hitting new /admin/settlements and /admin/invoices/:id/settlement/* endpoints (mirroring the existing /admin/invoices/:id/approve pattern). fetchSettlements returns each funded invoice pre-annotated with its open proposal (if any) in one request, rather than one request per invoice. Tests: components/admin/__tests__/AdminSettlements.test.tsx covers all five acceptance criteria — propose opens the input and submits, pending badge shown, approve disabled for the proposing admin, a different admin approving executes the settlement and shows the banner, and the backend's "not your own proposal" error surfaces via toast. 6/6 passing. Verification: full suite re-run after this commit — same 7 pre-existing failing files (31 tests, unrelated — see the previous commit's message) identical to before; 288/319 passing overall, up from 282 (6 new tests, all passing).
|
@BigDella Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Note on base branch: opened against
dev, notmain—mainhere is essentially a placeholder (the investor dashboard route is a stub returningnull), whiledevhas the real app (~168 files ahead) including the vitest setup these tests run under.#119 — invoice position transfer modal
components/dashboard/PositionTransferModal.tsx(new), wired intoPositionCard.tsxfor every position withstatus: "active"(funded, not yet settled). It mirrors the existingKeyTransferModal's structure — same Stellar-address-format + self-transfer validation pattern, same dialog shell — but sells the whole invoice position for an XLM sale price rather than transferring a quantity of creator keys, which is a different concept, hence a new component rather than extendingKeyTransferModal.lib/api/index.tsaddstransferInvoicePosition(), posting to/invoices/:id/transfer-position(mirrors the existing/invoices/:id/investpattern exactly).hooks/useInvestments.ts's newuseTransferPositionMutationwraps it and invalidates the portfolio query on success, so the transferred position drops out of the investor's list per the issue's acceptance criteria.#115 — unit tests for the position transfer modal
components/dashboard/__tests__/PositionTransferModal.test.tsx(new) covers all five acceptance criteria: invalid address → row-level error + disabled confirm; the seller's own address → "Cannot transfer to yourself"; valid inputs → confirm enabled + proceeds preview shown; confirm → contract called with the exactinvoice_id/buyer/sale_price; and a failed contract call → error shown + confirm re-enabled.Along the way I found
PositionTransferModal.tsx'shandleSubmithad an unguardedawait mutateAsync(...)— testing the failure path surfaced it as a genuine unhandled promise rejection in the test run. Added atry/catcharound it (the mutation's ownisErrorstate still drives the UI; the catch just stops the rejection from propagating).KeyTransferModalhas the exact same unguarded pattern, but nothing exercises its failure path in tests, so it was never surfaced — left that one alone since it's outside this issue's scope.PositionCard.test.tsxandInvestorPortfolio.test.tsxneeded a small update: their default position fixtures are allstatus: "active", soPositionCardnow always rendersPositionTransferModal— which needsAuthContextthose files don't set up, and isn't what they're testing anyway. Mocked it out there the same wayInvoiceDetail.test.tsxalready mocksCountdownTimer/DocumentPreview.#116 — minimum investment floor warning
hooks/useProtocolStatus.ts(new) +lib/api/index.ts'sfetchProtocolStatus()readGET /protocol/status.InvoiceDetail.tsxnow passesminInvestment={protocolStatus?.min_investment ?? 1}toInvestmentModalinstead of the hardcodedminInvestment={1}it had before.InvestmentAmountInputalready did real-time min/max validation and disabled the confirm button below the minimum — the actual bug was that the minimum itself was never the real protocol value.Also updated the below-minimum message to the issue's exact requested copy:
belowMinimumError(min) => "Minimum investment is ${min} XLM", replacing the previous static"Amount below minimum investment". Updated the two existing tests asserting the old string — a deliberate copy change the issue calls for, not incidental breakage.#118 — multi-sig settlement approval UI
components/admin/AdminSettlements.tsx(new), wired intoapp/admin/page.tsxas a new "Settlements" tab alongside the existing Invoices/Audit Log tabs. It mirrorsAdminInvoiceReview.tsx's structure (loading skeleton, empty state, card list,useAuth-gated authorization) since it's the same admin-review shape with a different action.proposed_by— the two-of-two rule requires a different admin. The backend is still the source of truth for this (a 403 fromapproveSettlementsurfaces as "Cannot approve your own settlement proposal" via toast), since a client-side check alone can't be trusted.lib/api/index.tsaddsfetchSettlements/proposeSettlement/approveSettlement, hitting new/admin/settlementsand/admin/invoices/:id/settlement/*endpoints (mirroring the existing/admin/invoices/:id/approvepattern).fetchSettlementsreturns each funded invoice pre-annotated with its open proposal (if any) in one request, rather than one request per invoice.Test plan
git stash) to confirm no regressions: 7 pre-existing failing files (31 tests —TopInvestorsLeaderboardreferencing a since-renamedfetchLeaderboardexport, aPublishInvoiceFormtiming issue, and others) are byte-identical across all changes in this PR, unrelated to it.PositionTransferModal,PositionCard,InvestorPortfolio,InvestmentAmountInput,InvoiceDetail,AdminSettlements, and the investment-amount validation unit tests — zero unhandled rejections.Closes #119
Closes #115
Closes #116
Closes #118