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
4 changes: 4 additions & 0 deletions lib/Repair/RenameCollidingSchemaSlugs.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ class RenameCollidingSchemaSlugs implements IRepairStep {
// claimed by three apps and all three share only `name`, so all three
// namespace: shillinq keeps the bare slug, stackiq took catalogService.
'service' => ['to' => 'appointmentService', 'with' => 'shillinq, stackiq'],
// The CRM task. `task` was claimed by three apps and they share only
// `description`, `priority` and `status`. planninq's project task is the
// largest and keeps the bare slug; dossiq took caseTask.
'task' => ['to' => 'crmTask', 'with' => 'planninq, dossiq'],
];

/**
Expand Down
2 changes: 1 addition & 1 deletion lib/Service/CallbackService.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class CallbackService {
*
* @var string
*/
private const TASK_SCHEMA_SLUG = 'task';
private const TASK_SCHEMA_SLUG = 'crmTask';

/**
* Fallback status-transition graph used only when the schema declaration is
Expand Down
2 changes: 1 addition & 1 deletion lib/Service/Marketing/JourneyStepRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ private function createTask(array $action, string $contactId): array {
$payload['assigneeUserId'] = $assignee;
}

$stored = $this->store->save(schemaSlug: $this->store->schemaSlug('task_schema', 'task'), payload: $payload);
$stored = $this->store->save(schemaSlug: $this->store->schemaSlug('task_schema', 'crmTask'), payload: $payload);
if ($stored === null) {
return ['state' => 'failed', 'reason' => 'task_write_failed'];
}
Expand Down
2 changes: 1 addition & 1 deletion lib/Service/SchemaMapService.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class SchemaMapService {
// booking, planninq's project booking and this app's billing/WIP record —
// so `SchemaMapper::find()` answered whichever it reached first.
'timeEntry_schema' => 'billingTimeEntry',
'task_schema' => 'task',
'task_schema' => 'crmTask',
'posTransaction_schema' => 'posTransaction',
// POS staff PIN + role permissions (pos-staff-pin-permissions).
'posRole_schema' => 'posRole',
Expand Down
4 changes: 3 additions & 1 deletion lib/Service/SettingsLoadService.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class SettingsLoadService {
'productCategory',
'billingCategory',
'leadProduct',
'task',
'crmTask',
'relationship',
'queue',
'skill',
Expand Down Expand Up @@ -319,6 +319,8 @@ private function applyRegisterConfig(array $importResult): void {
'appointmentBooking' => 'booking_schema',
// The bookable service, same reasoning as the booking above.
'appointmentService' => 'service_schema',
// The CRM task, same reasoning as the appointment schemas above.
'crmTask' => 'task_schema',
];

/**
Expand Down
6 changes: 3 additions & 3 deletions lib/Settings/pipelinq_mock_register.json
Original file line number Diff line number Diff line change
Expand Up @@ -6117,7 +6117,7 @@
{
"@self": {
"register": "pipelinq",
"schema": "task",
"schema": "crmTask",
"slug": "task-task-1-1"
},
"subject": "Voorbeeld Subject 1",
Expand Down Expand Up @@ -6148,7 +6148,7 @@
{
"@self": {
"register": "pipelinq",
"schema": "task",
"schema": "crmTask",
"slug": "task-task-2-2"
},
"subject": "Voorbeeld Subject 2",
Expand Down Expand Up @@ -6179,7 +6179,7 @@
{
"@self": {
"register": "pipelinq",
"schema": "task",
"schema": "crmTask",
"slug": "task-task-3-3"
},
"subject": "Voorbeeld Subject 3",
Expand Down
6 changes: 3 additions & 3 deletions lib/Settings/pipelinq_register.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"productCategory",
"leadProduct",
"contactmoment",
"task",
"crmTask",
"relationship",
"skill",
"agentProfile",
Expand Down Expand Up @@ -1426,9 +1426,9 @@
}
}
},
"task": {
"crmTask": {
"x-schema-org": "schema:Action",
"slug": "task",
"slug": "crmTask",
"x-openregister-notifications": {
"taskCompleted": {
"trigger": {
Expand Down
2 changes: 1 addition & 1 deletion lib/Settings/register.d/40-pos-cash-management.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"productCategory",
"leadProduct",
"contactmoment",
"task",
"crmTask",
"kennisartikel",
"kenniscategorie",
"kennisfeedback",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"productCategory",
"leadProduct",
"contactmoment",
"task",
"crmTask",
"kennisartikel",
"kenniscategorie",
"kennisfeedback",
Expand Down
2 changes: 1 addition & 1 deletion lib/Settings/register.d/60-pos-split-tender.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"productCategory",
"leadProduct",
"contactmoment",
"task",
"crmTask",
"kennisartikel",
"kenniscategorie",
"kennisfeedback",
Expand Down
2 changes: 1 addition & 1 deletion lib/Settings/register.d/97-lead-activity.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
},
"components": {
"schemas": {
"task": {
"crmTask": {
"properties": {
"lead": {
"type": "string",
Expand Down
28 changes: 28 additions & 0 deletions openspec/changes/namespace-the-crm-task/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Namespace the CRM task

## Why

`task` was claimed by three apps: planninq, dossiq and this one. They share
`description`, `priority` and `status` — the attributes any task-shaped record
carries, and nothing that identifies the record.

planninq's project task is the largest of the three and keeps the bare slug.
dossiq's becomes `caseTask`; this one is the CRM task, raised from a lead, a
ticket or the KCC werkplek.

## The decoys, which are a vocabulary rather than stray words

`task` is also an internal type name in two places, and neither is a schema:

- **Activity types.** `ActivityTimelineService` maps a source type to an
activity type (`'emailLink' => 'email'`, `'task' => 'task'`). Its siblings
give it away: `interaction` is not a schema either, it queries tickets by
`ticketType`. The source-type vocabulary stays.
- **Notification subjects.** `NotificationService` branches on
`$entityType === 'task'` to pick `task_assigned`, and passes
`objectType: 'task'` to Nextcloud's notification API. That is routing, not a
schema lookup.

Both were left alone. The schema references — the register lists, the
descriptor, `TASK_SCHEMA_SLUG`, `SchemaMapService`'s entity type, the exportable
schema list and the werkplek dialog's save — all moved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# OpenRegister integration

## ADDED Requirements

### Requirement: The CRM task is namespaced (REQ-ORI-047)

The CRM task schema SHALL be `crmTask` and SHALL NOT be `task`. planninq's
project task keeps the bare slug; dossiq uses `caseTask`.

The three claiming schemas share `description`, `priority` and `status` alone,
so all three are renamed apart rather than folded onto one owner.

The rename SHALL NOT touch `task` where it is an activity type in
`ActivityTimelineService` or a notification entity type or object type in
`NotificationService`. Those are internal vocabularies: `interaction` and
`emailLink` sit beside `task` in the first, and neither is a schema.

A repair step SHALL rename the row IN PLACE before the register import, scoped
to this app's own rows.

The config key SHALL remain `task_schema`.

#### Scenario: The slug is renamed in place

- **GIVEN** an install carrying a pipelinq-owned `task` schema
- **WHEN** the repair step runs
- **THEN** the row keeps its schema id, and so its shard table and objects.

#### Scenario: The activity timeline still classifies a task

- **WHEN** a task-sourced timeline entry is classified
- **THEN** its activity type is still `task`.
14 changes: 14 additions & 0 deletions openspec/changes/namespace-the-crm-task/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Tasks

- [x] 1.1 Move the key, slug and the four register-list entries
**files**: lib/Settings/pipelinq_register.json, lib/Settings/register.d/40-pos-cash-management.json, lib/Settings/register.d/50-pos-end-of-day-bookkeeping.json, lib/Settings/register.d/60-pos-split-tender.json, lib/Settings/register.d/97-lead-activity.json
- [x] 1.2 Follow the slug in the mock register and the manifests
**files**: lib/Settings/pipelinq_mock_register.json, src/manifest.json, src/manifest.d/85-kcc-werkplek.json
- [x] 2.1 Follow the slug in the callback service, schema map and journey runner
**files**: lib/Service/CallbackService.php, lib/Service/SchemaMapService.php, lib/Service/Marketing/JourneyStepRunner.php
- [x] 2.2 Follow the slug in the object-type map, export list and werkplek dialog
**files**: src/config/objectTypes.js, src/views/export/ExportJobForm.vue, src/components/werkplek/WerkplekNewTaskDialog.vue
- [x] 3.1 Move the slug in `SCHEMA_SLUGS`, pin the config key, extend the map to ten renames
**files**: lib/Service/SettingsLoadService.php, lib/Repair/RenameCollidingSchemaSlugs.php, tests/Unit/Repair/RenameCollidingSchemaSlugsTest.php
- [x] 4.1 Repoint the schema-slug test stubs, leaving activity types alone
**files**: tests/Unit/Service/CallbackServiceTest.php, tests/Unit/Service/Lifecycle/SchemaLifecycleGraphTest.php, tests/Unit/Service/Marketing/JourneyStepRunnerTest.php
2 changes: 1 addition & 1 deletion src/components/werkplek/WerkplekNewTaskDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export default {
status: 'open',
}
try {
const result = await this.objectStore.saveObject('task', payload)
const result = await this.objectStore.saveObject('crmTask', payload)
if (!result) {
try {
showError(
Expand Down
2 changes: 1 addition & 1 deletion src/config/objectTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export function objectTypes() {
description: t(APP, 'Billable categories and tags'),
},
{
slug: 'task',
slug: 'crmTask',
group: 'core',
label: t(APP, 'Task'),
description: t(APP, 'Work items / tasks'),
Expand Down
2 changes: 1 addition & 1 deletion src/manifest.d/85-kcc-werkplek.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"title": "Tasks",
"content": {
"register": "pipelinq",
"schema": "task",
"schema": "crmTask",
"filter": {
"assigneeUserId": "@me",
"status": "open"
Expand Down
8 changes: 4 additions & 4 deletions src/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2923,7 +2923,7 @@
"icon": "ClipboardCheckOutline",
"content": {
"register": "pipelinq",
"schema": "task",
"schema": "crmTask",
"filter": {
"lead": "@objectId"
},
Expand Down Expand Up @@ -3117,7 +3117,7 @@
"title": "Tasks",
"config": {
"register": "pipelinq",
"schema": "task",
"schema": "crmTask",
"documentationUrl": "https://pipelinq.conduction.nl",
"showViewAction": false,
"actions": [
Expand Down Expand Up @@ -3151,7 +3151,7 @@
"component": "CnWizardDialog",
"config": {
"register": "pipelinq",
"schema": "task"
"schema": "crmTask"
}
},
{
Expand All @@ -3161,7 +3161,7 @@
"title": "Task",
"config": {
"register": "pipelinq",
"schema": "task",
"schema": "crmTask",
"documentationUrl": "https://pipelinq.conduction.nl",
"_note": "Task/action-item archetype (bespoke-detail-pages). A task is a to-do an agent works to completion, so the body leads with the execution fields (type, status, priority, deadline, assignee user/group, callbackPhoneNumber, preferredTimeSlot, attempts, resultText) 8-wide and the source records it hangs off (client, request, originating contactmoment) at 4-wide. Documents full-width for any attached result artefact. NO email/calendar body widgets — task declares no comms linkedType; a callback task carries the phone number as a field, not a live mailbox. No child collection, so no object-list.",
"widgets": [
Expand Down
2 changes: 1 addition & 1 deletion src/views/export/ExportJobForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ import { useObjectStore } from '../../store/modules/object.js'
// `contactmoment` schemas are one schema now, discriminated by `ticketType`.
// Exporting `ticket` therefore covers all three; narrow to a single subtype
// with a row filter (e.g. `ticketType = 'complaint'`).
const EXPORTABLE_SCHEMAS = ['client', 'contact', 'lead', 'ticket', 'task', 'product']
const EXPORTABLE_SCHEMAS = ['client', 'contact', 'lead', 'ticket', 'crmTask', 'product']
const FORMATS = ['csv', 'parquet', 'jsonl']
const MODES = ['full', 'incremental']

Expand Down
5 changes: 4 additions & 1 deletion tests/Unit/Repair/RenameCollidingSchemaSlugsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,14 @@ public function testRenamesEverySlugInPlace(): void {
'message' => [607],
'booking' => [608],
'service' => [609],
'task' => [610],
]
);
$statements = &$this->captureStatements();

$this->step->run($this->createMock(IOutput::class));

$this->assertCount(9, $statements, 'exactly nine rows may be rewritten');
$this->assertCount(10, $statements, 'exactly ten rows may be rewritten');
foreach ($statements as $statement) {
$this->assertStringContainsString('openregister_schemas', $statement[0]);
$this->assertStringContainsString('SET slug', $statement[0]);
Expand All @@ -145,6 +146,7 @@ public function testRenamesEverySlugInPlace(): void {
$this->assertContains(['channelMessage', 607], $written);
$this->assertContains(['appointmentBooking', 608], $written);
$this->assertContains(['appointmentService', 609], $written);
$this->assertContains(['crmTask', 610], $written);

}//end testRenamesEverySlugInPlace()

Expand All @@ -165,6 +167,7 @@ public function testIsANoOpWhenTheOldSlugsAreAbsent(): void {
'channelMessage' => [607],
'appointmentBooking' => [608],
'appointmentService' => [609],
'crmTask' => [610],
]
);
$this->db->expects($this->never())->method('executeStatement');
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/Service/CallbackServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ public function testTransitionGraphIsSourcedFromSchemaDeclaration(): void {
// The schema declaration matches the documented mirror constant.
$graph = (new \OCA\Pipelinq\Service\Lifecycle\SchemaLifecycleGraph(
settingsDir: __DIR__ . '/../../../lib/Settings'
))->adjacencyFor(schemaSlug: 'task');
))->adjacencyFor(schemaSlug: 'crmTask');
$this->assertSame(CallbackService::ALLOWED_TRANSITIONS, $graph);
}//end testTransitionGraphIsSourcedFromSchemaDeclaration()

Expand Down
4 changes: 2 additions & 2 deletions tests/Unit/Service/Lifecycle/SchemaLifecycleGraphTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public function testTaskAdjacencyMatchesDeclaredGraph(): void {
'completed' => ['open'],
'expired' => ['open'],
],
actual: $this->graph()->adjacencyFor(schemaSlug: 'task')
actual: $this->graph()->adjacencyFor(schemaSlug: 'crmTask')
);
}//end testTaskAdjacencyMatchesDeclaredGraph()

Expand Down Expand Up @@ -202,6 +202,6 @@ public function testUnknownSchemaYieldsEmptyMap(): void {
*/
public function testUnreadableSettingsDirYieldsEmptyMap(): void {
$graph = new SchemaLifecycleGraph(settingsDir: '/nonexistent/path/Settings');
$this->assertSame(expected: [], actual: $graph->adjacencyFor(schemaSlug: 'task'));
$this->assertSame(expected: [], actual: $graph->adjacencyFor(schemaSlug: 'crmTask'));
}//end testUnreadableSettingsDirYieldsEmptyMap()
}//end class
2 changes: 1 addition & 1 deletion tests/Unit/Service/Marketing/JourneyStepRunnerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ public function testATaskStepDoesNotConsultTheConsentGate(): void {

$this->assertSame('task-created', $outcomes[0]['state']);

$tasks = $this->store->findAll('task');
$tasks = $this->store->findAll('crmTask');
$this->assertSame('Call about the renewal', $tasks[0]['subject']);
$this->assertSame('client-1', $tasks[0]['clientId']);
}//end testATaskStepDoesNotConsultTheConsentGate()
Expand Down
Loading