Skip to content

Investments

The Investments module facilitates user investments (one-time, recurring, or from saving goal conversion) into organization-owned pools, with ROI projections and a withdrawal notice flow. The InvestmentsController at /investments handles investment CRUD, recurring schedules, ROI previews, saving goals → investments conversion, and the withdrawal notice workflow (user submits; MORIA admin lists/marks eligible). Every investment requires a proof document uploaded via multipart together with its metadata (atomic transaction).

PropertyValue
Base URL{HOST}/v1
AuthBearer JWT (header Authorization) or cookie access_token
Content-Typeapplication/json · multipart/form-data for create + convert-goals
Error envelope{ "message": string | string[], "statusCode": number, "error": string }
ValidationGlobal ValidationPipe · whitelist: true, forbidNonWhitelisted: true · unknown field → 400
Related modulespools, saving-goals, document, withdrawal, payments
Document versionv1 · 2026-05-20
AudienceInternal FE devs (mobile + web)

Users create an investment (one-time or recurring) via POST /investments by attaching a proof document (multipart). Alternative: convert a set of saving goals into investments via POST /investments/convert-goals. For recurring, an internal scheduler debits funds based on frequency. Investment withdrawal uses a notice period: the user submits a notice via POST /:id/withdrawal-notice, and MORIA admin marks eligible once the notice period ends. Cancel/pause for recurring is available at any time.

MethodPathAuthSummary
GET/v1/investmentsbearerList user investments (or by organization)
GET/v1/investments/:id/previewbearerPreview projected ROI for a single investment
GET/v1/investments/withdrawal-noticesMORIAMORIA admin: list all withdrawal notices
PATCH/v1/investments/withdrawal-notices/mark-eligibleMORIAMORIA admin: bulk mark eligible
GET/v1/investments/:idbearerInvestment detail
POST/v1/investmentsbearerCreate one-time / recurring investment (multipart)
PATCH/v1/investments/:id/pausebearerPause/resume recurring investment
DELETE/v1/investments/:idbearerCancel recurring investment
POST/v1/investments/convert-goalsbearerConvert saving goals → investments (multipart)
GET/v1/investments/convert-goals/previewbearerPreview ROI before converting goals
GET/v1/investments/:id/schedulebearerGet recurring investment schedule
POST/v1/investments/:id/withdrawal-noticebearerSubmit withdrawal notice (start notice period)
GET/v1/investments/:id/withdrawal-noticeMORIAMORIA admin: get withdrawal notice detail
POST/v1/investments/:id/withdrawal-notice/cancelbearerUser: cancel withdrawal notice

List investments for the logged-in user. If organization_id is supplied, list all investments in that organization’s pool (for org admins). Without organization_id → user mode with pagination.

bearer read-investment RESOURCE_FETCHED
ParamTypeDefaultNotes
pagenumber1Page number (validated by IsNumberString + IsNotZero)
limitnumber10Records per page
order'asc' | 'desc'descOrder by created_at
organization_idUUIDoptionalSwitch to org mode — list all investments in that org’s pool
{
"status": "success",
"statusCode": 200,
"message": "Investments retrieved successfully",
"data": {
"limit": 10,
"count": 5,
"currentPage": 1,
"totalPages": 1,
"investments": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"user_id": "770e8400-e29b-41d4-a716-446655440222",
"pool_id": "660e8400-e29b-41d4-a716-446655440111",
"investment_type": "one_time",
"amount": "1000.0000",
"status": "active",
"projected_roi": "50.0000",
"maturity_date": "2027-05-20",
"created_at": "2026-05-20T08:30:00.000Z"
}
]
}
}
StatusWhen it occurs
400 Bad RequestInvalid query param
401 UnauthorizedBearer/cookie token invalid
403 ForbiddenMissing read-investment permission

GET /v1/investments/:id/preview bearer

Section titled “GET /v1/investments/:id/preview ”

Preview projected ROI for an existing investment. The service computes this from the organization pool yield rate and the investment amount.

bearer read-investment RESOURCE_FETCHED
ParamTypeNotes
idUUIDInvestment ID — validated by ParseUUIDPipe
{
"status": "success",
"statusCode": 200,
"message": "Projected roi for investment retrieved successfully",
"data": {
"projected_roi": "75.0000",
"investment": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"amount": "1000.0000",
"status": "active"
}
}
}
StatusWhen it occurs
400 Bad RequestNo investment selected for preview
401 UnauthorizedBearer/cookie token invalid
403 ForbiddenPermission insufficient
404 Not FoundInvestment not found

GET /v1/investments/withdrawal-notices MORIA

Section titled “GET /v1/investments/withdrawal-notices ”

MORIA admin: list all withdrawal notices on the platform. Supports status filter and pagination.

bearer MORIA read-withdrawal RESOURCE_FETCHED
ParamTypeDefaultNotes
pagenumber1Page number
limitnumber10Records per page
statusenum WithdrawalNoticeStatusoptionalpending, eligible, granted, cancelled
{
"status": "success",
"statusCode": 200,
"message": "Withdrawal notices retrieved successfully",
"data": {
"limit": 10,
"count": 5,
"currentPage": 1,
"totalPages": 1,
"withdrawal_notices": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"investment_id": "550e8400-e29b-41d4-a716-446655440001",
"amount": "1000.0000",
"notice_days_required": 30,
"notice_date": "2026-05-06",
"eligible_date": "2026-06-05",
"status": "pending",
"granted_date": null,
"reason": null,
"created_at": "2026-05-06T00:00:00.000Z",
"updated_at": "2026-05-06T00:00:00.000Z"
}
]
}
}
StatusWhen it occurs
401 UnauthorizedBearer/cookie token invalid
403 ForbiddenRole not MORIA or permission missing

PATCH /v1/investments/withdrawal-notices/mark-eligible MORIA

Section titled “PATCH /v1/investments/withdrawal-notices/mark-eligible ”

MORIA admin bulk-sets withdrawal notice status to eligible for a list of investment IDs (after the notice period ends). Investments still pending are updated; those already eligible/granted/cancelled are skipped.

bearer MORIA update-withdrawal WITHDRAWAL_PROCESSED

Request body — MarkWithdrawalsEligibleDto

Section titled “Request body — MarkWithdrawalsEligibleDto”
FieldTypeRequiredNotes
investment_idsstring[] (UUID)yesArray of investment UUIDs (IsUUID per item)
{
"investment_ids": [
"550e8400-e29b-41d4-a716-446655440000",
"550e8400-e29b-41d4-a716-446655440001"
]
}
{
"status": "success",
"statusCode": 200,
"message": "Withdrawal notices marked as eligible successfully",
"data": {
"count": 2,
"withdrawal_notices": [ { "...": "WithdrawalNoticeDto shape" } ]
}
}
StatusWhen it occurs
400 Bad RequestNo pending withdrawal notice in the list
401 UnauthorizedBearer/cookie token invalid
403 ForbiddenRole not MORIA or permission missing

GET /v1/investments/:id bearer

Section titled “GET /v1/investments/:id ”

Detail of a single investment. Response includes pool + user relations (see entity shape).

bearer read-investment RESOURCE_FETCHED
ParamTypeNotes
idUUIDInvestment ID
{
"status": "success",
"statusCode": 200,
"message": "Investment retrieved successfully",
"data": {
"investment": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"user_id": "770e8400-e29b-41d4-a716-446655440222",
"pool_id": "660e8400-e29b-41d4-a716-446655440111",
"investment_type": "one_time",
"amount": "1000.0000",
"saving_goal_id": null,
"payment_ref": "PAY-20260520-001",
"status": "active",
"projected_roi": "50.0000",
"maturity_date": "2027-05-20",
"withdrawal_notice_status": null,
"withdrawal_notice_date": null,
"withdrawal_eligible_date": null
}
}
}
StatusWhen it occurs
400 Bad Requestid is not a UUID
401 UnauthorizedBearer/cookie token invalid
403 ForbiddenPermission insufficient
404 Not FoundInvestment not found

Create a one-time or recurring investment. Multipart: the file field (proof file, max 50 MB) is required — investment + proof document are created atomically (single DB transaction).

bearer create-investment RESOURCE_CREATED

Request body — CreateInvestmentDto (multipart)

Section titled “Request body — CreateInvestmentDto (multipart)”
FieldTypeRequiredNotes
filebinaryyesProof file (multipart file part); max 50 MB
type'one_time' | 'recurring'yesInvestment kind
organization_idUUIDyesTarget pool organization
amountnumeric stringyesInvestment amount (one_time) or per cycle (recurring). Min 1000, max 2000000
document_typeenum DocumentTypeyesDefault RECEIPT if empty. Members: legal_agreement, signature, cause_report, donation_certificate, receipt, image, video
frequencyenum RecurringFrequencyoptionaldaily, weekly, monthly (default monthly) — only for recurring
start_datestring (YYYY-MM-DD)optionalDefault today for recurring
end_datestring (YYYY-MM-DD)optionalFor recurring
file: <binary>
type: recurring
organization_id: be2cb7da-e217-401c-8d07-b01e64adfb34
amount: 1000.00
document_type: receipt
frequency: monthly
start_date: 2026-02-01
end_date: 2027-02-01
{
"status": "success",
"statusCode": 201,
"message": "Recurring Investment created successfully",
"data": {
"investment": { "id": "550e8400-e29b-41d4-a716-446655440000", "investment_type": "recurring", "amount": "1000.0000", "status": "active" },
"schedule": { "id": "660e8400-e29b-41d4-a716-446655440111", "frequency": "monthly", "status": "active" }
}
}
StatusWhen it occurs
400 Bad Requestproof document file is required · amount < 1000 or > 2000000 · org_id invalid
401 UnauthorizedBearer/cookie token invalid
403 ForbiddenMissing create-investment permission
  • Atomic: row investments + row documents (product_type investment) are created in the same transaction.
  • For recurring: a row in recurring_investment_schedules is also created.
  • Emits BusinessEvent RESOURCE_CREATED (impact MEDIUM).

