# Coding Agent Instructions — `core` service

The **core** Django service of Fun Tours. Owns domain logic, CMS integration, CMS plugins, and the API.

## 1. Architecture and ownership

Follow the root `AGENTS.md` conventions:

* ownership first
* explicit contracts
* smallest coherent diff
* preserve existing architecture and intentional comments
* do not refactor for taste
Every feature should have a clear owner. Avoid generic packages that gradually become a second domain layer.

The `core` package is the Django project/configuration package. Project-wide Django configuration belongs there; domain logic and CMS integration should not be placed in `core` when they have a more specific owner.

## 1.1 Service layout and historical Django identity

Read `ARCHITECTURE.md` before moving responsibilities or changing cross-module contracts.

* `domains/business/` owns tours, support, staff, careers, and offers
* `domains/system/` owns authentication, user settings, managed content, and stored-file access
* `infrastructure/` owns generic framework integrations and adapters
* `core/` is the composition root: settings, URLs, application registration, and service orchestration
* Domain and infrastructure runtime code must not import `core` or old root-package namespaces
* Consume configured values through `django.conf.settings`
* Keep Django `AppConfig.label`, model labels, database tables, CMS plugin types and URL namespaces intentional; the current initial migrations define the clean pre-launch baseline
* Register domain-specific CMS content viewers in `core/composition/cms.py`, through the CMS registry contract
* Preserve `vl_core` and `vl_snippet` namespaces; they are outside the ten moved project packages

## 2. CMS integration

Project-owned integration with Django CMS lives in `infrastructure/c_cms/`.

```text
infrastructure/c_cms/
├── plugins/
│   ├── primitives/
│   └── widgets/
├── api/
│   ├── urls.py
│   ├── views.py
│   └── serializers.py
├── registry.py
└── utils.py
```

### 2.1 `c_cms` ownership

`c_cms` owns the integration layer between the project and Django CMS:

* reusable CMS plugins
* CMS API endpoints
* CMS content registry
* CMS-specific integration utilities
Do not create a generic `cms` package: `cms` is already the namespace/package provided by django CMS.

The `c_` prefix is intentional for project-owned packages whose names collide with third-party packages or established framework concepts.

### 2.2 CMS plugin ownership

**CMS plugins must live with their owner.**

A plugin that belongs to a domain must be implemented in that domain package:

```text
domains/business/tours/
├── cms_plugins.py
├── cms_apps.py
├── cms_config.py
├── cms_menus.py
└── models/
    └── cms.py
```

For example, a Tour-specific CMS plugin belongs to `domains/business/tours`, not to `infrastructure/c_cms/plugins/`.

`infrastructure/c_cms/plugins/` contains **only reusable, ownership-neutral CMS plugins**.

```text
infrastructure/c_cms/
└── plugins/
    ├── primitives/
    └── widgets/
```

There must be **no `domains/` directory under `infrastructure/c_cms/plugins/`**.

Do not create structures such as:

```text
infrastructure/c_cms/plugins/domains/tours/
```

If a plugin has a domain owner, put it in that domain's package instead.

This rule prevents `infrastructure/c_cms/plugins/` from becoming a second domain layer and keeps ownership explicit.

### 2.3 Generic CMS plugin layout

Reusable plugins under `infrastructure/c_cms/plugins/` use:

```text
<plugin>/
├── models.py
├── cms_plugins.py
└── serializers.py
```

* `models.py` — `CMSPlugin` subclass* `cms_plugins.py` — `CMSPluginBase` subclass registered through `plugin_pool`* `serializers.py` — API serializer
Plugin infrastructure shared by multiple reusable plugins belongs under `infrastructure/c_cms/plugins/`, not inside an individual plugin.

### 2.4 CMS API

The CMS HTTP/API layer belongs to:

```text
infrastructure/c_cms/api/
```

It contains CMS-specific URLs, views, and serializers.

CMS content lookup/serialization registration belongs to:

```text
infrastructure/c_cms/registry.py
```

CMS-specific request/path helpers belong to:

```text
infrastructure/c_cms/utils.py
```

Do not move CMS-specific registry or API logic back into the generic `core` package.

## 3. CMS plugin API contract

Contract with the frontend (`services/ui`):

* The `plugin_type` value in the API **must equal the backend plugin class name**
  (for example `BrandBannerPlugin`) and must match the frontend `*.cms.ts`
  `plugin_type`.
