Skip to content

Position transfer modal + tests, minimum investment floor warning - #125

Merged
Chucks1093 merged 3 commits into
StellarState:devfrom
BigDella:feat/119-115-position-transfer-modal-and-tests-v2
Aug 28, 2026
Merged

Position transfer modal + tests, minimum investment floor warning#125
Chucks1093 merged 3 commits into
StellarState:devfrom
BigDella:feat/119-115-position-transfer-modal-and-tests-v2

Conversation

@BigDella

@BigDella BigDella commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Note on base branch: opened against dev, not mainmain here is essentially a placeholder (the investor dashboard route is a stub returning null), while dev has 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 into PositionCard.tsx for every position with status: "active" (funded, not yet settled). It mirrors the existing KeyTransferModal'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 extending KeyTransferModal.

lib/api/index.ts adds transferInvoicePosition(), posting to /invoices/:id/transfer-position (mirrors the existing /invoices/:id/invest pattern exactly). hooks/useInvestments.ts's new useTransferPositionMutation wraps 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 exact invoice_id/buyer/sale_price; and a failed contract call → error shown + confirm re-enabled.

Along the way I found PositionTransferModal.tsx's handleSubmit had an unguarded await mutateAsync(...) — testing the failure path surfaced it as a genuine unhandled promise rejection in the test run. Added a try/catch around it (the mutation's own isError state still drives the UI; the catch just stops the rejection from propagating). KeyTransferModal has 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.tsx and InvestorPortfolio.test.tsx needed a small update: their default position fixtures are all status: "active", so PositionCard now always renders PositionTransferModal — which needs AuthContext those files don't set up, and isn't what they're testing anyway. Mocked it out there the same way InvoiceDetail.test.tsx already mocks CountdownTimer/DocumentPreview.

#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 — 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 into app/admin/page.tsx as a new "Settlements" tab alongside the existing Invoices/Audit Log tabs. It 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.

lib/api/index.ts adds 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.

Test plan

  • 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 byte-identical across all changes in this PR, unrelated to it.
  • All touched/new test files pass cleanly: 48/48 across PositionTransferModal, PositionCard, InvestorPortfolio, InvestmentAmountInput, InvoiceDetail, AdminSettlements, and the investment-amount validation unit tests — zero unhandled rejections.

Closes #119
Closes #115
Closes #116
Closes #118

Obiajulu-gif and others added 3 commits August 27, 2026 13:16
… 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).
@drips-wave

drips-wave Bot commented Aug 28, 2026

Copy link
Copy Markdown

@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! 🚀

Learn more about application limits

@Chucks1093
Chucks1093 merged commit 6501a88 into StellarState:dev Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment