Skip to content

Fix/invoice investment fixes batch - #653

Merged
TS-mfon merged 5 commits into
StellarState:devfrom
The-Big-Danny:fix/invoice-investment-fixes-batch
Oct 17, 2026
Merged

TS-mfon merged 5 commits into
StellarState:devfrom
The-Big-Danny:fix/invoice-investment-fixes-batch

Conversation

@The-Big-Danny

Copy link
Copy Markdown
Contributor

Description

Closes #623
Closes #624
Closes #622
Closes #617

Summary of all 4 fixes (branch: fix/invoice-investment-fixes-batch, 4 commits ahead of dev)

  1. Invoice due-date comparison used midnight instead of end-of-day
  • Root cause: Postgres date columns come back from TypeORM as a bare "YYYY-MM-DD" string. new Date("2026-10-17") parses that as midnight UTC, so comparing it directly against now expired invoices for investment at the start of their due date instead of the end.
  • Fix: added src/utils/date.utils.ts (isPastDueDate/endOfDayUTC) and applied it consistently to all 3 raw comparisons in investment.service.ts (investInInvoice, createInvestment, investor analytics overdue calc) and to Invoice.model.ts's isOverdue/isExpired, which had the identical bug.
  • Files: src/utils/date.utils.ts (new), src/services/investment.service.ts, src/models/Invoice.model.ts + tests.
  1. Invoice submission bypassed the state machine entirely
  • Root cause: submit.ts checked for a nonexistent InvoiceService.submitInvoice method, always falling into a fallback that wrote the entity straight to the repository — skipping InvoiceStateMachine validation, invoice_status_history, and all transition side effects/notifications.
  • Fix: submit.ts now calls InvoiceService.createInvoice (drafts it) then submitInvoiceForReview (runs the real DRAFT → PENDING transition with guards, history, side effects). Extended CreateInvoiceInput/InvoiceDTO to carry the self-serve columns (sellerWallet, title, faceValue, fundingTarget, yieldBps, fundingDeadline, ipfsDocumentUrl) that createInvoice had no way to persist before. Also fixed the error handler, which read a nonexistent err.status instead of ServiceError's statusCode — every service error was silently surfacing as a 500.
  • Files: src/routes/invoices/submit.ts, src/services/invoice.service.ts + tests.
  1. Investments ignored the funding deadline, only checked the repayment due date
  • Root cause: investInInvoice and createInvestment checked only invoice.dueDate (repayment date), ignoring invoice.fundingDeadline (the funding campaign's own cutoff). A long-tenor invoice could have its funding deadline pass while dueDate was still months out, letting investors keep buying into a closed campaign.
  • Fix: added a fundingDeadline check alongside the dueDate check in both methods → ServiceError("funding_deadline_passed", ..., 422). No date-utility needed here since fundingDeadline is a full timestamptz, not a bare date.
  • Files: src/services/investment.service.ts + tests.
  1. Soroban event indexer only polled the escrow contract
  • Root cause: EventIndexerService was constructed with contractIds: [config.sorobanEscrow.contractId] only. The read-model projections on its event bus (creator key, ACL, curve migration, atomic swap, royalty, dividend) dispatch by event topic, not contract ID — so they only ever see events from contracts actually polled, and none of those events come from the escrow contract. Those projections never updated in production.
  • Fix: built the contractIds list from every configured Soroban contract address already available via sorobanConfig (escrow, payment distributor, subscription, token), deduplicated, gated indexer startup on at least one being present.
  • Files: src/index.ts.

Verification: full suite run after each fix and again on the consolidated branch — 1644 tests passing; the only failure (fee-schedule.test.ts, 7 tests, pre-existing TypeORM metadata error unrelated to these changes) is present identically on dev. Type-check and lint clean on every touched file.

The-Big-Danny and others added 4 commits October 17, 2026 17:54
Postgres `date` columns come back from TypeORM as a bare "YYYY-MM-DD"
string, which new Date(...) parses as that day's midnight UTC. Comparing
that directly against "now" in investInInvoice, createInvestment, and the
investor analytics overdue calc closed invoices for investment at the
start of their due date instead of at the end of it.

Add src/utils/date.utils.ts (isPastDueDate/endOfDayUTC) and use it
consistently across investment.service.ts and Invoice.model.ts's
isOverdue/isExpired, which shared the same bug.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
POST /invoices (self-serve submission) checked for an InvoiceService.
submitInvoice method that doesn't exist, so it always fell through to a
fallback that wrote the entity straight to the repository. That bypassed
InvoiceStateMachine entirely: no transition validation, no
invoice_status_history row, and none of the transition side effects ran.

submit.ts now calls InvoiceService.createInvoice (drafts the invoice)
followed by submitInvoiceForReview (drives the draft -> pending
transition through the state machine, with its guard, history and
notification side effects). Extend CreateInvoiceInput/InvoiceDTO with the
sellerWallet/title/faceValue/fundingTarget/yieldBps/fundingDeadline/
ipfsDocumentUrl columns the self-serve flow captures, which createInvoice
previously had no way to persist. Also fix the error handler to read
ServiceError's statusCode (it was reading a nonexistent `status` field,
so every service error surfaced as a 500).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
investInInvoice and createInvestment only checked invoice.dueDate (the
repayment date) to decide whether an invoice could still accept
investment, ignoring invoice.fundingDeadline (the funding campaign's own
cutoff). A long-tenor invoice could have its funding deadline pass while
dueDate was still months out, letting investors keep buying into a
closed campaign.

