Attachments
The Attachment system provides polymorphic file storage for any entity in the ERP. Files are uploaded to server storage and associated with a parent model via a morph relation. This allows journal entries, invoices, business partners, and any other entity to have file attachments without dedicated file columns.
Purpose
- Attach files (PDFs, images, documents) to any entity in the system
- Support polymorphic relations so any model can have attachments
- Track upload metadata (file name, size, MIME type, uploader)
- Provide download and delete functionality
Entity Attributes
| Field | Type | Description |
|---|---|---|
id | integer | Primary key |
company_id | integer | Foreign key to the company |
attachable_type | string | Morph class name (e.g., Modules\Accounting\Models\JournalEntry) |
attachable_id | integer | ID of the parent entity |
file_name | string | Original file name (e.g., invoice.pdf) |
file_path | string | Storage path (e.g., attachments/1/2026-02/abc.pdf) |
file_size | integer | File size in bytes |
mime_type | string | MIME type (e.g., application/pdf, image/png) |
uploaded_by | integer | Foreign key to the user who uploaded the file |
created_at | datetime | Creation timestamp |
updated_at | datetime | Last update timestamp |
deleted_at | datetime? | Soft-delete timestamp |
Relationships
API Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/core/attachments | List attachments (filtered by parent) |
POST | /api/core/attachments | Upload a new attachment |
GET | /api/core/attachments/{id}/download | Download an attachment file |
DELETE | /api/core/attachments/{id} | Delete an attachment and its file |
Query Parameters for GET /api/core/attachments
| Parameter | Type | Description |
|---|---|---|
attachable_type | string | The morph class to filter by |
attachable_id | integer | The parent entity ID to filter by |
Request/Response Examples
GET /api/core/attachments
List attachments for a specific entity.
bash
curl -X GET "https://moon-erp.test/api/core/attachments?attachable_type=Modules%5CAccounting%5CModels%5CJournalEntry&attachable_id=1" \
-H "Accept: application/json" \
-H "Authorization: Bearer {token}"dart
final response = await http.get(
Uri.parse(
'https://moon-erp.test/api/core/attachments'
'?attachable_type=Modules\\Accounting\\Models\\JournalEntry'
'&attachable_id=1',
),
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer $token',
},
);Response 200 OK
json
{
"data": [
{
"id": 1,
"company_id": 1,
"attachable_type": "Modules\\Accounting\\Models\\JournalEntry",
"attachable_id": 1,
"file_name": "فاتورة-مبيعات.pdf",
"file_path": "attachments/1/2026-02/a1b2c3.pdf",
"file_size": 102400,
"mime_type": "application/pdf",
"uploaded_by": 1,
"uploader": { "id": 1, "name": "Ahmed Hamdi" },
"created_at": "2026-02-16T10:00:00.000000Z",
"updated_at": "2026-02-16T10:00:00.000000Z"
}
],
"links": { "first": "...?page=1", "last": "...?page=1", "prev": null, "next": null },
"meta": { "current_page": 1, "from": 1, "last_page": 1, "per_page": 25, "to": 1, "total": 1 }
}POST /api/core/attachments
Upload a file attachment. This endpoint uses multipart/form-data encoding.
bash
curl -X POST https://moon-erp.test/api/core/attachments \
-H "Accept: application/json" \
-H "Authorization: Bearer {token}" \
-F "file=@/path/to/invoice.pdf" \
-F "attachable_type=Modules\Accounting\Models\JournalEntry" \
-F "attachable_id=1"dart
import 'package:http/http.dart' as http;
final request = http.MultipartRequest(
'POST',
Uri.parse('https://moon-erp.test/api/core/attachments'),
);
request.headers['Accept'] = 'application/json';
request.headers['Authorization'] = 'Bearer $token';
request.fields['attachable_type'] = 'Modules\\Accounting\\Models\\JournalEntry';
request.fields['attachable_id'] = '1';
request.files.add(await http.MultipartFile.fromPath('file', '/path/to/invoice.pdf'));
final response = await request.send();Response 201 Created
json
{
"data": {
"id": 2,
"company_id": 1,
"attachable_type": "Modules\\Accounting\\Models\\JournalEntry",
"attachable_id": 1,
"file_name": "invoice.pdf",
"file_path": "attachments/1/2026-02/d4e5f6.pdf",
"file_size": 204800,
"mime_type": "application/pdf",
"uploaded_by": 1,
"uploader": { "id": 1, "name": "Ahmed Hamdi" },
"created_at": "2026-02-16T11:00:00.000000Z",
"updated_at": "2026-02-16T11:00:00.000000Z"
}
}GET /api/core/attachments/{id}/download
Download an attachment file. Returns the file as a streamed response with the original file name.
bash
curl -X GET https://moon-erp.test/api/core/attachments/1/download \
-H "Authorization: Bearer {token}" \
-o invoice.pdfdart
final response = await http.get(
Uri.parse('https://moon-erp.test/api/core/attachments/1/download'),
headers: {
'Authorization': 'Bearer $token',
},
);
// response.bodyBytes contains the file dataError Response 404 Not Found (file missing from storage)
json
{
"message": "File not found"
}DELETE /api/core/attachments/{id}
Delete an attachment record and its file from storage.
Response 200 OK
json
{
"message": "Deleted"
}Business Rules
- Company scoping -- attachments are scoped to the authenticated user's company via the
tenant()scope. - Polymorphic binding -- the
attachable_typemust be a valid, fully-qualified model class name. Theattachable_idmust correspond to an existing record of that type. - File size limit -- files are limited to 10 MB by the form request validation.
- Storage path -- files are stored in
attachments/{company_id}/{year-month}/with a generated filename to prevent collisions. - Upload tracking -- the
uploaded_byfield is automatically set to the authenticated user's ID. - File deletion -- when an attachment is deleted, both the database record and the physical file on disk are removed.
- Download by ID -- the download endpoint streams the file with the original
file_nameas the download name. If the file is missing from storage (e.g., manually deleted), a404is returned. - Supported by any model -- any Eloquent model can accept attachments by referencing its class name as
attachable_type. Models can optionally use theHasAttachmentstrait for convenience.