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).
Property Value Base URL {HOST}/v1Auth Bearer JWT (header Authorization) or cookie access_token Content-Type application/json · multipart/form-data for create + convert-goalsError envelope { "message": string | string[], "statusCode": number, "error": string }Validation Global ValidationPipe · whitelist: true, forbidNonWhitelisted: true · unknown field → 400 Related modules pools, saving-goals, document, withdrawal, payments Document version v1 · 2026-05-20 Audience Internal 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.
Method Path Auth Summary GET /v1/investmentsbearer List user investments (or by organization) GET /v1/investments/:id/previewbearer Preview projected ROI for a single investment GET /v1/investments/withdrawal-noticesMORIA MORIA admin: list all withdrawal notices PATCH /v1/investments/withdrawal-notices/mark-eligibleMORIA MORIA admin: bulk mark eligible GET /v1/investments/:idbearer Investment detail POST /v1/investmentsbearer Create one-time / recurring investment (multipart) PATCH /v1/investments/:id/pausebearer Pause/resume recurring investment DELETE /v1/investments/:idbearer Cancel recurring investment POST /v1/investments/convert-goalsbearer Convert saving goals → investments (multipart) GET /v1/investments/convert-goals/previewbearer Preview ROI before converting goals GET /v1/investments/:id/schedulebearer Get recurring investment schedule POST /v1/investments/:id/withdrawal-noticebearer Submit withdrawal notice (start notice period) GET /v1/investments/:id/withdrawal-noticeMORIA MORIA admin: get withdrawal notice detail POST /v1/investments/:id/withdrawal-notice/cancelbearer User: cancel withdrawal notice
Auth + scope notes
All endpoints require a Bearer JWT + specific permission.
The withdrawal-notices list and mark-eligible endpoints are restricted to @Roles(UserType.MORIA) — platform superadmin only.
The create + convert-goals endpoints use multipart/form-data: send the file field (proof, max 50 MB) alongside the metadata fields.
Withdrawal notice: after submission, the scheduler waits notice_days_required days before eligible. Admin then marks eligible in bulk.
The actual investment is executed via the payment-gateway / internal payments — the create endpoint here only creates the row + proof document.
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
Param Type Default Notes pagenumber 1Page number (validated by IsNumberString + IsNotZero) limitnumber 10Records per page order'asc' | 'desc'descOrder by created_at organization_idUUID optional Switch to org mode — list all investments in that org’s pool
"message" : " Investments retrieved successfully " ,
"id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"user_id" : " 770e8400-e29b-41d4-a716-446655440222 " ,
"pool_id" : " 660e8400-e29b-41d4-a716-446655440111 " ,
"investment_type" : " one_time " ,
"projected_roi" : " 50.0000 " ,
"maturity_date" : " 2027-05-20 " ,
"created_at" : " 2026-05-20T08:30:00.000Z "
Status When it occurs 400 Bad RequestInvalid query param 401 UnauthorizedBearer/cookie token invalid 403 ForbiddenMissing read-investment permission
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
Param Type Notes idUUID Investment ID — validated by ParseUUIDPipe
"message" : " Projected roi for investment retrieved successfully " ,
"projected_roi" : " 75.0000 " ,
"id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
Status When it occurs 400 Bad RequestNo investment selected for preview 401 UnauthorizedBearer/cookie token invalid 403 ForbiddenPermission insufficient 404 Not FoundInvestment not found
MORIA admin: list all withdrawal notices on the platform. Supports status filter and pagination.
bearer
MORIA
read-withdrawal
RESOURCE_FETCHED
Param Type Default Notes pagenumber 1Page number limitnumber 10Records per page statusenum WithdrawalNoticeStatus optional pending, eligible, granted, cancelled
"message" : " Withdrawal notices retrieved successfully " ,
"id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"investment_id" : " 550e8400-e29b-41d4-a716-446655440001 " ,
"notice_days_required" : 30 ,
"notice_date" : " 2026-05-06 " ,
"eligible_date" : " 2026-06-05 " ,
"created_at" : " 2026-05-06T00:00:00.000Z " ,
"updated_at" : " 2026-05-06T00:00:00.000Z "
Status When it occurs 401 UnauthorizedBearer/cookie token invalid 403 ForbiddenRole not MORIA or permission missing
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
Field Type Required Notes investment_idsstring[] (UUID) yes Array of investment UUIDs (IsUUID per item)
" 550e8400-e29b-41d4-a716-446655440000 " ,
" 550e8400-e29b-41d4-a716-446655440001 "
"message" : " Withdrawal notices marked as eligible successfully " ,
"withdrawal_notices" : [ { "..." : " WithdrawalNoticeDto shape " } ]
Status When it occurs 400 Bad RequestNo pending withdrawal notice in the list 401 UnauthorizedBearer/cookie token invalid 403 ForbiddenRole not MORIA or permission missing
Detail of a single investment. Response includes pool + user relations (see entity shape).
bearer
read-investment
RESOURCE_FETCHED
Param Type Notes idUUID Investment ID
"message" : " Investment retrieved successfully " ,
"id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"user_id" : " 770e8400-e29b-41d4-a716-446655440222 " ,
"pool_id" : " 660e8400-e29b-41d4-a716-446655440111 " ,
"investment_type" : " one_time " ,
"payment_ref" : " PAY-20260520-001 " ,
"projected_roi" : " 50.0000 " ,
"maturity_date" : " 2027-05-20 " ,
"withdrawal_notice_status" : null ,
"withdrawal_notice_date" : null ,
"withdrawal_eligible_date" : null
Status When it occurs 400 Bad Requestid is not a UUID401 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
Field Type Required Notes filebinary yes Proof file (multipart file part); max 50 MB type'one_time' | 'recurring'yes Investment kind organization_idUUID yes Target pool organization amountnumeric string yes Investment amount (one_time) or per cycle (recurring). Min 1000, max 2000000 document_typeenum DocumentType yes Default RECEIPT if empty. Members: legal_agreement, signature, cause_report, donation_certificate, receipt, image, video frequencyenum RecurringFrequency optional daily, weekly, monthly (default monthly) — only for recurringstart_datestring (YYYY-MM-DD) optional Default today for recurring end_datestring (YYYY-MM-DD) optional For recurring
organization_id: be2cb7da-e217-401c-8d07-b01e64adfb34
"message" : " Recurring Investment created successfully " ,
"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 " }
Status When it occurs 400 Bad Requestproof document file is required · amount < 1000 or > 2000000 · org_id invalid401 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).
Pause or resume a recurring investment. Only recurring investments can be paused. Users may only pause their own investments.
bearer
update-investment
RESOURCE_UPDATED
Param Type Notes idUUID Investment ID
Field Type Required Notes pauseboolean yes true → pause, false → resume
"message" : " Investment pause status updated successfully " ,
"data" : { "investment" : { "..." : " Investments shape, status=paused or active " } }
Status When 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
Cancel a recurring investment. Investment status is set to cancelled; the schedule is deactivated.
bearer
delete-investment
RESOURCE_DELETED
Param Type Notes idUUID Investment ID
"message" : " Investment cancelled successfully " ,
"data" : { "investment" : { "..." : " Investments shape, status=cancelled " } }
Status When it occurs 400 Bad Requestid is not a UUID401 UnauthorizedBearer/cookie token invalid 403 ForbiddenAttempt to cancel another user’s investment 404 Not FoundInvestment not found
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
Field Type Required Notes filebinary yes Proof file (multipart file part); max 50 MB organization_idUUID yes Target pool organization saving_goal_idsstring[] (UUID) yes Array of saving goal UUIDs (IsUUID per item) document_typeenum DocumentType yes Proof document type
organization_id: be2cb7da-e217-401c-8d07-b01e64adfb34
saving_goal_ids: ["be2cb7da-...", "ce2cb7da-..."]
"message" : " Saving goals converted to investments successfully " ,
{ "id" : " 550e8400-e29b-41d4-a716-446655440000 " , "investment_type" : " saving_goal_conversion " , "saving_goal_id" : " be2cb7da-... " , "amount" : " 850.0000 " , "status" : " active " }
Status When it occurs 400 Bad Requestproof document file is required · organization_id invalid401 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.
Preview projected ROI before converting saving goals. The goal_ids query is a comma-separated list of UUIDs.
bearer
read-investment
RESOURCE_FETCHED
Param Type Required Notes goal_idsstring yes Comma-separated UUID list (whitespace trimmed)
GET /v1/investments/convert-goals/preview?goal_ids=be2cb7da-...,ce2cb7da-...
"message" : " ROI preview retrieved successfully " ,
"total_amount" : " 1700.0000 " ,
"projected_roi" : " 85.0000 " ,
"goals" : [ { "id" : " be2cb7da-... " , "saved_amount" : " 850.0000 " , "..." : " SavingGoal shape " } ]
Status When 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 the schedule for a recurring investment. Contains frequency, start/end date, status, and execution history.
bearer
read-investment
RESOURCE_FETCHED
Param Type Notes idUUID Investment ID (must be type recurring)
"message" : " Investment schedule retrieved successfully " ,
"id" : " 660e8400-e29b-41d4-a716-446655440111 " ,
"investment_id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"start_date" : " 2026-02-01 " ,
"end_date" : " 2027-02-01 " ,
Status When it occurs 400 Bad Requestid is not a UUID401 UnauthorizedBearer/cookie token invalid 403 ForbiddenPermission insufficient 404 Not FoundInvestment/schedule not found
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
Param Type Notes idUUID Investment ID
Field Type Required Notes reasonstring optional Free-text reason, max 500 chars
{ "reason" : " Need funds for emergency " }
"message" : " Withdrawal notice submitted successfully " ,
"id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"investment_id" : " 550e8400-e29b-41d4-a716-446655440001 " ,
"notice_days_required" : 30 ,
"notice_date" : " 2026-05-06 " ,
"eligible_date" : " 2026-06-05 " ,
"reason" : " Need funds for emergency " ,
"created_at" : " 2026-05-06T00:00:00.000Z "
Status When 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
MORIA admin: get the withdrawal notice detail for an investment (status, eligible_date, amount, etc.).
bearer
MORIA
read-withdrawal
RESOURCE_FETCHED
Param Type Notes idUUID Investment ID
"message" : " Withdrawal notice retrieved successfully " ,
"withdrawal_notice" : { "..." : " WithdrawalNoticeDto shape " }
Status When it occurs 401 UnauthorizedBearer/cookie token invalid 403 ForbiddenRole not MORIA 404 Not FoundWithdrawal notice not found
User cancels a withdrawal notice still in pending. Notices already granted/cancelled cannot be cancelled again.
bearer
delete-withdrawal
WITHDRAWAL_CANCELLED
Param Type Notes idUUID Investment ID
"message" : " Withdrawal notice cancelled successfully " ,
"withdrawal_notice" : { "..." : " WithdrawalNoticeDto shape, status=cancelled " }
Status When 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
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 " ,
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