PATCH /v1/investments/:id/pause bearer

Section titled “PATCH /v1/investments/:id/pause ”

Pause or resume a recurring investment. Only recurring investments can be paused. Users may only pause their own investments.

bearer update-investment RESOURCE_UPDATED
ParamTypeNotes
idUUIDInvestment ID
FieldTypeRequiredNotes
pausebooleanyestrue → pause, false → resume
{ "pause": true }
{
"status": "success",
"statusCode": 200,
"message": "Investment pause status updated successfully",
"data": { "investment": { "...": "Investments shape, status=paused or active" } }
}
StatusWhen it occurs
400 Bad RequestNot a recurring investment
401 UnauthorizedBearer/cookie token invalid
403 ForbiddenAttempt to pause another user’s investment
404 Not FoundInvestment not found

DELETE /v1/investments/:id bearer

Section titled “DELETE /v1/investments/:id ”

Cancel a recurring investment. Investment status is set to cancelled; the schedule is deactivated.

bearer delete-investment RESOURCE_DELETED
ParamTypeNotes
idUUIDInvestment ID
{
"status": "success",
"statusCode": 200,
"message": "Investment cancelled successfully",
"data": { "investment": { "...": "Investments shape, status=cancelled" } }
}
StatusWhen it occurs
400 Bad Requestid is not a UUID
401 UnauthorizedBearer/cookie token invalid
403 ForbiddenAttempt to cancel another user’s investment
404 Not FoundInvestment not found

POST /v1/investments/convert-goals bearer

Section titled “POST /v1/investments/convert-goals ”

Convert a set of saving goals into separate investments. Multipart: the proof file is uploaded once, each new investment has its own documents row referencing the same object.

bearer lock-saving-goal RESOURCE_CREATED

Request body — ConvertGoalsDto (multipart)

Section titled “Request body — ConvertGoalsDto (multipart)”
FieldTypeRequiredNotes
filebinaryyesProof file (multipart file part); max 50 MB
organization_idUUIDyesTarget pool organization
saving_goal_idsstring[] (UUID)yesArray of saving goal UUIDs (IsUUID per item)
document_typeenum DocumentTypeyesProof document type
file: <binary>
organization_id: be2cb7da-e217-401c-8d07-b01e64adfb34
saving_goal_ids: ["be2cb7da-...", "ce2cb7da-..."]
document_type: receipt
{
"status": "success",
"statusCode": 201,
"message": "Saving goals converted to investments successfully",
"data": {
"investments": [
{ "id": "550e8400-e29b-41d4-a716-446655440000", "investment_type": "saving_goal_conversion", "saving_goal_id": "be2cb7da-...", "amount": "850.0000", "status": "active" }
]
}
}
StatusWhen it occurs
400 Bad Requestproof document file is required · organization_id invalid
401 UnauthorizedBearer/cookie token invalid
403 ForbiddenMissing lock-saving-goal permission
404 Not FoundSaving goals not found: <comma separated ids>
  • Status of converted saving goals changes to converted.
  • The saving goal holding account balance is moved to the organization pool.

GET /v1/investments/convert-goals/preview bearer

Section titled “GET /v1/investments/convert-goals/preview ”

Preview projected ROI before converting saving goals. The goal_ids query is a comma-separated list of UUIDs.

bearer read-investment RESOURCE_FETCHED
ParamTypeRequiredNotes
goal_idsstringyesComma-separated UUID list (whitespace trimmed)
GET /v1/investments/convert-goals/preview?goal_ids=be2cb7da-...,ce2cb7da-...
{
"status": "success",
"statusCode": 200,
"message": "ROI preview retrieved successfully",
"data": {
"total_amount": "1700.0000",
"projected_roi": "85.0000",
"goalCount": 2,
"goals": [ { "id": "be2cb7da-...", "saved_amount": "850.0000", "...": "SavingGoal shape" } ]
}
}
StatusWhen it occurs
400 Bad RequestYou haven't selected any saving goal for investment preview (list empty after trim)
401 UnauthorizedBearer/cookie token invalid
403 ForbiddenPermission insufficient
404 Not FoundSaving goals not found: <ids>

