Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/Application/Report/Commands/ShowReportCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace App\Application\Report\Commands;

final readonly class ShowReportCommand
{
public function __construct(
public int $id,
) {}
}
6 changes: 6 additions & 0 deletions app/Application/Report/Usecases/ReportUsecase.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use App\Application\Report\Commands\ScammerReportOrganizationInput;
use App\Application\Report\Commands\SearchReportsByTitleCommand;
use App\Application\Report\Commands\SearchReportsCommand;
use App\Application\Report\Commands\ShowReportCommand;
use App\Application\Report\Commands\StoreOrganizationReportCommand;
use App\Application\Report\Commands\StoreReportForOrganizationCommand;
use App\Application\Report\Commands\StoreReportForScammerCommand;
Expand Down Expand Up @@ -76,6 +77,11 @@ public function searchByTitle(SearchReportsByTitleCommand $command): PaginatedRe
);
}

public function show(ShowReportCommand $command): ?Report
{
return $this->reports->findActiveById($command->id);
}

public function storeTemporaryProofs(StoreTemporaryProofsCommand $command): array
{
$paths = [];
Expand Down
4 changes: 4 additions & 0 deletions app/Application/Report/Usecases/ReportUsecaseInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,24 @@

use App\Application\Report\Commands\SearchReportsByTitleCommand;
use App\Application\Report\Commands\SearchReportsCommand;
use App\Application\Report\Commands\ShowReportCommand;
use App\Application\Report\Commands\StoreOrganizationReportCommand;
use App\Application\Report\Commands\StoreReportForOrganizationCommand;
use App\Application\Report\Commands\StoreReportForScammerCommand;
use App\Application\Report\Commands\StoreScammerReportCommand;
use App\Application\Report\Commands\StoreTemporaryProofsCommand;
use App\Domain\Search\ValueObjects\CardSearchResult;
use App\Domain\Search\ValueObjects\PaginatedResult;
use App\Models\Report;

interface ReportUsecaseInterface
{
public function search(SearchReportsCommand $command): CardSearchResult;

public function searchByTitle(SearchReportsByTitleCommand $command): PaginatedResult;

public function show(ShowReportCommand $command): ?Report;

/**
* @return list<string>
*/
Expand Down
17 changes: 17 additions & 0 deletions app/Http/Controllers/Public/ReportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use App\Application\Report\Commands\SearchReportsByTitleCommand;
use App\Application\Report\Commands\SearchReportsCommand;
use App\Application\Report\Commands\ShowReportCommand;
use App\Application\Report\Commands\StoreOrganizationReportCommand;
use App\Application\Report\Commands\StoreReportForOrganizationCommand;
use App\Application\Report\Commands\StoreReportForScammerCommand;
Expand All @@ -17,6 +18,7 @@
use App\Http\Requests\Public\StoreProofRequest;
use App\Http\Requests\Public\StoreScammerReportRequest;
use App\Http\Resources\Public\ReportCardResource;
use App\Http\Resources\Public\ReportDetailResource;
use App\Http\Resources\Public\ReportResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
Expand Down Expand Up @@ -76,6 +78,21 @@ public function searchByTitle(Request $request): JsonResponse
]);
}

public function show(string $id): JsonResponse
{
if (filter_var($id, FILTER_VALIDATE_INT) === false) {
return response()->json(['message' => 'Invalid report ID'], 400);
}

$report = $this->reports->show(new ShowReportCommand((int) $id));

if ($report === null) {
return response()->json(['message' => 'Report not found'], 404);
}

return response()->json((new ReportDetailResource($report))->resolve());
}

public function storeTemporaryProof(StoreProofRequest $request): JsonResponse
{
$images = $request->file('images');
Expand Down
31 changes: 31 additions & 0 deletions app/Http/Resources/Public/ReportDetailResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace App\Http\Resources\Public;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;

/**
* @property string $title
* @property string|null $description
* @property bool $is_active
* @property Collection $proofs
* @property Collection $products
* @property Carbon $created_at
*/
class ReportDetailResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'title' => $this->title,
'description' => $this->description,
'is_active' => $this->is_active,
'proofs' => $this->proofs->pluck('path')->all(),
'products' => $this->products->pluck('name')->all(),
'created_at' => $this->created_at->format('Y-m-d'),
];
}
}
15 changes: 15 additions & 0 deletions app/Repositories/Report/ReportRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ public function attachProduct(Report $report, int $productId): void
SearchCache::invalidate();
}

