> ## Documentation Index
> Fetch the complete documentation index at: https://docs.a7manager.se/llms.txt
> Use this file to discover all available pages before exploring further.

# maintenance_plans

> Maintenance programmes and their tasks. Not a daily file, and it deletes.

Maintenance programmes and the tasks they contain. **Not part of the daily set** — delivered
only when a plan revision changes.

**Update key:** the plan's `external_system_id` plus `task_external_system_id`, within the project

<Warning>
  **This file deletes.** Any task that exists in the platform but is absent from a delivered
  file is **hard-deleted**, for every plan the file mentions. A plan file must therefore always
  carry the complete task list for each plan it names. Read [what gets
  deleted](#what-gets-deleted) before producing this file — the failure modes are destructive
  and not recoverable from the import itself.
</Warning>

## Layout

Plan and task columns sit on the **same row**, one row per task. Plan columns are repeated
identically across every row belonging to the same plan.

When a plan's rows disagree on the plan-level columns, the **first** row in the file wins
and the rest are ignored.

## Plan columns

| Column                     | Type        | Required | Notes                                        |
| -------------------------- | ----------- | :------: | -------------------------------------------- |
| `external_system_id`       | text(100)   |     ●    | The plan's identifier                        |
| `revision`                 | text(50)    |     ○    |                                              |
| `name`                     | text(200)   |     ●    |                                              |
| `description`              | text        |     ○    | Empty becomes an empty string                |
| `vehicle_type_code`        | text(50)    |     ◐    | Exactly one of this or `component_type_code` |
| `component_type_code`      | text(50)    |     ◐    | Exactly one of this or `vehicle_type_code`   |
| `plan_external_updated_at` | timestamptz |     ○    | Staleness key for the plan row               |

A plan targets a vehicle type **or** a component type, never both and never neither.

## Task columns

| Column                       | Type        | Required | Notes                                            |
| ---------------------------- | ----------- | :------: | ------------------------------------------------ |
| `task_external_system_id`    | text(100)   |     ●    | The task's identifier within the plan            |
| `task_name`                  | text(200)   |     ●    |                                                  |
| `task_description`           | text        |     ○    | Empty becomes an empty string                    |
| `task_type`                  | enum        |     ○    | `RECURRING` (default), `ONE_TIME`, `SEASONAL`    |
| `task_group`                 | text(100)   |     ○    | Free grouping label, e.g. `A-service`            |
| `interval_type`              | enum        |     ●    | `KILOMETERS`, `DAYS`, `HOURS`, `CYCLES`          |
| `interval_value`             | integer     |     ●    |                                                  |
| `tolerance_before`           | integer     |     ○    | Defaults to `0`                                  |
| `tolerance_after`            | integer     |     ○    | Defaults to `0`                                  |
| `secondary_interval_type`    | enum        |     ◐    | Same set. All-or-nothing with the next column    |
| `secondary_interval_value`   | integer     |     ◐    | All-or-nothing with the previous column          |
| `secondary_tolerance_before` | integer     |     ○    | Defaults to `0`                                  |
| `secondary_tolerance_after`  | integer     |     ○    | Defaults to `0`                                  |
| `season_start_month`         | smallint    |     ◐    | `SEASONAL` tasks only                            |
| `season_start_day`           | smallint    |     ◐    | `SEASONAL` tasks only                            |
| `season_end_month`           | smallint    |     ◐    | `SEASONAL` tasks only                            |
| `season_end_day`             | smallint    |     ◐    | `SEASONAL` tasks only                            |
| `safety`                     | boolean     |     ○    | Defaults to `false`                              |
| `location`                   | text(100)   |     ○    |                                                  |
| `sort_order`                 | integer     |     ○    | Defaults to `0`                                  |
| `task_external_updated_at`   | timestamptz |     ○    | Staleness key for the task row                   |
| `project_external_id`        | text(100)   |     ○    | **Inert** — present in the header but never read |

The tolerances are what decide whether work shows as due soon, due, or overdue, so they are
worth getting right even though they default to zero.

## Rules

* Exactly one of `vehicle_type_code` / `component_type_code`, and the code must resolve
  within the project.
* `secondary_interval_type` and `secondary_interval_value` must both be set or both be
  empty.
* A `SEASONAL` task requires all four season fields; a non-seasonal task must have none of
  them. Months are 1–12 and days 1–31.

## What gets deleted

After inserting and updating, the import deletes tasks. The scope is precise, and both of
its sharp edges are worth understanding.

**What is safe:** the delete only touches plans whose `external_system_id` appears in the
file you just delivered, and never crosses projects. Plans the file does not mention are
untouched.

**What is deleted:** within each plan the file *does* mention, every task in the platform
whose identifier is absent from that plan's rows in the file. This is a hard delete, not an
archive flag.

<Warning>
  **The delete is not protected by the staleness check.** If you re-deliver an old plan file,
  its plan and task rows will be skipped as stale — but the delete still runs, and removes
  every task added since. Re-sending an old file is destructive even though it appears to
  change nothing.

  **Splitting one plan across two files is also destructive.** Whichever file lands second
  deletes the tasks contributed by the first. One plan's complete task list must arrive in one
  file.
</Warning>

<Note>
  `estimated_manhours` and `estimated_cost` are maintained in the application by the Planner
  role. They are **not** in this file and are never overwritten by an import — so ordinary
  re-imports preserve them.

  The exception follows from the delete above: if a task disappears from a delivered revision,
  its row is removed and those locally maintained estimates go with it. Re-adding the task
  later creates a fresh row with empty estimates.
</Note>

## Example

Two rows of the same plan carrying two different tasks. Note the repeated plan columns:

```csv theme={null}
external_system_id,revision,name,description,vehicle_type_code,component_type_code,plan_external_updated_at,task_external_system_id,task_name,task_description,task_type,task_group,interval_type,interval_value,tolerance_before,tolerance_after,secondary_interval_type,secondary_interval_value,secondary_tolerance_before,secondary_tolerance_after,season_start_month,season_start_day,season_end_month,season_end_day,safety,location,sort_order,task_external_updated_at,project_external_id
MP-X61-VEH-001,1,X61 Fordonsunderhåll,Underhållsplan för Coradia X61 fordon,X61,,2026-03-09T14:00:00Z,MT-X61-OVS-25K,Översyn 25 000 km,Planerad översyn vid 25 000 km,RECURRING,A-service,KILOMETERS,25000,2500,2500,,,,,,,,,false,,1,2026-03-09T14:00:00Z,X61ST
MP-X61-VEH-001,1,X61 Fordonsunderhåll,Underhållsplan för Coradia X61 fordon,X61,,2026-03-09T14:00:00Z,MT-X61-OVS-50K,Översyn 50 000 km,Planerad översyn vid 50 000 km,RECURRING,B-service,KILOMETERS,50000,5000,5000,,,,,,,,,false,,2,2026-03-09T14:00:00Z,X61ST
```

## Why a file gets rejected

Required fields abort immediately, before anything else is checked:

```
Row 3: missing or empty external_system_id
Row 4: missing or empty name
Row 5: missing or empty task_external_system_id
Row 6: missing or empty task_name
Row 7: missing or empty interval_type
Row 8: missing or empty interval_value
```

Then the structural rules together:

```
Row 10: must have vehicle_type_code or component_type_code
Row 11: must have exactly one of vehicle_type_code or component_type_code, not both
Row 12: invalid task_type 'PERIODIC'
Row 13: invalid interval_type 'KM'
Row 14: invalid secondary_interval_type 'MONTHS'
Row 15: secondary_interval_type and secondary_interval_value must both be set or both be null
Row 16: SEASONAL task requires all season fields (start/end month/day)
Row 17: season fields out of range (month 1-12, day 1-31)
Row 18: non-SEASONAL task must not have season fields
```

Then type resolution:

```
Row 20: vehicle_type_code 'X62' not found in project
Row 21: component_type_code '0789-X' not found in project
```