* Plugin output uses `serializer_class` on the plugin class* Plugin serializers should extend `GenericPluginSerializer`* Local file/media URLs must be relative (storage paths), see [4.5](#45-media-url-contract)
Do not call `with_domain()` or `absolute_url()` on media paths in serializers/API output: a serializer output
must not depend on the host of the incoming request, otherwise a server-side request from another host leaks
its own address into the response and the client cannot fetch the file.

Use:

```python
from vl_core.utils.url import absolute_url
```

for explicit URL absolutization of non-media urls (pages, documents), where the host belongs to the url itself.

## 4. Universal media field mechanism (`vl_core.contrib.media_utils`)

`vl_core.contrib.media_utils` is the established mechanism for media fields and image API output.

Extend it; do not introduce another image abstraction for individual plugins or domains.

### 4.1 Model fields

Use:

```python
from vl_core.contrib.media_utils.fields import FileField, ImageField
from vl_core.contrib.media_utils.mixins import FileModelMixin
```

Example:

```python
class BrandBanner(FileModelMixin, CMSPlugin):
    file = FileField(_('Media file'))
    preview = ImageField(_('Preview'), null=True, blank=True)
```

`ImageField` supports:

* `thumb_options` for admin previews;
* `render_options` for output `format`/`quality`;
* `hint_options` for admin hints
`FileModelMixin` handles file lifecycle and provides helpers such as:

* `file_field_names`;
* `get_file_fields()`;
* `file_exists()`;
* `get_main_field()`
`ImageModelMixin` adds image admin helpers and variant cleanup.

### 4.2 Image variants

Use `variant_from_field()` and the existing `variant_data()` / `variant_html()` helpers instead of implementing variant generation in plugins.

`variant_from_field()` always returns:

```text
(variant_url, variant_width, variant_height, srcset, srcset_result)
```

Important invariants:

* always unpack all five values;
* the returned dimensions represent the actual generated image;
* `render_options` must be propagated through the complete call chain;
* variant URLs must reference existing files;
* SVGs do not generate raster variants;
* `max_width`/`max_height` cap the 1x base size only; density srcset items multiply the capped base and are not capped again
Variants are generated on first serialization and cached next to the original file. Cache filenames use the requested geometry; returned dimensions describe the actual pixels, which may be smaller because images are not upscaled. Keep the lookup and save keys identical, including when cropping or rounding changes the result.

`generate_variants=False` returns original image URLs without resizing. It does not schedule background generation or resize when the browser later requests the image. Browser `loading="lazy"` controls downloading separately; nginx serves existing media files.

The application configures `MEDIA_VARIANT_SERIALIZER` to delegate persisted API images to `infrastructure.media_processing`. Those serializers return a versioned envelope with actual dimensions and processing state, without encoding pixels. API profile declarations remain the source of truth; do not duplicate sizes in a task registry. See [background media processing](infrastructure/media_processing/README.md) for queue, profile bindings, migration, backfill and readiness contracts.

### 4.3 API image serializer

Use:

```python
from vl_core.contrib.media_utils.serializers import ImageField
```

Example:

```python
class StaffSerializer(serializers.ModelSerializer):
    pic = ImageField(read_only=True, height=1080)
    thumb = ImageField(
        source='pic_sm',
        width=300,
        srcset=['1x', '1.5x', '2x'],
    )
```

`ImageField` supports:

* `width`;
* `height`;
* `srcset`;
* `coordinates`;
* `render_options`;
* `breakpoints`;
* `max_width` / `max_height` — cap the 1x base size, defaults `DEFAULT_MAX_WIDTH` (3440) / `DEFAULT_MAX_HEIGHT` (1440); density srcset items multiply the capped 1x base, so `max_width=1000` with `srcset=['1x', '3x']` on a 4000 px original yields 1000 px for `1x` and 3000 px for `3x`; pass `None` to disable the cap
The field is read-only.

### 4.4 Breakpoint output

When `breakpoints` is used, the universal serializer generates breakpoint-specific srcsets.

The default breakpoints are:

```python
DEFAULT_BREAKPOINTS = [414, 926, 1024, 1440, 1920]
```

Use `DEFAULT_BREAKPOINTS` rather than repeating the values at plugin use sites.

Breakpoint mode defaults to:

```python
['1x', '2x', '3x']
```

The serialized breakpoint keys are strings.

Do not hand-roll breakpoint serialization in individual plugins.

### 4.5 Media URL contract

Local media is serialized as a path relative to the media base url (`/media/file_name.webp`), never as an
absolute url: an absolute url built from the incoming request host leaks an internal host into responses and
breaks server-side rendering on the frontend. The frontend resolves every local media path against its own
media base url (`services/ui`, `#infrastructure/api/media_url.ts`).

Files that are not media images are serialized with:

```python
from vl_core.contrib.media_utils.serializers import MediaFileField, MediaImageField
```

`MediaFileField` and `MediaImageField` serialize a single local file as a path, while DRF's own
`serializers.FileField`/`serializers.ImageField` prefix it with the request host, so they must not be used in API output.

For populated image fields, the serializer returns image metadata including the relative url, dimensions, ratio,
existence state, and srcset data.

An empty image field serializes as:

```text
''
```

Keep this contract: the frontend treats `''` as "no image".

## 5. Verification

Prefer verification inside the service container:

```bash
docker exec fun_tours__core__dev python /app/manage.py check
docker exec fun_tours__core__dev python -m py_compile <file>
```

After model changes:

```bash
docker exec fun_tours__core__dev python /app/manage.py makemigrations --check --dry-run
```

When a migration is intentionally written by hand, apply it and compare the result with Django's migration state.

For changed Python files, also verify syntax with:

```bash
python3 -m py_compile <changed files>
```


## CMS models placement

In the `core` service, files like `cms.py` inside domain apps may contain:
- CMS plugins (subclasses of `CMSPlugin`) used in django CMS
- regular Django models that are specific to CMS needs (for example, models used only to render content on the public site)

If a model is needed only for CMS/public site rendering and does not belong to the domain's general business logic outside CMS, it can live in the domain's `cms.py`.
Do not move such CMS-specific models to global/shared locations unless there is a clear cross-domain reuse requirement.