GET /v1/investments/:id/schedule bearer

Section titled “GET /v1/investments/:id/schedule ”

Get the schedule for a recurring investment. Contains frequency, start/end date, status, and execution history.

bearer read-investment RESOURCE_FETCHED
ParamTypeNotes
idUUIDInvestment ID (must be type recurring)
{
"status": "success",
"statusCode": 200,
"message": "Investment schedule retrieved successfully",
"data": {
"schedule": {
"id": "660e8400-e29b-41d4-a716-446655440111",
"investment_id": "550e8400-e29b-41d4-a716-446655440000",
"frequency": "monthly",
"start_date": "2026-02-01",
"end_date": "2027-02-01",
"status": "active"
}
}
}
StatusWhen it occurs
400 Bad Requestid is not a UUID
401 UnauthorizedBearer/cookie token invalid
403 ForbiddenPermission insufficient
404 Not FoundInvestment/schedule not found

POST /v1/investments/:id/withdrawal-notice bearer

Section titled “POST /v1/investments/:id/withdrawal-notice ”

User submits a withdrawal notice for an investment. Starts the notice period (e.g. 30 days). After the period ends, MORIA admin marks eligible, then funds can be withdrawn.

bearer create-withdrawal WITHDRAWAL_REQUESTED
ParamTypeNotes
idUUIDInvestment ID

Request body — CreateWithdrawalNoticeDto

Section titled “Request body — CreateWithdrawalNoticeDto”
FieldTypeRequiredNotes
reasonstringoptionalFree-text reason, max 500 chars
{ "reason": "Need funds for emergency" }
{
"status": "success",
"statusCode": 201,
"message": "Withdrawal notice submitted successfully",
"data": {
"withdrawal_notice": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"investment_id": "550e8400-e29b-41d4-a716-446655440001",
"amount": "1000.0000",
"notice_days_required": 30,
"notice_date": "2026-05-06",
"eligible_date": "2026-06-05",
"status": "pending",
"reason": "Need funds for emergency",
"created_at": "2026-05-06T00:00:00.000Z"
}
}
}
StatusWhen it occurs
400 Bad RequestWithdrawal not allowed (investment not active, pending notice already exists)
401 UnauthorizedBearer/cookie token invalid
403 ForbiddenPermission insufficient
404 Not FoundInvestment not found

GET /v1/investments/:id/withdrawal-notice MORIA

Section titled “GET /v1/investments/:id/withdrawal-notice ”

MORIA admin: get the withdrawal notice detail for an investment (status, eligible_date, amount, etc.).

bearer MORIA read-withdrawal RESOURCE_FETCHED
ParamTypeNotes
idUUIDInvestment ID
{
"status": "success",
"statusCode": 200,
"message": "Withdrawal notice retrieved successfully",
"data": {
"withdrawal_notice": { "...": "WithdrawalNoticeDto shape" }
}
}
StatusWhen it occurs
401 UnauthorizedBearer/cookie token invalid
403 ForbiddenRole not MORIA
404 Not FoundWithdrawal notice not found

POST /v1/investments/:id/withdrawal-notice/cancel bearer

Section titled “POST /v1/investments/:id/withdrawal-notice/cancel ”

User cancels a withdrawal notice still in pending. Notices already granted/cancelled cannot be cancelled again.

bearer delete-withdrawal WITHDRAWAL_CANCELLED
ParamTypeNotes
idUUIDInvestment ID
{
"status": "success",
"statusCode": 200,
"message": "Withdrawal notice cancelled successfully",
"data": {
"withdrawal_notice": { "...": "WithdrawalNoticeDto shape, status=cancelled" }
}
}
StatusWhen it occurs
400 Bad RequestNotice already granted or cancelled
401 UnauthorizedBearer/cookie token invalid
403 ForbiddenInvestment is not owned by user or permission insufficient
404 Not FoundWithdrawal notice not found

  • one_time — single-payment investment
  • recurring — periodic investment (active schedule)
  • saving_goal_conversion — result of saving goal conversion
  • active · paused · withdrawn · cancelled
  • daily · weekly · monthly
  • active · paused · completed · cancelled
  • pending — new notice, awaiting notice period
  • eligible — period complete, admin marked eligible
  • granted — funds disbursed
  • cancelled — cancelled by user
  • monthly · quarterly · yearly
{
"message": "Minimum investment amount is 1000",
"statusCode": 400,
"error": "Bad Request"
}

message may be a string or array of strings (multi-field validation errors).

  • 400 validation · amount out of range · file missing
  • 401 token expired / missing
  • 403 not owner · role not MORIA · permission insufficient
  • 404 investment/notice/saving goal not found
  • 500 internal — show a generic toast
  • Min: 1000
  • Max: 2000000