public function findActiveById(int $id): ?Report
{
return Cache::remember(
SearchCache::key("public:report:{$id}"),
self::CACHE_TTL_SECONDS,
fn () => Report::query()
->where('is_active', true)
->with([
'proofs' => fn ($query) => $query->orderBy('id'),
'products' => fn ($query) => $query->orderBy('name'),
])
->find($id),
);
}

public function findPaginatedByTitleSearch(string $query, int $page, int $count): PaginatedResult
{
if ($query === '') {
Expand Down
2 changes: 2 additions & 0 deletions app/Repositories/Report/ReportRepositoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,7 @@ public function addProof(Report $report, string $path): int;

public function attachProduct(Report $report, int $productId): void;

public function findActiveById(int $id): ?Report;

public function findPaginatedByTitleSearch(string $query, int $page, int $count): PaginatedResult;
}
59 changes: 59 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ info:
The service stores a copy under `reports/scammers/profiles` or `reports/organizations/profiles`
and returns the public URL of the stored file.
`POST /public/media/profiles` uploads a temporary profile picture for reports and profile updates.
`GET /public/reports/{id}` returns one active report, including its proof paths and product names.
`POST /public/scammers/{id}/reports` files a report against an active scammer.
`POST /public/organizations/{id}/reports` files a report against an active organization.
Each body sends a title, description, email, and at least one product. Proofs are optional
Expand Down Expand Up @@ -1185,6 +1186,53 @@ paths:
message: Invalid page or count
"429":
$ref: "#/components/responses/RateLimited"
/public/reports/{id}:
get:
tags: [Reports]
operationId: getReport
summary: Get a report by ID
description: |
Returns the full public view of an active report: title, description, status,
proof paths, product names, and the creation date. An inactive or soft-deleted
report is not found.
parameters:
- $ref: "#/components/parameters/ReportId"
responses:
"200":
description: Public report.
content:
application/json:
schema:
$ref: "#/components/schemas/ReportDetail"
example:
title: Scammed me while I was pregnant!
description: I'm devastated, this guy scammed me while I was pregnant.
is_active: true
proofs:
- reports/scammers/proofs/8f3c1a.jpg
- reports/scammers/proofs/9a4d2b.png
products:
- Phone
- Watch
created_at: "2026-09-01"
"400":
description: The report ID is not a valid integer.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorMessage"
example:
message: Invalid report ID
"404":
description: No active report exists for this ID.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorMessage"
example:
message: Report not found
"429":
$ref: "#/components/responses/RateLimited"
/public/media/profiles:
post:
tags: [Media]
Expand Down Expand Up @@ -1535,6 +1583,15 @@ components:
maximum: 100000
default: 1
example: 1
ReportId:
name: id
in: path
required: true
description: Numeric ID of the report.
schema:
type: integer
minimum: 1
example: 1
ReportTitleQuery:
name: q
in: query
Expand Down Expand Up @@ -2126,6 +2183,8 @@ components:
$ref: ./schemas/PaginatedPaymentMethods.yaml
Report:
$ref: ./schemas/Report.yaml
ReportDetail:
$ref: ./schemas/ReportDetail.yaml
PaginatedReports:
$ref: ./schemas/PaginatedReports.yaml
ScammerOrganization:
Expand Down
2 changes: 2 additions & 0 deletions routes/api.public.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@
->middleware(['throttle:public-search', AuditApiRequest::class])
->name('public.reports.search');

Route::get('reports/{id}', [ReportController::class, 'show']);

Route::post('media/profiles', [MediaController::class, 'storeProfilePicture']);
Route::post('reports/media/proofs', [ReportController::class, 'storeTemporaryProof']);

Expand Down
45 changes: 45 additions & 0 deletions schemas/ReportDetail.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
type: object
description: Full public view of one active report.
required:
- title
- description
- is_active
- proofs
- products
- created_at
properties:
title:
type: string
description: Report title.
example: Scammed me while I was pregnant!
description:
type:
- string
- "null"
description: Full report description.
example: I'm devastated, this guy scammed me while I was pregnant.
is_active:
type: boolean
description: Whether the report is active. Public reads only return active reports.
example: true
proofs:
type: array
description: Public-disk paths of the report's proof images, ordered by proof id.
items:
type: string
example:
- reports/scammers/proofs/8f3c1a.jpg
- reports/scammers/proofs/9a4d2b.png
products:
type: array
description: Names of the products attached to the report, ordered by name. Soft-deleted links are omitted.
items:
type: string
example:
- Phone
- Watch
created_at:
type: string
format: date
description: Date the report was created (`YYYY-MM-DD`).
example: "2026-09-01"
Loading
Loading