Add a fundingDeadline check alongside the existing dueDate check in both
methods, throwing ServiceError("funding_deadline_passed", ..., 422) when
it has passed. fundingDeadline is a `timestamptz` column (unlike the
`date`-typed dueDate), so a plain Date comparison is correct here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

# Conflicts:
#	tests/unit/invoice-invest-endpoint.test.ts
EventIndexerService was constructed with contractIds: [config.sorobanEscrow.contractId]
only. The read-model projections wired onto its eventBus (creator key,
ACL, curve migration, atomic swap, royalty, dividend) dispatch by event
topic rather than contract id, so they only ever see an event if the
contract that emits it is actually polled - and none of them are emitted
by the escrow contract alone. In production this meant those projections
never updated.

Build contractIds from every configured Soroban contract address
(escrow, payment distributor, subscription, token) via the sorobanConfig
already loaded earlier in this function, deduplicated, and gate the
indexer on at least one of them being present.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@drips-wave

drips-wave Bot commented Oct 17, 2026

Copy link
Copy Markdown

@The-Big-Danny 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

PR StellarState#653's 4 CI checks (Build, Lint, Type Check, Test) were failing, but
verification showed dev itself is red on the identical errors at the
same commit - unrelated to the 4 invoice/investment fixes in this PR.
Fixing them here so the PR can go green rather than leaving dev broken.

Type Check / Build:
- InvestorAccreditation.model.ts, SecondaryMarketPriceHistory.model.ts:
  add missing `!` definite-assignment on TypeORM columns.
- secondary-market-price.service.ts: fix the entity/field references to
  point at the real SecondaryListing model (askPrice/soldAt never
  existed; the actual fields are pricePerFraction/status).
- accreditation.routes.ts, refund.routes.ts, price-history.routes.ts:
  Express 5's ParamsDictionary types a route param as `string | string[]`
  to support wildcard segments, so narrow req.params to the route's own
  shape per this repo's existing PublishInvoiceRequest convention; fix
  HttpError's swapped (message, status) call sites to (status, message);
  fix req.user?.wallet (no such field) to req.user?.stellarAddress;
  requireAdmin() missing its adminWallets argument.
- invoice-refund.service.ts: ServiceError(message, status) swapped to
  (code, message, status); Investment has no `amount` field, only
  investmentAmount. Also fixed getRefundableInvoices, which returned
  every expired invoice platform-wide regardless of the wallet argument
  it was given - a real cross-investor data leak, not just a type error.

Lint: removed 9 unused imports/vars across 6 files (investor-risk-profile
.controller.ts, invoice-draft.controller.ts, investor.routes.ts,
investor-accreditation.service.ts, invoice-refund.service.ts,
issuer-reputation.service.ts, including a dead TIER_LABELS map that
duplicated computeTier's logic).

Test: tests/fee-schedule.test.ts's DataSource never initialized -
User's relations reference Transaction/KYCVerification/KycHistory/
Notification/SecondaryListing/Watchlist by string name, none of which
were registered in tests/helpers/test-data-source.ts, so TypeORM's
metadata builder couldn't resolve the inverse side. Registering them
surfaced a second, pre-existing issue: FeeSchedule's enum/timestamptz
columns aren't valid sqlite types, worked around the same way nine other
integration tests in this repo already do (rewriting column metadata
before the DataSource is built). That in turn surfaced two more
pre-existing test bugs once they could finally run: beforeEach cleared
the FK parent (FeeSchedule) before the child (FeeScheduleHistory),
and three assertions checked error.message for a ServiceError's `code`
instead of its `message`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@TS-mfon
TS-mfon merged commit cdcaf32 into StellarState:dev Oct 17, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment